From 3bbbd0694524e5fe8096e3186780b36414fc997a Mon Sep 17 00:00:00 2001 From: John Welch Date: Thu, 4 Oct 2018 09:52:22 +0100 Subject: [PATCH 1/7] Emit XmlElementAttribute for substitutionGroups with an abstract head. See https://github.com/mganss/XmlSchemaClassGenerator/issues/36#issuecomment-426937573 --- XmlSchemaClassGenerator/ModelBuilder.cs | 5 +++++ XmlSchemaClassGenerator/TypeModel.cs | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/XmlSchemaClassGenerator/ModelBuilder.cs b/XmlSchemaClassGenerator/ModelBuilder.cs index 93d21794..b2184269 100644 --- a/XmlSchemaClassGenerator/ModelBuilder.cs +++ b/XmlSchemaClassGenerator/ModelBuilder.cs @@ -96,6 +96,11 @@ public ModelBuilder(GeneratorConfiguration configuration, XmlSchemaSet set) if (type is ClassModel classModel) { classModel.Documentation.AddRange(GetDocumentation(rootElement)); + if (!rootElement.SubstitutionGroup.IsEmpty) + { + classModel.IsSubstitution = true; + classModel.SubstitutionName = rootElement.QualifiedName; + } } type.RootElementName = rootElement.QualifiedName; diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index f2d4a27d..67b3526b 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -225,6 +225,7 @@ public class ClassModel : TypeModel public bool IsAbstract { get; set; } public bool IsMixed { get; set; } public bool IsSubstitution { get; set; } + public XmlQualifiedName SubstitutionName { get; set; } public TypeModel BaseClass { get; set; } public List Properties { get; set; } public List Interfaces { get; set; } @@ -974,7 +975,7 @@ private IEnumerable GetAttributes(bool isArray) foreach (var derivedType in derivedTypes) { var derivedAttribute = new CodeAttributeDeclaration(new CodeTypeReference(typeof(XmlElementAttribute), Configuration.CodeTypeReferenceOptions), - new CodeAttributeArgument(new CodePrimitiveExpression(derivedType.XmlSchemaName.Name)), + new CodeAttributeArgument(new CodePrimitiveExpression((derivedType.SubstitutionName ?? derivedType.XmlSchemaName).Name)), new CodeAttributeArgument("Type", new CodeTypeOfExpression(derivedType.GetReferenceFor(OwningType.Namespace, false)))); if (Order != null) { From b3c1867e7e63b10abfedf9156c05dbb4b8c3e40e Mon Sep 17 00:00:00 2001 From: jokokko Date: Thu, 4 Oct 2018 16:17:21 +0300 Subject: [PATCH 2/7] EditorBrowsableAttribute values should respect CodeTypeReferenceOptions to mitigate/avoid collisions. --- XmlSchemaClassGenerator.Tests/XmlTests.cs | 42 ++++++++++++++++++++++- XmlSchemaClassGenerator/TypeModel.cs | 4 +-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/XmlSchemaClassGenerator.Tests/XmlTests.cs b/XmlSchemaClassGenerator.Tests/XmlTests.cs index 3278447e..216ff11d 100644 --- a/XmlSchemaClassGenerator.Tests/XmlTests.cs +++ b/XmlSchemaClassGenerator.Tests/XmlTests.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis; using Microsoft.Xml.XMLGen; using System; +using System.CodeDom; using System.Collections.Generic; using System.IO; using System.Linq; @@ -34,6 +35,7 @@ private IEnumerable ConvertXml(string name, string xsd, Generator genera EntityFramework = generatorPrototype.EntityFramework, GenerateInterfaces = generatorPrototype.GenerateInterfaces, MemberVisitor = generatorPrototype.MemberVisitor, + CodeTypeReferenceOptions = generatorPrototype.CodeTypeReferenceOptions }; var set = new XmlSchemaSet(); @@ -338,7 +340,45 @@ public void DontGenerateElementForEmptyCollectionInChoice() Assert.DoesNotContain("tags", xml, StringComparison.OrdinalIgnoreCase); } - [Fact] + + [Theory] + [InlineData(CodeTypeReferenceOptions.GlobalReference, "[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]")] + [InlineData((CodeTypeReferenceOptions)0, "[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]")] + public void EditorBrowsableAttributeRespectsCodeTypeReferenceOptions(CodeTypeReferenceOptions codeTypeReferenceOptions, string expectedLine) + { + const string xsd = @" + + + + + + + + + + + + +"; + + var generatedType = ConvertXml(nameof(EditorBrowsableAttributeRespectsCodeTypeReferenceOptions), xsd, new Generator + { + CodeTypeReferenceOptions = codeTypeReferenceOptions, + GenerateNullables = true, + GenerateInterfaces = false, + NamespaceProvider = new NamespaceProvider + { + GenerateNamespace = key => "Test" + } + }); + + Assert.Contains( + expectedLine, + generatedType.First()); + } + + + [Fact] public void ComplexTypeWithAttributeGroupExtension() { const string xsd = @" diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index 67b3526b..a71fa388 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -850,8 +850,8 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi typeDeclaration.Members.Add(nullableMember); var editorBrowsableAttribute = new CodeAttributeDeclaration(new CodeTypeReference(typeof(EditorBrowsableAttribute), Configuration.CodeTypeReferenceOptions)); - editorBrowsableAttribute.Arguments.Add(new CodeAttributeArgument(new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(typeof(EditorBrowsableState)), "Never"))); - specifiedMember.CustomAttributes.Add(editorBrowsableAttribute); + editorBrowsableAttribute.Arguments.Add(new CodeAttributeArgument(new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(new CodeTypeReference(typeof(EditorBrowsableState), Configuration.CodeTypeReferenceOptions)), "Never"))); + specifiedMember.CustomAttributes.Add(editorBrowsableAttribute); member.CustomAttributes.Add(editorBrowsableAttribute); if (Configuration.EntityFramework) { member.CustomAttributes.Add(notMappedAttribute); } } From 73239a0be3b2d6602723a8dc26f4cce78da8b78b Mon Sep 17 00:00:00 2001 From: jokokko Date: Mon, 8 Oct 2018 17:55:19 +0300 Subject: [PATCH 3/7] If member names collide (e.g. attributes with the same names but from different namespaces), override names to non-colliding. --- .../XmlSchemaClassGenerator.Tests.csproj | 6 +++++ XmlSchemaClassGenerator.Tests/XmlTests.cs | 10 +++++++++ .../xml/sameattributenames.xsd | 16 ++++++++++++++ .../xml/sameattributenames_import.xsd | 10 +++++++++ XmlSchemaClassGenerator/TypeModel.cs | 22 +++++++++++++++---- 5 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 XmlSchemaClassGenerator.Tests/xml/sameattributenames.xsd create mode 100644 XmlSchemaClassGenerator.Tests/xml/sameattributenames_import.xsd diff --git a/XmlSchemaClassGenerator.Tests/XmlSchemaClassGenerator.Tests.csproj b/XmlSchemaClassGenerator.Tests/XmlSchemaClassGenerator.Tests.csproj index 28c87204..73ecadbc 100644 --- a/XmlSchemaClassGenerator.Tests/XmlSchemaClassGenerator.Tests.csproj +++ b/XmlSchemaClassGenerator.Tests/XmlSchemaClassGenerator.Tests.csproj @@ -128,6 +128,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/XmlSchemaClassGenerator.Tests/XmlTests.cs b/XmlSchemaClassGenerator.Tests/XmlTests.cs index 216ff11d..b81f706a 100644 --- a/XmlSchemaClassGenerator.Tests/XmlTests.cs +++ b/XmlSchemaClassGenerator.Tests/XmlTests.cs @@ -378,6 +378,16 @@ public void EditorBrowsableAttributeRespectsCodeTypeReferenceOptions(CodeTypeRef } + [Theory] + [InlineData(@"xml/sameattributenames.xsd", @"xml/sameattributenames_import.xsd")] + public void CollidingAttributeAndPropertyNamesCanBeResolved(params string[] files) + { + // Compilation would previously throw due to duplicate type name within type + var assembly = Compiler.GenerateFiles("AttributesWithSameName", files); + + Assert.NotNull(assembly); + } + [Fact] public void ComplexTypeWithAttributeGroupExtension() { diff --git a/XmlSchemaClassGenerator.Tests/xml/sameattributenames.xsd b/XmlSchemaClassGenerator.Tests/xml/sameattributenames.xsd new file mode 100644 index 00000000..dda80a3c --- /dev/null +++ b/XmlSchemaClassGenerator.Tests/xml/sameattributenames.xsd @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/XmlSchemaClassGenerator.Tests/xml/sameattributenames_import.xsd b/XmlSchemaClassGenerator.Tests/xml/sameattributenames_import.xsd new file mode 100644 index 00000000..0ff0aa76 --- /dev/null +++ b/XmlSchemaClassGenerator.Tests/xml/sameattributenames_import.xsd @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index a71fa388..2f9f9074 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -383,10 +383,24 @@ public override CodeTypeDeclaration Generate() keyProperty.IsKey = true; } - foreach (var property in Properties) - property.AddMembersTo(classDeclaration, Configuration.EnableDataBinding); - - if (IsMixed && (BaseClass == null || (BaseClass is ClassModel && !AllBaseClasses.Any(b => b.IsMixed)))) + foreach (var property in Properties.GroupBy(x => x.Name)) + { + property.First().AddMembersTo(classDeclaration, Configuration.EnableDataBinding); + + if (property.Count() > 1) + { + var propertyIndex = 1; + foreach (var n in property.Skip(1)) + { + n.Name += $"_{propertyIndex}"; + propertyIndex++; + n.AddMembersTo(classDeclaration, Configuration.EnableDataBinding); + } + } + + } + + if (IsMixed && (BaseClass == null || (BaseClass is ClassModel && !AllBaseClasses.Any(b => b.IsMixed)))) { var text = new CodeMemberField(typeof(string), "Text"); // hack to generate automatic property From df3be5b7a25bbbb586e3c259fddfcce4ff12d512 Mon Sep 17 00:00:00 2001 From: jokokko Date: Tue, 9 Oct 2018 14:19:34 +0300 Subject: [PATCH 4/7] Always iterate over groups, no matter the count (saves one or two GetEnumerator calls). --- XmlSchemaClassGenerator/TypeModel.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index 2f9f9074..a65aab14 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -385,19 +385,16 @@ public override CodeTypeDeclaration Generate() foreach (var property in Properties.GroupBy(x => x.Name)) { - property.First().AddMembersTo(classDeclaration, Configuration.EnableDataBinding); - - if (property.Count() > 1) - { - var propertyIndex = 1; - foreach (var n in property.Skip(1)) + var propertyIndex = 0; + foreach (var p in property) + { + if (propertyIndex > 0) { - n.Name += $"_{propertyIndex}"; - propertyIndex++; - n.AddMembersTo(classDeclaration, Configuration.EnableDataBinding); + p.Name += $"_{propertyIndex}"; } - } - + p.AddMembersTo(classDeclaration, Configuration.EnableDataBinding); + propertyIndex++; + } } if (IsMixed && (BaseClass == null || (BaseClass is ClassModel && !AllBaseClasses.Any(b => b.IsMixed)))) From a9aef9abe76f4d34c151b0db783cb61e11debc27 Mon Sep 17 00:00:00 2001 From: jokokko Date: Wed, 10 Oct 2018 14:15:23 +0300 Subject: [PATCH 5/7] For mixed complexTypes, the XmlTextAttribute member should not collide with existing members. XmlTextAttribute member string -> string[] (so no data loss in serializing mixed elements). --- XmlSchemaClassGenerator.Tests/XmlTests.cs | 26 ++++++++++++++++++++++- XmlSchemaClassGenerator/ModelBuilder.cs | 3 ++- XmlSchemaClassGenerator/TypeModel.cs | 11 ++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/XmlSchemaClassGenerator.Tests/XmlTests.cs b/XmlSchemaClassGenerator.Tests/XmlTests.cs index b81f706a..7843952c 100644 --- a/XmlSchemaClassGenerator.Tests/XmlTests.cs +++ b/XmlSchemaClassGenerator.Tests/XmlTests.cs @@ -377,8 +377,32 @@ public void EditorBrowsableAttributeRespectsCodeTypeReferenceOptions(CodeTypeRef generatedType.First()); } + [Fact] + public void MixedTypeMustNotCollideWithExistingMembers() + { + const string xsd = @" + + + + + + +"; - [Theory] + var generatedType = ConvertXml(nameof(MixedTypeMustNotCollideWithExistingMembers), xsd, new Generator + { + NamespaceProvider = new NamespaceProvider + { + GenerateNamespace = key => "Test" + } + }); + + Assert.Contains( + @"public string[] Text_1 { get; set; }", + generatedType.First()); + } + + [Theory] [InlineData(@"xml/sameattributenames.xsd", @"xml/sameattributenames_import.xsd")] public void CollidingAttributeAndPropertyNamesCanBeResolved(params string[] files) { diff --git a/XmlSchemaClassGenerator/ModelBuilder.cs b/XmlSchemaClassGenerator/ModelBuilder.cs index b2184269..2afc7a68 100644 --- a/XmlSchemaClassGenerator/ModelBuilder.cs +++ b/XmlSchemaClassGenerator/ModelBuilder.cs @@ -53,7 +53,8 @@ public ModelBuilder(GeneratorConfiguration configuration, XmlSchemaSet set) foreach (var rootElement in set.GlobalElements.Values.Cast()) { - var source = new Uri(rootElement.GetSchema().SourceUri); + var rootSchema = rootElement.GetSchema(); + var source = !string.IsNullOrEmpty(rootSchema.SourceUri) ? new Uri(rootElement.GetSchema().SourceUri) : default(Uri); var qualifiedName = rootElement.ElementSchemaType.QualifiedName; if (qualifiedName.IsEmpty) { qualifiedName = rootElement.QualifiedName; } var type = CreateTypeModel(source, rootElement.ElementSchemaType, qualifiedName); diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index a65aab14..bcb3ec14 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -398,8 +398,15 @@ public override CodeTypeDeclaration Generate() } if (IsMixed && (BaseClass == null || (BaseClass is ClassModel && !AllBaseClasses.Any(b => b.IsMixed)))) - { - var text = new CodeMemberField(typeof(string), "Text"); + { + var propName = "Text"; + + // To not collide with any existing members + for (var propertyIndex = 1; Properties.Any(x => x.Name.Equals(propName, StringComparison.Ordinal)); propertyIndex++) + { + propName = $"Text_{propertyIndex}"; + } + var text = new CodeMemberField(typeof(string[]), propName); // hack to generate automatic property text.Name += " { get; set; }"; text.Attributes = MemberAttributes.Public; From e1e52821cf6cbe5cecace5bb4d992985dc01f155 Mon Sep 17 00:00:00 2001 From: jokokko Date: Wed, 10 Oct 2018 15:01:13 +0300 Subject: [PATCH 6/7] Also take into account containing type name when choosing XmlTextAttribute member name (yes, this collision is really happening in the schemas I need to process). --- XmlSchemaClassGenerator.Tests/XmlTests.cs | 24 +++++++++++++++++++++++ XmlSchemaClassGenerator/TypeModel.cs | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/XmlSchemaClassGenerator.Tests/XmlTests.cs b/XmlSchemaClassGenerator.Tests/XmlTests.cs index 7843952c..c67b332d 100644 --- a/XmlSchemaClassGenerator.Tests/XmlTests.cs +++ b/XmlSchemaClassGenerator.Tests/XmlTests.cs @@ -402,6 +402,30 @@ public void MixedTypeMustNotCollideWithExistingMembers() generatedType.First()); } + [Fact] + public void MixedTypeMustNotCollideWithContainingTypeName() + { + const string xsd = @" + + + + + +"; + + var generatedType = ConvertXml(nameof(MixedTypeMustNotCollideWithExistingMembers), xsd, new Generator + { + NamespaceProvider = new NamespaceProvider + { + GenerateNamespace = key => "Test" + } + }); + + Assert.Contains( + @"public string[] Text_1 { get; set; }", + generatedType.First()); + } + [Theory] [InlineData(@"xml/sameattributenames.xsd", @"xml/sameattributenames_import.xsd")] public void CollidingAttributeAndPropertyNamesCanBeResolved(params string[] files) diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index bcb3ec14..e45ce3f1 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -402,7 +402,7 @@ public override CodeTypeDeclaration Generate() var propName = "Text"; // To not collide with any existing members - for (var propertyIndex = 1; Properties.Any(x => x.Name.Equals(propName, StringComparison.Ordinal)); propertyIndex++) + for (var propertyIndex = 1; Properties.Any(x => x.Name.Equals(propName, StringComparison.Ordinal)) || propName.Equals(classDeclaration.Name, StringComparison.Ordinal); propertyIndex++) { propName = $"Text_{propertyIndex}"; } From 329cbd3ba3e93554f8b85c07623bee5e15e2f74a Mon Sep 17 00:00:00 2001 From: Girts Lemanis Date: Mon, 15 Oct 2018 17:27:57 +0300 Subject: [PATCH 7/7] 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; }