use std::collections::BTreeMap;
use std::path::Path;
use axum::{
extract::{Query, State},
http::StatusCode,
Json,
};
use serde::Deserialize;
use wsb::annotations::{ProjectId, Ref};
use super::sessions::ErrorResponse;
use crate::web::notes;
use crate::web::state::WebState;
type Refusal = (StatusCode, Json<ErrorResponse>);
fn refuse(status: StatusCode, error: impl std::fmt::Display) -> Refusal {
(status, Json(ErrorResponse { error: format!("{error:#}") }))
}
#[derive(Deserialize)]
pub struct PathQuery {
pub path: Option<String>,
}
pub async fn list_path_notes(
State(state): State<WebState>,
Query(query): Query<PathQuery>,
) -> Result<Json<serde_json::Value>, Refusal> {
let root = state.worktree_root.clone();
tokio::task::spawn_blocking(move || match query.path {
Some(path) => about(&root, &path),
None => counts(&root),
})
.await
.map_err(|e| refuse(StatusCode::INTERNAL_SERVER_ERROR, e))?
.map(Json)
}
fn about(root: &Path, path: &str) -> Result<serde_json::Value, Refusal> {
let mentioned_in =
notes::list_path_notes(root, path).map_err(|e| refuse(StatusCode::INTERNAL_SERVER_ERROR, e))?;
let mut problems = Vec::new();
let (about, linked) = match ProjectId::of_checkout(root) {
Ok(project) => {
let about: Ref = format!("path:{project}/{path}")
.parse()
.map_err(|e| refuse(StatusCode::BAD_REQUEST, e))?;
let index =
wsb::cli::annotations::index().map_err(|e| refuse(StatusCode::INTERNAL_SERVER_ERROR, e))?;
problems.extend(index.problems.iter().cloned());
let linked: Vec<_> =
wsb::cli::annotations::notes_about(&index, Some(&about)).into_iter().cloned().collect();
(Some(about.to_string()), linked)
}
Err(no_name) => {
problems.push(format!("{no_name:#}"));
(None, Vec::new())
}
};
Ok(serde_json::json!({
"about": about,
"notes": linked,
"mentioned_in": mentioned_in,
"problems": problems,
}))
}
fn counts(root: &Path) -> Result<serde_json::Value, Refusal> {
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for document in notes::list_all_notes(root).map_err(|e| refuse(StatusCode::INTERNAL_SERVER_ERROR, e))? {
for path in document.links {
*counts.entry(path).or_default() += 1;
}
}
let mut problems = Vec::new();
match ProjectId::of_checkout(root) {
Ok(project) => {
let index =
wsb::cli::annotations::index().map_err(|e| refuse(StatusCode::INTERNAL_SERVER_ERROR, e))?;
problems.extend(index.problems.iter().cloned());
for held in &index.links {
let ends = [(&held.link.from, &held.link.to), (&held.link.to, &held.link.from)];
for (here, there) in ends {
if let (Ref::Path { project: of, path }, Ref::Note { .. }) = (here, there) {
if *of == project {
*counts.entry(path.clone()).or_default() += 1;
}
}
}
}
}
Err(no_name) => problems.push(format!("{no_name:#}")),
}
Ok(serde_json::json!({ "counts": counts, "problems": problems }))
}
#[cfg(test)]
#[path = "path_notes_tests.rs"]
mod tests;