// Package enemyevents encodes durable D&D enemy-event artifacts. package enemyevents import ( "embed" "encoding/json" "fmt" "gitea.maximumdirect.net/eric/notarius/internal/core/source" "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 ( SchemaID = "notarius.dnd.enemy_events" SchemaName = "notarius_dnd_enemy_events_v1" SchemaVersion = "v1" MediaType = "application/json" ) //go:embed assets/schemas/dnd_enemy_events.v1.json var schemaAssets embed.FS var _ contracts.ArtifactCodec[dnd.EnemyEventList] = (*Codec)(nil) var _ contracts.CandidateArtifactCodec[dnd.EnemyEventList] = (*Codec)(nil) type Codec struct{} func New() *Codec { return &Codec{} } func (c *Codec) Kind() contracts.ArtifactKind { return dnd.EnemyEventListKind } func (c *Codec) Schema() contracts.ArtifactSchema { raw, err := schemaAssets.ReadFile("assets/schemas/dnd_enemy_events.v1.json") if err != nil { return contracts.ArtifactSchema{} } return contracts.ArtifactSchema{ ID: SchemaID, Name: SchemaName, Version: SchemaVersion, JSONSchema: append([]byte(nil), raw...), } } func (c *Codec) MediaType() string { return MediaType } func (c *Codec) Metadata(value dnd.EnemyEventList) map[string]any { return map[string]any{"event_count": len(value.Events)} } func (c *Codec) Encode(value dnd.EnemyEventList) ([]byte, error) { if err := validateRequiredValueFields(value); err != nil { return nil, fmt.Errorf("encode dnd enemy event list: %w", err) } return c.EncodeCandidate(value) } // EncodeCandidate provides the durable representation before semantic // validators have approved a value. func (c *Codec) EncodeCandidate(value dnd.EnemyEventList) ([]byte, error) { return candidatejson.EncodeCandidate("dnd enemy event list", cloneList(value)) } func (c *Codec) Decode(content []byte) (dnd.EnemyEventList, error) { value, err := c.DecodeCandidate(content) if err != nil { return dnd.EnemyEventList{}, err } if err := validateRequiredJSONFields(content); err != nil { return dnd.EnemyEventList{}, fmt.Errorf("decode dnd enemy event list: %w", err) } return value, nil } // DecodeCandidate reads one strict durable JSON value before semantic // validators have approved it. func (c *Codec) DecodeCandidate(content []byte) (dnd.EnemyEventList, error) { value, err := candidatejson.DecodeCandidate[dnd.EnemyEventList]("dnd enemy event list", content) if err != nil { return dnd.EnemyEventList{}, err } return cloneList(value), nil } func validateRequiredValueFields(value dnd.EnemyEventList) error { if value.Events == nil { return fmt.Errorf("events must be present") } for index, event := range value.Events { if event.SourceRefs == nil { return fmt.Errorf("events[%d].source_refs must be present", index) } } return nil } func validateRequiredJSONFields(content []byte) error { var root map[string]json.RawMessage if err := json.Unmarshal(content, &root); err != nil || root == nil { return fmt.Errorf("must be a JSON object") } events, err := requiredArray(root, "events", "") if err != nil { return err } for eventIndex, rawEvent := range events { path := fmt.Sprintf("events[%d]", eventIndex) var event map[string]json.RawMessage if err := json.Unmarshal(rawEvent, &event); err != nil || event == nil { return fmt.Errorf("%s must be an object", path) } for _, field := range []string{"name", "kind"} { if err := requireField(event, field, path); err != nil { return err } } refs, err := requiredArray(event, "source_refs", path) if err != nil { return err } for refIndex, rawRef := range refs { refPath := fmt.Sprintf("%s.source_refs[%d]", path, refIndex) var ref map[string]json.RawMessage if err := json.Unmarshal(rawRef, &ref); err != nil || ref == nil { return fmt.Errorf("%s must be an object", refPath) } for _, field := range []string{"source_id", "start_unit_id", "end_unit_id"} { if err := requireField(ref, field, refPath); err != nil { return err } } } } return nil } func requiredArray(object map[string]json.RawMessage, field, path string) ([]json.RawMessage, error) { raw, err := requiredField(object, field, path) if err != nil { return nil, err } var values []json.RawMessage if err := json.Unmarshal(raw, &values); err != nil { return nil, fmt.Errorf("%s must be an array", fieldPath(path, field)) } return values, nil } func requireField(object map[string]json.RawMessage, field, path string) error { _, err := requiredField(object, field, path) return err } func requiredField(object map[string]json.RawMessage, field, path string) (json.RawMessage, error) { raw, ok := object[field] if !ok || string(raw) == "null" { return nil, fmt.Errorf("%s must be present", fieldPath(path, field)) } return raw, nil } func fieldPath(path, field string) string { if path == "" { return field } return path + "." + field } func cloneList(value dnd.EnemyEventList) dnd.EnemyEventList { if value.Events == nil { return dnd.EnemyEventList{} } cloned := dnd.EnemyEventList{Events: make([]dnd.EnemyEvent, len(value.Events))} for index, event := range value.Events { cloned.Events[index] = event if event.SourceRefs != nil { cloned.Events[index].SourceRefs = append([]source.SourceRef(nil), event.SourceRefs...) } } return cloned }