Skip to content

Commit

Permalink
Merge pull request #187 from Etogy/integer-data-type-fallback
Browse files Browse the repository at this point in the history
Enhance deduction of integer type
  • Loading branch information
mganss authored Apr 30, 2020
2 parents 5bfc2fc + 88e420b commit 9ad47b4
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 1 deletion.
3 changes: 3 additions & 0 deletions XmlSchemaClassGenerator.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ static void Main(string[] args)
var namespaces = new List<string>();
var outputFolder = (string)null;
var integerType = typeof(string);
var useIntegerTypeAsFallback = false;
var namespacePrefix = "";
var verbose = false;
var nullables = false;
Expand Down Expand Up @@ -72,6 +73,7 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l
break;
}
} },
{ "fb|fallback|use-integer-type-as-fallback", v => useIntegerTypeAsFallback = v != null },
{ "e|edb|enable-data-binding", "enable INotifyPropertyChanged data binding", v => enableDataBinding = v != null },
{ "r|order", "emit order for all class members stored as XML element", v => emitOrder = v != null },
{ "c|pcl", "PCL compatible output", v => pclCompatible = v != null },
Expand Down Expand Up @@ -169,6 +171,7 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l
EnableDataBinding = enableDataBinding,
EmitOrder = emitOrder,
IntegerDataType = integerType,
UseIntegerDataTypeAsFallback = useIntegerTypeAsFallback,
EntityFramework = entityFramework,
GenerateInterfaces = interfaces,
NamingScheme = pascal ? NamingScheme.PascalCase : NamingScheme.Direct,
Expand Down
167 changes: 167 additions & 0 deletions XmlSchemaClassGenerator.Tests/IntegerTypeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Schema;
using Xunit;

namespace XmlSchemaClassGenerator.Tests {
public class IntegerTypeTests
{
private IEnumerable<string> ConvertXml(string name, string xsd, Generator generatorPrototype = null)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}

var writer = new MemoryOutputWriter();

var gen = new Generator
{
OutputWriter = writer,
Version = new VersionProvider("Tests", "1.0.0.1"),
NamespaceProvider = generatorPrototype.NamespaceProvider,
GenerateNullables = generatorPrototype.GenerateNullables,
IntegerDataType = generatorPrototype.IntegerDataType,
UseIntegerDataTypeAsFallback = generatorPrototype.UseIntegerDataTypeAsFallback,
DataAnnotationMode = generatorPrototype.DataAnnotationMode,
GenerateDesignerCategoryAttribute = generatorPrototype.GenerateDesignerCategoryAttribute,
GenerateComplexTypesForCollections = generatorPrototype.GenerateComplexTypesForCollections,
EntityFramework = generatorPrototype.EntityFramework,
AssemblyVisible = generatorPrototype.AssemblyVisible,
GenerateInterfaces = generatorPrototype.GenerateInterfaces,
MemberVisitor = generatorPrototype.MemberVisitor,
CodeTypeReferenceOptions = generatorPrototype.CodeTypeReferenceOptions
};

var set = new XmlSchemaSet();

using (var stringReader = new StringReader(xsd))
{
var schema = XmlSchema.Read(stringReader, (s, e) =>
{
throw new InvalidOperationException($"{e.Severity}: {e.Message}",e.Exception);
});

set.Add(schema);
}

gen.Generate(set);

return writer.Content;
}

[Theory]
[InlineData(2, "sbyte")]
[InlineData(4, "short")]
[InlineData(9, "int")]
[InlineData(18, "long")]
[InlineData(28, "decimal")]
[InlineData(29, "string")]
public void TestTotalDigits(int totalDigits, string expectedType)
{
var xsd = @$"<?xml version=""1.0"" encoding=""UTF-8""?>
<xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
<xs:complexType name=""document"">
<xs:sequence>
<xs:element name=""someValue"">
<xs:simpleType>
<xs:restriction base=""xs:integer"">
<xs:totalDigits value=""{totalDigits}""/>
</xs:restriction>
</xs:simpleType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:schema>";

var generatedType = ConvertXml(nameof(TestTotalDigits), xsd, new Generator
{
NamespaceProvider = new NamespaceProvider
{
GenerateNamespace = key => "Test"
}
});

var expectedProperty = $"public {expectedType} SomeValue";
Assert.Contains(expectedProperty, generatedType.First());
}

[Theory]
[InlineData(4, false, "long")]
[InlineData(30, false, "long")]
[InlineData(4, true, "short")]
[InlineData(30, true, "long")]
public void TestFallbackType(int totalDigits, bool useTypeAsFallback, string expectedType)
{
var xsd = @$"<?xml version=""1.0"" encoding=""UTF-8""?>
<xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
<xs:complexType name=""document"">
<xs:sequence>
<xs:element name=""someValue"">
<xs:simpleType>
<xs:restriction base=""xs:integer"">
<xs:totalDigits value=""{totalDigits}""/>
</xs:restriction>
</xs:simpleType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:schema>";

var generatedType = ConvertXml(nameof(TestTotalDigits), xsd, new Generator
{
NamespaceProvider = new NamespaceProvider
{
GenerateNamespace = key => "Test"
},
IntegerDataType = typeof(long),
UseIntegerDataTypeAsFallback = useTypeAsFallback
});

var expectedProperty = $"public {expectedType} SomeValue";
Assert.Contains(expectedProperty, generatedType.First());
}

