use crate::server::app::AppState;
use crate::server::body::parse_form;
use crate::server::errors::{error, method_not_allowed};
use axum::body::Bytes;
use axum::extract::{Path, State};
use axum::http::header::CACHE_CONTROL;
use axum::http::{HeaderValue, Method, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::{delete, get, post};
use axum::{Json, Router};
use nomoreide_core::config::BundleDef;
use nomoreide_core::service_graph::build_service_graph;
use nomoreide_daemon_client::protocol::ServiceRuntimeState;
use serde::Serialize;
use serde_json::json;
pub(crate) fn routes() -> Router<AppState> {
Router::new()
.route("/api/services/graph", get(graph).fallback(shadowed_graph))
.route(
"/api/services/:name/definition",
get(definition).fallback(method_not_allowed),
)
.route(
"/api/services/:name/project",
post(set_project).fallback(method_not_allowed),
)
.route("/api/bundles", post(register_bundle))
.route(
"/api/bundles/:name/restart",
post(restart_bundle).fallback(method_not_allowed),
)
.route(
"/api/services/:name",
delete(remove_service).fallback(method_not_allowed),
)
}
async fn shadowed_graph(state: State<AppState>, method: Method) -> Response {
shadow(state, method, "graph").await
}
pub(super) async fn shadowed_service_path(
state: State<AppState>,
method: Method,
uri: axum::http::Uri,
) -> Response {
let name = uri
.path()
.rsplit('/')
.next()
.unwrap_or_default()
.to_string();
shadow(state, method, &name).await
}
async fn shadow(state: State<AppState>, method: Method, name: &str) -> Response {
if method == Method::DELETE {
return remove_service(state, Path(name.to_string())).await;
}
method_not_allowed().await
}
#[derive(Serialize)]
struct ConfigEnvelope {
ok: bool,
config: serde_json::Value,
}
fn config_envelope(config: &nomoreide_core::config::Config) -> Response {
let value = serde_json::to_value(config.public_view()).unwrap_or_else(|_| json!({}));
Json(ConfigEnvelope {
ok: true,
config: value,
})
.into_response()
}
async fn graph(State(state): State<AppState>) -> Response {
let config = match state.config_store.load().await {
Ok(config) => config,
Err(_) => {
return error(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to load NoMoreIDE config.",
)
}
};
Json(json!({ "ok": true, "graph": build_service_graph(&config.services) })).into_response()
}
async fn definition(State(state): State<AppState>, Path(name): Path<String>) -> Response {
let config = match state.config_store.load().await {
Ok(config) => config,
Err(_) => {
return error(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to load NoMoreIDE config.",
)
}
};
let Some(service) = config.services.iter().find(|s| s.name == name) else {
return error(
StatusCode::NOT_FOUND,
&format!("Service \"{name}\" is not registered."),
);
};
let mut response = Json(json!({ "ok": true, "service": service })).into_response();
response
.headers_mut()
.insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
response
}
async fn set_project(
State(state): State<AppState>,
Path(name): Path<String>,
body: Bytes,
) -> Response {
let form = parse_form(&body);
match state
.config_store
.set_service_project(&name, form.get("projectPath").map(String::as_str))
.await
{
Ok(config) => config_envelope(&config),
Err(reason) => error(StatusCode::NOT_FOUND, &reason.to_string()),
}
}
async fn register_bundle(State(state): State<AppState>, body: Bytes) -> Response {
let form = parse_form(&body);
let Some(name) = form
.get("name")
.map(|value| value.trim())
.filter(|value| !value.is_empty())
else {
return error(StatusCode::INTERNAL_SERVER_ERROR, "name is required");
};
let services: Vec<String> = form
.get("services")
.map(String::as_str)
.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.collect();
let previous = form
.get("originalName")
.map(|value| value.trim())
.filter(|value| !value.is_empty());
match state
.config_store
.register_bundle(
BundleDef {
name: name.to_string(),
services,
},
previous,
)
.await
{
Ok(config) => config_envelope(&config),
Err(reason) => error(StatusCode::INTERNAL_SERVER_ERROR, &reason.to_string()),
}
}
async fn restart_bundle(State(state): State<AppState>, Path(name): Path<String>) -> Response {
match state.runtime.restart_bundle(&name).await {
Ok(statuses) => Json(json!({ "ok": true, "statuses": statuses })).into_response(),
Err(reason) => crate::server::errors::mutation_error(reason),
}
}
async fn remove_service(State(state): State<AppState>, Path(name): Path<String>) -> Response {
let running = state
.runtime
.status()
.into_iter()
.find(|entry| entry.name == name)
.is_some_and(|entry| {
matches!(
entry.state,
ServiceRuntimeState::Running | ServiceRuntimeState::Starting
)
});
if running {
return error(
StatusCode::CONFLICT,
&format!("Stop \"{name}\" before deleting it."),
);
}
match state.config_store.remove_service(&name).await {
Ok(config) => config_envelope(&config),
Err(reason) => error(StatusCode::BAD_REQUEST, &reason.to_string()),
}
}