Split weather data types from forecast derivation

This commit is contained in:
2026-06-09 20:16:47 +00:00
parent d8b417458b
commit 454f47b2b5
22 changed files with 234 additions and 223 deletions

View File

@@ -1,4 +1,4 @@
// Package weatherapi adapts the internal weather API to forecast bundles.
// Package weatherapi adapts the internal weather API to weather data bundles.
package weatherapi
import (
@@ -18,7 +18,7 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
)
type Client struct {
@@ -83,11 +83,11 @@ func New(cfg config.Config, opts ...Option) (*Client, error) {
return client, nil
}
func (c *Client) FetchBundle(ctx context.Context) (*forecast.Bundle, error) {
func (c *Client) FetchBundle(ctx context.Context) (*weatherdata.Bundle, error) {
fetchedAt := c.now()
builder := bundleBuilder{
client: c,
bundle: &forecast.Bundle{FetchedAt: fetchedAt},
bundle: &weatherdata.Bundle{FetchedAt: fetchedAt},
fetchedAt: fetchedAt,
}
@@ -121,7 +121,7 @@ func (c *Client) FetchBundle(ctx context.Context) (*forecast.Bundle, error) {
type bundleBuilder struct {
client *Client
bundle *forecast.Bundle
bundle *weatherdata.Bundle
fetchedAt time.Time
}
@@ -133,7 +133,7 @@ func (b *bundleBuilder) fetchObservation(ctx context.Context) error {
if raw == nil {
return b.handleMissing(&source, "observation data is missing", false)
}
var observation forecast.Observation
var observation weatherdata.Observation
if err := decodeSource(raw, &observation); err != nil {
return b.handleMalformed(&source, err, false)
}
@@ -151,7 +151,7 @@ func (b *bundleBuilder) fetchCurrent(ctx context.Context) error {
if raw == nil {
return b.handleMissing(&source, "current conditions data is missing", false)
}
var current forecast.Current
var current weatherdata.Current
if err := decodeSource(raw, &current); err != nil {
return b.handleMalformed(&source, err, false)
}
@@ -168,7 +168,7 @@ func (b *bundleBuilder) fetchHourly(ctx context.Context) error {
if raw == nil {
return b.handleMissing(&source, "hourly forecast data is missing", true)
}
var hourly forecast.ForecastRun
var hourly weatherdata.ForecastRun
if err := decodeSource(raw, &hourly); err != nil {
return fmt.Errorf("decode hourly forecast from %s: %w", source.Endpoint, err)
}
@@ -190,7 +190,7 @@ func (b *bundleBuilder) fetchNarrative(ctx context.Context) error {
if raw == nil {
return b.handleMissing(&source, "narrative forecast data is missing", false)
}
var narrative forecast.ForecastRun
var narrative weatherdata.ForecastRun
if err := decodeSource(raw, &narrative); err != nil {
return b.handleMalformed(&source, err, false)
}
@@ -210,11 +210,11 @@ func (b *bundleBuilder) fetchAlerts(ctx context.Context) error {
return b.handleMissing(&source, "active alerts data is missing", false)
}
if isJSONNull(raw) {
b.bundle.Alerts = &forecast.AlertRun{Raw: append(json.RawMessage(nil), raw...)}
b.bundle.Alerts = &weatherdata.AlertRun{Raw: append(json.RawMessage(nil), raw...)}
b.addSource(source)
return nil
}
var alerts forecast.AlertRun
var alerts weatherdata.AlertRun
if err := decodeSource(raw, &alerts); err != nil {
return b.handleMalformed(&source, err, false)
}
@@ -235,7 +235,7 @@ func (b *bundleBuilder) fetchDiscussion(ctx context.Context) error {
if raw == nil {
return b.handleMissing(&source, "forecast discussion data is missing", false)
}
var discussion forecast.Discussion
var discussion weatherdata.Discussion
if err := decodeSource(raw, &discussion); err != nil {
return b.handleMalformed(&source, err, false)
}
@@ -254,7 +254,7 @@ func (b *bundleBuilder) fetchWeatherStory(ctx context.Context) error {
if raw == nil {
return b.handleMissing(&source, "NWS weather story data is missing", false)
}
var story forecast.WeatherStory
var story weatherdata.WeatherStory
if err := decodeSource(raw, &story); err != nil {
return b.handleMalformed(&source, err, false)
}
@@ -268,7 +268,7 @@ func (b *bundleBuilder) fetchWeatherStory(ctx context.Context) error {
}
func (b *bundleBuilder) addStub(sourceName string, message string) error {
source := forecast.Source{
source := weatherdata.Source{
Name: sourceName,
FetchedAt: b.fetchedAt,
Missing: true,
@@ -276,7 +276,7 @@ func (b *bundleBuilder) addStub(sourceName string, message string) error {
return b.applyMissingPolicy(&source, "missing_source", message)
}
func (b *bundleBuilder) handleMissing(source *forecast.Source, message string, required bool) error {
func (b *bundleBuilder) handleMissing(source *weatherdata.Source, message string, required bool) error {
source.Missing = true
if required {
return fmt.Errorf("%s from %s is required", message, source.Endpoint)
@@ -284,7 +284,7 @@ func (b *bundleBuilder) handleMissing(source *forecast.Source, message string, r
return b.applyMissingPolicy(source, "missing_source", message)
}
func (b *bundleBuilder) handleMalformed(source *forecast.Source, err error, required bool) error {
func (b *bundleBuilder) handleMalformed(source *weatherdata.Source, err error, required bool) error {
if required {
return fmt.Errorf("decode %s from %s: %w", source.Name, source.Endpoint, err)
}
@@ -292,13 +292,13 @@ func (b *bundleBuilder) handleMalformed(source *forecast.Source, err error, requ
return b.applyMissingPolicy(source, "malformed_source", fmt.Sprintf("malformed %s data: %v", source.Name, err))
}
func (b *bundleBuilder) applyMissingPolicy(source *forecast.Source, code string, message string) error {
func (b *bundleBuilder) applyMissingPolicy(source *weatherdata.Source, code string, message string) error {
policy := b.client.policyFor(source.Name)
if policy == config.MissingSourceError {
return fmt.Errorf("%s: %s", source.Name, message)
}
if policy == config.MissingSourceWarn {
warning := forecast.SourceWarning{
warning := weatherdata.SourceWarning{
Source: source.Name,
Code: code,
Severity: "warning",
@@ -313,7 +313,7 @@ func (b *bundleBuilder) applyMissingPolicy(source *forecast.Source, code string,
return nil
}
func (b *bundleBuilder) addSource(source forecast.Source) {
func (b *bundleBuilder) addSource(source weatherdata.Source) {
b.bundle.Sources = append(b.bundle.Sources, source)
}
@@ -335,33 +335,33 @@ type envelope struct {
Data json.RawMessage `json:"data"`
}
func (c *Client) fetch(ctx context.Context, sourceName string, endpoint string, opts queryOptions) (json.RawMessage, forecast.Source, error) {
func (c *Client) fetch(ctx context.Context, sourceName string, endpoint string, opts queryOptions) (json.RawMessage, weatherdata.Source, error) {
reqURL := c.endpointURL(endpoint, opts)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
if err != nil {
return nil, forecast.Source{}, fmt.Errorf("create request for %s: %w", endpoint, err)
return nil, weatherdata.Source{}, fmt.Errorf("create request for %s: %w", endpoint, err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, forecast.Source{}, fmt.Errorf("fetch %s: %w", endpoint, err)
return nil, weatherdata.Source{}, fmt.Errorf("fetch %s: %w", endpoint, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20))
if err != nil {
return nil, forecast.Source{}, fmt.Errorf("read %s response: %w", endpoint, err)
return nil, weatherdata.Source{}, fmt.Errorf("read %s response: %w", endpoint, err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, forecast.Source{}, fmt.Errorf("fetch %s: unexpected HTTP status %d: %s", endpoint, resp.StatusCode, strings.TrimSpace(string(body)))
return nil, weatherdata.Source{}, fmt.Errorf("fetch %s: unexpected HTTP status %d: %s", endpoint, resp.StatusCode, strings.TrimSpace(string(body)))
}
var env envelope
if err := json.Unmarshal(body, &env); err != nil {
return nil, forecast.Source{}, fmt.Errorf("decode %s envelope: %w", endpoint, err)
return nil, weatherdata.Source{}, fmt.Errorf("decode %s envelope: %w", endpoint, err)
}
source := forecast.Source{
source := weatherdata.Source{
Name: sourceName,
Endpoint: endpoint,
Query: queryMap(reqURL.Query()),
@@ -430,7 +430,7 @@ func sourceHash(raw json.RawMessage) (string, error) {
return hex.EncodeToString(sum[:]), nil
}
func SaveBundle(path string, bundle *forecast.Bundle) error {
func SaveBundle(path string, bundle *weatherdata.Bundle) error {
if err := fileutil.WriteJSONAtomic(path, bundle); err != nil {
return fmt.Errorf("save bundle: %w", err)
}