1use oxjsonld::{JsonLdProfile, JsonLdProfileSet};
2use std::fmt;
3
4#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)]
8#[non_exhaustive]
9pub enum RdfFormat {
10 N3,
12 NQuads,
14 NTriples,
16 RdfXml,
18 TriG,
20 Turtle,
22 JsonLd { profile: JsonLdProfileSet },
24}
25
26impl RdfFormat {
27 #[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 #[inline]
58 pub const fn media_type(self) -> &'static str {
59 match self {
60 Self::JsonLd { profile } => {
61 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 #[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 #[inline]
105 pub const fn name(self) -> &'static str {
106 match self {
107 Self::JsonLd { profile } => {
108 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 #[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 #[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 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; }
243 "profile" => {
244 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 _ => (), }
258 }
259 return Some(candidate_id);
260 }
261 }
262 None
263 }
264
265 #[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}