Skip to main content

pidgin_server/
lib.rs

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}