Skip to main content

routa_server/api/
harness_templates.rs

1use axum::{
2    extract::{Query, State},
3    http::StatusCode,
4    routing::get,
5    Json, Router,
6};
7use routa_core::harness_template;
8use serde::Deserialize;
9use serde_json::Value;
10
11use crate::api::repo_context::{
12    json_error, resolve_repo_root, RepoContextQuery, ResolveRepoRootOptions,
13};
14use crate::error::ServerError;
15use crate::state::AppState;
16
17pub fn router() -> Router<AppState> {
18    Router::new()
19        .route("/", get(get_template_list))
20        .route("/validate", get(get_template_validate))
21        .route("/doctor", get(get_template_doctor))
22}
23
24#[derive(Debug, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct TemplateValidateQuery {
27    #[serde(flatten)]
28    pub context: RepoContextQuery,
29    pub template_id: Option<String>,
30}
31
32async fn get_template_list(
33    State(state): State<AppState>,
34    Query(query): Query<RepoContextQuery>,
35) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
36    let repo_root = resolve_repo_root(
37        &state,
38        query.workspace_id.as_deref(),
39        query.codebase_id.as_deref(),
40        query.repo_path.as_deref(),
41        "缺少 harness 上下文,请提供 workspaceId / codebaseId / repoPath 之一",
42        ResolveRepoRootOptions::default(),
43    )
44    .await
45    .map_err(map_context_error)?;
46
47    let report =
48        harness_template::list_templates(&repo_root).map_err(map_domain_error("模板列表"))?;
49
50    to_json_response(report, "模板列表")
51}
52
53async fn get_template_validate(
54    State(state): State<AppState>,
55    Query(query): Query<TemplateValidateQuery>,
56) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
57    let repo_root = resolve_repo_root(
58        &state,
59        query.context.workspace_id.as_deref(),
60        query.context.codebase_id.as_deref(),
61        query.context.repo_path.as_deref(),
62        "缺少 harness 上下文,请提供 workspaceId / codebaseId / repoPath 之一",
63        ResolveRepoRootOptions::default(),
64    )
65    .await
66    .map_err(map_context_error)?;
67
68    let template_id = query.template_id.as_deref().ok_or_else(|| {
69        (
70            StatusCode::BAD_REQUEST,
71            Json(json_error(
72                "缺少 templateId 参数",
73                "templateId is required".to_string(),
74            )),
75        )
76    })?;
77
78    let report = harness_template::validate_template(&repo_root, template_id)
79        .map_err(map_domain_error("模板验证"))?;
80
81    to_json_response(report, "模板验证")
82}
83
84async fn get_template_doctor(
85    State(state): State<AppState>,
86    Query(query): Query<RepoContextQuery>,
87) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
88    let repo_root = resolve_repo_root(
89        &state,
90        query.workspace_id.as_deref(),
91        query.codebase_id.as_deref(),
92        query.repo_path.as_deref(),
93        "缺少 harness 上下文,请提供 workspaceId / codebaseId / repoPath 之一",
94        ResolveRepoRootOptions::default(),
95    )
96    .await
97    .map_err(map_context_error)?;
98
99    let report = harness_template::doctor(&repo_root).map_err(map_domain_error("模板检查"))?;
100
101    to_json_response(report, "模板检查")
102}
103
104fn to_json_response<T: serde::Serialize>(
105    value: T,
106    label: &str,
107) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
108    Ok(Json(serde_json::to_value(value).map_err(|error| {
109        (
110            StatusCode::INTERNAL_SERVER_ERROR,
111            Json(json_error(&format!("序列化{label}失败"), error.to_string())),
112        )
113    })?))
114}
115
116fn map_context_error(error: ServerError) -> (StatusCode, Json<Value>) {
117    match error {
118        ServerError::BadRequest(details) => (
119            StatusCode::BAD_REQUEST,
120            Json(json_error("Harness template 上下文无效", details)),
121        ),
122        other => (
123            StatusCode::INTERNAL_SERVER_ERROR,
124            Json(json_error("读取 Harness template 失败", other.to_string())),
125        ),
126    }
127}
128
129fn map_domain_error(label: &'static str) -> impl Fn(String) -> (StatusCode, Json<Value>) + Clone {
130    move |error| {
131        (
132            StatusCode::INTERNAL_SERVER_ERROR,
133            Json(json_error(&format!("读取 Harness {label}失败"), error)),
134        )
135    }
136}