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.
345 lines
11 KiB
C#
345 lines
11 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|