Organize D&D extensions by domain

This commit is contained in:
2026-07-17 05:05:48 +00:00
parent a81b9f1e1f
commit 15c369c509
64 changed files with 469 additions and 194 deletions

View File

@@ -0,0 +1,69 @@
package sourcerefs
import (
"context"
"fmt"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/spellpayload"
)
const Key = "extract/dnd/spells/source_refs"
const ReasonCode = "invalid_source_refs"
var _ contracts.Validator = (*Validator)(nil)
type Validator struct{}
func New() *Validator {
return &Validator{}
}
func (v *Validator) Name() string {
return Key
}
func (v *Validator) ExecutionClass() contracts.ExecutionClass {
return contracts.ExecutionClassDeterministic
}
func (v *Validator) Validate(ctx context.Context, req contracts.ValidationRequest) (contracts.ValidationResult, error) {
payload, err := spellpayload.ValidationRequestPayload(req)
if err != nil {
return rejection(err.Error()), nil
}
if err := spellpayload.ValidateShape(payload); err != nil {
return rejection(err.Error()), nil
}
for spellIndex, spell := range payload.SpellCasts {
for refIndex, ref := range spellpayload.SourceRefCandidates(req.Source, spell) {
if err := source.ValidateRef(req.Source, ref); err != nil {
return rejection(fmt.Sprintf("spell_casts[%d].source_refs[%d]: %v", spellIndex, refIndex, err)), nil
}
}
}
return contracts.ValidationResult{Approved: true}, nil
}
func Spec() pipeline.ValidatorSpec {
return pipeline.ValidatorSpec{
Key: Key,
ExecutionClass: contracts.ExecutionClassDeterministic,
}
}
func Register(registry *pipeline.ValidatorRegistry) error {
return registry.RegisterWithSpec(Spec(), func() (contracts.Validator, error) {
return New(), nil
})
}
func rejection(message string) contracts.ValidationResult {
return contracts.ValidationResult{
Approved: false,
ReasonCode: ReasonCode,
Message: message,
}
}

View File

@@ -0,0 +1,83 @@
package sourcerefs
import (
"context"
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)
func TestValidatorApprovesValidSourceRefs(t *testing.T) {
result, err := New().Validate(context.Background(), requestWithPayload(validDocument(), `{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","effect":"heals","narrative_description":"Aria casts Cure Wounds.","source_refs":[{"source_id":"session","start_unit_id":1,"end_unit_id":2}]}]}`))
if err != nil {
t.Fatalf("Validate() error = %v, want nil", err)
}
if !result.Approved {
t.Fatalf("Validate() = %#v, want approved", result)
}
}
func TestValidatorRejectsInvalidSourceRefs(t *testing.T) {
result, err := New().Validate(context.Background(), requestWithPayload(validDocument(), `{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","effect":"heals","narrative_description":"Aria casts Cure Wounds.","source_refs":[{"source_id":"session","start_unit_id":99,"end_unit_id":99}]}]}`))
if err != nil {
t.Fatalf("Validate() error = %v, want nil", err)
}
if result.Approved {
t.Fatalf("Approved = true, want false")
}
if result.ReasonCode != ReasonCode {
t.Fatalf("ReasonCode = %q, want %q", result.ReasonCode, ReasonCode)
}
}
func TestValidatorRejectsMissingSourceDocument(t *testing.T) {
result, err := New().Validate(context.Background(), requestWithPayload(nil, `{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","effect":"heals","narrative_description":"Aria casts Cure Wounds.","source_refs":[{"source_id":"session","start_unit_id":1,"end_unit_id":1}]}]}`))
if err != nil {
t.Fatalf("Validate() error = %v, want nil", err)
}
if result.Approved {
t.Fatalf("Approved = true, want false")
}
if result.ReasonCode != ReasonCode {
t.Fatalf("ReasonCode = %q, want %q", result.ReasonCode, ReasonCode)
}
}
func TestSpecAndRegister(t *testing.T) {
registry := pipeline.NewValidatorRegistry()
if err := Register(registry); err != nil {
t.Fatalf("Register() error = %v, want nil", err)
}
validator, err := registry.Build(Key)
if err != nil {
t.Fatalf("Build(%q) error = %v, want nil", Key, err)
}
if validator.Name() != Key || validator.ExecutionClass() != contracts.ExecutionClassDeterministic {
t.Fatalf("validator = %q/%q, want key and deterministic execution", validator.Name(), validator.ExecutionClass())
}
}
func requestWithPayload(doc *source.SourceDocument, payload string) contracts.ValidationRequest {
return contracts.ValidationRequest{
Source: doc,
Payload: contracts.RawPayload{
Content: []byte(payload),
MediaType: "application/json",
},
}
}
func validDocument() *source.SourceDocument {
return &source.SourceDocument{
ID: "session",
Kind: "transcript",
Format: "application/json",
Digest: "sha256:session",
Units: []source.SourceUnit{
{ID: 1, Kind: "message", Text: "Aria raises her holy symbol."},
{ID: 2, Kind: "message", Text: "Aria casts Cure Wounds on Borin."},
},
}
}