1use std::net::SocketAddr;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use axum::Router;
6use axum::extract::{Json, State};
7use axum::http::StatusCode;
8use axum::routing::{get, post};
9use serde::Serialize;
10use tokio::net::TcpListener;
11
12use pidgin_lang::ast::{FieldValue, PgnPacket};
13use pidgin_lang::expander::expand_to_run_packet;
14use pidgin_lang::parser::parse_packet;
15use pidgin_lang::registry::{
16 ActionRegistry, SafetyRules, WorkflowRegistry, load_action_registry, load_safety_rules,
17 load_workflow_registry,
18};
19use pidgin_lang::resolver::{
20 ReferenceAliases, ResolutionStatus, ResolvedRef, ResolverContext, load_aliases, resolve_all,
21};
22use pidgin_lang::router::{explain_route, route};
23use pidgin_lang::safety::{SafetyResult, check_resolved_refs_safety, check_safety};
24use pidgin_lang::validator::schema::validate_schema;
25use pidgin_lang::validator::syntax::validate_syntax;
26
27pub struct AppState {
28 pub host_root: PathBuf,
29 pub workflows: WorkflowRegistry,
30 pub actions: ActionRegistry,
31 pub safety_rules: SafetyRules,
32 pub aliases: ReferenceAliases,
33}
34
35impl AppState {
36 pub fn load(host: &Path) -> Result<Self, ServerError> {
37 let config_dir = host.join(".pidgin");
38 Ok(Self {
39 host_root: host
40 .canonicalize()
41 .map_err(|e| ServerError::Config(format!("cannot canonicalize host: {}", e)))?,
42 workflows: load_workflow_registry(&config_dir.join("WORKFLOW_REGISTRY.yaml"))
43 .map_err(|e| ServerError::Config(format!("workflow registry: {}", e)))?,
44 actions: load_action_registry(&config_dir.join("ACTION_REGISTRY.yaml"))
45 .map_err(|e| ServerError::Config(format!("action registry: {}", e)))?,
46 safety_rules: load_safety_rules(&config_dir.join("SAFETY_RULES.yaml"))
47 .map_err(|e| ServerError::Config(format!("safety rules: {}", e)))?,
48 aliases: load_aliases(&config_dir.join("REFERENCE_ALIASES.yaml"))
49 .map_err(|e| ServerError::Config(format!("reference aliases: {}", e)))?,
50 })
51 }
52}
53
54#[derive(Debug, thiserror::Error)]
55pub enum ServerError {
56 #[error("configuration error: {0}")]
57 Config(String),
58 #[error("internal error: {0}")]
59 Internal(String),
60}
61
62#[derive(Debug, Serialize)]
63pub struct ApiResponse<T: Serialize> {
64 pub ok: bool,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub data: Option<T>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub error: Option<String>,
69}
70
71impl<T: Serialize> ApiResponse<T> {
72 pub fn success(data: T) -> Self {
73 Self {
74 ok: true,
75 data: Some(data),
76 error: None,
77 }
78 }
79
80 pub fn error(msg: String) -> Self {
81 Self {
82 ok: false,
83 data: None,
84 error: Some(msg),
85 }
86 }
87}
88
89#[derive(Debug, Serialize)]
90pub struct CheckData {
91 pub passed: bool,
92 pub validation_errors: Vec<String>,
93 pub safety_blocked: bool,
94 pub safety_rules: Vec<String>,
95 pub resolution: Vec<serde_json::Value>,
96}
97
98#[derive(Debug, Serialize)]
99pub struct ExpandedData {
100 pub yaml: String,
101 pub route: String,
102}
103
104#[derive(Debug, Serialize)]
105pub struct RunData {
106 pub yaml: String,
107 pub route: String,
108 pub resolution: Vec<serde_json::Value>,
109}
110
111struct CheckedPacket {
112 packet: PgnPacket,
113 safety: SafetyResult,
114 safety_rules: Vec<String>,
115 validation_errors: Vec<String>,
116 resolved: Vec<ResolvedRef>,
117}
118
119type ApiResult<T> = Result<Json<ApiResponse<T>>, (StatusCode, Json<ApiResponse<T>>)>;
120
121fn api_error<T: Serialize>(
122 status: StatusCode,
123 msg: impl Into<String>,
124) -> (StatusCode, Json<ApiResponse<T>>) {
125 (status, Json(ApiResponse::error(msg.into())))
126}
127
128fn fields_json(packet: &PgnPacket) -> serde_json::Value {
129 packet
130 .fields
131 .iter()
132 .map(|(k, v)| {
133 let val = match v {
134 FieldValue::Scalar(s) => serde_json::Value::String(s.clone()),
135 FieldValue::List(items) => serde_json::Value::Array(
136 items
137 .iter()
138 .cloned()
139 .map(serde_json::Value::String)
140 .collect(),
141 ),
142 };
143 (k.clone(), val)
144 })
145 .collect()
146}
147
148fn required_inputs(packet: &PgnPacket, state: &AppState) -> Vec<String> {
149 packet
150 .fields
151 .get("wf")
152 .and_then(|v| match v {
153 FieldValue::Scalar(s) => state.workflows.workflows.get(s),
154 _ => None,
155 })
156 .map(|w| w.required_inputs.clone())
157 .unwrap_or_default()
158}
159
160fn resolve_packet(packet: &PgnPacket, state: &AppState) -> Vec<ResolvedRef> {
161 let ctx = ResolverContext {
162 host_root: state.host_root.clone(),
163 aliases: state.aliases.clone(),
164 required_inputs: required_inputs(packet, state),
165 };
166 resolve_all(packet, &ctx)
167}
168
169fn resolution_json(resolved: &[ResolvedRef]) -> Vec<serde_json::Value> {
170 resolved
171 .iter()
172 .map(|r| {
173 serde_json::json!({
174 "original": r.original,
175 "namespace": r.namespace,
176 "ref_id": r.ref_id,
177 "status": match r.status {
178 ResolutionStatus::Resolved => "RESOLVED",
179 ResolutionStatus::Missing => "MISSING",
180 ResolutionStatus::Unresolved => "UNRESOLVED",
181 ResolutionStatus::Forbidden => "FORBIDDEN",
182 },
183 "required": r.required,
184 "path": r.resolved_path.as_ref().map(|p| p.display().to_string()),
185 })
186 })
187 .collect()
188}
189
190fn parse_and_check(state: &AppState, body: &str) -> Result<CheckedPacket, (StatusCode, String)> {
191 let packet =
192 parse_packet(body).map_err(|e| (StatusCode::BAD_REQUEST, format!("parse error: {}", e)))?;
193
194 let mut errors = validate_syntax(&packet);
195 errors.extend(validate_schema(&packet, &state.workflows));
196 let validation_errors: Vec<String> = errors
197 .iter()
198 .map(|e| format!("[{}] {}", e.code, e.message))
199 .collect();
200
201 let safety = check_safety(
202 &packet,
203 &state.actions,
204 &state.safety_rules,
205 &state.workflows,
206 );
207 let safety_rules = safety.fired_rules.iter().map(|r| r.to_string()).collect();
208 let resolved = resolve_packet(&packet, state);
209 let resolved_fired = check_resolved_refs_safety(&resolved, &state.safety_rules.private_paths);
210 if !resolved_fired.is_empty() {
211 let rules = resolved_fired
212 .iter()
213 .map(|r| r.to_string())
214 .collect::<Vec<_>>();
215 return Err((
216 StatusCode::FORBIDDEN,
217 format!("safety after resolution: {}", rules.join(", ")),
218 ));
219 }
220
221 Ok(CheckedPacket {
222 packet,
223 safety,
224 safety_rules,
225 validation_errors,
226 resolved,
227 })
228}
229
230async fn health(State(state): State<Arc<AppState>>) -> Json<ApiResponse<serde_json::Value>> {
231 Json(ApiResponse::success(serde_json::json!({
232 "status": "ok",
233 "version": env!("CARGO_PKG_VERSION"),
234 "host": state.host_root.display().to_string(),
235 })))
236}
237
238async fn parse_handler(
239 State(_state): State<Arc<AppState>>,
240 body: String,
241) -> ApiResult<serde_json::Value> {
242 let packet = parse_packet(&body)
243 .map_err(|e| api_error(StatusCode::BAD_REQUEST, format!("parse error: {}", e)))?;
244
245 Ok(Json(ApiResponse::success(serde_json::json!({
246 "run_id": packet.run_id,
247 "directive": packet.directive.directive_name(),
248 "field_count": packet.fields.len(),
249 "fields": fields_json(&packet),
250 }))))
251}
252
253async fn check_handler(State(state): State<Arc<AppState>>, body: String) -> ApiResult<CheckData> {
254 let checked = parse_and_check(&state, &body).map_err(|(status, msg)| api_error(status, msg))?;
255
256 Ok(Json(ApiResponse::success(CheckData {
257 passed: checked.validation_errors.is_empty() && !checked.safety.blocked,
258 validation_errors: checked.validation_errors,
259 safety_blocked: checked.safety.blocked,
260 safety_rules: checked.safety_rules,
261 resolution: resolution_json(&checked.resolved),
262 })))
263}
264
265async fn expand_handler(
266 State(state): State<Arc<AppState>>,
267 body: String,
268) -> ApiResult<ExpandedData> {
269 let checked = parse_and_check(&state, &body).map_err(|(status, msg)| api_error(status, msg))?;
270 if !checked.validation_errors.is_empty() {
271 return Err(api_error(
272 StatusCode::BAD_REQUEST,
273 format!("validation: {}", checked.validation_errors.join("; ")),
274 ));
275 }
276
277 let decision = route(&checked.packet, &state.workflows, &checked.safety);
278 let expanded = expand_to_run_packet(
279 &checked.packet,
280 &checked.resolved,
281 &checked.safety,
282 &state.workflows,
283 );
284 let yaml = serde_yaml::to_string(&expanded).map_err(|e| {
285 api_error(
286 StatusCode::INTERNAL_SERVER_ERROR,
287 format!("serialization: {}", e),
288 )
289 })?;
290
291 Ok(Json(ApiResponse::success(ExpandedData {
292 yaml,
293 route: explain_route(&decision),
294 })))
295}
296
297async fn run_handler(State(state): State<Arc<AppState>>, body: String) -> ApiResult<RunData> {
298 let checked = parse_and_check(&state, &body).map_err(|(status, msg)| api_error(status, msg))?;
299 if !checked.validation_errors.is_empty() {
300 return Err(api_error(
301 StatusCode::BAD_REQUEST,
302 format!("validation: {}", checked.validation_errors.join("; ")),
303 ));
304 }
305 if checked.safety.blocked {
306 return Err(api_error(
307 StatusCode::FORBIDDEN,
308 format!("safety blocked: {}", checked.safety_rules.join(", ")),
309 ));
310 }
311
312 let decision = route(&checked.packet, &state.workflows, &checked.safety);
313 let expanded = expand_to_run_packet(
314 &checked.packet,
315 &checked.resolved,
316 &checked.safety,
317 &state.workflows,
318 );
319 let yaml = serde_yaml::to_string(&expanded).map_err(|e| {
320 api_error(
321 StatusCode::INTERNAL_SERVER_ERROR,
322 format!("serialization: {}", e),
323 )
324 })?;
325
326 Ok(Json(ApiResponse::success(RunData {
327 yaml,
328 route: explain_route(&decision),
329 resolution: resolution_json(&checked.resolved),
330 })))
331}
332
333pub fn build_router(state: Arc<AppState>) -> Router {
334 Router::new()
335 .route("/api/v1/health", get(health))
336 .route("/api/v1/parse", post(parse_handler))
337 .route("/api/v1/check", post(check_handler))
338 .route("/api/v1/expand", post(expand_handler))
339 .route("/api/v1/run", post(run_handler))
340 .with_state(state)
341}
342
343pub async fn serve(bind: SocketAddr, host: PathBuf) -> Result<(), ServerError> {
344 let app = build_router(Arc::new(AppState::load(&host)?));
345 let listener = TcpListener::bind(bind)
346 .await
347 .map_err(|e| ServerError::Internal(format!("bind error: {}", e)))?;
348
349 axum::serve(listener, app)
350 .await
351 .map_err(|e| ServerError::Internal(format!("server error: {}", e)))
352}