Skip to content

The provider registry

Providers make themselves reachable by name. A consumer names a platform in configuration, and the registry turns that string into a constructed provider without the consumer importing anything platform-specific.

func Register(name string, f Factory) error
func Unregister(name string)
func Lookup(name string) (Factory, bool)
func Registered() []string
func NewClient(_ context.Context, name string, cfg ClientConfig) (Client, error)
func New(ctx context.Context, name string, cfg Config) (*Provider, error) // deprecated

type Factory func(ClientConfig) (Client, error)

The registry is package-level state guarded by a sync.RWMutex. All six functions are safe to call concurrently. There is exactly one registry per process; there is no way to create a second, isolated one.

Register: claiming a name

func Register(name string, f Factory) error
Condition Result
name is "" ErrInvalidName, nothing registered
f is nil ErrNilFactory, nothing registered
name already taken ErrAlreadyRegistered, the existing factory is kept
otherwise nil, the factory is registered

A duplicate is refused, not overwritten. Silently replacing would let a blank-imported provider displace another with no diagnostic, with initialisation order deciding the winner, so the failure would surface much later, as the wrong provider being used, a long way from its cause.

Registering returns an error rather than panicking, which leaves the decision with the caller. A provider registering from init() has nothing sensible to do with a failure and should panic at its own call site, where the panic names the module at fault:

func init() {
    if err := chatplatform.Register("discord", factory); err != nil {
        panic("chat-platform-discord: " + err.Error())
    }
}

Names are matched exactly. They are not normalised, lower-cased or trimmed, so "Discord" and "discord" are two different providers.

Unregister: removing a name

func Unregister(name string)

Removes the factory registered under name. It has no return value and removing a name that was never registered is not an error.

It exists for tests, which must be able to leave the registry as they found it. Production code registers once from init() and never removes. Nothing prevents you calling it at runtime, but a provider already constructed from a factory keeps working; unregistering affects future lookups only.

Lookup: getting a factory by name

func Lookup(name string) (Factory, bool)

Returns the registered factory and true, or nil and false if the name is unknown. It never returns a non-nil factory with false, so the two-value form is safe to ignore only if you are certain of the name.

The commonest cause of false is a missing blank import. A provider registers itself from init(), so a binary that never imports the provider module has nothing registered under its name:

import _ "gitlab.com/phpboyscout/go/chat-platform-discord"

Registered: listing what is available

func Registered() []string

Returns every registered name, sorted, in a freshly allocated slice, so callers rendering them get a stable order and cannot mutate the registry by mutating the result.

Sorted rather than insertion-ordered because the natural uses are a help string and an error listing valid values, and both look broken if the order changes between runs:

if _, ok := chatplatform.Lookup(cfg.Platform); !ok {
    return fmt.Errorf("unknown platform %q; available: %s",
        cfg.Platform, strings.Join(chatplatform.Registered(), ", "))
}

The slice is empty, not nil, when nothing is registered.

NewClient: look up and build a transport

func NewClient(_ context.Context, name string, cfg ClientConfig) (Client, error)

The one-call path for a consumer holding a platform name from configuration. It looks the name up and calls the factory, which returns a Client: a transport you then mint one Provider per space from, via Client.Provider.

It does not connect. Nothing on this path reaches the network, which is why the context.Context is named _ in the implementation: lookup is an in-memory map read, and a factory must not dial. Cancelling the context has no effect and no deadline applies. It is part of the signature so a future registry lookup can honour one without a breaking change.

The consequence worth acting on: ErrForbidden cannot come from here. A platform cannot refuse a declared Need before anybody has asked it for one, so a permission check belongs at Reader.Connect. What this returns is ErrNotFound for an unknown name, or whatever the provider's own constructor returns for a configuration it will not accept.

It returns ErrNotFound for an unknown provider name. That is the same sentinel a provider returns for a message, thread or member that does not exist, so errors.Is(err, chatplatform.ErrNotFound) on this result means "no such platform" and nothing else. To tell an unknown name from a factory failure, use Lookup and check the boolean:

f, ok := chatplatform.Lookup(name)
if !ok {
    return fmt.Errorf("no provider registered as %q", name)
}

c, err := f(cfg) // any error here came from the provider

Not every transport is in the registry, and one is kept out on purpose. A Factory takes a ClientConfig, and a ClientConfig carries a token. A transport whose whole reason to exist is that the token never reaches it, Discord's voice node, has a configuration with no token field and is built by hand instead. The registry is for a consumer holding a platform name; a consumer building a node knows which one it is building.

New: the deprecated single-space path

func New(ctx context.Context, name string, cfg Config) (*Provider, error)

Deprecated, and removed one minor release after the one that introduced Client. Use NewClient and Client.Provider.

It still works, and returns a Provider that owns its transport: closing its Reader closes the connection underneath, so it does not leak. That ownership is also the problem. Calling it once per space dials a transport per space, and on a platform carrying many spaces over one connection (Discord) that meets connection rate limits in production rather than in a test with one space.

Unlike NewClient, this one does use its context: it forwards it to the provider's Provider method, which may honour it.

Factory: what a provider supplies

type Factory func(ClientConfig) (Client, error)

A factory validates a ClientConfig and returns a Client: a transport, not a scope. Spaces are named afterwards, through Client.Provider, which is what lets several of them share one connection.

It must not open a connection, and must not require a working credential. Connect is where the network happens. Both rules are load-bearing for testing: the conformance harness constructs every provider with no token at all and never dials, so a factory that authenticates eagerly cannot be conformance-tested at all.

A factory returning both a nil client and a nil error is a contract breach; the harness reports it, because a caller has nothing to check.