Skip to content

Commit 8eb4a57

Browse files
authored
⚗️ choice group experiment with analyzer (#14)
1 parent 5cb53bd commit 8eb4a57

10 files changed

Lines changed: 344 additions & 4 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,3 +424,4 @@ src/siri/**/Generated/
424424
# Local xscgen pre-release package (band-aid until upstream merges).
425425
# Must appear after both the *.nupkg and **/[Pp]ackages/* rules to negate them.
426426
!**/[Pp]ackages/XmlSchemaClassGenerator-beta.*.nupkg
427+
!**/[Pp]ackages/XmlSchemaClassGenerator*.nupkg
-151 KB
Binary file not shown.
155 KB
Binary file not shown.
7.76 KB
Binary file not shown.

src/Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
<PackageVersion Include="MinVer" Version="6.0.0" />
99

1010
<!-- Generator dependencies -->
11-
<PackageVersion Include="XmlSchemaClassGenerator-beta" Version="99.0.5-local" />
11+
<PackageVersion Include="XmlSchemaClassGenerator-beta" Version="99.0.7-local" />
12+
<PackageVersion Include="XmlSchemaClassGenerator.Analyzer" Version="99.0.7-local" />
1213
<PackageVersion Include="System.CommandLine" Version="2.0.2" />
1314

1415
<!-- Test dependencies -->

src/generator/Spillgebees.Transmodel.Generator/Services/CodeGenerator.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ [new NamespaceKey(SharedDefaults.GmlXmlNamespace)] = gmlNamespace,
2727
[new NamespaceKey(SharedDefaults.W3XmlNamespace)] = w3Namespace
2828
};
2929

30-
var generator = CreateBaseGenerator(outputDirectory, namespaceProvider, verbose);
30+
var generator = CreateBaseGenerator(outputDirectory, rootNamespace, namespaceProvider, verbose);
3131
// Set NamingProvider after the object initializer to avoid being overwritten
3232
// by any NamingScheme setter. This renames abstract dummy elements (e.g. 'StopPlace_')
3333
// so that concrete elements get the clean C# class name (e.g. 'StopPlace').
@@ -91,7 +91,7 @@ [new NamespaceKey(SharedDefaults.GmlXmlNamespace)] = gmlNamespace,
9191
[new NamespaceKey(SharedDefaults.W3XmlNamespace)] = w3Namespace
9292
};
9393

94-
var generator = CreateBaseGenerator(outputDirectory, namespaceProvider, verbose);
94+
var generator = CreateBaseGenerator(outputDirectory, rootNamespace, namespaceProvider, verbose);
9595
// SIRI doesn't have the underscore naming convention, use default PascalCase
9696
generator.NamingProvider = new NamingProvider(NamingScheme.PascalCase);
9797

@@ -115,6 +115,7 @@ [new NamespaceKey(SharedDefaults.W3XmlNamespace)] = w3Namespace
115115

