Add pipeline module registries

This commit is contained in:
2026-07-03 15:30:44 +00:00
parent 75a0a9fa79
commit 62bc8983c7
15 changed files with 1446 additions and 28 deletions

View 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)
}

View 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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View 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)
}

View 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()
},
})
}

View 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
}

View 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)
}

View 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()
},
})
}

View 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)
}

View 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()
},
})
}

View 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)
}

View 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()
},
})
}