use parse_rust_core::{ErrorCode, ErrorOrigin, ParseError};
use serde_json::{json, Value as Json};
use crate::auth::Authority;
use crate::params::Params;
use crate::request::RequestContext;
use crate::routes::dispatch::{self, RouteError};
use crate::state::AppState;
const BATCH_PATH: &str = "/batch";
pub async fn handle(
state: &AppState,
rc: &RequestContext,
authority: &Authority,
mount_path: &str,
body: Option<&Json>,
) -> Result<Json, ParseError> {
let Some(Json::Object(body)) = body else {
return Err(ParseError::invalid_json("requests must be an array"));
};
if matches!(body.get("transaction"), Some(Json::Bool(true))) {
return Err(ParseError::new(
ErrorCode::CommandUnavailable,
"Batch transactions are not supported yet. Retry without `transaction: true`; \
the sub-requests will be applied independently and reported per operation.",
));
}
let Some(Json::Array(requests)) = body.get("requests") else {
return Err(ParseError::invalid_json("requests must be an array"));
};
let limit = state.config().batch_request_limit;
if limit > -1 && !authority.is_privileged() && requests.len() as i64 > limit {
return Err(ParseError::invalid_json(format!(
"Batch request contains {} sub-requests, which exceeds the limit of {limit}.",
requests.len()
)));
}
let mut parsed = Vec::with_capacity(requests.len());
for request in requests {
let Json::Object(request) = request else {
return Err(ParseError::invalid_json(
"batch request path must be a string",
));
};
let Some(Json::String(path)) = request.get("path") else {
return Err(ParseError::invalid_json(
"batch request path must be a string",
));
};
let method = match request.get("method") {
Some(Json::String(m)) => m.to_uppercase(),
_ => "GET".to_string(),
};
let routable = routable_path(path, mount_path)?;
if method == "POST" && routable == BATCH_PATH {
return Err(ParseError::invalid_json(
"nested batch requests are not allowed",
));
}
parsed.push((method, routable, request.get("body").cloned()));
}
let mut results = Vec::with_capacity(parsed.len());
for (method, path, body) in parsed {
results.push(run_one(state, rc, authority, &method, &path, body.as_ref()).await);
}
Ok(Json::Array(results))
}
async fn run_one(
state: &AppState,
rc: &RequestContext,
authority: &Authority,
method: &str,
path: &str,
body: Option<&Json>,
) -> Json {
let Ok(method) = method.parse::<http::Method>() else {
return json!({ "error": {
"code": ErrorCode::InvalidJson.as_i32(),
"error": format!("cannot route {method} {path}"),
}});
};
let Some(route) = dispatch::route_of(path) else {
return json!({ "error": {
"code": ErrorCode::InvalidJson.as_i32(),
"error": format!("cannot route {method} {path}"),
}});
};
let params = if matches!(method, http::Method::GET | http::Method::DELETE) {
Params::from_json(body)
} else {
Params::default()
};
let incoming = dispatch::Incoming {
method,
route,
path: path.to_string(),
params,
body: body.cloned(),
};
match dispatch::dispatch(state, rc, authority, &incoming).await {
Ok(response) => json!({ "success": response.body }),
Err(RouteError::Parse(e)) if e.origin == ErrorOrigin::Internal => {
json!({ "error": { "error": crate::response::INTERNAL_SERVER_ERROR_MESSAGE }})
}
Err(RouteError::Parse(e)) => json!({ "error": {
"code": e.code.as_i32(),
"error": e.message,
}}),
Err(RouteError::Http(e)) => json!({ "error": { "error": e.message }}),
Err(RouteError::NotFound { method, path }) => json!({ "error": {
"code": ErrorCode::InvalidJson.as_i32(),
"error": format!("cannot route {method} {path}"),
}}),
}
}
fn routable_path(path: &str, mount_path: &str) -> Result<String, ParseError> {
let prefix = mount_path.trim_end_matches('/');
let rest = if prefix.is_empty() {
Some(path)
} else {
path.strip_prefix(prefix)
};
let Some(rest) = rest else {
return Err(ParseError::invalid_json(format!(
"cannot route batch path {path}"
)));
};
let trimmed = rest.trim_matches('/');
if trimmed.is_empty() {
return Ok("/".to_string());
}
Ok(format!("/{trimmed}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_prefix_is_the_configured_mount_and_nothing_else() {
assert_eq!(
routable_path("/parse/classes/Post", "/parse").expect("routes"),
"/classes/Post"
);
assert_eq!(routable_path("/parse", "/parse").expect("routes"), "/");
assert_eq!(
routable_path("/classes/Post", "/").expect("routes"),
"/classes/Post"
);
}
#[test]
fn a_path_outside_the_prefix_is_refused_by_name() {
let e = routable_path("/other/classes/Post", "/parse").unwrap_err();
assert_eq!(e.code, ErrorCode::InvalidJson);
assert_eq!(e.message, "cannot route batch path /other/classes/Post");
}
#[test]
fn a_prefix_that_only_looks_like_the_mount_still_fails_to_route() {
let routable = routable_path("/parsexyz/classes/Post", "/parse").expect("prefix matches");
assert_eq!(routable, "/xyz/classes/Post");
assert!(dispatch::route_of(&routable).is_none());
}
}