Implement the initial framework skeleton

This commit is contained in:
2026-05-04 20:14:31 -05:00
parent a7feb0b097
commit c07320f9d0
18 changed files with 1563 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
package artifact
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"gitea.maximumdirect.net/eric/scriptorium/internal/domain"
"mime"
"os"
"path/filepath"
)
var (
ErrUnsupportedRefType = errors.New("unsupported artifact reference type")
ErrMissingInlineBody = errors.New("missing body for inline artifact")
ErrMissingFilePath = errors.New("missing file path for file artifact")
)
// Reader resolves artifact references into actual artifacts.
type Reader interface {
Read(ctx context.Context, ref domain.ArtifactRef) (*domain.Artifact, error)
}
// CompositeReader routes artifact resolution based on the reference type.
type CompositeReader struct {
inlineReader *inlineReader
fileReader *fileReader
}
func NewCompositeReader() Reader {
return &CompositeReader{
inlineReader: &inlineReader{},
fileReader: &fileReader{},
}
}
func (c *CompositeReader) Read(ctx context.Context, ref domain.ArtifactRef) (*domain.Artifact, error) {
switch ref.Type {
case domain.ArtifactRefInline:
return c.inlineReader.Read(ctx, ref)
case domain.ArtifactRefFile:
return c.fileReader.Read(ctx, ref)
default:
return nil, fmt.Errorf("%w: %s", ErrUnsupportedRefType, ref.Type)
}
}
type inlineReader struct{}
func (r *inlineReader) Read(ctx context.Context, ref domain.ArtifactRef) (*domain.Artifact, error) {
if ref.Body == "" {
return nil, ErrMissingInlineBody
}
body := []byte(ref.Body)
return &domain.Artifact{
Body: body,
Size: int64(len(body)),
Hash: fmt.Sprintf("%x", sha256.Sum256(body)),
URI: ref.URI,
}, nil
}
type fileReader struct{}
func (r *fileReader) Read(ctx context.Context, ref domain.ArtifactRef) (*domain.Artifact, error) {
if ref.URI == "" {
return nil, ErrMissingFilePath
}
data, err := os.ReadFile(ref.URI)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", ref.URI, err)
}
contentType := mime.TypeByExtension(filepath.Ext(ref.URI))
if contentType == "" {
contentType = "text/plain" // Default
}
return &domain.Artifact{
Name: filepath.Base(ref.URI),
ContentType: contentType,
Body: data,
URI: ref.URI,
Size: int64(len(data)),
Hash: fmt.Sprintf("%x", sha256.Sum256(data)),
}, nil
}

View File

@@ -0,0 +1,101 @@
package artifact
import (
"context"
"os"
"testing"
"gitea.maximumdirect.net/eric/scriptorium/internal/domain"
)
func TestCompositeReader_Read(t *testing.T) {
reader := NewCompositeReader()
ctx := context.Background()
t.Run("inline artifact", func(t *testing.T) {
ref := domain.ArtifactRef{
Type: domain.ArtifactRefInline,
Body: "hello world",
}
art, err := reader.Read(ctx, ref)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(art.Body) != "hello world" {
t.Errorf("expected 'hello world', got %s", string(art.Body))
}
if art.Hash == "" {
t.Error("expected hash to be computed")
}
})
t.Run("inline artifact missing body", func(t *testing.T) {
ref := domain.ArtifactRef{
Type: domain.ArtifactRefInline,
Body: "",
}
_, err := reader.Read(ctx, ref)
if err == nil || err != ErrMissingInlineBody {
t.Errorf("expected ErrMissingInlineBody, got %v", err)
}
})
t.Run("unsupported ref type", func(t *testing.T) {
ref := domain.ArtifactRef{
Type: domain.ArtifactRefS3,
URI: "s3://bucket/key",
}
_, err := reader.Read(ctx, ref)
if err == nil {
t.Error("expected error for unsupported type")
}
})
}
func TestFileReader_Read(t *testing.T) {
content := []byte("test file content")
tmpFile, err := os.CreateTemp("", "artifact_test_*.txt")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.Write(content); err != nil {
t.Fatal(err)
}
tmpFile.Close()
reader := NewCompositeReader()
ctx := context.Background()
t.Run("file artifact loading", func(t *testing.T) {
ref := domain.ArtifactRef{
Type: domain.ArtifactRefFile,
URI: tmpFile.Name(),
}
art, err := reader.Read(ctx, ref)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(art.Body) != string(content) {
t.Errorf("expected %s, got %s", string(content), string(art.Body))
}
if art.Name == "" {
t.Error("expected name to be inferred from filename")
}
if art.Hash == "" {
t.Error("expected hash to be computed")
}
})
t.Run("missing file path", func(t *testing.T) {
ref := domain.ArtifactRef{
Type: domain.ArtifactRefFile,
URI: "",
}
_, err := reader.Read(ctx, ref)
if err == nil || err != ErrMissingFilePath {
t.Errorf("expected ErrMissingFilePath, got %v", err)
}
})
}