From 7358e25f534e29130240b60a10b4cc7d945cc5dc Mon Sep 17 00:00:00 2001 From: Larry Golding Date: Fri, 28 Dec 2018 15:12:34 -0800 Subject: [PATCH] Fix #57, fix #58: Emit initializers and attributes for default values (#60) #57: When a schema property declares a default value, assign that value to the property in the default constructor. This ensures that the property has the correct default even if you construct the object by hand rather than by deserializing it from JSON. #58: When a schema property declares a default value, decorate the property with a `System.ComponentModel.DefaultValue` attribute. This avoids your having to specify those attributes in the CodeGenHints.json file, which is error-prone. Also: - Fix a bug where the generated doc comments for properties had an extra space: `cref="P: propName`" → `cref="P:propName`". - Address some hygiene-related IDE messages by marking some properties `readonly`, and by inlining the declarations of some `out` parameters. --- .../DataModelGeneratorTests.cs | 152 +++++++++++++++--- src/Json.Schema.ToDotNet/ClassGenerator.cs | 114 ++++++++++++- .../ClassOrInterfaceGenerator.cs | 4 +- .../EqualityComparerGenerator.cs | 10 +- .../InterfaceGenerator.cs | 2 +- .../Json.Schema.ToDotNet.csproj | 15 ++ src/Json.Schema.ToDotNet/PropertyInfo.cs | 10 ++ .../PropertyInfoDictionary.cs | 21 ++- .../Resources.Designer.cs | 4 +- src/Json.Schema.ToDotNet/Resources.resx | 2 +- .../RewritingVisitorGenerator.cs | 6 +- .../SyntaxNodeExtensions.cs | 8 +- src/Json.Schema.ToDotNet/TypeGenerator.cs | 9 +- src/Json.Schema.UnitTests/JsonSchemaTests.cs | 84 +++++++++- src/Json.Schema/JsonSchema.cs | 17 +- 15 files changed, 394 insertions(+), 64 deletions(-) diff --git a/src/Json.Schema.ToDotNet.UnitTests/DataModelGeneratorTests.cs b/src/Json.Schema.ToDotNet.UnitTests/DataModelGeneratorTests.cs index 9a702e0b..b7c4104c 100644 --- a/src/Json.Schema.ToDotNet.UnitTests/DataModelGeneratorTests.cs +++ b/src/Json.Schema.ToDotNet.UnitTests/DataModelGeneratorTests.cs @@ -871,10 +871,43 @@ public void GeneratesCloningCode() ""type"": ""integer"", ""description"": ""An integer property."" }, + ""intPropWithDefault"": { + ""type"": ""integer"", + ""description"": ""An integer property with a default value."", + ""default"": 42 + }, + ""numberProp"": { + ""type"": ""number"", + ""description"": ""A number property."" + }, + ""numberPropWithDefault"": { + ""type"": ""number"", + ""description"": ""A number property with a default value."", + ""default"": 42.1 + }, ""stringProp"": { ""type"": ""string"", ""description"": ""A string property."" }, + ""stringPropWithDefault"": { + ""type"": ""string"", + ""description"": ""A string property with a default value."", + ""default"": ""42"" + }, + ""boolProp"": { + ""type"": ""boolean"", + ""description"": ""A Boolean property."" + }, + ""boolPropWithTrueDefault"": { + ""type"": ""boolean"", + ""description"": ""A Boolean property with a true default value."", + ""default"": true + }, + ""boolPropWithFalseDefault"": { + ""type"": ""boolean"", + ""description"": ""A Boolean property with a false default value."", + ""default"": false + }, ""arrayProp"": { ""type"": ""array"", ""description"": ""An array property."", @@ -1006,6 +1039,7 @@ public void GeneratesCloningCode() @"using System; using System.CodeDom.Compiler; using System.Collections.Generic; +using System.ComponentModel; using System.Runtime.Serialization; namespace N @@ -1031,12 +1065,59 @@ public SNodeKind SNodeKind [DataMember(Name = ""intProp"", IsRequired = false, EmitDefaultValue = false)] public int IntProp { get; set; } + /// + /// An integer property with a default value. + /// + [DataMember(Name = ""intPropWithDefault"", IsRequired = false, EmitDefaultValue = false)] + [DefaultValue(42)] + public int IntPropWithDefault { get; set; } + + /// + /// A number property. + /// + [DataMember(Name = ""numberProp"", IsRequired = false, EmitDefaultValue = false)] + public double NumberProp { get; set; } + + /// + /// A number property with a default value. + /// + [DataMember(Name = ""numberPropWithDefault"", IsRequired = false, EmitDefaultValue = false)] + [DefaultValue(42.1)] + public double NumberPropWithDefault { get; set; } + /// /// A string property. /// [DataMember(Name = ""stringProp"", IsRequired = false, EmitDefaultValue = false)] public string StringProp { get; set; } + /// + /// A string property with a default value. + /// + [DataMember(Name = ""stringPropWithDefault"", IsRequired = false, EmitDefaultValue = false)] + [DefaultValue(""42"")] + public string StringPropWithDefault { get; set; } + + /// + /// A Boolean property. + /// + [DataMember(Name = ""boolProp"", IsRequired = false, EmitDefaultValue = false)] + public bool BoolProp { get; set; } + + /// + /// A Boolean property with a true default value. + /// + [DataMember(Name = ""boolPropWithTrueDefault"", IsRequired = false, EmitDefaultValue = false)] + [DefaultValue(true)] + public bool BoolPropWithTrueDefault { get; set; } + + /// + /// A Boolean property with a false default value. + /// + [DataMember(Name = ""boolPropWithFalseDefault"", IsRequired = false, EmitDefaultValue = false)] + [DefaultValue(false)] + public bool BoolPropWithFalseDefault { get; set; } + /// /// An array property. /// @@ -1110,56 +1191,82 @@ public SNodeKind SNodeKind /// public C() { + IntPropWithDefault = 42; + NumberPropWithDefault = 42.1; + StringPropWithDefault = ""42""; + BoolPropWithTrueDefault = true; + BoolPropWithFalseDefault = false; } /// /// Initializes a new instance of the class from the supplied values. /// /// - /// An initialization value for the property. + /// An initialization value for the property. + /// + /// + /// An initialization value for the property. + /// + /// + /// An initialization value for the property. + /// + /// + /// An initialization value for the property. /// /// - /// An initialization value for the property. + /// An initialization value for the property. + /// + /// + /// An initialization value for the property. + /// + /// + /// An initialization value for the property. + /// + /// + /// An initialization value for the property. + /// + /// + /// An initialization value for the property. /// /// - /// An initialization value for the property. + /// An initialization value for the property. /// /// - /// An initialization value for the property. + /// An initialization value for the property. /// /// - /// An initialization value for the property. + /// An initialization value for the property. /// /// - /// An initialization value for the property. + /// An initialization value for the property. /// /// - /// An initialization value for the property. + /// An initialization value for the property. /// /// - /// An initialization value for the property. + /// An initialization value for the property. /// /// - /// An initialization value for the property. + /// An initialization value for the property. /// /// - /// An initialization value for the property. + /// An initialization value for the property. /// /// - /// An initialization value for the property. + /// An initialization value for the property. /// /// - /// An initialization value for the property. + /// An initialization value for the property. /// /// - /// An initialization value for the property. + /// An initialization value for the property. /// /// - /// An initialization value for the property. + /// An initialization value for the property. /// - public C(int intProp, string stringProp, IEnumerable arrayProp, Uri uriProp, DateTime dateTimeProp, D referencedTypeProp, IEnumerable arrayOfRefProp, IEnumerable> arrayOfArrayProp, IDictionary dictionaryProp, IDictionary dictionaryWithPrimitiveSchemaProp, IDictionary dictionaryWithObjectSchemaProp, IDictionary> dictionaryWithObjectArraySchemaProp, IDictionary dictionaryWithUriKeyProp, IDictionary dictionaryWithHintedValueProp) + public C(int intProp, int intPropWithDefault, double numberProp, double numberPropWithDefault, string stringProp, string stringPropWithDefault, bool boolProp, bool boolPropWithTrueDefault, bool boolPropWithFalseDefault, IEnumerable arrayProp, Uri uriProp, DateTime dateTimeProp, D referencedTypeProp, IEnumerable arrayOfRefProp, IEnumerable> arrayOfArrayProp, IDictionary dictionaryProp, IDictionary dictionaryWithPrimitiveSchemaProp, IDictionary dictionaryWithObjectSchemaProp, IDictionary> dictionaryWithObjectArraySchemaProp, IDictionary dictionaryWithUriKeyProp, IDictionary dictionaryWithHintedValueProp) { - Init(intProp, stringProp, arrayProp, uriProp, dateTimeProp, referencedTypeProp, arrayOfRefProp, arrayOfArrayProp, dictionaryProp, dictionaryWithPrimitiveSchemaProp, dictionaryWithObjectSchemaProp, dictionaryWithObjectArraySchemaProp, dictionaryWithUriKeyProp, dictionaryWithHintedValueProp); + Init(intProp, intPropWithDefault, numberProp, numberPropWithDefault, stringProp, stringPropWithDefault, boolProp, boolPropWithTrueDefault, boolPropWithFalseDefault, arrayProp, uriProp, dateTimeProp, referencedTypeProp, arrayOfRefProp, arrayOfArrayProp, dictionaryProp, dictionaryWithPrimitiveSchemaProp, dictionaryWithObjectSchemaProp, dictionaryWithObjectArraySchemaProp, dictionaryWithUriKeyProp, dictionaryWithHintedValueProp); } /// @@ -1178,7 +1285,7 @@ public C(C other) throw new ArgumentNullException(nameof(other)); } - Init(other.IntProp, other.StringProp, other.ArrayProp, other.UriProp, other.DateTimeProp, other.ReferencedTypeProp, other.ArrayOfRefProp, other.ArrayOfArrayProp, other.DictionaryProp, other.DictionaryWithPrimitiveSchemaProp, other.DictionaryWithObjectSchemaProp, other.DictionaryWithObjectArraySchemaProp, other.DictionaryWithUriKeyProp, other.DictionaryWithHintedValueProp); + Init(other.IntProp, other.IntPropWithDefault, other.NumberProp, other.NumberPropWithDefault, other.StringProp, other.StringPropWithDefault, other.BoolProp, other.BoolPropWithTrueDefault, other.BoolPropWithFalseDefault, other.ArrayProp, other.UriProp, other.DateTimeProp, other.ReferencedTypeProp, other.ArrayOfRefProp, other.ArrayOfArrayProp, other.DictionaryProp, other.DictionaryWithPrimitiveSchemaProp, other.DictionaryWithObjectSchemaProp, other.DictionaryWithObjectArraySchemaProp, other.DictionaryWithUriKeyProp, other.DictionaryWithHintedValueProp); } ISNode ISNode.DeepClone() @@ -1199,10 +1306,17 @@ private ISNode DeepCloneCore() return new C(this); } - private void Init(int intProp, string stringProp, IEnumerable arrayProp, Uri uriProp, DateTime dateTimeProp, D referencedTypeProp, IEnumerable arrayOfRefProp, IEnumerable> arrayOfArrayProp, IDictionary dictionaryProp, IDictionary dictionaryWithPrimitiveSchemaProp, IDictionary dictionaryWithObjectSchemaProp, IDictionary> dictionaryWithObjectArraySchemaProp, IDictionary dictionaryWithUriKeyProp, IDictionary dictionaryWithHintedValueProp) + private void Init(int intProp, int intPropWithDefault, double numberProp, double numberPropWithDefault, string stringProp, string stringPropWithDefault, bool boolProp, bool boolPropWithTrueDefault, bool boolPropWithFalseDefault, IEnumerable arrayProp, Uri uriProp, DateTime dateTimeProp, D referencedTypeProp, IEnumerable arrayOfRefProp, IEnumerable> arrayOfArrayProp, IDictionary dictionaryProp, IDictionary dictionaryWithPrimitiveSchemaProp, IDictionary dictionaryWithObjectSchemaProp, IDictionary> dictionaryWithObjectArraySchemaProp, IDictionary dictionaryWithUriKeyProp, IDictionary dictionaryWithHintedValueProp) { IntProp = intProp; + IntPropWithDefault = intPropWithDefault; + NumberProp = numberProp; + NumberPropWithDefault = numberPropWithDefault; StringProp = stringProp; + StringPropWithDefault = stringPropWithDefault; + BoolProp = boolProp; + BoolPropWithTrueDefault = boolPropWithTrueDefault; + BoolPropWithFalseDefault = boolPropWithFalseDefault; if (arrayProp != null) { var destination_0 = new List(); @@ -2450,7 +2564,7 @@ public C() /// Initializes a new instance of the class from the supplied values. /// /// - /// An initialization value for the property. + /// An initialization value for the property. /// public C(IEnumerable uriFormattedStrings) { diff --git a/src/Json.Schema.ToDotNet/ClassGenerator.cs b/src/Json.Schema.ToDotNet/ClassGenerator.cs index 8d209405..e67590f3 100644 --- a/src/Json.Schema.ToDotNet/ClassGenerator.cs +++ b/src/Json.Schema.ToDotNet/ClassGenerator.cs @@ -27,6 +27,9 @@ public class ClassGenerator : ClassOrInterfaceGenerator // Name used for the parameters of the copy ctor. private const string OtherParameterName = "other"; + private const string DefaultValueAttributeNamespaceName = "System.ComponentModel"; + private const string DefaultValueAttributeName = "DefaultValue"; + private const string DataContractAttributeName = "DataContract"; private const string DataMemberAttributeName = "DataMember"; private const string DataMemberNamePropertyName = "Name"; @@ -241,7 +244,7 @@ private MemberDeclarationSyntax GenerateValueGetHashCodeMethod() .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); } - protected override AttributeSyntax[] GeneratePropertyAttributes(string propertyName, string serializedName, bool isRequired) + protected override AttributeSyntax[] GeneratePropertyAttributes(string propertyName, string serializedName, bool isRequired, object defaultValue) { var attributes = new List(); @@ -279,6 +282,24 @@ protected override AttributeSyntax[] GeneratePropertyAttributes(string propertyN attributes.Add(dataMemberAttribute); + if (defaultValue != null) + { + AddUsing(DefaultValueAttributeNamespaceName); + + var defaultValueArguments = new List + { + SyntaxFactory.AttributeArgument(GetLiteralExpressionForValue(defaultValue)) + }; + + AttributeSyntax defaultValueAttribute = + SyntaxFactory.Attribute( + SyntaxFactory.IdentifierName(DefaultValueAttributeName), + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SeparatedList(defaultValueArguments))); + + attributes.Add(defaultValueAttribute); + } + string hintDictionaryKey = MakeHintDictionaryKey(propertyName); AttributeHint[] attributeHints = HintDictionary?.GetHints(hintDictionaryKey); if (attributeHints != null) @@ -372,7 +393,7 @@ private ConstructorDeclarationSyntax GenerateDefaultConstructor() { return SyntaxFactory.ConstructorDeclaration(SuffixedTypeName) .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) - .AddBodyStatements() + .AddBodyStatements(GenerateDefaultInitializations()) .WithLeadingTrivia( SyntaxHelper.MakeDocComment( string.Format( @@ -381,6 +402,95 @@ private ConstructorDeclarationSyntax GenerateDefaultConstructor() SuffixedTypeName))); } + /// + /// Generates an initialization statement for each property for which the + /// schema specifies a default value. + /// + /// + /// The resulting statements are inserted into the default constructor. + /// This ensures that the default values are set even if the object is + /// not the result of deserializing a JSON instance document. + /// + /// + /// An array containing one initialization statement for each property + /// for which the schema specifies a default value. + /// + private ExpressionStatementSyntax[] GenerateDefaultInitializations() + { + var initializations = new List(); + + foreach (string propertyName in PropInfoDictionary.GetPropertyNames()) + { + if (IncludeProperty(propertyName)) + { + PropertyInfo propInfo = PropInfoDictionary[propertyName]; + object defaultValue = propInfo.DefaultValue; + + ExpressionStatementSyntax initializationStatement = GenerateDefaultInitialization(propertyName, defaultValue); + if (initializationStatement != null) + { + initializations.Add(initializationStatement); + } + } + } + + return initializations.ToArray(); + } + + private ExpressionStatementSyntax GenerateDefaultInitialization( + string propertyName, + object defaultValue) + { + LiteralExpressionSyntax defaultValueExpression = GetLiteralExpressionForValue(defaultValue); + if (defaultValueExpression == null) { return null; } + + return SyntaxFactory.ExpressionStatement( + SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName(propertyName), + defaultValueExpression)); + } + + private LiteralExpressionSyntax GetLiteralExpressionForValue(object value) + { + LiteralExpressionSyntax literalExpression = null; + + if (value is bool) + { + SyntaxKind literalSyntaxKind = (bool)value == true + ? SyntaxKind.TrueLiteralExpression + : SyntaxKind.FalseLiteralExpression; + + literalExpression = SyntaxFactory.LiteralExpression(literalSyntaxKind); + } + else if (value is long) + { + literalExpression = SyntaxFactory.LiteralExpression( + SyntaxKind.NumericLiteralExpression, + SyntaxFactory.Literal((int)(long)value)); + // Note: the extra cast compensates for a mismatch between our code generation + // and Newtonsoft.Json's deserialization behavior. Newtonsoft deserializes + // integer properties as Int64 (long), but we generate integer properties + // with type int. The extra cast causes Roslyn to emit the literal 42, + // which can be assigned to an int, rather than 42L, which cannot. We should + // consider changing the code generation to emit longs for integer properties. + } + else if (value is double) + { + literalExpression = SyntaxFactory.LiteralExpression( + SyntaxKind.NumericLiteralExpression, + SyntaxFactory.Literal((double)value)); + } + else if (value is string) + { + literalExpression = SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal((string)value)); + } + + return literalExpression; + } + private ConstructorDeclarationSyntax GeneratePropertyCtor() { // Generate the argument list that will be passed from the copy ctor to the diff --git a/src/Json.Schema.ToDotNet/ClassOrInterfaceGenerator.cs b/src/Json.Schema.ToDotNet/ClassOrInterfaceGenerator.cs index 43733be4..5312eee7 100644 --- a/src/Json.Schema.ToDotNet/ClassOrInterfaceGenerator.cs +++ b/src/Json.Schema.ToDotNet/ClassOrInterfaceGenerator.cs @@ -25,7 +25,7 @@ public ClassOrInterfaceGenerator( PropInfoDictionary = propertyInfoDictionary; } - protected abstract AttributeSyntax[] GeneratePropertyAttributes(string propertyName, string serializedName, bool isRequired); + protected abstract AttributeSyntax[] GeneratePropertyAttributes(string propertyName, string serializedName, bool isRequired, object defaultValue); protected abstract SyntaxToken[] GeneratePropertyModifiers(string propertyName); @@ -103,7 +103,7 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(string propertyName) .AddModifiers(GeneratePropertyModifiers(propertyName)) .AddAccessorListAccessors(GeneratePropertyAccessors()); - AttributeSyntax[] attributes = GeneratePropertyAttributes(propertyName, info.SerializedName, info.IsRequired); + AttributeSyntax[] attributes = GeneratePropertyAttributes(propertyName, info.SerializedName, info.IsRequired, info.DefaultValue); if (attributes.Length > 0) { propDecl = propDecl.AddAttributeLists(attributes diff --git a/src/Json.Schema.ToDotNet/EqualityComparerGenerator.cs b/src/Json.Schema.ToDotNet/EqualityComparerGenerator.cs index 08253b43..842cb174 100644 --- a/src/Json.Schema.ToDotNet/EqualityComparerGenerator.cs +++ b/src/Json.Schema.ToDotNet/EqualityComparerGenerator.cs @@ -111,16 +111,20 @@ internal string Generate(string className, PropertyInfoDictionary propertyInfoDi GenerateEqualsMethod(), GenerateGetHashCodeMethod()); - var usings = new List + var usings = new HashSet { "System", // For Object. "System.Collections.Generic" // For IEqualityComparer }; - usings.AddRange(_propertyInfoDictionary + IEnumerable namespaceNames = _propertyInfoDictionary .Values .Select(propertyInfo => propertyInfo.NamespaceName) - .Where(namespaceName => !string.IsNullOrWhiteSpace(namespaceName))); + .Where(namespaceName => !string.IsNullOrWhiteSpace(namespaceName)); + foreach (string namespaceName in namespaceNames) + { + usings.Add(namespaceName); + } return classDeclaration.Format( _copyrightNotice, diff --git a/src/Json.Schema.ToDotNet/InterfaceGenerator.cs b/src/Json.Schema.ToDotNet/InterfaceGenerator.cs index 589ffbf4..7a3e5814 100644 --- a/src/Json.Schema.ToDotNet/InterfaceGenerator.cs +++ b/src/Json.Schema.ToDotNet/InterfaceGenerator.cs @@ -41,7 +41,7 @@ public override void AddMembers() .AddMembers(GenerateProperties()); } - protected override AttributeSyntax[] GeneratePropertyAttributes(string propertyName, string serializedName, bool isRequired) + protected override AttributeSyntax[] GeneratePropertyAttributes(string propertyName, string serializedName, bool isRequired, object defaultValue) { return new AttributeSyntax[0]; } diff --git a/src/Json.Schema.ToDotNet/Json.Schema.ToDotNet.csproj b/src/Json.Schema.ToDotNet/Json.Schema.ToDotNet.csproj index 0ff38bee..8d6dc6b8 100644 --- a/src/Json.Schema.ToDotNet/Json.Schema.ToDotNet.csproj +++ b/src/Json.Schema.ToDotNet/Json.Schema.ToDotNet.csproj @@ -16,6 +16,21 @@ + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/src/Json.Schema.ToDotNet/PropertyInfo.cs b/src/Json.Schema.ToDotNet/PropertyInfo.cs index 1ab0e617..e67ccb07 100644 --- a/src/Json.Schema.ToDotNet/PropertyInfo.cs +++ b/src/Json.Schema.ToDotNet/PropertyInfo.cs @@ -42,6 +42,9 @@ public class PropertyInfo /// true if this property is required by the schema; /// otherwise false. /// + /// + /// The default value, if any, specified by the schema; otherwise null. + /// /// /// true if this property is of a type defined by the schema (or an; /// array of a schema-defined type otherwise false. @@ -61,6 +64,7 @@ public PropertyInfo( TypeSyntax type, string namespaceName, bool isRequired, + object defaultValue, bool isOfSchemaDefinedType, int arrayRank, int declarationOrder) @@ -74,6 +78,7 @@ public PropertyInfo( TypeName = type.ToString(); NamespaceName = namespaceName; IsRequired = isRequired; + DefaultValue = defaultValue; IsOfSchemaDefinedType = isOfSchemaDefinedType; ArrayRank = arrayRank; DeclarationOrder = declarationOrder; @@ -131,6 +136,11 @@ public PropertyInfo( /// public bool IsRequired { get; } + /// + /// Gets this property's default value, if the schema specifies one; otherwise null. + /// + public object DefaultValue; + /// /// Gets a value indicating whether this property is of a type defined by the schema. /// diff --git a/src/Json.Schema.ToDotNet/PropertyInfoDictionary.cs b/src/Json.Schema.ToDotNet/PropertyInfoDictionary.cs index aa98e83f..cf74dcf5 100644 --- a/src/Json.Schema.ToDotNet/PropertyInfoDictionary.cs +++ b/src/Json.Schema.ToDotNet/PropertyInfoDictionary.cs @@ -10,7 +10,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Json.Schema.ToDotNet.Hints; -using Newtonsoft.Json.Linq; namespace Microsoft.Json.Schema.ToDotNet { @@ -57,8 +56,8 @@ public class PropertyInfoDictionary : IReadOnlyDictionary /// public delegate void AdditionalTypeRequiredDelegate(AdditionalTypeRequiredInfo additionalTypeRequiredInfo); - private AdditionalTypeRequiredDelegate _additionalTypeRequiredDelegate; - private string _typeNameSuffix; + private readonly AdditionalTypeRequiredDelegate _additionalTypeRequiredDelegate; + private readonly string _typeNameSuffix; /// /// Initializes a new instance of the class. @@ -143,8 +142,7 @@ public static string MakeDictionaryItemKeyName(string propertyName) public static SyntaxKind GetTypeKeywordFromSchemaType(SchemaType type) { - SyntaxKind typeKeyword; - if (!s_SchemaTypeToSyntaxKindDictionary.TryGetValue(type, out typeKeyword)) + if (!s_SchemaTypeToSyntaxKindDictionary.TryGetValue(type, out SyntaxKind typeKeyword)) { typeKeyword = SyntaxKind.ObjectKeyword; } @@ -158,8 +156,7 @@ public PropertyInfo this[string key] { get { - PropertyInfo info; - if (!TryGetValue(key, out info)) + if (!TryGetValue(key, out PropertyInfo info)) { throw new ApplicationException($"The schema does not contain information describing the property or element {key}."); } @@ -229,8 +226,6 @@ private void AddPropertyInfoFromPropertySchema( string referencedEnumTypeName; bool isOfSchemaDefinedType = false; int arrayRank = 0; - EnumHint enumHint; - DictionaryHint dictionaryHint; if (propertySchema.IsDateTime()) { @@ -246,7 +241,7 @@ private void AddPropertyInfoFromPropertySchema( initializationKind = InitializationKind.Uri; type = MakeNamedType("System.Uri", out namespaceName); } - else if (propertySchema.ShouldBeDictionary(_typeName, schemaPropertyName, _hintDictionary, out dictionaryHint)) + else if (propertySchema.ShouldBeDictionary(_typeName, schemaPropertyName, _hintDictionary, out DictionaryHint dictionaryHint)) { comparisonKind = ComparisonKind.Dictionary; hashKind = HashKind.Dictionary; @@ -271,7 +266,7 @@ private void AddPropertyInfoFromPropertySchema( initializationKind = InitializationKind.SimpleAssign; type = MakeNamedType(referencedEnumTypeName, out namespaceName); } - else if (propertySchema.ShouldBeEnum(_typeName, schemaPropertyName, _hintDictionary, out enumHint)) + else if (propertySchema.ShouldBeEnum(_typeName, schemaPropertyName, _hintDictionary, out EnumHint enumHint)) { comparisonKind = ComparisonKind.OperatorEquals; hashKind = HashKind.ScalarValueType; @@ -284,7 +279,7 @@ private void AddPropertyInfoFromPropertySchema( OnAdditionalTypeRequired(enumHint, propertySchema); } else - { + { SchemaType propertyType = propertySchema.SafeGetType(); switch (propertyType) @@ -384,6 +379,7 @@ private void AddPropertyInfoFromPropertySchema( type, namespaceName, isRequired, + propertySchema.Default, isOfSchemaDefinedType, arrayRank, entries.Count))); @@ -477,6 +473,7 @@ private TypeSyntax MakeDictionaryType( type: valueType, namespaceName: dictionaryHint.NamespaceName, isRequired: true, + defaultValue: null, isOfSchemaDefinedType: false, arrayRank: 0, declarationOrder: 0))); diff --git a/src/Json.Schema.ToDotNet/Resources.Designer.cs b/src/Json.Schema.ToDotNet/Resources.Designer.cs index f1f0643c..01bc443b 100644 --- a/src/Json.Schema.ToDotNet/Resources.Designer.cs +++ b/src/Json.Schema.ToDotNet/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.Json.Schema.ToDotNet { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -214,7 +214,7 @@ internal static string KindEnumNoneDescription { } /// - /// Looks up a localized string similar to An initialization value for the <see cref="P: {0}" /> property.. + /// Looks up a localized string similar to An initialization value for the <see cref="P:{0}" /> property.. /// internal static string PropertyCtorParamDescription { get { diff --git a/src/Json.Schema.ToDotNet/Resources.resx b/src/Json.Schema.ToDotNet/Resources.resx index 44f5acbe..fffa6fac 100644 --- a/src/Json.Schema.ToDotNet/Resources.resx +++ b/src/Json.Schema.ToDotNet/Resources.resx @@ -169,7 +169,7 @@ An uninitialized kind. - An initialization value for the <see cref="P: {0}" /> property. + An initialization value for the <see cref="P:{0}" /> property. Initializes a new instance of the <see cref="{0}" /> class from the supplied values. diff --git a/src/Json.Schema.ToDotNet/RewritingVisitorGenerator.cs b/src/Json.Schema.ToDotNet/RewritingVisitorGenerator.cs index 99b73d2e..3eb94e56 100644 --- a/src/Json.Schema.ToDotNet/RewritingVisitorGenerator.cs +++ b/src/Json.Schema.ToDotNet/RewritingVisitorGenerator.cs @@ -79,7 +79,7 @@ internal string GenerateRewritingVisitor() .AddMembers( GenerateVisitClassMethods()); - var usings = new List { "System", "System.Collections.Generic", "System.Linq" }; + var usings = new HashSet { "System", "System.Collections.Generic", "System.Linq" }; string summaryComment = string.Format( CultureInfo.CurrentCulture, @@ -356,9 +356,7 @@ private StatementSyntax[] GeneratePropertyVisits(string className) continue; } - int arrayRank = 0; - bool isDictionary = false; - string propertyName = propertyNameWithRank.BasePropertyName(out arrayRank, out isDictionary); + string propertyName = propertyNameWithRank.BasePropertyName(out int arrayRank, out bool isDictionary); TypeSyntax collectionType = propertyInfoDictionary.GetConcreteListType(propertyName); TypeSyntax elementType = propertyInfoDictionary[propertyNameWithRank].Type; diff --git a/src/Json.Schema.ToDotNet/SyntaxNodeExtensions.cs b/src/Json.Schema.ToDotNet/SyntaxNodeExtensions.cs index 2176810d..f46481d6 100644 --- a/src/Json.Schema.ToDotNet/SyntaxNodeExtensions.cs +++ b/src/Json.Schema.ToDotNet/SyntaxNodeExtensions.cs @@ -54,7 +54,7 @@ internal static class SyntaxNodeExtensions internal static string Format( this BaseTypeDeclarationSyntax typeDecl, string copyrightNotice, - List usings, + HashSet usings, string namespaceName, string summaryComment) { @@ -74,15 +74,11 @@ internal static string Format( CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit() .AddMembers(namespaceDecl); - if (usings == null) - { - usings = new List(); - } + usings = usings ?? new HashSet(); usings.Add("System.CodeDom.Compiler"); // For GeneratedCodeAttribute UsingDirectiveSyntax[] usingDirectives = usings - .Distinct() .OrderBy(u => u, UsingComparer.Instance) .Select(u => SyntaxFactory.UsingDirective(MakeQualifiedName(u))) .ToArray(); diff --git a/src/Json.Schema.ToDotNet/TypeGenerator.cs b/src/Json.Schema.ToDotNet/TypeGenerator.cs index af93ece6..4522aff8 100644 --- a/src/Json.Schema.ToDotNet/TypeGenerator.cs +++ b/src/Json.Schema.ToDotNet/TypeGenerator.cs @@ -9,7 +9,7 @@ namespace Microsoft.Json.Schema.ToDotNet { public abstract class TypeGenerator { - private string _typeNameSuffix; + private readonly string _typeNameSuffix; protected TypeGenerator( JsonSchema schema, @@ -37,7 +37,7 @@ public string SuffixedTypeName /// protected BaseTypeDeclarationSyntax TypeDeclaration { get; set; } - protected List Usings { get; private set; } + protected HashSet Usings { get; private set; } public abstract BaseTypeDeclarationSyntax GenerateTypeDeclaration(); @@ -73,10 +73,7 @@ public string Generate(string namespaceName, string typeName, string copyrightNo protected void AddUsing(string namespaceName) { - if (Usings == null) - { - Usings = new List(); - } + Usings = Usings ?? new HashSet(); Usings.Add(namespaceName); } diff --git a/src/Json.Schema.UnitTests/JsonSchemaTests.cs b/src/Json.Schema.UnitTests/JsonSchemaTests.cs index 8e389934..72cb33de 100644 --- a/src/Json.Schema.UnitTests/JsonSchemaTests.cs +++ b/src/Json.Schema.UnitTests/JsonSchemaTests.cs @@ -73,7 +73,8 @@ public class JsonSchemaTests ""uniqueItems"": true, ""format"": ""date-time"", ""maximimum"": 2, - ""exclusiveMaximum"": false + ""exclusiveMaximum"": false, + ""default"": 2, }", @"{ ""id"": ""http://x/y#"", @@ -128,7 +129,8 @@ public class JsonSchemaTests ""uniqueItems"": true, ""format"": ""date-time"", ""maximimum"": 2, - ""exclusiveMaximum"": false + ""exclusiveMaximum"": false, + ""default"": 2 }", true ), @@ -773,6 +775,84 @@ public class JsonSchemaTests }", false ), + + new EqualityTestCase( + "Same integer defaults", + @"{ + ""default"": 2 + }", + @"{ + ""default"": 2 + }", + true), + + new EqualityTestCase( + "Different integer defaults", + @"{ + ""default"": 2 + }", + @"{ + ""default"": 3 + }", + false), + + new EqualityTestCase( + "Same string defaults", + @"{ + ""default"": ""2"" + }", + @"{ + ""default"": ""2"" + }", + true), + + new EqualityTestCase( + "Different string defaults", + @"{ + ""default"": ""2"" + }", + @"{ + ""default"": ""3"" + }", + false), + + new EqualityTestCase( + "Same Boolean defaults", + @"{ + ""default"": true + }", + @"{ + ""default"": true + }", + true), + + new EqualityTestCase( + "Different Boolean defaults", + @"{ + ""default"": false + }", + @"{ + ""default"": true + }", + false), + + new EqualityTestCase( + "Different default types", + @"{ + ""default"": 2 + }", + @"{ + ""default"": ""2"" + }", + false), + + new EqualityTestCase( + "Present and missing defaults", + @"{ + ""default"": 2 + }", + @"{}", + false) }; [Theory(DisplayName = "JsonSchema equality")] diff --git a/src/Json.Schema/JsonSchema.cs b/src/Json.Schema/JsonSchema.cs index 392d9e55..1a47aa5b 100644 --- a/src/Json.Schema/JsonSchema.cs +++ b/src/Json.Schema/JsonSchema.cs @@ -102,6 +102,7 @@ public JsonSchema(JsonSchema other) PatternProperties = new Dictionary(other.PatternProperties); } + Default = other.Default; Pattern = other.Pattern; MaxLength = other.MaxLength; MinLength = other.MinLength; @@ -266,6 +267,12 @@ public JsonSchema(JsonSchema other) /// public int? MinLength { get; set; } + /// + /// Gets or sets the value the property should be taken to have if it is absent + /// from an instance document. + /// + public object Default { get; set; } + /// /// Gets or sets a regular expression which a string schema instance must match. /// @@ -520,8 +527,7 @@ private static JsonSchema Collapse(JsonSchema schema, JsonSchema rootSchema) string definitionName = schema.Reference.GetDefinitionName(); - JsonSchema referencedSchema; - if (rootSchema.Definitions == null || !rootSchema.Definitions.TryGetValue(definitionName, out referencedSchema)) + if (rootSchema.Definitions == null || !rootSchema.Definitions.TryGetValue(definitionName, out JsonSchema referencedSchema)) { throw Error.CreateException( Resources.ErrorDefinitionDoesNotExist, @@ -547,6 +553,7 @@ private static JsonSchema Collapse(JsonSchema schema, JsonSchema rootSchema) collapsedSchema.Items.SingleSchema = referencedSchema.Items.SingleSchema; } + collapsedSchema.Default = referencedSchema.Default; collapsedSchema.Pattern = referencedSchema.Pattern; collapsedSchema.MaxLength = referencedSchema.MaxLength; collapsedSchema.MinLength = referencedSchema.MinLength; @@ -583,6 +590,7 @@ public override int GetHashCode() Required, Definitions, Reference, + Default, Pattern, MaxLength, MinLength, @@ -610,7 +618,7 @@ public override int GetHashCode() public bool Equals(JsonSchema other) { - if ((object)other == null) + if (other is null) { return false; } @@ -646,6 +654,7 @@ public bool Equals(JsonSchema other) && (Reference == null ? other.Reference == null : Reference.Equals(other.Reference)) + && Object.Equals(Default, other.Default) && Pattern == other.Pattern && MaxLength == other.MaxLength && MinLength == other.MinLength @@ -679,7 +688,7 @@ public bool Equals(JsonSchema other) return true; } - if ((object)left == null) + if (left is null) { return false; }