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) {
issuedAt := time.Date(2026, 7, 10, 15, 0, 0, 0, time.UTC)
updatedAt := issuedAt.Add(30 * time.Minute)

View File

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

View File

@@ -3,6 +3,7 @@
package presenter
import (
"encoding/json"
"math"
"testing"
"time"
@@ -83,6 +84,22 @@ func TestForecastPayloadUS(t *testing.T) {
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) {
loc := time.FixedZone("UTC-05:00", -5*60*60)
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))
}
}
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),
IsDay: boolPtr(row.IsDay),
ConditionCode: model.WMOCode(row.ConditionCode),
ConditionText: stringValue(row.ConditionText),
ProviderRawDescription: stringValue(row.ProviderRawDescription),
TextDescription: stringValue(row.TextDescription),
DetailedText: stringValue(row.DetailedText),
IconURL: stringValue(row.IconURL),
TemperatureC: float64Ptr(row.TemperatureC),
TemperatureCMin: float64Ptr(row.TemperatureCMin),
TemperatureCMax: float64Ptr(row.TemperatureCMax),

View File

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

View File

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

View File

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