Skip to content

feat: add self-registering provider factory (v0.2.0) - #7

Merged
Wikid82 merged 10 commits into
mainfrom
feature/provider-registry
Aug 17, 2026
Merged

feat: add self-registering provider factory (v0.2.0)#7
Wikid82 merged 10 commits into
mainfrom
feature/provider-registry

Conversation

@Wikid82

@Wikid82 Wikid82 commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a self-registering provider factory registry, mirroring database/sql's Register/driver pattern, so new providers become discoverable by a consuming project without hand-edited switch statements.

type Factory func(config map[string]any) (Sender, error)
func Register(name string, factory Factory)
func New(name string, config map[string]any) (Sender, error)
func RegisteredTypes() []string

Each of the 8 existing providers (discord, slack, gotify, pushover, ntfy, webhook, telegram, email) now self-registers via init(). A new providers/all package blank-imports all of them for one-line full discovery. The existing typed constructors (discord.New(Config{...}, w) etc.) are unchanged and remain the primary, fully type-safe API — the registry is additive, not a replacement.

This is a breaking/additive change to the public API introduced in v0.1.0 (new exported identifiers + init-time side effects), so it ships as v0.2.0. v0.1.0 has not been adopted by anyone outside this project yet, so there's no compatibility concern.

Why map[string]any over json.RawMessage

email.Config carries non-serializable Go values (Mailer/TemplateRenderer interfaces, a TemplateName closure) that can never round-trip through JSON. A generic map keeps the registry usable for every provider, including that one.

Docs

  • ARCHITECTURE.md (new): exactly how to add a new provider — package layout, the Sender contract, where the Register/init() call goes, providers/all wiring, test conventions. Written for a human contributor or a coding agent to follow without guessing.
  • docs/INTEGRATION.md (new): Five Ws and One H integration guide for adopting this module in a project other than Charon.
  • README: new section pointing at the registry pattern.
  • CHANGELOG: retroactively split the old catch-all "Unreleased" entry into [0.1.0] (already shipped) and added [0.2.0] on top, so it's accurate going forward.

Validation

  • go build/go vet/staticcheck: clean
  • go test ./... -race: passing
  • Coverage: 95.5% overall, no per-package regressions
  • Zero github.com/Wikid82/charon/* imports (grep-confirmed)
  • goreleaser release --snapshot --clean dry-run: succeeded

Downstream

Charon's adoption of this registry (replacing its hardcoded provider-construction switch with notify.New(...), while keeping its own explicit allowlist of which provider types it exposes) is in progress as part of Charon PR #1253, currently pinned to this branch via a local replace directive pending this merge + a real tagged release.

🤖 Generated with Claude Code

Wikid82 and others added 10 commits August 15, 2026 07:25
chore: propagate main into development
Register/New/RegisteredTypes at the module root, mirroring the
database/sql self-registration idiom: Register panics on a nil factory
or duplicate name (programmer error, caught at init time); New returns
an error (never panics) for an unregistered provider type. Thread-safe
via a sync.RWMutex-guarded map.

This is the enabling mechanism for per-provider init()-based
registration landing in follow-up commits.
StringField/StringSliceField extract plain string/[]string values out
of a Factory's generic map[string]any config, tolerating missing or
wrong-typed keys by returning the zero value rather than erroring —
each provider's register.go decides which zero values are actually
required. Avoids duplicating the same type-assert boilerplate across
eight register.go files.
Each package gains an init()-time notify.Register("<name>", factory)
call in a new register.go, adapting the generic map[string]any config
into the package's typed Config via the shared regconfig helpers. The
existing typed New(cfg, wrapper) constructors are unchanged.
…egistry

Same init()-time notify.Register pattern as discord/slack, adapted to
each package's own Config fields (gotify/ntfy: url+token; pushover:
user_key+api_token+base_url).
Same init()-time notify.Register pattern as the other providers,
adapted to each package's own Config fields (telegram: bot_token+
chat_id+base_url; webhook: url).
Highest-design-risk registration: email.Config carries non-serializable
Mailer/TemplateRenderer interfaces and a TemplateName closure, so its
factory takes these directly out of the map[string]any config (under
"mailer"/"renderer"/"template_name") rather than JSON-round-tripping
plain data like the seven HTTP providers. "mailer" is the one required
key; a missing or wrong-typed value returns an error, never a panic.
One-line "import _ .../providers/all" gets every built-in provider
registered, for consumers that want zero-touch discovery over
hand-picked imports. Includes a count-check test (asserting exactly 8
registered types) that acts as the CI safety net for the one step in
adding a new provider with no compiler enforcement — forgetting to add
it to this bundle.
- ARCHITECTURE.md (new): module layer overview, the Sender contract,
  a step-by-step "adding a new provider" guide (layout, Config/New
  conventions, the Register/factory template, providers/all wiring,
  naming/test conventions, error-handling conventions), registry
  internals, and a versioning note — precise enough for a human
  contributor or a coding agent to add a provider without guessing.
- README.md: new "Provider registry" section covering notify.Register/
  New, the providers/all quick-start, and a pointer to the fuller docs.
- docs/INTEGRATION.md (new): Five Ws and One H guide for integrating
  this module into an unrelated project, with a full go-get-to-dispatch
  code walkthrough.
Adds the [0.2.0] entry documenting the provider registry addition,
explaining why it's a minor bump despite being purely additive at the
Go-API level (init()-time global registration side effect,
panic-on-duplicate-name as a new failure mode). Also retroactively
splits the prior catch-all "Unreleased" entry into a proper [0.1.0]
section, since that content already shipped under the existing v0.1.0
tag and was never moved out of Unreleased.
@Wikid82
Wikid82 merged commit 3cc375e into main Aug 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant