Skip to main content

oxrdfio/
format.rs

1use oxjsonld::{JsonLdProfile, JsonLdProfileSet};
2use std::fmt;
3
4/// RDF serialization formats.
5///
6/// This enumeration is non exhaustive. New formats might be added in the future.
7#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)]
8#[non_exhaustive]
9pub enum RdfFormat {
10    /// [N3](https://w3c.github.io/N3/spec/)
11    N3,
12    /// [N-Quads](https://www.w3.org/TR/n-quads/)
13    NQuads,
14    /// [N-Triples](https://www.w3.org/TR/n-triples/)
15    NTriples,
16    /// [RDF/XML](https://www.w3.org/TR/rdf-syntax-grammar/)
17    RdfXml,
18    /// [TriG](https://www.w3.org/TR/trig/)
19    TriG,
20    /// [Turtle](https://www.w3.org/TR/turtle/)
21    Turtle,
22    /// [JSON-LD](https://www.w3.org/TR/json-ld/)
23    JsonLd { profile: JsonLdProfileSet },
24}
25
26impl RdfFormat {
27    /// The format canonical IRI according to the [Unique URIs for file formats registry](https://www.w3.org/ns/formats/).
28    ///
29    /// ```
30    /// use oxrdfio::RdfFormat;
31    ///
32    /// assert_eq!(
33    ///     RdfFormat::NTriples.iri(),
34    ///     "http://www.w3.org/ns/formats/N-Triples"
35    /// )
36    /// ```
37    #[inline]
38    pub const fn iri(self) -> &'static str {
39        match self {
40            Self::JsonLd { .. } => "https://www.w3.org/ns/formats/data/JSON-LD",
41            Self::N3 => "http://www.w3.org/ns/formats/N3",
42            Self::NQuads => "http://www.w3.org/ns/formats/N-Quads",
43            Self::NTriples => "http://www.w3.org/ns/formats/N-Triples",
44            Self::RdfXml => "http://www.w3.org/ns/formats/RDF_XML",
45            Self::TriG => "http://www.w3.org/ns/formats/TriG",
46            Self::Turtle => "http://www.w3.org/ns/formats/Turtle",
47        }
48    }
49
50    /// The format [IANA media type](https://tools.ietf.org/html/rfc2046).
51    ///
52    /// ```
53    /// use oxrdfio::RdfFormat;
54    ///
55    /// assert_eq!(RdfFormat::NTriples.media_type(), "application/n-triples")
56    /// ```
57    #[inline]
58    pub const fn media_type(self) -> &'static str {
59        match self {
60            Self::JsonLd { profile } => {
61                // TODO: more combinations
62                if profile.contains(JsonLdProfile::Streaming) {
63                    "application/ld+json;profile=http://www.w3.org/ns/json-ld#streaming"
64                } else {
65                    "application/ld+json"
66                }
67            }
68            Self::N3 => "text/n3",
69            Self::NQuads => "application/n-quads",
70            Self::NTriples => "application/n-triples",
71            Self::RdfXml => "application/rdf+xml",
72            Self::TriG => "application/trig",
73            Self::Turtle => "text/turtle",
74        }
75    }
76
77    /// The format [IANA-registered](https://tools.ietf.org/html/rfc2046) file extension.
78    ///
79    /// ```
80    /// use oxrdfio::RdfFormat;
81    ///
82    /// assert_eq!(RdfFormat::NTriples.file_extension(), "nt")
83    /// ```
84    #[inline]
85    pub const fn file_extension(self) -> &'static str {
86        match self {
87            Self::JsonLd { .. } => "jsonld",
88            Self::N3 => "n3",
89            Self::NQuads => "nq",
90            Self::NTriples => "nt",
91            Self::RdfXml => "rdf",
92            Self::TriG => "trig",
93            Self::Turtle => "ttl",
94        }
95    }
96
97    /// The format name.
98    ///
99    /// ```
100    /// use oxrdfio::RdfFormat;
101    ///
102    /// assert_eq!(RdfFormat::NTriples.name(), "N-Triples")
103    /// ```
104    #[inline]
105    pub const fn name(self) -> &'static str {
106        match self {
107            Self::JsonLd { profile } => {
108                // TODO: more combinations
109                if profile.contains(JsonLdProfile::Streaming) {
110                    "Streaming JSON-LD"
111                } else {
112                    "JSON-LD"
113                }
114            }
115            Self::N3 => "N3",
116            Self::NQuads => "N-Quads",
117            Self::NTriples => "N-Triples",
118            Self::RdfXml => "RDF/XML",
119            Self::TriG => "TriG",
120            Self::Turtle => "Turtle",
121        }
122    }
123
124    /// Checks if the formats supports [RDF datasets](https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset) and not only [RDF graphs](https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-graph).
125    ///
126    /// ```
127    /// use oxrdfio::RdfFormat;
128    ///
129    /// assert_eq!(RdfFormat::NTriples.supports_datasets(), false);
130    /// assert_eq!(RdfFormat::NQuads.supports_datasets(), true);
131    /// ```
132    #[inline]
133    pub const fn supports_datasets(self) -> bool {
134        matches!(self, Self::JsonLd { .. } | Self::NQuads | Self::TriG)
135    }
136
137    #[deprecated(note = "All format will soon support RDF 1.2", since = "0.2.0")]
138    #[inline]
139    #[cfg(feature = "rdf-12")]
140    pub const fn supports_rdf_star(self) -> bool {
141        matches!(
142            self,
143            Self::NTriples | Self::NQuads | Self::Turtle | Self::TriG
144        )
145    }
146
147    /// Looks for a known format from a media type.
148    ///
149    /// It supports some media type aliases.
150    /// For example, "application/xml" is going to return `RdfFormat::RdfXml` even if it is not its canonical media type.
151    ///
152    /// Example:
153    /// ```
154    /// use oxjsonld::JsonLdProfile;
155    /// use oxrdfio::RdfFormat;
156    ///
157    /// assert_eq!(
158    ///     RdfFormat::from_media_type("text/turtle; charset=utf-8"),
159    ///     Some(RdfFormat::Turtle)
160    /// );
161    /// assert_eq!(
162    ///     RdfFormat::from_media_type(
163    ///         "application/ld+json ; profile = http://www.w3.org/ns/json-ld#streaming"
164    ///     ),
165    ///     Some(RdfFormat::JsonLd {
166    ///         profile: JsonLdProfile::Streaming.into()
167    ///     })
168    /// )
169    /// ```
170    #[inline]
171    pub fn from_media_type(media_type: &str) -> Option<Self> {
172        const MEDIA_SUBTYPES: [(&str, RdfFormat); 14] = [
173            (
174                "activity+json",
175                RdfFormat::JsonLd {
176                    profile: JsonLdProfileSet::empty(),
177                },
178            ),
179            (
180                "json",
181                RdfFormat::JsonLd {
182                    profile: JsonLdProfileSet::empty(),
183                },
184            ),
185            (
186                "ld+json",
187                RdfFormat::JsonLd {
188                    profile: JsonLdProfileSet::empty(),
189                },
190            ),
191            (
192                "jsonld",
193                RdfFormat::JsonLd {
194                    profile: JsonLdProfileSet::empty(),
195                },
196            ),
197            ("n-quads", RdfFormat::NQuads),
198            ("n-triples", RdfFormat::NTriples),
199            ("n3", RdfFormat::N3),
200            ("nquads", RdfFormat::NQuads),
201            ("ntriples", RdfFormat::NTriples),
202            ("plain", RdfFormat::NTriples),
203            ("rdf+xml", RdfFormat::RdfXml),
204            ("trig", RdfFormat::TriG),
205            ("turtle", RdfFormat::Turtle),
206            ("xml", RdfFormat::RdfXml),
207        ];
208        const UTF8_CHARSETS: [&str; 3] = ["ascii", "utf8", "utf-8"];
209
210        let (type_subtype, parameters) = media_type.split_once(';').unwrap_or((media_type, ""));
211
212        let (r#type, subtype) = type_subtype.split_once('/')?;
213        let r#type = r#type.trim();
214        if !r#type.eq_ignore_ascii_case("application") && !r#type.eq_ignore_ascii_case("text") {
215            return None;
216        }
217        let subtype = subtype.trim();
218        let subtype = subtype.strip_prefix("x-").unwrap_or(subtype);
219
220        let parameters = parameters.trim();
221        let parameters = if parameters.is_empty() {
222            Vec::new()
223        } else {
224            parameters
225                .split(';')
226                .map(|p| {
227                    let (key, value) = p.split_once('=')?;
228                    Some((key.trim(), value.trim()))
229                })
230                .collect::<Option<Vec<_>>>()?
231        };
232
233        for (candidate_subtype, mut candidate_id) in MEDIA_SUBTYPES {
234            if candidate_subtype.eq_ignore_ascii_case(subtype) {
235                // We have a look at parameters
236                for (key, mut value) in parameters {
237                    match key {
238                        "charset"
239                            if !UTF8_CHARSETS.iter().any(|c| c.eq_ignore_ascii_case(value)) =>
240                        {
241                            return None; // No other charset than UTF-8 is supported
242                        }
243                        "profile" => {
244                            // We remove enclosing double quotes
245                            if value.starts_with('"') && value.ends_with('"') {
246                                value = &value[1..value.len() - 1];
247                            }
248                            if let RdfFormat::JsonLd { profile } = &mut candidate_id {
249                                for value in value.split(' ') {
250                                    if let Some(value) = JsonLdProfile::from_iri(value.trim()) {
251                                        *profile |= value;
252                                    }
253                                }
254                            }
255                        }
256                        _ => (), // We ignore
257                    }
258                }
259                return Some(candidate_id);
260            }
261        }
262        None
263    }
264
265    /// Looks for a known format from an extension.
266    ///
267    /// It supports some aliases.
268    ///
269    /// Example:
270    /// ```
271    /// use oxrdfio::RdfFormat;
272    ///
273    /// assert_eq!(RdfFormat::from_extension("nt"), Some(RdfFormat::NTriples))
274    /// ```
275    #[inline]
276    pub fn from_extension(extension: &str) -> Option<Self> {
277        const EXTENSIONS: [(&str, RdfFormat); 10] = [
278            (
279                "json",
280                RdfFormat::JsonLd {
281                    profile: JsonLdProfileSet::empty(),
282                },
283            ),
284            (
285                "jsonld",
286                RdfFormat::JsonLd {
287                    profile: JsonLdProfileSet::empty(),
288                },
289            ),
290            ("n3", RdfFormat::N3),
291            ("nq", RdfFormat::NQuads),
292            ("nt", RdfFormat::NTriples),
293            ("rdf", RdfFormat::RdfXml),
294            ("trig", RdfFormat::TriG),
295            ("ttl", RdfFormat::Turtle),
296            ("txt", RdfFormat::NTriples),
297            ("xml", RdfFormat::RdfXml),
298        ];
299        for (candidate_extension, candidate_id) in EXTENSIONS {
300            if candidate_extension.eq_ignore_ascii_case(extension) {
301                return Some(candidate_id);
302            }
303        }
304        None
305    }
306}
307
308impl fmt::Display for RdfFormat {
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        f.write_str(self.name())
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn test_from_media_type() {
320        assert_eq!(RdfFormat::from_media_type("foo/bar"), None);
321        assert_eq!(RdfFormat::from_media_type("text/csv"), None);
322        assert_eq!(
323            RdfFormat::from_media_type("text/turtle"),
324            Some(RdfFormat::Turtle)
325        );
326        assert_eq!(
327            RdfFormat::from_media_type("application/x-turtle"),
328            Some(RdfFormat::Turtle)
329        );
330        assert_eq!(
331            RdfFormat::from_media_type("application/ld+json"),
332            Some(RdfFormat::JsonLd {
333                profile: JsonLdProfileSet::empty()
334            })
335        );
336        assert_eq!(
337            RdfFormat::from_media_type("application/ld+json;profile=foo"),
338            Some(RdfFormat::JsonLd {
339                profile: JsonLdProfileSet::empty()
340            })
341        );
342        assert_eq!(
343            RdfFormat::from_media_type(
344                "application/ld+json;profile=http://www.w3.org/ns/json-ld#streaming"
345            ),
346            Some(RdfFormat::JsonLd {
347                profile: JsonLdProfile::Streaming.into()
348            })
349        );
350        assert_eq!(
351            RdfFormat::from_media_type(
352                "application/ld+json ; profile = \" http://www.w3.org/ns/json-ld#streaming  http://www.w3.org/ns/json-ld#expanded \" "
353            ),
354            Some(RdfFormat::JsonLd {
355                profile: JsonLdProfile::Streaming | JsonLdProfile::Expanded
356            })
357        );
358    }
359}