package pipeline import ( "fmt" "reflect" "sort" "strings" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" ) type LegacyRawValidatorConstructor func() (contracts.LegacyRawValidator, error) type LegacyRawValidatorBuilder func(BuildRequest) (contracts.LegacyRawValidator, error) type ValidatorSpec struct { Key string `json:"key"` ExecutionClass contracts.ExecutionClass `json:"execution_class"` } type SerializedValidatorSpec struct { ValidatorSpec SupportsChunks bool `json:"supports_chunks,omitempty"` SupportsArtifacts bool `json:"supports_artifacts,omitempty"` } type ValidatorTarget string const ( ValidatorTargetLegacyRaw ValidatorTarget = "legacy_raw" ValidatorTargetChunk ValidatorTarget = "chunk" ValidatorTargetSerialized ValidatorTarget = "serialized" ValidatorTargetTyped ValidatorTarget = "typed" ) type ValidatorRegistry struct { legacyBuilders map[string]LegacyRawValidatorBuilder legacyValidators map[string]OptionValidator legacySpecs map[string]ValidatorSpec typedEntries map[artifactVariantKey]typedValidatorEntry chunkEntries map[string]chunkValidatorEntry serializedEntries map[string]serializedValidatorEntry } type typedValidatorEntry struct { spec ValidatorSpec kind contracts.ArtifactKind valueType reflect.Type validateOptions OptionValidator builder func(BuildRequest) (any, error) } type chunkValidatorEntry struct { spec ValidatorSpec validateOptions OptionValidator builder func(BuildRequest) (contracts.ChunkValidator, error) } type serializedValidatorEntry struct { spec SerializedValidatorSpec validateOptions OptionValidator builder func(BuildRequest) (contracts.SerializedValidator, error) } func NewValidatorRegistry() *ValidatorRegistry { return &ValidatorRegistry{ legacyBuilders: make(map[string]LegacyRawValidatorBuilder), legacyValidators: make(map[string]OptionValidator), legacySpecs: make(map[string]ValidatorSpec), typedEntries: make(map[artifactVariantKey]typedValidatorEntry), chunkEntries: make(map[string]chunkValidatorEntry), serializedEntries: make(map[string]serializedValidatorEntry), } } func (r *ValidatorRegistry) RegisterLegacyRaw(key string, constructor LegacyRawValidatorConstructor) error { return r.RegisterLegacyRawWithSpec(ValidatorSpec{Key: key, ExecutionClass: contracts.ExecutionClassDeterministic}, constructor) } func (r *ValidatorRegistry) RegisterLegacyRawWithSpec(spec ValidatorSpec, constructor LegacyRawValidatorConstructor) error { if constructor == nil { return fmt.Errorf("validator constructor for %q must not be nil", strings.TrimSpace(spec.Key)) } return r.RegisterLegacyRawBuilderWithSpec(spec, allowLegacyOptions, func(BuildRequest) (contracts.LegacyRawValidator, error) { return constructor() }) } func (r *ValidatorRegistry) RegisterLegacyRawBuilderWithSpec(spec ValidatorSpec, validateOptions OptionValidator, builder LegacyRawValidatorBuilder) error { if r == nil { return fmt.Errorf("validator registry must not be nil") } normalizedSpec, err := normalizeValidatorSpec(spec) if err != nil { return err } if validateOptions == nil { return fmt.Errorf("validator option validator for %q must not be nil", normalizedSpec.Key) } if builder == nil { return fmt.Errorf("validator builder for %q must not be nil", normalizedSpec.Key) } if _, ok := r.legacyBuilders[normalizedSpec.Key]; ok { return fmt.Errorf("legacy raw validator %q is already registered", normalizedSpec.Key) } if r.legacyBuilders == nil { r.legacyBuilders = make(map[string]LegacyRawValidatorBuilder) } if r.legacyValidators == nil { r.legacyValidators = make(map[string]OptionValidator) } if r.legacySpecs == nil { r.legacySpecs = make(map[string]ValidatorSpec) } r.legacyBuilders[normalizedSpec.Key] = builder r.legacyValidators[normalizedSpec.Key] = validateOptions r.legacySpecs[normalizedSpec.Key] = normalizedSpec return nil } func RegisterTypedValidator[T any](registry *ValidatorRegistry, kind contracts.ArtifactKind, spec ValidatorSpec, constructor func() (contracts.TypedValidator[T], error)) error { if constructor == nil { return fmt.Errorf("validator constructor for %q must not be nil", strings.TrimSpace(spec.Key)) } return RegisterTypedValidatorBuilder(registry, kind, spec, allowLegacyOptions, func(BuildRequest) (contracts.TypedValidator[T], error) { return constructor() }) } func RegisterTypedValidatorBuilder[T any](registry *ValidatorRegistry, kind contracts.ArtifactKind, spec ValidatorSpec, validateOptions OptionValidator, builder func(BuildRequest) (contracts.TypedValidator[T], error)) error { if registry == nil { return fmt.Errorf("validator registry must not be nil") } normalizedSpec, err := normalizeValidatorSpec(spec) if err != nil { return err } kind = normalizeArtifactKind(kind) if kind == "" { return fmt.Errorf("typed validator %q artifact kind must not be empty", normalizedSpec.Key) } if validateOptions == nil { return fmt.Errorf("validator option validator for %q must not be nil", normalizedSpec.Key) } if builder == nil { return fmt.Errorf("validator builder for %q must not be nil", normalizedSpec.Key) } key := artifactVariantKey{module: normalizedSpec.Key, kind: kind} if _, ok := registry.typedEntries[key]; ok { return fmt.Errorf("validator %q variant for artifact kind %q is already registered", key.module, key.kind) } if registry.typedEntries == nil { registry.typedEntries = make(map[artifactVariantKey]typedValidatorEntry) } registry.typedEntries[key] = typedValidatorEntry{ spec: normalizedSpec, kind: kind, valueType: reflect.TypeFor[T](), validateOptions: validateOptions, builder: func(request BuildRequest) (any, error) { return builder(cloneBuildRequest(request)) }, } return nil } func RegisterChunkValidator(registry *ValidatorRegistry, spec ValidatorSpec, constructor func() (contracts.ChunkValidator, error)) error { if constructor == nil { return fmt.Errorf("validator constructor for %q must not be nil", strings.TrimSpace(spec.Key)) } return RegisterChunkValidatorBuilder(registry, spec, allowLegacyOptions, func(BuildRequest) (contracts.ChunkValidator, error) { return constructor() }) } func RegisterChunkValidatorBuilder(registry *ValidatorRegistry, spec ValidatorSpec, validateOptions OptionValidator, builder func(BuildRequest) (contracts.ChunkValidator, error)) error { if registry == nil { return fmt.Errorf("validator registry must not be nil") } normalizedSpec, err := normalizeValidatorSpec(spec) if err != nil { return err } if validateOptions == nil { return fmt.Errorf("validator option validator for %q must not be nil", normalizedSpec.Key) } if builder == nil { return fmt.Errorf("validator builder for %q must not be nil", normalizedSpec.Key) } if _, ok := registry.chunkEntries[normalizedSpec.Key]; ok { return fmt.Errorf("chunk validator %q is already registered", normalizedSpec.Key) } if registry.chunkEntries == nil { registry.chunkEntries = make(map[string]chunkValidatorEntry) } registry.chunkEntries[normalizedSpec.Key] = chunkValidatorEntry{spec: normalizedSpec, validateOptions: validateOptions, builder: builder} return nil } func RegisterSerializedValidator(registry *ValidatorRegistry, spec SerializedValidatorSpec, constructor func() (contracts.SerializedValidator, error)) error { if constructor == nil { return fmt.Errorf("validator constructor for %q must not be nil", strings.TrimSpace(spec.Key)) } return RegisterSerializedValidatorBuilder(registry, spec, allowLegacyOptions, func(BuildRequest) (contracts.SerializedValidator, error) { return constructor() }) } func RegisterSerializedValidatorBuilder(registry *ValidatorRegistry, spec SerializedValidatorSpec, validateOptions OptionValidator, builder func(BuildRequest) (contracts.SerializedValidator, error)) error { if registry == nil { return fmt.Errorf("validator registry must not be nil") } normalizedValidatorSpec, err := normalizeValidatorSpec(spec.ValidatorSpec) if err != nil { return err } spec.ValidatorSpec = normalizedValidatorSpec if !spec.SupportsChunks && !spec.SupportsArtifacts { return fmt.Errorf("serialized validator %q must support chunks, artifacts, or both", spec.Key) } if validateOptions == nil { return fmt.Errorf("validator option validator for %q must not be nil", spec.Key) } if builder == nil { return fmt.Errorf("validator builder for %q must not be nil", spec.Key) } if _, ok := registry.serializedEntries[spec.Key]; ok { return fmt.Errorf("serialized validator %q is already registered", spec.Key) } if registry.serializedEntries == nil { registry.serializedEntries = make(map[string]serializedValidatorEntry) } registry.serializedEntries[spec.Key] = serializedValidatorEntry{spec: spec, validateOptions: validateOptions, builder: builder} return nil } func (r *ValidatorRegistry) BuildLegacyRaw(key string) (contracts.LegacyRawValidator, error) { return r.BuildLegacyRawWithRequest(key, BuildRequest{}) } func (r *ValidatorRegistry) BuildLegacyRawWithRequest(key string, request BuildRequest) (contracts.LegacyRawValidator, 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") } builder, ok := r.legacyBuilders[normalizedKey] if !ok { return nil, fmt.Errorf("legacy raw validator %q is not registered", normalizedKey) } validator, err := builder(cloneBuildRequest(request)) 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()) } spec := r.legacySpecs[normalizedKey] if validator.ExecutionClass() != spec.ExecutionClass { return nil, fmt.Errorf("validator %q returned execution class %q, want %q", normalizedKey, validator.ExecutionClass(), spec.ExecutionClass) } return validator, nil } func (r *ValidatorRegistry) validateOptions(resolved ResolvedValidator) error { if r == nil { return fmt.Errorf("validator registry must not be nil") } key := strings.TrimSpace(resolved.Binding.Module) var validator OptionValidator switch resolved.Target { case ValidatorTargetTyped: entry, ok := r.typedEntry(key, resolved.ArtifactKind) if ok { validator = entry.validateOptions } case ValidatorTargetChunk: entry, ok := r.chunkEntry(key) if ok { validator = entry.validateOptions } case ValidatorTargetSerialized: entry, ok := r.serializedEntry(key) if ok { validator = entry.validateOptions } default: validator = r.legacyValidators[key] } if validator == nil { return fmt.Errorf("validator %q construction entry is not registered", key) } return validateRegisteredOptions(validator, resolved.Binding.Options) } func (r *ValidatorRegistry) Spec(key string) (ValidatorSpec, bool) { if r == nil { return ValidatorSpec{}, false } spec, ok := r.legacySpecs[strings.TrimSpace(key)] return spec, ok } func (r *ValidatorRegistry) typedEntry(key string, kind contracts.ArtifactKind) (typedValidatorEntry, bool) { if r == nil { return typedValidatorEntry{}, false } entry, ok := r.typedEntries[artifactVariantKey{module: strings.TrimSpace(key), kind: normalizeArtifactKind(kind)}] return entry, ok } func (r *ValidatorRegistry) chunkEntry(key string) (chunkValidatorEntry, bool) { if r == nil { return chunkValidatorEntry{}, false } entry, ok := r.chunkEntries[strings.TrimSpace(key)] return entry, ok } func (r *ValidatorRegistry) serializedEntry(key string) (serializedValidatorEntry, bool) { if r == nil { return serializedValidatorEntry{}, false } entry, ok := r.serializedEntries[strings.TrimSpace(key)] return entry, ok } func (r *ValidatorRegistry) registeredTypedKinds(key string) []contracts.ArtifactKind { if r == nil { return nil } module := strings.TrimSpace(key) kinds := make([]contracts.ArtifactKind, 0) for variant := range r.typedEntries { if variant.module == module { kinds = append(kinds, variant.kind) } } sortArtifactKinds(kinds) return kinds } func (r *ValidatorRegistry) RegisteredSpecs() []ValidatorSpec { if r == nil || len(r.legacySpecs) == 0 { return nil } keys := sortedRegistryKeys(r.legacySpecs) specs := make([]ValidatorSpec, 0, len(keys)) for _, key := range keys { specs = append(specs, r.legacySpecs[key]) } return specs } func (r *ValidatorRegistry) RegisteredKeys() []string { if r == nil { return nil } keys := make(map[string]struct{}) for key := range r.legacySpecs { keys[key] = struct{}{} } for key := range r.typedEntries { keys[key.module] = struct{}{} } for key := range r.chunkEntries { keys[key] = struct{}{} } for key := range r.serializedEntries { keys[key] = struct{}{} } return sortedRegistryKeys(keys) } func normalizeValidatorSpec(spec ValidatorSpec) (ValidatorSpec, error) { normalized := ValidatorSpec{Key: strings.TrimSpace(spec.Key), ExecutionClass: spec.ExecutionClass} if normalized.Key == "" { return ValidatorSpec{}, fmt.Errorf("validator key must not be empty") } switch normalized.ExecutionClass { case contracts.ExecutionClassDeterministic, contracts.ExecutionClassLLMBacked: default: return ValidatorSpec{}, fmt.Errorf("validator %q execution class %q is not supported", normalized.Key, normalized.ExecutionClass) } return normalized, nil } func sortValidatorSpecs(specs []ValidatorSpec) { sort.Slice(specs, func(i, j int) bool { return specs[i].Key < specs[j].Key }) }