use axum::extract::{FromRequestParts, Query};
use axum::http::request::Parts;
use modkit_odata::{CursorV1, Error as ODataError, ODataOrderBy, OrderKey, SortDir};
use serde::Deserialize;
pub use modkit_odata::ODataQuery;
pub mod error;
pub use error::odata_error_to_problem;
#[derive(Deserialize, Default)]
pub struct ODataParams {
#[serde(rename = "$filter")]
pub filter: Option<String>,
#[serde(rename = "$orderby")]
pub orderby: Option<String>,
#[serde(rename = "$select")]
pub select: Option<String>,
pub limit: Option<u64>,
pub cursor: Option<String>,
}
pub const MAX_FILTER_LEN: usize = 8 * 1024;
pub const MAX_NODES: usize = 2000;
pub const MAX_ORDERBY_LEN: usize = 1024;
pub const MAX_ORDER_FIELDS: usize = 10;
pub const MAX_SELECT_LEN: usize = 2048;
pub const MAX_SELECT_FIELDS: usize = 100;
#[allow(clippy::result_large_err)] pub fn parse_select(raw: &str) -> Result<Vec<String>, crate::api::problem::Problem> {
let raw = raw.trim();
if raw.is_empty() {
return Err(crate::api::bad_request("$select cannot be empty"));
}
if raw.len() > MAX_SELECT_LEN {
return Err(crate::api::bad_request("$select too long"));
}
let fields: Vec<String> = raw
.split(',')
.map(|f| f.trim().to_lowercase())
.filter(|f| !f.is_empty())
.collect();
if fields.is_empty() {
return Err(crate::api::bad_request(
"$select must contain at least one field",
));
}
if fields.len() > MAX_SELECT_FIELDS {
return Err(crate::api::bad_request("$select contains too many fields"));
}
let mut seen = std::collections::HashSet::new();
for field in &fields {
if !seen.insert(field.clone()) {
return Err(crate::api::bad_request(format!(
"duplicate field in $select: {field}",
)));
}
}
Ok(fields)
}
pub fn parse_orderby(raw: &str) -> Result<ODataOrderBy, modkit_odata::Error> {
let raw = raw.trim();
if raw.is_empty() {
return Ok(ODataOrderBy::empty());
}
if raw.len() > MAX_ORDERBY_LEN {
return Err(modkit_odata::Error::InvalidOrderByField(
"orderby too long".into(),
));
}
let mut keys = Vec::new();
for part in raw.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
let tokens: Vec<&str> = part.split_whitespace().collect();
let (field, dir) = match tokens.as_slice() {
[field] | [field, "asc"] => (*field, SortDir::Asc),
[field, "desc"] => (*field, SortDir::Desc),
_ => {
return Err(modkit_odata::Error::InvalidOrderByField(format!(
"invalid orderby clause: {part}"
)));
}
};
if field.is_empty() {
return Err(modkit_odata::Error::InvalidOrderByField(
"empty field name in orderby".into(),
));
}
keys.push(OrderKey {
field: field.to_owned(),
dir,
});
}
if keys.len() > MAX_ORDER_FIELDS {
return Err(modkit_odata::Error::InvalidOrderByField(
"too many order fields".into(),
));
}
Ok(ODataOrderBy(keys))
}
pub async fn extract_odata_query<S>(
parts: &mut Parts,
state: &S,
) -> Result<ODataQuery, crate::api::problem::Problem>
where
S: Send + Sync,
{
let Query(params) = Query::<ODataParams>::from_request_parts(parts, state)
.await
.map_err(|e| crate::api::bad_request(format!("Invalid query parameters: {e}")))?;
let mut query = ODataQuery::new();
if let Some(raw_filter) = params.filter.as_ref() {
let raw = raw_filter.trim();
if !raw.is_empty() {
if raw.len() > MAX_FILTER_LEN {
return Err(crate::api::bad_request("Filter too long"));
}
let parsed = modkit_odata::parse_filter_string(raw).map_err(|e| {
tracing::debug!(error = %e, filter_len = raw.len(), "OData filter parsing failed");
crate::api::odata::odata_error_to_problem(&e, parts.uri.path(), None)
})?;
if parsed.node_count() > MAX_NODES {
tracing::debug!(
node_count = parsed.node_count(),
max_nodes = MAX_NODES,
"Filter complexity budget exceeded"
);
return Err(crate::api::bad_request("Filter too complex"));
}
let filter_hash = modkit_odata::pagination::short_filter_hash(Some(parsed.as_expr()));
let core_expr = parsed.into_expr();
query = query.with_filter(core_expr);
if let Some(hash) = filter_hash {
query = query.with_filter_hash(hash);
}
}
}
if params.cursor.is_some() && params.orderby.is_some() {
return Err(crate::api::odata::odata_error_to_problem(
&ODataError::OrderWithCursor,
"/",
None,
));
}
if let Some(cursor_str) = params.cursor.as_ref() {
let cursor = CursorV1::decode(cursor_str).map_err(|_| {
crate::api::odata::odata_error_to_problem(&ODataError::InvalidCursor, "/", None)
})?;
query = query.with_cursor(cursor);
query = query.with_order(ODataOrderBy::empty());
} else if let Some(raw_orderby) = params.orderby.as_ref() {
let order = parse_orderby(raw_orderby)
.map_err(|e| crate::api::odata::odata_error_to_problem(&e, "/", None))?;
query = query.with_order(order);
}
if let Some(limit) = params.limit {
if limit == 0 {
return Err(crate::api::odata::odata_error_to_problem(
&ODataError::InvalidLimit,
"/",
None,
));
}
query = query.with_limit(limit);
}
if let Some(raw_select) = params.select.as_ref() {
let fields = parse_select(raw_select)?;
query = query.with_select(fields);
}
Ok(query)
}
use std::ops::Deref;
#[derive(Debug, Clone)]
pub struct OData(pub ODataQuery);
impl OData {
#[inline]
pub fn into_inner(self) -> ODataQuery {
self.0
}
}
impl Deref for OData {
type Target = ODataQuery;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<ODataQuery> for OData {
#[inline]
fn as_ref(&self) -> &ODataQuery {
&self.0
}
}
impl From<OData> for ODataQuery {
#[inline]
fn from(x: OData) -> Self {
x.0
}
}
impl<S> FromRequestParts<S> for OData
where
S: Send + Sync,
{
type Rejection = crate::api::problem::Problem;
#[allow(clippy::manual_async_fn)]
fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> impl core::future::Future<Output = Result<Self, Self::Rejection>> + Send {
async move {
let query = extract_odata_query(parts, state).await?;
Ok(OData(query))
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
#[path = "odata_tests.rs"]
mod odata_tests;