Implemented NWS weather stories support
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
This commit is contained in:
107
internal/normalizers/nws/weatherstories.go
Normal file
107
internal/normalizers/nws/weatherstories.go
Normal file
@@ -0,0 +1,107 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user