routa_server/api/
harness_templates.rs1use 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}