package nws import ( "context" "fmt" "strings" "time" "gitea.maximumdirect.net/ejr/feedkit/event" normcommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/normalizers/common" nwscommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/providers/nws" "gitea.maximumdirect.net/ejr/weatherfeeder/model" "gitea.maximumdirect.net/ejr/weatherfeeder/standards" ) // WeatherStoriesNormalizer converts: // // standards.SchemaRawNWSWeatherStoriesV1 -> standards.SchemaWeatherStoryV1 // // It maps the NWS /weatherstories JSON response into a canonical story snapshot. type WeatherStoriesNormalizer struct{} func (WeatherStoriesNormalizer) Match(e event.Event) bool { return strings.TrimSpace(e.Schema) == standards.SchemaRawNWSWeatherStoriesV1 } func (WeatherStoriesNormalizer) Normalize(ctx context.Context, in event.Event) (*event.Event, error) { _ = ctx fallbackAsOf := in.EmittedAt.UTC() if in.EffectiveAt != nil && !in.EffectiveAt.IsZero() { fallbackAsOf = in.EffectiveAt.UTC() } return normcommon.NormalizeJSON( in, "nws weatherstories", standards.SchemaWeatherStoryV1, func(parsed nwsWeatherStoriesResponse) (model.WeatherStoryRun, time.Time, error) { return buildWeatherStories(parsed, fallbackAsOf) }, ) } func buildWeatherStories(parsed nwsWeatherStoriesResponse, fallbackAsOf time.Time) (model.WeatherStoryRun, time.Time, error) { stories := make([]model.WeatherStory, 0, len(parsed.Stories)) var officeID string var asOf time.Time for i, raw := range parsed.Stories { startTime, err := parseRequiredNWSTime(raw.StartTime, fmt.Sprintf("stories[%d].startTime", i)) if err != nil { return model.WeatherStoryRun{}, time.Time{}, err } endTime, err := parseRequiredNWSTime(raw.EndTime, fmt.Sprintf("stories[%d].endTime", i)) if err != nil { return model.WeatherStoryRun{}, time.Time{}, err } updatedAt, err := parseRequiredNWSTime(raw.UpdateTime, fmt.Sprintf("stories[%d].updateTime", i)) if err != nil { return model.WeatherStoryRun{}, time.Time{}, err } storyOfficeID := strings.TrimSpace(raw.OfficeID) if officeID == "" && storyOfficeID != "" { officeID = storyOfficeID } if asOf.IsZero() || updatedAt.After(asOf) { asOf = updatedAt } stories = append(stories, model.WeatherStory{ OfficeID: storyOfficeID, StartTime: startTime, EndTime: endTime, UpdatedAt: updatedAt, Title: strings.TrimSpace(raw.Title), Description: strings.TrimSpace(raw.Description), AltText: strings.TrimSpace(raw.AltText), Priority: raw.Priority, Order: raw.Order, DownloadURL: strings.TrimSpace(raw.Download), }) } if asOf.IsZero() { asOf = fallbackAsOf.UTC() } run := model.WeatherStoryRun{ OfficeID: officeID, AsOf: asOf, Stories: stories, } return run, asOf, nil } func parseRequiredNWSTime(raw, field string) (time.Time, error) { if strings.TrimSpace(raw) == "" { return time.Time{}, fmt.Errorf("%s is required", field) } t, err := nwscommon.ParseTime(raw) if err != nil { return time.Time{}, fmt.Errorf("%s: %w", field, err) } return t.UTC(), nil }