use apiplant_abi::HttpMethod;
use ntex::http::Method;
use ntex::util::Bytes;
use ntex::web::types::{Path, State};
use ntex::web::{HttpRequest, HttpResponse};
use serde_json::json;
use crate::functions::HostBridge;
use crate::response::error;
use crate::sse;
use crate::state::AppState;
fn expected_method(m: HttpMethod) -> Method {
match m {
HttpMethod::Get => Method::GET,
HttpMethod::Post => Method::POST,
HttpMethod::Put => Method::PUT,
HttpMethod::Delete => Method::DELETE,
}
}
async fn admit(
req: &HttpRequest,
state: &State<AppState>,
name: &str,
body: String,
) -> Result<Ready, HttpResponse> {
let (method, access, config_json) = match state.functions.get(name) {
Some(f) => (
f.manifest.method,
f.manifest.access(),
f.config_json.clone(),
),
None => return Err(error(404, format!("unknown function `{name}`"))),
};
if req.method() != expected_method(method) {
return Err(error(405, "method not allowed"));
}
let principal = crate::access::check(state, req, &access, "unknown function").await?;
Ok(Ready {
input: match body.trim().is_empty() {
true => "{}".to_string(),
false => body,
},
config_json,
principal_id: principal
.as_ref()
.map(|p| p.user_id.to_string())
.unwrap_or_default(),
})
}
struct Ready {
input: String,
config_json: String,
principal_id: String,
}
fn bridge(state: &State<AppState>, ready: &Ready) -> HostBridge {
HostBridge::new(
state.db.clone(),
tokio::runtime::Handle::current(),
ready.config_json.clone(),
ready.principal_id.clone(),
)
.with_services(
state.mailer.clone(),
state.cache.clone(),
state.payments.clone(),
state.ai.clone(),
)
}
pub async fn invoke(
req: HttpRequest,
state: State<AppState>,
path: Path<String>,
body: String,
) -> HttpResponse {
let name = path.into_inner();
let ready = match admit(&req, &state, &name, body).await {
Ok(ready) => ready,
Err(response) => return response,
};
let functions = state.functions.clone();
let name2 = name.clone();
let bridge = bridge(&state, &ready);
let input = ready.input;
let result = tokio::task::spawn_blocking(move || {
let f = functions.get(&name2).expect("checked above");
f.invoke(bridge, &input)
})
.await;
match result {
Ok(Ok(json)) => HttpResponse::Ok()
.content_type("application/json")
.body(json),
Ok(Err(msg)) => match msg.strip_prefix(apiplant_abi::INTERNAL_ERROR_PREFIX) {
Some(detail) => {
tracing::error!(function = %name, detail, "function faulted");
error(500, "internal function error")
}
None => error(400, msg),
},
Err(_) => {
tracing::error!(function = %name, "function task panicked");
error(500, "internal function error")
}
}
}
pub async fn stream(
req: HttpRequest,
state: State<AppState>,
path: Path<String>,
body: String,
) -> HttpResponse {
let name = path.into_inner();
let ready = match admit(&req, &state, &name, body).await {
Ok(ready) => ready,
Err(response) => return response,
};
let (chunks, receiver) = tokio::sync::mpsc::unbounded_channel::<String>();
let functions = state.functions.clone();
let name2 = name.clone();
let bridge = bridge(&state, &ready).streaming(chunks);
let input = ready.input;
let finished = tokio::task::spawn_blocking(move || {
let f = functions.get(&name2).expect("checked above");
f.invoke(bridge, &input)
});
let deltas = futures_util::stream::unfold(receiver, |mut recv| async move {
recv.recv().await.map(|chunk| {
let frame: Result<Bytes, sse::Never> = Ok(sse::delta(&chunk));
(frame, recv)
})
});
let ending = futures_util::stream::once(async move {
let frame = match finished.await {
Ok(Ok(output)) => {
let result =
serde_json::from_str(&output).unwrap_or(serde_json::Value::String(output));
sse::done(&json!({ "result": result }))
}
Ok(Err(msg)) => match msg.strip_prefix(apiplant_abi::INTERNAL_ERROR_PREFIX) {
Some(detail) => {
tracing::error!(function = %name, detail, "streaming function faulted");
sse::failure("internal function error")
}
None => sse::failure(&msg),
},
Err(_) => {
tracing::error!(function = %name, "streaming function task panicked");
sse::failure("internal function error")
}
};
Ok::<Bytes, sse::Never>(frame)
});
let mut response = HttpResponse::Ok();
sse::headers(&mut response);
response.streaming(Box::pin(futures_util::StreamExt::chain(deltas, ending)))
}