Refactor the template variable framework

This commit is contained in:
2026-06-14 08:57:53 -05:00
parent bb8de054dc
commit 28b8391d53
8 changed files with 512 additions and 552 deletions

View File

@@ -11,7 +11,7 @@ func TestTemplateLookup(t *testing.T) {
if err != nil {
t.Fatalf("Template() error = %v", err)
}
for _, want := range []string{"# {{ .ReportTitle }}", "## Summary", "## Hourly Forecast", "## Weather Story"} {
for _, want := range []string{"# {{ .Report.Title }}", "## Summary", "## Hourly Forecast", "## Weather Story"} {
if !strings.Contains(source, want) {
t.Fatalf("template missing %q:\n%s", want, source)
}
@@ -54,29 +54,58 @@ func TestSchemaLookup(t *testing.T) {
func TestRenderHourly(t *testing.T) {
rendered, err := Render("hourly", testRenderContext{
ReportTitle: "Hourly Report",
LocationName: "Brentwood",
ValidPeriod: "May 29, 8:30 AM to 2:30 PM",
GeneratedAt: "May 29, 8:30 AM",
CurrentConditions: "74 F, light south wind.",
PrecipitationTiming: "Showers are most likely late morning.",
WeatherStory: "Morning storms remain the main story.",
Report: testReportContext{
Title: "Hourly Report",
LocationName: "Brentwood",
ValidPeriodLabel: "May 29, 8:30 AM to 2:30 PM",
GeneratedAtLabel: "May 29, 8:30 AM",
},
GeneratedText: testGeneratedText{
Summary: "Storm chances increase through late morning.",
Timing: "The main window is 10 AM to noon.",
Impacts: "Brief downpours may slow travel.",
Confidence: "Medium confidence in timing.",
},
HourlyForecast: []testHourlyRow{
{Time: "9 AM", Summary: "Cloudy", Temperature: "74 F", Precipitation: "30% showers", Wind: "S 8 mph"},
{Time: "10 AM", Summary: "Showers", Temperature: "75 F", Precipitation: "70% showers", Wind: "S 10 mph"},
},
Alerts: []string{"Flood Watch until 2:30 PM"},
SPCOutlooks: []string{"Slight Risk through afternoon"},
SPCDiscussions: []string{"Strong storms may develop late morning."},
ForecastDiscussion: testForecastDiscussion{
KeyMessages: []string{"Storms are most likely late morning."},
ShortTerm: "Short-term discussion favors increasing rain coverage.",
Modules: testModules{
CurrentConditions: &testCurrentConditions{
ConditionText: "Partly cloudy",
TemperatureF: floatPtr(74),
ApparentTemperatureF: floatPtr(76),
RelativeHumidityPercent: floatPtr(71),
WindDirection: "S",
WindSpeedMph: floatPtr(8),
},
HourlyForecast: &testHourlyForecast{
Periods: []testHourlyPeriod{
{PeriodBegins: "9 AM", TextDescription: "Cloudy", TemperatureF: floatPtr(74), ProbabilityOfPrecipitationPercent: floatPtr(30), WindDirection: "S", WindSpeedMph: floatPtr(8)},
{PeriodBegins: "10 AM", TextDescription: "Showers", TemperatureF: floatPtr(75), ProbabilityOfPrecipitationPercent: floatPtr(70), WindDirection: "S", WindSpeedMph: floatPtr(10)},
},
},
PrecipTiming: &testPrecipTiming{
MaxPopPercent: intPtr(70),
MaxPopTime: "10 AM",
PrecipitationWindows: []testPrecipWindow{
{PeriodBegins: "10 AM", PeriodEnds: "12 PM", MaxPopPercent: intPtr(70), MaxPopTime: "10 AM"},
},
},
AlertDigest: &testAlertDigest{
Relevant: []testAlert{{Event: "Flood Watch", Headline: "Flood Watch until 2:30 PM", Severity: "Moderate"}},
},
SPCConvectiveOutlooks: &testSPCOutlooks{
Outlooks: []testSPCOutlook{{LabelText: "Slight Risk", PeriodBegins: "8 AM", PeriodEnds: "2 PM"}},
},
SPCConvectiveDiscussion: &testSPCDiscussion{
Discussions: []testSPCDiscussionRecord{{Summary: "Strong storms may develop late morning."}},
},
AreaForecastDiscussion: &testForecastDiscussion{
KeyMessages: []string{"Storms are most likely late morning."},
ShortTerm: "Short-term discussion favors increasing rain coverage.",
},
WeatherStory: &testWeatherStory{
Available: true,
Title: "Morning storms",
Description: "Morning storms remain the main story.",
},
},
})
if err != nil {
@@ -88,8 +117,8 @@ func TestRenderHourly(t *testing.T) {
"Valid: May 29, 8:30 AM to 2:30 PM",
"Storm chances increase through late morning.",
"## Confidence",
"- 10 AM: Showers; 75 F; 70% showers; S 10 mph",
"- Flood Watch until 2:30 PM",
"- 10 AM: Showers; 75 F; 70% precipitation; wind S 10 mph",
"- Flood Watch: Flood Watch until 2:30 PM (Moderate)",
"Short-term discussion favors increasing rain coverage.",
} {
if !strings.Contains(text, want) {
@@ -126,7 +155,7 @@ func TestUnknownAssetsReturnActionableErrors(t *testing.T) {
}
func TestRenderFailsForMissingContextFields(t *testing.T) {
_, err := Render("hourly", map[string]any{"ReportTitle": "Hourly Report"})
_, err := Render("hourly", map[string]any{"Report": map[string]any{"Title": "Hourly Report"}})
if err == nil {
t.Fatal("Render() error = nil, want missing field error")
}
@@ -136,19 +165,16 @@ func TestRenderFailsForMissingContextFields(t *testing.T) {
}
type testRenderContext struct {
ReportTitle string
LocationName string
ValidPeriod string
GeneratedAt string
CurrentConditions string
PrecipitationTiming string
WeatherStory string
GeneratedText testGeneratedText
HourlyForecast []testHourlyRow
Alerts []string
SPCOutlooks []string
SPCDiscussions []string
ForecastDiscussion testForecastDiscussion
Report testReportContext
GeneratedText testGeneratedText
Modules testModules
}
type testReportContext struct {
Title string
LocationName string
ValidPeriodLabel string
GeneratedAtLabel string
}
type testGeneratedText struct {
@@ -158,12 +184,76 @@ type testGeneratedText struct {
Confidence string
}
type testHourlyRow struct {
Time string
Summary string
Temperature string
Precipitation string
Wind string
type testModules struct {
CurrentConditions *testCurrentConditions
HourlyForecast *testHourlyForecast
PrecipTiming *testPrecipTiming
AlertDigest *testAlertDigest
SPCConvectiveOutlooks *testSPCOutlooks
AreaForecastDiscussion *testForecastDiscussion
SPCConvectiveDiscussion *testSPCDiscussion
WeatherStory *testWeatherStory
}
type testCurrentConditions struct {
ConditionText string
TemperatureF *float64
ApparentTemperatureF *float64
RelativeHumidityPercent *float64
WindSpeedMph *float64
WindDirection string
}
type testHourlyForecast struct {
Periods []testHourlyPeriod
}
type testHourlyPeriod struct {
PeriodBegins string
Name string
TextDescription string
TemperatureF *float64
WindSpeedMph *float64
WindGustMph *float64
WindDirection string
ProbabilityOfPrecipitationPercent *float64
}
type testPrecipTiming struct {
MaxPopPercent *int
MaxPopTime string
PrecipitationWindows []testPrecipWindow
ThunderMentioned bool
}
type testPrecipWindow struct {
PeriodBegins string
PeriodEnds string
MaxPopPercent *int
MaxPopTime string
}
type testAlertDigest struct {
Missing bool
Relevant []testAlert
}
type testAlert struct {
Event string
Headline string
Severity string
}
type testSPCOutlooks struct {
Outlooks []testSPCOutlook
}
type testSPCOutlook struct {
Label string
LabelText string
OutlookType string
PeriodBegins string
PeriodEnds string
}
type testForecastDiscussion struct {
@@ -171,6 +261,30 @@ type testForecastDiscussion struct {
ShortTerm string
}
type testSPCDiscussion struct {
Discussions []testSPCDiscussionRecord
}
type testSPCDiscussionRecord struct {
Headline string
Summary string
Discussion string
}
type testWeatherStory struct {
Available bool
Title string
Description string
}
func floatPtr(value float64) *float64 {
return &value
}
func intPtr(value int) *int {
return &value
}
func assertOrderedText(t *testing.T, text string, ordered []string) {
t.Helper()
previousIndex := -1

View File

@@ -1,8 +1,8 @@
# {{ .ReportTitle }}
# {{ .Report.Title }}
{{ .LocationName }}
Valid: {{ .ValidPeriod }}
Generated: {{ .GeneratedAt }}
{{ .Report.LocationName }}
Valid: {{ .Report.ValidPeriodLabel }}
Generated: {{ .Report.GeneratedAtLabel }}
## Summary
@@ -23,49 +23,78 @@ Generated: {{ .GeneratedAt }}
{{ end }}
## Current Conditions
{{ .CurrentConditions }}
{{ with .Modules.CurrentConditions }}
{{ with .ConditionText }}{{ . }}{{ end }}{{ with .TemperatureF }}; {{ . }} F{{ end }}{{ with .ApparentTemperatureF }}; feels like {{ . }} F{{ end }}{{ with .RelativeHumidityPercent }}; humidity {{ . }}%{{ end }}{{ with .WindDirection }}; wind {{ . }}{{ end }}{{ with .WindSpeedMph }} {{ . }} mph{{ end }}.
{{ else }}
No current conditions available.
{{ end }}
## Hourly Forecast
{{ range .HourlyForecast }}
- {{ .Time }}: {{ .Summary }}{{ with .Temperature }}; {{ . }}{{ end }}{{ with .Precipitation }}; {{ . }}{{ end }}{{ with .Wind }}; {{ . }}{{ end }}
{{ with .Modules.HourlyForecast }}{{ range .Periods }}
- {{ if .PeriodBegins }}{{ .PeriodBegins }}{{ else }}{{ .Name }}{{ end }}: {{ if .TextDescription }}{{ .TextDescription }}{{ else }}{{ .Name }}{{ end }}{{ with .TemperatureF }}; {{ . }} F{{ end }}{{ with .ProbabilityOfPrecipitationPercent }}; {{ . }}% precipitation{{ end }}{{ if .WindDirection }}; wind {{ .WindDirection }}{{ with .WindSpeedMph }} {{ . }} mph{{ end }}{{ else }}{{ with .WindSpeedMph }}; wind {{ . }} mph{{ end }}{{ end }}{{ with .WindGustMph }}, gusts {{ . }} mph{{ end }}
{{ else }}
- No hourly forecast rows available.
{{ end }}{{ else }}
- No hourly forecast rows available.
{{ end }}
## Precipitation Timing
{{ .PrecipitationTiming }}
{{ with .Modules.PrecipTiming }}
{{ with .MaxPopPercent }}Peak precipitation probability: {{ . }}%{{ with $.Modules.PrecipTiming.MaxPopTime }} at {{ . }}{{ end }}.
{{ end }}{{ range .PrecipitationWindows }}{{ $window := . }}
- Window: {{ .PeriodBegins }}{{ with .PeriodEnds }} to {{ . }}{{ end }}{{ with .MaxPopPercent }}; max {{ . }}%{{ with $window.MaxPopTime }} at {{ . }}{{ end }}{{ end }}
{{ else }}
No precipitation windows above threshold.
{{ end }}{{ if .ThunderMentioned }}
Thunder is mentioned in the forecast.
{{ end }}{{ else }}
No precipitation timing signal above threshold.
{{ end }}
## Alerts
{{ range .Alerts }}
- {{ . }}
{{ with .Modules.AlertDigest }}{{ if .Missing }}
- Alert source missing.
{{ else }}{{ range .Relevant }}
- {{ if .Event }}{{ .Event }}{{ else }}{{ .Headline }}{{ end }}{{ with .Headline }}: {{ . }}{{ end }}{{ with .Severity }} ({{ . }}){{ end }}
{{ else }}
- No active alert overlaps for this report period.
{{ end }}{{ end }}{{ else }}
- No active alert overlaps for this report period.
{{ end }}
## SPC Outlooks
{{ range .SPCOutlooks }}
- {{ . }}
{{ with .Modules.SPCConvectiveOutlooks }}{{ range .Outlooks }}
- {{ if .LabelText }}{{ .LabelText }}{{ else }}{{ if .Label }}{{ .Label }}{{ else }}{{ .OutlookType }}{{ end }}{{ end }}{{ with .PeriodBegins }} from {{ . }}{{ end }}{{ with .PeriodEnds }} to {{ . }}{{ end }}
{{ else }}
- No overlapping SPC outlooks.
{{ end }}{{ else }}
- No overlapping SPC outlooks.
{{ end }}
## Forecast Discussion
{{ range .ForecastDiscussion.KeyMessages }}
{{ with .Modules.AreaForecastDiscussion }}{{ range .KeyMessages }}
- {{ . }}
{{ end }}{{ with .ForecastDiscussion.ShortTerm }}
{{ end }}{{ with .ShortTerm }}
{{ . }}
{{ end }}{{ else }}
No area forecast discussion available.
{{ end }}
## SPC Discussion
{{ range .SPCDiscussions }}
- {{ . }}
{{ with .Modules.SPCConvectiveDiscussion }}{{ range .Discussions }}
- {{ if .Headline }}{{ .Headline }}{{ else }}{{ if .Summary }}{{ .Summary }}{{ else }}{{ .Discussion }}{{ end }}{{ end }}{{ with .Summary }}: {{ . }}{{ end }}
{{ else }}
- No overlapping SPC discussion.
{{ end }}{{ else }}
- No overlapping SPC discussion.
{{ end }}
## Weather Story
{{ .WeatherStory }}
{{ with .Modules.WeatherStory }}{{ if .Available }}
{{ with .Title }}{{ . }}{{ end }}{{ with .Description }} - {{ . }}{{ end }}
{{ else }}
No weather story available.
{{ end }}{{ else }}
No weather story available.
{{ end }}