reckon_gater_stream_id (reckon_gater v2.3.1)

View Source

Stream-id format validator + generator — the protocol contract.

Single source of truth for what "a valid stream id" means across the reckon-db ecosystem. Lives in reckon-gater so it is reachable from both reckon-db (write-time validation) and reckon-evoq (adapter / dispatch-time generation) without dragging reckon-db's khepri/Ra payload into pure-routing consumers.

Accepted formats

  • **User stream:** <prefix>-<hex>, where:
    • <prefix> is [a-z]{1,32} — one to thirty-two ASCII lowercase letters. No digits, no hyphens, no $, no uppercase.
    • - is a single mandatory separator.
    • <hex> is [a-f0-9]{32} — exactly thirty-two lowercase hex digits. One UUID-worth of bits (128).
    Examples: order-018f6a7b8c9d4e5f60718293a4b5c6d7, sess-005774fd728b1b2866cd18ff294467e1.
  • **System stream:** $<namespace>:<name>, where:
    • $ is a mandatory prefix.
    • <namespace> is [a-z][a-z0-9-]* — lowercase identifier, may contain hyphens (e.g. link-sub).
    • : is a single mandatory separator.
    • <name> is [A-Za-z0-9][A-Za-z0-9_.-]* — intentionally human-readable; system streams exist for operational legibility.
    Examples: $link:high-value-orders, $link-sub:revenue.

History

v2.2.0 tightened the user-stream regex from ^[A-Za-z]+-[A-Fa-f0-9]+$ to ^[a-z]{1,32}-[a-f0-9]{32}$ and added the new/1 generator. Motivation: the permissive regex admitted a-0, Order-DEADBEEF, and demo-1779045695... — inconsistent shapes that broke logging, projection grouping, and downstream tooling. Lowercase + fixed-length 32-hex suffix gives every user id 128 bits of entropy and a predictable length.

Migration: previously stored ids that don't conform are still readable (validate/1 is only called from reckon_db_streams:append/4); they just can't accept new events. Since reckon-db has no production deployments at this point, no migration path is required.

Rejected (returns `{error, Reason}')

  • Empty binary — empty
  • Not a binary — not_binary
  • Anything starting with $ that isn't a well-formed system id — malformed_system_id
  • Anything not starting with $ that isn't a well-formed user id — malformed_user_id

Rejected examples: a-0 (suffix too short), Order-DEADBEEF (uppercase + suffix too short), account- (empty suffix), -deadbeef (empty prefix), test$basic-stream (mid-string $).

Summary

Types

Note: invalid_prefix is NOT a validation_error/0 — it is the reason class raised by new/1 via erlang:error/1 when the supplied prefix is malformed. validate/1 itself never sees a prefix in isolation, so it cannot produce that variant.

Functions

True if StreamId is in the system namespace (starts with $ and is well-formed). Note: $all is NOT a valid stream id — it's a subscription-selector sentinel only.

Boolean wrapper for use in guards / list-comprehensions.

Build a fresh, valid user stream id with the given prefix.

Extract the prefix segment from a well-formed user stream id. Returns undefined for system ids and malformed ids.

Extract the hex-suffix segment from a well-formed user stream id. Returns undefined for system ids and malformed ids.

Validate StreamId. Returns ok if it matches either accepted format, or {error, Reason}.

Types

prefix/0

-type prefix() :: atom() | binary().

validation_error/0

-type validation_error() :: empty | not_binary | malformed_user_id | malformed_system_id.

Note: invalid_prefix is NOT a validation_error/0 — it is the reason class raised by new/1 via erlang:error/1 when the supplied prefix is malformed. validate/1 itself never sees a prefix in isolation, so it cannot produce that variant.

Functions

is_system(Id)

-spec is_system(binary()) -> boolean().

True if StreamId is in the system namespace (starts with $ and is well-formed). Note: $all is NOT a valid stream id — it's a subscription-selector sentinel only.

is_valid(StreamId)

-spec is_valid(term()) -> boolean().

Boolean wrapper for use in guards / list-comprehensions.

new(Prefix)

-spec new(prefix()) -> nonempty_binary().

Build a fresh, valid user stream id with the given prefix.

Suffix is the 16-byte UUIDv7 from reckon_gater_uuid:v7/0 rendered as 32 lowercase hex chars — so the id is BOTH regex-compliant AND time-sortable.

Prefix must match [a-z]{1,32}; raises {invalid_prefix, Prefix} otherwise. Accepts atom or binary.

Examples:

  reckon_gater_stream_id:new(<<"sess">>).
  %% => <<"sess-019d7a4f3c2a7d8c9e0f1234567890ab">>
 
  reckon_gater_stream_id:new(order).
  %% => <<"order-019d7a4f3c2a7d8c9e0f1234567890ab">>

prefix_of(StreamId)

-spec prefix_of(binary()) -> binary() | undefined.

Extract the prefix segment from a well-formed user stream id. Returns undefined for system ids and malformed ids.

suffix_of(StreamId)

-spec suffix_of(binary()) -> binary() | undefined.

Extract the hex-suffix segment from a well-formed user stream id. Returns undefined for system ids and malformed ids.

validate(Id)

-spec validate(term()) -> ok | {error, validation_error()}.

Validate StreamId. Returns ok if it matches either accepted format, or {error, Reason}.