Skip to content

Commit 61472c9

Browse files
committed
Initial commit
0 parents  commit 61472c9

39 files changed

+1662
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: NuGet Publish
2+
3+
on:
4+
push:
5+
branches: [ main, master ]
6+
paths:
7+
- '**.cs'
8+
- '**.csproj'
9+
- '.github/workflows/nuget-publish.yml'
10+
release:
11+
types: [ published ]
12+
workflow_dispatch:
13+
14+
jobs:
15+
publish:
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- name: Checkout code
20+
uses: actions/checkout@v4
21+
22+
- name: Setup .NET SDKs
23+
uses: actions/setup-dotnet@v4
24+
with:
25+
dotnet-version: |
26+
10.0.x
27+
28+
- name: Restore dependencies
29+
run: dotnet restore
30+
31+
- name: Build solution
32+
run: dotnet build --configuration Release --no-restore
33+
34+
- name: Run tests
35+
run: dotnet test --configuration Release --no-build --verbosity normal
36+
37+
- name: Pack NuGet package
38+
run: dotnet pack SWEN3.Paperless.RabbitMq/SWEN3.Paperless.RabbitMq.csproj --configuration Release --no-build --output ./nupkg -p:GeneratePackageOnBuild=false
39+
40+
- name: Publish packages to NuGet.org
41+
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
42+
run: |
43+
dotnet nuget push ./nupkg/*.nupkg \
44+
--api-key ${{ secrets.NUGET_API_KEY }} \
45+
--source https://api.nuget.org/v3/index.json \
46+
--skip-duplicate

.github/workflows/tests.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Tests and Coverage
2+
3+
on:
4+
push:
5+
branches: [ main, master ]
6+
pull_request:
7+
branches: [ main, master ]
8+
9+
jobs:
10+
build-test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
17+
- name: Setup .NET SDKs
18+
uses: actions/setup-dotnet@v4
19+
with:
20+
dotnet-version: |
21+
10.0.x
22+
23+
- name: Restore dependencies
24+
run: dotnet restore
25+
26+
- name: Build
27+
run: dotnet build --no-restore --configuration Release
28+
29+
- name: Run tests with coverage
30+
run: |
31+
dotnet test --no-build --configuration Release \
32+
--collect:"XPlat Code Coverage" \
33+
--logger:"console;verbosity=detailed"
34+
35+
- name: Upload coverage to Codecov
36+
uses: codecov/codecov-action@v5
37+
with:
38+
token: ${{ secrets.CODECOV_TOKEN }}
39+
files: ./SWEN3.Paperless.RabbitMq.Tests/TestResults/**/coverage.cobertura.xml
40+
fail_ci_if_error: false

