Skip to main content

substrate_data/model/
reference.rs

1//! A reference's two parts, each checked once where it's made: its kind
2//! ([`KindName`], interned like an [`Atom`]) and its id ([`RefKey`]).
3
4use std::fmt;
5
6use crate::Atom;
7use crate::typed::TypedError;
8
9/// A reference's kind: lower case ASCII letters, digits and `_`, starting
10/// with a letter (`user`, `doc`, `file`). Checked when it's made, then a
11/// number: copied and compared as one.
12#[derive(Clone, Copy, PartialEq, Eq, Hash)]
13pub struct KindName(Atom);
14
15impl KindName {
16    /// `name` as a kind, if it's one.
17    ///
18    /// # Errors
19    ///
20    /// [`TypedError::BadKind`] when `name` isn't lower case letters, digits and `_`
21    /// starting with a letter.
22    pub fn new(name: &str) -> Result<KindName, TypedError> {
23        if Self::is_valid(name) {
24            Ok(KindName(Atom::new(name)))
25        } else {
26            Err(TypedError::BadKind {
27                kind: name.to_owned(),
28            })
29        }
30    }
31
32    /// A name `kind!` already checked at compile time.
33    #[must_use]
34    pub fn declared(name: &'static str) -> KindName {
35        debug_assert!(Self::is_valid(name), "`{name}` isn't a kind");
36        KindName(Atom::new(name))
37    }
38
39    /// Whether `name` is a kind — at compile time too (`kind!` checks its
40    /// names with it).
41    #[must_use]
42    pub const fn is_valid(name: &str) -> bool {
43        let b = name.as_bytes();
44        if b.is_empty() || !b[0].is_ascii_lowercase() {
45            return false;
46        }
47        let mut i = 1;
48        while i < b.len() {
49            let c = b[i];
50            if !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'_') {
51                return false;
52            }
53            i += 1;
54        }
55        true
56    }
57
58    /// As written.
59    #[must_use]
60    pub fn as_str(self) -> &'static str {
61        self.0.as_str()
62    }
63
64    /// Its number in this process's atom table.
65    #[must_use]
66    pub fn atom(self) -> Atom {
67        self.0
68    }
69}
70
71impl fmt::Debug for KindName {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        write!(f, "KindName({:?})", self.as_str())
74    }
75}
76
77impl fmt::Display for KindName {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        f.write_str(self.as_str())
80    }
81}
82
83impl PartialOrd for KindName {
84    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
85        Some(self.cmp(other))
86    }
87}
88
89impl Ord for KindName {
90    /// By name, not by number.
91    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
92        self.as_str().cmp(other.as_str())
93    }
94}
95
96impl PartialEq<str> for KindName {
97    fn eq(&self, other: &str) -> bool {
98        self.as_str() == other
99    }
100}
101
102impl PartialEq<&str> for KindName {
103    fn eq(&self, other: &&str) -> bool {
104        self.as_str() == *other
105    }
106}
107
108/// A reference's id as written, untyped: anything but empty, whitespace or
109/// control characters. What it names is the format's business; a typed
110/// field reads it as its kind's id type ([`crate::RefId`]).
111#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
112pub struct RefKey(Box<str>);
113
114impl RefKey {
115    /// `id` as a reference's id, if it's one.
116    ///
117    /// # Errors
118    ///
119    /// [`TypedError::BadId`] when `id` is empty or has whitespace or control characters.
120    pub fn new(id: &str) -> Result<RefKey, TypedError> {
121        if Self::is_valid(id) {
122            Ok(RefKey(id.into()))
123        } else {
124            Err(TypedError::BadId {
125                kind: None,
126                id: id.to_owned(),
127            })
128        }
129    }
130
131    /// Whether `id` may be a reference's id.
132    #[must_use]
133    pub fn is_valid(id: &str) -> bool {
134        if id.is_empty() {
135            return false;
136        }
137        // ASCII is checked byte by byte: whitespace and controls are the
138        // bytes up to the space, and DEL.
139        let mut ascii = true;
140        for &c in id.as_bytes() {
141            if !c.is_ascii() {
142                ascii = false;
143                break;
144            }
145            if c <= b' ' || c == 0x7f {
146                return false;
147            }
148        }
149        ascii || !id.chars().any(|c| c.is_whitespace() || c.is_control())
150    }
151
152    /// A typed id's spelling, as is: it's checked when it's read back.
153    pub(crate) fn spelled(id: &str) -> RefKey {
154        RefKey(id.into())
155    }
156
157    /// As written.
158    #[must_use]
159    pub fn as_str(&self) -> &str {
160        &self.0
161    }
162}
163
164impl fmt::Debug for RefKey {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        write!(f, "RefKey({:?})", &*self.0)
167    }
168}
169
170impl fmt::Display for RefKey {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        f.write_str(&self.0)
173    }
174}
175
176impl PartialEq<str> for RefKey {
177    fn eq(&self, other: &str) -> bool {
178        &*self.0 == other
179    }
180}
181
182impl PartialEq<&str> for RefKey {
183    fn eq(&self, other: &&str) -> bool {
184        &*self.0 == *other
185    }
186}
187
188impl std::ops::Deref for KindName {
189    type Target = str;
190    fn deref(&self) -> &str {
191        self.as_str()
192    }
193}
194
195impl AsRef<str> for KindName {
196    fn as_ref(&self) -> &str {
197        self.as_str()
198    }
199}
200
201impl PartialEq<String> for KindName {
202    fn eq(&self, other: &String) -> bool {
203        self.as_str() == other
204    }
205}
206
207impl std::ops::Deref for RefKey {
208    type Target = str;
209    fn deref(&self) -> &str {
210        &self.0
211    }
212}
213
214impl AsRef<str> for RefKey {
215    fn as_ref(&self) -> &str {
216        &self.0
217    }
218}
219
220/// A map keyed by ids is looked up by a `&str`: an id hashes and orders as its text.
221impl std::borrow::Borrow<str> for RefKey {
222    fn borrow(&self) -> &str {
223        &self.0
224    }
225}
226
227impl PartialEq<String> for RefKey {
228    fn eq(&self, other: &String) -> bool {
229        &*self.0 == other
230    }
231}
232
233impl PartialEq<RefKey> for String {
234    fn eq(&self, other: &RefKey) -> bool {
235        self.as_str() == &*other.0
236    }
237}
238
239impl PartialEq<RefKey> for str {
240    fn eq(&self, other: &RefKey) -> bool {
241        self == &*other.0
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn an_id_is_anything_but_empty_whitespace_or_controls() {
251        for ok in [
252            "a",
253            "images/logo.png",
254            "~!@#$%^&*()_+-=[]{};:'\",.<>?/|\\`",
255            "héllo",
256            "日本",
257        ] {
258            assert!(RefKey::is_valid(ok), "{ok:?}");
259        }
260        for bad in [
261            "",
262            " ",
263            "a b",
264            "a\tb",
265            "a\nb",
266            "a\rb",
267            "a\x0bb",
268            "a\x0cb",
269            "a\0b",
270            "a\x1fb",
271            "a\x7fb",
272            "é b",
273            "é\u{85}",
274            "é\u{a0}",
275            "é\u{2003}",
276            "日本\u{9f}",
277            "a\u{200a}",
278        ] {
279            assert!(!RefKey::is_valid(bad), "{bad:?}");
280        }
281    }
282
283    #[test]
284    fn the_ascii_path_agrees_with_the_character_rule() {
285        for byte in 0..=0x7f_u8 {
286            let id = format!("a{}b", char::from(byte));
287            let by_chars = !id.chars().any(|c| c.is_whitespace() || c.is_control());
288            assert_eq!(RefKey::is_valid(&id), by_chars, "{id:?}");
289        }
290    }
291}