Skip to content

Adds time-based id generation with an injectable byte source - #4

Merged
johnnyt merged 2 commits into
mainfrom
uxjv-94x-generate-ids
Oct 21, 2026
Merged

johnnyt merged 2 commits into
mainfrom
uxjv-94x-generate-ids

Conversation

@johnnyt

@johnnyt johnnyt commented Oct 21, 2026

Copy link
Copy Markdown
Member

What

Time-based id generation, the first public API of systems.riddler:uxid.

  • Uxid.generate() and Uxid.generate(UxidOptions) build the ids the uxid wire format (uxid-ex guides/wire-format.md at v2.10.0) defines: an optional prefix and delimiter, a standard (48-bit) or compact (low 40 bits) time field, and a random field, in Crockford Base32 with the short group first in the time and last in the random field.
  • UxidOptions.builder() takes prefix ("" is no prefix), size, randSize, compactTime, letterCase, delimiter, time, rand (fixed random bytes, documented as test vectors only) and byteSource.
  • Size (XS, S, M, L, XL; default XL), with Size.parse reading the short spellings and the long ones (xsmall, small, medium, large, xlarge) and wireName() giving the corpus spelling.
  • LetterCase (LOWER, the default, and UPPER), applied to the body only; the prefix is emitted as given.
  • ByteSource, a functional interface taking a count and returning that many bytes. The default, ByteSource.secureRandom(), draws from one shared SecureRandom; java.util.Random is not used anywhere.
  • UxidException, an IllegalArgumentException, whose code() is an ErrorCode. The enum is the wire format's closed set of generation errors, each constant's wireName() the corpus spelling: compact_time_out_of_range, rand_width_mismatch, rand_size_too_large, from_with_monotonic, rand_with_from, rand_with_monotonic. This change raises compact_time_out_of_range, rand_width_mismatch and rand_size_too_large; from_with_monotonic, rand_with_from and rand_with_monotonic belong to deterministic ids and the monotonic generator, which later changes add, and are declared here so the set is complete from the start.
  • The package-private anchor interface is deleted: the package now exports real types, so javac no longer needs it to export the package.
  • CLAUDE.md's description of the tree now names what the package holds; changelog.d/uxjv-94x.md records the additions.

Behaviour worth reading

  • The random width is the explicit randSize, or the size's width for the time form (the wire format's size table, held in Size.randBytes). Widths and fixed random bytes are counted in bytes.
  • generate refuses in the reference's order (uxid-ex UXID.Encoder.resolve and UXID.new at v2.10.0): a width above 10 bytes, then fixed bytes of the wrong count, then a compact time whose first character would be the marker. Every refusal comes before the byte source is called.
  • The byte source is not called for a zero-width field or when fixed bytes are set; a source that returns null or the wrong count raises IllegalStateException.
  • When no time is set, System.currentTimeMillis() is read inside the call.

Provenance

  • The API shape (a final Uxid class with static methods, a UxidOptions builder, ByteSource a functional interface, UxidException an IllegalArgumentException with an ErrorCode enum) was decided by the conductor under a standing consent, 2026-10-21.
  • Choices made in this change, 2026-10-21:
    • An option value wrong on its own (a null, an empty delimiter, a negative randSize, a time outside 0 to 2^48 - 1) is refused by its setter with a plain IllegalArgumentException or NullPointerException, so ErrorCode stays exactly the wire format's closed set; UxidException is kept for requests that contradict the format.
    • case is a Java keyword, so the option is letterCase(LetterCase).
    • The default byte source holds one SecureRandom in a static final instance, the shape java.util.UUID.randomUUID() uses. It holds no configuration and nothing a caller can observe or change; a new SecureRandom per id would seed a new generator on every call. The "no global state" rule names static mutable counters, caches and configuration, and this is none of them.
    • Size.parse matches the short and long lower-case spellings exactly and refuses anything else.

Tests

  • GenerateCorpusTest copies a hand-picked subset of the corpus at v2.10.0 into parameterized tests, each case named by its corpus id: encode cases across every size, both time forms and both cases, the all-ones random fields, time zero and the 48-bit maximum, the compact guard's last valid millisecond, no prefix, the empty prefix and a prefix containing the delimiter; and every generate refusal this change raises (the compact-guard cases, the width mismatches, the random width above 10). The full corpus runner comes with a later change.
  • UxidGenerateTest covers the defaults, the clock read, the byte source contract, explicit widths, compact widths per size, refusal ordering, letter case, delimiters, the prefix, defensive copies and the setter refusals. SizeAndErrorCodeTest covers the spellings, the wire names and the closed error set.
  • Every new test carries a sabotage note on its annotation line naming a mutation that was applied, run and restored; each turned that test red on an assertion.

Gate

mise run gate is green on this head (Temurin 25.0.4, Maven 3.9.16): the enforcer rules, the compile with every javac lint and Error Prone, the tests, Spotless, the JaCoCo line check, and the sources and javadoc jars with doclint. The commit is the exact tree that run passed on, and main had not moved.

Refs: uxjv-94x

Uxid.generate() and Uxid.generate(UxidOptions) build the time-based
ids the uxid wire format defines: an optional prefix and delimiter,
a standard or compact time field and a random field in Crockford
Base32, at the five sizes, in either letter case. The options builder
takes the prefix, size, random width, compact time, letter case,
delimiter, a fixed time and fixed random bytes for test vectors.

Random bytes come from a ByteSource, a functional interface taking a
count; the default draws from one shared SecureRandom, so callers can
inject another secure source without global configuration. A request
the wire format refuses throws UxidException, an
IllegalArgumentException carrying an ErrorCode from the format's
closed set of generation errors; an option wrong on its own is
refused by its setter instead. The tests carry a hand-picked subset
of the corpus encode and generate-refusal cases at v2.10.0.

The package-private anchor type goes: the package now exports real
types, so javac no longer needs it.

Refs: uxjv-94x
Re-running the mutation that makes generate draw width + 1 bytes
turns the test red on its id assertion, which runs before the counts
assertion, so the note now names the mutation's form and that
assertion.

Refs: uxjv-94x
@johnnyt
johnnyt merged commit 5d2607b into main Oct 21, 2026
4 checks passed
@johnnyt
johnnyt deleted the uxjv-94x-generate-ids branch October 21, 2026 21:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant