diff --git a/internal/adapters/httpapi/server_test.go b/internal/adapters/httpapi/server_test.go index 1d6300e..9fa80ee 100644 --- a/internal/adapters/httpapi/server_test.go +++ b/internal/adapters/httpapi/server_test.go @@ -232,3 +232,70 @@ func TestForecastReturnsUSUnits(t *testing.T) { t.Fatalf("expected forecast timestamp passed to repo") } } + +func TestNullFieldsAreOmittedFromJSON(t *testing.T) { + obsRepo := &fakeObservationRepo{ + summary: ports.ObservationSummaryMetric{}, + conditions: []ports.ObservationConditionMetric{ + { + ObservedAt: time.Date(2026, 3, 17, 12, 0, 0, 0, time.UTC), + }, + }, + } + fcRepo := &fakeForecastRepo{ + periods: []ports.ForecastPeriodMetric{ + { + PeriodIndex: 1, + StartTime: time.Date(2026, 3, 17, 12, 0, 0, 0, time.UTC), + EndTime: time.Date(2026, 3, 17, 13, 0, 0, 0, time.UTC), + ConditionCode: 1, + }, + }, + } + + server := NewServer( + observations.NewService(obsRepo, units.USConverter{}, constants.ObservationWindow), + forecasts.NewService(fcRepo, units.USConverter{}, constants.ForecastQueryLimit), + alerts.NewService(fakeAlertRepo{}), + ).Handler() + + wObs := httptest.NewRecorder() + server.ServeHTTP(wObs, httptest.NewRequest(http.MethodGet, "/observations/current", nil)) + if wObs.Code != http.StatusOK { + t.Fatalf("expected observations 200, got %d", wObs.Code) + } + + var obsPayload map[string]any + if err := json.Unmarshal(wObs.Body.Bytes(), &obsPayload); err != nil { + t.Fatalf("decode observations response: %v", err) + } + summary := obsPayload["summary"].(map[string]any) + if _, exists := summary["temperatureF"]; exists { + t.Fatalf("expected summary.temperatureF to be omitted when nil") + } + + conditions := obsPayload["conditions"].([]any) + firstCond := conditions[0].(map[string]any) + if _, exists := firstCond["temperatureF"]; exists { + t.Fatalf("expected conditions[0].temperatureF to be omitted when nil") + } + + wFc := httptest.NewRecorder() + server.ServeHTTP(wFc, httptest.NewRequest(http.MethodGet, "/forecast?timestamp=2026-03-17T12:30:00Z", nil)) + if wFc.Code != http.StatusOK { + t.Fatalf("expected forecast 200, got %d", wFc.Code) + } + + var fcPayload map[string]any + if err := json.Unmarshal(wFc.Body.Bytes(), &fcPayload); err != nil { + t.Fatalf("decode forecast response: %v", err) + } + periods := fcPayload["periods"].([]any) + firstPeriod := periods[0].(map[string]any) + if _, exists := firstPeriod["temperatureF"]; exists { + t.Fatalf("expected periods[0].temperatureF to be omitted when nil") + } + if _, exists := firstPeriod["windSpeedMph"]; exists { + t.Fatalf("expected periods[0].windSpeedMph to be omitted when nil") + } +} diff --git a/internal/application/alerts/service.go b/internal/application/alerts/service.go index e9a9fea..094407f 100644 --- a/internal/application/alerts/service.go +++ b/internal/application/alerts/service.go @@ -21,13 +21,13 @@ type Response struct { } type AlertResponse struct { - Effective *time.Time `json:"effective"` - Expires *time.Time `json:"expires"` - Severity *string `json:"severity"` - Event *string `json:"event"` - Headline *string `json:"headline"` - Instruction *string `json:"instruction"` - Description *string `json:"description"` + Effective *time.Time `json:"effective,omitempty"` + Expires *time.Time `json:"expires,omitempty"` + Severity *string `json:"severity,omitempty"` + Event *string `json:"event,omitempty"` + Headline *string `json:"headline,omitempty"` + Instruction *string `json:"instruction,omitempty"` + Description *string `json:"description,omitempty"` } func (s *Service) GetCurrent(ctx context.Context) (Response, error) { diff --git a/internal/application/forecasts/service.go b/internal/application/forecasts/service.go index a9c82ba..12c9267 100644 --- a/internal/application/forecasts/service.go +++ b/internal/application/forecasts/service.go @@ -28,30 +28,30 @@ type PeriodResponse struct { PeriodIndex int `json:"periodIndex"` StartTime time.Time `json:"startTime"` EndTime time.Time `json:"endTime"` - Name *string `json:"name"` - IsDay *bool `json:"isDay"` + Name *string `json:"name,omitempty"` + IsDay *bool `json:"isDay,omitempty"` ConditionCode int `json:"conditionCode"` - ConditionText *string `json:"conditionText"` - ProviderRawDescription *string `json:"providerRawDescription"` - TextDescription *string `json:"textDescription"` - DetailedText *string `json:"detailedText"` - IconURL *string `json:"iconUrl"` - TemperatureF *float64 `json:"temperatureF"` - TemperatureFMin *float64 `json:"temperatureFMin"` - TemperatureFMax *float64 `json:"temperatureFMax"` - DewpointF *float64 `json:"dewpointF"` - RelativeHumidityPercent *float64 `json:"relativeHumidityPercent"` - WindDirectionDegrees *float64 `json:"windDirectionDegrees"` - WindSpeedMph *float64 `json:"windSpeedMph"` - WindGustMph *float64 `json:"windGustMph"` - BarometricPressurePa *float64 `json:"barometricPressurePa"` - VisibilityMeters *float64 `json:"visibilityMeters"` - ApparentTemperatureF *float64 `json:"apparentTemperatureF"` - CloudCoverPercent *float64 `json:"cloudCoverPercent"` - ProbabilityOfPrecipitationPercent *float64 `json:"probabilityOfPrecipitationPercent"` - PrecipitationAmountMm *float64 `json:"precipitationAmountMm"` - SnowfallDepthMm *float64 `json:"snowfallDepthMm"` - UVIndex *float64 `json:"uvIndex"` + ConditionText *string `json:"conditionText,omitempty"` + ProviderRawDescription *string `json:"providerRawDescription,omitempty"` + TextDescription *string `json:"textDescription,omitempty"` + DetailedText *string `json:"detailedText,omitempty"` + IconURL *string `json:"iconUrl,omitempty"` + TemperatureF *float64 `json:"temperatureF,omitempty"` + TemperatureFMin *float64 `json:"temperatureFMin,omitempty"` + TemperatureFMax *float64 `json:"temperatureFMax,omitempty"` + DewpointF *float64 `json:"dewpointF,omitempty"` + RelativeHumidityPercent *float64 `json:"relativeHumidityPercent,omitempty"` + WindDirectionDegrees *float64 `json:"windDirectionDegrees,omitempty"` + WindSpeedMph *float64 `json:"windSpeedMph,omitempty"` + WindGustMph *float64 `json:"windGustMph,omitempty"` + BarometricPressurePa *float64 `json:"barometricPressurePa,omitempty"` + VisibilityMeters *float64 `json:"visibilityMeters,omitempty"` + ApparentTemperatureF *float64 `json:"apparentTemperatureF,omitempty"` + CloudCoverPercent *float64 `json:"cloudCoverPercent,omitempty"` + ProbabilityOfPrecipitationPercent *float64 `json:"probabilityOfPrecipitationPercent,omitempty"` + PrecipitationAmountMm *float64 `json:"precipitationAmountMm,omitempty"` + SnowfallDepthMm *float64 `json:"snowfallDepthMm,omitempty"` + UVIndex *float64 `json:"uvIndex,omitempty"` } func (s *Service) GetByTimestamp(ctx context.Context, ts time.Time) (Response, error) { diff --git a/internal/application/observations/service.go b/internal/application/observations/service.go index 85a04b0..2c9633d 100644 --- a/internal/application/observations/service.go +++ b/internal/application/observations/service.go @@ -26,18 +26,18 @@ type CurrentResponse struct { } type SummaryResponse struct { - TemperatureF *float64 `json:"temperatureF"` - ApparentTemperatureF *float64 `json:"apparentTemperatureF"` + TemperatureF *float64 `json:"temperatureF,omitempty"` + ApparentTemperatureF *float64 `json:"apparentTemperatureF,omitempty"` WindowMinutes int `json:"windowMinutes"` } type ConditionResponse struct { - StationID *string `json:"stationId"` + StationID *string `json:"stationId,omitempty"` ObservedAt time.Time `json:"observedAt"` - TemperatureF *float64 `json:"temperatureF"` - TextDescription *string `json:"textDescription"` - ProviderRawDescription *string `json:"providerRawDescription"` - ConditionText *string `json:"conditionText"` + TemperatureF *float64 `json:"temperatureF,omitempty"` + TextDescription *string `json:"textDescription,omitempty"` + ProviderRawDescription *string `json:"providerRawDescription,omitempty"` + ConditionText *string `json:"conditionText,omitempty"` } func (s *Service) GetCurrent(ctx context.Context) (CurrentResponse, error) {