Implemented the weather forecast discussion product from weatherfeeder v0.8.3
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful

This commit is contained in:
2026-03-28 17:36:35 -05:00
parent 291a9178c8
commit 0bccfc50d9
18 changed files with 651 additions and 3 deletions

View File

@@ -0,0 +1,38 @@
// discussion.go presents forecast discussion payloads.
// Layer: adapters/inbound/httpapi/presenter discussion payload mapping.
package presenter
import (
"time"
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
)
func DiscussionPayload(run *model.WeatherForecastDiscussion, _ Units, tz *time.Location) any {
if run == nil {
return nil
}
out := model.WeatherForecastDiscussion{
OfficeID: run.OfficeID,
OfficeName: run.OfficeName,
Product: run.Product,
IssuedAt: inLocationTime(run.IssuedAt, tz),
UpdatedAt: inLocationTimePtr(run.UpdatedAt, tz),
KeyMessages: append([]string(nil), run.KeyMessages...),
ShortTerm: copyDiscussionSection(run.ShortTerm, tz),
LongTerm: copyDiscussionSection(run.LongTerm, tz),
}
return &out
}
func copyDiscussionSection(in *model.WeatherForecastDiscussionSection, tz *time.Location) *model.WeatherForecastDiscussionSection {
if in == nil {
return nil
}
return &model.WeatherForecastDiscussionSection{
Qualifier: in.Qualifier,
IssuedAt: inLocationTimePtr(in.IssuedAt, tz),
Text: in.Text,
}
}

View File

@@ -175,6 +175,40 @@ func TestForecastPayloadNoTimezonePreservesUTCAndCopySemantics(t *testing.T) {
assertOffsetSeconds(t, metric.Periods[0].StartTime, 0)
}
func TestDiscussionPayloadTimezoneConversionAndCopySemantics(t *testing.T) {
loc := time.FixedZone("UTC-05:00", -5*60*60)
issuedAt := time.Date(2026, 7, 10, 12, 0, 0, 0, time.UTC)
updatedAt := issuedAt.Add(30 * time.Minute)
shortIssuedAt := issuedAt.Add(-15 * time.Minute)
run := &model.WeatherForecastDiscussion{
OfficeID: "LSX",
Product: model.ForecastDiscussionProductAFD,
IssuedAt: issuedAt,
UpdatedAt: &updatedAt,
KeyMessages: []string{"msg one"},
ShortTerm: &model.WeatherForecastDiscussionSection{Qualifier: "(Tonight)", IssuedAt: &shortIssuedAt, Text: "Short term text"},
}
payload := DiscussionPayload(run, UnitsMetric, loc)
discussion, ok := payload.(*model.WeatherForecastDiscussion)
if !ok {
t.Fatalf("expected *model.WeatherForecastDiscussion payload, got %T", payload)
}
if discussion == run {
t.Fatalf("expected discussion payload to be copied")
}
if discussion.ShortTerm == run.ShortTerm {
t.Fatalf("expected shortTerm pointer to be copied")
}
assertOffsetSeconds(t, discussion.IssuedAt, -5*60*60)
assertOffsetSeconds(t, *discussion.UpdatedAt, -5*60*60)
assertOffsetSeconds(t, *discussion.ShortTerm.IssuedAt, -5*60*60)
if discussion.KeyMessages[0] != "msg one" {
t.Fatalf("expected key message preserved, got %#v", discussion.KeyMessages)
}
assertOffsetSeconds(t, run.IssuedAt, 0)
}
func TestMetricCopyAndNilHandling(t *testing.T) {
obs := &model.WeatherObservation{
TemperatureC: float64Ptr(20.6),
@@ -202,6 +236,9 @@ func TestMetricCopyAndNilHandling(t *testing.T) {
if AlertsPayload(nil, UnitsUS) != nil {
t.Fatalf("expected nil alerts input to return nil payload")
}
if DiscussionPayload(nil, UnitsUS, nil) != nil {
t.Fatalf("expected nil discussion input to return nil payload")
}
if CurrentConditionsPayload(nil, UnitsUS, 0) != nil {
t.Fatalf("expected nil current conditions input to return nil payload")
}