Documentation
¶
Overview ¶
Package refid provides a shared, config-driven reference ID generation system for NSW services.
Overview ¶
Each ID format is defined as an ordered list of typed segments (literal, list, date, sequence, random) that are concatenated at generation time. Formats are grouped by issuer and identified by an idType, and are looked up from a Registry by (issuer, idType). A format may contain at most one stateful segment (sequence or random) — see "Stateful segment limit" below.
Usage ¶
cfg, err := refid.LoadConfig("refid_config.yaml")
store, err := postgres.NewSequence(db) // github.com/OpenNSW/core/refid/store/postgres
reg, err := refid.NewRegistry(cfg, refid.WithSequenceStore(store))
id, err := reg.Generate(ctx, "RTA", "application_id", map[string]string{
"officeCode": "COL",
})
// id == "RTA-APP-COL-20260817-000042"
WithSequenceStore and WithRandomStore are both optional — only supply the one(s) backing segment types actually used in cfg. If cfg uses random segments, also pass a RandomStore:
randomStore, err := postgres.NewRandom(db) reg, err := refid.NewRegistry(cfg, refid.WithSequenceStore(store), refid.WithRandomStore(randomStore))
Config format ¶
See the package-level example_config.yaml for a fully annotated example.
Scope key placeholders ¶
Sequence and random segments use a scopeKey template to determine the scope within which their values must be unique (a counter for sequence segments, the set of previously issued values for random segments). Curly braces '{' and '}' are reserved in scopeKey templates for placeholder delimiters. The following placeholders are resolved at generation time:
{issuer} — the issuer identifier for this format
{idType} — the ID type identifier for this format
{yyyy} — four-digit year (UTC)
{yyyyMM} — year and month (UTC)
{yyyyMMdd} — year, month, and day (UTC)
{<param>} — any caller-supplied param not already claimed above
Including {yyyyMMdd} in a scope key gives a daily-resetting counter (or daily-reset uniqueness set, for random segments); omitting all date components gives a scope that never resets.
Counter overflow ¶
If a counter exceeds the maximum value representable with the configured padding width, Generate returns ErrCounterOverflow. This is intentional: a wider-than-expected ID would silently break any downstream system that validates ID length. Operations should be alerted and the scope key configuration reviewed.
Random segment exhaustion ¶
A random segment generates a value from its charset/length and reserves it via RandomStore, retrying on collision up to maxAttempts (default 10). If every attempt collides, Generate returns ErrRandomExhausted — this means the charset/length combination is too small for the number of values already issued in that scope; widen the charset or length, or narrow the scope key (e.g. add {yyyyMMdd}).
Stateful segment limit ¶
Generate has no transaction or rollback across segments: it validates all segments, then renders them in order, and a sequence or random segment's store call (a counter increment, a random value reservation) takes effect immediately as a side effect of rendering. If a format had two or more stateful segments and a later one failed during render (e.g. a sequence segment overflowing, or a random segment exhausting its retries), an earlier stateful segment's already-committed side effect would be permanently orphaned — for a random segment, that permanently wastes one value from its bounded charset/length space for no returned ID. To rule this out, NewRegistry rejects any format with more than one sequence or random segment combined.
Index ¶
Constants ¶
const ( SegmentTypeLiteral = "literal" SegmentTypeList = "list" SegmentTypeDate = "date" SegmentTypeSequence = "sequence" SegmentTypeRandom = "random" )
Segment type names accepted by SegmentConfig.Type.
const ( CharsetNumeric = "numeric" CharsetAlpha = "alpha" CharsetAlphanumeric = "alphanumeric" )
Charset names accepted by a random segment's Charset config field.
Variables ¶
var ( // ErrUnknownIssuer is returned when Generate is called with an issuer // that was not present in the config used to build the registry. ErrUnknownIssuer = errors.New("refid: unknown issuer") // ErrUnknownIDType is returned when Generate is called with an idType // that was not declared under the given issuer. ErrUnknownIDType = errors.New("refid: unknown id type") // ErrInvalidParam is returned when a list segment's required caller-supplied // param is missing or its value is not in the allowed list. ErrInvalidParam = errors.New("refid: invalid or missing param") // ErrCounterOverflow is returned when the sequence counter value for a scope // key exceeds the number of digits allowed by the segment's padding setting. // For example, a counter of 1,000,001 with padding:6 would produce a 7-digit // string, breaking the expected ID format. Callers should alert operations // when this occurs; the scope key is likely configured too broadly. ErrCounterOverflow = errors.New("refid: sequence counter exceeds padding width") // ErrRandomCollision is returned by RandomStore.Reserve when the given // value is already reserved under the same scope key. A random segment // treats this as a signal to generate a new value and retry, up to its // configured maxAttempts. ErrRandomCollision = errors.New("refid: random value already reserved for this scope") // ErrRandomExhausted is returned when a random segment could not find an // unreserved value within its configured maxAttempts. This usually means // the charset/length combination is too small for the volume of IDs being // issued in that scope; widen the charset or length, or narrow the scope. ErrRandomExhausted = errors.New("refid: random segment exhausted attempts without finding an unreserved value") )
Sentinel errors returned by the refid package. Use errors.Is to check for these in calling code.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Issuers is the ordered list of issuer definitions. Each issuer owns one
// or more ID type formats.
Issuers []IssuerConfig `yaml:"issuers"`
// Lists is a map of named value sets. Segments of type "list" reference
// these by name, and the value supplied by the caller at generation time is
// validated against the corresponding slice.
Lists map[string][]string `yaml:"lists"`
}
Config is the top-level configuration for the ID generation system. It is typically loaded from a YAML file via LoadConfig, but can also be constructed programmatically (e.g. in tests or for embedded defaults).
func LoadConfig ¶
LoadConfig reads and parses a YAML configuration file at the given path. It performs no semantic validation; call NewRegistry to validate the config against the full constraint set.
type FormatConfig ¶
type FormatConfig struct {
// IDType uniquely identifies this format within the issuer, e.g.
// "application_id" or "permit_id". It is also available as the {idType}
// placeholder in scope key templates.
IDType string `yaml:"idType"`
// Segments is the ordered list of typed segments whose rendered outputs are
// concatenated to form the final ID string.
Segments []SegmentConfig `yaml:"segments"`
}
FormatConfig describes how a single ID type is assembled from an ordered list of segments.
type IssuerConfig ¶
type IssuerConfig struct {
// Issuer is the unique identifier for this issuing authority, e.g. "RTA".
// It is also available as the {issuer} placeholder in scope key templates.
Issuer string `yaml:"issuer"`
// Formats is the list of ID type formats owned by this issuer.
Formats []FormatConfig `yaml:"formats"`
}
IssuerConfig groups all ID format definitions for a single issuer.
type RandomSegmentConfig ¶ added in v0.2.0
type RandomSegmentConfig struct {
// ScopeKey is a template string determining the scope within which
// generated values must be unique. Placeholders are resolved at
// generation time; see the Registry documentation for the full
// placeholder reference.
ScopeKey string `yaml:"scopeKey"`
// Charset selects the alphabet to draw characters from.
// One of: "numeric", "alpha", "alphanumeric".
Charset string `yaml:"charset"`
// Length is the number of characters to generate.
Length int `yaml:"length"`
// MaxAttempts caps the number of collision retries before Generate
// returns ErrRandomExhausted. Defaults to 10 if unset; must be between
// 0 and 100.
MaxAttempts int `yaml:"maxAttempts,omitempty"`
}
RandomSegmentConfig holds the settings for a random segment.
type RandomStore ¶ added in v0.2.0
type RandomStore interface {
// Reserve atomically records value as issued under scopeKey. If value is
// already reserved under the same scope key, Reserve returns
// ErrRandomCollision without side effects, and the caller should generate
// a new value and retry.
Reserve(ctx context.Context, scopeKey, value string) error
}
RandomStore is the persistence interface for tracking issued random ID values so that collisions can be detected. Each distinct scope key has its own independent set of reserved values.
The refid/store/postgres and refid/store/sqlite subpackages each provide an implementation via their NewRandom constructor. Any caller that needs a different backend (Redis, in-memory for tests, etc.) can provide their own implementation.
type Registry ¶
type Registry interface {
// Generate produces a new ID for the given issuer and idType.
//
// params supplies caller-provided values consumed by list, sequence, and
// random segments (e.g. map[string]string{"officeCode": "COL"}). Unused
// keys are silently ignored; missing required keys return ErrInvalidParam.
//
// Errors:
// - ErrUnknownIssuer — issuer not found in config
// - ErrUnknownIDType — idType not found under the given issuer
// - ErrInvalidParam — a required param is missing or has an invalid value
// - ErrCounterOverflow — sequence counter exceeds padding width
// - ErrRandomExhausted — random segment found no free value within maxAttempts
Generate(ctx context.Context, issuer, idType string, params map[string]string) (string, error)
}
Registry is the entry point for generating IDs. Obtain one via NewRegistry.
Implementations must be safe for concurrent use by multiple goroutines.
func NewRegistry ¶
func NewRegistry(cfg Config, opts ...RegistryOption) (Registry, error)
NewRegistry validates cfg and compiles all formats into an internal lookup table. It returns an error if the config contains:
- duplicate (issuer, idType) pairs
- a segment that references an undefined list name
- a segment with missing required fields (e.g. empty scopeKey, empty layout)
- an unrecognised segment type
- more than one stateful (sequence or random) segment in a single format
Fail-fast at startup: every error that would surface at generation time is caught here instead.
type RegistryOption ¶ added in v0.2.0
type RegistryOption func(*registryConfig)
RegistryOption configures optional behavior for NewRegistry.
func WithRandomStore ¶ added in v0.2.0
func WithRandomStore(store RandomStore) RegistryOption
WithRandomStore supplies the RandomStore used to back "random" segments. It is only required if the config contains at least one random segment; NewRegistry returns an error when called if one is used without this option set.
func WithSequenceStore ¶ added in v0.2.0
func WithSequenceStore(store SequenceStore) RegistryOption
WithSequenceStore supplies the SequenceStore used to back "sequence" segments. It is only required if the config contains at least one sequence segment; NewRegistry returns an error when called if one is used without this option set.
type SegmentConfig ¶
type SegmentConfig struct {
// Type is one of the SegmentType* constants: SegmentTypeLiteral,
// SegmentTypeList, SegmentTypeDate, SegmentTypeSequence, SegmentTypeRandom.
Type string `yaml:"type"`
// Value is the fixed text for a literal segment.
Value string `yaml:"value,omitempty"`
// List is the name of a list defined in Config.Lists, used by list segments.
List string `yaml:"list,omitempty"`
// Param is the key the caller must supply in their params map, used by list
// segments to look up the caller-provided value.
Param string `yaml:"param,omitempty"`
// Layout is a Go reference-date format string (e.g. "20060102"), used by
// date segments.
Layout string `yaml:"layout,omitempty"`
// Sequence holds the settings for a sequence segment. Required (non-nil)
// when Type is "sequence".
Sequence *SequenceSegmentConfig `yaml:"sequence,omitempty"`
// Random holds the settings for a random segment. Required (non-nil) when
// Type is "random".
Random *RandomSegmentConfig `yaml:"random,omitempty"`
}
SegmentConfig is the raw configuration for a single segment. Fields are interpreted according to Type; unused fields are ignored.
type SequenceSegmentConfig ¶ added in v0.2.0
type SequenceSegmentConfig struct {
// ScopeKey is a template string determining the durable counter's scope.
// Placeholders are resolved at generation time. Curly braces '{' and '}'
// are reserved as placeholder delimiters in scope keys. See the Registry
// documentation for the full placeholder reference.
ScopeKey string `yaml:"scopeKey"`
// Padding is the minimum number of digits for the counter. The counter is
// zero-padded to this width. Must be between 1 and 18. If the counter
// exceeds the maximum value representable with Padding digits,
// ErrCounterOverflow is returned.
Padding int `yaml:"padding"`
}
SequenceSegmentConfig holds the settings for a sequence segment.
type SequenceStore ¶
type SequenceStore interface {
// Next atomically increments the counter for the given scope key and returns
// the new value, provided the current counter is less than max.
// If the counter would exceed max, Next returns ErrCounterOverflow without
// incrementing the counter in storage.
Next(ctx context.Context, scopeKey string, max int64) (int64, error)
}
SequenceStore is the persistence interface for durable, atomic sequence counters. Each distinct scope key gets its own counter, starting at 1.
The refid/store/postgres and refid/store/sqlite subpackages each provide an implementation via their NewSequence constructor. Any caller that needs a different backend (Redis, in-memory for tests, etc.) can provide their own implementation.
Directories
¶
| Path | Synopsis |
|---|---|
|
store
|
|
|
internal/sqlident
Package sqlident validates SQL identifiers (table names) that refid's storage backends interpolate directly into raw SQL strings.
|
Package sqlident validates SQL identifiers (table names) that refid's storage backends interpolate directly into raw SQL strings. |
|
postgres
Package postgres provides PostgreSQL-backed implementations of refid.SequenceStore (via NewSequence) and refid.RandomStore (via NewRandom) using database/sql and raw SQL — no ORM.
|
Package postgres provides PostgreSQL-backed implementations of refid.SequenceStore (via NewSequence) and refid.RandomStore (via NewRandom) using database/sql and raw SQL — no ORM. |
|
sqlite
Package sqlite provides SQLite-backed implementations of refid.SequenceStore (via NewSequence) and refid.RandomStore (via NewRandom) using database/sql and raw SQL — no ORM.
|
Package sqlite provides SQLite-backed implementations of refid.SequenceStore (via NewSequence) and refid.RandomStore (via NewRandom) using database/sql and raw SQL — no ORM. |