// FILE: ./internal/normalizers/common/json.go package common import ( "encoding/json" "fmt" "time" "gitea.maximumdirect.net/ejr/feedkit/event" ) // DecodeJSONPayload extracts the event payload as bytes and unmarshals it into T. // // This is the shared "spine" used by many normalizers: // - sources emit raw JSON payloads (typically json.RawMessage) // - normalizers decode into provider structs // // Errors include a small amount of stage context ("extract payload", "decode raw payload"). // Callers typically wrap these with a provider/kind label. func DecodeJSONPayload[T any](in event.Event) (T, error) { var zero T b, err := PayloadBytes(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 } // NormalizeJSON is a convenience wrapper for the common JSON-normalizer pattern: // // 1. Decode raw JSON payload into provider struct T // 2. Map T into canonical payload P (plus an EffectiveAt timestamp) // 3. Finalize the event envelope (schema/payload/effectiveAt) + Validate // // label should be short and specific, e.g. "openweather observation". // outSchema should be the canonical schema constant. // build should contain ONLY provider/domain mapping logic. func NormalizeJSON[T any, P any]( in event.Event, label string, outSchema string, build func(parsed T) (P, time.Time, error), ) (*event.Event, error) { parsed, err := DecodeJSONPayload[T](in) if err != nil { return nil, fmt.Errorf("%s normalize: %w", label, err) } payload, effectiveAt, err := build(parsed) if err != nil { // build() should already include provider-specific context where appropriate. return nil, err } out, err := Finalize(in, outSchema, payload, effectiveAt) if err != nil { return nil, fmt.Errorf("%s normalize: %w", label, err) } return out, nil }