From 2464e2d895a2e79a4b2d6c2fe47c714910714934 Mon Sep 17 00:00:00 2001 From: Rico Suter Date: Wed, 26 Sep 2018 13:14:14 +0200 Subject: [PATCH] Inline dictionary or array inheritance (#785) * Inline dictionary or array inheritance * Fix c# generation --- .../EnumTests.cs | 33 +++++++++++++++ .../InheritanceTests.cs | 41 ++++++++++++++++++ .../CSharpTypeResolver.cs | 33 +++++++++++++-- .../Models/ClassTemplateModel.cs | 6 +-- .../InheritanceTests.cs | 39 +++++++++++++++++ .../CodeArtifact.cs | 7 ++++ .../TypeResolverBase.cs | 18 ++++++-- .../Generation/JsonSchemaGenerator.cs | 42 ++++++++++++++----- src/NJsonSchema/JsonSchema4.cs | 19 +++++++-- 9 files changed, 214 insertions(+), 24 deletions(-) create mode 100644 src/NJsonSchema.CodeGeneration.CSharp.Tests/InheritanceTests.cs create mode 100644 src/NJsonSchema.CodeGeneration.TypeScript.Tests/InheritanceTests.cs diff --git a/src/NJsonSchema.CodeGeneration.CSharp.Tests/EnumTests.cs b/src/NJsonSchema.CodeGeneration.CSharp.Tests/EnumTests.cs index 9e60b33ec..a4c11a86e 100644 --- a/src/NJsonSchema.CodeGeneration.CSharp.Tests/EnumTests.cs +++ b/src/NJsonSchema.CodeGeneration.CSharp.Tests/EnumTests.cs @@ -226,5 +226,38 @@ public async Task When_type_name_hint_has_generics_then_they_are_converted() /// Assert Assert.Contains("public enum FirstMetdodOfMetValueGroupChar", code); } + + [Fact] + public async Task When_enum_property_is_not_required_in_Swagger2_then_it_is_nullable() + { + //// Arrange + var json = + @"{ + ""type"": ""object"", + ""required"": [ + ""name"", + ""photoUrls"" + ], + ""properties"": { + ""status"": { + ""type"": ""string"", + ""description"": ""pet status in the store"", + ""enum"": [ + ""available"", + ""pending"", + ""sold"" + ] + } + } +}"; + var schema = await JsonSchema4.FromJsonAsync(json); + var generator = new CSharpGenerator(schema, new CSharpGeneratorSettings { SchemaType = SchemaType.Swagger2 }); + + //// Act + var code = generator.GenerateFile("MyClass"); + + //// Assert + Assert.Contains("private MyClassStatus? _status;", code); + } } } diff --git a/src/NJsonSchema.CodeGeneration.CSharp.Tests/InheritanceTests.cs b/src/NJsonSchema.CodeGeneration.CSharp.Tests/InheritanceTests.cs new file mode 100644 index 000000000..2bb212293 --- /dev/null +++ b/src/NJsonSchema.CodeGeneration.CSharp.Tests/InheritanceTests.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NJsonSchema.CodeGeneration.CSharp; +using Xunit; + +namespace NJsonSchema.CodeGeneration.Tests.CSharp +{ + public class InheritanceTests + { + public class MyContainer + { + public EmptyClassInheritingDictionary CustomDictionary { get; set; } + } + + public sealed class EmptyClassInheritingDictionary : Dictionary + { + } + + [Fact] + public async Task When_empty_class_inherits_from_dictionary_then_allOf_inheritance_still_works() + { + //// Arrange + var schema = await JsonSchema4.FromTypeAsync(); + var data = schema.ToJson(); + + var generator = new CSharpGenerator(schema, new CSharpGeneratorSettings()); + + //// Act + var code = generator.GenerateFile(); + + //// Assert + var dschema = schema.Definitions["EmptyClassInheritingDictionary"]; + Assert.Equal(0, dschema.AllOf.Count); + Assert.True(dschema.IsDictionary); + + Assert.Contains("public EmptyClassInheritingDictionary CustomDictionary", code); + Assert.Contains("public partial class EmptyClassInheritingDictionary : System.Collections.Generic.Dictionary", code); + } + } +} diff --git a/src/NJsonSchema.CodeGeneration.CSharp/CSharpTypeResolver.cs b/src/NJsonSchema.CodeGeneration.CSharp/CSharpTypeResolver.cs index deea26cd0..6660dcbf4 100644 --- a/src/NJsonSchema.CodeGeneration.CSharp/CSharpTypeResolver.cs +++ b/src/NJsonSchema.CodeGeneration.CSharp/CSharpTypeResolver.cs @@ -42,6 +42,17 @@ public CSharpTypeResolver(CSharpGeneratorSettings settings, JsonSchema4 exceptio /// The type name hint to use when generating the type and the type name is missing. /// The type name. public override string Resolve(JsonSchema4 schema, bool isNullable, string typeNameHint) + { + return Resolve(schema, isNullable, typeNameHint, true); + } + + /// Resolves and possibly generates the specified schema. + /// The schema. + /// Specifies whether the given type usage is nullable. + /// The type name hint to use when generating the type and the type name is missing. + /// Checks whether a named schema is already registered. + /// The type name. + public string Resolve(JsonSchema4 schema, bool isNullable, string typeNameHint, bool checkForExistingSchema) { schema = schema.ActualSchema; @@ -59,9 +70,6 @@ public override string Resolve(JsonSchema4 schema, bool isNullable, string typeN JsonObjectType.String; } - if (type.HasFlag(JsonObjectType.Array)) - return ResolveArrayOrTuple(schema); - if (type.HasFlag(JsonObjectType.Number)) return ResolveNumber(schema, isNullable); @@ -74,6 +82,12 @@ public override string Resolve(JsonSchema4 schema, bool isNullable, string typeN if (type.HasFlag(JsonObjectType.String)) return ResolveString(schema, isNullable, typeNameHint); + if (Types.ContainsKey(schema) && checkForExistingSchema) + return Types[schema]; + + if (type.HasFlag(JsonObjectType.Array)) + return ResolveArrayOrTuple(schema); + if (type.HasFlag(JsonObjectType.File)) return "byte[]"; @@ -83,6 +97,19 @@ public override string Resolve(JsonSchema4 schema, bool isNullable, string typeN return GetOrGenerateTypeName(schema, typeNameHint); } + /// Checks whether the given schema should generate a type. + /// The schema. + /// True if the schema should generate a type. + protected override bool IsTypeSchema(JsonSchema4 schema) + { + if (schema.IsDictionary || schema.IsArray) + { + return true; + } + + return base.IsTypeSchema(schema); + } + private string ResolveString(JsonSchema4 schema, bool isNullable, string typeNameHint) { if (schema.Format == JsonFormatStrings.Date) diff --git a/src/NJsonSchema.CodeGeneration.CSharp/Models/ClassTemplateModel.cs b/src/NJsonSchema.CodeGeneration.CSharp/Models/ClassTemplateModel.cs index cb9970345..efd9ca53c 100644 --- a/src/NJsonSchema.CodeGeneration.CSharp/Models/ClassTemplateModel.cs +++ b/src/NJsonSchema.CodeGeneration.CSharp/Models/ClassTemplateModel.cs @@ -39,7 +39,7 @@ public class ClassTemplateModel : ClassTemplateModelBase .Select(property => new PropertyModel(this, property, _resolver, _settings)) .ToArray(); - if (HasInheritance) + if (schema.InheritedSchema != null) { BaseClass = new ClassTemplateModel(BaseClassName, settings, resolver, schema.InheritedSchema, rootObject); AllProperties = Properties.Concat(BaseClass.AllProperties).ToArray(); @@ -95,10 +95,10 @@ public class ClassTemplateModel : ClassTemplateModelBase public string Discriminator => _schema.Discriminator; /// Gets a value indicating whether the class has a parent class. - public bool HasInheritance => _schema.InheritedSchema != null; + public bool HasInheritance => _schema.InheritedTypeSchema != null; /// Gets the base class name. - public string BaseClassName => HasInheritance ? _resolver.Resolve(_schema.InheritedSchema, false, string.Empty) + public string BaseClassName => HasInheritance ? _resolver.Resolve(_schema.InheritedTypeSchema, false, string.Empty, false) .Replace(_settings.ArrayType + "<", _settings.ArrayBaseType + "<") .Replace(_settings.DictionaryType + "<", _settings.DictionaryBaseType + "<") : null; diff --git a/src/NJsonSchema.CodeGeneration.TypeScript.Tests/InheritanceTests.cs b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/InheritanceTests.cs new file mode 100644 index 000000000..af1c5585f --- /dev/null +++ b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/InheritanceTests.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace NJsonSchema.CodeGeneration.TypeScript.Tests +{ + public class InheritanceTests + { + public class MyContainer + { + public EmptyClassInheritingDictionary CustomDictionary { get; set; } + } + + public sealed class EmptyClassInheritingDictionary : Dictionary + { + } + + [Fact] + public async Task When_empty_class_inherits_from_dictionary_then_allOf_inheritance_still_works() + { + //// Arrange + var schema = await JsonSchema4.FromTypeAsync(); + var data = schema.ToJson(); + + var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings { TypeScriptVersion = 2.0m }); + + //// Act + var code = generator.GenerateFile("ContainerClass"); + + //// Assert + var dschema = schema.Definitions["EmptyClassInheritingDictionary"]; + Assert.Equal(0, dschema.AllOf.Count); + Assert.True(dschema.IsDictionary); + + Assert.DoesNotContain("EmptyClassInheritingDictionary", code); + Assert.Contains("customDictionary: { [key: string] : any; } | undefined;", code); + } + } +} diff --git a/src/NJsonSchema.CodeGeneration/CodeArtifact.cs b/src/NJsonSchema.CodeGeneration/CodeArtifact.cs index 2e7d3dc07..5aec76ab0 100644 --- a/src/NJsonSchema.CodeGeneration/CodeArtifact.cs +++ b/src/NJsonSchema.CodeGeneration/CodeArtifact.cs @@ -6,6 +6,8 @@ // Rico Suter, mail@rsuter.com //----------------------------------------------------------------------- +using System; + namespace NJsonSchema.CodeGeneration { /// The type generator result. @@ -49,6 +51,11 @@ public CodeArtifact(string typeName, CodeArtifactType type, CodeArtifactLanguage /// The template to render the code. public CodeArtifact(string typeName, string baseTypeName, CodeArtifactType type, CodeArtifactLanguage language, ITemplate template) { + if (typeName == baseTypeName) + { + throw new ArgumentException("The baseTypeName cannot equal typeName.", nameof(typeName)); + } + TypeName = typeName; BaseTypeName = baseTypeName; diff --git a/src/NJsonSchema.CodeGeneration/TypeResolverBase.cs b/src/NJsonSchema.CodeGeneration/TypeResolverBase.cs index 46a9cf60e..d67feac60 100644 --- a/src/NJsonSchema.CodeGeneration/TypeResolverBase.cs +++ b/src/NJsonSchema.CodeGeneration/TypeResolverBase.cs @@ -70,15 +70,27 @@ public void RegisterSchemaDefinitions(IDictionary definitio foreach (var pair in definitions) { var schema = pair.Value.ActualSchema; - var isCodeGeneratingSchema = !schema.IsDictionary && !schema.IsAnyType && - (schema.IsEnumeration || schema.Type == JsonObjectType.None || schema.Type.HasFlag(JsonObjectType.Object)); - if (isCodeGeneratingSchema) + if (IsTypeSchema(schema)) + { GetOrGenerateTypeName(schema, pair.Key); + } } } } + /// Checks whether the given schema should generate a type. + /// The schema. + /// True if the schema should generate a type. + protected virtual bool IsTypeSchema(JsonSchema4 schema) + { + return !schema.IsDictionary && + !schema.IsAnyType && + (schema.IsEnumeration || + schema.Type == JsonObjectType.None || + schema.Type.HasFlag(JsonObjectType.Object)); + } + /// Resolves the type of the dictionary value of the given schema (must be a dictionary schema). /// The schema. /// The fallback type (e.g. 'object'). diff --git a/src/NJsonSchema/Generation/JsonSchemaGenerator.cs b/src/NJsonSchema/Generation/JsonSchemaGenerator.cs index b4c125800..24335f80d 100644 --- a/src/NJsonSchema/Generation/JsonSchemaGenerator.cs +++ b/src/NJsonSchema/Generation/JsonSchemaGenerator.cs @@ -700,26 +700,46 @@ private async Task GenerateInheritanceAsync(Type type, JsonSchema4 else { var actualSchema = new JsonSchema4(); + await GeneratePropertiesAsync(type, actualSchema, schemaResolver).ConfigureAwait(false); await ApplyAdditionalPropertiesAsync(type, actualSchema, schemaResolver).ConfigureAwait(false); - var baseSchema = await GenerateAsync(baseType, schemaResolver).ConfigureAwait(false); var baseTypeInfo = Settings.ReflectionService.GetDescription(baseType, null, Settings); - if (baseTypeInfo.RequiresSchemaReference(Settings.TypeMappers)) + var requiresSchemaReference = baseTypeInfo.RequiresSchemaReference(Settings.TypeMappers); + + if (actualSchema.Properties.Any() || requiresSchemaReference) { - if (schemaResolver.RootObject != baseSchema.ActualSchema) - schemaResolver.AppendSchema(baseSchema.ActualSchema, Settings.SchemaNameGenerator.Generate(baseType)); + // Use allOf inheritance only if the schema is an object with properties + // (not empty class which just inherits from array or dictionary) - schema.AllOf.Add(new JsonSchema4 + var baseSchema = await GenerateAsync(baseType, schemaResolver).ConfigureAwait(false); + if (requiresSchemaReference) + { + if (schemaResolver.RootObject != baseSchema.ActualSchema) + { + schemaResolver.AppendSchema(baseSchema.ActualSchema, Settings.SchemaNameGenerator.Generate(baseType)); + } + + schema.AllOf.Add(new JsonSchema4 + { + Reference = baseSchema.ActualSchema + }); + } + else { - Reference = baseSchema.ActualSchema - }); + schema.AllOf.Add(baseSchema); + } + + // First schema is the (referenced) base schema, second is the type schema itself + schema.AllOf.Add(actualSchema); + return actualSchema; } else - schema.AllOf.Add(baseSchema); - - schema.AllOf.Add(actualSchema); - return actualSchema; + { + // Array and dictionary inheritance are not expressed with allOf but inline + await GenerateAsync(baseType, null, schema, schemaResolver).ConfigureAwait(false); + return schema; + } } } } diff --git a/src/NJsonSchema/JsonSchema4.cs b/src/NJsonSchema/JsonSchema4.cs index 8af744c4e..36fe466a6 100644 --- a/src/NJsonSchema/JsonSchema4.cs +++ b/src/NJsonSchema/JsonSchema4.cs @@ -205,11 +205,7 @@ internal static JsonSchema4 FromJsonWithoutReferenceHandling(string data) /// Gets the inherited/parent schema (most probable base schema in allOf). /// Used for code generation. [JsonIgnore] -#if !LEGACY - public JsonSchema4 InheritedSchema -#else public JsonSchema4 InheritedSchema -#endif { get { @@ -229,6 +225,21 @@ public JsonSchema4 InheritedSchema } } + /// Gets the inherited/parent schema which may also be inlined + /// (the schema itself if it is a dictionary or array, otherwise ). + /// Used for code generation. + [JsonIgnore] + public JsonSchema4 InheritedTypeSchema + { + get + { + if (ActualTypeSchema.IsDictionary || ActualTypeSchema.IsArray) + return ActualTypeSchema; + + return InheritedSchema; + } + } + /// Gets the list of all inherited/parent schemas. /// Used for code generation. [JsonIgnore]