use axum::extract::FromRequest;
use axum::extract::FromRequestParts;
use axum::extract::Request;
use axum::extract::rejection::{JsonRejection, QueryRejection};
use axum::http::request::Parts;
use crate::errors::OrionError;
pub struct OrionJson<T>(pub T);
pub struct OrionQuery<T>(pub T);
impl<T, S> FromRequestParts<S> for OrionQuery<T>
where
T: serde::de::DeserializeOwned,
S: Send + Sync,
{
type Rejection = OrionError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
match axum::extract::Query::<T>::from_request_parts(parts, state).await {
Ok(axum::extract::Query(value)) => Ok(OrionQuery(value)),
Err(rej) => Err(map_query_rejection(rej)),
}
}
}
fn map_query_rejection(rej: QueryRejection) -> OrionError {
OrionError::validation(format!("Invalid query string: {}", rej.body_text()))
}
impl<T, S> FromRequest<S> for OrionJson<T>
where
T: serde::de::DeserializeOwned,
S: Send + Sync,
{
type Rejection = OrionError;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
match axum::Json::<T>::from_request(req, state).await {
Ok(axum::Json(value)) => Ok(OrionJson(value)),
Err(rej) => Err(map_rejection(rej)),
}
}
}
pub struct OrionBody(pub axum::body::Bytes);
impl<S> FromRequest<S> for OrionBody
where
S: Send + Sync,
{
type Rejection = OrionError;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
match axum::body::Bytes::from_request(req, state).await {
Ok(bytes) => Ok(OrionBody(bytes)),
Err(_) => Err(OrionError::PayloadTooLarge(
"Request body exceeded `ingest.max_payload_size`".to_string(),
)),
}
}
}
fn map_rejection(rej: JsonRejection) -> OrionError {
match rej {
JsonRejection::JsonDataError(e) => {
let msg = e.body_text();
let path = extract_path_from_serde_message(&msg).unwrap_or_else(|| "body".to_string());
OrionError::invalid_field(path, "INVALID", msg)
}
JsonRejection::JsonSyntaxError(e) => {
OrionError::validation(format!("Invalid JSON: {}", e.body_text()))
}
JsonRejection::MissingJsonContentType(_) => OrionError::UnsupportedMediaType(
"Expected `content-type: application/json`".to_string(),
),
JsonRejection::BytesRejection(_) => OrionError::PayloadTooLarge(
"Request body exceeded the configured limit — see \
`ingest.max_payload_size` (data plane) or `server.max_admin_body_size` \
(admin plane)"
.to_string(),
),
other => OrionError::validation(other.body_text()),
}
}
fn extract_path_from_serde_message(msg: &str) -> Option<String> {
for marker in ["missing field `", "unknown field `", "for key `"] {
if let Some(rest) = msg.split_once(marker)
&& let Some((field, _)) = rest.1.split_once('`')
{
return Some(format!("body.{field}"));
}
}
None
}
pub struct PeerAddr(pub Option<std::net::SocketAddr>);
impl<S> FromRequestParts<S> for PeerAddr
where
S: Send + Sync,
{
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(PeerAddr(
parts
.extensions
.get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
.map(|ci| ci.0),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_path_missing_field() {
let msg = "missing field `name` at line 1 column 42";
assert_eq!(
extract_path_from_serde_message(msg),
Some("body.name".into())
);
}
#[test]
fn extract_path_unknown_field() {
let msg = "unknown field `extra`, expected one of `name`, `description`";
assert_eq!(
extract_path_from_serde_message(msg),
Some("body.extra".into())
);
}
#[test]
fn extract_path_unparseable_returns_none() {
let msg = "invalid type: string \"x\", expected u64";
assert_eq!(extract_path_from_serde_message(msg), None);
}
}