postrust_core/api_request/
mod.rs1pub mod types;
7pub mod query_params;
8pub mod payload;
9pub mod preferences;
10
11pub use types::*;
12pub use query_params::parse_query_params;
13pub use preferences::parse_preferences;
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.to_str().map_err(|_| Error::InvalidHeader("Accept-Profile"))?;
112 if !schemas.contains(&schema.to_string()) {
113 return Err(Error::UnacceptableSchema(schema.into()));
114 }
115 return Ok((schema.to_string(), true));
116 }
117
118 if let Some(profile) = req.headers().get("content-profile") {
120 let schema = profile.to_str().map_err(|_| Error::InvalidHeader("Content-Profile"))?;
121 if !schemas.contains(&schema.to_string()) {
122 return Err(Error::UnacceptableSchema(schema.into()));
123 }
124 return Ok((schema.to_string(), true));
125 }
126
127 Ok((default_schema.to_string(), false))
128}
129
130fn parse_action(method: &Method, resource: &Resource, schema: &str) -> Result<Action> {
132 match (method, resource) {
133 (&Method::GET, Resource::Schema) => Ok(Action::Db(DbAction::SchemaRead {
135 schema: schema.to_string(),
136 headers_only: false,
137 })),
138 (&Method::HEAD, Resource::Schema) => Ok(Action::Db(DbAction::SchemaRead {
139 schema: schema.to_string(),
140 headers_only: true,
141 })),
142 (&Method::OPTIONS, Resource::Schema) => Ok(Action::SchemaInfo),
143
144 (&Method::GET, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationRead {
146 qi: QualifiedIdentifier::new(schema, name),
147 headers_only: false,
148 })),
149 (&Method::HEAD, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationRead {
150 qi: QualifiedIdentifier::new(schema, name),
151 headers_only: true,
152 })),
153 (&Method::POST, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
154 qi: QualifiedIdentifier::new(schema, name),
155 mutation: Mutation::Create,
156 })),
157 (&Method::PATCH, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
158 qi: QualifiedIdentifier::new(schema, name),
159 mutation: Mutation::Update,
160 })),
161 (&Method::PUT, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
162 qi: QualifiedIdentifier::new(schema, name),
163 mutation: Mutation::SingleUpsert,
164 })),
165 (&Method::DELETE, Resource::Relation(name)) => Ok(Action::Db(DbAction::RelationMut {
166 qi: QualifiedIdentifier::new(schema, name),
167 mutation: Mutation::Delete,
168 })),
169 (&Method::OPTIONS, Resource::Relation(name)) => {
170 Ok(Action::RelationInfo(QualifiedIdentifier::new(schema, name)))
171 }
172
173 (&Method::GET, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
175 qi: QualifiedIdentifier::new(schema, name),
176 invoke_method: InvokeMethod::InvRead { headers_only: false },
177 })),
178 (&Method::HEAD, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
179 qi: QualifiedIdentifier::new(schema, name),
180 invoke_method: InvokeMethod::InvRead { headers_only: true },
181 })),
182 (&Method::POST, Resource::Routine(name)) => Ok(Action::Db(DbAction::Routine {
183 qi: QualifiedIdentifier::new(schema, name),
184 invoke_method: InvokeMethod::Inv,
185 })),
186 (&Method::OPTIONS, Resource::Routine(name)) => Ok(Action::RoutineInfo {
187 qi: QualifiedIdentifier::new(schema, name),
188 invoke_method: InvokeMethod::Inv,
189 }),
190
191 _ => Err(Error::UnsupportedMethod(method.to_string())),
193 }
194}
195
196fn parse_accept(headers: &http::HeaderMap) -> Result<Vec<MediaType>> {
198 if let Some(accept) = headers.get(http::header::ACCEPT) {
199 let accept_str = accept.to_str().map_err(|_| Error::InvalidHeader("Accept"))?;
200 let types: Vec<MediaType> = accept_str
202 .split(',')
203 .map(|s| s.trim())
204 .map(|s| s.split(';').next().unwrap_or(s).trim())
205 .map(parse_media_type)
206 .collect();
207 if types.is_empty() {
208 return Ok(vec![MediaType::ApplicationJson]);
209 }
210 return Ok(types);
211 }
212 Ok(vec![MediaType::ApplicationJson])
213}
214
215fn parse_media_type(s: &str) -> MediaType {
217 match s {
218 "application/json" => MediaType::ApplicationJson,
219 "application/geo+json" => MediaType::GeoJson,
220 "text/csv" => MediaType::TextCsv,
221 "text/plain" => MediaType::TextPlain,
222 "text/xml" => MediaType::TextXml,
223 "application/openapi+json" => MediaType::OpenApi,
224 "application/x-www-form-urlencoded" => MediaType::UrlEncoded,
225 "application/octet-stream" => MediaType::OctetStream,
226 "*/*" => MediaType::Any,
227 s if s.starts_with("application/vnd.pgrst.object") => {
228 MediaType::SingularJson { nullable: s.contains("nulls=null") }
229 }
230 s if s.starts_with("application/vnd.pgrst.array") => MediaType::ArrayJsonStrip,
231 other => MediaType::Other(other.to_string()),
232 }
233}
234
235fn parse_content_type(headers: &http::HeaderMap) -> Result<MediaType> {
237 if let Some(ct) = headers.get(http::header::CONTENT_TYPE) {
238 let ct_str = ct.to_str().map_err(|_| Error::InvalidHeader("Content-Type"))?;
239 let media_type = ct_str.split(';').next().unwrap_or(ct_str).trim();
240 return Ok(parse_media_type(media_type));
241 }
242 Ok(MediaType::ApplicationJson)
243}
244
245fn parse_range(headers: &http::HeaderMap) -> Result<Range> {
247 if let Some(range) = headers.get(http::header::RANGE) {
248 let range_str = range.to_str().map_err(|_| Error::InvalidHeader("Range"))?;
249 if let Some(range_value) = range_str.strip_prefix("0-") {
251 if range_value.is_empty() {
252 return Ok(Range::new(0, None));
253 }
254 if let Ok(end) = range_value.parse::<i64>() {
255 return Ok(Range::from_bounds(0, Some(end)));
256 }
257 }
258 }
260 Ok(Range::default())
261}
262
263fn extract_headers(headers: &http::HeaderMap) -> indexmap::IndexMap<String, String> {
265 headers
266 .iter()
267 .filter_map(|(k, v)| {
268 v.to_str().ok().map(|v| (k.to_string(), v.to_string()))
269 })
270 .collect()
271}
272
273fn extract_cookies(headers: &http::HeaderMap) -> indexmap::IndexMap<String, String> {
275 headers
276 .get(http::header::COOKIE)
277 .and_then(|v| v.to_str().ok())
278 .map(|s| {
279 s.split(';')
280 .filter_map(|cookie| {
281 let mut parts = cookie.trim().splitn(2, '=');
282 let key = parts.next()?;
283 let value = parts.next()?;
284 Some((key.to_string(), value.to_string()))
285 })
286 .collect()
287 })
288 .unwrap_or_default()
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_parse_resource() {
297 assert_eq!(parse_resource("/").unwrap(), Resource::Schema);
298 assert_eq!(
299 parse_resource("/users").unwrap(),
300 Resource::Relation("users".into())
301 );
302 assert_eq!(
303 parse_resource("/rpc/my_func").unwrap(),
304 Resource::Routine("my_func".into())
305 );
306 }
307
308 #[test]
309 fn test_parse_media_type() {
310 assert_eq!(parse_media_type("application/json"), MediaType::ApplicationJson);
311 assert_eq!(parse_media_type("text/csv"), MediaType::TextCsv);
312 assert_eq!(parse_media_type("*/*"), MediaType::Any);
313 }
314}