init push
This commit is contained in:
185
internal/queue/serializer.go
Normal file
185
internal/queue/serializer.go
Normal file
@@ -0,0 +1,185 @@
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user