Refactor the yaml data package to group data sources by category
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user