Ahead of the release
npm install @evanion/urn gives you 2.0.0. These pages document main, which has changes that release does not.
r-, q- and f-components
RFC 8141 §2.3 allows three optional components after the NSS. They are not part
of the identifier: the assigned name is scheme : NID : NSS, and everything
after it addresses something about resolving or using the named resource.
| Component | Introducer | RFC 8141 | Carries |
|---|---|---|---|
| r | ?+ | §2.3.1 | parameters for the resolution service |
| q | ?= | §2.3.2 | parameters for the named resource |
| f | # | §2.3.3 | a secondary resource within the named one |
urn:example:weather?+cache=no?=lat=39;lon=-77#today
└──── assigned name ────┘└─ r ─┘└─── q ───┘└ f ┘Reading them
parse returns each in its own field and never folds it into the nss:
class extends {
static override readonly = 'example';
}
.('urn:example:weather?=lat=39#today'); // -> { urn: 'urn', nid: 'example', nss: 'weather', fComponent: 'today', qComponent: 'lat=39' }WeatherURN.parse('urn:example:weather?=lat=39#today')Add ?+cache=no for an r-component, or #today for an f-component.
The three keys are absent, not undefined, when the URN carries none, so a URN
without them parses to exactly { urn, nid, nss } and a consumer destructuring
those three sees the shape it saw before components existed.
Writing them
Only the object form of stringify can express them:
URN.stringify({ nss: 'weather', nid: 'example', qComponent: 'lat=39' });
// 'urn:example:weather?=lat=39'
URN.stringify({
nss: 'foo',
nid: 'example',
rComponent: 'r',
qComponent: 'q',
fComponent: '',
});
// 'urn:example:foo?+r?=q#'The order on the wire is always r, then q, then f, whatever order the keys were
written in. A ParsedURN is assignable to URNParts, so
stringify(parse(x)) round-trips a URN in the parsing class’s own namespace,
components included.
How the split works
The NSS grammar needs no change to make this unambiguous: pchar contains
neither ? nor #, so neither delimiter can occur inside an assigned name.
The order the parser takes them off is fixed by the grammar:
- The f-component comes off first, at the first
#. It has to:#terminates the r- and q-components, while both of those may contain a bare?. - The first
?of what is left introduces the r-component when followed by+, and the q-component when followed by=. Anything else is aValidationError— a bare?is not a legal introducer. - An r-component runs to the first following
?=, which starts the q-component.
Grammars
URN.rComponentGrammar; // pchar *(pchar / "/" / "?")
URN.qComponentGrammar; // the same production
URN.fComponentGrammar; // RFC 3986 `fragment`The r- and q-components take the NSS set plus ? after the first character.
The f-component is RFC 3986’s fragment production: the same set, with no
first-character rule and no minimum length. It is the one component grammar that
accepts the empty string, so urn:example:foo# is well formed and parses to
fComponent: ''.
Nothing is percent-decoded. A component is carried in its wire form, without its introducing delimiter.
What ignores them
RFC 8141 §3.1 excludes all three from URN equivalence, so two URNs that differ only in their components are the same URN:
URN.equals('urn:example:foo?+r1#a', 'urn:example:foo?+r2#b'); // trueextractId drops them too, because it returns the identifier and a component is
not part of one:
URN.extractId('urn:example:foo?=q#f'); // 'foo'sameNamespace and belongsToNamespace only look at the scheme and the NID, so
components never affect them either.
Attach each component to the identifier below and read what parse makes of
it. The line that arrives with them is URN.equals answering for the carrying
form against the bare one, which is §3.1 holding rather than a sentence saying
it does. Each part is askable: press one for what it names.
scheme Always the same three letters, and they say what the rest of the string is: a name for something, not an address where it lives. A URL tells you where to go; this tells you what you mean.:namespace What kind of thing this names. Two systems can both number their records from 1, and this is what keeps those two 1s apart.:name The identifier itself, in whatever form the system it came from uses. Unique inside the namespace above, and nowhere else.
Custom separators switch this off
A subclass whose separator contains ? or # cannot tell a separator from a
component delimiter. There, the whole tail stays in the NSS, and writing a
component throws an InvalidError with property: 'COMPONENT' rather than
emitting something the same class could not read back.
Every other separator parses components normally.