Organize D&D extensions by domain
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package sourcerelatedness
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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_relatedness"
|
||||
const WarningReasonCode = "spell_not_near_source"
|
||||
|
||||
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 contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
if err := spellpayload.ValidateShape(payload); err != nil {
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
|
||||
var warnings []contracts.Warning
|
||||
for spellIndex, spell := range payload.SpellCasts {
|
||||
if !spellAppearsInCitedText(req.Source, spell) {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
Scope: fmt.Sprintf("spell_casts[%d]", spellIndex),
|
||||
ReasonCode: WarningReasonCode,
|
||||
Message: fmt.Sprintf("spell %q was not found in cited source text", strings.TrimSpace(spell.Spell)),
|
||||
})
|
||||
}
|
||||
}
|
||||
return contracts.ValidationResult{Approved: true, Warnings: warnings}, 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 spellAppearsInCitedText(doc *source.SourceDocument, spell spellpayload.SpellCast) bool {
|
||||
name := strings.ToLower(strings.TrimSpace(spell.Spell))
|
||||
if name == "" {
|
||||
return true
|
||||
}
|
||||
for _, ref := range spellpayload.SourceRefCandidates(doc, spell) {
|
||||
text, ok := spellpayload.CitedText(doc, ref)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(strings.ToLower(text), name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package sourcerelatedness
|
||||
|
||||
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 TestValidatorApprovesWithoutWarningWhenSpellAppearsInCitedText(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":2,"end_unit_id":2}]}]}`))
|
||||
if err != nil {
|
||||
t.Fatalf("Validate() error = %v, want nil", err)
|
||||
}
|
||||
if !result.Approved {
|
||||
t.Fatalf("Approved = false, want true")
|
||||
}
|
||||
if len(result.Warnings) != 0 {
|
||||
t.Fatalf("Warnings = %#v, want none", result.Warnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorWarnsWhenSpellDoesNotAppearInCitedText(t *testing.T) {
|
||||
result, err := New().Validate(context.Background(), requestWithPayload(validDocument(), `{"spell_casts":[{"caster":"Borin","spell":"Fire Bolt","effect":"scorches","narrative_description":"Borin casts Fire Bolt.","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 = false, want true")
|
||||
}
|
||||
if len(result.Warnings) != 1 {
|
||||
t.Fatalf("Warnings = %#v, want one warning", result.Warnings)
|
||||
}
|
||||
if result.Warnings[0].ReasonCode != WarningReasonCode {
|
||||
t.Fatalf("ReasonCode = %q, want %q", result.Warnings[0].ReasonCode, WarningReasonCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorApprovesMalformedPayloadWithoutWarning(t *testing.T) {
|
||||
result, err := New().Validate(context.Background(), requestWithPayload(validDocument(), `{"spell_casts":`))
|
||||
if err != nil {
|
||||
t.Fatalf("Validate() error = %v, want nil", err)
|
||||
}
|
||||
if !result.Approved {
|
||||
t.Fatalf("Approved = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
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: "Borin draws his dagger."},
|
||||
{ID: 2, Kind: "message", Text: "Aria casts Cure Wounds on Borin."},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user