sophia_turtle/serializer/
trig.rs

1//! Serializer for the [TriG] concrete syntax of RDF.
2//!
3//! **Important**:
4//! the methods in this module accepting a [`Write`]
5//! make no effort to minimize the number of write operations.
6//! Hence, in most cased, they should be passed a [`BufWriter`].
7//!
8//! [TriG]: https://www.w3.org/TR/trig/
9//! [`Write`]: https://doc.rust-lang.org/std/io/trait.Write.html
10//! [`BufWriter`]: https://doc.rust-lang.org/std/io/struct.BufWriter.html
11
12use rio_turtle::TriGFormatter;
13use sophia_api::quad::Quad;
14use sophia_api::serializer::{QuadSerializer, Stringifier};
15use sophia_api::source::{QuadSource, SinkError, SourceError, StreamResult};
16use sophia_api::term::Term;
17use sophia_rio::serializer::rio_format_quads;
18use std::io;
19
20pub(super) use super::_pretty::*;
21
22/// Trig serializer configuration.
23pub type TrigConfig = super::turtle::TurtleConfig;
24
25/// Trig serializer.
26pub struct TrigSerializer<W> {
27    pub(super) config: TrigConfig,
28    pub(super) write: W,
29}
30
31impl<W> TrigSerializer<W>
32where
33    W: io::Write,
34{
35    /// Build a new Trig serializer writing to `write`, with the default config.
36    #[inline]
37    pub fn new(write: W) -> TrigSerializer<W> {
38        Self::new_with_config(write, TrigConfig::default())
39    }
40
41    /// Build a new Trig serializer writing to `write`, with the given config.
42    pub fn new_with_config(write: W, config: TrigConfig) -> TrigSerializer<W> {
43        TrigSerializer { config, write }
44    }
45
46    /// Borrow this serializer's configuration.
47    pub fn config(&self) -> &TrigConfig {
48        &self.config
49    }
50}
51
52impl<W> QuadSerializer for TrigSerializer<W>
53where
54    W: io::Write,
55{
56    type Error = io::Error;
57
58    fn serialize_quads<TS>(
59        &mut self,
60        mut source: TS,
61    ) -> StreamResult<&mut Self, TS::Error, Self::Error>
62    where
63        TS: QuadSource,
64    {
65        if self.config.pretty {
66            let mut dataset = PrettifiableDataset::new();
67            source
68                .for_each_quad(|t| {
69                    let (spo, g) = t.spog();
70                    let spo = spo.map(|t| t.into_term());
71                    let g = g.map(|t| t.into_term());
72                    dataset.insert((g, spo));
73                })
74                .map_err(SourceError)?;
75            prettify(dataset, &mut self.write, &self.config, "").map_err(SinkError)?;
76        } else {
77            let mut tf = TriGFormatter::new(&mut self.write);
78            rio_format_quads(&mut tf, source)?;
79            tf.finish().map_err(SinkError)?;
80        }
81        Ok(self)
82    }
83}
84
85impl TrigSerializer<Vec<u8>> {
86    /// Create a new serializer which targets a `String`.
87    #[inline]
88    pub fn new_stringifier() -> Self {
89        TrigSerializer::new(Vec::new())
90    }
91    /// Create a new serializer which targets a `String` with a custom config.
92    #[inline]
93    pub fn new_stringifier_with_config(config: TrigConfig) -> Self {
94        TrigSerializer::new_with_config(Vec::new(), config)
95    }
96}
97
98impl Stringifier for TrigSerializer<Vec<u8>> {
99    fn as_utf8(&self) -> &[u8] {
100        &self.write[..]
101    }
102}
103
104// ---------------------------------------------------------------------------------
105//                                      tests
106// ---------------------------------------------------------------------------------
107
108#[cfg(test)]
109pub(crate) mod test {
110    use super::*;
111    use sophia_api::term::SimpleTerm;
112    use sophia_api::{dataset::Dataset, quad::Spog};
113    use sophia_isomorphism::isomorphic_datasets;
114    use std::error::Error;
115
116    const TESTS: &[&str] = &[
117        "#empty trig",
118        r#"# simple quads
119            PREFIX : <http://example.org/ns/>
120            :alice a :Person; :name "Alice"; :age 42.
121
122            GRAPH :g {
123                :bob a :Person, :Man; :nick "bob"@fr, "bobby"@en; :admin true.
124            }
125        "#,
126        r#"# lists
127            GRAPH <tag:g> { <tag:alice> <tag:likes> ( 1 2 ( 3 4 ) 5 6 ), ("a" "b"). }
128        "#,
129        r#"# subject lists
130            GRAPH <tag:g> { (1 2 3) a <tag:List>. }
131        "#,
132        r#"# malformed list
133            PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
134            GRAPH <tag:g> {
135                _:a rdf:first 42, 43; rdf:rest (44 45).
136                _:b rdf:first 42; rdf:rest (43), (44).
137            }
138        "#,
139        r#"# bnode cycles
140            PREFIX : <http://example.org/ns/>
141            GRAPH <tag:g> {
142                _:a :n "a"; :p [ :q [ :r _:a ]].
143                _:b :n "b"; :s [ :s _:b ].
144                _:c :b "c"; :t _:c.
145            }
146        "#,
147        r#"# quoted triples
148            PREFIX : <http://example.org/ns/>
149            GRAPH <tag:g> {
150                << :s :p :o1 >> :a :b.
151                :s :p :o2 {| :c :d |}.
152            }
153        "#,
154        r#"# blank node graph name
155            PREFIX : <http://example.org/ns/>
156            :lois :belives _:b.
157            GRAPH _:b1 { :clark a :Human }
158        "#,
159        r#"# blank node sharred across graphs
160            PREFIX : <http://example.org/ns/>
161            _:a :name "alice".
162            GRAPH <tag:g> { _:a a :Person }
163        "#,
164        r#"# list split over different graphs
165            PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
166            _:a rdf:first 42; rdf:rest _:b.
167
168            GRAPH [] {
169                _:b rdf:first 43; rdf:rest ().
170            }
171        "#,
172        r#"# issue 149
173            PREFIX : <https://example.org/>
174            :s :p :o .
175            GRAPH :g { _:b :p2 :o2 }
176        "#,
177    ];
178
179    #[test]
180    fn roundtrip_not_pretty() -> Result<(), Box<dyn std::error::Error>> {
181        for ttl in TESTS {
182            println!("==========\n{}\n----------", ttl);
183            let g1: Vec<Spog<SimpleTerm>> = crate::parser::trig::parse_str(ttl).collect_quads()?;
184
185            let out = TrigSerializer::new_stringifier()
186                .serialize_quads(g1.quads())?
187                .to_string();
188            println!("{}", &out);
189
190            let g2: Vec<Spog<SimpleTerm>> = crate::parser::trig::parse_str(&out).collect_quads()?;
191
192            assert!(isomorphic_datasets(&g1, &g2)?);
193        }
194        Ok(())
195    }
196
197    #[test]
198    fn roundtrip_pretty() -> Result<(), Box<dyn Error>> {
199        for ttl in TESTS {
200            println!("==========\n{}\n----------", ttl);
201            let g1: Vec<Spog<SimpleTerm>> = crate::parser::trig::parse_str(ttl).collect_quads()?;
202
203            let config = TrigConfig::new().with_pretty(true);
204            let out = TrigSerializer::new_stringifier_with_config(config)
205                .serialize_quads(g1.quads())?
206                .to_string();
207            println!("{}", &out);
208
209            let g2: Vec<Spog<SimpleTerm>> = crate::parser::trig::parse_str(&out).collect_quads()?;
210
211            assert!(isomorphic_datasets(&g1, &g2)?);
212        }
213        Ok(())
214    }
215}