API reference
@evanion/urn 2.1.0 and its patch releases up to 2.1.1, the version on npm. Values on this page were produced by running the source at f9b1d93, a later commit than the 2.1.0 tag, and are not re-executed.
@evanion/urn exports one class, two functions, two errors and four types, one
heading each below. It needs Node 20 or newer and ships ESM only, which
require() loads on Node 20.19, 22.12 or newer.
A URN, as defined by RFC 8141, with one deliberate deviation: RFC 8141
requires the literal scheme urn, and this class allows any scheme through
the overridable URN.urn static.
Full documentation
The class conforms to RFC 8141 only under the default : separator. A
subclass that overrides URN.separator checks the scheme and the NID
against a character class that excludes the separator, with no length
bounds, and is not RFC 8141 conformant. The NSS keeps the RFC grammar under
every separator.
The optional r-, q- and f-components of RFC 8141 2.3 are split off the tail
before the separator split, under every separator except one that itself
contains ? or #. Such a separator cannot be told apart from a component
introducer, so on that subclass parse throws an InvalidError with
property: 'NSS' for any ? or # after the NID, and stringify throws
one with property: 'COMPONENT' for any component.
Every static reads this, so they cannot be destructured the way
JSON.stringify can: const { stringify } = URN then stringify('a')
throws a TypeError, because the default parameter nid = this.nid runs
with this undefined. Call them on the class.
URN is never instantiated. A subclass overrides the statics in the first
three rows. parse, stringify and isValidFormat read all three off this.
extractId, equals and sameNamespace read only separator, and
belongsToNamespace reads separator and defaults its scheme to the class’s
urn. GameURN in the examples on this page is URN with
static override readonly nid = 'game'.
| Static | Default | What it is |
|---|---|---|
urn | 'urn' | the scheme this class writes and reads as its own |
nid | 'nid' | the namespace identifier this class writes and reads as its own |
separator | ':' | what the three parts are joined by and split on |
schemeGrammar | RFC 3986 scheme | a letter, then letters, digits, +, - and . |
nidGrammar | RFC 8141 NID | letters, digits and interior -, 2 to 32 characters, checked by stringify |
nssGrammar | RFC 8141 NSS | letters, digits, -._~!$&'()*+,;=:@ and percent-encoded characters, plus / after the first character |
rComponentGrammar | pchar *(pchar / "/" / "?") | the NSS set plus ? after the first character |
qComponentGrammar | pchar *(pchar / "/" / "?") | the same production as the r-component |
fComponentGrammar | RFC 3986 fragment | the NSS set plus ?, no first-character rule, and the only one to accept '' |
'nid' is a placeholder, not a registered namespace, so every real namespace
is foreign to the base URN class. Under a separator other than :,
schemeGrammar and nidGrammar become a character class that excludes the
separator, with no length bounds. parse accepts a one-character NID, which
RFC 2141, the URN syntax RFC 8141 replaced, permitted and nidGrammar refuses:
..('baize'); // -> true
..('game'); // -> true
..('g'); // -> false
.('urn:g:azul'); // -> true
..('brass-birmingham'); // -> true
..('brass birmingham'); // -> falsePassed as a bare callback, a static loses this. stringify, parse and
extractId throw a TypeError. isValidFormat, equals and sameNamespace
catch it and return false. belongsToNamespace throws only when its scheme
argument is left out, and returns false under filter or map, which pass
a third argument:
const = ['urn:game:azul', 'urn:game:hive'];
.(() => .()); // -> ['azul', 'hive']
let = '';
try {
.(.);
} catch () {
= ( as Error).;
}
; // -> 'TypeError'
.(.); // -> []
.(() => .()); // -> ['urn:game:azul', 'urn:game:hive']export declare class {
static readonly : string;
static readonly : string;
static readonly : string;
static get (): RegExp;
static get (): RegExp;
static get (): RegExp;
static get (): RegExp;
static get (): RegExp;
static get (): RegExp;
protected static get (): RegExp;
private static get ();
private static get ();
private static get ();
static (: string): ParsedURN;
static (: URNParts): string;
static (: string, ?: string, ?: string): string;
static (: string): boolean;
static (: string): string;
static (: string, : string): boolean;
static (: string, : string, ?: string): boolean;
static (: string, : string): boolean;
private static ;
private static ;
private static ;
private static ;
private static ;
private static ;
private static ;
private static ;
private static ;
private static ;
}What the tests state
102 behaviours
- stringify
- stringifies a basic URN
- stringifies with a custom NID
- rejects a scheme or NID carrying the separator
- accepts an NSS carrying the separator
- emits an NSS that starts with the NID verbatim
- keeps a foreign namespace prefix verbatim in the NSS
- accepts RFC 8141’s own example NSS
- refuses a URN carrying an invalid character
- refuses a NID carrying an invalid character
- rejects a scheme that does not start with a letter
- NID grammar
- accepts lengths 2 through 32 on the write path
- rejects a 1-character NID on the write path
- rejects a 33-character NID on both paths
- rejects a leading or trailing hyphen on both paths
- rejects ’_’, ’.’ and ’~’ in a NID on both paths
- accepts a 1-character NID on the read path only
- exposes the write grammar through nidGrammar
- NSS grammar
- accepts the whole pchar set plus slash and percent-triplets
- rejects a leading slash
- rejects characters outside pchar
- accepts the RFC 3986 sub-delims, colon and at-sign
- rejects a malformed percent-triplet with a structural reason
- neither encodes nor decodes on the write path
- returns percent-triplets intact on the read path
- scheme grammar
- follows RFC 3986 under the default separator
- round-tripping
- round-trips the full pchar set through stringify and parse
- round-trips an NSS carrying the separator
- round-trips RFC 8141’s own example URNs unchanged
- is not idempotent, and says so
- class inheritance
- derives from the URN class
- derives from the URN class and sets the separator
- derives from the URN class and sets the NID
- custom separator
- excludes the separator from the scheme and the NID
- drops the RFC length bounds for the scheme and the NID
- keeps the RFC pchar grammar for the NSS
- round-trips through parse, extractId and isValidFormat
- parse
- parses a URN into its parts
- retains a foreign NID in the NSS
- retains a foreign scheme in the NSS, verbatim
- distinguishes a foreign scheme from the class scheme
- returns the original case
- rejects character-invalid input
- utility methods
- isValidFormat · returns true for a valid URN
- isValidFormat · returns false for an invalid URN
- isValidFormat · agrees with parse on every input
- extractId · extracts the identifier from a URN
- extractId · extracts the identifier when the NSS carries a namespace
- extractId · throws on character-invalid input
- sameNamespace · returns true for URNs in the same namespace
- sameNamespace · folds the case of the scheme and the NID
- sameNamespace · returns false for URNs in different namespaces
- sameNamespace · returns false for invalid URNs
- belongsToNamespace · returns true when the URN belongs to the named namespace
- belongsToNamespace · folds the case of the scheme and the NID
- belongsToNamespace · returns false when the URN does not belong to the named namespace
- belongsToNamespace · returns false for invalid URNs
- equals · folds the case of the scheme and the NID
- equals · compares the NSS byte for byte
- equals · canonicalises percent-triplet hex digits to uppercase
- equals · does not treat a percent-triplet as equal to the octet it encodes
- equals · is not confused by a foreign NID the parse folded into the NSS
- equals · returns false for malformed input rather than throwing
- percent-encoding helpers
- round-trips through decodeNss
- malformed input
- throws instead of leaking the string “undefined:” from parse
- names the offending input in the parse error
- throws a ValidationError, not a bare Error, from extractId
- does not report two identical malformed strings as the same namespace
- empty components
- rejects an empty NSS rather than emitting an unparseable URN
- rejects an empty NID and URN scheme
- does not consider the empty string a valid component
- subclass awareness
- defaults belongsToNamespace to the subclass scheme
- still honours an explicit expectedUrn
- uses the subclass separator when retaining a foreign NID
- does not let the statics be destructured
- lets the unbound predicates answer false
- r-, q- and f-components
- parses a component-free URN to exactly three keys
- splits each component off the NSS
- ends the r-component at the first ’?=’
- takes the f-component off before looking for a ’?’
- keeps an empty f-component distinct from an absent one
- keeps the components out of the NSS on a foreign namespace
- emits the components from the object form of stringify
- round-trips a URN with components through parse and stringify
- rejects a component that breaks its grammar
- rejects a bare ’?’ that introduces nothing
- keeps the NSS argument of the positional form free of components
- excludes the components from equivalence, per RFC 8141 3.1
- drops the components from extractId
- exposes the component grammars
- parses components under a custom separator
- does not parse components when the separator carries a delimiter
- refuses a component tail when the separator carries a delimiter
- the object form of stringify
- accepts a parsed URN directly
- defaults the scheme and the NID to the class
- does not invert parse on a foreign namespace
- agrees with the positional form
- validates the object form the same way
- round-trips through the object form, not the spread
- error hierarchy
- exposes InvalidError as a ValidationError
- carries a structural reason when no single character is at fault
- falls back to a generic message when given neither
- lets a single catch handle both error kinds
Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.
import { , } from '@evanion/urn';
class extends {
static override readonly = 'game';
}
const = .('brass-birmingham');
; // -> 'urn:game:brass-birmingham'
let = '';
try {
.('brass birmingham');
} catch () {
if ( instanceof ) = .;
}
; // -> "NSS contains invalid character ' ' in 'brass birmingham'"URN.stringify
stringify(parts) and stringify(nss, nid?, urn?) write a URN string. nid
and urn default to the calling class’s own statics. Both forms throw
InvalidError when a part is empty or breaks its grammar:
.('azul'); // -> 'urn:game:azul'
.('azul', 'game'); // -> 'urn:game:azul'
.('azul', 'game', 'baize'); // -> 'baize:game:azul'
.({ : 'azul', : 'game' }); // -> 'urn:game:azul'
.(.('urn:game:azul')); // -> 'urn:game:azul'The object form’s keys match what parse returns, and only the object form
writes the r-, q- and f-components. The positional arguments run in the reverse
order of parse’s return, so stringify(...Object.values(parse(x))) puts the
scheme where the NSS belongs. stringify writes the NSS verbatim, so it neither
deduplicates nor is idempotent:
.('game:azul'); // -> 'urn:game:game:azul'
.('urn:game:game:azul').; // -> 'game:azul'
.(.('azul')); // -> 'urn:game:urn:game:azul'URN.parse
parse(urnString) returns a ParsedURN, with every part in the case it was
written in and every percent-encoded character intact. A scheme or NID that
differs from the class’s own, compared without regard to case, stays in the
nss:
.('urn:game:azul'); // -> { urn: 'urn', nid: 'game', nss: 'azul' }
.('URN:GAME:azul'); // -> { urn: 'URN', nid: 'GAME', nss: 'azul' }
.('urn:order:order-2026-0042'); // -> { urn: 'urn', nid: 'order', nss: 'order:order-2026-0042' }
.('baize:game:azul'); // -> { urn: 'baize', nid: 'game', nss: 'baize:game:azul' }parse throws ValidationError for a string that is not a URN, and
InvalidError for a part that breaks its grammar.
URN.isValidFormat
isValidFormat(urnString) returns true when parse would succeed. It calls
parse, so the two cannot disagree, and it never throws:
function (: string) {
return .() ? .() : null;
}
('urn:game:hive'); // -> { urn: 'urn', nid: 'game', nss: 'hive' }
('urn:game:spirit island'); // -> null
('hive'); // -> null
('urn:order:order-2026-0042'); // -> { urn: 'urn', nid: 'order', nss: 'order:order-2026-0042' }URN.extractId
extractId(urnString) returns everything after the scheme and the NID, with
any r-, q- or f-component dropped. On a foreign namespace it differs from
parse(urnString).nss, which keeps the NID. The base URN class’s own NID is
'nid', so game is foreign to it:
.('urn:game:azul').; // -> 'game:azul'
.('urn:game:azul').; // -> 'game'
.('urn:game:azul'); // -> 'azul'extractId throws ValidationError on malformed input.
URN.sameNamespace
sameNamespace(a, b) returns true when both URNs have the same scheme and
the same NID, compared case-folded. Malformed input returns false:
.('urn:game:azul', 'URN:Game:hive'); // -> true
.('urn:game:azul', 'urn:order:order-2026-0042'); // -> false
.('azul', 'azul'); // -> falseURN.belongsToNamespace
belongsToNamespace(urnString, nid, urn?) returns true when the URN is in the
named namespace. urn defaults to the calling class’s own scheme. Malformed
input returns false:
.('urn:game:azul', 'game'); // -> true
.('baize:game:azul', 'game'); // -> false
.('baize:game:azul', 'game', 'baize'); // -> trueURN.equals
equals(a, b) is RFC 8141 §3.1 equivalence. The scheme and the NID fold case.
The NSS compares character for character, except that the hex digits of a
percent-triplet fold to uppercase. A percent-encoded octet is never decoded, and
the r-, q- and f-components are ignored. Malformed input returns false:
.('URN:GAME:azul', 'urn:game:azul'); // -> true
.('urn:game:Brass%3a%20Birmingham', 'urn:game:Brass%3A%20Birmingham'); // -> true
.('urn:game:Brass%3A%20Birmingham', 'urn:game:Brass:%20Birmingham'); // -> false
.('urn:game:Azul', 'urn:game:azul'); // -> falsePercent-encodes an arbitrary string into a valid NSS.
Full documentation
Everything outside RFC 3986’s unreserved set is encoded as UTF-8
percent-triplets with uppercase hex, so the result is safe in any position
of an NSS. Neither stringify nor parse calls this: encoding is an
explicit, separate step, so a value can never be double-encoded by accident.
encodeNss(raw) leaves A-Z a-z 0-9 - . _ ~ alone and percent-encodes every
other character, : included.
function (: string): string;
What the tests state
2 behaviours
- encodeNss
- encodes everything outside the unreserved set
- produces an NSS the grammar accepts
Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.
import { , , } from '@evanion/urn';
.('urn:game:Brass: Birmingham'); // -> false
const = ('Brass: Birmingham');
; // -> 'Brass%3A%20Birmingham'
const = .(, 'game');
; // -> 'urn:game:Brass%3A%20Birmingham'
(.()); // -> 'Brass: Birmingham'Decodes the percent-triplets in an NSS back to the raw string.
throws {ValidationError} when the input contains a malformed or truncated percent sequence.
decodeNss(encoded) reverses encodeNss. It throws ValidationError on a %
that is not followed by two hex digits, and on triplets that do not decode as
UTF-8.
function (: string): string;
What the tests state
2 behaviours
- decodeNss
- decodes triplets the library itself never produces
- throws a ValidationError on a malformed percent sequence
Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.
import { , } from '@evanion/urn';
('Brass%3A%20Birmingham'); // -> 'Brass: Birmingham'
let = '';
try {
('Brass%3');
} catch () {
if ( instanceof ) = .;
}
; // -> "Malformed percent-encoding in 'Brass%3': '%' must be followed by two hex digits."Base class for every error this library throws. Catch it to handle any validation failure without naming the subclasses.
ValidationError is the base class of every error @evanion/urn throws.
parse, extractId and decodeNss throw it directly for input that is not a
URN or not a valid encoding, so catching it covers InvalidError too.
export declare class extends {
constructor(: string);
}import { , , } from '@evanion/urn';
function (: string): string {
try {
.();
return 'none';
} catch () {
if ( instanceof ) return `bad ${.}`;
if ( instanceof ) return 'not a URN';
throw ;
}
}
('urn:game:azul'); // -> 'none'
('urn:game:spirit island'); // -> 'bad NSS'
('azul'); // -> 'not a URN'Thrown when one part of a URN is empty, contains a character outside its role’s permitted set, or breaks a structural rule of its role’s grammar (a length bound, a leading hyphen, a truncated percent-triplet).
InvalidError extends ValidationError, and stringify and parse throw it
for one part that is empty or breaks its grammar.
| Property | What it holds |
|---|---|
property | the part: 'URN' for the scheme, 'NID', 'NSS', 'R-COMPONENT', 'Q-COMPONENT', 'F-COMPONENT' or 'COMPONENT' |
value | the value of that part |
invalidChar | the first character outside the part’s set, when one is at fault |
reason | the structural rule broken when every character is allowed, such as a length bound |
'COMPONENT' is the property when a subclass whose separator contains ? or
# is asked to write a component.
export declare class extends {
constructor(: string, : string, ?: string, ?: string);
private static ;
readonly : string;
readonly : string;
readonly ?: string;
readonly ?: string;
}import { , } from '@evanion/urn';
function (: string, : string): | undefined {
try {
.(, );
return ;
} catch () {
if ( instanceof ) return ;
throw ;
}
}
const = ('spirit island', 'game');
?.; // -> 'NSS'
?.; // -> ' '
?.; // -> "NSS contains invalid character ' ' in 'spirit island'"
const = ('spirit-island', 'g');
?.; // -> 'NID'
?.; // -> 'must be at least 2 characters long'The object returned by URN.parse.
Full documentation
The parts are plain strings, and deliberately not type parameters. parse
takes a runtime string, so it cannot know their literal types; free type
parameters here would let URN.parse<'a', 'b', 'c'>(someString) assert a
shape nothing verifies.
The three component keys are absent rather than undefined when the URN
carries no components, so such a URN parses to exactly { urn, nid, nss }.
ParsedURN is what parse returns: urn, nid and nss, plus whichever
components the URN carries.
interface ParsedURN extends URNComponents {
: string;
: string;
: string;
}
.('urn:game:brass-birmingham?=edition=2018#setup'); // -> { urn: 'urn', nid: 'game', nss: 'brass-birmingham', fComponent: 'setup', qComponent: 'edition=2018' }The object form of URN.stringify’s arguments.
Full documentation
The keys match ParsedURN, so a parsed URN can be handed straight
back: on one class, stringify(parse(x)) returns x for a URN in that
class’s own namespace. The scheme and the NID are optional and fall back to
the class’s own, which is what makes { nss } alone valid.
URNParts is what the object form of stringify takes. Only nss is
required, and urn and nid fall back to the class’s own. A ParsedURN is
assignable to it.
interface URNParts extends URNComponents {
?: string;
?: string;
: string;
}
import type { ParsedURN, URNParts } from '@evanion/urn';
class extends {
static override readonly = 'game';
}
const : ParsedURN = .('urn:game:azul#setup');
const : URNParts = { ..., : 'scoring' };
.(); // -> 'urn:game:azul#scoring'
const : URNParts = { : 'hive' };
.(); // -> 'urn:game:hive'The three optional components RFC 8141 2.3 allows after the NSS.
Full documentation
Each is carried in its wire form, without its introducing delimiter and
without any percent-decoding. RFC 8141 3.1 excludes all three from
URN-equivalence, so URN.equals ignores them.
URNComponents holds rComponent, qComponent and fComponent, each
optional. A key is absent when the URN carries no such component, and
fComponent may be ''. r-, q- and f-components covers the
syntax.
interface URNComponents {
?: string;
?: string;
?: string;
}
function ({ , }: URNComponents): string {
return [, ].().(' / ');
}
(.('urn:game:azul?=edition=2017#scoring')); // -> 'edition=2017 / scoring'
'qComponent' in .('urn:game:azul'); // -> falseA URN string with known parts, for annotating literals in consumer code.
Full documentation
Assumes the default : separator. A subclass with a custom separator cannot
be described by this type.
IFullURN<URN, NID, NSS> is the template literal type
`${URN}:${NID}:${NSS}`. stringify returns plain string, because a
subclass may override the :.
export type < extends string, extends string, extends string> = `${}:${}:${}`;import type { } from '@evanion/urn';
type = <'urn', 'game', string>;
const : = 'urn:game:azul';
.(); // -> 'azul'
// @ts-expect-error: an order URN is not a GameId
const : = 'urn:order:order-2026-0042';
.(); // -> true