Skip to content

Commit b83dccb

Browse files
committed
first commit
0 parents  commit b83dccb

25 files changed

Lines changed: 1851 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v5
16+
17+
- uses: actions/setup-dotnet@v4
18+
with:
19+
dotnet-version: "8.0.x"
20+
21+
- name: Restore
22+
run: dotnet restore
23+
24+
# Build also runs the Roslyn analyzers (latest-recommended); warnings are
25+
# errors, so a static-analysis finding fails the build.
26+
- name: Build (static analysis)
27+
run: dotnet build -c Release --no-restore
28+
29+
# The xUnit suite (incl. the SQS binding conformance test) runs against a
30+
# Moq-mocked IAmazonSQS — no AWS, no network.
31+
- name: Test with coverage gate (>=90% line)
32+
run: dotnet test -c Release /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:Threshold=90 /p:ThresholdType=line /p:ThresholdStat=total
33+
34+
conformance:
35+
name: Conformance suite in sync
36+
runs-on: ubuntu-latest
37+
steps:
38+
- uses: actions/checkout@v5
39+
- name: Verify vendored conformance matches the canonical suite
40+
run: |
41+
git clone --depth 1 https://github.com/BabelQueue/conformance.git "$RUNNER_TEMP/conformance"
42+
diff -ru "$RUNNER_TEMP/conformance/manifest.json" "tests/BabelQueue.Sqs.Tests/conformance/manifest.json"
43+
diff -ru "$RUNNER_TEMP/conformance/fixtures" "tests/BabelQueue.Sqs.Tests/conformance/fixtures"
44+
diff -ru "$RUNNER_TEMP/conformance/schema" "tests/BabelQueue.Sqs.Tests/conformance/schema"
45+
echo "Vendored conformance is in sync with the canonical suite."
46+
47+
ci-green:
48+
name: CI green
49+
runs-on: ubuntu-latest
50+
needs: [test, conformance]
51+
if: ${{ always() }}
52+
steps:
53+
- name: Fail if any required job did not pass
54+
run: |
55+
if ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}; then
56+
echo "A required job failed or was cancelled."
57+
exit 1
58+
fi

.github/workflows/release.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: [ "v*" ]
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
release:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v5
15+
16+
- uses: actions/setup-dotnet@v4
17+
with:
18+
dotnet-version: "8.0.x"
19+
20+
- name: Test
21+
run: dotnet test -c Release
22+
23+
- name: Pack
24+
run: dotnet pack src/BabelQueue.Sqs/BabelQueue.Sqs.csproj -c Release -o artifacts
25+
26+
- name: Publish to NuGet
27+
run: >
28+
dotnet nuget push "artifacts/*.nupkg"
29+
--api-key "${{ secrets.NUGET_API_KEY }}"
30+
--source https://api.nuget.org/v3/index.json
31+
--skip-duplicate
32+
33+
- name: Create GitHub Release
34+
uses: softprops/action-gh-release@v2
35+
with:
36+
generate_release_notes: true

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
bin/
2+
obj/
3+
*.user

