Files
notarius/internal/framework/pipeline/artifact_codec_registry.go

283 lines
9.4 KiB
Go

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)
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
},
}
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) 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 := "<nil>"
if expected != nil {
expectedName = expected.String()
}
actualName := "<nil>"
if actual != nil {
actualName = actual.String()
}
return &ArtifactCodecTypeError{
Operation: operation,
Kind: kind,
ExpectedType: expectedName,
ActualType: actualName,
}
}