std/identifier

Uuid (RFC 9562, versions 4 and 7), Ulid and the trait Identifier - made from a Random and a Timestamp that are arguments, so the package has no capability, read back with one IdentifierError and bridged to urn:uuid of std/uri.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

Die generierte Referenz von std/identifier zeigt jede öffentliche Deklaration mit Signatur, Doc-Kommentar und Beispielen.

std/identifier makes the two identifiers that fit in sixteen bytes: Uuid after RFC 9562 and Ulid. Neither reads anything: the randomness is a Random of std/random and the clock is a Timestamp, both arguments, so the package has no capability, a program that makes identifiers needs no clock or entropy of its own, and a test needs no mock (URI.md section 12). Where the identifier has to be unpredictable, the seed of the generator comes from Entropy of std/os; where it has to be reproducible, it is a number.

The bit layout of what the generators draw is stable, like that of Random: the same seed and the same Timestamp make the same identifier on every machine, and std/identifier/tests pins it with known answers.

Both types are values with the same abilities, named by the trait Identifier: shown as text, read back from it, compared bytewise (so a Ulid, and a Uuid of version 7, sort by creation time), hashable, and usable as the key of a Map or an element of a Set. They are Encode and Decode as their text, so JSON carries them as a string.

Import#

use Uuid, Ulid, IdentifierError from "std/identifier"
use Uuid from "std/identifier"
use Random from "std/random"

var source = Random.seeded 0
const made = Uuid.version4 source
print made

Declarations#

Identifier#

trait Identifier with Show, Equals, Hash, Compare, TryFrom<String, IdentifierError> {
  fn bytes(): List<UInt8>
}

bytes is the sixteen bytes, most significant first. Identifier has one failure of reading, IdentifierError, so a function generic over it needs no second type argument.

IdentifierError#

type IdentifierError with Show, Error {
  text: String
  expected: String
  fn show(): String
}

What a text is refused with: the text that was read and what was expected of it, a UUID or a ULID.

Uuid#

type Uuid with Identifier {
  fn version(): Int
  static fn version4(var source: Random): Self
  static fn version7(var source: Random, at: Timestamp): Self
  fn bytes(): List<UInt8>
  fn show(): String
  fn compare(other: Self): Ordering
}
extend Uuid with TryFrom<String, IdentifierError>
extend Uuid with TryFrom<Urn, IdentifierError>

version4 is 122 random bits from two uint64() draws. version7 is the 48-bit count of whole milliseconds since 1970 of at, then 74 random bits from two draws, which is what makes it sort by time and the better default for a key of a table. version is the version nibble, so it reads the identifiers of other versions too.

use Uuid from "std/identifier"
use Random from "std/random"

var source = Random.seeded 0
const first = Uuid.version7 source, at: Timestamp.fromUnixNanoseconds(0)
print first
print first.version()

Uuid.tryFrom(text) reads the thirty-six characters, five groups of hexadecimal digits of either case, and shows them in lower case. It does not check the version or the variant, so the nil and the max UUID are read. The failure is an IdentifierError.

use Uuid from "std/identifier"

const parsed = Uuid.tryFrom "F81D4FAE-7DEC-11D0-A765-00A0C91E6BF6"
print parsed.ok()?.version()
print Uuid.tryFrom("not-a-uuid").isError()

Ulid#

type Ulid with Identifier {
  fn at(): Timestamp
  static fn generated(var source: Random, at: Timestamp, after: Self? = None): Self
  fn bytes(): List<UInt8>
  fn show(): String
  fn compare(other: Self): Ordering
}
extend Ulid with TryFrom<String, IdentifierError>

A Ulid is the 48-bit millisecond count of at, then 80 random bits, written as 26 characters of Crockford's Base32. at() is the Timestamp of whole milliseconds that it carries.

Two identifiers of one millisecond are not ordered by their random part. after is the previous identifier of the caller: where it is of the same millisecond, the result is its random part plus one and nothing is drawn, as the ULID specification's monotonic mode asks, so a stream of them is strictly increasing.

use Ulid from "std/identifier"
use Random from "std/random"

var source = Random.seeded 0
const now = Timestamp.fromUnixNanoseconds 1_790_000_000_000_000_000
const first = Ulid.generated source, at: now
const second = Ulid.generated source, at: now, after: first
print(second > first)

Ulid.tryFrom(text) reads the 26 characters in either case, refuses a first character above 7 (the value would not fit 128 bits) and the letters I, L, O and U.

The bridge to std/uri#

Uri.from(uuid) is the urn:uuid: form of a Uuid and Uuid.tryFrom(urn) reads one back from a Urn, whose namespace may be in any case; a Urn of another namespace, or whose specific part is not a UUID, is an IdentifierError. String.from(uuid) and String.from(ulid) are the text. The bridge lives here, so std/uri does not depend on this package.

use Uuid from "std/identifier"
use Uri from "std/uri"

const uuid = Uuid.tryFrom("f81d4fae-7dec-11d0-a765-00a0c91e6bf6").expect("a UUID")
const uri = Uri.from uuid
print uri

Pitfalls#

  • A generator is a value that every draw changes: pass the same var on, and do not copy it to make a second identifier, or both are the same.
  • Uuid.version7 and Ulid.generated panic for a Timestamp before 1970: the millisecond field is unsigned.
  • The identifiers are not secrets. A Random seeded from Entropy is unpredictable enough for a key of a table, not for a token that must resist a guess; make that one from the bytes of Entropy itself.
  • A Uuid of version 7 sorts by its text and by its bytes alike, but a random version 4 does not: choose the version for the index it will live in.
  • std/random - Random, the source of the random bits.
  • std/time - Timestamp, the clock reading that goes in as at.
  • std/uri - Uri and Urn, the other side of the bridge.
  • URI.md - the decisions of section 12.
Esc