Add typed spell validation strategies

This commit is contained in:
2026-07-17 07:02:30 +00:00
parent 142ba36695
commit 52e6b31408
25 changed files with 708 additions and 410 deletions

View File

@@ -174,6 +174,30 @@ func TestMergeRejectsInvalidJSONAndNonJSONMediaTypes(t *testing.T) {
}
}
func TestTypedMergeUsesRequestOrderForReusableValueType(t *testing.T) {
type notes struct{ Values []string }
merger, err := NewTyped(func(values []notes) (notes, error) {
var combined notes
for _, value := range values {
combined.Values = append(combined.Values, value.Values...)
}
return combined, nil
})
if err != nil {
t.Fatalf("NewTyped() error = %v", err)
}
result, err := merger.Merge(context.Background(), contracts.TypedMergeRequest[notes]{ExtractOutputs: []contracts.ExtractArtifact[notes]{
{ChunkIndex: 4, Value: notes{Values: []string{"first"}}},
{ChunkIndex: 1, Value: notes{Values: []string{"second"}}},
}})
if err != nil {
t.Fatalf("Merge() error = %v", err)
}
if got := result.Value.Values; !reflect.DeepEqual(got, []string{"first", "second"}) {
t.Fatalf("Values = %#v", got)
}
}
func extractOutput(chunkID string, chunkIndex int, content string) contracts.ExtractOutput {
return contracts.ExtractOutput{
LaneID: "events",

View File

@@ -0,0 +1,62 @@
package appendorder
import (
"context"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)
// CombineFunc combines values in the source-chunk order supplied by the
// framework. Implementations must not reorder the slice.
type CombineFunc[T any] func([]T) (T, error)
type TypedMerger[T any] struct {
combine CombineFunc[T]
}
func NewTyped[T any](combine CombineFunc[T]) (*TypedMerger[T], error) {
if combine == nil {
return nil, mergerErrorf("combine function must not be nil")
}
return &TypedMerger[T]{combine: combine}, nil
}
func (m *TypedMerger[T]) Key() string { return Key }
func (m *TypedMerger[T]) Merge(ctx context.Context, req contracts.TypedMergeRequest[T]) (contracts.TypedMergeResult[T], error) {
if m == nil || m.combine == nil {
return contracts.TypedMergeResult[T]{}, mergerErrorf("merger must not be nil")
}
if ctx == nil {
return contracts.TypedMergeResult[T]{}, mergerErrorf("context must not be nil")
}
if err := ctx.Err(); err != nil {
return contracts.TypedMergeResult[T]{}, mergerErrorf("context error before merge: %w", err)
}
values := make([]T, len(req.ExtractOutputs))
for i, output := range req.ExtractOutputs {
values[i] = output.Value
}
value, err := m.combine(values)
if err != nil {
return contracts.TypedMergeResult[T]{}, mergerErrorf("combine values: %w", err)
}
return contracts.TypedMergeResult[T]{Value: value}, nil
}
func TypedModuleSpec(kind contracts.ArtifactKind) pipeline.ModuleSpec {
spec := ModuleSpec()
spec.ArtifactKind = kind
return spec
}
func RegisterTyped[T any](registry *pipeline.MergerRegistry, kind contracts.ArtifactKind, combine CombineFunc[T]) error {
validateOptions := func(options map[string]any) error { return pipeline.RejectUnknownOptions(options) }
return pipeline.RegisterMergerBuilder(registry, TypedModuleSpec(kind), validateOptions, func(request pipeline.BuildRequest) (contracts.Merger[T], error) {
if err := pipeline.RejectUnknownOptions(request.Options); err != nil {
return nil, err
}
return NewTyped(combine)
})
}