Skip to main content

rdf_model/heap/
term.rs

1// This is free and unencumbered software released into the public domain.
2
3use crate::{BaseDirection, CowTerm, DEFAULT_GRAPH_URN, Datatype, DefaultGraph, Term, TermKind};
4use alloc::{
5    borrow::Cow,
6    string::{String, ToString},
7};
8use xsd::{DecimalValue, PrimitiveValue, Value, primitive::Boolean};
9
10type Language = String; // TODO
11
12/// A heap-allocated term.
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14// #[cfg_attr(
15//     feature = "borsh",
16//     derive(borsh::BorshSerialize, borsh::BorshDeserialize)
17// )]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub enum HeapTerm {
20    /// An IRI or IRI reference term.
21    Iri(String),
22
23    /// A blank node (aka bnode) term.
24    BNode(String),
25
26    /// A plain string term.
27    /// The term's datatype is `xsd:string`.
28    String(String),
29
30    /// A language-tagged string term.
31    /// The term's datatype is either `rdf:langString` or `rdf:dirLangString`.
32    TaggedString(String, Language, Option<BaseDirection>),
33
34    /// A typed value term, without the original parsed lexical spelling.
35    /// Use [`Self::TypedLiteral`] when RDF lexical identity must be retained.
36    TypedValue(Value),
37
38    /// A typed literal retaining its lexical spelling and datatype without parsing.
39    /// It can be valid, ill-typed, or have an unsupported datatype. Distinct lexical
40    /// forms remain distinct even when they denote the same XSD value.
41    TypedLiteral(String, Datatype),
42
43    /// The default-graph singleton; distinct from `Iri(DEFAULT_GRAPH_URN)`.
44    DefaultGraph,
45}
46
47impl HeapTerm {
48    pub fn iri(value: impl Into<String>) -> Self {
49        Self::Iri(value.into())
50    }
51
52    pub fn bnode(id: impl Into<String>) -> Self {
53        Self::BNode(id.into())
54    }
55
56    pub fn string(value: impl Into<String>) -> Self {
57        Self::String(value.into())
58    }
59
60    pub fn tagged_string(value: impl Into<String>, tag: impl Into<Language>) -> Self {
61        Self::TaggedString(value.into(), tag.into(), None)
62    }
63
64    pub fn tagged_string_with_dir(
65        value: impl Into<String>,
66        tag: impl Into<Language>,
67        dir: impl Into<BaseDirection>,
68    ) -> Self {
69        Self::TaggedString(value.into(), tag.into(), Some(dir.into()))
70    }
71
72    /// Constructs a value-backed literal, formatting from the stored XSD value.
73    /// Original lexical spelling is unavailable; use [`Self::typed_literal`] to
74    /// preserve spelling from an RDF source. Requires `alloc`.
75    pub fn typed_value(value: impl Into<Value>) -> Self {
76        Self::TypedValue(value.into())
77    }
78
79    /// Constructs a lexical literal without parsing or normalizing its content.
80    ///
81    /// Preserves RDF lexical and datatype identity, including equivalent XSD
82    /// timezone spellings. No XSD validity check is performed. Requires `alloc`.
83    ///
84    /// ```
85    /// use rdf_model::HeapTerm;
86    /// let signed_zero = HeapTerm::typed_literal("2026-01-02+00:00", xsd::DATE);
87    /// let utc = HeapTerm::typed_literal("2026-01-02Z", xsd::DATE);
88    /// assert_ne!(signed_zero, utc);
89    /// assert_eq!(signed_zero.value_str(), "2026-01-02+00:00");
90    /// ```
91    pub fn typed_literal(literal: impl Into<String>, datatype: impl Into<Datatype>) -> Self {
92        Self::TypedLiteral(literal.into(), datatype.into())
93    }
94
95    pub fn kind(&self) -> TermKind {
96        match self {
97            Self::DefaultGraph => TermKind::DefaultGraph,
98            Self::Iri(_) => TermKind::Iri,
99            Self::BNode(_) => TermKind::BNode,
100            Self::String(_)
101            | Self::TaggedString(_, _, _)
102            | Self::TypedValue(_)
103            | Self::TypedLiteral(_, _) => TermKind::Literal,
104        }
105    }
106
107    pub fn value_str(&self) -> Cow<'_, str> {
108        Cow::Borrowed(match self {
109            Self::DefaultGraph => DEFAULT_GRAPH_URN,
110            Self::Iri(s) => s.as_str(),
111            Self::BNode(s) => s.as_str(),
112            Self::String(s) | Self::TaggedString(s, _, _) | Self::TypedLiteral(s, _) => s.as_str(),
113            Self::TypedValue(Value::Primitive(PrimitiveValue::Boolean(Boolean::FALSE))) => "false",
114            Self::TypedValue(Value::Primitive(PrimitiveValue::Boolean(Boolean::TRUE))) => "true",
115            Self::TypedValue(Value::Primitive(
116                PrimitiveValue::String(s) | PrimitiveValue::AnyUri(s),
117            )) => s.as_str(),
118            Self::TypedValue(v) => return Cow::Owned(v.to_string()),
119        })
120    }
121
122    #[cfg(feature = "serde")]
123    pub fn to_json(&self) -> Option<serde_json::Value> {
124        Some(self.clone().into_json())
125    }
126
127    #[cfg(feature = "serde")]
128    /// Exports this term in the JSON boundary representation. The default-graph
129    /// marker uses `urn:rdf:default-graph`; generic IRI imports do not implicitly
130    /// decode that convention. Serde's tagged enum encoding retains its identity.
131    pub fn into_json(self) -> serde_json::Value {
132        use serde_json::{Value as JsonValue, json};
133        match self {
134            Self::DefaultGraph => JsonValue::String(DEFAULT_GRAPH_URN.into()),
135            Self::Iri(str) => JsonValue::String(str),
136            Self::BNode(id) => JsonValue::String(id),
137            Self::String(val) => json!({ "@value": val }),
138            Self::TaggedString(val, lang, _) => json!({ "@value": val, "@language": lang }), // TODO: direction
139            Self::TypedLiteral(val, r#type) => {
140                json!({ "@value": val, "@type": r#type.to_string() })
141            },
142            Self::TypedValue(Value::Primitive(val)) => match val {
143                PrimitiveValue::String(val) => json!({ "@value": val }),
144                PrimitiveValue::Boolean(val) => val.into_json(),
145                PrimitiveValue::Double(val) => val.into_json(),
146                val => {
147                    let r#type = val.r#type();
148                    json!({ "@value": val.into_json(), "@type": r#type.curie() })
149                },
150            },
151            Self::TypedValue(Value::Decimal(val)) => match val {
152                DecimalValue::Integer(val) => json!({ "@value": val, "@type": "xsd:integer" }),
153                val => {
154                    let r#type = val.r#type();
155                    json!({ "@value": val.into_json(), "@type": r#type.curie() })
156                },
157            },
158        }
159    }
160
161    #[cfg(feature = "bson")]
162    pub fn to_bson(&self) -> Option<bson::Bson> {
163        Some(self.clone().into_bson())
164    }
165
166    #[cfg(feature = "bson")]
167    /// Exports the term as BSON, using `urn:rdf:default-graph` for the marker.
168    pub fn into_bson(self) -> bson::Bson {
169        use bson::{Bson, Document};
170        match self {
171            Self::DefaultGraph => Bson::String(DEFAULT_GRAPH_URN.into()),
172            Self::Iri(str) => Bson::String(str),
173            Self::BNode(id) => Bson::String(alloc::format!("_:{}", id)),
174            Self::String(val) => {
175                let mut doc = Document::new();
176                doc.insert("@value", val);
177                Bson::Document(doc)
178            },
179            Self::TaggedString(val, lang, _) => {
180                let mut doc = Document::new();
181                doc.insert("@value", val);
182                doc.insert("@language", lang);
183                Bson::Document(doc)
184            },
185            Self::TypedLiteral(lit, r#type) => {
186                let mut doc = Document::new();
187                doc.insert("@value", lit);
188                doc.insert("@type", r#type.to_string());
189                Bson::Document(doc)
190            },
191            Self::TypedValue(val) => {
192                let r#type = val.r#type();
193                let mut doc = Document::new();
194                doc.insert("@value", val.into_bson());
195                doc.insert("@type", r#type.curie());
196                Bson::Document(doc)
197            },
198        }
199    }
200}
201
202impl Term for HeapTerm {
203    fn kind(&self) -> TermKind {
204        self.kind()
205    }
206
207    fn value_str(&self) -> Cow<'_, str> {
208        self.value_str()
209    }
210}
211
212impl Term for &HeapTerm {
213    fn kind(&self) -> TermKind {
214        (*self).kind()
215    }
216
217    fn value_str(&self) -> Cow<'_, str> {
218        (*self).value_str()
219    }
220}
221
222impl From<DefaultGraph> for HeapTerm {
223    fn from(_: DefaultGraph) -> Self {
224        Self::DefaultGraph
225    }
226}
227
228impl From<DefaultGraph> for &HeapTerm {
229    fn from(_: DefaultGraph) -> Self {
230        &HeapTerm::DefaultGraph
231    }
232}
233
234impl<T> From<&T> for HeapTerm
235where
236    T: Clone + Into<Self>,
237{
238    fn from(t: &T) -> Self {
239        t.clone().into()
240    }
241}
242
243impl From<&dyn Term> for HeapTerm {
244    fn from(term: &dyn Term) -> Self {
245        match term.kind() {
246            TermKind::DefaultGraph => Self::DefaultGraph,
247            TermKind::Iri => Self::iri(term.value_str()),
248            TermKind::BNode => Self::bnode(term.value_str()),
249            TermKind::Literal => Self::string(term.value_str()), // FIXME
250        }
251    }
252}
253
254impl From<CowTerm<'_>> for HeapTerm {
255    fn from(input: CowTerm<'_>) -> Self {
256        match input {
257            CowTerm::DefaultGraph => Self::DefaultGraph,
258            CowTerm::Iri(s) => Self::Iri(s.to_string()),
259            CowTerm::BNode(s) => Self::BNode(s.to_string()),
260            CowTerm::String(s) => Self::String(s.to_string()),
261            CowTerm::TaggedString(s, lang, dir) => {
262                Self::TaggedString(s.to_string(), lang.clone(), dir.clone())
263            },
264            CowTerm::TypedValue(v) => Self::TypedValue(v.clone()),
265            CowTerm::TypedLiteral(s, dt) => Self::TypedLiteral(s.to_string(), dt.clone()),
266        }
267    }
268}
269
270impl From<&str> for HeapTerm {
271    fn from(value: &str) -> Self {
272        Self::string(value)
273    }
274}
275
276impl From<String> for HeapTerm {
277    fn from(value: String) -> Self {
278        Self::String(value)
279    }
280}
281
282impl From<Boolean> for HeapTerm {
283    fn from(value: Boolean) -> Self {
284        Self::TypedValue(value.into())
285    }
286}
287
288impl From<(String, Language)> for HeapTerm {
289    fn from((literal, language): (String, Language)) -> Self {
290        Self::TaggedString(literal, language, None)
291    }
292}
293
294/// Parses supported XSD literals, retaining the original lexical form and datatype
295/// as a [`HeapTerm::TypedLiteral`] when parsing fails or is unsupported.
296/// Successful parsing constructs a value-backed term and can normalize spelling;
297/// use [`HeapTerm::typed_literal`] instead to retain RDF lexical identity.
298/// Requires the `alloc` feature.
299impl From<(String, Datatype)> for HeapTerm {
300    fn from((literal, datatype): (String, Datatype)) -> Self {
301        let result = match &datatype {
302            Datatype::Xsd(t) => xsd::parse(&literal, t),
303            _ => return Self::TypedLiteral(literal, datatype),
304        };
305        match result {
306            Ok(value) => Self::TypedValue(value),
307            Err(_) => Self::TypedLiteral(literal, datatype),
308        }
309    }
310}
311
312impl From<(String, Option<Datatype>, Option<Language>)> for HeapTerm {
313    fn from((literal, datatype, language): (String, Option<Datatype>, Option<Language>)) -> Self {
314        match (language, datatype) {
315            (None, None) => Self::string(literal),
316            (None, Some(datatype)) => Self::from((literal, datatype)),
317            (Some(language), None) => Self::tagged_string(literal, language),
318            (Some(language), Some(_)) => Self::tagged_string(literal, language), // TODO
319        }
320    }
321}
322
323#[cfg(feature = "bson")]
324impl From<HeapTerm> for bson::Bson {
325    fn from(input: HeapTerm) -> Self {
326        input.into_bson()
327    }
328}
329
330#[cfg(feature = "serde")]
331impl TryFrom<serde_json::Value> for HeapTerm {
332    type Error = ();
333
334    fn try_from(input: serde_json::Value) -> Result<Self, Self::Error> {
335        use serde_json::Value;
336        Ok(match input {
337            Value::Null => return Err(()), // not supported
338            Value::Bool(b) => Self::TypedValue(b.into()),
339            Value::Number(n) => Self::TypedValue(n.as_f64().unwrap().into()), // FIXME
340            Value::String(s) if s.starts_with("_:") => Self::BNode(s.clone()),
341            Value::String(s) => Self::Iri(s.clone()),
342            Value::Array(_) => return Err(()), // not supported
343            Value::Object(mut input) => {
344                let value = input.remove("@value").ok_or(())?;
345                let r#type = input
346                    .remove("@type")
347                    .and_then(|s| s.as_str().map(|s| s.parse::<Datatype>().ok()))
348                    .flatten();
349                let language = input
350                    .remove("@language")
351                    .and_then(|s| s.as_str().map(|s| s.to_string()));
352                match (r#type, language) {
353                    (None, None) => Self::String(value.to_string()),
354                    (None, Some(lang)) => Self::TaggedString(value.to_string(), lang.into(), None),
355                    (Some(r#type), None) => Self::TypedLiteral(value.to_string(), r#type),
356                    (Some(_), Some(_)) => return Err(()), // invalid literal
357                }
358            },
359        })
360    }
361}
362
363#[cfg(feature = "serde")]
364impl TryFrom<&serde_json::Value> for HeapTerm {
365    type Error = ();
366
367    fn try_from(input: &serde_json::Value) -> Result<Self, Self::Error> {
368        input.clone().try_into()
369    }
370}