Add a new scheme
You want to teach the library a version dialect it does not yet ship. There is nothing to subclass and no engine to configure: a scheme is just a plain struct that conforms to isVersionScheme!S. Provide the two-member required surface, declare only the optional capabilities your ecosystem actually has, assert conformance, register it, and add tests.
This guide gives the recipe. For the exact concept definitions see the concepts reference; for the catalogue conventions (capability-matrix row, provenance) see the scheme catalogue; the normative signatures are in SPEC §3.1 and §6.1.
1. Write the required surface
Create schemes/<purl_type>.d, named for the purl type where one exists, else a descriptive snake_case name. The struct is both the version value and the scheme handle: it carries the instance opCmp / toString of a version and the static purlType / parse of a scheme.
The required members are a three-way opCmp and an output-range toString (this is isVersion!S), plus purlType, alias Version, and static parse (this is isVersionScheme!S). Provide opEquals and toHash consistent with opCmp so versions work as keys and under ==:
module sparkles.versions.schemes.myscheme;
import sparkles.versions.parsing : ParseExpected;
import sparkles.versions.ranges : Ranges;
struct MyScheme
{
// ... fields holding the parsed version ...
// Required version surface (isVersion!S).
int opCmp(in MyScheme other) const @safe pure nothrow @nogc;
bool opEquals(in MyScheme other) const @safe pure nothrow @nogc;
size_t toHash() const @safe pure nothrow @nogc;
void toString(W)(ref W sink) const; // writes into an output range
// Required scheme surface (isVersionScheme!S).
alias Version = MyScheme; // usually the struct itself
alias Range = Ranges!MyScheme;
enum string purlType = "myscheme"; // non-empty pURL type string
static ParseExpected!MyScheme parse(string s) @safe pure nothrow;
}opCmp must be a total order over every value the parser admits, and toString must round-trip — parse(s).value.toString reproduces s (or its documented normalised form).
2. Declare only the capabilities the ecosystem has
Each optional capability is an independently-detectable trait. Add a member only when the capability holds for every value of the type (the all-or-nothing rule), and only when its fast path agrees with the required-surface fallback (the equivalence rule). Both rules are explained in the design.
orderKey— an unsigned-integer key (ubyte…ulong). Declare it only when the order packs monotonically into an unsigned integer. Structural schemes whose order does not pack —pypi,maven,deb— must not declare it; theiropCmpdoes the full structural walk andsortfalls back to comparison sorting. Absence is never an error.components— anenum string[] componentsof named unsigned fields, most-significant first. Begin it with["major","minor","patch"]only if^/~genuinely apply; a calendar scheme declares["year","month","day"]instead, so it correctly gets no caret.isPrerelease(aboolproperty),build(aconst(char)[]property) — declare when the ecosystem has those notions.parseLoose,parseNativeRange— the scheme-level optional parsers; add them when the ecosystem has compatibility forms or a native range grammar. The VERS and pURL layersstatic ifon these.
The detection rules and behavioural impact of each are in SPEC §3.2 and §6.2.
3. Assert conformance at module scope
End the module with a compile-time check so any regression in the required surface becomes a build failure rather than a silent capability loss:
static assert(isVersion!MyScheme && isVersionScheme!MyScheme);4. Register the scheme
- Public-import the module from
schemes/package.d. - Add the scheme to the
AnyVersionandAnyRangesum types so it participates incompareAnyand the scheme-agnostic layers. - If the purl
typediffers from the scheme name — for instance many purl types map ontoSemVer— add the mapping to the purl→scheme table. A scheme with no published purl type still declares a synthetic, scheme-namedpurlTypeto satisfy the concept, but it is not added to that registry, so an incomingpkg:...never resolves to an internal scheme.
5. Add tests
- Round-trip per real-world example. For each example string
s, assertparse(s).value.toString == s(or the documented normalised form). Take the examples from authoritative sources and record the provenance, matching the catalogue convention. - Ascending order. Build a list that exercises the scheme's ordering edge cases, sort it, and assert the expected sequence.
orderKeyvsopCmpequivalence — only if you declaredorderKey. Cross-check the packed key against theopCmpreference across the corpus:sign(a.orderKey <=> b.orderKey) == sign(order(a, b))whenever the keys differ.
Finally, add the new row to the capability matrix and a per-scheme section in the scheme catalogue, citing the authoritative source for every example.
Notes
- No new required members. The bar of two required members is held deliberately high; everything else is opt-in. If you find yourself wanting a third mandatory member, that is a signal it belongs in the optional vocabulary instead — see the design.
- The baseline.
Genericis the scheme with no optional capabilities; if your scheme is opaque, model it onGenericand you exercise every fallback path for free.