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