Skip to main content

oxirs_core/jsonld/
from_rdf.rs

1#[cfg(feature = "async")]
2use json_event_parser::TokioAsyncWriterJsonSerializer;
3use json_event_parser::{JsonEvent, WriterJsonSerializer};
4// Native OxiRS types (Phase 3 migration completed)
5use crate::model::node::NamedOrBlankNodeRef;
6use crate::model::quad::GraphNameRef;
7use crate::model::triple::{ObjectRef, PredicateRef, SubjectRef};
8use crate::model::*;
9use crate::optimization::TermRef;
10use crate::vocab::xsd;
11use oxiri::{Iri, IriParseError};
12use std::borrow::Cow;
13use std::collections::{BTreeMap, BTreeSet};
14use std::io;
15use std::io::Write;
16#[cfg(feature = "async")]
17use tokio::io::AsyncWrite;
18
19/// A [JSON-LD](https://www.w3.org/TR/json-ld/) serializer.
20///
21/// Returns [Streaming JSON-LD](https://www.w3.org/TR/json-ld11-streaming/).
22///
23/// It does not implement exactly the [RDF as JSON-LD Algorithm](https://www.w3.org/TR/json-ld-api/#serialize-rdf-as-json-ld-algorithm)
24/// to be a streaming serializer but aims at being close to it.
25/// Features like `@json` and `@list` generation are not implemented.
26///
27/// ```
28/// use oxrdf::{GraphNameRef, LiteralRef, NamedNodeRef, QuadRef};
29/// use oxrdf::vocab::rdf;
30/// use oxjsonld::JsonLdSerializer;
31///
32/// let mut serializer = JsonLdSerializer::new().with_prefix("schema", "http://schema.org/")?.for_writer(Vec::new());
33/// serializer.serialize_quad(QuadRef::new(
34///     NamedNodeRef::new("http://example.com#me")?,
35///     rdf::TYPE,
36///     NamedNodeRef::new("http://schema.org/Person")?,
37///     GraphNameRef::DefaultGraph
38/// ))?;
39/// serializer.serialize_quad(QuadRef::new(
40///     NamedNodeRef::new("http://example.com#me")?,
41///     NamedNodeRef::new("http://schema.org/name")?,
42///     LiteralRef::new_language_tagged_literal_unchecked("Foo Bar", "en"),
43///     GraphNameRef::DefaultGraph
44/// ))?;
45/// assert_eq!(
46///     b"{\"@context\":{\"schema\":\"http://schema.org/\"},\"@graph\":[{\"@id\":\"http://example.com#me\",\"http://www.w3.org/1999/02/22-rdf-syntax-ns#type\":[{\"@id\":\"http://schema.org/Person\"}],\"http://schema.org/name\":[{\"@language\":\"en\",\"@value\":\"Foo Bar\"}]}]}",
47///     serializer.finish()?.as_slice()
48/// );
49/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
50/// ```
51#[derive(Default, Clone)]
52#[must_use]
53pub struct JsonLdSerializer {
54    prefixes: BTreeMap<String, String>,
55    base_iri: Option<Iri<String>>,
56}
57
58impl JsonLdSerializer {
59    /// Builds a new [`JsonLdSerializer`].
60    #[inline]
61    pub fn new() -> Self {
62        Self {
63            prefixes: BTreeMap::new(),
64            base_iri: None,
65        }
66    }
67
68    #[inline]
69    pub fn with_prefix(
70        mut self,
71        prefix_name: impl Into<String>,
72        prefix_iri: impl Into<String>,
73    ) -> Result<Self, IriParseError> {
74        self.prefixes.insert(
75            prefix_name.into(),
76            Iri::parse(prefix_iri.into())?.into_inner(),
77        );
78        Ok(self)
79    }
80
81    /// Allows to set the base IRI for serialization.
82    ///
83    /// Corresponds to the [`base` option from the algorithm specification](https://www.w3.org/TR/json-ld-api/#dom-jsonldoptions-base).
84    /// ```
85    /// use oxrdf::{GraphNameRef, NamedNodeRef, QuadRef};
86    /// use oxjsonld::JsonLdSerializer;
87    ///
88    /// let mut serializer = JsonLdSerializer::new()
89    ///     .with_base_iri("http://example.com")?
90    ///     .with_prefix("ex", "http://example.com/ns#")?
91    ///     .for_writer(Vec::new());
92    /// serializer.serialize_quad(QuadRef::new(
93    ///     NamedNodeRef::new("http://example.com#me")?,
94    ///     NamedNodeRef::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?,
95    ///     NamedNodeRef::new("http://example.com/ns#Person")?,
96    ///     GraphNameRef::DefaultGraph
97    /// ))?;
98    /// serializer.serialize_quad(QuadRef::new(
99    ///     NamedNodeRef::new("http://example.com#me")?,
100    ///     NamedNodeRef::new("http://example.com/ns#parent")?,
101    ///     NamedNodeRef::new("http://example.com#other")?,
102    ///     GraphNameRef::DefaultGraph
103    /// ))?;
104    /// assert_eq!(
105    ///     b"{\"@context\":{\"@base\":\"http://example.com\",\"ex\":\"http://example.com/ns#\"},\"@graph\":[{\"@id\":\"#me\",\"http://www.w3.org/1999/02/22-rdf-syntax-ns#type\":[{\"@id\":\"/ns#Person\"}],\"http://example.com/ns#parent\":[{\"@id\":\"#other\"}]}]}",
106    ///     serializer.finish()?.as_slice()
107    /// );
108    /// # Result::<_,Box<dyn std::error::Error>>::Ok(())
109    /// ```
110    #[inline]
111    pub fn with_base_iri(mut self, base_iri: impl Into<String>) -> Result<Self, IriParseError> {
112        self.base_iri = Some(Iri::parse(base_iri.into())?);
113        Ok(self)
114    }
115
116    /// Serializes a JSON-LD file to a [`Write`] implementation.
117    ///
118    /// This writer does unbuffered writes.
119    ///
120    /// ```
121    /// use oxrdf::{GraphNameRef, LiteralRef, NamedNodeRef, QuadRef};
122    /// use oxrdf::vocab::rdf;
123    /// use oxjsonld::JsonLdSerializer;
124    ///
125    /// let mut serializer = JsonLdSerializer::new().with_prefix("schema", "http://schema.org/")?.for_writer(Vec::new());
126    /// serializer.serialize_quad(QuadRef::new(
127    ///     NamedNodeRef::new("http://example.com#me")?,
128    ///     rdf::TYPE,
129    ///     NamedNodeRef::new("http://schema.org/Person")?,
130    ///     GraphNameRef::DefaultGraph
131    /// ))?;
132    /// serializer.serialize_quad(QuadRef::new(
133    ///     NamedNodeRef::new("http://example.com#me")?,
134    ///     NamedNodeRef::new("http://schema.org/name")?,
135    ///     LiteralRef::new_language_tagged_literal_unchecked("Foo Bar", "en"),
136    ///     GraphNameRef::DefaultGraph
137    /// ))?;
138    /// assert_eq!(
139    ///     b"{\"@context\":{\"schema\":\"http://schema.org/\"},\"@graph\":[{\"@id\":\"http://example.com#me\",\"http://www.w3.org/1999/02/22-rdf-syntax-ns#type\":[{\"@id\":\"http://schema.org/Person\"}],\"http://schema.org/name\":[{\"@language\":\"en\",\"@value\":\"Foo Bar\"}]}]}",
140    ///     serializer.finish()?.as_slice()
141    /// );
142    /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
143    /// ```
144    pub fn for_writer<W: Write>(self, writer: W) -> WriterJsonLdSerializer<W> {
145        WriterJsonLdSerializer {
146            writer: WriterJsonSerializer::new(writer),
147            inner: self.inner_writer(),
148        }
149    }
150
151    /// Serializes a JSON-LD file to a [`AsyncWrite`] implementation.
152    ///
153    /// This writer does unbuffered writes.
154    ///
155    /// ```
156    /// # #[tokio::main(flavor = "current_thread")]
157    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
158    /// use oxrdf::{NamedNodeRef, QuadRef, LiteralRef, GraphNameRef};
159    /// use oxrdf::vocab::rdf;
160    /// use oxjsonld::JsonLdSerializer;
161    ///
162    /// let mut serializer = JsonLdSerializer::new().with_prefix("schema", "http://schema.org/")?.for_tokio_async_writer(Vec::new());
163    /// serializer.serialize_quad(QuadRef::new(
164    ///     NamedNodeRef::new("http://example.com#me")?,
165    ///     rdf::TYPE,
166    ///     NamedNodeRef::new("http://schema.org/Person")?,
167    ///     GraphNameRef::DefaultGraph
168    /// )).await?;
169    /// serializer.serialize_quad(QuadRef::new(
170    ///     NamedNodeRef::new("http://example.com#me")?,
171    ///     NamedNodeRef::new("http://schema.org/name")?,
172    ///     LiteralRef::new_language_tagged_literal_unchecked("Foo Bar", "en"),
173    ///     GraphNameRef::DefaultGraph
174    /// )).await?;
175    /// assert_eq!(
176    ///     b"{\"@context\":{\"schema\":\"http://schema.org/\"},\"@graph\":[{\"@id\":\"http://example.com#me\",\"http://www.w3.org/1999/02/22-rdf-syntax-ns#type\":[{\"@id\":\"http://schema.org/Person\"}],\"http://schema.org/name\":[{\"@language\":\"en\",\"@value\":\"Foo Bar\"}]}]}",
177    ///     serializer.finish().await?.as_slice()
178    /// );
179    /// # Ok(())
180    /// # }
181    /// ```
182    #[cfg(feature = "async")]
183    pub fn for_tokio_async_writer<W: AsyncWrite + Unpin>(
184        self,
185        writer: W,
186    ) -> TokioAsyncWriterJsonLdSerializer<W> {
187        TokioAsyncWriterJsonLdSerializer {
188            writer: TokioAsyncWriterJsonSerializer::new(writer),
189            inner: self.inner_writer(),
190        }
191    }
192
193    fn inner_writer(self) -> InnerJsonLdWriter {
194        InnerJsonLdWriter {
195            started: false,
196            current_graph_name: None,
197            current_subject: None,
198            current_predicate: None,
199            emitted_predicates: BTreeSet::new(),
200            prefixes: self.prefixes,
201            base_iri: self.base_iri,
202        }
203    }
204}
205
206/// Serializes a JSON-LD file to a [`Write`] implementation.
207///
208/// Can be built using [`JsonLdSerializer::for_writer`].
209///
210/// ```
211/// use oxrdf::{GraphNameRef, LiteralRef, NamedNodeRef, QuadRef};
212/// use oxrdf::vocab::rdf;
213/// use oxjsonld::JsonLdSerializer;
214///
215/// let mut serializer = JsonLdSerializer::new().with_prefix("schema", "http://schema.org/")?.for_writer(Vec::new());
216/// serializer.serialize_quad(QuadRef::new(
217///     NamedNodeRef::new("http://example.com#me")?,
218///     rdf::TYPE,
219///     NamedNodeRef::new("http://schema.org/Person")?,
220///     GraphNameRef::DefaultGraph
221/// ))?;
222/// serializer.serialize_quad(QuadRef::new(
223///     NamedNodeRef::new("http://example.com#me")?,
224///     NamedNodeRef::new("http://schema.org/name")?,
225///     LiteralRef::new_language_tagged_literal_unchecked("Foo Bar", "en"),
226///     GraphNameRef::DefaultGraph
227/// ))?;
228/// assert_eq!(
229///     b"{\"@context\":{\"schema\":\"http://schema.org/\"},\"@graph\":[{\"@id\":\"http://example.com#me\",\"http://www.w3.org/1999/02/22-rdf-syntax-ns#type\":[{\"@id\":\"http://schema.org/Person\"}],\"http://schema.org/name\":[{\"@language\":\"en\",\"@value\":\"Foo Bar\"}]}]}",
230///     serializer.finish()?.as_slice()
231/// );
232/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
233/// ```
234#[must_use]
235pub struct WriterJsonLdSerializer<W: Write> {
236    writer: WriterJsonSerializer<W>,
237    inner: InnerJsonLdWriter,
238}
239
240impl<W: Write> WriterJsonLdSerializer<W> {
241    /// Serializes an extra quad.
242    pub fn serialize_quad<'a>(&mut self, t: impl Into<QuadRef<'a>>) -> io::Result<()> {
243        let mut buffer = Vec::new();
244        self.inner.serialize_quad(t, &mut buffer)?;
245        self.flush_buffer(&mut buffer)
246    }
247
248    /// Ends the write process and returns the underlying [`Write`].
249    pub fn finish(mut self) -> io::Result<W> {
250        let mut buffer = Vec::new();
251        self.inner.finish(&mut buffer);
252        self.flush_buffer(&mut buffer)?;
253        self.writer.finish()
254    }
255
256    fn flush_buffer(&mut self, buffer: &mut Vec<JsonEvent<'_>>) -> io::Result<()> {
257        for event in buffer.drain(0..) {
258            self.writer.serialize_event(event)?;
259        }
260        Ok(())
261    }
262}
263
264/// Serializes a JSON-LD file to a [`AsyncWrite`] implementation.
265///
266/// Can be built using [`JsonLdSerializer::for_tokio_async_writer`].
267///
268/// ```
269/// # #[tokio::main(flavor = "current_thread")]
270/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
271/// use oxrdf::{NamedNodeRef, QuadRef, LiteralRef, GraphNameRef};
272/// use oxrdf::vocab::rdf;
273/// use oxjsonld::JsonLdSerializer;
274///
275/// let mut serializer = JsonLdSerializer::new().with_prefix("schema", "http://schema.org/")?.for_tokio_async_writer(Vec::new());
276/// serializer.serialize_quad(QuadRef::new(
277///     NamedNodeRef::new("http://example.com#me")?,
278///     rdf::TYPE,
279///     NamedNodeRef::new("http://schema.org/Person")?,
280///     GraphNameRef::DefaultGraph
281/// )).await?;
282/// serializer.serialize_quad(QuadRef::new(
283///     NamedNodeRef::new("http://example.com#me")?,
284///     NamedNodeRef::new("http://schema.org/name")?,
285///     LiteralRef::new_language_tagged_literal_unchecked("Foo Bar", "en"),
286///     GraphNameRef::DefaultGraph
287/// )).await?;
288/// assert_eq!(
289///     b"{\"@context\":{\"schema\":\"http://schema.org/\"},\"@graph\":[{\"@id\":\"http://example.com#me\",\"http://www.w3.org/1999/02/22-rdf-syntax-ns#type\":[{\"@id\":\"http://schema.org/Person\"}],\"http://schema.org/name\":[{\"@language\":\"en\",\"@value\":\"Foo Bar\"}]}]}",
290///     serializer.finish().await?.as_slice()
291/// );
292/// # Ok(())
293/// # }
294/// ```
295#[cfg(feature = "async")]
296#[must_use]
297pub struct TokioAsyncWriterJsonLdSerializer<W: AsyncWrite + Unpin> {
298    writer: TokioAsyncWriterJsonSerializer<W>,
299    inner: InnerJsonLdWriter,
300}
301
302#[cfg(feature = "async")]
303impl<W: AsyncWrite + Unpin> TokioAsyncWriterJsonLdSerializer<W> {
304    /// Serializes an extra quad.
305    pub async fn serialize_quad<'a>(&mut self, t: impl Into<QuadRef<'a>>) -> io::Result<()> {
306        let mut buffer = Vec::new();
307        self.inner.serialize_quad(t, &mut buffer)?;
308        self.flush_buffer(&mut buffer).await
309    }
310
311    /// Ends the write process and returns the underlying [`Write`].
312    pub async fn finish(mut self) -> io::Result<W> {
313        let mut buffer = Vec::new();
314        self.inner.finish(&mut buffer);
315        self.flush_buffer(&mut buffer).await?;
316        self.writer.finish()
317    }
318
319    async fn flush_buffer(&mut self, buffer: &mut Vec<JsonEvent<'_>>) -> io::Result<()> {
320        for event in buffer.drain(0..) {
321            self.writer.serialize_event(event).await?;
322        }
323        Ok(())
324    }
325}
326
327pub struct InnerJsonLdWriter {
328    started: bool,
329    current_graph_name: Option<GraphName>,
330    current_subject: Option<NamedOrBlankNode>,
331    current_predicate: Option<NamedNode>,
332    emitted_predicates: BTreeSet<String>,
333    prefixes: BTreeMap<String, String>,
334    base_iri: Option<Iri<String>>,
335}
336
337impl InnerJsonLdWriter {
338    fn serialize_quad<'a>(
339        &mut self,
340        quad: impl Into<QuadRef<'a>>,
341        output: &mut Vec<JsonEvent<'a>>,
342    ) -> io::Result<()> {
343        if !self.started {
344            self.serialize_start(output);
345            self.started = true;
346        }
347
348        let quad = quad.into();
349        if self
350            .current_graph_name
351            .as_ref()
352            .is_some_and(|graph_name| graph_name != &quad.graph_name().to_owned())
353        {
354            output.push(JsonEvent::EndArray);
355            output.push(JsonEvent::EndObject);
356            if self
357                .current_graph_name
358                .as_ref()
359                .is_some_and(|g| !g.is_default_graph())
360            {
361                output.push(JsonEvent::EndArray);
362                output.push(JsonEvent::EndObject);
363            }
364            self.current_graph_name = None;
365            self.current_subject = None;
366            self.current_predicate = None;
367            self.emitted_predicates.clear();
368        } else if self.current_subject.as_ref().is_some_and(|subject| {
369            match quad.subject() {
370                SubjectRef::NamedNode(n) => subject != &NamedOrBlankNode::NamedNode(n.to_owned()),
371                SubjectRef::BlankNode(b) => subject != &NamedOrBlankNode::BlankNode(b.to_owned()),
372                _ => true, // Variable and QuotedTriple are not supported in JSON-LD
373            }
374        }) || self.current_predicate.as_ref().is_some_and(|predicate| {
375            match quad.predicate() {
376                PredicateRef::NamedNode(n) => predicate != &n.to_owned(),
377                _ => true, // Variable predicates are not supported in JSON-LD
378            }
379        }) && {
380            let pred_str = match quad.predicate() {
381                PredicateRef::NamedNode(n) => n.as_str(),
382                PredicateRef::Variable(v) => v.as_str(),
383            };
384            self.emitted_predicates.contains(pred_str)
385        } {
386            output.push(JsonEvent::EndArray);
387            output.push(JsonEvent::EndObject);
388            self.current_subject = None;
389            self.emitted_predicates.clear();
390            self.current_predicate = None;
391        } else if self.current_predicate.as_ref().is_some_and(|predicate| {
392            match quad.predicate() {
393                PredicateRef::NamedNode(n) => predicate != &n.to_owned(),
394                _ => true, // Variable predicates are not supported in JSON-LD
395            }
396        }) {
397            output.push(JsonEvent::EndArray);
398            if let Some(current_predicate) = self.current_predicate.take() {
399                self.emitted_predicates
400                    .insert(current_predicate.into_string());
401            }
402        }
403
404        if self.current_graph_name.is_none() {
405            if !quad.graph_name().is_default_graph() {
406                // We open a new graph name
407                output.push(JsonEvent::StartObject);
408                output.push(JsonEvent::ObjectKey("@id".into()));
409                output.push(JsonEvent::String(self.id_value(match quad.graph_name() {
410                    GraphNameRef::NamedNode(iri) => NamedOrBlankNodeRef::NamedNode(iri),
411                    GraphNameRef::BlankNode(bnode) => NamedOrBlankNodeRef::BlankNode(bnode),
412                    GraphNameRef::DefaultGraph => unreachable!(),
413                    GraphNameRef::Variable(_) => unreachable!(), // Variables shouldn't appear in actual data
414                })));
415                output.push(JsonEvent::ObjectKey("@graph".into()));
416                output.push(JsonEvent::StartArray);
417            }
418            self.current_graph_name = Some(quad.graph_name().to_owned());
419        }
420
421        // We open a new subject block if useful (ie. new subject or already used predicate)
422        if self.current_subject.is_none() {
423            output.push(JsonEvent::StartObject);
424            output.push(JsonEvent::ObjectKey("@id".into()));
425            #[allow(
426                unreachable_patterns,
427                clippy::match_wildcard_for_single_variants,
428                clippy::allow_attributes
429            )]
430            output.push(JsonEvent::String(self.id_value(match quad.subject() {
431                SubjectRef::NamedNode(iri) => NamedOrBlankNodeRef::NamedNode(iri),
432                SubjectRef::BlankNode(bnode) => NamedOrBlankNodeRef::BlankNode(bnode),
433                _ => {
434                    return Err(io::Error::new(
435                        io::ErrorKind::InvalidInput,
436                        "JSON-LD does not support RDF 1.2 yet",
437                    ));
438                }
439            })));
440            self.current_subject = Some(match quad.subject() {
441                SubjectRef::NamedNode(n) => NamedOrBlankNode::NamedNode(n.to_owned()),
442                SubjectRef::BlankNode(b) => NamedOrBlankNode::BlankNode(b.to_owned()),
443                _ => {
444                    return Err(io::Error::new(
445                        io::ErrorKind::InvalidInput,
446                        "JSON-LD does not support variables or quoted triples as subjects",
447                    ));
448                }
449            });
450        }
451
452        // We open a predicate key
453        if self.current_predicate.is_none() {
454            let predicate_str = match quad.predicate() {
455                PredicateRef::NamedNode(n) => {
456                    // Apply prefix compaction if available
457                    // Note: @type shorthand requires special object serialization (plain IRIs)
458                    // which is not yet implemented, so we use full IRI with prefix compaction
459                    self.compact_iri(n.as_str())
460                }
461                PredicateRef::Variable(v) => Cow::Borrowed(v.as_str()),
462            };
463            output.push(JsonEvent::ObjectKey(predicate_str));
464            output.push(JsonEvent::StartArray);
465            self.current_predicate = Some(match quad.predicate() {
466                PredicateRef::NamedNode(n) => n.to_owned(),
467                _ => {
468                    return Err(io::Error::new(
469                        io::ErrorKind::InvalidInput,
470                        "JSON-LD does not support variables as predicates",
471                    ));
472                }
473            });
474        }
475
476        let object_ref = match quad.object() {
477            ObjectRef::NamedNode(n) => TermRef::NamedNode(n.as_str()),
478            ObjectRef::BlankNode(b) => TermRef::BlankNode(b.as_str()),
479            ObjectRef::Literal(l) => TermRef::from_literal(l),
480            ObjectRef::Variable(v) => TermRef::Variable(v.as_str()),
481        };
482        self.serialize_term(object_ref, output)
483    }
484
485    fn serialize_start(&self, output: &mut Vec<JsonEvent<'_>>) {
486        if self.base_iri.is_some() || !self.prefixes.is_empty() {
487            output.push(JsonEvent::StartObject);
488            output.push(JsonEvent::ObjectKey("@context".into()));
489            output.push(JsonEvent::StartObject);
490            if let Some(base_iri) = &self.base_iri {
491                output.push(JsonEvent::ObjectKey("@base".into()));
492                output.push(JsonEvent::String(base_iri.to_string().into()));
493            }
494            for (prefix_name, prefix_iri) in &self.prefixes {
495                output.push(JsonEvent::ObjectKey(if prefix_name.is_empty() {
496                    "@vocab".into()
497                } else {
498                    prefix_name.clone().into()
499                }));
500                output.push(JsonEvent::String(prefix_iri.clone().into()));
501            }
502            output.push(JsonEvent::EndObject);
503            output.push(JsonEvent::ObjectKey("@graph".into()));
504        }
505        output.push(JsonEvent::StartArray);
506    }
507
508    fn serialize_term<'a>(
509        &self,
510        term: TermRef<'a>,
511        output: &mut Vec<JsonEvent<'a>>,
512    ) -> io::Result<()> {
513        output.push(JsonEvent::StartObject);
514        #[allow(
515            unreachable_patterns,
516            clippy::match_wildcard_for_single_variants,
517            clippy::allow_attributes
518        )]
519        match term {
520            TermRef::NamedNode(iri) => {
521                output.push(JsonEvent::ObjectKey("@id".into()));
522                // For named nodes, we can use the IRI directly with base IRI processing
523                output.push(JsonEvent::String(self.id_value_from_str(iri, false)));
524            }
525            TermRef::BlankNode(bnode) => {
526                output.push(JsonEvent::ObjectKey("@id".into()));
527                // For blank nodes, we need to add the "_:" prefix
528                output.push(JsonEvent::String(self.id_value_from_str(bnode, true)));
529            }
530            TermRef::Literal(value, datatype, language) => {
531                if let Some(lang) = language {
532                    output.push(JsonEvent::ObjectKey("@language".into()));
533                    output.push(JsonEvent::String(lang.into()));
534                } else if let Some(dt) = datatype {
535                    if dt != xsd::STRING.as_str() {
536                        output.push(JsonEvent::ObjectKey("@type".into()));
537                        // For datatypes, use the IRI directly
538                        output.push(JsonEvent::String(dt.into()));
539                    }
540                }
541                output.push(JsonEvent::ObjectKey("@value".into()));
542                output.push(JsonEvent::String(value.into()));
543            }
544            _ => {
545                return Err(io::Error::new(
546                    io::ErrorKind::InvalidInput,
547                    "JSON-LD does not support RDF 1.2 yet",
548                ));
549            }
550        }
551        output.push(JsonEvent::EndObject);
552        Ok(())
553    }
554
555    fn id_value<'a>(&self, id: NamedOrBlankNodeRef<'a>) -> Cow<'a, str> {
556        match id {
557            NamedOrBlankNodeRef::NamedNode(iri) => {
558                if let Some(base_iri) = &self.base_iri {
559                    if let Ok(relative) = base_iri.relativize(&Iri::parse_unchecked(iri.as_str())) {
560                        let relative = relative.into_inner();
561                        // We check the relative IRI is not considered as absolute by IRI expansion
562                        if !relative.split_once(':').is_some_and(|(prefix, suffix)| {
563                            prefix == "_" || suffix.starts_with("//")
564                        }) {
565                            return relative.into();
566                        }
567                    }
568                }
569                iri.as_str().into()
570            }
571            NamedOrBlankNodeRef::BlankNode(bnode) => bnode.to_string().into(),
572        }
573    }
574
575    fn id_value_from_str<'a>(&self, id: &'a str, is_blank_node: bool) -> Cow<'a, str> {
576        if is_blank_node {
577            // For blank nodes, add the "_:" prefix
578            format!("_:{id}").into()
579        } else {
580            // For IRIs, apply base IRI relativization
581            if let Some(base_iri) = &self.base_iri {
582                if let Ok(relative) = base_iri.relativize(&Iri::parse_unchecked(id)) {
583                    let relative = relative.into_inner();
584                    // We check the relative IRI is not considered as absolute by IRI expansion
585                    if !relative
586                        .split_once(':')
587                        .is_some_and(|(prefix, suffix)| prefix == "_" || suffix.starts_with("//"))
588                    {
589                        return relative.into();
590                    }
591                }
592            }
593            id.into()
594        }
595    }
596
597    #[allow(dead_code)]
598    fn type_value(id: NamedOrBlankNodeRef<'_>) -> Cow<'_, str> {
599        match id {
600            NamedOrBlankNodeRef::NamedNode(iri) => iri.as_str().into(),
601            NamedOrBlankNodeRef::BlankNode(bnode) => bnode.to_string().into(),
602        }
603    }
604
605    /// Compact an IRI using registered prefixes
606    ///
607    /// This applies JSON-LD prefix compaction to transform full IRIs into
608    /// compact prefix:localname form when a matching prefix is registered.
609    fn compact_iri<'a>(&self, iri: &'a str) -> Cow<'a, str> {
610        // Try to find a matching prefix for compaction
611        for (prefix, namespace) in &self.prefixes {
612            if let Some(local_name) = iri.strip_prefix(namespace.as_str()) {
613                // Found a matching prefix - return compact form
614                return Cow::Owned(format!("{prefix}:{local_name}"));
615            }
616        }
617        // No matching prefix found - return original IRI
618        Cow::Borrowed(iri)
619    }
620
621    fn finish(&mut self, output: &mut Vec<JsonEvent<'static>>) {
622        if !self.started {
623            self.serialize_start(output);
624        }
625        if self.current_predicate.is_some() {
626            output.push(JsonEvent::EndArray)
627        }
628        if self.current_subject.is_some() {
629            output.push(JsonEvent::EndObject)
630        }
631        if self
632            .current_graph_name
633            .as_ref()
634            .is_some_and(|g| !g.is_default_graph())
635        {
636            output.push(JsonEvent::EndArray);
637            output.push(JsonEvent::EndObject)
638        }
639        output.push(JsonEvent::EndArray);
640        if self.base_iri.is_some() || !self.prefixes.is_empty() {
641            output.push(JsonEvent::EndObject);
642        }
643    }
644}