feat(sources)!: split source contracts into PollSource/StreamSource and add mode-aware source config
- Introduce explicit source interfaces: sources.PollSource and sources.StreamSource, with shared sources.Input (Name() only). - Remove mandatory Kind() from the base source contract to support sources that emit multiple kinds. - Add config.SourceMode (poll, stream, or omitted/auto) and SourceConfig.Kinds (plural expected kinds), while keeping legacy SourceConfig.Kind for compatibility. - Enforce mode semantics in config validation (poll requires every, stream forbids every) and detect mode/driver mismatches in sources.Registry. - Update docs and tests for the new source model and config behavior.
This commit is contained in:
@@ -7,22 +7,25 @@ import (
|
||||
"gitea.maximumdirect.net/ejr/feedkit/config"
|
||||
)
|
||||
|
||||
// Factory constructs a configured Source instance from config.
|
||||
// PollFactory constructs a configured PollSource instance from config.
|
||||
//
|
||||
// This is how concrete daemons (weatherfeeder/newsfeeder/...) register their
|
||||
// domain-specific source drivers (Open-Meteo, NWS, RSS, etc.) while feedkit
|
||||
// remains domain-agnostic.
|
||||
type Factory func(cfg config.SourceConfig) (Source, error)
|
||||
type PollFactory func(cfg config.SourceConfig) (PollSource, error)
|
||||
type StreamFactory func(cfg config.SourceConfig) (StreamSource, error)
|
||||
|
||||
// Factory is the legacy alias for poll source factories.
|
||||
type Factory = PollFactory
|
||||
|
||||
type Registry struct {
|
||||
byDriver map[string]Factory
|
||||
byPollDriver map[string]PollFactory
|
||||
byStreamDriver map[string]StreamFactory
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
byDriver: map[string]Factory{},
|
||||
byPollDriver: map[string]PollFactory{},
|
||||
byStreamDriver: map[string]StreamFactory{},
|
||||
}
|
||||
}
|
||||
@@ -30,20 +33,28 @@ func NewRegistry() *Registry {
|
||||
// Register associates a driver name (e.g. "openmeteo_observation") with a factory.
|
||||
//
|
||||
// The driver string is the "lookup key" used by config.sources[].driver.
|
||||
func (r *Registry) Register(driver string, f Factory) {
|
||||
func (r *Registry) Register(driver string, f PollFactory) {
|
||||
r.RegisterPoll(driver, f)
|
||||
}
|
||||
|
||||
// RegisterPoll associates a driver name with a polling-source factory.
|
||||
func (r *Registry) RegisterPoll(driver string, f PollFactory) {
|
||||
driver = strings.TrimSpace(driver)
|
||||
if driver == "" {
|
||||
// Panic is appropriate here: registering an empty driver is always a programmer error,
|
||||
// and it will lead to extremely confusing runtime behavior if allowed.
|
||||
panic("sources.Registry.Register: driver cannot be empty")
|
||||
panic("sources.Registry.RegisterPoll: driver cannot be empty")
|
||||
}
|
||||
if f == nil {
|
||||
panic(fmt.Sprintf("sources.Registry.Register: factory cannot be nil (driver=%q)", driver))
|
||||
panic(fmt.Sprintf("sources.Registry.RegisterPoll: factory cannot be nil (driver=%q)", driver))
|
||||
}
|
||||
if _, exists := r.byStreamDriver[driver]; exists {
|
||||
panic(fmt.Sprintf("sources.Registry.Register: driver %q already registered as a stream source", driver))
|
||||
panic(fmt.Sprintf("sources.Registry.RegisterPoll: driver %q already registered as a stream source", driver))
|
||||
}
|
||||
r.byDriver[driver] = f
|
||||
if _, exists := r.byPollDriver[driver]; exists {
|
||||
panic(fmt.Sprintf("sources.Registry.RegisterPoll: driver %q already registered as a polling source", driver))
|
||||
}
|
||||
r.byPollDriver[driver] = f
|
||||
}
|
||||
|
||||
// RegisterStream is the StreamSource equivalent of Register.
|
||||
@@ -55,28 +66,71 @@ func (r *Registry) RegisterStream(driver string, f StreamFactory) {
|
||||
if f == nil {
|
||||
panic(fmt.Sprintf("sources.Registry.RegisterStream: factory cannot be nil (driver=%q)", driver))
|
||||
}
|
||||
if _, exists := r.byDriver[driver]; exists {
|
||||
if _, exists := r.byPollDriver[driver]; exists {
|
||||
panic(fmt.Sprintf("sources.Registry.RegisterStream: driver %q already registered as a polling source", driver))
|
||||
}
|
||||
if _, exists := r.byStreamDriver[driver]; exists {
|
||||
panic(fmt.Sprintf("sources.Registry.RegisterStream: driver %q already registered as a stream source", driver))
|
||||
}
|
||||
r.byStreamDriver[driver] = f
|
||||
}
|
||||
|
||||
// Build constructs a Source from a SourceConfig by looking up cfg.Driver.
|
||||
func (r *Registry) Build(cfg config.SourceConfig) (Source, error) {
|
||||
f, ok := r.byDriver[cfg.Driver]
|
||||
// Build constructs a polling source from a SourceConfig by looking up cfg.Driver.
|
||||
func (r *Registry) Build(cfg config.SourceConfig) (PollSource, error) {
|
||||
return r.BuildPoll(cfg)
|
||||
}
|
||||
|
||||
// BuildPoll constructs a polling source from a SourceConfig by looking up cfg.Driver.
|
||||
func (r *Registry) BuildPoll(cfg config.SourceConfig) (PollSource, error) {
|
||||
driver := strings.TrimSpace(cfg.Driver)
|
||||
if cfg.Mode.Normalize() == config.SourceModeStream {
|
||||
return nil, fmt.Errorf("source %q mode=stream cannot be built as polling source", cfg.Name)
|
||||
}
|
||||
|
||||
f, ok := r.byPollDriver[driver]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown source driver: %q", cfg.Driver)
|
||||
if _, streamExists := r.byStreamDriver[driver]; streamExists {
|
||||
return nil, fmt.Errorf("source driver %q is stream-only; cannot build as polling source", driver)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown source driver: %q", driver)
|
||||
}
|
||||
return f(cfg)
|
||||
}
|
||||
|
||||
// BuildInput can return either a polling Source or a StreamSource.
|
||||
func (r *Registry) BuildInput(cfg config.SourceConfig) (Input, error) {
|
||||
if f, ok := r.byStreamDriver[cfg.Driver]; ok {
|
||||
driver := strings.TrimSpace(cfg.Driver)
|
||||
mode := cfg.Mode.Normalize()
|
||||
if mode != config.SourceModeAuto && mode != config.SourceModePoll && mode != config.SourceModeStream {
|
||||
return nil, fmt.Errorf("source %q has invalid mode %q (expected \"poll\" or \"stream\")", cfg.Name, cfg.Mode)
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case config.SourceModePoll:
|
||||
f, ok := r.byPollDriver[driver]
|
||||
if !ok {
|
||||
if _, streamExists := r.byStreamDriver[driver]; streamExists {
|
||||
return nil, fmt.Errorf("source %q mode=poll conflicts with stream-only driver %q", cfg.Name, driver)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown source driver: %q", driver)
|
||||
}
|
||||
return f(cfg)
|
||||
case config.SourceModeStream:
|
||||
f, ok := r.byStreamDriver[driver]
|
||||
if !ok {
|
||||
if _, pollExists := r.byPollDriver[driver]; pollExists {
|
||||
return nil, fmt.Errorf("source %q mode=stream conflicts with polling driver %q", cfg.Name, driver)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown source driver: %q", driver)
|
||||
}
|
||||
return f(cfg)
|
||||
}
|
||||
if f, ok := r.byDriver[cfg.Driver]; ok {
|
||||
|
||||
if f, ok := r.byStreamDriver[driver]; ok {
|
||||
return f(cfg)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown source driver: %q", cfg.Driver)
|
||||
if f, ok := r.byPollDriver[driver]; ok {
|
||||
return f(cfg)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown source driver: %q", driver)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user