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 })
95}
96
97fn parse_resource(path: &str) -> Result<Resource> {
99 let path = path.trim_start_matches('/');
100
101 if path.is_empty() {
102 return Ok(Resource::Schema);
103 }
104
105 let path = path.trim_end_matches('/');
107 if path.is_empty() {
108 return Ok(Resource::Schema);
109 }
110
111 let decoded = |segment: &str| -> String {
116 percent_decode_str(segment)
117 .decode_utf8()
118 .map(|name| name.into_owned())
119 .unwrap_or_else(|_| segment.to_string())
120 };
121
122 if let Some(func_name) = path.strip_prefix("rpc/") {
123 if func_name.is_empty() {
124 return Err(Error::InvalidPath("Empty function name".into()));
125 }
126 if func_name.contains('/') {
127 return Err(Error::InvalidResourcePath);
128 }
129 return Ok(Resource::Routine(decoded(func_name)));
130 }
131
132 if path.contains('/') {
137 return Err(Error::InvalidResourcePath);
138 }
139
140 Ok(Resource::Relation(decoded(path)))
141}
142
143fn parse_schema<B>(
145 req: &Request<B>,
146 default_schema: &str,
147 schemas: &[String],
148) -> Result<(String, bool)> {
149 if let Some(profile) = req.headers().get("accept-profile") {
151 let schema = profile
152 .to_str()
153 .map_err(|_| Error::InvalidHeader("Accept-Profile"))?;
154 if !schemas.contains(&schema.to_string()) {
155 return Err(Error::UnacceptableSchema {
156 requested: schema.into(),
157 exposed: schemas.to_vec(),
158 });
159 }
160 return Ok((schema.to_string(), true));
161 }
162
163 if let Some(profile) = req.headers().get("content-profile") {
165 let schema = profile
166 .to_str()
167 .map_err(|_| Error::InvalidHeader("Content-Profile"))?;
168 if !schemas.contains(&schema.to_string()) {
169 return Err(Error::UnacceptableSchema {
170 requested: schema.into(),
171 exposed: schemas.to_vec(),
172 });
173 }
174 return Ok((schema.to_string(), true));
175 }
176
177 Ok((default_schema.to_string(), false))
178}
179
180fn parse_action(method: &Method, resource: &Resource, schema: &str) -> Result<Action> {
182 match (method, resource) {
183 (&Method::GET, Resource::Schema) => Ok(Action::Db(DbAction::SchemaRead {
185 schema: schema.to_string(),
186 headers_only: false,
187 })),
188 (&Method::HEAD, Resource::Schema) => Ok(Action::Db(DbAction::SchemaRead {
189 schema: schema.to_string(),
190 headers_only: true,
191 })),
192 (&Method::OPTIONS, Resource::Schema) => Ok(Action::SchemaInfo),
193
194 (&Method::GET, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationRead {
196 qi: QualifiedIdentifier::new(schema, name),
197 headers_only: false,
198 })),
199 (&Method::HEAD, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationRead {
200 qi: QualifiedIdentifier::new(schema, name),
201 headers_only: true,
202 })),
203 (&Method::POST, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
204 qi: QualifiedIdentifier::new(schema, name),
205 mutation: Mutation::Create,
206 })),
207 (&Method::PATCH, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
208 qi: QualifiedIdentifier::new(schema, name),
209 mutation: Mutation::Update,
210 })),
211 (&Method::PUT, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
212 qi: QualifiedIdentifier::new(schema, name),
213 mutation: Mutation::SingleUpsert,
214 })),
215 (&Method::DELETE, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
216 qi: QualifiedIdentifier::new(schema, name),
217 mutation: Mutation::Delete,
218 })),
219 (&Method::OPTIONS, Resource::Relation(name)) => {
220 Ok(Action::RelationInfo(QualifiedIdentifier::new(schema, name)))
221 }
222
223 (&Method::GET, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
225 qi: QualifiedIdentifier::new(schema, name),
226 invoke_method: InvokeMethod::InvRead {
227 headers_only: false,
228 },
229 })),
230 (&Method::HEAD, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
231 qi: QualifiedIdentifier::new(schema, name),
232 invoke_method: InvokeMethod::InvRead { headers_only: true },
233 })),
234 (&Method::POST, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
235 qi: QualifiedIdentifier::new(schema, name),
236 invoke_method: InvokeMethod::Inv,
237 })),
238 (&Method::OPTIONS, Resource::Routine(name)) => Ok(Action::RoutineInfo {
239 qi: QualifiedIdentifier::new(schema, name),
240 invoke_method: InvokeMethod::Inv,
241 }),
242
243 (_, Resource::Routine(_)) => Err(Error::InvalidRpcMethod(method.to_string())),
245 _ => Err(Error::UnsupportedMethod(method.to_string())),
246 }
247}
248
249fn parse_accept(headers: &http::HeaderMap) -> Result<Vec<MediaType>> {
251 if let Some(accept) = headers.get(http::header::ACCEPT) {
252 let accept_str = accept
253 .to_str()
254 .map_err(|_| Error::InvalidHeader("Accept"))?;
255 let types: Vec<MediaType> = accept_str
261 .split(',')
262 .map(str::trim)
263 .map(|entry| {
264 let kept: Vec<&str> = entry
265 .split(';')
266 .map(str::trim)
267 .filter(|part| !part.starts_with("q="))
268 .collect();
269 parse_media_type(&kept.join(";"))
270 })
271 .collect();
272 if types.is_empty() {
273 return Ok(vec![MediaType::ApplicationJson]);
274 }
275 return Ok(types);
276 }
277 Ok(vec![MediaType::ApplicationJson])
278}
279
280fn parse_media_type(s: &str) -> MediaType {
285 let mut parts = s.split(';').map(str::trim);
286 let base = parts.next().unwrap_or(s);
287 let parameters: Vec<(String, &str)> = parts
291 .filter_map(|part| part.split_once('='))
292 .map(|(key, value)| {
293 (
294 key.trim().to_ascii_lowercase(),
295 value.trim().trim_matches('"'),
296 )
297 })
298 .collect();
299 let param = |name: &str| {
300 parameters
301 .iter()
302 .find(|(key, _)| key == name)
303 .map(|(_, value)| *value)
304 };
305 let given = |parameter: &str| match parameter.split_once('=') {
306 Some((name, value)) => param(name) == Some(value),
307 None => false,
308 };
309
310 match base {
311 "application/json" => MediaType::ApplicationJson,
312 "application/geo+json" => MediaType::GeoJson,
313 "text/csv" => MediaType::TextCsv,
314 "text/plain" => MediaType::TextPlain,
315 "text/xml" => MediaType::TextXml,
316 "application/openapi+json" => MediaType::OpenApi,
317 "application/x-www-form-urlencoded" => MediaType::UrlEncoded,
318 "application/octet-stream" => MediaType::OctetStream,
319 "*/*" => MediaType::Any,
320 s if s.starts_with("application/vnd.pgrst.plan") => {
327 let format = match s.ends_with("+json") {
328 true => PlanFormat::Json,
329 false => PlanFormat::Text,
330 };
331 let requested = param("options").unwrap_or_default();
332 let options = [
335 ("analyze", PlanOption::Analyze),
336 ("verbose", PlanOption::Verbose),
337 ("settings", PlanOption::Settings),
338 ("buffers", PlanOption::Buffers),
339 ("wal", PlanOption::Wal),
340 ]
341 .into_iter()
342 .filter(|(name, _)| requested.split('|').any(|asked| asked == *name))
343 .map(|(_, option)| option)
344 .collect();
345 MediaType::Plan {
346 base: Box::new(parse_media_type(param("for").unwrap_or("application/json"))),
347 format,
348 options,
349 }
350 }
351 s if s.starts_with("application/vnd.pgrst.object") => MediaType::SingularJson {
352 nullable: given("nulls=null"),
353 strip_nulls: given("nulls=stripped"),
354 },
355 s if s.starts_with("application/vnd.pgrst.array") => MediaType::ArrayJson {
356 strip_nulls: given("nulls=stripped"),
357 },
358 other => MediaType::Other(other.to_string()),
359 }
360}
361
362fn parse_content_type(headers: &http::HeaderMap) -> Result<MediaType> {
364 if let Some(ct) = headers.get(http::header::CONTENT_TYPE) {
365 let ct_str = ct
366 .to_str()
367 .map_err(|_| Error::InvalidHeader("Content-Type"))?;
368 return Ok(parse_media_type(ct_str.trim()));
369 }
370 Ok(MediaType::ApplicationJson)
371}
372
373fn parse_range(headers: &http::HeaderMap) -> Result<Range> {
386 let Some(range) = headers.get(http::header::RANGE) else {
387 return Ok(Range::default());
388 };
389 let range_str = range.to_str().map_err(|_| Error::InvalidHeader("Range"))?;
390
391 let bounds = range_str
394 .split_once('=')
395 .map_or(range_str, |(_unit, bounds)| bounds)
396 .trim();
397
398 let Some((start, end)) = bounds.split_once('-') else {
399 return Ok(Range::default());
400 };
401 let Ok(start) = start.trim().parse::<i64>() else {
402 return Ok(Range::default());
403 };
404 if start < 0 {
405 return Ok(Range::default());
406 }
407
408 let end = end.trim();
409 if end.is_empty() {
410 return Ok(Range::new(start, None));
412 }
413 let Ok(end) = end.parse::<i64>() else {
414 return Ok(Range::default());
415 };
416 if end < start {
417 return Err(Error::InvalidRange(
418 "Requested range not satisfiable".into(),
419 ));
420 }
421
422 Ok(Range::from_bounds(start, Some(end)))
423}
424
425fn extract_headers(headers: &http::HeaderMap) -> indexmap::IndexMap<String, String> {
427 headers
428 .iter()
429 .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
430 .collect()
431}
432
433fn extract_cookies(headers: &http::HeaderMap) -> indexmap::IndexMap<String, String> {
435 headers
436 .get(http::header::COOKIE)
437 .and_then(|v| v.to_str().ok())
438 .map(|s| {
439 s.split(';')
440 .filter_map(|cookie| {
441 let (key, value) = cookie.trim().split_once('=')?;
442
443 Some((key.to_string(), value.to_string()))
444 })
445 .collect()
446 })
447 .unwrap_or_default()
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 fn range_of(value: &str) -> Result<Range> {
455 let mut headers = http::HeaderMap::new();
456 headers.insert(http::header::RANGE, value.parse().unwrap());
457 parse_range(&headers)
458 }
459
460 #[test]
462 fn a_range_header_names_the_rows_it_says() {
463 assert_eq!(range_of("0-9").unwrap(), Range::from_bounds(0, Some(9)));
464 assert_eq!(range_of("5-9").unwrap(), Range::from_bounds(5, Some(9)));
465 assert_eq!(range_of("10-19").unwrap(), Range::from_bounds(10, Some(19)));
466 assert_eq!(range_of("10-").unwrap(), Range::new(10, None));
468 assert_eq!(range_of("0-").unwrap(), Range::new(0, None));
469 assert_eq!(
471 range_of("items=5-9").unwrap(),
472 Range::from_bounds(5, Some(9))
473 );
474 }
475
476 #[test]
478 fn an_inverted_range_is_refused_rather_than_widened() {
479 assert!(matches!(range_of("9-5"), Err(Error::InvalidRange(_))));
480 }
481
482 #[test]
485 fn a_header_that_is_not_a_range_is_left_alone() {
486 for value in ["", "nonsense", "-5", "a-b", "5"] {
487 assert_eq!(range_of(value).unwrap(), Range::default(), "{value:?}");
488 }
489 }
490
491 #[test]
496 fn a_plan_media_type_is_named_back_in_full() {
497 let named = |s: &str| parse_media_type(s).to_mime();
498
499 assert_eq!(
500 named("application/vnd.pgrst.plan+json"),
501 "application/vnd.pgrst.plan+json; for=\"application/json\""
502 );
503 assert_eq!(
506 named("application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object\""),
507 "application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object+json\""
508 );
509 assert_eq!(
510 named("application/vnd.pgrst.plan; for=\"text/csv\""),
511 "application/vnd.pgrst.plan+text; for=\"text/csv\""
512 );
513 assert_eq!(
516 named("application/vnd.pgrst.plan+json; options=verbose|analyze|nonsense"),
517 "application/vnd.pgrst.plan+json; for=\"application/json\"; options=analyze|verbose"
518 );
519 }
520
521 #[test]
522 fn test_parse_resource() {
523 assert_eq!(parse_resource("/").unwrap(), Resource::Schema);
524 assert_eq!(
525 parse_resource("/users").unwrap(),
526 Resource::Relation("users".into())
527 );
528 assert_eq!(
529 parse_resource("/rpc/my_func").unwrap(),
530 Resource::Routine("my_func".into())
531 );
532 }
533
534 #[test]
535 fn test_parse_media_type() {
536 assert_eq!(
537 parse_media_type("application/json"),
538 MediaType::ApplicationJson
539 );
540 assert_eq!(parse_media_type("text/csv"), MediaType::TextCsv);
541 assert_eq!(parse_media_type("*/*"), MediaType::Any);
542 }
543}