Validate render identity and daypart fallbacks
This commit is contained in:
@@ -40,18 +40,24 @@ receive the same enforcement in this package.
|
||||
|
||||
## Render contexts
|
||||
|
||||
The catalog's report-specific builders receive briefing metadata, a rich module
|
||||
snapshot, collected facts, derived facts, and the matching validated generated
|
||||
text. They decode the module stanzas needed by the template and build typed
|
||||
Daily, Today, Tomorrow, or Hourly contexts. Context construction validates
|
||||
metadata and periods, preserves rich module values, and uses ordered slices for
|
||||
template iteration rather than maps.
|
||||
The catalog's report-specific builders receive the prepared report identity, a
|
||||
rich module snapshot, derived facts needed to order dayparts, and the matching
|
||||
validated generated text. They require the identity's report ID to match the
|
||||
selected builder. When the optional metadata stanza is present, every shared
|
||||
identity field must agree with that prepared authority before context
|
||||
construction continues. Builders then decode the module stanzas needed by the
|
||||
template and build typed Daily, Today, Tomorrow, or Hourly contexts. Contexts
|
||||
expose only display-ready report values, generated prose, and module values;
|
||||
they do not expose complete collected or derived fact bundles. Ordered slices
|
||||
remain the template iteration surface rather than maps.
|
||||
|
||||
Optional source stanzas become nil or fallback context fields. Missing required
|
||||
stanzas, type-decoding failures, invalid metadata, or a generated-text type
|
||||
that does not match the chosen handler fail before template execution. Prompt
|
||||
packages, raw Promptkit output handling, and template asset lookup remain
|
||||
outside this package.
|
||||
Optional source stanzas become nil or fallback context fields. Today also
|
||||
computes whether its ordered dayparts contain a displayable condition so the
|
||||
template can render either rows or its explicit no-details fallback. Missing
|
||||
required stanzas, type-decoding failures, conflicting identity values, invalid
|
||||
metadata, or a generated-text type that does not match the chosen handler fail
|
||||
before template execution. Prompt packages, raw Promptkit output handling, and
|
||||
template asset lookup remain outside this package.
|
||||
|
||||
## Verification and invariants
|
||||
|
||||
|
||||
@@ -28,8 +28,9 @@ failures actionable with template or partial context.
|
||||
Top-level templates decide which shared partials they invoke. The current
|
||||
partials cover daypart forecast variants, alert digest, and precipitation
|
||||
timing. Template code receives curated typed contexts rather than raw data
|
||||
packages, and it must not reimplement weather selection or generated-text
|
||||
validation.
|
||||
packages or complete fact bundles, and it must not reimplement weather
|
||||
selection or generated-text validation. Context construction rejects
|
||||
report-identity disagreements before template execution.
|
||||
|
||||
## Boundaries and verification
|
||||
|
||||
@@ -38,8 +39,8 @@ text, construct contexts, resolve report definitions, write state, execute
|
||||
Promptkit, or upload reports. It produces Markdown bytes for application
|
||||
orchestration to persist.
|
||||
|
||||
Focused tests cover template lookup, rendering, partial
|
||||
behavior, missing keys, and malformed context:
|
||||
Focused tests cover template lookup, rendering, partial behavior, daypart
|
||||
fallbacks, missing keys, and malformed context:
|
||||
|
||||
```sh
|
||||
go test ./internal/reporttemplate
|
||||
|
||||
@@ -51,7 +51,9 @@ from rendering.
|
||||
schema, validator, render context, and template together. The validation and
|
||||
catalog rules are owned by [Generated Text internals](internal/generatedtext.md).
|
||||
- Use `.Modules.Dayparts` for ordered daypart output. Do not range over
|
||||
`.Modules.DerivedDaypartSummaries`, which is a map.
|
||||
`.Modules.DerivedDaypartSummaries`, which is a map. The Today partial uses
|
||||
`.Modules.HasDaypartDetails` to ensure its heading has either rows or the
|
||||
explicit no-details fallback.
|
||||
|
||||
Minimal optional-value pattern:
|
||||
|
||||
@@ -92,7 +94,7 @@ to render the section:
|
||||
|
||||
## Render Context
|
||||
|
||||
Every rendered template receives one typed context with these five top-level
|
||||
Every rendered template receives one typed context with these three top-level
|
||||
fields:
|
||||
|
||||
| Field | Purpose |
|
||||
@@ -100,12 +102,6 @@ fields:
|
||||
| `.Report` | Display labels and canonical report timing metadata. |
|
||||
| `.GeneratedText` | Validated prose supplied by Promptkit. |
|
||||
| `.Modules` | Deterministic, typed values prepared for Markdown rendering. |
|
||||
| `.Collected` | Normalized upstream facts for advanced use. |
|
||||
| `.Derived` | Shared calculated facts for advanced use. |
|
||||
|
||||
`.Collected` and `.Derived` are available for an exceptional display need, but
|
||||
they are lower-level contracts. Keep reusable weather derivation in Go and use
|
||||
the module surface for normal template work.
|
||||
|
||||
### Report Metadata
|
||||
|
||||
@@ -144,8 +140,9 @@ Module pointers can be nil when their source or policy permits omission.
|
||||
|
||||
| Module field | Available in |
|
||||
| --- | --- |
|
||||
| `.Modules.Metadata`, `.Modules.CurrentConditions`, `.Modules.HourlyForecast`, `.Modules.PrecipTiming`, `.Modules.AlertDigest`, `.Modules.SPCConvectiveOutlooks`, `.Modules.AreaForecastDiscussion`, `.Modules.SPCConvectiveDiscussion`, `.Modules.WeatherStory` | All four contexts |
|
||||
| `.Modules.CurrentConditions`, `.Modules.HourlyForecast`, `.Modules.PrecipTiming`, `.Modules.AlertDigest`, `.Modules.SPCConvectiveOutlooks`, `.Modules.AreaForecastDiscussion`, `.Modules.SPCConvectiveDiscussion`, `.Modules.WeatherStory` | All four contexts |
|
||||
| `.Modules.DerivedDailySummary`, `.Modules.DerivedDaypartSummaries`, `.Modules.Dayparts` | Daily, Today, Tomorrow |
|
||||
| `.Modules.HasDaypartDetails` | Today |
|
||||
| `.Modules.OutdoorWindows`, `.Modules.DailyPlanning` | Daily |
|
||||
| `.Modules.TodayPlanning` | Today |
|
||||
| `.Modules.TomorrowPlanning` | Tomorrow |
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/collect"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/generatedtext"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
// executions for one resolved report.
|
||||
type preparedReport struct {
|
||||
resolved report.Resolved
|
||||
reportFacts ReportFacts
|
||||
derived facts.DerivedFacts
|
||||
moduleSnapshot module.Snapshot
|
||||
identity briefing.PreparedIdentity
|
||||
sourceWarnings []weatherdata.SourceWarning
|
||||
@@ -73,9 +74,9 @@ func prepareReport(req prepareReportRequest) (preparedReport, error) {
|
||||
return preparedReport{}, &preparationError{operation: "lookup generated text catalog", err: err}
|
||||
}
|
||||
|
||||
clonedFacts, err := clonePreparedValue(reportFacts)
|
||||
clonedDerived, err := clonePreparedValue(reportFacts.Derived)
|
||||
if err != nil {
|
||||
return preparedReport{}, &preparationError{operation: "copy prepared report facts", err: err}
|
||||
return preparedReport{}, &preparationError{operation: "copy prepared derived facts", err: err}
|
||||
}
|
||||
clonedSnapshot, err := clonePreparedValue(moduleSnapshot)
|
||||
if err != nil {
|
||||
@@ -87,7 +88,7 @@ func prepareReport(req prepareReportRequest) (preparedReport, error) {
|
||||
}
|
||||
prepared := preparedReport{
|
||||
resolved: cloneResolved(req.Resolved),
|
||||
reportFacts: clonedFacts,
|
||||
derived: clonedDerived,
|
||||
moduleSnapshot: clonedSnapshot,
|
||||
identity: clonedIdentity,
|
||||
sourceWarnings: append([]weatherdata.SourceWarning(nil), clonedIdentity.SourceWarnings...),
|
||||
@@ -120,20 +121,20 @@ func (p preparedReport) sourceWarningsCopy() []weatherdata.SourceWarning {
|
||||
return append([]weatherdata.SourceWarning(nil), p.sourceWarnings...)
|
||||
}
|
||||
|
||||
func (p preparedReport) renderInputs() (briefing.PreparedIdentity, module.Snapshot, ReportFacts, error) {
|
||||
func (p preparedReport) renderInputs() (briefing.PreparedIdentity, module.Snapshot, facts.DerivedFacts, error) {
|
||||
identity, err := clonePreparedValue(p.identity)
|
||||
if err != nil {
|
||||
return briefing.PreparedIdentity{}, module.Snapshot{}, ReportFacts{}, err
|
||||
return briefing.PreparedIdentity{}, module.Snapshot{}, facts.DerivedFacts{}, err
|
||||
}
|
||||
snapshot, err := clonePreparedValue(p.moduleSnapshot)
|
||||
if err != nil {
|
||||
return briefing.PreparedIdentity{}, module.Snapshot{}, ReportFacts{}, err
|
||||
return briefing.PreparedIdentity{}, module.Snapshot{}, facts.DerivedFacts{}, err
|
||||
}
|
||||
reportFacts, err := clonePreparedValue(p.reportFacts)
|
||||
derived, err := clonePreparedValue(p.derived)
|
||||
if err != nil {
|
||||
return briefing.PreparedIdentity{}, module.Snapshot{}, ReportFacts{}, err
|
||||
return briefing.PreparedIdentity{}, module.Snapshot{}, facts.DerivedFacts{}, err
|
||||
}
|
||||
return identity, snapshot, reportFacts, nil
|
||||
return identity, snapshot, derived, nil
|
||||
}
|
||||
|
||||
func clonePreparedValue[T any](value T) (T, error) {
|
||||
|
||||
@@ -37,14 +37,15 @@ func TestPrepareReportBuildsImmutableDeterministicInputs(t *testing.T) {
|
||||
|
||||
originalDataPackage := append([]byte(nil), prepared.dataPackage...)
|
||||
originalIdentity := prepared.identity
|
||||
originalDerived := prepared.derived
|
||||
originalWarnings := append([]weatherdata.SourceWarning(nil), prepared.sourceWarnings...)
|
||||
identity, snapshot, reportFacts, err := prepared.renderInputs()
|
||||
identity, snapshot, derived, err := prepared.renderInputs()
|
||||
if err != nil {
|
||||
t.Fatalf("renderInputs() error = %v", err)
|
||||
}
|
||||
identity.SourceWarnings = append(identity.SourceWarnings, weatherdata.SourceWarning{Source: "test", Message: "consumer mutation"})
|
||||
snapshot.Outputs = nil
|
||||
reportFacts.Collected.Hourly.Periods[0].TextDescription = "consumer mutation"
|
||||
derived.PrecipTiming.ThunderMentioned = false
|
||||
bundle.Hourly.Periods[0].TextDescription = "mutated after preparation"
|
||||
bundle.Warnings = append(bundle.Warnings, weatherdata.SourceWarning{Source: "test", Message: "mutated warning"})
|
||||
if len(bundle.Sources) > 0 {
|
||||
@@ -54,13 +55,10 @@ func TestPrepareReportBuildsImmutableDeterministicInputs(t *testing.T) {
|
||||
bundle.Sources[0].Query["mutated"] = "true"
|
||||
}
|
||||
|
||||
if !bytes.Equal(prepared.dataPackage, originalDataPackage) || !reflect.DeepEqual(prepared.identity, originalIdentity) || !reflect.DeepEqual(prepared.sourceWarnings, originalWarnings) {
|
||||
if !bytes.Equal(prepared.dataPackage, originalDataPackage) || !reflect.DeepEqual(prepared.identity, originalIdentity) || !reflect.DeepEqual(prepared.derived, originalDerived) || !reflect.DeepEqual(prepared.sourceWarnings, originalWarnings) {
|
||||
t.Fatalf("prepared values changed after caller mutation: %#v", prepared)
|
||||
}
|
||||
if prepared.reportFacts.Collected.Hourly.Periods[0].TextDescription == "mutated after preparation" {
|
||||
t.Fatal("prepared report facts retain caller-owned weather data")
|
||||
}
|
||||
if prepared.reportFacts.Collected.Hourly.Periods[0].TextDescription == "consumer mutation" || len(prepared.moduleSnapshot.Outputs) == 0 {
|
||||
if len(prepared.moduleSnapshot.Outputs) == 0 {
|
||||
t.Fatal("prepared report values retain consumer mutation")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,11 +115,11 @@ func executePreparedProfile(ctx context.Context, req profileExecutionRequest) (p
|
||||
if err != nil {
|
||||
return outcome, nil, &profileExecutionError{operation: "validate generated text", err: err}
|
||||
}
|
||||
identity, snapshot, reportFacts, err := req.Prepared.renderInputs()
|
||||
identity, snapshot, derived, err := req.Prepared.renderInputs()
|
||||
if err != nil {
|
||||
return outcome, nil, &profileExecutionError{operation: "copy prepared render inputs", err: err}
|
||||
}
|
||||
renderContext, err := req.Prepared.handler.BuildRenderContext(identity, snapshot, reportFacts.Collected, reportFacts.Derived, generatedText)
|
||||
renderContext, err := req.Prepared.handler.BuildRenderContext(identity, snapshot, derived, generatedText)
|
||||
if err != nil {
|
||||
return outcome, nil, &profileExecutionError{operation: "build render context", err: err}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const (
|
||||
|
||||
type validator func([]byte) (any, []byte, error)
|
||||
|
||||
type renderContextBuilder func(report.ID, string, briefing.PreparedIdentity, module.Snapshot, facts.CollectedFacts, facts.DerivedFacts, any) (any, error)
|
||||
type renderContextBuilder func(report.ID, string, briefing.PreparedIdentity, module.Snapshot, facts.DerivedFacts, any) (any, error)
|
||||
|
||||
type catalogEntry struct {
|
||||
reportID report.ID
|
||||
@@ -140,11 +140,11 @@ func (h Handler) Validate(data []byte) (any, []byte, error) {
|
||||
return h.validate(data)
|
||||
}
|
||||
|
||||
func (h Handler) BuildRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
||||
func (h Handler) BuildRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, derived facts.DerivedFacts, generated any) (any, error) {
|
||||
if h.renderContextBuilder == nil {
|
||||
return nil, fmt.Errorf("render-context builder is not registered for template %q on report %q", h.templateID, h.reportID)
|
||||
}
|
||||
return h.renderContextBuilder(h.reportID, h.templateID, identity, snapshot, collected, derived, generated)
|
||||
return h.renderContextBuilder(h.reportID, h.templateID, identity, snapshot, derived, generated)
|
||||
}
|
||||
|
||||
func (h Handler) Render(data any) ([]byte, error) {
|
||||
@@ -171,34 +171,34 @@ func validateTomorrow(data []byte) (any, []byte, error) {
|
||||
return ValidateTomorrow(data)
|
||||
}
|
||||
|
||||
func buildHourlyContext(reportID report.ID, templateID string, identity briefing.PreparedIdentity, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
||||
func buildHourlyContext(reportID report.ID, templateID string, identity briefing.PreparedIdentity, snapshot module.Snapshot, _ facts.DerivedFacts, generated any) (any, error) {
|
||||
hourly, ok := generated.(Hourly)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("report template %q requires hourly generated text for report %q", templateID, reportID)
|
||||
}
|
||||
return BuildHourlyRenderContext(identity, snapshot, hourly, collected, derived)
|
||||
return BuildHourlyRenderContext(identity, snapshot, hourly)
|
||||
}
|
||||
|
||||
func buildDailyContext(reportID report.ID, templateID string, identity briefing.PreparedIdentity, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
||||
func buildDailyContext(reportID report.ID, templateID string, identity briefing.PreparedIdentity, snapshot module.Snapshot, derived facts.DerivedFacts, generated any) (any, error) {
|
||||
daily, ok := generated.(Daily)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("report template %q requires daily generated text for report %q", templateID, reportID)
|
||||
}
|
||||
return BuildDailyRenderContext(identity, snapshot, daily, collected, derived)
|
||||
return BuildDailyRenderContext(identity, snapshot, daily, derived)
|
||||
}
|
||||
|
||||
func buildTodayContext(reportID report.ID, templateID string, identity briefing.PreparedIdentity, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
||||
func buildTodayContext(reportID report.ID, templateID string, identity briefing.PreparedIdentity, snapshot module.Snapshot, derived facts.DerivedFacts, generated any) (any, error) {
|
||||
today, ok := generated.(Today)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("report template %q requires today generated text for report %q", templateID, reportID)
|
||||
}
|
||||
return BuildTodayRenderContext(identity, snapshot, today, collected, derived)
|
||||
return BuildTodayRenderContext(identity, snapshot, today, derived)
|
||||
}
|
||||
|
||||
func buildTomorrowContext(reportID report.ID, templateID string, identity briefing.PreparedIdentity, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
||||
func buildTomorrowContext(reportID report.ID, templateID string, identity briefing.PreparedIdentity, snapshot module.Snapshot, derived facts.DerivedFacts, generated any) (any, error) {
|
||||
tomorrow, ok := generated.(Tomorrow)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("report template %q requires tomorrow generated text for report %q", templateID, reportID)
|
||||
}
|
||||
return BuildTomorrowRenderContext(identity, snapshot, tomorrow, collected, derived)
|
||||
return BuildTomorrowRenderContext(identity, snapshot, tomorrow, derived)
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ func TestCatalogBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LookupDefinition(hourly) error = %v", err)
|
||||
}
|
||||
_, err = hourlyHandler.BuildRenderContext(testMetadata(), testSnapshot(t), testCollected(), testDerived(), Tomorrow{
|
||||
_, err = hourlyHandler.BuildRenderContext(testMetadata(), testSnapshot(t), testDerived(), Tomorrow{
|
||||
Summary: "Storms become more likely tomorrow.",
|
||||
ForecastDiscussion: []string{"A front will keep showers in the forecast."},
|
||||
})
|
||||
@@ -290,7 +290,7 @@ func TestCatalogBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LookupDefinition(tomorrow) error = %v", err)
|
||||
}
|
||||
_, err = tomorrowHandler.BuildRenderContext(testTomorrowMetadata(), testTomorrowSnapshot(t), testCollected(), testTomorrowDerived(), Hourly{
|
||||
_, err = tomorrowHandler.BuildRenderContext(testTomorrowMetadata(), testTomorrowSnapshot(t), testTomorrowDerived(), Hourly{
|
||||
Summary: "Storm chances increase.",
|
||||
ForecastDiscussion: "A front will keep showers in the forecast.",
|
||||
})
|
||||
@@ -309,7 +309,7 @@ func TestCatalogBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LookupDefinition(today) error = %v", err)
|
||||
}
|
||||
_, err = todayHandler.BuildRenderContext(testTodayMetadata(), testTodaySnapshot(t), testCollected(), testTodayDerived(), Tomorrow{
|
||||
_, err = todayHandler.BuildRenderContext(testTodayMetadata(), testTodaySnapshot(t), testTodayDerived(), Tomorrow{
|
||||
Summary: "Storms become more likely tomorrow.",
|
||||
ForecastDiscussion: []string{"A front will keep showers in the forecast."},
|
||||
})
|
||||
@@ -328,7 +328,7 @@ func TestCatalogBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LookupDefinition(daily) error = %v", err)
|
||||
}
|
||||
_, err = dailyHandler.BuildRenderContext(testDailyMetadata(), testDailySnapshot(t), testCollected(), testDailyDerived(), Tomorrow{
|
||||
_, err = dailyHandler.BuildRenderContext(testDailyMetadata(), testDailySnapshot(t), testDailyDerived(), Tomorrow{
|
||||
Summary: "Storms become more likely tomorrow.",
|
||||
ForecastDiscussion: []string{"A front will keep showers in the forecast."},
|
||||
})
|
||||
@@ -349,7 +349,7 @@ func TestCatalogBuildRenderContextSupportsDaily(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LookupDefinition(daily) error = %v", err)
|
||||
}
|
||||
ctx, err := handler.BuildRenderContext(testDailyMetadata(), testDailySnapshot(t), testCollected(), testDailyDerived(), Daily{
|
||||
ctx, err := handler.BuildRenderContext(testDailyMetadata(), testDailySnapshot(t), testDailyDerived(), Daily{
|
||||
Summary: "Showers are possible during the selected day.",
|
||||
ForecastDiscussion: []string{"A front will keep rain chances in the forecast."},
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ package generatedtext
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
)
|
||||
|
||||
@@ -16,8 +18,6 @@ type HourlyRenderContext struct {
|
||||
Report HourlyReportContext
|
||||
GeneratedText Hourly
|
||||
Modules HourlyTemplateModules
|
||||
Collected facts.CollectedFacts
|
||||
Derived facts.DerivedFacts
|
||||
}
|
||||
|
||||
type HourlyReportContext struct {
|
||||
@@ -31,7 +31,6 @@ type HourlyReportContext struct {
|
||||
}
|
||||
|
||||
type HourlyTemplateModules struct {
|
||||
Metadata *briefing.MetadataModule
|
||||
CurrentConditions *briefing.CurrentConditionsModule
|
||||
HourlyForecast *briefing.HourlyForecastModule
|
||||
PrecipTiming *briefing.PrecipTimingModule
|
||||
@@ -46,24 +45,18 @@ type TomorrowRenderContext struct {
|
||||
Report TomorrowReportContext
|
||||
GeneratedText Tomorrow
|
||||
Modules TomorrowTemplateModules
|
||||
Collected facts.CollectedFacts
|
||||
Derived facts.DerivedFacts
|
||||
}
|
||||
|
||||
type DailyRenderContext struct {
|
||||
Report DailyReportContext
|
||||
GeneratedText Daily
|
||||
Modules DailyTemplateModules
|
||||
Collected facts.CollectedFacts
|
||||
Derived facts.DerivedFacts
|
||||
}
|
||||
|
||||
type TodayRenderContext struct {
|
||||
Report TodayReportContext
|
||||
GeneratedText Today
|
||||
Modules TodayTemplateModules
|
||||
Collected facts.CollectedFacts
|
||||
Derived facts.DerivedFacts
|
||||
}
|
||||
|
||||
type TodayReportContext struct {
|
||||
@@ -78,12 +71,12 @@ type TodayReportContext struct {
|
||||
}
|
||||
|
||||
type TodayTemplateModules struct {
|
||||
Metadata *briefing.MetadataModule
|
||||
CurrentConditions *briefing.CurrentConditionsModule
|
||||
HourlyForecast *briefing.HourlyForecastModule
|
||||
DerivedDailySummary *briefing.DerivedDailySummaryModule
|
||||
DerivedDaypartSummaries *map[string]briefing.DerivedDaypartSummaryModule
|
||||
Dayparts []TodayDaypartContext
|
||||
HasDaypartDetails bool
|
||||
PrecipTiming *briefing.PrecipTimingModule
|
||||
AlertDigest *briefing.AlertDigestModule
|
||||
SPCConvectiveOutlooks *briefing.SPCConvectiveOutlooksModule
|
||||
@@ -110,7 +103,6 @@ type TomorrowReportContext struct {
|
||||
}
|
||||
|
||||
type TomorrowTemplateModules struct {
|
||||
Metadata *briefing.MetadataModule
|
||||
CurrentConditions *briefing.CurrentConditionsModule
|
||||
HourlyForecast *briefing.HourlyForecastModule
|
||||
DerivedDailySummary *briefing.DerivedDailySummaryModule
|
||||
@@ -142,7 +134,6 @@ type DailyReportContext struct {
|
||||
}
|
||||
|
||||
type DailyTemplateModules struct {
|
||||
Metadata *briefing.MetadataModule
|
||||
CurrentConditions *briefing.CurrentConditionsModule
|
||||
HourlyForecast *briefing.HourlyForecastModule
|
||||
DerivedDailySummary *briefing.DerivedDailySummaryModule
|
||||
@@ -175,7 +166,6 @@ type dayStyleReportContext struct {
|
||||
}
|
||||
|
||||
type dayStyleTemplateModules struct {
|
||||
Metadata *briefing.MetadataModule
|
||||
CurrentConditions *briefing.CurrentConditionsModule
|
||||
HourlyForecast *briefing.HourlyForecastModule
|
||||
DerivedDailySummary *briefing.DerivedDailySummaryModule
|
||||
@@ -188,7 +178,10 @@ type dayStyleTemplateModules struct {
|
||||
WeatherStory *briefing.WeatherStoryModule
|
||||
}
|
||||
|
||||
func BuildHourlyRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Hourly, collected facts.CollectedFacts, derived facts.DerivedFacts) (HourlyRenderContext, error) {
|
||||
func BuildHourlyRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Hourly) (HourlyRenderContext, error) {
|
||||
if err := validateRenderIdentity(report.Hourly, identity, snapshot); err != nil {
|
||||
return HourlyRenderContext{}, err
|
||||
}
|
||||
location, err := timeutil.LoadLocation(identity.Timezone)
|
||||
if err != nil {
|
||||
return HourlyRenderContext{}, fmt.Errorf("build hourly render context: %w", err)
|
||||
@@ -215,12 +208,13 @@ func BuildHourlyRenderContext(identity briefing.PreparedIdentity, snapshot modul
|
||||
},
|
||||
GeneratedText: generated,
|
||||
Modules: modules,
|
||||
Collected: collected,
|
||||
Derived: derived,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func BuildDailyRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Daily, collected facts.CollectedFacts, derived facts.DerivedFacts) (DailyRenderContext, error) {
|
||||
func BuildDailyRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Daily, derived facts.DerivedFacts) (DailyRenderContext, error) {
|
||||
if err := validateRenderIdentity(report.Daily, identity, snapshot); err != nil {
|
||||
return DailyRenderContext{}, err
|
||||
}
|
||||
reportContext, err := buildDayStyleReportContext(identity, "daily", func(dayName string) string {
|
||||
return dayName + "'s Weather"
|
||||
})
|
||||
@@ -235,12 +229,13 @@ func BuildDailyRenderContext(identity briefing.PreparedIdentity, snapshot module
|
||||
Report: reportContext.dailyReportContext(),
|
||||
GeneratedText: generated,
|
||||
Modules: modules,
|
||||
Collected: collected,
|
||||
Derived: derived,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func BuildTodayRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Today, collected facts.CollectedFacts, derived facts.DerivedFacts) (TodayRenderContext, error) {
|
||||
func BuildTodayRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Today, derived facts.DerivedFacts) (TodayRenderContext, error) {
|
||||
if err := validateRenderIdentity(report.Today, identity, snapshot); err != nil {
|
||||
return TodayRenderContext{}, err
|
||||
}
|
||||
reportContext, err := buildDayStyleReportContext(identity, "today", func(string) string {
|
||||
return "Today's Weather"
|
||||
})
|
||||
@@ -255,12 +250,13 @@ func BuildTodayRenderContext(identity briefing.PreparedIdentity, snapshot module
|
||||
Report: reportContext.todayReportContext(),
|
||||
GeneratedText: generated,
|
||||
Modules: modules,
|
||||
Collected: collected,
|
||||
Derived: derived,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func BuildTomorrowRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Tomorrow, collected facts.CollectedFacts, derived facts.DerivedFacts) (TomorrowRenderContext, error) {
|
||||
func BuildTomorrowRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Tomorrow, derived facts.DerivedFacts) (TomorrowRenderContext, error) {
|
||||
if err := validateRenderIdentity(report.Tomorrow, identity, snapshot); err != nil {
|
||||
return TomorrowRenderContext{}, err
|
||||
}
|
||||
reportContext, err := buildDayStyleReportContext(identity, "tomorrow", func(dayName string) string {
|
||||
return dayName + "'s Weather"
|
||||
})
|
||||
@@ -275,11 +271,36 @@ func BuildTomorrowRenderContext(identity briefing.PreparedIdentity, snapshot mod
|
||||
Report: reportContext.tomorrowReportContext(),
|
||||
GeneratedText: generated,
|
||||
Modules: modules,
|
||||
Collected: collected,
|
||||
Derived: derived,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateRenderIdentity(expected report.ID, identity briefing.PreparedIdentity, snapshot module.Snapshot) error {
|
||||
if identity.ReportID != expected {
|
||||
return fmt.Errorf("render identity report %q does not match expected report %q", identity.ReportID, expected)
|
||||
}
|
||||
metadata, found, err := module.StanzaValue[briefing.MetadataModule](snapshot, string(module.Metadata))
|
||||
if err != nil {
|
||||
return fmt.Errorf("read metadata module for report %q: %w", expected, err)
|
||||
}
|
||||
if !found || metadataMatchesIdentity(metadata, identity) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("metadata module does not match render identity for report %q", expected)
|
||||
}
|
||||
|
||||
func metadataMatchesIdentity(metadata briefing.MetadataModule, identity briefing.PreparedIdentity) bool {
|
||||
if metadata.RunID != identity.RunID || metadata.ReportID != identity.ReportID || metadata.Variant != identity.Variant || metadata.PromptID != identity.PromptID || !metadata.GeneratedAt.Equal(identity.GeneratedAt) || metadata.Units != identity.Units || metadata.Timezone != identity.Timezone || metadata.ValidPeriod != identity.ValidPeriod || !reflect.DeepEqual(metadata.Location, identity.Location) || len(metadata.SourceWarnings) != len(identity.SourceWarnings) {
|
||||
return false
|
||||
}
|
||||
for index, warning := range identity.SourceWarnings {
|
||||
summary := metadata.SourceWarnings[index]
|
||||
if summary.Source != warning.Source || summary.Code != warning.Code || summary.Severity != warning.Severity || summary.Message != warning.Message || summary.CompletenessImpact != warning.CompletenessImpact {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func buildDayStyleReportContext(identity briefing.PreparedIdentity, name string, title func(string) string) (dayStyleReportContext, error) {
|
||||
location, err := timeutil.LoadLocation(identity.Timezone)
|
||||
if err != nil {
|
||||
@@ -359,7 +380,6 @@ func dailyTemplateModules(snapshot module.Snapshot, derived facts.DerivedFacts)
|
||||
return DailyTemplateModules{}, err
|
||||
}
|
||||
return DailyTemplateModules{
|
||||
Metadata: common.Metadata,
|
||||
CurrentConditions: common.CurrentConditions,
|
||||
HourlyForecast: common.HourlyForecast,
|
||||
DerivedDailySummary: common.DerivedDailySummary,
|
||||
@@ -385,13 +405,14 @@ func todayTemplateModules(snapshot module.Snapshot, derived facts.DerivedFacts)
|
||||
if err != nil {
|
||||
return TodayTemplateModules{}, err
|
||||
}
|
||||
dayparts := orderedTodayDayparts(common.DerivedDaypartSummaries, derived.DaypartSummaries)
|
||||
return TodayTemplateModules{
|
||||
Metadata: common.Metadata,
|
||||
CurrentConditions: common.CurrentConditions,
|
||||
HourlyForecast: common.HourlyForecast,
|
||||
DerivedDailySummary: common.DerivedDailySummary,
|
||||
DerivedDaypartSummaries: common.DerivedDaypartSummaries,
|
||||
Dayparts: orderedTodayDayparts(common.DerivedDaypartSummaries, derived.DaypartSummaries),
|
||||
Dayparts: dayparts,
|
||||
HasDaypartDetails: hasTodayDaypartDetails(dayparts),
|
||||
PrecipTiming: common.PrecipTiming,
|
||||
AlertDigest: common.AlertDigest,
|
||||
SPCConvectiveOutlooks: common.SPCConvectiveOutlooks,
|
||||
@@ -404,10 +425,6 @@ func todayTemplateModules(snapshot module.Snapshot, derived facts.DerivedFacts)
|
||||
|
||||
func hourlyTemplateModules(snapshot module.Snapshot) (HourlyTemplateModules, error) {
|
||||
lookup := newModuleSnapshotLookup(snapshot)
|
||||
metadata, err := lookup.metadata()
|
||||
if err != nil {
|
||||
return HourlyTemplateModules{}, err
|
||||
}
|
||||
current, err := lookup.currentConditions()
|
||||
if err != nil {
|
||||
return HourlyTemplateModules{}, err
|
||||
@@ -441,7 +458,6 @@ func hourlyTemplateModules(snapshot module.Snapshot) (HourlyTemplateModules, err
|
||||
return HourlyTemplateModules{}, err
|
||||
}
|
||||
return HourlyTemplateModules{
|
||||
Metadata: metadata,
|
||||
CurrentConditions: current,
|
||||
HourlyForecast: hourly,
|
||||
PrecipTiming: precip,
|
||||
@@ -463,7 +479,6 @@ func tomorrowTemplateModules(snapshot module.Snapshot, derived facts.DerivedFact
|
||||
return TomorrowTemplateModules{}, err
|
||||
}
|
||||
return TomorrowTemplateModules{
|
||||
Metadata: common.Metadata,
|
||||
CurrentConditions: common.CurrentConditions,
|
||||
HourlyForecast: common.HourlyForecast,
|
||||
DerivedDailySummary: common.DerivedDailySummary,
|
||||
@@ -481,10 +496,6 @@ func tomorrowTemplateModules(snapshot module.Snapshot, derived facts.DerivedFact
|
||||
|
||||
func dayStyleTemplateModuleSnapshot(snapshot module.Snapshot) (dayStyleTemplateModules, moduleSnapshotLookup, error) {
|
||||
lookup := newModuleSnapshotLookup(snapshot)
|
||||
metadata, err := lookup.metadata()
|
||||
if err != nil {
|
||||
return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err
|
||||
}
|
||||
current, err := lookup.currentConditions()
|
||||
if err != nil {
|
||||
return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err
|
||||
@@ -526,7 +537,6 @@ func dayStyleTemplateModuleSnapshot(snapshot module.Snapshot) (dayStyleTemplateM
|
||||
return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err
|
||||
}
|
||||
return dayStyleTemplateModules{
|
||||
Metadata: metadata,
|
||||
CurrentConditions: current,
|
||||
HourlyForecast: hourly,
|
||||
DerivedDailySummary: daily,
|
||||
@@ -556,10 +566,6 @@ func newModuleSnapshotLookup(snapshot module.Snapshot) moduleSnapshotLookup {
|
||||
}
|
||||
}
|
||||
|
||||
func (lookup moduleSnapshotLookup) metadata() (*briefing.MetadataModule, error) {
|
||||
return optionalStanza[briefing.MetadataModule](lookup, module.Metadata)
|
||||
}
|
||||
|
||||
func (lookup moduleSnapshotLookup) currentConditions() (*briefing.CurrentConditionsModule, error) {
|
||||
return optionalStanza[briefing.CurrentConditionsModule](lookup, module.CurrentConditions)
|
||||
}
|
||||
@@ -638,6 +644,15 @@ func orderedTodayDayparts(dayparts *map[string]briefing.DerivedDaypartSummaryMod
|
||||
return out
|
||||
}
|
||||
|
||||
func hasTodayDaypartDetails(dayparts []TodayDaypartContext) bool {
|
||||
for _, daypart := range dayparts {
|
||||
if daypart.Summary.DominantConditionDisplay != "" || daypart.Summary.DominantCondition != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func orderedDailyDayparts(dayparts *map[string]briefing.DerivedDaypartSummaryModule, ordered []forecast.DaypartSummary) []DailyDaypartContext {
|
||||
orderedRows := orderedDaypartRows(dayparts, ordered)
|
||||
out := make([]DailyDaypartContext, 0, len(orderedRows))
|
||||
|
||||
@@ -22,9 +22,7 @@ func TestBuildHourlyRenderContext(t *testing.T) {
|
||||
ForecastDiscussion: "A front will keep the region unsettled.",
|
||||
PrecipitationTiming: "A cold front is moving into the region.",
|
||||
}
|
||||
collected := testCollected()
|
||||
derived := testDerived()
|
||||
ctx, err := BuildHourlyRenderContext(metadata, snapshot, generated, collected, derived)
|
||||
ctx, err := BuildHourlyRenderContext(metadata, snapshot, generated)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildHourlyRenderContext() error = %v", err)
|
||||
}
|
||||
@@ -73,13 +71,6 @@ func TestBuildHourlyRenderContext(t *testing.T) {
|
||||
if ctx.Modules.WeatherStory == nil || ctx.Modules.WeatherStory.Title != "Morning storms" {
|
||||
t.Fatalf("Modules.WeatherStory = %#v, want story", ctx.Modules.WeatherStory)
|
||||
}
|
||||
if !ctx.Collected.FetchedAt.Equal(collected.FetchedAt) {
|
||||
t.Fatalf("Collected.FetchedAt = %s, want %s", ctx.Collected.FetchedAt, collected.FetchedAt)
|
||||
}
|
||||
if !ctx.Derived.PrecipTiming.ThunderMentioned {
|
||||
t.Fatalf("Derived.PrecipTiming.ThunderMentioned = false, want true")
|
||||
}
|
||||
|
||||
rendered, err := reporttemplate.Render("hourly", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Render() error = %v", err)
|
||||
@@ -113,7 +104,7 @@ func TestBuildHourlyRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
||||
ctx, err := BuildHourlyRenderContext(testMetadata(), snapshot, Hourly{
|
||||
Summary: "Storm chances increase.",
|
||||
ForecastDiscussion: "A front will keep the region unsettled.",
|
||||
}, testCollected(), facts.DerivedFacts{})
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildHourlyRenderContext() error = %v", err)
|
||||
}
|
||||
@@ -135,7 +126,7 @@ func TestBuildRenderContextReportsModuleExtractionError(t *testing.T) {
|
||||
_, err = BuildHourlyRenderContext(testMetadata(), snapshot, Hourly{
|
||||
Summary: "Storm chances increase.",
|
||||
ForecastDiscussion: "A front will keep the region unsettled.",
|
||||
}, testCollected(), facts.DerivedFacts{})
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("BuildHourlyRenderContext() error = nil, want extraction error")
|
||||
}
|
||||
@@ -144,6 +135,83 @@ func TestBuildRenderContextReportsModuleExtractionError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderContextsRejectConflictingIdentity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
build func() error
|
||||
}{
|
||||
{
|
||||
name: "hourly",
|
||||
build: func() error {
|
||||
identity := testMetadata()
|
||||
identity.ReportID = report.Daily
|
||||
_, err := BuildHourlyRenderContext(identity, testSnapshot(t), Hourly{})
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "daily",
|
||||
build: func() error {
|
||||
identity := testDailyMetadata()
|
||||
identity.ReportID = report.Today
|
||||
_, err := BuildDailyRenderContext(identity, testDailySnapshot(t), Daily{}, testDailyDerived())
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "today",
|
||||
build: func() error {
|
||||
identity := testTodayMetadata()
|
||||
identity.ReportID = report.Tomorrow
|
||||
_, err := BuildTodayRenderContext(identity, testTodaySnapshot(t), Today{}, testTodayDerived())
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tomorrow",
|
||||
build: func() error {
|
||||
identity := testTomorrowMetadata()
|
||||
identity.ReportID = report.Hourly
|
||||
_, err := BuildTomorrowRenderContext(identity, testTomorrowSnapshot(t), Tomorrow{}, testTomorrowDerived())
|
||||
return err
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.build()
|
||||
if err == nil {
|
||||
t.Fatal("render context error = nil, want conflicting identity rejection")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "render identity report") {
|
||||
t.Fatalf("render context error = %v, want identity mismatch", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderContextRejectsMetadataModuleMismatch(t *testing.T) {
|
||||
identity := testTodayMetadata()
|
||||
snapshot := testTodaySnapshot(t)
|
||||
for index := range snapshot.Outputs {
|
||||
if snapshot.Outputs[index].ID != module.Metadata {
|
||||
continue
|
||||
}
|
||||
metadata := snapshot.Outputs[index].Value.(briefing.MetadataModule)
|
||||
metadata.PromptID = "weather.daily_generated_text"
|
||||
snapshot.Outputs[index].Value = metadata
|
||||
}
|
||||
|
||||
_, err := BuildTodayRenderContext(identity, snapshot, Today{}, testTodayDerived())
|
||||
if err == nil {
|
||||
t.Fatal("BuildTodayRenderContext() error = nil, want metadata module mismatch")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "metadata module does not match render identity") {
|
||||
t.Fatalf("BuildTodayRenderContext() error = %v, want metadata mismatch", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTodayRenderContext(t *testing.T) {
|
||||
metadata := testTodayMetadata()
|
||||
snapshot := testTodaySnapshot(t)
|
||||
@@ -155,10 +223,9 @@ func TestBuildTodayRenderContext(t *testing.T) {
|
||||
},
|
||||
PrecipitationTiming: "The most likely rain window is from midafternoon into early evening.",
|
||||
}
|
||||
collected := testCollected()
|
||||
derived := testTodayDerived()
|
||||
|
||||
ctx, err := BuildTodayRenderContext(metadata, snapshot, generated, collected, derived)
|
||||
ctx, err := BuildTodayRenderContext(metadata, snapshot, generated, derived)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildTodayRenderContext() error = %v", err)
|
||||
}
|
||||
@@ -171,9 +238,6 @@ func TestBuildTodayRenderContext(t *testing.T) {
|
||||
if ctx.Report.GeneratedAtLabel != "Monday, June 15, 2026 at 7:14 AM" {
|
||||
t.Fatalf("Report.GeneratedAtLabel = %q, want friendly generated-at label", ctx.Report.GeneratedAtLabel)
|
||||
}
|
||||
if ctx.Modules.Metadata == nil || ctx.Modules.Metadata.ReportID != report.Today {
|
||||
t.Fatalf("Modules.Metadata = %#v, want today metadata", ctx.Modules.Metadata)
|
||||
}
|
||||
if ctx.Modules.CurrentConditions == nil || ctx.Modules.HourlyForecast == nil {
|
||||
t.Fatalf("current/hourly modules = %#v/%#v, want available in render context", ctx.Modules.CurrentConditions, ctx.Modules.HourlyForecast)
|
||||
}
|
||||
@@ -186,6 +250,9 @@ func TestBuildTodayRenderContext(t *testing.T) {
|
||||
if len(ctx.Modules.Dayparts) != 2 || ctx.Modules.Dayparts[0].Key != "morning" || ctx.Modules.Dayparts[1].Key != "afternoon" {
|
||||
t.Fatalf("Modules.Dayparts = %#v, want configured order", ctx.Modules.Dayparts)
|
||||
}
|
||||
if !ctx.Modules.HasDaypartDetails {
|
||||
t.Fatal("Modules.HasDaypartDetails = false, want displayable dayparts")
|
||||
}
|
||||
if morning := (*ctx.Modules.DerivedDaypartSummaries)["morning"]; morning.DominantConditionDisplay == "" || morning.DominantConditionLower == "" || morning.TemperatureSteadyPhraseF == "" {
|
||||
t.Fatalf("today rich morning daypart helpers = %#v, want render-context helper fields", morning)
|
||||
}
|
||||
@@ -201,13 +268,6 @@ func TestBuildTodayRenderContext(t *testing.T) {
|
||||
if ctx.Modules.TodayPlanning.MorningReadiness[0] != "Take sunglasses early." {
|
||||
t.Fatalf("Modules.TodayPlanning = %#v, want today planning facts", ctx.Modules.TodayPlanning)
|
||||
}
|
||||
if !ctx.Collected.FetchedAt.Equal(collected.FetchedAt) {
|
||||
t.Fatalf("Collected.FetchedAt = %s, want %s", ctx.Collected.FetchedAt, collected.FetchedAt)
|
||||
}
|
||||
if len(ctx.Derived.DaypartSummaries) != 2 {
|
||||
t.Fatalf("Derived.DaypartSummaries = %#v, want passthrough facts", ctx.Derived.DaypartSummaries)
|
||||
}
|
||||
|
||||
rendered, err := reporttemplate.Render("today", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Render() error = %v", err)
|
||||
@@ -260,7 +320,7 @@ func TestTodayRenderContextPreservesUnicodeDaypartDisplayNames(t *testing.T) {
|
||||
ctx, err := BuildTodayRenderContext(metadata, snapshot, Today{
|
||||
Summary: "Today summary.",
|
||||
ForecastDiscussion: []string{"Morning conditions stay quiet."},
|
||||
}, testCollected(), testTodayDerived())
|
||||
}, testTodayDerived())
|
||||
if err != nil {
|
||||
t.Fatalf("BuildTodayRenderContext() error = %v", err)
|
||||
}
|
||||
@@ -281,13 +341,16 @@ func TestBuildTodayRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
||||
ctx, err := BuildTodayRenderContext(testTodayMetadata(), snapshot, Today{
|
||||
Summary: "Dry weather is expected today.",
|
||||
ForecastDiscussion: []string{"High pressure keeps conditions quiet."},
|
||||
}, testCollected(), facts.DerivedFacts{})
|
||||
}, facts.DerivedFacts{})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildTodayRenderContext() error = %v", err)
|
||||
}
|
||||
if ctx.Modules.Metadata != nil || ctx.Modules.CurrentConditions != nil || ctx.Modules.PrecipTiming != nil || len(ctx.Modules.Dayparts) != 0 || ctx.Modules.TodayPlanning != nil {
|
||||
if ctx.Modules.CurrentConditions != nil || ctx.Modules.PrecipTiming != nil || len(ctx.Modules.Dayparts) != 0 || ctx.Modules.TodayPlanning != nil {
|
||||
t.Fatalf("Modules = %#v, want omitted optional modules", ctx.Modules)
|
||||
}
|
||||
if ctx.Modules.HasDaypartDetails {
|
||||
t.Fatal("Modules.HasDaypartDetails = true, want no displayable dayparts")
|
||||
}
|
||||
rendered, err := reporttemplate.Render("today", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Render() error = %v", err)
|
||||
@@ -310,7 +373,6 @@ func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *te
|
||||
generatedAtLabel string
|
||||
timezone string
|
||||
validPeriod timeutil.Period
|
||||
metadata bool
|
||||
current bool
|
||||
hourly bool
|
||||
dailySummary bool
|
||||
@@ -323,8 +385,6 @@ func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *te
|
||||
spcDiscussion bool
|
||||
story bool
|
||||
planning bool
|
||||
collected bool
|
||||
derived bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -340,12 +400,11 @@ func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *te
|
||||
wantDayparts: 3,
|
||||
build: func(t *testing.T) result {
|
||||
t.Helper()
|
||||
collected := testCollected()
|
||||
derived := testDailyDerived()
|
||||
ctx, err := BuildDailyRenderContext(testDailyMetadata(), testDailySnapshot(t), Daily{
|
||||
Summary: "Daily summary.",
|
||||
ForecastDiscussion: []string{"Daily discussion."},
|
||||
}, collected, derived)
|
||||
}, derived)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildDailyRenderContext() error = %v", err)
|
||||
}
|
||||
@@ -355,7 +414,6 @@ func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *te
|
||||
generatedAtLabel: ctx.Report.GeneratedAtLabel,
|
||||
timezone: ctx.Report.Timezone,
|
||||
validPeriod: ctx.Report.ValidPeriod,
|
||||
metadata: ctx.Modules.Metadata != nil && ctx.Modules.Metadata.ReportID == report.Daily,
|
||||
current: ctx.Modules.CurrentConditions != nil,
|
||||
hourly: ctx.Modules.HourlyForecast != nil,
|
||||
dailySummary: ctx.Modules.DerivedDailySummary != nil,
|
||||
@@ -368,8 +426,6 @@ func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *te
|
||||
spcDiscussion: ctx.Modules.SPCConvectiveDiscussion != nil,
|
||||
story: ctx.Modules.WeatherStory != nil,
|
||||
planning: ctx.Modules.DailyPlanning != nil,
|
||||
collected: ctx.Collected.FetchedAt.Equal(collected.FetchedAt),
|
||||
derived: len(ctx.Derived.DaypartSummaries) == len(derived.DaypartSummaries),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -380,12 +436,11 @@ func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *te
|
||||
wantDayparts: 2,
|
||||
build: func(t *testing.T) result {
|
||||
t.Helper()
|
||||
collected := testCollected()
|
||||
derived := testTodayDerived()
|
||||
ctx, err := BuildTodayRenderContext(testTodayMetadata(), testTodaySnapshot(t), Today{
|
||||
Summary: "Today summary.",
|
||||
ForecastDiscussion: []string{"Today discussion."},
|
||||
}, collected, derived)
|
||||
}, derived)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildTodayRenderContext() error = %v", err)
|
||||
}
|
||||
@@ -395,7 +450,6 @@ func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *te
|
||||
generatedAtLabel: ctx.Report.GeneratedAtLabel,
|
||||
timezone: ctx.Report.Timezone,
|
||||
validPeriod: ctx.Report.ValidPeriod,
|
||||
metadata: ctx.Modules.Metadata != nil && ctx.Modules.Metadata.ReportID == report.Today,
|
||||
current: ctx.Modules.CurrentConditions != nil,
|
||||
hourly: ctx.Modules.HourlyForecast != nil,
|
||||
dailySummary: ctx.Modules.DerivedDailySummary != nil,
|
||||
@@ -408,8 +462,6 @@ func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *te
|
||||
spcDiscussion: ctx.Modules.SPCConvectiveDiscussion != nil,
|
||||
story: ctx.Modules.WeatherStory != nil,
|
||||
planning: ctx.Modules.TodayPlanning != nil,
|
||||
collected: ctx.Collected.FetchedAt.Equal(collected.FetchedAt),
|
||||
derived: len(ctx.Derived.DaypartSummaries) == len(derived.DaypartSummaries),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -420,12 +472,11 @@ func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *te
|
||||
wantDayparts: 2,
|
||||
build: func(t *testing.T) result {
|
||||
t.Helper()
|
||||
collected := testCollected()
|
||||
derived := testTomorrowDerived()
|
||||
ctx, err := BuildTomorrowRenderContext(testTomorrowMetadata(), testTomorrowSnapshot(t), Tomorrow{
|
||||
Summary: "Tomorrow summary.",
|
||||
ForecastDiscussion: []string{"Tomorrow discussion."},
|
||||
}, collected, derived)
|
||||
}, derived)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildTomorrowRenderContext() error = %v", err)
|
||||
}
|
||||
@@ -435,7 +486,6 @@ func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *te
|
||||
generatedAtLabel: ctx.Report.GeneratedAtLabel,
|
||||
timezone: ctx.Report.Timezone,
|
||||
validPeriod: ctx.Report.ValidPeriod,
|
||||
metadata: ctx.Modules.Metadata != nil && ctx.Modules.Metadata.ReportID == report.Tomorrow,
|
||||
current: ctx.Modules.CurrentConditions != nil,
|
||||
hourly: ctx.Modules.HourlyForecast != nil,
|
||||
dailySummary: ctx.Modules.DerivedDailySummary != nil,
|
||||
@@ -448,8 +498,6 @@ func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *te
|
||||
spcDiscussion: ctx.Modules.SPCConvectiveDiscussion != nil,
|
||||
story: ctx.Modules.WeatherStory != nil,
|
||||
planning: ctx.Modules.TomorrowPlanning != nil,
|
||||
collected: ctx.Collected.FetchedAt.Equal(collected.FetchedAt),
|
||||
derived: len(ctx.Derived.DaypartSummaries) == len(derived.DaypartSummaries),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -473,7 +521,7 @@ func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *te
|
||||
if !result.validPeriod.IsValid() {
|
||||
t.Fatalf("validPeriod = %#v, want valid period", result.validPeriod)
|
||||
}
|
||||
if !result.metadata || !result.current || !result.hourly || !result.dailySummary || !result.daypartSummaries || !result.precip || !result.alerts || !result.outlooks || !result.discussion || !result.spcDiscussion || !result.story {
|
||||
if !result.current || !result.hourly || !result.dailySummary || !result.daypartSummaries || !result.precip || !result.alerts || !result.outlooks || !result.discussion || !result.spcDiscussion || !result.story {
|
||||
t.Fatalf("common modules = %#v, want all shared modules populated", result)
|
||||
}
|
||||
if result.dayparts != test.wantDayparts {
|
||||
@@ -482,9 +530,6 @@ func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *te
|
||||
if !result.planning {
|
||||
t.Fatalf("planning = false, want %s planning module populated", test.name)
|
||||
}
|
||||
if !result.collected || !result.derived {
|
||||
t.Fatalf("facts passthrough = collected:%t derived:%t, want both true", result.collected, result.derived)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -500,10 +545,9 @@ func TestBuildDailyRenderContext(t *testing.T) {
|
||||
},
|
||||
PrecipitationTiming: "The most likely rain window is from midafternoon into early evening.",
|
||||
}
|
||||
collected := testCollected()
|
||||
derived := testDailyDerived()
|
||||
|
||||
ctx, err := BuildDailyRenderContext(metadata, snapshot, generated, collected, derived)
|
||||
ctx, err := BuildDailyRenderContext(metadata, snapshot, generated, derived)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildDailyRenderContext() error = %v", err)
|
||||
}
|
||||
@@ -519,9 +563,6 @@ func TestBuildDailyRenderContext(t *testing.T) {
|
||||
if !ctx.Report.ValidPeriod.Start.Equal(metadata.ValidPeriod.Start) || !ctx.Report.ValidPeriod.End.Equal(metadata.ValidPeriod.End) || ctx.Report.Timezone != "America/Chicago" {
|
||||
t.Fatalf("period/timezone = %#v/%q, want metadata passthrough", ctx.Report.ValidPeriod, ctx.Report.Timezone)
|
||||
}
|
||||
if ctx.Modules.Metadata == nil || ctx.Modules.Metadata.ReportID != report.Daily {
|
||||
t.Fatalf("Modules.Metadata = %#v, want daily metadata", ctx.Modules.Metadata)
|
||||
}
|
||||
if ctx.Modules.CurrentConditions == nil || ctx.Modules.HourlyForecast == nil {
|
||||
t.Fatalf("current/hourly modules = %#v/%#v, want available in render context", ctx.Modules.CurrentConditions, ctx.Modules.HourlyForecast)
|
||||
}
|
||||
@@ -552,13 +593,6 @@ func TestBuildDailyRenderContext(t *testing.T) {
|
||||
if ctx.Modules.DailyPlanning.MorningReadiness[0] != "Take sunglasses early." {
|
||||
t.Fatalf("Modules.DailyPlanning = %#v, want daily planning facts", ctx.Modules.DailyPlanning)
|
||||
}
|
||||
if !ctx.Collected.FetchedAt.Equal(collected.FetchedAt) {
|
||||
t.Fatalf("Collected.FetchedAt = %s, want %s", ctx.Collected.FetchedAt, collected.FetchedAt)
|
||||
}
|
||||
if len(ctx.Derived.DaypartSummaries) != 2 {
|
||||
t.Fatalf("Derived.DaypartSummaries = %#v, want passthrough facts", ctx.Derived.DaypartSummaries)
|
||||
}
|
||||
|
||||
rendered, err := reporttemplate.Render("daily", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Render() error = %v", err)
|
||||
@@ -601,11 +635,11 @@ func TestBuildDailyRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
||||
ctx, err := BuildDailyRenderContext(testDailyMetadata(), snapshot, Daily{
|
||||
Summary: "Dry weather is expected for the selected day.",
|
||||
ForecastDiscussion: []string{"High pressure keeps conditions quiet."},
|
||||
}, testCollected(), facts.DerivedFacts{})
|
||||
}, facts.DerivedFacts{})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildDailyRenderContext() error = %v", err)
|
||||
}
|
||||
if ctx.Modules.Metadata != nil || ctx.Modules.CurrentConditions != nil || ctx.Modules.PrecipTiming != nil || ctx.Modules.OutdoorWindows != nil || ctx.Modules.DailyPlanning != nil || len(ctx.Modules.Dayparts) != 0 {
|
||||
if ctx.Modules.CurrentConditions != nil || ctx.Modules.PrecipTiming != nil || ctx.Modules.OutdoorWindows != nil || ctx.Modules.DailyPlanning != nil || len(ctx.Modules.Dayparts) != 0 {
|
||||
t.Fatalf("Modules = %#v, want omitted optional modules", ctx.Modules)
|
||||
}
|
||||
rendered, err := reporttemplate.Render("daily", ctx)
|
||||
@@ -632,10 +666,9 @@ func TestBuildTomorrowRenderContext(t *testing.T) {
|
||||
},
|
||||
PrecipitationTiming: "The most likely rain window is from midafternoon into early evening.",
|
||||
}
|
||||
collected := testCollected()
|
||||
derived := testTomorrowDerived()
|
||||
|
||||
ctx, err := BuildTomorrowRenderContext(metadata, snapshot, generated, collected, derived)
|
||||
ctx, err := BuildTomorrowRenderContext(metadata, snapshot, generated, derived)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildTomorrowRenderContext() error = %v", err)
|
||||
}
|
||||
@@ -648,9 +681,6 @@ func TestBuildTomorrowRenderContext(t *testing.T) {
|
||||
if ctx.Report.GeneratedAtLabel != "Sunday, June 14, 2026 at 9:14 AM" {
|
||||
t.Fatalf("Report.GeneratedAtLabel = %q, want friendly generated-at label", ctx.Report.GeneratedAtLabel)
|
||||
}
|
||||
if ctx.Modules.Metadata == nil || ctx.Modules.Metadata.ReportID != report.Tomorrow {
|
||||
t.Fatalf("Modules.Metadata = %#v, want tomorrow metadata", ctx.Modules.Metadata)
|
||||
}
|
||||
if ctx.Modules.CurrentConditions == nil || ctx.Modules.HourlyForecast == nil {
|
||||
t.Fatalf("current/hourly modules = %#v/%#v, want available in render context", ctx.Modules.CurrentConditions, ctx.Modules.HourlyForecast)
|
||||
}
|
||||
@@ -675,13 +705,6 @@ func TestBuildTomorrowRenderContext(t *testing.T) {
|
||||
if ctx.Modules.AlertDigest == nil || ctx.Modules.SPCConvectiveOutlooks == nil || ctx.Modules.AreaForecastDiscussion == nil || ctx.Modules.SPCConvectiveDiscussion == nil || ctx.Modules.WeatherStory == nil || ctx.Modules.TomorrowPlanning == nil {
|
||||
t.Fatalf("optional modules missing from render context: %#v", ctx.Modules)
|
||||
}
|
||||
if !ctx.Collected.FetchedAt.Equal(collected.FetchedAt) {
|
||||
t.Fatalf("Collected.FetchedAt = %s, want %s", ctx.Collected.FetchedAt, collected.FetchedAt)
|
||||
}
|
||||
if len(ctx.Derived.DaypartSummaries) != 2 {
|
||||
t.Fatalf("Derived.DaypartSummaries = %#v, want passthrough facts", ctx.Derived.DaypartSummaries)
|
||||
}
|
||||
|
||||
rendered, err := reporttemplate.Render("tomorrow", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Render() error = %v", err)
|
||||
@@ -721,11 +744,11 @@ func TestBuildTomorrowRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
||||
ctx, err := BuildTomorrowRenderContext(testTomorrowMetadata(), snapshot, Tomorrow{
|
||||
Summary: "Dry weather is expected tomorrow.",
|
||||
ForecastDiscussion: []string{"High pressure keeps conditions quiet."},
|
||||
}, testCollected(), facts.DerivedFacts{})
|
||||
}, facts.DerivedFacts{})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildTomorrowRenderContext() error = %v", err)
|
||||
}
|
||||
if ctx.Modules.Metadata != nil || ctx.Modules.CurrentConditions != nil || ctx.Modules.PrecipTiming != nil || len(ctx.Modules.Dayparts) != 0 {
|
||||
if ctx.Modules.CurrentConditions != nil || ctx.Modules.PrecipTiming != nil || len(ctx.Modules.Dayparts) != 0 {
|
||||
t.Fatalf("Modules = %#v, want omitted optional modules", ctx.Modules)
|
||||
}
|
||||
rendered, err := reporttemplate.Render("tomorrow", ctx)
|
||||
@@ -821,10 +844,6 @@ func testTodayMetadata() briefing.PreparedIdentity {
|
||||
}
|
||||
}
|
||||
|
||||
func testCollected() facts.CollectedFacts {
|
||||
return facts.CollectedFacts{FetchedAt: time.Date(2026, 5, 29, 13, 31, 0, 0, time.UTC)}
|
||||
}
|
||||
|
||||
func testDerived() facts.DerivedFacts {
|
||||
return facts.DerivedFacts{PrecipTiming: forecast.PrecipTiming{ThunderMentioned: true}}
|
||||
}
|
||||
@@ -878,6 +897,7 @@ func testTodaySnapshot(t *testing.T) module.Snapshot {
|
||||
Units: "imperial",
|
||||
Timezone: "America/Chicago",
|
||||
ValidPeriod: testTodayMetadata().ValidPeriod,
|
||||
Location: testTodayMetadata().Location,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1010,6 +1030,7 @@ func testDailySnapshot(t *testing.T) module.Snapshot {
|
||||
Units: "imperial",
|
||||
Timezone: "America/Chicago",
|
||||
ValidPeriod: testDailyMetadata().ValidPeriod,
|
||||
Location: testDailyMetadata().Location,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1289,6 +1310,7 @@ func testTomorrowSnapshot(t *testing.T) module.Snapshot {
|
||||
Units: "imperial",
|
||||
Timezone: "America/Chicago",
|
||||
ValidPeriod: testTomorrowMetadata().ValidPeriod,
|
||||
Location: testTomorrowMetadata().Location,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -704,6 +704,26 @@ func TestRenderDaypartTemplatesUseRichHelperFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderTodayUsesDaypartFallbackWhenRowsAreNotDisplayable(t *testing.T) {
|
||||
rendered, err := Render("today", testTodayRenderContext{
|
||||
Report: testTodayReportContext{Title: "Today", ForecastDateLabel: "Monday, June 15, 2026"},
|
||||
GeneratedText: testTomorrowGeneratedText{Summary: "Today summary."},
|
||||
Modules: testTodayModules{Dayparts: []testTomorrowDaypart{
|
||||
{Key: "morning", Summary: testDaypartSummary{DisplayName: "Morning"}},
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Render() error = %v", err)
|
||||
}
|
||||
text := string(rendered)
|
||||
if !strings.Contains(text, "## Daypart Forecast\n- No daypart forecast details are available.") {
|
||||
t.Fatalf("rendered template missing daypart fallback:\n%s", text)
|
||||
}
|
||||
if strings.Contains(text, "- **Morning:**") {
|
||||
t.Fatalf("rendered template included an unavailable daypart row:\n%s", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderTomorrowOmitsPrecipitationTimingWithoutWindows(t *testing.T) {
|
||||
rendered, err := Render("tomorrow", testTomorrowRenderContext{
|
||||
Report: testTomorrowReportContext{
|
||||
@@ -1013,6 +1033,15 @@ type testTodayModules struct {
|
||||
TodayPlanning *testTodayPlanning
|
||||
}
|
||||
|
||||
func (modules testTodayModules) HasDaypartDetails() bool {
|
||||
for _, daypart := range modules.Dayparts {
|
||||
if daypart.Summary.DominantConditionDisplay != "" || daypart.Summary.DominantCondition != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type testDailyModules struct {
|
||||
Dayparts []testDailyDaypart
|
||||
PrecipTiming *testPrecipTiming
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{ define "today_daypart_forecast" }}## Daypart Forecast
|
||||
{{ with .Modules.Dayparts }}{{ range . }}{{ $daypart := . }}{{ if or .Summary.DominantConditionDisplay .Summary.DominantCondition }}- **{{ if .Summary.DisplayName }}{{ .Summary.DisplayName }}{{ else }}{{ .Key }}{{ end }}:** {{ with .Summary.DominantConditionDisplay }}{{ . }}{{ else }}{{ with .Summary.DominantCondition }}{{ . }}{{ end }}{{ end }}{{ if eq .Summary.TemperatureTrend "rising" }}{{ with .Summary.TemperatureStartPhraseF }}, with temperatures rising from the {{ . }}{{ with $daypart.Summary.TemperatureEndPhraseF }} to the {{ . }}{{ end }}{{ end }}{{ else if eq .Summary.TemperatureTrend "falling" }}{{ with .Summary.TemperatureStartPhraseF }}, with temperatures falling from the {{ . }}{{ with $daypart.Summary.TemperatureEndPhraseF }} to the {{ . }}{{ end }}{{ end }}{{ else if eq .Summary.TemperatureTrend "peaking" }}{{ with .Summary.TemperaturePeakPhraseF }}, with temperatures peaking in the {{ . }}{{ end }}{{ else }}{{ with .Summary.TemperatureSteadyPhraseF }}, with temperatures in the {{ . }}{{ else }}{{ with .Summary.TemperaturePhraseF }}, with temperatures in the {{ . }}{{ end }}{{ end }}{{ end }}.{{ if .Summary.MentionPrecipitation }}{{ with .Summary.MaxPopPercent }} Chance of precipitation is {{ . }}%.{{ end }}{{ end }}
|
||||
{{ if .Modules.HasDaypartDetails }}{{ range .Modules.Dayparts }}{{ $daypart := . }}{{ if or .Summary.DominantConditionDisplay .Summary.DominantCondition }}- **{{ if .Summary.DisplayName }}{{ .Summary.DisplayName }}{{ else }}{{ .Key }}{{ end }}:** {{ with .Summary.DominantConditionDisplay }}{{ . }}{{ else }}{{ with .Summary.DominantCondition }}{{ . }}{{ end }}{{ end }}{{ if eq .Summary.TemperatureTrend "rising" }}{{ with .Summary.TemperatureStartPhraseF }}, with temperatures rising from the {{ . }}{{ with $daypart.Summary.TemperatureEndPhraseF }} to the {{ . }}{{ end }}{{ end }}{{ else if eq .Summary.TemperatureTrend "falling" }}{{ with .Summary.TemperatureStartPhraseF }}, with temperatures falling from the {{ . }}{{ with $daypart.Summary.TemperatureEndPhraseF }} to the {{ . }}{{ end }}{{ end }}{{ else if eq .Summary.TemperatureTrend "peaking" }}{{ with .Summary.TemperaturePeakPhraseF }}, with temperatures peaking in the {{ . }}{{ end }}{{ else }}{{ with .Summary.TemperatureSteadyPhraseF }}, with temperatures in the {{ . }}{{ else }}{{ with .Summary.TemperaturePhraseF }}, with temperatures in the {{ . }}{{ end }}{{ end }}{{ end }}.{{ if .Summary.MentionPrecipitation }}{{ with .Summary.MaxPopPercent }} Chance of precipitation is {{ . }}%.{{ end }}{{ end }}
|
||||
{{ end }}{{ end }}{{ else }}- No daypart forecast details are available.
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
Reference in New Issue
Block a user