698 lines
23 KiB
Go
698 lines
23 KiB
Go
package redisstate
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"galaxy/mail/internal/domain/attempt"
|
|
"galaxy/mail/internal/domain/common"
|
|
deliverydomain "galaxy/mail/internal/domain/delivery"
|
|
"galaxy/mail/internal/domain/idempotency"
|
|
"galaxy/mail/internal/domain/malformedcommand"
|
|
"galaxy/mail/internal/service/acceptgenericdelivery"
|
|
)
|
|
|
|
type deliveryRecord struct {
|
|
DeliveryID string `json:"delivery_id"`
|
|
ResendParentDeliveryID string `json:"resend_parent_delivery_id,omitempty"`
|
|
Source deliverydomain.Source `json:"source"`
|
|
PayloadMode deliverydomain.PayloadMode `json:"payload_mode"`
|
|
TemplateID string `json:"template_id,omitempty"`
|
|
TemplateVariables *map[string]any `json:"template_variables,omitempty"`
|
|
To []string `json:"to"`
|
|
Cc []string `json:"cc"`
|
|
Bcc []string `json:"bcc"`
|
|
ReplyTo []string `json:"reply_to"`
|
|
Subject string `json:"subject,omitempty"`
|
|
TextBody string `json:"text_body,omitempty"`
|
|
HTMLBody string `json:"html_body,omitempty"`
|
|
Attachments []attachmentRecord `json:"attachments"`
|
|
Locale string `json:"locale,omitempty"`
|
|
LocaleFallbackUsed bool `json:"locale_fallback_used"`
|
|
IdempotencyKey string `json:"idempotency_key"`
|
|
Status deliverydomain.Status `json:"status"`
|
|
AttemptCount int `json:"attempt_count"`
|
|
LastAttemptStatus attempt.Status `json:"last_attempt_status,omitempty"`
|
|
ProviderSummary string `json:"provider_summary,omitempty"`
|
|
CreatedAtMS int64 `json:"created_at_ms"`
|
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
|
SentAtMS *int64 `json:"sent_at_ms,omitempty"`
|
|
SuppressedAtMS *int64 `json:"suppressed_at_ms,omitempty"`
|
|
FailedAtMS *int64 `json:"failed_at_ms,omitempty"`
|
|
DeadLetteredAtMS *int64 `json:"dead_lettered_at_ms,omitempty"`
|
|
}
|
|
|
|
type attemptRecord struct {
|
|
DeliveryID string `json:"delivery_id"`
|
|
AttemptNo int `json:"attempt_no"`
|
|
ScheduledForMS int64 `json:"scheduled_for_ms"`
|
|
StartedAtMS *int64 `json:"started_at_ms,omitempty"`
|
|
FinishedAtMS *int64 `json:"finished_at_ms,omitempty"`
|
|
Status attempt.Status `json:"status"`
|
|
ProviderClassification string `json:"provider_classification,omitempty"`
|
|
ProviderSummary string `json:"provider_summary,omitempty"`
|
|
}
|
|
|
|
type idempotencyRecord struct {
|
|
Source deliverydomain.Source `json:"source"`
|
|
IdempotencyKey string `json:"idempotency_key"`
|
|
DeliveryID string `json:"delivery_id"`
|
|
RequestFingerprint string `json:"request_fingerprint"`
|
|
CreatedAtMS int64 `json:"created_at_ms"`
|
|
ExpiresAtMS int64 `json:"expires_at_ms"`
|
|
}
|
|
|
|
type deadLetterRecord struct {
|
|
DeliveryID string `json:"delivery_id"`
|
|
FinalAttemptNo int `json:"final_attempt_no"`
|
|
FailureClassification string `json:"failure_classification"`
|
|
ProviderSummary string `json:"provider_summary,omitempty"`
|
|
CreatedAtMS int64 `json:"created_at_ms"`
|
|
RecoveryHint string `json:"recovery_hint,omitempty"`
|
|
}
|
|
|
|
type deliveryPayloadRecord struct {
|
|
DeliveryID string `json:"delivery_id"`
|
|
Attachments []deliveryPayloadAttachmentRecord `json:"attachments"`
|
|
}
|
|
|
|
type deliveryPayloadAttachmentRecord struct {
|
|
Filename string `json:"filename"`
|
|
ContentType string `json:"content_type"`
|
|
ContentBase64 string `json:"content_base64"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
}
|
|
|
|
type malformedCommandRecord struct {
|
|
StreamEntryID string `json:"stream_entry_id"`
|
|
DeliveryID string `json:"delivery_id,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
|
FailureCode malformedcommand.FailureCode `json:"failure_code"`
|
|
FailureMessage string `json:"failure_message"`
|
|
RawFieldsJSON map[string]any `json:"raw_fields_json"`
|
|
RecordedAtMS int64 `json:"recorded_at_ms"`
|
|
}
|
|
|
|
type streamOffsetRecord struct {
|
|
Stream string `json:"stream"`
|
|
LastProcessedEntryID string `json:"last_processed_entry_id"`
|
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
|
}
|
|
|
|
// StreamOffset stores the persisted progress of one plain-XREAD consumer.
|
|
type StreamOffset struct {
|
|
// Stream stores the Redis Stream name.
|
|
Stream string
|
|
|
|
// LastProcessedEntryID stores the last durably processed entry id.
|
|
LastProcessedEntryID string
|
|
|
|
// UpdatedAt stores when the offset was updated.
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// Validate reports whether offset contains a complete persisted progress
|
|
// record.
|
|
func (offset StreamOffset) Validate() error {
|
|
if strings.TrimSpace(offset.Stream) == "" {
|
|
return fmt.Errorf("stream offset stream must not be empty")
|
|
}
|
|
if strings.TrimSpace(offset.LastProcessedEntryID) == "" {
|
|
return fmt.Errorf("stream offset last processed entry id must not be empty")
|
|
}
|
|
if err := common.ValidateTimestamp("stream offset updated at", offset.UpdatedAt); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type attachmentRecord struct {
|
|
Filename string `json:"filename"`
|
|
ContentType string `json:"content_type"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
}
|
|
|
|
// MarshalDelivery encodes record into the strict Redis JSON shape used for
|
|
// mail_delivery records.
|
|
func MarshalDelivery(record deliverydomain.Delivery) ([]byte, error) {
|
|
if err := record.Validate(); err != nil {
|
|
return nil, fmt.Errorf("marshal redis delivery record: %w", err)
|
|
}
|
|
|
|
stored := deliveryRecord{
|
|
DeliveryID: record.DeliveryID.String(),
|
|
ResendParentDeliveryID: record.ResendParentDeliveryID.String(),
|
|
Source: record.Source,
|
|
PayloadMode: record.PayloadMode,
|
|
TemplateID: record.TemplateID.String(),
|
|
TemplateVariables: optionalJSONObject(record.TemplateVariables),
|
|
To: cloneEmailStrings(record.Envelope.To),
|
|
Cc: cloneEmailStrings(record.Envelope.Cc),
|
|
Bcc: cloneEmailStrings(record.Envelope.Bcc),
|
|
ReplyTo: cloneEmailStrings(record.Envelope.ReplyTo),
|
|
Subject: record.Content.Subject,
|
|
TextBody: record.Content.TextBody,
|
|
HTMLBody: record.Content.HTMLBody,
|
|
Attachments: cloneAttachments(record.Attachments),
|
|
Locale: record.Locale.String(),
|
|
LocaleFallbackUsed: record.LocaleFallbackUsed,
|
|
IdempotencyKey: record.IdempotencyKey.String(),
|
|
Status: record.Status,
|
|
AttemptCount: record.AttemptCount,
|
|
LastAttemptStatus: record.LastAttemptStatus,
|
|
ProviderSummary: record.ProviderSummary,
|
|
CreatedAtMS: record.CreatedAt.UTC().UnixMilli(),
|
|
UpdatedAtMS: record.UpdatedAt.UTC().UnixMilli(),
|
|
SentAtMS: optionalUnixMilli(record.SentAt),
|
|
SuppressedAtMS: optionalUnixMilli(record.SuppressedAt),
|
|
FailedAtMS: optionalUnixMilli(record.FailedAt),
|
|
DeadLetteredAtMS: optionalUnixMilli(record.DeadLetteredAt),
|
|
}
|
|
|
|
payload, err := json.Marshal(stored)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal redis delivery record: %w", err)
|
|
}
|
|
|
|
return payload, nil
|
|
}
|
|
|
|
// UnmarshalDelivery decodes payload from the strict Redis JSON shape used for
|
|
// mail_delivery records.
|
|
func UnmarshalDelivery(payload []byte) (deliverydomain.Delivery, error) {
|
|
var stored deliveryRecord
|
|
if err := decodeStrictJSON("decode redis delivery record", payload, &stored); err != nil {
|
|
return deliverydomain.Delivery{}, err
|
|
}
|
|
|
|
record := deliverydomain.Delivery{
|
|
DeliveryID: common.DeliveryID(stored.DeliveryID),
|
|
ResendParentDeliveryID: common.DeliveryID(stored.ResendParentDeliveryID),
|
|
Source: stored.Source,
|
|
PayloadMode: stored.PayloadMode,
|
|
TemplateID: common.TemplateID(stored.TemplateID),
|
|
TemplateVariables: cloneJSONObjectPtr(stored.TemplateVariables),
|
|
Envelope: deliverydomain.Envelope{
|
|
To: cloneEmails(stored.To),
|
|
Cc: cloneEmails(stored.Cc),
|
|
Bcc: cloneEmails(stored.Bcc),
|
|
ReplyTo: cloneEmails(stored.ReplyTo),
|
|
},
|
|
Content: deliverydomain.Content{
|
|
Subject: stored.Subject,
|
|
TextBody: stored.TextBody,
|
|
HTMLBody: stored.HTMLBody,
|
|
},
|
|
Attachments: inflateAttachments(stored.Attachments),
|
|
Locale: common.Locale(stored.Locale),
|
|
LocaleFallbackUsed: stored.LocaleFallbackUsed,
|
|
IdempotencyKey: common.IdempotencyKey(stored.IdempotencyKey),
|
|
Status: stored.Status,
|
|
AttemptCount: stored.AttemptCount,
|
|
LastAttemptStatus: stored.LastAttemptStatus,
|
|
ProviderSummary: stored.ProviderSummary,
|
|
CreatedAt: time.UnixMilli(stored.CreatedAtMS).UTC(),
|
|
UpdatedAt: time.UnixMilli(stored.UpdatedAtMS).UTC(),
|
|
SentAt: inflateOptionalTime(stored.SentAtMS),
|
|
SuppressedAt: inflateOptionalTime(stored.SuppressedAtMS),
|
|
FailedAt: inflateOptionalTime(stored.FailedAtMS),
|
|
DeadLetteredAt: inflateOptionalTime(stored.DeadLetteredAtMS),
|
|
}
|
|
if err := record.Validate(); err != nil {
|
|
return deliverydomain.Delivery{}, fmt.Errorf("decode redis delivery record: %w", err)
|
|
}
|
|
|
|
return record, nil
|
|
}
|
|
|
|
// MarshalAttempt encodes record into the strict Redis JSON shape used for
|
|
// mail_attempt records.
|
|
func MarshalAttempt(record attempt.Attempt) ([]byte, error) {
|
|
if err := record.Validate(); err != nil {
|
|
return nil, fmt.Errorf("marshal redis attempt record: %w", err)
|
|
}
|
|
|
|
stored := attemptRecord{
|
|
DeliveryID: record.DeliveryID.String(),
|
|
AttemptNo: record.AttemptNo,
|
|
ScheduledForMS: record.ScheduledFor.UTC().UnixMilli(),
|
|
StartedAtMS: optionalUnixMilli(record.StartedAt),
|
|
FinishedAtMS: optionalUnixMilli(record.FinishedAt),
|
|
Status: record.Status,
|
|
ProviderClassification: record.ProviderClassification,
|
|
ProviderSummary: record.ProviderSummary,
|
|
}
|
|
|
|
payload, err := json.Marshal(stored)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal redis attempt record: %w", err)
|
|
}
|
|
|
|
return payload, nil
|
|
}
|
|
|
|
// UnmarshalAttempt decodes payload from the strict Redis JSON shape used for
|
|
// mail_attempt records.
|
|
func UnmarshalAttempt(payload []byte) (attempt.Attempt, error) {
|
|
var stored attemptRecord
|
|
if err := decodeStrictJSON("decode redis attempt record", payload, &stored); err != nil {
|
|
return attempt.Attempt{}, err
|
|
}
|
|
|
|
record := attempt.Attempt{
|
|
DeliveryID: common.DeliveryID(stored.DeliveryID),
|
|
AttemptNo: stored.AttemptNo,
|
|
ScheduledFor: time.UnixMilli(stored.ScheduledForMS).UTC(),
|
|
StartedAt: inflateOptionalTime(stored.StartedAtMS),
|
|
FinishedAt: inflateOptionalTime(stored.FinishedAtMS),
|
|
Status: stored.Status,
|
|
ProviderClassification: stored.ProviderClassification,
|
|
ProviderSummary: stored.ProviderSummary,
|
|
}
|
|
if err := record.Validate(); err != nil {
|
|
return attempt.Attempt{}, fmt.Errorf("decode redis attempt record: %w", err)
|
|
}
|
|
|
|
return record, nil
|
|
}
|
|
|
|
// MarshalIdempotency encodes record into the strict Redis JSON shape used for
|
|
// mail_idempotency_record values.
|
|
func MarshalIdempotency(record idempotency.Record) ([]byte, error) {
|
|
if err := record.Validate(); err != nil {
|
|
return nil, fmt.Errorf("marshal redis idempotency record: %w", err)
|
|
}
|
|
|
|
stored := idempotencyRecord{
|
|
Source: record.Source,
|
|
IdempotencyKey: record.IdempotencyKey.String(),
|
|
DeliveryID: record.DeliveryID.String(),
|
|
RequestFingerprint: record.RequestFingerprint,
|
|
CreatedAtMS: record.CreatedAt.UTC().UnixMilli(),
|
|
ExpiresAtMS: record.ExpiresAt.UTC().UnixMilli(),
|
|
}
|
|
|
|
payload, err := json.Marshal(stored)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal redis idempotency record: %w", err)
|
|
}
|
|
|
|
return payload, nil
|
|
}
|
|
|
|
// UnmarshalIdempotency decodes payload from the strict Redis JSON shape used
|
|
// for mail_idempotency_record values.
|
|
func UnmarshalIdempotency(payload []byte) (idempotency.Record, error) {
|
|
var stored idempotencyRecord
|
|
if err := decodeStrictJSON("decode redis idempotency record", payload, &stored); err != nil {
|
|
return idempotency.Record{}, err
|
|
}
|
|
|
|
record := idempotency.Record{
|
|
Source: stored.Source,
|
|
IdempotencyKey: common.IdempotencyKey(stored.IdempotencyKey),
|
|
DeliveryID: common.DeliveryID(stored.DeliveryID),
|
|
RequestFingerprint: stored.RequestFingerprint,
|
|
CreatedAt: time.UnixMilli(stored.CreatedAtMS).UTC(),
|
|
ExpiresAt: time.UnixMilli(stored.ExpiresAtMS).UTC(),
|
|
}
|
|
if err := record.Validate(); err != nil {
|
|
return idempotency.Record{}, fmt.Errorf("decode redis idempotency record: %w", err)
|
|
}
|
|
|
|
return record, nil
|
|
}
|
|
|
|
// MarshalDeadLetter encodes entry into the strict Redis JSON shape used for
|
|
// mail_dead_letter_entry values.
|
|
func MarshalDeadLetter(entry deliverydomain.DeadLetterEntry) ([]byte, error) {
|
|
if err := entry.Validate(); err != nil {
|
|
return nil, fmt.Errorf("marshal redis dead-letter record: %w", err)
|
|
}
|
|
|
|
stored := deadLetterRecord{
|
|
DeliveryID: entry.DeliveryID.String(),
|
|
FinalAttemptNo: entry.FinalAttemptNo,
|
|
FailureClassification: entry.FailureClassification,
|
|
ProviderSummary: entry.ProviderSummary,
|
|
CreatedAtMS: entry.CreatedAt.UTC().UnixMilli(),
|
|
RecoveryHint: entry.RecoveryHint,
|
|
}
|
|
|
|
payload, err := json.Marshal(stored)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal redis dead-letter record: %w", err)
|
|
}
|
|
|
|
return payload, nil
|
|
}
|
|
|
|
// UnmarshalDeadLetter decodes payload from the strict Redis JSON shape used
|
|
// for mail_dead_letter_entry values.
|
|
func UnmarshalDeadLetter(payload []byte) (deliverydomain.DeadLetterEntry, error) {
|
|
var stored deadLetterRecord
|
|
if err := decodeStrictJSON("decode redis dead-letter record", payload, &stored); err != nil {
|
|
return deliverydomain.DeadLetterEntry{}, err
|
|
}
|
|
|
|
entry := deliverydomain.DeadLetterEntry{
|
|
DeliveryID: common.DeliveryID(stored.DeliveryID),
|
|
FinalAttemptNo: stored.FinalAttemptNo,
|
|
FailureClassification: stored.FailureClassification,
|
|
ProviderSummary: stored.ProviderSummary,
|
|
CreatedAt: time.UnixMilli(stored.CreatedAtMS).UTC(),
|
|
RecoveryHint: stored.RecoveryHint,
|
|
}
|
|
if err := entry.Validate(); err != nil {
|
|
return deliverydomain.DeadLetterEntry{}, fmt.Errorf("decode redis dead-letter record: %w", err)
|
|
}
|
|
|
|
return entry, nil
|
|
}
|
|
|
|
// MarshalDeliveryPayload encodes payload into the strict Redis JSON shape used
|
|
// for raw generic-delivery attachment bundles.
|
|
func MarshalDeliveryPayload(payload acceptgenericdelivery.DeliveryPayload) ([]byte, error) {
|
|
if err := payload.Validate(); err != nil {
|
|
return nil, fmt.Errorf("marshal redis delivery payload record: %w", err)
|
|
}
|
|
|
|
stored := deliveryPayloadRecord{
|
|
DeliveryID: payload.DeliveryID.String(),
|
|
Attachments: cloneDeliveryPayloadAttachments(payload.Attachments),
|
|
}
|
|
|
|
encoded, err := json.Marshal(stored)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal redis delivery payload record: %w", err)
|
|
}
|
|
|
|
return encoded, nil
|
|
}
|
|
|
|
// UnmarshalDeliveryPayload decodes payload from the strict Redis JSON shape
|
|
// used for raw generic-delivery attachment bundles.
|
|
func UnmarshalDeliveryPayload(payload []byte) (acceptgenericdelivery.DeliveryPayload, error) {
|
|
var stored deliveryPayloadRecord
|
|
if err := decodeStrictJSON("decode redis delivery payload record", payload, &stored); err != nil {
|
|
return acceptgenericdelivery.DeliveryPayload{}, err
|
|
}
|
|
|
|
record := acceptgenericdelivery.DeliveryPayload{
|
|
DeliveryID: common.DeliveryID(stored.DeliveryID),
|
|
Attachments: inflateDeliveryPayloadAttachments(stored.Attachments),
|
|
}
|
|
if err := record.Validate(); err != nil {
|
|
return acceptgenericdelivery.DeliveryPayload{}, fmt.Errorf("decode redis delivery payload record: %w", err)
|
|
}
|
|
|
|
return record, nil
|
|
}
|
|
|
|
// MarshalMalformedCommand encodes entry into the strict Redis JSON shape used
|
|
// for operator-visible malformed async command records.
|
|
func MarshalMalformedCommand(entry malformedcommand.Entry) ([]byte, error) {
|
|
if err := entry.Validate(); err != nil {
|
|
return nil, fmt.Errorf("marshal redis malformed command record: %w", err)
|
|
}
|
|
|
|
stored := malformedCommandRecord{
|
|
StreamEntryID: entry.StreamEntryID,
|
|
DeliveryID: entry.DeliveryID,
|
|
Source: entry.Source,
|
|
IdempotencyKey: entry.IdempotencyKey,
|
|
FailureCode: entry.FailureCode,
|
|
FailureMessage: entry.FailureMessage,
|
|
RawFieldsJSON: cloneJSONObject(entry.RawFields),
|
|
RecordedAtMS: entry.RecordedAt.UTC().UnixMilli(),
|
|
}
|
|
|
|
encoded, err := json.Marshal(stored)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal redis malformed command record: %w", err)
|
|
}
|
|
|
|
return encoded, nil
|
|
}
|
|
|
|
// UnmarshalMalformedCommand decodes payload from the strict Redis JSON shape
|
|
// used for operator-visible malformed async command records.
|
|
func UnmarshalMalformedCommand(payload []byte) (malformedcommand.Entry, error) {
|
|
var stored malformedCommandRecord
|
|
if err := decodeStrictJSON("decode redis malformed command record", payload, &stored); err != nil {
|
|
return malformedcommand.Entry{}, err
|
|
}
|
|
|
|
entry := malformedcommand.Entry{
|
|
StreamEntryID: stored.StreamEntryID,
|
|
DeliveryID: stored.DeliveryID,
|
|
Source: stored.Source,
|
|
IdempotencyKey: stored.IdempotencyKey,
|
|
FailureCode: stored.FailureCode,
|
|
FailureMessage: stored.FailureMessage,
|
|
RawFields: cloneJSONObject(stored.RawFieldsJSON),
|
|
RecordedAt: time.UnixMilli(stored.RecordedAtMS).UTC(),
|
|
}
|
|
if err := entry.Validate(); err != nil {
|
|
return malformedcommand.Entry{}, fmt.Errorf("decode redis malformed command record: %w", err)
|
|
}
|
|
|
|
return entry, nil
|
|
}
|
|
|
|
// MarshalStreamOffset encodes offset into the strict Redis JSON shape used for
|
|
// persisted consumer progress.
|
|
func MarshalStreamOffset(offset StreamOffset) ([]byte, error) {
|
|
if err := offset.Validate(); err != nil {
|
|
return nil, fmt.Errorf("marshal redis stream offset record: %w", err)
|
|
}
|
|
|
|
stored := streamOffsetRecord{
|
|
Stream: offset.Stream,
|
|
LastProcessedEntryID: offset.LastProcessedEntryID,
|
|
UpdatedAtMS: offset.UpdatedAt.UTC().UnixMilli(),
|
|
}
|
|
|
|
encoded, err := json.Marshal(stored)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal redis stream offset record: %w", err)
|
|
}
|
|
|
|
return encoded, nil
|
|
}
|
|
|
|
// UnmarshalStreamOffset decodes payload from the strict Redis JSON shape used
|
|
// for persisted consumer progress.
|
|
func UnmarshalStreamOffset(payload []byte) (StreamOffset, error) {
|
|
var stored streamOffsetRecord
|
|
if err := decodeStrictJSON("decode redis stream offset record", payload, &stored); err != nil {
|
|
return StreamOffset{}, err
|
|
}
|
|
|
|
offset := StreamOffset{
|
|
Stream: stored.Stream,
|
|
LastProcessedEntryID: stored.LastProcessedEntryID,
|
|
UpdatedAt: time.UnixMilli(stored.UpdatedAtMS).UTC(),
|
|
}
|
|
if err := offset.Validate(); err != nil {
|
|
return StreamOffset{}, fmt.Errorf("decode redis stream offset record: %w", err)
|
|
}
|
|
|
|
return offset, nil
|
|
}
|
|
|
|
func decodeStrictJSON(operation string, payload []byte, target any) error {
|
|
decoder := json.NewDecoder(bytes.NewReader(payload))
|
|
decoder.DisallowUnknownFields()
|
|
|
|
if err := decoder.Decode(target); err != nil {
|
|
return fmt.Errorf("%s: %w", operation, err)
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
return fmt.Errorf("%s: unexpected trailing JSON input", operation)
|
|
}
|
|
return fmt.Errorf("%s: %w", operation, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func cloneEmailStrings(values []common.Email) []string {
|
|
if values == nil {
|
|
return nil
|
|
}
|
|
|
|
cloned := make([]string, len(values))
|
|
for index, value := range values {
|
|
cloned[index] = value.String()
|
|
}
|
|
|
|
return cloned
|
|
}
|
|
|
|
func cloneEmails(values []string) []common.Email {
|
|
if values == nil {
|
|
return nil
|
|
}
|
|
|
|
cloned := make([]common.Email, len(values))
|
|
for index, value := range values {
|
|
cloned[index] = common.Email(value)
|
|
}
|
|
|
|
return cloned
|
|
}
|
|
|
|
func cloneAttachments(values []common.AttachmentMetadata) []attachmentRecord {
|
|
if values == nil {
|
|
return nil
|
|
}
|
|
|
|
cloned := make([]attachmentRecord, len(values))
|
|
for index, value := range values {
|
|
cloned[index] = attachmentRecord{
|
|
Filename: value.Filename,
|
|
ContentType: value.ContentType,
|
|
SizeBytes: value.SizeBytes,
|
|
}
|
|
}
|
|
|
|
return cloned
|
|
}
|
|
|
|
func inflateAttachments(values []attachmentRecord) []common.AttachmentMetadata {
|
|
if values == nil {
|
|
return nil
|
|
}
|
|
|
|
cloned := make([]common.AttachmentMetadata, len(values))
|
|
for index, value := range values {
|
|
cloned[index] = common.AttachmentMetadata{
|
|
Filename: value.Filename,
|
|
ContentType: value.ContentType,
|
|
SizeBytes: value.SizeBytes,
|
|
}
|
|
}
|
|
|
|
return cloned
|
|
}
|
|
|
|
func optionalJSONObject(value map[string]any) *map[string]any {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
cloned := make(map[string]any, len(value))
|
|
for key, item := range value {
|
|
cloned[key] = cloneJSONValue(item)
|
|
}
|
|
|
|
return &cloned
|
|
}
|
|
|
|
func cloneJSONObjectPtr(value *map[string]any) map[string]any {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
cloned := make(map[string]any, len(*value))
|
|
for key, item := range *value {
|
|
cloned[key] = cloneJSONValue(item)
|
|
}
|
|
|
|
return cloned
|
|
}
|
|
|
|
func cloneJSONObject(value map[string]any) map[string]any {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
cloned := make(map[string]any, len(value))
|
|
for key, item := range value {
|
|
cloned[key] = cloneJSONValue(item)
|
|
}
|
|
|
|
return cloned
|
|
}
|
|
|
|
func cloneJSONValue(value any) any {
|
|
switch typed := value.(type) {
|
|
case map[string]any:
|
|
cloned := make(map[string]any, len(typed))
|
|
for key, item := range typed {
|
|
cloned[key] = cloneJSONValue(item)
|
|
}
|
|
return cloned
|
|
case []any:
|
|
cloned := make([]any, len(typed))
|
|
for index, item := range typed {
|
|
cloned[index] = cloneJSONValue(item)
|
|
}
|
|
return cloned
|
|
default:
|
|
return typed
|
|
}
|
|
}
|
|
|
|
func cloneDeliveryPayloadAttachments(values []acceptgenericdelivery.AttachmentPayload) []deliveryPayloadAttachmentRecord {
|
|
if values == nil {
|
|
return nil
|
|
}
|
|
|
|
cloned := make([]deliveryPayloadAttachmentRecord, len(values))
|
|
for index, value := range values {
|
|
cloned[index] = deliveryPayloadAttachmentRecord{
|
|
Filename: value.Filename,
|
|
ContentType: value.ContentType,
|
|
ContentBase64: value.ContentBase64,
|
|
SizeBytes: value.SizeBytes,
|
|
}
|
|
}
|
|
|
|
return cloned
|
|
}
|
|
|
|
func inflateDeliveryPayloadAttachments(values []deliveryPayloadAttachmentRecord) []acceptgenericdelivery.AttachmentPayload {
|
|
if values == nil {
|
|
return nil
|
|
}
|
|
|
|
cloned := make([]acceptgenericdelivery.AttachmentPayload, len(values))
|
|
for index, value := range values {
|
|
cloned[index] = acceptgenericdelivery.AttachmentPayload{
|
|
Filename: value.Filename,
|
|
ContentType: value.ContentType,
|
|
ContentBase64: value.ContentBase64,
|
|
SizeBytes: value.SizeBytes,
|
|
}
|
|
}
|
|
|
|
return cloned
|
|
}
|
|
|
|
func optionalUnixMilli(value *time.Time) *int64 {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
milliseconds := value.UTC().UnixMilli()
|
|
return &milliseconds
|
|
}
|
|
|
|
func inflateOptionalTime(value *int64) *time.Time {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
converted := time.UnixMilli(*value).UTC()
|
|
return &converted
|
|
}
|