Skip to main content

buildfix_artifacts/
lib.rs

1//! Artifact serialization and persistence for buildfix outputs.
2
3use anyhow::Context;
4use buildfix_render::{render_apply_md, render_comment_md, render_plan_md};
5use buildfix_types::apply::BuildfixApply;
6use buildfix_types::plan::BuildfixPlan;
7use buildfix_types::report::BuildfixReport;
8use buildfix_types::schema::BUILDFIX_REPORT_V1;
9use buildfix_types::wire::{PlanV1, ReportV1};
10use camino::Utf8Path;
11use std::collections::BTreeMap;
12use std::fs;
13
14/// Filesystem-facing abstraction for artifact emission.
15pub trait ArtifactWriter {
16    fn write_file(&self, path: &Utf8Path, contents: &[u8]) -> anyhow::Result<()>;
17    fn create_dir_all(&self, path: &Utf8Path) -> anyhow::Result<()>;
18}
19
20/// Standard filesystem implementation.
21#[derive(Debug, Default, Clone)]
22pub struct FsArtifactWriter;
23
24impl ArtifactWriter for FsArtifactWriter {
25    fn write_file(&self, path: &Utf8Path, contents: &[u8]) -> anyhow::Result<()> {
26        if let Some(parent) = path.parent() {
27            fs::create_dir_all(parent)
28                .with_context(|| format!("create parent dir for {}", path))?;
29        }
30        fs::write(path, contents).with_context(|| format!("write {}", path))
31    }
32
33    fn create_dir_all(&self, path: &Utf8Path) -> anyhow::Result<()> {
34        fs::create_dir_all(path).with_context(|| format!("create dir {}", path))
35    }
36}
37
38fn render_plan_report(
39    plan: &BuildfixPlan,
40    report: &BuildfixReport,
41    patch: &str,
42    out_dir: &Utf8Path,
43) -> anyhow::Result<BTreeMap<String, Vec<u8>>> {
44    let plan_wire = PlanV1::try_from(plan).context("convert plan to wire")?;
45    let plan_json = serde_json::to_string_pretty(&plan_wire).context("serialize plan")?;
46
47    let report_wire = ReportV1::from(report);
48    let report_json = serde_json::to_string_pretty(&report_wire).context("serialize report")?;
49
50    let mut extra_report = report.clone();
51    extra_report.schema = BUILDFIX_REPORT_V1.to_string();
52    let extras_wire = ReportV1::from(&extra_report);
53    let extras_json =
54        serde_json::to_string_pretty(&extras_wire).context("serialize extras report")?;
55
56    let mut files = BTreeMap::new();
57    files.insert(
58        out_dir.join("plan.json").to_string(),
59        plan_json.into_bytes(),
60    );
61    files.insert(
62        out_dir.join("plan.md").to_string(),
63        render_plan_md(plan).into_bytes(),
64    );
65    files.insert(
66        out_dir.join("comment.md").to_string(),
67        render_comment_md(plan).into_bytes(),
68    );
69    files.insert(
70        out_dir.join("patch.diff").to_string(),
71        patch.as_bytes().to_vec(),
72    );
73    files.insert(
74        out_dir.join("report.json").to_string(),
75        report_json.into_bytes(),
76    );
77    files.insert(
78        out_dir
79            .join("extras")
80            .join("buildfix.report.v1.json")
81            .to_string(),
82        extras_json.into_bytes(),
83    );
84    Ok(files)
85}
86
87fn render_apply_report(
88    apply: &BuildfixApply,
89    report: &BuildfixReport,
90    patch: &str,
91    out_dir: &Utf8Path,
92) -> anyhow::Result<BTreeMap<String, Vec<u8>>> {
93    let apply_wire =
94        buildfix_types::wire::ApplyV1::try_from(apply).context("convert apply to wire")?;
95    let apply_json = serde_json::to_string_pretty(&apply_wire).context("serialize apply")?;
96
97    let report_wire = ReportV1::from(report);
98    let report_json = serde_json::to_string_pretty(&report_wire).context("serialize report")?;
99
100    let mut extra_report = report.clone();
101    extra_report.schema = BUILDFIX_REPORT_V1.to_string();
102    let extras_wire = ReportV1::from(&extra_report);
103    let extras_json =
104        serde_json::to_string_pretty(&extras_wire).context("serialize extras report")?;
105
106    let mut files = BTreeMap::new();
107    files.insert(
108        out_dir.join("apply.json").to_string(),
109        apply_json.into_bytes(),
110    );
111    files.insert(
112        out_dir.join("apply.md").to_string(),
113        render_apply_md(apply).into_bytes(),
114    );
115    files.insert(
116        out_dir.join("patch.diff").to_string(),
117        patch.as_bytes().to_vec(),
118    );
119    files.insert(
120        out_dir.join("report.json").to_string(),
121        report_json.into_bytes(),
122    );
123    files.insert(
124        out_dir
125            .join("extras")
126            .join("buildfix.report.v1.json")
127            .to_string(),
128        extras_json.into_bytes(),
129    );
130    Ok(files)
131}
132
133fn write_files<W: ArtifactWriter>(
134    files: BTreeMap<String, Vec<u8>>,
135    writer: &W,
136) -> anyhow::Result<()> {
137    for (path, contents) in files {
138        writer.write_file(Utf8Path::new(&path), &contents)?;
139    }
140    Ok(())
141}
142
143/// Emit all plan artifacts (plan.json, plan.md, comment.md, patch, report, extras).
144pub fn write_plan_artifacts<W: ArtifactWriter>(
145    plan: &BuildfixPlan,
146    report: &BuildfixReport,
147    patch: &str,
148    out_dir: &Utf8Path,
149    writer: &W,
150) -> anyhow::Result<()> {
151    writer.create_dir_all(out_dir)?;
152    writer.create_dir_all(&out_dir.join("extras"))?;
153    let files = render_plan_report(plan, report, patch, out_dir)?;
154    write_files(files, writer)
155}
156
157/// Emit all apply artifacts (apply.json, apply.md, patch, report, extras).
158pub fn write_apply_artifacts<W: ArtifactWriter>(
159    apply: &BuildfixApply,
160    report: &BuildfixReport,
161    patch: &str,
162    out_dir: &Utf8Path,
163    writer: &W,
164) -> anyhow::Result<()> {
165    writer.create_dir_all(out_dir)?;
166    writer.create_dir_all(&out_dir.join("extras"))?;
167    let files = render_apply_report(apply, report, patch, out_dir)?;
168    write_files(files, writer)
169}