refid

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: Apache-2.0 Imports: 8 Imported by: 1

README

// SPDX-License-Identifier: Apache-2.0 // Copyright (c) 2026 Lanka Software Foundation

Reference ID Generation System (refid)

refid is a shared Go package in the OpenNSW system that generates structured, sequential reference IDs based on YAML configuration. It allows any NSW service to define and issue custom reference IDs without writing custom ID generation code.

Features

  • Config-Driven: Define ID structures for multiple Issuers and ID Types purely via YAML.
  • Typed Segments: Concatenate literal, list, date, and sequence segments into custom ID formats.
  • Durable Counters, Pluggable Backend: Atomic sequence increment via raw SQL (no ORM) against either the bundled PostgreSQL (refid/store/postgres) or SQLite (refid/store/sqlite) backend, or bring your own refid.SequenceStore implementation.
  • Flexible Resets: Scope key templates allow counters to reset daily ({yyyyMMdd}), monthly ({yyyyMM}), yearly ({yyyy}), or never.
  • Fail-Fast & Side-Effect Free: Two-pass generation validates all caller parameters before executing database side-effects.

Quickstart

package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"

	"github.com/OpenNSW/core/refid"
	"github.com/OpenNSW/core/refid/store/postgres"

	_ "github.com/jackc/pgx/v5/stdlib" // registers the "pgx" database/sql driver
)

func main() {
	ctx := context.Background()

	// 1. Load configuration
	cfg, err := refid.LoadConfig("config.yaml")
	if err != nil {
		log.Fatalf("failed to load config: %v", err)
	}

	// 2. Connect DB and migrate the sequence table
	dsn := "host=localhost port=5432 user=postgres password=postgres dbname=postgres sslmode=disable"
	db, err := sql.Open("pgx", dsn)
	if err != nil {
		log.Fatalf("failed to open db: %v", err)
	}
	if err := postgres.Migrate(ctx, db); err != nil {
		log.Fatalf("migration failed: %v", err)
	}

	// 3. Initialize Registry
	store, err := postgres.New(db)
	if err != nil {
		log.Fatalf("failed to create store: %v", err)
	}
	reg, err := refid.NewRegistry(cfg, store)
	if err != nil {
		log.Fatalf("failed to create registry: %v", err)
	}

	// 4. Generate reference ID
	id, err := reg.Generate(ctx, "RTA", "application_id", map[string]string{
		"officeCode": "COL",
	})
	if err != nil {
		log.Fatalf("generation failed: %v", err)
	}

	fmt.Println("Generated Reference ID:", id)
	// Output: RTA-APP-COL-20260818-000001
}

Segment Types

Segment Description Key Fields Example
literal Fixed text string value: "FCAU-" FCAU-
list Parameter value validated against a controlled list list: office_location, param: officeCode COL
date Current UTC date/time using Go reference layout layout: "20060102" 20260818
sequence Zero-padded durable counter scopeKey: "{issuer}:{idType}:{officeCode}:{yyyyMMdd}", padding: 6 000042

Scope Key Placeholders & Reset Cadence

Sequence segments resolve a scopeKey template per generation call. Each unique scope key gets its own independent counter.

Reserved placeholders:

  • {issuer} — Issuing authority (e.g. "RTA")
  • {idType} — Format identifier (e.g. "application_id")
  • {yyyy} — Current 4-digit UTC year (Yearly reset)
  • {yyyyMM} — Current UTC year + month (Monthly reset)
  • {yyyyMMdd} — Current UTC year + month + day (Daily reset)
  • {<param>} — Any caller-supplied param (e.g. {officeCode})

[!NOTE] Curly braces { and } are reserved syntax for placeholder delimiters in scopeKey templates.


Database Setup

refid.SequenceStore is a pluggable interface (Next(ctx, scopeKey, max) (int64, error)); the package ships two raw-SQL backends, each in its own subpackage.

Neither backend registers a database/sql driver — they only issue SQL against the *sql.DB you hand them, so you import the driver, open the connection, and pass the result in. That keeps the driver choice yours, and avoids an init panic in a binary that already registers the same driver name.

