Skip to content

Commit

Permalink
Add basic support for union type coalescence (fixes #397)
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Ganss committed Jul 17, 2023
1 parent 438f970 commit 45e06bc
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 56 deletions.
26 changes: 26 additions & 0 deletions XmlSchemaClassGenerator.Tests/XmlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ private static IEnumerable<string> ConvertXml(string name, string xsd, Generator
const string WfsPattern = "xsd/wfs/schemas.opengis.net/wfs/2.0/wfs.xsd";
const string EppPattern = "xsd/epp/*.xsd";
const string GraphMLPattern = "xsd/graphml/ygraphml.xsd";
const string UnionPattern = "xsd/union/union.xsd";
const string NullableReferenceAttributesPattern = "xsd/nullablereferenceattributes/nullablereference.xsd";

// IATA test takes too long to perform every time
Expand Down Expand Up @@ -155,6 +156,31 @@ public void TestClient()
SharedTestFunctions.TestSamples(Output, "Client", ClientPattern);
}

[Fact, TestPriority(1)]
[UseCulture("en-US")]
public void TestUnion()
{
var assembly = Compiler.Generate("Union", UnionPattern);
Assert.NotNull(assembly);

SharedTestFunctions.TestSamples(Output, "Union", UnionPattern);

var snapshotType = assembly.GetType("Union.Snapshot");
Assert.NotNull(snapshotType);

var date = snapshotType.GetProperty("Date");
Assert.NotNull(date);
Assert.Equal(typeof(DateTime), date.PropertyType);

var count = snapshotType.GetProperty("Count");
Assert.NotNull(count);
Assert.Equal(typeof(int), count.PropertyType);

var num = snapshotType.GetProperty("Num");
Assert.NotNull(num);
Assert.Equal(typeof(decimal), num.PropertyType);
}

[Fact, TestPriority(1)]
[UseCulture("en-US")]
public void TestList()
Expand Down
29 changes: 29 additions & 0 deletions XmlSchemaClassGenerator.Tests/xsd/union/union.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0.48448">
<xs:simpleType name="SnapshotDate">
<xs:restriction base="xs:date" />
</xs:simpleType>
<xs:simpleType name="SnapshotDateTime">
<xs:restriction base="xs:dateTime" />
</xs:simpleType>

<xs:element name="Snapshot">
<xs:complexType>
<xs:attribute name="Date" use="required">
<xs:simpleType>
<xs:union memberTypes="SnapshotDate SnapshotDateTime" />
</xs:simpleType>
</xs:attribute>
<xs:attribute name="Count" use="required">
<xs:simpleType>
<xs:union memberTypes="xs:nonNegativeInteger xs:short xs:byte" />
</xs:simpleType>
</xs:attribute>
<xs:attribute name="Num" use="required">
<xs:simpleType>
<xs:union memberTypes="xs:float xs:double xs:decimal" />
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>
94 changes: 92 additions & 2 deletions XmlSchemaClassGenerator/CodeUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ private static Type GetIntegerDerivedType(XmlSchemaDatatype xml, GeneratorConfig
Type FromFallback() => configuration.UseIntegerDataTypeAsFallback && configuration.IntegerDataType != null ? configuration.IntegerDataType : typeof(string);
}

public static Type GetEffectiveType(this XmlSchemaDatatype type, GeneratorConfiguration configuration, IEnumerable<RestrictionModel> restrictions, bool attribute = false)
public static Type GetEffectiveType(this XmlSchemaDatatype type, GeneratorConfiguration configuration, IEnumerable<RestrictionModel> restrictions, XmlSchemaType schemaType, bool attribute = false)
{
var resultType = type.TypeCode switch
{
XmlTypeCode.AnyAtomicType => typeof(string),// union
XmlTypeCode.AnyAtomicType => GetUnionType(configuration, schemaType, attribute), // union
XmlTypeCode.AnyUri or XmlTypeCode.GDay or XmlTypeCode.GMonth or XmlTypeCode.GMonthDay or XmlTypeCode.GYear or XmlTypeCode.GYearMonth => typeof(string),
XmlTypeCode.Duration => configuration.NetCoreSpecificCode ? type.ValueType : typeof(string),
XmlTypeCode.Time => typeof(DateTime),
Expand Down Expand Up @@ -131,6 +131,96 @@ public static Type GetEffectiveType(this XmlSchemaDatatype type, GeneratorConfig
return resultType;
}

static readonly Type[] intTypes = new[] { typeof(byte), typeof(sbyte), typeof(ushort), typeof(short), typeof(uint), typeof(int), typeof(ulong), typeof(long), typeof(decimal) };
static readonly Type[] decimalTypes = new[] { typeof(float), typeof(double), typeof(decimal) };

private static Type GetUnionType(GeneratorConfiguration configuration, XmlSchemaType schemaType, bool attribute)
{
if (schemaType is not XmlSchemaSimpleType simpleType || simpleType.Content is not XmlSchemaSimpleTypeUnion unionType) return typeof(string);

var baseMemberEffectiveTypes = unionType.BaseMemberTypes.Select(t =>
{
var restriction = t.Content as XmlSchemaSimpleTypeRestriction;
var facets = restriction?.Facets.OfType<XmlSchemaFacet>().ToList();
var restrictions = GetRestrictions(facets, t, configuration).Where(r => r != null).Sanitize().ToList();
return GetEffectiveType(t.Datatype, configuration, restrictions, t, attribute);
}).ToList();

// all member types are the same
if (baseMemberEffectiveTypes.Distinct().Count() == 1) return baseMemberEffectiveTypes[0];

// all member types are integer types
if (baseMemberEffectiveTypes.All(t => intTypes.Contains(t)))
{
var maxTypeIndex = baseMemberEffectiveTypes.Max(t => Array.IndexOf(intTypes, t));
var maxType = intTypes[maxTypeIndex];
// if the max type is signed and the corresponding unsigned type is also in the set we have to use the next higher type
if (maxTypeIndex % 2 == 1 && baseMemberEffectiveTypes.Any(t => Array.IndexOf(intTypes, t) == maxTypeIndex - 1))
return intTypes[maxTypeIndex + 1];
return maxType;
}

// all member types are float/double/decimal
if (baseMemberEffectiveTypes.All(t => decimalTypes.Contains(t)))
{
var maxTypeIndex = baseMemberEffectiveTypes.Max(t => Array.IndexOf(decimalTypes, t));
var maxType = decimalTypes[maxTypeIndex];
return maxType;
}

return typeof(string);
}

public static IEnumerable<RestrictionModel> GetRestrictions(IEnumerable<XmlSchemaFacet> facets, XmlSchemaSimpleType type, GeneratorConfiguration _configuration)
{
var min = facets.OfType<XmlSchemaMinLengthFacet>().Select(f => int.Parse(f.Value)).DefaultIfEmpty().Max();
var max = facets.OfType<XmlSchemaMaxLengthFacet>().Select(f => int.Parse(f.Value)).DefaultIfEmpty().Min();

if (_configuration.DataAnnotationMode == DataAnnotationMode.All)
{
if (min > 0) yield return new MinLengthRestrictionModel(_configuration) { Value = min };
if (max > 0) yield return new MaxLengthRestrictionModel(_configuration) { Value = max };
}
else if (min > 0 || max > 0)
{
yield return new MinMaxLengthRestrictionModel(_configuration) { Min = min, Max = max };
}

foreach (var facet in facets)
{
var valueType = type.Datatype.ValueType;
switch (facet)
{
case XmlSchemaLengthFacet:
var value = int.Parse(facet.Value);
if (_configuration.DataAnnotationMode == DataAnnotationMode.All)
{
yield return new MinLengthRestrictionModel(_configuration) { Value = value };
yield return new MaxLengthRestrictionModel(_configuration) { Value = value };
}
else
{
yield return new MinMaxLengthRestrictionModel(_configuration) { Min = value, Max = value };
}
break;
case XmlSchemaTotalDigitsFacet:
yield return new TotalDigitsRestrictionModel(_configuration) { Value = int.Parse(facet.Value) }; break;
case XmlSchemaFractionDigitsFacet:
yield return new FractionDigitsRestrictionModel(_configuration) { Value = int.Parse(facet.Value) }; break;
case XmlSchemaPatternFacet:
yield return new PatternRestrictionModel(_configuration) { Value = facet.Value }; break;
case XmlSchemaMinInclusiveFacet:
yield return new MinInclusiveRestrictionModel(_configuration) { Value = facet.Value, Type = valueType }; break;
case XmlSchemaMinExclusiveFacet:
yield return new MinExclusiveRestrictionModel(_configuration) { Value = facet.Value, Type = valueType }; break;
case XmlSchemaMaxInclusiveFacet:
yield return new MaxInclusiveRestrictionModel(_configuration) { Value = facet.Value, Type = valueType }; break;
case XmlSchemaMaxExclusiveFacet:
yield return new MaxExclusiveRestrictionModel(_configuration) { Value = facet.Value, Type = valueType }; break;
}
}
}

public static XmlQualifiedName GetQualifiedName(this XmlSchemaType schemaType)
{
return schemaType.QualifiedName.IsEmpty
Expand Down
54 changes: 2 additions & 52 deletions XmlSchemaClassGenerator/ModelBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ XmlSchemaSimpleTypeUnion typeUnion when AllMembersHaveFacets(typeUnion, out base
if (enumFacets.Count > 0 && (baseFacets is null || baseFacets.All(fs => fs.OfType<XmlSchemaEnumerationFacet>().Any())) && !_configuration.EnumAsString)
return CreateEnumModel(simpleType, enumFacets);

restrictions = GetRestrictions(facets, simpleType).Where(r => r != null).Sanitize().ToList();
restrictions = CodeUtilities.GetRestrictions(facets, simpleType, _configuration).Where(r => r != null).Sanitize().ToList();
}

return CreateSimpleModel(simpleType, restrictions ?? new());
Expand All @@ -653,56 +653,6 @@ static bool AllMembersHaveFacets(XmlSchemaSimpleTypeUnion typeUnion, out List<IE
}
}

private IEnumerable<RestrictionModel> GetRestrictions(IEnumerable<XmlSchemaFacet> facets, XmlSchemaSimpleType type)
{
var min = facets.OfType<XmlSchemaMinLengthFacet>().Select(f => int.Parse(f.Value)).DefaultIfEmpty().Max();
var max = facets.OfType<XmlSchemaMaxLengthFacet>().Select(f => int.Parse(f.Value)).DefaultIfEmpty().Min();

if (_configuration.DataAnnotationMode == DataAnnotationMode.All)
{
if (min > 0) yield return new MinLengthRestrictionModel(_configuration) { Value = min };
if (max > 0) yield return new MaxLengthRestrictionModel(_configuration) { Value = max };
}
else if (min > 0 || max > 0)
{
yield return new MinMaxLengthRestrictionModel(_configuration) { Min = min, Max = max };
}

foreach (var facet in facets)
{
var valueType = type.Datatype.ValueType;
switch (facet)
{
case XmlSchemaLengthFacet:
var value = int.Parse(facet.Value);
if (_configuration.DataAnnotationMode == DataAnnotationMode.All)
{
yield return new MinLengthRestrictionModel(_configuration) { Value = value };
yield return new MaxLengthRestrictionModel(_configuration) { Value = value };
}
else
{
yield return new MinMaxLengthRestrictionModel(_configuration) { Min = value, Max = value };
}
break;
case XmlSchemaTotalDigitsFacet:
yield return new TotalDigitsRestrictionModel(_configuration) { Value = int.Parse(facet.Value) }; break;
case XmlSchemaFractionDigitsFacet:
yield return new FractionDigitsRestrictionModel(_configuration) { Value = int.Parse(facet.Value) }; break;
case XmlSchemaPatternFacet:
yield return new PatternRestrictionModel(_configuration) { Value = facet.Value }; break;
case XmlSchemaMinInclusiveFacet:
yield return new MinInclusiveRestrictionModel(_configuration) { Value = facet.Value, Type = valueType }; break;
case XmlSchemaMinExclusiveFacet:
yield return new MinExclusiveRestrictionModel(_configuration) { Value = facet.Value, Type = valueType }; break;
case XmlSchemaMaxInclusiveFacet:
yield return new MaxInclusiveRestrictionModel(_configuration) { Value = facet.Value, Type = valueType }; break;
case XmlSchemaMaxExclusiveFacet:
yield return new MaxExclusiveRestrictionModel(_configuration) { Value = facet.Value, Type = valueType }; break;
}
}
}

private static List<EnumValueModel> EnsureEnumValuesUnique(List<EnumValueModel> enumModelValues)
{
var enumValueGroups = from enumValue in enumModelValues
Expand Down Expand Up @@ -775,7 +725,7 @@ private SimpleModel CreateSimpleModel(XmlSchemaSimpleType simpleType, List<Restr
Namespace = namespaceModel,
XmlSchemaName = qualifiedName,
XmlSchemaType = simpleType,
ValueType = simpleType.Datatype.GetEffectiveType(_configuration, restrictions),
ValueType = simpleType.Datatype.GetEffectiveType(_configuration, restrictions, simpleType),
};

simpleModel.Documentation.AddRange(docs);
Expand Down
4 changes: 2 additions & 2 deletions XmlSchemaClassGenerator/TypeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1226,7 +1226,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.Datatype.GetEffectiveType(Configuration, Restrictions, attribute);
type = XmlSchemaType.Datatype.GetEffectiveType(Configuration, Restrictions, XmlSchemaType, attribute);
UseDataTypeAttribute = XmlSchemaType.Datatype.IsDataTypeAttributeAllowed() ?? UseDataTypeAttribute;
}

Expand Down Expand Up @@ -1260,7 +1260,7 @@ public override CodeExpression GetDefaultValueFor(string defaultString, bool att

if (XmlSchemaType != null)
{
type = XmlSchemaType.Datatype.GetEffectiveType(Configuration, Restrictions, attribute);
type = XmlSchemaType.Datatype.GetEffectiveType(Configuration, Restrictions, XmlSchemaType, attribute);
}

if (type == typeof(XmlQualifiedName))
Expand Down

0 comments on commit 45e06bc

Please sign in to comment.