diff --git a/.vscode/settings.json b/.vscode/settings.json index 8acc1158..77087efc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "unstringified", - "Unstringify" + "Unstringify", + "Xunit" ] } \ No newline at end of file diff --git a/README.md b/README.md index 38d63a52..ee51aad4 100644 --- a/README.md +++ b/README.md @@ -372,7 +372,7 @@ jsonSerializer.AddConverter(new MyCustomJsonConverter()); // Access to JSON schema generation var jsonSchema = reflector.JsonSchema; -var schema = jsonSchema.GetSchema(reflector, justRef: false); +var schema = jsonSchema.GetSchema(reflector); ``` #### Converter System Architecture diff --git a/ReflectorNet.Tests.OuterAssembly/Model/Complex_Address.cs b/ReflectorNet.Tests.OuterAssembly/Model/Complex_Address.cs new file mode 100644 index 00000000..5cea346b --- /dev/null +++ b/ReflectorNet.Tests.OuterAssembly/Model/Complex_Address.cs @@ -0,0 +1,11 @@ +// Complex class: Address with multiple properties +namespace com.IvanMurzak.ReflectorNet.OuterAssembly.Model +{ + public class Address + { + public string Street { get; set; } = string.Empty; + public string City { get; set; } = string.Empty; + public string? Zip { get; set; } + public string Country { get; set; } = string.Empty; + } +} diff --git a/ReflectorNet.Tests.OuterAssembly/Model/Complex_Company.cs b/ReflectorNet.Tests.OuterAssembly/Model/Complex_Company.cs new file mode 100644 index 00000000..388061fd --- /dev/null +++ b/ReflectorNet.Tests.OuterAssembly/Model/Complex_Company.cs @@ -0,0 +1,14 @@ +// Complex class: Company with nested collections and references +using System.Collections.Generic; + +namespace com.IvanMurzak.ReflectorNet.OuterAssembly.Model +{ + public class Company + { + public string Name { get; set; } = string.Empty; + public Address? Headquarters { get; set; } + public List Employees { get; } = new List(); + public Dictionary> Teams { get; } = new Dictionary>(); + public Dictionary> Directory { get; } = new(); + } +} diff --git a/ReflectorNet.Tests.OuterAssembly/Model/Complex_Person.cs b/ReflectorNet.Tests.OuterAssembly/Model/Complex_Person.cs new file mode 100644 index 00000000..b9c1cc69 --- /dev/null +++ b/ReflectorNet.Tests.OuterAssembly/Model/Complex_Person.cs @@ -0,0 +1,32 @@ +// Complex class: Person with mixed fields/properties and nested types +using System; +using System.Collections.Generic; + +namespace com.IvanMurzak.ReflectorNet.OuterAssembly.Model +{ + public class Person + { + // Fields + public string FirstName = string.Empty; + public string LastName = string.Empty; + private DateTime _birthDate; + + // Properties + public int Age { get; set; } + public Address? Address { get; set; } + public List Tags { get; } = new List(); + public Dictionary Scores { get; } = new Dictionary(); + + // Arrays + public int[] Numbers { get; set; } = Array.Empty(); + public string[][] JaggedAliases { get; set; } = Array.Empty(); + public int[,] Matrix2x2 { get; set; } = new int[2, 2]; + + // Methods using primitives and collections + public void SetBirthDate(DateTime date) => _birthDate = date; + public DateTime GetBirthDate() => _birthDate; + + public void AddTag(string tag) => Tags.Add(tag); + public void SetScore(string key, int value) => Scores[key] = value; + } +} diff --git a/ReflectorNet.Tests.OuterAssembly/Model/NestedClass.cs b/ReflectorNet.Tests.OuterAssembly/Model/NestedClass.cs index 3f179069..44661feb 100644 --- a/ReflectorNet.Tests.OuterAssembly/Model/NestedClass.cs +++ b/ReflectorNet.Tests.OuterAssembly/Model/NestedClass.cs @@ -45,5 +45,24 @@ public class WrapperClass [JsonInclude] public T? ValueField; public T? ValueProperty { get; set; } + + public WrapperClass() { } + public WrapperClass(T? valueField, T? valueProperty) + { + ValueField = valueField; + ValueProperty = valueProperty; + } + + /// + /// Echo method that returns the provided value unchanged. + /// Used for testing return type schemas with various generic types. + /// + public T Echo(T value) => value; + + /// + /// Echo method that returns a nullable version of the provided value. + /// Used for testing return type schemas with nullable generic types. + /// + public T? EchoNullable(T? value) => value; } } \ No newline at end of file diff --git a/ReflectorNet.Tests/JsonConverterTests/JsonConverterTests.cs b/ReflectorNet.Tests/JsonConverterTests/JsonConverterTests.cs index e423837b..765c077b 100644 --- a/ReflectorNet.Tests/JsonConverterTests/JsonConverterTests.cs +++ b/ReflectorNet.Tests/JsonConverterTests/JsonConverterTests.cs @@ -1,4 +1,5 @@ -using com.IvanMurzak.ReflectorNet.Tests.Model; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; +using com.IvanMurzak.ReflectorNet.Tests.Model; using Xunit.Abstractions; namespace com.IvanMurzak.ReflectorNet.Tests.Utils diff --git a/ReflectorNet.Tests/Model/JsonConvertors/ObjectRefConverter.cs b/ReflectorNet.Tests/Model/JsonConvertors/ObjectRefConverter.cs index b3e3df28..33165524 100644 --- a/ReflectorNet.Tests/Model/JsonConvertors/ObjectRefConverter.cs +++ b/ReflectorNet.Tests/Model/JsonConvertors/ObjectRefConverter.cs @@ -1,16 +1,15 @@ using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using com.IvanMurzak.ReflectorNet.Json; using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Tests.Model { - public class ObjectRefConverter : JsonConverter, IJsonSchemaConverter + public class ObjectRefConverter : JsonSchemaConverter, IJsonSchemaConverter { - public string Id => typeof(ObjectRef).GetTypeId(); - public JsonNode GetScheme() => new JsonObject + public override JsonNode GetSchema() => new JsonObject { [JsonSchema.Type] = JsonSchema.Object, [JsonSchema.Properties] = new JsonObject @@ -21,7 +20,7 @@ public class ObjectRefConverter : JsonConverter, IJsonSchemaConverter }, [JsonSchema.Required] = new JsonArray { nameof(ObjectRef.instanceID) } }; - public JsonNode GetSchemeRef() => new JsonObject + public override JsonNode GetSchemaRef() => new JsonObject { [JsonSchema.Ref] = JsonSchema.RefValue + Id }; diff --git a/ReflectorNet.Tests/Model/JsonConvertors/SerializedMemberCustomDescriptionConverter.cs b/ReflectorNet.Tests/Model/JsonConvertors/SerializedMemberCustomDescriptionConverter.cs index 568bfe60..6e3f512d 100644 --- a/ReflectorNet.Tests/Model/JsonConvertors/SerializedMemberCustomDescriptionConverter.cs +++ b/ReflectorNet.Tests/Model/JsonConvertors/SerializedMemberCustomDescriptionConverter.cs @@ -1,18 +1,16 @@ using System; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using com.IvanMurzak.ReflectorNet.Json; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Tests.Model { - public class SerializedMemberCustomDescriptionConverter : JsonConverter, IJsonSchemaConverter + public class SerializedMemberCustomDescriptionConverter : JsonSchemaConverter, IJsonSchemaConverter { public const string CustomDescription = "Custom description, used for testing purposes."; - public static string StaticId => TypeUtils.GetTypeId(); public static JsonNode Schema => new JsonObject { [JsonSchema.Type] = JsonSchema.Object, @@ -64,15 +62,13 @@ public class SerializedMemberCustomDescriptionConverter : JsonConverter StaticId; - public SerializedMemberCustomDescriptionConverter(Reflector reflector) { _reflector = reflector ?? throw new ArgumentNullException(nameof(reflector)); } - public JsonNode GetSchemeRef() => SchemaRef; - public JsonNode GetScheme() => Schema; + public override JsonNode GetSchemaRef() => SchemaRef; + public override JsonNode GetSchema() => Schema; public override SerializedMember? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/ReflectorNet.Tests/Model/NestedClass.cs b/ReflectorNet.Tests/Model/NestedClass.cs deleted file mode 100644 index e6510873..00000000 --- a/ReflectorNet.Tests/Model/NestedClass.cs +++ /dev/null @@ -1,56 +0,0 @@ - -using System.Text.Json.Serialization; - -namespace com.IvanMurzak.ReflectorNet.Tests.Model -{ - // Non static class with nested classes and static members - public class ParentClass - { - public class NestedClass - { - public static string NestedStaticField = "I am static field"; - public static string NestedStaticProperty { get; set; } = "I am static property"; - - [JsonInclude] - public string NestedField = "I am field"; - public string NestedProperty { get; set; } = "I am property"; - } - public static class NestedStaticClass - { - public static string NestedStaticField = "I am static field"; - public static string NestedStaticProperty { get; set; } = "I am static property"; - } - } - // Static class with nested classes and static members - public static class StaticParentClass - { - public class NestedClass - { - public static string NestedStaticField = "I am static field"; - public static string NestedStaticProperty { get; set; } = "I am static property"; - - [JsonInclude] - public string NestedField = "I am field"; - public string NestedProperty { get; set; } = "I am property"; - } - public static class NestedStaticClass - { - public static string NestedStaticField = "I am static field"; - public static string NestedStaticProperty { get; set; } = "I am static property"; - } - } - // Wrapper class - public class WrapperClass - { - [JsonInclude] - public T? ValueField; - public T? ValueProperty { get; set; } - - public WrapperClass() { } - public WrapperClass(T? valueField, T? valueProperty) - { - ValueField = valueField; - ValueProperty = valueProperty; - } - } -} \ No newline at end of file diff --git a/ReflectorNet.Tests/ReflectorTests/DeserializationTests.cs b/ReflectorNet.Tests/ReflectorTests/DeserializationTests.cs index ea147c43..701fbc95 100644 --- a/ReflectorNet.Tests/ReflectorTests/DeserializationTests.cs +++ b/ReflectorNet.Tests/ReflectorTests/DeserializationTests.cs @@ -1,10 +1,10 @@ using System.Text; using com.IvanMurzak.ReflectorNet.Utils; -using com.IvanMurzak.ReflectorNet.Tests.Model; using Xunit.Abstractions; using com.IvanMurzak.ReflectorNet.Model; using System.Collections.Generic; using System; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; namespace com.IvanMurzak.ReflectorNet.Tests.Utils { diff --git a/ReflectorNet.Tests/ReflectorTests/SerializationTests.cs b/ReflectorNet.Tests/ReflectorTests/SerializationTests.cs index 5f0f5e84..bc91bf51 100644 --- a/ReflectorNet.Tests/ReflectorTests/SerializationTests.cs +++ b/ReflectorNet.Tests/ReflectorTests/SerializationTests.cs @@ -2,6 +2,7 @@ using com.IvanMurzak.ReflectorNet.Utils; using com.IvanMurzak.ReflectorNet.Tests.Model; using Xunit.Abstractions; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; namespace com.IvanMurzak.ReflectorNet.Tests.Utils { diff --git a/ReflectorNet.Tests/ReflectorTests/SerializeDeserializationTests.cs b/ReflectorNet.Tests/ReflectorTests/SerializeDeserializationTests.cs index 4abda6c5..7bdfc0b8 100644 --- a/ReflectorNet.Tests/ReflectorTests/SerializeDeserializationTests.cs +++ b/ReflectorNet.Tests/ReflectorTests/SerializeDeserializationTests.cs @@ -2,6 +2,7 @@ using com.IvanMurzak.ReflectorNet.Tests.Model; using Xunit.Abstractions; using System; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; namespace com.IvanMurzak.ReflectorNet.Tests.Utils { diff --git a/ReflectorNet.Tests/ReflectorTests/SerializePopulateTests.cs b/ReflectorNet.Tests/ReflectorTests/SerializePopulateTests.cs index 1345a7bc..b7138bbf 100644 --- a/ReflectorNet.Tests/ReflectorTests/SerializePopulateTests.cs +++ b/ReflectorNet.Tests/ReflectorTests/SerializePopulateTests.cs @@ -2,6 +2,7 @@ using com.IvanMurzak.ReflectorNet.Tests.Model; using Xunit.Abstractions; using System; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; namespace com.IvanMurzak.ReflectorNet.Tests.Utils { diff --git a/ReflectorNet.Tests/SchemaTests/CollectionsTests.cs b/ReflectorNet.Tests/SchemaTests/CollectionsTests.cs index 889a48c3..81689cb6 100644 --- a/ReflectorNet.Tests/SchemaTests/CollectionsTests.cs +++ b/ReflectorNet.Tests/SchemaTests/CollectionsTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.Json; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.ReflectorNet.Utils; using Xunit.Abstractions; @@ -57,7 +58,7 @@ public void GetTypeId_SimpleArray_ShouldAppendArray() { var result = reflector.GetSchema(type); - _output.WriteLine($"Type: {type.GetTypeShortName()}\n{result.ToJsonString()}\n"); + _output.WriteLine($"Type: {type.GetTypeShortName()}\n{result.ToJsonString(new JsonSerializerOptions { WriteIndented = true })}\n"); // Assert Assert.NotNull(result); diff --git a/ReflectorNet.Tests/SchemaTests/PerformanceTests.cs b/ReflectorNet.Tests/SchemaTests/PerformanceTests.cs index c2b6e058..44a146ca 100644 --- a/ReflectorNet.Tests/SchemaTests/PerformanceTests.cs +++ b/ReflectorNet.Tests/SchemaTests/PerformanceTests.cs @@ -82,7 +82,7 @@ public void Reflector_Introspection_Tests() var testType = typeof(GameObjectRef); // Act - Test introspection capabilities - var schema = testType.GetSchema(reflector, justRef: false); + var schema = testType.GetSchema(reflector); var typeId = testType.GetTypeId(); // Assert @@ -133,7 +133,7 @@ public void JsonUtils_Comprehensive_Tests() var serializedJson = reflector.JsonSerializer.Serialize(testObject); var deserializedObject = reflector.JsonSerializer.Deserialize(serializedJson); - var schema = reflector.GetSchema(typeof(GameObjectRefList), justRef: false); + var schema = reflector.GetSchema(typeof(GameObjectRefList)); var argumentsSchema = reflector.GetArgumentsSchema( typeof(MethodHelper).GetMethod(nameof(MethodHelper.ListObject_ListObject))!); diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index 0e36055f..c93a2faa 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -1,11 +1,11 @@ using System; -using System.Linq; using System.Reflection; using System.Text.Json.Nodes; using System.Threading.Tasks; using com.IvanMurzak.ReflectorNet.Utils; -using Xunit; using Xunit.Abstractions; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; +using System.Collections.Generic; namespace com.IvanMurzak.ReflectorNet.Tests.SchemaTests { @@ -39,20 +39,84 @@ private void VoidMethod() { } private Task TaskIntMethod() => Task.FromResult(42); private Task TaskBoolMethod() => Task.FromResult(true); private Task TaskCustomTypeMethod() => Task.FromResult(new CustomReturnType()); + private Task TaskPersonMethod() => Task.FromResult(new Person()); + private Task
TaskAddressMethod() => Task.FromResult(new Address()); + private Task TaskCompanyMethod() => Task.FromResult(new Company()); // ValueTask return types (should be unwrapped) private ValueTask ValueTaskStringMethod() => ValueTask.FromResult("test"); private ValueTask ValueTaskIntMethod() => ValueTask.FromResult(42); private ValueTask ValueTaskCustomTypeMethod() => ValueTask.FromResult(new CustomReturnType()); + private ValueTask ValueTaskPersonMethod() => ValueTask.FromResult(new Person()); + private ValueTask
ValueTaskAddressMethod() => ValueTask.FromResult(new Address()); + private ValueTask ValueTaskCompanyMethod() => ValueTask.FromResult(new Company()); // Custom types private CustomReturnType CustomTypeMethod() => new CustomReturnType(); private ComplexReturnType ComplexTypeMethod() => new ComplexReturnType(); + private Person PersonMethod() => new Person(); + private Address AddressMethod() => new Address(); + private Company CompanyMethod() => new Company(); // Collections private string[] StringArrayMethod() => new[] { "test" }; - private System.Collections.Generic.List ListIntMethod() => new System.Collections.Generic.List(); - private System.Collections.Generic.Dictionary DictionaryMethod() => new System.Collections.Generic.Dictionary(); + private List ListIntMethod() => new List(); + private Dictionary DictionaryMethod() => new Dictionary(); + + // List variations + private List ListComplexTypeMethod() => new List(); + private List ListNullableComplexTypeMethod() => new List(); + private List? NullableListComplexTypeMethod() => new List(); + private List? NullableListNullableComplexTypeMethod() => new List(); + private Task> TaskListComplexTypeMethod() => Task.FromResult(new List()); + private Task>? NullableTaskListComplexTypeMethod() => Task.FromResult(new List()); + private Task?> TaskNullableListComplexTypeMethod() => Task.FromResult?>(new List()); + private Task> TaskListNullableComplexTypeMethod() => Task.FromResult(new List()); + private Task?>? NullableTaskNullableListComplexTypeMethod() => Task.FromResult?>(new List()); + private Task?> TaskNullableListNullableComplexTypeMethod() => Task.FromResult?>(new List()); + private Task>? NullableTaskListNullableComplexTypeMethod() => Task.FromResult(new List()); + private Task?>? NullableTaskNullableListNullableComplexTypeMethod() => Task.FromResult?>(new List()); + + // Nullable return types + private string? NullableStringMethod() => "test"; + private int? NullableIntMethod() => 42; + private bool? NullableBoolMethod() => true; + private double? NullableDoubleMethod() => 3.14; + private DateTime? NullableDateTimeMethod() => DateTime.Now; + private CustomReturnType? NullableCustomTypeMethod() => new CustomReturnType(); + private ComplexReturnType? NullableComplexTypeMethod() => new ComplexReturnType(); + private Person? NullablePersonMethod() => new Person(); + private Address? NullableAddressMethod() => new Address(); + private Company? NullableCompanyMethod() => new Company(); + private string[]? NullableStringArrayMethod() => new[] { "test" }; + + // Task return types (nullable wrapped in Task) + private Task TaskNullableStringMethod() => Task.FromResult("test"); + private Task TaskNullableIntMethod() => Task.FromResult(42); + private Task TaskNullableBoolMethod() => Task.FromResult(true); + private Task TaskNullableCustomTypeMethod() => Task.FromResult(new CustomReturnType()); + private Task TaskNullablePersonMethod() => Task.FromResult(new Person()); + private Task TaskNullableAddressMethod() => Task.FromResult(new Address()); + private Task TaskNullableCompanyMethod() => Task.FromResult(new Company()); + + // ValueTask return types (nullable wrapped in ValueTask) + private ValueTask ValueTaskNullableStringMethod() => ValueTask.FromResult("test"); + private ValueTask ValueTaskNullableIntMethod() => ValueTask.FromResult(42); + private ValueTask ValueTaskNullableCustomTypeMethod() => ValueTask.FromResult(new CustomReturnType()); + private ValueTask ValueTaskNullablePersonMethod() => ValueTask.FromResult(new Person()); + private ValueTask ValueTaskNullableAddressMethod() => ValueTask.FromResult(new Address()); + private ValueTask ValueTaskNullableCompanyMethod() => ValueTask.FromResult(new Company()); + + // Task? return types (nullable T wrapped in nullable Task) + private Task? NullableTaskNullableStringMethod() => Task.FromResult("test"); + private Task? NullableTaskNullableIntMethod() => Task.FromResult(42); + private Task? NullableTaskNullableCustomTypeMethod() => Task.FromResult(new CustomReturnType()); + private Task? NullableTaskNullablePersonMethod() => Task.FromResult(new Person()); + private Task? NullableTaskNullableAddressMethod() => Task.FromResult(new Address()); + private Task? NullableTaskNullableCompanyMethod() => Task.FromResult(new Company()); + + // NOTE: ValueTask? is not a valid scenario because ValueTask is a struct, not a class + // So it cannot be made nullable in the reference type sense. We removed these tests. #endregion @@ -76,227 +140,547 @@ public class ComplexReturnType #region Void Return Type Tests - [Fact] - public void GetReturnSchema_VoidMethod_ReturnsNull() + [Theory] + [InlineData(nameof(VoidMethod))] + [InlineData(nameof(TaskMethod))] + [InlineData(nameof(ValueTaskMethod))] + public void GetReturnSchema_VoidMethods_ReturnsNull(string methodName) { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(VoidMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = GetReturnSchemaForMethod(methodName); + Assert.Null(schema); + } - // Act - var schema = reflector.GetReturnSchema(methodInfo); + #endregion - // Assert - Assert.Null(schema); + #region Primitive Return Type Tests + + [Theory] + [InlineData(nameof(StringMethod), JsonSchema.String)] + [InlineData(nameof(IntMethod), JsonSchema.Integer)] + [InlineData(nameof(BoolMethod), JsonSchema.Boolean)] + [InlineData(nameof(DoubleMethod), JsonSchema.Number)] + public void GetReturnSchema_PrimitiveMethod_ReturnsCorrectSchema(string methodName, string expectedType) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: true); + AssertAllRefsDefined(schema!); } - [Fact] - public void GetReturnSchema_TaskMethod_ReturnsNull() + #endregion + + #region Nullable Primitive Return Type Tests + + [Theory] + [InlineData(nameof(NullableStringMethod), JsonSchema.String)] + [InlineData(nameof(NullableIntMethod), JsonSchema.Integer)] + [InlineData(nameof(NullableBoolMethod), JsonSchema.Boolean)] + [InlineData(nameof(NullableDoubleMethod), JsonSchema.Number)] + [InlineData(nameof(NullableDateTimeMethod), JsonSchema.String)] // DateTime serialized as string + public void GetReturnSchema_NullablePrimitiveMethod_ReturnsSchemaWithoutRequired(string methodName, string expectedType) { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: false); + AssertAllRefsDefined(schema!); + } - // Act - var schema = reflector.GetReturnSchema(methodInfo); + #endregion - // Assert - Assert.Null(schema); + #region Task Unwrapping Tests + + [Theory] + [InlineData(nameof(TaskStringMethod), JsonSchema.String)] + [InlineData(nameof(TaskIntMethod), JsonSchema.Integer)] + [InlineData(nameof(TaskBoolMethod), JsonSchema.Boolean)] + public void GetReturnSchema_TaskPrimitive_UnwrapsCorrectly(string methodName, string expectedType) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: true); + AssertAllRefsDefined(schema!); } [Fact] - public void GetReturnSchema_ValueTaskMethod_ReturnsNull() + public void GetReturnSchema_TaskCustomType_UnwrapsToCustomTypeSchema() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ValueTaskMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = GetReturnSchemaForMethod(nameof(TaskCustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: true); + AssertAllRefsDefined(schema!); + } - // Act - var schema = reflector.GetReturnSchema(methodInfo); + [Fact] + public void GetReturnSchema_TaskPerson_UnwrapsCorrectly() + { + var schema = GetReturnSchemaForMethod(nameof(TaskPersonMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); + } - // Assert - Assert.Null(schema); + [Fact] + public void GetReturnSchema_TaskAddress_UnwrapsCorrectly() + { + var schema = GetReturnSchemaForMethod(nameof(TaskAddressMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); + } + + [Fact] + public void GetReturnSchema_TaskCompany_UnwrapsCorrectly() + { + var schema = GetReturnSchemaForMethod(nameof(TaskCompanyMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } #endregion - #region Primitive Return Type Tests + #region Task Nullable Unwrapping Tests + + [Theory] + [InlineData(nameof(TaskNullableStringMethod), JsonSchema.String)] + [InlineData(nameof(TaskNullableIntMethod), JsonSchema.Integer)] + [InlineData(nameof(TaskNullableBoolMethod), JsonSchema.Boolean)] + public void GetReturnSchema_TaskNullablePrimitive_UnwrapsWithoutRequired(string methodName, string expectedType) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: false); + AssertAllRefsDefined(schema!); + } [Fact] - public void GetReturnSchema_StringMethod_ReturnsStringSchema() + public void GetReturnSchema_TaskNullableCustomType_UnwrapsToCustomTypeSchemaWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(StringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = GetReturnSchemaForMethod(nameof(TaskNullableCustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); + AssertAllRefsDefined(schema!); + } - // Act - var schema = reflector.GetReturnSchema(methodInfo); + [Fact] + public void GetReturnSchema_TaskNullablePerson_UnwrapsWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(TaskNullablePersonMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); + } - // Assert + [Fact] + public void GetReturnSchema_TaskNullableAddress_UnwrapsWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(TaskNullableAddressMethod)); Assert.NotNull(schema); - Assert.Equal(JsonSchema.String, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] - public void GetReturnSchema_IntMethod_ReturnsIntegerSchema() + public void GetReturnSchema_TaskNullableCompany_UnwrapsWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(IntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = GetReturnSchemaForMethod(nameof(TaskNullableCompanyMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); + } - // Act - var schema = reflector.GetReturnSchema(methodInfo); + #endregion - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Integer, schema[JsonSchema.Type]?.ToString()); + #region Nullable Task Unwrapping Tests + + [Theory] + [InlineData(nameof(NullableTaskNullableStringMethod), JsonSchema.String)] + [InlineData(nameof(NullableTaskNullableIntMethod), JsonSchema.Integer)] + public void GetReturnSchema_NullableTaskNullablePrimitive_UnwrapsWithoutRequired(string methodName, string expectedType) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: false); + AssertAllRefsDefined(schema!); } [Fact] - public void GetReturnSchema_BoolMethod_ReturnsBooleanSchema() + public void GetReturnSchema_NullableTaskNullableCustomType_UnwrapsToCustomTypeSchemaWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(BoolMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); + var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullableCustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); + AssertAllRefsDefined(schema!); + } - // Assert + [Fact] + public void GetReturnSchema_NullableTaskNullablePerson_UnwrapsWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullablePersonMethod)); Assert.NotNull(schema); - Assert.Equal(JsonSchema.Boolean, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] - public void GetReturnSchema_DoubleMethod_ReturnsNumberSchema() + public void GetReturnSchema_NullableTaskNullableAddress_UnwrapsWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(DoubleMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); + var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullableAddressMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); + } - // Assert + [Fact] + public void GetReturnSchema_NullableTaskNullableCompany_UnwrapsWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullableCompanyMethod)); Assert.NotNull(schema); - Assert.Equal(JsonSchema.Number, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } #endregion - #region Task Unwrapping Tests + #region ValueTask Unwrapping Tests + + [Theory] + [InlineData(nameof(ValueTaskStringMethod), JsonSchema.String)] + [InlineData(nameof(ValueTaskIntMethod), JsonSchema.Integer)] + public void GetReturnSchema_ValueTaskPrimitive_UnwrapsCorrectly(string methodName, string expectedType) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: true); + AssertAllRefsDefined(schema!); + } [Fact] - public void GetReturnSchema_TaskString_UnwrapsToStringSchema() + public void GetReturnSchema_ValueTaskCustomType_UnwrapsToCustomTypeSchema() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskStringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = GetReturnSchemaForMethod(nameof(ValueTaskCustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: true); + AssertAllRefsDefined(schema!); + } - // Act - var schema = reflector.GetReturnSchema(methodInfo); + [Fact] + public void GetReturnSchema_ValueTaskPerson_UnwrapsCorrectly() + { + var schema = GetReturnSchemaForMethod(nameof(ValueTaskPersonMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); + } - // Assert + [Fact] + public void GetReturnSchema_ValueTaskAddress_UnwrapsCorrectly() + { + var schema = GetReturnSchemaForMethod(nameof(ValueTaskAddressMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); + } + + [Fact] + public void GetReturnSchema_ValueTaskCompany_UnwrapsCorrectly() + { + var schema = GetReturnSchemaForMethod(nameof(ValueTaskCompanyMethod)); Assert.NotNull(schema); - Assert.Equal(JsonSchema.String, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); + } - // Verify it's not a schema for Task, but for string - Assert.False(schema.AsObject().ContainsKey(JsonSchema.Properties)); + #endregion + + #region ValueTask Nullable Unwrapping Tests + + [Theory] + [InlineData(nameof(ValueTaskNullableStringMethod), JsonSchema.String)] + [InlineData(nameof(ValueTaskNullableIntMethod), JsonSchema.Integer)] + public void GetReturnSchema_ValueTaskNullablePrimitive_UnwrapsWithoutRequired(string methodName, string expectedType) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: false); } [Fact] - public void GetReturnSchema_TaskInt_UnwrapsToIntegerSchema() + public void GetReturnSchema_ValueTaskNullableCustomType_UnwrapsToCustomTypeSchemaWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullableCustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); + AssertAllRefsDefined(schema!); + } - // Act - var schema = reflector.GetReturnSchema(methodInfo); + [Fact] + public void GetReturnSchema_ValueTaskNullablePerson_UnwrapsWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullablePersonMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); + } - // Assert + [Fact] + public void GetReturnSchema_ValueTaskNullableAddress_UnwrapsWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullableAddressMethod)); Assert.NotNull(schema); - Assert.Equal(JsonSchema.Integer, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] - public void GetReturnSchema_TaskBool_UnwrapsToBooleanSchema() + public void GetReturnSchema_ValueTaskNullableCompany_UnwrapsWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskBoolMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullableCompanyMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); + } - // Act - var schema = reflector.GetReturnSchema(methodInfo); + #endregion - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Boolean, schema[JsonSchema.Type]?.ToString()); + // NOTE: Nullable ValueTask tests removed because ValueTask is a struct and cannot be made nullable + // in the reference type sense (ValueTask? is not valid) + + #region Custom Type Tests + + [Fact] + public void GetReturnSchema_CustomType_ReturnsValidObjectSchema() + { + var schema = GetReturnSchemaForMethod(nameof(CustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: true); + AssertAllRefsDefined(schema!); } [Fact] - public void GetReturnSchema_TaskCustomType_UnwrapsToCustomTypeSchema() + public void GetReturnSchema_ComplexType_ReturnsValidNestedSchema() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskCustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = GetReturnSchemaForMethod(nameof(ComplexTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "StringProperty", "IntProperty", "NestedObject", "StringArray" }, shouldBeRequired: true); + AssertResultDefines(schema!, typeof(ComplexReturnType), typeof(CustomReturnType)); + AssertAllRefsDefined(schema!); + } - // Act - var schema = reflector.GetReturnSchema(methodInfo); + [Fact] + public void GetReturnSchema_Person_ReturnsValidObjectSchema() + { + var schema = GetReturnSchemaForMethod(nameof(PersonMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); + } - // Assert + [Fact] + public void GetReturnSchema_Address_ReturnsValidObjectSchema() + { + var schema = GetReturnSchemaForMethod(nameof(AddressMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); + } - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey("Name")); - Assert.True(properties.ContainsKey("Value")); + [Fact] + public void GetReturnSchema_Company_ReturnsValidObjectSchema() + { + var schema = GetReturnSchemaForMethod(nameof(CompanyMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } #endregion - #region ValueTask Unwrapping Tests + #region Nullable Custom Type Tests [Fact] - public void GetReturnSchema_ValueTaskString_UnwrapsToStringSchema() + public void GetReturnSchema_NullableCustomType_ReturnsValidObjectSchemaWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ValueTaskStringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = GetReturnSchemaForMethod(nameof(NullableCustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); + AssertAllRefsDefined(schema!); + } - // Act - var schema = reflector.GetReturnSchema(methodInfo); + [Fact] + public void GetReturnSchema_NullableComplexType_ReturnsValidNestedSchemaWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(NullableComplexTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "StringProperty", "IntProperty", "NestedObject", "StringArray" }, shouldBeRequired: false); + AssertResultDefines(schema!, typeof(ComplexReturnType), typeof(CustomReturnType)); + AssertAllRefsDefined(schema!); + } - // Assert + [Fact] + public void GetReturnSchema_NullablePerson_ReturnsValidObjectSchemaWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(NullablePersonMethod)); Assert.NotNull(schema); - Assert.Equal(JsonSchema.String, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] - public void GetReturnSchema_ValueTaskInt_UnwrapsToIntegerSchema() + public void GetReturnSchema_NullableAddress_ReturnsValidObjectSchemaWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ValueTaskIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); + var schema = GetReturnSchemaForMethod(nameof(NullableAddressMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); + } - // Assert + [Fact] + public void GetReturnSchema_NullableCompany_ReturnsValidObjectSchemaWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(NullableCompanyMethod)); Assert.NotNull(schema); - Assert.Equal(JsonSchema.Integer, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); + } + + #endregion + + #region Collection Tests + + [Theory] + [InlineData(nameof(StringArrayMethod), JsonSchema.String)] + [InlineData(nameof(ListIntMethod), JsonSchema.Integer)] + public void GetReturnSchema_CollectionTypes_ReturnsArraySchema(string methodName, string expectedItemType) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertArrayReturnSchema(schema!, expectedItemType, shouldBeRequired: true); + AssertAllRefsDefined(schema!); } + #endregion + + #region Nullable Collection Tests + [Fact] - public void GetReturnSchema_ValueTaskCustomType_UnwrapsToCustomTypeSchema() + public void GetReturnSchema_NullableStringArray_ReturnsArraySchemaWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(NullableStringArrayMethod)); + AssertArrayReturnSchema(schema!, JsonSchema.String, shouldBeRequired: false); + AssertAllRefsDefined(schema!); + } + + #endregion + + #region List Tests + + [Theory] + [InlineData(nameof(ListComplexTypeMethod), true)] + [InlineData(nameof(NullableListComplexTypeMethod), false)] + public void GetReturnSchema_ListComplexType_ReturnsArraySchemaWithComplexItems(string methodName, bool shouldBeRequired) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertComplexListReturnSchema(schema!, shouldBeRequired); + AssertAllRefsDefined(schema!); + } + + [Theory] + [InlineData(nameof(ListNullableComplexTypeMethod), true)] + [InlineData(nameof(NullableListNullableComplexTypeMethod), false)] + public void GetReturnSchema_ListNullableComplexType_ReturnsArraySchemaWithNullableComplexItems(string methodName, bool shouldBeRequired) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertComplexListReturnSchema(schema!, shouldBeRequired, itemsAreNullable: true); + AssertAllRefsDefined(schema!); + } + + [Theory] + [InlineData(nameof(TaskListComplexTypeMethod), true)] +#if NET5_0_OR_GREATER + [InlineData(nameof(NullableTaskListComplexTypeMethod), false)] +#else + [InlineData(nameof(NullableTaskListComplexTypeMethod), true)] // netstandard2.1 cannot detect Task? nullability +#endif + public void GetReturnSchema_TaskListComplexType_UnwrapsToArraySchemaWithComplexItems(string methodName, bool shouldBeRequired) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertComplexListReturnSchema(schema!, shouldBeRequired); + AssertAllRefsDefined(schema!); + } + + [Theory] + [InlineData(nameof(TaskNullableListComplexTypeMethod), false)] + [InlineData(nameof(NullableTaskNullableListComplexTypeMethod), false)] + public void GetReturnSchema_TaskNullableListComplexType_UnwrapsToArraySchemaWithoutRequired(string methodName, bool shouldBeRequired) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertComplexListReturnSchema(schema!, shouldBeRequired); + AssertAllRefsDefined(schema!); + } + + [Theory] + [InlineData(nameof(TaskListNullableComplexTypeMethod), true)] +#if NET5_0_OR_GREATER + [InlineData(nameof(NullableTaskListNullableComplexTypeMethod), false)] +#else + [InlineData(nameof(NullableTaskListNullableComplexTypeMethod), true)] // netstandard2.1 cannot detect Task? nullability +#endif + public void GetReturnSchema_TaskListNullableComplexType_UnwrapsToArraySchemaWithNullableItems(string methodName, bool shouldBeRequired) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertComplexListReturnSchema(schema!, shouldBeRequired, itemsAreNullable: true); + AssertAllRefsDefined(schema!); + } + + [Theory] + [InlineData(nameof(TaskNullableListNullableComplexTypeMethod), false)] + [InlineData(nameof(NullableTaskNullableListNullableComplexTypeMethod), false)] + public void GetReturnSchema_TaskNullableListNullableComplexType_UnwrapsToArraySchemaWithNullableItemsWithoutRequired(string methodName, bool shouldBeRequired) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertComplexListReturnSchema(schema!, shouldBeRequired, itemsAreNullable: true); + AssertAllRefsDefined(schema!); + } + + #endregion + + #region JustRef Parameter Tests + + [Fact] + public void GetReturnSchema_CustomType_WithJustRef_ReturnsRefSchema() { // Arrange var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ValueTaskCustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var methodInfo = GetType().GetMethod(nameof(CustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; // Act - var schema = reflector.GetReturnSchema(methodInfo); + var schema = reflector.GetReturnSchema(methodInfo, justRef: true); // Assert Assert.NotNull(schema); @@ -304,23 +688,27 @@ public void GetReturnSchema_ValueTaskCustomType_UnwrapsToCustomTypeSchema() Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey("Name")); - Assert.True(properties.ContainsKey("Value")); - } + Assert.True(properties.ContainsKey(JsonSchema.Result)); - #endregion + // The result property should contain a $ref to CustomReturnType + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + Assert.True(resultSchema.ContainsKey(JsonSchema.Ref)); + Assert.Contains("CustomReturnType", resultSchema[JsonSchema.Ref]?.ToString()); - #region Custom Type Tests + // $defs should exist with the full type definition + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + AssertAllRefsDefined(schema); + } [Fact] - public void GetReturnSchema_CustomType_ReturnsValidObjectSchema() + public void GetReturnSchema_PrimitiveType_WithJustRef_ReturnsFullSchema() { // Arrange var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(CustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var methodInfo = GetType().GetMethod(nameof(StringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; // Act - var schema = reflector.GetReturnSchema(methodInfo); + var schema = reflector.GetReturnSchema(methodInfo, justRef: true); // Assert Assert.NotNull(schema); @@ -328,23 +716,27 @@ public void GetReturnSchema_CustomType_ReturnsValidObjectSchema() Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey("Name")); - Assert.True(properties.ContainsKey("Value")); + Assert.True(properties.ContainsKey(JsonSchema.Result)); - // Verify property types - Assert.Equal(JsonSchema.String, properties["Name"]![JsonSchema.Type]?.ToString()); - Assert.Equal(JsonSchema.Integer, properties["Value"]![JsonSchema.Type]?.ToString()); + // Primitive types should be inlined even with justRef=true + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + Assert.Equal(JsonSchema.String, resultSchema[JsonSchema.Type]?.ToString()); + Assert.False(resultSchema.ContainsKey(JsonSchema.Ref)); + AssertAllRefsDefined(schema); } - [Fact] - public void GetReturnSchema_ComplexType_ReturnsValidNestedSchema() + [Theory] + [InlineData(nameof(PersonMethod), "Person")] + [InlineData(nameof(AddressMethod), "Address")] + [InlineData(nameof(CompanyMethod), "Company")] + public void GetReturnSchema_OuterAssemblyType_WithJustRef_ReturnsRefSchema(string methodName, string expectedTypeName) { // Arrange var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ComplexTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var methodInfo = GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance)!; // Act - var schema = reflector.GetReturnSchema(methodInfo); + var schema = reflector.GetReturnSchema(methodInfo, justRef: true); // Assert Assert.NotNull(schema); @@ -352,103 +744,221 @@ public void GetReturnSchema_ComplexType_ReturnsValidNestedSchema() Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey("StringProperty")); - Assert.True(properties.ContainsKey("IntProperty")); - Assert.True(properties.ContainsKey("NestedObject")); - Assert.True(properties.ContainsKey("StringArray")); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + + // The result property should contain a $ref to the OuterAssembly type + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + Assert.True(resultSchema.ContainsKey(JsonSchema.Ref)); + Assert.Contains(expectedTypeName, resultSchema[JsonSchema.Ref]?.ToString()); + + // $defs should exist with the full type definition + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + AssertAllRefsDefined(schema); } #endregion - #region Collection Tests + #region Error Handling Tests [Fact] - public void GetReturnSchema_StringArray_ReturnsArraySchema() + public void GetReturnSchema_NullMethodInfo_ThrowsArgumentNullException() { // Arrange var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(StringArrayMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - // Act + // Act & Assert + Assert.Throws(() => reflector.GetReturnSchema(null!)); + } + + #endregion + + #region WrapperClass Echo Method Tests + + /// + /// Helper to get return schema for a WrapperClass method + /// + private JsonNode? GetWrapperMethodReturnSchema(Type wrapperType, string methodName) + { + var reflector = new Reflector(); + var methodInfo = wrapperType.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)!; var schema = reflector.GetReturnSchema(methodInfo); - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Array, schema[JsonSchema.Type]?.ToString()); - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Items)); + _output.WriteLine($"Return schema for {wrapperType.GetTypeShortName()}.{methodName}:"); + _output.WriteLine(schema?.ToString() ?? "null"); + _output.WriteLine(""); - var items = schema[JsonSchema.Items]!; - Assert.Equal(JsonSchema.String, items[JsonSchema.Type]?.ToString()); + return schema; } - [Fact] - public void GetReturnSchema_ListInt_ReturnsArraySchema() + [Theory] + [InlineData(typeof(string), nameof(WrapperClass.Echo), JsonSchema.String, false)] // string is reference type, T is nullable + [InlineData(typeof(int), nameof(WrapperClass.Echo), JsonSchema.Integer, true)] // int is value type, T is non-nullable + [InlineData(typeof(bool), nameof(WrapperClass.Echo), JsonSchema.Boolean, true)] // bool is value type, T is non-nullable + [InlineData(typeof(double), nameof(WrapperClass.Echo), JsonSchema.Number, true)] // double is value type, T is non-nullable + [InlineData(typeof(string), nameof(WrapperClass.EchoNullable), JsonSchema.String, false)] // string? is nullable reference + [InlineData(typeof(int), nameof(WrapperClass.EchoNullable), JsonSchema.Integer, true)] // int? (Nullable) is itself non-nullable struct + [InlineData(typeof(bool), nameof(WrapperClass.EchoNullable), JsonSchema.Boolean, true)] // bool? (Nullable) is itself non-nullable struct + [InlineData(typeof(double), nameof(WrapperClass.EchoNullable), JsonSchema.Number, true)] // double? (Nullable) is itself non-nullable struct + public void GetReturnSchema_WrapperEchoPrimitive_ReturnsCorrectSchema(Type genericType, string methodName, string expectedType, bool shouldBeRequired) { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ListIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var schema = GetWrapperMethodReturnSchema(wrapperType, methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired); + AssertAllRefsDefined(schema!); + } - // Act - var schema = reflector.GetReturnSchema(methodInfo); + [Theory] + [InlineData(typeof(CustomReturnType), true)] + [InlineData(typeof(ComplexReturnType), true)] + public void GetReturnSchema_WrapperEchoCustomType_ReturnsCorrectSchema(Type genericType, bool shouldBeRequired) + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); - // Assert Assert.NotNull(schema); - Assert.Equal(JsonSchema.Array, schema[JsonSchema.Type]?.ToString()); - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Items)); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var items = schema[JsonSchema.Items]!; - Assert.Equal(JsonSchema.Integer, items[JsonSchema.Type]?.ToString()); + if (shouldBeRequired) + AssertResultRequired(schema); + else + AssertResultNotRequired(schema); + AssertAllRefsDefined(schema); } - #endregion + [Theory] + [InlineData(typeof(CustomReturnType), false)] + [InlineData(typeof(ComplexReturnType), false)] + public void GetReturnSchema_WrapperEchoNullableCustomType_ReturnsCorrectSchema(Type genericType, bool shouldBeRequired) + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); - #region JustRef Parameter Tests + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + + if (shouldBeRequired) + AssertResultRequired(schema); + else + AssertResultNotRequired(schema); + AssertAllRefsDefined(schema); + } + + [Theory] + [InlineData(typeof(string[]), JsonSchema.String, true)] + [InlineData(typeof(int[]), JsonSchema.Integer, true)] + public void GetReturnSchema_WrapperEchoArray_ReturnsCorrectSchema(Type genericType, string expectedItemType, bool shouldBeRequired) + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + AssertArrayReturnSchema(schema!, expectedItemType, shouldBeRequired); + AssertAllRefsDefined(schema!); + } + + [Theory] + [InlineData(typeof(string[]), JsonSchema.String, false)] + [InlineData(typeof(int[]), JsonSchema.Integer, false)] + public void GetReturnSchema_WrapperEchoNullableArray_ReturnsCorrectSchema(Type genericType, string expectedItemType, bool shouldBeRequired) + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + AssertArrayReturnSchema(schema!, expectedItemType, shouldBeRequired); + AssertAllRefsDefined(schema!); + } [Fact] - public void GetReturnSchema_CustomType_WithJustRef_ReturnsRefSchema() + public void GetReturnSchema_WrapperEchoListComplex_ReturnsCorrectSchema() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(CustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var wrapperType = typeof(WrapperClass>); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass>.Echo)); + AssertComplexListReturnSchema(schema!, shouldBeRequired: true); + AssertAllRefsDefined(schema!); + } - // Act - var schema = reflector.GetReturnSchema(methodInfo, justRef: true); + [Fact] + public void GetReturnSchema_WrapperEchoNullableListComplex_ReturnsCorrectSchema() + { + var wrapperType = typeof(WrapperClass>); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass>.EchoNullable)); + AssertComplexListReturnSchema(schema!, shouldBeRequired: false); + AssertAllRefsDefined(schema!); + } + + [Fact] + public void GetReturnSchema_WrapperEchoPerson_ReturnsCorrectSchema() + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(Person)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); - // Assert Assert.NotNull(schema); - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Ref)); - Assert.Contains("CustomReturnType", schema[JsonSchema.Ref]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] - public void GetReturnSchema_PrimitiveType_WithJustRef_ReturnsFullSchema() + public void GetReturnSchema_WrapperEchoAddress_ReturnsCorrectSchema() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(StringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(Address)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass
.Echo)); - // Act - var schema = reflector.GetReturnSchema(methodInfo, justRef: true); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); + } + + [Fact] + public void GetReturnSchema_WrapperEchoCompany_ReturnsCorrectSchema() + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(Company)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); - // Assert Assert.NotNull(schema); - // Primitive types should be inlined even with justRef=true - Assert.Equal(JsonSchema.String, schema[JsonSchema.Type]?.ToString()); - Assert.False(schema.AsObject().ContainsKey(JsonSchema.Ref)); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } - #endregion + [Fact] + public void GetReturnSchema_WrapperEchoNullablePerson_ReturnsCorrectSchema() + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(Person)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); - #region Error Handling Tests + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); + } [Fact] - public void GetReturnSchema_NullMethodInfo_ThrowsArgumentNullException() + public void GetReturnSchema_WrapperEchoNullableAddress_ReturnsCorrectSchema() { - // Arrange - var reflector = new Reflector(); + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(Address)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass
.EchoNullable)); - // Act & Assert - Assert.Throws(() => reflector.GetReturnSchema(null!)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); + } + + [Fact] + public void GetReturnSchema_WrapperEchoNullableCompany_ReturnsCorrectSchema() + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(Company)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } #endregion diff --git a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs index 1dc9d49a..845a9ac6 100644 --- a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs +++ b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.Json.Nodes; @@ -17,7 +18,7 @@ protected SchemaTestBase(ITestOutputHelper output) : base(output) { reflector ??= new Reflector(); - var schema = reflector.GetSchema(type, justRef: false); + var schema = reflector.GetSchema(type); _output.WriteLine($"Schema for {type.GetTypeShortName()}"); _output.WriteLine($"{schema}"); @@ -35,7 +36,7 @@ protected void TestMethodInputs_PropertyRefs(Reflector? reflector, MethodInfo me { reflector ??= new Reflector(); - var schema = reflector.GetArgumentsSchema(methodInfo, justRef: false)!; + var schema = reflector.GetArgumentsSchema(methodInfo)!; _output.WriteLine(schema.ToString()); @@ -53,7 +54,7 @@ protected void TestMethodInputs_PropertyRefs(Reflector? reflector, MethodInfo me var methodParameter = methodInfo.GetParameters().FirstOrDefault(p => p.Name == parameterName); Assert.NotNull(methodParameter); - var typeId = methodParameter.ParameterType.GetTypeId(); + var typeId = methodParameter.ParameterType.GetSchemaTypeId(); var refString = $"{JsonSchema.RefValue}{typeId}"; var targetDefine = defines[typeId]; @@ -73,7 +74,7 @@ protected void TestMethodInputs_PropertyRefs(Reflector? reflector, MethodInfo me { reflector ??= new Reflector(); - var schema = reflector.GetArgumentsSchema(methodInfo, justRef: false)!; + var schema = reflector.GetArgumentsSchema(methodInfo)!; _output.WriteLine(schema.ToString()); @@ -88,12 +89,339 @@ protected void TestMethodInputs_PropertyRefs(Reflector? reflector, MethodInfo me foreach (var expectedType in expectedTypes) { - var typeId = expectedType.GetTypeId(); + var typeId = expectedType.GetSchemaTypeId(); var targetDefine = defines[typeId]; Assert.NotNull(targetDefine); } return schema; } + + #region Return Schema Helper Methods + + /// + /// Gets the return schema for a method by name + /// + protected JsonNode? GetReturnSchemaForMethod(string methodName, bool justRef = false) + { + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = reflector.GetReturnSchema(methodInfo, justRef); + + _output.WriteLine($"Return schema for {methodName}:"); + _output.WriteLine(schema?.ToString() ?? "null"); + _output.WriteLine(""); + + return schema; + } + + /// + /// Asserts that a primitive return type has the correct schema structure + /// + protected void AssertPrimitiveReturnSchema(JsonNode schema, string expectedJsonType, bool shouldBeRequired = true) + { + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(expectedJsonType, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + if (shouldBeRequired) + { + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Required)); + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.Single(required); + Assert.Equal(JsonSchema.Result, required[0]?.ToString()); + } + else + { + AssertResultNotRequired(schema); + } + } + + /// + /// Asserts that "result" is NOT in the required array (for nullable types) + /// + protected void AssertResultNotRequired(JsonNode schema) + { + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + /// + /// Asserts that "result" IS in the required array (for non-nullable types) + /// + protected void AssertResultRequired(JsonNode schema) + { + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.Contains(required, r => r?.ToString() == JsonSchema.Result); + } + } + + /// + /// Asserts that all expected types are defined in the $defs section of the schema OR referenced within the schema. + /// This method recursively checks all $ref values in the schema to ensure nested types are properly referenced. + /// Only non-primitive and non-enum types should be included in $defs, as primitives and enums are inlined. + /// Verifies that at minimum the expected types are present (additional types may be included by the schema generator). + /// + protected void AssertResultDefines(JsonNode schema, params Type[] expectedTypes) + { + Assert.NotNull(schema); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs), "Schema should contain $defs section"); + + var defines = schema[JsonSchema.Defs]!.AsObject(); + Assert.NotNull(defines); + + // Collect all $ref values throughout the schema (includes nested references) + var allReferences = new HashSet(); + CollectAllReferences(schema, allReferences); + + // Filter expected types to only include non-primitive, non-enum types + var nonPrimitiveTypes = expectedTypes.Where(t => !TypeUtils.IsPrimitive(t) && !t.IsEnum).ToArray(); + + // Verify all expected types are present in $defs and referenced + foreach (var expectedType in nonPrimitiveTypes) + { + var expectedTypeId = expectedType.GetSchemaTypeId(); + + // Check if the type is either: + // 1. Directly defined in $defs (exact match) + var isDirectlyDefined = defines.ContainsKey(expectedTypeId); + + // 2. Referenced somewhere in the schema (exact match in $ref) + var expectedRef = $"{JsonSchema.RefValue}{expectedTypeId}"; + var isReferenced = allReferences.Contains(expectedRef); + + Assert.True(isDirectlyDefined && isReferenced, + $"Expected type '{expectedType.GetTypeShortName()}' with ID '{expectedTypeId}' should be both defined in $defs and referenced in schema. " + + $"Expected $ref: '{expectedRef}'. " + + $"Defined types: {string.Join(", ", defines.Select(d => d.Key))}. " + + $"Referenced types: {string.Join(", ", allReferences)}"); + } + + // Verify that if any of the expected types appear in $defs, they are not primitive or enum + foreach (var expectedType in expectedTypes) + { + var expectedTypeId = expectedType.GetSchemaTypeId(); + if (defines.ContainsKey(expectedTypeId)) + { + Assert.False(TypeUtils.IsPrimitive(expectedType) || expectedType.IsEnum, + $"Type '{expectedType.GetTypeShortName()}' with ID '{expectedTypeId}' is primitive or enum and should not be in $defs. " + + $"Primitives and enums should be inlined in the schema."); + } + } + } + + /// + /// Helper method to recursively collect all $ref values in a JSON schema + /// + private void CollectAllReferences(JsonNode? node, HashSet references) + { + if (node == null) return; + + if (node is JsonObject obj) + { + foreach (var kvp in obj) + { + if (kvp.Key == JsonSchema.Ref && kvp.Value != null) + { + references.Add(kvp.Value.ToString()); + } + CollectAllReferences(kvp.Value, references); + } + } + else if (node is JsonArray arr) + { + foreach (var item in arr) + { + CollectAllReferences(item, references); + } + } + } + + /// + /// Asserts that all $ref references found in the schema are defined in the $defs section. + /// This method recursively scans the entire schema for all $ref values and verifies that + /// each referenced type has a corresponding definition in $defs. + /// + protected void AssertAllRefsDefined(JsonNode schema) + { + Assert.NotNull(schema); + + // Collect all $ref values in the schema + var allReferences = new HashSet(); + CollectAllReferences(schema, allReferences); + + // If there are no references, we're done + if (allReferences.Count == 0) + { + return; + } + + // Schema must have $defs if there are references + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs), + $"Schema contains {allReferences.Count} $ref reference(s) but no $defs section. References: {string.Join(", ", allReferences)}"); + + var defines = schema[JsonSchema.Defs]!.AsObject(); + Assert.NotNull(defines); + + // Check each reference to ensure it's defined + foreach (var reference in allReferences) + { + // Extract the type ID from the reference (e.g., "#/$defs/TypeId" -> "TypeId") + var typeId = reference.Replace(JsonSchema.RefValue, string.Empty); + + Assert.True(defines.ContainsKey(typeId), + $"Reference '{reference}' (type ID: '{typeId}') is not defined in $defs. " + + $"Available definitions: {string.Join(", ", defines.Select(d => d.Key))}"); + } + } + + /// + /// Asserts that a custom type return schema has the correct structure + /// + protected void AssertCustomTypeReturnSchema(JsonNode schema, string[] expectedProperties, bool shouldBeRequired = true) + { + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); + + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + + // Check if it's a ref or inline + if (resultSchema.ContainsKey(JsonSchema.Ref)) + { + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else if (resultSchema.ContainsKey(JsonSchema.Properties)) + { + var typeProperties = resultSchema[JsonSchema.Properties]!.AsObject(); + foreach (var expectedProp in expectedProperties) + { + Assert.True(typeProperties.ContainsKey(expectedProp)); + } + } + else + { + // If neither ref nor inline properties, this is unexpected + Assert.True(resultSchema.ContainsKey(JsonSchema.Ref) || resultSchema.ContainsKey(JsonSchema.Properties), + "Result schema should contain either $ref or properties"); + } + + if (shouldBeRequired) + AssertResultRequired(schema); + else + AssertResultNotRequired(schema); + } + + /// + /// Asserts that an array return type has the correct schema structure + /// + protected void AssertArrayReturnSchema(JsonNode schema, string expectedItemType, bool shouldBeRequired = true) + { + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); + + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + + var resultNode = properties[JsonSchema.Result]!; + + if (resultNode is JsonObject resultSchema && resultSchema.ContainsKey(JsonSchema.Ref)) + { + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else if (resultNode is JsonObject resultInlineSchema) + { + Assert.Equal(JsonSchema.Array, resultInlineSchema[JsonSchema.Type]?.ToString()); + Assert.True(resultInlineSchema.ContainsKey(JsonSchema.Items)); + + var items = resultInlineSchema[JsonSchema.Items]!; + Assert.Equal(expectedItemType, items[JsonSchema.Type]?.ToString()); + } + else + { + Assert.Fail("Expected result to be a schema object"); + } + + if (shouldBeRequired) + AssertResultRequired(schema); + else + AssertResultNotRequired(schema); + } + + /// + /// Asserts that a List return type has the correct schema structure + /// + protected void AssertComplexListReturnSchema(JsonNode schema, bool shouldBeRequired = true, bool itemsAreNullable = false) + { + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); + + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + + var resultNode = properties[JsonSchema.Result]!; + + if (resultNode is JsonObject resultSchema && resultSchema.ContainsKey(JsonSchema.Ref)) + { + // Result is a reference to a list definition + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else if (resultNode is JsonObject resultInlineSchema) + { + // Result is an inline array schema + Assert.Equal(JsonSchema.Array, resultInlineSchema[JsonSchema.Type]?.ToString()); + Assert.True(resultInlineSchema.ContainsKey(JsonSchema.Items)); + + var items = resultInlineSchema[JsonSchema.Items]!; + + // Items should be either a reference to ComplexReturnType or an inline object + if (items is JsonObject itemsSchema) + { + if (itemsSchema.ContainsKey(JsonSchema.Ref)) + { + // Items are a reference to ComplexReturnType + Assert.Contains("ComplexReturnType", itemsSchema[JsonSchema.Ref]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else if (itemsSchema.ContainsKey(JsonSchema.Type)) + { + // Items are inlined + Assert.Equal(JsonSchema.Object, itemsSchema[JsonSchema.Type]?.ToString()); + } + else + { + Assert.Fail("Items schema should contain either $ref or type"); + } + } + else + { + Assert.Fail("Expected items to be a schema object"); + } + } + else + { + Assert.Fail("Expected result to be a schema object"); + } + + if (shouldBeRequired) + AssertResultRequired(schema); + else + AssertResultNotRequired(schema); + } + + #endregion } } diff --git a/ReflectorNet.Tests/SchemaTests/TestDescription.Utils.cs b/ReflectorNet.Tests/SchemaTests/TestDescription.Utils.cs index bd178478..71aedb19 100644 --- a/ReflectorNet.Tests/SchemaTests/TestDescription.Utils.cs +++ b/ReflectorNet.Tests/SchemaTests/TestDescription.Utils.cs @@ -15,7 +15,7 @@ void TestClassMembersDescription(Type type, Reflector? reflector = null, Binding _output.WriteLine($"Testing members description for type: '{type.GetTypeShortName()}'"); - var schema = reflector.GetSchema(type, justRef: false); + var schema = reflector.GetSchema(type); Assert.NotNull(schema); var properties = default(JsonNode?); diff --git a/ReflectorNet.Tests/SchemaTests/TestDescription.cs b/ReflectorNet.Tests/SchemaTests/TestDescription.cs index 278454b7..f8052bd8 100644 --- a/ReflectorNet.Tests/SchemaTests/TestDescription.cs +++ b/ReflectorNet.Tests/SchemaTests/TestDescription.cs @@ -41,7 +41,7 @@ public void PropertyDescriptionOfCollectionWithDescriptions() TestClassMembersDescription(typeof(TestCollectionWithDescription), reflector); // For List, just test schema generation - var listComplexSchema = reflector.GetSchema(typeof(List), justRef: false); + var listComplexSchema = reflector.GetSchema(typeof(List)); Assert.NotNull(listComplexSchema); _output.WriteLine($"List schema: {listComplexSchema}"); } @@ -58,11 +58,11 @@ public void PropertyDescriptionOfReflectorNetModels() // SerializedMember and SerializedMemberList use custom converters with descriptions // Let's test their schema generation separately - var serializedMemberSchema = reflector.GetSchema(typeof(SerializedMember), justRef: false); + var serializedMemberSchema = reflector.GetSchema(typeof(SerializedMember)); Assert.NotNull(serializedMemberSchema); _output.WriteLine($"SerializedMember schema: {serializedMemberSchema}"); - var serializedMemberListSchema = reflector.GetSchema(typeof(SerializedMemberList), justRef: false); + var serializedMemberListSchema = reflector.GetSchema(typeof(SerializedMemberList)); Assert.NotNull(serializedMemberListSchema); _output.WriteLine($"SerializedMemberList schema: {serializedMemberListSchema}"); } @@ -77,11 +77,11 @@ public void PropertyDescriptionOfArrayTypes() TestClassMembersDescription(typeof(TestClassWithDescriptions[]), reflector); // For primitive arrays, just test schema generation - var stringArraySchema = reflector.GetSchema(typeof(string[]), justRef: false); + var stringArraySchema = reflector.GetSchema(typeof(string[])); Assert.NotNull(stringArraySchema); _output.WriteLine($"string[] schema: {stringArraySchema}"); - var intArraySchema = reflector.GetSchema(typeof(int[]), justRef: false); + var intArraySchema = reflector.GetSchema(typeof(int[])); Assert.NotNull(intArraySchema); _output.WriteLine($"int[] schema: {intArraySchema}"); } @@ -96,15 +96,15 @@ public void PropertyDescriptionOfGenericCollections() TestClassMembersDescription(typeof(List), reflector); // For primitive collections, just test schema generation - var listStringSchema = reflector.GetSchema(typeof(List), justRef: false); + var listStringSchema = reflector.GetSchema(typeof(List)); Assert.NotNull(listStringSchema); _output.WriteLine($"List schema: {listStringSchema}"); - var listIntSchema = reflector.GetSchema(typeof(List), justRef: false); + var listIntSchema = reflector.GetSchema(typeof(List)); Assert.NotNull(listIntSchema); _output.WriteLine($"List schema: {listIntSchema}"); - var dictionarySchema = reflector.GetSchema(typeof(Dictionary), justRef: false); + var dictionarySchema = reflector.GetSchema(typeof(Dictionary)); Assert.NotNull(dictionarySchema); _output.WriteLine($"Dictionary schema: {dictionarySchema}"); } @@ -115,19 +115,19 @@ public void PropertyDescriptionOfPrimitiveTypes() var reflector = new Reflector(); // Primitive types don't have members to test, but we can test schema generation - var stringSchema = reflector.GetSchema(typeof(string), justRef: false); + var stringSchema = reflector.GetSchema(typeof(string)); Assert.NotNull(stringSchema); _output.WriteLine($"String schema: {stringSchema}"); - var intSchema = reflector.GetSchema(typeof(int), justRef: false); + var intSchema = reflector.GetSchema(typeof(int)); Assert.NotNull(intSchema); _output.WriteLine($"Int schema: {intSchema}"); - var boolSchema = reflector.GetSchema(typeof(bool), justRef: false); + var boolSchema = reflector.GetSchema(typeof(bool)); Assert.NotNull(boolSchema); _output.WriteLine($"Bool schema: {boolSchema}"); - var doubleSchema = reflector.GetSchema(typeof(double), justRef: false); + var doubleSchema = reflector.GetSchema(typeof(double)); Assert.NotNull(doubleSchema); _output.WriteLine($"Double schema: {doubleSchema}"); } @@ -138,15 +138,15 @@ public void PropertyDescriptionOfNullableTypes() var reflector = new Reflector(); // Nullable types also don't have members to test, but we can test schema generation - var nullableIntSchema = reflector.GetSchema(typeof(int?), justRef: false); + var nullableIntSchema = reflector.GetSchema(typeof(int?)); Assert.NotNull(nullableIntSchema); _output.WriteLine($"Nullable int schema: {nullableIntSchema}"); - var nullableBoolSchema = reflector.GetSchema(typeof(bool?), justRef: false); + var nullableBoolSchema = reflector.GetSchema(typeof(bool?)); Assert.NotNull(nullableBoolSchema); _output.WriteLine($"Nullable bool schema: {nullableBoolSchema}"); - var nullableDateTimeSchema = reflector.GetSchema(typeof(DateTime?), justRef: false); + var nullableDateTimeSchema = reflector.GetSchema(typeof(DateTime?)); Assert.NotNull(nullableDateTimeSchema); _output.WriteLine($"Nullable DateTime schema: {nullableDateTimeSchema}"); } @@ -157,7 +157,7 @@ public void ClassLevelDescription() var reflector = new Reflector(); // Test that class-level descriptions are properly captured - var schema = reflector.GetSchema(typeof(GameObjectRef), justRef: false); + var schema = reflector.GetSchema(typeof(GameObjectRef)); Assert.NotNull(schema); var description = schema[JsonSchema.Description]?.ToString(); @@ -173,7 +173,7 @@ public void ClassLevelDescriptionForTestClass() var reflector = new Reflector(); // Test class-level description for our test class - var schema = reflector.GetSchema(typeof(TestClassWithDescriptions), justRef: false); + var schema = reflector.GetSchema(typeof(TestClassWithDescriptions)); Assert.NotNull(schema); var description = schema[JsonSchema.Description]?.ToString(); @@ -189,7 +189,7 @@ public void MissingDescriptionsHandledGracefully() var reflector = new Reflector(); // Create a simple class without descriptions to test graceful handling - var schema = reflector.GetSchema(typeof(string), justRef: false); + var schema = reflector.GetSchema(typeof(string)); Assert.NotNull(schema); // Primitive types might not have descriptions, that's OK @@ -223,7 +223,7 @@ public void StructLevelDescription() var reflector = new Reflector(); // Test that struct-level descriptions are properly captured - var schema = reflector.GetSchema(typeof(TestStructWithDescriptions), justRef: false); + var schema = reflector.GetSchema(typeof(TestStructWithDescriptions)); Assert.NotNull(schema); var description = schema[JsonSchema.Description]?.ToString(); @@ -239,7 +239,7 @@ public void EnumLevelDescription() var reflector = new Reflector(); // Test that enum-level descriptions are properly captured - var schema = reflector.GetSchema(typeof(TestEnumWithDescriptions), justRef: false); + var schema = reflector.GetSchema(typeof(TestEnumWithDescriptions)); Assert.NotNull(schema); // For enums, the schema might be different - let's just verify it generates @@ -275,7 +275,7 @@ public void ValidateSpecificDescriptionContent() var reflector = new Reflector(); // Test that specific descriptions are correctly applied to schema properties - var schema = reflector.GetSchema(typeof(GameObjectRef), justRef: false); + var schema = reflector.GetSchema(typeof(GameObjectRef)); Assert.NotNull(schema); var properties = schema[JsonSchema.Properties]?.AsObject(); @@ -318,7 +318,7 @@ public void CustomConverterDescriptions() reflector.JsonSerializer.AddConverter(new SerializedMemberCustomDescriptionConverter(reflector)); // Test that custom JSON converters provide proper descriptions in schema - var serializedMemberSchema = reflector.GetSchema(typeof(SerializedMember), justRef: false); + var serializedMemberSchema = reflector.GetSchema(typeof(SerializedMember)); Assert.NotNull(serializedMemberSchema); var properties = serializedMemberSchema[JsonSchema.Properties]?.AsObject(); @@ -335,7 +335,7 @@ void NoEmptyOrNullDescriptions(Reflector reflector, IEnumerable types) { foreach (var type in types) { - var schema = reflector.GetSchema(type, justRef: false); + var schema = reflector.GetSchema(type); var descriptions = JsonSchema.FindAllProperties(schema, JsonSchema.Description); diff --git a/ReflectorNet.Tests/SchemaTests/TestSchemaTypeId.cs b/ReflectorNet.Tests/SchemaTests/TestSchemaTypeId.cs new file mode 100644 index 00000000..a718637a --- /dev/null +++ b/ReflectorNet.Tests/SchemaTests/TestSchemaTypeId.cs @@ -0,0 +1,428 @@ +using System.Collections.Generic; +using com.IvanMurzak.ReflectorNet.Utils; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.SchemaTests +{ + public class TestSchemaTypeId : BaseTest + { + public TestSchemaTypeId(ITestOutputHelper output) : base(output) { } + + [Fact] + public void GetSchemaTypeId_SimpleArray_ShouldAppendArray() + { + // Arrange + var type = typeof(int[]); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"int[] -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_NestedArray_ShouldAppendMultipleArrays() + { + // Arrange + var type = typeof(int[][]); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"int[][] -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_TripleNestedArray_ShouldAppendThreeArrays() + { + // Arrange + var type = typeof(int[][][]); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"int[][][] -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_StringArray_ShouldWorkForAnyType() + { + // Arrange + var type = typeof(string[]); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.String{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"string[] -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfInt_ShouldNormalizeToArray() + { + // Arrange + var listType = typeof(List); + var arrayType = typeof(int[]); + + // Act + var listResult = listType.GetSchemaTypeId(); + var arrayResult = arrayType.GetSchemaTypeId(); + + // Assert + Assert.Equal(arrayResult, listResult); + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", listResult); + _output.WriteLine($"List -> {listResult}"); + _output.WriteLine($"int[] -> {arrayResult}"); + _output.WriteLine($"Both should be equal: {listResult == arrayResult}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfString_ShouldMatchStringArray() + { + // Arrange + var listType = typeof(List); + var arrayType = typeof(string[]); + + // Act + var listResult = listType.GetSchemaTypeId(); + var arrayResult = arrayType.GetSchemaTypeId(); + + // Assert + Assert.Equal(arrayResult, listResult); + Assert.Equal($"System.String{TypeUtils.ArraySuffix}", listResult); + _output.WriteLine($"List -> {listResult}"); + _output.WriteLine($"string[] -> {arrayResult}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfIntArray_ShouldNormalizeToDoubleArray() + { + // Arrange + var type = typeof(List); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"List -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfListOfInt_ShouldNormalizeToDoubleArray() + { + // Arrange + var type = typeof(List>); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"List> -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfListOfInt_ShouldMatchIntDoubleArray() + { + // Arrange + var listType = typeof(List>); + var arrayType = typeof(int[][]); + + // Act + var listResult = listType.GetSchemaTypeId(); + var arrayResult = arrayType.GetSchemaTypeId(); + + // Assert + Assert.Equal(arrayResult, listResult); + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", listResult); + _output.WriteLine($"List> -> {listResult}"); + _output.WriteLine($"int[][] -> {arrayResult}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfDoubleNestedArray_ShouldAppendThreeArrays() + { + // Arrange + var type = typeof(List); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"List -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfStringArray_ShouldWorkForAnyType() + { + // Arrange + var type = typeof(List); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.String{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"List -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ComplexNestedCollections_ShouldHandleDeepNesting() + { + // Arrange + var type = typeof(List[]>); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.String{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"List[]> -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_NonCollectionType_ShouldReturnFullName() + { + // Arrange + var type = typeof(string); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal("System.String", result); + _output.WriteLine($"string -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_NullableType_ShouldHandleUnderlyingType() + { + // Arrange + var type = typeof(int?); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal("System.Int32", result); + _output.WriteLine($"int? -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_NullableArrayType_ShouldHandleUnderlyingArrayType() + { + // Arrange + var type = typeof(int?[]); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"int?[] -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_IEnumerableOfInt_ShouldReturnGenericFormat() + { + // Arrange + var type = typeof(IEnumerable); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal("System.Collections.Generic.IEnumerable", result); + _output.WriteLine($"IEnumerable -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ICollectionOfString_ShouldReturnGenericFormat() + { + // Arrange + var type = typeof(ICollection); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal("System.Collections.Generic.ICollection", result); + _output.WriteLine($"ICollection -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_IListOfInt_ShouldReturnGenericFormat() + { + // Arrange + var type = typeof(IList); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal("System.Collections.Generic.IList", result); + _output.WriteLine($"IList -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_HashSetOfInt_ShouldNormalizeToArray() + { + // Arrange + var type = typeof(HashSet); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"HashSet -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_SortedSetOfString_ShouldNormalizeToArray() + { + // Arrange + var type = typeof(SortedSet); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.String{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"SortedSet -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_LinkedListOfInt_ShouldNormalizeToArray() + { + // Arrange + var type = typeof(LinkedList); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"LinkedList -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_QueueOfString_ShouldNormalizeToArray() + { + // Arrange + var type = typeof(Queue); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.String{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"Queue -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_StackOfInt_ShouldNormalizeToArray() + { + // Arrange + var type = typeof(Stack); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"Stack -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_NestedGenericCollections_AllShouldNormalize() + { + // Arrange + var listOfHashSet = typeof(List>); + var queueOfStack = typeof(Queue>); + var arrayOfLinkedList = typeof(LinkedList[]); + + // Act + var result1 = listOfHashSet.GetSchemaTypeId(); + var result2 = queueOfStack.GetSchemaTypeId(); + var result3 = arrayOfLinkedList.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result1); + Assert.Equal($"System.String{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result2); + Assert.Equal($"System.Double{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result3); + _output.WriteLine($"List> -> {result1}"); + _output.WriteLine($"Queue> -> {result2}"); + _output.WriteLine($"LinkedList[] -> {result3}"); + } + + [Fact] + public void GetSchemaTypeId_CustomClass_ShouldReturnFullName() + { + // Arrange + var type = typeof(TestType); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal("com.IvanMurzak.ReflectorNet.Tests.SchemaTests.TestType", result); + _output.WriteLine($"TestType -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfCustomClass_ShouldNormalizeToArray() + { + // Arrange + var type = typeof(List); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"com.IvanMurzak.ReflectorNet.Tests.SchemaTests.TestType{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"List -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ArrayOfCustomClass_ShouldAppendArray() + { + // Arrange + var type = typeof(TestType[]); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"com.IvanMurzak.ReflectorNet.Tests.SchemaTests.TestType{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"TestType[] -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ListAndArrayOfCustomClass_ShouldMatch() + { + // Arrange + var listType = typeof(List); + var arrayType = typeof(TestType[]); + + // Act + var listResult = listType.GetSchemaTypeId(); + var arrayResult = arrayType.GetSchemaTypeId(); + + // Assert + Assert.Equal(arrayResult, listResult); + Assert.Equal($"com.IvanMurzak.ReflectorNet.Tests.SchemaTests.TestType{TypeUtils.ArraySuffix}", listResult); + _output.WriteLine($"List -> {listResult}"); + _output.WriteLine($"TestType[] -> {arrayResult}"); + } + } +} diff --git a/ReflectorNet.Tests/SchemaTests/TestType.cs b/ReflectorNet.Tests/SchemaTests/TestType.cs index 3bf00a36..c5cb9ff5 100644 --- a/ReflectorNet.Tests/SchemaTests/TestType.cs +++ b/ReflectorNet.Tests/SchemaTests/TestType.cs @@ -66,12 +66,7 @@ void TestTypeName(Type type) Assert.Equal(type, TypeUtils.GetType(typeName)); } - [Fact] - public void GetTypeName_CurrentAssembly() - { - TestTypeName(typeof(Model.ParentClass)); - TestTypeName(typeof(Model.ParentClass.NestedClass)); - } + [Fact] public void GetTypeName_OuterAssembly() diff --git a/ReflectorNet.Tests/Utils/JsonObjectBuilder.cs b/ReflectorNet.Tests/Utils/JsonObjectBuilder.cs new file mode 100644 index 00000000..a6403457 --- /dev/null +++ b/ReflectorNet.Tests/Utils/JsonObjectBuilder.cs @@ -0,0 +1,190 @@ +using System; +using System.Text.Json; +using System.Text.Json.Nodes; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Tests.Model +{ + internal class JsonObjectBuilder + { + public JsonObject? Result { get; private set; } + + public JsonObjectBuilder() + { + Result = new JsonObject(); + } + + public JsonObjectBuilder SetNull() + { + Result = null; + return this; + } + + public JsonObjectBuilder SetTypeObject() + { + if (Result == null) + throw new InvalidOperationException("Cannot set type on a null schema."); + + Result[JsonSchema.Type] = JsonSchema.Object; + return this; + } + + public JsonObjectBuilder SetTypeArray() + { + if (Result == null) + throw new InvalidOperationException("Cannot set type on a null schema."); + + Result[JsonSchema.Type] = JsonSchema.Array; + return this; + } + + public JsonObjectBuilder AddRequired(string name) + { + if (Result == null) + throw new InvalidOperationException("Cannot add required property to a null schema."); + + if (Result[JsonSchema.Required] == null) + Result[JsonSchema.Required] = new JsonArray(); + + Result[JsonSchema.Required]!.AsArray().Add(name); + + return this; + } + + public JsonObjectBuilder AddSimpleProperty(string name, string type, bool required = false, string? description = null) + { + if (Result == null) + throw new InvalidOperationException("Cannot add property to a null schema."); + + if (Result[JsonSchema.Properties] == null) + Result[JsonSchema.Properties] = new JsonObject(); + + Result[JsonSchema.Properties]![name] = new JsonObject + { + [JsonSchema.Type] = type + }; + + if (required) + AddRequired(name); + + if (description != null) + Result[JsonSchema.Properties]![name]![JsonSchema.Description] = description; + + return this; + } + + public JsonObjectBuilder AddJsonElementProperty(string name, JsonObject? value, bool required = false, string? description = null) + { + if (Result == null) + throw new InvalidOperationException("Cannot add property to a null schema."); + + if (Result[JsonSchema.Properties] == null) + Result[JsonSchema.Properties] = new JsonObject(); + + Result[JsonSchema.Properties]![name] = value; + + if (required) + AddRequired(name); + + if (description != null) + Result[JsonSchema.Properties]![name]![JsonSchema.Description] = description; + + return this; + } + + public JsonObjectBuilder AddArrayProperty(string name, string itemType, bool required = false, string? description = null) + { + if (Result == null) + throw new InvalidOperationException("Cannot add property to a null schema."); + + if (Result[JsonSchema.Properties] == null) + Result[JsonSchema.Properties] = new JsonObject(); + + Result[JsonSchema.Properties]![name] = new JsonObject + { + [JsonSchema.Type] = JsonSchema.Array, + [JsonSchema.Items] = new JsonObject + { + [JsonSchema.Type] = itemType + } + }; + + if (required) + AddRequired(name); + + if (description != null) + Result[JsonSchema.Properties]![name]![JsonSchema.Description] = description; + + return this; + } + + public JsonObjectBuilder AddRefPropertyAndDefinition(string name, bool required, JsonObject? definition, string? description = null) where T : notnull + { + return AddRefProperty(name, TypeUtils.GetSchemaTypeId(), required, description) + .AddDefinition(TypeUtils.GetSchemaTypeId(), definition); + } + + public JsonObjectBuilder AddRefProperty(string name, bool required = false, string? description = null) where T : notnull + { + return AddRefProperty(name, TypeUtils.GetSchemaTypeId(), required, description); + } + + public JsonObjectBuilder AddRefProperty(string name, string typeId, bool required = false, string? description = null) + { + if (Result == null) + throw new InvalidOperationException("Cannot add property to a null schema."); + + if (Result[JsonSchema.Properties] == null) + Result[JsonSchema.Properties] = new JsonObject(); + + Result[JsonSchema.Properties]![name] = new JsonObject + { + [JsonSchema.Ref] = JsonSchema.RefValue + typeId + }; + + if (required) + AddRequired(name); + + if (description != null) + Result[JsonSchema.Properties]![name]![JsonSchema.Description] = description; + + return this; + } + + public JsonObjectBuilder AddDefinition(string name, JsonObject? definition) + { + if (Result == null) + throw new InvalidOperationException("Cannot add definition to a null schema."); + + if (Result[JsonSchema.Defs] == null) + Result[JsonSchema.Defs] = new JsonObject(); + + Result[JsonSchema.Defs]![name] = definition; + + return this; + } + + public JsonObjectBuilder AddArrayDefinition(string name, string itemType) + { + var arrayDefinition = new JsonObject + { + [JsonSchema.Type] = JsonSchema.Array, + [JsonSchema.Items] = new JsonObject + { + [JsonSchema.Type] = itemType + } + }; + + return AddDefinition(name, arrayDefinition); + } + + public JsonObject? BuildJsonObject() + { + return Result; + } + public JsonElement? BuildJsonElement() + { + return BuildJsonObject()?.ToJsonElement(); + } + } +} \ No newline at end of file diff --git a/ReflectorNet.Tests/Utils/TestUtils.Types.cs b/ReflectorNet.Tests/Utils/TestUtils.Types.cs index 9829584d..0bc93acc 100644 --- a/ReflectorNet.Tests/Utils/TestUtils.Types.cs +++ b/ReflectorNet.Tests/Utils/TestUtils.Types.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Reflection; using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; namespace com.IvanMurzak.ReflectorNet.Tests.Model { diff --git a/ReflectorNet/ReflectorNet.csproj b/ReflectorNet/ReflectorNet.csproj index caa87a30..2948a2ec 100644 --- a/ReflectorNet/ReflectorNet.csproj +++ b/ReflectorNet/ReflectorNet.csproj @@ -9,7 +9,7 @@ com.IvanMurzak.ReflectorNet - 2.1.0 + 2.1.1 Ivan Murzak Copyright © Ivan Murzak 2025 ReflectorNet is an advanced .NET reflection toolkit designed for AI-driven scenarios. Effortlessly search for C# methods using natural language queries, invoke any method by supplying arguments as JSON, and receive results as JSON. The library also provides a powerful API to inspect, modify, and manage in-memory object instances dynamically via JSON data. Ideal for automation, testing, and AI integration workflows. diff --git a/ReflectorNet/src/Convertor/IJsonSchemeConvertor.cs b/ReflectorNet/src/Convertor/IJsonSchemaConvertor.cs similarity index 71% rename from ReflectorNet/src/Convertor/IJsonSchemeConvertor.cs rename to ReflectorNet/src/Convertor/IJsonSchemaConvertor.cs index 2bfbf197..7223f2ef 100644 --- a/ReflectorNet/src/Convertor/IJsonSchemeConvertor.cs +++ b/ReflectorNet/src/Convertor/IJsonSchemaConvertor.cs @@ -5,6 +5,8 @@ * Licensed under the Apache License, Version 2.0. See LICENSE file in the project root for full license information. */ +using System; +using System.Collections.Generic; using System.Text.Json.Nodes; namespace com.IvanMurzak.ReflectorNet.Json @@ -12,7 +14,8 @@ namespace com.IvanMurzak.ReflectorNet.Json public interface IJsonSchemaConverter { string Id { get; } - JsonNode GetScheme(); - JsonNode GetSchemeRef(); + JsonNode GetSchema(); + JsonNode GetSchemaRef(); + IEnumerable GetDefinedTypes(); } } \ No newline at end of file diff --git a/ReflectorNet/src/Convertor/Json/JsonSchemaConverter.cs b/ReflectorNet/src/Convertor/Json/JsonSchemaConverter.cs new file mode 100644 index 00000000..43824ec7 --- /dev/null +++ b/ReflectorNet/src/Convertor/Json/JsonSchemaConverter.cs @@ -0,0 +1,27 @@ +/* + * ReflectorNet + * Author: Ivan Murzak (https://github.com/IvanMurzak) + * Copyright (c) 2025 Ivan Murzak + * Licensed under the Apache License, Version 2.0. See LICENSE file in the project root for full license information. + */ + +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + public abstract class JsonSchemaConverter : JsonConverter, IJsonSchemaConverter + { + public static string StaticId => TypeUtils.GetSchemaTypeId(); + + private static readonly Type[] _emptyTypes = Array.Empty(); + + public virtual string Id => StaticId; + public abstract JsonNode GetSchema(); + public abstract JsonNode GetSchemaRef(); + public virtual IEnumerable GetDefinedTypes() => _emptyTypes; + } +} \ No newline at end of file diff --git a/ReflectorNet/src/Convertor/Json/MethodDataConverter.cs b/ReflectorNet/src/Convertor/Json/MethodDataConverter.cs index e594d5c7..77957a1c 100644 --- a/ReflectorNet/src/Convertor/Json/MethodDataConverter.cs +++ b/ReflectorNet/src/Convertor/Json/MethodDataConverter.cs @@ -10,7 +10,6 @@ using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.ReflectorNet.Utils; @@ -18,10 +17,8 @@ namespace com.IvanMurzak.ReflectorNet.Json { using JsonSerializer = System.Text.Json.JsonSerializer; - public class MethodDataConverter : JsonConverter, IJsonSchemaConverter + public class MethodDataConverter : JsonSchemaConverter { - public static string StaticId => TypeUtils.GetTypeId(); - public static JsonNode Schema => new JsonObject { [JsonSchema.Type] = JsonSchema.Object, @@ -101,7 +98,7 @@ public class MethodDataConverter : JsonConverter, IJsonSchemaConvert [JsonSchema.Type] = JsonSchema.Array, [JsonSchema.Items] = new JsonObject { - [JsonSchema.Ref] = JsonSchema.RefValue + TypeUtils.GetTypeId() + [JsonSchema.Ref] = JsonSchema.RefValue + TypeUtils.GetSchemaTypeId() }, [JsonSchema.Description] = TypeUtils.GetDescription( typeof(MethodData) @@ -116,10 +113,12 @@ public class MethodDataConverter : JsonConverter, IJsonSchemaConvert [JsonSchema.Ref] = JsonSchema.RefValue + StaticId }; - public string Id => StaticId; - - public JsonNode GetSchemeRef() => SchemaRef; - public JsonNode GetScheme() => Schema; + public override JsonNode GetSchemaRef() => SchemaRef; + public override JsonNode GetSchema() => Schema; + public override IEnumerable GetDefinedTypes() + { + yield return typeof(MethodData.Parameter); + } public override MethodData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs b/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs index 1b84c0a5..e458324f 100644 --- a/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs +++ b/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs @@ -6,18 +6,17 @@ */ using System; +using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Json { - public class SerializedMemberConverter : JsonConverter, IJsonSchemaConverter + public class SerializedMemberConverter : JsonSchemaConverter, IJsonSchemaConverter { - public static string StaticId => TypeUtils.GetTypeId(); public static JsonNode Schema => new JsonObject { [JsonSchema.Type] = JsonSchema.Object, @@ -84,15 +83,17 @@ public class SerializedMemberConverter : JsonConverter, IJsonS readonly Reflector _reflector; - public string Id => StaticId; - public SerializedMemberConverter(Reflector reflector) { _reflector = reflector ?? throw new ArgumentNullException(nameof(reflector)); } - public JsonNode GetSchemeRef() => SchemaRef; - public JsonNode GetScheme() => Schema; + public override JsonNode GetSchemaRef() => SchemaRef; + public override JsonNode GetSchema() => Schema; + public override IEnumerable GetDefinedTypes() + { + yield return typeof(SerializedMemberList); + } public override SerializedMember? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs b/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs index 2bc8c291..917d9aed 100644 --- a/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs +++ b/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs @@ -6,17 +6,16 @@ */ using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Json { - public class SerializedMemberListConverter : JsonConverter, IJsonSchemaConverter + public class SerializedMemberListConverter : JsonSchemaConverter, IJsonSchemaConverter { - public static string StaticId => TypeUtils.GetTypeId(); public static JsonNode Schema => new JsonObject { [JsonSchema.Type] = JsonSchema.Array, @@ -26,19 +25,21 @@ public class SerializedMemberListConverter : JsonConverter } }; - public string Id => StaticId; - public SerializedMemberListConverter(Reflector reflector) { if (reflector == null) throw new ArgumentNullException(nameof(reflector)); } - public JsonNode GetScheme() => Schema; - public JsonNode GetSchemeRef() => new JsonObject + public override JsonNode GetSchema() => Schema; + public override JsonNode GetSchemaRef() => new JsonObject { [JsonSchema.Ref] = JsonSchema.RefValue + Id }; + public override IEnumerable GetDefinedTypes() + { + yield return typeof(SerializedMember); + } public override SerializedMemberList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/ReflectorNet/src/Extension/ExtensionsType.cs b/ReflectorNet/src/Extension/ExtensionsType.cs index 9bbaf029..25b73db8 100644 --- a/ReflectorNet/src/Extension/ExtensionsType.cs +++ b/ReflectorNet/src/Extension/ExtensionsType.cs @@ -13,10 +13,12 @@ namespace com.IvanMurzak.ReflectorNet { public static class ExtensionsType { - public static JsonNode? GetSchema(this Type type, Reflector reflector, bool justRef = false) => reflector.GetSchema(type, justRef); + public static JsonNode? GetSchema(this Type type, Reflector reflector) => reflector.GetSchema(type); + public static JsonNode? GetSchemaRef(this Type type, Reflector reflector) => reflector.GetSchemaRef(type); public static string GetTypeShortName(this Type? type) => TypeUtils.GetTypeShortName(type); public static string GetTypeName(this Type? type, bool pretty = false) => TypeUtils.GetTypeName(type, pretty); public static string GetTypeId(this Type type) => TypeUtils.GetTypeId(type); + public static string GetSchemaTypeId(this Type type) => TypeUtils.GetSchemaTypeId(type); public static bool IsMatch(this Type? type, string? typeName) => TypeUtils.IsNameMatch(type, typeName); } } \ No newline at end of file diff --git a/ReflectorNet/src/Model/MethodData.cs b/ReflectorNet/src/Model/MethodData.cs index 6a44c356..d50b5e36 100644 --- a/ReflectorNet/src/Model/MethodData.cs +++ b/ReflectorNet/src/Model/MethodData.cs @@ -45,9 +45,13 @@ public MethodData(Reflector reflector, MethodInfo methodInfo, bool justRef = fal ReturnType = methodInfo.ReturnType.GetTypeName(pretty: false); ReturnSchema = methodInfo.ReturnType == typeof(void) ? null - : reflector.GetSchema(methodInfo.ReturnType, justRef: justRef); + : justRef + ? reflector.GetSchemaRef(methodInfo.ReturnType) + : reflector.GetSchema(methodInfo.ReturnType); InputParametersSchema = methodInfo.GetParameters() - ?.Select(parameter => reflector.GetSchema(parameter.ParameterType, justRef: justRef)!) + ?.Select(parameter => justRef + ? reflector.GetSchemaRef(parameter.ParameterType) + : reflector.GetSchema(parameter.ParameterType)) ?.ToList(); } } diff --git a/ReflectorNet/src/Reflector/Reflector.Error.cs b/ReflectorNet/src/Reflector/Reflector.Error.cs index acddf4d5..8113a5aa 100644 --- a/ReflectorNet/src/Reflector/Reflector.Error.cs +++ b/ReflectorNet/src/Reflector/Reflector.Error.cs @@ -44,7 +44,7 @@ public static string NotSupportedInRuntime(Type type) public static string MoreThanOneMethodFound(Reflector reflector, List methods) { - var methodDataList = methods.Select(method => new MethodData(reflector, method, justRef: false)); + var methodDataList = methods.Select(method => new MethodData(reflector, method)); var methodsString = methodDataList.ToJson(reflector, options: null); return @$"[Error] Found more than one method. Only single method should be targeted. Please specify the method name more precisely. diff --git a/ReflectorNet/src/Reflector/Reflector.Json.cs b/ReflectorNet/src/Reflector/Reflector.Json.cs index 04329214..fd3c4b16 100644 --- a/ReflectorNet/src/Reflector/Reflector.Json.cs +++ b/ReflectorNet/src/Reflector/Reflector.Json.cs @@ -33,10 +33,47 @@ public partial class Reflector /// - Error handling: Provides detailed error information for schema generation failures /// /// The type for which to generate the JSON Schema. - /// Whether to generate a compact reference schema (true) or full schema definition (false). Default is false. /// A JsonNode containing the JSON Schema representation of the specified type. - public JsonNode GetSchema(bool justRef = false) - => jsonSchema.GetSchema(this, justRef); + public JsonNode GetSchema() + => jsonSchema.GetSchema(this); + + /// + /// Generates a JSON Schema representation for the specified generic type parameter. + /// This method provides comprehensive schema generation supporting both simple references + /// and full schema definitions with proper type metadata and documentation. + /// + /// Behavior: + /// - Type resolution: Automatically handles nullable types by unwrapping to underlying type + /// - Reference mode: When justRef=true, generates compact $ref schemas for non-primitive types + /// - Full schema mode: When justRef=false, generates complete schema definitions with properties + /// - Primitive optimization: Generates inline schemas for primitive types regardless of justRef setting + /// - Documentation extraction: Includes descriptions from DescriptionAttribute and XML documentation + /// - Recursive handling: Manages complex nested types and generic type parameters + /// - Error handling: Provides detailed error information for schema generation failures + /// + /// The type for which to generate the JSON Schema. + /// A JsonNode containing the JSON Schema representation of the specified type. + public JsonNode GetSchemaRef() + => jsonSchema.GetSchemaRef(this); + + /// + /// Generates a JSON Schema representation for the specified type. + /// This method provides comprehensive schema generation supporting both simple references + /// and full schema definitions with proper type metadata and documentation. + /// + /// Behavior: + /// - Type resolution: Automatically handles nullable types by unwrapping to underlying type + /// - Reference mode: When justRef=true, generates compact $ref schemas for non-primitive types + /// - Full schema mode: When justRef=false, generates complete schema definitions with properties + /// - Primitive optimization: Generates inline schemas for primitive types regardless of justRef setting + /// - Documentation extraction: Includes descriptions from DescriptionAttribute and XML documentation + /// - Recursive handling: Manages complex nested types and generic type parameters + /// - Error handling: Provides detailed error information for schema generation failures + /// + /// The Type for which to generate the JSON Schema. + /// A JsonNode containing the JSON Schema representation of the specified type. + public JsonNode GetSchema(Type type) + => jsonSchema.GetSchema(this, type); /// /// Generates a JSON Schema representation for the specified type. @@ -53,10 +90,9 @@ public JsonNode GetSchema(bool justRef = false) /// - Error handling: Provides detailed error information for schema generation failures /// /// The Type for which to generate the JSON Schema. - /// Whether to generate a compact reference schema (true) or full schema definition (false). Default is false. /// A JsonNode containing the JSON Schema representation of the specified type. - public JsonNode GetSchema(Type type, bool justRef = false) - => jsonSchema.GetSchema(this, type, justRef); + public JsonNode GetSchemaRef(Type type) + => jsonSchema.GetSchemaRef(this, type); /// /// Generates a comprehensive JSON Schema for method parameters, enabling dynamic method invocation @@ -82,8 +118,8 @@ public JsonNode GetSchema(Type type, bool justRef = false) /// The MethodInfo for which to generate the parameter schema. /// Whether to use compact references for complex types. Default is false. /// A JsonNode containing the complete JSON Schema for the method's parameters. - public JsonNode GetArgumentsSchema(MethodInfo methodInfo, bool justRef = false) - => jsonSchema.GetArgumentsSchema(this, methodInfo, justRef); + public JsonNode GetArgumentsSchema(MethodInfo methodInfo, bool justRef = false, JsonObject? defines = null) + => jsonSchema.GetArgumentsSchema(this, methodInfo, justRef, defines); /// /// Generates a comprehensive JSON Schema for the return type of a method. @@ -93,7 +129,7 @@ public JsonNode GetArgumentsSchema(MethodInfo methodInfo, bool justRef = false) /// The MethodInfo for which to generate the return type schema. /// Whether to use compact references for complex types. Default is false. /// A JsonNode containing the complete JSON Schema for the method's return type. - public JsonNode? GetReturnSchema(MethodInfo methodInfo, bool justRef = false) - => jsonSchema.GetReturnSchema(this, methodInfo, justRef); + public JsonNode? GetReturnSchema(MethodInfo methodInfo, bool justRef = false, JsonObject? defines = null) + => jsonSchema.GetReturnSchema(this, methodInfo, justRef, defines); } } diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.Consts.cs b/ReflectorNet/src/Utils/Json/JsonSchema.Consts.cs index f45a0b32..54ff73d9 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.Consts.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.Consts.cs @@ -17,6 +17,7 @@ public partial class JsonSchema public const string Array = "array"; public const string AnyOf = "anyOf"; public const string Required = "required"; + public const string Result = "result"; public const string Error = "error"; public const string AdditionalProperties = "additionalProperties"; diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs b/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs index 47e5470e..363d181f 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs @@ -97,7 +97,7 @@ void PostprocessFields(JsonNode? node) /// Generate a JSON schema from a type using ReflectorNet's introspection capabilities. /// This method uses the Reflector's converter system to understand the type structure. /// - JsonNode GenerateSchemaFromType(Reflector reflector, Type type) + JsonNode GenerateSchemaFromType(Reflector reflector, Type type, JsonObject defines) { // Handle primitive types if (TypeUtils.IsPrimitive(type)) @@ -111,23 +111,45 @@ JsonNode GenerateSchemaFromType(Reflector reflector, Type type) var itemType = TypeUtils.GetEnumerableItemType(type); if (itemType != null) { + var itemTypeId = itemType.GetSchemaTypeId(); + var isItemPrimitive = TypeUtils.IsPrimitive(itemType); + + if (!isItemPrimitive && defines.ContainsKey(itemTypeId) == false) + { + // Add placeholder first to prevent infinite recursion + defines[itemTypeId] = new JsonObject { [Type] = Object }; + defines[itemTypeId] = GetSchema(reflector, itemType, defines: defines); + } + + var typeId = type.GetSchemaTypeId(); + if (defines.ContainsKey(typeId) == false) + { + // Add placeholder first to prevent infinite recursion + defines[typeId] = new JsonObject + { + [Type] = Array, + [Items] = isItemPrimitive + ? GetSchema(reflector, itemType, defines: defines) + : GetSchemaRef(reflector, itemType) + }; + } + return new JsonObject { [Type] = Array, - [Items] = GetSchema(reflector, itemType, justRef: !TypeUtils.IsPrimitive(itemType)) + [Items] = isItemPrimitive + ? GetSchema(reflector, itemType, defines: defines) + : GetSchemaRef(reflector, itemType) }; } } // Handle regular objects by introspecting their fields and properties - var schema = new JsonObject - { - [Type] = Object, - [Properties] = new JsonObject() - }; - - var properties = schema[Properties] as JsonObject; + var properties = new JsonObject(); var required = new JsonArray(); + var schema = new JsonObject { [Type] = Object }; + + defines ??= new(); // Get serializable fields var fields = reflector.GetSerializableFields(type); @@ -138,18 +160,32 @@ JsonNode GenerateSchemaFromType(Reflector reflector, Type type) if (field.GetCustomAttribute() != null) continue; + var underlyingType = Nullable.GetUnderlyingType(field.FieldType); + var isPrimitive = TypeUtils.IsPrimitive(underlyingType ?? field.FieldType); var fieldName = field.GetCustomAttribute()?.Name ?? field.Name; - var fieldSchema = GetSchema(reflector, field.FieldType, justRef: !TypeUtils.IsPrimitive(field.FieldType)); + var schemaRef = isPrimitive + ? GetSchema(reflector, field.FieldType, defines: defines) + : GetSchemaRef(reflector, field.FieldType); + + if (!isPrimitive) + { + var typeId = field.FieldType.GetSchemaTypeId(); + if (!defines.ContainsKey(typeId)) + { + // Add placeholder first to prevent infinite recursion + defines[typeId] = new JsonObject { [Type] = Object }; + defines[typeId] = GetSchema(reflector, field.FieldType, defines: defines); + } + } // Add description if available var description = TypeUtils.GetFieldDescription(field); - if (!string.IsNullOrEmpty(description) && fieldSchema is JsonObject fieldSchemaObj) + if (!string.IsNullOrEmpty(description) && schemaRef is JsonObject fieldSchemaObj) fieldSchemaObj[Description] = JsonValue.Create(description); - properties![fieldName] = fieldSchema; + properties[fieldName] = schemaRef; // Fields are typically required unless they are nullable - var underlyingType = Nullable.GetUnderlyingType(field.FieldType); if (underlyingType == null && !field.FieldType.IsClass) required.Add(fieldName); } @@ -164,18 +200,32 @@ JsonNode GenerateSchemaFromType(Reflector reflector, Type type) if (prop.GetCustomAttribute() != null) continue; + var underlyingType = Nullable.GetUnderlyingType(prop.PropertyType); + var isPrimitive = TypeUtils.IsPrimitive(underlyingType ?? prop.PropertyType); var propName = prop.GetCustomAttribute()?.Name ?? prop.Name; - var propSchema = GetSchema(reflector, prop.PropertyType, justRef: !TypeUtils.IsPrimitive(prop.PropertyType)); + var schemaRef = isPrimitive + ? GetSchema(reflector, prop.PropertyType, defines: defines) + : GetSchemaRef(reflector, prop.PropertyType); + + if (!isPrimitive) + { + var typeId = prop.PropertyType.GetSchemaTypeId(); + if (!defines.ContainsKey(typeId)) + { + // Add placeholder first to prevent infinite recursion + defines[typeId] = new JsonObject { [Type] = Object }; + defines[typeId] = GetSchema(reflector, prop.PropertyType, defines: defines); + } + } // Add description if available var description = TypeUtils.GetPropertyDescription(prop); - if (!string.IsNullOrEmpty(description) && propSchema is JsonObject propSchemaObj) + if (!string.IsNullOrEmpty(description) && schemaRef is JsonObject propSchemaObj) propSchemaObj[Description] = JsonValue.Create(description); - properties![propName] = propSchema; + properties![propName] = schemaRef; // Properties are required if they are value types and not nullable - var underlyingType = Nullable.GetUnderlyingType(prop.PropertyType); if (underlyingType == null && !prop.PropertyType.IsClass && prop.CanWrite) required.Add(propName); } @@ -185,6 +235,9 @@ JsonNode GenerateSchemaFromType(Reflector reflector, Type type) if (required.Count > 0) schema[Required] = required; + if (properties.Count > 0) + schema[Properties] = properties; + return schema; } @@ -226,8 +279,8 @@ JsonNode GeneratePrimitiveSchema(Type type) enumValues.Add(JsonValue.Create(enumValue.ToString())); } - return new JsonObject - { + return new JsonObject + { [Type] = String, ["enum"] = enumValues }; diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index fcc250a1..455f5d20 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -6,7 +6,8 @@ */ using System; -using System.ComponentModel; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Text.Json.Nodes; using System.Threading.Tasks; @@ -65,11 +66,37 @@ public partial class JsonSchema /// /// The type for which to generate the JSON Schema. /// The Reflector instance used for type analysis and converter access. - /// Whether to generate a compact reference schema for non-primitive types. Default is false. /// A JsonNode containing the JSON Schema representation of the specified type. /// Thrown when schema generation fails for the specified type. - public JsonNode GetSchema(Reflector reflector, bool justRef = false) - => GetSchema(reflector, typeof(T), justRef); + public JsonNode GetSchema(Reflector reflector, JsonObject? defines = null) + => GetSchema(reflector, typeof(T), defines); + + /// + /// Generates a comprehensive JSON Schema representation for the specified generic type. + /// This method provides flexible schema generation supporting both full schema definitions + /// and compact reference schemas, with intelligent handling of primitive vs complex types. + /// + /// Schema Generation Features: + /// - Type unwrapping: Automatically handles nullable types by unwrapping to underlying type + /// - Converter integration: Leverages registered JsonConverter implementations for custom schema logic + /// - Reference optimization: Generates compact $ref schemas for complex types when justRef=true + /// - Documentation extraction: Includes descriptions from DescriptionAttribute and type metadata + /// - Error handling: Provides detailed error information for schema generation failures + /// - Primitive handling: Optimizes schema generation for built-in types + /// + /// Schema Modes: + /// - Full schema (justRef=false): Complete schema definition with all properties and nested types + /// - Reference schema (justRef=true): Compact $ref pointing to type definition in $defs + /// - Primitive inline: Primitive types are always inlined regardless of justRef setting + /// + /// Generated schemas conform to JSON Schema Draft 2020-12 specification. + /// + /// The type for which to generate the JSON Schema. + /// The Reflector instance used for type analysis and converter access. + /// A JsonNode containing the JSON Schema representation of the specified type. + /// Thrown when schema generation fails for the specified type. + public JsonNode GetSchemaRef(Reflector reflector) + => GetSchemaRef(reflector, typeof(T)); /// /// Generates a comprehensive JSON Schema representation for the specified type. @@ -93,51 +120,60 @@ public JsonNode GetSchema(Reflector reflector, bool justRef = false) /// /// The Reflector instance used for type analysis and converter access. /// The Type for which to generate the JSON Schema. - /// Whether to generate a compact reference schema for non-primitive types. Default is false. /// A JsonNode containing the JSON Schema representation of the specified type. /// Thrown when schema generation fails for the specified type. - public JsonNode GetSchema(Reflector reflector, Type type, bool justRef = false) + public JsonNode GetSchema(Reflector reflector, Type type, JsonObject? defines = null) { // Handle nullable types type = Nullable.GetUnderlyingType(type) ?? type; - var schema = default(JsonNode); + var typeId = type.GetSchemaTypeId(); try { + var definesNeeded = defines == null; + defines ??= new JsonObject(); + + var defineContainsType = defines.ContainsKey(typeId); + var jsonConverter = reflector.JsonSerializerOptions.GetConverter(type); if (jsonConverter is IJsonSchemaConverter schemeConvertor) { - schema = justRef - ? schemeConvertor.GetSchemeRef() - : schemeConvertor.GetScheme(); + // Add placeholder to prevent infinite recursion + if (definesNeeded && !defineContainsType) + defines[typeId] = new JsonObject { [Type] = Object }; + + schema = schemeConvertor.GetSchema(); + + if (definesNeeded && !defineContainsType) + defines[typeId] = schema; + + foreach (var defType in schemeConvertor.GetDefinedTypes()) + { + var defTypeId = defType.GetSchemaTypeId(); + if (defines.ContainsKey(defTypeId)) + continue; + + // Add placeholder to prevent infinite recursion + defines[defTypeId] = new JsonObject { [Type] = Object }; + + var def = GetSchema(reflector, defType, defines); + if (def != null) + defines[defTypeId] = def; + } } else { - if (justRef && !TypeUtils.IsPrimitive(type)) - { - // If justRef is true and the type is not primitive, we return a reference schema - schema = new JsonObject - { - [Ref] = RefValue + type.GetTypeId() - }; - - // Get description from the type if available - var description = TypeUtils.GetDescription(type); - if (!string.IsNullOrEmpty(description)) - schema[Description] = JsonValue.Create(description); - } - else - { - // If not justRef, we generate the full schema - schema = GenerateSchemaFromType(reflector, type); + schema = GenerateSchemaFromType(reflector, type, defines); - // Get description from the type if available - var description = TypeUtils.GetDescription(type); - if (!string.IsNullOrEmpty(description)) - schema[Description] = JsonValue.Create(description); - } + if (definesNeeded && defines.Count > 0) + schema[Defs] = defines; } + + // Get description from the type if available + var description = TypeUtils.GetDescription(type); + if (!string.IsNullOrEmpty(description)) + schema[Description] = JsonValue.Create(description); } catch (Exception ex) { @@ -153,13 +189,83 @@ public JsonNode GetSchema(Reflector reflector, Type type, bool justRef = false) PostprocessFields(schema); - if (schema is JsonObject parameterSchemaObject) + if (schema is not JsonObject) { - var propertyDescription = type.GetCustomAttribute()?.Description; - if (!string.IsNullOrEmpty(propertyDescription)) - parameterSchemaObject[Description] = JsonValue.Create(propertyDescription); + return new JsonObject() + { + [Error] = $"Unexpected schema type for '{type.GetTypeName(pretty: false)}'. Json Schema type: {schema.GetType().GetTypeName()}" + }; } - else + return schema; + } + + /// + /// Generates a comprehensive JSON Schema representation for the specified type. + /// This method provides flexible schema generation supporting both full schema definitions + /// and compact reference schemas, with intelligent handling of primitive vs complex types. + /// + /// Schema Generation Features: + /// - Type unwrapping: Automatically handles nullable types by unwrapping to underlying type + /// - Converter integration: Leverages registered JsonConverter implementations for custom schema logic + /// - Reference optimization: Generates compact $ref schemas for complex types when justRef=true + /// - Documentation extraction: Includes descriptions from DescriptionAttribute and type metadata + /// - Error handling: Provides detailed error information for schema generation failures + /// - Primitive handling: Optimizes schema generation for built-in types + /// + /// Schema Modes: + /// - Full schema (justRef=false): Complete schema definition with all properties and nested types + /// - Reference schema (justRef=true): Compact $ref pointing to type definition in $defs + /// - Primitive inline: Primitive types are always inlined regardless of justRef setting + /// + /// Generated schemas conform to JSON Schema Draft 2020-12 specification. + /// + /// The Reflector instance used for type analysis and converter access. + /// The Type for which to generate the JSON Schema. + /// A JsonNode containing the JSON Schema representation of the specified type. + /// Thrown when schema generation fails for the specified type. + public JsonNode GetSchemaRef(Reflector reflector, Type type) + { + // Handle nullable types + type = Nullable.GetUnderlyingType(type) ?? type; + var schema = default(JsonNode); + + try + { + var jsonConverter = reflector.JsonSerializerOptions.GetConverter(type); + if (jsonConverter is IJsonSchemaConverter schemeConvertor) + { + schema = schemeConvertor.GetSchemaRef(); + } + else + { + var typeId = type.GetSchemaTypeId(); + // If justRef is true and the type is not primitive, we return a reference schema + schema = new JsonObject + { + [Ref] = RefValue + typeId + }; + } + + // Get description from the type if available + var description = TypeUtils.GetDescription(type); + if (!string.IsNullOrEmpty(description)) + schema[Description] = JsonValue.Create(description); + } + catch (Exception ex) + { + // Handle exceptions and return null or an error message + return new JsonObject() + { + [Error] = $"Failed to get schema for '{type.GetTypeName(pretty: false)}':\n{ex.Message}\n{ex.StackTrace}\n" + }; + } + + if (schema == null) + throw new InvalidOperationException($"Failed to get schema for type '{type.GetTypeName(pretty: false)}'."); + + PostprocessFields(schema); + + if (schema is not JsonObject) { return new JsonObject() { @@ -168,6 +274,66 @@ public JsonNode GetSchema(Reflector reflector, Type type, bool justRef = false) } return schema; } + + + /// + /// Recursively collects all nested non-primitive types from a given type and adds them to the definitions. + /// This method traverses through properties, fields, generic arguments, and collection item types + /// to find all types that need to be included in the $defs section. + /// + /// The Reflector instance used for type analysis. + /// The type to analyze for nested types. + /// The JsonObject to accumulate type definitions. + /// Set of already visited types to prevent infinite recursion. + /// + /// This method is recursive: it calls itself for each generic argument, collection item type, + /// property type, and field type found within the provided . To prevent + /// infinite recursion in the case of cyclic type references, it maintains a + /// set and skips types that have already been processed. The recursion ensures that all nested, + /// non-primitive types are discovered and can be included in the schema definitions. + /// + void CollectNestedTypes(Reflector reflector, Type type, HashSet? visitedTypes = null) + { + visitedTypes ??= new HashSet(); + + // Avoid infinite recursion + if (visitedTypes.Contains(type)) + return; + + visitedTypes.Add(type); + + // Skip primitive types + if (TypeUtils.IsPrimitive(type)) + return; + + // Handle generic type arguments (e.g., List, Dictionary) + foreach (var genericArgument in TypeUtils.GetGenericTypes(type)) + CollectNestedTypes(reflector, genericArgument, visitedTypes); + + // Handle collection item types (e.g., T[], List, IEnumerable) + if (TypeUtils.IsIEnumerable(type)) + { + var itemType = TypeUtils.GetEnumerableItemType(type); + if (itemType != null) + CollectNestedTypes(reflector, itemType, visitedTypes); + } + + // Handle properties and fields to find nested types + var properties = reflector.GetSerializableProperties(type); + if (properties != null) + { + foreach (var prop in properties) + CollectNestedTypes(reflector, prop.PropertyType, visitedTypes); + } + + var fields = reflector.GetSerializableFields(type); + if (fields != null) + { + foreach (var field in fields) + CollectNestedTypes(reflector, field.FieldType, visitedTypes); + } + } + /// /// Generates a comprehensive JSON Schema for method parameters, creating schemas suitable for /// dynamic method invocation, API documentation, form generation, and parameter validation. @@ -206,7 +372,7 @@ public JsonNode GetSchema(Reflector reflector, Type type, bool justRef = false) /// Whether to use compact references for complex types in parameter schemas. Default is false. /// A JsonNode containing the complete JSON Schema for the method's parameters. /// Thrown when method parameter is null. - public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool justRef = false) + public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool justRef = false, JsonObject? defines = null) { if (method == null) throw new ArgumentNullException(nameof(method)); @@ -215,8 +381,149 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool if (parameters.Length == 0) return new JsonObject { [Type] = Object }; + var types = parameters + .Select(p => ( + type: p.ParameterType, + name: p.Name ?? throw new InvalidOperationException($"Parameter in method '{method.Name}' has no name."), + description: TypeUtils.GetDescription(p), + required: !p.HasDefaultValue)); + + return GenerateSchema(reflector, types, justRef, defines); + } + + /// + /// Generates a comprehensive JSON Schema for the return type of a method. + /// This method analyzes method return types and produces schemas that describe the structure + /// of the returned data, enabling type-safe result handling in dynamic execution environments. + /// + /// Schema Structure: + /// - Root object schema with "properties" containing the "result" property + /// - "required" array listing the "result" property (for non-void returns) + /// - "$defs" section containing complex type definitions to avoid duplication + /// - Return type wrapped as a property named "result" + /// - Proper JSON Schema Draft 2020-12 compliance + /// + /// Schema Generation Features: + /// - Async unwrapping: Automatically unwraps Task<T> and ValueTask<T> to return schema for T + /// - Void handling: Returns null for void, Task, and ValueTask (methods with no return value) + /// - Type resolution: Generates appropriate schemas for complex return types + /// - Reference optimization: Supports both full schema definitions and compact $ref references + /// - Documentation: Extracts descriptions from return type attributes + /// - Proper $defs handling: Complex types are placed in $defs section with $ref references + /// + /// Return Type Handling: + /// - void → null (no return value) + /// - Task → null (async method with no return value) + /// - ValueTask → null (async method with no return value) + /// - Task<string> → object schema with "result" property of type string (unwrapped) + /// - ValueTask<int> → object schema with "result" property of type int (unwrapped) + /// - Any other type → object schema with "result" property of that type with $defs support + /// + /// Use Cases: + /// - API documentation generation for response schemas + /// - Dynamic result type validation + /// - Code generation and template systems + /// - AI-driven method understanding and invocation + /// + /// The Reflector instance used for type analysis and schema generation. + /// The MethodInfo for which to generate the return type schema. + /// Whether to use compact references for complex types. Default is false. + /// A JsonNode containing the JSON Schema for the method's return type, or null for void/Task/ValueTask. + /// Thrown when methodInfo parameter is null. + public JsonNode? GetReturnSchema(Reflector reflector, MethodInfo methodInfo, bool justRef = false, JsonObject? defines = null) + { + if (methodInfo == null) + throw new ArgumentNullException(nameof(methodInfo)); + + var returnType = methodInfo.ReturnType; + + // Handle void, Task, and ValueTask - these have no return value + if (returnType == typeof(void) || + returnType == typeof(Task) || + returnType == typeof(ValueTask)) + return null; + + // Check if return type is Task or ValueTask + var isAsyncWrapper = returnType.IsGenericType && + (returnType.GetGenericTypeDefinition() == typeof(Task<>) || + returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)); + + // Unwrap Task/ValueTask to get the inner T + var unwrappedType = isAsyncWrapper + ? returnType.GetGenericArguments()[0] + : returnType; + + var isNullable = false; + + // Check for Nullable (value types like int?, DateTime?) + var nullableUnderlyingType = Nullable.GetUnderlyingType(unwrappedType); + if (nullableUnderlyingType != null) + { + // This handles Nullable for value types (e.g., Task, int?) + isNullable = true; + unwrappedType = nullableUnderlyingType; + } + else if (!unwrappedType.IsValueType) + { + // For reference types, check nullability using NullabilityInfoContext +#if NET5_0_OR_GREATER + try + { + var nullabilityContext = new NullabilityInfoContext(); + var nullabilityInfo = nullabilityContext.Create(methodInfo.ReturnParameter); + + isNullable = nullabilityInfo.ReadState == NullabilityState.Nullable; + // If the return type is Task or ValueTask, check the generic argument nullability + if (isAsyncWrapper) + { + // Check the nullability of the T inside Task or ValueTask + if (nullabilityInfo.GenericTypeArguments.Length > 0) + isNullable |= nullabilityInfo.GenericTypeArguments[0].ReadState == NullabilityState.Nullable; + } + } + catch (Exception) + { + // If we can't determine nullability, assume not nullable + isNullable = false; + } +#else + // For .NET Standard 2.1 and earlier, we cannot determine reference type nullability + // Assume not nullable by default + isNullable = false; +#endif + } + + var types = new (Type type, string name, string? description, bool required)[] + { + ( + type: unwrappedType, + name: Result, + description: null, + required: isNullable == false + ) + }; + return GenerateSchema(reflector, types, justRef, defines); + } + + /// + /// Generates a JSON Schema for a collection of types, typically representing method parameters. + /// This method constructs a schema object with properties for each type, handling required fields, + /// + /// + /// + /// + /// If it is not null, it will be used to fill new definitions without injecting it into the schema. Needed for recursive call case, when need to generate a single top level definitions object. + /// + public JsonNode GenerateSchema( + Reflector reflector, + IEnumerable<(Type type, string name, string? description, bool required)> types, + bool justRef = false, + JsonObject? defines = null) + { + var needToAddDefines = defines == null; + defines ??= new(); + var properties = new JsonObject(); - var defines = new JsonObject(); var required = new JsonArray(); // Create a schema object manually @@ -224,129 +531,69 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool { // [SchemaDraft] = JsonValue.Create(SchemaDraftValue), [Type] = Object, - [Properties] = properties, - [Required] = required, - [Defs] = defines + [Properties] = properties }; - foreach (var parameter in parameters) + foreach (var parameter in types) { var parameterSchema = default(JsonNode); - var isPrimitive = TypeUtils.IsPrimitive(parameter.ParameterType); + var isPrimitive = TypeUtils.IsPrimitive(parameter.type); if (isPrimitive) { - parameterSchema = GetSchema(reflector, parameter.ParameterType, justRef: justRef); - if (parameterSchema == null) - continue; + parameterSchema = GetSchema(reflector, parameter.type, defines: defines); } else { - var typeId = parameter.ParameterType.GetTypeId(); - if (defines.ContainsKey(typeId)) - { - parameterSchema = GetSchema(reflector, parameter.ParameterType, justRef: true); - } - else + parameterSchema = GetSchemaRef(reflector, parameter.type); + + var typeId = parameter.type.GetSchemaTypeId(); + if (!defines.ContainsKey(typeId)) { - var fullSchema = GetSchema(reflector, parameter.ParameterType, justRef: false); + var fullSchema = GetSchema(reflector, parameter.type, defines: defines); if (fullSchema == null) continue; defines[typeId] = fullSchema; - parameterSchema = GetSchema(reflector, parameter.ParameterType, justRef: true); } - - if (parameterSchema == null) - continue; } - properties[parameter.Name!] = parameterSchema; + if (parameterSchema == null) + continue; + + properties[parameter.name!] = parameterSchema; if (parameterSchema is JsonObject parameterSchemaObject) { - var propertyDescription = TypeUtils.GetDescription(parameter); + var propertyDescription = parameter.description; if (!string.IsNullOrEmpty(propertyDescription)) parameterSchemaObject[Description] = JsonValue.Create(propertyDescription); } // Check if the parameter has a default value - if (!parameter.HasDefaultValue) - required.Add(parameter.Name!); + if (parameter.required) + required.Add(parameter.name!); // Add generic type parameters recursively if any - foreach (var genericArgument in TypeUtils.GetGenericTypes(parameter.ParameterType)) + foreach (var genericArgument in TypeUtils.GetGenericTypes(parameter.type)) { if (TypeUtils.IsPrimitive(genericArgument)) continue; - var typeId = genericArgument.GetTypeId(); + var typeId = genericArgument.GetSchemaTypeId(); if (defines.ContainsKey(typeId)) continue; - var genericSchema = GetSchema(reflector, genericArgument, justRef: false); + var genericSchema = GetSchema(reflector, genericArgument, defines: defines); if (genericSchema != null) defines[typeId] = genericSchema; } } - if (defines.Count == 0) - schema.Remove(Defs); - return schema; - } - - /// - /// Generates a comprehensive JSON Schema for the return type of a method. - /// This method analyzes method return types and produces schemas that describe the structure - /// of the returned data, enabling type-safe result handling in dynamic execution environments. - /// - /// Schema Generation Features: - /// - Async unwrapping: Automatically unwraps Task<T> and ValueTask<T> to return schema for T - /// - Void handling: Returns null for void, Task, and ValueTask (methods with no return value) - /// - Type resolution: Generates appropriate schemas for complex return types - /// - Reference optimization: Supports both full schema definitions and compact $ref references - /// - Documentation: Extracts descriptions from return type attributes - /// - /// Return Type Handling: - /// - void → null (no return value) - /// - Task → null (async method with no return value) - /// - ValueTask → null (async method with no return value) - /// - Task<string> → schema for string (unwrapped) - /// - ValueTask<int> → schema for int (unwrapped) - /// - Any other type → schema for that type - /// - /// Use Cases: - /// - API documentation generation for response schemas - /// - Dynamic result type validation - /// - Code generation and template systems - /// - AI-driven method understanding and invocation - /// - /// The Reflector instance used for type analysis and schema generation. - /// The MethodInfo for which to generate the return type schema. - /// Whether to use compact references for complex types. Default is false. - /// A JsonNode containing the JSON Schema for the method's return type, or null for void/Task/ValueTask. - /// Thrown when methodInfo parameter is null. - public JsonNode? GetReturnSchema(Reflector reflector, MethodInfo methodInfo, bool justRef = false) - { - if (methodInfo == null) - throw new ArgumentNullException(nameof(methodInfo)); - - var returnType = methodInfo.ReturnType; - var unwrappedType = Nullable.GetUnderlyingType(returnType) ?? returnType; + if (defines.Count > 0 && needToAddDefines) + schema[Defs] = defines; + if (required.Count > 0) + schema[Required] = required; - // Handle void, Task, and ValueTask - these have no return value - if (unwrappedType == typeof(void) || - unwrappedType == typeof(Task) || - unwrappedType == typeof(ValueTask)) - return null; - - // Unwrap Task and ValueTask to get the actual return type T - if (unwrappedType.IsGenericType) - { - var genericDefinition = unwrappedType.GetGenericTypeDefinition(); - if (genericDefinition == typeof(Task<>) || genericDefinition == typeof(ValueTask<>)) - unwrappedType = unwrappedType.GetGenericArguments()[0]; - } - - return GetSchema(reflector, unwrappedType, justRef); + return schema; } } } \ No newline at end of file diff --git a/ReflectorNet/src/Utils/TypeUtils.Name.cs b/ReflectorNet/src/Utils/TypeUtils.Name.cs index da3d2bd3..9020c411 100644 --- a/ReflectorNet/src/Utils/TypeUtils.Name.cs +++ b/ReflectorNet/src/Utils/TypeUtils.Name.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; namespace com.IvanMurzak.ReflectorNet.Utils @@ -32,7 +33,7 @@ public static string GetTypeId(Type type) // Special case: string is technically IEnumerable but shouldn't be treated as an array if (type == typeof(string)) - return type.GetTypeName(pretty: true); + return GetTypeName(type, pretty: true); // If type is a generic type, use its full name with generic arguments if (type.IsGenericType) @@ -58,8 +59,67 @@ public static string GetTypeId(Type type) return $"{GetTypeId(elementType)}{ArraySuffix}"; } - return type.GetTypeName(pretty: true); + return GetTypeName(type, pretty: true); } + + public static string GetSchemaTypeId() => GetSchemaTypeId(typeof(T)); + public static string GetSchemaTypeId(Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + // Handle nullable types + type = Nullable.GetUnderlyingType(type) ?? type; + + // Special case: string is technically IEnumerable but shouldn't be treated as an array + if (type == typeof(string)) + return GetTypeName(type, pretty: true); + + // If type is a generic type, use its full name with generic arguments + if (type.IsGenericType) + { + var genericTypes = type.GetGenericArguments(); + if (genericTypes.Length == 1) + { + if (typeof(System.Collections.IList).IsAssignableFrom(type) || + typeof(ISet<>).MakeGenericType(genericTypes).IsAssignableFrom(type) || + typeof(LinkedList<>).MakeGenericType(genericTypes).IsAssignableFrom(type) || + typeof(Queue<>).MakeGenericType(genericTypes).IsAssignableFrom(type) || + typeof(Stack<>).MakeGenericType(genericTypes).IsAssignableFrom(type) || + typeof(List<>).MakeGenericType(genericTypes).IsAssignableFrom(type) || + typeof(SortedSet<>).MakeGenericType(genericTypes).IsAssignableFrom(type)) + { + var itemType = genericTypes[0]; + if (itemType == null) + throw new InvalidOperationException($"Array type '{type}' has no item type."); + return $"{GetSchemaTypeId(itemType)}{ArraySuffix}"; + } + } + + var genericTypeName = type.GetGenericTypeDefinition().GetTypeName(pretty: true); + if (StringUtils.IsNullOrEmpty(genericTypeName)) + throw new InvalidOperationException($"Generic type '{type}' does not have a full name."); + + var tickIndex = genericTypeName.IndexOf('`'); + if (tickIndex > 0) + genericTypeName = genericTypeName.Substring(0, tickIndex); + + // Recursively get the type ID for each generic argument + var genericArgs = genericTypes.Select(GetSchemaTypeId); + return $"{genericTypeName}<{string.Join(",", genericArgs)}>"; + } + + if (type.IsArray) + { + var elementType = type.GetElementType(); + if (elementType == null) + throw new InvalidOperationException($"Array type '{type}' has no element type."); + return $"{GetSchemaTypeId(elementType)}{ArraySuffix}"; + } + + return GetTypeName(type, pretty: true); + } + public static bool IsNameMatch(Type? type, string? typeName) { if (type == null || string.IsNullOrEmpty(typeName))