postrust_core/api_request/
mod.rs1pub 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;
13pub use types::*;
14
15use crate::error::{Error, Result};
16use http::{Method, Request};
17use std::collections::{HashMap, HashSet};
18
19pub fn parse_request<B>(
25 req: &Request<B>,
26 default_schema: &str,
27 schemas: &[String],
28 max_rows: Option<i64>,
29) -> Result<ApiRequest>
30where
31 B: AsRef<[u8]>,
32{
33 let method = req.method();
34 let path = req.uri().path();
35 let query = req.uri().query().unwrap_or("");
36
37 let resource = parse_resource(path)?;
39
40 let (schema, negotiated_by_profile) = parse_schema(req, default_schema, schemas)?;
42
43 let action = parse_action(method, &resource, &schema)?;
45
46 let query_params = parse_query_params(query)?;
48
49 let preferences = parse_preferences(req.headers())?;
51
52 let accept_media_types = parse_accept(req.headers())?;
54
55 let content_media_type = parse_content_type(req.headers())?;
57
58 let top_level_range = parse_range(req.headers())?;
60
61 let headers = extract_headers(req.headers());
63 let cookies = extract_cookies(req.headers());
64
65 Ok(ApiRequest {
66 action,
67 schema,
68 payload: None, query_params,
70 accept_media_types,
71 content_media_type,
72 preferences,
73 columns: HashSet::new(),
74 top_level_range,
75 range_map: HashMap::new(),
76 max_rows,
77 negotiated_by_profile,
78 method: method.to_string(),
79 path: path.to_string(),
80 headers,
81 cookies,
82 })
83}
84
85fn parse_resource(path: &str) -> Result<Resource> {
87 let path = path.trim_start_matches('/');
88
89 if path.is_empty() {
90 return Ok(Resource::Schema);
91 }
92
93 if let Some(func_name) = path.strip_prefix("rpc/") {
94 if func_name.is_empty() {
95 return Err(Error::InvalidPath("Empty function name".into()));
96 }
97 return Ok(Resource::Routine(func_name.to_string()));
98 }
99
100 let name = path.split('/').next().unwrap_or(path);
102 if name.is_empty() {
103 return Err(Error::InvalidPath("Empty resource name".into()));
104 }
105
106 Ok(Resource::Relation(name.to_string()))
107}
108
109fn parse_schema<B>(
111 req: &Request<B>,
112 default_schema: &str,
113 schemas: &[String],
114) -> Result<(String, bool)> {
115 if let Some(profile) = req.headers().get("accept-profile") {
117 let schema = profile
118 .to_str()
119 .map_err(|_| Error::InvalidHeader("Accept-Profile"))?;
120 if !schemas.contains(&schema.to_string()) {
121 return Err(Error::UnacceptableSchema(schema.into()));
122 }
123 return Ok((schema.to_string(), true));
124 }
125
126 if let Some(profile) = req.headers().get("content-profile") {
128 let schema = profile
129 .to_str()
130 .map_err(|_| Error::InvalidHeader("Content-Profile"))?;
131 if !schemas.contains(&schema.to_string()) {
132 return Err(Error::UnacceptableSchema(schema.into()));
133 }
134 return Ok((schema.to_string(), true));
135 }
136
137 Ok((default_schema.to_string(), false))
138}
139
140fn parse_action(method: &Method, resource: &Resource, schema: &str) -> Result<Action> {
142 match (method, resource) {
143 (&Method::GET, Resource::Schema) => Ok(Action::Db(DbAction::SchemaRead {
145 schema: schema.to_string(),
146 headers_only: false,
147 })),
148 (&Method::HEAD, Resource::Schema) => Ok(Action::Db(DbAction::SchemaRead {
149 schema: schema.to_string(),
150 headers_only: true,
151 })),
152 (&Method::OPTIONS, Resource::Schema) => Ok(Action::SchemaInfo),
153
154 (&Method::GET, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationRead {
156 qi: QualifiedIdentifier::new(schema, name),
157 headers_only: false,
158 })),
159 (&Method::HEAD, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationRead {
160 qi: QualifiedIdentifier::new(schema, name),
161 headers_only: true,
162 })),
163 (&Method::POST, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
164 qi: QualifiedIdentifier::new(schema, name),
165 mutation: Mutation::Create,
166 })),
167 (&Method::PATCH, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
168 qi: QualifiedIdentifier::new(schema, name),
169 mutation: Mutation::Update,
170 })),
171 (&Method::PUT, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
172 qi: QualifiedIdentifier::new(schema, name),
173 mutation: Mutation::SingleUpsert,
174 })),
175 (&Method::DELETE, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
176 qi: QualifiedIdentifier::new(schema, name),
177 mutation: Mutation::Delete,
178 })),
179 (&Method::OPTIONS, Resource::Relation(name)) => {
180 Ok(Action::RelationInfo(QualifiedIdentifier::new(schema, name)))
181 }
182
183 (&Method::GET, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
185 qi: QualifiedIdentifier::new(schema, name),
186 invoke_method: InvokeMethod::InvRead {
187 headers_only: false,
188 },
189 })),
190 (&Method::HEAD, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
191 qi: QualifiedIdentifier::new(schema, name),
192 invoke_method: InvokeMethod::InvRead { headers_only: true },
193 })),
194 (&Method::POST, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
195 qi: QualifiedIdentifier::new(schema, name),
196 invoke_method: InvokeMethod::Inv,
197 })),
198 (&Method::OPTIONS, Resource::Routine(name)) => Ok(Action::RoutineInfo {
199 qi: QualifiedIdentifier::new(schema, name),
200 invoke_method: InvokeMethod::Inv,
201 }),
202
203 _ => Err(Error::UnsupportedMethod(method.to_string())),
205 }
206}
207
208fn parse_accept(headers: &http::HeaderMap) -> Result<Vec<MediaType>> {
210 if let Some(accept) = headers.get(http::header::ACCEPT) {
211 let accept_str = accept
212 .to_str()
213 .map_err(|_| Error::InvalidHeader("Accept"))?;
214 let types: Vec<MediaType> = accept_str
216 .split(',')
217 .map(|s| s.trim())
218 .map(|s| s.split(';').next().unwrap_or(s).trim())
219 .map(parse_media_type)
220 .collect();
221 if types.is_empty() {
222 return Ok(vec![MediaType::ApplicationJson]);
223 }
224 return Ok(types);
225 }
226 Ok(vec![MediaType::ApplicationJson])
227}
228
229fn parse_media_type(s: &str) -> MediaType {
231 match s {
232 "application/json" => MediaType::ApplicationJson,
233 "application/geo+json" => MediaType::GeoJson,
234 "text/csv" => MediaType::TextCsv,
235 "text/plain" => MediaType::TextPlain,
236 "text/xml" => MediaType::TextXml,
237 "application/openapi+json" => MediaType::OpenApi,
238 "application/x-www-form-urlencoded" => MediaType::UrlEncoded,
239 "application/octet-stream" => MediaType::OctetStream,
240 "*/*" => MediaType::Any,
241 s if s.starts_with("application/vnd.pgrst.object") => MediaType::SingularJson {
242 nullable: s.contains("nulls=null"),
243 },
244 s if s.starts_with("application/vnd.pgrst.array") => MediaType::ArrayJsonStrip,
245 other => MediaType::Other(other.to_string()),
246 }
247}
248
249fn parse_content_type(headers: &http::HeaderMap) -> Result<MediaType> {
251 if let Some(ct) = headers.get(http::header::CONTENT_TYPE) {
252 let ct_str = ct
253 .to_str()
254 .map_err(|_| Error::InvalidHeader("Content-Type"))?;
255 let media_type = ct_str.split(';').next().unwrap_or(ct_str).trim();
256 return Ok(parse_media_type(media_type));
257 }
258 Ok(MediaType::ApplicationJson)
259}
260
261fn parse_range(headers: &http::HeaderMap) -> Result<Range> {
263 if let Some(range) = headers.get(http::header::RANGE) {
264 let range_str = range.to_str().map_err(|_| Error::InvalidHeader("Range"))?;
265 if let Some(range_value) = range_str.strip_prefix("0-") {
267 if range_value.is_empty() {
268 return Ok(Range::new(0, None));
269 }
270 if let Ok(end) = range_value.parse::<i64>() {
271 return Ok(Range::from_bounds(0, Some(end)));
272 }
273 }
274 }
276 Ok(Range::default())
277}
278
279fn extract_headers(headers: &http::HeaderMap) -> indexmap::IndexMap<String, String> {
281 headers
282 .iter()
283 .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
284 .collect()
285}
286
287fn extract_cookies(headers: &http::HeaderMap) -> indexmap::IndexMap<String, String> {
289 headers
290 .get(http::header::COOKIE)
291 .and_then(|v| v.to_str().ok())
292 .map(|s| {
293 s.split(';')
294 .filter_map(|cookie| {
295 let (key, value) = cookie.trim().split_once('=')?;
296
297 Some((key.to_string(), value.to_string()))
298 })
299 .collect()
300 })
301 .unwrap_or_default()
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_parse_resource() {
310 assert_eq!(parse_resource("/").unwrap(), Resource::Schema);
311 assert_eq!(
312 parse_resource("/users").unwrap(),
313 Resource::Relation("users".into())
314 );
315 assert_eq!(
316 parse_resource("/rpc/my_func").unwrap(),
317 Resource::Routine("my_func".into())
318 );
319 }
320
321 #[test]
322 fn test_parse_media_type() {
323 assert_eq!(
324 parse_media_type("application/json"),
325 MediaType::ApplicationJson
326 );
327 assert_eq!(parse_media_type("text/csv"), MediaType::TextCsv);
328 assert_eq!(parse_media_type("*/*"), MediaType::Any);
329 }
330}