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)
153 .await
154 .unwrap_or_else(|_| Query(ODataParams::default()));
155
156 let mut query = ODataQuery::new();
157
158 if let Some(raw_filter) = params.filter.as_ref() {
160 let raw = raw_filter.trim();
161 if !raw.is_empty() {
162 if raw.len() > MAX_FILTER_LEN {
163 return Err(crate::api::bad_request("Filter too long"));
164 }
165
166 let parsed = modkit_odata::parse_filter_string(raw).map_err(|e| {
168 tracing::debug!(error = %e, filter_len = raw.len(), "OData filter parsing failed");
170
171 crate::api::odata::odata_error_to_problem(&e, parts.uri.path(), None)
174 })?;
175
176 if parsed.node_count() > MAX_NODES {
177 tracing::debug!(
178 node_count = parsed.node_count(),
179 max_nodes = MAX_NODES,
180 "Filter complexity budget exceeded"
181 );
182 return Err(crate::api::bad_request("Filter too complex"));
183 }
184
185 let filter_hash = modkit_odata::pagination::short_filter_hash(Some(parsed.as_expr()));
187
188 let core_expr = parsed.into_expr();
190
191 query = query.with_filter(core_expr);
192 if let Some(hash) = filter_hash {
193 query = query.with_filter_hash(hash);
194 }
195 }
196 }
197
198 if params.cursor.is_some() && params.orderby.is_some() {
200 return Err(crate::api::odata::odata_error_to_problem(
201 &ODataError::OrderWithCursor,
202 "/",
203 None,
204 ));
205 }
206
207 if let Some(cursor_str) = params.cursor.as_ref() {
209 let cursor = CursorV1::decode(cursor_str).map_err(|_| {
210 crate::api::odata::odata_error_to_problem(&ODataError::InvalidCursor, "/", None)
211 })?;
212 query = query.with_cursor(cursor);
213 query = query.with_order(ODataOrderBy::empty());
215 } else if let Some(raw_orderby) = params.orderby.as_ref() {
216 let order = parse_orderby(raw_orderby)
218 .map_err(|e| crate::api::odata::odata_error_to_problem(&e, "/", None))?;
219 query = query.with_order(order);
220 }
221
222 if let Some(limit) = params.limit {
224 if limit == 0 {
225 return Err(crate::api::odata::odata_error_to_problem(
226 &ODataError::InvalidLimit,
227 "/",
228 None,
229 ));
230 }
231 query = query.with_limit(limit);
232 }
233
234 if let Some(raw_select) = params.select.as_ref() {
236 let fields = parse_select(raw_select)?;
237 query = query.with_select(fields);
238 }
239
240 Ok(query)
241}
242
243use std::ops::Deref;
244
245#[derive(Debug, Clone)]
250pub struct OData(pub ODataQuery);
251
252impl OData {
253 #[inline]
254 pub fn into_inner(self) -> ODataQuery {
255 self.0
256 }
257}
258
259impl Deref for OData {
260 type Target = ODataQuery;
261 #[inline]
262 fn deref(&self) -> &Self::Target {
263 &self.0
264 }
265}
266
267impl AsRef<ODataQuery> for OData {
268 #[inline]
269 fn as_ref(&self) -> &ODataQuery {
270 &self.0
271 }
272}
273
274impl From<OData> for ODataQuery {
275 #[inline]
276 fn from(x: OData) -> Self {
277 x.0
278 }
279}
280
281impl<S> FromRequestParts<S> for OData
282where
283 S: Send + Sync,
284{
285 type Rejection = crate::api::problem::Problem;
286
287 #[allow(clippy::manual_async_fn)]
288 fn from_request_parts(
289 parts: &mut Parts,
290 state: &S,
291 ) -> impl core::future::Future<Output = Result<Self, Self::Rejection>> + Send {
292 async move {
293 let query = extract_odata_query(parts, state).await?;
294 Ok(OData(query))
295 }
296 }
297}
298
299#[cfg(test)]
300#[cfg_attr(coverage_nightly, coverage(off))]
301#[path = "odata_tests.rs"]
302mod odata_tests;