Add outlook v2 model contract

This commit is contained in:
2026-06-12 04:19:56 +00:00
parent 819ac24aed
commit 435d1ade07
11 changed files with 109 additions and 42 deletions

View File

@@ -26,6 +26,7 @@ func TestDocumentedConsumerModelTypes(t *testing.T) {
"WeatherAlert",
"WeatherAlertReference",
"WeatherOutlookRun",
"WeatherOutlookDiscussion",
"WeatherOutlook",
"WMOCode",
}

View File

@@ -8,13 +8,23 @@ import (
// WeatherOutlookRun is a snapshot of convective outlook polygons for a
// configured location as-of a provider issue time.
type WeatherOutlookRun struct {
LocationID string `json:"locationId,omitempty"`
LocationName string `json:"locationName,omitempty"`
Latitude *float64 `json:"latitude,omitempty"`
Longitude *float64 `json:"longitude,omitempty"`
AsOf time.Time `json:"asOf"`
IssuedAt *time.Time `json:"issuedAt,omitempty"`
Outlooks []WeatherOutlook `json:"outlooks"`
LocationID string `json:"locationId,omitempty"`
LocationName string `json:"locationName,omitempty"`
Latitude *float64 `json:"latitude,omitempty"`
Longitude *float64 `json:"longitude,omitempty"`
AsOf time.Time `json:"asOf"`
IssuedAt *time.Time `json:"issuedAt,omitempty"`
Outlooks []WeatherOutlook `json:"outlooks"`
Discussions []WeatherOutlookDiscussion `json:"discussions"`
}
// WeatherOutlookDiscussion is run-level SPC outlook prose for one outlook day.
type WeatherOutlookDiscussion struct {
Day int `json:"day"`
Headline string `json:"headline,omitempty"`
Summary string `json:"summary,omitempty"`
Discussion string `json:"discussion,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
}
// WeatherOutlook is a canonical representation of one outlook polygon.
@@ -32,9 +42,6 @@ type WeatherOutlook struct {
IssuedAt time.Time `json:"issuedAt"`
ExpiresAt time.Time `json:"expiresAt"`
Forecaster string `json:"forecaster,omitempty"`
Headline string `json:"headline,omitempty"`
Summary string `json:"summary,omitempty"`
Discussion string `json:"discussion,omitempty"`
SourceURL string `json:"sourceUrl,omitempty"`
ImageURL string `json:"imageUrl,omitempty"`
ContainsLocation bool `json:"containsLocation"`

60
model/outlook_test.go Normal file
View File

@@ -0,0 +1,60 @@
package model
import (
"encoding/json"
"strings"
"testing"
"time"
)
func TestWeatherOutlookJSONShape(t *testing.T) {
updatedAt := time.Date(2026, 6, 11, 16, 30, 0, 0, time.UTC)
run := WeatherOutlookRun{
AsOf: time.Date(2026, 6, 11, 19, 45, 0, 0, time.UTC),
Outlooks: []WeatherOutlook{{
ID: "outlook-1",
Provider: "spc",
Product: "convective",
Day: 1,
OutlookType: "categorical",
Label: "SLGT",
ValidFrom: time.Date(2026, 6, 11, 13, 0, 0, 0, time.UTC),
ValidTo: time.Date(2026, 6, 12, 12, 0, 0, 0, time.UTC),
IssuedAt: time.Date(2026, 6, 11, 12, 34, 56, 0, time.UTC),
ExpiresAt: time.Date(2026, 6, 12, 12, 0, 0, 0, time.UTC),
ContainsLocation: true,
Geometry: json.RawMessage(`{"type":"Polygon","coordinates":[[[-91,38],[-90,38],[-90,39],[-91,39],[-91,38]]]}`),
}},
Discussions: []WeatherOutlookDiscussion{{
Day: 1,
Headline: "Day 1 Convective Outlook",
Summary: "Severe thunderstorms are possible.",
Discussion: "Full discussion text.",
UpdatedAt: &updatedAt,
}},
}
raw, err := json.Marshal(run)
if err != nil {
t.Fatalf("Marshal(WeatherOutlookRun) error = %v", err)
}
got := string(raw)
for _, want := range []string{`"outlooks"`, `"discussions"`, `"headline"`, `"summary"`, `"discussion"`, `"updatedAt"`} {
if !strings.Contains(got, want) {
t.Fatalf("WeatherOutlookRun JSON missing %s: %s", want, got)
}
}
outlookStart := strings.Index(got, `"outlooks"`)
discussionStart := strings.Index(got, `"discussions"`)
if outlookStart == -1 || discussionStart == -1 || discussionStart <= outlookStart {
t.Fatalf("WeatherOutlookRun JSON has unexpected outlook/discussion order: %s", got)
}
outlookJSON := got[outlookStart:discussionStart]
for _, unwanted := range []string{`"headline"`, `"summary"`, `"discussion"`} {
if strings.Contains(outlookJSON, unwanted) {
t.Fatalf("WeatherOutlook JSON contains polygon-level prose key %s: %s", unwanted, got)
}
}
}