Files
weatherreporter/internal/promptinput/package.go

141 lines
4.3 KiB
Go

// Package promptinput builds prompt data packages from briefing packages.
package promptinput
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
)
const SchemaVersion = "weatherreporter.data_package.v1"
type Package struct {
SchemaVersion string `json:"schemaVersion"`
RunID string `json:"runId"`
Report Report `json:"report"`
Briefing briefing.Package `json:"briefing"`
RecentChanges RecentChanges `json:"recentChanges"`
SourceWarnings []forecast.SourceWarning `json:"sourceWarnings,omitempty"`
}
type Report struct {
ID report.ID `json:"id"`
Variant string `json:"variant,omitempty"`
PromptID string `json:"promptId"`
GeneratedAt time.Time `json:"generatedAt"`
Timezone string `json:"timezone"`
ValidPeriod timeutil.Period `json:"validPeriod"`
}
type RecentChanges struct {
Items []Change `json:"items"`
}
type Change struct {
Type string `json:"type"`
Message string `json:"message"`
}
func Build(briefingPackage briefing.Package) (Package, error) {
pkg := Package{
SchemaVersion: SchemaVersion,
RunID: briefingPackage.Metadata.RunID,
Report: Report{
ID: briefingPackage.Metadata.ReportID,
Variant: briefingPackage.Metadata.Variant,
PromptID: briefingPackage.Metadata.PromptID,
GeneratedAt: briefingPackage.Metadata.GeneratedAt,
Timezone: briefingPackage.Metadata.Timezone,
ValidPeriod: briefingPackage.Metadata.ValidPeriod,
},
Briefing: briefingPackage,
RecentChanges: RecentChanges{Items: []Change{}},
SourceWarnings: briefingPackage.Metadata.SourceWarnings,
}
if err := Validate(pkg); err != nil {
return Package{}, err
}
return pkg, nil
}
func Validate(pkg Package) error {
if pkg.SchemaVersion == "" {
return fmt.Errorf("schemaVersion is required")
}
if pkg.RunID == "" {
return fmt.Errorf("runId is required")
}
if pkg.Report.ID == "" {
return fmt.Errorf("report.id is required")
}
if pkg.Report.PromptID == "" {
return fmt.Errorf("report.promptId is required")
}
if pkg.Report.GeneratedAt.IsZero() {
return fmt.Errorf("report.generatedAt is required")
}
if pkg.Report.Timezone == "" {
return fmt.Errorf("report.timezone is required")
}
if !pkg.Report.ValidPeriod.IsValid() {
return fmt.Errorf("report.validPeriod must be valid")
}
if pkg.Briefing.Metadata.RunID == "" {
return fmt.Errorf("briefing.metadata.runId is required")
}
if pkg.Briefing.Metadata.RunID != pkg.RunID {
return fmt.Errorf("briefing.metadata.runId must match runId")
}
if pkg.Briefing.Metadata.SchemaVersion == "" {
return fmt.Errorf("briefing.metadata.schemaVersion is required")
}
if pkg.Briefing.Metadata.PromptID != pkg.Report.PromptID {
return fmt.Errorf("briefing.metadata.promptId must match report.promptId")
}
if pkg.Briefing.Metadata.ReportID != pkg.Report.ID {
return fmt.Errorf("briefing.metadata.reportId must match report.id")
}
if pkg.Briefing.Daily == nil {
return fmt.Errorf("briefing.daily is required")
}
return nil
}
func Save(path string, pkg Package) error {
if err := Validate(pkg); err != nil {
return err
}
data, err := json.MarshalIndent(pkg, "", " ")
if err != nil {
return fmt.Errorf("marshal data package: %w", err)
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("create data package directory %q: %w", filepath.Dir(path), err)
}
tmp, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*.tmp")
if err != nil {
return fmt.Errorf("create temporary data package file: %w", err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return fmt.Errorf("write temporary data package file: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close temporary data package file: %w", err)
}
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("save data package %q: %w", path, err)
}
return nil
}