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/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 reportFacts ReportFacts moduleSnapshot module.Snapshot briefingMetadata briefing.Metadata sourceWarnings []weatherdata.SourceWarning dataPackage []byte handler generatedtext.Handler } type prepareReportRequest struct { Config config.Config Resolved report.Resolved Collection collect.Result } 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} } moduleSnapshot, err := BuildModuleSnapshotFromFacts(ModuleSnapshotRequest{Config: req.Config, Resolved: req.Resolved}, reportFacts) if err != nil { return preparedReport{}, &preparationError{operation: "build module snapshot", err: err} } metadata := briefing.BuildMetadata(briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected)) dataPackage, err := promptinput.Build(promptinput.BuildRequest{Metadata: promptMetadata(metadata), 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} } handler, err := generatedtext.LookupDefinition(req.Resolved.Definition) if err != nil { return preparedReport{}, &preparationError{operation: "lookup generated text catalog", err: err} } clonedFacts, err := clonePreparedValue(reportFacts) if err != nil { return preparedReport{}, &preparationError{operation: "copy prepared report facts", err: err} } clonedSnapshot, err := clonePreparedValue(moduleSnapshot) if err != nil { return preparedReport{}, &preparationError{operation: "copy prepared module snapshot", err: err} } clonedMetadata, err := clonePreparedValue(metadata) if err != nil { return preparedReport{}, &preparationError{operation: "copy prepared briefing metadata", err: err} } prepared := preparedReport{ resolved: cloneResolved(req.Resolved), reportFacts: clonedFacts, moduleSnapshot: clonedSnapshot, briefingMetadata: clonedMetadata, sourceWarnings: append([]weatherdata.SourceWarning(nil), clonedMetadata.SourceWarnings...), dataPackage: append([]byte(nil), serializedDataPackage...), handler: 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.Metadata, module.Snapshot, ReportFacts, error) { metadata, err := clonePreparedValue(p.briefingMetadata) if err != nil { return briefing.Metadata{}, module.Snapshot{}, ReportFacts{}, err } snapshot, err := clonePreparedValue(p.moduleSnapshot) if err != nil { return briefing.Metadata{}, module.Snapshot{}, ReportFacts{}, err } reportFacts, err := clonePreparedValue(p.reportFacts) if err != nil { return briefing.Metadata{}, module.Snapshot{}, ReportFacts{}, err } return metadata, snapshot, reportFacts, 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 }