1 //! Machine JSON (m3, f0rge issue #8): `api/*.json` built from the SAME2 //! structs used for HTML (no divergent logic).3 //!4 //! [`render`] takes the already-loaded readers — [`crate::forge::ForgeData`]5 //! (issues/pulls/tasks/ci), the commit log slice from [`crate::gitx::log_n`]6 //! (the same vec rendered to `commits.html`), the release slice from7 //! [`crate::releases::load`], and the wiki pages from [`crate::wiki::render`]8 //! — and writes compact fixed-order JSON with `serde_json::to_string`9 //! (never pretty). Array lengths therefore equal the HTML list counts by10 //! construction; see `tests/releases_api.rs`.11 //!12 //! SECURITY: bodies are untrusted text carried as JSON strings (never HTML).13 14 use std::path::Path;15 16 use serde::Serialize;17 18 use crate::forge::ForgeData;19 use crate::gitx::Commit;20 use crate::releases::Release;21 use crate::wiki::WikiPage;22 23 /// One issue row (fixed field order).24 #[derive(Debug, Clone, Serialize)]25 pub struct IssueJson {26 pub number: usize,27 pub id: String,28 pub title: String,29 pub state: String,30 pub labels: Vec<String>,31 pub assignees: Vec<String>,32 pub milestone: Option<String>,33 pub created: String,34 pub comments: usize,35 pub body: String,36 }37 38 /// One pull row (fixed field order).39 #[derive(Debug, Clone, Serialize)]40 pub struct PullJson {41 pub number: usize,42 pub id: String,43 pub title: String,44 pub state: String,45 pub base: String,46 pub head: String,47 pub draft: bool,48 pub created: String,49 pub comments: usize,50 pub reviews: usize,51 pub body: String,52 }53 54 /// One commit row (fixed field order).55 #[derive(Debug, Clone, Serialize)]56 pub struct CommitJson {57 pub sha: String,58 pub subject: String,59 pub author: String,60 pub date: String,61 }62 63 /// One release row (fixed field order).64 #[derive(Debug, Clone, Serialize)]65 pub struct ReleaseJson {66 pub version: String,67 pub title: Option<String>,68 pub created: String,69 pub sha: Option<String>,70 pub artifacts: usize,71 pub body: String,72 }73 74 /// One wiki row (fixed field order).75 #[derive(Debug, Clone, Serialize)]76 pub struct WikiJson {77 pub path: String,78 pub title: String,79 pub size: u64,80 }81 82 fn write_json(out: &Path, rel: &str, value: &impl Serialize) -> Result<(), String> {83 // Compact fixed-order JSON (serde struct order, no pretty printing).84 let text = serde_json::to_string(value).map_err(|e| format!("hu13: encode {rel}: {e}"))?;85 let dest = out.join(rel);86 if let Some(parent) = dest.parent() {87 std::fs::create_dir_all(parent).map_err(|e| format!("hu13: create api dir: {e}"))?;88 }89 std::fs::write(&dest, text).map_err(|e| format!("hu13: write {rel}: {e}"))90 }91 92 /// Build `api/issues.json`, `api/pulls.json`, `api/commits.json`,93 /// `api/releases.json`, and `api/wiki.json` from the same structs rendered94 /// to HTML. Callers must pass the exact slices/vecs used for the HTML pass.95 /// Issues/pulls rows follow the index display order (open-first,96 /// newest-first; numbers stay creation-rank).97 pub fn render(98 out: &Path,99 forge: &ForgeData,100 commits: &[Commit],101 releases: &[Release],102 wiki: &[WikiPage],103 ) -> Result<(), String> {104 let issues: Vec<IssueJson> = forge105 .issues_in_display_order()106 .into_iter()107 .map(|i| IssueJson {108 number: i.number,109 id: i.id.clone(),110 title: i.title.clone(),111 state: i.state.clone(),112 labels: i.labels.clone(),113 assignees: i.assignees.clone(),114 milestone: i.milestone.clone(),115 created: i.created.clone(),116 comments: i.comments.len(),117 body: i.body.clone(),118 })119 .collect();120 let pulls: Vec<PullJson> = forge121 .prs_in_display_order()122 .into_iter()123 .map(|p| PullJson {124 number: p.number,125 id: p.id.clone(),126 title: p.title.clone(),127 state: p.state.clone(),128 base: p.base.clone(),129 head: p.head.clone(),130 draft: p.draft,131 created: p.created.clone(),132 comments: p.comments.len(),133 reviews: p.reviews.len(),134 body: p.body.clone(),135 })136 .collect();137 let commits_json: Vec<CommitJson> = commits138 .iter()139 .map(|c| CommitJson {140 sha: c.sha.clone(),141 subject: c.subject.clone(),142 author: c.author.clone(),143 date: c.date.clone(),144 })145 .collect();146 let releases_json: Vec<ReleaseJson> = releases147 .iter()148 .map(|r| ReleaseJson {149 version: r.version.clone(),150 title: r.title.clone(),151 created: r.created.clone(),152 sha: r.sha.clone(),153 artifacts: r.artifacts.len(),154 body: r.body.clone(),155 })156 .collect();157 let wiki_json: Vec<WikiJson> = wiki158 .iter()159 .map(|w| WikiJson {160 path: w.path.clone(),161 title: w.title.clone(),162 size: w.size,163 })164 .collect();165 166 write_json(out, "api/issues.json", &issues)?;167 write_json(out, "api/pulls.json", &pulls)?;168 write_json(out, "api/commits.json", &commits_json)?;169 write_json(out, "api/releases.json", &releases_json)?;170 write_json(out, "api/wiki.json", &wiki_json)?;171 Ok(())172 }