Skip to main content

oxrdf/
blank_node.rs

1#![allow(clippy::host_endian_bytes)] // We use it to go around 16 bytes alignment of u128
2use rand::random;
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
5use std::io::Write;
6use std::{fmt, str};
7
8/// An owned RDF [blank node](https://www.w3.org/TR/rdf11-concepts/#dfn-blank-node).
9///
10/// The common way to create a new blank node is to use the [`BlankNode::default()`] function.
11///
12/// It is also possible to create a blank node from a blank node identifier using the [`BlankNode::new()`] function.
13/// The blank node identifier must be valid according to N-Triples, Turtle, and SPARQL grammars.
14///
15/// The default string formatter is returning an N-Triples, Turtle, and SPARQL compatible representation:
16/// ```
17/// use oxrdf::BlankNode;
18///
19/// assert_eq!("_:a122", BlankNode::new("a122")?.to_string());
20/// # Result::<_,oxrdf::BlankNodeIdParseError>::Ok(())
21/// ```
22#[derive(Eq, PartialEq, Debug, Clone, Hash)]
23pub struct BlankNode(BlankNodeContent);
24
25#[derive(PartialEq, Eq, Debug, Clone, Hash)]
26enum BlankNodeContent {
27    Named(String),
28    Anonymous { id: [u8; 16], str: IdStr },
29}
30
31impl BlankNode {
32    /// Creates a blank node from a unique identifier.
33    ///
34    /// The blank node identifier must be valid according to N-Triples, Turtle, and SPARQL grammars.
35    ///
36    /// In most cases, it is much more convenient to create a blank node using [`BlankNode::default()`]
37    /// that creates a random ID that could be easily inlined by Oxigraph stores.
38    pub fn new(id: impl Into<String>) -> Result<Self, BlankNodeIdParseError> {
39        let id = id.into();
40        validate_blank_node_identifier(&id)?;
41        Ok(Self::new_unchecked(id))
42    }
43
44    /// Creates a blank node from a unique identifier without validation.
45    ///
46    /// It is the caller's responsibility to ensure that `id` is a valid blank node identifier
47    /// according to N-Triples, Turtle, and SPARQL grammars.
48    ///
49    /// [`BlankNode::new()`] is a safe version of this constructor and should be used for untrusted data.
50    #[inline]
51    pub fn new_unchecked(id: impl Into<String>) -> Self {
52        let id = id.into();
53        if let Some(numerical_id) = to_integer_id(&id) {
54            Self::new_from_unique_id(numerical_id)
55        } else {
56            Self(BlankNodeContent::Named(id))
57        }
58    }
59
60    /// Creates a blank node from a unique numerical id.
61    ///
62    /// In most cases, it is much more convenient to create a blank node using [`BlankNode::default()`].
63    #[inline]
64    pub fn new_from_unique_id(id: u128) -> Self {
65        Self(BlankNodeContent::Anonymous {
66            id: id.to_ne_bytes(),
67            str: IdStr::new(id),
68        })
69    }
70
71    /// Returns the underlying ID of this blank node.
72    #[inline]
73    pub fn as_str(&self) -> &str {
74        match &self.0 {
75            BlankNodeContent::Named(id) => id,
76            BlankNodeContent::Anonymous { str, .. } => str.as_str(),
77        }
78    }
79
80    /// Returns the underlying ID of this blank node.
81    #[inline]
82    pub fn into_string(self) -> String {
83        match self.0 {
84            BlankNodeContent::Named(id) => id,
85            BlankNodeContent::Anonymous { str, .. } => str.as_str().to_owned(),
86        }
87    }
88
89    #[inline]
90    pub fn as_ref(&self) -> BlankNodeRef<'_> {
91        BlankNodeRef(match &self.0 {
92            BlankNodeContent::Named(id) => BlankNodeRefContent::Named(id.as_str()),
93            BlankNodeContent::Anonymous { id, str } => BlankNodeRefContent::Anonymous {
94                id: *id,
95                str: str.as_str(),
96            },
97        })
98    }
99}
100
101impl fmt::Display for BlankNode {
102    #[inline]
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        self.as_ref().fmt(f)
105    }
106}
107
108impl Default for BlankNode {
109    /// Builds a new RDF [blank node](https://www.w3.org/TR/rdf11-concepts/#dfn-blank-node) with a unique id.
110    #[inline]
111    fn default() -> Self {
112        // We ensure the ID does not start with a number to be also valid with RDF/XML
113        loop {
114            let id = random();
115            let str = IdStr::new(id);
116            if matches!(str.as_str().as_bytes().first(), Some(b'a'..=b'f')) {
117                return Self(BlankNodeContent::Anonymous {
118                    id: id.to_ne_bytes(),
119                    str,
120                });
121            }
122        }
123    }
124}
125
126/// A borrowed RDF [blank node](https://www.w3.org/TR/rdf11-concepts/#dfn-blank-node).
127///
128/// The common way to create a new blank node is to use the [`BlankNode::default`] trait method.
129///
130/// It is also possible to create a blank node from a blank node identifier using the [`BlankNodeRef::new()`] function.
131/// The blank node identifier must be valid according to N-Triples, Turtle, and SPARQL grammars.
132///
133/// The default string formatter is returning an N-Triples, Turtle, and SPARQL compatible representation:
134/// ```
135/// use oxrdf::BlankNodeRef;
136///
137/// assert_eq!("_:a122", BlankNodeRef::new("a122")?.to_string());
138/// # Result::<_,oxrdf::BlankNodeIdParseError>::Ok(())
139/// ```
140#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)]
141pub struct BlankNodeRef<'a>(BlankNodeRefContent<'a>);
142
143#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
144enum BlankNodeRefContent<'a> {
145    Named(&'a str),
146    Anonymous { id: [u8; 16], str: &'a str },
147}
148
149impl<'a> BlankNodeRef<'a> {
150    /// Creates a blank node from a unique identifier.
151    ///
152    /// The blank node identifier must be valid according to N-Triples, Turtle, and SPARQL grammars.
153    ///
154    /// In most cases, it is much more convenient to create a blank node using [`BlankNode::default()`].
155    /// that creates a random ID that could be easily inlined by Oxigraph stores.
156    pub fn new(id: &'a str) -> Result<Self, BlankNodeIdParseError> {
157        validate_blank_node_identifier(id)?;
158        Ok(Self::new_unchecked(id))
159    }
160
161    /// Creates a blank node from a unique identifier without validation.
162    ///
163    /// It is the caller's responsibility to ensure that `id` is a valid blank node identifier
164    /// according to N-Triples, Turtle, and SPARQL grammars.
165    ///
166    /// [`BlankNodeRef::new()`) is a safe version of this constructor and should be used for untrusted data.
167    #[inline]
168    pub fn new_unchecked(id: &'a str) -> Self {
169        if let Some(numerical_id) = to_integer_id(id) {
170            Self(BlankNodeRefContent::Anonymous {
171                id: numerical_id.to_ne_bytes(),
172                str: id,
173            })
174        } else {
175            Self(BlankNodeRefContent::Named(id))
176        }
177    }
178
179    /// Returns the underlying ID of this blank node.
180    #[inline]
181    pub const fn as_str(self) -> &'a str {
182        match self.0 {
183            BlankNodeRefContent::Named(id) => id,
184            BlankNodeRefContent::Anonymous { str, .. } => str,
185        }
186    }
187
188    /// Returns the internal numerical ID of this blank node if it has been created using [`BlankNode::new_from_unique_id`].
189    ///
190    /// ```
191    /// use oxrdf::BlankNode;
192    ///
193    /// assert_eq!(
194    ///     BlankNode::new_from_unique_id(128).as_ref().unique_id(),
195    ///     Some(128)
196    /// );
197    /// assert_eq!(BlankNode::new("foo")?.as_ref().unique_id(), None);
198    /// # Result::<_,oxrdf::BlankNodeIdParseError>::Ok(())
199    /// ```
200    #[inline]
201    pub const fn unique_id(&self) -> Option<u128> {
202        match self.0 {
203            BlankNodeRefContent::Named(_) => None,
204            BlankNodeRefContent::Anonymous { id, .. } => Some(u128::from_ne_bytes(id)),
205        }
206    }
207
208    #[inline]
209    pub fn into_owned(self) -> BlankNode {
210        BlankNode(match self.0 {
211            BlankNodeRefContent::Named(id) => BlankNodeContent::Named(id.to_owned()),
212            BlankNodeRefContent::Anonymous { id, .. } => BlankNodeContent::Anonymous {
213                id,
214                str: IdStr::new(u128::from_ne_bytes(id)),
215            },
216        })
217    }
218}
219
220impl fmt::Display for BlankNodeRef<'_> {
221    #[inline]
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        write!(f, "_:{}", self.as_str())
224    }
225}
226
227impl<'a> From<&'a BlankNode> for BlankNodeRef<'a> {
228    #[inline]
229    fn from(node: &'a BlankNode) -> Self {
230        node.as_ref()
231    }
232}
233
234impl<'a> From<BlankNodeRef<'a>> for BlankNode {
235    #[inline]
236    fn from(node: BlankNodeRef<'a>) -> Self {
237        node.into_owned()
238    }
239}
240
241impl PartialEq<BlankNode> for BlankNodeRef<'_> {
242    #[inline]
243    fn eq(&self, other: &BlankNode) -> bool {
244        *self == other.as_ref()
245    }
246}
247
248impl PartialEq<BlankNodeRef<'_>> for BlankNode {
249    #[inline]
250    fn eq(&self, other: &BlankNodeRef<'_>) -> bool {
251        self.as_ref() == *other
252    }
253}
254
255#[derive(PartialEq, Eq, Debug, Clone, Hash)]
256struct IdStr([u8; 32]);
257
258impl IdStr {
259    #[inline]
260    fn new(id: u128) -> Self {
261        let mut str = [0; 32];
262        write!(&mut str[..], "{id:x}").unwrap();
263        Self(str)
264    }
265
266    #[inline]
267    fn as_str(&self) -> &str {
268        let len = self.0.iter().position(|x| x == &0).unwrap_or(32);
269        str::from_utf8(&self.0[..len]).unwrap()
270    }
271}
272
273fn validate_blank_node_identifier(id: &str) -> Result<(), BlankNodeIdParseError> {
274    let mut chars = id.chars();
275    let front = chars.next().ok_or(BlankNodeIdParseError)?;
276    match front {
277        '0'..='9'
278        | '_'
279        | ':'
280        | 'A'..='Z'
281        | 'a'..='z'
282        | '\u{00C0}'..='\u{00D6}'
283        | '\u{00D8}'..='\u{00F6}'
284        | '\u{00F8}'..='\u{02FF}'
285        | '\u{0370}'..='\u{037D}'
286        | '\u{037F}'..='\u{1FFF}'
287        | '\u{200C}'..='\u{200D}'
288        | '\u{2070}'..='\u{218F}'
289        | '\u{2C00}'..='\u{2FEF}'
290        | '\u{3001}'..='\u{D7FF}'
291        | '\u{F900}'..='\u{FDCF}'
292        | '\u{FDF0}'..='\u{FFFD}'
293        | '\u{10000}'..='\u{EFFFF}' => (),
294        _ => return Err(BlankNodeIdParseError),
295    }
296    for c in chars {
297        match c {
298            '.' // validated later
299            | '-'
300            | '0'..='9'
301            | '\u{00B7}'
302            | '\u{0300}'..='\u{036F}'
303            | '\u{203F}'..='\u{2040}'
304            | '_'
305            | ':'
306            | 'A'..='Z'
307            | 'a'..='z'
308            | '\u{00C0}'..='\u{00D6}'
309            | '\u{00D8}'..='\u{00F6}'
310            | '\u{00F8}'..='\u{02FF}'
311            | '\u{0370}'..='\u{037D}'
312            | '\u{037F}'..='\u{1FFF}'
313            | '\u{200C}'..='\u{200D}'
314            | '\u{2070}'..='\u{218F}'
315            | '\u{2C00}'..='\u{2FEF}'
316            | '\u{3001}'..='\u{D7FF}'
317            | '\u{F900}'..='\u{FDCF}'
318            | '\u{FDF0}'..='\u{FFFD}'
319            | '\u{10000}'..='\u{EFFFF}' => (),
320            _ => return Err(BlankNodeIdParseError),
321        }
322    }
323
324    // Could not end with a dot
325    if id.ends_with('.') {
326        Err(BlankNodeIdParseError)
327    } else {
328        Ok(())
329    }
330}
331
332#[inline]
333fn to_integer_id(id: &str) -> Option<u128> {
334    let digits = id.as_bytes();
335    let mut value: u128 = 0;
336    if let None | Some(b'0') = digits.first() {
337        return None; // No empty string or leading zeros
338    }
339    for digit in digits {
340        value = value.checked_mul(16)?.checked_add(
341            match *digit {
342                b'0'..=b'9' => digit - b'0',
343                b'a'..=b'f' => digit - b'a' + 10,
344                _ => return None,
345            }
346            .into(),
347        )?;
348    }
349    Some(value)
350}
351
352/// An error raised during [`BlankNode`] IDs validation.
353#[derive(Debug, thiserror::Error)]
354#[error("The blank node identifier is invalid")]
355pub struct BlankNodeIdParseError;
356
357#[cfg(feature = "serde")]
358impl Serialize for BlankNode {
359    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
360        self.as_ref().serialize(serializer)
361    }
362}
363
364#[cfg(feature = "serde")]
365impl Serialize for BlankNodeRef<'_> {
366    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
367        #[derive(Serialize)]
368        #[serde(rename = "BlankNode")]
369        struct Value<'a> {
370            value: &'a str,
371        }
372        Value {
373            value: self.as_str(),
374        }
375        .serialize(serializer)
376    }
377}
378
379#[cfg(feature = "serde")]
380impl<'de> Deserialize<'de> for BlankNode {
381    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
382    where
383        D: Deserializer<'de>,
384    {
385        #[derive(Deserialize)]
386        #[serde(rename = "BlankNode")]
387        struct Value {
388            value: String,
389        }
390        Self::new(Value::deserialize(deserializer)?.value).map_err(de::Error::custom)
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    #[cfg(not(target_family = "wasm"))]
398    use std::mem::{align_of, size_of};
399
400    #[test]
401    fn as_str_partial() {
402        let b = BlankNode::new_from_unique_id(0x42);
403        assert_eq!(b.as_str(), "42");
404    }
405
406    #[test]
407    fn as_str_full() {
408        let b = BlankNode::new_from_unique_id(0x7777_6666_5555_4444_3333_2222_1111_0000);
409        assert_eq!(b.as_str(), "77776666555544443333222211110000");
410    }
411
412    #[test]
413    fn new_validation() {
414        BlankNode::new("").unwrap_err();
415        BlankNode::new("a").unwrap();
416        BlankNode::new("-").unwrap_err();
417        BlankNode::new("a-").unwrap();
418        BlankNode::new(".").unwrap_err();
419        BlankNode::new("a.").unwrap_err();
420        BlankNode::new("a.a").unwrap();
421    }
422
423    #[test]
424    fn new_numerical() {
425        assert_eq!(
426            BlankNode::new("100a").unwrap(),
427            BlankNode::new_from_unique_id(0x100a),
428        );
429        assert_ne!(
430            BlankNode::new("100A").unwrap(),
431            BlankNode::new_from_unique_id(0x100a)
432        );
433    }
434
435    #[test]
436    fn test_equals() {
437        assert_eq!(
438            BlankNode::new("100a").unwrap(),
439            BlankNodeRef::new("100a").unwrap()
440        );
441        assert_eq!(
442            BlankNode::new("zzz").unwrap(),
443            BlankNodeRef::new("zzz").unwrap()
444        );
445    }
446
447    #[cfg(target_pointer_width = "64")]
448    #[test]
449    fn test_size_and_alignment() {
450        assert_eq!(size_of::<BlankNode>(), 56);
451        assert_eq!(size_of::<BlankNodeRef<'_>>(), 32);
452        assert_eq!(align_of::<BlankNode>(), 8);
453        assert_eq!(align_of::<BlankNodeRef<'_>>(), 8);
454    }
455
456    #[test]
457    #[cfg(feature = "serde")]
458    fn test_serde() {
459        let b = BlankNode::new("123a").unwrap();
460        let json = serde_json::to_string(&b).unwrap();
461        assert_eq!(json, "{\"value\":\"123a\"}");
462        let b2: BlankNode = serde_json::from_str(&json).unwrap();
463        assert_eq!(b2, b);
464    }
465}