Skip to content

Commit e54df2c

Browse files
authored
Merge pull request #578 from mganss/copilot/fix-issue-561-continue-work-577
Add EnumValue adapter property for simpleContent enum restrictions
2 parents d974d57 + 3c93a82 commit e54df2c

4 files changed

Lines changed: 258 additions & 28 deletions

File tree

XmlSchemaClassGenerator.Tests/XmlTests.cs

Lines changed: 90 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -186,40 +186,102 @@ public void TestGuid()
186186

187187
[Fact, TestPriority(1)]
188188
[UseCulture("en-US")]
189-
public void TestUnion()
189+
public void TestUnion()
190+
{
191+
var assembly = Compiler.Generate("Union", UnionPattern, new Generator
192+
{
193+
NamespacePrefix = "Union",
194+
IntegerDataType = typeof(int),
195+
MapUnionToWidestCommonType = true
196+
});
197+
198+
Assert.NotNull(assembly);
199+
200+
SharedTestFunctions.TestSamples(Output, "Union", UnionPattern);
201+
202+
var snapshotType = assembly.GetType("Union.Snapshot");
203+
Assert.NotNull(snapshotType);
204+
205+
var date = snapshotType.GetProperty("Date");
206+
Assert.NotNull(date);
207+
Assert.Equal(typeof(DateTime), date.PropertyType);
208+
209+
var count = snapshotType.GetProperty("Count");
210+
Assert.NotNull(count);
211+
Assert.Equal(typeof(int), count.PropertyType);
212+
213+
var num = snapshotType.GetProperty("Num");
214+
Assert.NotNull(num);
215+
Assert.Equal(typeof(decimal), num.PropertyType);
216+
}
217+
218+
[Fact, TestPriority(1)]
219+
[UseCulture("en-US")]
220+
public void TestSimpleContentEnum()
190221
{
191-
var assembly = Compiler.Generate("Union", UnionPattern, new Generator
192-
{
193-
NamespacePrefix = "Union",
194-
IntegerDataType = typeof(int),
195-
MapUnionToWidestCommonType = true
196-
});
197-
198-
Assert.NotNull(assembly);
199-
200-
SharedTestFunctions.TestSamples(Output, "Union", UnionPattern);
222+
var assembly = Compiler.Generate("SimpleContentEnum", "xsd/simple/simplecontent-enum.xsd");
201223

202-
var snapshotType = assembly.GetType("Union.Snapshot");
203-
Assert.NotNull(snapshotType);
224+
const string ns = "SimpleContentEnum.Simplecontent";
204225

205-
var date = snapshotType.GetProperty("Date");
206-
Assert.NotNull(date);
207-
Assert.Equal(typeof(DateTime), date.PropertyType);
226+
// The enum type should be generated
227+
var enumType = assembly.GetType($"{ns}.TransConfirmationCodeTypeEnum");
228+
if (enumType == null)
229+
{
230+
var names = string.Join(", ", assembly.GetTypes().Select(t => t.FullName));
231+
Assert.Fail($"Enum type not found. Available types: {names}");
232+
}
208233

209-
var count = snapshotType.GetProperty("Count");
210-
Assert.NotNull(count);
211-
Assert.Equal(typeof(int), count.PropertyType);
234+
// Verify it's an enum with the expected values
235+
Assert.True(enumType.IsEnum);
236+
var enumValues = Enum.GetNames(enumType);
237+
Assert.Contains("Always", enumValues);
238+
Assert.Contains("Never", enumValues);
239+
Assert.Contains("OnError", enumValues);
212240

213-
var num = snapshotType.GetProperty("Num");
214-
Assert.NotNull(num);
215-
Assert.Equal(typeof(decimal), num.PropertyType);
216-
}
241+
// The derived class should exist and inherit from the base
242+
var type = assembly.GetType($"{ns}.TransConfirmationCodeType");
243+
Assert.NotNull(type);
217244

