Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

verreg

Go Report Card Go Reference License: MIT

A small, generic versioned registry. It maps (version, name) pairs to values of any type F — a handler, a factory, a codec, a validator, anything. Versions are additive: each version inherits every entry from the one before it and may override individual names.

No global state. No lazy init. No dependencies beyond the standard library.


Concept

You only register what changes. Every version implicitly inherits everything from the version before it.

foo bar baz qux new
v0 defined defined defined defined
v1 defined (v0) override (v0)
v2 (v1) (v0) (v1) (v0) defined
  • reg.Resolve(2, "bar") → the value registered in v0
  • reg.Resolve(1, "baz") → the override registered in v1
  • reg.Resolve(2, "baz") → the same v1 override, inherited by v2

Use cases

Protocol message dispatch — a network protocol where each version may add or change message types. Register a handler or factory per (version, verb) pair; after version negotiation with a peer, a single Resolve call finds the right one.

API request handlers — a versioned HTTP or RPC API where /v2/payments shares most logic with /v1/payments but a few endpoints changed. Register handlers by version and route; only override the routes that differ.

File format codecs — a file format that evolves across versions. Register a decoder per (version, chunk-type); older chunk types are automatically inherited by newer format versions.

Feature flags or validators — any keyed behaviour that needs to evolve across numbered releases without re-registering unchanged entries every time.


Usage

type Handler func(Input) (Output, error)

reg := verreg.NewRegistry[Handler]()

// Version 0 — initial set
reg.Register(0, "foo", fooV0)
reg.Register(0, "bar", barV0)
reg.Register(0, "baz", bazV0)

// Version 1 — only declare what changes
reg.Register(1, "baz", bazV1)

// Mark versions that should no longer be used
reg.DeprecateVersion(0)

// Build must be called once before any Resolve call
reg.Build()

Looking up an entry:

handler, err := reg.Resolve(version, name)
switch {
case errors.Is(err, verreg.ErrDeprecatedVersion):
    // version is deprecated; ask the caller to upgrade
case errors.Is(err, verreg.ErrUnknownVersion):
    // version was never registered
case errors.Is(err, verreg.ErrUnknownEntry):
    // name not registered for this version
case err != nil:
    // verreg.ErrNotBuilt — Build() was not called
}

result, err := handler(input)

The caller already holds version and name, so the sentinel errors carry no redundant payload. Wrap them yourself if you need context in logs:

if err != nil {
    return fmt.Errorf("resolve %q (v%d): %w", name, version, err)
}

Re-registration

Register resets the built flag. Call Build again after any late additions:

reg.Register(2, "upload", uploadV2)
reg.Build() // re-flattens; prior Resolve results are unaffected

Checking version support

if !reg.SupportsVersion(v) {
    // reject the connection before dispatching any messages
}

SupportsVersion is safe to call without Build — it reports on the flattened table, so it returns false for anything not yet built.


Errors

Sentinel Condition
ErrNotBuilt Resolve called before Build
ErrDeprecatedVersion version was marked with DeprecateVersion
ErrUnknownVersion version has no registrations at all
ErrUnknownEntry name not registered for that version

All sentinels are plain errors.New values — no allocation on the error path, compatible with errors.Is.


Dependencies

None.

About

A small, generic versioned registry

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages