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    /// Checks if the formats supports [RDF-star quoted triples](https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html#dfn-quoted).
138    ///
139    /// ```
140    /// use oxrdfio::RdfFormat;
141    ///
142    /// assert_eq!(RdfFormat::NTriples.supports_rdf_star(), true);
143    /// assert_eq!(RdfFormat::RdfXml.supports_rdf_star(), false);
144    /// ```
145    #[inline]
146    #[cfg(feature = "rdf-star")]
147    pub const fn supports_rdf_star(self) -> bool {
148        matches!(
149            self,
150            Self::NTriples | Self::NQuads | Self::Turtle | Self::TriG
151        )
152    }
153
154    /// Looks for a known format from a media type.
155    ///
156    /// It supports some media type aliases.
157    /// For example, "application/xml" is going to return `RdfFormat::RdfXml` even if it is not its canonical media type.
158    ///
159    /// Example:
160    /// ```
161    /// use oxjsonld::JsonLdProfile;
162    /// use oxrdfio::RdfFormat;
163    ///
164    /// assert_eq!(
165    ///     RdfFormat::from_media_type("text/turtle; charset=utf-8"),
166    ///     Some(RdfFormat::Turtle)
167    /// );
168    /// assert_eq!(
169    ///     RdfFormat::from_media_type(
170    ///         "application/ld+json ; profile = http://www.w3.org/ns/json-ld#streaming"
171    ///     ),
172    ///     Some(RdfFormat::JsonLd {
173    ///         profile: JsonLdProfile::Streaming.into()
174    ///     })
175    /// )
176    /// ```
177    #[inline]
178    pub fn from_media_type(media_type: &str) -> Option<Self> {
179        const MEDIA_SUBTYPES: [(&str, RdfFormat); 14] = [
180            (
181                "activity+json",
182                RdfFormat::JsonLd {
183                    profile: JsonLdProfileSet::empty(),
184                },
185            ),
186            (
187                "json",
188                RdfFormat::JsonLd {
189                    profile: JsonLdProfileSet::empty(),
190                },
191            ),
192            (
193                "ld+json",
194                RdfFormat::JsonLd {
195                    profile: JsonLdProfileSet::empty(),
196                },
197            ),
198            (
199                "jsonld",
200                RdfFormat::JsonLd {
201                    profile: JsonLdProfileSet::empty(),
202                },
203            ),
204            ("n-quads", RdfFormat::NQuads),
205            ("n-triples", RdfFormat::NTriples),
206            ("n3", RdfFormat::N3),
207            ("nquads", RdfFormat::NQuads),
208            ("ntriples", RdfFormat::NTriples),
209            ("plain", RdfFormat::NTriples),
210            ("rdf+xml", RdfFormat::RdfXml),
211            ("trig", RdfFormat::TriG),
212            ("turtle", RdfFormat::Turtle),
213            ("xml", RdfFormat::RdfXml),
214        ];
215        const UTF8_CHARSETS: [&str; 3] = ["ascii", "utf8", "utf-8"];
216
217        let (type_subtype, parameters) = media_type.split_once(';').unwrap_or((media_type, ""));
218
219        let (r#type, subtype) = type_subtype.split_once('/')?;
220        let r#type = r#type.trim();
221        if !r#type.eq_ignore_ascii_case("application") && !r#type.eq_ignore_ascii_case("text") {
222            return None;
223        }
224        let subtype = subtype.trim();
225        let subtype = subtype.strip_prefix("x-").unwrap_or(subtype);
226
227        let parameters = parameters.trim();
228        let parameters = if parameters.is_empty() {
229            Vec::new()
230        } else {
231            parameters
232                .split(';')
233                .map(|p| {
234                    let (key, value) = p.split_once('=')?;
235                    Some((key.trim(), value.trim()))
236                })
237                .collect::<Option<Vec<_>>>()?
238        };
239
240        for (candidate_subtype, mut candidate_id) in MEDIA_SUBTYPES {
241            if candidate_subtype.eq_ignore_ascii_case(subtype) {
242                // We have a look at parameters
243                for (key, mut value) in parameters {
244                    match key {
245                        "charset" => {
246                            if !UTF8_CHARSETS.iter().any(|c| c.eq_ignore_ascii_case(value)) {
247                                return None; // No other charset than UTF-8 is supported
248                            }
249                        }
250                        "profile" => {
251                            // We remove enclosing double quotes
252                            if value.starts_with('"') && value.ends_with('"') {
253                                value = &value[1..value.len() - 1];
254                            }
255                            if let RdfFormat::JsonLd { profile } = &mut candidate_id {
256                                for value in value.split(' ') {
257                                    if let Some(value) = JsonLdProfile::from_iri(value.trim()) {
258                                        *profile |= value;
259                                    }
260                                }
261                            }
262                        }
263                        _ => (), // We ignore
264                    }
265                }
266                return Some(candidate_id);
267            }
268        }
269        None
270    }
271
272    /// Looks for a known format from an extension.
273    ///
274    /// It supports some aliases.
275    ///
276    /// Example:
277    /// ```
278    /// use oxrdfio::RdfFormat;
279    ///
280    /// assert_eq!(RdfFormat::from_extension("nt"), Some(RdfFormat::NTriples))
281    /// ```
282    #[inline]
283    pub fn from_extension(extension: &str) -> Option<Self> {
284        const EXTENSIONS: [(&str, RdfFormat); 10] = [
285            (
286                "json",
287                RdfFormat::JsonLd {
288                    profile: JsonLdProfileSet::empty(),
289                },
290            ),
291            (
292                "jsonld",
293                RdfFormat::JsonLd {
294                    profile: JsonLdProfileSet::empty(),
295                },
296            ),
297            ("n3", RdfFormat::N3),
298            ("nq", RdfFormat::NQuads),
299            ("nt", RdfFormat::NTriples),
300            ("rdf", RdfFormat::RdfXml),
301            ("trig", RdfFormat::TriG),
302            ("ttl", RdfFormat::Turtle),
303            ("txt", RdfFormat::NTriples),
304            ("xml", RdfFormat::RdfXml),
305        ];
306        for (candidate_extension, candidate_id) in EXTENSIONS {
307            if candidate_extension.eq_ignore_ascii_case(extension) {
308                return Some(candidate_id);
309            }
310        }
311        None
312    }
313}
314
315impl fmt::Display for RdfFormat {
316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317        f.write_str(self.name())
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn test_from_media_type() {
327        assert_eq!(RdfFormat::from_media_type("foo/bar"), None);
328        assert_eq!(RdfFormat::from_media_type("text/csv"), None);
329        assert_eq!(
330            RdfFormat::from_media_type("text/turtle"),
331            Some(RdfFormat::Turtle)
332        );
333        assert_eq!(
334            RdfFormat::from_media_type("application/x-turtle"),
335            Some(RdfFormat::Turtle)
336        );
337        assert_eq!(
338            RdfFormat::from_media_type("application/ld+json"),
339            Some(RdfFormat::JsonLd {
340                profile: JsonLdProfileSet::empty()
341            })
342        );
343        assert_eq!(
344            RdfFormat::from_media_type("application/ld+json;profile=foo"),
345            Some(RdfFormat::JsonLd {
346                profile: JsonLdProfileSet::empty()
347            })
348        );
349        assert_eq!(
350            RdfFormat::from_media_type(
351                "application/ld+json;profile=http://www.w3.org/ns/json-ld#streaming"
352            ),
353            Some(RdfFormat::JsonLd {
354                profile: JsonLdProfile::Streaming.into()
355            })
356        );
357        assert_eq!(
358            RdfFormat::from_media_type("application/ld+json ; profile = \" http://www.w3.org/ns/json-ld#streaming  http://www.w3.org/ns/json-ld#expanded \" "),
359            Some(RdfFormat::JsonLd {
360                profile: JsonLdProfile::Streaming | JsonLdProfile::Expanded
361            })
362        );
363    }
364}