Skip to main content

jsonld_syntax/context/definition/
key.rs

1use crate::{CompactIri, Keyword};
2use iri_rs::Iri;
3use rdfx::BlankId;
4use smol_str::SmolStr;
5use std::{borrow::Borrow, fmt, hash::Hash};
6
7/// Context key.
8///
9/// A key of up to 23 bytes is stored inline, so creating, cloning and dropping
10/// it allocates nothing and touches no shared counter; nearly every term of a
11/// real context is that short. A longer key is shared behind a reference
12/// count.
13///
14/// A key owns its string and frees it when dropped, so the memory a process
15/// spends on keys is bounded by the contexts it keeps.
16///
17/// Equality, ordering and hashing are those of the string, so a map keyed by
18/// `Key` can be searched with a `&str`.
19#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub struct Key(SmolStr);
21
22impl Key {
23    /// Parses this key as an IRI, returning `None` if it is not one.
24    #[must_use]
25    pub fn as_iri(&self) -> Option<Iri<&str>> {
26        Iri::parse(self.0.as_str()).ok()
27    }
28
29    /// Parses this key as a compact IRI, returning `None` if it is not one.
30    #[must_use]
31    pub fn as_compact_iri(&self) -> Option<&CompactIri> {
32        CompactIri::new(self.0.as_str()).ok()
33    }
34
35    /// Parses this key as a blank node identifier, returning `None` if it is
36    /// not one.
37    #[must_use]
38    pub fn as_blank_id(&self) -> Option<&BlankId> {
39        BlankId::new(self.0.as_str()).ok()
40    }
41
42    /// Returns the string of this key.
43    #[must_use]
44    pub fn as_str(&self) -> &str {
45        self.0.as_str()
46    }
47
48    /// Returns the length of this key, in bytes.
49    #[must_use]
50    pub fn len(&self) -> usize {
51        self.0.len()
52    }
53
54    /// Checks whether this key is the empty string.
55    #[must_use]
56    pub fn is_empty(&self) -> bool {
57        self.0.is_empty()
58    }
59
60    /// Copies this key into a newly allocated `String`.
61    #[must_use]
62    pub fn into_string(self) -> String {
63        String::from(self.0)
64    }
65
66    /// Checks whether this key is keyword-like: an `@` followed by ASCII
67    /// letters. See [`crate::is_keyword_like`].
68    #[must_use]
69    pub fn is_keyword_like(&self) -> bool {
70        crate::is_keyword_like(self.0.as_str())
71    }
72}
73
74impl From<jstrict::object::Key> for Key {
75    fn from(k: jstrict::object::Key) -> Self {
76        Self::from(k.into_string())
77    }
78}
79
80impl From<String> for Key {
81    fn from(k: String) -> Self {
82        Self(SmolStr::from(k))
83    }
84}
85
86impl<'a> From<&'a str> for Key {
87    fn from(value: &'a str) -> Self {
88        Self(SmolStr::new(value))
89    }
90}
91
92impl fmt::Display for Key {
93    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
94        self.as_str().fmt(f)
95    }
96}
97
98impl Borrow<str> for Key {
99    fn borrow(&self) -> &str {
100        self.as_str()
101    }
102}
103
104#[cfg(feature = "serde")]
105impl serde::Serialize for Key {
106    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
107    where
108        S: serde::Serializer,
109    {
110        serializer.serialize_str(self.as_str())
111    }
112}
113
114#[cfg(feature = "serde")]
115impl<'de> serde::Deserialize<'de> for Key {
116    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
117    where
118        D: serde::Deserializer<'de>,
119    {
120        struct Visitor;
121
122        impl serde::de::Visitor<'_> for Visitor {
123            type Value = Key;
124
125            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
126                f.write_str("a string")
127            }
128
129            // `visit_borrowed_str` and `visit_string` forward here by
130            // default; a key copies its string either way, so one method covers
131            // borrowing formats, owned buffers (`from_reader`, escaped
132            // keys) and serde's internal `Content` buffering
133            // (`#[serde(flatten)]`) alike.
134            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Key, E> {
135                Ok(Key::from(v))
136            }
137        }
138
139        deserializer.deserialize_str(Visitor)
140    }
141}
142
143#[derive(Clone, Copy, PartialEq, Eq, Debug)]
144/// Borrowed context definition key.
145pub struct KeyRef<'a>(&'a str);
146
147impl<'a> KeyRef<'a> {
148    /// Checks whether this key is the empty string.
149    #[must_use]
150    pub fn is_empty(&self) -> bool {
151        self.0.is_empty()
152    }
153
154    /// Checks whether this key is keyword-like: an `@` followed by ASCII
155    /// letters. See [`crate::is_keyword_like`].
156    #[must_use]
157    pub fn is_keyword_like(&self) -> bool {
158        crate::is_keyword_like(self.as_str())
159    }
160
161    /// Returns the borrowed string of this key.
162    #[must_use]
163    pub fn as_str(&self) -> &'a str {
164        self.0
165    }
166
167    /// Copies this key into an owned [`Key`].
168    #[must_use]
169    pub fn to_owned(self) -> Key {
170        Key::from(self.0)
171    }
172}
173
174impl<'a> From<&'a str> for KeyRef<'a> {
175    fn from(s: &'a str) -> Self {
176        Self(s)
177    }
178}
179
180impl<'a> From<&'a Key> for KeyRef<'a> {
181    fn from(k: &'a Key) -> Self {
182        Self(k.as_str())
183    }
184}
185
186impl fmt::Display for KeyRef<'_> {
187    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
188        self.0.fmt(f)
189    }
190}
191
192#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
193/// Context definition key that may also be a JSON-LD keyword.
194pub enum KeyOrKeyword {
195    /// A JSON-LD keyword.
196    Keyword(Keyword),
197    /// A term key.
198    Key(Key),
199}
200
201impl KeyOrKeyword {
202    /// Checks whether this is the empty key. A keyword is never empty.
203    #[must_use]
204    pub fn is_empty(&self) -> bool {
205        match self {
206            Self::Keyword(_) => false,
207            Self::Key(k) => k.is_empty(),
208        }
209    }
210
211    /// Returns the keyword, or `None` if this is a term key.
212    #[must_use]
213    pub fn into_keyword(self) -> Option<Keyword> {
214        match self {
215            Self::Keyword(k) => Some(k),
216            Self::Key(_) => None,
217        }
218    }
219
220    /// Returns the term key, or `None` if this is a keyword.
221    #[must_use]
222    pub fn into_key(self) -> Option<Key> {
223        match self {
224            Self::Keyword(_) => None,
225            Self::Key(k) => Some(k),
226        }
227    }
228
229    /// Returns the keyword, or `None` if this is a term key.
230    #[must_use]
231    pub fn as_keyword(&self) -> Option<Keyword> {
232        match self {
233            Self::Keyword(k) => Some(*k),
234            Self::Key(_) => None,
235        }
236    }
237
238    /// Borrows the term key, or returns `None` if this is a keyword.
239    #[must_use]
240    pub fn as_key(&self) -> Option<&Key> {
241        match self {
242            Self::Keyword(_) => None,
243            Self::Key(k) => Some(k),
244        }
245    }
246
247    /// Returns this value as it is spelled in the context.
248    #[must_use]
249    pub fn as_str(&self) -> &str {
250        match self {
251            Self::Keyword(k) => k.into_str(),
252            Self::Key(k) => k.as_str(),
253        }
254    }
255}
256
257#[allow(clippy::derived_hash_with_manual_eq)]
258impl Hash for KeyOrKeyword {
259    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
260        self.as_str().hash(state);
261    }
262}
263
264impl fmt::Display for KeyOrKeyword {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        match self {
267            Self::Key(k) => k.fmt(f),
268            Self::Keyword(k) => k.fmt(f),
269        }
270    }
271}
272
273#[derive(Clone, Copy, PartialEq, Eq)]
274/// Borrowed context definition key that may also be a keyword.
275pub enum KeyOrKeywordRef<'a> {
276    /// A JSON-LD keyword.
277    Keyword(Keyword),
278    /// A term key.
279    Key(KeyRef<'a>),
280}
281
282impl<'a> KeyOrKeywordRef<'a> {
283    /// Copies the term key, if any, into an owned [`KeyOrKeyword`].
284    #[must_use]
285    pub fn to_owned(self) -> KeyOrKeyword {
286        match self {
287            Self::Keyword(k) => KeyOrKeyword::Keyword(k),
288            Self::Key(k) => KeyOrKeyword::Key(k.to_owned()),
289        }
290    }
291
292    /// Returns this value as it is spelled in the context.
293    #[must_use]
294    pub fn as_str(&self) -> &'a str {
295        match self {
296            Self::Keyword(k) => k.into_str(),
297            Self::Key(k) => k.as_str(),
298        }
299    }
300}
301
302impl<'a> From<&'a str> for KeyOrKeywordRef<'a> {
303    fn from(s: &'a str) -> Self {
304        match Keyword::try_from(s) {
305            Ok(k) => Self::Keyword(k),
306            Err(_) => Self::Key(s.into()),
307        }
308    }
309}
310
311impl<'a> From<&'a KeyOrKeyword> for KeyOrKeywordRef<'a> {
312    fn from(k: &'a KeyOrKeyword) -> Self {
313        match k {
314            KeyOrKeyword::Keyword(k) => Self::Keyword(*k),
315            KeyOrKeyword::Key(k) => Self::Key(k.into()),
316        }
317    }
318}
319
320impl<'a> From<KeyRef<'a>> for KeyOrKeywordRef<'a> {
321    fn from(k: KeyRef<'a>) -> Self {
322        Self::Key(k)
323    }
324}
325
326impl<'a> From<&'a Key> for KeyOrKeywordRef<'a> {
327    fn from(k: &'a Key) -> Self {
328        Self::Key(k.into())
329    }
330}
331
332/// Context definition key that may also be the `@type` entry.
333pub enum KeyOrType {
334    /// A term key.
335    Key(Key),
336    /// The `@type` entry.
337    Type,
338}
339
340impl KeyOrType {
341    /// Returns this value as it is spelled in the context.
342    #[must_use]
343    pub fn as_str(&self) -> &str {
344        match self {
345            Self::Key(k) => k.as_str(),
346            Self::Type => "@type",
347        }
348    }
349}
350
351impl fmt::Display for KeyOrType {
352    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
353        self.as_str().fmt(f)
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    #![allow(clippy::unwrap_used)]
360    use super::*;
361    use crate::{TryFromJson, context::Context};
362    use jstrict::Parse;
363    use std::{
364        collections::HashMap,
365        hash::{BuildHasher, RandomState},
366    };
367
368    /// Parses `text` as a context holding a single context definition.
369    fn parse_definition(text: &str) -> crate::context::Definition {
370        let (value, _) = jstrict::Value::parse_str(text).unwrap();
371        match Context::try_from_json(&value).unwrap() {
372            Context::One(crate::ContextEntry::Definition(definition)) => Some(definition),
373            Context::One(_) | Context::Many(_) => None,
374        }
375        .unwrap()
376    }
377
378    /// A key built from a borrowed string and one built from an owned string
379    /// are equal and hash the same, and a map keyed by `Key` is searchable by
380    /// `&str`, which is what the `Borrow<str>` contract requires.
381    #[test]
382    fn keys_from_borrowed_and_owned_strings_agree() {
383        let borrowed = Key::from("speed");
384        let owned = Key::from(String::from("speed"));
385        assert_eq!(borrowed, owned);
386
387        let hasher = RandomState::new();
388        assert_eq!(hasher.hash_one(&borrowed), hasher.hash_one(&owned));
389        assert_eq!(hasher.hash_one(&borrowed), hasher.hash_one("speed"));
390
391        let map = HashMap::from([(borrowed, 7_u8)]);
392        assert_eq!(map.get("speed"), Some(&7));
393    }
394
395    /// Keys from two separately parsed contexts are equal and order equal, and
396    /// a key as short as a typical term is stored without an allocation.
397    #[test]
398    fn keys_of_separately_parsed_contexts_are_equal() {
399        let first = parse_definition(r#"{"speed": "https://example.org/speed"}"#);
400        let second = parse_definition(r#"{"speed": "https://example.org/velocity"}"#);
401        let first_key = first.bindings.get_key("speed").unwrap();
402        let second_key = second.bindings.get_key("speed").unwrap();
403        assert_eq!(first_key, second_key);
404        assert_eq!(first_key.cmp(second_key), std::cmp::Ordering::Equal);
405        assert!(!first_key.0.is_heap_allocated());
406        assert_eq!(*first_key, first_key.clone());
407    }
408
409    /// `#[serde(flatten)]` on `Definition::bindings` buffers entries into
410    /// serde's internal `Content` representation and replays keys through
411    /// `visit_str` (transient, not borrowed). The former
412    /// `<&str>::deserialize`-based impl failed on that path.
413    #[cfg(feature = "serde")]
414    #[test]
415    fn key_deserializes_through_serde_content_buffer() {
416        let (value, _) = jstrict::Value::parse_str(r#"{"@vocab": "http://example.com/", "term": "http://example.com/term"}"#).unwrap();
417        let definition: crate::context::Definition = crate::from_value(value).unwrap();
418        assert!(definition.bindings.get(&Key::from("term")).is_some());
419    }
420
421    /// Owned-string input (`visit_string` → `visit_str`), as produced by
422    /// escaped keys and reader-based formats.
423    #[cfg(feature = "serde")]
424    #[test]
425    fn key_deserializes_from_owned_string() {
426        let (value, _) = jstrict::Value::parse_str(r#""api""#).unwrap();
427        let key: Key = crate::from_value(value).unwrap();
428        assert_eq!(key.as_str(), "api");
429    }
430}