1use axum::extract::{FromRequestParts, Query};
2use axum::http::request::Parts;
3use serde::Deserialize;
4use toolkit_canonical_errors::CanonicalError;
5use toolkit_odata::errors::OdataError;
6use toolkit_odata::{CursorV1, Error as ODataError, ODataOrderBy, OrderKey, SortDir};
7
8pub use toolkit_odata::ODataQuery;
10#[derive(Deserialize, Default)]
13pub struct ODataParams {
14 #[serde(rename = "$filter")]
15 pub filter: Option<String>,
16 #[serde(rename = "$orderby")]
17 pub orderby: Option<String>,
18 #[serde(rename = "$select")]
19 pub select: Option<String>,
20 pub limit: Option<u64>,
21 pub cursor: Option<String>,
22}
23
24pub const MAX_FILTER_LEN: usize = 8 * 1024;
25pub const MAX_NODES: usize = 2000;
26pub const MAX_ORDERBY_LEN: usize = 1024;
27pub const MAX_ORDER_FIELDS: usize = 10;
28pub const MAX_SELECT_LEN: usize = 2048;
29pub const MAX_SELECT_FIELDS: usize = 100;
30
31fn select_invalid_arg(detail: impl Into<String>, reason: &'static str) -> CanonicalError {
33 OdataError::invalid_argument()
34 .with_field_violation("$select", detail, reason)
35 .create()
36}
37
38#[allow(clippy::result_large_err)]
47pub fn parse_select(raw: &str) -> Result<Vec<String>, CanonicalError> {
48 let raw = raw.trim();
49 if raw.is_empty() {
50 return Err(select_invalid_arg(
51 "$select cannot be empty",
52 "INVALID_SELECT",
53 ));
54 }
55
56 if raw.len() > MAX_SELECT_LEN {
57 return Err(select_invalid_arg("$select too long", "INVALID_SELECT"));
58 }
59
60 let fields: Vec<String> = raw
61 .split(',')
62 .map(|f| f.trim().to_lowercase())
63 .filter(|f| !f.is_empty())
64 .collect();
65
66 if fields.is_empty() {
67 return Err(select_invalid_arg(
68 "$select must contain at least one field",
69 "INVALID_SELECT",
70 ));
71 }
72
73 if fields.len() > MAX_SELECT_FIELDS {
74 return Err(select_invalid_arg(
75 "$select contains too many fields",
76 "INVALID_SELECT",
77 ));
78 }
79
80 let mut seen = std::collections::HashSet::new();
82 for field in &fields {
83 if !seen.insert(field.clone()) {
84 return Err(select_invalid_arg(
85 format!("duplicate field in $select: {field}"),
86 "INVALID_SELECT",
87 ));
88 }
89 }
90
91 Ok(fields)
92}
93
94pub fn parse_orderby(raw: &str) -> Result<ODataOrderBy, toolkit_odata::Error> {
101 let raw = raw.trim();
102 if raw.is_empty() {
103 return Ok(ODataOrderBy::empty());
104 }
105
106 if raw.len() > MAX_ORDERBY_LEN {
107 return Err(toolkit_odata::Error::InvalidOrderByField(
108 "orderby too long".into(),
109 ));
110 }
111
112 let mut keys = Vec::new();
113
114 for part in raw.split(',') {
115 let part = part.trim();
116 if part.is_empty() {
117 continue;
118 }
119
120 let tokens: Vec<&str> = part.split_whitespace().collect();
121 let (field, dir) = match tokens.as_slice() {
122 [field] | [field, "asc"] => (*field, SortDir::Asc),
123 [field, "desc"] => (*field, SortDir::Desc),
124 _ => {
125 return Err(toolkit_odata::Error::InvalidOrderByField(format!(
126 "invalid orderby clause: {part}"
127 )));
128 }
129 };
130
131 if field.is_empty() {
132 return Err(toolkit_odata::Error::InvalidOrderByField(
133 "empty field name in orderby".into(),
134 ));
135 }
136
137 keys.push(OrderKey {
138 field: field.to_owned(),
139 dir,
140 });
141 }
142
143 if keys.len() > MAX_ORDER_FIELDS {
144 return Err(toolkit_odata::Error::InvalidOrderByField(
145 "too many order fields".into(),
146 ));
147 }
148
149 Ok(ODataOrderBy(keys))
150}
151
152fn filter_invalid_arg(detail: impl Into<String>, reason: &'static str) -> CanonicalError {
154 OdataError::invalid_argument()
155 .with_field_violation("$filter", detail, reason)
156 .create()
157}
158
159fn query_params_invalid_arg(detail: impl Into<String>) -> CanonicalError {
162 OdataError::invalid_argument()
163 .with_field_violation("query", detail, "INVALID_QUERY_PARAMS")
164 .create()
165}
166
167pub async fn extract_odata_query<S>(
178 parts: &mut Parts,
179 state: &S,
180) -> Result<ODataQuery, CanonicalError>
181where
182 S: Send + Sync,
183{
184 let Query(params) = Query::<ODataParams>::from_request_parts(parts, state)
185 .await
186 .map_err(|e| query_params_invalid_arg(format!("Invalid query parameters: {e}")))?;
187
188 let mut query = ODataQuery::new();
189
190 if let Some(raw_filter) = params.filter.as_ref() {
192 let raw = raw_filter.trim();
193 if !raw.is_empty() {
194 if raw.len() > MAX_FILTER_LEN {
195 return Err(filter_invalid_arg("Filter too long", "FILTER_TOO_LONG"));
196 }
197
198 let parsed = toolkit_odata::parse_filter_string(raw).map_err(|e| {
200 tracing::debug!(error = %e, filter_len = raw.len(), "OData filter parsing failed");
203 CanonicalError::from(e)
204 })?;
205
206 if parsed.node_count() > MAX_NODES {
207 tracing::debug!(
208 node_count = parsed.node_count(),
209 max_nodes = MAX_NODES,
210 "Filter complexity budget exceeded"
211 );
212 return Err(filter_invalid_arg(
213 "Filter too complex",
214 "FILTER_TOO_COMPLEX",
215 ));
216 }
217
218 let filter_hash = toolkit_odata::pagination::short_filter_hash(Some(parsed.as_expr()));
220
221 let core_expr = parsed.into_expr();
223
224 query = query.with_filter(core_expr);
225 if let Some(hash) = filter_hash {
226 query = query.with_filter_hash(hash);
227 }
228 }
229 }
230
231 if params.cursor.is_some() && params.orderby.is_some() {
233 return Err(ODataError::OrderWithCursor.into());
234 }
235
236 if let Some(cursor_str) = params.cursor.as_ref() {
238 let cursor = CursorV1::decode(cursor_str).map_err(|_| ODataError::InvalidCursor)?;
239 query = query.with_cursor(cursor);
240 query = query.with_order(ODataOrderBy::empty());
242 } else if let Some(raw_orderby) = params.orderby.as_ref() {
243 let order = parse_orderby(raw_orderby).map_err(CanonicalError::from)?;
245 query = query.with_order(order);
246 }
247
248 if let Some(limit) = params.limit {
250 if limit == 0 {
251 return Err(ODataError::InvalidLimit.into());
252 }
253 query = query.with_limit(limit);
254 }
255
256 if let Some(raw_select) = params.select.as_ref() {
258 let fields = parse_select(raw_select)?;
259 query = query.with_select(fields);
260 }
261
262 Ok(query)
263}
264
265use std::ops::Deref;
266
267#[derive(Debug, Clone)]
272pub struct OData(pub ODataQuery);
273
274impl OData {
275 #[inline]
276 pub fn into_inner(self) -> ODataQuery {
277 self.0
278 }
279}
280
281impl Deref for OData {
282 type Target = ODataQuery;
283 #[inline]
284 fn deref(&self) -> &Self::Target {
285 &self.0
286 }
287}
288
289impl AsRef<ODataQuery> for OData {
290 #[inline]
291 fn as_ref(&self) -> &ODataQuery {
292 &self.0
293 }
294}
295
296impl From<OData> for ODataQuery {
297 #[inline]
298 fn from(x: OData) -> Self {
299 x.0
300 }
301}
302
303impl<S> FromRequestParts<S> for OData
304where
305 S: Send + Sync,
306{
307 type Rejection = CanonicalError;
308
309 #[allow(clippy::manual_async_fn)]
310 fn from_request_parts(
311 parts: &mut Parts,
312 state: &S,
313 ) -> impl core::future::Future<Output = Result<Self, Self::Rejection>> + Send {
314 async move {
315 let query = extract_odata_query(parts, state).await?;
316 Ok(OData(query))
317 }
318 }
319}
320
321#[cfg(test)]
322#[cfg_attr(coverage_nightly, coverage(off))]
323#[path = "odata_tests.rs"]
324mod odata_tests;