BabelQueue.Sqs.sln

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.31903.59
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{84A922B7-8839-40C8-A583-4A037AD3978E}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BabelQueue.Sqs", "src\BabelQueue.Sqs\BabelQueue.Sqs.csproj", "{CAC62308-402C-451B-86A4-F3260CF4798A}"
9+
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{81B86CE9-45AC-4EC0-806C-DE056AA4021C}"
11+
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BabelQueue.Sqs.Tests", "tests\BabelQueue.Sqs.Tests\BabelQueue.Sqs.Tests.csproj", "{D0BD498C-0E91-4EFD-AF8E-AA6845EA26AE}"
13+
EndProject
14+
Global
15+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
16+
Debug|Any CPU = Debug|Any CPU
17+
Release|Any CPU = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
23+
{CAC62308-402C-451B-86A4-F3260CF4798A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24+
{CAC62308-402C-451B-86A4-F3260CF4798A}.Debug|Any CPU.Build.0 = Debug|Any CPU
25+
{CAC62308-402C-451B-86A4-F3260CF4798A}.Release|Any CPU.ActiveCfg = Release|Any CPU
26+
{CAC62308-402C-451B-86A4-F3260CF4798A}.Release|Any CPU.Build.0 = Release|Any CPU
27+
{D0BD498C-0E91-4EFD-AF8E-AA6845EA26AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28+
{D0BD498C-0E91-4EFD-AF8E-AA6845EA26AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
29+
{D0BD498C-0E91-4EFD-AF8E-AA6845EA26AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
30+
{D0BD498C-0E91-4EFD-AF8E-AA6845EA26AE}.Release|Any CPU.Build.0 = Release|Any CPU
31+
EndGlobalSection
32+
GlobalSection(NestedProjects) = preSolution
33+
{CAC62308-402C-451B-86A4-F3260CF4798A} = {84A922B7-8839-40C8-A583-4A037AD3978E}
34+
{D0BD498C-0E91-4EFD-AF8E-AA6845EA26AE} = {81B86CE9-45AC-4EC0-806C-DE056AA4021C}
35+
EndGlobalSection
36+
EndGlobal

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Changelog
2+
3+
All notable changes to `BabelQueue.Sqs` are documented here.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
6+
this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
The envelope wire format is versioned separately by `meta.schema_version`
8+
(currently **1**) — see the contract at [babelqueue.com](https://babelqueue.com).
9+
10+
## [1.0.0] - 2026-06-12
11+
12+
### Added
13+
- Initial release. An Amazon SQS transport on `BabelQueue.Core` + the AWS SDK for .NET v4
14+
(`AWSSDK.SQS`): `SqsPublisher` (canonical-envelope `SendMessage` with the §3
15+
`MessageAttributes` projection — `bq-job`/`bq-trace-id`/`bq-message-id`/
16+
`bq-schema-version`/`bq-source-lang`/`bq-created-at`; FIFO group/dedup) and
17+
`SqsConsumer` (long-poll receive → URN-routed `BabelHandler`s → `DeleteMessage`;
18+
SQS-native visibility-timeout retry; `attempts` reconciled to
19+
`ApproximateReceiveCount − 1`, never lowering a runtime-incremented count;
20+
`OnError`/`OnUnknownUrn` hooks). `net8.0`, Roslyn analyzers (latest-recommended,
21+
warnings-as-errors); 16 xUnit tests (incl. the cross-SDK SQS binding conformance) run
22+
with a Moq-mocked `IAmazonSQS` (no AWS, no network). The envelope is unchanged
23+
(`schema_version: 1`); SQS is purely additive.
24+
25+
[Unreleased]: https://github.com/BabelQueue/babelqueue-dotnet-sqs/compare/v1.0.0...HEAD
26+
[1.0.0]: https://github.com/BabelQueue/babelqueue-dotnet-sqs/releases/tag/v1.0.0

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# BabelQueue.Sqs
2+
3+
Amazon SQS transport for [BabelQueue](https://babelqueue.com) — "Polyglot Queues,
4+
Simplified." Built on the AWS SDK for .NET and the framework-agnostic
5+
[`BabelQueue.Core`](https://www.nuget.org/packages/BabelQueue.Core).
6+
7+
A canonical-envelope **publisher** and a URN-routed **consumer**, so an SQS-based .NET
8+
service speaks the same wire contract (envelope shape, URN identity, trace propagation)
9+
as the PHP/Laravel, Python, Go, Node and Java SDKs. Implements
10+
[§3 of the broker-bindings contract](https://babelqueue.com).
11+
12+
## Install
13+
14+
```bash
15+
dotnet add package BabelQueue.Sqs
16+
```
17+
18+
It pulls `BabelQueue.Core` and `AWSSDK.SQS` transitively.
19+
20+
## Use
21+
22+
```csharp
23+
using Amazon.SQS;
24+
using BabelQueue.Sqs;
25+
26+
IAmazonSQS sqs = new AmazonSQSClient(); // your AWS config / credentials chain
27+
var url = "https://sqs.eu-central-1.amazonaws.com/123456789012/orders";
28+
29+
// produce
30+
var id = await new SqsPublisher(sqs, url)
31+
.PublishAsync("urn:babel:orders:created", new Dictionary<string, object?> { ["order_id"] = 1042 });
32+
33+
// consume
34+
var handlers = new Dictionary<string, BabelHandler>
35+
{
36+
["urn:babel:orders:created"] = async (env, message, ct) =>
37+
{
38+
// env.Data, env.TraceId, env.Attempts ...
39+
},
40+
};
41+
var consumer = new SqsConsumer(sqs, url, handlers, new SqsConsumerOptions
42+
{
43+
OnError = (err, env, msg) => Console.Error.WriteLine(err),
44+
});
45+
await consumer.RunAsync(cancellationToken); // long-polls until cancelled
46+
```
47+
48+
FIFO: `new SqsPublisher(sqs, url, fifo: true)` (the URL must end in `.fifo`). For
49+
LocalStack/ElasticMQ, point the `AmazonSQSClient`'s `ServiceURL` there.
50+
51+
## Contract mapping (§3)
52+
53+
| Envelope | SQS |
54+
| :--- | :--- |
55+
| body | `MessageBody` (byte-identical across SDKs) |
56+
| `job` (URN) | `MessageAttributes.bq-job` |
57+
| `trace_id` | `MessageAttributes.bq-trace-id` |
58+
| `meta.id` | `MessageAttributes.bq-message-id` |
59+
| `meta.schema_version` | `MessageAttributes.bq-schema-version` (Number) |
60+
| `meta.lang` | `MessageAttributes.bq-source-lang` |
61+
| `meta.created_at` | `MessageAttributes.bq-created-at` (Number, ms) |
62+
| `attempts` | reconciled to `ApproximateReceiveCount − 1` on receive |
63+
| reserve / ack | visibility timeout → `DeleteMessage` |
64+
65+
Retry is **SQS-native**: a throwing handler leaves the message undeleted, so SQS
66+
redelivers it after the visibility timeout (at-least-once). The poll loop never stops on
67+
a bad message — observe via `OnError` / `OnUnknownUrn`. The envelope is unchanged
68+
(`schema_version` stays `1`); SQS is purely additive.
69+
70+
## Build & test
71+
72+
```bash
73+
dotnet test
74+
```
75+
76+
`IAmazonSQS` is an interface, so the unit tests mock it with Moq — no AWS, no network.
77+
78+
## License
79+
80+
MIT

src/BabelQueue.Sqs/BabelHandler.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Amazon.SQS.Model;
2+
3+
namespace BabelQueue.Sqs;
4+
5+
/// <summary>
6+
/// Processes one decoded, validated envelope and the raw SQS message it arrived on.
7+
/// Completing normally acknowledges it (the consumer deletes it); throwing leaves it
8+
/// for SQS to redeliver after the visibility timeout.
9+
/// </summary>
10+
public delegate Task BabelHandler(Envelope envelope, Message message, CancellationToken cancellationToken);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<LangVersion>latest</LangVersion>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
9+
<NoWarn>$(NoWarn);CS1591</NoWarn>
10+
11+
<!-- Static analysis: Roslyn analyzers at the recommended ruleset, failing the
12+
build on any finding. -->
13+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
14+
<EnableNETAnalyzers>true</EnableNETAnalyzers>
15+
<AnalysisLevel>latest-recommended</AnalysisLevel>
16+
17+
<!-- NuGet package metadata -->
18+
<PackageId>BabelQueue.Sqs</PackageId>
19+
<Version>1.0.0</Version>
20+
<Authors>Muhammet Şafak</Authors>
21+
<Company>BabelQueue</Company>
22+
<Description>Amazon SQS transport for BabelQueue — a canonical-envelope publisher and a URN-routed consumer over the AWS SDK for .NET, on the framework-agnostic core.</Description>
23+
<PackageProjectUrl>https://babelqueue.com</PackageProjectUrl>
24+
<RepositoryUrl>https://github.com/BabelQueue/babelqueue-dotnet-sqs</RepositoryUrl>
25+
<RepositoryType>git</RepositoryType>
26+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
27+
<PackageReadmeFile>README.md</PackageReadmeFile>
28+
<PackageTags>babelqueue;sqs;aws;queue;polyglot;messaging</PackageTags>
29+
<Copyright>Copyright (c) 2026 Muhammet Şafak</Copyright>
30+
<IncludeSymbols>true</IncludeSymbols>
31+
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
32+
</PropertyGroup>
33+
34+
<ItemGroup>
35+
<PackageReference Include="AWSSDK.SQS" Version="4.0.3.3" />
36+
<PackageReference Include="BabelQueue.Core" Version="1.0.0" />
37+
</ItemGroup>
38+
39+
<ItemGroup>
40+
<!-- Let the test project assert the internal attribute projection against the conformance golden. -->
41+
<InternalsVisibleTo Include="BabelQueue.Sqs.Tests" />
42+
</ItemGroup>
43+
44+
<ItemGroup>
45+
<None Include="../../README.md" Pack="true" PackagePath="\" />
46+
</ItemGroup>
47+
48+
</Project>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.Globalization;
2+
using Amazon.SQS.Model;
3+
4+
namespace BabelQueue.Sqs;
5+
6+
/// <summary>
7+
/// Projects the envelope's contract fields onto native SQS <c>MessageAttributes</c> —
8+
/// a redundant, routable view of the body (the body stays authoritative). Contract §3.2.
9+
/// </summary>
10+
internal static class SqsAttributes
11+
{
12+
public static Dictionary<string, MessageAttributeValue> Project(Envelope envelope)
13+
{
14+
var attributes = new Dictionary<string, MessageAttributeValue>(StringComparer.Ordinal);
15+
AddString(attributes, "bq-job", envelope.Job);
16+
AddString(attributes, "bq-trace-id", envelope.TraceId);
17+
18+
var meta = envelope.Meta;
19+
if (meta is not null)
20+
{
21+
AddString(attributes, "bq-message-id", meta.Id);
22+
attributes["bq-schema-version"] = Number(meta.SchemaVersion.ToString(CultureInfo.InvariantCulture));
23+
AddString(attributes, "bq-source-lang", meta.Lang);
24+
attributes["bq-created-at"] = Number(meta.CreatedAt.ToString(CultureInfo.InvariantCulture));
25+
}
26+
27+
return attributes;
28+
}
29+
30+
private static void AddString(Dictionary<string, MessageAttributeValue> attributes, string key, string? value)
31+
{
32+
if (!string.IsNullOrEmpty(value))
33+
{
34+
attributes[key] = new MessageAttributeValue { DataType = "String", StringValue = value };
35+
}
36+
}
37+
38+
private static MessageAttributeValue Number(string value)
39+
=> new() { DataType = "Number", StringValue = value };
40+
}

0 commit comments

Comments
 (0)