1use crate::{Error, Result};
12
13use std::borrow::Borrow;
14use std::fmt::{Display, Formatter};
15use std::hash::{Hash, Hasher};
16use std::iter::Peekable;
17use std::ops::Deref;
18use std::sync::LazyLock;
19
20use serde::{Deserialize, Serialize};
21
22pub static EMPTY_RESOURCE_NAME: LazyLock<ResourceName> =
26 LazyLock::new(|| ResourceName::new(&[] as &[String]));
27
28#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize)]
49#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
50#[cfg_attr(feature = "sqlx", sqlx(transparent, no_pg_array))]
51pub struct ResourceName(Vec<String>);
52
53impl ResourceName {
54 pub fn new<A>(iter: impl IntoIterator<Item = A>) -> Self
56 where
57 Self: FromIterator<A>,
58 {
59 iter.into_iter().collect()
60 }
61
62 pub fn from_naive_str_split(name: impl AsRef<str>) -> Self {
67 Self::new(name.as_ref().split(FIELD_SEPARATOR))
68 }
69
70 pub fn parse_column_name_list(names: impl AsRef<str>) -> Result<Vec<ResourceName>> {
82 let names = names.as_ref();
83 let chars = &mut names.chars().peekable();
84
85 drop_leading_whitespace(chars);
86 let mut ending = match chars.peek() {
87 Some(_) => FieldEnding::NextColumn,
88 None => FieldEnding::InputExhausted,
89 };
90
91 let mut cols = vec![];
92 while ending == FieldEnding::NextColumn {
93 let (col, new_ending) = parse_resource_name(chars)?;
94 cols.push(col);
95 ending = new_ending;
96 }
97 Ok(cols)
98 }
99
100 pub fn join(&self, right: &ResourceName) -> ResourceName {
102 [self.clone(), right.clone()].into_iter().collect()
103 }
104
105 pub fn path(&self) -> &[String] {
107 &self.0
108 }
109
110 pub fn into_inner(self) -> Vec<String> {
112 self.0
113 }
114
115 pub fn prefix_matches(&self, prefix: &ResourceName) -> bool {
143 if self.len() < prefix.len() {
144 return false;
145 }
146 prefix
147 .iter()
148 .zip(self.iter())
149 .all(|(a, b)| a.eq_ignore_ascii_case(b))
150 }
151
152 pub fn eq_ignore_ascii_case(&self, other: &ResourceName) -> bool {
168 self.len() == other.len()
169 && self
170 .iter()
171 .zip(other.iter())
172 .all(|(a, b)| a.eq_ignore_ascii_case(b))
173 }
174}
175
176impl<A: Into<String>> FromIterator<A> for ResourceName {
177 fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
178 let path = iter.into_iter().map(|s| s.into()).collect();
179 Self(path)
180 }
181}
182
183impl From<Vec<String>> for ResourceName {
184 fn from(path: Vec<String>) -> Self {
185 Self(path)
186 }
187}
188
189impl FromIterator<ResourceName> for ResourceName {
190 fn from_iter<T: IntoIterator<Item = ResourceName>>(iter: T) -> Self {
191 let path = iter.into_iter().flat_map(|c| c.into_iter()).collect();
192 Self(path)
193 }
194}
195
196impl IntoIterator for ResourceName {
197 type Item = String;
198 type IntoIter = std::vec::IntoIter<Self::Item>;
199
200 fn into_iter(self) -> Self::IntoIter {
201 self.0.into_iter()
202 }
203}
204
205impl Deref for ResourceName {
206 type Target = [String];
207
208 fn deref(&self) -> &[String] {
209 &self.0
210 }
211}
212
213impl Borrow<[String]> for ResourceName {
214 fn borrow(&self) -> &[String] {
215 self
216 }
217}
218
219impl Borrow<[String]> for &ResourceName {
220 fn borrow(&self) -> &[String] {
221 self
222 }
223}
224
225impl Hash for ResourceName {
226 fn hash<H: Hasher>(&self, hasher: &mut H) {
227 (**self).hash(hasher)
228 }
229}
230
231impl Display for ResourceName {
232 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
233 for (i, s) in self.iter().enumerate() {
234 use std::fmt::Write as _;
235
236 if i > 0 {
237 f.write_char(FIELD_SEPARATOR)?;
238 }
239
240 let digit_char = |c: char| c.is_ascii_digit();
241 if s.is_empty() || s.starts_with(digit_char) || s.contains(|c| !is_simple_char(c)) {
242 f.write_char(FIELD_ESCAPE_CHAR)?;
243 for c in s.chars() {
244 f.write_char(c)?;
245 if c == FIELD_ESCAPE_CHAR {
246 f.write_char(c)?;
247 }
248 }
249 f.write_char(FIELD_ESCAPE_CHAR)?;
250 } else {
251 f.write_str(s)?;
252 }
253 }
254 Ok(())
255 }
256}
257
258fn is_simple_char(c: char) -> bool {
259 c.is_ascii_alphanumeric() || c == '_'
260}
261
262fn drop_leading_whitespace(iter: &mut Peekable<impl Iterator<Item = char>>) {
263 while iter.next_if(|c| c.is_whitespace()).is_some() {}
264}
265
266impl std::str::FromStr for ResourceName {
267 type Err = Error;
268
269 fn from_str(s: &str) -> Result<Self> {
280 match parse_resource_name(&mut s.chars().peekable())? {
281 (_, FieldEnding::NextColumn) => Err(Error::generic("Trailing comma in column name")),
282 (col, _) => Ok(col),
283 }
284 }
285}
286
287type Chars<'a> = Peekable<std::str::Chars<'a>>;
288
289#[derive(PartialEq)]
290enum FieldEnding {
291 InputExhausted,
292 NextField,
293 NextColumn,
294}
295
296const FIELD_ESCAPE_CHAR: char = '`';
297const FIELD_SEPARATOR: char = '.';
298const COLUMN_SEPARATOR: char = ',';
299
300fn parse_resource_name(chars: &mut Chars<'_>) -> Result<(ResourceName, FieldEnding)> {
301 drop_leading_whitespace(chars);
302 let mut ending = if chars.peek().is_none() {
303 FieldEnding::InputExhausted
304 } else if chars.next_if_eq(&COLUMN_SEPARATOR).is_some() {
305 FieldEnding::NextColumn
306 } else {
307 FieldEnding::NextField
308 };
309
310 let mut path = vec![];
311 while ending == FieldEnding::NextField {
312 drop_leading_whitespace(chars);
313 let field_name = match chars.next_if_eq(&FIELD_ESCAPE_CHAR) {
314 Some(_) => parse_escaped_field_name(chars)?,
315 None => parse_simple_field_name(chars)?,
316 };
317
318 ending = match chars.find(|c| !c.is_whitespace()) {
319 None => FieldEnding::InputExhausted,
320 Some(FIELD_SEPARATOR) => FieldEnding::NextField,
321 Some(COLUMN_SEPARATOR) => FieldEnding::NextColumn,
322 Some(other) => {
323 return Err(Error::generic(format!(
324 "Invalid character {other:?} after field {field_name:?}",
325 )));
326 }
327 };
328 path.push(field_name);
329 }
330 Ok((ResourceName::new(path), ending))
331}
332
333fn parse_simple_field_name(chars: &mut Chars<'_>) -> Result<String> {
334 let mut name = String::new();
335 let mut first = true;
336 while let Some(c) = chars.next_if(|c| is_simple_char(*c)) {
337 if first && c.is_ascii_digit() {
338 return Err(Error::generic(format!(
339 "Unescaped field name cannot start with a digit {c:?}"
340 )));
341 }
342 name.push(c);
343 first = false;
344 }
345 Ok(name)
346}
347
348fn parse_escaped_field_name(chars: &mut Chars<'_>) -> Result<String> {
349 let mut name = String::new();
350 loop {
351 match chars.next() {
352 Some(FIELD_ESCAPE_CHAR) if chars.next_if_eq(&FIELD_ESCAPE_CHAR).is_none() => break,
353 Some(c) => name.push(c),
354 None => {
355 return Err(Error::generic(format!(
356 "No closing {FIELD_ESCAPE_CHAR:?} after field {name:?}"
357 )));
358 }
359 }
360 }
361 Ok(name)
362}
363
364#[cfg(test)]
365mod test {
366 use super::*;
367
368 impl ResourceName {
369 fn empty() -> Self {
370 Self::new(&[] as &[String])
371 }
372 }
373
374 #[test]
375 fn test_column_name_methods() {
376 let simple: ResourceName = "x".parse().unwrap();
377 let nested: ResourceName = "x.y".parse().unwrap();
378
379 assert_eq!(simple.path(), ["x"]);
380 assert_eq!(nested.path(), ["x", "y"]);
381
382 assert_eq!(simple.clone().into_inner(), ["x"]);
383 assert_eq!(nested.clone().into_inner(), ["x", "y"]);
384
385 let name: &[String] = &nested;
386 assert_eq!(name, &["x", "y"]);
387
388 let name: ResourceName = ["x", "y"].into_iter().collect();
389 assert_eq!(name, nested);
390
391 let name: ResourceName = [&nested, &simple].into_iter().cloned().collect();
392 assert_eq!(name, ResourceName::new(["x", "y", "x"]));
393 }
394
395 #[test]
396 fn test_column_name_from_str() {
397 let cases = [
398 ("", Some(ResourceName::empty())),
399 (".", Some(ResourceName::new(["", ""]))),
400 (" . ", Some(ResourceName::new(["", ""]))),
401 (" ", Some(ResourceName::empty())),
402 ("0", None),
403 (".a", Some(ResourceName::new(["", "a"]))),
404 ("a.", Some(ResourceName::new(["a", ""]))),
405 (" a . ", Some(ResourceName::new(["a", ""]))),
406 ("a..b", Some(ResourceName::new(["a", "", "b"]))),
407 ("`a", None),
408 ("a`", None),
409 ("a`b`", None),
410 ("`a`b", None),
411 ("`a``b`", Some(ResourceName::new(["a`b"]))),
412 (" `a``b` ", Some(ResourceName::new(["a`b"]))),
413 ("`a`` b`", Some(ResourceName::new(["a` b"]))),
414 ("a", Some(ResourceName::new(["a"]))),
415 ("a0", Some(ResourceName::new(["a0"]))),
416 ("`a`", Some(ResourceName::new(["a"]))),
417 (" `a` ", Some(ResourceName::new(["a"]))),
418 ("` `", Some(ResourceName::new([" "]))),
419 (" ` ` ", Some(ResourceName::new([" "]))),
420 ("`0`", Some(ResourceName::new(["0"]))),
421 ("`.`", Some(ResourceName::new(["."]))),
422 ("`.`.`.`", Some(ResourceName::new([".", "."]))),
423 ("` `.` `", Some(ResourceName::new([" ", " "]))),
424 ("a.b", Some(ResourceName::new(["a", "b"]))),
425 ("a b", None),
426 ("a.`b`", Some(ResourceName::new(["a", "b"]))),
427 ("`a`.b", Some(ResourceName::new(["a", "b"]))),
428 ("`a`.`b`", Some(ResourceName::new(["a", "b"]))),
429 ("`a`.`b`.`c`", Some(ResourceName::new(["a", "b", "c"]))),
430 ("`a``.`b```", None),
431 ("`a```.`b``", None),
432 ("`a```.`b```", Some(ResourceName::new(["a`", "b`"]))),
433 ("`a.`b``.c`", None),
434 ("`a.``b`.c`", None),
435 ("`a.``b``.c`", Some(ResourceName::new(["a.`b`.c"]))),
436 ("a`.b``", None),
437 ];
438 for (input, expected_output) in cases {
439 let output: Result<ResourceName> = input.parse();
440 match (&output, &expected_output) {
441 (Ok(output), Some(expected_output)) => {
442 assert_eq!(output, expected_output, "from {input}")
443 }
444 (Err(_), None) => {}
445 _ => panic!("Expected {input} to parse as {expected_output:?}, got {output:?}"),
446 }
447 }
448 }
449
450 #[test]
451 fn test_column_name_to_string() {
452 let cases = [
453 ("", ResourceName::empty()),
454 ("``.``", ResourceName::new(["", ""])),
455 ("``.a", ResourceName::new(["", "a"])),
456 ("a.``", ResourceName::new(["a", ""])),
457 ("a.``.b", ResourceName::new(["a", "", "b"])),
458 ("a", ResourceName::new(["a"])),
459 ("a0", ResourceName::new(["a0"])),
460 ("`a `", ResourceName::new(["a "])),
461 ("` `", ResourceName::new([" "])),
462 ("`0`", ResourceName::new(["0"])),
463 ("`.`", ResourceName::new(["."])),
464 ("`.`.`.`", ResourceName::new([".", "."])),
465 ("` `.` `", ResourceName::new([" ", " "])),
466 ("a.b", ResourceName::new(["a", "b"])),
467 ("a.b.c", ResourceName::new(["a", "b", "c"])),
468 ("a.`b.c`.d", ResourceName::new(["a", "b.c", "d"])),
469 ("`a```.`b```", ResourceName::new(["a`", "b`"])),
470 ];
471 for (expected_output, input) in cases {
472 let output = input.to_string();
473 assert_eq!(output, expected_output);
474
475 let parsed: ResourceName = output.parse().expect(&output);
476 assert_eq!(parsed, input);
477 }
478
479 let cases = [
480 (" `a` ", "a", ResourceName::new(["a"])),
481 (" `a0` ", "a0", ResourceName::new(["a0"])),
482 (" `a` . `b` ", "a.b", ResourceName::new(["a", "b"])),
483 ];
484 for (input, expected_output, expected_parsed) in cases {
485 let parsed: ResourceName = input.parse().unwrap();
486 assert_eq!(parsed, expected_parsed);
487 assert_eq!(parsed.to_string(), expected_output);
488 }
489 }
490
491 #[test]
492 fn test_parse_column_name_list() {
493 let cases = [
494 ("", Some(vec![])),
495 (
496 " , ",
497 Some(vec![ResourceName::empty(), ResourceName::empty()]),
498 ),
499 (" a ", Some(vec![ResourceName::new(["a"])])),
500 (
501 " , a ",
502 Some(vec![ResourceName::empty(), ResourceName::new(["a"])]),
503 ),
504 (
505 " a , ",
506 Some(vec![ResourceName::new(["a"]), ResourceName::empty()]),
507 ),
508 (
509 "a , b",
510 Some(vec![ResourceName::new(["a"]), ResourceName::new(["b"])]),
511 ),
512 ("`a, b`", Some(vec![ResourceName::new(["a, b"])])),
513 (
514 "a.b, c",
515 Some(vec![
516 ResourceName::new(["a", "b"]),
517 ResourceName::new(["c"]),
518 ]),
519 ),
520 ];
521 for (input, expected_output) in cases {
522 let output = ResourceName::parse_column_name_list(input);
523 match (&output, &expected_output) {
524 (Ok(output), Some(expected_output)) => {
525 assert_eq!(output, expected_output, "from \"{input}\"")
526 }
527 (Err(_), None) => {}
528 _ => panic!("Expected {input} to parse as {expected_output:?}, got {output:?}"),
529 }
530 }
531 }
532}