Add SPC convective outlook data contracts
This commit is contained in:
@@ -7,17 +7,18 @@ import (
|
||||
)
|
||||
|
||||
type Bundle struct {
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
Observation *Observation `json:"observation,omitempty"`
|
||||
Current *Current `json:"current,omitempty"`
|
||||
Hourly *ForecastRun `json:"hourly,omitempty"`
|
||||
Narrative *ForecastRun `json:"narrative,omitempty"`
|
||||
Alerts *AlertRun `json:"alerts,omitempty"`
|
||||
Discussion *Discussion `json:"discussion,omitempty"`
|
||||
Daily *ForecastRun `json:"daily,omitempty"`
|
||||
WeatherStory *WeatherStory `json:"weatherStory,omitempty"`
|
||||
Sources []Source `json:"sources"`
|
||||
Warnings []SourceWarning `json:"warnings,omitempty"`
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
Observation *Observation `json:"observation,omitempty"`
|
||||
Current *Current `json:"current,omitempty"`
|
||||
Hourly *ForecastRun `json:"hourly,omitempty"`
|
||||
Narrative *ForecastRun `json:"narrative,omitempty"`
|
||||
Alerts *AlertRun `json:"alerts,omitempty"`
|
||||
Discussion *Discussion `json:"discussion,omitempty"`
|
||||
Daily *ForecastRun `json:"daily,omitempty"`
|
||||
WeatherStory *WeatherStory `json:"weatherStory,omitempty"`
|
||||
SPCConvectiveOutlooks *ConvectiveOutlookRun `json:"spcConvectiveOutlooks,omitempty"`
|
||||
Sources []Source `json:"sources"`
|
||||
Warnings []SourceWarning `json:"warnings,omitempty"`
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
@@ -166,3 +167,42 @@ type WeatherStory struct {
|
||||
Order int `json:"order"`
|
||||
DownloadURL string `json:"downloadUrl,omitempty"`
|
||||
}
|
||||
|
||||
type ConvectiveOutlookRun struct {
|
||||
LocationID string `json:"locationId,omitempty"`
|
||||
LocationName string `json:"locationName,omitempty"`
|
||||
AsOf *time.Time `json:"asOf,omitempty"`
|
||||
IssuedAt *time.Time `json:"issuedAt,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
|
||||
Product string `json:"product,omitempty"`
|
||||
Outlooks []ConvectiveOutlook `json:"outlooks,omitempty"`
|
||||
Discussions []ConvectiveOutlookDiscussion `json:"discussions,omitempty"`
|
||||
}
|
||||
|
||||
type ConvectiveOutlook struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Product string `json:"product,omitempty"`
|
||||
Day int `json:"day,omitempty"`
|
||||
OutlookType string `json:"outlookType,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
LabelText string `json:"labelText,omitempty"`
|
||||
Forecaster string `json:"forecaster,omitempty"`
|
||||
SeverityRank *int `json:"severityRank,omitempty"`
|
||||
ValidFrom time.Time `json:"validFrom"`
|
||||
ValidTo time.Time `json:"validTo"`
|
||||
IssuedAt *time.Time `json:"issuedAt,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||
SourceURL string `json:"sourceUrl,omitempty"`
|
||||
ImageURL string `json:"imageUrl,omitempty"`
|
||||
ContainsLocation bool `json:"containsLocation"`
|
||||
Geometry json.RawMessage `json:"geometry,omitempty"`
|
||||
}
|
||||
|
||||
type ConvectiveOutlookDiscussion struct {
|
||||
Day int `json:"day,omitempty"`
|
||||
Headline string `json:"headline,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Discussion string `json:"discussion,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
|
||||
}
|
||||
|
||||
78
internal/weatherdata/bundle_test.go
Normal file
78
internal/weatherdata/bundle_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package weatherdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConvectiveOutlookRunJSONPreservesGeometry(t *testing.T) {
|
||||
severity := 3
|
||||
validFrom := mustParseBundleTime("2026-06-12T13:00:00Z")
|
||||
validTo := mustParseBundleTime("2026-06-13T12:00:00Z")
|
||||
issuedAt := mustParseBundleTime("2026-06-12T12:30:00Z")
|
||||
updatedAt := mustParseBundleTime("2026-06-12T12:45:00Z")
|
||||
run := ConvectiveOutlookRun{
|
||||
LocationID: "test-grid",
|
||||
LocationName: "Brentwood",
|
||||
AsOf: &updatedAt,
|
||||
IssuedAt: &issuedAt,
|
||||
Product: "convective_outlook",
|
||||
Outlooks: []ConvectiveOutlook{{
|
||||
ID: "day1-categorical-slight",
|
||||
Provider: "spc",
|
||||
Product: "convective_outlook",
|
||||
Day: 1,
|
||||
OutlookType: "categorical",
|
||||
Label: "SLGT",
|
||||
LabelText: "Slight Risk",
|
||||
Forecaster: "Smith",
|
||||
SeverityRank: &severity,
|
||||
ValidFrom: validFrom,
|
||||
ValidTo: validTo,
|
||||
IssuedAt: &issuedAt,
|
||||
ExpiresAt: &validTo,
|
||||
SourceURL: "https://example.test/source",
|
||||
ImageURL: "https://example.test/image.png",
|
||||
ContainsLocation: true,
|
||||
Geometry: json.RawMessage(`{"type":"Polygon","coordinates":[[[-90.1,38.1],[-90.0,38.2],[-90.1,38.1]]]}`),
|
||||
}},
|
||||
Discussions: []ConvectiveOutlookDiscussion{{
|
||||
Day: 1,
|
||||
Headline: "Severe storms possible",
|
||||
Summary: "Scattered severe storms are possible.",
|
||||
Discussion: "Discussion text.",
|
||||
UpdatedAt: &updatedAt,
|
||||
}},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(run)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal() error = %v", err)
|
||||
}
|
||||
|
||||
var decoded ConvectiveOutlookRun
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("Unmarshal() error = %v", err)
|
||||
}
|
||||
if len(decoded.Outlooks) != 1 || string(decoded.Outlooks[0].Geometry) == "" {
|
||||
t.Fatalf("decoded outlooks = %#v, want preserved geometry", decoded.Outlooks)
|
||||
}
|
||||
if got := string(decoded.Outlooks[0].Geometry); got != `{"type":"Polygon","coordinates":[[[-90.1,38.1],[-90.0,38.2],[-90.1,38.1]]]}` {
|
||||
t.Fatalf("Geometry = %s, want preserved GeoJSON coordinates", got)
|
||||
}
|
||||
if decoded.Outlooks[0].SeverityRank == nil || *decoded.Outlooks[0].SeverityRank != severity {
|
||||
t.Fatalf("SeverityRank = %#v, want %d", decoded.Outlooks[0].SeverityRank, severity)
|
||||
}
|
||||
if len(decoded.Discussions) != 1 || decoded.Discussions[0].Headline != "Severe storms possible" {
|
||||
t.Fatalf("Discussions = %#v, want decoded discussion", decoded.Discussions)
|
||||
}
|
||||
}
|
||||
|
||||
func mustParseBundleTime(value string) time.Time {
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
Reference in New Issue
Block a user