Files
weatherreporter/internal/state/metadata.go

152 lines
7.4 KiB
Go

package state
import (
"encoding/json"
"fmt"
"strings"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
)
const (
MetadataSchemaVersion = "weatherreporter.metadata.v2"
)
// Metadata is the durable record used by the transitional state package.
type Metadata struct {
SchemaVersion string `json:"schemaVersion"`
RunID string `json:"runId"`
MetadataPath string `json:"-"`
ReportID report.ID `json:"reportId"`
Variant string `json:"variant,omitempty"`
PromptID string `json:"promptId"`
GeneratedAt time.Time `json:"generatedAt"`
Timezone string `json:"timezone"`
ValidPeriod timeutil.Period `json:"validPeriod"`
Location *briefing.LocationContext `json:"location,omitempty"`
SourceLocationID string `json:"sourceLocationId,omitempty"`
SourceLocation string `json:"sourceLocation,omitempty"`
Sources []briefing.SourceMetadata `json:"sources,omitempty"`
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
ModuleSnapshotPath string `json:"moduleSnapshotPath"`
DataPackagePath string `json:"dataPackagePath"`
PreparationPath string `json:"preparationPath,omitempty"`
ExecutionPath string `json:"executionPath,omitempty"`
NotificationPath string `json:"notificationPath,omitempty"`
RenderedReportPath string `json:"renderedReportPath,omitempty"`
GeneratedTextSchemaID string `json:"generatedTextSchemaId,omitempty"`
GeneratedTextRawPath string `json:"generatedTextRawPath,omitempty"`
GeneratedTextPath string `json:"generatedTextPath,omitempty"`
RenderContextPath string `json:"renderContextPath,omitempty"`
}
type metadataJSON struct {
SchemaVersion string `json:"schemaVersion"`
RunID string `json:"runId"`
ReportID report.ID `json:"reportId"`
Variant string `json:"variant,omitempty"`
PromptID string `json:"promptId"`
GeneratedAt time.Time `json:"generatedAt"`
Timezone string `json:"timezone"`
ValidPeriod timeutil.Period `json:"validPeriod"`
Location *briefing.LocationContext `json:"location,omitempty"`
SourceLocationID string `json:"sourceLocationId,omitempty"`
SourceLocation string `json:"sourceLocation,omitempty"`
Sources []briefing.SourceMetadata `json:"sources,omitempty"`
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
ModuleSnapshotPath string `json:"moduleSnapshotPath"`
DataPackagePath string `json:"dataPackagePath"`
PreparationPath string `json:"preparationPath,omitempty"`
ExecutionPath string `json:"executionPath,omitempty"`
NotificationPath string `json:"notificationPath,omitempty"`
RenderedReportPath string `json:"renderedReportPath,omitempty"`
GeneratedTextSchemaID string `json:"generatedTextSchemaId,omitempty"`
GeneratedTextRawPath string `json:"generatedTextRawPath,omitempty"`
GeneratedTextPath string `json:"generatedTextPath,omitempty"`
RenderContextPath string `json:"renderContextPath,omitempty"`
}
func (m Metadata) MarshalJSON() ([]byte, error) {
w := metadataJSON{
SchemaVersion: m.SchemaVersion, RunID: m.RunID, ReportID: m.ReportID,
Variant: m.Variant, PromptID: m.PromptID, GeneratedAt: m.GeneratedAt,
Timezone: m.Timezone, ValidPeriod: m.ValidPeriod, Location: m.Location,
SourceLocationID: m.SourceLocationID, SourceLocation: m.SourceLocation,
Sources: m.Sources, SourceWarnings: m.SourceWarnings,
ModuleSnapshotPath: m.ModuleSnapshotPath, DataPackagePath: m.DataPackagePath,
NotificationPath: m.NotificationPath, RenderedReportPath: m.RenderedReportPath,
GeneratedTextSchemaID: m.GeneratedTextSchemaID, GeneratedTextRawPath: m.GeneratedTextRawPath,
GeneratedTextPath: m.GeneratedTextPath, RenderContextPath: m.RenderContextPath,
}
if m.SchemaVersion != MetadataSchemaVersion {
return nil, fmt.Errorf("unsupported metadata schema version %q", m.SchemaVersion)
}
w.PreparationPath = m.PreparationPath
w.ExecutionPath = m.ExecutionPath
return json.Marshal(w)
}
func (m *Metadata) UnmarshalJSON(data []byte) error {
type metadataWire Metadata
var decoded metadataWire
if err := json.Unmarshal(data, &decoded); err != nil {
return err
}
if decoded.SchemaVersion != MetadataSchemaVersion {
return fmt.Errorf("unsupported metadata schema version %q", decoded.SchemaVersion)
}
*m = Metadata(decoded)
return nil
}
func (m Metadata) Validate() error {
if strings.TrimSpace(m.RunID) == "" {
return fmt.Errorf("metadata run id is required")
}
if strings.TrimSpace(m.ModuleSnapshotPath) == "" {
return fmt.Errorf("metadata module snapshot path is required")
}
if strings.TrimSpace(m.DataPackagePath) == "" {
return fmt.Errorf("metadata data package path is required")
}
if strings.TrimSpace(m.MetadataPath) == "" {
return fmt.Errorf("metadata path is required")
}
if m.SchemaVersion != MetadataSchemaVersion {
return fmt.Errorf("unsupported metadata schema version %q", m.SchemaVersion)
}
if strings.TrimSpace(m.PreparationPath) == "" {
return fmt.Errorf("metadata preparation path is required")
}
return nil
}
// BuildPromptMetadataFromBriefingMetadata creates the V2 record used by the
// prompt execution workflow. Callers populate preparation and execution paths
// only after their corresponding artifacts have been saved.
func BuildPromptMetadataFromBriefingMetadata(resolved report.Resolved, briefingMetadata briefing.Metadata, paths ArtifactPaths) Metadata {
metadata := resolved.Metadata()
return Metadata{
SchemaVersion: MetadataSchemaVersion, RunID: metadata.RunID, MetadataPath: paths.Metadata,
ReportID: metadata.ReportID, Variant: briefingMetadata.Variant, PromptID: metadata.PromptID,
GeneratedAt: metadata.GeneratedAt, Timezone: metadata.Timezone, ValidPeriod: metadata.ValidPeriod,
Location: copyLocation(briefingMetadata.Location), SourceLocationID: briefingMetadata.SourceLocationID,
SourceLocation: briefingMetadata.SourceLocation, Sources: briefingMetadata.Sources,
SourceWarnings: briefingMetadata.SourceWarnings, ModuleSnapshotPath: paths.ModuleSnapshot,
DataPackagePath: paths.DataPackage,
GeneratedTextSchemaID: resolved.Definition.GeneratedTextSchemaID,
}
}
func copyLocation(location *briefing.LocationContext) *briefing.LocationContext {
if location == nil {
return nil
}
copied := *location
return &copied
}