Add typed artifact codec foundation
This commit is contained in:
67
internal/framework/contracts/artifact.go
Normal file
67
internal/framework/contracts/artifact.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// ArtifactKind is the stable logical identity of a domain artifact.
|
||||
type ArtifactKind string
|
||||
|
||||
// ArtifactSchema describes the durable representation owned by an artifact
|
||||
// codec. JSONSchema is cloned whenever framework ownership changes.
|
||||
type ArtifactSchema struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
JSONSchema []byte `json:"-"`
|
||||
}
|
||||
|
||||
// SerializedArtifact is the domain-neutral representation of a typed
|
||||
// artifact at an explicit serialization boundary.
|
||||
type SerializedArtifact struct {
|
||||
Kind ArtifactKind `json:"kind"`
|
||||
Schema ArtifactSchema `json:"schema"`
|
||||
MediaType string `json:"media_type"`
|
||||
Content []byte `json:"-"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ArtifactCodec owns the stable encoding for one concrete artifact type.
|
||||
type ArtifactCodec[T any] interface {
|
||||
Kind() ArtifactKind
|
||||
Schema() ArtifactSchema
|
||||
MediaType() string
|
||||
Encode(T) ([]byte, error)
|
||||
Decode([]byte) (T, error)
|
||||
}
|
||||
|
||||
// DigestArtifactSchema returns the SHA-256 digest of the exact JSON Schema
|
||||
// bytes. Schema formatting is therefore part of the registered identity.
|
||||
func DigestArtifactSchema(schema ArtifactSchema) string {
|
||||
sum := sha256.Sum256(schema.JSONSchema)
|
||||
return "sha256:" + hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func CloneArtifactSchema(schema ArtifactSchema) ArtifactSchema {
|
||||
schema.JSONSchema = append([]byte(nil), schema.JSONSchema...)
|
||||
return schema
|
||||
}
|
||||
|
||||
func CloneSerializedArtifact(artifact SerializedArtifact) SerializedArtifact {
|
||||
artifact.Schema = CloneArtifactSchema(artifact.Schema)
|
||||
artifact.Content = append([]byte(nil), artifact.Content...)
|
||||
artifact.Metadata = cloneArtifactMetadata(artifact.Metadata)
|
||||
return artifact
|
||||
}
|
||||
|
||||
func cloneArtifactMetadata(metadata map[string]any) map[string]any {
|
||||
if len(metadata) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]any, len(metadata))
|
||||
for key, value := range metadata {
|
||||
out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
45
internal/framework/contracts/artifact_test.go
Normal file
45
internal/framework/contracts/artifact_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package contracts
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDigestArtifactSchemaUsesExactBytes(t *testing.T) {
|
||||
first := ArtifactSchema{JSONSchema: []byte(`{"type":"object"}`)}
|
||||
second := ArtifactSchema{JSONSchema: []byte("{\n \"type\": \"object\"\n}")}
|
||||
|
||||
if got := DigestArtifactSchema(first); got != "sha256:a2c799262a3ce3c19ef5cdd983bf3d12b43ab3c426227091b909dcb7054738c0" {
|
||||
t.Fatalf("DigestArtifactSchema() = %q, want stable SHA-256", got)
|
||||
}
|
||||
if DigestArtifactSchema(first) == DigestArtifactSchema(second) {
|
||||
t.Fatal("schema digests match for different exact bytes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactCloneHelpersOwnSlicesAndMaps(t *testing.T) {
|
||||
schema := ArtifactSchema{ID: "notes.v1", Name: "notes", Version: "v1", JSONSchema: []byte(`{"type":"object"}`)}
|
||||
artifact := SerializedArtifact{
|
||||
Kind: "test/notes",
|
||||
Schema: schema,
|
||||
MediaType: "application/json",
|
||||
Content: []byte(`{"items":["one"]}`),
|
||||
Metadata: map[string]any{"origin": "test"},
|
||||
}
|
||||
|
||||
clonedSchema := CloneArtifactSchema(schema)
|
||||
cloned := CloneSerializedArtifact(artifact)
|
||||
schema.JSONSchema[0] = '['
|
||||
artifact.Content[0] = '['
|
||||
artifact.Metadata["origin"] = "changed"
|
||||
|
||||
if string(clonedSchema.JSONSchema) != `{"type":"object"}` {
|
||||
t.Fatalf("cloned schema = %q, want original bytes", clonedSchema.JSONSchema)
|
||||
}
|
||||
if string(cloned.Schema.JSONSchema) != `{"type":"object"}` {
|
||||
t.Fatalf("serialized artifact schema = %q, want original bytes", cloned.Schema.JSONSchema)
|
||||
}
|
||||
if string(cloned.Content) != `{"items":["one"]}` {
|
||||
t.Fatalf("cloned content = %q, want original bytes", cloned.Content)
|
||||
}
|
||||
if cloned.Metadata["origin"] != "test" {
|
||||
t.Fatalf("cloned metadata = %#v, want independent map", cloned.Metadata)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user