Skip to content

Typed config section adapters

Reusable packages no longer read config.Containable inside their cores. Each extraction-candidate package now owns a typed settings struct (with mapstructure tags), and GTB reads config into that struct at an adapter boundary. The practical rule for callers:

  • Core constructors take typed settings (and a *slog.Logger — see Logger slog-first boundary).
  • Framework callers that read GTB config use the *FromContainable / *FromConfig / *FromProps adapter, which preserves the existing config keys, precedence, env binding, and hot-reload behaviour.

This is an intentional pre-1.0 breaking change. The config keys themselves are unchanged — only the Go API moved.

New pkg/config APIs (for adapter authors)

If you write your own adapters, these read a resolved config section into a typed struct:

// one-shot
section, err := config.UnmarshalSection[http.ServerSettings](cfg, "server.http")
_ = section.Value   // T
_ = section.Exists  // was the section present?

// long-lived / reload-aware: re-unmarshals on each successful reload,
// retains the last valid snapshot on a rejected reload, exposes Version()
observed, err := config.ObserveSection[http.ServerSettings](cfg, "server.http",
    config.WithSectionValidator(validate),
    config.WithSectionApply(apply),
)

// source-aware existence, distinct from a defaulted zero value
if cfg.SectionExists("telemetry") { ... }

config.WithLogger and config.NewContainerFromViper now take a *slog.Logger.

Transports (pkg/http, pkg/grpc)

Core Register/Start now take a typed ServerSettings and a *slog.Logger; the config-reading forms were renamed. Rename the call, keep the arguments:

// before
srv, _ := grpc.Register(ctx, "grpc", controller, cfg, log)
controls.WithStart(grpc.Start(cfg, log, srv))
// after
srv, _ := grpc.RegisterFromContainable(ctx, "grpc", controller, cfg, log)
controls.WithStart(grpc.StartFromContainable(cfg, log, srv))

The same applies to http.Registerhttp.RegisterFromContainable and http.Start*http.StartFromContainable. Stop keeps its name but now takes a *slog.Logger (grpc.Stop(logger.ToSlog(log), srv)). For reload-aware composition, bind long-lived servers with ObserveServerSettingsFromConfig (exposes Current() / Version()); NewServerFromContainable remains for one-shot construction.

Gateway (pkg/gateway)

gateway.Register core takes typed http.ServerSettings + a prepared *grpc.ClientConn; the config-driven entrypoints are gateway.RegisterFromContainable (raw config.Containable) and gateway.RegisterFromConfig. ObserveSettingsFromConfig backs reload-aware gateways.

TLS (pkg/tls)

TLS resolution is now typed. The core is tls.ResolvePair(shared, transport, overrides) Pair and Pair.ServerConfig(...); GTB reads it with the adapter:

pair := tls.Resolve(cfg, "server.http") // config.Containable + prefix -> tls.Pair

VCS release providers (pkg/vcs/*)

Provider constructors take typed settings instead of config.Containable:

// before
p, err := gitlab.NewReleaseProvider(cfg)               // config.Containable subtree
// after
settings := gitlab.SettingsFromConfig(src, tokenCfg)   // GTB adapter builds it
p, err := gitlab.NewReleaseProvider(settings)

The same shape applies to the github, gitea, bitbucket, and direct providers.