diff --git a/XmlSchemaClassGenerator.Console/Program.cs b/XmlSchemaClassGenerator.Console/Program.cs index 386e7801..d281cd15 100644 --- a/XmlSchemaClassGenerator.Console/Program.cs +++ b/XmlSchemaClassGenerator.Console/Program.cs @@ -22,6 +22,7 @@ static void Main(string[] args) var namespaces = new List(); var outputFolder = (string)null; var integerType = typeof(string); + var useIntegerTypeAsFallback = false; var namespacePrefix = ""; var verbose = false; var nullables = false; @@ -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 }, @@ -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, diff --git a/XmlSchemaClassGenerator.Tests/IntegerTypeTests.cs b/XmlSchemaClassGenerator.Tests/IntegerTypeTests.cs new file mode 100644 index 00000000..8cf71e5e --- /dev/null +++ b/XmlSchemaClassGenerator.Tests/IntegerTypeTests.cs @@ -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 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 = @$" + + + + + + + + + + + + +"; + + 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 = @$" + + + + + + + + + + + + +"; + + 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 = @$" + + + + + + + + + + + + + +"; + + var generatedType = ConvertXml(nameof(TestTotalDigits), xsd, new Generator + { + NamespaceProvider = new NamespaceProvider + { + GenerateNamespace = key => "Test" + } + }); + + var expectedProperty = $"public {expectedType} SomeValue"; + Assert.Contains(expectedProperty, generatedType.First()); + } + } +} diff --git a/XmlSchemaClassGenerator/CodeUtilities.cs b/XmlSchemaClassGenerator/CodeUtilities.cs index a96def4a..8b48070b 100644 --- a/XmlSchemaClassGenerator/CodeUtilities.cs +++ b/XmlSchemaClassGenerator/CodeUtilities.cs @@ -57,12 +57,67 @@ public static string ToBackingField(this string propertyName, string privateFiel private static Type GetIntegerDerivedType(XmlSchemaDatatype type, GeneratorConfiguration configuration, IEnumerable 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().SingleOrDefault(); + var minInclusive = restrictions.OfType().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) @@ -70,6 +125,8 @@ private static Type GetIntegerDerivedType(XmlSchemaDatatype type, GeneratorConfi || xmlTypeCode == XmlTypeCode.NegativeInteger || xmlTypeCode == XmlTypeCode.NonPositiveInteger) && totalDigits.Value >= 29)) { + if (configuration.UseIntegerDataTypeAsFallback && configuration.IntegerDataType != null) + return configuration.IntegerDataType; return typeof(string); } diff --git a/XmlSchemaClassGenerator/Generator.cs b/XmlSchemaClassGenerator/Generator.cs index 628f5ff7..bb9a6181 100644 --- a/XmlSchemaClassGenerator/Generator.cs +++ b/XmlSchemaClassGenerator/Generator.cs @@ -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; } diff --git a/XmlSchemaClassGenerator/GeneratorConfiguration.cs b/XmlSchemaClassGenerator/GeneratorConfiguration.cs index 89ea94cf..df0b9574 100644 --- a/XmlSchemaClassGenerator/GeneratorConfiguration.cs +++ b/XmlSchemaClassGenerator/GeneratorConfiguration.cs @@ -135,6 +135,10 @@ public NamingScheme NamingScheme /// public Type IntegerDataType { get; set; } /// + /// Use only if no better type can be inferred + /// + public bool UseIntegerDataTypeAsFallback { get; set; } + /// /// Generate Entity Framework Code First compatible classes /// public bool EntityFramework { get; set; }