From 125dd2eae71dd4a560eb7051e65e84a6029d18c5 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Wed, 21 Oct 2026 17:13:05 -0600 Subject: [PATCH] Adds the README and the wire format pointer The README now reads what, why, install (the Maven dependency block and the Gradle coordinate at 1.0.0), one basic-usage block that was run, Compatibility and Documentation, and states the behaviours a caller can hit: decode never throws, the stricter validity checks than the Elixir reference, the deterministic prefix refusal, and the state a Monotonic keeps. docs/wire-format.md points at the reference spec at v2.10.0. The Javadoc now says the same: Builder.prefix on the surrogate refusal, decode on the clock edge near Long.MIN_VALUE, Monotonic on the pair of time and random value that increases and on its state growing with each prefix and width, and the two validity checks on their stricter reading. Tests: a time-based id keeps a prefix with no UTF-8 encoding; the SOURCE.json tag and sha are pinned to v2.10.0 and its commit; two sabotage notes name the assertion that actually fails. Refs: uxjv-0tl --- CLAUDE.md | 24 +- README.md | 212 ++++++++++++++++++ changelog.d/uxjv-5tk.md | 4 +- docs/wire-format.md | 15 ++ .../java/systems/riddler/uxid/Monotonic.java | 18 +- src/main/java/systems/riddler/uxid/Uxid.java | 12 +- .../systems/riddler/uxid/UxidOptions.java | 4 +- .../systems/riddler/uxid/CorpusHashTest.java | 7 +- .../systems/riddler/uxid/DecodeClockTest.java | 2 +- .../systems/riddler/uxid/UxidDecodeTest.java | 2 +- .../riddler/uxid/UxidGenerateTest.java | 9 + 11 files changed, 279 insertions(+), 30 deletions(-) create mode 100644 README.md create mode 100644 docs/wire-format.md diff --git a/CLAUDE.md b/CLAUDE.md index 3db3984..3369f61 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -156,16 +156,16 @@ is `systems.riddler:uxid`, and its module and package are `systems.riddler.uxid`. The tree on `main` today holds these agent instructions, the wurk manifest -(`.claude/wurk.json`), the tracker files under `.beads/`, the build -(`pom.xml`, `mise.toml` and `.mvn/jvm.config`), the module descriptor, the -package it exports, `LICENSE`, `CHANGELOG.md` with the fragment rule in -`changelog.d/README.md`, the vendored conformance corpus under `conformance/` -(with `.gitattributes`, which keeps its line endings as committed), and the two -workflows: CI (`.github/workflows/ci.yml`) and the release workflow -(`.github/workflows/release.yml`). The package holds time-based and -deterministic generation (`Uxid.generate` and `UxidOptions`), decoding -(`Uxid.decode`, `DecodeOptions` and `DecodeResult`), the validity checks -(`Uxid.isValid` and `Uxid.isDeterministic`) and the monotonic generator +(`.claude/wurk.json`), the tracker files under `.beads/`, the build (`pom.xml`, +`mise.toml` and `.mvn/jvm.config`), the module descriptor, the package it +exports, `LICENSE`, `README.md`, `docs/wire-format.md`, `CHANGELOG.md` with the +fragment rule in `changelog.d/README.md`, the vendored conformance corpus under +`conformance/` (with `.gitattributes`, which keeps its line endings as +committed), and the two workflows: CI (`.github/workflows/ci.yml`) and the +release workflow (`.github/workflows/release.yml`). The package holds +time-based and deterministic generation (`Uxid.generate` and `UxidOptions`), +decoding (`Uxid.decode`, `DecodeOptions` and `DecodeResult`), the validity +checks (`Uxid.isValid` and `Uxid.isDeterministic`) and the monotonic generator (`Monotonic`, with its `DrawSource`). The sentences in this file about the 1.0.0 package describe the target, not the current tree: it is not yet published. @@ -181,8 +181,8 @@ published. worked round here. An answer the corpus does not fix follows uxid-ex's spec at that tag. -`README.md`, which a later change adds, is the user reference; `CHANGELOG.md` -records each release. +`README.md` is the user reference, and `docs/wire-format.md` points at the +reference spec at its tag; `CHANGELOG.md` records each release. ## Build & Test diff --git a/README.md b/README.md new file mode 100644 index 0000000..81c77f8 --- /dev/null +++ b/README.md @@ -0,0 +1,212 @@ +# uxid + +[![MIT License][badge_license_url]](LICENSE) +[![CI][badge_ci_url]](https://github.com/riddler/uxid-java/actions/workflows/ci.yml) + +User eXperience focused IDentifiers for Java: prefixed, K-sortable, +Stripe-style ids a person can read, copy and route back to their resource. An +id looks like `usr_01hf7yat0004h1`: the prefix names the resource, and the +body carries a time and randomness in lower-case Crockford Base32. The ids +follow the wire format of the Elixir reference implementation, +[uxid-ex](https://github.com/riddler/uxid-ex), and the package is checked +against that implementation's conformance corpus, every case, with no skip +list. + +## Why UXID + +An auto-increment key leaks how many rows a table holds and invites +enumeration; a random UUID fixes both but is long, unordered and anonymous, so +in a log line, a URL or a support thread nobody can tell what it points at, and +a double-click selects only part of it. A UXID names its resource in its +prefix, selects whole on a double-click, reads aloud without ambiguity, sorts +by creation time so it indexes well, and is generated in the application with +no coordination between nodes. Its size is tunable for low-cardinality +resources, a monotonic generator keeps a burst within one millisecond unique +and in order, and a deterministic mode maps the same input to the same id. + +Because the format is shared, an id this package makes is read by the other +implementations of the format, and the other way round. + +## Install + +Maven: + +```xml + + systems.riddler + uxid + 1.0.0 + +``` + +Gradle (Kotlin or Groovy DSL, on `mavenCentral()`): + +```kotlin +implementation("systems.riddler:uxid:1.0.0") +``` + +## Basic usage + +```java +import systems.riddler.uxid.DecodeResult; +import systems.riddler.uxid.Monotonic; +import systems.riddler.uxid.Size; +import systems.riddler.uxid.Uxid; +import systems.riddler.uxid.UxidOptions; + +public class Example { + public static void main(String[] args) { + // A prefix names the resource; the body is a time and randomness. + String id = Uxid.generate(UxidOptions.builder().prefix("usr").build()); + System.out.println(id.startsWith("usr_")); // true + + // Shrink the random part for low-cardinality resources. + // Sizes: XS, S, M, L and XL (the default). + String small = Uxid.generate(UxidOptions.builder().prefix("cus").size(Size.S).build()); + System.out.println(small.length() == "cus_".length() + 14); // true + + // Deterministic: the same prefix and input give the same id, every time. + String named = + Uxid.generate(UxidOptions.builder().prefix("usr").from("alice@example.com").build()); + System.out.println(named); // usr_zcvt7epac0t1ebcsjfyf7cwz25 + System.out.println(Uxid.isDeterministic(named)); // true + + // Read an id back. A malformed id is a result naming a reason, never an exception. + DecodeResult result = Uxid.decode("cus_01hf7yat0004h1"); + if (result instanceof DecodeResult.Decoded decoded) { + System.out.println(decoded.prefix()); // Optional[cus] + System.out.println(decoded.size()); // S + System.out.println(decoded.time()); // OptionalLong[1700000000000] + } + System.out.println(Uxid.isValid("cus_01hf7yat0004h1", "_", "cus")); // true + System.out.println(Uxid.decode("cus_")); // Invalid[reason=EMPTY] + + // Monotonic: ids from one generator increase, also within one millisecond. + // You hold the generator; the package keeps no global state. + Monotonic ids = new Monotonic(); + UxidOptions options = UxidOptions.builder().prefix("usr").size(Size.M).build(); + String first = ids.generate(options); + String second = ids.generate(options); + System.out.println(first.compareTo(second) < 0); // true + } +} +``` + +A generation request the wire format refuses, such as a random width above 10 +bytes, throws a `UxidException`, an `IllegalArgumentException`; switch on its +`code()`, an `ErrorCode`, or log its `wireName()`, a stable `snake_case` name, +rather than its message. A malformed argument of another kind, such as a null +prefix or a time outside the 48-bit range, throws the usual +`NullPointerException` or `IllegalArgumentException`. + +`Uxid.generate` and `Monotonic.generate` read the system clock when they need a +time, and a `SecureRandom` when they need random bytes. `UxidOptions.Builder.time` +and `byteSource` replace them; `DecodeOptions.Builder.clock` replaces the +`java.time.Clock` that `Uxid.decode` reads for a compact time. `rand` takes +fixed random bytes for test vectors: an id built from them is exactly as +guessable as those bytes. + +`Uxid.decode` never throws on malformed input. It answers a +`DecodeResult.Invalid` whose `InvalidReason` is the first rule the string +breaks, in the order `empty`, `invalid_length`, `invalid_character`. A compact +time carries only its low 40 bits, so a decoded compact time takes the rest +from the clock; the arithmetic is in `long`, so a clock within about 2^40 +milliseconds of `Long.MIN_VALUE` wraps silently, which no real clock does. + +`Uxid.isValid(id, delimiter, requiredPrefix)` and +`Uxid.isDeterministic(id, delimiter)` take the delimiter the id was generated +with, and read no clock. `isValid` answers whether the id decodes (a body of a +length the format defines, in Crockford Base32 characters) and, given a +required prefix, whether it carries exactly that prefix. `isDeterministic` +answers whether the id decodes and its body starts with the marker `z` or `Z`. +Both are stricter than the Elixir reference, whose `valid?` accepts any +alphabet body of at least 8 characters and whose `deterministic?` checks only +the first body character: here a malformed string is neither valid nor +deterministic. The released TypeScript, Rust and .NET ports read them the +same way, and the conformance corpus does not fix these two answers. + +A deterministic id hashes the prefix and the input as UTF-8, so a prefix or an +input holding an unpaired surrogate, which has no UTF-8 encoding, is refused +with an `IllegalArgumentException`. A time-based id writes its prefix as given +and accepts one. + +A `Monotonic` keeps the last time and random value for each prefix and random +width it is given, and the pair of time and random value increases with each +id (so does the id string, within one letter case, delimiter and time form). +The random value alone does not: when the clock reaches a later millisecond, +or the field overflows into the next one, the id carries a fresh seed, which +can be smaller than the last value. That state grows by one entry for each +distinct prefix and random width the generator is given and lives as long as +the generator. + +## Compatibility + +- **Java 17 and later.** The jars are built for `maven.compiler.release` 17. + CI proves the floor by running the tests on Temurin 17, 21 and 25. +- **No dependencies.** The package has no runtime dependency; JUnit 5 is a test + dependency only. +- **Module `systems.riddler.uxid`.** The jar declares the module and exports its + one package, `systems.riddler.uxid`. On the class path it is an ordinary + package of the same name. +- **Thread safety.** A `Monotonic` is safe for concurrent use: a lock inside + the instance orders its calls, so its ids increase in the order the calls + take the lock, whichever thread asks. Two generators never affect each + other, and ids from two generators carry no order between them within a + millisecond. `Uxid.generate`, `Uxid.decode` and the validity checks keep no + state and can be called from any thread. + +## Documentation + +- Look up + - The API reference is the Javadoc shipped with the package as a Javadoc + jar: every public type and member carries it, and a missing comment fails + the build. + - [The wire format](docs/wire-format.md): where the normative layout of an + id lives, and which version of it this package follows. + - [The changelog](CHANGELOG.md): what changed in each version. + - [The conformance corpus](conformance): the cases every implementation of + the wire format passes, vendored here from uxid-ex. +- Understand + - [The reference spec, at v2.10.0][guide_wire_format_url]: the normative + layout of an id - alphabet, bit packing, sizes, prefix grammar, + deterministic and monotonic bodies, and decoding. + - [Why a UXID has a prefix, a time and randomness][guide_why_url]: what each + part of an id is for, and what you trade when you tune it. + - [Why monotonic mode exists and what it costs][guide_monotonic_url]: + same-millisecond uniqueness and ordering, and the security tradeoff. + - [_Designing APIs for humans: object IDs_][stripe_ids_url]: the Stripe id + design many of UXID's choices follow. + +## Development + +After checking out the repository, run `mise install` for the JDK and Maven +`mise.toml` pins. Then `mise run gate:loop` runs the inner loop (compile and +tests), and `mise run gate` runs the full quality gate: the format check, the +compile with Error Prone and every javac lint, the tests with 90 percent line +coverage, the Javadoc and sources jars with the Javadoc lint on, and the +conformance corpus check. The tests include the conformance corpus, every case +with no skip list. + +A release is published by CI, never from a checkout: pushing a tag `vX.Y.Z` +that names the version in `pom.xml` runs `.github/workflows/release.yml`, which +publishes the package to Maven Central. + +## Contributing + +Bug reports and pull requests are welcome on GitHub at +https://github.com/riddler/uxid-java. + +## License + +The package is available as open source under the terms of the +[MIT License](https://opensource.org/licenses/MIT). + + +[guide_wire_format_url]: https://github.com/riddler/uxid-ex/blob/v2.10.0/guides/wire-format.md +[guide_why_url]: https://github.com/riddler/uxid-ex/blob/v2.10.0/guides/why-prefix-time-randomness.md +[guide_monotonic_url]: https://github.com/riddler/uxid-ex/blob/v2.10.0/guides/monotonic.md +[stripe_ids_url]: https://dev.to/stripe/designing-apis-for-humans-object-ids-3o5a + + +[badge_license_url]: https://img.shields.io/badge/license-MIT-brightgreen.svg +[badge_ci_url]: https://github.com/riddler/uxid-java/actions/workflows/ci.yml/badge.svg diff --git a/changelog.d/uxjv-5tk.md b/changelog.d/uxjv-5tk.md index 397dfd4..0a9cfb9 100644 --- a/changelog.d/uxjv-5tk.md +++ b/changelog.d/uxjv-5tk.md @@ -1,6 +1,6 @@ ### Added -- `Monotonic` is a generator the caller holds whose ids for one prefix and random width increase strictly, also within one millisecond and when the clock moves back: `generate()` and `generate(UxidOptions)` keep the last time and random value per prefix and random width, step the random field by a value drawn uniformly from 1 to 2^(4w) within a millisecond, and move the time one millisecond forward with a fresh seed when the field overflows. A monotonic `xs` uses compact time unless the options set the time form. -- One `Monotonic` may be shared between threads: a lock inside the instance orders the calls, so its ids still increase strictly. Nothing is kept in a static field, and two generators never affect each other. +- `Monotonic` is a generator the caller holds whose ids for one prefix and random width increase, also within one millisecond and when the clock moves back (the pair of time and random value increases, and the id string with it within one letter case, delimiter and time form; a fresh seed after a later clock or an overflow can be a smaller random value on its own): `generate()` and `generate(UxidOptions)` keep the last time and random value per prefix and random width, step the random field by a value drawn uniformly from 1 to 2^(4w) within a millisecond, and move the time one millisecond forward with a fresh seed when the field overflows. A monotonic `xs` uses compact time unless the options set the time form. +- One `Monotonic` may be shared between threads: a lock inside the instance orders the calls, so its ids still increase in the order the calls take the lock. Its state grows by one entry for each distinct prefix and random width it is given and lives as long as the generator. Nothing is kept in a static field, and two generators never affect each other. - `DrawSource` replaces a generator's seeds and steps to replay known draws; by default they come from each call's byte source, the step masked from its bytes with no modulo bias. - `Monotonic.generate(UxidOptions)` refuses a `from` input with `from_with_monotonic` and fixed random bytes with `rand_with_monotonic`. A call that throws leaves the generator's state as it was. diff --git a/docs/wire-format.md b/docs/wire-format.md new file mode 100644 index 0000000..c3c518e --- /dev/null +++ b/docs/wire-format.md @@ -0,0 +1,15 @@ +# Wire format + +This package does not define the id format; it follows it. The normative +statement of what a UXID string is - the alphabet, the bit packing, the sizes, +the prefix grammar, deterministic and monotonic bodies, and what a decoder +reads back - is the reference specification in uxid-ex: + +- [`guides/wire-format.md` at the tag `v2.10.0`](https://github.com/riddler/uxid-ex/blob/v2.10.0/guides/wire-format.md) + +The tag is the one `conformance/SOURCE.json` names. That file also records +the commit and the hash of the vendored conformance corpus, the cases every +implementation of the format passes; the cases in `conformance/` are the +arbiter where the specification and a reference implementation could be read +two ways. When the corpus is bumped to a newer tag, `SOURCE.json` and this +pointer move together. diff --git a/src/main/java/systems/riddler/uxid/Monotonic.java b/src/main/java/systems/riddler/uxid/Monotonic.java index cbc13da..7e00d49 100644 --- a/src/main/java/systems/riddler/uxid/Monotonic.java +++ b/src/main/java/systems/riddler/uxid/Monotonic.java @@ -7,12 +7,15 @@ import java.util.concurrent.locks.ReentrantLock; /** - * A monotonic generator: for one prefix and random width, the time and random value of each id it - * makes are greater than the last one's, also when several fall in one millisecond and when the - * clock moves back, so among ids of one letter case, delimiter and time form each sorts after the - * last as a string. It follows the "Monotonic generation" rule of the wire format. A monotonic id - * is an ordinary time-based id of its size and length: nothing in it says how it was made, and - * {@link Uxid#decode(String)} reads it as any other. + * A monotonic generator: for one prefix and random width, the pair of time and random value of each + * id it makes is greater than the last one's (the time is at least the last time, and when it is + * equal the random value is greater), also when several fall in one millisecond and when the clock + * moves back, so among ids of one letter case, delimiter and time form each sorts after the last as + * a string. The random value alone is not monotonic: when the clock reaches a later millisecond, or + * the field overflows into the next one, the id carries a fresh seed, which can be smaller than the + * last value. It follows the "Monotonic generation" rule of the wire format. A monotonic id is an + * ordinary time-based id of its size and length: nothing in it says how it was made, and {@link + * Uxid#decode(String)} reads it as any other. * *

