Files
galaxy-game/mail/internal/domain/malformedcommand/model.go
T
2026-04-17 18:39:16 +02:00

131 lines
3.9 KiB
Go

// Package malformedcommand defines the operator-visible record used for
// malformed asynchronous generic delivery commands.
package malformedcommand
import (
"encoding/json"
"fmt"
"strings"
"time"
"galaxy/mail/internal/domain/common"
)
// FailureCode identifies the stable malformed-command rejection reason.
type FailureCode string
const (
// FailureCodeInvalidEnvelope reports that the command could not be accepted
// because the recipient envelope was invalid.
FailureCodeInvalidEnvelope FailureCode = "invalid_envelope"
// FailureCodeInvalidPayload reports that the command payload could not be
// decoded or validated.
FailureCodeInvalidPayload FailureCode = "invalid_payload"
// FailureCodeInvalidCommand reports that the top-level stream envelope was
// malformed or unsupported.
FailureCodeInvalidCommand FailureCode = "invalid_command"
// FailureCodeIdempotencyConflict reports that the stream command reused an
// existing idempotency scope with a different request fingerprint.
FailureCodeIdempotencyConflict FailureCode = "idempotency_conflict"
)
// IsKnown reports whether code belongs to the frozen malformed-command
// rejection surface.
func (code FailureCode) IsKnown() bool {
switch code {
case FailureCodeInvalidEnvelope,
FailureCodeInvalidPayload,
FailureCodeInvalidCommand,
FailureCodeIdempotencyConflict:
return true
default:
return false
}
}
// Entry stores one operator-visible malformed asynchronous command record.
type Entry struct {
// StreamEntryID stores the Redis Stream entry identifier of the malformed
// command.
StreamEntryID string
// DeliveryID stores the optional raw delivery identifier extracted from the
// stream entry when available.
DeliveryID string
// Source stores the optional raw source value extracted from the stream
// entry when available.
Source string
// IdempotencyKey stores the optional raw idempotency key extracted from the
// stream entry when available.
IdempotencyKey string
// FailureCode stores the stable malformed-command rejection reason.
FailureCode FailureCode
// FailureMessage stores the detailed validation or decoding failure.
FailureMessage string
// RawFields stores the raw top-level stream fields captured for later
// operator inspection.
RawFields map[string]any
// RecordedAt stores when the malformed command was durably recorded.
RecordedAt time.Time
}
// Validate reports whether entry contains a complete malformed-command record.
func (entry Entry) Validate() error {
if strings.TrimSpace(entry.StreamEntryID) == "" {
return fmt.Errorf("malformed command stream entry id must not be empty")
}
if !entry.FailureCode.IsKnown() {
return fmt.Errorf("malformed command failure code %q is unsupported", entry.FailureCode)
}
if strings.TrimSpace(entry.FailureMessage) == "" {
return fmt.Errorf("malformed command failure message must not be empty")
}
if strings.TrimSpace(entry.FailureMessage) != entry.FailureMessage {
return fmt.Errorf("malformed command failure message must not contain surrounding whitespace")
}
if entry.RawFields == nil {
return fmt.Errorf("malformed command raw fields must not be nil")
}
if err := validateJSONObject("malformed command raw fields", entry.RawFields); err != nil {
return err
}
if err := common.ValidateTimestamp("malformed command recorded at", entry.RecordedAt); err != nil {
return err
}
return nil
}
func validateJSONObject(name string, value map[string]any) error {
if value == nil {
return fmt.Errorf("%s must not be nil", name)
}
payload, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("%s: %w", name, err)
}
if string(payload) == "null" {
return fmt.Errorf("%s must encode as a JSON object", name)
}
var decoded map[string]any
if err := json.Unmarshal(payload, &decoded); err != nil {
return fmt.Errorf("%s: %w", name, err)
}
if decoded == nil {
return fmt.Errorf("%s must encode as a JSON object", name)
}
return nil
}