From 43a0e26b337b548be26bfea1ab90f960cd81844a Mon Sep 17 00:00:00 2001 From: Oleg Shuruev Date: Wed, 15 Nov 2023 22:06:39 -0800 Subject: [PATCH] Migrate to 10.9.0 (#10) * Update README.md * Handle single quote in properties names (#1574) * added abstract schema checking to CSharpValueGenerator GetDefaultValue (#1570) Co-authored-by: Gergo Vandor * Recursive sample generation (#1561) * Add tests that show generator doesnt handle definition re-use/recursion * Allow definition re-use/recursion in SampleJsonDataGenerator Adds a MaxRecursionLevel to SampleJsonDataGeneratorSettings * Add test of recursion level * Fix race condition in GetName (#1571) * Update Namotion.Reflection * v11.1.0 * Revert "v11.1.0" This reverts commit 1015b01d29d1ce144edcad1bcbb49837655e697d. * Post-merge adjustments --------- Co-authored-by: Rico Suter Co-authored-by: kal <35899782+kalilistic@users.noreply.github.com> Co-authored-by: Gergo Vandor Co-authored-by: Gergo Vandor Co-authored-by: Flemming Madsen Co-authored-by: SeongChan Lee Co-authored-by: Rico Suter --- README.md | 3 +- src/Directory.Build.props | 2 +- .../CSharpPropertyNameGenerator.cs | 1 + .../CSharpValueGenerator.cs | 2 +- .../NJsonSchema.CodeGeneration.CSharp.csproj | 2 +- .../DefaultValueGeneratorTests.cs | 27 +++ ...sonSchema.CodeGeneration.TypeScript.csproj | 2 +- .../NJsonSchema.CodeGeneration.csproj | 2 +- .../SampleJsonDataGeneratorTests.cs | 164 ++++++++++++++++++ .../Generation/SampleJsonDataGenerator.cs | 122 +++++++------ .../SampleJsonDataGeneratorSettings.cs | 3 + .../Infrastructure/TypeExtensions.cs | 30 +++- src/NJsonSchema/NJsonSchema.csproj | 4 +- 13 files changed, 295 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 32e7e3a7c..d8134db35 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,7 @@ [![Azure DevOps](https://img.shields.io/azure-devops/build/rsuter/NJsonSchema/17/master.svg)](https://rsuter.visualstudio.com/NJsonSchema/_build?definitionId=17) [![Nuget](https://img.shields.io/nuget/v/NJsonSchema.svg)](https://www.nuget.org/packages?q=NJsonSchema) [![MyGet](https://img.shields.io/myget/njsonschema/v/NJsonSchema.svg?label=preview%20nuget)](https://www.myget.org/feed/Packages/njsonschema) -[![Gitter](https://img.shields.io/badge/gitter-join%20chat-1dce73.svg)](https://gitter.im/NJsonSchema/NJsonSchema) -[![Discord](https://img.shields.io/badge/Discord-join%20chat-1dce73.svg)](https://discord.gg/4x48JjUT) +[![Discord](https://img.shields.io/badge/Discord-join%20chat-1dce73.svg)](https://discord.gg/BxQNy25WF6) [![StackOverflow](https://img.shields.io/badge/questions-on%20StackOverflow-orange.svg?style=flat)](http://stackoverflow.com/questions/tagged/njsonschema) [![Wiki](https://img.shields.io/badge/docs-in%20wiki-orange.svg?style=flat)](https://github.com/RicoSuter/njsonschema/wiki) [![Apimundo](https://img.shields.io/badge/Architecture-Apimundo-728199.svg)](https://apimundo.com/organizations/github/projects/ricosuter?tab=repositories) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index fbd57c0b7..a95cf50a5 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - 10.8.0 + 10.9.0 Rico Suter Copyright © Rico Suter, 2022 diff --git a/src/NJsonSchema.CodeGeneration.CSharp/CSharpPropertyNameGenerator.cs b/src/NJsonSchema.CodeGeneration.CSharp/CSharpPropertyNameGenerator.cs index e2fe88f72..bfb5c54b0 100644 --- a/src/NJsonSchema.CodeGeneration.CSharp/CSharpPropertyNameGenerator.cs +++ b/src/NJsonSchema.CodeGeneration.CSharp/CSharpPropertyNameGenerator.cs @@ -18,6 +18,7 @@ public virtual string Generate(JsonSchemaProperty property) { return ConversionUtilities.ConvertToUpperCamelCase(property.Name .Replace("\"", string.Empty) + .Replace("'", string.Empty) .Replace("@", string.Empty) .Replace("?", string.Empty) .Replace("$", string.Empty) diff --git a/src/NJsonSchema.CodeGeneration.CSharp/CSharpValueGenerator.cs b/src/NJsonSchema.CodeGeneration.CSharp/CSharpValueGenerator.cs index ae4dc542d..6c21eb7fc 100644 --- a/src/NJsonSchema.CodeGeneration.CSharp/CSharpValueGenerator.cs +++ b/src/NJsonSchema.CodeGeneration.CSharp/CSharpValueGenerator.cs @@ -74,7 +74,7 @@ public override string GetDefaultValue(JsonSchema schema, bool allowsNull, strin ? _settings.ArrayInstanceType + targetType.Substring(_settings.ArrayType.Length) : targetType; - return $"new {targetType}()"; + return schema.IsAbstract ? null : $"new {targetType}()"; } } } diff --git a/src/NJsonSchema.CodeGeneration.CSharp/NJsonSchema.CodeGeneration.CSharp.csproj b/src/NJsonSchema.CodeGeneration.CSharp/NJsonSchema.CodeGeneration.CSharp.csproj index a45dcc723..0c6630d6e 100644 --- a/src/NJsonSchema.CodeGeneration.CSharp/NJsonSchema.CodeGeneration.CSharp.csproj +++ b/src/NJsonSchema.CodeGeneration.CSharp/NJsonSchema.CodeGeneration.CSharp.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/NJsonSchema.CodeGeneration.Tests/DefaultValueGeneratorTests.cs b/src/NJsonSchema.CodeGeneration.Tests/DefaultValueGeneratorTests.cs index ceaf3f07e..b277d98f3 100644 --- a/src/NJsonSchema.CodeGeneration.Tests/DefaultValueGeneratorTests.cs +++ b/src/NJsonSchema.CodeGeneration.Tests/DefaultValueGeneratorTests.cs @@ -218,5 +218,32 @@ public void When_schema_has_default_value_of_enum_it_is_generated_in_CSharp_and_ Assert.Equal("Ns.MyEnum.bar", csharpValue); Assert.Equal("MyEnum.bar", typescriptValue); } + + [Fact] + public void When_schema_has_required_abstract_class_it_generates_no_default_value_for_in_CSharp_and_TypeScript() + { + //// Arrange + var csharpSettings = new CSharpGeneratorSettings(); + var csharpGenerator = new CSharpValueGenerator(csharpSettings); + var csharpTypeResolver = new CSharpTypeResolver(csharpSettings); + + //// Act + var schema = new JsonSchema() + { + Type = JsonObjectType.Object, + IsAbstract = true + }; + + var typescriptSettings = new TypeScriptGeneratorSettings(); + var typescriptGenerator = new TypeScriptValueGenerator(typescriptSettings); + var typescriptTypeResolver = new TypeScriptTypeResolver(typescriptSettings); + + var csharpValue = csharpGenerator.GetDefaultValue(schema, false, "BaseClass", "BaseClass", true, csharpTypeResolver); + var typescriptValue = typescriptGenerator.GetDefaultValue(schema, false, "BaseClass", "BaseClass", true, typescriptTypeResolver); + + //// Assert + Assert.Null(csharpValue); + Assert.Null(typescriptValue); + } } } diff --git a/src/NJsonSchema.CodeGeneration.TypeScript/NJsonSchema.CodeGeneration.TypeScript.csproj b/src/NJsonSchema.CodeGeneration.TypeScript/NJsonSchema.CodeGeneration.TypeScript.csproj index 113e2a601..74f3224ce 100644 --- a/src/NJsonSchema.CodeGeneration.TypeScript/NJsonSchema.CodeGeneration.TypeScript.csproj +++ b/src/NJsonSchema.CodeGeneration.TypeScript/NJsonSchema.CodeGeneration.TypeScript.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/NJsonSchema.CodeGeneration/NJsonSchema.CodeGeneration.csproj b/src/NJsonSchema.CodeGeneration/NJsonSchema.CodeGeneration.csproj index 2dc6ad7b0..d596fcfda 100644 --- a/src/NJsonSchema.CodeGeneration/NJsonSchema.CodeGeneration.csproj +++ b/src/NJsonSchema.CodeGeneration/NJsonSchema.CodeGeneration.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/NJsonSchema.Tests/Generation/SampleJsonDataGeneratorTests.cs b/src/NJsonSchema.Tests/Generation/SampleJsonDataGeneratorTests.cs index 41b9a0fb8..79f44a3bf 100644 --- a/src/NJsonSchema.Tests/Generation/SampleJsonDataGeneratorTests.cs +++ b/src/NJsonSchema.Tests/Generation/SampleJsonDataGeneratorTests.cs @@ -185,6 +185,170 @@ public async Task PropertyWithIntegerMinimumDefiniton() Assert.Equal(1, testJson.SelectToken("body.numberContent.value").Value()); } + [Fact] + public async Task SchemaWithRecursiveDefinition() + { + //// Arrange + var data = @"{ + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""title"": ""test schema"", + ""type"": ""object"", + ""required"": [ + ""body"", ""footer"" + ], + ""properties"": { + ""body"": { + ""$ref"": ""#/definitions/body"" + }, + ""footer"": { + ""$ref"": ""#/definitions/numberContent"" + } + }, + ""definitions"": { + ""body"": { + ""type"": ""object"", + ""additionalProperties"": false, + ""properties"": { + ""numberContent"": { + ""$ref"": ""#/definitions/numberContent"" + } + } + }, + ""numberContent"": { + ""type"": ""object"", + ""additionalProperties"": false, + ""properties"": { + ""value"": { + ""type"": ""number"", + ""maximum"": 5.00001, + ""minimum"": 1.000012 + }, + ""data"": { + ""$ref"": ""#/definitions/body"" + } + } + } + } + }"; + var generator = new SampleJsonDataGenerator(); + var schema = await JsonSchema.FromJsonAsync(data); + //// Act + var testJson = generator.Generate(schema); + + //// Assert + var footerToken = testJson.SelectToken("body.numberContent.data.numberContent.value"); + Assert.NotNull(footerToken); + + var validationResult = schema.Validate(testJson); + Assert.NotNull(validationResult); + Assert.Equal(1.000012, testJson.SelectToken("footer.value").Value()); + Assert.True(validationResult.Count > 0); // It is expected to fail validating the recursive properties (because of max recursion level) + } + + [Fact] + public async Task GeneratorAdheresToMaxRecursionLevel() + { + //// Arrange + var data = @"{ + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""title"": ""test schema"", + ""type"": ""object"", + ""required"": [ + ""body"", ""footer"" + ], + ""properties"": { + ""body"": { + ""$ref"": ""#/definitions/body"" + } + }, + ""definitions"": { + ""body"": { + ""type"": ""object"", + ""additionalProperties"": false, + ""properties"": { + ""text"": { ""type"": ""string"", ""enum"": [""my_string""] }, + ""body"": { + ""$ref"": ""#/definitions/body"" + } + } + } + } + }"; + var generator = new SampleJsonDataGenerator(new SampleJsonDataGeneratorSettings() { MaxRecursionLevel = 2 }); + var schema = await JsonSchema.FromJsonAsync(data); + //// Act + var testJson = generator.Generate(schema); + + //// Assert + var secondBodyToken = testJson.SelectToken("body.body"); + Assert.NotNull(secondBodyToken); + + var thirdBodyToken = testJson.SelectToken("body.body.body") as JValue; + Assert.NotNull(thirdBodyToken); + Assert.Equal(JTokenType.Null, thirdBodyToken.Type); + + var validationResult = schema.Validate(testJson); + Assert.NotNull(validationResult); + Assert.True(validationResult.Count > 0); // It is expected to fail validating the recursive properties (because of max recursion level) + } + + [Fact] + public async Task SchemaWithDefinitionUseMultipleTimes() + { + //// Arrange + var data = @"{ + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""title"": ""test schema"", + ""type"": ""object"", + ""required"": [ + ""body"", ""footer"" + ], + ""properties"": { + ""body"": { + ""$ref"": ""#/definitions/body"" + }, + ""footer"": { + ""$ref"": ""#/definitions/numberContent"" + } + }, + ""definitions"": { + ""body"": { + ""type"": ""object"", + ""additionalProperties"": false, + ""properties"": { + ""numberContent"": { + ""$ref"": ""#/definitions/numberContent"" + } + } + }, + ""numberContent"": { + ""type"": ""object"", + ""additionalProperties"": false, + ""properties"": { + ""value"": { + ""type"": ""number"", + ""maximum"": 5.00001, + ""minimum"": 1.000012 + } + } + } + } + }"; + var generator = new SampleJsonDataGenerator(); + var schema = await JsonSchema.FromJsonAsync(data); + + //// Act + var testJson = generator.Generate(schema); + + //// Assert + var footerToken = testJson.SelectToken("footer.value"); + Assert.NotNull(footerToken); + + var validationResult = schema.Validate(testJson); + Assert.NotNull(validationResult); + Assert.Equal(0, validationResult.Count); + Assert.Equal(1.000012, testJson.SelectToken("body.numberContent.value").Value()); + } [Fact] public async Task PropertyWithFloatMinimumDefinition() diff --git a/src/NJsonSchema/Generation/SampleJsonDataGenerator.cs b/src/NJsonSchema/Generation/SampleJsonDataGenerator.cs index 78caf0c54..9b42ec9f6 100644 --- a/src/NJsonSchema/Generation/SampleJsonDataGenerator.cs +++ b/src/NJsonSchema/Generation/SampleJsonDataGenerator.cs @@ -40,83 +40,95 @@ public SampleJsonDataGenerator(SampleJsonDataGeneratorSettings settings) /// The JSON token. public JToken Generate(JsonSchema schema) { - return Generate(schema, new HashSet()); + var stack = new Stack(); + stack.Push(schema); + return Generate(schema, stack); } - private JToken Generate(JsonSchema schema, HashSet usedSchemas) + private JToken Generate(JsonSchema schema, Stack schemaStack) { var property = schema as JsonSchemaProperty; schema = schema.ActualSchema; - if (usedSchemas.Contains(schema)) + try { - return null; - } + schemaStack.Push(schema); + if (schemaStack.Count(s => s == schema) > _settings.MaxRecursionLevel) + { + return null; + } - if (schema.Type.IsObject() || GetPropertiesToGenerate(schema.AllOf).Any()) - { - usedSchemas.Add(schema); + if (schema.Type.IsObject() || GetPropertiesToGenerate(schema.AllOf).Any()) + { + var schemas = new[] { schema }.Concat(schema.AllOf.Select(x => x.ActualSchema)); + var properties = GetPropertiesToGenerate(schemas); - var schemas = new[] { schema }.Concat(schema.AllOf.Select(x => x.ActualSchema)); - var properties = GetPropertiesToGenerate(schemas); + var obj = new JObject(); + foreach (var p in properties) + { + obj[p.Key] = Generate(p.Value, schemaStack); + } - var obj = new JObject(); - foreach (var p in properties) + return obj; + } + else if (schema.Default != null) { - obj[p.Key] = Generate(p.Value, usedSchemas); + return JToken.FromObject(schema.Default); } - return obj; - } - else if (schema.Default != null) - { - return JToken.FromObject(schema.Default); - } - else if (schema.Type.IsArray()) - { - if (schema.Item != null) + else if (schema.Type.IsArray()) { - var array = new JArray(); - var item = Generate(schema.Item, usedSchemas); - if (item != null) + if (schema.Item != null) + { + var array = new JArray(); + + var item = Generate(schema.Item, schemaStack); + if (item != null) + { + array.Add(item); + } + + return array; + } + else if (schema.Items.Count > 0) { - array.Add(item); + var array = new JArray(); + foreach (var item in schema.Items) + { + array.Add(Generate(item, schemaStack)); + } + + return array; } - return array; } - else if (schema.Items.Count > 0) + else { - var array = new JArray(); - foreach (var item in schema.Items) + if (schema.IsEnumeration) + { + return JToken.FromObject(schema.Enumeration.First()); + } + else if (schema.Type.IsInteger()) { - array.Add(Generate(item, usedSchemas)); + return HandleIntegerType(schema); + } + else if (schema.Type.IsNumber()) + { + return HandleNumberType(schema); + } + else if (schema.Type.IsString()) + { + return HandleStringType(schema, property); + } + else if (schema.Type.IsBoolean()) + { + return JToken.FromObject(false); } - return array; } + + return null; } - else + finally { - if (schema.IsEnumeration) - { - return JToken.FromObject(schema.Enumeration.First()); - } - else if (schema.Type.IsInteger()) - { - return HandleIntegerType(schema); - } - else if (schema.Type.IsNumber()) - { - return HandleNumberType(schema); - } - else if (schema.Type.IsString()) - { - return HandleStringType(schema, property); - } - else if (schema.Type.IsBoolean()) - { - return JToken.FromObject(false); - } + schemaStack.Pop(); } - - return null; } private JToken HandleNumberType(JsonSchema schema) { diff --git a/src/NJsonSchema/Generation/SampleJsonDataGeneratorSettings.cs b/src/NJsonSchema/Generation/SampleJsonDataGeneratorSettings.cs index 055e6ac7a..0dee8e309 100644 --- a/src/NJsonSchema/Generation/SampleJsonDataGeneratorSettings.cs +++ b/src/NJsonSchema/Generation/SampleJsonDataGeneratorSettings.cs @@ -5,5 +5,8 @@ public class SampleJsonDataGeneratorSettings { /// Gets or sets a value indicating whether to generate optional properties (default: true). public bool GenerateOptionalProperties { get; set; } = true; + + /// Gets or sets a value indicating the max level of recursion the generator is allowed to perform (default: 3) + public int MaxRecursionLevel { get; set; } = 3; } } diff --git a/src/NJsonSchema/Infrastructure/TypeExtensions.cs b/src/NJsonSchema/Infrastructure/TypeExtensions.cs index 1cb69e3db..b031c1448 100644 --- a/src/NJsonSchema/Infrastructure/TypeExtensions.cs +++ b/src/NJsonSchema/Infrastructure/TypeExtensions.cs @@ -12,29 +12,49 @@ using System; using System.Collections.Generic; using System.Runtime.Serialization; +using System.Threading; namespace NJsonSchema.Infrastructure { /// Provides extension methods for reading contextual type names and descriptions. public static class TypeExtensions { + private static ReaderWriterLockSlim _namesLock = new ReaderWriterLockSlim(); private static Dictionary _names = new Dictionary(); /// Gets the name of the property for JSON serialization. /// The name. internal static string GetName(this ContextualAccessorInfo accessorInfo) { - if (!_names.ContainsKey(accessorInfo)) + _namesLock.EnterUpgradeableReadLock(); + try { - lock (_names) + if (_names.TryGetValue(accessorInfo, out var name)) { - if (!_names.ContainsKey(accessorInfo)) + return name; + } + + _namesLock.EnterWriteLock(); + try + { + if (_names.TryGetValue(accessorInfo, out name)) { - _names[accessorInfo] = GetNameWithoutCache(accessorInfo); + return name; } + + name = GetNameWithoutCache(accessorInfo); + _names[accessorInfo] = name; + return name; } + finally + { + _namesLock.ExitWriteLock(); + } + } + finally + { + _namesLock.ExitUpgradeableReadLock(); } - return _names[accessorInfo]; } private static string GetNameWithoutCache(ContextualAccessorInfo accessorInfo) diff --git a/src/NJsonSchema/NJsonSchema.csproj b/src/NJsonSchema/NJsonSchema.csproj index f448810cd..0cf4d7319 100644 --- a/src/NJsonSchema/NJsonSchema.csproj +++ b/src/NJsonSchema/NJsonSchema.csproj @@ -10,8 +10,8 @@ bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - - + +