Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 90 additions & 28 deletions XmlSchemaClassGenerator.Tests/XmlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,40 +186,102 @@ public void TestGuid()

[Fact, TestPriority(1)]
[UseCulture("en-US")]
public void TestUnion()
public void TestUnion()
{
var assembly = Compiler.Generate("Union", UnionPattern, new Generator
{
NamespacePrefix = "Union",
IntegerDataType = typeof(int),
MapUnionToWidestCommonType = true
});

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 TestSimpleContentEnum()
{
var assembly = Compiler.Generate("Union", UnionPattern, new Generator
{
NamespacePrefix = "Union",
IntegerDataType = typeof(int),
MapUnionToWidestCommonType = true
});

Assert.NotNull(assembly);

SharedTestFunctions.TestSamples(Output, "Union", UnionPattern);
var assembly = Compiler.Generate("SimpleContentEnum", "xsd/simple/simplecontent-enum.xsd");

var snapshotType = assembly.GetType("Union.Snapshot");
Assert.NotNull(snapshotType);
const string ns = "SimpleContentEnum.Simplecontent";

var date = snapshotType.GetProperty("Date");
Assert.NotNull(date);
Assert.Equal(typeof(DateTime), date.PropertyType);
// The enum type should be generated
var enumType = assembly.GetType($"{ns}.TransConfirmationCodeTypeEnum");
if (enumType == null)
{
var names = string.Join(", ", assembly.GetTypes().Select(t => t.FullName));
Assert.Fail($"Enum type not found. Available types: {names}");
}

var count = snapshotType.GetProperty("Count");
Assert.NotNull(count);
Assert.Equal(typeof(int), count.PropertyType);
// Verify it's an enum with the expected values
Assert.True(enumType.IsEnum);
var enumValues = Enum.GetNames(enumType);
Assert.Contains("Always", enumValues);
Assert.Contains("Never", enumValues);
Assert.Contains("OnError", enumValues);

var num = snapshotType.GetProperty("Num");
Assert.NotNull(num);
Assert.Equal(typeof(decimal), num.PropertyType);
}
// The derived class should exist and inherit from the base
var type = assembly.GetType($"{ns}.TransConfirmationCodeType");
Assert.NotNull(type);

[Fact, TestPriority(1)]
[UseCulture("en-US")]
public void TestList()
{
Compiler.Generate("List", ListPattern);
var baseType = assembly.GetType($"{ns}.CodeType");
Assert.Equal(baseType, type.BaseType);

// The derived class inherits the string Value property from the base class
var valueProperty = type.GetProperty("Value");
Assert.NotNull(valueProperty);
Assert.Equal(typeof(string), valueProperty.PropertyType);

// The derived class should have an EnumValue adapter property
var enumValueProperty = type.GetProperty("EnumValue");
Assert.NotNull(enumValueProperty);
Assert.Equal(typeof(Nullable<>).MakeGenericType(enumType), enumValueProperty.PropertyType);

// Test that the EnumValue property works correctly
var instance = Activator.CreateInstance(type);
Assert.NotNull(instance);

// Set Value to a string and verify EnumValue returns the correct enum
valueProperty.SetValue(instance, "Always");
var enumValue = enumValueProperty.GetValue(instance);
Assert.NotNull(enumValue);
Assert.Equal("Always", enumValue.ToString());

// Set EnumValue and verify Value is updated
var alwaysValue = Enum.Parse(enumType, "Never");
enumValueProperty.SetValue(instance, alwaysValue);
var stringValue = valueProperty.GetValue(instance);
Assert.Equal("Never", stringValue);

// Set EnumValue to null and verify Value is null
enumValueProperty.SetValue(instance, null);
stringValue = valueProperty.GetValue(instance);
Assert.Null(stringValue);
}

[Fact, TestPriority(1)]
[UseCulture("en-US")]
public void TestList()
{
Compiler.Generate("List", ListPattern);
SharedTestFunctions.TestSamples(Output, "List", ListPattern);
}

Expand Down
22 changes: 22 additions & 0 deletions XmlSchemaClassGenerator.Tests/xsd/simple/simplecontent-enum.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://example.com/simplecontent" targetNamespace="http://example.com/simplecontent" elementFormDefault="qualified">
<xs:complexType name="CodeType">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="listID" type="xs:string" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>

<xs:complexType name="TransConfirmationCodeType">
<xs:simpleContent>
<xs:restriction base="CodeType">
<xs:enumeration value="Always"/>
<xs:enumeration value="Never"/>
<xs:enumeration value="OnError"/>
</xs:restriction>
</xs:simpleContent>
</xs:complexType>

<xs:element name="Root" type="TransConfirmationCodeType"/>
</xs:schema>
52 changes: 52 additions & 0 deletions XmlSchemaClassGenerator/ModelBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,15 @@ private TypeModel CreateTypeModel(XmlSchemaComplexType complexType)
}
}

