Skip to content

M2/conformance: ModuleID rejects spec-allowed custom module IDs — add the raw-preserving catch-all the code already promises #238

Description

@duyhuynh-vn

Goal

Make crate::version::ModuleID tolerate the custom module IDs the OCPI spec explicitly allows, instead of hard-rejecting the entire /versions/{version} details payload when it meets one. This is a conformance gap on the version-information / credentials-handshake path (M2) in 2.2.1 (the primary production target) — and it is the follow-up the crate's own doc comment already says it needs but which was never filed.

The spec allows custom module IDs (2.2.1 and 2.3.0, verbatim)

specs/ocpi/2.2.1/version_information_endpoint.asciidoc:189-194 (identical text in specs/ocpi/2.3.0/version_information_endpoint.asciidoc, under ===== Custom Modules; 2.3.0 also re-titles the section ==== ModuleID _OpenEnum_):

Custom Modules — Parties are allowed to create custom modules or customized versions of the existing modules. To do so, the ModuleID enum can be extended with additional custom moduleIDs. […] It is advised to use a prefix (e.g. country-code + party-id) […]
For example: nltnm-tokens

So a spec-conformant peer MAY list an endpoint whose identifier is nltnm-tokens (or any <cc>-<party>-… custom id) in its VersionDetails.

Current crate behaviour (a hard reject — verified against main)

crates/ocpi-types/src/version.rs:

  • ModuleID is a closed enum (10 fixed variants: cdrs, chargingprofiles, … payments) with no #[serde(other)] / Custom(String) catch-all.
  • Endpoint.identifier is typed ModuleID (not a string), and VersionDetails.endpoints is Vec<Endpoint>.
  • The type's own doc comment (version.rs:95-97) already records the gap and promises the fix:

    "Custom module IDs ("nltnm-tokens" style) are spec-allowed but not yet modelled; deserializing one will return an error. A follow-up issue will add an Other(String) catch-all."

That follow-up issue does not exist (searched open + closed: no ModuleID / "custom module" / nltnm-tokens issue). This issue is it.

Because serde fails the whole struct on an unknown enum variant, one custom endpoint entry makes the entire VersionDetails (and therefore the whole GET /versions/{version} response) fail to deserialize — not just the one unknown endpoint. That happens on the credentials/version-negotiation path, before any module traffic flows.

Note: this is deliberately out of scope of #184. #184's OpenEnum decision-support comment explicitly punts ModuleID/VersionNumber to "the version layer, handled separately," and recommends keeping them out of any blanket Other(String) change. VersionNumber forward-compat is already handled (recognition-only 2.0/#182 and 3.0/#219,#223). ModuleID custom-module tolerance is the one piece of that "handled separately" story that is not actually handled or tracked — this issue closes it, without touching #184's enum-policy decision.

Real use case — why this PR is needed

ocpi-rs is consumed by evlinked/charge-hub (a hosted OCPI roaming Hub) and evlinked/stationlink. A Hub fetches every partner's GET /versions/{version} catalogue during the credentials handshake to discover which module endpoints that partner exposes. The spec deliberately lets partners advertise custom / customized modules (prefixed like nltnm-tokens) alongside the standard ones. Under today's closed ModuleID, a single custom endpoint in a partner's version details makes the whole catalogue fail to deserialize — so the Hub cannot even read the partner's standard Locations/Sessions/CDRs endpoints, and the handshake breaks, purely because the partner also offered a module the Hub doesn't recognise. That is a hard onboarding failure for a case the spec explicitly designed to be additive: the Hub should read the endpoints it understands, preserve the custom id (round-trip it, ignore it for routing), and carry on — exactly the "tolerant where the spec is open, strict where it is closed" contract the crate already applies to OcpiStatusCode::Unknown(u16). This unblocks charge-hub onboarding partners that ship any custom module, on the 2.2.1 path it depends on most.

Proposed shape (implementation notes for whoever picks this up)

  • Add a raw-preserving fallback to ModuleID — a Custom(String) variant with #[serde(untagged)]/manual Deserialize, or a #[serde(other)]-style catch-all that keeps the original string — so an unknown id round-trips as data instead of erroring. Mirror the OcpiStatusCode::Unknown(u16) precedent.
  • Ripple to flag: ModuleID currently derives Copy. Custom(String) is not Copy, so Copy must be dropped and the (small) set of Copy-reliant call sites adjusted to Clone. Call this out in the PR — it is the one non-mechanical part.
  • Keep standard ids serializing exactly as today (cdrs, chargingprofiles, …); only genuinely-unknown ids land in Custom.
  • Ensure a Custom id is never treated as a known module by any routing / endpoint-lookup logic (it is opaque, agreement-only data per the spec's "only send to parties you have an agreement with").

Acceptance criteria

  • ModuleID deserializes a custom id ("nltnm-tokens") into a raw-preserving Custom(String) and re-serializes it byte-for-byte (round-trip test).
  • A VersionDetails / Endpoint list mixing standard ids and a custom id deserializes successfully, with the standard endpoints intact and the custom one preserved (the load-bearing regression fence for the handshake path).
  • Standard ids still round-trip unchanged; add a fence that a standard id never lands in Custom.
  • Routing / endpoint-selection treats a Custom id as opaque (never matches a standard module).
  • Copy-drop ripple resolved; cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, cargo test --workspace --all-features green.
  • No new dependencies; no unsafe. Update the version.rs:95-97 doc comment to record the catch-all now exists (removing the "a follow-up issue will add…" promise). Reviewable slice (≤ ~150 LOC).

Spec / references


Filed by the nightly routine as a grooming issue from a spec-vs-crate diff: the open-PR queue is at the 5-PR cap tonight (#223 / #226 / #227 / #229 / #231 — all CI-green, no conflicts, each carrying a Real-use-case section + Claude session link), so per the "if the queue is full, groom + review the goal" cadence no new implementation PR was opened. This surfaces the one spec-designated forward-compat gap the crate's own doc comment flags but no issue tracked. Unblocked and non-decision-gated whenever a PR slot frees.

Filed via Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    type:typesocpi-types cratev2.2.1OCPI version 2.2.1 (primary target)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions