Skip to main content

lex_api/
issues_http.rs

1//! Derived issue/project state over HTTP (#949 phase 3).
2//!
3//! A board is a *view*: `GET /v1/issues` returns every issue with its state
4//! computed from the op-log (open / in progress / verified / blocked),
5//! `GET /v1/issues/<id>` adds the recorded verdicts, and `GET /v1/projects`
6//! groups issues by project with per-state counts. Nothing here is stored or
7//! moved by hand — see `lex_store::issues` for the derivation rules — so the
8//! board cannot drift from the code. The hub delegates these per tenant; the
9//! browser console's owner-auth wrapping is lex-hub's concern.
10
11use std::collections::BTreeMap;
12use std::io::Cursor;
13use tiny_http::Response;
14
15use crate::handlers::{error_response, json_response, State};
16use lex_store::issues::{
17    all_issue_status, issue_status, issue_verdicts, issues_in_progress, IssueState, IssueStatus,
18};
19use lex_vcs::IssueLog;
20
21/// `GET /v1/issues` — every issue with its derived state.
22pub fn issues_state_handler(state: &State) -> Response<Cursor<Vec<u8>>> {
23    let store = state.store.lock().unwrap();
24    match all_issue_status(&store) {
25        Ok(issues) => json_response(200, &serde_json::json!({ "issues": issues })),
26        Err(e) => error_response(500, format!("deriving issue state: {e}")),
27    }
28}
29
30/// `GET /v1/issues/<id>` — one issue, its derived state, and every recorded
31/// `IssueVerified` verdict. 404 for an unknown id.
32pub fn issue_detail_handler(state: &State, id: &str) -> Response<Cursor<Vec<u8>>> {
33    let store = state.store.lock().unwrap();
34    let log = match IssueLog::open(store.root()) {
35        Ok(l) => l,
36        Err(e) => return error_response(500, format!("opening issue log: {e}")),
37    };
38    let issue = match log.get(&id.to_string()) {
39        Ok(Some(i)) => i,
40        Ok(None) => return error_response(404, format!("unknown issue `{id}`")),
41        Err(e) => return error_response(500, format!("reading issue {id}: {e}")),
42    };
43    let in_progress = match issues_in_progress(&store) {
44        Ok(s) => s,
45        Err(e) => return error_response(500, format!("scanning provenance: {e}")),
46    };
47    let status = match issue_status(&store, &issue, &in_progress) {
48        Ok(s) => s,
49        Err(e) => return error_response(500, format!("deriving state for {id}: {e}")),
50    };
51    let verdicts = match issue_verdicts(&store, id) {
52        Ok(v) => v,
53        Err(e) => return error_response(500, format!("reading verdicts for {id}: {e}")),
54    };
55    json_response(200, &serde_json::json!({
56        "issue": status.issue,
57        "state": status.state,
58        "blocked_on": status.blocked_on,
59        "has_work": status.has_work,
60        "verdicts": verdicts,
61    }))
62}
63
64/// `GET /v1/projects` — issues grouped by project (a project is a subgraph
65/// with a goal), each with per-state counts. Issues with no project appear
66/// in `/v1/issues` only.
67pub fn projects_handler(state: &State) -> Response<Cursor<Vec<u8>>> {
68    let store = state.store.lock().unwrap();
69    let all = match all_issue_status(&store) {
70        Ok(v) => v,
71        Err(e) => return error_response(500, format!("deriving issue state: {e}")),
72    };
73    let mut by_project: BTreeMap<String, Vec<&IssueStatus>> = BTreeMap::new();
74    for s in &all {
75        if let Some(p) = &s.issue.project {
76            by_project.entry(p.clone()).or_default().push(s);
77        }
78    }
79    let projects: Vec<serde_json::Value> = by_project
80        .into_iter()
81        .map(|(name, issues)| {
82            let count = |st: IssueState| issues.iter().filter(|s| s.state == st).count();
83            serde_json::json!({
84                "name": name,
85                "counts": {
86                    "open": count(IssueState::Open),
87                    "in_progress": count(IssueState::InProgress),
88                    "verified": count(IssueState::Verified),
89                    "blocked": count(IssueState::Blocked),
90                },
91                "issues": issues.iter().map(|s| serde_json::json!({
92                    "issue_id": s.issue.issue_id,
93                    "title": s.issue.title,
94                    "shape": s.issue.acceptance.shape(),
95                    "state": s.state,
96                    "blocked_on": s.blocked_on,
97                })).collect::<Vec<_>>(),
98            })
99        })
100        .collect();
101    json_response(200, &serde_json::json!({ "projects": projects }))
102}