1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub enum JsonLdProfile {
12 Standard,
14 Expanded,
16 Compacted,
18 Flattened,
20 Streaming,
22}
23
24impl JsonLdProfile {
25 pub fn from_iri(iri: &str) -> Option<Self> {
27 match iri {
28 "http://www.w3.org/ns/json-ld#expanded" => Some(Self::Expanded),
29 "http://www.w3.org/ns/json-ld#compacted" => Some(Self::Compacted),
30 "http://www.w3.org/ns/json-ld#flattened" => Some(Self::Flattened),
31 "http://www.w3.org/ns/json-ld#streaming" => Some(Self::Streaming),
32 _ => None,
33 }
34 }
35
36 pub fn iri(&self) -> &'static str {
38 match self {
39 Self::Standard => "http://www.w3.org/ns/json-ld#standard",
40 Self::Expanded => "http://www.w3.org/ns/json-ld#expanded",
41 Self::Compacted => "http://www.w3.org/ns/json-ld#compacted",
42 Self::Flattened => "http://www.w3.org/ns/json-ld#flattened",
43 Self::Streaming => "http://www.w3.org/ns/json-ld#streaming",
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
50pub struct JsonLdProfileSet {
51 profiles: Vec<JsonLdProfile>,
52}
53
54impl JsonLdProfileSet {
55 pub const fn empty() -> Self {
57 Self {
58 profiles: Vec::new(),
59 }
60 }
61
62 pub fn from_profile(profile: JsonLdProfile) -> Self {
64 Self {
65 profiles: vec![profile],
66 }
67 }
68
69 pub fn contains(&self, profile: JsonLdProfile) -> bool {
71 self.profiles.contains(&profile)
72 }
73
74 pub fn insert(&mut self, profile: JsonLdProfile) {
76 if !self.contains(profile) {
77 self.profiles.push(profile);
78 }
79 }
80
81 pub fn profiles(&self) -> &[JsonLdProfile] {
83 &self.profiles
84 }
85
86 pub fn to_jsonld_profile_set(&self) -> crate::jsonld::JsonLdProfileSet {
90 use crate::jsonld;
91 let mut set = jsonld::JsonLdProfileSet::empty();
92
93 for profile in &self.profiles {
94 let jsonld_profile = match profile {
95 JsonLdProfile::Standard => continue, JsonLdProfile::Expanded => jsonld::JsonLdProfile::Expanded,
97 JsonLdProfile::Compacted => jsonld::JsonLdProfile::Compacted,
98 JsonLdProfile::Flattened => jsonld::JsonLdProfile::Flattened,
99 JsonLdProfile::Streaming => jsonld::JsonLdProfile::Streaming,
100 };
101 set |= jsonld_profile;
102 }
103
104 set
105 }
106
107 pub fn from_jsonld_profile_set(jsonld_set: crate::jsonld::JsonLdProfileSet) -> Self {
111 use crate::jsonld;
112 let mut profiles = Vec::new();
113
114 for jsonld_profile in jsonld_set {
115 let profile = match jsonld_profile {
116 jsonld::JsonLdProfile::Expanded => JsonLdProfile::Expanded,
117 jsonld::JsonLdProfile::Compacted => JsonLdProfile::Compacted,
118 jsonld::JsonLdProfile::Flattened => JsonLdProfile::Flattened,
119 jsonld::JsonLdProfile::Streaming => JsonLdProfile::Streaming,
120 jsonld::JsonLdProfile::Context => continue,
122 jsonld::JsonLdProfile::Frame => continue,
123 jsonld::JsonLdProfile::Framed => continue,
124 };
125 profiles.push(profile);
126 }
127
128 Self { profiles }
129 }
130}
131
132impl From<JsonLdProfile> for JsonLdProfileSet {
133 fn from(profile: JsonLdProfile) -> Self {
134 Self::from_profile(profile)
135 }
136}
137
138impl std::ops::BitOr for JsonLdProfile {
139 type Output = JsonLdProfileSet;
140
141 fn bitor(self, rhs: Self) -> Self::Output {
142 let mut set = JsonLdProfileSet::from_profile(self);
143 set.insert(rhs);
144 set
145 }
146}
147
148impl std::ops::BitOrAssign<JsonLdProfile> for JsonLdProfileSet {
149 fn bitor_assign(&mut self, rhs: JsonLdProfile) {
150 self.insert(rhs);
151 }
152}
153
154#[derive(Eq, PartialEq, Debug, Clone, Hash, Serialize, Deserialize, Default)]
159#[non_exhaustive]
160pub enum RdfFormat {
161 N3,
163 NQuads,
165 NTriples,
167 RdfXml,
169 TriG,
171 #[default]
173 Turtle,
174 JsonLd { profile: JsonLdProfileSet },
176}
177
178impl RdfFormat {
179 pub const fn iri(&self) -> &'static str {
190 match self {
191 Self::JsonLd { .. } => "https://www.w3.org/ns/formats/data/JSON-LD",
192 Self::N3 => "http://www.w3.org/ns/formats/N3",
193 Self::NQuads => "http://www.w3.org/ns/formats/N-Quads",
194 Self::NTriples => "http://www.w3.org/ns/formats/N-Triples",
195 Self::RdfXml => "http://www.w3.org/ns/formats/RDF_XML",
196 Self::TriG => "http://www.w3.org/ns/formats/TriG",
197 Self::Turtle => "http://www.w3.org/ns/formats/Turtle",
198 }
199 }
200
201 pub fn media_type(&self) -> &'static str {
209 match self {
210 Self::JsonLd { profile } => {
211 if profile.contains(JsonLdProfile::Streaming) {
212 "application/ld+json;profile=http://www.w3.org/ns/json-ld#streaming"
213 } else {
214 "application/ld+json"
215 }
216 }
217 Self::N3 => "text/n3",
218 Self::NQuads => "application/n-quads",
219 Self::NTriples => "application/n-triples",
220 Self::RdfXml => "application/rdf+xml",
221 Self::TriG => "application/trig",
222 Self::Turtle => "text/turtle",
223 }
224 }
225
226 pub const fn file_extension(&self) -> &'static str {
234 match self {
235 Self::JsonLd { .. } => "jsonld",
236 Self::N3 => "n3",
237 Self::NQuads => "nq",
238 Self::NTriples => "nt",
239 Self::RdfXml => "rdf",
240 Self::TriG => "trig",
241 Self::Turtle => "ttl",
242 }
243 }
244
245 pub fn name(&self) -> &'static str {
253 match self {
254 Self::JsonLd { profile } => {
255 if profile.contains(JsonLdProfile::Streaming) {
256 "Streaming JSON-LD"
257 } else {
258 "JSON-LD"
259 }
260 }
261 Self::N3 => "N3",
262 Self::NQuads => "N-Quads",
263 Self::NTriples => "N-Triples",
264 Self::RdfXml => "RDF/XML",
265 Self::TriG => "TriG",
266 Self::Turtle => "Turtle",
267 }
268 }
269
270 pub const fn supports_datasets(&self) -> bool {
279 matches!(self, Self::JsonLd { .. } | Self::NQuads | Self::TriG)
280 }
281
282 pub const fn supports_rdf_star(&self) -> bool {
291 matches!(
292 self,
293 Self::NTriples | Self::NQuads | Self::Turtle | Self::TriG
294 )
295 }
296
297 pub fn from_media_type(media_type: &str) -> Option<Self> {
320 const MEDIA_SUBTYPES: [(&str, RdfFormat); 14] = [
321 (
322 "activity+json",
323 RdfFormat::JsonLd {
324 profile: JsonLdProfileSet::empty(),
325 },
326 ),
327 (
328 "json",
329 RdfFormat::JsonLd {
330 profile: JsonLdProfileSet::empty(),
331 },
332 ),
333 (
334 "ld+json",
335 RdfFormat::JsonLd {
336 profile: JsonLdProfileSet::empty(),
337 },
338 ),
339 (
340 "jsonld",
341 RdfFormat::JsonLd {
342 profile: JsonLdProfileSet::empty(),
343 },
344 ),
345 ("n-quads", RdfFormat::NQuads),
346 ("n-triples", RdfFormat::NTriples),
347 ("n3", RdfFormat::N3),
348 ("nquads", RdfFormat::NQuads),
349 ("ntriples", RdfFormat::NTriples),
350 ("plain", RdfFormat::NTriples),
351 ("rdf+xml", RdfFormat::RdfXml),
352 ("trig", RdfFormat::TriG),
353 ("turtle", RdfFormat::Turtle),
354 ("xml", RdfFormat::RdfXml),
355 ];
356 const UTF8_CHARSETS: [&str; 3] = ["ascii", "utf8", "utf-8"];
357
358 let (type_subtype, parameters) = media_type.split_once(';').unwrap_or((media_type, ""));
359
360 let (r#type, subtype) = type_subtype.split_once('/')?;
361 let r#type = r#type.trim();
362 if !r#type.eq_ignore_ascii_case("application") && !r#type.eq_ignore_ascii_case("text") {
363 return None;
364 }
365 let subtype = subtype.trim();
366 let subtype = subtype.strip_prefix("x-").unwrap_or(subtype);
367
368 let parameters = parameters.trim();
369 let parameters = if parameters.is_empty() {
370 Vec::new()
371 } else {
372 parameters
373 .split(';')
374 .map(|p| {
375 let (key, value) = p.split_once('=')?;
376 Some((key.trim(), value.trim()))
377 })
378 .collect::<Option<Vec<_>>>()?
379 };
380
381 for (candidate_subtype, mut candidate_id) in MEDIA_SUBTYPES {
382 if candidate_subtype.eq_ignore_ascii_case(subtype) {
383 for (key, mut value) in parameters {
385 match key {
386 "charset"
387 if !UTF8_CHARSETS.iter().any(|c| c.eq_ignore_ascii_case(value)) =>
388 {
389 return None; }
391 "profile" => {
392 if value.starts_with('"') && value.ends_with('"') {
394 value = &value[1..value.len() - 1];
395 }
396 if let RdfFormat::JsonLd { profile } = &mut candidate_id {
397 for value in value.split(' ') {
398 if let Some(value) = JsonLdProfile::from_iri(value.trim()) {
399 profile.insert(value);
400 }
401 }
402 }
403 }
404 _ => (), }
406 }
407 return Some(candidate_id);
408 }
409 }
410 None
411 }
412
413 pub fn from_extension(extension: &str) -> Option<Self> {
424 const EXTENSIONS: [(&str, RdfFormat); 10] = [
425 (
426 "json",
427 RdfFormat::JsonLd {
428 profile: JsonLdProfileSet::empty(),
429 },
430 ),
431 (
432 "jsonld",
433 RdfFormat::JsonLd {
434 profile: JsonLdProfileSet::empty(),
435 },
436 ),
437 ("n3", RdfFormat::N3),
438 ("nq", RdfFormat::NQuads),
439 ("nt", RdfFormat::NTriples),
440 ("rdf", RdfFormat::RdfXml),
441 ("trig", RdfFormat::TriG),
442 ("ttl", RdfFormat::Turtle),
443 ("txt", RdfFormat::NTriples),
444 ("xml", RdfFormat::RdfXml),
445 ];
446 for (candidate_extension, candidate_id) in EXTENSIONS {
447 if candidate_extension.eq_ignore_ascii_case(extension) {
448 return Some(candidate_id);
449 }
450 }
451 None
452 }
453}
454
455impl fmt::Display for RdfFormat {
456 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457 f.write_str(self.name())
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_from_media_type() {
467 assert_eq!(RdfFormat::from_media_type("foo/bar"), None);
468 assert_eq!(RdfFormat::from_media_type("text/csv"), None);
469 assert_eq!(
470 RdfFormat::from_media_type("text/turtle"),
471 Some(RdfFormat::Turtle)
472 );
473 assert_eq!(
474 RdfFormat::from_media_type("application/x-turtle"),
475 Some(RdfFormat::Turtle)
476 );
477 assert_eq!(
478 RdfFormat::from_media_type("application/ld+json"),
479 Some(RdfFormat::JsonLd {
480 profile: JsonLdProfileSet::empty()
481 })
482 );
483 assert_eq!(
484 RdfFormat::from_media_type("application/ld+json;profile=foo"),
485 Some(RdfFormat::JsonLd {
486 profile: JsonLdProfileSet::empty()
487 })
488 );
489 assert_eq!(
490 RdfFormat::from_media_type(
491 "application/ld+json;profile=http://www.w3.org/ns/json-ld#streaming"
492 ),
493 Some(RdfFormat::JsonLd {
494 profile: JsonLdProfile::Streaming.into()
495 })
496 );
497 }
498
499 #[test]
500 fn test_from_extension() {
501 assert_eq!(RdfFormat::from_extension("ttl"), Some(RdfFormat::Turtle));
502 assert_eq!(RdfFormat::from_extension("nt"), Some(RdfFormat::NTriples));
503 assert_eq!(RdfFormat::from_extension("nq"), Some(RdfFormat::NQuads));
504 assert_eq!(RdfFormat::from_extension("rdf"), Some(RdfFormat::RdfXml));
505 assert_eq!(
506 RdfFormat::from_extension("jsonld"),
507 Some(RdfFormat::JsonLd {
508 profile: JsonLdProfileSet::empty()
509 })
510 );
511 assert_eq!(RdfFormat::from_extension("unknown"), None);
512 }
513
514 #[test]
515 fn test_format_properties() {
516 assert!(RdfFormat::NQuads.supports_datasets());
517 assert!(!RdfFormat::NTriples.supports_datasets());
518
519 assert!(RdfFormat::Turtle.supports_rdf_star());
520 assert!(!RdfFormat::RdfXml.supports_rdf_star());
521
522 assert_eq!(RdfFormat::Turtle.file_extension(), "ttl");
523 assert_eq!(RdfFormat::NTriples.media_type(), "application/n-triples");
524 assert_eq!(RdfFormat::Turtle.name(), "Turtle");
525 }
526
527 #[test]
528 fn test_jsonld_profiles() {
529 let mut profile_set = JsonLdProfileSet::empty();
530 assert!(!profile_set.contains(JsonLdProfile::Streaming));
531
532 profile_set.insert(JsonLdProfile::Streaming);
533 assert!(profile_set.contains(JsonLdProfile::Streaming));
534
535 let combined = JsonLdProfile::Streaming | JsonLdProfile::Expanded;
536 assert!(combined.contains(JsonLdProfile::Streaming));
537 assert!(combined.contains(JsonLdProfile::Expanded));
538 }
539}