diff --git a/cmd/weatherfeeder/main_test.go b/cmd/weatherfeeder/main_test.go index d8494f3..4508d42 100644 --- a/cmd/weatherfeeder/main_test.go +++ b/cmd/weatherfeeder/main_test.go @@ -20,6 +20,7 @@ import ( wfnormalizers "gitea.maximumdirect.net/ejr/weatherfeeder/internal/normalizers" wfsources "gitea.maximumdirect.net/ejr/weatherfeeder/internal/sources" + "gitea.maximumdirect.net/ejr/weatherfeeder/standards" ) type testInput struct { @@ -36,10 +37,10 @@ type testKindsSource struct { func (s testKindsSource) Kinds() []fkevent.Kind { return s.kinds } func TestValidateSourceExpectedKindsSubsetAllowed(t *testing.T) { - sc := config.SourceConfig{Kinds: []string{"observation"}} + sc := config.SourceConfig{Kinds: []string{standards.KindObservation}} in := testKindsSource{ testInput: testInput{name: "test"}, - kinds: []fkevent.Kind{"observation", "forecast"}, + kinds: []fkevent.Kind{fkevent.Kind(standards.KindObservation), fkevent.Kind(standards.KindForecast)}, } if err := fksources.ValidateExpectedKinds(sc, in); err != nil { @@ -48,10 +49,10 @@ func TestValidateSourceExpectedKindsSubsetAllowed(t *testing.T) { } func TestValidateSourceExpectedKindsMismatchFails(t *testing.T) { - sc := config.SourceConfig{Kinds: []string{"alert"}} + sc := config.SourceConfig{Kinds: []string{standards.KindAlert}} in := testKindsSource{ testInput: testInput{name: "test"}, - kinds: []fkevent.Kind{"observation", "forecast"}, + kinds: []fkevent.Kind{fkevent.Kind(standards.KindObservation), fkevent.Kind(standards.KindForecast)}, } err := fksources.ValidateExpectedKinds(sc, in) @@ -64,7 +65,7 @@ func TestValidateSourceExpectedKindsMismatchFails(t *testing.T) { } func TestValidateSourceExpectedKindsNoMetadataSkipsCheck(t *testing.T) { - sc := config.SourceConfig{Kinds: []string{"alert"}} + sc := config.SourceConfig{Kinds: []string{standards.KindAlert}} in := testInput{name: "test"} if err := fksources.ValidateExpectedKinds(sc, in); err != nil { @@ -158,7 +159,7 @@ func TestNormalizeNoMatchPassThrough(t *testing.T) { pl := &fkpipeline.Pipeline{Processors: chain} in := fkevent.Event{ ID: "evt-no-match", - Kind: fkevent.Kind("observation"), + Kind: fkevent.Kind(standards.KindObservation), Source: "test", EmittedAt: time.Now().UTC(), Schema: "raw.weatherfeeder.unknown.v1", @@ -188,7 +189,7 @@ func TestDedupeDropsSecondEventWithSameID(t *testing.T) { pl := &fkpipeline.Pipeline{Processors: chain} in := fkevent.Event{ ID: "evt-dedupe-1", - Kind: fkevent.Kind("observation"), + Kind: fkevent.Kind(standards.KindObservation), Source: "test", EmittedAt: time.Now().UTC(), Schema: "raw.weatherfeeder.unknown.v1", diff --git a/internal/normalizers/common/finalize_test.go b/internal/normalizers/common/finalize_test.go index d32e898..03ef61f 100644 --- a/internal/normalizers/common/finalize_test.go +++ b/internal/normalizers/common/finalize_test.go @@ -5,6 +5,7 @@ import ( "time" "gitea.maximumdirect.net/ejr/feedkit/event" + "gitea.maximumdirect.net/ejr/weatherfeeder/standards" ) func TestFinalizeRoundsWeatherPayloadFloats(t *testing.T) { @@ -14,7 +15,7 @@ func TestFinalizeRoundsWeatherPayloadFloats(t *testing.T) { in := event.Event{ ID: "evt-1", - Kind: event.Kind("observation"), + Kind: event.Kind(standards.KindObservation), Source: "source-a", EmittedAt: time.Date(2026, 3, 28, 12, 0, 0, 0, time.UTC), Schema: "raw.example.v1", diff --git a/internal/normalizers/nws/forecast_discussion_test.go b/internal/normalizers/nws/forecast_discussion_test.go index 3e7332a..bbe864f 100644 --- a/internal/normalizers/nws/forecast_discussion_test.go +++ b/internal/normalizers/nws/forecast_discussion_test.go @@ -18,7 +18,7 @@ func TestForecastDiscussionNormalizerProducesCanonicalSchema(t *testing.T) { out, err := (ForecastDiscussionNormalizer{}).Normalize(nil, event.Event{ ID: "evt-discussion-1", - Kind: event.Kind("forecast_discussion"), + Kind: event.Kind(standards.KindForecastDiscussion), Source: "nws-discussion-test", EmittedAt: time.Date(2026, 3, 28, 19, 25, 0, 0, time.UTC), Schema: standards.SchemaRawNWSForecastDiscussionV1, @@ -33,7 +33,7 @@ func TestForecastDiscussionNormalizerProducesCanonicalSchema(t *testing.T) { if out.Schema != standards.SchemaWeatherForecastDiscussionV1 { t.Fatalf("Schema = %q, want %q", out.Schema, standards.SchemaWeatherForecastDiscussionV1) } - if out.Kind != event.Kind("forecast_discussion") { + if out.Kind != event.Kind(standards.KindForecastDiscussion) { t.Fatalf("Kind = %q, want forecast_discussion", out.Kind) } @@ -74,7 +74,7 @@ func TestForecastDiscussionNormalizerProducesCanonicalSchema(t *testing.T) { func TestForecastDiscussionNormalizerRejectsMissingIssueTime(t *testing.T) { _, err := (ForecastDiscussionNormalizer{}).Normalize(nil, event.Event{ ID: "evt-discussion-bad", - Kind: event.Kind("forecast_discussion"), + Kind: event.Kind(standards.KindForecastDiscussion), Source: "nws-discussion-test", EmittedAt: time.Date(2026, 3, 28, 19, 25, 0, 0, time.UTC), Schema: standards.SchemaRawNWSForecastDiscussionV1, @@ -93,7 +93,7 @@ func TestForecastDiscussionNormalizerWireShapeHasNoUnexpectedKeys(t *testing.T) out, err := (ForecastDiscussionNormalizer{}).Normalize(nil, event.Event{ ID: "evt-discussion-2", - Kind: event.Kind("forecast_discussion"), + Kind: event.Kind(standards.KindForecastDiscussion), Source: "nws-discussion-test", EmittedAt: time.Date(2026, 3, 28, 19, 25, 0, 0, time.UTC), Schema: standards.SchemaRawNWSForecastDiscussionV1, diff --git a/internal/normalizers/nws/forecast_test.go b/internal/normalizers/nws/forecast_test.go index 3f16c41..a722e0d 100644 --- a/internal/normalizers/nws/forecast_test.go +++ b/internal/normalizers/nws/forecast_test.go @@ -182,7 +182,7 @@ func TestNormalizeForecastEventBySchemaProducesCanonicalWeatherForecastSchema(t t.Run(tt.name, func(t *testing.T) { out, err := normalizeForecastEventBySchema(event.Event{ ID: "evt-1", - Kind: event.Kind("forecast"), + Kind: event.Kind(standards.KindForecast), Source: "nws-test", EmittedAt: time.Date(2026, 3, 16, 18, 0, 0, 0, time.UTC), Schema: tt.schema, diff --git a/internal/normalizers/nws/weatherstories_test.go b/internal/normalizers/nws/weatherstories_test.go index afaa33d..2dfdd16 100644 --- a/internal/normalizers/nws/weatherstories_test.go +++ b/internal/normalizers/nws/weatherstories_test.go @@ -22,7 +22,7 @@ func TestWeatherStoriesNormalizerProducesCanonicalSchemaAndMapsSample(t *testing if out.Schema != standards.SchemaWeatherStoryV1 { t.Fatalf("Schema = %q, want %q", out.Schema, standards.SchemaWeatherStoryV1) } - if out.Kind != event.Kind("weather_story") { + if out.Kind != event.Kind(standards.KindWeatherStory) { t.Fatalf("Kind = %q, want weather_story", out.Kind) } @@ -118,7 +118,7 @@ func TestWeatherStoriesNormalizerMatch(t *testing.T) { func weatherStoriesRawEvent(payload string) event.Event { return event.Event{ ID: "evt-weatherstories-1", - Kind: event.Kind("weather_story"), + Kind: event.Kind(standards.KindWeatherStory), Source: "nws-weatherstories-test", EmittedAt: time.Date(2026, 5, 30, 9, 5, 0, 0, time.UTC), Schema: standards.SchemaRawNWSWeatherStoriesV1, diff --git a/internal/normalizers/spc/convective_outlook_test.go b/internal/normalizers/spc/convective_outlook_test.go index aa59322..370af4f 100644 --- a/internal/normalizers/spc/convective_outlook_test.go +++ b/internal/normalizers/spc/convective_outlook_test.go @@ -32,7 +32,7 @@ func TestConvectiveOutlookNormalizerProducesCanonicalSchemaAndMapsSample(t *test if out.Schema != standards.SchemaWeatherOutlookV1 { t.Fatalf("Schema = %q, want %q", out.Schema, standards.SchemaWeatherOutlookV1) } - if out.Kind != event.Kind("outlook") { + if out.Kind != event.Kind(standards.KindOutlook) { t.Fatalf("Kind = %q, want outlook", out.Kind) } @@ -276,7 +276,7 @@ func spcRawEvent(t *testing.T, bundle spcprovider.RawConvectiveOutlookBundle) ev effectiveAt := time.Date(2026, 6, 11, 19, 45, 0, 0, time.UTC) return event.Event{ ID: "evt-spc-outlook-1", - Kind: event.Kind("outlook"), + Kind: event.Kind(standards.KindOutlook), Source: "spc-test", EmittedAt: time.Date(2026, 6, 11, 20, 5, 0, 0, time.UTC), EffectiveAt: &effectiveAt, diff --git a/internal/sources/builtins.go b/internal/sources/builtins.go index b7246bd..836237a 100644 --- a/internal/sources/builtins.go +++ b/internal/sources/builtins.go @@ -16,20 +16,20 @@ type pollDriverRegistration struct { } var pollDriverRegistrations = []pollDriverRegistration{ - {driver: "nws_observation", factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return nws.NewObservationSource(cfg) }}, - {driver: "nws_alerts", factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return nws.NewAlertsSource(cfg) }}, - {driver: "nws_forecast_hourly", factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return nws.NewHourlyForecastSource(cfg) }}, - {driver: "nws_forecast_narrative", factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return nws.NewNarrativeForecastSource(cfg) }}, - {driver: "nws_forecast_discussion", factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { + {driver: nws.DriverObservation, factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return nws.NewObservationSource(cfg) }}, + {driver: nws.DriverAlerts, factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return nws.NewAlertsSource(cfg) }}, + {driver: nws.DriverForecastHourly, factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return nws.NewHourlyForecastSource(cfg) }}, + {driver: nws.DriverForecastNarrative, factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return nws.NewNarrativeForecastSource(cfg) }}, + {driver: nws.DriverForecastDiscussion, factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return nws.NewForecastDiscussionSource(cfg) }}, - {driver: "nws_weatherstories", factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return nws.NewWeatherStoriesSource(cfg) }}, - {driver: "openmeteo_observation", factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return openmeteo.NewObservationSource(cfg) }}, - {driver: "openmeteo_forecast", factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return openmeteo.NewForecastSource(cfg) }}, - {driver: "openweather_observation", factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { + {driver: nws.DriverWeatherStories, factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return nws.NewWeatherStoriesSource(cfg) }}, + {driver: openmeteo.DriverObservation, factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return openmeteo.NewObservationSource(cfg) }}, + {driver: openmeteo.DriverForecast, factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return openmeteo.NewForecastSource(cfg) }}, + {driver: openweather.DriverObservation, factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return openweather.NewObservationSource(cfg) }}, - {driver: "spc_convective_outlook", factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { + {driver: spc.DriverConvectiveOutlook, factory: func(cfg config.SourceConfig) (fksource.PollSource, error) { return spc.NewConvectiveOutlookSource(cfg) }}, } diff --git a/internal/sources/builtins_test.go b/internal/sources/builtins_test.go index e1bd186..0781157 100644 --- a/internal/sources/builtins_test.go +++ b/internal/sources/builtins_test.go @@ -6,13 +6,17 @@ import ( "gitea.maximumdirect.net/ejr/feedkit/config" fksource "gitea.maximumdirect.net/ejr/feedkit/sources" + "gitea.maximumdirect.net/ejr/weatherfeeder/internal/sources/nws" + "gitea.maximumdirect.net/ejr/weatherfeeder/internal/sources/openmeteo" + "gitea.maximumdirect.net/ejr/weatherfeeder/internal/sources/openweather" + "gitea.maximumdirect.net/ejr/weatherfeeder/internal/sources/spc" ) func TestRegisterBuiltinsRegistersNWSHourlyForecastDriver(t *testing.T) { reg := fksource.NewRegistry() RegisterBuiltins(reg) - in, err := reg.BuildInput(sourceConfigForDriver("nws_forecast_hourly")) + in, err := reg.BuildInput(sourceConfigForDriver(nws.DriverForecastHourly)) if err != nil { t.Fatalf("BuildInput(nws_forecast_hourly) error = %v", err) } @@ -25,7 +29,7 @@ func TestRegisterBuiltinsRegistersNWSNarrativeForecastDriver(t *testing.T) { reg := fksource.NewRegistry() RegisterBuiltins(reg) - in, err := reg.BuildInput(sourceConfigForDriver("nws_forecast_narrative")) + in, err := reg.BuildInput(sourceConfigForDriver(nws.DriverForecastNarrative)) if err != nil { t.Fatalf("BuildInput(nws_forecast_narrative) error = %v", err) } @@ -38,7 +42,7 @@ func TestRegisterBuiltinsRegistersNWSForecastDiscussionDriver(t *testing.T) { reg := fksource.NewRegistry() RegisterBuiltins(reg) - in, err := reg.BuildInput(sourceConfigForDriver("nws_forecast_discussion")) + in, err := reg.BuildInput(sourceConfigForDriver(nws.DriverForecastDiscussion)) if err != nil { t.Fatalf("BuildInput(nws_forecast_discussion) error = %v", err) } @@ -51,7 +55,7 @@ func TestRegisterBuiltinsRegistersNWSWeatherStoriesDriver(t *testing.T) { reg := fksource.NewRegistry() RegisterBuiltins(reg) - in, err := reg.BuildInput(sourceConfigForDriver("nws_weatherstories")) + in, err := reg.BuildInput(sourceConfigForDriver(nws.DriverWeatherStories)) if err != nil { t.Fatalf("BuildInput(nws_weatherstories) error = %v", err) } @@ -78,16 +82,16 @@ func TestRegisterBuiltinsRegistersAllCurrentDrivers(t *testing.T) { RegisterBuiltins(reg) drivers := []string{ - "nws_observation", - "nws_alerts", - "nws_forecast_hourly", - "nws_forecast_narrative", - "nws_forecast_discussion", - "nws_weatherstories", - "openmeteo_observation", - "openmeteo_forecast", - "openweather_observation", - "spc_convective_outlook", + nws.DriverObservation, + nws.DriverAlerts, + nws.DriverForecastHourly, + nws.DriverForecastNarrative, + nws.DriverForecastDiscussion, + nws.DriverWeatherStories, + openmeteo.DriverObservation, + openmeteo.DriverForecast, + openweather.DriverObservation, + spc.DriverConvectiveOutlook, } for _, driver := range drivers { @@ -103,14 +107,14 @@ func TestRegisterBuiltinsRegistersAllCurrentDrivers(t *testing.T) { func sourceConfigForDriver(driver string) config.SourceConfig { url := "https://example.invalid" - if driver == "openweather_observation" { + if driver == openweather.DriverObservation { url = "https://example.invalid?units=metric" } params := map[string]any{ "url": url, "user_agent": "test-agent", } - if driver == "spc_convective_outlook" { + if driver == spc.DriverConvectiveOutlook { params["latitude"] = 38.6239 params["longitude"] = -90.3571 } diff --git a/internal/sources/nws/alerts.go b/internal/sources/nws/alerts.go index f244c46..9bb5079 100644 --- a/internal/sources/nws/alerts.go +++ b/internal/sources/nws/alerts.go @@ -26,10 +26,8 @@ type AlertsSource struct { } func NewAlertsSource(cfg config.SourceConfig) (*AlertsSource, error) { - const driver = "nws_alerts" - // NWS alerts responses are GeoJSON-ish; allow fallback to plain JSON as well. - hs, err := fksources.NewHTTPSource(driver, cfg, "application/geo+json, application/json") + hs, err := fksources.NewHTTPSource(DriverAlerts, cfg, "application/geo+json, application/json") if err != nil { return nil, err } @@ -40,7 +38,7 @@ func NewAlertsSource(cfg config.SourceConfig) (*AlertsSource, error) { func (s *AlertsSource) Name() string { return s.http.Name } // Kinds is used for routing/policy. -func (s *AlertsSource) Kinds() []event.Kind { return []event.Kind{event.Kind("alert")} } +func (s *AlertsSource) Kinds() []event.Kind { return []event.Kind{event.Kind(standards.KindAlert)} } func (s *AlertsSource) Poll(ctx context.Context) ([]event.Event, error) { raw, meta, changed, err := s.fetchRaw(ctx) @@ -71,7 +69,7 @@ func (s *AlertsSource) Poll(ctx context.Context) ([]event.Event, error) { eventID := fksources.DefaultEventID("", s.http.Name, effectiveAt, emittedAt) return fksources.SingleEvent( - event.Kind("alert"), + event.Kind(standards.KindAlert), s.http.Name, standards.SchemaRawNWSAlertsV1, eventID, diff --git a/internal/sources/nws/driver.go b/internal/sources/nws/driver.go new file mode 100644 index 0000000..bf7d930 --- /dev/null +++ b/internal/sources/nws/driver.go @@ -0,0 +1,11 @@ +package nws + +// Source driver strings registered by weatherfeeder for NWS sources. +const ( + DriverObservation = "nws_observation" + DriverAlerts = "nws_alerts" + DriverForecastHourly = "nws_forecast_hourly" + DriverForecastNarrative = "nws_forecast_narrative" + DriverForecastDiscussion = "nws_forecast_discussion" + DriverWeatherStories = "nws_weatherstories" +) diff --git a/internal/sources/nws/forecast_common.go b/internal/sources/nws/forecast_common.go index 3d67b03..5210d85 100644 --- a/internal/sources/nws/forecast_common.go +++ b/internal/sources/nws/forecast_common.go @@ -10,6 +10,7 @@ import ( "gitea.maximumdirect.net/ejr/feedkit/event" fksources "gitea.maximumdirect.net/ejr/feedkit/sources" nwscommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/providers/nws" + "gitea.maximumdirect.net/ejr/weatherfeeder/standards" ) const nwsForecastAccept = "application/geo+json, application/json" @@ -44,7 +45,9 @@ func newForecastSource(cfg config.SourceConfig, driver, rawSchema string) (*fore func (s *forecastSource) Name() string { return s.http.Name } -func (s *forecastSource) Kinds() []event.Kind { return []event.Kind{event.Kind("forecast")} } +func (s *forecastSource) Kinds() []event.Kind { + return []event.Kind{event.Kind(standards.KindForecast)} +} func (s *forecastSource) Poll(ctx context.Context) ([]event.Event, error) { raw, meta, changed, err := s.fetchRaw(ctx) @@ -69,7 +72,7 @@ func (s *forecastSource) Poll(ctx context.Context) ([]event.Event, error) { eventID := fksources.DefaultEventID("", s.http.Name, effectiveAt, emittedAt) return fksources.SingleEvent( - event.Kind("forecast"), + event.Kind(standards.KindForecast), s.http.Name, s.rawSchema, eventID, diff --git a/internal/sources/nws/forecast_discussion.go b/internal/sources/nws/forecast_discussion.go index ce50b8b..0dc528b 100644 --- a/internal/sources/nws/forecast_discussion.go +++ b/internal/sources/nws/forecast_discussion.go @@ -20,9 +20,7 @@ type ForecastDiscussionSource struct { } func NewForecastDiscussionSource(cfg config.SourceConfig) (*ForecastDiscussionSource, error) { - const driver = "nws_forecast_discussion" - - hs, err := fksources.NewHTTPSource(driver, cfg, "text/html, application/xhtml+xml") + hs, err := fksources.NewHTTPSource(DriverForecastDiscussion, cfg, "text/html, application/xhtml+xml") if err != nil { return nil, err } @@ -33,7 +31,7 @@ func NewForecastDiscussionSource(cfg config.SourceConfig) (*ForecastDiscussionSo func (s *ForecastDiscussionSource) Name() string { return s.http.Name } func (s *ForecastDiscussionSource) Kinds() []event.Kind { - return []event.Kind{event.Kind("forecast_discussion")} + return []event.Kind{event.Kind(standards.KindForecastDiscussion)} } func (s *ForecastDiscussionSource) Poll(ctx context.Context) ([]event.Event, error) { @@ -57,7 +55,7 @@ func (s *ForecastDiscussionSource) Poll(ctx context.Context) ([]event.Event, err eventID := fksources.DefaultEventID("", s.http.Name, effectiveAt, emittedAt) return fksources.SingleEvent( - event.Kind("forecast_discussion"), + event.Kind(standards.KindForecastDiscussion), s.http.Name, standards.SchemaRawNWSForecastDiscussionV1, eventID, diff --git a/internal/sources/nws/forecast_discussion_test.go b/internal/sources/nws/forecast_discussion_test.go index 271f339..1dc1927 100644 --- a/internal/sources/nws/forecast_discussion_test.go +++ b/internal/sources/nws/forecast_discussion_test.go @@ -27,7 +27,7 @@ func TestForecastDiscussionSourcePollEmitsExpectedEvent(t *testing.T) { if err != nil { t.Fatalf("NewForecastDiscussionSource() error = %v", err) } - if got := src.Kinds(); len(got) != 1 || got[0] != event.Kind("forecast_discussion") { + if got := src.Kinds(); len(got) != 1 || got[0] != event.Kind(standards.KindForecastDiscussion) { t.Fatalf("Kinds() = %#v, want [forecast_discussion]", got) } @@ -40,7 +40,7 @@ func TestForecastDiscussionSourcePollEmitsExpectedEvent(t *testing.T) { } got := events[0] - if got.Kind != event.Kind("forecast_discussion") { + if got.Kind != event.Kind(standards.KindForecastDiscussion) { t.Fatalf("Kind = %q, want forecast_discussion", got.Kind) } if got.Schema != standards.SchemaRawNWSForecastDiscussionV1 { @@ -117,7 +117,7 @@ func TestForecastDiscussionSourcePollRejectsInvalidHTML(t *testing.T) { func forecastDiscussionSourceConfig(url string) config.SourceConfig { return config.SourceConfig{ Name: "test-forecast-discussion-source", - Driver: "nws_forecast_discussion", + Driver: DriverForecastDiscussion, Mode: config.SourceModePoll, Params: map[string]any{ "url": url, diff --git a/internal/sources/nws/forecast_hourly.go b/internal/sources/nws/forecast_hourly.go index 3cf5e14..5b5c5e0 100644 --- a/internal/sources/nws/forecast_hourly.go +++ b/internal/sources/nws/forecast_hourly.go @@ -18,8 +18,7 @@ type HourlyForecastSource struct { } func NewHourlyForecastSource(cfg config.SourceConfig) (*HourlyForecastSource, error) { - const driver = "nws_forecast_hourly" - src, err := newForecastSource(cfg, driver, standards.SchemaRawNWSHourlyForecastV1) + src, err := newForecastSource(cfg, DriverForecastHourly, standards.SchemaRawNWSHourlyForecastV1) if err != nil { return nil, err } diff --git a/internal/sources/nws/forecast_narrative.go b/internal/sources/nws/forecast_narrative.go index ab02560..e8b24b2 100644 --- a/internal/sources/nws/forecast_narrative.go +++ b/internal/sources/nws/forecast_narrative.go @@ -18,8 +18,7 @@ type NarrativeForecastSource struct { } func NewNarrativeForecastSource(cfg config.SourceConfig) (*NarrativeForecastSource, error) { - const driver = "nws_forecast_narrative" - src, err := newForecastSource(cfg, driver, standards.SchemaRawNWSNarrativeForecastV1) + src, err := newForecastSource(cfg, DriverForecastNarrative, standards.SchemaRawNWSNarrativeForecastV1) if err != nil { return nil, err } diff --git a/internal/sources/nws/forecast_test.go b/internal/sources/nws/forecast_test.go index 89ca44f..fcaf120 100644 --- a/internal/sources/nws/forecast_test.go +++ b/internal/sources/nws/forecast_test.go @@ -26,7 +26,7 @@ func TestForecastSourcesEmitExpectedSchemaAndPreferGeneratedAt(t *testing.T) { }{ { name: "hourly", - driver: "nws_forecast_hourly", + driver: DriverForecastHourly, wantSchema: standards.SchemaRawNWSHourlyForecastV1, newSource: func(cfg config.SourceConfig) (forecastPoller, error) { return NewHourlyForecastSource(cfg) @@ -34,7 +34,7 @@ func TestForecastSourcesEmitExpectedSchemaAndPreferGeneratedAt(t *testing.T) { }, { name: "narrative", - driver: "nws_forecast_narrative", + driver: DriverForecastNarrative, wantSchema: standards.SchemaRawNWSNarrativeForecastV1, newSource: func(cfg config.SourceConfig) (forecastPoller, error) { return NewNarrativeForecastSource(cfg) @@ -55,7 +55,7 @@ func TestForecastSourcesEmitExpectedSchemaAndPreferGeneratedAt(t *testing.T) { } if ks, ok := src.(interface{ Kinds() []event.Kind }); !ok { t.Fatalf("source does not implement Kinds()") - } else if gotKinds := ks.Kinds(); len(gotKinds) != 1 || gotKinds[0] != event.Kind("forecast") { + } else if gotKinds := ks.Kinds(); len(gotKinds) != 1 || gotKinds[0] != event.Kind(standards.KindForecast) { t.Fatalf("Kinds() = %#v, want [forecast]", gotKinds) } @@ -69,7 +69,7 @@ func TestForecastSourcesEmitExpectedSchemaAndPreferGeneratedAt(t *testing.T) { if got[0].Schema != tt.wantSchema { t.Fatalf("Poll() schema = %q, want %q", got[0].Schema, tt.wantSchema) } - if got[0].Kind != event.Kind("forecast") { + if got[0].Kind != event.Kind(standards.KindForecast) { t.Fatalf("Poll() kind = %q, want forecast", got[0].Kind) } @@ -117,7 +117,7 @@ func TestForecastSourcePollEffectiveAtFallbackOrder(t *testing.T) { })) defer srv.Close() - src, err := NewHourlyForecastSource(forecastSourceConfig("nws_forecast_hourly", srv.URL)) + src, err := NewHourlyForecastSource(forecastSourceConfig(DriverForecastHourly, srv.URL)) if err != nil { t.Fatalf("NewHourlyForecastSource() error = %v", err) } @@ -148,7 +148,7 @@ func TestForecastSourcePollMetadataDecodeFailureStillEmitsRawEvent(t *testing.T) })) defer srv.Close() - src, err := NewNarrativeForecastSource(forecastSourceConfig("nws_forecast_narrative", srv.URL)) + src, err := NewNarrativeForecastSource(forecastSourceConfig(DriverForecastNarrative, srv.URL)) if err != nil { t.Fatalf("NewNarrativeForecastSource() error = %v", err) } diff --git a/internal/sources/nws/observation.go b/internal/sources/nws/observation.go index e139102..d9c6efd 100644 --- a/internal/sources/nws/observation.go +++ b/internal/sources/nws/observation.go @@ -20,9 +20,7 @@ type ObservationSource struct { } func NewObservationSource(cfg config.SourceConfig) (*ObservationSource, error) { - const driver = "nws_observation" - - hs, err := fksources.NewHTTPSource(driver, cfg, "application/geo+json, application/json") + hs, err := fksources.NewHTTPSource(DriverObservation, cfg, "application/geo+json, application/json") if err != nil { return nil, err } @@ -32,7 +30,9 @@ func NewObservationSource(cfg config.SourceConfig) (*ObservationSource, error) { func (s *ObservationSource) Name() string { return s.http.Name } -func (s *ObservationSource) Kinds() []event.Kind { return []event.Kind{event.Kind("observation")} } +func (s *ObservationSource) Kinds() []event.Kind { + return []event.Kind{event.Kind(standards.KindObservation)} +} func (s *ObservationSource) Poll(ctx context.Context) ([]event.Event, error) { raw, meta, changed, err := s.fetchRaw(ctx) @@ -54,7 +54,7 @@ func (s *ObservationSource) Poll(ctx context.Context) ([]event.Event, error) { eventID := fksources.DefaultEventID(meta.ID, s.http.Name, effectiveAt, emittedAt) return fksources.SingleEvent( - event.Kind("observation"), + event.Kind(standards.KindObservation), s.http.Name, standards.SchemaRawNWSObservationV1, eventID, diff --git a/internal/sources/nws/observation_test.go b/internal/sources/nws/observation_test.go index a6e20d7..7998cc2 100644 --- a/internal/sources/nws/observation_test.go +++ b/internal/sources/nws/observation_test.go @@ -8,6 +8,7 @@ import ( "gitea.maximumdirect.net/ejr/feedkit/config" "gitea.maximumdirect.net/ejr/feedkit/event" + "gitea.maximumdirect.net/ejr/weatherfeeder/standards" ) func TestObservationSourcePollReturnsNoEventsOn304(t *testing.T) { @@ -31,7 +32,7 @@ func TestObservationSourcePollReturnsNoEventsOn304(t *testing.T) { src, err := NewObservationSource(config.SourceConfig{ Name: "NWSObservationTest", - Driver: "nws_observation", + Driver: DriverObservation, Mode: config.SourceModePoll, Params: map[string]any{ "url": srv.URL, @@ -41,7 +42,7 @@ func TestObservationSourcePollReturnsNoEventsOn304(t *testing.T) { if err != nil { t.Fatalf("NewObservationSource() error = %v", err) } - if got := src.Kinds(); len(got) != 1 || got[0] != event.Kind("observation") { + if got := src.Kinds(); len(got) != 1 || got[0] != event.Kind(standards.KindObservation) { t.Fatalf("Kinds() = %#v, want [observation]", got) } @@ -52,7 +53,7 @@ func TestObservationSourcePollReturnsNoEventsOn304(t *testing.T) { if len(first) != 1 { t.Fatalf("first Poll() len = %d, want 1", len(first)) } - if first[0].Kind != event.Kind("observation") { + if first[0].Kind != event.Kind(standards.KindObservation) { t.Fatalf("first Poll() kind = %q", first[0].Kind) } diff --git a/internal/sources/nws/weatherstories.go b/internal/sources/nws/weatherstories.go index e6a1949..e9994c5 100644 --- a/internal/sources/nws/weatherstories.go +++ b/internal/sources/nws/weatherstories.go @@ -22,9 +22,7 @@ type WeatherStoriesSource struct { } func NewWeatherStoriesSource(cfg config.SourceConfig) (*WeatherStoriesSource, error) { - const driver = "nws_weatherstories" - - hs, err := fksources.NewHTTPSource(driver, cfg, "application/geo+json, application/json") + hs, err := fksources.NewHTTPSource(DriverWeatherStories, cfg, "application/geo+json, application/json") if err != nil { return nil, err } @@ -35,7 +33,7 @@ func NewWeatherStoriesSource(cfg config.SourceConfig) (*WeatherStoriesSource, er func (s *WeatherStoriesSource) Name() string { return s.http.Name } func (s *WeatherStoriesSource) Kinds() []event.Kind { - return []event.Kind{event.Kind("weather_story")} + return []event.Kind{event.Kind(standards.KindWeatherStory)} } func (s *WeatherStoriesSource) Poll(ctx context.Context) ([]event.Event, error) { @@ -61,7 +59,7 @@ func (s *WeatherStoriesSource) Poll(ctx context.Context) ([]event.Event, error) eventID := fksources.DefaultEventID("", s.http.Name, effectiveAt, emittedAt) return fksources.SingleEvent( - event.Kind("weather_story"), + event.Kind(standards.KindWeatherStory), s.http.Name, standards.SchemaRawNWSWeatherStoriesV1, eventID, diff --git a/internal/sources/nws/weatherstories_test.go b/internal/sources/nws/weatherstories_test.go index 4813013..d0591c0 100644 --- a/internal/sources/nws/weatherstories_test.go +++ b/internal/sources/nws/weatherstories_test.go @@ -28,7 +28,7 @@ func TestWeatherStoriesSourcePollEmitsExpectedEventAndPrefersLatestUpdateTime(t if err != nil { t.Fatalf("NewWeatherStoriesSource() error = %v", err) } - if got := src.Kinds(); len(got) != 1 || got[0] != event.Kind("weather_story") { + if got := src.Kinds(); len(got) != 1 || got[0] != event.Kind(standards.KindWeatherStory) { t.Fatalf("Kinds() = %#v, want [weather_story]", got) } @@ -41,7 +41,7 @@ func TestWeatherStoriesSourcePollEmitsExpectedEventAndPrefersLatestUpdateTime(t } got := events[0] - if got.Kind != event.Kind("weather_story") { + if got.Kind != event.Kind(standards.KindWeatherStory) { t.Fatalf("Kind = %q, want weather_story", got.Kind) } if got.Schema != standards.SchemaRawNWSWeatherStoriesV1 { @@ -148,7 +148,7 @@ func TestWeatherStoriesSourcePollMetadataDecodeFailureStillEmitsRawEvent(t *test func weatherStoriesSourceConfig(url string) config.SourceConfig { return config.SourceConfig{ Name: "test-weatherstories-source", - Driver: "nws_weatherstories", + Driver: DriverWeatherStories, Mode: config.SourceModePoll, Params: map[string]any{ "url": url, diff --git a/internal/sources/openmeteo/driver.go b/internal/sources/openmeteo/driver.go new file mode 100644 index 0000000..594e404 --- /dev/null +++ b/internal/sources/openmeteo/driver.go @@ -0,0 +1,7 @@ +package openmeteo + +// Source driver strings registered by weatherfeeder for Open-Meteo sources. +const ( + DriverObservation = "openmeteo_observation" + DriverForecast = "openmeteo_forecast" +) diff --git a/internal/sources/openmeteo/forecast.go b/internal/sources/openmeteo/forecast.go index 7749099..f4467cd 100644 --- a/internal/sources/openmeteo/forecast.go +++ b/internal/sources/openmeteo/forecast.go @@ -19,9 +19,7 @@ type ForecastSource struct { } func NewForecastSource(cfg config.SourceConfig) (*ForecastSource, error) { - const driver = "openmeteo_forecast" - - hs, err := fksources.NewHTTPSource(driver, cfg, "application/json") + hs, err := fksources.NewHTTPSource(DriverForecast, cfg, "application/json") if err != nil { return nil, err } @@ -31,7 +29,9 @@ func NewForecastSource(cfg config.SourceConfig) (*ForecastSource, error) { func (s *ForecastSource) Name() string { return s.http.Name } -func (s *ForecastSource) Kinds() []event.Kind { return []event.Kind{event.Kind("forecast")} } +func (s *ForecastSource) Kinds() []event.Kind { + return []event.Kind{event.Kind(standards.KindForecast)} +} func (s *ForecastSource) Poll(ctx context.Context) ([]event.Event, error) { raw, meta, changed, err := s.fetchRaw(ctx) @@ -55,7 +55,7 @@ func (s *ForecastSource) Poll(ctx context.Context) ([]event.Event, error) { eventID := fksources.DefaultEventID("", s.http.Name, effectiveAt, emittedAt) return fksources.SingleEvent( - event.Kind("forecast"), + event.Kind(standards.KindForecast), s.http.Name, standards.SchemaRawOpenMeteoHourlyForecastV1, eventID, diff --git a/internal/sources/openmeteo/observation.go b/internal/sources/openmeteo/observation.go index 6eeb276..07eab9a 100644 --- a/internal/sources/openmeteo/observation.go +++ b/internal/sources/openmeteo/observation.go @@ -19,9 +19,7 @@ type ObservationSource struct { } func NewObservationSource(cfg config.SourceConfig) (*ObservationSource, error) { - const driver = "openmeteo_observation" - - hs, err := fksources.NewHTTPSource(driver, cfg, "application/json") + hs, err := fksources.NewHTTPSource(DriverObservation, cfg, "application/json") if err != nil { return nil, err } @@ -31,7 +29,9 @@ func NewObservationSource(cfg config.SourceConfig) (*ObservationSource, error) { func (s *ObservationSource) Name() string { return s.http.Name } -func (s *ObservationSource) Kinds() []event.Kind { return []event.Kind{event.Kind("observation")} } +func (s *ObservationSource) Kinds() []event.Kind { + return []event.Kind{event.Kind(standards.KindObservation)} +} func (s *ObservationSource) Poll(ctx context.Context) ([]event.Event, error) { raw, meta, changed, err := s.fetchRaw(ctx) @@ -52,7 +52,7 @@ func (s *ObservationSource) Poll(ctx context.Context) ([]event.Event, error) { eventID := fksources.DefaultEventID("", s.http.Name, effectiveAt, emittedAt) return fksources.SingleEvent( - event.Kind("observation"), + event.Kind(standards.KindObservation), s.http.Name, standards.SchemaRawOpenMeteoCurrentV1, eventID, diff --git a/internal/sources/openmeteo/source_test.go b/internal/sources/openmeteo/source_test.go index b010678..3622816 100644 --- a/internal/sources/openmeteo/source_test.go +++ b/internal/sources/openmeteo/source_test.go @@ -5,12 +5,13 @@ import ( "gitea.maximumdirect.net/ejr/feedkit/config" "gitea.maximumdirect.net/ejr/feedkit/event" + "gitea.maximumdirect.net/ejr/weatherfeeder/standards" ) func TestObservationSourceAdvertisesKinds(t *testing.T) { src, err := NewObservationSource(config.SourceConfig{ Name: "openmeteo-observation-test", - Driver: "openmeteo_observation", + Driver: DriverObservation, Mode: config.SourceModePoll, Params: map[string]any{ "url": "https://example.invalid", @@ -20,7 +21,7 @@ func TestObservationSourceAdvertisesKinds(t *testing.T) { if err != nil { t.Fatalf("NewObservationSource() error = %v", err) } - if got := src.Kinds(); len(got) != 1 || got[0] != event.Kind("observation") { + if got := src.Kinds(); len(got) != 1 || got[0] != event.Kind(standards.KindObservation) { t.Fatalf("Kinds() = %#v, want [observation]", got) } } @@ -28,7 +29,7 @@ func TestObservationSourceAdvertisesKinds(t *testing.T) { func TestForecastSourceAdvertisesKinds(t *testing.T) { src, err := NewForecastSource(config.SourceConfig{ Name: "openmeteo-forecast-test", - Driver: "openmeteo_forecast", + Driver: DriverForecast, Mode: config.SourceModePoll, Params: map[string]any{ "url": "https://example.invalid", @@ -38,7 +39,7 @@ func TestForecastSourceAdvertisesKinds(t *testing.T) { if err != nil { t.Fatalf("NewForecastSource() error = %v", err) } - if got := src.Kinds(); len(got) != 1 || got[0] != event.Kind("forecast") { + if got := src.Kinds(); len(got) != 1 || got[0] != event.Kind(standards.KindForecast) { t.Fatalf("Kinds() = %#v, want [forecast]", got) } } diff --git a/internal/sources/openweather/driver.go b/internal/sources/openweather/driver.go new file mode 100644 index 0000000..13687d8 --- /dev/null +++ b/internal/sources/openweather/driver.go @@ -0,0 +1,6 @@ +package openweather + +// Source driver strings registered by weatherfeeder for OpenWeather sources. +const ( + DriverObservation = "openweather_observation" +) diff --git a/internal/sources/openweather/observation.go b/internal/sources/openweather/observation.go index cb99c42..3830ff4 100644 --- a/internal/sources/openweather/observation.go +++ b/internal/sources/openweather/observation.go @@ -19,9 +19,7 @@ type ObservationSource struct { } func NewObservationSource(cfg config.SourceConfig) (*ObservationSource, error) { - const driver = "openweather_observation" - - hs, err := fksources.NewHTTPSource(driver, cfg, "application/json") + hs, err := fksources.NewHTTPSource(DriverObservation, cfg, "application/json") if err != nil { return nil, err } @@ -35,7 +33,9 @@ func NewObservationSource(cfg config.SourceConfig) (*ObservationSource, error) { func (s *ObservationSource) Name() string { return s.http.Name } -func (s *ObservationSource) Kinds() []event.Kind { return []event.Kind{event.Kind("observation")} } +func (s *ObservationSource) Kinds() []event.Kind { + return []event.Kind{event.Kind(standards.KindObservation)} +} func (s *ObservationSource) Poll(ctx context.Context) ([]event.Event, error) { if err := owcommon.RequireMetricUnits(s.http.URL); err != nil { @@ -60,7 +60,7 @@ func (s *ObservationSource) Poll(ctx context.Context) ([]event.Event, error) { eventID := fksources.DefaultEventID("", s.http.Name, effectiveAt, emittedAt) return fksources.SingleEvent( - event.Kind("observation"), + event.Kind(standards.KindObservation), s.http.Name, standards.SchemaRawOpenWeatherCurrentV1, eventID, diff --git a/internal/sources/openweather/source_test.go b/internal/sources/openweather/source_test.go index 170c6e6..0270287 100644 --- a/internal/sources/openweather/source_test.go +++ b/internal/sources/openweather/source_test.go @@ -5,12 +5,13 @@ import ( "gitea.maximumdirect.net/ejr/feedkit/config" "gitea.maximumdirect.net/ejr/feedkit/event" + "gitea.maximumdirect.net/ejr/weatherfeeder/standards" ) func TestObservationSourceAdvertisesKinds(t *testing.T) { src, err := NewObservationSource(config.SourceConfig{ Name: "openweather-observation-test", - Driver: "openweather_observation", + Driver: DriverObservation, Mode: config.SourceModePoll, Params: map[string]any{ "url": "https://example.invalid?units=metric", @@ -20,7 +21,7 @@ func TestObservationSourceAdvertisesKinds(t *testing.T) { if err != nil { t.Fatalf("NewObservationSource() error = %v", err) } - if got := src.Kinds(); len(got) != 1 || got[0] != event.Kind("observation") { + if got := src.Kinds(); len(got) != 1 || got[0] != event.Kind(standards.KindObservation) { t.Fatalf("Kinds() = %#v, want [observation]", got) } } diff --git a/internal/sources/spc/convective_outlook.go b/internal/sources/spc/convective_outlook.go index c3a1833..492e622 100644 --- a/internal/sources/spc/convective_outlook.go +++ b/internal/sources/spc/convective_outlook.go @@ -20,8 +20,6 @@ import ( ) const ( - driverConvectiveOutlook = "spc_convective_outlook" - acceptGeoJSON = "application/geo+json, application/json" acceptDiscussion = "text/html, application/xhtml+xml" acceptRSS = "application/rss+xml, application/xml, text/xml" @@ -58,24 +56,24 @@ type ConvectiveOutlookSource struct { func NewConvectiveOutlookSource(cfg config.SourceConfig) (*ConvectiveOutlookSource, error) { name := strings.TrimSpace(cfg.Name) if name == "" { - return nil, fmt.Errorf("%s: name is required", driverConvectiveOutlook) + return nil, fmt.Errorf("%s: name is required", DriverConvectiveOutlook) } if cfg.Params == nil { - return nil, fmt.Errorf("%s %q: params are required", driverConvectiveOutlook, name) + return nil, fmt.Errorf("%s %q: params are required", DriverConvectiveOutlook, name) } userAgent, ok := cfg.ParamString("user_agent", "userAgent") if !ok { - return nil, fmt.Errorf("%s %q: params.user_agent is required", driverConvectiveOutlook, name) + return nil, fmt.Errorf("%s %q: params.user_agent is required", DriverConvectiveOutlook, name) } latitude, err := requireFloatParam(cfg, "latitude") if err != nil { - return nil, fmt.Errorf("%s %q: %w", driverConvectiveOutlook, name, err) + return nil, fmt.Errorf("%s %q: %w", DriverConvectiveOutlook, name, err) } longitude, err := requireFloatParam(cfg, "longitude") if err != nil { - return nil, fmt.Errorf("%s %q: %w", driverConvectiveOutlook, name, err) + return nil, fmt.Errorf("%s %q: %w", DriverConvectiveOutlook, name, err) } timeout := transport.DefaultHTTPTimeout @@ -98,11 +96,11 @@ func NewConvectiveOutlookSource(cfg config.SourceConfig) (*ConvectiveOutlookSour geoJSONProducts, err := configuredGeoJSONProducts(cfg) if err != nil { - return nil, fmt.Errorf("%s %q: %w", driverConvectiveOutlook, name, err) + return nil, fmt.Errorf("%s %q: %w", DriverConvectiveOutlook, name, err) } discussions, err := configuredDiscussionProducts(cfg) if err != nil { - return nil, fmt.Errorf("%s %q: %w", driverConvectiveOutlook, name, err) + return nil, fmt.Errorf("%s %q: %w", DriverConvectiveOutlook, name, err) } rssURL := "" @@ -131,7 +129,7 @@ func NewConvectiveOutlookSource(cfg config.SourceConfig) (*ConvectiveOutlookSour func (s *ConvectiveOutlookSource) Name() string { return s.name } func (s *ConvectiveOutlookSource) Kinds() []event.Kind { - return []event.Kind{event.Kind("outlook")} + return []event.Kind{event.Kind(standards.KindOutlook)} } func (s *ConvectiveOutlookSource) Poll(ctx context.Context) ([]event.Event, error) { @@ -221,7 +219,7 @@ func (s *ConvectiveOutlookSource) Poll(ctx context.Context) ([]event.Event, erro eventID := fksources.DefaultEventID("", s.name, &effectiveAt, emittedAt) return fksources.SingleEvent( - event.Kind("outlook"), + event.Kind(standards.KindOutlook), s.name, standards.SchemaRawSPCConvectiveOutlookV1, eventID, diff --git a/internal/sources/spc/convective_outlook_test.go b/internal/sources/spc/convective_outlook_test.go index 507bb92..79d3811 100644 --- a/internal/sources/spc/convective_outlook_test.go +++ b/internal/sources/spc/convective_outlook_test.go @@ -22,7 +22,7 @@ func TestConvectiveOutlookSourceKinds(t *testing.T) { t.Fatalf("NewConvectiveOutlookSource() error = %v", err) } got := src.Kinds() - if len(got) != 1 || got[0] != event.Kind("outlook") { + if len(got) != 1 || got[0] != event.Kind(standards.KindOutlook) { t.Fatalf("Kinds() = %#v, want [outlook]", got) } } @@ -57,7 +57,7 @@ func TestConvectiveOutlookSourcePollEmitsRawBundle(t *testing.T) { t.Fatalf("Poll() returned %d events, want 1", len(events)) } got := events[0] - if got.Kind != event.Kind("outlook") { + if got.Kind != event.Kind(standards.KindOutlook) { t.Fatalf("Kind = %q, want outlook", got.Kind) } if got.Schema != standards.SchemaRawSPCConvectiveOutlookV1 { @@ -277,7 +277,7 @@ func convectiveOutlookConfig(extra map[string]any) config.SourceConfig { } return config.SourceConfig{ Name: "spc-test", - Driver: driverConvectiveOutlook, + Driver: DriverConvectiveOutlook, Mode: config.SourceModePoll, Params: params, } diff --git a/internal/sources/spc/driver.go b/internal/sources/spc/driver.go new file mode 100644 index 0000000..3f233de --- /dev/null +++ b/internal/sources/spc/driver.go @@ -0,0 +1,6 @@ +package spc + +// Source driver strings registered by weatherfeeder for SPC sources. +const ( + DriverConvectiveOutlook = "spc_convective_outlook" +) diff --git a/standards/kind.go b/standards/kind.go new file mode 100644 index 0000000..addd7cc --- /dev/null +++ b/standards/kind.go @@ -0,0 +1,11 @@ +package standards + +// Event kind strings used by weatherfeeder events and routing policy. +const ( + KindObservation = "observation" + KindForecast = "forecast" + KindForecastDiscussion = "forecast_discussion" + KindWeatherStory = "weather_story" + KindAlert = "alert" + KindOutlook = "outlook" +)