if (complexType.ContentModel?.Content is XmlSchemaSimpleContentRestriction simpleContentRestriction)
{
var enumFacets = simpleContentRestriction.Facets?.OfType<XmlSchemaEnumerationFacet>().ToList();
if (enumFacets?.Count > 0 && !_configuration.EnumAsString)
{
classModel.TextValueType = CreateSimpleContentEnumModel(classModel, enumFacets);
}
}

XmlSchemaParticle xmlParticle = null;
if (classModel.BaseClass != null)
{
Expand Down Expand Up @@ -723,6 +732,49 @@ private static List<EnumValueModel> EnsureEnumValuesUnique(List<EnumValueModel>
return enumModelValues;
}

private EnumModel CreateSimpleContentEnumModel(ClassModel classModel, List<XmlSchemaEnumerationFacet> enumFacets)
{
var enumNamespace = namespaceModel?.Key.XmlSchemaNamespace ?? qualifiedName?.Namespace ?? "";
var enumQualifiedName = qualifiedName == null || qualifiedName.IsEmpty
? new XmlQualifiedName($"{classModel.Name}Enum", enumNamespace)
: new XmlQualifiedName($"{qualifiedName.Name}Enum", enumNamespace);

var enumName = $"{classModel.Name}Enum";
if (namespaceModel != null)
enumName = namespaceModel.GetUniqueTypeName(enumName);

var enumModel = new EnumModel(_configuration)
{
Name = enumName,
Namespace = namespaceModel,
XmlSchemaName = enumQualifiedName,
IsAnonymous = false,
};

foreach (var facet in enumFacets.DistinctBy(f => f.Value))
{
var value = new EnumValueModel
{
Name = _configuration.NamingProvider.EnumMemberNameFromValue(enumModel.Name, facet.Value, facet),
Value = facet.Value
};

var valueDocs = GetDocumentation(facet);
value.Documentation.AddRange(valueDocs);

value.IsDeprecated = facet.Annotation?.Items.OfType<XmlSchemaAppInfo>()
.Any(a => Array.Exists(a.Markup, m => m.Name == "annox:annotate" && m.HasChildNodes && m.FirstChild.Name == "jl:Deprecated")) == true;

enumModel.Values.Add(value);
}

enumModel.Values = EnsureEnumValuesUnique(enumModel.Values);
if (namespaceModel != null)
namespaceModel.Types[enumModel.Name] = enumModel;

return enumModel;
}

private EnumModel CreateEnumModel(XmlSchemaSimpleType simpleType, List<XmlSchemaEnumerationFacet> enumFacets)
{
// we got an enum
Expand Down
94 changes: 94 additions & 0 deletions XmlSchemaClassGenerator/Models/ClassModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class ClassModel(GeneratorConfiguration configuration) : ReferenceTypeMod
public bool IsMixed { get; set; }
public bool IsSubstitution { get; set; }
public TypeModel BaseClass { get; set; }
public TypeModel TextValueType { get; set; }
public List<ClassModel> DerivedTypes { get; set; } = [];
public override bool IsSubtype => BaseClass != null;

Expand Down Expand Up @@ -95,6 +96,99 @@ public override CodeTypeDeclaration Generate()
if (BaseClass is ClassModel)
{
classDeclaration.BaseTypes.Add(BaseClass.GetReferenceFor(Namespace));

// When a derived class has a simpleContent restriction with enum facets (TextValueType != null),
// we generate an adapter property that allows strongly-typed access to the enum values.
// We cannot add a new XmlText property because the XmlSerializer doesn't allow it when
// the base class already has one. Instead, we use an [XmlIgnore] adapter property.
if (TextValueType != null && !string.IsNullOrEmpty(Configuration.TextValuePropertyName))
{
var textName = Configuration.TextValuePropertyName;
var enumTypeReference = TextValueType.GetReferenceFor(Namespace);
var nullableEnumTypeReference = new CodeTypeReference(typeof(Nullable<>));
nullableEnumTypeReference.TypeArguments.Add(enumTypeReference);

// Create the EnumValue adapter property
var enumValueProperty = new CodeMemberProperty
{
Name = "EnumValue",
Type = nullableEnumTypeReference,
Attributes = MemberAttributes.Public,
HasGet = true,
HasSet = true
};

// Add [XmlIgnore] attribute
var ignoreAttribute = AttributeDecl<XmlIgnoreAttribute>();
enumValueProperty.CustomAttributes.Add(ignoreAttribute);

// Getter: Try to parse the Value property to enum
// if (Enum.TryParse(typeof(EnumType), Value, true, out var result))
// return (EnumType)result;
// return null;
var resultVariable = new CodeVariableDeclarationStatement(typeof(object), "result");
var tryParseCondition = new CodeMethodInvokeExpression(
new CodeTypeReferenceExpression(typeof(Enum)),
"TryParse",
new CodeTypeOfExpression(enumTypeReference),
new CodePropertyReferenceExpression(
new CodeThisReferenceExpression(),
textName),
new CodePrimitiveExpression(true),
new CodeDirectionExpression(FieldDirection.Out, new CodeVariableReferenceExpression("result")));

var returnCastResult = new CodeMethodReturnStatement(
new CodeCastExpression(
nullableEnumTypeReference,
new CodeVariableReferenceExpression("result")));

var ifTryParse = new CodeConditionStatement(
tryParseCondition,
returnCastResult);

enumValueProperty.GetStatements.Add(resultVariable);
enumValueProperty.GetStatements.Add(ifTryParse);
enumValueProperty.GetStatements.Add(new CodeMethodReturnStatement(new CodePrimitiveExpression(null)));

// Setter: Value = value?.ToString();
// Since CodeDOM doesn't support null-conditional operator, we need to check and set
var valueNotNull = new CodeBinaryOperatorExpression(
new CodePropertySetValueReferenceExpression(),
CodeBinaryOperatorType.IdentityInequality,
new CodePrimitiveExpression(null));

var setToString = new CodeAssignStatement(
new CodePropertyReferenceExpression(
new CodeThisReferenceExpression(),
textName),
new CodeMethodInvokeExpression(
new CodePropertySetValueReferenceExpression(),
"ToString"));

var setToNull = new CodeAssignStatement(
new CodePropertyReferenceExpression(
new CodeThisReferenceExpression(),
textName),
new CodePrimitiveExpression(null));

enumValueProperty.SetStatements.Add(
new CodeConditionStatement(
valueNotNull,
new CodeStatement[] { setToString },
new CodeStatement[] { setToNull }));

var docs = new List<DocumentationModel> {
new() { Language = English, Text = "Gets or sets the typed value of the text content." },
new() { Language = German, Text = "Ruft den typisierten Wert des Textinhalts ab oder legt diesen fest." }
};

enumValueProperty.Comments.AddRange(GetComments(docs).ToArray());

classDeclaration.Members.Add(enumValueProperty);

var enumValuePropertyModel = new PropertyModel(Configuration, "EnumValue", TextValueType, this);
Configuration.MemberVisitor(enumValueProperty, enumValuePropertyModel);
}
}
else if (!string.IsNullOrEmpty(Configuration.TextValuePropertyName))
{
Expand Down