Add SPC convective outlook data contracts
This commit is contained in:
@@ -20,7 +20,8 @@ Inputs:
|
||||
Outputs:
|
||||
|
||||
- `facts.CollectedFacts` with normalized source facts plus separate source
|
||||
provenance and warnings
|
||||
provenance and warnings. SPC convective outlook source data is carried
|
||||
through when present in the bundle.
|
||||
- `facts.DerivedFacts` with valid-period forecast slices, alert overlaps,
|
||||
daily summaries, daypart summaries, and Storm Report window summary
|
||||
|
||||
@@ -55,6 +56,7 @@ and inspection.
|
||||
derivation error for reports that require daily summaries.
|
||||
- Missing optional narrative, alert, discussion, daily, or weather story data
|
||||
produces empty or nil derived fields.
|
||||
- Missing optional SPC convective outlook data produces a nil collected field.
|
||||
|
||||
## Tests
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ The registry recognizes these IDs:
|
||||
Every registered module has a builder. Report composition entries that refer to
|
||||
unknown or unimplemented module IDs fail validation instead of being skipped.
|
||||
|
||||
The package also defines `spc_convective_outlooks` and
|
||||
`spc_convective_discussion` IDs and empty option structs for the collected
|
||||
contract. They are not registered modules in the briefing registry.
|
||||
|
||||
## Options
|
||||
|
||||
Most modules use an empty options struct. `area_forecast_discussion` accepts:
|
||||
|
||||
@@ -21,7 +21,8 @@ Outputs:
|
||||
|
||||
- `weatherdata.Bundle` with observation, current conditions, hourly forecast,
|
||||
narrative forecast, active alerts, discussion, latest weather story, source
|
||||
records, and source warnings
|
||||
records, source warnings, and an optional typed SPC convective outlook field
|
||||
when that source has been populated
|
||||
- optional saved bundle JSON through app fetch helpers
|
||||
|
||||
## Boundaries
|
||||
|
||||
@@ -162,6 +162,8 @@ func collectedFactAvailable(requirement module.FactRequirement, ctx ModuleContex
|
||||
return ctx.Collected.Discussion != nil
|
||||
case module.CollectedWeatherStory:
|
||||
return ctx.Collected.WeatherStory != nil
|
||||
case module.CollectedSPCConvectiveOutlooks:
|
||||
return ctx.Collected.SPCConvectiveOutlooks != nil
|
||||
case module.CollectedSourceMetadata:
|
||||
return len(ctx.Collected.SourceProvenance) > 0 || len(ctx.Collected.SourceWarnings) > 0
|
||||
default:
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
func TestDefaultModuleRegistryValidatesReportDefaults(t *testing.T) {
|
||||
@@ -128,6 +130,18 @@ func TestModuleRegistryAcceptsTypedOptions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSPCConvectiveOutlookCollectedRequirementAvailability(t *testing.T) {
|
||||
ctx := ModuleContext{}
|
||||
if collectedFactAvailable(module.CollectedSPCConvectiveOutlooks, ctx) {
|
||||
t.Fatal("collectedFactAvailable() = true, want false without source")
|
||||
}
|
||||
|
||||
ctx.Collected = facts.CollectedFacts{SPCConvectiveOutlooks: &weatherdata.ConvectiveOutlookRun{}}
|
||||
if !collectedFactAvailable(module.CollectedSPCConvectiveOutlooks, ctx) {
|
||||
t.Fatal("collectedFactAvailable() = false, want true with checked source")
|
||||
}
|
||||
}
|
||||
|
||||
func noopModuleBuilder(ModuleContext, any) (*module.Output, error) {
|
||||
return &module.Output{ID: module.Metadata, StanzaName: "metadata", Value: struct{}{}}, nil
|
||||
}
|
||||
|
||||
@@ -12,15 +12,16 @@ import (
|
||||
)
|
||||
|
||||
type CollectedFacts struct {
|
||||
FetchedAt time.Time
|
||||
Observation *weatherdata.Observation
|
||||
Current *weatherdata.Current
|
||||
Hourly *weatherdata.ForecastRun
|
||||
Narrative *weatherdata.ForecastRun
|
||||
Alerts *weatherdata.AlertRun
|
||||
Discussion *weatherdata.Discussion
|
||||
Daily *weatherdata.ForecastRun
|
||||
WeatherStory *weatherdata.WeatherStory
|
||||
FetchedAt time.Time
|
||||
Observation *weatherdata.Observation
|
||||
Current *weatherdata.Current
|
||||
Hourly *weatherdata.ForecastRun
|
||||
Narrative *weatherdata.ForecastRun
|
||||
Alerts *weatherdata.AlertRun
|
||||
Discussion *weatherdata.Discussion
|
||||
Daily *weatherdata.ForecastRun
|
||||
WeatherStory *weatherdata.WeatherStory
|
||||
SPCConvectiveOutlooks *weatherdata.ConvectiveOutlookRun
|
||||
|
||||
SourceProvenance []weatherdata.Source
|
||||
SourceWarnings []weatherdata.SourceWarning
|
||||
@@ -31,33 +32,35 @@ func BuildCollected(bundle *weatherdata.Bundle) CollectedFacts {
|
||||
return CollectedFacts{}
|
||||
}
|
||||
return CollectedFacts{
|
||||
FetchedAt: bundle.FetchedAt,
|
||||
Observation: bundle.Observation,
|
||||
Current: bundle.Current,
|
||||
Hourly: bundle.Hourly,
|
||||
Narrative: bundle.Narrative,
|
||||
Alerts: bundle.Alerts,
|
||||
Discussion: bundle.Discussion,
|
||||
Daily: bundle.Daily,
|
||||
WeatherStory: bundle.WeatherStory,
|
||||
SourceProvenance: append([]weatherdata.Source(nil), bundle.Sources...),
|
||||
SourceWarnings: append([]weatherdata.SourceWarning(nil), bundle.Warnings...),
|
||||
FetchedAt: bundle.FetchedAt,
|
||||
Observation: bundle.Observation,
|
||||
Current: bundle.Current,
|
||||
Hourly: bundle.Hourly,
|
||||
Narrative: bundle.Narrative,
|
||||
Alerts: bundle.Alerts,
|
||||
Discussion: bundle.Discussion,
|
||||
Daily: bundle.Daily,
|
||||
WeatherStory: bundle.WeatherStory,
|
||||
SPCConvectiveOutlooks: bundle.SPCConvectiveOutlooks,
|
||||
SourceProvenance: append([]weatherdata.Source(nil), bundle.Sources...),
|
||||
SourceWarnings: append([]weatherdata.SourceWarning(nil), bundle.Warnings...),
|
||||
}
|
||||
}
|
||||
|
||||
func (f CollectedFacts) Bundle() *weatherdata.Bundle {
|
||||
return &weatherdata.Bundle{
|
||||
FetchedAt: f.FetchedAt,
|
||||
Observation: f.Observation,
|
||||
Current: f.Current,
|
||||
Hourly: f.Hourly,
|
||||
Narrative: f.Narrative,
|
||||
Alerts: f.Alerts,
|
||||
Discussion: f.Discussion,
|
||||
Daily: f.Daily,
|
||||
WeatherStory: f.WeatherStory,
|
||||
Sources: append([]weatherdata.Source(nil), f.SourceProvenance...),
|
||||
Warnings: append([]weatherdata.SourceWarning(nil), f.SourceWarnings...),
|
||||
FetchedAt: f.FetchedAt,
|
||||
Observation: f.Observation,
|
||||
Current: f.Current,
|
||||
Hourly: f.Hourly,
|
||||
Narrative: f.Narrative,
|
||||
Alerts: f.Alerts,
|
||||
Discussion: f.Discussion,
|
||||
Daily: f.Daily,
|
||||
WeatherStory: f.WeatherStory,
|
||||
SPCConvectiveOutlooks: f.SPCConvectiveOutlooks,
|
||||
Sources: append([]weatherdata.Source(nil), f.SourceProvenance...),
|
||||
Warnings: append([]weatherdata.SourceWarning(nil), f.SourceWarnings...),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,24 @@ func TestBuildCollectedCopiesBundleFactsAndKeepsSourcesSeparate(t *testing.T) {
|
||||
FetchedAt: fetchedAt,
|
||||
Current: &weatherdata.Current{ConditionText: "Clear"},
|
||||
Hourly: &weatherdata.ForecastRun{Product: "hourly"},
|
||||
Sources: []weatherdata.Source{{Name: "hourly"}},
|
||||
Warnings: []weatherdata.SourceWarning{{Source: "discussion", Code: "missing_source"}},
|
||||
SPCConvectiveOutlooks: &weatherdata.ConvectiveOutlookRun{
|
||||
Product: "convective_outlook",
|
||||
Outlooks: []weatherdata.ConvectiveOutlook{{
|
||||
ID: "day1-categorical-slight",
|
||||
Label: "SLGT",
|
||||
}},
|
||||
},
|
||||
Sources: []weatherdata.Source{{Name: "hourly"}},
|
||||
Warnings: []weatherdata.SourceWarning{{Source: "discussion", Code: "missing_source"}},
|
||||
}
|
||||
|
||||
collected := BuildCollected(bundle)
|
||||
if collected.FetchedAt != fetchedAt || collected.Current.ConditionText != "Clear" || collected.Hourly.Product != "hourly" {
|
||||
t.Fatalf("CollectedFacts = %#v, want source facts copied from bundle", collected)
|
||||
}
|
||||
if collected.SPCConvectiveOutlooks == nil || collected.SPCConvectiveOutlooks.Outlooks[0].Label != "SLGT" {
|
||||
t.Fatalf("SPCConvectiveOutlooks = %#v, want source copied from bundle", collected.SPCConvectiveOutlooks)
|
||||
}
|
||||
if len(collected.SourceProvenance) != 1 || collected.SourceProvenance[0].Name != "hourly" {
|
||||
t.Fatalf("SourceProvenance = %#v, want hourly source", collected.SourceProvenance)
|
||||
}
|
||||
@@ -36,6 +46,11 @@ func TestBuildCollectedCopiesBundleFactsAndKeepsSourcesSeparate(t *testing.T) {
|
||||
if collected.SourceProvenance[0].Name != "hourly" || collected.SourceWarnings[0].Source != "discussion" {
|
||||
t.Fatalf("collected source slices changed after bundle mutation: %#v %#v", collected.SourceProvenance, collected.SourceWarnings)
|
||||
}
|
||||
|
||||
roundTrip := collected.Bundle()
|
||||
if roundTrip.SPCConvectiveOutlooks == nil || roundTrip.SPCConvectiveOutlooks.Outlooks[0].ID != "day1-categorical-slight" {
|
||||
t.Fatalf("Bundle().SPCConvectiveOutlooks = %#v, want collected source restored", roundTrip.SPCConvectiveOutlooks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDerivedDailySlicesDaypartsAndAlerts(t *testing.T) {
|
||||
|
||||
@@ -21,6 +21,8 @@ const (
|
||||
AlertDigest ID = "alert_digest"
|
||||
AreaForecastDiscussion ID = "area_forecast_discussion"
|
||||
WeatherStory ID = "weather_story"
|
||||
SPCConvectiveOutlooks ID = "spc_convective_outlooks"
|
||||
SPCConvectiveDiscussion ID = "spc_convective_discussion"
|
||||
OutdoorWindows ID = "outdoor_windows"
|
||||
TomorrowPlanning ID = "tomorrow_planning"
|
||||
)
|
||||
@@ -105,19 +107,21 @@ func StanzaValue[T any](s Snapshot, name string) (T, bool, error) {
|
||||
type FactRequirement string
|
||||
|
||||
const (
|
||||
CollectedCurrentConditions FactRequirement = "collected.current_conditions"
|
||||
CollectedNarrativeForecast FactRequirement = "collected.narrative_forecast"
|
||||
CollectedHourlyForecast FactRequirement = "collected.hourly_forecast"
|
||||
CollectedAlerts FactRequirement = "collected.alerts"
|
||||
CollectedDiscussion FactRequirement = "collected.discussion"
|
||||
CollectedWeatherStory FactRequirement = "collected.weather_story"
|
||||
CollectedSourceMetadata FactRequirement = "collected.source_metadata"
|
||||
RequiresDerivedHourlyPeriods FactRequirement = "derived.hourly_periods"
|
||||
RequiresDerivedNarrativePeriods FactRequirement = "derived.narrative_periods"
|
||||
RequiresDerivedAlertOverlaps FactRequirement = "derived.alert_overlaps"
|
||||
RequiresDerivedDailySummaries FactRequirement = "derived.daily_summaries"
|
||||
RequiresDerivedDaypartSummaries FactRequirement = "derived.daypart_summaries"
|
||||
RequiresDerivedPrecipTiming FactRequirement = "derived.precip_timing"
|
||||
CollectedCurrentConditions FactRequirement = "collected.current_conditions"
|
||||
CollectedNarrativeForecast FactRequirement = "collected.narrative_forecast"
|
||||
CollectedHourlyForecast FactRequirement = "collected.hourly_forecast"
|
||||
CollectedAlerts FactRequirement = "collected.alerts"
|
||||
CollectedDiscussion FactRequirement = "collected.discussion"
|
||||
CollectedWeatherStory FactRequirement = "collected.weather_story"
|
||||
CollectedSPCConvectiveOutlooks FactRequirement = "collected.spc_convective_outlooks"
|
||||
CollectedSourceMetadata FactRequirement = "collected.source_metadata"
|
||||
RequiresDerivedHourlyPeriods FactRequirement = "derived.hourly_periods"
|
||||
RequiresDerivedNarrativePeriods FactRequirement = "derived.narrative_periods"
|
||||
RequiresDerivedAlertOverlaps FactRequirement = "derived.alert_overlaps"
|
||||
RequiresDerivedDailySummaries FactRequirement = "derived.daily_summaries"
|
||||
RequiresDerivedDaypartSummaries FactRequirement = "derived.daypart_summaries"
|
||||
RequiresDerivedPrecipTiming FactRequirement = "derived.precip_timing"
|
||||
RequiresDerivedSPCConvectiveOutlooks FactRequirement = "derived.spc_convective_outlooks"
|
||||
)
|
||||
|
||||
type MissingDataBehavior string
|
||||
@@ -141,5 +145,7 @@ type AreaForecastDiscussionOptions struct {
|
||||
Sections []string `json:"sections,omitempty" yaml:"sections,omitempty"`
|
||||
}
|
||||
type WeatherStoryOptions struct{}
|
||||
type SPCConvectiveOutlooksOptions struct{}
|
||||
type SPCConvectiveDiscussionOptions struct{}
|
||||
type OutdoorWindowsOptions struct{}
|
||||
type TomorrowPlanningOptions struct{}
|
||||
|
||||
@@ -82,3 +82,21 @@ func TestStanzaValueDecodesTypedOutput(t *testing.T) {
|
||||
t.Fatal("StanzaValue(missing) found = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSPCConvectiveModuleContractsAreStable(t *testing.T) {
|
||||
if SPCConvectiveOutlooks != ID("spc_convective_outlooks") {
|
||||
t.Fatalf("SPCConvectiveOutlooks = %q, want stable source/module ID", SPCConvectiveOutlooks)
|
||||
}
|
||||
if SPCConvectiveDiscussion != ID("spc_convective_discussion") {
|
||||
t.Fatalf("SPCConvectiveDiscussion = %q, want stable discussion module ID", SPCConvectiveDiscussion)
|
||||
}
|
||||
if CollectedSPCConvectiveOutlooks != FactRequirement("collected.spc_convective_outlooks") {
|
||||
t.Fatalf("CollectedSPCConvectiveOutlooks = %q, want collected requirement", CollectedSPCConvectiveOutlooks)
|
||||
}
|
||||
if RequiresDerivedSPCConvectiveOutlooks != FactRequirement("derived.spc_convective_outlooks") {
|
||||
t.Fatalf("RequiresDerivedSPCConvectiveOutlooks = %q, want derived requirement", RequiresDerivedSPCConvectiveOutlooks)
|
||||
}
|
||||
|
||||
_ = SPCConvectiveOutlooksOptions{}
|
||||
_ = SPCConvectiveDiscussionOptions{}
|
||||
}
|
||||
|
||||
@@ -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