Each call takes a {@link UxidOptions} as {@link Uxid#generate(UxidOptions)} does, and the * generator keeps its state per prefix and per random width: for each such pair it has seen, the @@ -56,7 +59,8 @@ * whichever thread makes them. The draw source or byte source is called with the lock held. The * state lives in the instance alone, with nothing kept in a static field, so two generators never * affect each other, and ids from two generators carry no order between them within a millisecond. - * The state for each pair is kept for the generator's lifetime. + * The state for each pair is kept for the generator's lifetime, so it grows by one entry for each + * distinct prefix and random width the generator is given. * *

A refused call. A call that throws leaves the generator as it was: the state of every * pair stays what it was before the call, and the next call starts from it. Draws a refused call diff --git a/src/main/java/systems/riddler/uxid/Uxid.java b/src/main/java/systems/riddler/uxid/Uxid.java index 4127330..fba5dcb 100644 --- a/src/main/java/systems/riddler/uxid/Uxid.java +++ b/src/main/java/systems/riddler/uxid/Uxid.java @@ -187,7 +187,9 @@ public static DecodeResult decode(String id) { * the clock. So the time comes back exact while the id and the clock fall in the same 2^40 ms * window (about 34.8 years) and the id is no more than one day ahead of the clock. With the clock * in the first window after the Unix epoch, the window before ends at the epoch, so a time read - * from it is negative: nothing clamps it at zero. The clock is read only for a compact body. + * from it is negative: nothing clamps it at zero. The clock is read only for a compact body. The + * arithmetic is in {@code long}, so a clock within about 2^40 ms of {@link Long#MIN_VALUE} wraps + * silently and the time read is meaningless; no real clock is there. * *

A string that is not a well-formed id is refused with the first rule its body breaks, * checked in this order: {@link InvalidReason#EMPTY} for an empty body, {@link @@ -228,6 +230,11 @@ public static boolean isValid(String id) { * has no prefix, so it fails any other. Validity does not depend on the clock, so this reads * none. * + *

This is stricter than the Elixir reference's {@code valid?}, which accepts any alphabet body + * of at least 8 characters: a body of a length the format does not define is not valid here. The + * conformance corpus does not fix this answer; the TypeScript, Rust and .NET ports read it the + * same way. + * * @param id the string to check * @param delimiter the delimiter the id was generated with, not empty * @param requiredPrefix the prefix the id must carry, or null or {@code ""} for any @@ -262,7 +269,8 @@ public static boolean isDeterministic(String id) { * Whether {@code id} is a well-formed deterministic id under {@code delimiter}: one {@link * #decode(String, DecodeOptions) decode} answers a {@link DecodeResult.Decoded} for whose body * starts with the marker {@code z} or {@code Z}. A string that is not well formed is not one, - * whatever its first body character. It reads no clock. + * whatever its first body character, which is stricter than the Elixir reference's {@code + * deterministic?}, which checks only that character. It reads no clock. * * @param id the string to check * @param delimiter the delimiter the id was generated with, not empty diff --git a/src/main/java/systems/riddler/uxid/UxidOptions.java b/src/main/java/systems/riddler/uxid/UxidOptions.java index d89a305..b3ebf9c 100644 --- a/src/main/java/systems/riddler/uxid/UxidOptions.java +++ b/src/main/java/systems/riddler/uxid/UxidOptions.java @@ -135,7 +135,9 @@ private Builder() {} /** * Sets the prefix, emitted before the delimiter exactly as given. The empty string, the * default, is no prefix: the id is the bare body, with no delimiter. A prefix may contain the - * delimiter. + * delimiter. A prefix is written as given for a time-based id, whatever characters it holds; + * for a {@link #from(String) deterministic} id it is hashed as UTF-8, so a prefix holding an + * unpaired surrogate is refused when the id is generated. * * @param prefix the prefix, or {@code ""} for none * @return this builder diff --git a/src/test/java/systems/riddler/uxid/CorpusHashTest.java b/src/test/java/systems/riddler/uxid/CorpusHashTest.java index 39a789f..1b07858 100644 --- a/src/test/java/systems/riddler/uxid/CorpusHashTest.java +++ b/src/test/java/systems/riddler/uxid/CorpusHashTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; import org.junit.jupiter.api.Test; @@ -46,7 +45,7 @@ void theManifestListsExactlyTheFilesTheRunnerRuns() { assertEquals(KINDS, manifest.files().stream().map(Corpus.Entry::kind).toList()); } - @Test // (sabotage: SOURCE.json hash edited -> the recorded hash assertion failed) + @Test // (sabotage: SOURCE.json tag or sha edited -> the tag or sha assertion failed) void sourceJsonRecordsTheSameHashAndTheTagItCameFrom() { Fields source = Fields.parse(Corpus.text("SOURCE.json")); String repo = source.string("repo"); @@ -56,8 +55,8 @@ void sourceJsonRecordsTheSameHashAndTheTagItCameFrom() { source.done(); assertEquals("riddler/uxid-ex", repo); - assertTrue(tag.startsWith("v"), tag); - assertTrue(sha.matches("[0-9a-f]{40}"), sha); + assertEquals("v2.10.0", tag); + assertEquals("57235777cb29c70f751552ce7aa20ab5fe71fb74", sha); assertEquals(Corpus.manifest().corpusHash(), recorded); assertEquals(recorded, Corpus.hash(CASE_FILES)); } diff --git a/src/test/java/systems/riddler/uxid/DecodeClockTest.java b/src/test/java/systems/riddler/uxid/DecodeClockTest.java index 7347e12..d1f5732 100644 --- a/src/test/java/systems/riddler/uxid/DecodeClockTest.java +++ b/src/test/java/systems/riddler/uxid/DecodeClockTest.java @@ -28,7 +28,7 @@ void theWindowStartIsInclusiveAndTheMillisecondBeforeItMissesTheCase() { OptionalLong.of(ClockWindow.CASE_TIME - WINDOW), timeAt(COMPACT_S, ClockWindow.START - 1)); } - @Test // (sabotage: > ONE_DAY made >= -> the END - 1 assertion failed) + @Test // (sabotage: > ONE_DAY made >= -> the END assertion failed) void theWindowEndIsExclusiveAndItsLastMillisecondRecoversTheGuardCase() { assertEquals(OptionalLong.of(ClockWindow.GUARD_LAST_VALID), timeAt(GUARD, ClockWindow.END - 1)); assertEquals( diff --git a/src/test/java/systems/riddler/uxid/UxidDecodeTest.java b/src/test/java/systems/riddler/uxid/UxidDecodeTest.java index 7782a56..cd8bda0 100644 --- a/src/test/java/systems/riddler/uxid/UxidDecodeTest.java +++ b/src/test/java/systems/riddler/uxid/UxidDecodeTest.java @@ -96,7 +96,7 @@ void bodyLengthsTenAndEighteenDecodeToTheStandardReading() { } } - @Test // (sabotage: upper-case folding removed -> the equality failed) + @Test // (sabotage: upper-case folding removed -> the decoded() instance check failed) void eitherCaseDecodesToTheSameValue() { assertEquals(decoded("usr_hf7yat0004hm5"), decoded("usr_HF7YAT0004HM5")); assertEquals(decoded("usr_01hf7yat0004h1"), decoded("usr_01Hf7YaT0004h1")); diff --git a/src/test/java/systems/riddler/uxid/UxidGenerateTest.java b/src/test/java/systems/riddler/uxid/UxidGenerateTest.java index fd8fb68..ebef772 100644 --- a/src/test/java/systems/riddler/uxid/UxidGenerateTest.java +++ b/src/test/java/systems/riddler/uxid/UxidGenerateTest.java @@ -219,6 +219,15 @@ void theDefaultSourceDrawsFreshBytesFromOneSharedInstance() { assertTrue(!java.util.Arrays.equals(first, second), "two draws of 80 bits were equal"); } + @Test // (sabotage: prefix UTF-8 encoded in the time path -> assertDoesNotThrow failed) + void aTimeBasedIdKeepsAPrefixWithNoUtf8EncodingAsGiven() { + TestSources.Counting source = new TestSources.Counting(); + UxidOptions options = + UxidOptions.builder().prefix("us\uD800").size(Size.S).time(TIME).byteSource(source).build(); + + assertEquals("us\uD800_01hf7yat000001", assertDoesNotThrow(() -> Uxid.generate(options))); + } + /** Reads the standard time from the first 10 characters of a body. */ private static long standardTime(String body) { long time = 0;