230 lines
7.2 KiB
C#
230 lines
7.2 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Basic JSON Schema Draft 7 generator using System.Text.Json reflection.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// Supports:
|
|
/// - Primitive types (string, number, boolean)
|
|
/// - Object types with properties
|
|
/// - Nullable types
|
|
/// - Required properties (non-nullable reference types)
|
|
/// - Basic type descriptions
|
|
/// </para>
|
|
/// </remarks>
|
|
public class SystemTextJsonSchemaGenerator : IJsonSchemaGenerator
|
|
{
|
|
private static readonly JsonSerializerOptions _jsonOptions = new()
|
|
{
|
|
WriteIndented = true
|
|
};
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GenerateSchemaAsync(
|
|
Type type,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (type == null)
|
|
throw new ArgumentNullException(nameof(type));
|
|
|
|
var schema = GenerateSchemaObject(type, new HashSet<Type>());
|
|
var json = JsonSerializer.Serialize(schema, _jsonOptions);
|
|
return Task.FromResult(json);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> 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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<string>> GetValidationErrorsAsync(
|
|
string jsonData,
|
|
string jsonSchema,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
// Basic implementation: Just check JSON parsing errors
|
|
var errors = new List<string>();
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(jsonData);
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
errors.Add($"Invalid JSON: {ex.Message}");
|
|
}
|
|
|
|
return Task.FromResult<IReadOnlyList<string>>(errors);
|
|
}
|
|
|
|
private Dictionary<string, object> GenerateSchemaObject(Type type, HashSet<Type> visitedTypes)
|
|
{
|
|
// Prevent infinite recursion for circular references
|
|
if (visitedTypes.Contains(type))
|
|
{
|
|
return new Dictionary<string, object>
|
|
{
|
|
["$ref"] = $"#/definitions/{type.Name}"
|
|
};
|
|
}
|
|
|
|
visitedTypes.Add(type);
|
|
|
|
var schema = new Dictionary<string, object>
|
|
{
|
|
["$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<string, object>();
|
|
var required = new List<string>();
|
|
|
|
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
|
{
|
|
var propName = GetJsonPropertyName(prop);
|
|
var propSchema = new Dictionary<string, object>
|
|
{
|
|
["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<Type>(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<System.Text.Json.Serialization.JsonPropertyNameAttribute>();
|
|
if (attr != null)
|
|
return attr.Name;
|
|
|
|
// Default: camelCase
|
|
var name = property.Name;
|
|
return char.ToLowerInvariant(name[0]) + name.Substring(1);
|
|
}
|
|
}
|