package queue import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "strings" "time" ) var ErrInvalidEnvelope = errors.New("invalid queue envelope") type JSONSerializer struct { version string enableLegacyFallback bool queueType string messageGroupID string } type LegacyJSONSerializer struct{} func NewJSONSerializer(version string, enableLegacyFallback bool, queueType, messageGroupID string) *JSONSerializer { version = strings.TrimSpace(version) if version == "" { version = "v1" } queueType = strings.ToLower(strings.TrimSpace(queueType)) if queueType == "" { queueType = QueueTypeStandard } return &JSONSerializer{ version: version, enableLegacyFallback: enableLegacyFallback, queueType: queueType, messageGroupID: strings.TrimSpace(messageGroupID), } } func (s *JSONSerializer) Serialize(ctx context.Context, job EmailJob) (*OutboundMessage, error) { envelope, err := NewEnvelope(ctx, s.version, job) if err != nil { return nil, err } body, err := json.Marshal(envelope) if err != nil { return nil, err } msg := &OutboundMessage{ ID: envelope.MessageID, Body: body, Attributes: map[string]string{ "message_kind": envelope.Kind, "message_id": envelope.MessageID, "correlation_id": envelope.CorrelationID, "trace_id": envelope.TraceID, "schema_version": envelope.Version, "job_type": envelope.Payload.NormalizedType(), }, } if s.queueType == QueueTypeFIFO { msg.MessageGroupID = s.messageGroupID if msg.MessageGroupID == "" { msg.MessageGroupID = "default" } msg.DeduplicationID = envelope.DeduplicationKey() } return msg, nil } func (s *JSONSerializer) Deserialize(body []byte, attributes map[string]string) (*Envelope, error) { var envelope Envelope if err := json.Unmarshal(body, &envelope); err == nil { if err := validateEnvelope(&envelope); err == nil { return &envelope, nil } } if !s.enableLegacyFallback { return nil, ErrInvalidEnvelope } var job EmailJob if err := json.Unmarshal(body, &job); err != nil { return nil, errors.Join(ErrInvalidEnvelope, err) } job = job.Canonical() if err := job.Validate(); err != nil { return nil, errors.Join(ErrInvalidEnvelope, err) } id := messageHash(body) correlationID := strings.TrimSpace(attributes["correlation_id"]) if correlationID == "" { correlationID = id } traceID := strings.TrimSpace(attributes["trace_id"]) if traceID == "" { traceID = correlationID } return &Envelope{ Version: "legacy", MessageID: id, Kind: EmailKind, OccurredAt: time.Now().UTC(), CorrelationID: correlationID, TraceID: traceID, IdempotencyKey: id, Payload: job, }, nil } func (LegacyJSONSerializer) Serialize(ctx context.Context, job EmailJob) (*OutboundMessage, error) { job = job.Canonical() if err := job.Validate(); err != nil { return nil, err } body, err := json.Marshal(job) if err != nil { return nil, err } id := messageHash(body) correlationID := CorrelationIDFromContext(ctx) if correlationID == "" { correlationID = id } traceID := TraceIDFromContext(ctx) if traceID == "" { traceID = correlationID } return &OutboundMessage{ ID: id, Body: body, Attributes: map[string]string{ "message_kind": EmailKind, "message_id": id, "correlation_id": correlationID, "trace_id": traceID, "schema_version": "legacy", "job_type": job.NormalizedType(), }, }, nil } func (LegacyJSONSerializer) Deserialize(body []byte, attributes map[string]string) (*Envelope, error) { return NewJSONSerializer("v1", true, QueueTypeStandard, "").Deserialize(body, attributes) } func validateEnvelope(envelope *Envelope) error { if envelope == nil { return ErrInvalidEnvelope } if strings.TrimSpace(envelope.Version) == "" { return errors.Join(ErrInvalidEnvelope, errors.New("version is required")) } if strings.TrimSpace(envelope.MessageID) == "" { return errors.Join(ErrInvalidEnvelope, errors.New("message id is required")) } if strings.TrimSpace(envelope.Kind) == "" { return errors.Join(ErrInvalidEnvelope, errors.New("kind is required")) } if envelope.OccurredAt.IsZero() { return errors.Join(ErrInvalidEnvelope, errors.New("occurred at is required")) } if err := envelope.Payload.Validate(); err != nil { return errors.Join(ErrInvalidEnvelope, err) } envelope.Payload = envelope.Payload.Canonical() if strings.TrimSpace(envelope.CorrelationID) == "" { envelope.CorrelationID = envelope.MessageID } if strings.TrimSpace(envelope.TraceID) == "" { envelope.TraceID = envelope.CorrelationID } if strings.TrimSpace(envelope.IdempotencyKey) == "" { envelope.IdempotencyKey = envelope.MessageID } return nil } func messageHash(body []byte) string { sum := sha256.Sum256(body) return hex.EncodeToString(sum[:16]) }