From 329cbd3ba3e93554f8b85c07623bee5e15e2f74a Mon Sep 17 00:00:00 2001 From: Girts Lemanis Date: Mon, 15 Oct 2018 17:27:57 +0300 Subject: [PATCH] More fine-grained mapping for xs:integer and derived types --- README.md | 22 ++- XmlSchemaClassGenerator.Tests/XmlTests.cs | 177 ++++++++++++++++++++-- XmlSchemaClassGenerator.sln | 7 +- XmlSchemaClassGenerator/CodeUtilities.cs | 107 ++++++++++--- XmlSchemaClassGenerator/ModelBuilder.cs | 2 +- XmlSchemaClassGenerator/TypeModel.cs | 3 +- 6 files changed, 280 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index a3a137d2..f71d2356 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Features from schema restrictions * Use [`Collection`](http://msdn.microsoft.com/en-us/library/ms132397.aspx) properties (initialized in constructor and with private setter) -* Use either int, long, decimal, or string for xs:integer and derived types +* Map xs:integer and derived types to the closest possible .NET type, if not possible - fall back to string. Can be overriden by explicitly defined type (int, long, or decimal) * Automatic properties * Pascal case for classes and properties * Generate nullable adapter properties for optional elements and attributes without default values (see [below](#nullables)) @@ -255,6 +255,26 @@ Collection types Values for the `--collectionType` and `--collectionImplementationType` options have to be given in the format accepted by the [`Type.GetType()`](https://docs.microsoft.com/en-us/dotnet/api/system.type.gettype) method. For the `System.Collections.Generic.List` class this means ``System.Collections.Generic.List`1``. +Integer and derived types +--------------------- +Not all numeric types defined by XML Schema can be safely and accurately mapped to .NET numeric data types, however, it's possible to approximate the mapping based on the integer bounds and restrictions such as `totalDigits`. +If an explicit integer type mapping is specified via `--integer=TYPE`, that type will be used, otherwise an approximation will be made based on the following table: + +| XML Schema type | totalDigits | C# type| +|-----------------|-------------|---------| +| xs:positiveInteger, xs:nonNegativeInteger| <3 | byte | +| xs:positiveInteger, xs:nonNegativeInteger| <5 | ushort | +| xs:positiveInteger, xs:nonNegativeInteger| <10 | uint | +| xs:positiveInteger, xs:nonNegativeInteger| <20 | ulong | +| xs:positiveInteger, xs:nonNegativeInteger| <30 | decimal | +| xs:positiveInteger, xs:nonNegativeInteger| >=30 | string | +| xs:integer, xs:nonPositiveInteger, xs:negativeInteger| <3 | sbyte | +| xs:integer, xs:nonPositiveInteger, xs:negativeInteger| <5 | short | +| xs:integer, xs:nonPositiveInteger, xs:negativeInteger| <10 | int | +| xs:integer, xs:nonPositiveInteger, xs:negativeInteger| <19 | long | +| xs:integer, xs:nonPositiveInteger, xs:negativeInteger| <29 | decimal | +| xs:integer, xs:nonPositiveInteger, xs:negativeInteger| >=29 | string | + Contributing ------------ diff --git a/XmlSchemaClassGenerator.Tests/XmlTests.cs b/XmlSchemaClassGenerator.Tests/XmlTests.cs index c67b332d..ac323f62 100644 --- a/XmlSchemaClassGenerator.Tests/XmlTests.cs +++ b/XmlSchemaClassGenerator.Tests/XmlTests.cs @@ -1,7 +1,4 @@ -using Ganss.IO; -using Microsoft.CodeAnalysis; -using Microsoft.Xml.XMLGen; -using System; +using System; using System.CodeDom; using System.Collections.Generic; using System.IO; @@ -12,6 +9,9 @@ using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; +using Ganss.IO; +using Microsoft.CodeAnalysis; +using Microsoft.Xml.XMLGen; using Xunit; namespace XmlSchemaClassGenerator.Tests @@ -347,7 +347,7 @@ public void DontGenerateElementForEmptyCollectionInChoice() public void EditorBrowsableAttributeRespectsCodeTypeReferenceOptions(CodeTypeReferenceOptions codeTypeReferenceOptions, string expectedLine) { const string xsd = @" - + @@ -358,7 +358,7 @@ public void EditorBrowsableAttributeRespectsCodeTypeReferenceOptions(CodeTypeRef - + "; var generatedType = ConvertXml(nameof(EditorBrowsableAttributeRespectsCodeTypeReferenceOptions), xsd, new Generator @@ -381,12 +381,12 @@ public void EditorBrowsableAttributeRespectsCodeTypeReferenceOptions(CodeTypeRef public void MixedTypeMustNotCollideWithExistingMembers() { const string xsd = @" - - + + - + "; var generatedType = ConvertXml(nameof(MixedTypeMustNotCollideWithExistingMembers), xsd, new Generator @@ -406,11 +406,11 @@ public void MixedTypeMustNotCollideWithExistingMembers() public void MixedTypeMustNotCollideWithContainingTypeName() { const string xsd = @" - - + + - - + + "; var generatedType = ConvertXml(nameof(MixedTypeMustNotCollideWithExistingMembers), xsd, new Generator @@ -431,7 +431,7 @@ public void MixedTypeMustNotCollideWithContainingTypeName() public void CollidingAttributeAndPropertyNamesCanBeResolved(params string[] files) { // Compilation would previously throw due to duplicate type name within type - var assembly = Compiler.GenerateFiles("AttributesWithSameName", files); + var assembly = Compiler.GenerateFiles("AttributesWithSameName", files); Assert.NotNull(assembly); } @@ -587,5 +587,154 @@ private static void CompareOutput(string expected, string actual) string Normalize(string input) => Regex.Replace(input, @"[ \t]*\r\n", "\n"); Assert.Equal(Normalize(expected), Normalize(actual)); } + + + [Theory] + [InlineData(typeof(decimal), "decimal")] + [InlineData(typeof(long), "long")] + [InlineData(null, "string")] + public void UnmappedIntegerDerivedTypesAreMappedToExpectedCSharpType(Type integerDataType, string expectedTypeName) + { + const string xsd = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + + var generatedType = ConvertXml(nameof(UnmappedIntegerDerivedTypesAreMappedToExpectedCSharpType), xsd, new Generator + { + NamespaceProvider = new NamespaceProvider + { + GenerateNamespace = key => "Test", + }, + GenerateNullables = true, + NamingScheme = NamingScheme.PascalCase, + IntegerDataType = integerDataType, + }).First(); + + Assert.Contains($"public {expectedTypeName} UnboundedInteger01", generatedType); + Assert.Contains($"public {expectedTypeName} UnboundedInteger02", generatedType); + Assert.Contains($"public {expectedTypeName} UnboundedInteger03", generatedType); + Assert.Contains($"public {expectedTypeName} UnboundedInteger04", generatedType); + Assert.Contains($"public {expectedTypeName} UnboundedInteger05", generatedType); + Assert.Contains($"public {expectedTypeName} OutOfBoundsInteger01", generatedType); + Assert.Contains($"public {expectedTypeName} OutOfBoundsInteger02", generatedType); + Assert.Contains($"public {expectedTypeName} OutOfBoundsInteger03", generatedType); + Assert.Contains($"public {expectedTypeName} OutOfBoundsInteger04", generatedType); + Assert.Contains($"public {expectedTypeName} OutOfBoundsInteger05", generatedType); + } + + [Theory] + [InlineData("xs:positiveInteger", 1, 2, "byte")] + [InlineData("xs:nonNegativeInteger", 1, 2, "byte")] + [InlineData("xs:integer", 1, 2, "sbyte")] + [InlineData("xs:negativeInteger", 1, 2, "sbyte")] + [InlineData("xs:nonPositiveInteger", 1, 2, "sbyte")] + [InlineData("xs:positiveInteger", 3, 4, "ushort")] + [InlineData("xs:nonNegativeInteger", 3, 4, "ushort")] + [InlineData("xs:integer", 3, 4, "short")] + [InlineData("xs:negativeInteger", 3, 4, "short")] + [InlineData("xs:nonPositiveInteger", 3, 4, "short")] + [InlineData("xs:positiveInteger", 5, 9, "uint")] + [InlineData("xs:nonNegativeInteger", 5, 9, "uint")] + [InlineData("xs:integer", 5, 9, "int")] + [InlineData("xs:negativeInteger", 5, 9, "int")] + [InlineData("xs:nonPositiveInteger", 5, 9, "int")] + [InlineData("xs:positiveInteger", 10, 19, "ulong")] + [InlineData("xs:nonNegativeInteger", 10, 19, "ulong")] + [InlineData("xs:integer", 10, 18, "long")] + [InlineData("xs:negativeInteger", 10, 18, "long")] + [InlineData("xs:nonPositiveInteger", 10, 18, "long")] + [InlineData("xs:positiveInteger", 20, 29, "decimal")] + [InlineData("xs:nonNegativeInteger", 20, 29, "decimal")] + [InlineData("xs:integer", 20, 28, "decimal")] + [InlineData("xs:negativeInteger", 20, 28, "decimal")] + [InlineData("xs:nonPositiveInteger", 20, 28, "decimal")] + public void RestrictedIntegerDerivedTypesAreMappedToExpectedCSharpTypes(string restrictionBase, int totalDigitsRangeFrom, int totalDigitsRangeTo, string expectedTypeName) + { + const string xsdTemplate = @" + + + + {0} + + + + {1} +"; + + const string elementTemplate = @""; + + const string simpleTypeTemplate = @" + + + + + +"; + + string elementDefinitions = "", simpleTypeDefinitions = ""; + for (var i = totalDigitsRangeFrom; i <= totalDigitsRangeTo; i++) + { + elementDefinitions += string.Format(elementTemplate, i); + simpleTypeDefinitions += string.Format(simpleTypeTemplate, restrictionBase, i); + } + + var xsd = string.Format(xsdTemplate, elementDefinitions, simpleTypeDefinitions); + var generatedType = ConvertXml(nameof(RestrictedIntegerDerivedTypesAreMappedToExpectedCSharpTypes), xsd, + new Generator + { + NamespaceProvider = new NamespaceProvider + { + GenerateNamespace = key => "Test", + }, + GenerateNullables = true, + NamingScheme = NamingScheme.PascalCase, + }).First(); + + for (var i = totalDigitsRangeFrom; i <= totalDigitsRangeTo; i++) + { + Assert.Contains($"public {expectedTypeName} RestrictedInteger{i}", generatedType); + } + } } } diff --git a/XmlSchemaClassGenerator.sln b/XmlSchemaClassGenerator.sln index 8744d63f..4eeb8618 100644 --- a/XmlSchemaClassGenerator.sln +++ b/XmlSchemaClassGenerator.sln @@ -11,7 +11,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XmlSampleGenerator", "XmlSa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XmlSchemaClassGenerator.Console", "XmlSchemaClassGenerator.Console\XmlSchemaClassGenerator.Console.csproj", "{F0000FE1-DE27-4BF9-A179-FC9643A57EEE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "xscgen", "xscgen\xscgen.csproj", "{C5C1FF7F-31AD-4D4F-81F3-C9F54516D9D0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xscgen", "xscgen\xscgen.csproj", "{C5C1FF7F-31AD-4D4F-81F3-C9F54516D9D0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6ED7BE60-B3EC-4CBA-8732-09DA45AC14BF}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/XmlSchemaClassGenerator/CodeUtilities.cs b/XmlSchemaClassGenerator/CodeUtilities.cs index 6e0237b8..43a4893a 100644 --- a/XmlSchemaClassGenerator/CodeUtilities.cs +++ b/XmlSchemaClassGenerator/CodeUtilities.cs @@ -12,6 +12,12 @@ public static class CodeUtilities // Match non-letter followed by letter static Regex PascalCaseRegex = new Regex(@"[^\p{L}]\p{L}", RegexOptions.Compiled); + private static readonly XmlTypeCode[] IntegerDerivedTypeCodes = + { + XmlTypeCode.Integer, XmlTypeCode.NegativeInteger, XmlTypeCode.NonNegativeInteger, + XmlTypeCode.NonPositiveInteger, XmlTypeCode.PositiveInteger + }; + // Uppercases first letter and all letters following non-letters. // Examples: testcase -> Testcase, html5element -> Html5Element, test_case -> Test_Case public static string ToPascalCase(this string s) @@ -76,7 +82,78 @@ public static string ToBackingField(this string propertyName, bool doNotUseUnder return IsDataTypeAttributeAllowed(type.TypeCode, configuration); } - private static Type GetEffectiveType(XmlTypeCode typeCode, XmlSchemaDatatypeVariety variety, GeneratorConfiguration configuration) + private static Type GetIntegerDerivedType(XmlSchemaDatatype type, GeneratorConfiguration configuration, + IEnumerable restrictions) + { + if (configuration.IntegerDataType != null) return configuration.IntegerDataType; + + var xmlTypeCode = type.TypeCode; + + Type result = null; + + if (!(restrictions.SingleOrDefault(r => r is TotalDigitsRestrictionModel) is TotalDigitsRestrictionModel totalDigits) + || ((xmlTypeCode == XmlTypeCode.PositiveInteger + || xmlTypeCode == XmlTypeCode.NonNegativeInteger) && totalDigits.Value >= 30) + || ((xmlTypeCode == XmlTypeCode.Integer + || xmlTypeCode == XmlTypeCode.NegativeInteger + || xmlTypeCode == XmlTypeCode.NonPositiveInteger) && totalDigits.Value >= 29)) + { + return typeof(string); + } + + switch (xmlTypeCode) + { + case XmlTypeCode.PositiveInteger: + case XmlTypeCode.NonNegativeInteger: + switch (totalDigits.Value) + { + case int n when (n < 3): + result = typeof(byte); + break; + case int n when (n < 5): + result = typeof(ushort); + break; + case int n when (n < 10): + result = typeof(uint); + break; + case int n when (n < 20): + result = typeof(ulong); + break; + case int n when (n < 30): + result = typeof(decimal); + break; + } + + break; + + case XmlTypeCode.Integer: + case XmlTypeCode.NegativeInteger: + case XmlTypeCode.NonPositiveInteger: + switch (totalDigits.Value) + { + case int n when (n < 3): + result = typeof(sbyte); + break; + case int n when (n < 5): + result = typeof(short); + break; + case int n when (n < 10): + result = typeof(int); + break; + case int n when (n < 19): + result = typeof(long); + break; + case int n when (n < 29): + result = typeof(decimal); + break; + } + break; + } + + return result; + } + + private static Type GetEffectiveType(XmlTypeCode typeCode, XmlSchemaDatatypeVariety variety) { Type result; switch (typeCode) @@ -100,20 +177,6 @@ private static Type GetEffectiveType(XmlTypeCode typeCode, XmlSchemaDatatypeVari case XmlTypeCode.Idref: result = typeof(string); break; - case XmlTypeCode.Integer: - case XmlTypeCode.NegativeInteger: - case XmlTypeCode.NonNegativeInteger: - case XmlTypeCode.NonPositiveInteger: - case XmlTypeCode.PositiveInteger: - if (configuration.IntegerDataType == null || configuration.IntegerDataType == typeof(string)) - { - result = typeof(string); - } - else - { - result = configuration.IntegerDataType; - } - break; default: result = null; break; @@ -121,14 +184,20 @@ private static Type GetEffectiveType(XmlTypeCode typeCode, XmlSchemaDatatypeVari return result; } - public static Type GetEffectiveType(this XmlSchemaDatatype type, GeneratorConfiguration configuration) + public static Type GetEffectiveType(this XmlSchemaDatatype type, GeneratorConfiguration configuration, + IEnumerable restrictions) { - return GetEffectiveType(type.TypeCode, type.Variety, configuration) ?? type.ValueType; + var xmlTypeCode = type.TypeCode; + + return IntegerDerivedTypeCodes.Contains(xmlTypeCode) + ? GetIntegerDerivedType(type, configuration, restrictions) ?? type.ValueType + : GetEffectiveType(xmlTypeCode, type.Variety) ?? type.ValueType; } - public static Type GetEffectiveType(this XmlSchemaType type, GeneratorConfiguration configuration) + public static Type GetEffectiveType(this XmlSchemaType type, GeneratorConfiguration configuration, + IEnumerable restrictions) { - return GetEffectiveType(type.TypeCode, type.Datatype.Variety, configuration) ?? type.Datatype.ValueType; + return GetEffectiveType(type.Datatype, configuration, restrictions); } public static XmlQualifiedName GetQualifiedName(this XmlSchemaType schemaType) diff --git a/XmlSchemaClassGenerator/ModelBuilder.cs b/XmlSchemaClassGenerator/ModelBuilder.cs index 2afc7a68..e8c69ba2 100644 --- a/XmlSchemaClassGenerator/ModelBuilder.cs +++ b/XmlSchemaClassGenerator/ModelBuilder.cs @@ -377,7 +377,7 @@ private TypeModel CreateTypeModel(XmlSchemaSimpleType simpleType, NamespaceModel Namespace = namespaceModel, XmlSchemaName = qualifiedName, XmlSchemaType = simpleType, - ValueType = simpleType.Datatype.GetEffectiveType(_configuration), + ValueType = simpleType.Datatype.GetEffectiveType(_configuration, restrictions), }; simpleModel.Documentation.AddRange(docs); diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index e45ce3f1..9aefb816 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -7,7 +7,6 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics; using System.Linq; -using System.Reflection; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Schema; @@ -1190,7 +1189,7 @@ public override CodeTypeReference GetReferenceFor(NamespaceModel referencingName // http://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlelementattribute.datatype(v=vs.110).aspx // XmlSerializer is inconsistent: maps xs:decimal to decimal but xs:integer to string, // even though xs:integer is a restriction of xs:decimal - type = XmlSchemaType.GetEffectiveType(Configuration); + type = XmlSchemaType.GetEffectiveType(Configuration, Restrictions); UseDataTypeAttribute = XmlSchemaType.IsDataTypeAttributeAllowed(Configuration) ?? UseDataTypeAttribute; }