Simplified the forecast schema and removed fields deprecated upstream in weatherfeeder
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful

This commit is contained in:
2026-03-26 21:35:39 -05:00
parent dbefa8ed28
commit 78dc7817e9
7 changed files with 93 additions and 28 deletions

View File

@@ -394,6 +394,51 @@ func TestForecastPrecisionTwo(t *testing.T) {
} }
} }
func TestForecastJSONOmitsLegacyDescriptionFields(t *testing.T) {
h := newHandler(t, &fakeService{
forecast: &model.WeatherForecastRun{
Product: model.ForecastProductHourly,
IssuedAt: time.Now().UTC(),
Periods: []model.WeatherForecastPeriod{{
StartTime: time.Now().UTC(),
EndTime: time.Now().UTC().Add(time.Hour),
ConditionCode: model.WMOUnknown,
TextDescription: "Cloudy",
}},
},
}, "/forecast/hourly")
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/forecast/hourly", nil)
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
var payload struct {
Data struct {
Periods []map[string]any `json:"periods"`
} `json:"data"`
}
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode forecast payload: %v", err)
}
if len(payload.Data.Periods) == 0 {
t.Fatalf("expected at least one period")
}
period := payload.Data.Periods[0]
for _, key := range []string{"conditionText", "providerRawDescription", "detailedText", "iconUrl"} {
if _, ok := period[key]; ok {
t.Fatalf("unexpected legacy field %q in forecast response period: %#v", key, period)
}
}
if period["textDescription"] != "Cloudy" {
t.Fatalf("expected textDescription Cloudy, got %#v", period["textDescription"])
}
}
func TestForecastTimezoneOffsetUppercaseTZConvertsAllTimes(t *testing.T) { func TestForecastTimezoneOffsetUppercaseTZConvertsAllTimes(t *testing.T) {
issuedAt := time.Date(2026, 7, 10, 15, 0, 0, 0, time.UTC) issuedAt := time.Date(2026, 7, 10, 15, 0, 0, 0, time.UTC)
updatedAt := issuedAt.Add(30 * time.Minute) updatedAt := issuedAt.Add(30 * time.Minute)

View File

@@ -28,11 +28,7 @@ type WeatherForecastPeriodUS struct {
Name string `json:"name,omitempty" xml:"name,omitempty"` Name string `json:"name,omitempty" xml:"name,omitempty"`
IsDay *bool `json:"isDay,omitempty" xml:"isDay,omitempty"` IsDay *bool `json:"isDay,omitempty" xml:"isDay,omitempty"`
ConditionCode model.WMOCode `json:"conditionCode" xml:"conditionCode"` ConditionCode model.WMOCode `json:"conditionCode" xml:"conditionCode"`
ConditionText string `json:"conditionText,omitempty" xml:"conditionText,omitempty"`
ProviderRawDescription string `json:"providerRawDescription,omitempty" xml:"providerRawDescription,omitempty"`
TextDescription string `json:"textDescription,omitempty" xml:"textDescription,omitempty"` TextDescription string `json:"textDescription,omitempty" xml:"textDescription,omitempty"`
DetailedText string `json:"detailedText,omitempty" xml:"detailedText,omitempty"`
IconURL string `json:"iconUrl,omitempty" xml:"iconUrl,omitempty"`
TemperatureF *float64 `json:"temperatureF,omitempty" xml:"temperatureF,omitempty"` TemperatureF *float64 `json:"temperatureF,omitempty" xml:"temperatureF,omitempty"`
TemperatureFMin *float64 `json:"temperatureFMin,omitempty" xml:"temperatureFMin,omitempty"` TemperatureFMin *float64 `json:"temperatureFMin,omitempty" xml:"temperatureFMin,omitempty"`
TemperatureFMax *float64 `json:"temperatureFMax,omitempty" xml:"temperatureFMax,omitempty"` TemperatureFMax *float64 `json:"temperatureFMax,omitempty" xml:"temperatureFMax,omitempty"`
@@ -74,11 +70,7 @@ func ForecastPayload(run *model.WeatherForecastRun, units Units, precision int,
Name: p.Name, Name: p.Name,
IsDay: copyBoolPtr(p.IsDay), IsDay: copyBoolPtr(p.IsDay),
ConditionCode: p.ConditionCode, ConditionCode: p.ConditionCode,
ConditionText: p.ConditionText,
ProviderRawDescription: p.ProviderRawDescription,
TextDescription: p.TextDescription, TextDescription: p.TextDescription,
DetailedText: p.DetailedText,
IconURL: p.IconURL,
TemperatureF: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureC), precision), TemperatureF: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureC), precision),
TemperatureFMin: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureCMin), precision), TemperatureFMin: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureCMin), precision),
TemperatureFMax: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureCMax), precision), TemperatureFMax: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureCMax), precision),
@@ -119,11 +111,7 @@ func ForecastPayload(run *model.WeatherForecastRun, units Units, precision int,
Name: p.Name, Name: p.Name,
IsDay: copyBoolPtr(p.IsDay), IsDay: copyBoolPtr(p.IsDay),
ConditionCode: p.ConditionCode, ConditionCode: p.ConditionCode,
ConditionText: p.ConditionText,
ProviderRawDescription: p.ProviderRawDescription,
TextDescription: p.TextDescription, TextDescription: p.TextDescription,
DetailedText: p.DetailedText,
IconURL: p.IconURL,
TemperatureC: roundedPtr(copyFloat64Ptr(p.TemperatureC), precision), TemperatureC: roundedPtr(copyFloat64Ptr(p.TemperatureC), precision),
TemperatureCMin: roundedPtr(copyFloat64Ptr(p.TemperatureCMin), precision), TemperatureCMin: roundedPtr(copyFloat64Ptr(p.TemperatureCMin), precision),
TemperatureCMax: roundedPtr(copyFloat64Ptr(p.TemperatureCMax), precision), TemperatureCMax: roundedPtr(copyFloat64Ptr(p.TemperatureCMax), precision),

View File

@@ -3,6 +3,7 @@
package presenter package presenter
import ( import (
"encoding/json"
"math" "math"
"testing" "testing"
"time" "time"
@@ -83,6 +84,22 @@ func TestForecastPayloadUS(t *testing.T) {
assertApprox(t, period.SnowfallDepthIn, 2.0, 0.0001) assertApprox(t, period.SnowfallDepthIn, 2.0, 0.0001)
} }
func TestForecastPayloadOmitsLegacyDescriptionFields(t *testing.T) {
run := &model.WeatherForecastRun{
Product: model.ForecastProductHourly,
IssuedAt: time.Date(2026, 3, 20, 12, 0, 0, 0, time.UTC),
Periods: []model.WeatherForecastPeriod{{
StartTime: time.Date(2026, 3, 20, 12, 0, 0, 0, time.UTC),
EndTime: time.Date(2026, 3, 20, 13, 0, 0, 0, time.UTC),
ConditionCode: model.WMOUnknown,
TextDescription: "Cloudy",
}},
}
assertForecastPayloadHasNoLegacyDescriptionFields(t, ForecastPayload(run, UnitsMetric, 0, nil))
assertForecastPayloadHasNoLegacyDescriptionFields(t, ForecastPayload(run, UnitsUS, 0, nil))
}
func TestForecastPayloadTimezoneConversionMetricAndUS(t *testing.T) { func TestForecastPayloadTimezoneConversionMetricAndUS(t *testing.T) {
loc := time.FixedZone("UTC-05:00", -5*60*60) loc := time.FixedZone("UTC-05:00", -5*60*60)
issuedAt := time.Date(2026, 7, 10, 12, 0, 0, 0, time.UTC) issuedAt := time.Date(2026, 7, 10, 12, 0, 0, 0, time.UTC)
@@ -308,3 +325,34 @@ func assertOffsetSeconds(t *testing.T, ts time.Time, want int) {
t.Fatalf("expected offset %d, got %d for %s", want, got, ts.Format(time.RFC3339)) t.Fatalf("expected offset %d, got %d for %s", want, got, ts.Format(time.RFC3339))
} }
} }
func assertForecastPayloadHasNoLegacyDescriptionFields(t *testing.T, payload any) {
t.Helper()
b, err := json.Marshal(payload)
if err != nil {
t.Fatalf("json.Marshal(payload) error = %v", err)
}
var root map[string]any
if err := json.Unmarshal(b, &root); err != nil {
t.Fatalf("json.Unmarshal(payload) error = %v", err)
}
periodsRaw, ok := root["periods"].([]any)
if !ok || len(periodsRaw) == 0 {
t.Fatalf("expected non-empty periods in payload: %#v", root["periods"])
}
period, ok := periodsRaw[0].(map[string]any)
if !ok {
t.Fatalf("expected first period map, got %#v", periodsRaw[0])
}
for _, key := range []string{"conditionText", "providerRawDescription", "detailedText", "iconUrl"} {
if _, exists := period[key]; exists {
t.Fatalf("unexpected legacy field %q in payload period: %#v", key, period)
}
}
if period["textDescription"] != "Cloudy" {
t.Fatalf("expected textDescription Cloudy, got %#v", period["textDescription"])
}
}

View File

@@ -24,11 +24,7 @@ func mapForecastPeriodRow(row forecastPeriodRow) model.WeatherForecastPeriod {
Name: stringValue(row.Name), Name: stringValue(row.Name),
IsDay: boolPtr(row.IsDay), IsDay: boolPtr(row.IsDay),
ConditionCode: model.WMOCode(row.ConditionCode), ConditionCode: model.WMOCode(row.ConditionCode),
ConditionText: stringValue(row.ConditionText),
ProviderRawDescription: stringValue(row.ProviderRawDescription),
TextDescription: stringValue(row.TextDescription), TextDescription: stringValue(row.TextDescription),
DetailedText: stringValue(row.DetailedText),
IconURL: stringValue(row.IconURL),
TemperatureC: float64Ptr(row.TemperatureC), TemperatureC: float64Ptr(row.TemperatureC),
TemperatureCMin: float64Ptr(row.TemperatureCMin), TemperatureCMin: float64Ptr(row.TemperatureCMin),
TemperatureCMax: float64Ptr(row.TemperatureCMax), TemperatureCMax: float64Ptr(row.TemperatureCMax),

View File

@@ -27,11 +27,7 @@ SELECT
name, name,
is_day, is_day,
condition_code, condition_code,
condition_text,
provider_raw_description,
text_description, text_description,
detailed_text,
icon_url,
temperature_c, temperature_c,
temperature_c_min, temperature_c_min,
temperature_c_max, temperature_c_max,

View File

@@ -63,11 +63,7 @@ func (r *Repository) loadForecastPeriods(ctx context.Context, eventID string) ([
&row.Name, &row.Name,
&row.IsDay, &row.IsDay,
&row.ConditionCode, &row.ConditionCode,
&row.ConditionText,
&row.ProviderRawDescription,
&row.TextDescription, &row.TextDescription,
&row.DetailedText,
&row.IconURL,
&row.TemperatureC, &row.TemperatureC,
&row.TemperatureCMin, &row.TemperatureCMin,
&row.TemperatureCMax, &row.TemperatureCMax,

View File

@@ -26,11 +26,7 @@ type forecastPeriodRow struct {
Name sql.NullString Name sql.NullString
IsDay sql.NullBool IsDay sql.NullBool
ConditionCode int ConditionCode int
ConditionText sql.NullString
ProviderRawDescription sql.NullString
TextDescription sql.NullString TextDescription sql.NullString
DetailedText sql.NullString
IconURL sql.NullString
TemperatureC sql.NullFloat64 TemperatureC sql.NullFloat64
TemperatureCMin sql.NullFloat64 TemperatureCMin sql.NullFloat64
TemperatureCMax sql.NullFloat64 TemperatureCMax sql.NullFloat64