116116
private static XscGenerator CreateBaseGenerator(
117117
string outputDirectory,
118+
string rootNamespace,
118119
NamespaceProvider namespaceProvider,
119120
bool verbose)
120121
{
@@ -143,10 +144,12 @@ private static XscGenerator CreateBaseGenerator(
143144
CollectionType = typeof(List<>),
144145
EnumCollection = true,
145146

146-
// Nullable and required
147+
// Nullable, required, and choice groups
147148
EnableNullableDirective = true,
148149
GenerateRequiredModifier = true,
149150
UseShouldSerializePattern = true,
151+
GenerateChoiceGroupAttributes = true,
152+
ChoiceGroupAttributeNamespace = rootNamespace,
150153

151154
// Minimal attribute noise
152155
GenerateSerializableAttribute = false,

src/netex/NeTEx.Models.targets

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
<None Include="../Spillgebees.NeTEx.Models/README.md" Pack="true" PackagePath="" />
1919
</ItemGroup>
2020

21+
<ItemGroup>
22+
<PackageReference Include="XmlSchemaClassGenerator.Analyzer" PrivateAssets="none">
23+
<IncludeAssets>analyzers</IncludeAssets>
24+
</PackageReference>
25+
</ItemGroup>
26+
2127
<!-- Trigger generator to run before compilation if NetexVersion is set and no generated files exist -->
2228
<Target Name="GenerateNetexModels"
2329
Condition="'$(NetexVersion)' != ''"
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System.Reflection;
2+
using AwesomeAssertions;
3+
using Spillgebees.NeTEx.Models.V1_3_1;
4+
using Spillgebees.NeTEx.Models.V1_3_1.NeTEx;
5+
6+
namespace Spillgebees.NeTEx.Models.Tests;
7+
8+
/// <summary>
9+
/// Tests verifying that the generated NeTEx v1.3.1 models carry correct
10+
/// <see cref="XmlChoiceGroupAttribute"/> annotations on properties that
11+
/// originate from <c>xs:choice</c> groups in the NeTEx XSD schemas.
12+
/// </summary>
13+
public class ChoiceGroupAttributeTests
14+
{
15+
// -- helpers ------------------------------------------------------------------
16+
17+
private static XmlChoiceGroupAttribute? GetChoiceGroupAttribute<T>(string propertyName) =>
18+
typeof(T).GetProperty(propertyName)?
19+
.GetCustomAttribute<XmlChoiceGroupAttribute>();
20+
21+
private static XmlChoiceGroupAttribute GetRequiredChoiceGroupAttribute<T>(string propertyName)
22+
{
23+
var attr = GetChoiceGroupAttribute<T>(propertyName);
24+
attr.Should().NotBeNull($"property {typeof(T).Name}.{propertyName} should have [XmlChoiceGroupAttribute]");
25+
return attr!;
26+
}
27+
28+
// -- DistanceMatrixElementDerivedViewStructure: two parallel choice groups -----
29+
30+
[Test]
31+
public void Should_have_choice_group_on_start_stop_point_ref()
32+
{
33+
var attr = GetRequiredChoiceGroupAttribute<DistanceMatrixElementDerivedViewStructure>(
34+
nameof(DistanceMatrixElementDerivedViewStructure.StartStopPointRef));
35+
36+
attr.ArmId.Should().Be(0);
37+
}
38+
39+
[Test]
40+
public void Should_have_choice_group_on_start_tariff_zone_ref()
41+
{
42+
var attr = GetRequiredChoiceGroupAttribute<DistanceMatrixElementDerivedViewStructure>(
43+
nameof(DistanceMatrixElementDerivedViewStructure.StartTariffZoneRef));
44+
45+
attr.ArmId.Should().Be(1);
46+
}
47+
48+
[Test]
49+
public void Should_have_same_group_id_for_start_choice()
50+
{
51+
var stopRef = GetRequiredChoiceGroupAttribute<DistanceMatrixElementDerivedViewStructure>(
52+
nameof(DistanceMatrixElementDerivedViewStructure.StartStopPointRef));
53+
var zoneRef = GetRequiredChoiceGroupAttribute<DistanceMatrixElementDerivedViewStructure>(
54+
nameof(DistanceMatrixElementDerivedViewStructure.StartTariffZoneRef));
55+
56+
stopRef.GroupId.Should().Be(zoneRef.GroupId);
57+
}
58+
59+
[Test]
60+
public void Should_have_choice_group_on_end_stop_point_ref()
61+
{
62+
var attr = GetRequiredChoiceGroupAttribute<DistanceMatrixElementDerivedViewStructure>(
63+
nameof(DistanceMatrixElementDerivedViewStructure.EndStopPointRef));
64+
65+
attr.ArmId.Should().Be(0);
66+
}
67+
68+
[Test]
69+
public void Should_have_choice_group_on_end_tariff_zone_ref()
70+
{
71+
var attr = GetRequiredChoiceGroupAttribute<DistanceMatrixElementDerivedViewStructure>(
72+
nameof(DistanceMatrixElementDerivedViewStructure.EndTariffZoneRef));
73+
74+
attr.ArmId.Should().Be(1);
75+
}
76+
77+
[Test]
78+
public void Should_have_same_group_id_for_end_choice()
79+
{
80+
var stopRef = GetRequiredChoiceGroupAttribute<DistanceMatrixElementDerivedViewStructure>(
81+
nameof(DistanceMatrixElementDerivedViewStructure.EndStopPointRef));
82+
var zoneRef = GetRequiredChoiceGroupAttribute<DistanceMatrixElementDerivedViewStructure>(
83+
nameof(DistanceMatrixElementDerivedViewStructure.EndTariffZoneRef));
84+
85+
stopRef.GroupId.Should().Be(zoneRef.GroupId);
86+
}
87+
88+
[Test]
89+
public void Should_have_different_group_ids_for_start_and_end_choices()
90+
{
91+
var startAttr = GetRequiredChoiceGroupAttribute<DistanceMatrixElementDerivedViewStructure>(
92+
nameof(DistanceMatrixElementDerivedViewStructure.StartStopPointRef));
93+
var endAttr = GetRequiredChoiceGroupAttribute<DistanceMatrixElementDerivedViewStructure>(
94+
nameof(DistanceMatrixElementDerivedViewStructure.EndStopPointRef));
95+
96+
startAttr.GroupId.Should().NotBe(endAttr.GroupId);
97+
}
98+
99+
[Test]
100+
public void Should_not_have_choice_group_on_non_choice_property()
101+
{
102+
// StartName is not part of any choice group
103+
var attr = GetChoiceGroupAttribute<DistanceMatrixElementDerivedViewStructure>("StartName");
104+
105+
attr.Should().BeNull();
106+
}
107+
108+
// -- FareStructureElementVersionStructure: multiple choice groups in NeTEx -----
109+
110+
[Test]
111+
public void Should_have_multiple_distinct_choice_groups_on_fare_structure_element()
112+
{
113+
var properties = typeof(FareStructureElementVersionStructure).GetProperties();
114+
var choiceGroupIds = properties
115+
.Select(p => p.GetCustomAttribute<XmlChoiceGroupAttribute>())
116+
.Where(a => a is not null)
117+
.Select(a => a!.GroupId)
118+
.Distinct()
119+
.ToList();
120+
121+
choiceGroupIds.Count.Should().BeGreaterThanOrEqualTo(2);
122+
}
123+
}

src/siri/SIRI.Models.targets

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
<None Include="../Spillgebees.SIRI.Models/README.md" Pack="true" PackagePath="" />
1919
</ItemGroup>
2020

21+
<ItemGroup>
22+
<PackageReference Include="XmlSchemaClassGenerator.Analyzer" PrivateAssets="none">
23+
<IncludeAssets>analyzers</IncludeAssets>
24+
</PackageReference>
25+
</ItemGroup>
26+
2127
<!-- Trigger generator to run before compilation if SiriVersion is set and no generated files exist -->
2228
<Target Name="GenerateSiriModels"
2329
Condition="'$(SiriVersion)' != ''"

0 commit comments

Comments
 (0)