package pipeline import ( "fmt" "reflect" "sort" "strings" "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" ) // ArtifactEvidenceProjector returns the direct source references represented // by one normalized artifact value. type ArtifactEvidenceProjector[T any] func(T) []source.SourceRef // ArtifactEvidenceRegistry keeps the typed projection boundary private while // allowing preparation and execution to discover registered capabilities. type ArtifactEvidenceRegistry struct { entries map[contracts.ArtifactKind]artifactEvidenceEntry } type artifactEvidenceEntry struct { valueType reflect.Type project func(any) ([]source.SourceRef, error) } func NewArtifactEvidenceRegistry() *ArtifactEvidenceRegistry { return &ArtifactEvidenceRegistry{entries: make(map[contracts.ArtifactKind]artifactEvidenceEntry)} } // RegisterArtifactEvidence registers one evidence projector for an artifact // kind. Projectors are invoked only after an exact Go-type check. func RegisterArtifactEvidence[T any](registry *ArtifactEvidenceRegistry, kind contracts.ArtifactKind, projector ArtifactEvidenceProjector[T]) error { if registry == nil { return fmt.Errorf("artifact evidence registry must not be nil") } if projector == nil { return fmt.Errorf("artifact evidence projector must not be nil") } kind = normalizeArtifactKind(kind) if kind == "" { return fmt.Errorf("artifact evidence kind must not be empty") } if _, ok := registry.entries[kind]; ok { return fmt.Errorf("artifact evidence %q is already registered", kind) } valueType := reflect.TypeFor[T]() if registry.entries == nil { registry.entries = make(map[contracts.ArtifactKind]artifactEvidenceEntry) } registry.entries[kind] = artifactEvidenceEntry{ valueType: valueType, project: func(value any) ([]source.SourceRef, error) { actualType := reflect.TypeOf(value) if actualType != valueType { return nil, newArtifactEvidenceTypeError(kind, valueType, actualType) } typed, ok := value.(T) if !ok { return nil, newArtifactEvidenceTypeError(kind, valueType, actualType) } return append([]source.SourceRef(nil), projector(typed)...), nil }, } return nil } func (r *ArtifactEvidenceRegistry) RegisteredKinds() []contracts.ArtifactKind { if r == nil || len(r.entries) == 0 { return nil } kinds := make([]contracts.ArtifactKind, 0, len(r.entries)) for kind := range r.entries { kinds = append(kinds, kind) } sort.Slice(kinds, func(i, j int) bool { return kinds[i] < kinds[j] }) return kinds } // Project returns independently owned direct references for a registered // artifact value. func (r *ArtifactEvidenceRegistry) Project(kind contracts.ArtifactKind, value any) ([]source.SourceRef, error) { entry, normalizedKind, err := r.entry(kind) if err != nil { return nil, err } refs, err := entry.project(value) if err != nil { return nil, fmt.Errorf("project artifact evidence %q: %w", normalizedKind, err) } return append([]source.SourceRef(nil), refs...), nil } func (r *ArtifactEvidenceRegistry) entry(kind contracts.ArtifactKind) (artifactEvidenceEntry, contracts.ArtifactKind, error) { if r == nil { return artifactEvidenceEntry{}, "", fmt.Errorf("artifact evidence registry must not be nil") } kind = normalizeArtifactKind(kind) if kind == "" { return artifactEvidenceEntry{}, "", fmt.Errorf("artifact evidence kind must not be empty") } entry, ok := r.entries[kind] if !ok { return artifactEvidenceEntry{}, kind, fmt.Errorf("artifact evidence %q is not registered", kind) } return entry, kind, nil } func (r *ArtifactEvidenceRegistry) valueType(kind contracts.ArtifactKind) (reflect.Type, bool) { if r == nil { return nil, false } entry, ok := r.entries[normalizeArtifactKind(kind)] if !ok { return nil, false } return entry.valueType, true } func newArtifactEvidenceTypeError(kind contracts.ArtifactKind, expected, actual reflect.Type) error { actualName := "" if actual != nil { actualName = actual.String() } return fmt.Errorf("project artifact evidence %q: expected exact Go type %s, got %s", kind, expected, actualName) } func normalizeEvidenceLaneIDs(values []string) ([]string, error) { if len(values) == 0 { return nil, fmt.Errorf("evidence lane ids must not be empty") } seen := make(map[string]struct{}, len(values)) lanes := make([]string, 0, len(values)) for _, raw := range values { lane := strings.TrimSpace(raw) if lane == "" { return nil, fmt.Errorf("evidence lane id must not be empty") } if _, ok := seen[lane]; ok { return nil, fmt.Errorf("evidence lane id %q is duplicated", lane) } seen[lane] = struct{}{} lanes = append(lanes, lane) } sort.Strings(lanes) return lanes, nil }