diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..409579c
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,150 @@
+# AGENTS.md
+
+Guidelines for AI agents operating in this repository.
+
+## Project overview
+
+C# XML bindings for [SIRI](https://www.siri-cen.eu/) and [NeTEx](https://netex-cen.eu/) public transport schemas. A CLI generator (`Spillgebees.Transmodel.Generator`) downloads official XSD schemas and produces C# classes via a fork of `XmlSchemaClassGenerator`. Version-specific model projects (e.g. `Spillgebees.NeTEx.Models.V1_3_1`) are thin wrappers whose `Generated/` contents are created at build time.
+
+## Build / test / lint
+
+```bash
+# Build (from repo root)
+dotnet build Spillgebees.Transmodel.slnx
+
+# Run all tests
+dotnet test --solution Spillgebees.Transmodel.slnx
+
+# Run a single test by name
+dotnet test --solution Spillgebees.Transmodel.slnx --filter "FullyQualifiedName~Should_serialize_and_deserialize_stop_place"
+
+# Run tests in one project
+dotnet test src/netex/Spillgebees.NeTEx.Models.Tests
+
+# Clean (removes Generated/ dirs; next build regenerates them)
+dotnet clean Spillgebees.Transmodel.slnx
+```
+
+There is no separate lint command. `TreatWarningsAsErrors=True` is set globally in `src/General.targets`, so building IS linting. The `.editorconfig` at `src/.editorconfig` configures all Roslyn analyzers.
+
+## Critical rules
+
+1. **Never edit files under `Generated/` directories.** They are machine-generated, `.gitignore`d, and recreated every build. All fixes must go in the xscgen fork or the generator.
+2. **Never manually set package versions in `.csproj` files.** Use `src/Directory.Packages.props` (central package management).
+3. **Never add other target frameworks.** The project targets `net10.0` only.
+4. **`.targets` files are shared MSBuild imports.** Changes to `General.targets`, `SIRI.Models.targets`, or `NeTEx.Models.targets` affect every project that imports them.
+5. **The `packages/` directory contains vendored local `.nupkg` files** for a pre-release xscgen fork. These are intentionally committed.
+
+## Project structure
+
+```
+Spillgebees.Transmodel.slnx Solution file
+global.json .NET 10 SDK pinning
+nuget.config NuGet sources (nuget.org + local packages/)
+packages/ Vendored local nupkgs (xscgen fork)
+src/
+ .editorconfig Code style / analyzer rules
+ Directory.Packages.props Central package version management
+ General.targets Shared: TFM, nullable, TreatWarningsAsErrors
+ generator/
+ Spillgebees.Transmodel.Generator/ CLI tool (System.CommandLine)
+ siri/
+ SIRI.Models.targets NuGet metadata + build-time generation
+ Spillgebees.SIRI.Models/ Meta-package (all SIRI versions)
+ Spillgebees.SIRI.Models.V2_1/ SIRI v2.1 bindings
+ Spillgebees.SIRI.Models.V2_2/ SIRI v2.2 bindings
+ Spillgebees.SIRI.Models.Tests/ Tests
+ netex/
+ NeTEx.Models.targets NuGet metadata + build-time generation
+ Spillgebees.NeTEx.Models/ Meta-package (all NeTEx versions)
+ Spillgebees.NeTEx.Models.V1_*/ Version-specific bindings (5 versions)
+ Spillgebees.NeTEx.Models.Tests/ Tests
+```
+
+## Code style
+
+Enforced by `src/.editorconfig` and `TreatWarningsAsErrors`. Key rules:
+
+### Formatting
+- **No region markers or decorative comment dividers.** Do not use comments like `// -- section name -----` or `#region`/`#endregion`. If a file needs sectioning, it should likely be split into separate files instead.
+- **4 spaces** for C#; **2 spaces** for XML/csproj/json/yaml
+- **Allman braces** (opening brace on new line for all constructs)
+- **Always use braces**, even for single-line `if`/`for`/etc.
+- **File-scoped namespaces** (`namespace Foo;` not `namespace Foo { }`)
+- No multiple consecutive blank lines
+
+### Types and keywords
+- **Use `var` everywhere** (built-in types, apparent types, elsewhere)
+- **Use predefined type keywords** (`int` not `Int32`, `string` not `String`)
+- **Nullable reference types** are enabled globally
+- Prefer expression-bodied members, object/collection initializers, pattern matching, null propagation
+
+### Naming conventions
+| Symbol | Convention | Example |
+|---|---|---|
+| Constants | PascalCase | `MaxRetries` |
+| Private/internal fields | `_camelCase` | `_groupId` |
+| Methods, properties | PascalCase | `GenerateCode()` |
+| Local variables | camelCase | `rootNamespace` |
+
+- No `this.` qualifier
+- Modifier order: `public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async`
+
+### Imports
+- System namespaces first (`dotnet_sort_system_directives_first = true`)
+- Placed outside the namespace declaration
+- Implicit usings are enabled (`System`, `System.Collections.Generic`, `System.Linq`, etc. are available without explicit `using`)
+
+## Test conventions
+
+### Framework
+- **TUnit** (not xUnit/NUnit/MSTest) with `[Test]` attribute
+- **AwesomeAssertions** for fluent assertions (`.Should().Be(...)`, `.Should().NotBeNull()`)
+- Test runner: `Microsoft.Testing.Platform` (configured in `global.json`)
+
+### Naming
+Tests use `Should_describe_expected_behavior` in snake_case:
+```csharp
+[Test]
+public void Should_serialize_and_deserialize_stop_place_with_stop_place_type()
+```
+
+### Structure
+Use Arrange/Act/Assert with comments:
+```csharp
+[Test]
+public void Should_round_trip_multilingual_string()
+{
+ // arrange
+ var serializer = new XmlSerializer(typeof(MultilingualString));
+ var original = new MultilingualString { Value = "hello", Lang = "en" };
+
+ // act
+ using var writer = new StringWriter();
+ serializer.Serialize(writer, original);
+ var xml = writer.ToString();
+
+ using var reader = new StringReader(xml);
+ var deserialized = (MultilingualString?)serializer.Deserialize(reader);
+
+ // assert
+ deserialized.Should().NotBeNull();
+ deserialized!.Value.Should().Be("hello");
+}
+```
+
+### Organization
+- `Smoke/` -- Type existence and XML namespace verification
+- `Serialization/` -- XmlSerializer round-trip tests
+- `Deserialization/` -- XML parsing tests (with `TestData/` fixtures)
+- Root level -- Cross-cutting concerns (choice groups, nullability, required modifiers)
+
+Test classes are plain `public class` with no base class or constructor injection. Test projects reference multiple versioned model assemblies.
+
+## Tooling
+
+- **SDK**: .NET 10.0 (`global.json` pins `10.0.102`, rolls forward within feature band)
+- **Versioning**: MinVer (automatic from Git tags, no manual versions in csproj)
+- **Reproducible builds**: `DotNet.ReproducibleBuilds` package
+- **License**: EUPL-1.2
+- **CI**: GitHub Actions (`.github/workflows/`) -- build, test, pack, publish to nuget.org on release
diff --git a/packages/XmlSchemaClassGenerator-beta.99.0.7-local.nupkg b/packages/XmlSchemaClassGenerator-beta.99.0.7-local.nupkg
deleted file mode 100644
index 9380141..0000000
Binary files a/packages/XmlSchemaClassGenerator-beta.99.0.7-local.nupkg and /dev/null differ
diff --git a/packages/XmlSchemaClassGenerator-beta.99.0.8-local.nupkg b/packages/XmlSchemaClassGenerator-beta.99.0.8-local.nupkg
new file mode 100644
index 0000000..1ab031b
Binary files /dev/null and b/packages/XmlSchemaClassGenerator-beta.99.0.8-local.nupkg differ
diff --git a/packages/XmlSchemaClassGenerator.Analyzer.99.0.7-local.nupkg b/packages/XmlSchemaClassGenerator.Analyzer.99.0.7-local.nupkg
deleted file mode 100644
index 369954e..0000000
Binary files a/packages/XmlSchemaClassGenerator.Analyzer.99.0.7-local.nupkg and /dev/null differ
diff --git a/packages/XmlSchemaClassGenerator.Analyzer.99.0.8-local.nupkg b/packages/XmlSchemaClassGenerator.Analyzer.99.0.8-local.nupkg
new file mode 100644
index 0000000..e1ed1f6
Binary files /dev/null and b/packages/XmlSchemaClassGenerator.Analyzer.99.0.8-local.nupkg differ
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 54d0b1d..3fa9c93 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -8,8 +8,8 @@
-
-
+
+
diff --git a/src/generator/Spillgebees.Transmodel.Generator/Services/CodeGenerator.cs b/src/generator/Spillgebees.Transmodel.Generator/Services/CodeGenerator.cs
index a84eb01..81436c3 100644
--- a/src/generator/Spillgebees.Transmodel.Generator/Services/CodeGenerator.cs
+++ b/src/generator/Spillgebees.Transmodel.Generator/Services/CodeGenerator.cs
@@ -150,6 +150,8 @@ private static XscGenerator CreateBaseGenerator(
UseShouldSerializePattern = true,
GenerateChoiceGroupAttributes = true,
ChoiceGroupAttributeNamespace = rootNamespace,
+ GenerateStrictFixedValues = true,
+ GenerateStrictRangeBounds = true,
// Minimal attribute noise
GenerateSerializableAttribute = false,
diff --git a/src/netex/Spillgebees.NeTEx.Models.Tests/ChoiceGroupAttributeTests.cs b/src/netex/Spillgebees.NeTEx.Models.Tests/ChoiceGroupAttributeTests.cs
index 030768f..fe8a4cb 100644
--- a/src/netex/Spillgebees.NeTEx.Models.Tests/ChoiceGroupAttributeTests.cs
+++ b/src/netex/Spillgebees.NeTEx.Models.Tests/ChoiceGroupAttributeTests.cs
@@ -12,8 +12,6 @@ namespace Spillgebees.NeTEx.Models.Tests;
///
public class ChoiceGroupAttributeTests
{
- // -- helpers ------------------------------------------------------------------
-
private static XmlChoiceGroupAttribute? GetChoiceGroupAttribute(string propertyName) =>
typeof(T).GetProperty(propertyName)?
.GetCustomAttribute();
@@ -25,8 +23,6 @@ private static XmlChoiceGroupAttribute GetRequiredChoiceGroupAttribute(string
return attr!;
}
- // -- DistanceMatrixElementDerivedViewStructure: two parallel choice groups -----
-
[Test]
public void Should_have_choice_group_on_start_stop_point_ref()
{
@@ -105,8 +101,6 @@ public void Should_not_have_choice_group_on_non_choice_property()
attr.Should().BeNull();
}
- // -- FareStructureElementVersionStructure: multiple choice groups in NeTEx -----
-
[Test]
public void Should_have_multiple_distinct_choice_groups_on_fare_structure_element()
{
diff --git a/src/netex/Spillgebees.NeTEx.Models.Tests/NullabilityAndRequiredTests.cs b/src/netex/Spillgebees.NeTEx.Models.Tests/NullabilityAndRequiredTests.cs
index 3181e09..d2f4a74 100644
--- a/src/netex/Spillgebees.NeTEx.Models.Tests/NullabilityAndRequiredTests.cs
+++ b/src/netex/Spillgebees.NeTEx.Models.Tests/NullabilityAndRequiredTests.cs
@@ -16,8 +16,6 @@ namespace Spillgebees.NeTEx.Models.Tests;
///
public class NullabilityAndRequiredTests
{
- // -- behavioral: required properties ------------------------------------------
-
[Test]
public void Should_construct_publication_delivery_with_required_properties()
{
@@ -64,8 +62,6 @@ public void Should_construct_natural_language_string_with_required_value()
str.Value.Should().Be("hello");
}
- // -- behavioral: optional nullable properties ---------------------------------
-
[Test]
public void Should_allow_null_on_optional_reference_type_properties()
{
@@ -101,8 +97,6 @@ public void Should_allow_null_on_optional_string_attribute()
str.Lang.Should().BeNull();
}
- // -- behavioral: XmlSerializer bypasses required ------------------------------
-
[Test]
public void Should_round_trip_required_properties_via_xml_serializer()
{
@@ -125,8 +119,6 @@ public void Should_round_trip_required_properties_via_xml_serializer()
result.Lang.Should().BeNull();
}
- // -- behavioral: collections are not required ---------------------------------
-
[Test]
public void Should_allow_creating_organisation_without_setting_collection()
{
@@ -142,8 +134,6 @@ public void Should_allow_creating_organisation_without_setting_collection()
org.OrganisationType.Should().NotBeNull();
}
- // -- metadata: required modifier via reflection -------------------------------
-
[Test]
public void Should_have_required_modifier_on_publication_timestamp()
{
@@ -246,8 +236,6 @@ public void Should_not_have_required_modifier_on_collection_property()
isRequired.Should().BeFalse();
}
- // -- metadata: nullable annotations via reflection ----------------------------
-
[Test]
public void Should_have_nullable_annotation_on_optional_reference_type_property()
{
@@ -344,8 +332,6 @@ public void Should_not_have_nullable_annotation_on_min_length_constrained_xml_te
info.ReadState.Should().Be(NullabilityState.NotNull);
}
- // -- metadata: #nullable enable directive -------------------------------------
-
[Test]
public void Should_have_nullable_context_enabled_on_generated_types()
{
diff --git a/src/netex/Spillgebees.NeTEx.Models.Tests/XsdTypeMappingTests.cs b/src/netex/Spillgebees.NeTEx.Models.Tests/XsdTypeMappingTests.cs
new file mode 100644
index 0000000..cbb9108
--- /dev/null
+++ b/src/netex/Spillgebees.NeTEx.Models.Tests/XsdTypeMappingTests.cs
@@ -0,0 +1,242 @@
+using System.ComponentModel;
+using System.Reflection;
+using System.Xml.Serialization;
+using AwesomeAssertions;
+using Spillgebees.NeTEx.Models.V1_3_1.NeTEx;
+
+namespace Spillgebees.NeTEx.Models.Tests;
+
+///
+/// Tests verifying that the xscg fork correctly maps XSD types to C# types,
+/// generates default values for XSD fixed/default attributes, applies the
+/// ShouldSerialize pattern for optional value types, uses init setters on
+/// collections, and renames substitution group heads via NetexNamingProvider.
+///
+public class XsdTypeMappingTests
+{
+ [Test]
+ public void Should_map_xs_date_to_date_only_for_required_property()
+ {
+ // arrange & act — OperatingDayVersionStructure.CalendarDate is xs:date
+ // with minOccurs=1, mapped to System.DateOnly with the required modifier.
+ var property = typeof(OperatingDayVersionStructure)
+ .GetProperty(nameof(OperatingDayVersionStructure.CalendarDate))!;
+
+ // assert
+ property.PropertyType.Should().Be(typeof(DateOnly));
+ }
+
+ [Test]
+ public void Should_map_xs_date_to_nullable_date_only_for_optional_property()
+ {
+ // arrange & act — CustomerVersionStructure.DateOfBirth is an optional xs:date,
+ // mapped to Nullable.
+ var property = typeof(CustomerVersionStructure)
+ .GetProperty(nameof(CustomerVersionStructure.DateOfBirth))!;
+
+ // assert
+ property.PropertyType.Should().Be(typeof(DateOnly?));
+ }
+
+ [Test]
+ public void Should_map_xs_date_time_to_date_time_offset_for_required_property()
+ {
+ // arrange & act — ClosedTimestampRangeStructure.StartTime is xs:dateTime
+ // with minOccurs=1, mapped to DateTimeOffset with the required modifier.
+ var property = typeof(ClosedTimestampRangeStructure)
+ .GetProperty(nameof(ClosedTimestampRangeStructure.StartTime))!;
+
+ // assert
+ property.PropertyType.Should().Be(typeof(DateTimeOffset));
+ }
+
+ [Test]
+ public void Should_map_xs_date_time_to_nullable_date_time_offset_for_optional_property()
+ {
+ // arrange & act — CustomerVersionStructure.EmailVerified is an optional
+ // xs:dateTime, mapped to Nullable.
+ var property = typeof(CustomerVersionStructure)
+ .GetProperty(nameof(CustomerVersionStructure.EmailVerified))!;
+
+ // assert
+ property.PropertyType.Should().Be(typeof(DateTimeOffset?));
+ }
+
+ [Test]
+ public void Should_round_trip_closed_timestamp_range_via_xml_serializer()
+ {
+ // arrange
+ var serializer = new XmlSerializer(typeof(ClosedTimestampRangeStructure));
+ var original = new ClosedTimestampRangeStructure
+ {
+ StartTime = new DateTimeOffset(2026, 6, 1, 8, 0, 0, TimeSpan.FromHours(2)),
+ EndTime = new DateTimeOffset(2026, 6, 1, 18, 0, 0, TimeSpan.FromHours(2)),
+ };
+
+ // act
+ using var writer = new StringWriter();
+ serializer.Serialize(writer, original);
+ var xml = writer.ToString();
+
+ using var reader = new StringReader(xml);
+ var deserialized = serializer.Deserialize(reader) as ClosedTimestampRangeStructure;
+
+ // assert
+ deserialized.Should().NotBeNull();
+ deserialized.StartTime.Should().Be(original.StartTime);
+ deserialized.EndTime.Should().Be(original.EndTime);
+ }
+
+ [Test]
+ public void Should_generate_default_value_for_xsd_default_version_attribute()
+ {
+ // arrange & act — PublicationDeliveryStructure.Version has default="1.0"
+ // in the XSD, which the generator emits as a backing field initialized
+ // to "1.0" and a [DefaultValue("1.0")] attribute.
+ var delivery = new PublicationDeliveryStructure
+ {
+ PublicationTimestamp = DateTimeOffset.UtcNow,
+ ParticipantRef = "test",
+ };
+
+ // assert — version is automatically set to the XSD default
+ delivery.Version.Should().Be("1.0");
+ }
+
+ [Test]
+ public void Should_have_default_value_attribute_on_version_property()
+ {
+ // arrange
+ var property = typeof(PublicationDeliveryStructure)
+ .GetProperty(nameof(PublicationDeliveryStructure.Version))!;
+
+ // act
+ var attr = property.GetCustomAttribute();
+
+ // assert
+ attr.Should().NotBeNull();
+ attr!.Value.Should().Be("1.0");
+ }
+
+ [Test]
+ public void Should_generate_should_serialize_method_for_nullable_date_only()
+ {
+ // arrange — CustomerVersionStructure has a ShouldSerializeDateOfBirth() method
+ // generated because DateOfBirth is Nullable.
+ var method = typeof(CustomerVersionStructure)
+ .GetMethod("ShouldSerializeDateOfBirth");
+
+ // assert
+ method.Should().NotBeNull();
+ method!.ReturnType.Should().Be(typeof(bool));
+ }
+
+ [Test]
+ public void Should_return_false_from_should_serialize_when_nullable_date_only_is_null()
+ {
+ // arrange
+ var customer = new CustomerVersionStructure();
+
+ // act & assert
+ customer.DateOfBirth.Should().BeNull();
+ customer.ShouldSerializeDateOfBirth().Should().BeFalse();
+ }
+
+ [Test]
+ public void Should_return_true_from_should_serialize_when_nullable_date_only_is_set()
+ {
+ // arrange
+ var customer = new CustomerVersionStructure
+ {
+ DateOfBirth = new DateOnly(1990, 1, 15),
+ };
+
+ // act & assert
+ customer.ShouldSerializeDateOfBirth().Should().BeTrue();
+ }
+
+ [Test]
+ public void Should_use_init_setter_on_collection_properties()
+ {
+ // arrange — Organisation.OrganisationType is a List<> with an init setter,
+ // meaning it can be set via object initializer but not reassigned afterwards.
+ var property = typeof(Organisation)
+ .GetProperty(nameof(Organisation.OrganisationType))!;
+ var setter = property.GetSetMethod();
+
+ // assert — init-only setters have the IsExternalInit modreq
+ setter.Should().NotBeNull();
+ setter!.ReturnParameter.GetRequiredCustomModifiers()
+ .Should().Contain(typeof(System.Runtime.CompilerServices.IsExternalInit));
+ }
+
+ [Test]
+ public void Should_rename_substitution_group_head_to_base_suffix()
+ {
+ // arrange — The NeTEx XSD defines StopPlace_ as an abstract dummy element
+ // used as a substitution group head. The NetexNamingProvider renames it to
+ // StopPlaceBase so the concrete StopPlace class gets the clean name.
+
+ // act
+ var baseType = typeof(StopPlaceBase);
+ var xmlType = baseType.GetCustomAttribute();
+
+ // assert — C# class is "StopPlaceBase", but XmlType still references "StopPlace_"
+ baseType.Name.Should().Be("StopPlaceBase");
+ xmlType.Should().NotBeNull();
+ xmlType!.TypeName.Should().Be("StopPlace_");
+ }
+
+ [Test]
+ public void Should_give_concrete_type_the_clean_name()
+ {
+ // arrange & act — StopPlace is the concrete type with the clean name
+ var concreteType = typeof(StopPlace);
+ var xmlType = concreteType.GetCustomAttribute();
+
+ // assert
+ concreteType.Name.Should().Be("StopPlace");
+ xmlType.Should().NotBeNull();
+ xmlType!.TypeName.Should().Be("StopPlace");
+ }
+
+ [Test]
+ public void Should_generate_should_serialize_for_nullable_time_only()
+ {
+ // arrange — OperatingDayVersionStructure.EarliestTime is Nullable
+ var operatingDay = new OperatingDayVersionStructure
+ {
+ CalendarDate = new DateOnly(2026, 6, 15),
+ };
+
+ // act & assert — null by default, ShouldSerialize returns false
+ operatingDay.EarliestTime.Should().BeNull();
+ operatingDay.ShouldSerializeEarliestTime().Should().BeFalse();
+
+ // act — set the value
+ operatingDay.EarliestTime = new TimeOnly(4, 30);
+
+ // assert — ShouldSerialize now returns true
+ operatingDay.ShouldSerializeEarliestTime().Should().BeTrue();
+ }
+
+ [Test]
+ public void Should_generate_should_serialize_for_nullable_time_span()
+ {
+ // arrange — OperatingDayVersionStructure.DayLength is Nullable
+ var operatingDay = new OperatingDayVersionStructure
+ {
+ CalendarDate = new DateOnly(2026, 6, 15),
+ };
+
+ // act & assert — null by default, ShouldSerialize returns false
+ operatingDay.DayLength.Should().BeNull();
+ operatingDay.ShouldSerializeDayLength().Should().BeFalse();
+
+ // act — set the value
+ operatingDay.DayLength = TimeSpan.FromHours(28);
+
+ // assert — ShouldSerialize now returns true
+ operatingDay.ShouldSerializeDayLength().Should().BeTrue();
+ }
+}
diff --git a/src/siri/Spillgebees.SIRI.Models.Tests/ChoiceGroupAttributeTests.cs b/src/siri/Spillgebees.SIRI.Models.Tests/ChoiceGroupAttributeTests.cs
index 0d36024..3b329a4 100644
--- a/src/siri/Spillgebees.SIRI.Models.Tests/ChoiceGroupAttributeTests.cs
+++ b/src/siri/Spillgebees.SIRI.Models.Tests/ChoiceGroupAttributeTests.cs
@@ -12,8 +12,6 @@ namespace Spillgebees.SIRI.Models.Tests;
///
public class ChoiceGroupAttributeTests
{
- // -- helpers ------------------------------------------------------------------
-
private static XmlChoiceGroupAttribute? GetChoiceGroupAttribute(string propertyName) =>
typeof(T).GetProperty(propertyName)?
.GetCustomAttribute();
@@ -25,8 +23,6 @@ private static XmlChoiceGroupAttribute GetRequiredChoiceGroupAttribute(string
return attr!;
}
- // -- MonitoredCountingStructure: simple choice (Count vs Percentage) -----------
-
[Test]
public void Should_have_choice_group_attribute_on_monitored_counting_count()
{
@@ -56,8 +52,6 @@ public void Should_have_same_group_id_for_monitored_counting_choice()
countAttr.GroupId.Should().Be(percentageAttr.GroupId);
}
- // -- LocationStructure: sequence arm (Lon/Lat/Alt share arm, Coordinates is other) --
-
[Test]
public void Should_have_choice_group_attribute_on_location_longitude()
{
@@ -117,8 +111,6 @@ public void Should_have_same_group_id_for_location_choice()
coordAttr.ArmId.Should().NotBe(lonAttr.ArmId);
}
- // -- PlannedStopAssignmentStructure: both arms are sequences -------------------
-
[Test]
public void Should_have_choice_group_attribute_on_planned_stop_assignment_quay_arm()
{
@@ -162,8 +154,6 @@ public void Should_have_same_group_id_across_planned_stop_assignment_choice()
quayRef.GroupId.Should().Be(bpRef.GroupId);
}
- // -- non-choice properties should NOT have the attribute -----------------------
-
[Test]
public void Should_not_have_choice_group_attribute_on_non_choice_property()
{
@@ -182,8 +172,6 @@ public void Should_not_have_choice_group_attribute_on_location_id()
attr.Should().BeNull();
}
- // -- ControlActionStructure: multiple choice groups in same type ---------------
-
[Test]
public void Should_have_multiple_distinct_choice_groups_on_control_action_structure()
{
diff --git a/src/siri/Spillgebees.SIRI.Models.Tests/NullabilityAndRequiredTests.cs b/src/siri/Spillgebees.SIRI.Models.Tests/NullabilityAndRequiredTests.cs
index a904eb4..82ea2c4 100644
--- a/src/siri/Spillgebees.SIRI.Models.Tests/NullabilityAndRequiredTests.cs
+++ b/src/siri/Spillgebees.SIRI.Models.Tests/NullabilityAndRequiredTests.cs
@@ -14,8 +14,6 @@ namespace Spillgebees.SIRI.Models.Tests;
///
public class NullabilityAndRequiredTests
{
- // -- behavioral: required properties ------------------------------------------
-
[Test]
public void Should_construct_key_value_with_required_properties()
{
@@ -58,8 +56,6 @@ public void Should_construct_accessibility_assessment_with_required_bool()
assessment.MobilityImpairedAccess.Should().BeTrue();
}
- // -- behavioral: optional nullable properties ---------------------------------
-
[Test]
public void Should_allow_null_on_optional_reference_type_properties()
{
@@ -91,8 +87,6 @@ public void Should_allow_null_on_optional_string_attribute()
str.Lang.Should().BeNull();
}
- // -- behavioral: ShouldSerialize pattern for nullable value types --------------
-
[Test]
public void Should_serialize_nullable_value_type_when_set()
{
@@ -122,8 +116,6 @@ public void Should_not_serialize_nullable_value_type_when_null()
link.ShouldSerializeLinkContent().Should().BeFalse();
}
- // -- behavioral: collections --------------------------------------------------
-
[Test]
public void Should_initialize_collection_properties_via_constructor()
{
@@ -153,8 +145,6 @@ public void Should_not_serialize_empty_collections()
delivery.ShouldSerializeEstimatedTimetableDelivery().Should().BeFalse();
}
- // -- behavioral: XmlSerializer bypasses required ------------------------------
-
[Test]
public void Should_round_trip_required_properties_via_xml_serializer()
{
@@ -176,8 +166,6 @@ public void Should_round_trip_required_properties_via_xml_serializer()
result.Lang.Should().BeNull();
}
- // -- metadata: required modifier via reflection -------------------------------
-
[Test]
public void Should_have_required_modifier_on_key_value_key_property()
{
@@ -220,8 +208,6 @@ public void Should_not_have_required_modifier_on_optional_property()
isRequired.Should().BeFalse();
}
- // -- metadata: nullable annotations via reflection ----------------------------
-
[Test]
public void Should_have_nullable_annotation_on_optional_reference_type_property()
{
@@ -269,8 +255,6 @@ public void Should_have_nullable_annotation_on_optional_string_attribute()
info.WriteState.Should().Be(NullabilityState.Nullable);
}
- // -- metadata: #nullable enable directive -------------------------------------
-
[Test]
public void Should_have_nullable_context_enabled_on_generated_types()
{
diff --git a/src/siri/Spillgebees.SIRI.Models.Tests/Serialization/IfoptSerializationTests.cs b/src/siri/Spillgebees.SIRI.Models.Tests/Serialization/IfoptSerializationTests.cs
index 2a34f29..12d0825 100644
--- a/src/siri/Spillgebees.SIRI.Models.Tests/Serialization/IfoptSerializationTests.cs
+++ b/src/siri/Spillgebees.SIRI.Models.Tests/Serialization/IfoptSerializationTests.cs
@@ -31,13 +31,11 @@ public void Should_serialize_and_deserialize_stop_place_ref_structure()
deserialized.Value.Should().Be("NSR:StopPlace:1234");
}
- // -- CountryRefStructure (enum base: CountryCodeType) --
- // XmlSerializer cannot handle Nullable with [XmlText], so the generator
- // must emit a non-nullable enum property. These tests verify that.
-
[Test]
public void Should_construct_xml_serializer_for_country_ref_structure()
{
+ // XmlSerializer cannot handle Nullable with [XmlText], so the generator
+ // must emit a non-nullable enum property. This verifies that.
var act = () => new XmlSerializer(typeof(CountryRefStructure));
act.Should().NotThrow();
@@ -85,8 +83,6 @@ public void Should_round_trip_country_ref_structure()
deserialized.Value.Should().Be(CountryCodeType.De);
}
- // -- AccessibilityStructure (enum base: AccessibilityEnumeration) --
-
[Test]
public void Should_construct_xml_serializer_for_accessibility_structure()
{
@@ -119,8 +115,6 @@ public void Should_round_trip_accessibility_structure()
xml.Should().Contain(">true<");
}
- // -- MeasureType (double base + required uom attribute) --
-
[Test]
public void Should_construct_xml_serializer_for_measure_type()
{
diff --git a/src/siri/Spillgebees.SIRI.Models.Tests/XsdTypeMappingTests.cs b/src/siri/Spillgebees.SIRI.Models.Tests/XsdTypeMappingTests.cs
new file mode 100644
index 0000000..7bcb595
--- /dev/null
+++ b/src/siri/Spillgebees.SIRI.Models.Tests/XsdTypeMappingTests.cs
@@ -0,0 +1,281 @@
+using System.ComponentModel;
+using System.Reflection;
+using System.Xml.Serialization;
+using AwesomeAssertions;
+using Spillgebees.SIRI.Models.V2_2.SIRI;
+
+namespace Spillgebees.SIRI.Models.Tests;
+
+///
+/// Tests verifying that the xscg fork correctly maps XSD types to C# types,
+/// generates default values for XSD fixed/default attributes, applies the
+/// ShouldSerialize pattern, uses init setters on collections, and handles
+/// xs:list enum collection serialization for SIRI models.
+///
+public class XsdTypeMappingTests
+{
+ [Test]
+ public void Should_map_xs_date_time_to_date_time_offset_for_required_property()
+ {
+ // arrange & act — HalfOpenTimestampOutputRangeStructure.StartTime is xs:dateTime
+ // with minOccurs=1, mapped to DateTimeOffset with the required modifier.
+ var property = typeof(HalfOpenTimestampOutputRangeStructure)
+ .GetProperty(nameof(HalfOpenTimestampOutputRangeStructure.StartTime))!;
+
+ // assert
+ property.PropertyType.Should().Be(typeof(DateTimeOffset));
+ }
+
+ [Test]
+ public void Should_map_xs_date_time_to_nullable_date_time_offset_for_optional_property()
+ {
+ // arrange & act — HalfOpenTimestampOutputRangeStructure.EndTime is an optional
+ // xs:dateTime, mapped to Nullable.
+ var property = typeof(HalfOpenTimestampOutputRangeStructure)
+ .GetProperty(nameof(HalfOpenTimestampOutputRangeStructure.EndTime))!;
+
+ // assert
+ property.PropertyType.Should().Be(typeof(DateTimeOffset?));
+ }
+
+ [Test]
+ public void Should_round_trip_half_open_timestamp_range_via_xml_serializer()
+ {
+ // arrange
+ var serializer = new XmlSerializer(typeof(HalfOpenTimestampOutputRangeStructure));
+ var original = new HalfOpenTimestampOutputRangeStructure
+ {
+ StartTime = new DateTimeOffset(2026, 3, 15, 9, 0, 0, TimeSpan.FromHours(1)),
+ EndTime = new DateTimeOffset(2026, 3, 15, 17, 30, 0, TimeSpan.FromHours(1)),
+ };
+
+ // act
+ using var writer = new StringWriter();
+ serializer.Serialize(writer, original);
+ var xml = writer.ToString();
+
+ using var reader = new StringReader(xml);
+ var deserialized = serializer.Deserialize(reader) as HalfOpenTimestampOutputRangeStructure;
+
+ // assert
+ deserialized.Should().NotBeNull();
+ deserialized.StartTime.Should().Be(original.StartTime);
+ deserialized.EndTime.Should().Be(original.EndTime);
+ }
+
+ [Test]
+ public void Should_not_serialize_null_end_time_in_half_open_range()
+ {
+ // arrange — EndTime is optional; when null it should not appear in XML
+ var serializer = new XmlSerializer(typeof(HalfOpenTimestampOutputRangeStructure));
+ var original = new HalfOpenTimestampOutputRangeStructure
+ {
+ StartTime = new DateTimeOffset(2026, 3, 15, 9, 0, 0, TimeSpan.Zero),
+ };
+
+ // act
+ using var writer = new StringWriter();
+ serializer.Serialize(writer, original);
+ var xml = writer.ToString();
+
+ // assert — the ShouldSerialize pattern prevents serialization
+ original.ShouldSerializeEndTime().Should().BeFalse();
+ xml.Should().NotContain("");
+ }
+
+ [Test]
+ public void Should_generate_default_value_for_xsd_default_version_attribute()
+ {
+ // arrange & act — Siri.Version has default="2.1" in the XSD, which the
+ // generator emits as a backing field initialized to "2.1" and a
+ // [DefaultValue("2.1")] attribute.
+ var siri = new Siri();
+
+ // assert — version is automatically set to the XSD default
+ siri.Version.Should().Be("2.1");
+ }
+
+ [Test]
+ public void Should_have_default_value_attribute_on_siri_version_property()
+ {
+ // arrange
+ var property = typeof(Siri)
+ .GetProperty(nameof(Siri.Version))!;
+
+ // act
+ var attr = property.GetCustomAttribute();
+
+ // assert
+ attr.Should().NotBeNull();
+ attr!.Value.Should().Be("2.1");
+ }
+
+ [Test]
+ public void Should_generate_default_value_for_xsd_default_enum_property()
+ {
+ // arrange & act — HalfOpenTimestampOutputRangeStructure.EndTimeStatus has
+ // default="undefined" in the XSD, mapped to EndTimeStatusEnumeration.Undefined.
+ var range = new HalfOpenTimestampOutputRangeStructure
+ {
+ StartTime = DateTimeOffset.UtcNow,
+ };
+
+ // assert
+ range.EndTimeStatus.Should().Be(EndTimeStatusEnumeration.Undefined);
+ }
+
+ [Test]
+ public void Should_have_default_value_attribute_on_enum_property()
+ {
+ // arrange
+ var property = typeof(HalfOpenTimestampOutputRangeStructure)
+ .GetProperty(nameof(HalfOpenTimestampOutputRangeStructure.EndTimeStatus))!;
+
+ // act
+ var attr = property.GetCustomAttribute();
+
+ // assert
+ attr.Should().NotBeNull();
+ attr!.Value.Should().Be(EndTimeStatusEnumeration.Undefined);
+ }
+
+ [Test]
+ public void Should_use_init_setter_on_collection_properties()
+ {
+ // arrange — TrainElementStructure.MaximumPassengerCapacities is a List<>
+ // with an init setter.
+ var property = typeof(TrainElementStructure)
+ .GetProperty(nameof(TrainElementStructure.MaximumPassengerCapacities))!;
+ var setter = property.GetSetMethod();
+
+ // assert — init-only setters have the IsExternalInit modreq
+ setter.Should().NotBeNull();
+ setter!.ReturnParameter.GetRequiredCustomModifiers()
+ .Should().Contain(typeof(System.Runtime.CompilerServices.IsExternalInit));
+ }
+
+ [Test]
+ public void Should_initialize_enum_collection_in_constructor()
+ {
+ // arrange & act — TrainElementStructure constructor initializes FareClasses
+ var element = new TrainElementStructure
+ {
+ TrainElementCode = "TE:001",
+ };
+
+ // assert
+ element.FareClasses.Should().NotBeNull();
+ element.FareClasses.Should().HaveCount(0);
+ }
+
+ [Test]
+ public void Should_serialize_enum_collection_as_space_separated_xsd_list()
+ {
+ // arrange — FareClasses is an xs:list of FareClassEnumeration values.
+ // The xscg fork generates a proxy *Xml property that serializes as a
+ // single element with space-separated values.
+ var serializer = new XmlSerializer(typeof(TrainElementStructure));
+ var element = new TrainElementStructure
+ {
+ TrainElementCode = "TE:001",
+ FareClasses =
+ [
+ FareClassEnumeration.FirstClass,
+ FareClassEnumeration.StandardClass
+ ],
+ };
+
+ // act
+ using var writer = new StringWriter();
+ serializer.Serialize(writer, element);
+ var xml = writer.ToString();
+
+ // assert — xs:list serializes as space-separated values in one element
+ xml.Should().Contain("firstClass standardClass");
+ }
+
+ [Test]
+ public void Should_round_trip_enum_collection_via_xml_serializer()
+ {
+ // arrange
+ var serializer = new XmlSerializer(typeof(TrainElementStructure));
+ var original = new TrainElementStructure
+ {
+ TrainElementCode = "TE:002",
+ FareClasses =
+ [
+ FareClassEnumeration.BusinessClass,
+ FareClassEnumeration.EconomyClass
+ ],
+ };
+
+ // act
+ using var writer = new StringWriter();
+ serializer.Serialize(writer, original);
+ var xml = writer.ToString();
+
+ using var reader = new StringReader(xml);
+ var deserialized = serializer.Deserialize(reader) as TrainElementStructure;
+
+ // assert
+ deserialized.Should().NotBeNull();
+ deserialized.FareClasses.Should().HaveCount(2);
+ deserialized.FareClasses[0].Should().Be(FareClassEnumeration.BusinessClass);
+ deserialized.FareClasses[1].Should().Be(FareClassEnumeration.EconomyClass);
+ }
+
+ [Test]
+ public void Should_not_serialize_empty_enum_collection()
+ {
+ // arrange — empty FareClasses list should not appear in XML
+ var serializer = new XmlSerializer(typeof(TrainElementStructure));
+ var element = new TrainElementStructure
+ {
+ TrainElementCode = "TE:003",
+ };
+
+ // act
+ using var writer = new StringWriter();
+ serializer.Serialize(writer, element);
+ var xml = writer.ToString();
+
+ // assert
+ element.ShouldSerializeFareClasses().Should().BeFalse();
+ xml.Should().NotContain("");
+ }
+
+ [Test]
+ public void Should_generate_should_serialize_for_nullable_value_type()
+ {
+ // arrange — TrainElementStructure.TrainElementType is Nullable
+ var element = new TrainElementStructure
+ {
+ TrainElementCode = "TE:004",
+ };
+
+ // act & assert — null by default
+ element.TrainElementType.Should().BeNull();
+ element.ShouldSerializeTrainElementType().Should().BeFalse();
+
+ // act — set the value
+ element.TrainElementType = TrainElementTypeEnumeration.Carriage;
+
+ // assert
+ element.ShouldSerializeTrainElementType().Should().BeTrue();
+ }
+
+ [Test]
+ public void Should_have_default_value_for_boolean_property_with_xsd_default()
+ {
+ // arrange & act — TrainElementStructure.ReversingDirection defaults to true
+ // per the XSD default value.
+ var element = new TrainElementStructure
+ {
+ TrainElementCode = "TE:005",
+ };
+
+ // assert
+ element.ReversingDirection.Should().BeTrue();
+ element.SelfPropelled.Should().BeTrue();
+ }
+}