131 lines
3.9 KiB
Go
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
|
|
}
|