postgrest_parser/parser/
schema.rs1use crate::ast::ResolvedTable;
2use crate::error::{Error, ParseError};
3use std::collections::HashMap;
4
5pub fn resolve_schema(
38 table: &str,
39 method: &str,
40 headers: Option<&HashMap<String, String>>,
41) -> Result<ResolvedTable, Error> {
42 if table.is_empty() {
43 return Err(Error::Parse(ParseError::InvalidTableName(
44 "Table name cannot be empty".to_string(),
45 )));
46 }
47
48 if let Some((schema, name)) = parse_qualified_table(table)? {
50 validate_identifier(&schema)?;
51 validate_identifier(&name)?;
52 return Ok(ResolvedTable::new(schema, name));
53 }
54
55 validate_identifier(table)?;
57
58 let schema = get_profile_header(method, headers).unwrap_or_else(|| "public".to_string());
59 validate_identifier(&schema)?;
60
61 Ok(ResolvedTable::new(schema, table))
62}
63
64pub fn parse_qualified_table(table: &str) -> Result<Option<(String, String)>, Error> {
80 let parts: Vec<&str> = table.split('.').collect();
81
82 match parts.len() {
83 1 => Ok(None),
84 2 => {
85 let schema = parts[0].trim();
86 let name = parts[1].trim();
87
88 if schema.is_empty() || name.is_empty() {
89 return Err(Error::Parse(ParseError::InvalidTableName(format!(
90 "Invalid qualified table name: '{}'",
91 table
92 ))));
93 }
94
95 Ok(Some((schema.to_string(), name.to_string())))
96 }
97 _ => Err(Error::Parse(ParseError::InvalidTableName(format!(
98 "Invalid table name format: '{}'. Expected 'table' or 'schema.table'",
99 table
100 )))),
101 }
102}
103
104pub fn get_profile_header(
126 method: &str,
127 headers: Option<&HashMap<String, String>>,
128) -> Option<String> {
129 let headers = headers?;
130
131 let header_name = match method.to_uppercase().as_str() {
132 "GET" => "Accept-Profile",
133 "POST" | "PATCH" | "DELETE" => "Content-Profile",
134 _ => return None,
135 };
136
137 headers
139 .get(header_name)
140 .or_else(|| {
141 let lowercase = header_name.to_lowercase();
142 headers
143 .iter()
144 .find(|(k, _)| k.to_lowercase() == lowercase)
145 .map(|(_, v)| v)
146 })
147 .map(|s| s.trim().to_string())
148 .filter(|s| !s.is_empty())
149}
150
151fn validate_identifier(identifier: &str) -> Result<(), Error> {
158 if identifier.is_empty() {
159 return Err(Error::Parse(ParseError::InvalidTableName(
160 "Identifier cannot be empty".to_string(),
161 )));
162 }
163
164 let first_char = identifier.chars().next().unwrap();
165 if !first_char.is_alphabetic() && first_char != '_' {
166 return Err(Error::Parse(ParseError::InvalidSchema(format!(
167 "Identifier '{}' must start with a letter or underscore",
168 identifier
169 ))));
170 }
171
172 for ch in identifier.chars() {
173 if !ch.is_alphanumeric() && ch != '_' {
174 return Err(Error::Parse(ParseError::InvalidSchema(format!(
175 "Identifier '{}' contains invalid character '{}'",
176 identifier, ch
177 ))));
178 }
179 }
180
181 Ok(())
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn test_resolve_schema_explicit_in_table_name() {
190 let table = resolve_schema("auth.users", "GET", None).unwrap();
191 assert_eq!(table.schema, "auth");
192 assert_eq!(table.name, "users");
193 }
194
195 #[test]
196 fn test_resolve_schema_with_accept_profile_header() {
197 let mut headers = HashMap::new();
198 headers.insert("Accept-Profile".to_string(), "myschema".to_string());
199 let table = resolve_schema("users", "GET", Some(&headers)).unwrap();
200 assert_eq!(table.schema, "myschema");
201 assert_eq!(table.name, "users");
202 }
203
204 #[test]
205 fn test_resolve_schema_with_content_profile_header() {
206 let mut headers = HashMap::new();
207 headers.insert("Content-Profile".to_string(), "auth".to_string());
208 let table = resolve_schema("users", "POST", Some(&headers)).unwrap();
209 assert_eq!(table.schema, "auth");
210 assert_eq!(table.name, "users");
211 }
212
213 #[test]
214 fn test_resolve_schema_default_public() {
215 let table = resolve_schema("users", "POST", None).unwrap();
216 assert_eq!(table.schema, "public");
217 assert_eq!(table.name, "users");
218 }
219
220 #[test]
221 fn test_resolve_schema_explicit_overrides_header() {
222 let mut headers = HashMap::new();
223 headers.insert("Accept-Profile".to_string(), "other".to_string());
224 let table = resolve_schema("auth.users", "GET", Some(&headers)).unwrap();
225 assert_eq!(table.schema, "auth"); assert_eq!(table.name, "users");
227 }
228
229 #[test]
230 fn test_resolve_schema_empty_table_name() {
231 let result = resolve_schema("", "GET", None);
232 assert!(result.is_err());
233 }
234
235 #[test]
236 fn test_parse_qualified_table_with_schema() {
237 let result = parse_qualified_table("auth.users").unwrap();
238 assert_eq!(result, Some(("auth".to_string(), "users".to_string())));
239 }
240
241 #[test]
242 fn test_parse_qualified_table_without_schema() {
243 let result = parse_qualified_table("users").unwrap();
244 assert_eq!(result, None);
245 }
246
247 #[test]
248 fn test_parse_qualified_table_multiple_dots() {
249 let result = parse_qualified_table("schema.table.extra");
250 assert!(result.is_err());
251 }
252
253 #[test]
254 fn test_parse_qualified_table_empty_parts() {
255 let result = parse_qualified_table(".users");
256 assert!(result.is_err());
257
258 let result = parse_qualified_table("schema.");
259 assert!(result.is_err());
260 }
261
262 #[test]
263 fn test_get_profile_header_accept_profile() {
264 let mut headers = HashMap::new();
265 headers.insert("Accept-Profile".to_string(), "myschema".to_string());
266 let schema = get_profile_header("GET", Some(&headers));
267 assert_eq!(schema, Some("myschema".to_string()));
268 }
269
270 #[test]
271 fn test_get_profile_header_content_profile_post() {
272 let mut headers = HashMap::new();
273 headers.insert("Content-Profile".to_string(), "auth".to_string());
274 let schema = get_profile_header("POST", Some(&headers));
275 assert_eq!(schema, Some("auth".to_string()));
276 }
277
278 #[test]
279 fn test_get_profile_header_content_profile_patch() {
280 let mut headers = HashMap::new();
281 headers.insert("Content-Profile".to_string(), "auth".to_string());
282 let schema = get_profile_header("PATCH", Some(&headers));
283 assert_eq!(schema, Some("auth".to_string()));
284 }
285
286 #[test]
287 fn test_get_profile_header_content_profile_delete() {
288 let mut headers = HashMap::new();
289 headers.insert("Content-Profile".to_string(), "auth".to_string());
290 let schema = get_profile_header("DELETE", Some(&headers));
291 assert_eq!(schema, Some("auth".to_string()));
292 }
293
294 #[test]
295 fn test_get_profile_header_no_headers() {
296 let schema = get_profile_header("GET", None);
297 assert_eq!(schema, None);
298 }
299
300 #[test]
301 fn test_get_profile_header_empty_value() {
302 let mut headers = HashMap::new();
303 headers.insert("Accept-Profile".to_string(), "".to_string());
304 let schema = get_profile_header("GET", Some(&headers));
305 assert_eq!(schema, None);
306 }
307
308 #[test]
309 fn test_get_profile_header_whitespace_trimmed() {
310 let mut headers = HashMap::new();
311 headers.insert("Accept-Profile".to_string(), " myschema ".to_string());
312 let schema = get_profile_header("GET", Some(&headers));
313 assert_eq!(schema, Some("myschema".to_string()));
314 }
315
316 #[test]
317 fn test_get_profile_header_case_insensitive() {
318 let mut headers = HashMap::new();
319 headers.insert("accept-profile".to_string(), "myschema".to_string());
320 let schema = get_profile_header("GET", Some(&headers));
321 assert_eq!(schema, Some("myschema".to_string()));
322 }
323
324 #[test]
325 fn test_validate_identifier_valid() {
326 assert!(validate_identifier("users").is_ok());
327 assert!(validate_identifier("_users").is_ok());
328 assert!(validate_identifier("users_table").is_ok());
329 assert!(validate_identifier("users123").is_ok());
330 assert!(validate_identifier("MyTable").is_ok());
331 }
332
333 #[test]
334 fn test_validate_identifier_invalid_start() {
335 assert!(validate_identifier("123users").is_err());
336 assert!(validate_identifier("-users").is_err());
337 }
338
339 #[test]
340 fn test_validate_identifier_invalid_chars() {
341 assert!(validate_identifier("users-table").is_err());
342 assert!(validate_identifier("users.table").is_err());
343 assert!(validate_identifier("users@table").is_err());
344 }
345
346 #[test]
347 fn test_validate_identifier_empty() {
348 assert!(validate_identifier("").is_err());
349 }
350
351 #[test]
352 fn test_resolved_table_qualified_name() {
353 let table = ResolvedTable::new("auth", "users");
354 assert_eq!(table.qualified_name(), "\"auth\".\"users\"");
355 }
356}