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            ObjectRef::QuotedTriple(_) => {
482                return Err(io::Error::new(
483                    io::ErrorKind::InvalidInput,
484                    "JSON-LD does not support RDF-star quoted triples as objects",
485                ));
486            }
487        };
488        self.serialize_term(object_ref, output)
489    }
490
491    fn serialize_start(&self, output: &mut Vec<JsonEvent<'_>>) {
492        if self.base_iri.is_some() || !self.prefixes.is_empty() {
493            output.push(JsonEvent::StartObject);
494            output.push(JsonEvent::ObjectKey("@context".into()));
495            output.push(JsonEvent::StartObject);
496            if let Some(base_iri) = &self.base_iri {
497                output.push(JsonEvent::ObjectKey("@base".into()));
498                output.push(JsonEvent::String(base_iri.to_string().into()));
499            }
500            for (prefix_name, prefix_iri) in &self.prefixes {
501                output.push(JsonEvent::ObjectKey(if prefix_name.is_empty() {
502                    "@vocab".into()
503                } else {
504                    prefix_name.clone().into()
505                }));
506                output.push(JsonEvent::String(prefix_iri.clone().into()));
507            }
508            output.push(JsonEvent::EndObject);
509            output.push(JsonEvent::ObjectKey("@graph".into()));
510        }
511        output.push(JsonEvent::StartArray);
512    }
513
514    fn serialize_term<'a>(
515        &self,
516        term: TermRef<'a>,
517        output: &mut Vec<JsonEvent<'a>>,
518    ) -> io::Result<()> {
519        output.push(JsonEvent::StartObject);
520        #[allow(
521            unreachable_patterns,
522            clippy::match_wildcard_for_single_variants,
523            clippy::allow_attributes
524        )]
525        match term {
526            TermRef::NamedNode(iri) => {
527                output.push(JsonEvent::ObjectKey("@id".into()));
528                // For named nodes, we can use the IRI directly with base IRI processing
529                output.push(JsonEvent::String(self.id_value_from_str(iri, false)));
530            }
531            TermRef::BlankNode(bnode) => {
532                output.push(JsonEvent::ObjectKey("@id".into()));
533                // For blank nodes, we need to add the "_:" prefix
534                output.push(JsonEvent::String(self.id_value_from_str(bnode, true)));
535            }
536            TermRef::Literal(value, datatype, language) => {
537                if let Some(lang) = language {
538                    output.push(JsonEvent::ObjectKey("@language".into()));
539                    output.push(JsonEvent::String(lang.into()));
540                } else if let Some(dt) = datatype {
541                    if dt != xsd::STRING.as_str() {
542                        output.push(JsonEvent::ObjectKey("@type".into()));
543                        // For datatypes, use the IRI directly
544                        output.push(JsonEvent::String(dt.into()));
545                    }
546                }
547                output.push(JsonEvent::ObjectKey("@value".into()));
548                output.push(JsonEvent::String(value.into()));
549            }
550            _ => {
551                return Err(io::Error::new(
552                    io::ErrorKind::InvalidInput,
553                    "JSON-LD does not support RDF 1.2 yet",
554                ));
555            }
556        }
557        output.push(JsonEvent::EndObject);
558        Ok(())
559    }
560
561    fn id_value<'a>(&self, id: NamedOrBlankNodeRef<'a>) -> Cow<'a, str> {
562        match id {
563            NamedOrBlankNodeRef::NamedNode(iri) => {
564                if let Some(base_iri) = &self.base_iri {
565                    if let Ok(relative) = base_iri.relativize(&Iri::parse_unchecked(iri.as_str())) {
566                        let relative = relative.into_inner();
567                        // We check the relative IRI is not considered as absolute by IRI expansion
568                        if !relative.split_once(':').is_some_and(|(prefix, suffix)| {
569                            prefix == "_" || suffix.starts_with("//")
570                        }) {
571                            return relative.into();
572                        }
573                    }
574                }
575                iri.as_str().into()
576            }
577            NamedOrBlankNodeRef::BlankNode(bnode) => bnode.to_string().into(),
578        }
579    }
580
581    fn id_value_from_str<'a>(&self, id: &'a str, is_blank_node: bool) -> Cow<'a, str> {
582        if is_blank_node {
583            // For blank nodes, add the "_:" prefix
584            format!("_:{id}").into()
585        } else {
586            // For IRIs, apply base IRI relativization
587            if let Some(base_iri) = &self.base_iri {
588                if let Ok(relative) = base_iri.relativize(&Iri::parse_unchecked(id)) {
589                    let relative = relative.into_inner();
590                    // We check the relative IRI is not considered as absolute by IRI expansion
591                    if !relative
592                        .split_once(':')
593                        .is_some_and(|(prefix, suffix)| prefix == "_" || suffix.starts_with("//"))
594                    {
595                        return relative.into();
596                    }
597                }
598            }
599            id.into()
600        }
601    }
602
603    #[allow(dead_code)]
604    fn type_value(id: NamedOrBlankNodeRef<'_>) -> Cow<'_, str> {
605        match id {
606            NamedOrBlankNodeRef::NamedNode(iri) => iri.as_str().into(),
607            NamedOrBlankNodeRef::BlankNode(bnode) => bnode.to_string().into(),
608        }
609    }
610
611    /// Compact an IRI using registered prefixes
612    ///
613    /// This applies JSON-LD prefix compaction to transform full IRIs into
614    /// compact prefix:localname form when a matching prefix is registered.
615    fn compact_iri<'a>(&self, iri: &'a str) -> Cow<'a, str> {
616        // Try to find a matching prefix for compaction
617        for (prefix, namespace) in &self.prefixes {
618            if let Some(local_name) = iri.strip_prefix(namespace.as_str()) {
619                // Found a matching prefix - return compact form
620                return Cow::Owned(format!("{prefix}:{local_name}"));
621            }
622        }
623        // No matching prefix found - return original IRI
624        Cow::Borrowed(iri)
625    }
626
627    fn finish(&mut self, output: &mut Vec<JsonEvent<'static>>) {
628        if !self.started {
629            self.serialize_start(output);
630        }
631        if self.current_predicate.is_some() {
632            output.push(JsonEvent::EndArray)
633        }
634        if self.current_subject.is_some() {
635            output.push(JsonEvent::EndObject)
636        }
637        if self
638            .current_graph_name
639            .as_ref()
640            .is_some_and(|g| !g.is_default_graph())
641        {
642            output.push(JsonEvent::EndArray);
643            output.push(JsonEvent::EndObject)
644        }
645        output.push(JsonEvent::EndArray);
646        if self.base_iri.is_some() || !self.prefixes.is_empty() {
647            output.push(JsonEvent::EndObject);
648        }
649    }
650}