Skip to main content

dbrest_core/api_request/
mod.rs

1//! API Request module
2//!
3//! Transforms raw HTTP requests into domain-specific `ApiRequest` objects.
4//! This is the bridge between HTTP and domain logic, mirroring the
5//! Haskell `ApiRequest` module for full API compatibility.
6//!
7//! # Architecture
8//!
9//! ```text
10//! HTTP Request
11//!   ├─ URL path   → Resource + Action
12//!   ├─ Query      → QueryParams (select, filter, order, logic)
13//!   ├─ Headers    → Preferences, Range, Schema, MediaType
14//!   └─ Body       → Payload
15//!       ↓
16//!   ApiRequest struct
17//! ```
18
19pub mod payload;
20pub mod preferences;
21pub mod query_params;
22pub mod range;
23pub mod types;
24
25// Re-export key types
26pub use preferences::Preferences;
27pub use query_params::QueryParams;
28pub use range::Range;
29pub use types::{
30    Action, AggregateFunction, DbAction, EmbedPath, Filter, InvokeMethod, JoinType, LogicTree,
31    Mutation, OpExpr, Operation, OrderTerm, Payload, Resource, SelectItem,
32};
33
34use bytes::Bytes;
35use compact_str::CompactString;
36use std::collections::HashSet;
37
38use crate::config::AppConfig;
39use crate::error::Error;
40use crate::types::identifiers::QualifiedIdentifier;
41use crate::types::media::MediaType;
42
43/// The core API request struct.
44///
45/// The core `ApiRequest` data type. Contains all parsed and
46/// validated information from an HTTP request.
47#[derive(Debug, Clone)]
48pub struct ApiRequest {
49    /// The resolved action to perform
50    pub action: Action,
51    /// Ranges keyed by embed level (e.g., "limit" for top-level)
52    pub ranges: std::collections::HashMap<CompactString, Range>,
53    /// The top-level range for the main query
54    pub top_level_range: Range,
55    /// Parsed request body
56    pub payload: Option<Payload>,
57    /// Parsed Prefer headers
58    pub preferences: Preferences,
59    /// Parsed query parameters
60    pub query_params: QueryParams,
61    /// Column names from payload or &columns parameter
62    pub columns: HashSet<CompactString>,
63    /// HTTP headers (lowercased name, value)
64    pub headers: Vec<(String, String)>,
65    /// Request cookies
66    pub cookies: Vec<(String, String)>,
67    /// Raw request path
68    pub path: String,
69    /// HTTP method
70    pub method: String,
71    /// The request schema (from profile headers or default)
72    pub schema: CompactString,
73    /// Whether the schema was negotiated via profile headers
74    pub negotiated_by_profile: bool,
75    /// Accepted media types from Accept header (sorted by quality)
76    pub accept_media_types: Vec<MediaType>,
77    /// Content-Type of the request body
78    pub content_media_type: MediaType,
79}
80
81/// Build an `ApiRequest` from HTTP request parts.
82///
83/// Build an `ApiRequest` from raw HTTP components.
84pub fn from_request(
85    config: &AppConfig,
86    prefs: &Preferences,
87    method: &str,
88    path: &str,
89    query_string: &str,
90    headers: &[(String, String)],
91    body: Bytes,
92) -> Result<ApiRequest, Error> {
93    // 1. Parse resource from path
94    let resource = get_resource(config, path)?;
95
96    // 2. Get schema from profile headers
97    let (schema, negotiated_by_profile) = get_schema(config, headers, method)?;
98
99    // 3. Determine action
100    let action = get_action(&resource, &schema, method)?;
101
102    // 4. Parse query parameters
103    let query_params = query_params::parse(action.is_invoke_safe(), query_string)?;
104
105    // 5. Parse range (from Range header and limit/offset)
106    let (top_level_range, ranges) = get_ranges(method, &query_params, headers)?;
107
108    // 6. Get content type
109    let content_media_type = get_content_type(headers);
110
111    // 7. Parse payload
112    let (payload, columns) =
113        payload::get_payload(body, &content_media_type, &query_params, &action)?;
114
115    // 8. Parse accept media types
116    let accept_media_types = get_accept_media_types(headers);
117
118    // 9. Extract headers and cookies
119    let (req_headers, cookies) = extract_headers_and_cookies(headers);
120
121    Ok(ApiRequest {
122        action,
123        ranges,
124        top_level_range,
125        payload,
126        preferences: prefs.clone(),
127        query_params,
128        columns,
129        headers: req_headers,
130        cookies,
131        path: path.to_string(),
132        method: method.to_string(),
133        schema,
134        negotiated_by_profile,
135        accept_media_types,
136        content_media_type,
137    })
138}
139
140// ==========================================================================
141// Resource resolution
142// ==========================================================================
143
144/// Parse URL path into a Resource.
145fn get_resource(config: &AppConfig, path: &str) -> Result<Resource, Error> {
146    let path = path.trim_start_matches('/');
147    let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
148
149    match segments.as_slice() {
150        [] => {
151            // Root path
152            if let Some(ref root_spec) = config.db_root_spec {
153                Ok(Resource::Routine(root_spec.name.clone()))
154            } else {
155                Ok(Resource::Schema)
156            }
157        }
158        [table] => Ok(Resource::Relation(CompactString::from(*table))),
159        ["rpc", func_name] => Ok(Resource::Routine(CompactString::from(*func_name))),
160        _ => Err(Error::ParseError {
161            location: "path".to_string(),
162            message: format!("invalid resource path: /{}", segments.join("/")),
163        }),
164    }
165}
166
167// ==========================================================================
168// Action resolution
169// ==========================================================================
170
171/// Determine the Action from resource, schema, and HTTP method.
172fn get_action(resource: &Resource, schema: &str, method: &str) -> Result<Action, Error> {
173    let qi = |name: &str| QualifiedIdentifier::new(schema, name);
174
175    match (resource, method) {
176        // Routines
177        (Resource::Routine(name), "HEAD") => Ok(Action::Db(DbAction::Routine {
178            qi: qi(name),
179            inv_method: InvokeMethod::InvRead(true),
180        })),
181        (Resource::Routine(name), "GET") => Ok(Action::Db(DbAction::Routine {
182            qi: qi(name),
183            inv_method: InvokeMethod::InvRead(false),
184        })),
185        (Resource::Routine(name), "POST") => Ok(Action::Db(DbAction::Routine {
186            qi: qi(name),
187            inv_method: InvokeMethod::Inv,
188        })),
189        (Resource::Routine(name), "OPTIONS") => {
190            Ok(Action::RoutineInfo(qi(name), InvokeMethod::InvRead(true)))
191        }
192
193        // Relations
194        (Resource::Relation(name), "HEAD") => Ok(Action::Db(DbAction::RelationRead {
195            qi: qi(name),
196            headers_only: true,
197        })),
198        (Resource::Relation(name), "GET") => Ok(Action::Db(DbAction::RelationRead {
199            qi: qi(name),
200            headers_only: false,
201        })),
202        (Resource::Relation(name), "POST") => Ok(Action::Db(DbAction::RelationMut {
203            qi: qi(name),
204            mutation: Mutation::MutationCreate,
205        })),
206        (Resource::Relation(name), "PUT") => Ok(Action::Db(DbAction::RelationMut {
207            qi: qi(name),
208            mutation: Mutation::MutationSingleUpsert,
209        })),
210        (Resource::Relation(name), "PATCH") => Ok(Action::Db(DbAction::RelationMut {
211            qi: qi(name),
212            mutation: Mutation::MutationUpdate,
213        })),
214        (Resource::Relation(name), "DELETE") => Ok(Action::Db(DbAction::RelationMut {
215            qi: qi(name),
216            mutation: Mutation::MutationDelete,
217        })),
218        (Resource::Relation(name), "OPTIONS") => Ok(Action::RelationInfo(qi(name))),
219
220        // Schema
221        (Resource::Schema, "HEAD") => Ok(Action::Db(DbAction::SchemaRead {
222            schema: CompactString::from(schema),
223            headers_only: true,
224        })),
225        (Resource::Schema, "GET") => Ok(Action::Db(DbAction::SchemaRead {
226            schema: CompactString::from(schema),
227            headers_only: false,
228        })),
229        (Resource::Schema, "OPTIONS") => Ok(Action::SchemaInfo),
230
231        // Unsupported
232        (_, method) => Err(Error::ParseError {
233            location: "method".to_string(),
234            message: format!("unsupported method: {}", method),
235        }),
236    }
237}
238
239// ==========================================================================
240// Schema resolution
241// ==========================================================================
242
243/// Determine the request schema from profile headers.
244fn get_schema(
245    config: &AppConfig,
246    headers: &[(String, String)],
247    method: &str,
248) -> Result<(CompactString, bool), Error> {
249    let profile = match method {
250        "DELETE" | "PATCH" | "POST" | "PUT" => find_header(headers, "content-profile"),
251        _ => find_header(headers, "accept-profile"),
252    };
253
254    match profile {
255        Some(p) => {
256            if config.db_schemas.iter().any(|s| s == &p) {
257                Ok((CompactString::from(p.as_str()), true))
258            } else {
259                Err(Error::ParseError {
260                    location: "schema".to_string(),
261                    message: format!(
262                        "schema '{}' not in allowed schemas: {:?}",
263                        p, config.db_schemas
264                    ),
265                })
266            }
267        }
268        None => {
269            let default = config
270                .db_schemas
271                .first()
272                .map(|s| s.as_str())
273                .unwrap_or("public");
274            Ok((CompactString::from(default), false))
275        }
276    }
277}
278
279// ==========================================================================
280// Range resolution
281// ==========================================================================
282
283/// Resolve ranges from Range header and query parameters.
284fn get_ranges(
285    method: &str,
286    query_params: &QueryParams,
287    headers: &[(String, String)],
288) -> Result<(Range, std::collections::HashMap<CompactString, Range>), Error> {
289    // Range header only applies to GET
290    let header_range = if method == "GET" {
291        find_header(headers, "range")
292            .and_then(|v| range::parse_range_header(&v))
293            .unwrap_or_else(Range::all)
294    } else {
295        Range::all()
296    };
297
298    let limit_range = query_params
299        .ranges
300        .get("limit")
301        .copied()
302        .unwrap_or_else(Range::all);
303
304    let header_and_limit = header_range.intersect(&limit_range);
305
306    let mut ranges = query_params.ranges.clone();
307    ranges.insert(
308        "limit".into(),
309        limit_range.convert_to_limit_zero(&header_and_limit),
310    );
311
312    let top_level = ranges.get("limit").copied().unwrap_or_else(Range::all);
313
314    // Validate range
315    if top_level.is_empty_range() && !limit_range.has_limit_zero() {
316        return Err(Error::InvalidRange("invalid range".to_string()));
317    }
318
319    if method == "PUT" && !top_level.is_all() {
320        return Err(Error::InvalidRange(
321            "PUT with limit/offset is not allowed".to_string(),
322        ));
323    }
324
325    Ok((top_level, ranges))
326}
327
328// ==========================================================================
329// Header helpers
330// ==========================================================================
331
332fn find_header(headers: &[(String, String)], name: &str) -> Option<String> {
333    headers
334        .iter()
335        .find(|(k, _)| k.eq_ignore_ascii_case(name))
336        .map(|(_, v)| v.clone())
337}
338
339fn get_content_type(headers: &[(String, String)]) -> MediaType {
340    find_header(headers, "content-type")
341        .map(|v| MediaType::parse(&v))
342        .unwrap_or(MediaType::ApplicationJson)
343}
344
345fn get_accept_media_types(headers: &[(String, String)]) -> Vec<MediaType> {
346    find_header(headers, "accept")
347        .map(|v| {
348            crate::types::media::parse_accept_header(&v)
349                .into_iter()
350                .map(|item| item.media_type)
351                .collect()
352        })
353        .unwrap_or_else(|| vec![MediaType::Any])
354}
355
356/// Headers list: (name, value) pairs
357type HeaderList = Vec<(String, String)>;
358
359fn extract_headers_and_cookies(headers: &[(String, String)]) -> (HeaderList, HeaderList) {
360    let mut req_headers = Vec::new();
361    let mut cookies = Vec::new();
362
363    for (name, value) in headers {
364        let lower = name.to_lowercase();
365        if lower == "cookie" {
366            // Parse cookies
367            for cookie in value.split(';') {
368                let cookie = cookie.trim();
369                if let Some((k, v)) = cookie.split_once('=') {
370                    cookies.push((k.trim().to_string(), v.trim().to_string()));
371                }
372            }
373        } else {
374            req_headers.push((lower, value.clone()));
375        }
376    }
377
378    (req_headers, cookies)
379}
380
381// ==========================================================================
382// Tests
383// ==========================================================================
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    fn test_config() -> AppConfig {
390        let mut config = AppConfig::default();
391        config.db_schemas = vec!["public".to_string(), "api".to_string()];
392        config
393    }
394
395    // ---------- Resource resolution tests ----------
396
397    #[test]
398    fn test_get_resource_root() {
399        let config = test_config();
400        let resource = get_resource(&config, "/").unwrap();
401        assert_eq!(resource, Resource::Schema);
402    }
403
404    #[test]
405    fn test_get_resource_table() {
406        let config = test_config();
407        let resource = get_resource(&config, "/items").unwrap();
408        assert_eq!(resource, Resource::Relation("items".into()));
409    }
410
411    #[test]
412    fn test_get_resource_rpc() {
413        let config = test_config();
414        let resource = get_resource(&config, "/rpc/my_func").unwrap();
415        assert_eq!(resource, Resource::Routine("my_func".into()));
416    }
417
418    #[test]
419    fn test_get_resource_invalid() {
420        let config = test_config();
421        let result = get_resource(&config, "/a/b/c");
422        assert!(result.is_err());
423    }
424
425    // ---------- Action resolution tests ----------
426
427    #[test]
428    fn test_get_action_get_table() {
429        let action = get_action(&Resource::Relation("items".into()), "public", "GET").unwrap();
430        assert!(matches!(
431            action,
432            Action::Db(DbAction::RelationRead {
433                headers_only: false,
434                ..
435            })
436        ));
437    }
438
439    #[test]
440    fn test_get_action_head_table() {
441        let action = get_action(&Resource::Relation("items".into()), "public", "HEAD").unwrap();
442        assert!(matches!(
443            action,
444            Action::Db(DbAction::RelationRead {
445                headers_only: true,
446                ..
447            })
448        ));
449    }
450
451    #[test]
452    fn test_get_action_post_table() {
453        let action = get_action(&Resource::Relation("items".into()), "public", "POST").unwrap();
454        assert!(matches!(
455            action,
456            Action::Db(DbAction::RelationMut {
457                mutation: Mutation::MutationCreate,
458                ..
459            })
460        ));
461    }
462
463    #[test]
464    fn test_get_action_put_table() {
465        let action = get_action(&Resource::Relation("items".into()), "public", "PUT").unwrap();
466        assert!(matches!(
467            action,
468            Action::Db(DbAction::RelationMut {
469                mutation: Mutation::MutationSingleUpsert,
470                ..
471            })
472        ));
473    }
474
475    #[test]
476    fn test_get_action_patch_table() {
477        let action = get_action(&Resource::Relation("items".into()), "public", "PATCH").unwrap();
478        assert!(matches!(
479            action,
480            Action::Db(DbAction::RelationMut {
481                mutation: Mutation::MutationUpdate,
482                ..
483            })
484        ));
485    }
486
487    #[test]
488    fn test_get_action_delete_table() {
489        let action = get_action(&Resource::Relation("items".into()), "public", "DELETE").unwrap();
490        assert!(matches!(
491            action,
492            Action::Db(DbAction::RelationMut {
493                mutation: Mutation::MutationDelete,
494                ..
495            })
496        ));
497    }
498
499    #[test]
500    fn test_get_action_options_table() {
501        let action = get_action(&Resource::Relation("items".into()), "public", "OPTIONS").unwrap();
502        assert!(matches!(action, Action::RelationInfo(_)));
503    }
504
505    #[test]
506    fn test_get_action_get_rpc() {
507        let action = get_action(&Resource::Routine("func".into()), "public", "GET").unwrap();
508        assert!(matches!(
509            action,
510            Action::Db(DbAction::Routine {
511                inv_method: InvokeMethod::InvRead(false),
512                ..
513            })
514        ));
515    }
516
517    #[test]
518    fn test_get_action_post_rpc() {
519        let action = get_action(&Resource::Routine("func".into()), "public", "POST").unwrap();
520        assert!(matches!(
521            action,
522            Action::Db(DbAction::Routine {
523                inv_method: InvokeMethod::Inv,
524                ..
525            })
526        ));
527    }
528
529    #[test]
530    fn test_get_action_schema_get() {
531        let action = get_action(&Resource::Schema, "public", "GET").unwrap();
532        assert!(matches!(action, Action::Db(DbAction::SchemaRead { .. })));
533    }
534
535    #[test]
536    fn test_get_action_schema_options() {
537        let action = get_action(&Resource::Schema, "public", "OPTIONS").unwrap();
538        assert!(matches!(action, Action::SchemaInfo));
539    }
540
541    #[test]
542    fn test_get_action_unsupported() {
543        let result = get_action(&Resource::Schema, "public", "TRACE");
544        assert!(result.is_err());
545    }
546
547    // ---------- Schema resolution tests ----------
548
549    #[test]
550    fn test_get_schema_default() {
551        let config = test_config();
552        let headers: Vec<(String, String)> = vec![];
553        let (schema, negotiated) = get_schema(&config, &headers, "GET").unwrap();
554        assert_eq!(schema.as_str(), "public");
555        assert!(!negotiated); // no profile header used
556    }
557
558    #[test]
559    fn test_get_schema_accept_profile() {
560        let config = test_config();
561        let headers = vec![("accept-profile".to_string(), "api".to_string())];
562        let (schema, negotiated) = get_schema(&config, &headers, "GET").unwrap();
563        assert_eq!(schema.as_str(), "api");
564        assert!(negotiated);
565    }
566
567    #[test]
568    fn test_get_schema_content_profile_for_post() {
569        let config = test_config();
570        let headers = vec![("content-profile".to_string(), "api".to_string())];
571        let (schema, negotiated) = get_schema(&config, &headers, "POST").unwrap();
572        assert_eq!(schema.as_str(), "api");
573        assert!(negotiated);
574    }
575
576    #[test]
577    fn test_get_schema_invalid() {
578        let config = test_config();
579        let headers = vec![("accept-profile".to_string(), "nonexistent".to_string())];
580        let result = get_schema(&config, &headers, "GET");
581        assert!(result.is_err());
582    }
583
584    // ---------- Range tests ----------
585
586    #[test]
587    fn test_get_ranges_default() {
588        let qp = QueryParams::default();
589        let headers: Vec<(String, String)> = vec![];
590        let (top, _) = get_ranges("GET", &qp, &headers).unwrap();
591        assert!(top.is_all());
592    }
593
594    #[test]
595    fn test_get_ranges_with_header() {
596        let qp = QueryParams::default();
597        let headers = vec![("range".to_string(), "items=0-24".to_string())];
598        let (top, _) = get_ranges("GET", &qp, &headers).unwrap();
599        assert_eq!(top.offset, 0);
600        assert_eq!(top.limit_to, Some(24));
601    }
602
603    #[test]
604    fn test_get_ranges_header_ignored_for_post() {
605        let qp = QueryParams::default();
606        let headers = vec![("range".to_string(), "items=0-24".to_string())];
607        let (top, _) = get_ranges("POST", &qp, &headers).unwrap();
608        assert!(top.is_all()); // Range header ignored for non-GET
609    }
610
611    // ---------- Header helper tests ----------
612
613    #[test]
614    fn test_find_header() {
615        let headers = vec![
616            ("Content-Type".to_string(), "application/json".to_string()),
617            ("Accept".to_string(), "text/csv".to_string()),
618        ];
619        assert_eq!(
620            find_header(&headers, "content-type").as_deref(),
621            Some("application/json")
622        );
623        assert_eq!(find_header(&headers, "accept").as_deref(), Some("text/csv"));
624        assert!(find_header(&headers, "nonexistent").is_none());
625    }
626
627    #[test]
628    fn test_get_content_type() {
629        let headers = vec![("content-type".to_string(), "text/csv".to_string())];
630        assert_eq!(get_content_type(&headers), MediaType::TextCsv);
631
632        let empty: Vec<(String, String)> = vec![];
633        assert_eq!(get_content_type(&empty), MediaType::ApplicationJson);
634    }
635
636    #[test]
637    fn test_get_accept_media_types() {
638        let headers = vec![(
639            "accept".to_string(),
640            "text/csv, application/json;q=0.5".to_string(),
641        )];
642        let types = get_accept_media_types(&headers);
643        assert_eq!(types.len(), 2);
644        // Sorted by quality
645        assert_eq!(types[0], MediaType::TextCsv);
646        assert_eq!(types[1], MediaType::ApplicationJson);
647    }
648
649    #[test]
650    fn test_extract_headers_and_cookies() {
651        let headers = vec![
652            ("Content-Type".to_string(), "application/json".to_string()),
653            ("Cookie".to_string(), "session=abc123; lang=en".to_string()),
654            ("X-Custom".to_string(), "value".to_string()),
655        ];
656        let (hdrs, cookies) = extract_headers_and_cookies(&headers);
657        assert_eq!(hdrs.len(), 2);
658        assert_eq!(cookies.len(), 2);
659        assert_eq!(cookies[0].0, "session");
660        assert_eq!(cookies[0].1, "abc123");
661    }
662
663    // ---------- Full from_request tests ----------
664
665    #[test]
666    fn test_from_request_get() {
667        let config = test_config();
668        let prefs = Preferences::default();
669        let headers = vec![("accept".to_string(), "application/json".to_string())];
670        let body = Bytes::new();
671
672        let req = from_request(
673            &config,
674            &prefs,
675            "GET",
676            "/items",
677            "select=id,name",
678            &headers,
679            body,
680        )
681        .unwrap();
682
683        assert!(matches!(
684            req.action,
685            Action::Db(DbAction::RelationRead { .. })
686        ));
687        assert_eq!(req.query_params.select.len(), 2);
688        assert_eq!(req.schema.as_str(), "public");
689        assert_eq!(req.method, "GET");
690        assert_eq!(req.path, "/items");
691    }
692
693    #[test]
694    fn test_from_request_post() {
695        let config = test_config();
696        let prefs = Preferences::default();
697        let headers = vec![("content-type".to_string(), "application/json".to_string())];
698        let body = Bytes::from(r#"{"id":1,"name":"test"}"#);
699
700        let req = from_request(&config, &prefs, "POST", "/items", "", &headers, body).unwrap();
701
702        assert!(matches!(
703            req.action,
704            Action::Db(DbAction::RelationMut {
705                mutation: Mutation::MutationCreate,
706                ..
707            })
708        ));
709        assert!(req.payload.is_some());
710        assert_eq!(req.columns.len(), 2);
711    }
712
713    #[test]
714    fn test_from_request_rpc_get() {
715        let config = test_config();
716        let prefs = Preferences::default();
717        let headers: Vec<(String, String)> = vec![];
718        let body = Bytes::new();
719
720        let req = from_request(
721            &config,
722            &prefs,
723            "GET",
724            "/rpc/my_func",
725            "id=5",
726            &headers,
727            body,
728        )
729        .unwrap();
730
731        assert!(matches!(
732            req.action,
733            Action::Db(DbAction::Routine {
734                inv_method: InvokeMethod::InvRead(false),
735                ..
736            })
737        ));
738        // For RPC GET, params without operators become rpc params
739        assert_eq!(req.query_params.params.len(), 1);
740    }
741
742    #[test]
743    fn test_from_request_schema() {
744        let config = test_config();
745        let prefs = Preferences::default();
746        let headers: Vec<(String, String)> = vec![];
747        let body = Bytes::new();
748
749        let req = from_request(&config, &prefs, "GET", "/", "", &headers, body).unwrap();
750
751        assert!(matches!(
752            req.action,
753            Action::Db(DbAction::SchemaRead { .. })
754        ));
755    }
756
757    #[test]
758    fn test_from_request_with_profile() {
759        let config = test_config();
760        let prefs = Preferences::default();
761        let headers = vec![("accept-profile".to_string(), "api".to_string())];
762        let body = Bytes::new();
763
764        let req = from_request(&config, &prefs, "GET", "/items", "", &headers, body).unwrap();
765
766        assert_eq!(req.schema.as_str(), "api");
767        assert!(req.negotiated_by_profile);
768    }
769
770    #[test]
771    fn test_from_request_with_range() {
772        let config = test_config();
773        let prefs = Preferences::default();
774        let headers = vec![("range".to_string(), "items=0-24".to_string())];
775        let body = Bytes::new();
776
777        let req = from_request(&config, &prefs, "GET", "/items", "", &headers, body).unwrap();
778
779        assert_eq!(req.top_level_range.offset, 0);
780        assert_eq!(req.top_level_range.limit_to, Some(24));
781    }
782
783    #[test]
784    fn test_from_request_with_filters() {
785        let config = test_config();
786        let prefs = Preferences::default();
787        let headers: Vec<(String, String)> = vec![];
788        let body = Bytes::new();
789
790        let req = from_request(
791            &config,
792            &prefs,
793            "GET",
794            "/items",
795            "id=eq.5&name=like.*john*",
796            &headers,
797            body,
798        )
799        .unwrap();
800
801        assert_eq!(req.query_params.filters_root.len(), 2);
802    }
803
804    #[test]
805    fn test_from_request_delete() {
806        let config = test_config();
807        let prefs = Preferences::default();
808        let headers: Vec<(String, String)> = vec![];
809        let body = Bytes::new();
810
811        let req = from_request(
812            &config, &prefs, "DELETE", "/items", "id=eq.1", &headers, body,
813        )
814        .unwrap();
815
816        assert!(matches!(
817            req.action,
818            Action::Db(DbAction::RelationMut {
819                mutation: Mutation::MutationDelete,
820                ..
821            })
822        ));
823        assert!(req.payload.is_none()); // DELETE doesn't parse payload
824    }
825}