Centralize D&D candidate JSON codecs
This commit is contained in:
38
internal/modules/dnd/codec/candidatejson/candidate_json.go
Normal file
38
internal/modules/dnd/codec/candidatejson/candidate_json.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Package candidatejson provides strict JSON mechanics for D&D artifact
|
||||
// candidates before artifact-specific validation.
|
||||
package candidatejson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// EncodeCandidate encodes a typed candidate with an artifact-specific error
|
||||
// label.
|
||||
func EncodeCandidate[T any](label string, value T) ([]byte, error) {
|
||||
content, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode %s: %w", label, err)
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// DecodeCandidate decodes exactly one typed JSON value while rejecting unknown
|
||||
// fields and trailing values with an artifact-specific error label.
|
||||
func DecodeCandidate[T any](label string, content []byte) (T, error) {
|
||||
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||
decoder.DisallowUnknownFields()
|
||||
var value T
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
var zero T
|
||||
return zero, fmt.Errorf("decode %s: %w", label, err)
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
var zero T
|
||||
return zero, fmt.Errorf("decode %s: multiple JSON values", label)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package candidatejson
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testCandidate struct {
|
||||
Items []string `json:"items"`
|
||||
}
|
||||
|
||||
func TestCandidateJSON(t *testing.T) {
|
||||
input := testCandidate{Items: []string{"one", "two"}}
|
||||
content, err := EncodeCandidate("test candidate", input)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeCandidate() error = %v", err)
|
||||
}
|
||||
if !json.Valid(content) {
|
||||
t.Fatalf("EncodeCandidate() = %q, want JSON", content)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
content []byte
|
||||
want testCandidate
|
||||
wantErr string
|
||||
}{
|
||||
{name: "typed round trip", content: content, want: input},
|
||||
{name: "unknown field", content: []byte(`{"items":[],"unexpected":true}`), wantErr: "unknown field"},
|
||||
{name: "malformed JSON", content: []byte(`{"items":`), wantErr: "decode test candidate"},
|
||||
{name: "trailing value", content: []byte(`{"items":[]} {}`), wantErr: "multiple JSON values"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := DecodeCandidate[testCandidate]("test candidate", test.content)
|
||||
if test.wantErr != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
|
||||
t.Fatalf("DecodeCandidate() error = %v, want %q", err, test.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil || !reflect.DeepEqual(got, test.want) {
|
||||
t.Fatalf("DecodeCandidate() = %#v, %v; want %#v", got, err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package combatturns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/candidatejson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -59,11 +57,7 @@ func (c *Codec) Encode(value dnd.CombatTurnList) ([]byte, error) {
|
||||
// EncodeCandidate provides the durable representation before typed validators
|
||||
// have approved a value.
|
||||
func (c *Codec) EncodeCandidate(value dnd.CombatTurnList) ([]byte, error) {
|
||||
content, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode dnd combat turn list: %w", err)
|
||||
}
|
||||
return content, nil
|
||||
return candidatejson.EncodeCandidate("dnd combat turn list", value)
|
||||
}
|
||||
|
||||
func (c *Codec) Decode(content []byte) (dnd.CombatTurnList, error) {
|
||||
@@ -80,17 +74,7 @@ func (c *Codec) Decode(content []byte) (dnd.CombatTurnList, error) {
|
||||
// DecodeCandidate reads one strict durable JSON value before semantic
|
||||
// validators have approved it.
|
||||
func (c *Codec) DecodeCandidate(content []byte) (dnd.CombatTurnList, error) {
|
||||
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||
decoder.DisallowUnknownFields()
|
||||
var value dnd.CombatTurnList
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
return dnd.CombatTurnList{}, fmt.Errorf("decode dnd combat turn list: %w", err)
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
return dnd.CombatTurnList{}, fmt.Errorf("decode dnd combat turn list: multiple JSON values")
|
||||
}
|
||||
return value, nil
|
||||
return candidatejson.DecodeCandidate[dnd.CombatTurnList]("dnd combat turn list", content)
|
||||
}
|
||||
|
||||
func validate(value dnd.CombatTurnList) error {
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
package npcinteractions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/candidatejson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -60,11 +58,7 @@ func (c *Codec) Encode(value dnd.NPCInteractionList) ([]byte, error) {
|
||||
// EncodeCandidate provides the durable representation before semantic
|
||||
// validators have approved a value.
|
||||
func (c *Codec) EncodeCandidate(value dnd.NPCInteractionList) ([]byte, error) {
|
||||
content, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode dnd npc interaction list: %w", err)
|
||||
}
|
||||
return content, nil
|
||||
return candidatejson.EncodeCandidate("dnd npc interaction list", value)
|
||||
}
|
||||
|
||||
func (c *Codec) Decode(content []byte) (dnd.NPCInteractionList, error) {
|
||||
@@ -81,17 +75,7 @@ func (c *Codec) Decode(content []byte) (dnd.NPCInteractionList, error) {
|
||||
// DecodeCandidate reads one strict durable JSON value before semantic
|
||||
// validators have approved it.
|
||||
func (c *Codec) DecodeCandidate(content []byte) (dnd.NPCInteractionList, error) {
|
||||
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||
decoder.DisallowUnknownFields()
|
||||
var value dnd.NPCInteractionList
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
return dnd.NPCInteractionList{}, fmt.Errorf("decode dnd npc interaction list: %w", err)
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
return dnd.NPCInteractionList{}, fmt.Errorf("decode dnd npc interaction list: multiple JSON values")
|
||||
}
|
||||
return value, nil
|
||||
return candidatejson.DecodeCandidate[dnd.NPCInteractionList]("dnd npc interaction list", content)
|
||||
}
|
||||
|
||||
func validate(value dnd.NPCInteractionList) error {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package npcs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/candidatejson"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||
)
|
||||
|
||||
@@ -60,11 +58,7 @@ func (c *Codec) Encode(value dnd.NPCList) ([]byte, error) {
|
||||
// EncodeCandidate provides the durable representation before semantic
|
||||
// validators have approved a value.
|
||||
func (c *Codec) EncodeCandidate(value dnd.NPCList) ([]byte, error) {
|
||||
content, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode dnd npc list: %w", err)
|
||||
}
|
||||
return content, nil
|
||||
return candidatejson.EncodeCandidate("dnd npc list", value)
|
||||
}
|
||||
|
||||
func (c *Codec) Decode(content []byte) (dnd.NPCList, error) {
|
||||
@@ -81,17 +75,7 @@ func (c *Codec) Decode(content []byte) (dnd.NPCList, error) {
|
||||
// DecodeCandidate reads one strict durable JSON value before semantic
|
||||
// validators have approved it.
|
||||
func (c *Codec) DecodeCandidate(content []byte) (dnd.NPCList, error) {
|
||||
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||
decoder.DisallowUnknownFields()
|
||||
var value dnd.NPCList
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
return dnd.NPCList{}, fmt.Errorf("decode dnd npc list: %w", err)
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
return dnd.NPCList{}, fmt.Errorf("decode dnd npc list: multiple JSON values")
|
||||
}
|
||||
return value, nil
|
||||
return candidatejson.DecodeCandidate[dnd.NPCList]("dnd npc list", content)
|
||||
}
|
||||
|
||||
func validate(value dnd.NPCList) error {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package scenedescriptions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/candidatejson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -59,11 +57,7 @@ func (c *Codec) Encode(value dnd.SceneDescriptionList) ([]byte, error) {
|
||||
// EncodeCandidate provides the durable representation before typed validators
|
||||
// have approved a value.
|
||||
func (c *Codec) EncodeCandidate(value dnd.SceneDescriptionList) ([]byte, error) {
|
||||
content, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode dnd scene description list: %w", err)
|
||||
}
|
||||
return content, nil
|
||||
return candidatejson.EncodeCandidate("dnd scene description list", value)
|
||||
}
|
||||
|
||||
func (c *Codec) Decode(content []byte) (dnd.SceneDescriptionList, error) {
|
||||
@@ -80,17 +74,7 @@ func (c *Codec) Decode(content []byte) (dnd.SceneDescriptionList, error) {
|
||||
// DecodeCandidate reads one strict durable JSON value before typed validators
|
||||
// have approved it.
|
||||
func (c *Codec) DecodeCandidate(content []byte) (dnd.SceneDescriptionList, error) {
|
||||
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||
decoder.DisallowUnknownFields()
|
||||
var value dnd.SceneDescriptionList
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
return dnd.SceneDescriptionList{}, fmt.Errorf("decode dnd scene description list: %w", err)
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
return dnd.SceneDescriptionList{}, fmt.Errorf("decode dnd scene description list: multiple JSON values")
|
||||
}
|
||||
return value, nil
|
||||
return candidatejson.DecodeCandidate[dnd.SceneDescriptionList]("dnd scene description list", content)
|
||||
}
|
||||
|
||||
func validate(value dnd.SceneDescriptionList) error {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package spells
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/candidatejson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,11 +52,7 @@ func (c *Codec) Encode(value dnd.SpellList) ([]byte, error) {
|
||||
// EncodeCandidate provides the same stable representation before typed
|
||||
// validators have approved a value.
|
||||
func (c *Codec) EncodeCandidate(value dnd.SpellList) ([]byte, error) {
|
||||
content, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode dnd spell list: %w", err)
|
||||
}
|
||||
return content, nil
|
||||
return candidatejson.EncodeCandidate("dnd spell list", value)
|
||||
}
|
||||
|
||||
func (c *Codec) Decode(content []byte) (dnd.SpellList, error) {
|
||||
@@ -75,17 +69,7 @@ func (c *Codec) Decode(content []byte) (dnd.SpellList, error) {
|
||||
// DecodeCandidate reads the durable representation before semantic validators
|
||||
// have approved it.
|
||||
func (c *Codec) DecodeCandidate(content []byte) (dnd.SpellList, error) {
|
||||
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||
decoder.DisallowUnknownFields()
|
||||
var value dnd.SpellList
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
return dnd.SpellList{}, fmt.Errorf("decode dnd spell list: %w", err)
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
return dnd.SpellList{}, fmt.Errorf("decode dnd spell list: multiple JSON values")
|
||||
}
|
||||
return value, nil
|
||||
return candidatejson.DecodeCandidate[dnd.SpellList]("dnd spell list", content)
|
||||
}
|
||||
|
||||
func validate(value dnd.SpellList) error {
|
||||
|
||||
Reference in New Issue
Block a user