Add unit tests for RabbitMq and Udp transport servers and clients
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling. - Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options. - Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation. - Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios. - Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling. - Included tests for UdpTransportOptions to verify default values and modification capabilities. - Enhanced service registration tests for Udp transport services in the dependency injection container.
This commit is contained in:
@@ -52,4 +52,37 @@ internal static class DiagnosticDescriptors
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Info,
|
||||
isEnabledByDefault: false);
|
||||
|
||||
/// <summary>
|
||||
/// Schema generation failed for a type.
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor SchemaGenerationFailed = new(
|
||||
id: "STELLA005",
|
||||
title: "Schema generation failed",
|
||||
messageFormat: "Failed to generate JSON Schema for type '{0}' in endpoint '{1}'",
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
/// <summary>
|
||||
/// [ValidateSchema] applied to non-typed endpoint.
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor ValidateSchemaOnRawEndpoint = new(
|
||||
id: "STELLA006",
|
||||
title: "ValidateSchema on raw endpoint",
|
||||
messageFormat: "[ValidateSchema] on class '{0}' is ignored because it implements IRawStellaEndpoint",
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
/// <summary>
|
||||
/// External schema resource not found.
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor SchemaResourceNotFound = new(
|
||||
id: "STELLA007",
|
||||
title: "Schema resource not found",
|
||||
messageFormat: "External schema resource '{0}' not found for endpoint '{1}'",
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true);
|
||||
}
|
||||
|
||||
@@ -14,4 +14,16 @@ internal sealed record EndpointInfo(
|
||||
string[] RequiredClaims,
|
||||
string? RequestTypeName,
|
||||
string? ResponseTypeName,
|
||||
bool IsRaw);
|
||||
bool IsRaw,
|
||||
bool ValidateRequest = false,
|
||||
bool ValidateResponse = false,
|
||||
string? RequestSchemaJson = null,
|
||||
string? ResponseSchemaJson = null,
|
||||
string? RequestSchemaResource = null,
|
||||
string? ResponseSchemaResource = null,
|
||||
string? Summary = null,
|
||||
string? Description = null,
|
||||
string[]? Tags = null,
|
||||
bool Deprecated = false,
|
||||
string? RequestSchemaId = null,
|
||||
string? ResponseSchemaId = null);
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace StellaOps.Microservice.SourceGen;
|
||||
|
||||
/// <summary>
|
||||
/// Generates JSON Schema (draft 2020-12) from C# types at compile time.
|
||||
/// </summary>
|
||||
internal static class SchemaGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a JSON Schema string from a type symbol.
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">The type to generate schema for.</param>
|
||||
/// <param name="compilation">The compilation context.</param>
|
||||
/// <returns>A JSON Schema string, or null if generation fails.</returns>
|
||||
public static string? GenerateSchema(ITypeSymbol typeSymbol, Compilation compilation)
|
||||
{
|
||||
if (typeSymbol is null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var context = new SchemaContext(compilation);
|
||||
var schema = GenerateTypeSchema(typeSymbol, context, isRoot: true);
|
||||
return schema;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateTypeSchema(ITypeSymbol typeSymbol, SchemaContext context, bool isRoot)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("{");
|
||||
|
||||
if (isRoot)
|
||||
{
|
||||
sb.AppendLine(" \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",");
|
||||
}
|
||||
|
||||
// Check for simple types first
|
||||
var simpleType = GetSimpleTypeSchema(typeSymbol);
|
||||
if (simpleType is not null)
|
||||
{
|
||||
sb.Append(simpleType);
|
||||
sb.AppendLine();
|
||||
sb.Append("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// Check for nullable
|
||||
if (typeSymbol.NullableAnnotation == NullableAnnotation.Annotated ||
|
||||
(typeSymbol is INamedTypeSymbol namedType && namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T))
|
||||
{
|
||||
var underlyingType = GetUnderlyingType(typeSymbol);
|
||||
if (underlyingType is not null)
|
||||
{
|
||||
var underlyingSimple = GetSimpleTypeSchema(underlyingType);
|
||||
if (underlyingSimple is not null)
|
||||
{
|
||||
// Nullable simple type
|
||||
var nullableSchema = underlyingSimple.Replace("\"type\":", "\"type\": [") + ", \"null\"]";
|
||||
sb.Append(nullableSchema);
|
||||
sb.AppendLine();
|
||||
sb.Append("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for arrays/lists
|
||||
if (IsCollectionType(typeSymbol, out var elementType))
|
||||
{
|
||||
sb.AppendLine(" \"type\": \"array\",");
|
||||
if (elementType is not null)
|
||||
{
|
||||
var elementSchema = GetSimpleTypeSchema(elementType);
|
||||
if (elementSchema is not null)
|
||||
{
|
||||
sb.AppendLine(" \"items\": {");
|
||||
sb.Append(" ");
|
||||
sb.AppendLine(elementSchema);
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" \"items\": { \"type\": \"object\" }");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" \"items\": {}");
|
||||
}
|
||||
sb.Append("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// Object type
|
||||
sb.AppendLine(" \"type\": \"object\",");
|
||||
|
||||
// Get properties
|
||||
var properties = GetPublicProperties(typeSymbol);
|
||||
var requiredProps = new List<string>();
|
||||
|
||||
if (properties.Count > 0)
|
||||
{
|
||||
sb.AppendLine(" \"properties\": {");
|
||||
|
||||
var propIndex = 0;
|
||||
foreach (var prop in properties.OrderBy(p => p.Name))
|
||||
{
|
||||
var propSchema = GeneratePropertySchema(prop, context);
|
||||
var isRequired = IsPropertyRequired(prop);
|
||||
|
||||
if (isRequired)
|
||||
{
|
||||
requiredProps.Add(ToCamelCase(prop.Name));
|
||||
}
|
||||
|
||||
sb.Append($" \"{ToCamelCase(prop.Name)}\": ");
|
||||
sb.Append(propSchema);
|
||||
|
||||
if (propIndex < properties.Count - 1)
|
||||
{
|
||||
sb.AppendLine(",");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
propIndex++;
|
||||
}
|
||||
|
||||
sb.AppendLine(" },");
|
||||
}
|
||||
|
||||
// Required array
|
||||
if (requiredProps.Count > 0)
|
||||
{
|
||||
sb.Append(" \"required\": [");
|
||||
sb.Append(string.Join(", ", requiredProps.Select(p => $"\"{p}\"")));
|
||||
sb.AppendLine("],");
|
||||
}
|
||||
|
||||
sb.AppendLine(" \"additionalProperties\": false");
|
||||
sb.Append("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GeneratePropertySchema(IPropertySymbol prop, SchemaContext context)
|
||||
{
|
||||
var type = prop.Type;
|
||||
|
||||
// Handle nullable types
|
||||
var isNullable = type.NullableAnnotation == NullableAnnotation.Annotated;
|
||||
if (isNullable)
|
||||
{
|
||||
type = GetUnderlyingType(type) ?? type;
|
||||
}
|
||||
|
||||
var simpleSchema = GetSimpleTypeSchema(type);
|
||||
if (simpleSchema is not null)
|
||||
{
|
||||
if (isNullable)
|
||||
{
|
||||
// Convert "type": "X" to "type": ["X", "null"]
|
||||
return "{ " + MakeTypeNullable(simpleSchema) + " }";
|
||||
}
|
||||
return "{ " + simpleSchema + " }";
|
||||
}
|
||||
|
||||
// Collections
|
||||
if (IsCollectionType(type, out var elementType))
|
||||
{
|
||||
var itemSchema = elementType is not null
|
||||
? GetSimpleTypeSchema(elementType) ?? "\"type\": \"object\""
|
||||
: "\"type\": \"object\"";
|
||||
|
||||
return $"{{ \"type\": \"array\", \"items\": {{ {itemSchema} }} }}";
|
||||
}
|
||||
|
||||
// Complex object - just use object type for now
|
||||
return "{ \"type\": \"object\" }";
|
||||
}
|
||||
|
||||
private static string? GetSimpleTypeSchema(ITypeSymbol type)
|
||||
{
|
||||
var fullName = type.ToDisplayString();
|
||||
|
||||
return fullName switch
|
||||
{
|
||||
"string" or "System.String" => "\"type\": \"string\"",
|
||||
"int" or "System.Int32" => "\"type\": \"integer\"",
|
||||
"long" or "System.Int64" => "\"type\": \"integer\"",
|
||||
"short" or "System.Int16" => "\"type\": \"integer\"",
|
||||
"byte" or "System.Byte" => "\"type\": \"integer\"",
|
||||
"uint" or "System.UInt32" => "\"type\": \"integer\"",
|
||||
"ulong" or "System.UInt64" => "\"type\": \"integer\"",
|
||||
"ushort" or "System.UInt16" => "\"type\": \"integer\"",
|
||||
"sbyte" or "System.SByte" => "\"type\": \"integer\"",
|
||||
"float" or "System.Single" => "\"type\": \"number\"",
|
||||
"double" or "System.Double" => "\"type\": \"number\"",
|
||||
"decimal" or "System.Decimal" => "\"type\": \"number\"",
|
||||
"bool" or "System.Boolean" => "\"type\": \"boolean\"",
|
||||
"System.DateTime" or "System.DateTimeOffset" => "\"type\": \"string\", \"format\": \"date-time\"",
|
||||
"System.DateOnly" => "\"type\": \"string\", \"format\": \"date\"",
|
||||
"System.TimeOnly" or "System.TimeSpan" => "\"type\": \"string\", \"format\": \"time\"",
|
||||
"System.Guid" => "\"type\": \"string\", \"format\": \"uuid\"",
|
||||
"System.Uri" => "\"type\": \"string\", \"format\": \"uri\"",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static ITypeSymbol? GetUnderlyingType(ITypeSymbol type)
|
||||
{
|
||||
if (type is INamedTypeSymbol namedType)
|
||||
{
|
||||
if (namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T &&
|
||||
namedType.TypeArguments.Length > 0)
|
||||
{
|
||||
return namedType.TypeArguments[0];
|
||||
}
|
||||
}
|
||||
|
||||
// For reference types with nullable annotation, just return the type itself
|
||||
return type;
|
||||
}
|
||||
|
||||
private static bool IsCollectionType(ITypeSymbol type, out ITypeSymbol? elementType)
|
||||
{
|
||||
elementType = null;
|
||||
|
||||
if (type is IArrayTypeSymbol arrayType)
|
||||
{
|
||||
elementType = arrayType.ElementType;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type is INamedTypeSymbol namedType)
|
||||
{
|
||||
// Check for List<T>, IList<T>, IEnumerable<T>, ICollection<T>, etc.
|
||||
var fullName = namedType.OriginalDefinition.ToDisplayString();
|
||||
if ((fullName.StartsWith("System.Collections.Generic.List") ||
|
||||
fullName.StartsWith("System.Collections.Generic.IList") ||
|
||||
fullName.StartsWith("System.Collections.Generic.IEnumerable") ||
|
||||
fullName.StartsWith("System.Collections.Generic.ICollection") ||
|
||||
fullName.StartsWith("System.Collections.Generic.IReadOnlyList") ||
|
||||
fullName.StartsWith("System.Collections.Generic.IReadOnlyCollection")) &&
|
||||
namedType.TypeArguments.Length > 0)
|
||||
{
|
||||
elementType = namedType.TypeArguments[0];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<IPropertySymbol> GetPublicProperties(ITypeSymbol type)
|
||||
{
|
||||
var properties = new List<IPropertySymbol>();
|
||||
|
||||
foreach (var member in type.GetMembers())
|
||||
{
|
||||
if (member is IPropertySymbol prop &&
|
||||
prop.DeclaredAccessibility == Accessibility.Public &&
|
||||
!prop.IsStatic &&
|
||||
!prop.IsIndexer &&
|
||||
prop.GetMethod is not null)
|
||||
{
|
||||
properties.Add(prop);
|
||||
}
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static bool IsPropertyRequired(IPropertySymbol prop)
|
||||
{
|
||||
// Check for [Required] attribute
|
||||
foreach (var attr in prop.GetAttributes())
|
||||
{
|
||||
var attrName = attr.AttributeClass?.ToDisplayString();
|
||||
if (attrName == "System.ComponentModel.DataAnnotations.RequiredAttribute")
|
||||
return true;
|
||||
}
|
||||
|
||||
// Non-nullable reference types are required
|
||||
if (prop.Type.IsReferenceType &&
|
||||
prop.NullableAnnotation != NullableAnnotation.Annotated &&
|
||||
prop.Type.NullableAnnotation != NullableAnnotation.Annotated)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string MakeTypeNullable(string simpleSchema)
|
||||
{
|
||||
// Convert "type": "X" to "type": ["X", "null"]
|
||||
// Input: "type": "string"
|
||||
// Output: "type": ["string", "null"]
|
||||
const string typePrefix = "\"type\": \"";
|
||||
var idx = simpleSchema.IndexOf(typePrefix, StringComparison.Ordinal);
|
||||
if (idx < 0)
|
||||
return simpleSchema;
|
||||
|
||||
var startOfType = idx + typePrefix.Length;
|
||||
var endOfType = simpleSchema.IndexOf('"', startOfType);
|
||||
if (endOfType < 0)
|
||||
return simpleSchema;
|
||||
|
||||
var typeName = simpleSchema.Substring(startOfType, endOfType - startOfType);
|
||||
var rest = simpleSchema.Substring(endOfType + 1);
|
||||
return $"\"type\": [\"{typeName}\", \"null\"]{rest}";
|
||||
}
|
||||
|
||||
private static string ToCamelCase(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return name;
|
||||
|
||||
if (name.Length == 1)
|
||||
return name.ToLowerInvariant();
|
||||
|
||||
return char.ToLowerInvariant(name[0]) + name.Substring(1);
|
||||
}
|
||||
|
||||
private sealed class SchemaContext
|
||||
{
|
||||
public Compilation Compilation { get; }
|
||||
public Dictionary<string, string> Definitions { get; } = new();
|
||||
|
||||
public SchemaContext(Compilation compilation)
|
||||
{
|
||||
Compilation = compilation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,13 @@ namespace StellaOps.Microservice.SourceGen;
|
||||
|
||||
/// <summary>
|
||||
/// Incremental source generator for [StellaEndpoint] decorated classes.
|
||||
/// Generates endpoint descriptors and DI registration at compile time.
|
||||
/// Generates endpoint descriptors, DI registration, and JSON Schemas at compile time.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public sealed class StellaEndpointGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string StellaEndpointAttributeName = "StellaOps.Microservice.StellaEndpointAttribute";
|
||||
private const string ValidateSchemaAttributeName = "StellaOps.Microservice.ValidateSchemaAttribute";
|
||||
private const string IStellaEndpointName = "StellaOps.Microservice.IStellaEndpoint";
|
||||
private const string IRawStellaEndpointName = "StellaOps.Microservice.IRawStellaEndpoint";
|
||||
|
||||
@@ -88,7 +89,7 @@ public sealed class StellaEndpointGenerator : IIncrementalGenerator
|
||||
if (classSymbol is null)
|
||||
continue;
|
||||
|
||||
var endpoint = ExtractEndpointInfo(classSymbol, context);
|
||||
var endpoint = ExtractEndpointInfo(classSymbol, compilation, context);
|
||||
if (endpoint is not null)
|
||||
{
|
||||
endpoints.Add(endpoint);
|
||||
@@ -124,20 +125,37 @@ public sealed class StellaEndpointGenerator : IIncrementalGenerator
|
||||
context.AddSource("StellaEndpoints.g.cs", SourceText.From(source, Encoding.UTF8));
|
||||
|
||||
// Generate the provider class
|
||||
var providerSource = GenerateProviderClass();
|
||||
var providerSource = GenerateProviderClass(endpoints);
|
||||
context.AddSource("GeneratedEndpointProvider.g.cs", SourceText.From(providerSource, Encoding.UTF8));
|
||||
|
||||
// Generate schema provider if any endpoints have validation enabled
|
||||
var endpointsWithSchemas = endpoints.Where(e => e.ValidateRequest || e.ValidateResponse).ToList();
|
||||
if (endpointsWithSchemas.Count > 0)
|
||||
{
|
||||
var schemaProviderSource = GenerateSchemaProviderClass(endpointsWithSchemas);
|
||||
context.AddSource("GeneratedSchemaProvider.g.cs", SourceText.From(schemaProviderSource, Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
|
||||
private static EndpointInfo? ExtractEndpointInfo(INamedTypeSymbol classSymbol, SourceProductionContext context)
|
||||
private static EndpointInfo? ExtractEndpointInfo(
|
||||
INamedTypeSymbol classSymbol,
|
||||
Compilation compilation,
|
||||
SourceProductionContext context)
|
||||
{
|
||||
// Find StellaEndpoint attribute
|
||||
AttributeData? stellaAttribute = null;
|
||||
AttributeData? validateSchemaAttribute = null;
|
||||
|
||||
foreach (var attr in classSymbol.GetAttributes())
|
||||
{
|
||||
if (attr.AttributeClass?.ToDisplayString() == StellaEndpointAttributeName)
|
||||
var attrName = attr.AttributeClass?.ToDisplayString();
|
||||
if (attrName == StellaEndpointAttributeName)
|
||||
{
|
||||
stellaAttribute = attr;
|
||||
break;
|
||||
}
|
||||
else if (attrName == ValidateSchemaAttributeName)
|
||||
{
|
||||
validateSchemaAttribute = attr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +210,8 @@ public sealed class StellaEndpointGenerator : IIncrementalGenerator
|
||||
// Find handler interface implementation
|
||||
string? requestTypeName = null;
|
||||
string? responseTypeName = null;
|
||||
ITypeSymbol? requestTypeSymbol = null;
|
||||
ITypeSymbol? responseTypeSymbol = null;
|
||||
bool isRaw = false;
|
||||
|
||||
foreach (var iface in classSymbol.AllInterfaces)
|
||||
@@ -200,8 +220,10 @@ public sealed class StellaEndpointGenerator : IIncrementalGenerator
|
||||
|
||||
if (fullName.StartsWith(IStellaEndpointName) && iface.TypeArguments.Length == 2)
|
||||
{
|
||||
requestTypeName = iface.TypeArguments[0].ToDisplayString();
|
||||
responseTypeName = iface.TypeArguments[1].ToDisplayString();
|
||||
requestTypeSymbol = iface.TypeArguments[0];
|
||||
responseTypeSymbol = iface.TypeArguments[1];
|
||||
requestTypeName = requestTypeSymbol.ToDisplayString();
|
||||
responseTypeName = responseTypeSymbol.ToDisplayString();
|
||||
isRaw = false;
|
||||
break;
|
||||
}
|
||||
@@ -223,6 +245,113 @@ public sealed class StellaEndpointGenerator : IIncrementalGenerator
|
||||
return null;
|
||||
}
|
||||
|
||||
// Process ValidateSchema attribute
|
||||
bool validateRequest = false;
|
||||
bool validateResponse = false;
|
||||
string? requestSchemaJson = null;
|
||||
string? responseSchemaJson = null;
|
||||
string? requestSchemaResource = null;
|
||||
string? responseSchemaResource = null;
|
||||
string? summary = null;
|
||||
string? description = null;
|
||||
string[]? tags = null;
|
||||
bool deprecated = false;
|
||||
string? requestSchemaId = null;
|
||||
string? responseSchemaId = null;
|
||||
|
||||
if (validateSchemaAttribute is not null)
|
||||
{
|
||||
// Warn if applied to raw endpoint
|
||||
if (isRaw)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
DiagnosticDescriptors.ValidateSchemaOnRawEndpoint,
|
||||
Location.None,
|
||||
classSymbol.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Extract ValidateSchema named arguments
|
||||
validateRequest = true; // Default is true
|
||||
validateResponse = false; // Default is false
|
||||
|
||||
foreach (var namedArg in validateSchemaAttribute.NamedArguments)
|
||||
{
|
||||
switch (namedArg.Key)
|
||||
{
|
||||
case "ValidateRequest":
|
||||
validateRequest = (bool)(namedArg.Value.Value ?? true);
|
||||
break;
|
||||
case "ValidateResponse":
|
||||
validateResponse = (bool)(namedArg.Value.Value ?? false);
|
||||
break;
|
||||
case "RequestSchemaResource":
|
||||
requestSchemaResource = namedArg.Value.Value as string;
|
||||
break;
|
||||
case "ResponseSchemaResource":
|
||||
responseSchemaResource = namedArg.Value.Value as string;
|
||||
break;
|
||||
case "Summary":
|
||||
summary = namedArg.Value.Value as string;
|
||||
break;
|
||||
case "Description":
|
||||
description = namedArg.Value.Value as string;
|
||||
break;
|
||||
case "Tags":
|
||||
if (!namedArg.Value.IsNull && namedArg.Value.Values.Length > 0)
|
||||
{
|
||||
tags = namedArg.Value.Values
|
||||
.Select(v => v.Value as string)
|
||||
.Where(s => s is not null)
|
||||
.Cast<string>()
|
||||
.ToArray();
|
||||
}
|
||||
break;
|
||||
case "Deprecated":
|
||||
deprecated = (bool)(namedArg.Value.Value ?? false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate schemas if no external resource specified
|
||||
if (validateRequest && requestSchemaResource is null && requestTypeSymbol is not null)
|
||||
{
|
||||
requestSchemaJson = SchemaGenerator.GenerateSchema(requestTypeSymbol, compilation);
|
||||
if (requestSchemaJson is null)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
DiagnosticDescriptors.SchemaGenerationFailed,
|
||||
Location.None,
|
||||
requestTypeName,
|
||||
classSymbol.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Generate schema ID from type name
|
||||
requestSchemaId = GetSchemaId(requestTypeSymbol);
|
||||
}
|
||||
}
|
||||
|
||||
if (validateResponse && responseSchemaResource is null && responseTypeSymbol is not null)
|
||||
{
|
||||
responseSchemaJson = SchemaGenerator.GenerateSchema(responseTypeSymbol, compilation);
|
||||
if (responseSchemaJson is null)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
DiagnosticDescriptors.SchemaGenerationFailed,
|
||||
Location.None,
|
||||
responseTypeName,
|
||||
classSymbol.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Generate schema ID from type name
|
||||
responseSchemaId = GetSchemaId(responseTypeSymbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ns = classSymbol.ContainingNamespace.IsGlobalNamespace
|
||||
? string.Empty
|
||||
: classSymbol.ContainingNamespace.ToDisplayString();
|
||||
@@ -238,7 +367,26 @@ public sealed class StellaEndpointGenerator : IIncrementalGenerator
|
||||
RequiredClaims: requiredClaims,
|
||||
RequestTypeName: requestTypeName,
|
||||
ResponseTypeName: responseTypeName,
|
||||
IsRaw: isRaw);
|
||||
IsRaw: isRaw,
|
||||
ValidateRequest: validateRequest,
|
||||
ValidateResponse: validateResponse,
|
||||
RequestSchemaJson: requestSchemaJson,
|
||||
ResponseSchemaJson: responseSchemaJson,
|
||||
RequestSchemaResource: requestSchemaResource,
|
||||
ResponseSchemaResource: responseSchemaResource,
|
||||
Summary: summary,
|
||||
Description: description,
|
||||
Tags: tags,
|
||||
Deprecated: deprecated,
|
||||
RequestSchemaId: requestSchemaId,
|
||||
ResponseSchemaId: responseSchemaId);
|
||||
}
|
||||
|
||||
private static string GetSchemaId(ITypeSymbol typeSymbol)
|
||||
{
|
||||
// Use simple name for schema ID, stripping namespace
|
||||
var name = typeSymbol.Name;
|
||||
return name;
|
||||
}
|
||||
|
||||
private static string GenerateEndpointsClass(List<EndpointInfo> endpoints)
|
||||
@@ -292,7 +440,42 @@ public sealed class StellaEndpointGenerator : IIncrementalGenerator
|
||||
}
|
||||
sb.AppendLine(" },");
|
||||
}
|
||||
sb.AppendLine($" HandlerType = typeof(global::{ep.FullyQualifiedName})");
|
||||
sb.AppendLine($" HandlerType = typeof(global::{ep.FullyQualifiedName}),");
|
||||
|
||||
// Add SchemaInfo if endpoint has validation or documentation
|
||||
if (ep.ValidateRequest || ep.ValidateResponse || ep.Summary is not null || ep.Description is not null || ep.Tags is not null || ep.Deprecated)
|
||||
{
|
||||
sb.AppendLine(" SchemaInfo = new global::StellaOps.Router.Common.Models.EndpointSchemaInfo");
|
||||
sb.AppendLine(" {");
|
||||
if (ep.RequestSchemaId is not null)
|
||||
{
|
||||
sb.AppendLine($" RequestSchemaId = \"{EscapeString(ep.RequestSchemaId)}\",");
|
||||
}
|
||||
if (ep.ResponseSchemaId is not null)
|
||||
{
|
||||
sb.AppendLine($" ResponseSchemaId = \"{EscapeString(ep.ResponseSchemaId)}\",");
|
||||
}
|
||||
if (ep.Summary is not null)
|
||||
{
|
||||
sb.AppendLine($" Summary = \"{EscapeString(ep.Summary)}\",");
|
||||
}
|
||||
if (ep.Description is not null)
|
||||
{
|
||||
sb.AppendLine($" Description = \"{EscapeString(ep.Description)}\",");
|
||||
}
|
||||
if (ep.Tags is not null && ep.Tags.Length > 0)
|
||||
{
|
||||
sb.Append(" Tags = new string[] { ");
|
||||
sb.Append(string.Join(", ", ep.Tags.Select(t => $"\"{EscapeString(t)}\"")));
|
||||
sb.AppendLine(" },");
|
||||
}
|
||||
sb.AppendLine($" Deprecated = {(ep.Deprecated ? "true" : "false")}");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" SchemaInfo = null");
|
||||
}
|
||||
sb.Append(" }");
|
||||
if (i < endpoints.Count - 1)
|
||||
{
|
||||
@@ -355,7 +538,7 @@ public sealed class StellaEndpointGenerator : IIncrementalGenerator
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateProviderClass()
|
||||
private static string GenerateProviderClass(List<EndpointInfo> endpoints)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
@@ -381,6 +564,121 @@ public sealed class StellaEndpointGenerator : IIncrementalGenerator
|
||||
sb.AppendLine(" /// <inheritdoc />");
|
||||
sb.AppendLine(" public global::System.Collections.Generic.IReadOnlyList<global::System.Type> GetHandlerTypes()");
|
||||
sb.AppendLine(" => StellaEndpoints.GetHandlerTypes();");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" /// <inheritdoc />");
|
||||
sb.AppendLine(" public global::System.Collections.Generic.IReadOnlyDictionary<string, global::StellaOps.Router.Common.Models.SchemaDefinition> GetSchemaDefinitions()");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" var schemas = new global::System.Collections.Generic.Dictionary<string, global::StellaOps.Router.Common.Models.SchemaDefinition>();");
|
||||
|
||||
// Collect unique schemas
|
||||
var schemas = new Dictionary<string, EndpointInfo>();
|
||||
foreach (var ep in endpoints)
|
||||
{
|
||||
if (ep.RequestSchemaId is not null && ep.RequestSchemaJson is not null && !schemas.ContainsKey(ep.RequestSchemaId))
|
||||
{
|
||||
schemas[ep.RequestSchemaId] = ep;
|
||||
}
|
||||
if (ep.ResponseSchemaId is not null && ep.ResponseSchemaJson is not null && !schemas.ContainsKey(ep.ResponseSchemaId))
|
||||
{
|
||||
schemas[ep.ResponseSchemaId] = ep;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var kvp in schemas)
|
||||
{
|
||||
var schemaId = kvp.Key;
|
||||
var ep = kvp.Value;
|
||||
var schemaJson = schemaId == ep.RequestSchemaId ? ep.RequestSchemaJson : ep.ResponseSchemaJson;
|
||||
if (schemaJson is not null)
|
||||
{
|
||||
sb.AppendLine($" schemas[\"{EscapeString(schemaId)}\"] = new global::StellaOps.Router.Common.Models.SchemaDefinition");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" SchemaId = \"{EscapeString(schemaId)}\",");
|
||||
sb.AppendLine($" SchemaJson = @\"{EscapeVerbatimString(schemaJson)}\",");
|
||||
sb.AppendLine($" ETag = ComputeETag(@\"{EscapeVerbatimString(schemaJson)}\")");
|
||||
sb.AppendLine(" };");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine(" return schemas;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" private static string ComputeETag(string content)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" using var sha256 = global::System.Security.Cryptography.SHA256.Create();");
|
||||
sb.AppendLine(" var hash = sha256.ComputeHash(global::System.Text.Encoding.UTF8.GetBytes(content));");
|
||||
sb.AppendLine(" return $\"\\\"{global::System.Convert.ToHexString(hash)[..16]}\\\"\";");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateSchemaProviderClass(List<EndpointInfo> endpoints)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("// <auto-generated/>");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("namespace StellaOps.Microservice.Generated");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// Generated implementation of IGeneratedSchemaProvider.");
|
||||
sb.AppendLine(" /// Provides JSON Schemas for endpoints with [ValidateSchema] attribute.");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" [global::System.CodeDom.Compiler.GeneratedCode(\"StellaOps.Microservice.SourceGen\", \"1.0.0\")]");
|
||||
sb.AppendLine(" internal sealed class GeneratedSchemaProvider : global::StellaOps.Microservice.Validation.IGeneratedSchemaProvider");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" /// <inheritdoc />");
|
||||
sb.AppendLine(" public global::System.Collections.Generic.IReadOnlyList<global::StellaOps.Microservice.Validation.EndpointSchemaDefinition> GetSchemaDefinitions()");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" return new global::StellaOps.Microservice.Validation.EndpointSchemaDefinition[]");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
for (int i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
var ep = endpoints[i];
|
||||
sb.AppendLine(" new global::StellaOps.Microservice.Validation.EndpointSchemaDefinition(");
|
||||
sb.AppendLine($" Method: \"{EscapeString(ep.Method)}\",");
|
||||
sb.AppendLine($" Path: \"{EscapeString(ep.Path)}\",");
|
||||
|
||||
// Request schema
|
||||
if (ep.RequestSchemaJson is not null)
|
||||
{
|
||||
sb.AppendLine($" RequestSchemaJson: @\"{EscapeVerbatimString(ep.RequestSchemaJson)}\",");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" RequestSchemaJson: null,");
|
||||
}
|
||||
|
||||
// Response schema
|
||||
if (ep.ResponseSchemaJson is not null)
|
||||
{
|
||||
sb.AppendLine($" ResponseSchemaJson: @\"{EscapeVerbatimString(ep.ResponseSchemaJson)}\",");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" ResponseSchemaJson: null,");
|
||||
}
|
||||
|
||||
sb.AppendLine($" ValidateRequest: {(ep.ValidateRequest ? "true" : "false")},");
|
||||
sb.Append($" ValidateResponse: {(ep.ValidateResponse ? "true" : "false")})");
|
||||
|
||||
if (i < endpoints.Count - 1)
|
||||
{
|
||||
sb.AppendLine(",");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
@@ -396,4 +694,10 @@ public sealed class StellaEndpointGenerator : IIncrementalGenerator
|
||||
.Replace("\r", "\\r")
|
||||
.Replace("\t", "\\t");
|
||||
}
|
||||
|
||||
private static string EscapeVerbatimString(string value)
|
||||
{
|
||||
// In verbatim strings, only " needs escaping (as "")
|
||||
return value.Replace("\"", "\"\"");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user