Files
weatherreporter/internal/app/prepared_report.go

149 lines
5.3 KiB
Go

package app
import (
"encoding/json"
"fmt"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/collect"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
"gitea.maximumdirect.net/eric/weatherreporter/internal/generatedtext"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
)
// preparedReport contains the immutable deterministic inputs shared by prompt
// executions for one resolved report.
type preparedReport struct {
resolved report.Resolved
derived facts.DerivedFacts
moduleSnapshot module.Snapshot
identity briefing.PreparedIdentity
sourceWarnings []weatherdata.SourceWarning
dataPackage []byte
handler generatedtext.Handler
}
type prepareReportRequest struct {
Config config.Config
Resolved report.Resolved
Collection collect.Result
handler generatedtext.Handler
}
type preparationError struct {
operation string
err error
}
func (e *preparationError) Error() string {
return e.operation + ": " + e.err.Error()
}
func (e *preparationError) Unwrap() error {
return e.err
}
func prepareReport(req prepareReportRequest) (preparedReport, error) {
if req.Collection.Bundle == nil {
return preparedReport{}, &preparationError{operation: "prepare report", err: fmt.Errorf("collected weather bundle is required")}
}
reportFacts, err := BuildReportFacts(ModuleSnapshotRequest{Config: req.Config, Resolved: req.Resolved}, req.Collection.Bundle)
if err != nil {
return preparedReport{}, &preparationError{operation: "build report facts", err: err}
}
buildContext := briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected)
identity := briefing.BuildPreparedIdentity(buildContext)
moduleSnapshot, err := BuildModuleSnapshotFromFacts(ModuleSnapshotRequest{Config: req.Config, Resolved: req.Resolved, Identity: identity}, reportFacts)
if err != nil {
return preparedReport{}, &preparationError{operation: "build module snapshot", err: err}
}
dataPackage, err := promptinput.Build(promptinput.BuildRequest{Metadata: promptMetadata(identity), Modules: moduleSnapshot})
if err != nil {
return preparedReport{}, &preparationError{operation: "build data package", err: err}
}
serializedDataPackage, err := promptinput.MarshalYAML(dataPackage)
if err != nil {
return preparedReport{}, &preparationError{operation: "marshal data package", err: err}
}
clonedDerived, err := clonePreparedValue(reportFacts.Derived)
if err != nil {
return preparedReport{}, &preparationError{operation: "copy prepared derived facts", err: err}
}
clonedSnapshot, err := clonePreparedValue(moduleSnapshot)
if err != nil {
return preparedReport{}, &preparationError{operation: "copy prepared module snapshot", err: err}
}
clonedIdentity, err := clonePreparedValue(identity)
if err != nil {
return preparedReport{}, &preparationError{operation: "copy prepared identity", err: err}
}
prepared := preparedReport{
resolved: cloneResolved(req.Resolved),
derived: clonedDerived,
moduleSnapshot: clonedSnapshot,
identity: clonedIdentity,
sourceWarnings: append([]weatherdata.SourceWarning(nil), clonedIdentity.SourceWarnings...),
dataPackage: append([]byte(nil), serializedDataPackage...),
handler: req.handler,
}
return prepared, nil
}
func cloneResolved(value report.Resolved) report.Resolved {
cloned := value
cloned.Definition.DistributorPathTemplates = append([]string(nil), value.Definition.DistributorPathTemplates...)
cloned.Definition.Modules = make([]module.ConfigItem, len(value.Definition.Modules))
for i, item := range value.Definition.Modules {
cloned.Definition.Modules[i] = item
switch options := item.Options.(type) {
case module.AreaForecastDiscussionOptions:
options.Sections = append([]string(nil), options.Sections...)
cloned.Definition.Modules[i].Options = options
}
}
return cloned
}
func (p preparedReport) dataPackageCopy() []byte {
return append([]byte(nil), p.dataPackage...)
}
func (p preparedReport) sourceWarningsCopy() []weatherdata.SourceWarning {
return append([]weatherdata.SourceWarning(nil), p.sourceWarnings...)
}
func (p preparedReport) renderInputs() (briefing.PreparedIdentity, module.Snapshot, facts.DerivedFacts, error) {
identity, err := clonePreparedValue(p.identity)
if err != nil {
return briefing.PreparedIdentity{}, module.Snapshot{}, facts.DerivedFacts{}, err
}
snapshot, err := clonePreparedValue(p.moduleSnapshot)
if err != nil {
return briefing.PreparedIdentity{}, module.Snapshot{}, facts.DerivedFacts{}, err
}
derived, err := clonePreparedValue(p.derived)
if err != nil {
return briefing.PreparedIdentity{}, module.Snapshot{}, facts.DerivedFacts{}, err
}
return identity, snapshot, derived, nil
}
func clonePreparedValue[T any](value T) (T, error) {
encoded, err := json.Marshal(value)
if err != nil {
var zero T
return zero, fmt.Errorf("marshal immutable prepared value: %w", err)
}
var cloned T
if err := json.Unmarshal(encoded, &cloned); err != nil {
var zero T
return zero, fmt.Errorf("unmarshal immutable prepared value: %w", err)
}
return cloned, nil
}