jacquardSnapshot

← snapshot

4792 bytes
//! Route table and shared handler helpers.

use std::collections::BTreeMap;

use axum::Router;
use axum::routing::{get, post};
use serde_json::json;

use crate::error::ApiError;
use crate::state::{AppState, RepoEntry};

pub(crate) mod commits;
pub(crate) mod decisions;
pub(crate) mod import;
pub(crate) mod promote;
pub(crate) mod remarks;
pub(crate) mod rendezvous;
pub(crate) mod repos;
pub(crate) mod snapshots;
pub(crate) mod telemetry;
pub(crate) mod transcribe;

/// Assembles every route under `/api`.
pub(crate) fn router() -> Router<crate::SharedState> {
    Router::new()
        .route("/api/health", get(health))
        .route("/api/repos", post(repos::init).get(repos::list))
        .route("/api/repos/{slug}", get(repos::detail))
        .route("/api/repos/{slug}/identities", post(repos::add_identity))
        .route(
            "/api/repos/{slug}/refs",
            get(repos::list_refs).post(repos::branch),
        )
        .route("/api/repos/{slug}/log", get(snapshots::log))
        .route("/api/repos/{slug}/snapshots/{hex}", get(snapshots::detail))
        .route(
            "/api/repos/{slug}/snapshots/{hex}/tree",
            get(snapshots::tree),
        )
        .route("/api/repos/{slug}/blobs/{hex}", get(snapshots::blob))
        .route("/api/repos/{slug}/diff", get(snapshots::diff))
        .route("/api/import/github", post(import::github))
        .route("/api/repos/{slug}/commits", post(commits::create))
        .route(
            "/api/repos/{slug}/decisions",
            get(decisions::list).post(decisions::propose),
        )
        .route("/api/repos/{slug}/decisions/{hex}", get(decisions::detail))
        .route(
            "/api/repos/{slug}/decisions/{hex}/attest",
            post(decisions::attest),
        )
        .route(
            "/api/repos/{slug}/remarks",
            get(remarks::list).post(remarks::raise),
        )
        .route(
            "/api/repos/{slug}/remarks/{id}/settle",
            post(remarks::settle),
        )
        .route(
            "/api/transcribe",
            get(transcribe::available)
                .post(transcribe::run)
                // Audio is far larger than any JSON this surface accepts, so
                // this one route opts out of the default 2 MiB body cap.
                .layer(axum::extract::DefaultBodyLimit::max(
                    transcribe::MAX_AUDIO_BYTES,
                )),
        )
        .route("/api/repos/{slug}/promote/preview", get(promote::preview))
        .route("/api/repos/{slug}/promote", post(promote::run))
        .route(
            "/api/repos/{slug}/sketches/snapshot/{hex}",
            get(rendezvous::sketch_snapshot),
        )
        .route(
            "/api/repos/{slug}/sketches/decision/{hex}",
            get(rendezvous::sketch_decision),
        )
        .route(
            "/api/repos/{slug}/rendezvous/publish",
            post(rendezvous::publish),
        )
        .route(
            "/api/repos/{slug}/rendezvous/find-similar",
            post(rendezvous::find_similar),
        )
        .route(
            "/api/rendezvous/publications",
            get(rendezvous::publications),
        )
        .route(
            "/api/rendezvous/introductions",
            get(rendezvous::introductions).post(rendezvous::broker),
        )
        .route("/api/demo", post(crate::demo::seed_route))
        .route("/api/telemetry/web-vitals", post(telemetry::web_vitals))
}

async fn health() -> axum::Json<serde_json::Value> {
    axum::Json(json!({ "ok": true }))
}

/// Runs `f` with read access to the named repo.
pub(crate) fn with_repo<T>(
    state: &AppState,
    slug: &str,
    f: impl FnOnce(&RepoEntry) -> Result<T, ApiError>,
) -> Result<T, ApiError> {
    let repos = read_repos(state)?;
    let entry = repos
        .get(slug)
        .ok_or_else(|| ApiError::not_found(format!("no repo named `{slug}`")))?;
    f(entry)
}

/// Runs `f` with write access to the named repo.
pub(crate) fn with_repo_mut<T>(
    state: &AppState,
    slug: &str,
    f: impl FnOnce(&mut RepoEntry) -> Result<T, ApiError>,
) -> Result<T, ApiError> {
    let mut repos = write_repos(state)?;
    let entry = repos
        .get_mut(slug)
        .ok_or_else(|| ApiError::not_found(format!("no repo named `{slug}`")))?;
    f(entry)
}

pub(crate) fn read_repos(
    state: &AppState,
) -> Result<std::sync::RwLockReadGuard<'_, BTreeMap<String, RepoEntry>>, ApiError> {
    state
        .repos
        .read()
        .map_err(|_| ApiError::internal("state lock poisoned"))
}

pub(crate) fn write_repos(
    state: &AppState,
) -> Result<std::sync::RwLockWriteGuard<'_, BTreeMap<String, RepoEntry>>, ApiError> {
    state
        .repos
        .write()
        .map_err(|_| ApiError::internal("state lock poisoned"))
}