routa-server 0.15.0

Routa.js HTTP Server — axum adapter on top of routa-core
Documentation
use axum::{
    extract::{Query, State},
    http::StatusCode,
    routing::get,
    Json, Router,
};
use routa_core::harness_template;
use serde::Deserialize;
use serde_json::Value;

use crate::api::repo_context::{
    json_error, resolve_repo_root, RepoContextQuery, ResolveRepoRootOptions,
};
use crate::error::ServerError;
use crate::state::AppState;

pub fn router() -> Router<AppState> {
    Router::new()
        .route("/", get(get_template_list))
        .route("/validate", get(get_template_validate))
        .route("/doctor", get(get_template_doctor))
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TemplateValidateQuery {
    #[serde(flatten)]
    pub context: RepoContextQuery,
    pub template_id: Option<String>,
}

async fn get_template_list(
    State(state): State<AppState>,
    Query(query): Query<RepoContextQuery>,
) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
    let repo_root = resolve_repo_root(
        &state,
        query.workspace_id.as_deref(),
        query.codebase_id.as_deref(),
        query.repo_path.as_deref(),
        "缺少 harness 上下文,请提供 workspaceId / codebaseId / repoPath 之一",
        ResolveRepoRootOptions::default(),
    )
    .await
    .map_err(map_context_error)?;

    let report =
        harness_template::list_templates(&repo_root).map_err(map_domain_error("模板列表"))?;

    to_json_response(report, "模板列表")
}

async fn get_template_validate(
    State(state): State<AppState>,
    Query(query): Query<TemplateValidateQuery>,
) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
    let repo_root = resolve_repo_root(
        &state,
        query.context.workspace_id.as_deref(),
        query.context.codebase_id.as_deref(),
        query.context.repo_path.as_deref(),
        "缺少 harness 上下文,请提供 workspaceId / codebaseId / repoPath 之一",
        ResolveRepoRootOptions::default(),
    )
    .await
    .map_err(map_context_error)?;

    let template_id = query.template_id.as_deref().ok_or_else(|| {
        (
            StatusCode::BAD_REQUEST,
            Json(json_error(
                "缺少 templateId 参数",
                "templateId is required".to_string(),
            )),
        )
    })?;

    let report = harness_template::validate_template(&repo_root, template_id)
        .map_err(map_domain_error("模板验证"))?;

    to_json_response(report, "模板验证")
}

async fn get_template_doctor(
    State(state): State<AppState>,
    Query(query): Query<RepoContextQuery>,
) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
    let repo_root = resolve_repo_root(
        &state,
        query.workspace_id.as_deref(),
        query.codebase_id.as_deref(),
        query.repo_path.as_deref(),
        "缺少 harness 上下文,请提供 workspaceId / codebaseId / repoPath 之一",
        ResolveRepoRootOptions::default(),
    )
    .await
    .map_err(map_context_error)?;

    let report = harness_template::doctor(&repo_root).map_err(map_domain_error("模板检查"))?;

    to_json_response(report, "模板检查")
}

fn to_json_response<T: serde::Serialize>(
    value: T,
    label: &str,
) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
    Ok(Json(serde_json::to_value(value).map_err(|error| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(json_error(&format!("序列化{label}失败"), error.to_string())),
        )
    })?))
}

fn map_context_error(error: ServerError) -> (StatusCode, Json<Value>) {
    match error {
        ServerError::BadRequest(details) => (
            StatusCode::BAD_REQUEST,
            Json(json_error("Harness template 上下文无效", details)),
        ),
        other => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(json_error("读取 Harness template 失败", other.to_string())),
        ),
    }
}

fn map_domain_error(label: &'static str) -> impl Fn(String) -> (StatusCode, Json<Value>) + Clone {
    move |error| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(json_error(&format!("读取 Harness {label}失败"), error)),
        )
    }
}