package normalize import ( "encoding/json" "fmt" "time" "gitea.maximumdirect.net/ejr/feedkit/event" ) // PayloadJSONBytes extracts a JSON payload into bytes suitable for json.Unmarshal. // // Supported payload shapes: // - json.RawMessage // - []byte // - string // - map[string]any func PayloadJSONBytes(e event.Event) ([]byte, error) { if e.Payload == nil { return nil, fmt.Errorf("payload is nil") } switch v := e.Payload.(type) { case json.RawMessage: if len(v) == 0 { return nil, fmt.Errorf("payload is empty json.RawMessage") } return []byte(v), nil case []byte: if len(v) == 0 { return nil, fmt.Errorf("payload is empty []byte") } return v, nil case string: if v == "" { return nil, fmt.Errorf("payload is empty string") } return []byte(v), nil case map[string]any: b, err := json.Marshal(v) if err != nil { return nil, fmt.Errorf("marshal map payload: %w", err) } return b, nil default: return nil, fmt.Errorf("unsupported payload type %T", e.Payload) } } // DecodeJSONPayload extracts the event payload as bytes and unmarshals it into T. func DecodeJSONPayload[T any](in event.Event) (T, error) { var zero T b, err := PayloadJSONBytes(in) if err != nil { return zero, fmt.Errorf("extract payload: %w", err) } var parsed T if err := json.Unmarshal(b, &parsed); err != nil { return zero, fmt.Errorf("decode raw payload: %w", err) } return parsed, nil } // FinalizeEvent builds the output event envelope by copying the input and applying // the new schema/payload, plus optional EffectiveAt. func FinalizeEvent(in event.Event, outSchema string, outPayload any, effectiveAt time.Time) (*event.Event, error) { out := in out.Schema = outSchema out.Payload = outPayload if !effectiveAt.IsZero() { t := effectiveAt.UTC() out.EffectiveAt = &t } if err := out.Validate(); err != nil { return nil, err } return &out, nil }