package pipeline import ( "bytes" "encoding/json" "fmt" "reflect" "sort" "strings" "github.com/santhosh-tekuri/jsonschema/v6" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" ) type ArtifactCodecSpec struct { Kind contracts.ArtifactKind Schema contracts.ArtifactSchema SchemaDigest string MediaType string } // ArtifactCodecTypeError reports a value that does not have the exact Go type // registered for an artifact kind. type ArtifactCodecTypeError struct { Operation string Kind contracts.ArtifactKind ExpectedType string ActualType string } func (e *ArtifactCodecTypeError) Error() string { return fmt.Sprintf("%s artifact %q: expected exact Go type %s, got %s", e.Operation, e.Kind, e.ExpectedType, e.ActualType) } // ArtifactCodecCompatibilityError reports serialized metadata that does not // identify the registered representation for an artifact kind. type ArtifactCodecCompatibilityError struct { Kind contracts.ArtifactKind Reason string } func (e *ArtifactCodecCompatibilityError) Error() string { return fmt.Sprintf("decode artifact %q: %s", e.Kind, e.Reason) } // ArtifactCodecOperationError preserves an encode or decode failure from the // registered domain codec. type ArtifactCodecOperationError struct { Operation string Kind contracts.ArtifactKind Err error } func (e *ArtifactCodecOperationError) Error() string { return fmt.Sprintf("%s artifact %q: %v", e.Operation, e.Kind, e.Err) } func (e *ArtifactCodecOperationError) Unwrap() error { return e.Err } type ArtifactCodecRegistry struct { entries map[contracts.ArtifactKind]artifactCodecEntry } type artifactCodecEntry struct { spec ArtifactCodecSpec valueType reflect.Type encode func(any) ([]byte, error) encodeCandidate func(any) ([]byte, error) metadata func(any) map[string]any decode func([]byte) (any, error) } func NewArtifactCodecRegistry() *ArtifactCodecRegistry { return &ArtifactCodecRegistry{entries: make(map[contracts.ArtifactKind]artifactCodecEntry)} } // RegisterArtifactCodec registers one codec for T. The concrete type is kept // private and checked at every erased encode boundary. func RegisterArtifactCodec[T any](registry *ArtifactCodecRegistry, codec contracts.ArtifactCodec[T]) error { if registry == nil { return fmt.Errorf("artifact codec registry must not be nil") } if nilInterface(codec) { return fmt.Errorf("artifact codec must not be nil") } spec, err := artifactCodecSpec(codec.Kind(), codec.Schema(), codec.MediaType()) if err != nil { return err } if _, ok := registry.entries[spec.Kind]; ok { return fmt.Errorf("artifact codec %q is already registered", spec.Kind) } valueType := reflect.TypeFor[T]() entry := artifactCodecEntry{ spec: cloneArtifactCodecSpec(spec), valueType: valueType, encode: func(value any) ([]byte, error) { actualType := reflect.TypeOf(value) if actualType != valueType { return nil, newArtifactCodecTypeError("encode", spec.Kind, valueType, actualType) } typed, ok := value.(T) if !ok { return nil, newArtifactCodecTypeError("encode", spec.Kind, valueType, actualType) } encoded, err := codec.Encode(typed) if err != nil { return nil, &ArtifactCodecOperationError{Operation: "encode", Kind: spec.Kind, Err: err} } return append([]byte(nil), encoded...), nil }, decode: func(content []byte) (any, error) { decoded, err := codec.Decode(append([]byte(nil), content...)) if err != nil { return nil, &ArtifactCodecOperationError{Operation: "decode", Kind: spec.Kind, Err: err} } return decoded, nil }, } entry.encodeCandidate = func(value any) ([]byte, error) { typed, err := exactTypedValue[T]("encode candidate artifact", value) if err != nil { return nil, err } content, err := codec.EncodeCandidate(typed) if err != nil { return nil, &ArtifactCodecOperationError{Operation: "encode candidate", Kind: spec.Kind, Err: err} } return append([]byte(nil), content...), nil } if provider, ok := any(codec).(interface{ Metadata(T) map[string]any }); ok { entry.metadata = func(value any) map[string]any { typed, err := exactTypedValue[T]("artifact metadata", value) if err != nil { return nil } return cloneMetadata(provider.Metadata(typed)) } } if registry.entries == nil { registry.entries = make(map[contracts.ArtifactKind]artifactCodecEntry) } registry.entries[spec.Kind] = entry return nil } func (r *ArtifactCodecRegistry) Spec(kind contracts.ArtifactKind) (ArtifactCodecSpec, bool) { if r == nil { return ArtifactCodecSpec{}, false } entry, ok := r.entries[normalizeArtifactKind(kind)] if !ok { return ArtifactCodecSpec{}, false } return cloneArtifactCodecSpec(entry.spec), true } func (r *ArtifactCodecRegistry) 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 (r *ArtifactCodecRegistry) 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 } // Encode serializes an erased framework value after proving its exact // registered Go type. func (r *ArtifactCodecRegistry) Encode(kind contracts.ArtifactKind, value any) (contracts.SerializedArtifact, error) { entry, normalizedKind, err := r.entry(kind) if err != nil { return contracts.SerializedArtifact{}, err } content, err := entry.encode(value) if err != nil { return contracts.SerializedArtifact{}, err } return contracts.SerializedArtifact{ Kind: normalizedKind, Schema: contracts.CloneArtifactSchema(entry.spec.Schema), MediaType: entry.spec.MediaType, Content: content, }, nil } // Decode verifies serialized identity before invoking the registered codec. func (r *ArtifactCodecRegistry) Decode(artifact contracts.SerializedArtifact) (any, error) { entry, normalizedKind, err := r.entry(artifact.Kind) if err != nil { return nil, err } if artifact.Schema.ID != entry.spec.Schema.ID || artifact.Schema.Name != entry.spec.Schema.Name || artifact.Schema.Version != entry.spec.Schema.Version { return nil, &ArtifactCodecCompatibilityError{Kind: normalizedKind, Reason: "schema identity does not match registered codec"} } if contracts.DigestArtifactSchema(artifact.Schema) != entry.spec.SchemaDigest { return nil, &ArtifactCodecCompatibilityError{Kind: normalizedKind, Reason: "schema digest does not match registered codec"} } if strings.TrimSpace(artifact.MediaType) != entry.spec.MediaType { return nil, &ArtifactCodecCompatibilityError{Kind: normalizedKind, Reason: "media type does not match registered codec"} } return entry.decode(artifact.Content) } func (r *ArtifactCodecRegistry) entry(kind contracts.ArtifactKind) (artifactCodecEntry, contracts.ArtifactKind, error) { if r == nil { return artifactCodecEntry{}, "", fmt.Errorf("artifact codec registry must not be nil") } normalizedKind := normalizeArtifactKind(kind) if normalizedKind == "" { return artifactCodecEntry{}, "", fmt.Errorf("artifact kind must not be empty") } entry, ok := r.entries[normalizedKind] if !ok { return artifactCodecEntry{}, normalizedKind, fmt.Errorf("artifact codec %q is not registered", normalizedKind) } return entry, normalizedKind, nil } func artifactCodecSpec(kind contracts.ArtifactKind, schema contracts.ArtifactSchema, mediaType string) (ArtifactCodecSpec, error) { kind = normalizeArtifactKind(kind) schema.ID = strings.TrimSpace(schema.ID) schema.Name = strings.TrimSpace(schema.Name) schema.Version = strings.TrimSpace(schema.Version) mediaType = strings.TrimSpace(mediaType) switch { case kind == "": return ArtifactCodecSpec{}, fmt.Errorf("artifact codec kind must not be empty") case schema.ID == "": return ArtifactCodecSpec{}, fmt.Errorf("artifact codec %q schema id must not be empty", kind) case schema.Name == "": return ArtifactCodecSpec{}, fmt.Errorf("artifact codec %q schema name must not be empty", kind) case schema.Version == "": return ArtifactCodecSpec{}, fmt.Errorf("artifact codec %q schema version must not be empty", kind) case mediaType == "": return ArtifactCodecSpec{}, fmt.Errorf("artifact codec %q media type must not be empty", kind) case len(bytes.TrimSpace(schema.JSONSchema)) == 0: return ArtifactCodecSpec{}, fmt.Errorf("artifact codec %q JSON Schema must not be empty", kind) case !json.Valid(schema.JSONSchema): return ArtifactCodecSpec{}, fmt.Errorf("artifact codec %q JSON Schema must be valid JSON", kind) } schemaDocument, err := jsonschema.UnmarshalJSON(bytes.NewReader(schema.JSONSchema)) if err != nil { return ArtifactCodecSpec{}, fmt.Errorf("artifact codec %q JSON Schema is invalid: %w", kind, err) } compiler := jsonschema.NewCompiler() if err := compiler.AddResource("artifact-schema.json", schemaDocument); err != nil { return ArtifactCodecSpec{}, fmt.Errorf("artifact codec %q JSON Schema is invalid: %w", kind, err) } if _, err := compiler.Compile("artifact-schema.json"); err != nil { return ArtifactCodecSpec{}, fmt.Errorf("artifact codec %q JSON Schema is invalid: %w", kind, err) } schema = contracts.CloneArtifactSchema(schema) return ArtifactCodecSpec{ Kind: kind, Schema: schema, SchemaDigest: contracts.DigestArtifactSchema(schema), MediaType: mediaType, }, nil } func normalizeArtifactKind(kind contracts.ArtifactKind) contracts.ArtifactKind { return contracts.ArtifactKind(strings.TrimSpace(string(kind))) } func cloneArtifactCodecSpec(spec ArtifactCodecSpec) ArtifactCodecSpec { spec.Schema = contracts.CloneArtifactSchema(spec.Schema) return spec } func nilInterface(value any) bool { if value == nil { return true } reflected := reflect.ValueOf(value) switch reflected.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: return reflected.IsNil() default: return false } } func newArtifactCodecTypeError(operation string, kind contracts.ArtifactKind, expected reflect.Type, actual reflect.Type) error { expectedName := "" if expected != nil { expectedName = expected.String() } actualName := "" if actual != nil { actualName = actual.String() } return &ArtifactCodecTypeError{ Operation: operation, Kind: kind, ExpectedType: expectedName, ActualType: actualName, } }