From 2ba95f8becf7dda3674acebb3bca7afacfd4bd7d Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 23 Jan 2024 10:41:20 +0000 Subject: [PATCH 1/5] Add regression test for sub-discriminator schemas --- .../DiscriminatorGenerationTests.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Bonsai.Sgen.Tests/DiscriminatorGenerationTests.cs b/Bonsai.Sgen.Tests/DiscriminatorGenerationTests.cs index 0fb82f2..86e35b5 100644 --- a/Bonsai.Sgen.Tests/DiscriminatorGenerationTests.cs +++ b/Bonsai.Sgen.Tests/DiscriminatorGenerationTests.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using NJsonSchema; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -219,5 +220,44 @@ public void GenerateFromArrayItemDiscriminatorRef_EnsureFallbackDiscriminatorBas AssertDiscriminatorAttribute(code, serializerLibraries, "kind"); CompilerTestHelper.CompileFromSource(code); } + + [TestMethod] + [DataRow(SerializerLibraries.YamlDotNet)] + [DataRow(SerializerLibraries.NewtonsoftJson)] + [DataRow(SerializerLibraries.NewtonsoftJson | SerializerLibraries.YamlDotNet)] + public void GenerateFromSubDiscriminatorSchemas_InheritanceHierarchyIsPreserved(SerializerLibraries serializerLibraries) + { + var subTypeSchemas = SchemaTestHelper.CreateDerivedSchemas("fur", "long", "short"); + var subDiscriminator = SchemaTestHelper.CreateDiscriminatorSchema("fur", subTypeSchemas); + var derivedSchemas = SchemaTestHelper.CreateDerivedSchemas("type", "cat", "dog"); + derivedSchemas[0].Value.DiscriminatorObject = subDiscriminator.DiscriminatorObject; + foreach (var subSchema in subDiscriminator.OneOf) + { + derivedSchemas[0].Value.OneOf.Add(subSchema); + } + var discriminator = SchemaTestHelper.CreateDiscriminatorSchema("type", derivedSchemas); + var schema = SchemaTestHelper.CreateContainerSchema(new Dictionary + { + { "LongFurCat", subTypeSchemas[0].Value }, + { "Cat", derivedSchemas[0].Value }, + { "Dog", derivedSchemas[1].Value }, + { "Animal", discriminator }, + { "ShortFurCat", subTypeSchemas[1].Value } + }); + schema.Properties.Add("Animals", new() + { + Type = JsonObjectType.Array, + Item = new() { Reference = discriminator } + }); + + var generator = TestHelper.CreateGenerator(schema, serializerLibraries); + var code = generator.GenerateFile(); + Assert.IsTrue(code.Contains("class Cat : Animal"), "Derived types do not inherit from base type."); + Assert.IsTrue(!code.Contains("public enum LongFurCatFur"), "Discriminator property is repeated in derived types."); + Assert.IsTrue(code.Contains("List Animals"), "Container element type does not match base type."); + Assert.IsTrue(code.Contains("[JsonInheritanceAttribute(\"dog\", typeof(Dog))]")); + AssertDiscriminatorAttribute(code, serializerLibraries, "type"); + CompilerTestHelper.CompileFromSource(code); + } } } From e19ec0a94be48d4e9e7a7253548df6daf69c946b Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 23 Jan 2024 10:42:54 +0000 Subject: [PATCH 2/5] Fix regression test assertion --- Bonsai.Sgen.Tests/DiscriminatorGenerationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bonsai.Sgen.Tests/DiscriminatorGenerationTests.cs b/Bonsai.Sgen.Tests/DiscriminatorGenerationTests.cs index 86e35b5..aa5d33f 100644 --- a/Bonsai.Sgen.Tests/DiscriminatorGenerationTests.cs +++ b/Bonsai.Sgen.Tests/DiscriminatorGenerationTests.cs @@ -215,7 +215,7 @@ public void GenerateFromArrayItemDiscriminatorRef_EnsureFallbackDiscriminatorBas var code = generator.GenerateFile(); Assert.IsTrue(code.Contains("class Dog : Animal"), "Derived types do not inherit from base type."); Assert.IsTrue(!code.Contains("public enum DogKind"), "Discriminator property is repeated in derived types."); - Assert.IsTrue(code.Contains("List Animal"), "Container element type does not match base type."); + Assert.IsTrue(code.Contains("List Animals"), "Container element type does not match base type."); Assert.IsTrue(code.Contains("[JsonInheritanceAttribute(\"Dog\", typeof(Dog))]")); AssertDiscriminatorAttribute(code, serializerLibraries, "kind"); CompilerTestHelper.CompileFromSource(code); From 3a4ad66b408a01f58f52eb67e132f7759a47a796 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 23 Jan 2024 10:53:35 +0000 Subject: [PATCH 3/5] Add support for deep oneOf inheritance --- Bonsai.Sgen.Tests/TestHelper.cs | 2 +- Bonsai.Sgen/JsonSchemaExtensions.cs | 61 +++++++++++++++++------------ Bonsai.Sgen/Program.cs | 2 +- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/Bonsai.Sgen.Tests/TestHelper.cs b/Bonsai.Sgen.Tests/TestHelper.cs index 5c5d5ec..7f23e0b 100644 --- a/Bonsai.Sgen.Tests/TestHelper.cs +++ b/Bonsai.Sgen.Tests/TestHelper.cs @@ -8,7 +8,7 @@ public static CSharpCodeDomGenerator CreateGenerator( JsonSchema schema, SerializerLibraries serializerLibraries = SerializerLibraries.YamlDotNet | SerializerLibraries.NewtonsoftJson) { - schema = schema.WithUniqueDiscriminatorProperties(); + schema = schema.WithResolvedDiscriminatorInheritance(); var settings = new CSharpCodeDomGeneratorSettings { Namespace = nameof(TestHelper), diff --git a/Bonsai.Sgen/JsonSchemaExtensions.cs b/Bonsai.Sgen/JsonSchemaExtensions.cs index 77ecd7f..4f7e1f4 100644 --- a/Bonsai.Sgen/JsonSchemaExtensions.cs +++ b/Bonsai.Sgen/JsonSchemaExtensions.cs @@ -1,21 +1,22 @@ using NJsonSchema; -using NJsonSchema.CodeGeneration; using NJsonSchema.Visitors; namespace Bonsai.Sgen { internal static class JsonSchemaExtensions { - public static JsonSchema WithUniqueDiscriminatorProperties(this JsonSchema schema) + public static JsonSchema WithResolvedDiscriminatorInheritance(this JsonSchema schema) { - var visitor = new DiscriminatorSchemaVisitor(schema); - visitor.Visit(schema); + var discriminatorVisitor = new DiscriminatorSchemaVisitor(schema); + var derivedDiscriminatorVisitor = new DerivedDiscriminatorSchemaVisitor(); + discriminatorVisitor.Visit(schema); + derivedDiscriminatorVisitor.Visit(schema); return schema; } class DiscriminatorSchemaVisitor : JsonSchemaVisitorBase { - readonly Dictionary reverseTypeNameLookup = new(); + readonly Dictionary definitionTypeNameLookup = new(); public DiscriminatorSchemaVisitor(JsonSchema rootObject) { @@ -34,7 +35,11 @@ private void ResolveOneOfInheritance(JsonSchema schema, JsonSchema baseSchema) continue; } - derivedSchema.ActualSchema.AllOf.Add(new JsonSchema { Reference = baseSchema }); + var actualSchema = derivedSchema.ActualSchema; + if (!actualSchema.AllOf.Any(schema => schema.Reference == baseSchema)) + { + actualSchema.AllOf.Add(new JsonSchema { Reference = baseSchema }); + } } } @@ -43,15 +48,16 @@ protected override JsonSchema VisitSchema(JsonSchema schema, string path, string var actualSchema = schema.ActualSchema; if (actualSchema.DiscriminatorObject != null) { - if (schema is JsonSchemaProperty || schema.ParentSchema?.Item == schema) + var isDefinition = definitionTypeNameLookup.TryGetValue(actualSchema, out _); + if (schema is JsonSchemaProperty || schema.ParentSchema?.Item == schema || isDefinition) { - if (string.IsNullOrEmpty(typeNameHint) && - !reverseTypeNameLookup.TryGetValue(actualSchema, out typeNameHint)) + var discriminatorSchema = isDefinition ? actualSchema : null; + if (string.IsNullOrEmpty(typeNameHint)) { typeNameHint = "Anonymous"; } - if (!RootObject.Definitions.TryGetValue(typeNameHint, out JsonSchema? discriminatorSchema)) + if (discriminatorSchema == null && !RootObject.Definitions.TryGetValue(typeNameHint, out discriminatorSchema)) { discriminatorSchema = new JsonSchema(); discriminatorSchema.DiscriminatorObject = actualSchema.DiscriminatorObject; @@ -64,23 +70,13 @@ protected override JsonSchema VisitSchema(JsonSchema schema, string path, string if (discriminatorSchema.OneOf.Count > 0) { ResolveOneOfInheritance(discriminatorSchema, discriminatorSchema); - discriminatorSchema.OneOf.Clear(); } } - schema.DiscriminatorObject = null; - schema.IsAbstract = false; - return schema; - } - - foreach (var derivedSchema in schema.GetDerivedSchemas(RootObject).Keys) - { - foreach (var property in derivedSchema.Properties.Keys.ToList()) + if (!isDefinition) { - if (property == schema.Discriminator) - { - derivedSchema.Properties.Remove(property); - } + actualSchema.DiscriminatorObject = null; + actualSchema.IsAbstract = false; } } } @@ -111,7 +107,7 @@ private void VisitDefinitions(JsonSchema schema) { foreach (var definition in schema.Definitions) { - reverseTypeNameLookup[definition.Value] = definition.Key; + definitionTypeNameLookup[definition.Value] = definition.Key; VisitDefinitions(definition.Value); } } @@ -139,5 +135,22 @@ private void VisitDefinitions(IDictionary dictionary } } } + + class DerivedDiscriminatorSchemaVisitor : JsonSchemaVisitorBase + { + protected override JsonSchema VisitSchema(JsonSchema schema, string path, string typeNameHint) + { + foreach (var baseSchema in schema.AllInheritedSchemas) + { + var discriminatorSchema = baseSchema.DiscriminatorObject; + if (discriminatorSchema != null) + { + schema.Properties.Remove(discriminatorSchema.PropertyName); + } + } + + return schema; + } + } } } diff --git a/Bonsai.Sgen/Program.cs b/Bonsai.Sgen/Program.cs index 3fe6702..6e3a155 100644 --- a/Bonsai.Sgen/Program.cs +++ b/Bonsai.Sgen/Program.cs @@ -63,7 +63,7 @@ static async Task Main(string[] args) SerializerLibraries = serializerLibraries }; - schema = schema.WithUniqueDiscriminatorProperties(); + schema = schema.WithResolvedDiscriminatorInheritance(); var generator = new CSharpCodeDomGenerator(schema, settings); var code = generator.GenerateFile(generatorTypeName); if (string.IsNullOrEmpty(outputFilePath)) From 1193061cf416fb45dc8c7d612d48c15d6719c166 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 23 Jan 2024 10:55:30 +0000 Subject: [PATCH 4/5] Ensure generator uses local discriminator objects --- Bonsai.Sgen/CSharpClassTemplate.cs | 7 ++++--- Bonsai.Sgen/CSharpCodeDomGenerator.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Bonsai.Sgen/CSharpClassTemplate.cs b/Bonsai.Sgen/CSharpClassTemplate.cs index f024187..001596f 100644 --- a/Bonsai.Sgen/CSharpClassTemplate.cs +++ b/Bonsai.Sgen/CSharpClassTemplate.cs @@ -4,6 +4,7 @@ using System.Text; using System.Xml.Serialization; using Newtonsoft.Json; +using NJsonSchema; using NJsonSchema.Converters; using YamlDotNet.Serialization; @@ -30,7 +31,7 @@ public override void BuildType(CodeTypeDeclaration type) var jsonSerializer = Settings.SerializerLibraries.HasFlag(SerializerLibraries.NewtonsoftJson); var yamlSerializer = Settings.SerializerLibraries.HasFlag(SerializerLibraries.YamlDotNet); if (Model.IsAbstract) type.TypeAttributes |= System.Reflection.TypeAttributes.Abstract; - if (Model.HasDiscriminator) + if (Model.Schema.DiscriminatorObject is OpenApiDiscriminator discriminator) { if (jsonSerializer || yamlSerializer) { @@ -39,13 +40,13 @@ public override void BuildType(CodeTypeDeclaration type) type.CustomAttributes.Add(new CodeAttributeDeclaration( new CodeTypeReference(typeof(JsonConverter)), new CodeAttributeArgument(new CodeTypeOfExpression(nameof(JsonInheritanceConverter))), - new CodeAttributeArgument(new CodePrimitiveExpression(Model.Discriminator)))); + new CodeAttributeArgument(new CodePrimitiveExpression(discriminator.PropertyName)))); } if (yamlSerializer) { type.CustomAttributes.Add(new CodeAttributeDeclaration( new CodeTypeReference("YamlDiscriminator"), - new CodeAttributeArgument(new CodePrimitiveExpression(Model.Discriminator)))); + new CodeAttributeArgument(new CodePrimitiveExpression(discriminator.PropertyName)))); } foreach (var derivedModel in Model.DerivedClasses) diff --git a/Bonsai.Sgen/CSharpCodeDomGenerator.cs b/Bonsai.Sgen/CSharpCodeDomGenerator.cs index db2c3f7..aa5d1fb 100644 --- a/Bonsai.Sgen/CSharpCodeDomGenerator.cs +++ b/Bonsai.Sgen/CSharpCodeDomGenerator.cs @@ -105,7 +105,7 @@ public override IEnumerable GenerateTypes() let classType = type as CSharpClassCodeArtifact where classType != null select classType).ToList(); - var discriminatorTypes = classTypes.Where(modelType => modelType.Model.HasDiscriminator).ToList(); + var discriminatorTypes = classTypes.Where(modelType => modelType.Model.Schema.DiscriminatorObject != null).ToList(); foreach (var type in discriminatorTypes) { var matchTemplate = new CSharpTypeMatchTemplate(type, _provider, _options, Settings); From b493bb959a33ddb3eef3f733dfa9d214f3e2f1ef Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 23 Jan 2024 10:55:39 +0000 Subject: [PATCH 5/5] Remove unused constructor --- Bonsai.Sgen/CSharpTypeResolver.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Bonsai.Sgen/CSharpTypeResolver.cs b/Bonsai.Sgen/CSharpTypeResolver.cs index ebeb901..79e8c78 100644 --- a/Bonsai.Sgen/CSharpTypeResolver.cs +++ b/Bonsai.Sgen/CSharpTypeResolver.cs @@ -12,11 +12,6 @@ public CSharpTypeResolver(CSharpGeneratorSettings settings) { } - public CSharpTypeResolver(CSharpGeneratorSettings settings, JsonSchema exceptionSchema) - : base(settings, exceptionSchema) - { - } - public override JsonSchema RemoveNullability(JsonSchema schema) { JsonSchema? selectedSchema = null;