12 // Schema is the supported envelope and payload schema version.
15 // Metadata holds message identity and optional UTC times. Retries keep the same
16 // ID. A receiver checks ExpiresAt against its clock before processing.
17 type Metadata struct {
18 MessageID string `json:"message_id"`
19 SentAt *time.Time `json:"sent_at,omitempty"`
20 ExpiresAt *time.Time `json:"expires_at,omitempty"`
21 CorrelationID string `json:"correlation_id,omitempty"`
22 CausationID string `json:"causation_id,omitempty"`
25 // wireEnvelope carries one JSON object and its schema and metadata.
26 type wireEnvelope struct {
27 Schema int `json:"schema"`
29 Payload json.RawMessage `json:"payload"`
32 // encodeEnvelope validates metadata and encodes a schema-1 object payload.
33 func encodeEnvelope(metadata Metadata, payload any) ([]byte, error) {
34 encoded, err := json.Marshal(payload)
36 return nil, fmt.Errorf("encode payload: %w", err)
39 envelope := wireEnvelope{
44 err = validateEnvelope(envelope)
46 return nil, fmt.Errorf("validate envelope: %w", err)
49 encoded, err = json.Marshal(envelope)
51 return nil, fmt.Errorf("encode envelope: %w", err)
57 // parseEnvelope rejects unknown fields, trailing JSON, unsupported schemas,
58 // invalid IDs, non-UTC times, and missing or non-object payloads.
59 func parseEnvelope(data []byte) (wireEnvelope, error) {
60 var parsed wireEnvelope
62 err := decodeObject(data, &parsed)
64 return wireEnvelope{}, fmt.Errorf("decode envelope: %w", err)
67 err = validateEnvelope(parsed)
69 return wireEnvelope{}, fmt.Errorf("validate envelope: %w", err)
75 func validateEnvelope(envelope wireEnvelope) error {
76 if envelope.Schema != Schema {
77 return fmt.Errorf("unsupported schema %d", envelope.Schema)
80 if !ValidID(envelope.MessageID) {
81 return fmt.Errorf("message ID must be a canonical ULID")
84 if envelope.CorrelationID != "" && !ValidID(envelope.CorrelationID) {
85 return fmt.Errorf("correlation ID must be a canonical ULID")
88 if envelope.CausationID != "" && !ValidID(envelope.CausationID) {
89 return fmt.Errorf("causation ID must be a canonical ULID")
92 for _, timestamp := range []*time.Time{envelope.SentAt, envelope.ExpiresAt} {
94 _, offset := timestamp.Zone()
96 return fmt.Errorf("message times must use UTC")
101 if !isObject(envelope.Payload) {
102 return fmt.Errorf("payload must be a JSON object")
108 func decodeObject(data []byte, target any) error {
110 return fmt.Errorf("expected one JSON object")
113 decoder := json.NewDecoder(bytes.NewReader(data))
114 decoder.DisallowUnknownFields()
116 err := decoder.Decode(target)
118 return fmt.Errorf("decode object: %w", err)
123 err = decoder.Decode(&extra)
124 if !errors.Is(err, io.EOF) {
125 return fmt.Errorf("expected end of JSON object")
131 func isObject(data []byte) bool {
132 data = bytes.TrimSpace(data)
133 return len(data) > 0 && data[0] == '{' && json.Valid(data)