218-
[Fact, TestPriority(1)]
219-
[UseCulture("en-US")]
220-
public void TestList()
221-
{
222-
Compiler.Generate("List", ListPattern);
245+
var baseType = assembly.GetType($"{ns}.CodeType");
246+
Assert.Equal(baseType, type.BaseType);
247+
248+
// The derived class inherits the string Value property from the base class
249+
var valueProperty = type.GetProperty("Value");
250+
Assert.NotNull(valueProperty);
251+
Assert.Equal(typeof(string), valueProperty.PropertyType);
252+
253+
// The derived class should have an EnumValue adapter property
254+
var enumValueProperty = type.GetProperty("EnumValue");
255+
Assert.NotNull(enumValueProperty);
256+
Assert.Equal(typeof(Nullable<>).MakeGenericType(enumType), enumValueProperty.PropertyType);
257+
258+
// Test that the EnumValue property works correctly
259+
var instance = Activator.CreateInstance(type);
260+
Assert.NotNull(instance);
261+
262+
// Set Value to a string and verify EnumValue returns the correct enum
263+
valueProperty.SetValue(instance, "Always");
264+
var enumValue = enumValueProperty.GetValue(instance);
265+
Assert.NotNull(enumValue);
266+
Assert.Equal("Always", enumValue.ToString());
267+
268+
// Set EnumValue and verify Value is updated
269+
var alwaysValue = Enum.Parse(enumType, "Never");
270+
enumValueProperty.SetValue(instance, alwaysValue);
271+
var stringValue = valueProperty.GetValue(instance);
272+
Assert.Equal("Never", stringValue);
273+
274+
// Set EnumValue to null and verify Value is null
275+
enumValueProperty.SetValue(instance, null);
276+
stringValue = valueProperty.GetValue(instance);
277+
Assert.Null(stringValue);
278+
}
279+
280+
[Fact, TestPriority(1)]
281+
[UseCulture("en-US")]
282+
public void TestList()
283+
{
284+
Compiler.Generate("List", ListPattern);
223285
SharedTestFunctions.TestSamples(Output, "List", ListPattern);
224286
}
225287

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://example.com/simplecontent" targetNamespace="http://example.com/simplecontent" elementFormDefault="qualified">
3+
<xs:complexType name="CodeType">
4+
<xs:simpleContent>
5+
<xs:extension base="xs:string">
6+
<xs:attribute name="listID" type="xs:string" use="optional"/>
7+
</xs:extension>
8+
</xs:simpleContent>
9+
</xs:complexType>
10+
11+
<xs:complexType name="TransConfirmationCodeType">
12+
<xs:simpleContent>
13+
<xs:restriction base="CodeType">
14+
<xs:enumeration value="Always"/>
15+
<xs:enumeration value="Never"/>
16+
<xs:enumeration value="OnError"/>
17+
</xs:restriction>
18+
</xs:simpleContent>
19+
</xs:complexType>
20+
21+
<xs:element name="Root" type="TransConfirmationCodeType"/>
22+
</xs:schema>

XmlSchemaClassGenerator/ModelBuilder.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,15 @@ private TypeModel CreateTypeModel(XmlSchemaComplexType complexType)
551551
}
552552
}
553553

