From 349150438edc4bc24300cb7c403bd83d7cd76109 Mon Sep 17 00:00:00 2001 From: Sergey Bessonov Date: Fri, 22 Mar 2024 15:03:13 +0500 Subject: [PATCH 1/4] fix(autodoc): OpenRpcDoc: format enum values as declared --- .../OpenRpcConstants.cs | 1 - .../Services/OpenRpcSchemaGenerator.cs | 2 +- .../Tochka.JsonRpc.OpenRpc.csproj | 1 - .../Services/OpenRpcSchemaGeneratorTests.cs | 37 +++++++++++++++++-- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/Tochka.JsonRpc.OpenRpc/OpenRpcConstants.cs b/src/Tochka.JsonRpc.OpenRpc/OpenRpcConstants.cs index 117fc9d7..b3783bf0 100644 --- a/src/Tochka.JsonRpc.OpenRpc/OpenRpcConstants.cs +++ b/src/Tochka.JsonRpc.OpenRpc/OpenRpcConstants.cs @@ -43,7 +43,6 @@ public static class OpenRpcConstants WriteIndented = true, Converters = { - new JsonStringEnumConverter(JsonNamingPolicies.KebabCaseLower), new UriConverter() }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull diff --git a/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs b/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs index 18b61395..a445708e 100644 --- a/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs +++ b/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs @@ -61,7 +61,7 @@ public JsonSchema CreateOrRef(Type type, string methodName, JsonSerializerOption // adding it just for the same logic as for normal types registeredSchemaKeys.Add(typeName); registeredSchemas[typeName] = new JsonSchemaBuilder() - .Enum(type.GetEnumNames().Select(jsonSerializerOptions.ConvertName)) + .Enum(type.GetEnumNames()) .Build(); return null; } diff --git a/src/Tochka.JsonRpc.OpenRpc/Tochka.JsonRpc.OpenRpc.csproj b/src/Tochka.JsonRpc.OpenRpc/Tochka.JsonRpc.OpenRpc.csproj index 535fb97a..a7ff107c 100644 --- a/src/Tochka.JsonRpc.OpenRpc/Tochka.JsonRpc.OpenRpc.csproj +++ b/src/Tochka.JsonRpc.OpenRpc/Tochka.JsonRpc.OpenRpc.csproj @@ -36,7 +36,6 @@ - all runtime; build; native; contentfiles; analyzers diff --git a/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs b/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs index 45461b61..b319dfe2 100644 --- a/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs +++ b/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs @@ -75,7 +75,7 @@ public void CreateOrRef_CollectionWithEnumItems_ReturnSchemaWithRefForItems() var expectedRegistrations = new Dictionary { [expectedTypeName] = new JsonSchemaBuilder() - .Enum("one", "two") + .Enum("One", "Two") .Build() }; schemaGenerator.GetAllSchemas().Should().BeEquivalentTo(expectedRegistrations); @@ -137,7 +137,7 @@ public void CreateOrRef_Enum_ReturnRef() var expectedRegistrations = new Dictionary { [expectedTypeName] = new JsonSchemaBuilder() - .Enum("one", "two") + .Enum("One", "Two") .Build() }; schemaGenerator.GetAllSchemas().Should().BeEquivalentTo(expectedRegistrations); @@ -229,7 +229,7 @@ public void CreateOrRef_AlreadyRegisteredEnum_DontRegisterAgain() var expectedRegistrations = new Dictionary { [expectedTypeName] = new JsonSchemaBuilder() - .Enum("one", "two") + .Enum("One", "Two") .Build() }; schemaGenerator.GetAllSchemas().Should().BeEquivalentTo(expectedRegistrations); @@ -312,6 +312,28 @@ public void CreateOrRef_AlreadyRegisteredTypeWithPropertiesWithAnotherMethodName }; schemaGenerator.GetAllSchemas().Should().BeEquivalentTo(expectedRegistrations); } + + [Test] + public void CreateOrRef_EnumValuesFormatedAsDeclared() + { + var type = typeof(Enum2); + var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicies.SnakeCaseLower }; + + var actualSchema = schemaGenerator.CreateOrRef(type, MethodName, jsonSerializerOptions); + + var expectedTypeName = $"{MethodName} {nameof(Enum2)}"; + var expectedSchema = new JsonSchemaBuilder() + .Ref($"#/components/schemas/{expectedTypeName}") + .Build(); + actualSchema.Should().BeEquivalentTo(expectedSchema); + var expectedRegistrations = new Dictionary + { + [expectedTypeName] = new JsonSchemaBuilder() + .Enum("Value1", "ValueValue2", "value3", "value_value4") + .Build() + }; + schemaGenerator.GetAllSchemas().Should().BeEquivalentTo(expectedRegistrations); + } [Test] public void GetAllSchemas_ChangingCollection_DontAffectInnerCollection() @@ -333,6 +355,15 @@ private enum Enum One, Two } + + [SuppressMessage("ReSharper", "UnusedMember.Local")] + private enum Enum2 + { + Value1, + ValueValue2, + value3, + value_value4 + } private record TypeWithProperties(int IntProperty, string StringProperty, TypeWithProperties NestedProperty, AnotherTypeWithProperties AnotherProperty); From 96da1bdad49ba62031b86b09fb5152e97bd6a9ac Mon Sep 17 00:00:00 2001 From: Sergey Bessonov Date: Fri, 22 Mar 2024 15:28:11 +0500 Subject: [PATCH 2/4] fix(autodoc): OpenRpcDoc: DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan, Guid format as string by default, instead of object with properties --- .../Services/OpenRpcSchemaGenerator.cs | 18 ++++++++++ .../Services/OpenRpcSchemaGeneratorTests.cs | 36 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs b/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs index a445708e..9bc17682 100644 --- a/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs +++ b/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs @@ -15,6 +15,16 @@ public class OpenRpcSchemaGenerator : IOpenRpcSchemaGenerator private readonly Dictionary registeredSchemas = new(); private readonly HashSet registeredSchemaKeys = new(); + private readonly Dictionary defaultStringConvertedSimpleTypes = new() + { + { typeof(DateTime), Formats.DateTime }, + { typeof(DateTimeOffset), Formats.DateTime }, + { typeof(DateOnly), Formats.Date }, + { typeof(TimeOnly), Formats.Time }, + { typeof(TimeSpan), Formats.Duration }, + { typeof(Guid), Formats.Uuid } + }; + /// public Dictionary GetAllSchemas() => new(registeredSchemas); @@ -75,6 +85,14 @@ public JsonSchema CreateOrRef(Type type, string methodName, JsonSerializerOption // string, int, bool, etc... return schema; } + + if (defaultStringConvertedSimpleTypes.TryGetValue(type, out var format)) + { + return new JsonSchemaBuilder() + .Type(SchemaValueType.String) + .Format(format) + .Build(); + } // required to break infinite recursion registeredSchemaKeys.Add(typeName); diff --git a/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs b/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs index b319dfe2..1b9366ed 100644 --- a/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs +++ b/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using FluentAssertions; @@ -334,6 +335,37 @@ public void CreateOrRef_EnumValuesFormatedAsDeclared() }; schemaGenerator.GetAllSchemas().Should().BeEquivalentTo(expectedRegistrations); } + + [Test] + public void CreateOrRef_DefaultSimpleTypesFormattedAsString() + { + var type = typeof(TypeWithSimpleProperties); + var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicies.SnakeCaseLower }; + + var actualSchema = schemaGenerator.CreateOrRef(type, MethodName, jsonSerializerOptions); + + var expectedTypeName = $"{MethodName} {nameof(TypeWithSimpleProperties)}"; + var expectedSchema = new JsonSchemaBuilder() + .Ref($"#/components/schemas/{expectedTypeName}") + .Build(); + actualSchema.Should().BeEquivalentTo(expectedSchema); + var expectedRegistrations = new Dictionary + { + [expectedTypeName] = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["date_time"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Format(Formats.DateTime).Build(), + ["date_time_offset"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Format(Formats.DateTime).Build(), + ["date_only"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Format(Formats.Date).Build(), + ["time_only"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Format(Formats.Time).Build(), + ["time_span"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Format(Formats.Duration).Build(), + ["guid"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Format(Formats.Uuid).Build() + }) + .Build() + }; + schemaGenerator.GetAllSchemas().Should().BeEquivalentTo(expectedRegistrations); + } [Test] public void GetAllSchemas_ChangingCollection_DontAffectInnerCollection() @@ -368,6 +400,8 @@ private enum Enum2 private record TypeWithProperties(int IntProperty, string StringProperty, TypeWithProperties NestedProperty, AnotherTypeWithProperties AnotherProperty); private record AnotherTypeWithProperties(bool BoolProperty); + + private record TypeWithSimpleProperties(DateTime DateTime, DateTimeOffset DateTimeOffset, DateOnly DateOnly, TimeOnly TimeOnly, TimeSpan TimeSpan, Guid Guid); private record CustomSimpleType; } From e40847174c37d2a7774da73fae40b4bc285fe47e Mon Sep 17 00:00:00 2001 From: Sergey Bessonov Date: Fri, 22 Mar 2024 19:27:40 +0500 Subject: [PATCH 3/4] feature(autodoc): OpenRpcDoc: summaries for response object properties now mapped as titles in jsonschema --- .../Services/OpenRpcSchemaGenerator.cs | 126 +++++++++++------- .../Services/OpenRpcSchemaGeneratorTests.cs | 124 ++++++++++++++++- 2 files changed, 201 insertions(+), 49 deletions(-) diff --git a/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs b/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs index 9bc17682..011a5f11 100644 --- a/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs +++ b/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs @@ -29,18 +29,11 @@ public class OpenRpcSchemaGenerator : IOpenRpcSchemaGenerator public Dictionary GetAllSchemas() => new(registeredSchemas); /// - public JsonSchema CreateOrRef(Type type, string methodName, JsonSerializerOptions jsonSerializerOptions) + public JsonSchema CreateOrRef(Type type, string methodName, JsonSerializerOptions jsonSerializerOptions) => + CreateOrRefInternal(type, methodName, null, jsonSerializerOptions); + + private JsonSchema CreateOrRefInternal(Type type, string methodName, string? propertySummary, JsonSerializerOptions jsonSerializerOptions) { - var itemType = type.GetEnumerableItemType(); - if (typeof(IEnumerable).IsAssignableFrom(type) && itemType != null) - { - // returning schema itself if it's collection - return new JsonSchemaBuilder() - .Type(SchemaValueType.Array) - .Items(CreateOrRef(itemType, methodName, jsonSerializerOptions)) - .Build(); - } - var typeName = type.Name; if (!typeName.StartsWith($"{methodName} ", StringComparison.Ordinal)) { @@ -48,64 +41,101 @@ public JsonSchema CreateOrRef(Type type, string methodName, JsonSerializerOption typeName = $"{methodName} {typeName}"; } - if (!registeredSchemas.ContainsKey(typeName) && !registeredSchemaKeys.Contains(typeName)) - { - var schema = BuildSchema(type, typeName, methodName, jsonSerializerOptions); - if (schema != null) - { - // returning schema itself if it's simple type - return schema; - } - } - - // returning ref if it's enum or regular type with properties - return new JsonSchemaBuilder() - .Ref($"#/components/schemas/{typeName}") - .Build(); + return BuildSchema(type, typeName, methodName, propertySummary, jsonSerializerOptions); } - private JsonSchema? BuildSchema(Type type, string typeName, string methodName, JsonSerializerOptions jsonSerializerOptions) + private JsonSchema BuildSchema(Type type, string typeName, string methodName, string? propertySummary, JsonSerializerOptions jsonSerializerOptions) { + if (registeredSchemas.ContainsKey(typeName) || registeredSchemaKeys.Contains(typeName)) + { + return CreateRefSchema(typeName, propertySummary); + } + + var itemType = type.GetEnumerableItemType(); + if (typeof(IEnumerable).IsAssignableFrom(type) && itemType != null) + { + var collectionScheme = new JsonSchemaBuilder() + .Type(SchemaValueType.Array) + .Items(CreateOrRefInternal(itemType, methodName, null, jsonSerializerOptions)) + .TryAppendTitle(propertySummary) + .Build(); + // returning schema itself if it's collection + return collectionScheme; + } + if (type.IsEnum) { - // adding it just for the same logic as for normal types - registeredSchemaKeys.Add(typeName); - registeredSchemas[typeName] = new JsonSchemaBuilder() - .Enum(type.GetEnumNames()) - .Build(); - return null; + var enumSchema = new JsonSchemaBuilder() + .Enum(type.GetEnumNames()) + .Build(); + RegisterSchema(typeName, enumSchema); + // returning ref if it's enum or regular type with properties + return CreateRefSchema(typeName, propertySummary); } + var simpleTypeSchema = new JsonSchemaBuilder() + .FromType(type) + .TryAppendTitle(propertySummary) + .Build(); // can't check type.GetProperties() here because simple types have properties too - var schema = new JsonSchemaBuilder() - .FromType(type) - .Build(); - if (schema.GetProperties() == null) + if (simpleTypeSchema.GetProperties() == null) { + // returning schema itself if it's simple type // string, int, bool, etc... - return schema; + return simpleTypeSchema; } if (defaultStringConvertedSimpleTypes.TryGetValue(type, out var format)) { - return new JsonSchemaBuilder() - .Type(SchemaValueType.String) - .Format(format) - .Build(); + var simpleStringSchema = new JsonSchemaBuilder() + .Type(SchemaValueType.String) + .Format(format) + .TryAppendTitle(propertySummary) + .Build(); + return simpleStringSchema; } - // required to break infinite recursion + // required to break infinite recursion by ref to same type in property registeredSchemaKeys.Add(typeName); - registeredSchemas[typeName] = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(BuildPropertiesSchemas(type, methodName, jsonSerializerOptions)) - .Build(); - return null; + + var objectSchema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(BuildPropertiesSchemas(type, methodName, jsonSerializerOptions)) + .Build(); + RegisterSchema(typeName, objectSchema); + return CreateRefSchema(typeName, propertySummary); + } + + private static JsonSchema CreateRefSchema(string typeName, string? propertySummary) + { + var refSchemaBuilder = new JsonSchemaBuilder() + .Ref($"#/components/schemas/{typeName}") + .TryAppendTitle(propertySummary); + + return refSchemaBuilder.Build(); + } + + private void RegisterSchema(string key, JsonSchema schema) + { + registeredSchemaKeys.Add(key); + registeredSchemas[key] = schema; } private Dictionary BuildPropertiesSchemas(Type type, string methodName, JsonSerializerOptions jsonSerializerOptions) => type .GetProperties() .ToDictionary(p => jsonSerializerOptions.ConvertName(p.Name), - p => CreateOrRef(p.PropertyType, methodName, jsonSerializerOptions)); + p => CreateOrRefInternal(p.PropertyType, methodName, p.GetXmlDocsSummary(), jsonSerializerOptions)); } + +internal static class JsonSchemaBuilderExtensions +{ + public static JsonSchemaBuilder TryAppendTitle(this JsonSchemaBuilder builder, string? propertySummary) + { + if (propertySummary is { Length: > 0 }) + { + builder.Title(propertySummary); + } + return builder; + } +} \ No newline at end of file diff --git a/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs b/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs index 1b9366ed..b0a3a944 100644 --- a/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs +++ b/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Text.Json; using FluentAssertions; using Json.Schema; @@ -366,6 +367,88 @@ public void CreateOrRef_DefaultSimpleTypesFormattedAsString() }; schemaGenerator.GetAllSchemas().Should().BeEquivalentTo(expectedRegistrations); } + + [Test] + public void CreateOrRef_SummariesFromResultObjectPropertiesCollectedAsTitlesOnJsonSchema() + { + var type = typeof(TypeWithSummaries); + var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicies.SnakeCaseLower }; + + var actualSchema = schemaGenerator.CreateOrRef(type, MethodName, jsonSerializerOptions); + + var expectedTypeName = $"{MethodName} {nameof(TypeWithSummaries)}"; + var expectedTypeNameInner = $"{MethodName} {nameof(TypeWithSummariesInner)}"; + var expectedTypeNameInnerEnum = $"{MethodName} {nameof(TypeWithSummariesInnerEnum)}"; + + actualSchema.Should().BeEquivalentTo(new JsonSchemaBuilder() + .Ref($"#/components/schemas/{expectedTypeName}") + .Build()); + + var actualSchemas = schemaGenerator.GetAllSchemas(); + + var expectedSchemas = new Dictionary + { + [expectedTypeNameInner] = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["inner_prop1"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + .Title("InnerProp1") + .Build() + }) + .Build(), + [expectedTypeNameInnerEnum] = new JsonSchemaBuilder() + .Enum("Bla") + .Build(), + [expectedTypeName] = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["prop1"] = new JsonSchemaBuilder().Ref($"#/components/schemas/{expectedTypeNameInner}") + .Title("Prop1") + .Build(), + ["prop2"] = new JsonSchemaBuilder().Ref($"#/components/schemas/{expectedTypeNameInner}") + .Title("Prop2") + .Build(), + ["prop3"] = new JsonSchemaBuilder().Type(SchemaValueType.Array) + .Items(new JsonSchemaBuilder().Ref($"#/components/schemas/{expectedTypeNameInner}") + .Build()) + .Title("Prop3") + .Build(), + ["prop4"] = new JsonSchemaBuilder().Ref($"#/components/schemas/{expectedTypeNameInnerEnum}") + .Title("Prop4") + .Build(), + ["prop5"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + .Format(Formats.Duration) + .Title("Prop5") + .Build() + }) + .Build(), + }; + + actualSchemas.Count.Should().Be(expectedSchemas.Count); + + var actualKeys = actualSchemas.Keys.ToArray(); + var actualValues = actualSchemas.Values.ToArray(); + + var expectedKeys = expectedSchemas.Keys.ToArray(); + var expectedValues = expectedSchemas.Values.ToArray(); + + actualKeys.Length.Should().Be(expectedKeys.Length); + actualValues.Length.Should().Be(expectedValues.Length); + + for (var i = 0; i < expectedSchemas.Count; i++) + { + var actualKey = actualKeys[i]; + var actualValue = actualValues[i]; + + var expectedKey = expectedKeys[i]; + var expectedValue = expectedValues[i]; + + actualKey.Should().BeEquivalentTo(expectedKey); + actualValue.Should().BeEquivalentTo(expectedValue); + } + } [Test] public void GetAllSchemas_ChangingCollection_DontAffectInnerCollection() @@ -402,6 +485,45 @@ private record TypeWithProperties(int IntProperty, string StringProperty, TypeWi private record AnotherTypeWithProperties(bool BoolProperty); private record TypeWithSimpleProperties(DateTime DateTime, DateTimeOffset DateTimeOffset, DateOnly DateOnly, TimeOnly TimeOnly, TimeSpan TimeSpan, Guid Guid); - + + private class TypeWithSummaries + { + /// + /// Prop1 + /// + public TypeWithSummariesInner Prop1 { get; set; } + + /// + /// Prop2 + /// + public TypeWithSummariesInner Prop2 { get; set; } + + /// + /// Prop3 + /// + public TypeWithSummariesInner[] Prop3 { get; set; } + + /// + /// Prop4 + /// + public TypeWithSummariesInnerEnum Prop4 { get; set; } + + /// + /// Prop5 + /// + public TimeSpan Prop5 { get; set; } + } + private class TypeWithSummariesInner + { + /// + /// InnerProp1 + /// + public string InnerProp1 { get; set; } + } + private enum TypeWithSummariesInnerEnum + { + Bla + } + private record CustomSimpleType; } From 88b9c1ffe4ec6f3043dfce73d231c72d5d8c5a5a Mon Sep 17 00:00:00 2001 From: Sergey Bessonov Date: Tue, 26 Mar 2024 19:35:04 +0500 Subject: [PATCH 4/4] fix(autodoc): OpenRpcDoc: nullable enum type parse as enum type --- .../OpenRpcConstants.cs | 1 + .../Services/OpenRpcSchemaGenerator.cs | 13 +++++--- .../Tochka.JsonRpc.OpenRpc.csproj | 1 + .../Services/OpenRpcSchemaGeneratorTests.cs | 32 ++++++++++++++++--- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/Tochka.JsonRpc.OpenRpc/OpenRpcConstants.cs b/src/Tochka.JsonRpc.OpenRpc/OpenRpcConstants.cs index b3783bf0..117fc9d7 100644 --- a/src/Tochka.JsonRpc.OpenRpc/OpenRpcConstants.cs +++ b/src/Tochka.JsonRpc.OpenRpc/OpenRpcConstants.cs @@ -43,6 +43,7 @@ public static class OpenRpcConstants WriteIndented = true, Converters = { + new JsonStringEnumConverter(JsonNamingPolicies.KebabCaseLower), new UriConverter() }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull diff --git a/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs b/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs index 011a5f11..afdc474e 100644 --- a/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs +++ b/src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs @@ -34,14 +34,17 @@ public JsonSchema CreateOrRef(Type type, string methodName, JsonSerializerOption private JsonSchema CreateOrRefInternal(Type type, string methodName, string? propertySummary, JsonSerializerOptions jsonSerializerOptions) { - var typeName = type.Name; - if (!typeName.StartsWith($"{methodName} ", StringComparison.Ordinal)) + // Unwrap nullable type + var clearType = Nullable.GetUnderlyingType(type) ?? type; + + var clearTypeName = clearType.Name; + if (!clearTypeName.StartsWith($"{methodName} ", StringComparison.Ordinal)) { // adding method name in case it uses not default serializer settings - typeName = $"{methodName} {typeName}"; + clearTypeName = $"{methodName} {clearTypeName}"; } - return BuildSchema(type, typeName, methodName, propertySummary, jsonSerializerOptions); + return BuildSchema(clearType, clearTypeName, methodName, propertySummary, jsonSerializerOptions); } private JsonSchema BuildSchema(Type type, string typeName, string methodName, string? propertySummary, JsonSerializerOptions jsonSerializerOptions) @@ -66,7 +69,7 @@ private JsonSchema BuildSchema(Type type, string typeName, string methodName, st if (type.IsEnum) { var enumSchema = new JsonSchemaBuilder() - .Enum(type.GetEnumNames()) + .Enum(type.GetEnumNames().Select(jsonSerializerOptions.ConvertName)) .Build(); RegisterSchema(typeName, enumSchema); // returning ref if it's enum or regular type with properties diff --git a/src/Tochka.JsonRpc.OpenRpc/Tochka.JsonRpc.OpenRpc.csproj b/src/Tochka.JsonRpc.OpenRpc/Tochka.JsonRpc.OpenRpc.csproj index a7ff107c..535fb97a 100644 --- a/src/Tochka.JsonRpc.OpenRpc/Tochka.JsonRpc.OpenRpc.csproj +++ b/src/Tochka.JsonRpc.OpenRpc/Tochka.JsonRpc.OpenRpc.csproj @@ -36,6 +36,7 @@ + all runtime; build; native; contentfiles; analyzers diff --git a/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs b/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs index b0a3a944..18a82c31 100644 --- a/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs +++ b/src/tests/Tochka.JsonRpc.OpenRpc.Tests/Services/OpenRpcSchemaGeneratorTests.cs @@ -77,7 +77,7 @@ public void CreateOrRef_CollectionWithEnumItems_ReturnSchemaWithRefForItems() var expectedRegistrations = new Dictionary { [expectedTypeName] = new JsonSchemaBuilder() - .Enum("One", "Two") + .Enum("one", "two") .Build() }; schemaGenerator.GetAllSchemas().Should().BeEquivalentTo(expectedRegistrations); @@ -139,11 +139,33 @@ public void CreateOrRef_Enum_ReturnRef() var expectedRegistrations = new Dictionary { [expectedTypeName] = new JsonSchemaBuilder() - .Enum("One", "Two") + .Enum("one", "two") .Build() }; schemaGenerator.GetAllSchemas().Should().BeEquivalentTo(expectedRegistrations); } + + [Test] + public void CreateOrRef_NullableEnum_ReturnRef() + { + var type = typeof(Enum?); + var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicies.SnakeCaseLower }; + + var result = schemaGenerator.CreateOrRef(type, MethodName, jsonSerializerOptions); + + var expectedTypeName = $"{MethodName} {nameof(Enum)}"; + var expectedSchema = new JsonSchemaBuilder() + .Ref($"#/components/schemas/{expectedTypeName}") + .Build(); + result.Should().BeEquivalentTo(expectedSchema); + var expectedRegistrations = new Dictionary + { + [expectedTypeName] = new JsonSchemaBuilder() + .Enum("one", "two") + .Build() + }; + schemaGenerator.GetAllSchemas().Should().BeEquivalentTo(expectedRegistrations); + } [Test] public void CreateOrRef_SimpleType_ReturnSchemaItself() @@ -231,7 +253,7 @@ public void CreateOrRef_AlreadyRegisteredEnum_DontRegisterAgain() var expectedRegistrations = new Dictionary { [expectedTypeName] = new JsonSchemaBuilder() - .Enum("One", "Two") + .Enum("one", "two") .Build() }; schemaGenerator.GetAllSchemas().Should().BeEquivalentTo(expectedRegistrations); @@ -319,7 +341,7 @@ public void CreateOrRef_AlreadyRegisteredTypeWithPropertiesWithAnotherMethodName public void CreateOrRef_EnumValuesFormatedAsDeclared() { var type = typeof(Enum2); - var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicies.SnakeCaseLower }; + var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = null }; var actualSchema = schemaGenerator.CreateOrRef(type, MethodName, jsonSerializerOptions); @@ -398,7 +420,7 @@ public void CreateOrRef_SummariesFromResultObjectPropertiesCollectedAsTitlesOnJs }) .Build(), [expectedTypeNameInnerEnum] = new JsonSchemaBuilder() - .Enum("Bla") + .Enum("bla") .Build(), [expectedTypeName] = new JsonSchemaBuilder() .Type(SchemaValueType.Object)