Simplify Weather API source fetching
This commit is contained in:
@@ -130,18 +130,32 @@ type bundleBuilder struct {
|
|||||||
fetchedAt time.Time
|
fetchedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sourceRequest struct {
|
||||||
|
name string
|
||||||
|
endpoint string
|
||||||
|
query queryOptions
|
||||||
|
missingMessage string
|
||||||
|
required bool
|
||||||
|
decodeLabel string
|
||||||
|
}
|
||||||
|
|
||||||
|
type fetchedSource struct {
|
||||||
|
raw json.RawMessage
|
||||||
|
source weatherdata.Source
|
||||||
|
}
|
||||||
|
|
||||||
func (b *bundleBuilder) fetchObservation(ctx context.Context) error {
|
func (b *bundleBuilder) fetchObservation(ctx context.Context) error {
|
||||||
raw, source, err := b.client.fetch(ctx, "observations", "/observations", queryOptions{precision: true})
|
var observation weatherdata.Observation
|
||||||
if err != nil {
|
fetched, ok, err := b.fetchDecodedSource(ctx, sourceRequest{
|
||||||
|
name: "observations",
|
||||||
|
endpoint: "/observations",
|
||||||
|
query: queryOptions{precision: true},
|
||||||
|
missingMessage: "observation data is missing",
|
||||||
|
}, &observation)
|
||||||
|
if err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if raw == nil {
|
source := fetched.source
|
||||||
return b.handleMissing(&source, "observation data is missing", false)
|
|
||||||
}
|
|
||||||
var observation weatherdata.Observation
|
|
||||||
if err := decodeSource(raw, &observation); err != nil {
|
|
||||||
return b.handleMalformed(&source, err, false)
|
|
||||||
}
|
|
||||||
source.IssuedAt = &observation.Timestamp
|
source.IssuedAt = &observation.Timestamp
|
||||||
b.bundle.Observation = &observation
|
b.bundle.Observation = &observation
|
||||||
b.addSource(source)
|
b.addSource(source)
|
||||||
@@ -149,34 +163,36 @@ func (b *bundleBuilder) fetchObservation(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *bundleBuilder) fetchCurrent(ctx context.Context) error {
|
func (b *bundleBuilder) fetchCurrent(ctx context.Context) error {
|
||||||
raw, source, err := b.client.fetch(ctx, "current", "/conditions/current", queryOptions{precision: true})
|
var current weatherdata.Current
|
||||||
if err != nil {
|
fetched, ok, err := b.fetchDecodedSource(ctx, sourceRequest{
|
||||||
|
name: "current",
|
||||||
|
endpoint: "/conditions/current",
|
||||||
|
query: queryOptions{precision: true},
|
||||||
|
missingMessage: "current conditions data is missing",
|
||||||
|
}, ¤t)
|
||||||
|
if err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if raw == nil {
|
source := fetched.source
|
||||||
return b.handleMissing(&source, "current conditions data is missing", false)
|
|
||||||
}
|
|
||||||
var current weatherdata.Current
|
|
||||||
if err := decodeSource(raw, ¤t); err != nil {
|
|
||||||
return b.handleMalformed(&source, err, false)
|
|
||||||
}
|
|
||||||
b.bundle.Current = ¤t
|
b.bundle.Current = ¤t
|
||||||
b.addSource(source)
|
b.addSource(source)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *bundleBuilder) fetchHourly(ctx context.Context) error {
|
func (b *bundleBuilder) fetchHourly(ctx context.Context) error {
|
||||||
raw, source, err := b.client.fetch(ctx, "hourly", "/forecast/hourly", queryOptions{precision: true, timezone: true})
|
var hourly weatherdata.ForecastRun
|
||||||
if err != nil {
|
fetched, ok, err := b.fetchDecodedSource(ctx, sourceRequest{
|
||||||
|
name: "hourly",
|
||||||
|
endpoint: "/forecast/hourly",
|
||||||
|
query: queryOptions{precision: true, timezone: true},
|
||||||
|
missingMessage: "hourly forecast data is missing",
|
||||||
|
required: true,
|
||||||
|
decodeLabel: "hourly forecast",
|
||||||
|
}, &hourly)
|
||||||
|
if err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if raw == nil {
|
source := fetched.source
|
||||||
return b.handleMissing(&source, "hourly forecast data is missing", true)
|
|
||||||
}
|
|
||||||
var hourly weatherdata.ForecastRun
|
|
||||||
if err := decodeSource(raw, &hourly); err != nil {
|
|
||||||
return fmt.Errorf("decode hourly forecast from %s: %w", source.Endpoint, err)
|
|
||||||
}
|
|
||||||
if len(hourly.Periods) == 0 {
|
if len(hourly.Periods) == 0 {
|
||||||
return fmt.Errorf("hourly forecast from %s contains no periods", source.Endpoint)
|
return fmt.Errorf("hourly forecast from %s contains no periods", source.Endpoint)
|
||||||
}
|
}
|
||||||
@@ -188,17 +204,17 @@ func (b *bundleBuilder) fetchHourly(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *bundleBuilder) fetchNarrative(ctx context.Context) error {
|
func (b *bundleBuilder) fetchNarrative(ctx context.Context) error {
|
||||||
raw, source, err := b.client.fetch(ctx, "narrative", "/forecast/narrative", queryOptions{precision: true, timezone: true})
|
var narrative weatherdata.ForecastRun
|
||||||
if err != nil {
|
fetched, ok, err := b.fetchDecodedSource(ctx, sourceRequest{
|
||||||
|
name: "narrative",
|
||||||
|
endpoint: "/forecast/narrative",
|
||||||
|
query: queryOptions{precision: true, timezone: true},
|
||||||
|
missingMessage: "narrative forecast data is missing",
|
||||||
|
}, &narrative)
|
||||||
|
if err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if raw == nil {
|
source := fetched.source
|
||||||
return b.handleMissing(&source, "narrative forecast data is missing", false)
|
|
||||||
}
|
|
||||||
var narrative weatherdata.ForecastRun
|
|
||||||
if err := decodeSource(raw, &narrative); err != nil {
|
|
||||||
return b.handleMalformed(&source, err, false)
|
|
||||||
}
|
|
||||||
source.IssuedAt = &narrative.IssuedAt
|
source.IssuedAt = &narrative.IssuedAt
|
||||||
source.UpdatedAt = narrative.UpdatedAt
|
source.UpdatedAt = narrative.UpdatedAt
|
||||||
b.bundle.Narrative = &narrative
|
b.bundle.Narrative = &narrative
|
||||||
@@ -221,7 +237,7 @@ func (b *bundleBuilder) fetchAlerts(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
var alerts weatherdata.AlertRun
|
var alerts weatherdata.AlertRun
|
||||||
if err := decodeSource(raw, &alerts); err != nil {
|
if err := decodeSource(raw, &alerts); err != nil {
|
||||||
return b.handleMalformed(&source, err, false)
|
return b.handleMalformed(&source, err, sourceRequest{name: "alerts"})
|
||||||
}
|
}
|
||||||
alerts.Raw = append(json.RawMessage(nil), raw...)
|
alerts.Raw = append(json.RawMessage(nil), raw...)
|
||||||
if alerts.AsOf != nil {
|
if alerts.AsOf != nil {
|
||||||
@@ -233,17 +249,17 @@ func (b *bundleBuilder) fetchAlerts(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *bundleBuilder) fetchDiscussion(ctx context.Context) error {
|
func (b *bundleBuilder) fetchDiscussion(ctx context.Context) error {
|
||||||
raw, source, err := b.client.fetch(ctx, "discussion", "/discussion", queryOptions{timezone: true})
|
var discussion weatherdata.Discussion
|
||||||
if err != nil {
|
fetched, ok, err := b.fetchDecodedSource(ctx, sourceRequest{
|
||||||
|
name: "discussion",
|
||||||
|
endpoint: "/discussion",
|
||||||
|
query: queryOptions{timezone: true},
|
||||||
|
missingMessage: "forecast discussion data is missing",
|
||||||
|
}, &discussion)
|
||||||
|
if err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if raw == nil {
|
source := fetched.source
|
||||||
return b.handleMissing(&source, "forecast discussion data is missing", false)
|
|
||||||
}
|
|
||||||
var discussion weatherdata.Discussion
|
|
||||||
if err := decodeSource(raw, &discussion); err != nil {
|
|
||||||
return b.handleMalformed(&source, err, false)
|
|
||||||
}
|
|
||||||
source.IssuedAt = &discussion.IssuedAt
|
source.IssuedAt = &discussion.IssuedAt
|
||||||
source.UpdatedAt = discussion.UpdatedAt
|
source.UpdatedAt = discussion.UpdatedAt
|
||||||
b.bundle.Discussion = &discussion
|
b.bundle.Discussion = &discussion
|
||||||
@@ -252,17 +268,17 @@ func (b *bundleBuilder) fetchDiscussion(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *bundleBuilder) fetchWeatherStory(ctx context.Context) error {
|
func (b *bundleBuilder) fetchWeatherStory(ctx context.Context) error {
|
||||||
raw, source, err := b.client.fetch(ctx, "weather_story", "/weatherstories/latest", queryOptions{omitUnits: true})
|
var story weatherdata.WeatherStory
|
||||||
if err != nil {
|
fetched, ok, err := b.fetchDecodedSource(ctx, sourceRequest{
|
||||||
|
name: "weather_story",
|
||||||
|
endpoint: "/weatherstories/latest",
|
||||||
|
query: queryOptions{omitUnits: true},
|
||||||
|
missingMessage: "NWS weather story data is missing",
|
||||||
|
}, &story)
|
||||||
|
if err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if raw == nil {
|
source := fetched.source
|
||||||
return b.handleMissing(&source, "NWS weather story data is missing", false)
|
|
||||||
}
|
|
||||||
var story weatherdata.WeatherStory
|
|
||||||
if err := decodeSource(raw, &story); err != nil {
|
|
||||||
return b.handleMalformed(&source, err, false)
|
|
||||||
}
|
|
||||||
if !story.StartTime.IsZero() {
|
if !story.StartTime.IsZero() {
|
||||||
source.IssuedAt = &story.StartTime
|
source.IssuedAt = &story.StartTime
|
||||||
}
|
}
|
||||||
@@ -273,17 +289,17 @@ func (b *bundleBuilder) fetchWeatherStory(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *bundleBuilder) fetchSPCConvectiveOutlooks(ctx context.Context) error {
|
func (b *bundleBuilder) fetchSPCConvectiveOutlooks(ctx context.Context) error {
|
||||||
raw, source, err := b.client.fetch(ctx, sourceSPCConvectiveOutlooks, convectiveOutlooksEndpoint, queryOptions{timezone: true, omitUnits: true})
|
var run weatherdata.ConvectiveOutlookRun
|
||||||
if err != nil {
|
fetched, ok, err := b.fetchDecodedSource(ctx, sourceRequest{
|
||||||
|
name: sourceSPCConvectiveOutlooks,
|
||||||
|
endpoint: convectiveOutlooksEndpoint,
|
||||||
|
query: queryOptions{timezone: true, omitUnits: true},
|
||||||
|
missingMessage: "SPC convective outlook data is missing",
|
||||||
|
}, &run)
|
||||||
|
if err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if raw == nil {
|
source := fetched.source
|
||||||
return b.handleMissing(&source, "SPC convective outlook data is missing", false)
|
|
||||||
}
|
|
||||||
var run weatherdata.ConvectiveOutlookRun
|
|
||||||
if err := decodeSource(raw, &run); err != nil {
|
|
||||||
return b.handleMalformed(&source, err, false)
|
|
||||||
}
|
|
||||||
if run.IssuedAt != nil {
|
if run.IssuedAt != nil {
|
||||||
source.IssuedAt = run.IssuedAt
|
source.IssuedAt = run.IssuedAt
|
||||||
} else {
|
} else {
|
||||||
@@ -295,6 +311,28 @@ func (b *bundleBuilder) fetchSPCConvectiveOutlooks(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *bundleBuilder) fetchDecodedSource(ctx context.Context, request sourceRequest, target any) (fetchedSource, bool, error) {
|
||||||
|
fetched, ok, err := b.fetchSource(ctx, request)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return fetchedSource{}, false, err
|
||||||
|
}
|
||||||
|
if err := decodeSource(fetched.raw, target); err != nil {
|
||||||
|
return fetchedSource{}, false, b.handleMalformed(&fetched.source, err, request)
|
||||||
|
}
|
||||||
|
return fetched, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bundleBuilder) fetchSource(ctx context.Context, request sourceRequest) (fetchedSource, bool, error) {
|
||||||
|
raw, source, err := b.client.fetch(ctx, request.name, request.endpoint, request.query)
|
||||||
|
if err != nil {
|
||||||
|
return fetchedSource{}, false, err
|
||||||
|
}
|
||||||
|
if raw == nil {
|
||||||
|
return fetchedSource{}, false, b.handleMissing(&source, request.missingMessage, request.required)
|
||||||
|
}
|
||||||
|
return fetchedSource{raw: raw, source: source}, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *bundleBuilder) handleMissing(source *weatherdata.Source, message string, required bool) error {
|
func (b *bundleBuilder) handleMissing(source *weatherdata.Source, message string, required bool) error {
|
||||||
source.Missing = true
|
source.Missing = true
|
||||||
if required {
|
if required {
|
||||||
@@ -303,9 +341,13 @@ func (b *bundleBuilder) handleMissing(source *weatherdata.Source, message string
|
|||||||
return b.applyMissingPolicy(source, "missing_source", message)
|
return b.applyMissingPolicy(source, "missing_source", message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *bundleBuilder) handleMalformed(source *weatherdata.Source, err error, required bool) error {
|
func (b *bundleBuilder) handleMalformed(source *weatherdata.Source, err error, request sourceRequest) error {
|
||||||
if required {
|
if request.required {
|
||||||
return fmt.Errorf("decode %s from %s: %w", source.Name, source.Endpoint, err)
|
label := request.name
|
||||||
|
if request.decodeLabel != "" {
|
||||||
|
label = request.decodeLabel
|
||||||
|
}
|
||||||
|
return fmt.Errorf("decode %s from %s: %w", label, source.Endpoint, err)
|
||||||
}
|
}
|
||||||
source.Missing = true
|
source.Missing = true
|
||||||
return b.applyMissingPolicy(source, "malformed_source", fmt.Sprintf("malformed %s data: %v", source.Name, err))
|
return b.applyMissingPolicy(source, "malformed_source", fmt.Sprintf("malformed %s data: %v", source.Name, err))
|
||||||
|
|||||||
@@ -70,6 +70,24 @@ func TestFetchBundleFromFixtures(t *testing.T) {
|
|||||||
if len(bundle.Warnings) != 0 {
|
if len(bundle.Warnings) != 0 {
|
||||||
t.Fatalf("Warnings length = %d, want no warnings", len(bundle.Warnings))
|
t.Fatalf("Warnings length = %d, want no warnings", len(bundle.Warnings))
|
||||||
}
|
}
|
||||||
|
wantPaths := []string{
|
||||||
|
"/observations",
|
||||||
|
"/conditions/current",
|
||||||
|
"/forecast/hourly",
|
||||||
|
"/forecast/narrative",
|
||||||
|
"/alerts/active",
|
||||||
|
"/discussion",
|
||||||
|
"/weatherstories/latest",
|
||||||
|
convectiveOutlooksEndpoint,
|
||||||
|
}
|
||||||
|
if len(requested) != len(wantPaths) {
|
||||||
|
t.Fatalf("requested paths = %v, want %d source endpoints", requested, len(wantPaths))
|
||||||
|
}
|
||||||
|
for _, want := range wantPaths {
|
||||||
|
if !containsPath(requested, want) {
|
||||||
|
t.Fatalf("requested paths = %v, want %s", requested, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
if !containsPath(requested, "/forecast/hourly") || containsPath(requested, "/forecast/hourly/today") {
|
if !containsPath(requested, "/forecast/hourly") || containsPath(requested, "/forecast/hourly/today") {
|
||||||
t.Fatalf("requested paths = %v, want full hourly endpoint only", requested)
|
t.Fatalf("requested paths = %v, want full hourly endpoint only", requested)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user