Skip to main content

oxrdfxml/
serializer.rs

1use crate::utils::*;
2use oxiri::{Iri, IriParseError};
3#[cfg(feature = "rdf-12")]
4use oxrdf::BaseDirection;
5use oxrdf::vocab::{rdf, xsd};
6use oxrdf::{NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, TermRef, TripleRef};
7use quick_xml::Writer;
8use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
9use std::borrow::Cow;
10use std::collections::BTreeMap;
11use std::io;
12use std::io::Write;
13#[cfg(feature = "async-tokio")]
14use std::sync::Arc;
15#[cfg(feature = "async-tokio")]
16use tokio::io::AsyncWrite;
17
18/// A [RDF/XML](https://www.w3.org/TR/rdf-syntax-grammar/) serializer.
19///
20/// ```
21/// use oxrdf::{LiteralRef, NamedNodeRef, TripleRef};
22/// use oxrdf::vocab::rdf;
23/// use oxrdfxml::RdfXmlSerializer;
24///
25/// let mut serializer = RdfXmlSerializer::new().with_prefix("schema", "http://schema.org/")?.for_writer(Vec::new());
26/// serializer.serialize_triple(TripleRef::new(
27///     NamedNodeRef::new("http://example.com#me")?,
28///     rdf::TYPE,
29///     NamedNodeRef::new("http://schema.org/Person")?,
30/// ))?;
31/// serializer.serialize_triple(TripleRef::new(
32///     NamedNodeRef::new("http://example.com#me")?,
33///     NamedNodeRef::new("http://schema.org/name")?,
34///     LiteralRef::new_language_tagged_literal_unchecked("Foo Bar", "en"),
35/// ))?;
36/// assert_eq!(
37///     b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rdf:RDF xmlns:schema=\"http://schema.org/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:its=\"http://www.w3.org/2005/11/its\">\n\t<schema:Person rdf:about=\"http://example.com#me\">\n\t\t<schema:name xml:lang=\"en\">Foo Bar</schema:name>\n\t</schema:Person>\n</rdf:RDF>",
38///     serializer.finish()?.as_slice()
39/// );
40/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
41/// ```
42#[derive(Default, Clone)]
43#[must_use]
44pub struct RdfXmlSerializer {
45    prefixes: BTreeMap<String, String>,
46    base_iri: Option<Iri<String>>,
47}
48
49impl RdfXmlSerializer {
50    /// Builds a new [`RdfXmlSerializer`].
51    #[inline]
52    pub fn new() -> Self {
53        Self {
54            prefixes: BTreeMap::new(),
55            base_iri: None,
56        }
57    }
58
59    #[inline]
60    pub fn with_prefix(
61        mut self,
62        prefix_name: impl Into<String>,
63        prefix_iri: impl Into<String>,
64    ) -> Result<Self, IriParseError> {
65        let prefix_name = prefix_name.into();
66        if prefix_name == "oxprefix" {
67            return Ok(self); // It is reserved
68        }
69        self.prefixes
70            .insert(prefix_name, Iri::parse(prefix_iri.into())?.into_inner());
71        Ok(self)
72    }
73
74    /// ```
75    /// use oxrdf::{NamedNodeRef, TripleRef};
76    /// use oxrdfxml::RdfXmlSerializer;
77    ///
78    /// let mut serializer = RdfXmlSerializer::new()
79    ///     .with_base_iri("http://example.com")?
80    ///     .with_prefix("ex", "http://example.com/ns#")?
81    ///     .for_writer(Vec::new());
82    /// serializer.serialize_triple(TripleRef::new(
83    ///     NamedNodeRef::new("http://example.com#me")?,
84    ///     NamedNodeRef::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?,
85    ///     NamedNodeRef::new("http://example.com/ns#Person")?,
86    /// ))?;
87    /// serializer.serialize_triple(TripleRef::new(
88    ///     NamedNodeRef::new("http://example.com#me")?,
89    ///     NamedNodeRef::new("http://example.com/ns#parent")?,
90    ///     NamedNodeRef::new("http://example.com#other")?,
91    /// ))?;
92    /// assert_eq!(
93    ///     b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rdf:RDF xml:base=\"http://example.com\" xmlns:ex=\"http://example.com/ns#\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:its=\"http://www.w3.org/2005/11/its\">\n\t<ex:Person rdf:about=\"#me\">\n\t\t<ex:parent rdf:resource=\"#other\"/>\n\t</ex:Person>\n</rdf:RDF>",
94    ///     serializer.finish()?.as_slice()
95    /// );
96    /// # Result::<_,Box<dyn std::error::Error>>::Ok(())
97    /// ```
98    #[inline]
99    pub fn with_base_iri(mut self, base_iri: impl Into<String>) -> Result<Self, IriParseError> {
100        self.base_iri = Some(Iri::parse(base_iri.into())?);
101        Ok(self)
102    }
103
104    /// Serializes a RDF/XML file to a [`Write`] implementation.
105    ///
106    /// This writer does unbuffered writes.
107    ///
108    /// ```
109    /// use oxrdf::{LiteralRef, NamedNodeRef, TripleRef};
110    /// use oxrdf::vocab::rdf;
111    /// use oxrdfxml::RdfXmlSerializer;
112    ///
113    /// let mut serializer = RdfXmlSerializer::new().with_prefix("schema", "http://schema.org/")?.for_writer(Vec::new());
114    /// serializer.serialize_triple(TripleRef::new(
115    ///     NamedNodeRef::new("http://example.com#me")?,
116    ///     rdf::TYPE,
117    ///     NamedNodeRef::new("http://schema.org/Person")?,
118    /// ))?;
119    /// serializer.serialize_triple(TripleRef::new(
120    ///     NamedNodeRef::new("http://example.com#me")?,
121    ///     NamedNodeRef::new("http://schema.org/name")?,
122    ///     LiteralRef::new_language_tagged_literal_unchecked("Foo Bar", "en"),
123    /// ))?;
124    /// assert_eq!(
125    ///     b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rdf:RDF xmlns:schema=\"http://schema.org/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:its=\"http://www.w3.org/2005/11/its\">\n\t<schema:Person rdf:about=\"http://example.com#me\">\n\t\t<schema:name xml:lang=\"en\">Foo Bar</schema:name>\n\t</schema:Person>\n</rdf:RDF>",
126    ///     serializer.finish()?.as_slice()
127    /// );
128    /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
129    /// ```
130    pub fn for_writer<W: Write>(self, writer: W) -> WriterRdfXmlSerializer<W> {
131        WriterRdfXmlSerializer {
132            writer: Writer::new_with_indent(writer, b'\t', 1),
133            inner: self.inner_writer(),
134        }
135    }
136
137    /// Serializes a RDF/XML file to a [`AsyncWrite`] implementation.
138    ///
139    /// This writer does unbuffered writes.
140    ///
141    /// ```
142    /// # #[tokio::main(flavor = "current_thread")]
143    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
144    /// use oxrdf::{NamedNodeRef, TripleRef, LiteralRef};
145    /// use oxrdf::vocab::rdf;
146    /// use oxrdfxml::RdfXmlSerializer;
147    ///
148    /// let mut serializer = RdfXmlSerializer::new().with_prefix("schema", "http://schema.org/")?.for_tokio_async_writer(Vec::new());
149    /// serializer.serialize_triple(TripleRef::new(
150    ///     NamedNodeRef::new("http://example.com#me")?,
151    ///     rdf::TYPE,
152    ///     NamedNodeRef::new("http://schema.org/Person")?,
153    /// )).await?;
154    /// serializer.serialize_triple(TripleRef::new(
155    ///     NamedNodeRef::new("http://example.com#me")?,
156    ///     NamedNodeRef::new("http://schema.org/name")?,
157    ///     LiteralRef::new_language_tagged_literal_unchecked("Foo Bar", "en"),
158    /// )).await?;
159    /// assert_eq!(
160    ///     b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rdf:RDF xmlns:schema=\"http://schema.org/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:its=\"http://www.w3.org/2005/11/its\">\n\t<schema:Person rdf:about=\"http://example.com#me\">\n\t\t<schema:name xml:lang=\"en\">Foo Bar</schema:name>\n\t</schema:Person>\n</rdf:RDF>",
161    ///     serializer.finish().await?.as_slice()
162    /// );
163    /// # Ok(())
164    /// # }
165    /// ```
166    #[cfg(feature = "async-tokio")]
167    pub fn for_tokio_async_writer<W: AsyncWrite + Unpin>(
168        self,
169        writer: W,
170    ) -> TokioAsyncWriterRdfXmlSerializer<W> {
171        TokioAsyncWriterRdfXmlSerializer {
172            writer: Writer::new_with_indent(writer, b'\t', 1),
173            inner: self.inner_writer(),
174        }
175    }
176
177    fn inner_writer(mut self) -> InnerRdfXmlWriter {
178        // Makes sure 'rdf' and 'its' are the proper prefixes, by first removing them
179        self.prefixes.remove("rdf");
180        self.prefixes.remove("its");
181        let custom_default_prefix = self.prefixes.contains_key("");
182        // The serializer want to have the URL first, we swap
183        let mut prefixes = self
184            .prefixes
185            .into_iter()
186            .map(|(key, value)| (value, key))
187            .collect::<BTreeMap<_, _>>();
188        prefixes.insert(
189            "http://www.w3.org/1999/02/22-rdf-syntax-ns#".into(),
190            "rdf".into(),
191        );
192        prefixes.insert("http://www.w3.org/2005/11/its".into(), "its".into());
193        InnerRdfXmlWriter {
194            current_subject: None,
195            current_resource_tag: None,
196            custom_default_prefix,
197            prefixes_by_iri: prefixes,
198            base_iri: self.base_iri,
199        }
200    }
201}
202
203/// Serializes a RDF/XML file to a [`Write`] implementation.
204///
205/// Can be built using [`RdfXmlSerializer::for_writer`].
206///
207/// ```
208/// use oxrdf::{LiteralRef, NamedNodeRef, TripleRef};
209/// use oxrdf::vocab::rdf;
210/// use oxrdfxml::RdfXmlSerializer;
211///
212/// let mut serializer = RdfXmlSerializer::new().with_prefix("schema", "http://schema.org/")?.for_writer(Vec::new());
213/// serializer.serialize_triple(TripleRef::new(
214///     NamedNodeRef::new("http://example.com#me")?,
215///     rdf::TYPE,
216///     NamedNodeRef::new("http://schema.org/Person")?,
217/// ))?;
218/// serializer.serialize_triple(TripleRef::new(
219///     NamedNodeRef::new("http://example.com#me")?,
220///     NamedNodeRef::new("http://schema.org/name")?,
221///     LiteralRef::new_language_tagged_literal_unchecked("Foo Bar", "en"),
222/// ))?;
223/// assert_eq!(
224///     b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rdf:RDF xmlns:schema=\"http://schema.org/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:its=\"http://www.w3.org/2005/11/its\">\n\t<schema:Person rdf:about=\"http://example.com#me\">\n\t\t<schema:name xml:lang=\"en\">Foo Bar</schema:name>\n\t</schema:Person>\n</rdf:RDF>",
225///     serializer.finish()?.as_slice()
226/// );
227/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
228/// ```
229#[must_use]
230pub struct WriterRdfXmlSerializer<W: Write> {
231    writer: Writer<W>,
232    inner: InnerRdfXmlWriter,
233}
234
235impl<W: Write> WriterRdfXmlSerializer<W> {
236    /// Serializes an extra triple.
237    pub fn serialize_triple<'a>(&mut self, t: impl Into<TripleRef<'a>>) -> io::Result<()> {
238        let mut buffer = Vec::new();
239        self.inner.serialize_triple(t, &mut buffer)?;
240        self.flush_buffer(&mut buffer)
241    }
242
243    /// Ends the write process and returns the underlying [`Write`].
244    pub fn finish(mut self) -> io::Result<W> {
245        let mut buffer = Vec::new();
246        self.inner.finish(&mut buffer);
247        self.flush_buffer(&mut buffer)?;
248        Ok(self.writer.into_inner())
249    }
250
251    fn flush_buffer(&mut self, buffer: &mut Vec<Event<'_>>) -> io::Result<()> {
252        for event in buffer.drain(0..) {
253            self.writer.write_event(event)?;
254        }
255        Ok(())
256    }
257}
258
259/// Serializes a RDF/XML file to a [`AsyncWrite`] implementation.
260///
261/// Can be built using [`RdfXmlSerializer::for_tokio_async_writer`].
262///
263/// ```
264/// # #[tokio::main(flavor = "current_thread")]
265/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
266/// use oxrdf::{NamedNodeRef, TripleRef, LiteralRef};
267/// use oxrdf::vocab::rdf;
268/// use oxrdfxml::RdfXmlSerializer;
269///
270/// let mut serializer = RdfXmlSerializer::new().with_prefix("schema", "http://schema.org/")?.for_tokio_async_writer(Vec::new());
271/// serializer.serialize_triple(TripleRef::new(
272///     NamedNodeRef::new("http://example.com#me")?,
273///     rdf::TYPE,
274///     NamedNodeRef::new("http://schema.org/Person")?,
275/// )).await?;
276/// serializer.serialize_triple(TripleRef::new(
277///     NamedNodeRef::new("http://example.com#me")?,
278///     NamedNodeRef::new("http://schema.org/name")?,
279///     LiteralRef::new_language_tagged_literal_unchecked("Foo Bar", "en"),
280/// )).await?;
281/// assert_eq!(
282///     b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rdf:RDF xmlns:schema=\"http://schema.org/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:its=\"http://www.w3.org/2005/11/its\">\n\t<schema:Person rdf:about=\"http://example.com#me\">\n\t\t<schema:name xml:lang=\"en\">Foo Bar</schema:name>\n\t</schema:Person>\n</rdf:RDF>",
283///     serializer.finish().await?.as_slice()
284/// );
285/// # Ok(())
286/// # }
287/// ```
288#[cfg(feature = "async-tokio")]
289#[must_use]
290pub struct TokioAsyncWriterRdfXmlSerializer<W: AsyncWrite + Unpin> {
291    writer: Writer<W>,
292    inner: InnerRdfXmlWriter,
293}
294
295#[cfg(feature = "async-tokio")]
296impl<W: AsyncWrite + Unpin> TokioAsyncWriterRdfXmlSerializer<W> {
297    /// Serializes an extra triple.
298    pub async fn serialize_triple<'a>(&mut self, t: impl Into<TripleRef<'a>>) -> io::Result<()> {
299        let mut buffer = Vec::new();
300        self.inner.serialize_triple(t, &mut buffer)?;
301        self.flush_buffer(&mut buffer).await
302    }
303
304    /// Ends the write process and returns the underlying [`Write`].
305    pub async fn finish(mut self) -> io::Result<W> {
306        let mut buffer = Vec::new();
307        self.inner.finish(&mut buffer);
308        self.flush_buffer(&mut buffer).await?;
309        Ok(self.writer.into_inner())
310    }
311
312    async fn flush_buffer(&mut self, buffer: &mut Vec<Event<'_>>) -> io::Result<()> {
313        for event in buffer.drain(0..) {
314            self.writer
315                .write_event_async(event)
316                .await
317                .map_err(map_err)?;
318        }
319        Ok(())
320    }
321}
322
323const RESERVED_SYNTAX_TERMS: [&str; 9] = [
324    "http://www.w3.org/1999/02/22-rdf-syntax-ns#Description",
325    "http://www.w3.org/1999/02/22-rdf-syntax-ns#li",
326    "http://www.w3.org/1999/02/22-rdf-syntax-ns#RDF",
327    "http://www.w3.org/1999/02/22-rdf-syntax-ns#ID",
328    "http://www.w3.org/1999/02/22-rdf-syntax-ns#about",
329    "http://www.w3.org/1999/02/22-rdf-syntax-ns#parseType",
330    "http://www.w3.org/1999/02/22-rdf-syntax-ns#resource",
331    "http://www.w3.org/1999/02/22-rdf-syntax-ns#nodeID",
332    "http://www.w3.org/1999/02/22-rdf-syntax-ns#datatype",
333];
334
335pub struct InnerRdfXmlWriter {
336    current_subject: Option<NamedOrBlankNode>,
337    current_resource_tag: Option<String>,
338    custom_default_prefix: bool,
339    prefixes_by_iri: BTreeMap<String, String>,
340    base_iri: Option<Iri<String>>,
341}
342
343impl InnerRdfXmlWriter {
344    fn serialize_triple<'a>(
345        &mut self,
346        t: impl Into<TripleRef<'a>>,
347        output: &mut Vec<Event<'a>>,
348    ) -> io::Result<()> {
349        if self.current_subject.is_none() {
350            self.write_start(output);
351        }
352
353        let triple = t.into();
354        // We open a new rdf:Description if useful
355        if self.current_subject.as_ref().map(NamedOrBlankNode::as_ref) != Some(triple.subject) {
356            if self.current_subject.is_some() {
357                output.push(Event::End(
358                    self.current_resource_tag
359                        .take()
360                        .map_or_else(|| BytesEnd::new("rdf:Description"), BytesEnd::new),
361                ));
362            }
363            self.current_subject = Some(triple.subject.into_owned());
364
365            let (mut description_start, with_type_tag) = if triple.predicate == rdf::TYPE {
366                if let TermRef::NamedNode(t) = triple.object {
367                    if RESERVED_SYNTAX_TERMS.contains(&t.as_str()) {
368                        (BytesStart::new("rdf:Description"), false)
369                    } else {
370                        let (prop_qname, prop_xmlns) = self.uri_to_qname_and_xmlns(t);
371                        let mut description_start = BytesStart::new(prop_qname.clone());
372                        if let Some(prop_xmlns) = prop_xmlns {
373                            description_start.push_attribute(prop_xmlns);
374                        }
375                        self.current_resource_tag = Some(prop_qname.into_owned());
376                        (description_start, true)
377                    }
378                } else {
379                    (BytesStart::new("rdf:Description"), false)
380                }
381            } else {
382                (BytesStart::new("rdf:Description"), false)
383            };
384            description_start.push_attribute(match triple.subject {
385                NamedOrBlankNodeRef::NamedNode(node) => {
386                    ("rdf:about", relative_iri(node.as_str(), &self.base_iri))
387                }
388                NamedOrBlankNodeRef::BlankNode(node) => ("rdf:nodeID", node.as_str().into()),
389            });
390            output.push(Event::Start(description_start));
391            if with_type_tag {
392                return Ok(()); // No need for a value
393            }
394        }
395        self.write_predicate_object(triple.predicate, triple.object, output)
396    }
397
398    fn write_predicate_object<'a>(
399        &mut self,
400        predicate: NamedNodeRef<'a>,
401        object: TermRef<'a>,
402        output: &mut Vec<Event<'a>>,
403    ) -> io::Result<()> {
404        if RESERVED_SYNTAX_TERMS.contains(&predicate.as_str()) {
405            return Err(io::Error::new(
406                io::ErrorKind::InvalidInput,
407                "RDF/XML reserved syntax term is not allowed as a predicate",
408            ));
409        }
410
411        let (prop_qname, prop_xmlns) = self.uri_to_qname_and_xmlns(predicate);
412        let mut property_open = BytesStart::new(prop_qname.clone());
413        if let Some(prop_xmlns) = prop_xmlns {
414            property_open.push_attribute(prop_xmlns);
415        }
416        #[allow(
417            unreachable_patterns,
418            clippy::match_wildcard_for_single_variants,
419            clippy::allow_attributes
420        )]
421        match object {
422            TermRef::NamedNode(node) => {
423                property_open
424                    .push_attribute(("rdf:resource", relative_iri(node.as_str(), &self.base_iri)));
425                output.push(Event::Empty(property_open));
426            }
427            TermRef::BlankNode(node) => {
428                property_open.push_attribute(("rdf:nodeID", node.as_str()));
429                output.push(Event::Empty(property_open));
430            }
431            TermRef::Literal(literal) => {
432                if let Some(language) = literal.language() {
433                    property_open.push_attribute(("xml:lang", language));
434                } else if literal.datatype() != xsd::STRING {
435                    property_open.push_attribute((
436                        "rdf:datatype",
437                        relative_iri(literal.datatype().as_str(), &self.base_iri),
438                    ));
439                }
440                #[cfg(feature = "rdf-12")]
441                if let Some(base_direction) = literal.direction() {
442                    property_open.push_attribute(("rdf:version", "1.2-basic"));
443                    property_open.push_attribute(("its:version", "2.0"));
444                    property_open.push_attribute((
445                        "its:dir",
446                        match base_direction {
447                            BaseDirection::Ltr => "ltr",
448                            BaseDirection::Rtl => "rtl",
449                        },
450                    ));
451                }
452                output.push(Event::Start(property_open));
453                output.push(Event::Text(BytesText::new(literal.value())));
454                output.push(Event::End(BytesEnd::new(prop_qname)));
455            }
456            #[cfg(feature = "rdf-12")]
457            TermRef::Triple(triple) => {
458                property_open.push_attribute(("rdf:version", "1.2"));
459                property_open.push_attribute(("rdf:parseType", "Triple"));
460                output.push(Event::Start(property_open));
461                let mut subject_start = BytesStart::new("rdf:Description");
462                subject_start.push_attribute(match triple.subject.as_ref() {
463                    NamedOrBlankNodeRef::NamedNode(node) => {
464                        ("rdf:about", relative_iri(node.as_str(), &self.base_iri))
465                    }
466                    NamedOrBlankNodeRef::BlankNode(node) => ("rdf:nodeID", node.as_str().into()),
467                });
468                output.push(Event::Start(subject_start));
469                self.write_predicate_object(
470                    triple.predicate.as_ref(),
471                    triple.object.as_ref(),
472                    output,
473                )?;
474                output.push(Event::End(BytesEnd::new("rdf:Description")));
475                output.push(Event::End(BytesEnd::new(prop_qname)));
476            }
477        }
478        Ok(())
479    }
480
481    fn write_start(&self, output: &mut Vec<Event<'_>>) {
482        output.push(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)));
483        let mut rdf_open = BytesStart::new("rdf:RDF");
484        if let Some(base_iri) = &self.base_iri {
485            rdf_open.push_attribute(("xml:base", base_iri.as_str()));
486        }
487        for (prefix_value, prefix_name) in &self.prefixes_by_iri {
488            rdf_open.push_attribute((
489                if prefix_name.is_empty() {
490                    "xmlns".into()
491                } else {
492                    format!("xmlns:{prefix_name}")
493                }
494                .as_str(),
495                prefix_value.as_str(),
496            ));
497        }
498        output.push(Event::Start(rdf_open))
499    }
500
501    fn finish(&mut self, output: &mut Vec<Event<'static>>) {
502        if self.current_subject.is_some() {
503            output.push(Event::End(
504                self.current_resource_tag
505                    .take()
506                    .map_or_else(|| BytesEnd::new("rdf:Description"), BytesEnd::new),
507            ));
508        } else {
509            self.write_start(output);
510        }
511        output.push(Event::End(BytesEnd::new("rdf:RDF")));
512    }
513
514    fn uri_to_qname_and_xmlns<'a>(
515        &self,
516        uri: NamedNodeRef<'a>,
517    ) -> (Cow<'a, str>, Option<(&'a str, &'a str)>) {
518        let (prop_prefix, prop_value) = split_iri(uri.as_str());
519        if let Some(prop_prefix) = self.prefixes_by_iri.get(prop_prefix) {
520            (
521                if prop_prefix.is_empty() {
522                    Cow::Borrowed(prop_value)
523                } else {
524                    Cow::Owned(format!("{prop_prefix}:{prop_value}"))
525                },
526                None,
527            )
528        } else if prop_prefix == "http://www.w3.org/2000/xmlns/" {
529            (Cow::Owned(format!("xmlns:{prop_value}")), None)
530        } else if !prop_value.is_empty() && !self.custom_default_prefix {
531            (Cow::Borrowed(prop_value), Some(("xmlns", prop_prefix)))
532        } else {
533            // TODO: does not work on recursive elements
534            (
535                Cow::Owned(format!("oxprefix:{prop_value}")),
536                Some(("xmlns:oxprefix", prop_prefix)),
537            )
538        }
539    }
540}
541
542#[cfg(feature = "async-tokio")]
543fn map_err(error: quick_xml::Error) -> io::Error {
544    if let quick_xml::Error::Io(error) = error {
545        Arc::try_unwrap(error).unwrap_or_else(|error| io::Error::new(error.kind(), error))
546    } else {
547        io::Error::other(error)
548    }
549}
550
551fn split_iri(iri: &str) -> (&str, &str) {
552    if let Some(position_base) = iri.rfind(|c| !is_name_char(c) || c == ':') {
553        if let Some(position_add) = iri[position_base..].find(|c| is_name_start_char(c) && c != ':')
554        {
555            (
556                &iri[..position_base + position_add],
557                &iri[position_base + position_add..],
558            )
559        } else {
560            (iri, "")
561        }
562    } else {
563        (iri, "")
564    }
565}
566
567fn relative_iri<'a>(iri: &'a str, base_iri: &Option<Iri<String>>) -> Cow<'a, str> {
568    if let Some(base_iri) = base_iri {
569        if let Ok(relative) = base_iri.relativize(&Iri::parse_unchecked(iri)) {
570            return relative.into_inner().into();
571        }
572    }
573    iri.into()
574}
575
576#[cfg(test)]
577#[expect(clippy::panic_in_result_fn)]
578mod tests {
579    use super::*;
580    use std::error::Error;
581
582    #[test]
583    fn test_split_iri() {
584        assert_eq!(
585            split_iri("http://schema.org/Person"),
586            ("http://schema.org/", "Person")
587        );
588        assert_eq!(split_iri("http://schema.org/"), ("http://schema.org/", ""));
589        assert_eq!(
590            split_iri("http://schema.org#foo"),
591            ("http://schema.org#", "foo")
592        );
593        assert_eq!(split_iri("urn:isbn:foo"), ("urn:isbn:", "foo"));
594    }
595
596    #[test]
597    fn test_custom_rdf_ns() -> Result<(), Box<dyn Error>> {
598        let output = RdfXmlSerializer::new()
599            .with_prefix("rdf", "http://example.com/")?
600            .for_writer(Vec::new())
601            .finish()?;
602        assert_eq!(output, b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:its=\"http://www.w3.org/2005/11/its\">\n</rdf:RDF>");
603        Ok(())
604    }
605
606    #[test]
607    fn test_custom_empty_ns() -> Result<(), Box<dyn Error>> {
608        let mut serializer = RdfXmlSerializer::new()
609            .with_prefix("", "http://example.com/")?
610            .for_writer(Vec::new());
611        serializer.serialize_triple(TripleRef::new(
612            NamedNodeRef::new("http://example.com/s")?,
613            rdf::TYPE,
614            NamedNodeRef::new("http://example.org/o")?,
615        ))?;
616        serializer.serialize_triple(TripleRef::new(
617            NamedNodeRef::new("http://example.com/s")?,
618            NamedNodeRef::new("http://example.com/p")?,
619            NamedNodeRef::new("http://example.com/o2")?,
620        ))?;
621        let output = serializer.finish()?;
622        assert_eq!(
623            String::from_utf8_lossy(&output),
624            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rdf:RDF xmlns=\"http://example.com/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:its=\"http://www.w3.org/2005/11/its\">\n\t<oxprefix:o xmlns:oxprefix=\"http://example.org/\" rdf:about=\"http://example.com/s\">\n\t\t<p rdf:resource=\"http://example.com/o2\"/>\n\t</oxprefix:o>\n</rdf:RDF>"
625        );
626        Ok(())
627    }
628}