PostgreSQL (refid/store/postgres)

A single table (refid_sequences by default) with row-level atomic upsert:

CREATE TABLE IF NOT EXISTS refid_sequences (
    scope_key  TEXT        NOT NULL PRIMARY KEY,
    counter    BIGINT      NOT NULL DEFAULT 0,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Initialize it automatically via postgres.Migrate(ctx, db). To use a custom table name:

store, err := postgres.New(db, postgres.WithTableName("custom_sequences"))
err = postgres.Migrate(ctx, db, postgres.WithTableName("custom_sequences"))

db is a *sql.DB you opened yourself — the queries use PostgreSQL's native $1 placeholders, so any PostgreSQL driver works. The Quickstart above uses pgx.

SQLite (refid/store/sqlite)

Same schema shape and API. Import a SQLite driver — modernc.org/sqlite is pure Go, no CGO:

import _ "modernc.org/sqlite" // registers the "sqlite" driver

db, err := sql.Open("sqlite", "refid.db")
if err := sqlite.Migrate(ctx, db); err != nil { ... }
store, err := sqlite.New(db)

SQLite allows only one writer at a time, and nothing sets a busy timeout by default, so concurrent access can fail immediately with SQLITE_BUSY. Set a busy timeout in the DSN (e.g. sql.Open("sqlite", "file:refid.db?_busy_timeout=5000")) if you need Next to wait instead of failing, or use refid/store/postgres for real concurrent-safe access. This makes SQLite a convenient choice for local development and tests.

Bring your own backend

Any type implementing refid.SequenceStore works — a Redis-backed counter, an in-memory store for tests, etc. Whether Next is safe under concurrent or multi-process use is entirely up to your implementation; the two bundled backends above sit at different points on that spectrum, so check their docs for what each one actually guarantees.


Error Handling

Check sentinel errors using errors.Is(err, refid.Err...):

  • refid.ErrUnknownIssuer — Issuer not configured in registry.
  • refid.ErrUnknownIDType — ID Type not found under specified issuer.
  • refid.ErrInvalidParam — Required param missing, not in allowed list, or scopeKey placeholder un-substituted.
  • refid.ErrCounterOverflow — Sequence counter value exceeds configured padding width.

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) 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).

Usage

cfg, err := refid.LoadConfig("refid_config.yaml")
store, err := postgres.New(db) // github.com/OpenNSW/core/refid/store/postgres
reg, err := refid.NewRegistry(cfg, store)

id, err := reg.Generate(ctx, "RTA", "application_id", map[string]string{
    "officeCode": "COL",
})
// id == "RTA-APP-COL-20260817-000042"

Config format

See the package-level example_config.yaml for a fully annotated example.

Scope key placeholders

Sequence segments use a scopeKey template to determine counter scope. 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; omitting all date components gives a counter 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.

Index

Constants

This section is empty.

Variables

View Source
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")
)

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

func LoadConfig(path string) (Config, error)

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 Registry

type Registry interface {
	// Generate produces a new ID for the given issuer and idType.
	//
	// params supplies caller-provided values consumed by list and sequence
	// 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
	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, store SequenceStore) (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

Fail-fast at startup: every error that would surface at generation time is caught here instead.

type SegmentConfig

type SegmentConfig struct {
	// Type is one of: "literal", "list", "date", "sequence".
	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"`

	// ScopeKey is a template string for sequence segments. 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,omitempty"`

	// Padding is the minimum number of digits for a sequence segment's 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,omitempty"`
}

SegmentConfig is the raw configuration for a single segment. Fields are interpreted according to Type; unused fields are ignored.

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 New. 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 a PostgreSQL-backed implementation of refid.SequenceStore using database/sql and raw SQL — no ORM.
Package postgres provides a PostgreSQL-backed implementation of refid.SequenceStore using database/sql and raw SQL — no ORM.
sqlite
Package sqlite provides a SQLite-backed implementation of refid.SequenceStore using database/sql and raw SQL — no ORM.
Package sqlite provides a SQLite-backed implementation of refid.SequenceStore using database/sql and raw SQL — no ORM.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL