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#[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 #[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); }
69 self.prefixes
70 .insert(prefix_name, Iri::parse(prefix_iri.into())?.into_inner());
71 Ok(self)
72 }
73
74 #[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 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 #[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 self.prefixes.remove("rdf");
180 self.prefixes.remove("its");
181 let custom_default_prefix = self.prefixes.contains_key("");
182 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#[must_use]
230pub struct WriterRdfXmlSerializer<W: Write> {
231 writer: Writer<W>,
232 inner: InnerRdfXmlWriter,
233}
234
235impl<W: Write> WriterRdfXmlSerializer<W> {
236 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 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#[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 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 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 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(()); }
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 (
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}