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 #[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 #[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 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; }
249 }
250 "profile" => {
251 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 _ => (), }
265 }
266 return Some(candidate_id);
267 }
268 }
269 None
270 }
271
272 #[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}