Skip to content

Commit e54bbf7

Browse files
Initial port from FishyFlip
1 parent 48c351d commit e54bbf7

15 files changed

Lines changed: 1389 additions & 1 deletion

File tree

CarpaNet.Samples.slnx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
<Project Path="src/CarpaNet.OAuth/CarpaNet.OAuth.csproj" />
55
<Project Path="src/CarpaNet.SourceGen/CarpaNet.SourceGen.csproj" />
66
<Project Path="src/CarpaNet/CarpaNet.csproj" />
7+
<Project Path="src/CarpaNet.AspNetCore/CarpaNet.AspNetCore.csproj" />
78
</Folder>
89
<Folder Name="/samples/">
910
<Project Path="samples/AuthTest/AuthTest.csproj" />
1011
<Project Path="samples/FirehoseTest/FirehoseTest.csproj" />
1112
<Project Path="samples/JetstreamTest/JetstreamTest.csproj" />
1213
<Project Path="samples/RemoteResolution/RemoteResolution.csproj" />
14+
<Project Path="samples/XrpcServer/XrpcServer.csproj" />
1315
</Folder>
1416
<Folder Name="/tests/">
1517
<Project Path="tests/CarpaNet.UnitTests/CarpaNet.UnitTests.csproj" />

CarpaNet.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<Project Path="src/CarpaNet.OAuth/CarpaNet.OAuth.csproj" />
66
<Project Path="src/CarpaNet.SourceGen/CarpaNet.SourceGen.csproj" />
77
<Project Path="src/CarpaNet/CarpaNet.csproj" />
8+
<Project Path="src/CarpaNet.AspNetCore/CarpaNet.AspNetCore.csproj" />
89
</Folder>
910
<Folder Name="/tests/">
1011
<Project Path="tests/CarpaNet.UnitTests/CarpaNet.UnitTests.csproj" />
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using CarpaNet.AspNetCore;
2+
using Microsoft.AspNetCore.Http.HttpResults;
3+
4+
namespace XrpcServer.Controllers;
5+
6+
/// <summary>
7+
/// Sample implementation of the generated abstract IdentityController.
8+
/// </summary>
9+
public class MyIdentityController : Xrpc.ComAtproto.Identity.IdentityController
10+
{
11+
/// <inheritdoc/>
12+
public override Task<Results<Ok<ComAtproto.Identity.ResolveHandleOutput>, ATErrorResult>> ResolveHandleAsync(
13+
string handle,
14+
CancellationToken cancellationToken = default)
15+
{
16+
if (string.IsNullOrEmpty(handle))
17+
{
18+
return Task.FromResult<Results<Ok<ComAtproto.Identity.ResolveHandleOutput>, ATErrorResult>>(
19+
ATErrorResult.BadRequest("Handle is required"));
20+
}
21+
22+
var output = new ComAtproto.Identity.ResolveHandleOutput
23+
{
24+
Did = new CarpaNet.ATDid("did:plc:example123"),
25+
};
26+
27+
return Task.FromResult<Results<Ok<ComAtproto.Identity.ResolveHandleOutput>, ATErrorResult>>(
28+
TypedResults.Ok(output));
29+
}
30+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using CarpaNet.AspNetCore;
2+
using Microsoft.AspNetCore.Http.HttpResults;
3+
4+
namespace XrpcServer.Controllers;
5+
6+
/// <summary>
7+
/// Sample implementation of the generated abstract ServerController.
8+
/// Demonstrates how to implement XRPC server endpoints using CarpaNet-generated controllers.
9+
/// </summary>
10+
public class MyServerController : Xrpc.ComAtproto.Server.ServerController
11+
{
12+
/// <inheritdoc/>
13+
public override Task<Results<Ok<ComAtproto.Server.DescribeServerOutput>, ATErrorResult>> DescribeServerAsync(
14+
CancellationToken cancellationToken = default)
15+
{
16+
var output = new ComAtproto.Server.DescribeServerOutput
17+
{
18+
AvailableUserDomains = new List<string> { ".example.com" },
19+
Did = new CarpaNet.ATDid("did:web:example.com"),
20+
};
21+
22+
return Task.FromResult<Results<Ok<ComAtproto.Server.DescribeServerOutput>, ATErrorResult>>(
23+
TypedResults.Ok(output));
24+
}
25+
}

samples/XrpcServer/Program.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
var builder = WebApplication.CreateBuilder(args);
2+
builder.Services.AddControllers();
3+
4+
var app = builder.Build();
5+
app.MapControllers();
6+
app.Run();

samples/XrpcServer/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# XrpcServer
2+
3+
A sample application for generating [XRPC](https://docs.bsky.app/docs/api/at-protocol-xrpc-api) endpoints via the CarpaNet source generator.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
8+
<CarpaNet_EmitValidationAttributes>true</CarpaNet_EmitValidationAttributes>
9+
<CarpaNet_EmitXrpcEndpoints>true</CarpaNet_EmitXrpcEndpoints>
10+
<CarpaNet_JsonContextName>ATProtoJsonContext</CarpaNet_JsonContextName>
11+
<CarpaNet_LexiconAutoResolve>true</CarpaNet_LexiconAutoResolve>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="../../src/CarpaNet/CarpaNet.csproj" />
16+
<ProjectReference Include="../../src/CarpaNet.AspNetCore/CarpaNet.AspNetCore.csproj" />
17+
<ProjectReference Include="../../src/CarpaNet.SourceGen/CarpaNet.SourceGen.csproj"
18+
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
19+
</ItemGroup>
20+
21+
<Import Project="..\..\src\CarpaNet\build\CarpaNet.targets" />
22+
23+
<ItemGroup>
24+
<LexiconResolve Include="com.atproto.server.describeServer" />
25+
<LexiconResolve Include="com.atproto.identity.resolveHandle" />
26+
</ItemGroup>
27+
28+
</Project>
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// <auto-generated />
2+
#nullable enable
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace CarpaNet.AspNetCore;
7+
8+
/// <summary>
9+
/// ATProtocol XRPC error result implementing <see cref="IResult"/>.
10+
/// </summary>
11+
public sealed class ATErrorResult : IResult
12+
{
13+
/// <summary>
14+
/// Gets the HTTP status code.
15+
/// </summary>
16+
public int StatusCode { get; private init; }
17+
18+
/// <summary>
19+
/// Gets the error type identifier.
20+
/// </summary>
21+
public string? Error { get; private init; }
22+
23+
/// <summary>
24+
/// Gets the human-readable error message.
25+
/// </summary>
26+
public string? Message { get; private init; }
27+
28+
/// <summary>
29+
/// Creates a Bad Request (400) error.
30+
/// </summary>
31+
public static ATErrorResult BadRequest(string? message = null)
32+
=> new() { StatusCode = 400, Error = "BadRequest", Message = message ?? "Bad Request" };
33+
34+
/// <summary>
35+
/// Creates an Unauthorized (401) error.
36+
/// </summary>
37+
public static ATErrorResult Unauthorized(string? message = null)
38+
=> new() { StatusCode = 401, Error = "AuthMissing", Message = message ?? "Authentication Required" };
39+
40+
/// <summary>
41+
/// Creates a Forbidden (403) error.
42+
/// </summary>
43+
public static ATErrorResult Forbidden(string? message = null)
44+
=> new() { StatusCode = 403, Error = "Forbidden", Message = message ?? "Forbidden" };
45+
46+
/// <summary>
47+
/// Creates a Not Found (404) error.
48+
/// </summary>
49+
public static ATErrorResult NotFound(string? message = null)
50+
=> new() { StatusCode = 404, Error = "NotFound", Message = message ?? "Not Found" };
51+
52+
/// <summary>
53+
/// Creates a Payload Too Large (413) error.
54+
/// </summary>
55+
public static ATErrorResult PayloadTooLarge(string? message = null)
56+
=> new() { StatusCode = 413, Error = "PayloadTooLarge", Message = message ?? "Payload Too Large" };
57+
58+
/// <summary>
59+
/// Creates a Too Many Requests (429) error.
60+
/// </summary>
61+
public static ATErrorResult TooManyRequests(string? message = null)
62+
=> new() { StatusCode = 429, Error = "TooManyRequests", Message = message ?? "Too Many Requests" };
63+
64+
/// <summary>
65+
/// Creates an Internal Server Error (500) error.
66+
/// </summary>
67+
public static ATErrorResult InternalServerError(string? message = null)
68+
=> new() { StatusCode = 500, Error = "InternalServerError", Message = message ?? "Internal Server Error" };
69+
70+
/// <summary>
71+
/// Creates an Internal Server Error (500) from an exception.
72+
/// </summary>
73+
public static ATErrorResult InternalServerError(Exception exception)
74+
=> new() { StatusCode = 500, Error = "InternalServerError", Message = exception.Message };
75+
76+
/// <summary>
77+
/// Creates a Not Implemented (501) error.
78+
/// </summary>
79+
public static ATErrorResult NotImplemented(string? message = null)
80+
=> new() { StatusCode = 501, Error = "NotImplemented", Message = message ?? "Not Implemented" };
81+
82+
/// <summary>
83+
/// Creates a Bad Gateway (502) error.
84+
/// </summary>
85+
public static ATErrorResult BadGateway(string? message = null)
86+
=> new() { StatusCode = 502, Error = "BadGateway", Message = message ?? "Bad Gateway" };
87+
88+
/// <summary>
89+
/// Creates a Service Unavailable (503) error.
90+
/// </summary>
91+
public static ATErrorResult ServiceUnavailable(string? message = null)
92+
=> new() { StatusCode = 503, Error = "ServiceUnavailable", Message = message ?? "Service Unavailable" };
93+
94+
/// <summary>
95+
/// Creates a Gateway Timeout (504) error.
96+
/// </summary>
97+
public static ATErrorResult GatewayTimeout(string? message = null)
98+
=> new() { StatusCode = 504, Error = "GatewayTimeout", Message = message ?? "Gateway Timeout" };
99+
100+
/// <inheritdoc/>
101+
public Task ExecuteAsync(HttpContext httpContext)
102+
{
103+
httpContext.Response.StatusCode = StatusCode;
104+
httpContext.Response.ContentType = "application/json";
105+
var error = new XrpcError
106+
{
107+
Error = this.Error,
108+
Message = this.Message,
109+
};
110+
return httpContext.Response.WriteAsync(error.ToJson());
111+
}
112+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<LangVersion>latest</LangVersion>
8+
<IsAotCompatible>true</IsAotCompatible>
9+
<PackageId>CarpaNet.AspNetCore</PackageId>
10+
<Description>ASP.NET Core XRPC endpoint support for CarpaNet ATProtocol library.</Description>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
15+
<ProjectReference Include="..\CarpaNet\CarpaNet.csproj" />
16+
</ItemGroup>
17+
18+
<ItemGroup Condition=" ('$(IsPackable)' == 'true') or ('$(PackAsTool)' == 'true') ">
19+
<None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath=""
20+
Visible="false" />
21+
</ItemGroup>
22+
23+
<Import Project="..\..\Version.props" />
24+
</Project>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// <auto-generated />
2+
#nullable enable
3+
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
using System.Text.Json.Serialization.Metadata;
7+
8+
namespace CarpaNet.AspNetCore;
9+
10+
/// <summary>
11+
/// ATProtocol XRPC error response body.
12+
/// </summary>
13+
public sealed class XrpcError
14+
{
15+
/// <summary>
16+
/// Gets the error type identifier.
17+
/// </summary>
18+
[JsonPropertyName("error")]
19+
public string? Error { get; init; }
20+
21+
/// <summary>
22+
/// Gets the human-readable error message.
23+
/// </summary>
24+
[JsonPropertyName("message")]
25+
public string? Message { get; init; }
26+
27+
/// <summary>
28+
/// Serializes this error to a JSON string.
29+
/// </summary>
30+
public string ToJson()
31+
{
32+
return JsonSerializer.Serialize(this, XrpcJsonContext.Default.XrpcError);
33+
}
34+
}

0 commit comments

Comments
 (0)