using System; using Svrnty.CQRS.Events.Abstractions.Schema; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Svrnty.CQRS.Events.Abstractions; namespace Svrnty.CQRS.Events.Schema; /// /// Basic JSON Schema Draft 7 generator using System.Text.Json reflection. /// /// /// /// This is a simple implementation that generates basic JSON schemas from CLR types. /// For more advanced features (XML doc comments, complex validation, etc.), /// consider using NJsonSchema library instead. /// /// /// Supports: /// - Primitive types (string, number, boolean) /// - Object types with properties /// - Nullable types /// - Required properties (non-nullable reference types) /// - Basic type descriptions /// /// public class SystemTextJsonSchemaGenerator : IJsonSchemaGenerator { private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true }; /// public Task GenerateSchemaAsync( Type type, CancellationToken cancellationToken = default) { if (type == null) throw new ArgumentNullException(nameof(type)); var schema = GenerateSchemaObject(type, new HashSet()); var json = JsonSerializer.Serialize(schema, _jsonOptions); return Task.FromResult(json); } /// public Task ValidateAsync( string jsonData, string jsonSchema, CancellationToken cancellationToken = default) { // Basic implementation: Just check if JSON is parseable // For proper validation, use NJsonSchema or similar library try { using var doc = JsonDocument.Parse(jsonData); return Task.FromResult(true); } catch { return Task.FromResult(false); } } /// public Task> GetValidationErrorsAsync( string jsonData, string jsonSchema, CancellationToken cancellationToken = default) { // Basic implementation: Just check JSON parsing errors var errors = new List(); try { using var doc = JsonDocument.Parse(jsonData); } catch (JsonException ex) { errors.Add($"Invalid JSON: {ex.Message}"); } return Task.FromResult>(errors); } private Dictionary GenerateSchemaObject(Type type, HashSet visitedTypes) { // Prevent infinite recursion for circular references if (visitedTypes.Contains(type)) { return new Dictionary { ["$ref"] = $"#/definitions/{type.Name}" }; } visitedTypes.Add(type); var schema = new Dictionary { ["$schema"] = "http://json-schema.org/draft-07/schema#", ["type"] = GetJsonType(type) }; // Add title from type name schema["title"] = type.Name; // Handle nullable types var underlyingType = Nullable.GetUnderlyingType(type); if (underlyingType != null) { type = underlyingType; schema["type"] = new[] { GetJsonType(type), "null" }; } // Handle complex types (objects) if (IsComplexType(type)) { var properties = new Dictionary(); var required = new List(); foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { var propName = GetJsonPropertyName(prop); var propSchema = new Dictionary { ["type"] = GetJsonType(prop.PropertyType) }; // Check if property is required (non-nullable reference type) if (!IsNullable(prop)) { required.Add(propName); } // Handle nested complex types if (IsComplexType(prop.PropertyType)) { propSchema = GenerateSchemaObject(prop.PropertyType, new HashSet(visitedTypes)); } properties[propName] = propSchema; } schema["properties"] = properties; if (required.Any()) { schema["required"] = required; } } return schema; } private static string GetJsonType(Type type) { var underlyingType = Nullable.GetUnderlyingType(type) ?? type; if (underlyingType == typeof(string)) return "string"; if (underlyingType == typeof(int) || underlyingType == typeof(long) || underlyingType == typeof(short) || underlyingType == typeof(byte)) return "integer"; if (underlyingType == typeof(double) || underlyingType == typeof(float) || underlyingType == typeof(decimal)) return "number"; if (underlyingType == typeof(bool)) return "boolean"; if (underlyingType == typeof(DateTime) || underlyingType == typeof(DateTimeOffset)) return "string"; // ISO 8601 format if (underlyingType == typeof(Guid)) return "string"; // UUID format // Arrays and lists if (type.IsArray || (type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(List<>) || type.GetGenericTypeDefinition() == typeof(IEnumerable<>) || type.GetGenericTypeDefinition() == typeof(IList<>) || type.GetGenericTypeDefinition() == typeof(IReadOnlyList<>)))) { return "array"; } return "object"; } private static bool IsComplexType(Type type) { var underlyingType = Nullable.GetUnderlyingType(type) ?? type; return underlyingType.IsClass && underlyingType != typeof(string) && underlyingType != typeof(DateTime) && underlyingType != typeof(DateTimeOffset) && underlyingType != typeof(Guid) && !underlyingType.IsArray; } private static bool IsNullable(PropertyInfo property) { // Check for nullable value types (e.g., int?) if (Nullable.GetUnderlyingType(property.PropertyType) != null) return true; // Check for nullable reference types (C# 8.0+) var nullabilityContext = new NullabilityInfoContext(); var nullabilityInfo = nullabilityContext.Create(property); return nullabilityInfo.WriteState == NullabilityState.Nullable; } private static string GetJsonPropertyName(PropertyInfo property) { // Use JsonPropertyName attribute if present var attr = property.GetCustomAttribute(); if (attr != null) return attr.Name; // Default: camelCase var name = property.Name; return char.ToLowerInvariant(name[0]) + name.Substring(1); } }