Skip to content

Commit 2dc9d4b

Browse files
authored
Merge pull request #630 from riccardofrancesconi/feature/collection-item-string-length
Emit CollectionItemStringLengthAttribute for restricted string collections
2 parents 1b9ded9 + 6e1637f commit 2dc9d4b

8 files changed

Lines changed: 661 additions & 21 deletions

File tree

‎README.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,9 +484,18 @@ Note that semantic issues might arise with this approach. For example, `DateTime
484484
Restriction attributes
485485
----------------------
486486

487-
When `EmitMetadataAttributes` is enabled, the generator emits custom attributes for XML schema restrictions that aren't covered by standard DataAnnotations. For example, `xs:fractionDigits` becomes `FractionDigitsAttribute`.
487+
When `EmitMetadataAttributes` is enabled, the generator emits custom attributes for XML schema restrictions that aren't covered by standard DataAnnotations:
488488

489-
The attribute definition is automatically generated in the namespace specified through `--metadataNamespace`. If not specified, the default namespace is `XmlSchemaClassGenerator.Metadata`.
489+
<table>
490+
<tr>
491+
XML Schema facet
492+
Generated attribute
493+
tr>
494+
<tr><td>xs:fractionDigitstd><td><code>FractionDigitsAttributecode>td>tr>
495+
<tr><td>xs:maxLength / xs:minLength on a repeating elementtd><td><code>CollectionItemStringLengthAttributecode>td>tr>
496+
table>
497+
498+
The attribute definitions are automatically generated in the namespace specified through `--metadataNamespace`. If not specified, the default namespace is `XmlSchemaClassGenerator.Metadata`.
490499

491500
Contributing
492501
------------

‎XmlSchemaClassGenerator.Tests/CollectionItemStringLengthDataAnnotationTests.cs‎

Lines changed: 443 additions & 0 deletions
Large diffs are not rendered by default.

‎XmlSchemaClassGenerator.Tests/MetadataHelperEmitterTests.cs‎

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,79 @@ private static void EnsureFractionDigitsAttributeEmitted(GeneratorConfiguration
7171
var emitterType = typeof(Generator).Assembly.GetType("XmlSchemaClassGenerator.Metadata.MetadataHelperEmitter", throwOnError: true);
7272
var emitter = Activator.CreateInstance(emitterType, configuration);
7373
Assert.NotNull(emitter);
74-
74+
7575
var method = emitterType.GetMethod("EnsureFractionDigitsAttributeEmitted", BindingFlags.Instance | BindingFlags.Public);
7676
Assert.NotNull(method);
7777

7878
method.Invoke(emitter, [codeNamespaces]);
7979
}
80+
81+
[Fact]
82+
public void EnsureCollectionItemStringLengthAttributeEmitted_AddsType_WhenNamespaceAlreadyExistsAndTypeIsMissing()
83+
{
84+
var configuration = new GeneratorConfiguration
85+
{
86+
EmitMetadataAttributes = true,
87+
MetadataNamespace = "Shared.Metadata",
88+
};
89+
90+
var metadataNamespace = new CodeNamespace("Shared.Metadata");
91+
var codeNamespaces = new List<CodeNamespace> { metadataNamespace };
92+
93+
EnsureCollectionItemStringLengthAttributeEmitted(configuration, codeNamespaces);
94+
95+
Assert.Single(metadataNamespace.Types.OfType<CodeTypeDeclaration>(), t => t.Name == "CollectionItemStringLengthAttribute");
96+
}
97+
98+
[Fact]
99+
public void EnsureCollectionItemStringLengthAttributeEmitted_DoesNotDuplicateType_WhenTypeAlreadyExists()
100+
{
101+
var configuration = new GeneratorConfiguration
102+
{
103+
EmitMetadataAttributes = true,
104+
MetadataNamespace = "Shared.Metadata",
105+
};
106+
107+
var metadataNamespace = new CodeNamespace("Shared.Metadata");
108+
metadataNamespace.Types.Add(new CodeTypeDeclaration("CollectionItemStringLengthAttribute"));
109+
var codeNamespaces = new List<CodeNamespace> { metadataNamespace };
110+
111+
EnsureCollectionItemStringLengthAttributeEmitted(configuration, codeNamespaces);
112+
113+
Assert.Single(metadataNamespace.Types.OfType<CodeTypeDeclaration>(), t => t.Name == "CollectionItemStringLengthAttribute");
114+
}
115+
116+
[Fact]
117+
public void EnsureCollectionItemStringLengthAttributeEmitted_AddsConfiguredImports_WhenCreatingMetadataNamespace()
118+
{
119+
var configuration = new GeneratorConfiguration
120+
{
121+
EmitMetadataAttributes = true,
122+
MetadataNamespace = "Shared.Metadata",
123+
CompactTypeNames = true,
124+
DataAnnotationMode = DataAnnotationMode.All,
125+
};
126+
127+
var codeNamespaces = new List<CodeNamespace>();
128+
129+
EnsureCollectionItemStringLengthAttributeEmitted(configuration, codeNamespaces);
130+
131+
var metadataNamespace = Assert.Single(codeNamespaces);
132+
var imports = metadataNamespace.Imports.Cast<CodeNamespaceImport>().Select(i => i.Namespace).ToList();
133+
134+
Assert.Contains("System", imports);
135+
Assert.Contains("System.ComponentModel.DataAnnotations", imports);
136+
}
137+
138+
private static void EnsureCollectionItemStringLengthAttributeEmitted(GeneratorConfiguration configuration, ICollection<CodeNamespace> codeNamespaces)
139+
{
140+
var emitterType = typeof(Generator).Assembly.GetType("XmlSchemaClassGenerator.Metadata.MetadataHelperEmitter", throwOnError: true);
141+
var emitter = Activator.CreateInstance(emitterType, configuration);
142+
Assert.NotNull(emitter);
143+
144+
var method = emitterType.GetMethod("EnsureCollectionItemStringLengthAttributeEmitted", BindingFlags.Instance | BindingFlags.Public);
145+
Assert.NotNull(method);
146+
147+
method.Invoke(emitter, [codeNamespaces]);
148+
}
80149
}

‎XmlSchemaClassGenerator/CodeUtilities.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,7 @@ internal static class Attributes
523523
private const string DataAnnotations = "System.ComponentModel.DataAnnotations";
524524
private const string CodeAnalysis = "System.Diagnostics.CodeAnalysis";
525525
internal const string FractionDigitsAttributeName = "FractionDigitsAttribute";
526+
internal const string CollectionItemStringLengthAttributeName = "CollectionItemStringLengthAttribute";
526527

527528
private static TypeInfo Make(string @namespace, [CallerMemberName] string name = null)
528529
=> new(@namespace, name + "Attribute");
@@ -536,6 +537,7 @@ private static TypeInfo Make(string @namespace, [CallerMemberName] string name =
536537
public static TypeInfo RegularExpression { get; } = Make(DataAnnotations);
537538
public static TypeInfo NotMapped { get; } = Make($"{DataAnnotations}.Schema");
538539
public static TypeInfo FractionDigits(string metadataNamespace) => new(metadataNamespace, FractionDigitsAttributeName);
540+
public static TypeInfo CollectionItemStringLength(string metadataNamespace) => new(metadataNamespace, CollectionItemStringLengthAttributeName);
539541

540542
public static TypeInfo AllowNull { get; } = Make(CodeAnalysis);
541543
public static TypeInfo MaybeNull { get; } = Make(CodeAnalysis);

‎XmlSchemaClassGenerator/Metadata/MetadataHelperEmitter.cs‎

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public MetadataHelperEmitter(GeneratorConfiguration configuration)
1616
}
1717

1818
public void EnsureFractionDigitsAttributeEmitted(ICollection<CodeNamespace> codeNamespaces)
19+
=> EnsureAttributeEmitted(codeNamespaces, Attributes.FractionDigitsAttributeName, CreateFractionDigitsAttributeType);
20+
21+
public void EnsureCollectionItemStringLengthAttributeEmitted(ICollection<CodeNamespace> codeNamespaces)
22+
=> EnsureAttributeEmitted(codeNamespaces, Attributes.CollectionItemStringLengthAttributeName, CreateCollectionItemStringLengthAttributeType);
23+
24+
private void EnsureAttributeEmitted(ICollection<CodeNamespace> codeNamespaces, string typeName, Func<CodeTypeDeclaration> createType)
1925
{
2026
if (!_configuration.EmitMetadataAttributes)
2127
return;
@@ -25,13 +31,13 @@ public void EnsureFractionDigitsAttributeEmitted(ICollection code
2531
if (existingNamespace == null)
2632
{
2733
var metadataNamespace = GenerateMetadataNamespace(_configuration.MetadataNamespace);
28-
metadataNamespace.Types.Add(CreateFractionDigitsAttributeType());
34+
metadataNamespace.Types.Add(createType());
2935
codeNamespaces.Add(metadataNamespace);
3036
return;
3137
}
3238

33-
if (!ContainsType(existingNamespace, Attributes.FractionDigitsAttributeName))
34-
existingNamespace.Types.Add(CreateFractionDigitsAttributeType());
39+
if (!ContainsType(existingNamespace, typeName))
40+
existingNamespace.Types.Add(createType());
3541
}
3642

3743
private static bool ContainsType(CodeNamespace codeNamespace, string typeName)
@@ -49,17 +55,7 @@ private CodeNamespace GenerateMetadataNamespace(string namespaceName)
4955

5056
private CodeTypeDeclaration CreateFractionDigitsAttributeType()
5157
{
52-
var attribute = new CodeTypeDeclaration(Attributes.FractionDigitsAttributeName)
53-
{
54-
IsClass = true,
55-
TypeAttributes = TypeAttributes.Public | TypeAttributes.Sealed,
56-
};
57-
attribute.BaseTypes.Add(CodeUtilities.CreateTypeReference(typeof(Attribute), _configuration));
58-
59-
attribute.CustomAttributes.Add(new CodeAttributeDeclaration(
60-
CodeUtilities.CreateTypeReference(typeof(AttributeUsageAttribute), _configuration),
61-
new CodeAttributeArgument(new CodeSnippetExpression("System.AttributeTargets.Property | System.AttributeTargets.Field")),
62-
new CodeAttributeArgument("AllowMultiple", new CodePrimitiveExpression(false))));
58+
var attribute = CreateAttributeTypeShell(Attributes.FractionDigitsAttributeName);
6359

6460
attribute.Members.Add(new CodeMemberField(typeof(int), "_fractionDigits")
6561
{
@@ -92,4 +88,75 @@ private CodeTypeDeclaration CreateFractionDigitsAttributeType()
9288

9389
return attribute;
9490
}
91+
92+
private CodeTypeDeclaration CreateCollectionItemStringLengthAttributeType()
93+
{
94+
var attribute = CreateAttributeTypeShell(Attributes.CollectionItemStringLengthAttributeName);
95+
96+
attribute.Members.Add(new CodeMemberField(typeof(int), "_maximumLength")
97+
{
98+
Attributes = MemberAttributes.Private,
99+
});
100+
attribute.Members.Add(new CodeMemberField(typeof(int), "_minimumLength")
101+
{
102+
Attributes = MemberAttributes.Private,
103+
});
104+
105+
var constructor = new CodeConstructor
106+
{
107+
Attributes = MemberAttributes.Public,
108+
};
109+
constructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(int), "maximumLength"));
110+
constructor.Statements.Add(new CodeConditionStatement(
111+
new CodeBinaryOperatorExpression(new CodeArgumentReferenceExpression("maximumLength"), CodeBinaryOperatorType.LessThan, new CodePrimitiveExpression(0)),
112+
new CodeThrowExceptionStatement(new CodeObjectCreateExpression(typeof(ArgumentOutOfRangeException), new CodePrimitiveExpression("maximumLength")))));
113+
constructor.Statements.Add(new CodeAssignStatement(
114+
new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_maximumLength"),
115+
new CodeArgumentReferenceExpression("maximumLength")));
116+
attribute.Members.Add(constructor);
117+
118+
var maximumLengthProperty = new CodeMemberProperty
119+
{
120+
Name = "MaximumLength",
121+
Type = CodeUtilities.CreateTypeReference(typeof(int), _configuration),
122+
Attributes = MemberAttributes.Public | MemberAttributes.Final,
123+
HasGet = true,
124+
HasSet = false,
125+
};
126+
maximumLengthProperty.GetStatements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_maximumLength")));
127+
attribute.Members.Add(maximumLengthProperty);
128+
129+
var minimumLengthProperty = new CodeMemberProperty
130+
{
131+
Name = "MinimumLength",
132+
Type = CodeUtilities.CreateTypeReference(typeof(int), _configuration),
133+
Attributes = MemberAttributes.Public | MemberAttributes.Final,
134+
HasGet = true,
135+
HasSet = true,
136+
};
137+
minimumLengthProperty.GetStatements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_minimumLength")));
138+
minimumLengthProperty.SetStatements.Add(new CodeAssignStatement(
139+
new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_minimumLength"),
140+
new CodePropertySetValueReferenceExpression()));
141+
attribute.Members.Add(minimumLengthProperty);
142+
143+
return attribute;
144+
}
145+
146+
private CodeTypeDeclaration CreateAttributeTypeShell(string typeName)
147+
{
148+
var attribute = new CodeTypeDeclaration(typeName)
149+
{
150+
IsClass = true,
151+
TypeAttributes = TypeAttributes.Public | TypeAttributes.Sealed,
152+
};
153+
attribute.BaseTypes.Add(CodeUtilities.CreateTypeReference(typeof(Attribute), _configuration));
154+
155+
attribute.CustomAttributes.Add(new CodeAttributeDeclaration(
156+
CodeUtilities.CreateTypeReference(typeof(AttributeUsageAttribute), _configuration),
157+
new CodeAttributeArgument(new CodeSnippetExpression("System.AttributeTargets.Property | System.AttributeTargets.Field")),
158+
new CodeAttributeArgument("AllowMultiple", new CodePrimitiveExpression(false))));
159+
160+
return attribute;
161+
}
95162
}

‎XmlSchemaClassGenerator/ModelBuilder.cs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,12 @@ public IEnumerable GenerateCode()
12521252
metadataHelperEmitter.EnsureFractionDigitsAttributeEmitted(codeNamespaces);
12531253
}
12541254

1255+
if (HasSupportedCollectionItemStringLengthRestrictions())
1256+
{
1257+
var metadataHelperEmitter = new MetadataHelperEmitter(_configuration);
1258+
metadataHelperEmitter.EnsureCollectionItemStringLengthAttributeEmitted(codeNamespaces);
1259+
}
1260+
12551261
return codeNamespaces;
12561262
}
12571263

@@ -1260,6 +1266,12 @@ private bool HasSupportedFractionDigitsRestrictions()
12601266
.OfType<SimpleModel>()
12611267
.Any(model => model.Restrictions.OfType<FractionDigitsRestrictionModel>().Any(restriction => restriction.IsSupported));
12621268

1269+
private bool HasSupportedCollectionItemStringLengthRestrictions()
1270+
=> Types.Values
1271+
.OfType<ReferenceTypeModel>()
1272+
.SelectMany(model => model.Properties)
1273+
.Any(property => property.HasCollectionItemStringLengthAttribute);
1274+
12631275
private string BuildNamespace(Uri source, string xmlNamespace)
12641276
{
12651277
var key = new NamespaceKey(source, xmlNamespace);

‎XmlSchemaClassGenerator/Models/PropertyModel.cs‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,29 @@ private void AddDocs(CodeTypeMember member)
155155

156156
AddDescription(member.CustomAttributes, docs);
157157

158-
if (PropertyType is SimpleModel simpleType && !IsEnumerable)
158+
if (PropertyType is SimpleModel simpleType)
159159
{
160-
docs.AddRange(simpleType.Documentation);
161-
docs.AddRange(simpleType.Restrictions.Select(r => new DocumentationModel { Language = English, Text = r.Description }));
162-
member.CustomAttributes.AddRange(simpleType.GetRestrictionAttributes().ToArray());
160+
if (!IsEnumerable)
161+
{
162+
docs.AddRange(simpleType.Documentation);
163+
docs.AddRange(simpleType.Restrictions.Select(r => new DocumentationModel { Language = English, Text = r.Description }));
164+
member.CustomAttributes.AddRange(simpleType.GetRestrictionAttributes().ToArray());
165+
}
166+
else if (simpleType.GetCollectionItemStringLengthAttribute() is { } collectionAttribute)
167+
{
168+
member.CustomAttributes.Add(collectionAttribute);
169+
docs.AddRange(simpleType.Restrictions
170+
.Where(r => r is MinLengthRestrictionModel or MaxLengthRestrictionModel or MinMaxLengthRestrictionModel)
171+
.Select(r => new DocumentationModel { Language = English, Text = r.Description }));
172+
}
163173
}
164174

165175
member.Comments.AddRange(GetComments(docs).ToArray());
166176
}
167177

178+
internal bool HasCollectionItemStringLengthAttribute
179+
=> IsEnumerable && PropertyType is SimpleModel simpleType && simpleType.GetCollectionItemStringLengthAttribute() != null;
180+
168181
private CodeAttributeDeclaration CreateDefaultValueAttribute(CodeTypeReference typeReference, CodeExpression defaultValueExpression)
169182
{
170183
var defaultValueAttribute = AttributeDecl<DefaultValueAttribute>();

‎XmlSchemaClassGenerator/Models/SimpleModel.cs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,29 @@ public IEnumerable GetRestrictionAttributes()
203203
yield return rangeAttribute;
204204
}
205205
}
206+
207+
public CodeAttributeDeclaration GetCollectionItemStringLengthAttribute()
208+
{
209+
if (!Configuration.EmitMetadataAttributes)
210+
return null;
211+
212+
var minMax = Restrictions.OfType<MinMaxLengthRestrictionModel>().FirstOrDefault();
213+
var minLength = Restrictions.OfType<MinLengthRestrictionModel>().FirstOrDefault();
214+
var maxLength = Restrictions.OfType<MaxLengthRestrictionModel>().FirstOrDefault();
215+
216+
var min = minMax?.Min ?? minLength?.Value ?? 0;
217+
var max = minMax?.Max ?? maxLength?.Value ?? 0;
218+
219+
if (min <= 0 && max <= 0)
220+
return null;
221+
222+
var attribute = new CodeAttributeDeclaration(
223+
CodeUtilities.CreateTypeReference(Attributes.CollectionItemStringLength(Configuration.MetadataNamespace), Configuration),
224+
new CodeAttributeArgument(max > 0 ? new CodePrimitiveExpression(max) : new CodeSnippetExpression("int.MaxValue")));
225+
226+
if (min > 0)
227+
attribute.Arguments.Add(new CodeAttributeArgument("MinimumLength", new CodePrimitiveExpression(min)));
228+
229+
return attribute;
230+
}
206231
}

0 commit comments

Comments
 (0)