1use crate::{Error, Result};
2
3use std::borrow::Borrow;
4use std::fmt::{Display, Formatter};
5use std::hash::{Hash, Hasher};
6use std::iter::Peekable;
7use std::ops::Deref;
8use std::sync::LazyLock;
9
10use serde::{Deserialize, Serialize};
11
12pub static EMPTY_RESOURCE_NAME: LazyLock<ResourceName> =
13 LazyLock::new(|| ResourceName::new(&[] as &[String]));
14
15#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize)]
17#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
18#[cfg_attr(feature = "sqlx", sqlx(transparent, no_pg_array))]
19pub struct ResourceName(Vec<String>);
20
21impl ResourceName {
22 pub fn new<A>(iter: impl IntoIterator<Item = A>) -> Self
24 where
25 Self: FromIterator<A>,
26 {
27 iter.into_iter().collect()
28 }
29
30 pub fn from_naive_str_split(name: impl AsRef<str>) -> Self {
35 Self::new(name.as_ref().split(FIELD_SEPARATOR))
36 }
37
38 pub fn parse_column_name_list(names: impl AsRef<str>) -> Result<Vec<ResourceName>> {
41 let names = names.as_ref();
42 let chars = &mut names.chars().peekable();
43
44 drop_leading_whitespace(chars);
45 let mut ending = match chars.peek() {
46 Some(_) => FieldEnding::NextColumn,
47 None => FieldEnding::InputExhausted,
48 };
49
50 let mut cols = vec![];
51 while ending == FieldEnding::NextColumn {
52 let (col, new_ending) = parse_resource_name(chars)?;
53 cols.push(col);
54 ending = new_ending;
55 }
56 Ok(cols)
57 }
58
59 pub fn join(&self, right: &ResourceName) -> ResourceName {
61 [self.clone(), right.clone()].into_iter().collect()
62 }
63
64 pub fn path(&self) -> &[String] {
66 &self.0
67 }
68
69 pub fn into_inner(self) -> Vec<String> {
71 self.0
72 }
73
74 pub fn prefix_matches(&self, prefix: &ResourceName) -> bool {
76 if self.len() < prefix.len() {
77 return false;
78 }
79 prefix.iter().zip(self.iter()).all(|(a, b)| a == b)
80 }
81}
82
83impl<A: Into<String>> FromIterator<A> for ResourceName {
84 fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
85 let path = iter.into_iter().map(|s| s.into()).collect();
86 Self(path)
87 }
88}
89
90impl From<Vec<String>> for ResourceName {
91 fn from(path: Vec<String>) -> Self {
92 Self(path)
93 }
94}
95
96impl FromIterator<ResourceName> for ResourceName {
97 fn from_iter<T: IntoIterator<Item = ResourceName>>(iter: T) -> Self {
98 let path = iter.into_iter().flat_map(|c| c.into_iter()).collect();
99 Self(path)
100 }
101}
102
103impl IntoIterator for ResourceName {
104 type Item = String;
105 type IntoIter = std::vec::IntoIter<Self::Item>;
106
107 fn into_iter(self) -> Self::IntoIter {
108 self.0.into_iter()
109 }
110}
111
112impl Deref for ResourceName {
113 type Target = [String];
114
115 fn deref(&self) -> &[String] {
116 &self.0
117 }
118}
119
120impl Borrow<[String]> for ResourceName {
121 fn borrow(&self) -> &[String] {
122 self
123 }
124}
125
126impl Borrow<[String]> for &ResourceName {
127 fn borrow(&self) -> &[String] {
128 self
129 }
130}
131
132impl Hash for ResourceName {
133 fn hash<H: Hasher>(&self, hasher: &mut H) {
134 (**self).hash(hasher)
135 }
136}
137
138impl Display for ResourceName {
139 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
140 for (i, s) in self.iter().enumerate() {
141 use std::fmt::Write as _;
142
143 if i > 0 {
144 f.write_char(FIELD_SEPARATOR)?;
145 }
146
147 let digit_char = |c: char| c.is_ascii_digit();
148 if s.is_empty() || s.starts_with(digit_char) || s.contains(|c| !is_simple_char(c)) {
149 f.write_char(FIELD_ESCAPE_CHAR)?;
150 for c in s.chars() {
151 f.write_char(c)?;
152 if c == FIELD_ESCAPE_CHAR {
153 f.write_char(c)?;
154 }
155 }
156 f.write_char(FIELD_ESCAPE_CHAR)?;
157 } else {
158 f.write_str(s)?;
159 }
160 }
161 Ok(())
162 }
163}
164
165fn is_simple_char(c: char) -> bool {
166 c.is_ascii_alphanumeric() || c == '_'
167}
168
169fn drop_leading_whitespace(iter: &mut Peekable<impl Iterator<Item = char>>) {
170 while iter.next_if(|c| c.is_whitespace()).is_some() {}
171}
172
173impl std::str::FromStr for ResourceName {
174 type Err = Error;
175
176 fn from_str(s: &str) -> Result<Self> {
177 match parse_resource_name(&mut s.chars().peekable())? {
178 (_, FieldEnding::NextColumn) => Err(Error::generic("Trailing comma in column name")),
179 (col, _) => Ok(col),
180 }
181 }
182}
183
184type Chars<'a> = Peekable<std::str::Chars<'a>>;
185
186#[derive(PartialEq)]
187enum FieldEnding {
188 InputExhausted,
189 NextField,
190 NextColumn,
191}
192
193const FIELD_ESCAPE_CHAR: char = '`';
194const FIELD_SEPARATOR: char = '.';
195const COLUMN_SEPARATOR: char = ',';
196
197fn parse_resource_name(chars: &mut Chars<'_>) -> Result<(ResourceName, FieldEnding)> {
198 drop_leading_whitespace(chars);
199 let mut ending = if chars.peek().is_none() {
200 FieldEnding::InputExhausted
201 } else if chars.next_if_eq(&COLUMN_SEPARATOR).is_some() {
202 FieldEnding::NextColumn
203 } else {
204 FieldEnding::NextField
205 };
206
207 let mut path = vec![];
208 while ending == FieldEnding::NextField {
209 drop_leading_whitespace(chars);
210 let field_name = match chars.next_if_eq(&FIELD_ESCAPE_CHAR) {
211 Some(_) => parse_escaped_field_name(chars)?,
212 None => parse_simple_field_name(chars)?,
213 };
214
215 ending = match chars.find(|c| !c.is_whitespace()) {
216 None => FieldEnding::InputExhausted,
217 Some(FIELD_SEPARATOR) => FieldEnding::NextField,
218 Some(COLUMN_SEPARATOR) => FieldEnding::NextColumn,
219 Some(other) => {
220 return Err(Error::generic(format!(
221 "Invalid character {other:?} after field {field_name:?}",
222 )));
223 }
224 };
225 path.push(field_name);
226 }
227 Ok((ResourceName::new(path), ending))
228}
229
230fn parse_simple_field_name(chars: &mut Chars<'_>) -> Result<String> {
231 let mut name = String::new();
232 let mut first = true;
233 while let Some(c) = chars.next_if(|c| is_simple_char(*c)) {
234 if first && c.is_ascii_digit() {
235 return Err(Error::generic(format!(
236 "Unescaped field name cannot start with a digit {c:?}"
237 )));
238 }
239 name.push(c);
240 first = false;
241 }
242 Ok(name)
243}
244
245fn parse_escaped_field_name(chars: &mut Chars<'_>) -> Result<String> {
246 let mut name = String::new();
247 loop {
248 match chars.next() {
249 Some(FIELD_ESCAPE_CHAR) if chars.next_if_eq(&FIELD_ESCAPE_CHAR).is_none() => break,
250 Some(c) => name.push(c),
251 None => {
252 return Err(Error::generic(format!(
253 "No closing {FIELD_ESCAPE_CHAR:?} after field {name:?}"
254 )));
255 }
256 }
257 }
258 Ok(name)
259}
260
261#[cfg(test)]
262mod test {
263 use super::*;
264
265 impl ResourceName {
266 fn empty() -> Self {
267 Self::new(&[] as &[String])
268 }
269 }
270
271 #[test]
272 fn test_column_name_methods() {
273 let simple: ResourceName = "x".parse().unwrap();
274 let nested: ResourceName = "x.y".parse().unwrap();
275
276 assert_eq!(simple.path(), ["x"]);
277 assert_eq!(nested.path(), ["x", "y"]);
278
279 assert_eq!(simple.clone().into_inner(), ["x"]);
280 assert_eq!(nested.clone().into_inner(), ["x", "y"]);
281
282 let name: &[String] = &nested;
283 assert_eq!(name, &["x", "y"]);
284
285 let name: ResourceName = ["x", "y"].into_iter().collect();
286 assert_eq!(name, nested);
287
288 let name: ResourceName = [&nested, &simple].into_iter().cloned().collect();
289 assert_eq!(name, ResourceName::new(["x", "y", "x"]));
290 }
291
292 #[test]
293 fn test_column_name_from_str() {
294 let cases = [
295 ("", Some(ResourceName::empty())),
296 (".", Some(ResourceName::new(["", ""]))),
297 (" . ", Some(ResourceName::new(["", ""]))),
298 (" ", Some(ResourceName::empty())),
299 ("0", None),
300 (".a", Some(ResourceName::new(["", "a"]))),
301 ("a.", Some(ResourceName::new(["a", ""]))),
302 (" a . ", Some(ResourceName::new(["a", ""]))),
303 ("a..b", Some(ResourceName::new(["a", "", "b"]))),
304 ("`a", None),
305 ("a`", None),
306 ("a`b`", None),
307 ("`a`b", None),
308 ("`a``b`", Some(ResourceName::new(["a`b"]))),
309 (" `a``b` ", Some(ResourceName::new(["a`b"]))),
310 ("`a`` b`", Some(ResourceName::new(["a` b"]))),
311 ("a", Some(ResourceName::new(["a"]))),
312 ("a0", Some(ResourceName::new(["a0"]))),
313 ("`a`", Some(ResourceName::new(["a"]))),
314 (" `a` ", Some(ResourceName::new(["a"]))),
315 ("` `", Some(ResourceName::new([" "]))),
316 (" ` ` ", Some(ResourceName::new([" "]))),
317 ("`0`", Some(ResourceName::new(["0"]))),
318 ("`.`", Some(ResourceName::new(["."]))),
319 ("`.`.`.`", Some(ResourceName::new([".", "."]))),
320 ("` `.` `", Some(ResourceName::new([" ", " "]))),
321 ("a.b", Some(ResourceName::new(["a", "b"]))),
322 ("a b", None),
323 ("a.`b`", Some(ResourceName::new(["a", "b"]))),
324 ("`a`.b", Some(ResourceName::new(["a", "b"]))),
325 ("`a`.`b`", Some(ResourceName::new(["a", "b"]))),
326 ("`a`.`b`.`c`", Some(ResourceName::new(["a", "b", "c"]))),
327 ("`a``.`b```", None),
328 ("`a```.`b``", None),
329 ("`a```.`b```", Some(ResourceName::new(["a`", "b`"]))),
330 ("`a.`b``.c`", None),
331 ("`a.``b`.c`", None),
332 ("`a.``b``.c`", Some(ResourceName::new(["a.`b`.c"]))),
333 ("a`.b``", None),
334 ];
335 for (input, expected_output) in cases {
336 let output: Result<ResourceName> = input.parse();
337 match (&output, &expected_output) {
338 (Ok(output), Some(expected_output)) => {
339 assert_eq!(output, expected_output, "from {input}")
340 }
341 (Err(_), None) => {}
342 _ => panic!("Expected {input} to parse as {expected_output:?}, got {output:?}"),
343 }
344 }
345 }
346
347 #[test]
348 fn test_column_name_to_string() {
349 let cases = [
350 ("", ResourceName::empty()),
351 ("``.``", ResourceName::new(["", ""])),
352 ("``.a", ResourceName::new(["", "a"])),
353 ("a.``", ResourceName::new(["a", ""])),
354 ("a.``.b", ResourceName::new(["a", "", "b"])),
355 ("a", ResourceName::new(["a"])),
356 ("a0", ResourceName::new(["a0"])),
357 ("`a `", ResourceName::new(["a "])),
358 ("` `", ResourceName::new([" "])),
359 ("`0`", ResourceName::new(["0"])),
360 ("`.`", ResourceName::new(["."])),
361 ("`.`.`.`", ResourceName::new([".", "."])),
362 ("` `.` `", ResourceName::new([" ", " "])),
363 ("a.b", ResourceName::new(["a", "b"])),
364 ("a.b.c", ResourceName::new(["a", "b", "c"])),
365 ("a.`b.c`.d", ResourceName::new(["a", "b.c", "d"])),
366 ("`a```.`b```", ResourceName::new(["a`", "b`"])),
367 ];
368 for (expected_output, input) in cases {
369 let output = input.to_string();
370 assert_eq!(output, expected_output);
371
372 let parsed: ResourceName = output.parse().expect(&output);
373 assert_eq!(parsed, input);
374 }
375
376 let cases = [
377 (" `a` ", "a", ResourceName::new(["a"])),
378 (" `a0` ", "a0", ResourceName::new(["a0"])),
379 (" `a` . `b` ", "a.b", ResourceName::new(["a", "b"])),
380 ];
381 for (input, expected_output, expected_parsed) in cases {
382 let parsed: ResourceName = input.parse().unwrap();
383 assert_eq!(parsed, expected_parsed);
384 assert_eq!(parsed.to_string(), expected_output);
385 }
386 }
387
388 #[test]
389 fn test_parse_column_name_list() {
390 let cases = [
391 ("", Some(vec![])),
392 (
393 " , ",
394 Some(vec![ResourceName::empty(), ResourceName::empty()]),
395 ),
396 (" a ", Some(vec![ResourceName::new(["a"])])),
397 (
398 " , a ",
399 Some(vec![ResourceName::empty(), ResourceName::new(["a"])]),
400 ),
401 (
402 " a , ",
403 Some(vec![ResourceName::new(["a"]), ResourceName::empty()]),
404 ),
405 (
406 "a , b",
407 Some(vec![ResourceName::new(["a"]), ResourceName::new(["b"])]),
408 ),
409 ("`a, b`", Some(vec![ResourceName::new(["a, b"])])),
410 (
411 "a.b, c",
412 Some(vec![
413 ResourceName::new(["a", "b"]),
414 ResourceName::new(["c"]),
415 ]),
416 ),
417 ];
418 for (input, expected_output) in cases {
419 let output = ResourceName::parse_column_name_list(input);
420 match (&output, &expected_output) {
421 (Ok(output), Some(expected_output)) => {
422 assert_eq!(output, expected_output, "from \"{input}\"")
423 }
424 (Err(_), None) => {}
425 _ => panic!("Expected {input} to parse as {expected_output:?}, got {output:?}"),
426 }
427 }
428 }
429}