[Theory]
[InlineData(1, 100, "byte")]
[InlineData(-100, 100, "sbyte")]
[InlineData(1, 1000, "ushort")]
[InlineData(-1000, 1000, "short")]
[InlineData(1, 100000, "uint")]
[InlineData(-100000, 100000, "int")]
[InlineData(1, 10000000000, "ulong")]
[InlineData(-10000000000, 10000000000, "long")]
public void TestInclusiveRange(long minInclusive, long maxInclusive, string expectedType)
{
var xsd = @$"<?xml version=""1.0"" encoding=""UTF-8""?>
<xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
<xs:complexType name=""document"">
<xs:sequence>
<xs:element name=""someValue"">
<xs:simpleType>
<xs:restriction base=""xs:integer"">
<xs:minInclusive value=""{minInclusive}""/>
<xs:maxInclusive value=""{maxInclusive}""/>
</xs:restriction>
</xs:simpleType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:schema>";

var generatedType = ConvertXml(nameof(TestTotalDigits), xsd, new Generator
{
NamespaceProvider = new NamespaceProvider
{
GenerateNamespace = key => "Test"
}
});

var expectedProperty = $"public {expectedType} SomeValue";
Assert.Contains(expectedProperty, generatedType.First());
}
}
}
59 changes: 58 additions & 1 deletion XmlSchemaClassGenerator/CodeUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,76 @@ public static string ToBackingField(this string propertyName, string privateFiel

private static Type GetIntegerDerivedType(XmlSchemaDatatype type, GeneratorConfiguration configuration, IEnumerable<RestrictionModel> restrictions)
{
if (configuration.IntegerDataType != null) return configuration.IntegerDataType;
if (configuration.IntegerDataType != null && !configuration.UseIntegerDataTypeAsFallback) return configuration.IntegerDataType;

var xmlTypeCode = type.TypeCode;

Type result = null;

var maxInclusive = restrictions.OfType<MaxInclusiveRestrictionModel>().SingleOrDefault();
var minInclusive = restrictions.OfType<MinInclusiveRestrictionModel>().SingleOrDefault();

decimal? maxInclusiveValue = null;
if (maxInclusive is null && xmlTypeCode == XmlTypeCode.NegativeInteger)
{
maxInclusiveValue = -1;
}
else if (maxInclusive is null && xmlTypeCode == XmlTypeCode.NonPositiveInteger)
{
maxInclusiveValue = 0;
}
else if (maxInclusive != null && decimal.TryParse(maxInclusive.Value, out decimal value))
{
maxInclusiveValue = value;
}

decimal? minInclusiveValue = null;
if (minInclusive is null && xmlTypeCode == XmlTypeCode.PositiveInteger)
{
minInclusiveValue = 1;
}
else if (minInclusive is null && xmlTypeCode == XmlTypeCode.NonNegativeInteger)
{
minInclusiveValue = 0;
}
else if (minInclusive != null && decimal.TryParse(minInclusive.Value, out decimal value))
{
minInclusiveValue = value;
}

// If either value is null, then that value is either unbounded or too large to fit in any numeric type.
if (minInclusiveValue != null && maxInclusiveValue != null) {
if (minInclusiveValue >= byte.MinValue && maxInclusiveValue <= byte.MaxValue)
result = typeof(byte);
else if (minInclusiveValue >= sbyte.MinValue && maxInclusiveValue <= sbyte.MaxValue)
result = typeof(sbyte);
else if (minInclusiveValue >= ushort.MinValue && maxInclusiveValue <= ushort.MaxValue)
result = typeof(ushort);
else if (minInclusiveValue >= short.MinValue && maxInclusiveValue <= short.MaxValue)
result = typeof(short);
else if (minInclusiveValue >= uint.MinValue && maxInclusiveValue <= uint.MaxValue)
result = typeof(uint);
else if (minInclusiveValue >= int.MinValue && maxInclusiveValue <= int.MaxValue)
result = typeof(int);
else if (minInclusiveValue >= ulong.MinValue && maxInclusiveValue <= ulong.MaxValue)
result = typeof(ulong);
else if (minInclusiveValue >= long.MinValue && maxInclusiveValue <= long.MaxValue)
result = typeof(long);
else // If it didn't fit in a decimal, we could not have gotten here.
result = typeof(decimal);

return result;
}

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))
{
if (configuration.UseIntegerDataTypeAsFallback && configuration.IntegerDataType != null)
return configuration.IntegerDataType;
return typeof(string);
}

Expand Down
6 changes: 6 additions & 0 deletions XmlSchemaClassGenerator/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ public Type IntegerDataType
set { _configuration.IntegerDataType = value; }
}

public bool UseIntegerDataTypeAsFallback
{
get { return _configuration.UseIntegerDataTypeAsFallback; }
set { _configuration.UseIntegerDataTypeAsFallback = value; }
}

public bool EntityFramework
{
get { return _configuration.EntityFramework; }
Expand Down
4 changes: 4 additions & 0 deletions XmlSchemaClassGenerator/GeneratorConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ public NamingScheme NamingScheme
/// </summary>
public Type IntegerDataType { get; set; }
/// <summary>
/// Use <see cref="IntegerDataType"/> only if no better type can be inferred
/// </summary>
public bool UseIntegerDataTypeAsFallback { get; set; }
/// <summary>
/// Generate Entity Framework Code First compatible classes
/// </summary>
public bool EntityFramework { get; set; }
Expand Down

0 comments on commit 9ad47b4

Please sign in to comment.