Validate fact requirements through briefing vocabulary
This commit is contained in:
@@ -10,7 +10,10 @@ snapshot, construct YAML, invoke Promptkit, or render a report.
|
|||||||
|
|
||||||
Every `ModuleDefinition` declares an ID, stanza name, default option value,
|
Every `ModuleDefinition` declares an ID, stanza name, default option value,
|
||||||
required collected and derived facts, supported report IDs, missing-data
|
required collected and derived facts, supported report IDs, missing-data
|
||||||
behavior, duplicate policy, builder, and optional prompt exporter.
|
behavior, duplicate policy, builder, and optional prompt exporter. The
|
||||||
|
briefing-owned fact-requirement vocabulary supplies each prerequisite's stable
|
||||||
|
identity, category, and availability predicate; registry construction rejects
|
||||||
|
unknown requirements and requirements listed under the wrong category.
|
||||||
|
|
||||||
`BuildModule` first verifies the requested module, report compatibility, and
|
`BuildModule` first verifies the requested module, report compatibility, and
|
||||||
option shape. It then applies the declared missing-data behavior:
|
option shape. It then applies the declared missing-data behavior:
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type factRequirement struct {
|
type factRequirement struct {
|
||||||
identity string
|
identity string
|
||||||
category factRequirementCategory
|
category factRequirementCategory
|
||||||
|
available func(ModuleContext) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r factRequirement) String() string {
|
func (r factRequirement) String() string {
|
||||||
@@ -17,25 +18,55 @@ func (r factRequirement) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
currentConditionsRequirement = factRequirement{identity: "collected.current_conditions", category: collectedFactRequirement}
|
currentConditionsRequirement = &factRequirement{identity: "collected.current_conditions", category: collectedFactRequirement, available: func(ctx ModuleContext) bool {
|
||||||
narrativeForecastRequirement = factRequirement{identity: "collected.narrative_forecast", category: collectedFactRequirement}
|
return ctx.Collected.Current != nil
|
||||||
hourlyForecastRequirement = factRequirement{identity: "collected.hourly_forecast", category: collectedFactRequirement}
|
}}
|
||||||
alertsRequirement = factRequirement{identity: "collected.alerts", category: collectedFactRequirement}
|
narrativeForecastRequirement = &factRequirement{identity: "collected.narrative_forecast", category: collectedFactRequirement, available: func(ctx ModuleContext) bool {
|
||||||
discussionRequirement = factRequirement{identity: "collected.discussion", category: collectedFactRequirement}
|
return ctx.Collected.Narrative != nil
|
||||||
weatherStoryRequirement = factRequirement{identity: "collected.weather_story", category: collectedFactRequirement}
|
}}
|
||||||
spcOutlooksRequirement = factRequirement{identity: "collected.spc_convective_outlooks", category: collectedFactRequirement}
|
hourlyForecastRequirement = &factRequirement{identity: "collected.hourly_forecast", category: collectedFactRequirement, available: func(ctx ModuleContext) bool {
|
||||||
sourceMetadataRequirement = factRequirement{identity: "collected.source_metadata", category: collectedFactRequirement}
|
return ctx.Collected.Hourly != nil
|
||||||
|
}}
|
||||||
|
alertsRequirement = &factRequirement{identity: "collected.alerts", category: collectedFactRequirement, available: func(ctx ModuleContext) bool {
|
||||||
|
return ctx.Collected.Alerts != nil
|
||||||
|
}}
|
||||||
|
discussionRequirement = &factRequirement{identity: "collected.discussion", category: collectedFactRequirement, available: func(ctx ModuleContext) bool {
|
||||||
|
return ctx.Collected.Discussion != nil
|
||||||
|
}}
|
||||||
|
weatherStoryRequirement = &factRequirement{identity: "collected.weather_story", category: collectedFactRequirement, available: func(ctx ModuleContext) bool {
|
||||||
|
return ctx.Collected.WeatherStory != nil
|
||||||
|
}}
|
||||||
|
spcOutlooksRequirement = &factRequirement{identity: "collected.spc_convective_outlooks", category: collectedFactRequirement, available: func(ctx ModuleContext) bool {
|
||||||
|
return ctx.Collected.SPCConvectiveOutlooks != nil
|
||||||
|
}}
|
||||||
|
sourceMetadataRequirement = &factRequirement{identity: "collected.source_metadata", category: collectedFactRequirement, available: func(ctx ModuleContext) bool {
|
||||||
|
return len(ctx.Collected.SourceProvenance) > 0 || len(ctx.Collected.SourceWarnings) > 0
|
||||||
|
}}
|
||||||
|
|
||||||
hourlyPeriodsRequirement = factRequirement{identity: "derived.hourly_periods", category: derivedFactRequirement}
|
hourlyPeriodsRequirement = &factRequirement{identity: "derived.hourly_periods", category: derivedFactRequirement, available: func(ctx ModuleContext) bool {
|
||||||
narrativePeriodsRequirement = factRequirement{identity: "derived.narrative_periods", category: derivedFactRequirement}
|
return len(ctx.Derived.ValidPeriodHourlyPeriods) > 0
|
||||||
alertOverlapsRequirement = factRequirement{identity: "derived.alert_overlaps", category: derivedFactRequirement}
|
}}
|
||||||
dailySummariesRequirement = factRequirement{identity: "derived.daily_summaries", category: derivedFactRequirement}
|
narrativePeriodsRequirement = &factRequirement{identity: "derived.narrative_periods", category: derivedFactRequirement, available: func(ctx ModuleContext) bool {
|
||||||
daypartSummariesRequirement = factRequirement{identity: "derived.daypart_summaries", category: derivedFactRequirement}
|
return len(ctx.Derived.ValidPeriodNarrativePeriods) > 0
|
||||||
precipTimingRequirement = factRequirement{identity: "derived.precip_timing", category: derivedFactRequirement}
|
}}
|
||||||
spcDerivedOutlooksRequirement = factRequirement{identity: "derived.spc_convective_outlooks", category: derivedFactRequirement}
|
alertOverlapsRequirement = &factRequirement{identity: "derived.alert_overlaps", category: derivedFactRequirement, available: func(ModuleContext) bool {
|
||||||
|
return true
|
||||||
|
}}
|
||||||
|
dailySummariesRequirement = &factRequirement{identity: "derived.daily_summaries", category: derivedFactRequirement, available: func(ctx ModuleContext) bool {
|
||||||
|
return len(ctx.Derived.DailySummaries) > 0
|
||||||
|
}}
|
||||||
|
daypartSummariesRequirement = &factRequirement{identity: "derived.daypart_summaries", category: derivedFactRequirement, available: func(ctx ModuleContext) bool {
|
||||||
|
return len(ctx.Derived.DaypartSummaries) > 0
|
||||||
|
}}
|
||||||
|
precipTimingRequirement = &factRequirement{identity: "derived.precip_timing", category: derivedFactRequirement, available: func(ModuleContext) bool {
|
||||||
|
return true
|
||||||
|
}}
|
||||||
|
spcDerivedOutlooksRequirement = &factRequirement{identity: "derived.spc_convective_outlooks", category: derivedFactRequirement, available: func(ctx ModuleContext) bool {
|
||||||
|
return ctx.Derived.SPCConvectiveOutlooks != nil
|
||||||
|
}}
|
||||||
)
|
)
|
||||||
|
|
||||||
var factRequirementVocabulary = []factRequirement{
|
var factRequirementVocabulary = []*factRequirement{
|
||||||
currentConditionsRequirement,
|
currentConditionsRequirement,
|
||||||
narrativeForecastRequirement,
|
narrativeForecastRequirement,
|
||||||
hourlyForecastRequirement,
|
hourlyForecastRequirement,
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ type ModuleDefinition struct {
|
|||||||
ID module.ID
|
ID module.ID
|
||||||
StanzaName string
|
StanzaName string
|
||||||
DefaultOptions any
|
DefaultOptions any
|
||||||
RequiredCollected []factRequirement
|
RequiredCollected []*factRequirement
|
||||||
RequiredDerived []factRequirement
|
RequiredDerived []*factRequirement
|
||||||
SupportedReports []report.ID
|
SupportedReports []report.ID
|
||||||
MissingData module.MissingDataBehavior
|
MissingData module.MissingDataBehavior
|
||||||
AllowDuplicate bool
|
AllowDuplicate bool
|
||||||
@@ -76,6 +76,9 @@ func NewModuleRegistry(definitions []ModuleDefinition) (ModuleRegistry, error) {
|
|||||||
if definition.MissingData == module.MissingDataWarn {
|
if definition.MissingData == module.MissingDataWarn {
|
||||||
return ModuleRegistry{}, fmt.Errorf("module %q uses unsupported missing data behavior %q", definition.ID, definition.MissingData)
|
return ModuleRegistry{}, fmt.Errorf("module %q uses unsupported missing data behavior %q", definition.ID, definition.MissingData)
|
||||||
}
|
}
|
||||||
|
if err := validateFactRequirements(definition); err != nil {
|
||||||
|
return ModuleRegistry{}, err
|
||||||
|
}
|
||||||
if existingID, ok := seenStanzas[definition.StanzaName]; ok {
|
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)
|
return ModuleRegistry{}, fmt.Errorf("duplicate stanza name %q for modules %q and %q", definition.StanzaName, existingID, definition.ID)
|
||||||
}
|
}
|
||||||
@@ -153,60 +156,48 @@ func (r ModuleRegistry) BuildModule(ctx ModuleContext, item module.ConfigItem) (
|
|||||||
func missingRequirements(definition ModuleDefinition, ctx ModuleContext) []string {
|
func missingRequirements(definition ModuleDefinition, ctx ModuleContext) []string {
|
||||||
var missing []string
|
var missing []string
|
||||||
for _, requirement := range definition.RequiredCollected {
|
for _, requirement := range definition.RequiredCollected {
|
||||||
if !collectedFactAvailable(requirement, ctx) {
|
if !requirement.available(ctx) {
|
||||||
missing = append(missing, requirement.String())
|
missing = append(missing, requirement.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, requirement := range definition.RequiredDerived {
|
for _, requirement := range definition.RequiredDerived {
|
||||||
if !derivedFactAvailable(requirement, ctx) {
|
if !requirement.available(ctx) {
|
||||||
missing = append(missing, requirement.String())
|
missing = append(missing, requirement.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return missing
|
return missing
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectedFactAvailable(requirement factRequirement, ctx ModuleContext) bool {
|
func validateFactRequirements(definition ModuleDefinition) error {
|
||||||
switch requirement {
|
if err := validateFactRequirementCategory(definition.ID, definition.RequiredCollected, collectedFactRequirement); err != nil {
|
||||||
case currentConditionsRequirement:
|
return err
|
||||||
return ctx.Collected.Current != nil
|
|
||||||
case narrativeForecastRequirement:
|
|
||||||
return ctx.Collected.Narrative != nil
|
|
||||||
case hourlyForecastRequirement:
|
|
||||||
return ctx.Collected.Hourly != nil
|
|
||||||
case alertsRequirement:
|
|
||||||
return ctx.Collected.Alerts != nil
|
|
||||||
case discussionRequirement:
|
|
||||||
return ctx.Collected.Discussion != nil
|
|
||||||
case weatherStoryRequirement:
|
|
||||||
return ctx.Collected.WeatherStory != nil
|
|
||||||
case spcOutlooksRequirement:
|
|
||||||
return ctx.Collected.SPCConvectiveOutlooks != nil
|
|
||||||
case sourceMetadataRequirement:
|
|
||||||
return len(ctx.Collected.SourceProvenance) > 0 || len(ctx.Collected.SourceWarnings) > 0
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return validateFactRequirementCategory(definition.ID, definition.RequiredDerived, derivedFactRequirement)
|
||||||
}
|
}
|
||||||
|
|
||||||
func derivedFactAvailable(requirement factRequirement, ctx ModuleContext) bool {
|
func validateFactRequirementCategory(moduleID module.ID, requirements []*factRequirement, want factRequirementCategory) error {
|
||||||
switch requirement {
|
for _, requirement := range requirements {
|
||||||
case hourlyPeriodsRequirement:
|
if requirement == nil {
|
||||||
return len(ctx.Derived.ValidPeriodHourlyPeriods) > 0
|
return fmt.Errorf("module %q uses unknown %s fact requirement %q", moduleID, want, "")
|
||||||
case narrativePeriodsRequirement:
|
}
|
||||||
return len(ctx.Derived.ValidPeriodNarrativePeriods) > 0
|
descriptor, ok := lookupFactRequirement(requirement.identity)
|
||||||
case alertOverlapsRequirement:
|
if !ok || descriptor != requirement {
|
||||||
return true
|
return fmt.Errorf("module %q uses unknown %s fact requirement %q", moduleID, want, requirement.identity)
|
||||||
case dailySummariesRequirement:
|
}
|
||||||
return len(ctx.Derived.DailySummaries) > 0
|
if descriptor.category != want {
|
||||||
case daypartSummariesRequirement:
|
return fmt.Errorf("module %q lists %s fact requirement %q as %s", moduleID, descriptor.category, descriptor.identity, want)
|
||||||
return len(ctx.Derived.DaypartSummaries) > 0
|
}
|
||||||
case precipTimingRequirement:
|
|
||||||
return true
|
|
||||||
case spcDerivedOutlooksRequirement:
|
|
||||||
return ctx.Derived.SPCConvectiveOutlooks != nil
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupFactRequirement(identity string) (*factRequirement, bool) {
|
||||||
|
for _, requirement := range factRequirementVocabulary {
|
||||||
|
if requirement.identity == identity {
|
||||||
|
return requirement, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r ModuleRegistry) ValidateComposition(reportID report.ID, items []module.ConfigItem) error {
|
func (r ModuleRegistry) ValidateComposition(reportID report.ID, items []module.ConfigItem) error {
|
||||||
@@ -291,7 +282,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.Metadata,
|
ID: module.Metadata,
|
||||||
StanzaName: "metadata",
|
StanzaName: "metadata",
|
||||||
DefaultOptions: module.MetadataOptions{},
|
DefaultOptions: module.MetadataOptions{},
|
||||||
RequiredCollected: []factRequirement{sourceMetadataRequirement},
|
RequiredCollected: []*factRequirement{sourceMetadataRequirement},
|
||||||
SupportedReports: allReports,
|
SupportedReports: allReports,
|
||||||
MissingData: module.MissingDataEmpty,
|
MissingData: module.MissingDataEmpty,
|
||||||
Builder: buildMetadataModule,
|
Builder: buildMetadataModule,
|
||||||
@@ -300,7 +291,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.CurrentConditions,
|
ID: module.CurrentConditions,
|
||||||
StanzaName: "current_conditions",
|
StanzaName: "current_conditions",
|
||||||
DefaultOptions: module.CurrentConditionsOptions{},
|
DefaultOptions: module.CurrentConditionsOptions{},
|
||||||
RequiredCollected: []factRequirement{currentConditionsRequirement},
|
RequiredCollected: []*factRequirement{currentConditionsRequirement},
|
||||||
SupportedReports: allReports,
|
SupportedReports: allReports,
|
||||||
MissingData: module.MissingDataOmit,
|
MissingData: module.MissingDataOmit,
|
||||||
Builder: buildCurrentConditionsModule,
|
Builder: buildCurrentConditionsModule,
|
||||||
@@ -310,8 +301,8 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.NarrativeForecast,
|
ID: module.NarrativeForecast,
|
||||||
StanzaName: "narrative_forecast",
|
StanzaName: "narrative_forecast",
|
||||||
DefaultOptions: module.NarrativeForecastOptions{},
|
DefaultOptions: module.NarrativeForecastOptions{},
|
||||||
RequiredCollected: []factRequirement{narrativeForecastRequirement},
|
RequiredCollected: []*factRequirement{narrativeForecastRequirement},
|
||||||
RequiredDerived: []factRequirement{narrativePeriodsRequirement},
|
RequiredDerived: []*factRequirement{narrativePeriodsRequirement},
|
||||||
SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow},
|
SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow},
|
||||||
MissingData: module.MissingDataOmit,
|
MissingData: module.MissingDataOmit,
|
||||||
Builder: buildNarrativeForecastModule,
|
Builder: buildNarrativeForecastModule,
|
||||||
@@ -320,8 +311,8 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.HourlyForecast,
|
ID: module.HourlyForecast,
|
||||||
StanzaName: "hourly_forecast",
|
StanzaName: "hourly_forecast",
|
||||||
DefaultOptions: module.HourlyForecastOptions{},
|
DefaultOptions: module.HourlyForecastOptions{},
|
||||||
RequiredCollected: []factRequirement{hourlyForecastRequirement},
|
RequiredCollected: []*factRequirement{hourlyForecastRequirement},
|
||||||
RequiredDerived: []factRequirement{hourlyPeriodsRequirement},
|
RequiredDerived: []*factRequirement{hourlyPeriodsRequirement},
|
||||||
SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow, report.Hourly},
|
SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow, report.Hourly},
|
||||||
MissingData: module.MissingDataOmit,
|
MissingData: module.MissingDataOmit,
|
||||||
Builder: buildHourlyForecastModule,
|
Builder: buildHourlyForecastModule,
|
||||||
@@ -331,7 +322,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.DerivedDailySummary,
|
ID: module.DerivedDailySummary,
|
||||||
StanzaName: "derived_daily_summary",
|
StanzaName: "derived_daily_summary",
|
||||||
DefaultOptions: module.DerivedDailySummaryOptions{},
|
DefaultOptions: module.DerivedDailySummaryOptions{},
|
||||||
RequiredDerived: []factRequirement{dailySummariesRequirement, precipTimingRequirement},
|
RequiredDerived: []*factRequirement{dailySummariesRequirement, precipTimingRequirement},
|
||||||
SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow},
|
SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow},
|
||||||
MissingData: module.MissingDataError,
|
MissingData: module.MissingDataError,
|
||||||
Builder: buildDerivedDailySummaryModule,
|
Builder: buildDerivedDailySummaryModule,
|
||||||
@@ -340,7 +331,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.DerivedDaypartSummaries,
|
ID: module.DerivedDaypartSummaries,
|
||||||
StanzaName: "derived_daypart_summaries",
|
StanzaName: "derived_daypart_summaries",
|
||||||
DefaultOptions: module.DerivedDaypartSummariesOptions{},
|
DefaultOptions: module.DerivedDaypartSummariesOptions{},
|
||||||
RequiredDerived: []factRequirement{daypartSummariesRequirement},
|
RequiredDerived: []*factRequirement{daypartSummariesRequirement},
|
||||||
SupportedReports: daypartReports,
|
SupportedReports: daypartReports,
|
||||||
MissingData: module.MissingDataError,
|
MissingData: module.MissingDataError,
|
||||||
Builder: buildDerivedDaypartSummariesModule,
|
Builder: buildDerivedDaypartSummariesModule,
|
||||||
@@ -350,7 +341,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.PrecipTiming,
|
ID: module.PrecipTiming,
|
||||||
StanzaName: "precip_timing",
|
StanzaName: "precip_timing",
|
||||||
DefaultOptions: module.PrecipTimingOptions{},
|
DefaultOptions: module.PrecipTimingOptions{},
|
||||||
RequiredDerived: []factRequirement{precipTimingRequirement},
|
RequiredDerived: []*factRequirement{precipTimingRequirement},
|
||||||
SupportedReports: allReports,
|
SupportedReports: allReports,
|
||||||
MissingData: module.MissingDataEmpty,
|
MissingData: module.MissingDataEmpty,
|
||||||
Builder: buildPrecipTimingModule,
|
Builder: buildPrecipTimingModule,
|
||||||
@@ -359,8 +350,8 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.AlertDigest,
|
ID: module.AlertDigest,
|
||||||
StanzaName: "alert_digest",
|
StanzaName: "alert_digest",
|
||||||
DefaultOptions: module.AlertDigestOptions{},
|
DefaultOptions: module.AlertDigestOptions{},
|
||||||
RequiredCollected: []factRequirement{alertsRequirement},
|
RequiredCollected: []*factRequirement{alertsRequirement},
|
||||||
RequiredDerived: []factRequirement{alertOverlapsRequirement},
|
RequiredDerived: []*factRequirement{alertOverlapsRequirement},
|
||||||
SupportedReports: allReports,
|
SupportedReports: allReports,
|
||||||
MissingData: module.MissingDataEmpty,
|
MissingData: module.MissingDataEmpty,
|
||||||
Builder: buildAlertDigestModule,
|
Builder: buildAlertDigestModule,
|
||||||
@@ -369,8 +360,8 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.SPCConvectiveOutlooks,
|
ID: module.SPCConvectiveOutlooks,
|
||||||
StanzaName: string(module.SPCConvectiveOutlooks),
|
StanzaName: string(module.SPCConvectiveOutlooks),
|
||||||
DefaultOptions: module.SPCConvectiveOutlooksOptions{},
|
DefaultOptions: module.SPCConvectiveOutlooksOptions{},
|
||||||
RequiredCollected: []factRequirement{spcOutlooksRequirement},
|
RequiredCollected: []*factRequirement{spcOutlooksRequirement},
|
||||||
RequiredDerived: []factRequirement{spcDerivedOutlooksRequirement},
|
RequiredDerived: []*factRequirement{spcDerivedOutlooksRequirement},
|
||||||
SupportedReports: allReports,
|
SupportedReports: allReports,
|
||||||
MissingData: module.MissingDataEmpty,
|
MissingData: module.MissingDataEmpty,
|
||||||
Builder: buildSPCConvectiveOutlooksModule,
|
Builder: buildSPCConvectiveOutlooksModule,
|
||||||
@@ -379,7 +370,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.AreaForecastDiscussion,
|
ID: module.AreaForecastDiscussion,
|
||||||
StanzaName: "area_forecast_discussion",
|
StanzaName: "area_forecast_discussion",
|
||||||
DefaultOptions: module.AreaForecastDiscussionOptions{},
|
DefaultOptions: module.AreaForecastDiscussionOptions{},
|
||||||
RequiredCollected: []factRequirement{discussionRequirement},
|
RequiredCollected: []*factRequirement{discussionRequirement},
|
||||||
SupportedReports: allReports,
|
SupportedReports: allReports,
|
||||||
MissingData: module.MissingDataOmit,
|
MissingData: module.MissingDataOmit,
|
||||||
Builder: buildAreaForecastDiscussionModule,
|
Builder: buildAreaForecastDiscussionModule,
|
||||||
@@ -388,8 +379,8 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.SPCConvectiveDiscussion,
|
ID: module.SPCConvectiveDiscussion,
|
||||||
StanzaName: string(module.SPCConvectiveDiscussion),
|
StanzaName: string(module.SPCConvectiveDiscussion),
|
||||||
DefaultOptions: module.SPCConvectiveDiscussionOptions{},
|
DefaultOptions: module.SPCConvectiveDiscussionOptions{},
|
||||||
RequiredCollected: []factRequirement{spcOutlooksRequirement},
|
RequiredCollected: []*factRequirement{spcOutlooksRequirement},
|
||||||
RequiredDerived: []factRequirement{spcDerivedOutlooksRequirement},
|
RequiredDerived: []*factRequirement{spcDerivedOutlooksRequirement},
|
||||||
SupportedReports: allReports,
|
SupportedReports: allReports,
|
||||||
MissingData: module.MissingDataOmit,
|
MissingData: module.MissingDataOmit,
|
||||||
Builder: buildSPCConvectiveDiscussionModule,
|
Builder: buildSPCConvectiveDiscussionModule,
|
||||||
@@ -398,7 +389,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.WeatherStory,
|
ID: module.WeatherStory,
|
||||||
StanzaName: "weather_story",
|
StanzaName: "weather_story",
|
||||||
DefaultOptions: module.WeatherStoryOptions{},
|
DefaultOptions: module.WeatherStoryOptions{},
|
||||||
RequiredCollected: []factRequirement{weatherStoryRequirement},
|
RequiredCollected: []*factRequirement{weatherStoryRequirement},
|
||||||
SupportedReports: allReports,
|
SupportedReports: allReports,
|
||||||
MissingData: module.MissingDataOmit,
|
MissingData: module.MissingDataOmit,
|
||||||
Builder: buildWeatherStoryModule,
|
Builder: buildWeatherStoryModule,
|
||||||
@@ -407,7 +398,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.OutdoorWindows,
|
ID: module.OutdoorWindows,
|
||||||
StanzaName: "outdoor_windows",
|
StanzaName: "outdoor_windows",
|
||||||
DefaultOptions: module.OutdoorWindowsOptions{},
|
DefaultOptions: module.OutdoorWindowsOptions{},
|
||||||
RequiredDerived: []factRequirement{daypartSummariesRequirement},
|
RequiredDerived: []*factRequirement{daypartSummariesRequirement},
|
||||||
SupportedReports: daypartReports,
|
SupportedReports: daypartReports,
|
||||||
MissingData: module.MissingDataEmpty,
|
MissingData: module.MissingDataEmpty,
|
||||||
Builder: buildOutdoorWindowsModule,
|
Builder: buildOutdoorWindowsModule,
|
||||||
@@ -416,7 +407,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.TodayPlanning,
|
ID: module.TodayPlanning,
|
||||||
StanzaName: "today_planning",
|
StanzaName: "today_planning",
|
||||||
DefaultOptions: module.TodayPlanningOptions{},
|
DefaultOptions: module.TodayPlanningOptions{},
|
||||||
RequiredDerived: []factRequirement{dailySummariesRequirement},
|
RequiredDerived: []*factRequirement{dailySummariesRequirement},
|
||||||
SupportedReports: []report.ID{report.Today},
|
SupportedReports: []report.ID{report.Today},
|
||||||
MissingData: module.MissingDataEmpty,
|
MissingData: module.MissingDataEmpty,
|
||||||
Builder: buildTodayPlanningModule,
|
Builder: buildTodayPlanningModule,
|
||||||
@@ -425,7 +416,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.TomorrowPlanning,
|
ID: module.TomorrowPlanning,
|
||||||
StanzaName: "tomorrow_planning",
|
StanzaName: "tomorrow_planning",
|
||||||
DefaultOptions: module.TomorrowPlanningOptions{},
|
DefaultOptions: module.TomorrowPlanningOptions{},
|
||||||
RequiredDerived: []factRequirement{dailySummariesRequirement},
|
RequiredDerived: []*factRequirement{dailySummariesRequirement},
|
||||||
SupportedReports: []report.ID{report.Tomorrow},
|
SupportedReports: []report.ID{report.Tomorrow},
|
||||||
MissingData: module.MissingDataEmpty,
|
MissingData: module.MissingDataEmpty,
|
||||||
Builder: buildTomorrowPlanningModule,
|
Builder: buildTomorrowPlanningModule,
|
||||||
@@ -434,7 +425,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
ID: module.DailyPlanning,
|
ID: module.DailyPlanning,
|
||||||
StanzaName: "daily_planning",
|
StanzaName: "daily_planning",
|
||||||
DefaultOptions: module.DailyPlanningOptions{},
|
DefaultOptions: module.DailyPlanningOptions{},
|
||||||
RequiredDerived: []factRequirement{dailySummariesRequirement},
|
RequiredDerived: []*factRequirement{dailySummariesRequirement},
|
||||||
SupportedReports: []report.ID{report.Daily},
|
SupportedReports: []report.ID{report.Daily},
|
||||||
MissingData: module.MissingDataEmpty,
|
MissingData: module.MissingDataEmpty,
|
||||||
Builder: buildDailyPlanningModule,
|
Builder: buildDailyPlanningModule,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func TestDefaultModuleRegistryValidatesReportDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFactRequirementVocabularyMatchesModuleDefinitions(t *testing.T) {
|
func TestFactRequirementVocabularyMatchesModuleDefinitions(t *testing.T) {
|
||||||
vocabulary := make(map[string]factRequirement, len(factRequirementVocabulary))
|
vocabulary := make(map[string]*factRequirement, len(factRequirementVocabulary))
|
||||||
for _, requirement := range factRequirementVocabulary {
|
for _, requirement := range factRequirementVocabulary {
|
||||||
if requirement.identity == "" {
|
if requirement.identity == "" {
|
||||||
t.Fatal("fact requirement identity is empty")
|
t.Fatal("fact requirement identity is empty")
|
||||||
@@ -42,10 +42,13 @@ func TestFactRequirementVocabularyMatchesModuleDefinitions(t *testing.T) {
|
|||||||
if _, exists := vocabulary[requirement.identity]; exists {
|
if _, exists := vocabulary[requirement.identity]; exists {
|
||||||
t.Fatalf("duplicate fact requirement %q", requirement.identity)
|
t.Fatalf("duplicate fact requirement %q", requirement.identity)
|
||||||
}
|
}
|
||||||
|
if requirement.available == nil {
|
||||||
|
t.Fatalf("fact requirement %q has no availability predicate", requirement.identity)
|
||||||
|
}
|
||||||
vocabulary[requirement.identity] = requirement
|
vocabulary[requirement.identity] = requirement
|
||||||
}
|
}
|
||||||
|
|
||||||
used := map[factRequirement]struct{}{}
|
used := map[*factRequirement]struct{}{}
|
||||||
for _, definition := range defaultModuleDefinitions() {
|
for _, definition := range defaultModuleDefinitions() {
|
||||||
for _, requirement := range definition.RequiredCollected {
|
for _, requirement := range definition.RequiredCollected {
|
||||||
assertFactRequirementCategory(t, vocabulary, used, definition.ID, requirement, collectedFactRequirement)
|
assertFactRequirementCategory(t, vocabulary, used, definition.ID, requirement, collectedFactRequirement)
|
||||||
@@ -61,7 +64,7 @@ func TestFactRequirementVocabularyMatchesModuleDefinitions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertFactRequirementCategory(t *testing.T, vocabulary map[string]factRequirement, used map[factRequirement]struct{}, moduleID module.ID, requirement factRequirement, want factRequirementCategory) {
|
func assertFactRequirementCategory(t *testing.T, vocabulary map[string]*factRequirement, used map[*factRequirement]struct{}, moduleID module.ID, requirement *factRequirement, want factRequirementCategory) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
descriptor, ok := vocabulary[requirement.identity]
|
descriptor, ok := vocabulary[requirement.identity]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -480,6 +483,52 @@ func TestModuleRegistryRejectsDefinitionsWithoutBuilders(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestModuleRegistryRejectsInvalidFactRequirements(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
configure func(*ModuleDefinition)
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Unknown",
|
||||||
|
configure: func(definition *ModuleDefinition) {
|
||||||
|
definition.RequiredCollected = []*factRequirement{{identity: "collected.unknown", category: collectedFactRequirement}}
|
||||||
|
},
|
||||||
|
wantErr: `module "metadata" uses unknown collected fact requirement "collected.unknown"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DerivedListedAsCollected",
|
||||||
|
configure: func(definition *ModuleDefinition) {
|
||||||
|
definition.RequiredCollected = []*factRequirement{dailySummariesRequirement}
|
||||||
|
},
|
||||||
|
wantErr: `module "metadata" lists derived fact requirement "derived.daily_summaries" as collected`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CollectedListedAsDerived",
|
||||||
|
configure: func(definition *ModuleDefinition) {
|
||||||
|
definition.RequiredDerived = []*factRequirement{currentConditionsRequirement}
|
||||||
|
},
|
||||||
|
wantErr: `module "metadata" lists collected fact requirement "collected.current_conditions" as derived`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
definition := ModuleDefinition{
|
||||||
|
ID: module.Metadata,
|
||||||
|
StanzaName: "metadata",
|
||||||
|
DefaultOptions: module.MetadataOptions{},
|
||||||
|
Builder: noopModuleBuilder,
|
||||||
|
}
|
||||||
|
tt.configure(&definition)
|
||||||
|
_, err := NewModuleRegistry([]ModuleDefinition{definition})
|
||||||
|
if err == nil || err.Error() != tt.wantErr {
|
||||||
|
t.Fatalf("error = %v, want %q", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestModuleRegistryRejectsUnsupportedMissingDataWarn(t *testing.T) {
|
func TestModuleRegistryRejectsUnsupportedMissingDataWarn(t *testing.T) {
|
||||||
_, err := NewModuleRegistry([]ModuleDefinition{
|
_, err := NewModuleRegistry([]ModuleDefinition{
|
||||||
{ID: module.Metadata, StanzaName: "metadata", DefaultOptions: module.MetadataOptions{}, MissingData: module.MissingDataWarn, Builder: noopModuleBuilder},
|
{ID: module.Metadata, StanzaName: "metadata", DefaultOptions: module.MetadataOptions{}, MissingData: module.MissingDataWarn, Builder: noopModuleBuilder},
|
||||||
@@ -513,25 +562,25 @@ func TestModuleRegistryAcceptsTypedOptions(t *testing.T) {
|
|||||||
|
|
||||||
func TestSPCConvectiveOutlookCollectedRequirementAvailability(t *testing.T) {
|
func TestSPCConvectiveOutlookCollectedRequirementAvailability(t *testing.T) {
|
||||||
ctx := ModuleContext{}
|
ctx := ModuleContext{}
|
||||||
if collectedFactAvailable(spcOutlooksRequirement, ctx) {
|
if spcOutlooksRequirement.available(ctx) {
|
||||||
t.Fatal("collectedFactAvailable() = true, want false without source")
|
t.Fatal("SPC outlook requirement is available, want false without source")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Collected = facts.CollectedFacts{SPCConvectiveOutlooks: &weatherdata.ConvectiveOutlookRun{}}
|
ctx.Collected = facts.CollectedFacts{SPCConvectiveOutlooks: &weatherdata.ConvectiveOutlookRun{}}
|
||||||
if !collectedFactAvailable(spcOutlooksRequirement, ctx) {
|
if !spcOutlooksRequirement.available(ctx) {
|
||||||
t.Fatal("collectedFactAvailable() = false, want true with checked source")
|
t.Fatal("SPC outlook requirement is unavailable, want true with checked source")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSPCConvectiveOutlookDerivedRequirementAvailability(t *testing.T) {
|
func TestSPCConvectiveOutlookDerivedRequirementAvailability(t *testing.T) {
|
||||||
ctx := ModuleContext{}
|
ctx := ModuleContext{}
|
||||||
if derivedFactAvailable(spcDerivedOutlooksRequirement, ctx) {
|
if spcDerivedOutlooksRequirement.available(ctx) {
|
||||||
t.Fatal("derivedFactAvailable() = true, want false without derived outlooks")
|
t.Fatal("derived SPC outlook requirement is available, want false without derived outlooks")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Derived = facts.DerivedFacts{SPCConvectiveOutlooks: []weatherdata.ConvectiveOutlook{}}
|
ctx.Derived = facts.DerivedFacts{SPCConvectiveOutlooks: []weatherdata.ConvectiveOutlook{}}
|
||||||
if !derivedFactAvailable(spcDerivedOutlooksRequirement, ctx) {
|
if !spcDerivedOutlooksRequirement.available(ctx) {
|
||||||
t.Fatal("derivedFactAvailable() = false, want true for checked empty derived outlooks")
|
t.Fatal("derived SPC outlook requirement is unavailable, want true for checked empty derived outlooks")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user