Refactor the yaml data package to group data sources by category

This commit is contained in:
2026-06-10 11:50:22 -05:00
parent 276e4f1189
commit f149563c68
4 changed files with 256 additions and 32 deletions

View File

@@ -18,6 +18,35 @@ import (
const SchemaVersion = "weatherreporter.data_package.v2"
const (
metadataStanza = "metadata"
categoryApplicableRiskProducts = "applicable_risk_products"
categoryDerivedSummaries = "derived_summaries"
categoryNarrativeProducts = "narrative_products"
categoryRawData = "raw_data"
)
var briefingCategoryOrder = []string{
categoryApplicableRiskProducts,
categoryDerivedSummaries,
categoryNarrativeProducts,
categoryRawData,
}
var briefingStanzaCategories = map[string]string{
string(module.AlertDigest): categoryApplicableRiskProducts,
string(module.DerivedDailySummary): categoryDerivedSummaries,
string(module.DerivedDaypartSummaries): categoryDerivedSummaries,
string(module.PrecipTiming): categoryDerivedSummaries,
string(module.OutdoorWindows): categoryDerivedSummaries,
string(module.TomorrowPlanning): categoryDerivedSummaries,
string(module.NarrativeForecast): categoryNarrativeProducts,
string(module.AreaForecastDiscussion): categoryNarrativeProducts,
string(module.WeatherStory): categoryNarrativeProducts,
string(module.CurrentConditions): categoryRawData,
string(module.HourlyForecast): categoryRawData,
}
type BuildRequest struct {
Metadata Metadata
Modules module.Snapshot
@@ -144,13 +173,24 @@ func Validate(pkg Package) error {
if len(pkg.Briefing.Order) == 0 {
return fmt.Errorf("briefing stanzas are required")
}
seen := map[string]struct{}{}
for _, name := range pkg.Briefing.Order {
if name == "" {
return fmt.Errorf("briefing stanza name is required")
}
if _, ok := seen[name]; ok {
return fmt.Errorf("duplicate briefing stanza %q", name)
}
seen[name] = struct{}{}
if _, ok := pkg.Briefing.Values[name]; !ok {
return fmt.Errorf("briefing stanza %q is missing", name)
}
if name == metadataStanza {
continue
}
if _, ok := briefingStanzaCategories[name]; !ok {
return fmt.Errorf("briefing stanza %q has no prompt-input category", name)
}
}
return nil
}
@@ -191,17 +231,40 @@ func LoadYAML(data []byte) (Package, error) {
func (b BriefingStanzas) MarshalYAML() (any, error) {
node := &yaml.Node{Kind: yaml.MappingNode}
categoryNames := map[string][]string{}
for _, name := range b.Order {
value, ok := b.Values[name]
if !ok {
continue
}
keyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: name}
valueNode, err := yamlNode(value)
if err != nil {
return nil, fmt.Errorf("marshal briefing stanza %q: %w", name, err)
if name == metadataStanza {
if err := appendYAMLMappingValue(node, name, value); err != nil {
return nil, err
}
continue
}
node.Content = append(node.Content, keyNode, valueNode)
category, ok := briefingStanzaCategories[name]
if !ok {
return nil, fmt.Errorf("briefing stanza %q has no prompt-input category", name)
}
categoryNames[category] = append(categoryNames[category], name)
}
for _, category := range briefingCategoryOrder {
names := categoryNames[category]
if len(names) == 0 {
continue
}
categoryNode := &yaml.Node{Kind: yaml.MappingNode}
for _, name := range names {
value := b.Values[name]
if err := appendYAMLMappingValue(categoryNode, name, value); err != nil {
return nil, err
}
}
node.Content = append(node.Content,
&yaml.Node{Kind: yaml.ScalarNode, Value: category},
categoryNode,
)
}
return node, nil
}
@@ -212,14 +275,46 @@ func (b *BriefingStanzas) UnmarshalYAML(value *yaml.Node) error {
}
values := map[string]any{}
order := make([]string, 0, len(value.Content)/2)
seen := map[string]struct{}{}
seenCategories := map[string]struct{}{}
categoryOrder := map[string][]string{}
for i := 0; i < len(value.Content); i += 2 {
name := value.Content[i].Value
var stanza any
if err := value.Content[i+1].Decode(&stanza); err != nil {
return err
if name == metadataStanza {
if err := decodeBriefingStanza(value.Content[i+1], name, values, &order, seen); err != nil {
return err
}
continue
}
order = append(order, name)
values[name] = stanza
if !knownBriefingCategory(name) {
return fmt.Errorf("unknown briefing category %q", name)
}
if _, ok := seenCategories[name]; ok {
return fmt.Errorf("duplicate briefing category %q", name)
}
seenCategories[name] = struct{}{}
categoryNode := value.Content[i+1]
if categoryNode.Kind != yaml.MappingNode {
return fmt.Errorf("briefing category %q must be a mapping", name)
}
var names []string
for j := 0; j < len(categoryNode.Content); j += 2 {
stanzaName := categoryNode.Content[j].Value
category, ok := briefingStanzaCategories[stanzaName]
if !ok {
return fmt.Errorf("briefing stanza %q has no prompt-input category", stanzaName)
}
if category != name {
return fmt.Errorf("briefing stanza %q belongs under category %q, not %q", stanzaName, category, name)
}
if err := decodeBriefingStanza(categoryNode.Content[j+1], stanzaName, values, &names, seen); err != nil {
return err
}
}
categoryOrder[name] = names
}
for _, category := range briefingCategoryOrder {
order = append(order, categoryOrder[category]...)
}
b.Order = order
b.Values = values
@@ -250,6 +345,39 @@ func (b *BriefingStanzas) UnmarshalJSON(data []byte) error {
return nil
}
func appendYAMLMappingValue(node *yaml.Node, name string, value any) error {
keyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: name}
valueNode, err := yamlNode(value)
if err != nil {
return fmt.Errorf("marshal briefing stanza %q: %w", name, err)
}
node.Content = append(node.Content, keyNode, valueNode)
return nil
}
func decodeBriefingStanza(node *yaml.Node, name string, values map[string]any, order *[]string, seen map[string]struct{}) error {
if _, ok := seen[name]; ok {
return fmt.Errorf("duplicate briefing stanza %q", name)
}
var stanza any
if err := node.Decode(&stanza); err != nil {
return err
}
seen[name] = struct{}{}
*order = append(*order, name)
values[name] = stanza
return nil
}
func knownBriefingCategory(name string) bool {
for _, category := range briefingCategoryOrder {
if name == category {
return true
}
}
return false
}
func yamlNode(value any) (*yaml.Node, error) {
data, err := json.Marshal(value)
if err != nil {