Skip to main content

haystack_server/ops/
invoke.rs

1//! The `invokeAction` op — invoke an action on an entity.
2
3use axum::extract::State;
4use axum::http::HeaderMap;
5use axum::response::{IntoResponse, Response};
6
7use haystack_core::kinds::Kind;
8
9use crate::content;
10use crate::error::HaystackError;
11use crate::state::SharedState;
12
13/// POST /api/invokeAction
14pub async fn handle(
15    State(state): State<SharedState>,
16    headers: HeaderMap,
17    body: String,
18) -> Result<Response, HaystackError> {
19    let content_type = headers
20        .get("Content-Type")
21        .and_then(|v| v.to_str().ok())
22        .unwrap_or("");
23    let accept = headers
24        .get("Accept")
25        .and_then(|v| v.to_str().ok())
26        .unwrap_or("");
27
28    let request_grid = content::decode_request_grid(&body, content_type)
29        .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
30
31    let row = request_grid
32        .row(0)
33        .ok_or_else(|| HaystackError::bad_request("request grid has no rows"))?;
34
35    let ref_val = match row.get("id") {
36        Some(Kind::Ref(r)) => &r.val,
37        _ => {
38            return Err(HaystackError::bad_request(
39                "request row must have an 'id' column with a Ref value",
40            ));
41        }
42    };
43
44    let action = match row.get("action") {
45        Some(Kind::Str(s)) => s.as_str(),
46        _ => {
47            return Err(HaystackError::bad_request(
48                "request row must have an 'action' column with a Str value",
49            ));
50        }
51    };
52
53    // Resolve entity from the graph
54    let entity = state
55        .graph
56        .get(ref_val)
57        .ok_or_else(|| HaystackError::not_found(format!("entity not found: {ref_val}")))?;
58
59    // The remaining tags in the row serve as arguments
60    let args = row.clone();
61
62    log::info!("invokeAction: id={ref_val} action={action}");
63
64    // Dispatch to the action registry
65    let result_grid = state
66        .actions
67        .invoke(&entity, action, &args)
68        .map_err(HaystackError::bad_request)?;
69
70    let (encoded, ct) = content::encode_response_grid(&result_grid, accept)
71        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
72
73    Ok(([(axum::http::header::CONTENT_TYPE, ct)], encoded).into_response())
74}