fabro/lib/apps/fabro-server/src/server/handler/mod.rs
2026-08-17 13:56:19 -04:00

235 lines
8.4 KiB
Rust

use std::sync::Arc;
use axum::Router;
use axum::http::{HeaderMap, HeaderValue, header};
use axum::routing::{get, post};
use serde::Serialize;
use super::{ApiError, AppState, IntoResponse, Json, Response, StatusCode, demo};
mod artifacts;
mod automations;
mod billing;
mod completions;
mod environments;
pub(in crate::server) mod events;
pub(in crate::server) mod graph;
pub(in crate::server) mod lifecycle;
mod llm_sse;
mod mcp_servers;
mod models;
mod pair;
mod playground;
pub(in crate::server) mod pull_requests;
pub(in crate::server) mod runs;
mod sandbox;
mod sandboxes;
mod secrets;
mod sessions;
mod steer;
pub(in crate::server) mod system;
mod variables;
mod worker_control;
mod workflow_versions;
pub(super) use system::{health, openapi_spec};
async fn not_implemented() -> Response {
ApiError::new(StatusCode::NOT_IMPLEMENTED, "Not implemented.").into_response()
}
fn parse_required_if_match<R>(
headers: &HeaderMap,
resource: &str,
id: &impl std::fmt::Display,
) -> Result<R, ApiError>
where
R: std::str::FromStr,
R::Err: std::fmt::Display,
{
let Some(value) = headers.get(header::IF_MATCH) else {
return Err(ApiError::new(
StatusCode::PRECONDITION_REQUIRED,
format!("If-Match header is required for {resource}: {id}"),
));
};
let value = value
.to_str()
.map_err(|_| ApiError::bad_request("If-Match header must be visible ASCII"))?;
let value = unquote_etag(value.trim());
value.parse::<R>().map_err(|err| {
ApiError::bad_request(format!("invalid If-Match {resource} revision: {err}"))
})
}
fn unquote_etag(value: &str) -> &str {
value
.strip_prefix('"')
.and_then(|unquoted| unquoted.strip_suffix('"'))
.unwrap_or(value)
}
fn json_with_etag_response<T>(
status: StatusCode,
resource: &str,
revision: &impl std::fmt::Display,
body: T,
) -> Response
where
T: Serialize,
{
let etag = HeaderValue::from_str(&format!("\"{revision}\""))
.unwrap_or_else(|_| panic!("{resource} revisions are valid ETag header values"));
let mut response = (status, Json(body)).into_response();
response.headers_mut().insert(header::ETAG, etag);
response
}
pub(super) fn demo_routes() -> Router<Arc<AppState>> {
Router::new()
.route("/runs", get(demo::list_runs).post(demo::create_run_stub))
.route("/runs/resolve", get(demo::resolve_run))
.route("/attach", get(demo::attach_events_stub))
.route("/runs/{id}", get(demo::get_run_status))
.route("/runs/{id}/questions", get(demo::get_questions_stub))
.route("/runs/{id}/questions/{qid}/answer", post(demo::answer_stub))
.route("/runs/{id}/state", get(not_implemented))
.route("/runs/{id}/logs", get(not_implemented))
.route(
"/runs/{id}/events",
get(not_implemented).post(not_implemented),
)
.route("/runs/{id}/attach", get(demo::run_events_stub))
.route("/runs/{id}/blobs", post(not_implemented))
.route("/runs/{id}/blobs/{blobHash}", get(not_implemented))
.route(
"/runs/{id}/stages/{stageId}/logs/output",
get(not_implemented),
)
.route("/runs/{id}/checkpoint", get(demo::checkpoint_stub))
.route("/runs/{id}/cancel", post(demo::cancel_stub))
.route("/runs/{id}/start", post(demo::start_run_stub))
.route("/runs/{id}/approve", post(demo::start_run_stub))
.route("/runs/{id}/deny", post(demo::deny_run_stub))
.route("/runs/{id}/pause", post(demo::pause_stub))
.route("/runs/{id}/unpause", post(demo::unpause_stub))
.route("/runs/{id}/graph", get(demo::get_run_graph))
.route("/runs/{id}/graph/source", get(demo::get_run_graph_source))
.route("/runs/{id}/stages", get(demo::get_run_stages))
.route("/runs/{id}/artifacts", get(demo::list_run_artifacts_stub))
.route("/runs/{id}/artifacts/download", get(not_implemented))
.route("/runs/{id}/files", get(demo::list_run_files_stub))
.route("/runs/{id}/commits", get(demo::list_run_commits_stub))
.route(
"/runs/{id}/stages/{stageId}/events",
get(demo::get_stage_events),
)
.route(
"/runs/{id}/stages/{stageId}/context-window",
get(not_implemented),
)
.route(
"/runs/{id}/stages/{stageId}/artifacts",
get(not_implemented).post(not_implemented),
)
.route(
"/runs/{id}/stages/{stageId}/artifacts/download",
get(not_implemented),
)
.route("/runs/{id}/billing", get(demo::get_run_billing))
.route("/runs/{id}/settings", get(demo::get_run_settings))
.route("/runs/{id}/preview", post(demo::generate_preview_url_stub))
.route("/runs/{id}/ssh", post(demo::create_ssh_access_stub))
.route(
"/runs/{id}/sandbox/files",
get(demo::list_sandbox_files_stub),
)
.route(
"/runs/{id}/sandbox/services",
get(demo::list_sandbox_services_stub),
)
.route(
"/runs/{id}/sandbox/file",
get(demo::get_sandbox_file_stub).put(demo::put_sandbox_file_stub),
)
.route(
"/insights/queries",
get(demo::list_saved_queries).post(demo::save_query_stub),
)
.route(
"/insights/queries/{id}",
get(demo::get_saved_query)
.put(demo::update_query_stub)
.delete(demo::delete_query_stub),
)
.route("/insights/execute", post(demo::execute_query_stub))
.route("/insights/history", get(demo::list_query_history))
.route(
"/secrets",
get(demo::list_secrets)
.post(demo::create_secret)
.delete(demo::delete_secret_by_name),
)
.route("/repos/github/{owner}/{name}", get(demo::get_github_repo))
.route("/health/diagnostics", post(demo::run_diagnostics))
.route("/settings", get(demo::get_server_settings))
.route("/system/info", get(demo::get_system_info))
.route("/system/integrations", get(demo::get_system_integrations))
.route("/system/resources", get(demo::get_system_resources))
.route("/system/df", get(demo::get_system_disk_usage))
.route("/system/repair/runs", get(demo::get_system_repair_runs))
.route("/system/prune/runs", post(demo::prune_runs))
.route("/billing", get(demo::get_aggregate_billing))
.route("/workflows", get(demo::list_workflows))
.route("/workflows/{name}", get(demo::get_workflow))
.route("/workflows/{name}/runs", get(demo::list_workflow_runs))
.merge(runs::manifest_routes())
.merge(graph::manifest_routes())
.merge(models::routes())
.merge(completions::routes())
.merge(playground::routes())
}
pub(super) fn real_routes() -> Router<Arc<AppState>> {
Router::new()
.route("/runs/{id}/stages/{stageId}/turns", get(not_implemented))
.route("/workflows", get(not_implemented))
.route("/workflows/{name}", get(not_implemented))
.route("/workflows/{name}/runs", get(not_implemented))
.route(
"/insights/queries",
get(not_implemented).post(not_implemented),
)
.route(
"/insights/queries/{id}",
get(not_implemented)
.put(not_implemented)
.delete(not_implemented),
)
.route("/insights/execute", post(not_implemented))
.route("/insights/history", get(not_implemented))
.merge(runs::routes())
.merge(events::routes())
.merge(billing::routes())
.merge(pull_requests::routes())
.merge(artifacts::routes())
.merge(automations::routes())
.merge(mcp_servers::routes())
.merge(environments::routes())
.merge(sandbox::routes())
.merge(sandboxes::routes())
.merge(lifecycle::routes())
.merge(steer::routes())
.merge(pair::routes())
.merge(graph::manifest_routes())
.merge(graph::run_routes())
.merge(models::routes())
.merge(secrets::routes())
.merge(variables::routes())
.merge(worker_control::routes())
.merge(workflow_versions::routes())
.merge(sessions::routes())
.merge(system::routes())
.merge(completions::routes())
.merge(playground::routes())
}