bonsaidb_core/schema/
names.rs

1use std::borrow::Cow;
2use std::fmt::{Debug, Display, Write};
3use std::ops::Deref;
4use std::str::FromStr;
5use std::sync::Arc;
6
7use serde::{Deserialize, Serialize};
8
9/// A schema name. Cloning is inexpensive.
10#[derive(Hash, PartialEq, Eq, Deserialize, Serialize, Debug, Clone, Ord, PartialOrd)]
11#[serde(try_from = "String")]
12#[serde(into = "String")]
13pub struct Name {
14    name: Arc<Cow<'static, str>>,
15    needs_escaping: bool,
16}
17
18/// A name was unable to e parsed.
19#[derive(thiserror::Error, Debug, Serialize, Deserialize, Clone)]
20#[error("invalid name: {0}")]
21pub struct InvalidNameError(pub String);
22
23impl Name {
24    /// Creates a new name.
25    pub fn new<T: Into<Self>>(contents: T) -> Self {
26        contents.into()
27    }
28
29    /// Parses a name that was previously encoded via [`Self::encoded()`].
30    ///
31    /// # Errors
32    ///
33    /// Returns [`InvalidNameError`] if the name contains invalid escape
34    /// sequences.
35    pub fn parse_encoded(encoded: &str) -> Result<Self, InvalidNameError> {
36        let mut bytes = encoded.bytes();
37        let mut decoded = Vec::with_capacity(encoded.len());
38        while let Some(byte) = bytes.next() {
39            if byte == b'_' {
40                if let (Some(high), Some(low)) = (bytes.next(), bytes.next()) {
41                    if let Some(byte) = hex_chars_to_byte(high, low) {
42                        decoded.push(byte);
43                        continue;
44                    }
45                }
46                return Err(InvalidNameError(encoded.to_string()));
47            }
48
49            decoded.push(byte);
50        }
51
52        String::from_utf8(decoded)
53            .map(Self::from)
54            .map_err(|_| InvalidNameError(encoded.to_string()))
55    }
56
57    /// Returns an encoded version of this name that contains only alphanumeric
58    /// ASCII, underscore, and hyphen.
59    #[must_use]
60    pub fn encoded(&self) -> String {
61        format!("{self:#}")
62    }
63}
64
65impl From<Cow<'static, str>> for Name {
66    fn from(value: Cow<'static, str>) -> Self {
67        let needs_escaping = !value
68            .bytes()
69            .all(|b| b.is_ascii_alphanumeric() || b == b'-');
70        Self {
71            name: Arc::new(value),
72            needs_escaping,
73        }
74    }
75}
76
77impl From<&'static str> for Name {
78    fn from(value: &'static str) -> Self {
79        Self::from(Cow::Borrowed(value))
80    }
81}
82
83impl From<String> for Name {
84    fn from(value: String) -> Self {
85        Self::from(Cow::Owned(value))
86    }
87}
88
89#[allow(clippy::from_over_into)] // the auto into impl doesn't work with serde(into)
90impl Into<String> for Name {
91    fn into(self) -> String {
92        self.name.to_string()
93    }
94}
95
96impl Display for Name {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        if f.alternate() && self.needs_escaping {
99            for byte in self.name.bytes() {
100                if byte.is_ascii_alphanumeric() || byte == b'-' {
101                    f.write_char(byte as char)?;
102                } else {
103                    // Encode the byte as _FF
104                    f.write_char('_')?;
105                    f.write_char(nibble_to_hex_char(byte >> 4))?;
106                    f.write_char(nibble_to_hex_char(byte & 0xF))?;
107                }
108            }
109            Ok(())
110        } else {
111            Display::fmt(&self.name, f)
112        }
113    }
114}
115
116const fn nibble_to_hex_char(nibble: u8) -> char {
117    let ch = match nibble {
118        0..=9 => b'0' + nibble,
119        _ => b'a' + nibble - 10,
120    };
121    ch as char
122}
123
124const fn hex_chars_to_byte(high_nibble: u8, low_nibble: u8) -> Option<u8> {
125    match (
126        hex_char_to_nibble(high_nibble),
127        hex_char_to_nibble(low_nibble),
128    ) {
129        (Some(high_nibble), Some(low_nibble)) => Some(high_nibble << 4 | low_nibble),
130        _ => None,
131    }
132}
133
134const fn hex_char_to_nibble(nibble: u8) -> Option<u8> {
135    let ch = match nibble {
136        b'0'..=b'9' => nibble - b'0',
137        b'a'..=b'f' => nibble - b'a' + 10,
138        _ => return None,
139    };
140    Some(ch)
141}
142
143impl AsRef<str> for Name {
144    fn as_ref(&self) -> &str {
145        self.name.as_ref()
146    }
147}
148
149/// The owner of a schema item. This should represent the company, group, or
150/// individual that created the item in question. This value is used for
151/// namespacing. Changing this after values are in use is not supported without
152/// manual migrations at this time.
153#[derive(Hash, PartialEq, Eq, Deserialize, Serialize, Debug, Clone, Ord, PartialOrd)]
154#[serde(transparent)]
155pub struct Authority(Name);
156
157impl From<Cow<'static, str>> for Authority {
158    fn from(value: Cow<'static, str>) -> Self {
159        Self::from(Name::from(value))
160    }
161}
162
163impl From<&'static str> for Authority {
164    fn from(value: &'static str) -> Self {
165        Self::from(Cow::Borrowed(value))
166    }
167}
168
169impl From<String> for Authority {
170    fn from(value: String) -> Self {
171        Self::from(Cow::Owned(value))
172    }
173}
174
175impl From<Name> for Authority {
176    fn from(value: Name) -> Self {
177        Self(value)
178    }
179}
180
181impl Display for Authority {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        Display::fmt(&self.0, f)
184    }
185}
186
187impl AsRef<str> for Authority {
188    fn as_ref(&self) -> &str {
189        self.0.as_ref()
190    }
191}
192
193/// A namespaced name.
194#[derive(Hash, PartialEq, Eq, Deserialize, Serialize, Debug, Clone, Ord, PartialOrd)]
195pub struct QualifiedName {
196    /// The authority that defines this name.
197    pub authority: Authority,
198
199    /// The name, unique within `authority`.
200    pub name: Name,
201}
202
203impl Display for QualifiedName {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        Display::fmt(&self.authority, f)?;
206        f.write_char('.')?;
207        Display::fmt(&self.name, f)
208    }
209}
210
211impl FromStr for QualifiedName {
212    type Err = InvalidNameError;
213
214    fn from_str(s: &str) -> Result<Self, Self::Err> {
215        Self::parse_encoded(s)
216    }
217}
218
219/// Functions for creating qualified names
220pub trait Qualified: Display + Sized {
221    /// Creates a name that is not meant to be shared with other developers or
222    /// projects.
223    #[must_use]
224    fn private<N: Into<Name>>(name: N) -> Self {
225        Self::new(Authority::from("private"), name)
226    }
227
228    /// Creates a new qualified name.
229    #[must_use]
230    fn new<A: Into<Authority>, N: Into<Name>>(authority: A, name: N) -> Self;
231
232    /// Parses a schema name that was previously encoded via
233    /// [`Self::encoded()`].
234    ///
235    /// # Errors
236    ///
237    /// Returns [`InvalidNameError`] if the name contains invalid escape
238    /// sequences or contains more than two periods.
239    fn parse_encoded(schema_name: &str) -> Result<Self, InvalidNameError> {
240        let mut parts = schema_name.split('.');
241        if let (Some(authority), Some(name), None) = (parts.next(), parts.next(), parts.next()) {
242            let authority = Name::parse_encoded(authority)?;
243            let name = Name::parse_encoded(name)?;
244
245            Ok(Self::new(authority, name))
246        } else {
247            Err(InvalidNameError(schema_name.to_string()))
248        }
249    }
250
251    /// Encodes this schema name such that the authority and name can be
252    /// safely parsed using [`Self::parse_encoded`].
253    #[must_use]
254    fn encoded(&self) -> String {
255        format!("{self:#}")
256    }
257}
258
259impl Qualified for QualifiedName {
260    fn new<A: Into<Authority>, N: Into<Name>>(authority: A, name: N) -> Self {
261        Self {
262            authority: authority.into(),
263            name: name.into(),
264        }
265    }
266}
267
268/// The name of a [`Schema`](super::Schema).
269#[derive(Hash, PartialEq, Eq, Deserialize, Serialize, Debug, Clone, Ord, PartialOrd)]
270#[serde(transparent)]
271pub struct SchemaName(pub(crate) QualifiedName);
272
273impl Qualified for SchemaName {
274    fn new<A: Into<Authority>, N: Into<Name>>(authority: A, name: N) -> Self {
275        Self(QualifiedName::new(authority, name))
276    }
277}
278
279impl Display for SchemaName {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        Display::fmt(&self.0, f)
282    }
283}
284
285impl Deref for SchemaName {
286    type Target = QualifiedName;
287
288    fn deref(&self) -> &Self::Target {
289        &self.0
290    }
291}
292
293impl From<CollectionName> for SchemaName {
294    fn from(name: CollectionName) -> Self {
295        Self(name.0)
296    }
297}
298
299impl FromStr for SchemaName {
300    type Err = InvalidNameError;
301
302    fn from_str(s: &str) -> Result<Self, Self::Err> {
303        Self::parse_encoded(s)
304    }
305}
306
307/// The namespaced name of a [`Collection`](super::Collection).
308#[derive(Hash, PartialEq, Eq, Deserialize, Serialize, Debug, Clone, Ord, PartialOrd)]
309#[serde(transparent)]
310pub struct CollectionName(pub(crate) QualifiedName);
311
312impl Qualified for CollectionName {
313    fn new<A: Into<Authority>, N: Into<Name>>(authority: A, name: N) -> Self {
314        Self(QualifiedName::new(authority, name))
315    }
316}
317
318impl Display for CollectionName {
319    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        Display::fmt(&self.0, f)
321    }
322}
323
324impl Deref for CollectionName {
325    type Target = QualifiedName;
326
327    fn deref(&self) -> &Self::Target {
328        &self.0
329    }
330}
331
332impl FromStr for CollectionName {
333    type Err = InvalidNameError;
334
335    fn from_str(s: &str) -> Result<Self, Self::Err> {
336        Self::parse_encoded(s)
337    }
338}
339
340/// The name of a [`View`](super::View).
341#[derive(Hash, PartialEq, Eq, Deserialize, Serialize, Debug, Clone, PartialOrd, Ord)]
342pub struct ViewName {
343    /// The name of the collection that contains this view.
344    pub collection: CollectionName,
345    /// The name of this view.
346    pub name: Name,
347}
348
349impl ViewName {
350    /// Creates a new view name.
351    pub fn new<
352        C: TryInto<CollectionName, Error = InvalidNameError>,
353        N: TryInto<Name, Error = InvalidNameError>,
354    >(
355        collection: C,
356        name: N,
357    ) -> Result<Self, InvalidNameError> {
358        let collection = collection.try_into()?;
359        let name = name.try_into()?;
360        Ok(Self { collection, name })
361    }
362}
363
364impl Display for ViewName {
365    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366        Display::fmt(&self.collection, f)?;
367        f.write_char('.')?;
368        Display::fmt(&self.name, f)
369    }
370}
371
372impl FromStr for ViewName {
373    type Err = InvalidNameError;
374
375    fn from_str(s: &str) -> Result<Self, Self::Err> {
376        let (first, view_name) = s
377            .rsplit_once('.')
378            .ok_or_else(|| InvalidNameError(s.to_string()))?;
379
380        let collection = first.parse()?;
381        let name = Name::parse_encoded(view_name)?;
382        Ok(Self { collection, name })
383    }
384}
385
386#[test]
387fn name_escaping_tests() {
388    const VALID_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-";
389    const INVALID_CHARS: &str = "._hello\u{1F680}";
390    const ESCAPED_INVALID: &str = "_2e_5fhello_f0_9f_9a_80";
391    assert_eq!(Name::new(VALID_CHARS).to_string(), VALID_CHARS);
392    assert_eq!(Name::new(INVALID_CHARS).to_string(), INVALID_CHARS);
393    assert_eq!(Name::new(INVALID_CHARS).encoded(), ESCAPED_INVALID);
394    assert_eq!(
395        Name::parse_encoded(ESCAPED_INVALID).unwrap(),
396        Name::new(INVALID_CHARS)
397    );
398    Name::parse_encoded("_").unwrap_err();
399    Name::parse_encoded("_0").unwrap_err();
400    Name::parse_encoded("_z").unwrap_err();
401    Name::parse_encoded("_0z").unwrap_err();
402}
403
404#[test]
405fn joined_names_tests() {
406    const INVALID_CHARS: &str = "._hello\u{1F680}.._world\u{1F680}";
407    const ESCAPED_INVALID: &str = "_2e_5fhello_f0_9f_9a_80._2e_5fworld_f0_9f_9a_80";
408    let collection = CollectionName::parse_encoded(ESCAPED_INVALID).unwrap();
409    assert_eq!(collection.to_string(), INVALID_CHARS);
410    assert_eq!(collection.encoded(), ESCAPED_INVALID);
411
412    let schema_name = SchemaName::parse_encoded(ESCAPED_INVALID).unwrap();
413    assert_eq!(schema_name.to_string(), INVALID_CHARS);
414    assert_eq!(schema_name.encoded(), ESCAPED_INVALID);
415}