1pub mod payload;
20pub mod preferences;
21pub mod query_params;
22pub mod range;
23pub mod types;
24
25pub 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#[derive(Debug, Clone)]
48pub struct ApiRequest {
49 pub action: Action,
51 pub ranges: std::collections::HashMap<CompactString, Range>,
53 pub top_level_range: Range,
55 pub payload: Option<Payload>,
57 pub preferences: Preferences,
59 pub query_params: QueryParams,
61 pub columns: HashSet<CompactString>,
63 pub headers: Vec<(String, String)>,
65 pub cookies: Vec<(String, String)>,
67 pub path: String,
69 pub method: String,
71 pub schema: CompactString,
73 pub negotiated_by_profile: bool,
75 pub accept_media_types: Vec<MediaType>,
77 pub content_media_type: MediaType,
79}
80
81pub 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 let resource = get_resource(config, path)?;
95
96 let (schema, negotiated_by_profile) = get_schema(config, headers, method)?;
98
99 let action = get_action(&resource, &schema, method)?;
101
102 let query_params = query_params::parse(action.is_invoke_safe(), query_string)?;
104
105 let (top_level_range, ranges) = get_ranges(method, &query_params, headers)?;
107
108 let content_media_type = get_content_type(headers);
110
111 let (payload, columns) =
113 payload::get_payload(body, &content_media_type, &query_params, &action)?;
114
115 let accept_media_types = get_accept_media_types(headers);
117
118 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
140fn 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 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
167fn 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 (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 (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 (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 (_, method) => Err(Error::ParseError {
233 location: "method".to_string(),
234 message: format!("unsupported method: {}", method),
235 }),
236 }
237}
238
239fn 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
279fn get_ranges(
285 method: &str,
286 query_params: &QueryParams,
287 headers: &[(String, String)],
288) -> Result<(Range, std::collections::HashMap<CompactString, Range>), Error> {
289 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 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
328fn 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
356type 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 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#[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 #[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 #[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 #[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); }
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 #[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()); }
610
611 #[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 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 #[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 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()); }
825}