src/api.rs 5.2 KiB 172 lines Raw
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 }