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
150 changes: 150 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4 changes: 2 additions & 2 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<PackageVersion Include="MinVer" Version="6.0.0" />

<!-- Generator dependencies -->
<PackageVersion Include="XmlSchemaClassGenerator-beta" Version="99.0.7-local" />
<PackageVersion Include="XmlSchemaClassGenerator.Analyzer" Version="99.0.7-local" />
<PackageVersion Include="XmlSchemaClassGenerator-beta" Version="99.0.8-local" />
<PackageVersion Include="XmlSchemaClassGenerator.Analyzer" Version="99.0.8-local" />
<PackageVersion Include="System.CommandLine" Version="2.0.2" />

<!-- Test dependencies -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ private static XscGenerator CreateBaseGenerator(
UseShouldSerializePattern = true,
GenerateChoiceGroupAttributes = true,
ChoiceGroupAttributeNamespace = rootNamespace,
GenerateStrictFixedValues = true,
GenerateStrictRangeBounds = true,

// Minimal attribute noise
GenerateSerializableAttribute = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ namespace Spillgebees.NeTEx.Models.Tests;
/// </summary>
public class ChoiceGroupAttributeTests
{
// -- helpers ------------------------------------------------------------------

private static XmlChoiceGroupAttribute? GetChoiceGroupAttribute<T>(string propertyName) =>
typeof(T).GetProperty(propertyName)?
.GetCustomAttribute<XmlChoiceGroupAttribute>();
Expand All @@ -25,8 +23,6 @@ private static XmlChoiceGroupAttribute GetRequiredChoiceGroupAttribute<T>(string
return attr!;
}

// -- DistanceMatrixElementDerivedViewStructure: two parallel choice groups -----

[Test]
public void Should_have_choice_group_on_start_stop_point_ref()
{
Expand Down Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ namespace Spillgebees.NeTEx.Models.Tests;
/// </summary>
public class NullabilityAndRequiredTests
{
// -- behavioral: required properties ------------------------------------------

[Test]
public void Should_construct_publication_delivery_with_required_properties()
{
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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()
{
Expand All @@ -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()
{
Expand All @@ -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()
{
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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()
{
Expand Down
Loading