Add pipeline module registries
This commit is contained in:
102
internal/framework/pipeline/chunker_registry.go
Normal file
102
internal/framework/pipeline/chunker_registry.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
type ChunkerConstructor func() (contracts.Chunker, error)
|
||||
|
||||
type ChunkerRegistry struct {
|
||||
constructors map[string]ChunkerConstructor
|
||||
specs map[string]ModuleSpec
|
||||
}
|
||||
|
||||
func NewChunkerRegistry() *ChunkerRegistry {
|
||||
return &ChunkerRegistry{
|
||||
constructors: make(map[string]ChunkerConstructor),
|
||||
specs: make(map[string]ModuleSpec),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ChunkerRegistry) Register(key string, constructor ChunkerConstructor) error {
|
||||
return r.RegisterWithSpec(defaultModuleSpec(key, StageChunk), constructor)
|
||||
}
|
||||
|
||||
func (r *ChunkerRegistry) RegisterWithSpec(spec ModuleSpec, constructor ChunkerConstructor) error {
|
||||
if r == nil {
|
||||
return fmt.Errorf("chunker registry must not be nil")
|
||||
}
|
||||
|
||||
normalizedSpec := normalizeModuleSpec(spec)
|
||||
if err := validateModuleSpec("chunker", StageChunk, normalizedSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
if constructor == nil {
|
||||
return fmt.Errorf("chunker constructor for %q must not be nil", normalizedSpec.Key)
|
||||
}
|
||||
if _, ok := r.constructors[normalizedSpec.Key]; ok {
|
||||
return fmt.Errorf("chunker %q is already registered", normalizedSpec.Key)
|
||||
}
|
||||
|
||||
if r.constructors == nil {
|
||||
r.constructors = make(map[string]ChunkerConstructor)
|
||||
}
|
||||
if r.specs == nil {
|
||||
r.specs = make(map[string]ModuleSpec)
|
||||
}
|
||||
r.constructors[normalizedSpec.Key] = constructor
|
||||
r.specs[normalizedSpec.Key] = cloneModuleSpec(normalizedSpec)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ChunkerRegistry) Build(key string) (contracts.Chunker, error) {
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("chunker registry must not be nil")
|
||||
}
|
||||
|
||||
normalizedKey := strings.TrimSpace(key)
|
||||
if normalizedKey == "" {
|
||||
return nil, fmt.Errorf("chunker key must not be empty")
|
||||
}
|
||||
|
||||
constructor, ok := r.constructors[normalizedKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("chunker %q is not registered", normalizedKey)
|
||||
}
|
||||
|
||||
chunker, err := constructor()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build chunker %q: %w", normalizedKey, err)
|
||||
}
|
||||
if chunker == nil {
|
||||
return nil, fmt.Errorf("chunker %q constructor returned nil", normalizedKey)
|
||||
}
|
||||
if chunker.Key() != normalizedKey {
|
||||
return nil, fmt.Errorf("chunker %q returned key %q", normalizedKey, chunker.Key())
|
||||
}
|
||||
|
||||
return chunker, nil
|
||||
}
|
||||
|
||||
func (r *ChunkerRegistry) Spec(key string) (ModuleSpec, bool) {
|
||||
if r == nil {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
|
||||
spec, ok := r.specs[strings.TrimSpace(key)]
|
||||
if !ok {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
return cloneModuleSpec(spec), true
|
||||
}
|
||||
|
||||
func (r *ChunkerRegistry) RegisteredKeys() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sortedRegistryKeys(r.constructors)
|
||||
}
|
||||
383
internal/framework/pipeline/chunker_registry_test.go
Normal file
383
internal/framework/pipeline/chunker_registry_test.go
Normal file
@@ -0,0 +1,383 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
type registryBehaviorCase[M any] struct {
|
||||
name string
|
||||
key string
|
||||
stage ModuleStage
|
||||
wrongStage ModuleStage
|
||||
newRegistry func() any
|
||||
register func(any, string, func() (M, error)) error
|
||||
registerWithSpec func(any, ModuleSpec, func() (M, error)) error
|
||||
build func(any, string) (M, error)
|
||||
spec func(any, string) (ModuleSpec, bool)
|
||||
registeredKeys func(any) []string
|
||||
nilRegister func(string, func() (M, error)) error
|
||||
nilBuild func(string) (M, error)
|
||||
nilSpec func(string) (ModuleSpec, bool)
|
||||
nilRegisteredKey func() []string
|
||||
constructor func(string) func() (M, error)
|
||||
moduleKey func(M) string
|
||||
}
|
||||
|
||||
func TestChunkerRegistryBehavior(t *testing.T) {
|
||||
runRegistryBehaviorTests(t, registryBehaviorCase[contracts.Chunker]{
|
||||
name: "ChunkerRegistry",
|
||||
key: "generic-chunker",
|
||||
stage: StageChunk,
|
||||
wrongStage: StageExtract,
|
||||
newRegistry: func() any {
|
||||
return NewChunkerRegistry()
|
||||
},
|
||||
register: func(registry any, key string, constructor func() (contracts.Chunker, error)) error {
|
||||
return registry.(*ChunkerRegistry).Register(key, constructor)
|
||||
},
|
||||
registerWithSpec: func(registry any, spec ModuleSpec, constructor func() (contracts.Chunker, error)) error {
|
||||
return registry.(*ChunkerRegistry).RegisterWithSpec(spec, constructor)
|
||||
},
|
||||
build: func(registry any, key string) (contracts.Chunker, error) {
|
||||
return registry.(*ChunkerRegistry).Build(key)
|
||||
},
|
||||
spec: func(registry any, key string) (ModuleSpec, bool) {
|
||||
return registry.(*ChunkerRegistry).Spec(key)
|
||||
},
|
||||
registeredKeys: func(registry any) []string {
|
||||
return registry.(*ChunkerRegistry).RegisteredKeys()
|
||||
},
|
||||
nilRegister: func(key string, constructor func() (contracts.Chunker, error)) error {
|
||||
var registry *ChunkerRegistry
|
||||
return registry.Register(key, constructor)
|
||||
},
|
||||
nilBuild: func(key string) (contracts.Chunker, error) {
|
||||
var registry *ChunkerRegistry
|
||||
return registry.Build(key)
|
||||
},
|
||||
nilSpec: func(key string) (ModuleSpec, bool) {
|
||||
var registry *ChunkerRegistry
|
||||
return registry.Spec(key)
|
||||
},
|
||||
nilRegisteredKey: func() []string {
|
||||
var registry *ChunkerRegistry
|
||||
return registry.RegisteredKeys()
|
||||
},
|
||||
constructor: func(key string) func() (contracts.Chunker, error) {
|
||||
return func() (contracts.Chunker, error) {
|
||||
return registryChunker{key: key}, nil
|
||||
}
|
||||
},
|
||||
moduleKey: func(module contracts.Chunker) string {
|
||||
return module.Key()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func runRegistryBehaviorTests[M any](t *testing.T, testCase registryBehaviorCase[M]) {
|
||||
t.Helper()
|
||||
|
||||
t.Run(testCase.name+"/register and build", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
if err := testCase.register(registry, testCase.key, testCase.constructor(testCase.key)); err != nil {
|
||||
t.Fatalf("Register() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
module, err := testCase.build(registry, testCase.key)
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v, want nil", err)
|
||||
}
|
||||
if got := testCase.moduleKey(module); got != testCase.key {
|
||||
t.Fatalf("module key = %q, want %q", got, testCase.key)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/metadata registration and lookup", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
spec := ModuleSpec{
|
||||
Key: " " + testCase.key + " ",
|
||||
Stage: testCase.stage,
|
||||
Provides: []string{" beta ", "alpha", "", "beta"},
|
||||
Requires: []string{" source ", "source", ""},
|
||||
}
|
||||
if err := testCase.registerWithSpec(registry, spec, testCase.constructor(testCase.key)); err != nil {
|
||||
t.Fatalf("RegisterWithSpec() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
got, ok := testCase.spec(registry, " "+testCase.key+"\n")
|
||||
if !ok {
|
||||
t.Fatal("Spec() ok = false, want true")
|
||||
}
|
||||
want := ModuleSpec{
|
||||
Key: testCase.key,
|
||||
Stage: testCase.stage,
|
||||
Provides: []string{"alpha", "beta"},
|
||||
Requires: []string{"source"},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("Spec() = %#v, want %#v", got, want)
|
||||
}
|
||||
|
||||
got.Provides[0] = "changed"
|
||||
again, ok := testCase.spec(registry, testCase.key)
|
||||
if !ok {
|
||||
t.Fatal("Spec() after caller mutation ok = false, want true")
|
||||
}
|
||||
if !reflect.DeepEqual(again, want) {
|
||||
t.Fatalf("Spec() after caller mutation = %#v, want %#v", again, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/default spec from register", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
if err := testCase.register(registry, " "+testCase.key+" ", testCase.constructor(testCase.key)); err != nil {
|
||||
t.Fatalf("Register() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
spec, ok := testCase.spec(registry, testCase.key)
|
||||
if !ok {
|
||||
t.Fatal("Spec() ok = false, want true")
|
||||
}
|
||||
want := ModuleSpec{Key: testCase.key, Stage: testCase.stage}
|
||||
if !reflect.DeepEqual(spec, want) {
|
||||
t.Fatalf("Spec() = %#v, want %#v", spec, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/wrong stage rejection", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
err := testCase.registerWithSpec(registry, ModuleSpec{Key: testCase.key, Stage: testCase.wrongStage}, testCase.constructor(testCase.key))
|
||||
if err == nil {
|
||||
t.Fatal("RegisterWithSpec() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "stage") {
|
||||
t.Fatalf("RegisterWithSpec() error = %q, want stage error", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/key trimming", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
if err := testCase.register(registry, " "+testCase.key+" ", testCase.constructor(testCase.key)); err != nil {
|
||||
t.Fatalf("Register() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
module, err := testCase.build(registry, "\t"+testCase.key+"\n")
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v, want nil", err)
|
||||
}
|
||||
if got := testCase.moduleKey(module); got != testCase.key {
|
||||
t.Fatalf("module key = %q, want %q", got, testCase.key)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/empty key rejection", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
err := testCase.register(registry, " \t", testCase.constructor(testCase.key))
|
||||
if err == nil {
|
||||
t.Fatal("Register() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "key must not be empty") {
|
||||
t.Fatalf("Register() error = %q, want empty key error", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/duplicate key rejection", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
if err := testCase.register(registry, testCase.key, testCase.constructor(testCase.key)); err != nil {
|
||||
t.Fatalf("Register() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
err := testCase.register(registry, " "+testCase.key+" ", testCase.constructor(testCase.key))
|
||||
if err == nil {
|
||||
t.Fatal("Register() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "already registered") {
|
||||
t.Fatalf("Register() error = %q, want duplicate key error", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/nil constructor rejection", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
err := testCase.register(registry, testCase.key, nil)
|
||||
if err == nil {
|
||||
t.Fatal("Register() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "constructor") {
|
||||
t.Fatalf("Register() error = %q, want constructor error", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/unknown key build error", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
_, err := testCase.build(registry, "missing")
|
||||
if err == nil {
|
||||
t.Fatal("Build() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not registered") {
|
||||
t.Fatalf("Build() error = %q, want unknown key error", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/constructor error wrapping", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
constructorErr := errors.New("constructor failed")
|
||||
if err := testCase.register(registry, testCase.key, func() (M, error) {
|
||||
var zero M
|
||||
return zero, constructorErr
|
||||
}); err != nil {
|
||||
t.Fatalf("Register() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
_, err := testCase.build(registry, testCase.key)
|
||||
if err == nil {
|
||||
t.Fatal("Build() error = nil, want error")
|
||||
}
|
||||
if !errors.Is(err, constructorErr) {
|
||||
t.Fatalf("Build() error = %v, want wrapped constructor error", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), testCase.key) {
|
||||
t.Fatalf("Build() error = %q, want key context", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/nil module rejection", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
if err := testCase.register(registry, testCase.key, func() (M, error) {
|
||||
var zero M
|
||||
return zero, nil
|
||||
}); err != nil {
|
||||
t.Fatalf("Register() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
_, err := testCase.build(registry, testCase.key)
|
||||
if err == nil {
|
||||
t.Fatal("Build() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "returned nil") {
|
||||
t.Fatalf("Build() error = %q, want nil module error", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/key mismatch rejection", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
if err := testCase.register(registry, testCase.key, testCase.constructor("other")); err != nil {
|
||||
t.Fatalf("Register() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
_, err := testCase.build(registry, testCase.key)
|
||||
if err == nil {
|
||||
t.Fatal("Build() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "returned") {
|
||||
t.Fatalf("Build() error = %q, want mismatch error", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/sorted registered keys", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
for _, key := range []string{"zeta", "alpha", "middle"} {
|
||||
if err := testCase.register(registry, key, testCase.constructor(key)); err != nil {
|
||||
t.Fatalf("Register(%q) error = %v, want nil", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
keys := testCase.registeredKeys(registry)
|
||||
want := []string{"alpha", "middle", "zeta"}
|
||||
if !reflect.DeepEqual(keys, want) {
|
||||
t.Fatalf("RegisteredKeys() = %#v, want %#v", keys, want)
|
||||
}
|
||||
|
||||
keys[0] = "changed"
|
||||
if got := testCase.registeredKeys(registry); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("RegisteredKeys() after caller mutation = %#v, want %#v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/nil registry behavior", func(t *testing.T) {
|
||||
if err := testCase.nilRegister(testCase.key, testCase.constructor(testCase.key)); err == nil {
|
||||
t.Fatal("Register() error = nil, want error")
|
||||
}
|
||||
if _, err := testCase.nilBuild(testCase.key); err == nil {
|
||||
t.Fatal("Build() error = nil, want error")
|
||||
}
|
||||
if _, ok := testCase.nilSpec(testCase.key); ok {
|
||||
t.Fatal("Spec() ok = true, want false")
|
||||
}
|
||||
if keys := testCase.nilRegisteredKey(); keys != nil {
|
||||
t.Fatalf("RegisteredKeys() = %#v, want nil", keys)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(testCase.name+"/unknown spec lookup", func(t *testing.T) {
|
||||
registry := testCase.newRegistry()
|
||||
if _, ok := testCase.spec(registry, "missing"); ok {
|
||||
t.Fatal("Spec() ok = true, want false")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type registryChunker struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (chunker registryChunker) Key() string {
|
||||
return chunker.key
|
||||
}
|
||||
|
||||
func (chunker registryChunker) Chunk(ctx context.Context, req contracts.ChunkRequest) (contracts.ChunkResult, error) {
|
||||
return contracts.ChunkResult{}, nil
|
||||
}
|
||||
|
||||
type registryMerger struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (merger registryMerger) Key() string {
|
||||
return merger.key
|
||||
}
|
||||
|
||||
func (merger registryMerger) Merge(ctx context.Context, req contracts.MergeRequest) (contracts.MergeResult, error) {
|
||||
return contracts.MergeResult{}, nil
|
||||
}
|
||||
|
||||
type registryNormalizer struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (normalizer registryNormalizer) Key() string {
|
||||
return normalizer.key
|
||||
}
|
||||
|
||||
func (normalizer registryNormalizer) Normalize(ctx context.Context, req contracts.NormalizeRequest) (contracts.NormalizeResult, error) {
|
||||
return contracts.NormalizeResult{}, nil
|
||||
}
|
||||
|
||||
type registryOutputEncoder struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (encoder registryOutputEncoder) Key() string {
|
||||
return encoder.key
|
||||
}
|
||||
|
||||
func (encoder registryOutputEncoder) Encode(ctx context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) {
|
||||
return contracts.OutputResult{}, nil
|
||||
}
|
||||
|
||||
type registryValidator struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (validator registryValidator) Name() string {
|
||||
return validator.name
|
||||
}
|
||||
|
||||
func (validator registryValidator) Validate(ctx context.Context, req contracts.ValidationRequest) (contracts.ValidationResult, error) {
|
||||
return contracts.ValidationResult{}, nil
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package pipeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
@@ -12,31 +11,44 @@ type ExtractorConstructor func() (contracts.Extractor, error)
|
||||
|
||||
type ExtractorRegistry struct {
|
||||
constructors map[string]ExtractorConstructor
|
||||
specs map[string]ModuleSpec
|
||||
}
|
||||
|
||||
func NewExtractorRegistry() *ExtractorRegistry {
|
||||
return &ExtractorRegistry{
|
||||
constructors: make(map[string]ExtractorConstructor),
|
||||
specs: make(map[string]ModuleSpec),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ExtractorRegistry) Register(key string, constructor ExtractorConstructor) error {
|
||||
return r.RegisterWithSpec(defaultModuleSpec(key, StageExtract), constructor)
|
||||
}
|
||||
|
||||
func (r *ExtractorRegistry) RegisterWithSpec(spec ModuleSpec, constructor ExtractorConstructor) error {
|
||||
if r == nil {
|
||||
return fmt.Errorf("extractor registry must not be nil")
|
||||
}
|
||||
|
||||
normalizedKey := strings.TrimSpace(key)
|
||||
if normalizedKey == "" {
|
||||
return fmt.Errorf("extractor key must not be empty")
|
||||
normalizedSpec := normalizeModuleSpec(spec)
|
||||
if err := validateModuleSpec("extractor", StageExtract, normalizedSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
if constructor == nil {
|
||||
return fmt.Errorf("extractor constructor for %q must not be nil", normalizedKey)
|
||||
return fmt.Errorf("extractor constructor for %q must not be nil", normalizedSpec.Key)
|
||||
}
|
||||
if _, ok := r.constructors[normalizedKey]; ok {
|
||||
return fmt.Errorf("extractor %q is already registered", normalizedKey)
|
||||
if _, ok := r.constructors[normalizedSpec.Key]; ok {
|
||||
return fmt.Errorf("extractor %q is already registered", normalizedSpec.Key)
|
||||
}
|
||||
|
||||
r.constructors[normalizedKey] = constructor
|
||||
if r.constructors == nil {
|
||||
r.constructors = make(map[string]ExtractorConstructor)
|
||||
}
|
||||
if r.specs == nil {
|
||||
r.specs = make(map[string]ModuleSpec)
|
||||
}
|
||||
r.constructors[normalizedSpec.Key] = constructor
|
||||
r.specs[normalizedSpec.Key] = cloneModuleSpec(normalizedSpec)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -69,15 +81,22 @@ func (r *ExtractorRegistry) Build(key string) (contracts.Extractor, error) {
|
||||
return extractor, nil
|
||||
}
|
||||
|
||||
func (r *ExtractorRegistry) Spec(key string) (ModuleSpec, bool) {
|
||||
if r == nil {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
|
||||
spec, ok := r.specs[strings.TrimSpace(key)]
|
||||
if !ok {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
return cloneModuleSpec(spec), true
|
||||
}
|
||||
|
||||
func (r *ExtractorRegistry) RegisteredKeys() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(r.constructors))
|
||||
for key := range r.constructors {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
return sortedRegistryKeys(r.constructors)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,81 @@ func TestExtractorRegistryRegisterAndBuildTrimKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractorRegistryRegisterWithSpecStoresMetadata(t *testing.T) {
|
||||
registry := NewExtractorRegistry()
|
||||
spec := ModuleSpec{
|
||||
Key: " generic-extractor ",
|
||||
Stage: StageExtract,
|
||||
Provides: []string{" generic-artifact ", "source-citations", "generic-artifact", ""},
|
||||
Requires: []string{" source-document ", "source-document", ""},
|
||||
}
|
||||
|
||||
if err := registry.RegisterWithSpec(spec, fakeExtractorConstructor("generic-extractor")); err != nil {
|
||||
t.Fatalf("RegisterWithSpec() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
got, ok := registry.Spec("\tgeneric-extractor\n")
|
||||
if !ok {
|
||||
t.Fatal("Spec() ok = false, want true")
|
||||
}
|
||||
want := ModuleSpec{
|
||||
Key: "generic-extractor",
|
||||
Stage: StageExtract,
|
||||
Provides: []string{"generic-artifact", "source-citations"},
|
||||
Requires: []string{"source-document"},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("Spec() = %#v, want %#v", got, want)
|
||||
}
|
||||
|
||||
got.Provides[0] = "changed"
|
||||
again, ok := registry.Spec("generic-extractor")
|
||||
if !ok {
|
||||
t.Fatal("Spec() after caller mutation ok = false, want true")
|
||||
}
|
||||
if !reflect.DeepEqual(again, want) {
|
||||
t.Fatalf("Spec() after caller mutation = %#v, want %#v", again, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractorRegistryRegisterStoresDefaultSpec(t *testing.T) {
|
||||
registry := NewExtractorRegistry()
|
||||
|
||||
if err := registry.Register(" generic-extractor ", fakeExtractorConstructor("generic-extractor")); err != nil {
|
||||
t.Fatalf("Register() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
got, ok := registry.Spec("generic-extractor")
|
||||
if !ok {
|
||||
t.Fatal("Spec() ok = false, want true")
|
||||
}
|
||||
want := ModuleSpec{Key: "generic-extractor", Stage: StageExtract}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("Spec() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractorRegistryRegisterWithSpecRejectsWrongStage(t *testing.T) {
|
||||
registry := NewExtractorRegistry()
|
||||
|
||||
err := registry.RegisterWithSpec(ModuleSpec{Key: "generic-extractor", Stage: StageInput}, fakeExtractorConstructor("generic-extractor"))
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("RegisterWithSpec() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "stage") {
|
||||
t.Fatalf("RegisterWithSpec() error = %q, want stage error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractorRegistrySpecRejectsUnknownKey(t *testing.T) {
|
||||
registry := NewExtractorRegistry()
|
||||
|
||||
if _, ok := registry.Spec("missing-extractor"); ok {
|
||||
t.Fatal("Spec() ok = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractorRegistryRegisterRejectsEmptyKey(t *testing.T) {
|
||||
registry := NewExtractorRegistry()
|
||||
|
||||
@@ -183,6 +258,9 @@ func TestExtractorRegistryNilRegistryBehavior(t *testing.T) {
|
||||
if _, err := registry.Build("generic-extractor"); err == nil {
|
||||
t.Fatal("Build() error = nil, want error")
|
||||
}
|
||||
if _, ok := registry.Spec("generic-extractor"); ok {
|
||||
t.Fatal("Spec() ok = true, want false")
|
||||
}
|
||||
if keys := registry.RegisteredKeys(); keys != nil {
|
||||
t.Fatalf("RegisteredKeys() = %#v, want nil", keys)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package pipeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
@@ -12,31 +11,44 @@ type InputAdapterConstructor func() (contracts.InputAdapter, error)
|
||||
|
||||
type InputAdapterRegistry struct {
|
||||
constructors map[string]InputAdapterConstructor
|
||||
specs map[string]ModuleSpec
|
||||
}
|
||||
|
||||
func NewInputAdapterRegistry() *InputAdapterRegistry {
|
||||
return &InputAdapterRegistry{
|
||||
constructors: make(map[string]InputAdapterConstructor),
|
||||
specs: make(map[string]ModuleSpec),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *InputAdapterRegistry) Register(key string, constructor InputAdapterConstructor) error {
|
||||
return r.RegisterWithSpec(defaultModuleSpec(key, StageInput), constructor)
|
||||
}
|
||||
|
||||
func (r *InputAdapterRegistry) RegisterWithSpec(spec ModuleSpec, constructor InputAdapterConstructor) error {
|
||||
if r == nil {
|
||||
return fmt.Errorf("input adapter registry must not be nil")
|
||||
}
|
||||
|
||||
normalizedKey := strings.TrimSpace(key)
|
||||
if normalizedKey == "" {
|
||||
return fmt.Errorf("input adapter key must not be empty")
|
||||
normalizedSpec := normalizeModuleSpec(spec)
|
||||
if err := validateModuleSpec("input adapter", StageInput, normalizedSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
if constructor == nil {
|
||||
return fmt.Errorf("input adapter constructor for %q must not be nil", normalizedKey)
|
||||
return fmt.Errorf("input adapter constructor for %q must not be nil", normalizedSpec.Key)
|
||||
}
|
||||
if _, ok := r.constructors[normalizedKey]; ok {
|
||||
return fmt.Errorf("input adapter %q is already registered", normalizedKey)
|
||||
if _, ok := r.constructors[normalizedSpec.Key]; ok {
|
||||
return fmt.Errorf("input adapter %q is already registered", normalizedSpec.Key)
|
||||
}
|
||||
|
||||
r.constructors[normalizedKey] = constructor
|
||||
if r.constructors == nil {
|
||||
r.constructors = make(map[string]InputAdapterConstructor)
|
||||
}
|
||||
if r.specs == nil {
|
||||
r.specs = make(map[string]ModuleSpec)
|
||||
}
|
||||
r.constructors[normalizedSpec.Key] = constructor
|
||||
r.specs[normalizedSpec.Key] = cloneModuleSpec(normalizedSpec)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -69,15 +81,22 @@ func (r *InputAdapterRegistry) Build(key string) (contracts.InputAdapter, error)
|
||||
return adapter, nil
|
||||
}
|
||||
|
||||
func (r *InputAdapterRegistry) Spec(key string) (ModuleSpec, bool) {
|
||||
if r == nil {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
|
||||
spec, ok := r.specs[strings.TrimSpace(key)]
|
||||
if !ok {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
return cloneModuleSpec(spec), true
|
||||
}
|
||||
|
||||
func (r *InputAdapterRegistry) RegisteredKeys() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(r.constructors))
|
||||
for key := range r.constructors {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
return sortedRegistryKeys(r.constructors)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,81 @@ func TestInputAdapterRegistryRegisterAndBuildTrimKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputAdapterRegistryRegisterWithSpecStoresMetadata(t *testing.T) {
|
||||
registry := NewInputAdapterRegistry()
|
||||
spec := ModuleSpec{
|
||||
Key: " generic-input ",
|
||||
Stage: StageInput,
|
||||
Provides: []string{" parsed-source ", "source-document", "parsed-source", ""},
|
||||
Requires: []string{" raw-bytes ", "raw-bytes", ""},
|
||||
}
|
||||
|
||||
if err := registry.RegisterWithSpec(spec, fakeInputAdapterConstructor("generic-input")); err != nil {
|
||||
t.Fatalf("RegisterWithSpec() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
got, ok := registry.Spec("\tgeneric-input\n")
|
||||
if !ok {
|
||||
t.Fatal("Spec() ok = false, want true")
|
||||
}
|
||||
want := ModuleSpec{
|
||||
Key: "generic-input",
|
||||
Stage: StageInput,
|
||||
Provides: []string{"parsed-source", "source-document"},
|
||||
Requires: []string{"raw-bytes"},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("Spec() = %#v, want %#v", got, want)
|
||||
}
|
||||
|
||||
got.Provides[0] = "changed"
|
||||
again, ok := registry.Spec("generic-input")
|
||||
if !ok {
|
||||
t.Fatal("Spec() after caller mutation ok = false, want true")
|
||||
}
|
||||
if !reflect.DeepEqual(again, want) {
|
||||
t.Fatalf("Spec() after caller mutation = %#v, want %#v", again, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputAdapterRegistryRegisterStoresDefaultSpec(t *testing.T) {
|
||||
registry := NewInputAdapterRegistry()
|
||||
|
||||
if err := registry.Register(" generic-input ", fakeInputAdapterConstructor("generic-input")); err != nil {
|
||||
t.Fatalf("Register() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
got, ok := registry.Spec("generic-input")
|
||||
if !ok {
|
||||
t.Fatal("Spec() ok = false, want true")
|
||||
}
|
||||
want := ModuleSpec{Key: "generic-input", Stage: StageInput}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("Spec() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputAdapterRegistryRegisterWithSpecRejectsWrongStage(t *testing.T) {
|
||||
registry := NewInputAdapterRegistry()
|
||||
|
||||
err := registry.RegisterWithSpec(ModuleSpec{Key: "generic-input", Stage: StageExtract}, fakeInputAdapterConstructor("generic-input"))
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("RegisterWithSpec() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "stage") {
|
||||
t.Fatalf("RegisterWithSpec() error = %q, want stage error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputAdapterRegistrySpecRejectsUnknownKey(t *testing.T) {
|
||||
registry := NewInputAdapterRegistry()
|
||||
|
||||
if _, ok := registry.Spec("missing-input"); ok {
|
||||
t.Fatal("Spec() ok = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputAdapterRegistryRegisterRejectsEmptyKey(t *testing.T) {
|
||||
registry := NewInputAdapterRegistry()
|
||||
|
||||
@@ -184,6 +259,9 @@ func TestInputAdapterRegistryNilRegistryBehavior(t *testing.T) {
|
||||
if _, err := registry.Build("generic-input"); err == nil {
|
||||
t.Fatal("Build() error = nil, want error")
|
||||
}
|
||||
if _, ok := registry.Spec("generic-input"); ok {
|
||||
t.Fatal("Spec() ok = true, want false")
|
||||
}
|
||||
if keys := registry.RegisteredKeys(); keys != nil {
|
||||
t.Fatalf("RegisteredKeys() = %#v, want nil", keys)
|
||||
}
|
||||
|
||||
102
internal/framework/pipeline/merger_registry.go
Normal file
102
internal/framework/pipeline/merger_registry.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
type MergerConstructor func() (contracts.Merger, error)
|
||||
|
||||
type MergerRegistry struct {
|
||||
constructors map[string]MergerConstructor
|
||||
specs map[string]ModuleSpec
|
||||
}
|
||||
|
||||
func NewMergerRegistry() *MergerRegistry {
|
||||
return &MergerRegistry{
|
||||
constructors: make(map[string]MergerConstructor),
|
||||
specs: make(map[string]ModuleSpec),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MergerRegistry) Register(key string, constructor MergerConstructor) error {
|
||||
return r.RegisterWithSpec(defaultModuleSpec(key, StageMerge), constructor)
|
||||
}
|
||||
|
||||
func (r *MergerRegistry) RegisterWithSpec(spec ModuleSpec, constructor MergerConstructor) error {
|
||||
if r == nil {
|
||||
return fmt.Errorf("merger registry must not be nil")
|
||||
}
|
||||
|
||||
normalizedSpec := normalizeModuleSpec(spec)
|
||||
if err := validateModuleSpec("merger", StageMerge, normalizedSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
if constructor == nil {
|
||||
return fmt.Errorf("merger constructor for %q must not be nil", normalizedSpec.Key)
|
||||
}
|
||||
if _, ok := r.constructors[normalizedSpec.Key]; ok {
|
||||
return fmt.Errorf("merger %q is already registered", normalizedSpec.Key)
|
||||
}
|
||||
|
||||
if r.constructors == nil {
|
||||
r.constructors = make(map[string]MergerConstructor)
|
||||
}
|
||||
if r.specs == nil {
|
||||
r.specs = make(map[string]ModuleSpec)
|
||||
}
|
||||
r.constructors[normalizedSpec.Key] = constructor
|
||||
r.specs[normalizedSpec.Key] = cloneModuleSpec(normalizedSpec)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MergerRegistry) Build(key string) (contracts.Merger, error) {
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("merger registry must not be nil")
|
||||
}
|
||||
|
||||
normalizedKey := strings.TrimSpace(key)
|
||||
if normalizedKey == "" {
|
||||
return nil, fmt.Errorf("merger key must not be empty")
|
||||
}
|
||||
|
||||
constructor, ok := r.constructors[normalizedKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("merger %q is not registered", normalizedKey)
|
||||
}
|
||||
|
||||
merger, err := constructor()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build merger %q: %w", normalizedKey, err)
|
||||
}
|
||||
if merger == nil {
|
||||
return nil, fmt.Errorf("merger %q constructor returned nil", normalizedKey)
|
||||
}
|
||||
if merger.Key() != normalizedKey {
|
||||
return nil, fmt.Errorf("merger %q returned key %q", normalizedKey, merger.Key())
|
||||
}
|
||||
|
||||
return merger, nil
|
||||
}
|
||||
|
||||
func (r *MergerRegistry) Spec(key string) (ModuleSpec, bool) {
|
||||
if r == nil {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
|
||||
spec, ok := r.specs[strings.TrimSpace(key)]
|
||||
if !ok {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
return cloneModuleSpec(spec), true
|
||||
}
|
||||
|
||||
func (r *MergerRegistry) RegisteredKeys() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sortedRegistryKeys(r.constructors)
|
||||
}
|
||||
58
internal/framework/pipeline/merger_registry_test.go
Normal file
58
internal/framework/pipeline/merger_registry_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
func TestMergerRegistryBehavior(t *testing.T) {
|
||||
runRegistryBehaviorTests(t, registryBehaviorCase[contracts.Merger]{
|
||||
name: "MergerRegistry",
|
||||
key: "generic-merger",
|
||||
stage: StageMerge,
|
||||
wrongStage: StageExtract,
|
||||
newRegistry: func() any {
|
||||
return NewMergerRegistry()
|
||||
},
|
||||
register: func(registry any, key string, constructor func() (contracts.Merger, error)) error {
|
||||
return registry.(*MergerRegistry).Register(key, constructor)
|
||||
},
|
||||
registerWithSpec: func(registry any, spec ModuleSpec, constructor func() (contracts.Merger, error)) error {
|
||||
return registry.(*MergerRegistry).RegisterWithSpec(spec, constructor)
|
||||
},
|
||||
build: func(registry any, key string) (contracts.Merger, error) {
|
||||
return registry.(*MergerRegistry).Build(key)
|
||||
},
|
||||
spec: func(registry any, key string) (ModuleSpec, bool) {
|
||||
return registry.(*MergerRegistry).Spec(key)
|
||||
},
|
||||
registeredKeys: func(registry any) []string {
|
||||
return registry.(*MergerRegistry).RegisteredKeys()
|
||||
},
|
||||
nilRegister: func(key string, constructor func() (contracts.Merger, error)) error {
|
||||
var registry *MergerRegistry
|
||||
return registry.Register(key, constructor)
|
||||
},
|
||||
nilBuild: func(key string) (contracts.Merger, error) {
|
||||
var registry *MergerRegistry
|
||||
return registry.Build(key)
|
||||
},
|
||||
nilSpec: func(key string) (ModuleSpec, bool) {
|
||||
var registry *MergerRegistry
|
||||
return registry.Spec(key)
|
||||
},
|
||||
nilRegisteredKey: func() []string {
|
||||
var registry *MergerRegistry
|
||||
return registry.RegisteredKeys()
|
||||
},
|
||||
constructor: func(key string) func() (contracts.Merger, error) {
|
||||
return func() (contracts.Merger, error) {
|
||||
return registryMerger{key: key}, nil
|
||||
}
|
||||
},
|
||||
moduleKey: func(module contracts.Merger) string {
|
||||
return module.Key()
|
||||
},
|
||||
})
|
||||
}
|
||||
99
internal/framework/pipeline/module.go
Normal file
99
internal/framework/pipeline/module.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ModuleStage string
|
||||
|
||||
const (
|
||||
StageInput ModuleStage = "input"
|
||||
StageChunk ModuleStage = "chunk"
|
||||
StageExtract ModuleStage = "extract"
|
||||
StageMerge ModuleStage = "merge"
|
||||
StageNormalize ModuleStage = "normalize"
|
||||
StageValidate ModuleStage = "validate"
|
||||
StageOutput ModuleStage = "output"
|
||||
)
|
||||
|
||||
type ModuleSpec struct {
|
||||
Key string
|
||||
Stage ModuleStage
|
||||
Provides []string
|
||||
Requires []string
|
||||
}
|
||||
|
||||
func defaultModuleSpec(key string, stage ModuleStage) ModuleSpec {
|
||||
return ModuleSpec{
|
||||
Key: key,
|
||||
Stage: stage,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeModuleSpec(spec ModuleSpec) ModuleSpec {
|
||||
return ModuleSpec{
|
||||
Key: strings.TrimSpace(spec.Key),
|
||||
Stage: spec.Stage,
|
||||
Provides: normalizeCapabilities(spec.Provides),
|
||||
Requires: normalizeCapabilities(spec.Requires),
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCapabilities(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
normalized := strings.TrimSpace(value)
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
seen[normalized] = struct{}{}
|
||||
}
|
||||
if len(seen) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
capabilities := make([]string, 0, len(seen))
|
||||
for value := range seen {
|
||||
capabilities = append(capabilities, value)
|
||||
}
|
||||
sort.Strings(capabilities)
|
||||
return capabilities
|
||||
}
|
||||
|
||||
func cloneModuleSpec(spec ModuleSpec) ModuleSpec {
|
||||
return ModuleSpec{
|
||||
Key: spec.Key,
|
||||
Stage: spec.Stage,
|
||||
Provides: append([]string(nil), spec.Provides...),
|
||||
Requires: append([]string(nil), spec.Requires...),
|
||||
}
|
||||
}
|
||||
|
||||
func validateModuleSpec(kind string, expectedStage ModuleStage, spec ModuleSpec) error {
|
||||
if spec.Key == "" {
|
||||
return fmt.Errorf("%s key must not be empty", kind)
|
||||
}
|
||||
if spec.Stage != expectedStage {
|
||||
return fmt.Errorf("%s %q must use %q stage, got %q", kind, spec.Key, expectedStage, spec.Stage)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sortedRegistryKeys[C any](constructors map[string]C) []string {
|
||||
if len(constructors) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(constructors))
|
||||
for key := range constructors {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
102
internal/framework/pipeline/normalizer_registry.go
Normal file
102
internal/framework/pipeline/normalizer_registry.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
type NormalizerConstructor func() (contracts.Normalizer, error)
|
||||
|
||||
type NormalizerRegistry struct {
|
||||
constructors map[string]NormalizerConstructor
|
||||
specs map[string]ModuleSpec
|
||||
}
|
||||
|
||||
func NewNormalizerRegistry() *NormalizerRegistry {
|
||||
return &NormalizerRegistry{
|
||||
constructors: make(map[string]NormalizerConstructor),
|
||||
specs: make(map[string]ModuleSpec),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *NormalizerRegistry) Register(key string, constructor NormalizerConstructor) error {
|
||||
return r.RegisterWithSpec(defaultModuleSpec(key, StageNormalize), constructor)
|
||||
}
|
||||
|
||||
func (r *NormalizerRegistry) RegisterWithSpec(spec ModuleSpec, constructor NormalizerConstructor) error {
|
||||
if r == nil {
|
||||
return fmt.Errorf("normalizer registry must not be nil")
|
||||
}
|
||||
|
||||
normalizedSpec := normalizeModuleSpec(spec)
|
||||
if err := validateModuleSpec("normalizer", StageNormalize, normalizedSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
if constructor == nil {
|
||||
return fmt.Errorf("normalizer constructor for %q must not be nil", normalizedSpec.Key)
|
||||
}
|
||||
if _, ok := r.constructors[normalizedSpec.Key]; ok {
|
||||
return fmt.Errorf("normalizer %q is already registered", normalizedSpec.Key)
|
||||
}
|
||||
|
||||
if r.constructors == nil {
|
||||
r.constructors = make(map[string]NormalizerConstructor)
|
||||
}
|
||||
if r.specs == nil {
|
||||
r.specs = make(map[string]ModuleSpec)
|
||||
}
|
||||
r.constructors[normalizedSpec.Key] = constructor
|
||||
r.specs[normalizedSpec.Key] = cloneModuleSpec(normalizedSpec)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *NormalizerRegistry) Build(key string) (contracts.Normalizer, error) {
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("normalizer registry must not be nil")
|
||||
}
|
||||
|
||||
normalizedKey := strings.TrimSpace(key)
|
||||
if normalizedKey == "" {
|
||||
return nil, fmt.Errorf("normalizer key must not be empty")
|
||||
}
|
||||
|
||||
constructor, ok := r.constructors[normalizedKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("normalizer %q is not registered", normalizedKey)
|
||||
}
|
||||
|
||||
normalizer, err := constructor()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build normalizer %q: %w", normalizedKey, err)
|
||||
}
|
||||
if normalizer == nil {
|
||||
return nil, fmt.Errorf("normalizer %q constructor returned nil", normalizedKey)
|
||||
}
|
||||
if normalizer.Key() != normalizedKey {
|
||||
return nil, fmt.Errorf("normalizer %q returned key %q", normalizedKey, normalizer.Key())
|
||||
}
|
||||
|
||||
return normalizer, nil
|
||||
}
|
||||
|
||||
func (r *NormalizerRegistry) Spec(key string) (ModuleSpec, bool) {
|
||||
if r == nil {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
|
||||
spec, ok := r.specs[strings.TrimSpace(key)]
|
||||
if !ok {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
return cloneModuleSpec(spec), true
|
||||
}
|
||||
|
||||
func (r *NormalizerRegistry) RegisteredKeys() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sortedRegistryKeys(r.constructors)
|
||||
}
|
||||
58
internal/framework/pipeline/normalizer_registry_test.go
Normal file
58
internal/framework/pipeline/normalizer_registry_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
func TestNormalizerRegistryBehavior(t *testing.T) {
|
||||
runRegistryBehaviorTests(t, registryBehaviorCase[contracts.Normalizer]{
|
||||
name: "NormalizerRegistry",
|
||||
key: "generic-normalizer",
|
||||
stage: StageNormalize,
|
||||
wrongStage: StageExtract,
|
||||
newRegistry: func() any {
|
||||
return NewNormalizerRegistry()
|
||||
},
|
||||
register: func(registry any, key string, constructor func() (contracts.Normalizer, error)) error {
|
||||
return registry.(*NormalizerRegistry).Register(key, constructor)
|
||||
},
|
||||
registerWithSpec: func(registry any, spec ModuleSpec, constructor func() (contracts.Normalizer, error)) error {
|
||||
return registry.(*NormalizerRegistry).RegisterWithSpec(spec, constructor)
|
||||
},
|
||||
build: func(registry any, key string) (contracts.Normalizer, error) {
|
||||
return registry.(*NormalizerRegistry).Build(key)
|
||||
},
|
||||
spec: func(registry any, key string) (ModuleSpec, bool) {
|
||||
return registry.(*NormalizerRegistry).Spec(key)
|
||||
},
|
||||
registeredKeys: func(registry any) []string {
|
||||
return registry.(*NormalizerRegistry).RegisteredKeys()
|
||||
},
|
||||
nilRegister: func(key string, constructor func() (contracts.Normalizer, error)) error {
|
||||
var registry *NormalizerRegistry
|
||||
return registry.Register(key, constructor)
|
||||
},
|
||||
nilBuild: func(key string) (contracts.Normalizer, error) {
|
||||
var registry *NormalizerRegistry
|
||||
return registry.Build(key)
|
||||
},
|
||||
nilSpec: func(key string) (ModuleSpec, bool) {
|
||||
var registry *NormalizerRegistry
|
||||
return registry.Spec(key)
|
||||
},
|
||||
nilRegisteredKey: func() []string {
|
||||
var registry *NormalizerRegistry
|
||||
return registry.RegisteredKeys()
|
||||
},
|
||||
constructor: func(key string) func() (contracts.Normalizer, error) {
|
||||
return func() (contracts.Normalizer, error) {
|
||||
return registryNormalizer{key: key}, nil
|
||||
}
|
||||
},
|
||||
moduleKey: func(module contracts.Normalizer) string {
|
||||
return module.Key()
|
||||
},
|
||||
})
|
||||
}
|
||||
102
internal/framework/pipeline/output_registry.go
Normal file
102
internal/framework/pipeline/output_registry.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
type OutputEncoderConstructor func() (contracts.OutputEncoder, error)
|
||||
|
||||
type OutputEncoderRegistry struct {
|
||||
constructors map[string]OutputEncoderConstructor
|
||||
specs map[string]ModuleSpec
|
||||
}
|
||||
|
||||
func NewOutputEncoderRegistry() *OutputEncoderRegistry {
|
||||
return &OutputEncoderRegistry{
|
||||
constructors: make(map[string]OutputEncoderConstructor),
|
||||
specs: make(map[string]ModuleSpec),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *OutputEncoderRegistry) Register(key string, constructor OutputEncoderConstructor) error {
|
||||
return r.RegisterWithSpec(defaultModuleSpec(key, StageOutput), constructor)
|
||||
}
|
||||
|
||||
func (r *OutputEncoderRegistry) RegisterWithSpec(spec ModuleSpec, constructor OutputEncoderConstructor) error {
|
||||
if r == nil {
|
||||
return fmt.Errorf("output encoder registry must not be nil")
|
||||
}
|
||||
|
||||
normalizedSpec := normalizeModuleSpec(spec)
|
||||
if err := validateModuleSpec("output encoder", StageOutput, normalizedSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
if constructor == nil {
|
||||
return fmt.Errorf("output encoder constructor for %q must not be nil", normalizedSpec.Key)
|
||||
}
|
||||
if _, ok := r.constructors[normalizedSpec.Key]; ok {
|
||||
return fmt.Errorf("output encoder %q is already registered", normalizedSpec.Key)
|
||||
}
|
||||
|
||||
if r.constructors == nil {
|
||||
r.constructors = make(map[string]OutputEncoderConstructor)
|
||||
}
|
||||
if r.specs == nil {
|
||||
r.specs = make(map[string]ModuleSpec)
|
||||
}
|
||||
r.constructors[normalizedSpec.Key] = constructor
|
||||
r.specs[normalizedSpec.Key] = cloneModuleSpec(normalizedSpec)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *OutputEncoderRegistry) Build(key string) (contracts.OutputEncoder, error) {
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("output encoder registry must not be nil")
|
||||
}
|
||||
|
||||
normalizedKey := strings.TrimSpace(key)
|
||||
if normalizedKey == "" {
|
||||
return nil, fmt.Errorf("output encoder key must not be empty")
|
||||
}
|
||||
|
||||
constructor, ok := r.constructors[normalizedKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("output encoder %q is not registered", normalizedKey)
|
||||
}
|
||||
|
||||
encoder, err := constructor()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build output encoder %q: %w", normalizedKey, err)
|
||||
}
|
||||
if encoder == nil {
|
||||
return nil, fmt.Errorf("output encoder %q constructor returned nil", normalizedKey)
|
||||
}
|
||||
if encoder.Key() != normalizedKey {
|
||||
return nil, fmt.Errorf("output encoder %q returned key %q", normalizedKey, encoder.Key())
|
||||
}
|
||||
|
||||
return encoder, nil
|
||||
}
|
||||
|
||||
func (r *OutputEncoderRegistry) Spec(key string) (ModuleSpec, bool) {
|
||||
if r == nil {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
|
||||
spec, ok := r.specs[strings.TrimSpace(key)]
|
||||
if !ok {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
return cloneModuleSpec(spec), true
|
||||
}
|
||||
|
||||
func (r *OutputEncoderRegistry) RegisteredKeys() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sortedRegistryKeys(r.constructors)
|
||||
}
|
||||
58
internal/framework/pipeline/output_registry_test.go
Normal file
58
internal/framework/pipeline/output_registry_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
func TestOutputEncoderRegistryBehavior(t *testing.T) {
|
||||
runRegistryBehaviorTests(t, registryBehaviorCase[contracts.OutputEncoder]{
|
||||
name: "OutputEncoderRegistry",
|
||||
key: "generic-output",
|
||||
stage: StageOutput,
|
||||
wrongStage: StageExtract,
|
||||
newRegistry: func() any {
|
||||
return NewOutputEncoderRegistry()
|
||||
},
|
||||
register: func(registry any, key string, constructor func() (contracts.OutputEncoder, error)) error {
|
||||
return registry.(*OutputEncoderRegistry).Register(key, constructor)
|
||||
},
|
||||
registerWithSpec: func(registry any, spec ModuleSpec, constructor func() (contracts.OutputEncoder, error)) error {
|
||||
return registry.(*OutputEncoderRegistry).RegisterWithSpec(spec, constructor)
|
||||
},
|
||||
build: func(registry any, key string) (contracts.OutputEncoder, error) {
|
||||
return registry.(*OutputEncoderRegistry).Build(key)
|
||||
},
|
||||
spec: func(registry any, key string) (ModuleSpec, bool) {
|
||||
return registry.(*OutputEncoderRegistry).Spec(key)
|
||||
},
|
||||
registeredKeys: func(registry any) []string {
|
||||
return registry.(*OutputEncoderRegistry).RegisteredKeys()
|
||||
},
|
||||
nilRegister: func(key string, constructor func() (contracts.OutputEncoder, error)) error {
|
||||
var registry *OutputEncoderRegistry
|
||||
return registry.Register(key, constructor)
|
||||
},
|
||||
nilBuild: func(key string) (contracts.OutputEncoder, error) {
|
||||
var registry *OutputEncoderRegistry
|
||||
return registry.Build(key)
|
||||
},
|
||||
nilSpec: func(key string) (ModuleSpec, bool) {
|
||||
var registry *OutputEncoderRegistry
|
||||
return registry.Spec(key)
|
||||
},
|
||||
nilRegisteredKey: func() []string {
|
||||
var registry *OutputEncoderRegistry
|
||||
return registry.RegisteredKeys()
|
||||
},
|
||||
constructor: func(key string) func() (contracts.OutputEncoder, error) {
|
||||
return func() (contracts.OutputEncoder, error) {
|
||||
return registryOutputEncoder{key: key}, nil
|
||||
}
|
||||
},
|
||||
moduleKey: func(module contracts.OutputEncoder) string {
|
||||
return module.Key()
|
||||
},
|
||||
})
|
||||
}
|
||||
102
internal/framework/pipeline/validator_registry.go
Normal file
102
internal/framework/pipeline/validator_registry.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
type ValidatorConstructor func() (contracts.Validator, error)
|
||||
|
||||
type ValidatorRegistry struct {
|
||||
constructors map[string]ValidatorConstructor
|
||||
specs map[string]ModuleSpec
|
||||
}
|
||||
|
||||
func NewValidatorRegistry() *ValidatorRegistry {
|
||||
return &ValidatorRegistry{
|
||||
constructors: make(map[string]ValidatorConstructor),
|
||||
specs: make(map[string]ModuleSpec),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ValidatorRegistry) Register(key string, constructor ValidatorConstructor) error {
|
||||
return r.RegisterWithSpec(defaultModuleSpec(key, StageValidate), constructor)
|
||||
}
|
||||
|
||||
func (r *ValidatorRegistry) RegisterWithSpec(spec ModuleSpec, constructor ValidatorConstructor) error {
|
||||
if r == nil {
|
||||
return fmt.Errorf("validator registry must not be nil")
|
||||
}
|
||||
|
||||
normalizedSpec := normalizeModuleSpec(spec)
|
||||
if err := validateModuleSpec("validator", StageValidate, normalizedSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
if constructor == nil {
|
||||
return fmt.Errorf("validator constructor for %q must not be nil", normalizedSpec.Key)
|
||||
}
|
||||
if _, ok := r.constructors[normalizedSpec.Key]; ok {
|
||||
return fmt.Errorf("validator %q is already registered", normalizedSpec.Key)
|
||||
}
|
||||
|
||||
if r.constructors == nil {
|
||||
r.constructors = make(map[string]ValidatorConstructor)
|
||||
}
|
||||
if r.specs == nil {
|
||||
r.specs = make(map[string]ModuleSpec)
|
||||
}
|
||||
r.constructors[normalizedSpec.Key] = constructor
|
||||
r.specs[normalizedSpec.Key] = cloneModuleSpec(normalizedSpec)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ValidatorRegistry) Build(key string) (contracts.Validator, error) {
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("validator registry must not be nil")
|
||||
}
|
||||
|
||||
normalizedKey := strings.TrimSpace(key)
|
||||
if normalizedKey == "" {
|
||||
return nil, fmt.Errorf("validator key must not be empty")
|
||||
}
|
||||
|
||||
constructor, ok := r.constructors[normalizedKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("validator %q is not registered", normalizedKey)
|
||||
}
|
||||
|
||||
validator, err := constructor()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build validator %q: %w", normalizedKey, err)
|
||||
}
|
||||
if validator == nil {
|
||||
return nil, fmt.Errorf("validator %q constructor returned nil", normalizedKey)
|
||||
}
|
||||
if validator.Name() != normalizedKey {
|
||||
return nil, fmt.Errorf("validator %q returned name %q", normalizedKey, validator.Name())
|
||||
}
|
||||
|
||||
return validator, nil
|
||||
}
|
||||
|
||||
func (r *ValidatorRegistry) Spec(key string) (ModuleSpec, bool) {
|
||||
if r == nil {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
|
||||
spec, ok := r.specs[strings.TrimSpace(key)]
|
||||
if !ok {
|
||||
return ModuleSpec{}, false
|
||||
}
|
||||
return cloneModuleSpec(spec), true
|
||||
}
|
||||
|
||||
func (r *ValidatorRegistry) RegisteredKeys() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sortedRegistryKeys(r.constructors)
|
||||
}
|
||||
58
internal/framework/pipeline/validator_registry_test.go
Normal file
58
internal/framework/pipeline/validator_registry_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
func TestValidatorRegistryBehavior(t *testing.T) {
|
||||
runRegistryBehaviorTests(t, registryBehaviorCase[contracts.Validator]{
|
||||
name: "ValidatorRegistry",
|
||||
key: "generic-validator",
|
||||
stage: StageValidate,
|
||||
wrongStage: StageExtract,
|
||||
newRegistry: func() any {
|
||||
return NewValidatorRegistry()
|
||||
},
|
||||
register: func(registry any, key string, constructor func() (contracts.Validator, error)) error {
|
||||
return registry.(*ValidatorRegistry).Register(key, constructor)
|
||||
},
|
||||
registerWithSpec: func(registry any, spec ModuleSpec, constructor func() (contracts.Validator, error)) error {
|
||||
return registry.(*ValidatorRegistry).RegisterWithSpec(spec, constructor)
|
||||
},
|
||||
build: func(registry any, key string) (contracts.Validator, error) {
|
||||
return registry.(*ValidatorRegistry).Build(key)
|
||||
},
|
||||
spec: func(registry any, key string) (ModuleSpec, bool) {
|
||||
return registry.(*ValidatorRegistry).Spec(key)
|
||||
},
|
||||
registeredKeys: func(registry any) []string {
|
||||
return registry.(*ValidatorRegistry).RegisteredKeys()
|
||||
},
|
||||
nilRegister: func(key string, constructor func() (contracts.Validator, error)) error {
|
||||
var registry *ValidatorRegistry
|
||||
return registry.Register(key, constructor)
|
||||
},
|
||||
nilBuild: func(key string) (contracts.Validator, error) {
|
||||
var registry *ValidatorRegistry
|
||||
return registry.Build(key)
|
||||
},
|
||||
nilSpec: func(key string) (ModuleSpec, bool) {
|
||||
var registry *ValidatorRegistry
|
||||
return registry.Spec(key)
|
||||
},
|
||||
nilRegisteredKey: func() []string {
|
||||
var registry *ValidatorRegistry
|
||||
return registry.RegisteredKeys()
|
||||
},
|
||||
constructor: func(key string) func() (contracts.Validator, error) {
|
||||
return func() (contracts.Validator, error) {
|
||||
return registryValidator{name: key}, nil
|
||||
}
|
||||
},
|
||||
moduleKey: func(module contracts.Validator) string {
|
||||
return module.Name()
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user