Skip to Content
URNr-, q- and f-components

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.

ComponentIntroducerRFC 8141Carries
r?+§2.3.1parameters for the resolution service
q?=§2.3.2parameters for the named resource
f#§2.3.3a 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')

{ urn: 'urn', nid: 'example', nss: 'weather', fComponent: 'today', qComponent: 'lat=39' }

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:

  1. 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 ?.
  2. The first ? of what is left introduces the r-component when followed by +, and the q-component when followed by =. Anything else is a ValidationError — a bare ? is not a legal introducer.
  3. 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'); // true

extractId 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.urn:game:brass-birmingham

Attach

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.

Last updated on