1use axum::extract::{FromRequestParts, Query};
2use axum::http::request::Parts;
3use modkit_odata::{CursorV1, Error as ODataError, ODataOrderBy, OrderKey, SortDir};
4use serde::Deserialize;
5
6pub use modkit_odata::ODataQuery;
8pub mod error;
12pub use error::odata_error_to_problem;
13
14#[derive(Deserialize, Default)]
15pub struct ODataParams {
16 #[serde(rename = "$filter")]
17 pub filter: Option<String>,
18 #[serde(rename = "$orderby")]
19 pub orderby: Option<String>,
20 #[serde(rename = "$select")]
21 pub select: Option<String>,
22 pub limit: Option<u64>,
23 pub cursor: Option<String>,
24}
25
26pub const MAX_FILTER_LEN: usize = 8 * 1024;
27pub const MAX_NODES: usize = 2000;
28pub const MAX_ORDERBY_LEN: usize = 1024;
29pub const MAX_ORDER_FIELDS: usize = 10;
30pub const MAX_SELECT_LEN: usize = 2048;
31pub const MAX_SELECT_FIELDS: usize = 100;
32
33#[allow(clippy::result_large_err)] pub fn parse_select(raw: &str) -> Result<Vec<String>, crate::api::problem::Problem> {
41 let raw = raw.trim();
42 if raw.is_empty() {
43 return Err(crate::api::bad_request("$select cannot be empty"));
44 }
45
46 if raw.len() > MAX_SELECT_LEN {
47 return Err(crate::api::bad_request("$select too long"));
48 }
49
50 let fields: Vec<String> = raw
51 .split(',')
52 .map(|f| f.trim().to_lowercase())
53 .filter(|f| !f.is_empty())
54 .collect();
55
56 if fields.is_empty() {
57 return Err(crate::api::bad_request(
58 "$select must contain at least one field",
59 ));
60 }
61
62 if fields.len() > MAX_SELECT_FIELDS {
63 return Err(crate::api::bad_request("$select contains too many fields"));
64 }
65
66 let mut seen = std::collections::HashSet::new();
68 for field in &fields {
69 if !seen.insert(field.clone()) {
70 return Err(crate::api::bad_request(format!(
71 "duplicate field in $select: {field}",
72 )));
73 }
74 }
75
76 Ok(fields)
77}
78
79pub fn parse_orderby(raw: &str) -> Result<ODataOrderBy, modkit_odata::Error> {
86 let raw = raw.trim();
87 if raw.is_empty() {
88 return Ok(ODataOrderBy::empty());
89 }
90
91 if raw.len() > MAX_ORDERBY_LEN {
92 return Err(modkit_odata::Error::InvalidOrderByField(
93 "orderby too long".into(),
94 ));
95 }
96
97 let mut keys = Vec::new();
98
99 for part in raw.split(',') {
100 let part = part.trim();
101 if part.is_empty() {
102 continue;
103 }
104
105 let tokens: Vec<&str> = part.split_whitespace().collect();
106 let (field, dir) = match tokens.as_slice() {
107 [field] | [field, "asc"] => (*field, SortDir::Asc),
108 [field, "desc"] => (*field, SortDir::Desc),
109 _ => {
110 return Err(modkit_odata::Error::InvalidOrderByField(format!(
111 "invalid orderby clause: {part}"
112 )));
113 }
114 };
115
116 if field.is_empty() {
117 return Err(modkit_odata::Error::InvalidOrderByField(
118 "empty field name in orderby".into(),
119 ));
120 }
121
122 keys.push(OrderKey {
123 field: field.to_owned(),
124 dir,
125 });
126 }
127
128 if keys.len() > MAX_ORDER_FIELDS {
129 return Err(modkit_odata::Error::InvalidOrderByField(
130 "too many order fields".into(),
131 ));
132 }
133
134 Ok(ODataOrderBy(keys))
135}
136
137pub async fn extract_odata_query<S>(
145 parts: &mut Parts,
146 state: &S,
147) -> Result<ODataQuery, crate::api::problem::Problem>
148where
149 S: Send + Sync,
150{
151 let Query(params) = Query::<ODataParams>::from_request_parts(parts, state)
152 .await
153 .map_err(|e| crate::api::bad_request(format!("Invalid query parameters: {e}")))?;
154
155 let mut query = ODataQuery::new();
156
157 if let Some(raw_filter) = params.filter.as_ref() {
159 let raw = raw_filter.trim();
160 if !raw.is_empty() {
161 if raw.len() > MAX_FILTER_LEN {
162 return Err(crate::api::bad_request("Filter too long"));
163 }
164
165 let parsed = modkit_odata::parse_filter_string(raw).map_err(|e| {
167 tracing::debug!(error = %e, filter_len = raw.len(), "OData filter parsing failed");
169
170 crate::api::odata::odata_error_to_problem(&e, parts.uri.path(), None)
173 })?;
174
175 if parsed.node_count() > MAX_NODES {
176 tracing::debug!(
177 node_count = parsed.node_count(),
178 max_nodes = MAX_NODES,
179 "Filter complexity budget exceeded"
180 );
181 return Err(crate::api::bad_request("Filter too complex"));
182 }
183
184 let filter_hash = modkit_odata::pagination::short_filter_hash(Some(parsed.as_expr()));
186
187 let core_expr = parsed.into_expr();
189
190 query = query.with_filter(core_expr);
191 if let Some(hash) = filter_hash {
192 query = query.with_filter_hash(hash);
193 }
194 }
195 }
196
197 if params.cursor.is_some() && params.orderby.is_some() {
199 return Err(crate::api::odata::odata_error_to_problem(
200 &ODataError::OrderWithCursor,
201 "/",
202 None,
203 ));
204 }
205
206 if let Some(cursor_str) = params.cursor.as_ref() {
208 let cursor = CursorV1::decode(cursor_str).map_err(|_| {
209 crate::api::odata::odata_error_to_problem(&ODataError::InvalidCursor, "/", None)
210 })?;
211 query = query.with_cursor(cursor);
212 query = query.with_order(ODataOrderBy::empty());
214 } else if let Some(raw_orderby) = params.orderby.as_ref() {
215 let order = parse_orderby(raw_orderby)
217 .map_err(|e| crate::api::odata::odata_error_to_problem(&e, "/", None))?;
218 query = query.with_order(order);
219 }
220
221 if let Some(limit) = params.limit {
223 if limit == 0 {
224 return Err(crate::api::odata::odata_error_to_problem(
225 &ODataError::InvalidLimit,
226 "/",
227 None,
228 ));
229 }
230 query = query.with_limit(limit);
231 }
232
233 if let Some(raw_select) = params.select.as_ref() {
235 let fields = parse_select(raw_select)?;
236 query = query.with_select(fields);
237 }
238
239 Ok(query)
240}
241
242use std::ops::Deref;
243
244#[derive(Debug, Clone)]
249pub struct OData(pub ODataQuery);
250
251impl OData {
252 #[inline]
253 pub fn into_inner(self) -> ODataQuery {
254 self.0
255 }
256}
257
258impl Deref for OData {
259 type Target = ODataQuery;
260 #[inline]
261 fn deref(&self) -> &Self::Target {
262 &self.0
263 }
264}
265
266impl AsRef<ODataQuery> for OData {
267 #[inline]
268 fn as_ref(&self) -> &ODataQuery {
269 &self.0
270 }
271}
272
273impl From<OData> for ODataQuery {
274 #[inline]
275 fn from(x: OData) -> Self {
276 x.0
277 }
278}
279
280impl<S> FromRequestParts<S> for OData
281where
282 S: Send + Sync,
283{
284 type Rejection = crate::api::problem::Problem;
285
286 #[allow(clippy::manual_async_fn)]
287 fn from_request_parts(
288 parts: &mut Parts,
289 state: &S,
290 ) -> impl core::future::Future<Output = Result<Self, Self::Rejection>> + Send {
291 async move {
292 let query = extract_odata_query(parts, state).await?;
293 Ok(OData(query))
294 }
295 }
296}
297
298#[cfg(test)]
299#[cfg_attr(coverage_nightly, coverage(off))]
300#[path = "odata_tests.rs"]
301mod odata_tests;