Cleanup after implementation of the module architecture and remove completed roadmap files

This commit is contained in:
2026-06-10 07:49:29 -05:00
parent 1ddd88231a
commit 1e9c29aa55
14 changed files with 222 additions and 1607 deletions

View File

@@ -3,6 +3,7 @@ package briefing
import (
"fmt"
"reflect"
"strings"
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
@@ -61,6 +62,12 @@ func NewModuleRegistry(definitions []ModuleDefinition) (ModuleRegistry, error) {
if _, ok := registry.definitions[definition.ID]; ok {
return ModuleRegistry{}, fmt.Errorf("duplicate module definition %q", definition.ID)
}
if definition.Builder == nil {
return ModuleRegistry{}, fmt.Errorf("module %q has no builder", definition.ID)
}
if definition.MissingData == module.MissingDataWarn {
return ModuleRegistry{}, fmt.Errorf("module %q uses unsupported missing data behavior %q", definition.ID, definition.MissingData)
}
if existingID, ok := seenStanzas[definition.StanzaName]; ok {
return ModuleRegistry{}, fmt.Errorf("duplicate stanza name %q for modules %q and %q", definition.StanzaName, existingID, definition.ID)
}
@@ -92,6 +99,20 @@ func (r ModuleRegistry) BuildModule(ctx ModuleContext, item module.ConfigItem) (
if definition.Builder == nil {
return nil, fmt.Errorf("module %q has no builder", item.ID)
}
missing := missingRequirements(definition, ctx)
if len(missing) > 0 {
switch definition.MissingData {
case module.MissingDataOmit:
return nil, nil
case module.MissingDataError:
return nil, fmt.Errorf("module %q missing required facts: %s", item.ID, strings.Join(missing, ", "))
case module.MissingDataEmpty:
case module.MissingDataWarn:
return nil, fmt.Errorf("module %q uses unsupported missing data behavior %q", item.ID, definition.MissingData)
default:
return nil, fmt.Errorf("module %q has unknown missing data behavior %q", item.ID, definition.MissingData)
}
}
options := item.Options
if options == nil {
options = definition.DefaultOptions
@@ -112,6 +133,57 @@ func (r ModuleRegistry) BuildModule(ctx ModuleContext, item module.ConfigItem) (
return output, nil
}
func missingRequirements(definition ModuleDefinition, ctx ModuleContext) []string {
var missing []string
for _, requirement := range definition.RequiredCollected {
if !collectedFactAvailable(requirement, ctx) {
missing = append(missing, string(requirement))
}
}
for _, requirement := range definition.RequiredDerived {
if !derivedFactAvailable(requirement, ctx) {
missing = append(missing, string(requirement))
}
}
return missing
}
func collectedFactAvailable(requirement module.FactRequirement, ctx ModuleContext) bool {
switch requirement {
case module.CollectedCurrentConditions:
return ctx.Collected.Current != nil
case module.CollectedAlerts:
return ctx.Collected.Alerts != nil
case module.CollectedDiscussion:
return ctx.Collected.Discussion != nil
case module.CollectedWeatherStory:
return ctx.Collected.WeatherStory != nil
case module.CollectedSourceMetadata:
return len(ctx.Collected.SourceProvenance) > 0 || len(ctx.Collected.SourceWarnings) > 0
default:
return false
}
}
func derivedFactAvailable(requirement module.FactRequirement, ctx ModuleContext) bool {
switch requirement {
case module.RequiresDerivedHourlyPeriods:
return len(ctx.Derived.ValidPeriodHourlyPeriods) > 0
case module.RequiresDerivedNarrativePeriods:
return len(ctx.Derived.ValidPeriodNarrativePeriods) > 0
case module.RequiresDerivedAlertOverlaps:
return true
case module.RequiresDerivedDailySummaries:
return len(ctx.Derived.DailySummaries) > 0
case module.RequiresDerivedDaypartSummaries:
return len(ctx.Derived.DaypartSummaries) > 0
case module.RequiresDerivedPrecipTiming:
return true
default:
return false
}
}
func (r ModuleRegistry) ValidateComposition(reportID report.ID, items []module.ConfigItem) error {
seenModules := map[module.ID]struct{}{}
seenStanzas := map[string]module.ID{}
@@ -208,14 +280,6 @@ func defaultModuleDefinitions() []ModuleDefinition {
MissingData: module.MissingDataError,
Builder: buildDerivedDaypartSummariesModule,
},
{
ID: module.HourlyTable,
StanzaName: "hourly_table",
DefaultOptions: module.HourlyTableOptions{},
RequiredDerived: []module.FactRequirement{module.RequiresDerivedHourlyPeriods},
SupportedReports: []report.ID{report.Storm},
MissingData: module.MissingDataError,
},
{
ID: module.PrecipTiming,
StanzaName: "precip_timing",
@@ -253,13 +317,6 @@ func defaultModuleDefinitions() []ModuleDefinition {
MissingData: module.MissingDataOmit,
Builder: buildWeatherStoryModule,
},
{
ID: module.ForecastDelta,
StanzaName: "forecast_delta",
DefaultOptions: module.ForecastDeltaOptions{},
SupportedReports: []report.ID{report.DailyToday, report.DailyTomorrow, report.ThreeDay},
MissingData: module.MissingDataEmpty,
},
{
ID: module.OutdoorWindows,
StanzaName: "outdoor_windows",
@@ -278,21 +335,5 @@ func defaultModuleDefinitions() []ModuleDefinition {
MissingData: module.MissingDataEmpty,
Builder: buildTomorrowPlanningModule,
},
{
ID: module.WeekendPlanning,
StanzaName: "weekend_planning",
DefaultOptions: module.WeekendPlanningOptions{},
RequiredDerived: []module.FactRequirement{module.RequiresDerivedDailySummaries},
SupportedReports: []report.ID{report.Weekend},
MissingData: module.MissingDataEmpty,
},
{
ID: module.StormWindowSummary,
StanzaName: "storm_window_summary",
DefaultOptions: module.StormWindowSummaryOptions{},
RequiredDerived: []module.FactRequirement{module.RequiresDerivedStormWindowSummary},
SupportedReports: []report.ID{report.Storm},
MissingData: module.MissingDataError,
},
}
}

View File

@@ -14,6 +14,41 @@ func TestDefaultModuleRegistryValidatesReportDefaults(t *testing.T) {
if err := registry.ValidateComposition(definition.ID, definition.Modules); err != nil {
t.Fatalf("ValidateComposition(%s) error = %v", definition.ID, err)
}
for _, item := range definition.Modules {
moduleDefinition, err := registry.Lookup(item.ID)
if err != nil {
t.Fatalf("Lookup(%s) error = %v", item.ID, err)
}
if moduleDefinition.Builder == nil {
t.Fatalf("report %s module %s has no builder", definition.ID, item.ID)
}
}
}
}
func TestDefaultReportModulesBuildSnapshots(t *testing.T) {
registry := MustDefaultModuleRegistry()
for _, definition := range report.DefaultRegistry().All() {
t.Run(string(definition.ID), func(t *testing.T) {
ctx := derivedModuleContext(definition.ID)
var outputs []module.Output
for _, item := range definition.Modules {
output, err := registry.BuildModule(ctx, item)
if err != nil {
t.Fatalf("BuildModule(%s) error = %v", item.ID, err)
}
if output != nil {
outputs = append(outputs, *output)
}
}
snapshot, err := module.NewSnapshot(outputs)
if err != nil {
t.Fatalf("NewSnapshot() error = %v", err)
}
if len(snapshot.Outputs) == 0 {
t.Fatal("snapshot outputs = 0, want default report modules")
}
})
}
}
@@ -38,8 +73,8 @@ func TestModuleRegistryRejectsDuplicateModuleIDs(t *testing.T) {
func TestModuleRegistryRejectsDuplicateStanzaNames(t *testing.T) {
_, err := NewModuleRegistry([]ModuleDefinition{
{ID: module.Metadata, StanzaName: "metadata", DefaultOptions: module.MetadataOptions{}},
{ID: module.CurrentConditions, StanzaName: "metadata", DefaultOptions: module.CurrentConditionsOptions{}},
{ID: module.Metadata, StanzaName: "metadata", DefaultOptions: module.MetadataOptions{}, Builder: noopModuleBuilder},
{ID: module.CurrentConditions, StanzaName: "metadata", DefaultOptions: module.CurrentConditionsOptions{}, Builder: noopModuleBuilder},
})
if err == nil || !strings.Contains(err.Error(), `duplicate stanza name "metadata"`) {
t.Fatalf("error = %v, want duplicate stanza name", err)
@@ -48,12 +83,30 @@ func TestModuleRegistryRejectsDuplicateStanzaNames(t *testing.T) {
func TestModuleRegistryRejectsIncompatibleReports(t *testing.T) {
registry := MustDefaultModuleRegistry()
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{{ID: module.StormWindowSummary}})
if err == nil || !strings.Contains(err.Error(), `module "storm_window_summary" is not compatible with report "daily_today"`) {
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{{ID: module.TomorrowPlanning}})
if err == nil || !strings.Contains(err.Error(), `module "tomorrow_planning" is not compatible with report "daily_today"`) {
t.Fatalf("error = %v, want incompatible report", err)
}
}
func TestModuleRegistryRejectsDefinitionsWithoutBuilders(t *testing.T) {
_, err := NewModuleRegistry([]ModuleDefinition{
{ID: module.Metadata, StanzaName: "metadata", DefaultOptions: module.MetadataOptions{}},
})
if err == nil || !strings.Contains(err.Error(), `module "metadata" has no builder`) {
t.Fatalf("error = %v, want missing builder", err)
}
}
func TestModuleRegistryRejectsUnsupportedMissingDataWarn(t *testing.T) {
_, err := NewModuleRegistry([]ModuleDefinition{
{ID: module.Metadata, StanzaName: "metadata", DefaultOptions: module.MetadataOptions{}, MissingData: module.MissingDataWarn, Builder: noopModuleBuilder},
})
if err == nil || !strings.Contains(err.Error(), `unsupported missing data behavior`) {
t.Fatalf("error = %v, want unsupported missing-data behavior", err)
}
}
func TestModuleRegistryRejectsInvalidOptionShapes(t *testing.T) {
registry := MustDefaultModuleRegistry()
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{
@@ -74,3 +127,7 @@ func TestModuleRegistryAcceptsTypedOptions(t *testing.T) {
t.Fatalf("ValidateComposition() error = %v", err)
}
}
func noopModuleBuilder(ModuleContext, any) (*module.Output, error) {
return &module.Output{ID: module.Metadata, StanzaName: "metadata", Value: struct{}{}}, nil
}