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 }