1pub mod payload;
7pub mod preferences;
8pub mod query_params;
9pub mod types;
10
11pub use preferences::parse_preferences;
12pub use query_params::{parse_query_params, value_is_filter};
13pub use types::*;
14
15use crate::error::{Error, Result};
16use http::{Method, Request};
17use percent_encoding::percent_decode_str;
18use std::collections::{HashMap, HashSet};
19
20pub fn parse_request<B>(
26 req: &Request<B>,
27 default_schema: &str,
28 schemas: &[String],
29 max_rows: Option<i64>,
30) -> Result<ApiRequest>
31where
32 B: AsRef<[u8]>,
33{
34 let method = req.method();
35 let path = req.uri().path();
36 let query = req.uri().query().unwrap_or("");
37
38 let resource = parse_resource(path)?;
40
41 let (schema, negotiated_by_profile) = parse_schema(req, default_schema, schemas)?;
43
44 let action = parse_action(method, &resource, &schema)?;
46
47 let is_rpc = matches!(
53 action,
54 Action::Db(DbAction::Routine {
55 invoke_method: InvokeMethod::InvRead { .. },
56 ..
57 }) | Action::RoutineInfo { .. }
58 );
59 let query_params = parse_query_params(query, is_rpc)?;
60
61 let preferences = parse_preferences(req.headers())?;
63
64 let accept_media_types = parse_accept(req.headers())?;
66
67 let content_media_type = parse_content_type(req.headers())?;
69
70 let top_level_range = parse_range(req.headers())?;
72
73 let headers = extract_headers(req.headers());
75 let cookies = extract_cookies(req.headers());
76
77 Ok(ApiRequest {
78 action,
79 schema,
80 payload: None, query_params,
82 accept_media_types,
83 content_media_type,
84 preferences,
85 columns: HashSet::new(),
86 top_level_range,
87 range_map: HashMap::new(),
88 max_rows,
89 negotiated_by_profile,
90 method: method.to_string(),
91 path: path.to_string(),
92 headers,
93 cookies,
94 trace_id: None,
97 })
98}
99
100fn parse_resource(path: &str) -> Result<Resource> {
102 let path = path.trim_start_matches('/');
103
104 if path.is_empty() {
105 return Ok(Resource::Schema);
106 }
107
108 let path = path.trim_end_matches('/');
110 if path.is_empty() {
111 return Ok(Resource::Schema);
112 }
113
114 let decoded = |segment: &str| -> String {
119 percent_decode_str(segment)
120 .decode_utf8()
121 .map(|name| name.into_owned())
122 .unwrap_or_else(|_| segment.to_string())
123 };
124
125 if let Some(func_name) = path.strip_prefix("rpc/") {
126 if func_name.is_empty() {
127 return Err(Error::InvalidPath("Empty function name".into()));
128 }
129 if func_name.contains('/') {
130 return Err(Error::InvalidResourcePath);
131 }
132 return Ok(Resource::Routine(decoded(func_name)));
133 }
134
135 if path.contains('/') {
140 return Err(Error::InvalidResourcePath);
141 }
142
143 Ok(Resource::Relation(decoded(path)))
144}
145
146fn parse_schema<B>(
148 req: &Request<B>,
149 default_schema: &str,
150 schemas: &[String],
151) -> Result<(String, bool)> {
152 if let Some(profile) = req.headers().get("accept-profile") {
154 let schema = profile
155 .to_str()
156 .map_err(|_| Error::InvalidHeader("Accept-Profile"))?;
157 if !schemas.contains(&schema.to_string()) {
158 return Err(Error::UnacceptableSchema {
159 requested: schema.into(),
160 exposed: schemas.to_vec(),
161 });
162 }
163 return Ok((schema.to_string(), true));
164 }
165
166 if let Some(profile) = req.headers().get("content-profile") {
168 let schema = profile
169 .to_str()
170 .map_err(|_| Error::InvalidHeader("Content-Profile"))?;
171 if !schemas.contains(&schema.to_string()) {
172 return Err(Error::UnacceptableSchema {
173 requested: schema.into(),
174 exposed: schemas.to_vec(),
175 });
176 }
177 return Ok((schema.to_string(), true));
178 }
179
180 Ok((default_schema.to_string(), false))
181}
182
183fn parse_action(method: &Method, resource: &Resource, schema: &str) -> Result<Action> {
185 match (method, resource) {
186 (&Method::GET, Resource::Schema) => Ok(Action::Db(DbAction::SchemaRead {
188 schema: schema.to_string(),
189 headers_only: false,
190 })),
191 (&Method::HEAD, Resource::Schema) => Ok(Action::Db(DbAction::SchemaRead {
192 schema: schema.to_string(),
193 headers_only: true,
194 })),
195 (&Method::OPTIONS, Resource::Schema) => Ok(Action::SchemaInfo),
196
197 (&Method::GET, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationRead {
199 qi: QualifiedIdentifier::new(schema, name),
200 headers_only: false,
201 })),
202 (&Method::HEAD, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationRead {
203 qi: QualifiedIdentifier::new(schema, name),
204 headers_only: true,
205 })),
206 (&Method::POST, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
207 qi: QualifiedIdentifier::new(schema, name),
208 mutation: Mutation::Create,
209 })),
210 (&Method::PATCH, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
211 qi: QualifiedIdentifier::new(schema, name),
212 mutation: Mutation::Update,
213 })),
214 (&Method::PUT, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
215 qi: QualifiedIdentifier::new(schema, name),
216 mutation: Mutation::SingleUpsert,
217 })),
218 (&Method::DELETE, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
219 qi: QualifiedIdentifier::new(schema, name),
220 mutation: Mutation::Delete,
221 })),
222 (&Method::OPTIONS, Resource::Relation(name)) => {
223 Ok(Action::RelationInfo(QualifiedIdentifier::new(schema, name)))
224 }
225
226 (&Method::GET, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
228 qi: QualifiedIdentifier::new(schema, name),
229 invoke_method: InvokeMethod::InvRead {
230 headers_only: false,
231 },
232 })),
233 (&Method::HEAD, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
234 qi: QualifiedIdentifier::new(schema, name),
235 invoke_method: InvokeMethod::InvRead { headers_only: true },
236 })),
237 (&Method::POST, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
238 qi: QualifiedIdentifier::new(schema, name),
239 invoke_method: InvokeMethod::Inv,
240 })),
241 (&Method::OPTIONS, Resource::Routine(name)) => Ok(Action::RoutineInfo {
242 qi: QualifiedIdentifier::new(schema, name),
243 invoke_method: InvokeMethod::Inv,
244 }),
245
246 (_, Resource::Routine(_)) => Err(Error::InvalidRpcMethod(method.to_string())),
248 _ => Err(Error::UnsupportedMethod(method.to_string())),
249 }
250}
251
252fn parse_accept(headers: &http::HeaderMap) -> Result<Vec<MediaType>> {
254 if let Some(accept) = headers.get(http::header::ACCEPT) {
255 let accept_str = accept
256 .to_str()
257 .map_err(|_| Error::InvalidHeader("Accept"))?;
258 let types: Vec<MediaType> = accept_str
264 .split(',')
265 .map(str::trim)
266 .map(|entry| {
267 let kept: Vec<&str> = entry
268 .split(';')
269 .map(str::trim)
270 .filter(|part| !part.starts_with("q="))
271 .collect();
272 parse_media_type(&kept.join(";"))
273 })
274 .collect();
275 if types.is_empty() {
276 return Ok(vec![MediaType::ApplicationJson]);
277 }
278 return Ok(types);
279 }
280 Ok(vec![MediaType::ApplicationJson])
281}
282
283fn parse_media_type(s: &str) -> MediaType {
288 let mut parts = s.split(';').map(str::trim);
289 let base = parts.next().unwrap_or(s);
290 let parameters: Vec<(String, &str)> = parts
294 .filter_map(|part| part.split_once('='))
295 .map(|(key, value)| {
296 (
297 key.trim().to_ascii_lowercase(),
298 value.trim().trim_matches('"'),
299 )
300 })
301 .collect();
302 let param = |name: &str| {
303 parameters
304 .iter()
305 .find(|(key, _)| key == name)
306 .map(|(_, value)| *value)
307 };
308 let given = |parameter: &str| match parameter.split_once('=') {
309 Some((name, value)) => param(name) == Some(value),
310 None => false,
311 };
312
313 match base {
314 "application/json" => MediaType::ApplicationJson,
315 "application/geo+json" => MediaType::GeoJson,
316 "text/csv" => MediaType::TextCsv,
317 "text/plain" => MediaType::TextPlain,
318 "text/xml" => MediaType::TextXml,
319 "application/openapi+json" => MediaType::OpenApi,
320 "application/x-www-form-urlencoded" => MediaType::UrlEncoded,
321 "application/octet-stream" => MediaType::OctetStream,
322 "*/*" => MediaType::Any,
323 s if s.starts_with("application/vnd.pgrst.plan") => {
330 let format = match s.ends_with("+json") {
331 true => PlanFormat::Json,
332 false => PlanFormat::Text,
333 };
334 let requested = param("options").unwrap_or_default();
335 let options = [
338 ("analyze", PlanOption::Analyze),
339 ("verbose", PlanOption::Verbose),
340 ("settings", PlanOption::Settings),
341 ("buffers", PlanOption::Buffers),
342 ("wal", PlanOption::Wal),
343 ]
344 .into_iter()
345 .filter(|(name, _)| requested.split('|').any(|asked| asked == *name))
346 .map(|(_, option)| option)
347 .collect();
348 MediaType::Plan {
349 base: Box::new(parse_media_type(param("for").unwrap_or("application/json"))),
350 format,
351 options,
352 }
353 }
354 s if s.starts_with("application/vnd.pgrst.object") => MediaType::SingularJson {
355 nullable: given("nulls=null"),
356 strip_nulls: given("nulls=stripped"),
357 },
358 s if s.starts_with("application/vnd.pgrst.array") => MediaType::ArrayJson {
359 strip_nulls: given("nulls=stripped"),
360 },
361 other => MediaType::Other(other.to_string()),
362 }
363}
364
365fn parse_content_type(headers: &http::HeaderMap) -> Result<MediaType> {
367 if let Some(ct) = headers.get(http::header::CONTENT_TYPE) {
368 let ct_str = ct
369 .to_str()
370 .map_err(|_| Error::InvalidHeader("Content-Type"))?;
371 return Ok(parse_media_type(ct_str.trim()));
372 }
373 Ok(MediaType::ApplicationJson)
374}
375
376fn parse_range(headers: &http::HeaderMap) -> Result<Range> {
389 let Some(range) = headers.get(http::header::RANGE) else {
390 return Ok(Range::default());
391 };
392 let range_str = range.to_str().map_err(|_| Error::InvalidHeader("Range"))?;
393
394 let bounds = range_str
397 .split_once('=')
398 .map_or(range_str, |(_unit, bounds)| bounds)
399 .trim();
400
401 let Some((start, end)) = bounds.split_once('-') else {
402 return Ok(Range::default());
403 };
404 let Ok(start) = start.trim().parse::<i64>() else {
405 return Ok(Range::default());
406 };
407 if start < 0 {
408 return Ok(Range::default());
409 }
410
411 let end = end.trim();
412 if end.is_empty() {
413 return Ok(Range::new(start, None));
415 }
416 let Ok(end) = end.parse::<i64>() else {
417 return Ok(Range::default());
418 };
419 if end < start {
420 return Err(Error::InvalidRange(
421 "Requested range not satisfiable".into(),
422 ));
423 }
424
425 Ok(Range::from_bounds(start, Some(end)))
426}
427
428fn extract_headers(headers: &http::HeaderMap) -> indexmap::IndexMap<String, String> {
430 headers
431 .iter()
432 .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
433 .collect()
434}
435
436fn extract_cookies(headers: &http::HeaderMap) -> indexmap::IndexMap<String, String> {
438 headers
439 .get(http::header::COOKIE)
440 .and_then(|v| v.to_str().ok())
441 .map(|s| {
442 s.split(';')
443 .filter_map(|cookie| {
444 let (key, value) = cookie.trim().split_once('=')?;
445
446 Some((key.to_string(), value.to_string()))
447 })
448 .collect()
449 })
450 .unwrap_or_default()
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 fn range_of(value: &str) -> Result<Range> {
458 let mut headers = http::HeaderMap::new();
459 headers.insert(http::header::RANGE, value.parse().unwrap());
460 parse_range(&headers)
461 }
462
463 #[test]
465 fn a_range_header_names_the_rows_it_says() {
466 assert_eq!(range_of("0-9").unwrap(), Range::from_bounds(0, Some(9)));
467 assert_eq!(range_of("5-9").unwrap(), Range::from_bounds(5, Some(9)));
468 assert_eq!(range_of("10-19").unwrap(), Range::from_bounds(10, Some(19)));
469 assert_eq!(range_of("10-").unwrap(), Range::new(10, None));
471 assert_eq!(range_of("0-").unwrap(), Range::new(0, None));
472 assert_eq!(
474 range_of("items=5-9").unwrap(),
475 Range::from_bounds(5, Some(9))
476 );
477 }
478
479 #[test]
481 fn an_inverted_range_is_refused_rather_than_widened() {
482 assert!(matches!(range_of("9-5"), Err(Error::InvalidRange(_))));
483 }
484
485 #[test]
488 fn a_header_that_is_not_a_range_is_left_alone() {
489 for value in ["", "nonsense", "-5", "a-b", "5"] {
490 assert_eq!(range_of(value).unwrap(), Range::default(), "{value:?}");
491 }
492 }
493
494 #[test]
499 fn a_plan_media_type_is_named_back_in_full() {
500 let named = |s: &str| parse_media_type(s).to_mime();
501
502 assert_eq!(
503 named("application/vnd.pgrst.plan+json"),
504 "application/vnd.pgrst.plan+json; for=\"application/json\""
505 );
506 assert_eq!(
509 named("application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object\""),
510 "application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object+json\""
511 );
512 assert_eq!(
513 named("application/vnd.pgrst.plan; for=\"text/csv\""),
514 "application/vnd.pgrst.plan+text; for=\"text/csv\""
515 );
516 assert_eq!(
519 named("application/vnd.pgrst.plan+json; options=verbose|analyze|nonsense"),
520 "application/vnd.pgrst.plan+json; for=\"application/json\"; options=analyze|verbose"
521 );
522 }
523
524 #[test]
525 fn test_parse_resource() {
526 assert_eq!(parse_resource("/").unwrap(), Resource::Schema);
527 assert_eq!(
528 parse_resource("/users").unwrap(),
529 Resource::Relation("users".into())
530 );
531 assert_eq!(
532 parse_resource("/rpc/my_func").unwrap(),
533 Resource::Routine("my_func".into())
534 );
535 }
536
537 #[test]
538 fn test_parse_media_type() {
539 assert_eq!(
540 parse_media_type("application/json"),
541 MediaType::ApplicationJson
542 );
543 assert_eq!(parse_media_type("text/csv"), MediaType::TextCsv);
544 assert_eq!(parse_media_type("*/*"), MediaType::Any);
545 }
546}