pub mod payload;
pub mod preferences;
pub mod query_params;
pub mod types;
pub use preferences::parse_preferences;
pub use query_params::{parse_query_params, value_is_filter};
pub use types::*;
use crate::error::{Error, Result};
use http::{Method, Request};
use percent_encoding::percent_decode_str;
use std::collections::{HashMap, HashSet};
pub fn parse_request<B>(
req: &Request<B>,
default_schema: &str,
schemas: &[String],
max_rows: Option<i64>,
) -> Result<ApiRequest>
where
B: AsRef<[u8]>,
{
let method = req.method();
let path = req.uri().path();
let query = req.uri().query().unwrap_or("");
let resource = parse_resource(path)?;
let (schema, negotiated_by_profile) = parse_schema(req, default_schema, schemas)?;
let action = parse_action(method, &resource, &schema)?;
let is_rpc = matches!(
action,
Action::Db(DbAction::Routine {
invoke_method: InvokeMethod::InvRead { .. },
..
}) | Action::RoutineInfo { .. }
);
let query_params = parse_query_params(query, is_rpc)?;
let preferences = parse_preferences(req.headers())?;
let accept_media_types = parse_accept(req.headers())?;
let content_media_type = parse_content_type(req.headers())?;
let top_level_range = parse_range(req.headers())?;
let headers = extract_headers(req.headers());
let cookies = extract_cookies(req.headers());
Ok(ApiRequest {
action,
schema,
payload: None, query_params,
accept_media_types,
content_media_type,
preferences,
columns: HashSet::new(),
top_level_range,
range_map: HashMap::new(),
max_rows,
negotiated_by_profile,
method: method.to_string(),
path: path.to_string(),
headers,
cookies,
})
}
fn parse_resource(path: &str) -> Result<Resource> {
let path = path.trim_start_matches('/');
if path.is_empty() {
return Ok(Resource::Schema);
}
let path = path.trim_end_matches('/');
if path.is_empty() {
return Ok(Resource::Schema);
}
let decoded = |segment: &str| -> String {
percent_decode_str(segment)
.decode_utf8()
.map(|name| name.into_owned())
.unwrap_or_else(|_| segment.to_string())
};
if let Some(func_name) = path.strip_prefix("rpc/") {
if func_name.is_empty() {
return Err(Error::InvalidPath("Empty function name".into()));
}
if func_name.contains('/') {
return Err(Error::InvalidResourcePath);
}
return Ok(Resource::Routine(decoded(func_name)));
}
if path.contains('/') {
return Err(Error::InvalidResourcePath);
}
Ok(Resource::Relation(decoded(path)))
}
fn parse_schema<B>(
req: &Request<B>,
default_schema: &str,
schemas: &[String],
) -> Result<(String, bool)> {
if let Some(profile) = req.headers().get("accept-profile") {
let schema = profile
.to_str()
.map_err(|_| Error::InvalidHeader("Accept-Profile"))?;
if !schemas.contains(&schema.to_string()) {
return Err(Error::UnacceptableSchema {
requested: schema.into(),
exposed: schemas.to_vec(),
});
}
return Ok((schema.to_string(), true));
}
if let Some(profile) = req.headers().get("content-profile") {
let schema = profile
.to_str()
.map_err(|_| Error::InvalidHeader("Content-Profile"))?;
if !schemas.contains(&schema.to_string()) {
return Err(Error::UnacceptableSchema {
requested: schema.into(),
exposed: schemas.to_vec(),
});
}
return Ok((schema.to_string(), true));
}
Ok((default_schema.to_string(), false))
}
fn parse_action(method: &Method, resource: &Resource, schema: &str) -> Result<Action> {
match (method, resource) {
(&Method::GET, Resource::Schema) => Ok(Action::Db(DbAction::SchemaRead {
schema: schema.to_string(),
headers_only: false,
})),
(&Method::HEAD, Resource::Schema) => Ok(Action::Db(DbAction::SchemaRead {
schema: schema.to_string(),
headers_only: true,
})),
(&Method::OPTIONS, Resource::Schema) => Ok(Action::SchemaInfo),
(&Method::GET, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationRead {
qi: QualifiedIdentifier::new(schema, name),
headers_only: false,
})),
(&Method::HEAD, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationRead {
qi: QualifiedIdentifier::new(schema, name),
headers_only: true,
})),
(&Method::POST, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
qi: QualifiedIdentifier::new(schema, name),
mutation: Mutation::Create,
})),
(&Method::PATCH, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
qi: QualifiedIdentifier::new(schema, name),
mutation: Mutation::Update,
})),
(&Method::PUT, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
qi: QualifiedIdentifier::new(schema, name),
mutation: Mutation::SingleUpsert,
})),
(&Method::DELETE, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
qi: QualifiedIdentifier::new(schema, name),
mutation: Mutation::Delete,
})),
(&Method::OPTIONS, Resource::Relation(name)) => {
Ok(Action::RelationInfo(QualifiedIdentifier::new(schema, name)))
}
(&Method::GET, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
qi: QualifiedIdentifier::new(schema, name),
invoke_method: InvokeMethod::InvRead {
headers_only: false,
},
})),
(&Method::HEAD, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
qi: QualifiedIdentifier::new(schema, name),
invoke_method: InvokeMethod::InvRead { headers_only: true },
})),
(&Method::POST, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
qi: QualifiedIdentifier::new(schema, name),
invoke_method: InvokeMethod::Inv,
})),
(&Method::OPTIONS, Resource::Routine(name)) => Ok(Action::RoutineInfo {
qi: QualifiedIdentifier::new(schema, name),
invoke_method: InvokeMethod::Inv,
}),
(_, Resource::Routine(_)) => Err(Error::InvalidRpcMethod(method.to_string())),
_ => Err(Error::UnsupportedMethod(method.to_string())),
}
}
fn parse_accept(headers: &http::HeaderMap) -> Result<Vec<MediaType>> {
if let Some(accept) = headers.get(http::header::ACCEPT) {
let accept_str = accept
.to_str()
.map_err(|_| Error::InvalidHeader("Accept"))?;
let types: Vec<MediaType> = accept_str
.split(',')
.map(str::trim)
.map(|entry| {
let kept: Vec<&str> = entry
.split(';')
.map(str::trim)
.filter(|part| !part.starts_with("q="))
.collect();
parse_media_type(&kept.join(";"))
})
.collect();
if types.is_empty() {
return Ok(vec![MediaType::ApplicationJson]);
}
return Ok(types);
}
Ok(vec![MediaType::ApplicationJson])
}
fn parse_media_type(s: &str) -> MediaType {
let mut parts = s.split(';').map(str::trim);
let base = parts.next().unwrap_or(s);
let parameters: Vec<(String, &str)> = parts
.filter_map(|part| part.split_once('='))
.map(|(key, value)| {
(
key.trim().to_ascii_lowercase(),
value.trim().trim_matches('"'),
)
})
.collect();
let param = |name: &str| {
parameters
.iter()
.find(|(key, _)| key == name)
.map(|(_, value)| *value)
};
let given = |parameter: &str| match parameter.split_once('=') {
Some((name, value)) => param(name) == Some(value),
None => false,
};
match base {
"application/json" => MediaType::ApplicationJson,
"application/geo+json" => MediaType::GeoJson,
"text/csv" => MediaType::TextCsv,
"text/plain" => MediaType::TextPlain,
"text/xml" => MediaType::TextXml,
"application/openapi+json" => MediaType::OpenApi,
"application/x-www-form-urlencoded" => MediaType::UrlEncoded,
"application/octet-stream" => MediaType::OctetStream,
"*/*" => MediaType::Any,
s if s.starts_with("application/vnd.pgrst.plan") => {
let format = match s.ends_with("+json") {
true => PlanFormat::Json,
false => PlanFormat::Text,
};
let requested = param("options").unwrap_or_default();
let options = [
("analyze", PlanOption::Analyze),
("verbose", PlanOption::Verbose),
("settings", PlanOption::Settings),
("buffers", PlanOption::Buffers),
("wal", PlanOption::Wal),
]
.into_iter()
.filter(|(name, _)| requested.split('|').any(|asked| asked == *name))
.map(|(_, option)| option)
.collect();
MediaType::Plan {
base: Box::new(parse_media_type(param("for").unwrap_or("application/json"))),
format,
options,
}
}
s if s.starts_with("application/vnd.pgrst.object") => MediaType::SingularJson {
nullable: given("nulls=null"),
strip_nulls: given("nulls=stripped"),
},
s if s.starts_with("application/vnd.pgrst.array") => MediaType::ArrayJson {
strip_nulls: given("nulls=stripped"),
},
other => MediaType::Other(other.to_string()),
}
}
fn parse_content_type(headers: &http::HeaderMap) -> Result<MediaType> {
if let Some(ct) = headers.get(http::header::CONTENT_TYPE) {
let ct_str = ct
.to_str()
.map_err(|_| Error::InvalidHeader("Content-Type"))?;
return Ok(parse_media_type(ct_str.trim()));
}
Ok(MediaType::ApplicationJson)
}
fn parse_range(headers: &http::HeaderMap) -> Result<Range> {
let Some(range) = headers.get(http::header::RANGE) else {
return Ok(Range::default());
};
let range_str = range.to_str().map_err(|_| Error::InvalidHeader("Range"))?;
let bounds = range_str
.split_once('=')
.map_or(range_str, |(_unit, bounds)| bounds)
.trim();
let Some((start, end)) = bounds.split_once('-') else {
return Ok(Range::default());
};
let Ok(start) = start.trim().parse::<i64>() else {
return Ok(Range::default());
};
if start < 0 {
return Ok(Range::default());
}
let end = end.trim();
if end.is_empty() {
return Ok(Range::new(start, None));
}
let Ok(end) = end.parse::<i64>() else {
return Ok(Range::default());
};
if end < start {
return Err(Error::InvalidRange(
"Requested range not satisfiable".into(),
));
}
Ok(Range::from_bounds(start, Some(end)))
}
fn extract_headers(headers: &http::HeaderMap) -> indexmap::IndexMap<String, String> {
headers
.iter()
.filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
.collect()
}
fn extract_cookies(headers: &http::HeaderMap) -> indexmap::IndexMap<String, String> {
headers
.get(http::header::COOKIE)
.and_then(|v| v.to_str().ok())
.map(|s| {
s.split(';')
.filter_map(|cookie| {
let (key, value) = cookie.trim().split_once('=')?;
Some((key.to_string(), value.to_string()))
})
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn range_of(value: &str) -> Result<Range> {
let mut headers = http::HeaderMap::new();
headers.insert(http::header::RANGE, value.parse().unwrap());
parse_range(&headers)
}
#[test]
fn a_range_header_names_the_rows_it_says() {
assert_eq!(range_of("0-9").unwrap(), Range::from_bounds(0, Some(9)));
assert_eq!(range_of("5-9").unwrap(), Range::from_bounds(5, Some(9)));
assert_eq!(range_of("10-19").unwrap(), Range::from_bounds(10, Some(19)));
assert_eq!(range_of("10-").unwrap(), Range::new(10, None));
assert_eq!(range_of("0-").unwrap(), Range::new(0, None));
assert_eq!(
range_of("items=5-9").unwrap(),
Range::from_bounds(5, Some(9))
);
}
#[test]
fn an_inverted_range_is_refused_rather_than_widened() {
assert!(matches!(range_of("9-5"), Err(Error::InvalidRange(_))));
}
#[test]
fn a_header_that_is_not_a_range_is_left_alone() {
for value in ["", "nonsense", "-5", "a-b", "5"] {
assert_eq!(range_of(value).unwrap(), Range::default(), "{value:?}");
}
}
#[test]
fn a_plan_media_type_is_named_back_in_full() {
let named = |s: &str| parse_media_type(s).to_mime();
assert_eq!(
named("application/vnd.pgrst.plan+json"),
"application/vnd.pgrst.plan+json; for=\"application/json\""
);
assert_eq!(
named("application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object\""),
"application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object+json\""
);
assert_eq!(
named("application/vnd.pgrst.plan; for=\"text/csv\""),
"application/vnd.pgrst.plan+text; for=\"text/csv\""
);
assert_eq!(
named("application/vnd.pgrst.plan+json; options=verbose|analyze|nonsense"),
"application/vnd.pgrst.plan+json; for=\"application/json\"; options=analyze|verbose"
);
}
#[test]
fn test_parse_resource() {
assert_eq!(parse_resource("/").unwrap(), Resource::Schema);
assert_eq!(
parse_resource("/users").unwrap(),
Resource::Relation("users".into())
);
assert_eq!(
parse_resource("/rpc/my_func").unwrap(),
Resource::Routine("my_func".into())
);
}
#[test]
fn test_parse_media_type() {
assert_eq!(
parse_media_type("application/json"),
MediaType::ApplicationJson
);
assert_eq!(parse_media_type("text/csv"), MediaType::TextCsv);
assert_eq!(parse_media_type("*/*"), MediaType::Any);
}
}