.gitignore

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
## A streamlined .gitignore for modern .NET projects
2+
## including temporary files, build results, and
3+
## files generated by popular .NET tools. If you are
4+
## developing with Visual Studio, the VS .gitignore
5+
## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
6+
## has more thorough IDE-specific entries.
7+
##
8+
## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore
9+
10+
# Build results
11+
[Dd]ebug/
12+
[Dd]ebugPublic/
13+
[Rr]elease/
14+
[Rr]eleases/
15+
x64/
16+
x86/
17+
[Ww][Ii][Nn]32/
18+
[Aa][Rr][Mm]/
19+
[Aa][Rr][Mm]64/
20+
bld/
21+
[Bb]in/
22+
[Oo]bj/
23+
[Ll]og/
24+
[Ll]ogs/
25+
26+
# .NET Core
27+
project.lock.json
28+
project.fragment.lock.json
29+
artifacts/
30+
31+
# ASP.NET Scaffolding
32+
ScaffoldingReadMe.txt
33+
34+
# NuGet Packages
35+
*.nupkg
36+
# NuGet Symbol Packages
37+
*.snupkg
38+
39+
# Others
40+
~$*
41+
*~
42+
CodeCoverage/
43+
44+
# MSBuild Binary and Structured Log
45+
*.binlog
46+
47+
# MSTest test Results
48+
[Tt]est[Rr]esult*/
49+
[Bb]uild[Ll]og.*
50+
51+
# NUnit
52+
*.VisualState.xml
53+
TestResult.xml
54+
nunit-*.xml

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Alexander Nachtmann
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
[![codecov](https://codecov.io/gh/ANcpLua/SWEN3.Paperless.RabbitMq/branch/main/graph/badge.svg?token=lgxIXBnFrn)](https://codecov.io/gh/ANcpLua/SWEN3.Paperless.RabbitMq)
2+
[![.NET 10](https://img.shields.io/badge/.NET-10.0_Preview-7C3AED)](https://dotnet.microsoft.com/download/dotnet/10.0)
3+
[![NuGet](https://img.shields.io/nuget/v/SWEN3.Paperless.RabbitMq?label=NuGet&color=0891B2)](https://www.nuget.org/packages/SWEN3.Paperless.RabbitMq/)
4+
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/ANcpLua/SWEN3.Paperless.RabbitMq/blob/main/LICENSE)
5+
6+
# SWEN3.Paperless.RabbitMq
7+
8+
RabbitMQ messaging library for .NET with SSE support.
9+
10+
## Installation
11+
12+
```bash
13+
dotnet add package Paperless.RabbitMq
14+
```
15+
16+
## Configuration
17+
18+
```json
19+
{
20+
"RabbitMQ": {
21+
"Uri": "amqp://guest:guest@localhost:5672"
22+
}
23+
}
24+
```
25+
26+
## Usage
27+
28+
### Basic Setup
29+
30+
```csharp
31+
// Add RabbitMQ
32+
builder.Services.AddPaperlessRabbitMq(builder.Configuration);
33+
34+
// With SSE support
35+
builder.Services.AddPaperlessRabbitMq(configuration, includeOcrResultStream: true);
36+
```
37+
38+
### Publishing
39+
40+
```csharp
41+
var command = new OcrCommand(docId, fileName, storagePath);
42+
await publisher.PublishOcrCommandAsync(command);
43+
44+
var result = new OcrEvent(jobId, "Completed", text, DateTimeOffset.UtcNow);
45+
await publisher.PublishOcrEventAsync(result);
46+
```
47+
48+
### Consuming
49+
50+
```csharp
51+
await using var consumer = await factory.CreateConsumerAsync<OcrCommand>();
52+
53+
await foreach (var command in consumer.ConsumeAsync(cancellationToken))
54+
{
55+
try
56+
{
57+
// Process message
58+
await consumer.AckAsync();
59+
}
60+
catch
61+
{
62+
await consumer.NackAsync(requeue: true);
63+
}
64+
}
65+
```
66+
67+
### SSE Endpoint
68+
69+
```csharp
70+
// Map endpoint
71+
app.MapOcrEventStream();
72+
73+
// Client-side
74+
const eventSource = new EventSource('/api/v1/ocr-results');
75+
eventSource.addEventListener('ocr-completed', (event) => {
76+
const data = JSON.parse(event.data);
77+
console.log(data);
78+
});
79+
```
80+
81+
## Message Types
82+
83+
```csharp
84+
public record OcrCommand(Guid JobId, string FileName, string FilePath);
85+
public record OcrEvent(Guid JobId, string Status, string? Text, DateTimeOffset ProcessedAt);
86+
```
87+
88+
## License
89+
90+
This project is licensed under the [MIT License](LICENSE).
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
global using Xunit;
2+
global using FluentAssertions;
3+
global using Moq;
4+
global using System.Net;
5+
global using System.Net.Http;
6+
global using System.Text;
7+
global using System.Text.Json;
8+
global using Microsoft.AspNetCore.Builder;
9+
global using Microsoft.AspNetCore.Hosting;
10+
global using Microsoft.AspNetCore.Routing;
11+
global using Microsoft.AspNetCore.TestHost;
12+
global using Microsoft.Extensions.Hosting;
13+
global using Microsoft.Extensions.DependencyInjection;
14+
global using Microsoft.Extensions.Configuration;
15+
global using RabbitMQ.Client;
16+
global using RabbitMQ.Client.Events;
17+
global using Testcontainers.RabbitMq;
18+
global using SWEN3.Paperless.RabbitMq.Consuming;
19+
global using SWEN3.Paperless.RabbitMq.Internal;
20+
global using SWEN3.Paperless.RabbitMq.Models;
21+
global using SWEN3.Paperless.RabbitMq.Publishing;
22+
global using SWEN3.Paperless.RabbitMq.Sse;
23+
global using System.Threading.Channels;
24+
global using SWEN3.Paperless.RabbitMq.Tests.Helpers;
25+
global using SWEN3.Paperless.RabbitMq.Schema;
26+
global using System.Diagnostics.CodeAnalysis;
27+
global using System.Runtime.CompilerServices;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace SWEN3.Paperless.RabbitMq.Tests.Helpers;
2+
3+
internal static class TestServerFactory
4+
{
5+
public static TestServer CreateSseTestServer<T>(ISseStream<T> sseStream,
6+
Action<IEndpointRouteBuilder> configureEndpoints) where T : class
7+
{
8+
var builder = new WebHostBuilder()
9+
.ConfigureServices(services =>
10+
{
11+
services.AddSingleton(sseStream);
12+
services.AddRouting();
13+
})
14+
.Configure(app =>
15+
{
16+
app.UseRouting();
17+
app.UseEndpoints(configureEndpoints);
18+
});
19+
20+
return new TestServer(builder);
21+
}
22+
23+
public static TestServer CreateSseTestServerWithTestEndpoint(ISseStream<Messages.SseTestEvent> sseStream)
24+
{
25+
return CreateSseTestServer(sseStream, endpoints =>
26+
{
27+
endpoints.MapSse<Messages.SseTestEvent>(
28+
"/sse-test",
29+
evt => new { evt.Id, evt.Message },
30+
_ => "test-event"
31+
);
32+
});
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace SWEN3.Paperless.RabbitMq.Tests.Integration;
2+
3+
public class OcrEventStreamIntegrationTests
4+
{
5+
[Theory]
6+
[InlineData("Completed", "ocr-completed")]
7+
[InlineData("Failed", "ocr-failed")]
8+
[InlineData("Processing", "ocr-failed")]
9+
public async Task MapOcrEventStream_ShouldEmitCorrectEventType(string status, string expectedEventType)
10+
{
11+
var sseStream = new SseStream<OcrEvent>();
12+
var ocrEvent = new OcrEvent(Guid.NewGuid(), status, status is "Completed" ? "Text" : null,
13+
DateTimeOffset.UtcNow);
14+
15+
using var server = TestServerFactory.CreateSseTestServer(sseStream,
16+
endpoints => endpoints.MapOcrEventStream());
17+
using var client = server.CreateClient();
18+
19+
var responseTask = client.GetAsync("/api/v1/ocr-results", HttpCompletionOption.ResponseHeadersRead,
20+
TestContext.Current.CancellationToken);
21+
await Task.Delay(50, TestContext.Current.CancellationToken);
22+
23+
sseStream.Publish(ocrEvent);
24+
25+
var response = await responseTask;
26+
await using var stream = await response.Content.ReadAsStreamAsync(TestContext.Current.CancellationToken);
27+
using var reader = new StreamReader(stream, Encoding.UTF8);
28+
29+
var eventLine = await reader.ReadLineAsync(TestContext.Current.CancellationToken);
30+
eventLine.Should().Be($"event: {expectedEventType}");
31+
32+
response.Dispose();
33+
}
34+
}

0 commit comments

Comments
 (0)