wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
//! The editor's code intelligence over HTTP: a page opens, edits and closes files and asks
//! about them; what the servers push arrives as `code` events on the worktree's event stream.
//!
//! Every answer names its kind of failure by status, because the page does something
//! different for each: 409 — the text moved on (`stale`: ask again) or the copies disagree
//! (`resync`: send the whole text); 400 — the file or feature is not served; 503 — the
//! server cannot answer now; 422 — it answered with an error.

use axum::Json;
use axum::extract::{Query, State};
use axum::http::StatusCode;
use serde::Deserialize;
use serde_json::{Value, json};

use crate::code::text::Change;
use crate::code::{AskError, Opened};
use crate::web::state::WebState;

type Refusal = (StatusCode, Json<Value>);

fn refusal(e: AskError) -> Refusal {
    let message = e.to_string();
    match e {
        AskError::Stale(_) => (StatusCode::CONFLICT, Json(json!({ "error": message, "stale": true }))),
        AskError::Resync(_) => (StatusCode::CONFLICT, Json(json!({ "error": message, "resync": true }))),
        AskError::NotServed(_) => (StatusCode::BAD_REQUEST, Json(json!({ "error": message }))),
        AskError::Unavailable(_) => (StatusCode::SERVICE_UNAVAILABLE, Json(json!({ "error": message }))),
        AskError::Failed(_) => (StatusCode::UNPROCESSABLE_ENTITY, Json(json!({ "error": message }))),
    }
}

fn failed(message: String) -> Refusal {
    (StatusCode::UNPROCESSABLE_ENTITY, Json(json!({ "error": message })))
}

/// GET /code/status — the `omni` in use (or why there is none) and every root's server.
pub async fn status(State(state): State<WebState>) -> Json<Value> {
    let omni = match state.code.omni() {
        Ok(o) => json!({ "available": o }),
        Err(why) => json!({ "unavailable": why, "message": why.to_string() }),
    };
    Json(json!({ "omni": omni, "roots": state.code.status().await }))
}

#[derive(Deserialize)]
pub struct Open {
    client: String,
    path: String,
    version: i64,
    text: String,
}

/// POST /code/open — a page opened a file: whether it is served, by which root, with what.
pub async fn open(State(state): State<WebState>, Json(b): Json<Open>) -> Result<Json<Opened>, Refusal> {
    state.code.open(&b.client, &b.path, b.version, &b.text).await.map(Json).map_err(failed)
}

#[derive(Deserialize)]
pub struct Edit {
    client: String,
    path: String,
    from: i64,
    to: i64,
    changes: Vec<Change>,
}

/// POST /code/change — a page edited a file it has open.
pub async fn change(State(state): State<WebState>, Json(b): Json<Edit>) -> Result<Json<Value>, Refusal> {
    state.code.change(&b.client, &b.path, b.from, b.to, &b.changes).await.map_err(refusal)?;
    Ok(Json(json!({})))
}

#[derive(Deserialize)]
pub struct Close {
    client: String,
    path: String,
}

/// POST /code/close — a page closed a file.
pub async fn close(State(state): State<WebState>, Json(b): Json<Close>) -> Json<Value> {
    state.code.close(&b.client, &b.path).await;
    Json(json!({}))
}

#[derive(Deserialize)]
pub struct Ask {
    client: String,
    path: String,
    version: i64,
    /// One of the features the file's language is served with.
    feature: String,
    /// The protocol parameters beyond the document (`position`, `range`, `context`).
    #[serde(default = "empty")]
    params: Value,
}

fn empty() -> Value {
    json!({})
}

/// POST /code/ask — what the server says about a file, as the asking page has it.
pub async fn ask(State(state): State<WebState>, Json(b): Json<Ask>) -> Result<Json<Value>, Refusal> {
    if !b.params.is_object() {
        return Err((StatusCode::BAD_REQUEST, Json(json!({ "error": "params must be an object" }))));
    }
    let result = state.code.ask(&b.client, &b.path, b.version, &b.feature, b.params).await.map_err(refusal)?;
    Ok(Json(json!({ "result": result })))
}

#[derive(Deserialize)]
pub struct Restart {
    root: String,
}

/// POST /code/restart — start a root's server again.
pub async fn restart(State(state): State<WebState>, Json(b): Json<Restart>) -> Result<Json<Value>, Refusal> {
    state.code.restart(&b.root).await.map_err(|e| (StatusCode::SERVICE_UNAVAILABLE, Json(json!({ "error": e }))))?;
    Ok(Json(json!({})))
}

/// GET /code/diagnostics — every diagnostic the editor holds, as the events that carry them.
pub async fn diagnostics(State(state): State<WebState>) -> Json<Value> {
    Json(json!({ "events": state.code.diagnostics().await }))
}

#[derive(Deserialize)]
pub struct External {
    token: String,
}

/// GET /code/external?token= — a file outside the worktree a server named, to read.
pub async fn external(State(state): State<WebState>, Query(q): Query<External>) -> Result<Json<Value>, Refusal> {
    let (path, text) =
        state.code.external(&q.token).await.map_err(|e| (StatusCode::NOT_FOUND, Json(json!({ "error": e }))))?;
    Ok(Json(json!({ "path": path, "text": text })))
}