554+
if (complexType.ContentModel?.Content is XmlSchemaSimpleContentRestriction simpleContentRestriction)
555+
{
556+
var enumFacets = simpleContentRestriction.Facets?.OfType<XmlSchemaEnumerationFacet>().ToList();
557+
if (enumFacets?.Count > 0 && !_configuration.EnumAsString)
558+
{
559+
classModel.TextValueType = CreateSimpleContentEnumModel(classModel, enumFacets);
560+
}
561+
}
562+
554563
XmlSchemaParticle xmlParticle = null;
555564
if (classModel.BaseClass != null)
556565
{
@@ -723,6 +732,49 @@ private static List<EnumValueModel> EnsureEnumValuesUnique(List<EnumValueModel>
723732
return enumModelValues;
724733
}
725734

735+
private EnumModel CreateSimpleContentEnumModel(ClassModel classModel, List<XmlSchemaEnumerationFacet> enumFacets)
736+
{
737+
var enumNamespace = namespaceModel?.Key.XmlSchemaNamespace ?? qualifiedName?.Namespace ?? "";
738+
var enumQualifiedName = qualifiedName == null || qualifiedName.IsEmpty
739+
? new XmlQualifiedName($"{classModel.Name}Enum", enumNamespace)
740+
: new XmlQualifiedName($"{qualifiedName.Name}Enum", enumNamespace);
741+
742+
var enumName = $"{classModel.Name}Enum";
743+
if (namespaceModel != null)
744+
enumName = namespaceModel.GetUniqueTypeName(enumName);
745+
746+
var enumModel = new EnumModel(_configuration)
747+
{
748+
Name = enumName,
749+
Namespace = namespaceModel,
750+
XmlSchemaName = enumQualifiedName,
751+
IsAnonymous = false,
752+
};
753+
754+
foreach (var facet in enumFacets.DistinctBy(f => f.Value))
755+
{
756+
var value = new EnumValueModel
757+
{
758+
Name = _configuration.NamingProvider.EnumMemberNameFromValue(enumModel.Name, facet.Value, facet),
759+
Value = facet.Value
760+
};
761+
762+
var valueDocs = GetDocumentation(facet);
763+
value.Documentation.AddRange(valueDocs);
764+
765+
value.IsDeprecated = facet.Annotation?.Items.OfType<XmlSchemaAppInfo>()
766+
.Any(a => Array.Exists(a.Markup, m => m.Name == "annox:annotate" && m.HasChildNodes && m.FirstChild.Name == "jl:Deprecated")) == true;
767+
768+
enumModel.Values.Add(value);
769+
}
770+
771+
enumModel.Values = EnsureEnumValuesUnique(enumModel.Values);
772+
if (namespaceModel != null)
773+
namespaceModel.Types[enumModel.Name] = enumModel;
774+
775+
return enumModel;
776+
}
777+
726778
private EnumModel CreateEnumModel(XmlSchemaSimpleType simpleType, List<XmlSchemaEnumerationFacet> enumFacets)
727779
{
728780
// we got an enum

XmlSchemaClassGenerator/Models/ClassModel.cs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class ClassModel(GeneratorConfiguration configuration) : ReferenceTypeMod
1717
public bool IsMixed { get; set; }
1818
public bool IsSubstitution { get; set; }
1919
public TypeModel BaseClass { get; set; }
20+
public TypeModel TextValueType { get; set; }
2021
public List<ClassModel> DerivedTypes { get; set; } = [];
2122
public override bool IsSubtype => BaseClass != null;
2223

@@ -95,6 +96,99 @@ public override CodeTypeDeclaration Generate()
9596
if (BaseClass is ClassModel)
9697
{
9798
classDeclaration.BaseTypes.Add(BaseClass.GetReferenceFor(Namespace));
99+
100+
// When a derived class has a simpleContent restriction with enum facets (TextValueType != null),
101+
// we generate an adapter property that allows strongly-typed access to the enum values.
102+
// We cannot add a new XmlText property because the XmlSerializer doesn't allow it when
103+
// the base class already has one. Instead, we use an [XmlIgnore] adapter property.
104+
if (TextValueType != null && !string.IsNullOrEmpty(Configuration.TextValuePropertyName))
105+
{
106+
var textName = Configuration.TextValuePropertyName;
107+
var enumTypeReference = TextValueType.GetReferenceFor(Namespace);
108+
var nullableEnumTypeReference = new CodeTypeReference(typeof(Nullable<>));
109+
nullableEnumTypeReference.TypeArguments.Add(enumTypeReference);
110+
111+
// Create the EnumValue adapter property
112+
var enumValueProperty = new CodeMemberProperty
113+
{
114+
Name = "EnumValue",
115+
Type = nullableEnumTypeReference,
116+
Attributes = MemberAttributes.Public,
117+
HasGet = true,
118+
HasSet = true
119+
};
120+
121+
// Add [XmlIgnore] attribute
122+
var ignoreAttribute = AttributeDecl<XmlIgnoreAttribute>();
123+
enumValueProperty.CustomAttributes.Add(ignoreAttribute);
124+
125+
// Getter: Try to parse the Value property to enum
126+
// if (Enum.TryParse(typeof(EnumType), Value, true, out var result))
127+
// return (EnumType)result;
128+
// return null;
129+
var resultVariable = new CodeVariableDeclarationStatement(typeof(object), "result");
130+
var tryParseCondition = new CodeMethodInvokeExpression(
131+
new CodeTypeReferenceExpression(typeof(Enum)),
132+
"TryParse",
133+
new CodeTypeOfExpression(enumTypeReference),
134+
new CodePropertyReferenceExpression(
135+
new CodeThisReferenceExpression(),
136+
textName),
137+
new CodePrimitiveExpression(true),
138+
new CodeDirectionExpression(FieldDirection.Out, new CodeVariableReferenceExpression("result")));
139+
140+
var returnCastResult = new CodeMethodReturnStatement(
141+
new CodeCastExpression(
142+
nullableEnumTypeReference,
143+
new CodeVariableReferenceExpression("result")));
144+
145+
var ifTryParse = new CodeConditionStatement(
146+
tryParseCondition,
147+
returnCastResult);
148+
149+
enumValueProperty.GetStatements.Add(resultVariable);
150+
enumValueProperty.GetStatements.Add(ifTryParse);
151+
enumValueProperty.GetStatements.Add(new CodeMethodReturnStatement(new CodePrimitiveExpression(null)));
152+
153+
// Setter: Value = value?.ToString();
154+
// Since CodeDOM doesn't support null-conditional operator, we need to check and set
155+
var valueNotNull = new CodeBinaryOperatorExpression(
156+
new CodePropertySetValueReferenceExpression(),
157+
CodeBinaryOperatorType.IdentityInequality,
158+
new CodePrimitiveExpression(null));
159+
160+
var setToString = new CodeAssignStatement(
161+
new CodePropertyReferenceExpression(
162+
new CodeThisReferenceExpression(),
163+
textName),
164+
new CodeMethodInvokeExpression(
165+
new CodePropertySetValueReferenceExpression(),
166+
"ToString"));
167+
168+
var setToNull = new CodeAssignStatement(
169+
new CodePropertyReferenceExpression(
170+
new CodeThisReferenceExpression(),
171+
textName),
172+
new CodePrimitiveExpression(null));
173+
174+
enumValueProperty.SetStatements.Add(
175+
new CodeConditionStatement(
176+
valueNotNull,
177+
new CodeStatement[] { setToString },
178+
new CodeStatement[] { setToNull }));
179+
180+
var docs = new List<DocumentationModel> {
181+
new() { Language = English, Text = "Gets or sets the typed value of the text content." },
182+
new() { Language = German, Text = "Ruft den typisierten Wert des Textinhalts ab oder legt diesen fest." }
183+
};
184+
185+
enumValueProperty.Comments.AddRange(GetComments(docs).ToArray());
186+
187+
classDeclaration.Members.Add(enumValueProperty);
188+
189+
var enumValuePropertyModel = new PropertyModel(Configuration, "EnumValue", TextValueType, this);
190+
Configuration.MemberVisitor(enumValueProperty, enumValuePropertyModel);
191+
}
98192
}
99193
else if (!string.IsNullOrEmpty(Configuration.TextValuePropertyName))
100194
{

0 commit comments

Comments
 (0)