prefixmap/
prefixmap.rs

1use colored::*;
2use indexmap::map::Iter;
3use indexmap::IndexMap;
4use iri_s::*;
5use serde::{Deserialize, Serialize};
6
7use crate::{IriRef, PrefixMapError};
8use std::str::FromStr;
9use std::{collections::HashMap, fmt};
10
11/// Contains declarations of prefix maps which are used in TURTLE, SPARQL and ShEx
12#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Default)]
13#[serde(transparent)]
14pub struct PrefixMap {
15    /// Proper prefix map associations of an alias `String` to an `IriS`
16    pub map: IndexMap<String, IriS>,
17
18    /// Color of prefix aliases when qualifying an IRI that has an alias
19    #[serde(skip)]
20    qualify_prefix_color: Option<Color>,
21
22    /// Color of local names when qualifying an IRI that has an alias
23    #[serde(skip)]
24    qualify_localname_color: Option<Color>,
25
26    /// Color of semicolon when qualifying an IRI that has an alias
27    #[serde(skip)]
28    qualify_semicolon_color: Option<Color>,
29
30    /// Whether to generate hyperlink when qualifying an IRI
31    #[serde(skip)]
32    hyperlink: bool,
33}
34
35fn split(str: &str) -> Option<(&str, &str)> {
36    str.rsplit_once(':')
37}
38
39impl PrefixMap {
40    /// Creates an empty ("map
41    pub fn new() -> PrefixMap {
42        PrefixMap::default()
43    }
44
45    /// Change ("color when qualifying a IRI
46    pub fn with_qualify_prefix_color(mut self, color: Option<Color>) -> Self {
47        self.qualify_prefix_color = color;
48        self
49    }
50
51    /// Change color of localname when qualifying a IRI
52    pub fn with_qualify_localname_color(mut self, color: Option<Color>) -> Self {
53        self.qualify_localname_color = color;
54        self
55    }
56
57    /// Change color of semicolon when qualifying a IRI
58    pub fn with_qualify_semicolon_color(mut self, color: Option<Color>) -> Self {
59        self.qualify_semicolon_color = color;
60        self
61    }
62
63    pub fn without_rich_qualifying(self) -> Self {
64        self.with_hyperlink(false)
65            .with_qualify_localname_color(None)
66            .with_qualify_prefix_color(None)
67            .with_qualify_semicolon_color(None)
68    }
69
70    /// Inserts an alias association to an IRI
71    pub fn insert(&mut self, alias: &str, iri: &IriS) -> Result<(), PrefixMapError> {
72        match self.map.entry(alias.to_string()) {
73            indexmap::map::Entry::Occupied(mut e) => {
74                // TODO: Possible error with repeated aliases??
75                e.insert(iri.to_owned());
76            }
77            indexmap::map::Entry::Vacant(v) => {
78                v.insert(iri.to_owned());
79            }
80        };
81        Ok(())
82    }
83
84    pub fn find(&self, str: &str) -> Option<&IriS> {
85        self.map.get(str)
86    }
87
88    pub fn from_hashmap(hm: &HashMap<&str, &str>) -> Result<PrefixMap, PrefixMapError> {
89        let mut pm = PrefixMap::new();
90        for (a, s) in hm.iter() {
91            let iri = IriS::from_str(s)?;
92            pm.insert(a, &iri)?;
93        }
94        Ok(pm)
95    }
96
97    /// Return an iterator over the key-value pairs of the ("map, in their order
98    pub fn iter(&self) -> Iter<String, IriS> {
99        self.map.iter()
100    }
101
102    /// Resolves a string against a prefix map
103    /// Example:
104    /// Given a string like "ex:a" and a prefixmap that has alias "ex" with value "http://example.org/", the result will be "http://example.org/a"
105    /// ```
106    /// use std::collections::HashMap;
107    /// use prefixmap::PrefixMap;
108    /// use prefixmap::PrefixMapError;
109    /// use iri_s::*;
110    /// use std::str::FromStr;
111    ///
112    ///
113    /// let pm: PrefixMap = PrefixMap::from_hashmap(
114    ///   &HashMap::from([
115    ///     ("", "http://example.org/"),
116    ///     ("schema", "http://schema.org/")])
117    /// )?;
118    /// let a = pm.resolve(":a")?;
119    /// let a_resolved = IriS::from_str("http://example.org/a")?;
120    /// assert_eq!(a, a_resolved);
121    /// Ok::<(), PrefixMapError>(());
122    ///
123    /// let knows = pm.resolve("schema:knows")?;
124    /// let knows_resolved = IriS::from_str("http://schema.org/knows")?;
125    /// assert_eq!(knows, knows_resolved);
126    /// Ok::<(), PrefixMapError>(())
127    /// ```
128    pub fn resolve(&self, str: &str) -> Result<IriS, PrefixMapError> {
129        match split(str) {
130            Some((prefix, local)) => {
131                let iri = self.resolve_prefix_local(prefix, local)?;
132                Ok(iri)
133            }
134            None => {
135                let iri = IriS::from_str(str)?;
136                Ok(iri)
137            }
138        }
139    }
140
141    /// Resolves an IriRef against a prefix map
142    pub fn resolve_iriref(&self, iri_ref: &IriRef) -> Result<IriS, PrefixMapError> {
143        match iri_ref {
144            IriRef::Prefixed { prefix, local } => {
145                let iri = self.resolve_prefix_local(prefix, local)?;
146                Ok(iri)
147            }
148            IriRef::Iri(iri) => Ok(iri.clone()),
149        }
150    }
151
152    /// Resolves a prefixed alias and a local name in a prefix map to obtain the full IRI
153    /// ```
154    /// use std::collections::HashMap;
155    /// use prefixmap::PrefixMap;
156    /// # use prefixmap::PrefixMapError;
157    /// # use iri_s::*;
158    /// # use std::str::FromStr;
159    ///
160    ///
161    /// let pm = PrefixMap::from_hashmap(
162    ///   &HashMap::from([
163    ///     ("", "http://example.org/"),
164    ///     ("schema", "http://schema.org/"),
165    ///     ("xsd", "http://www.w3.org/2001/XMLSchema#")
166    /// ]))?;
167    ///
168    /// let a = pm.resolve_prefix_local("", "a")?;
169    /// let a_resolved = IriS::from_str("http://example.org/a")?;
170    /// assert_eq!(a, a_resolved);
171    ///
172    /// let knows = pm.resolve_prefix_local("schema","knows")?;
173    /// let knows_resolved = IriS::from_str("http://schema.org/knows")?;
174    /// assert_eq!(knows, knows_resolved);
175    ///
176    /// let xsd_string = pm.resolve_prefix_local("xsd","string")?;
177    /// let xsd_string_resolved = IriS::from_str("http://www.w3.org/2001/XMLSchema#string")?;
178    /// assert_eq!(xsd_string, xsd_string_resolved);
179    /// # Ok::<(), PrefixMapError>(())
180    /// ```
181    pub fn resolve_prefix_local(&self, prefix: &str, local: &str) -> Result<IriS, PrefixMapError> {
182        match self.find(prefix) {
183            Some(iri) => {
184                let new_iri = iri.extend(local)?;
185                Ok(new_iri)
186            }
187            None => Err(PrefixMapError::PrefixNotFound {
188                prefix: prefix.to_string(),
189                prefixmap: self.clone(),
190            }),
191        }
192    }
193
194    /// Qualifies an IRI against a prefix map
195    ///
196    /// If it can't qualify the IRI, it returns the iri between `<` and `>`
197    /// ```
198    /// # use std::collections::HashMap;
199    /// # use prefixmap::PrefixMap;
200    /// # use prefixmap::PrefixMapError;
201    /// # use iri_s::*;
202    /// # use std::str::FromStr;
203    /// let pm = PrefixMap::from_hashmap(
204    ///   &HashMap::from([
205    ///     ("", "http://example.org/"),
206    ///     ("schema", "http://schema.org/")])
207    /// )?;
208    /// let a = IriS::from_str("http://example.org/a")?;
209    /// assert_eq!(pm.qualify(&a), ":a");
210    ///
211    /// let knows = IriS::from_str("http://schema.org/knows")?;
212    /// assert_eq!(pm.qualify(&knows), "schema:knows");
213    ///
214    /// let other = IriS::from_str("http://other.org/foo")?;
215    /// assert_eq!(pm.qualify(&other), "<http://other.org/foo>");
216    /// # Ok::<(), PrefixMapError>(())
217    /// ```
218    pub fn qualify(&self, iri: &IriS) -> String {
219        if let Some(qualified) = self.qualify_optional(iri) {
220            qualified
221        } else {
222            format!("<{iri}>")
223        }
224    }
225
226    /// Qualifies an IRI against a prefix map
227    ///
228    /// If it can't qualify the IRI, returns None
229    ///
230    /// ```
231    /// # use std::collections::HashMap;
232    /// # use prefixmap::PrefixMap;
233    /// # use prefixmap::PrefixMapError;
234    /// # use iri_s::*;
235    /// # use std::str::FromStr;
236    /// let pm = PrefixMap::from_hashmap(
237    ///   &HashMap::from([
238    ///     ("", "http://example.org/"),
239    ///     ("schema", "http://schema.org/")])
240    /// )?;
241    /// let a = IriS::from_str("http://example.org/a")?;
242    /// assert_eq!(pm.qualify(&a), Some(":a"));
243    ///
244    /// let knows = IriS::from_str("http://schema.org/knows")?;
245    /// assert_eq!(pm.qualify(&knows), Some("schema:knows"));
246    ///
247    /// let other = IriS::from_str("http://other.org/foo")?;
248    /// assert_eq!(pm.qualify(&other), None);
249    /// # Ok::<(), PrefixMapError>(())
250    /// ```
251    pub fn qualify_optional(&self, iri: &IriS) -> Option<String> {
252        let mut founds: Vec<_> = self
253            .map
254            .iter()
255            .filter_map(|(alias, pm_iri)| {
256                iri.as_str()
257                    .strip_prefix(pm_iri.as_str())
258                    .map(|rest| (alias, rest))
259            })
260            .collect();
261        founds.sort_by_key(|(_, iri)| iri.len());
262        let str = if let Some((alias, rest)) = founds.first() {
263            let prefix_colored = match self.qualify_prefix_color {
264                Some(color) => alias.color(color),
265                None => ColoredString::from(alias.as_str()),
266            };
267            let rest_colored = match self.qualify_localname_color {
268                Some(color) => rest.color(color),
269                None => ColoredString::from(*rest),
270            };
271            let semicolon_colored = match self.qualify_semicolon_color {
272                Some(color) => ":".color(color),
273                None => ColoredString::from(":"),
274            };
275            Some(format!(
276                "{}{}{}",
277                prefix_colored, semicolon_colored, rest_colored
278            ))
279        } else {
280            None
281        };
282        if self.hyperlink {
283            str.map(|s| format!("\u{1b}]8;;{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\", s.as_str(), s))
284        } else {
285            str
286        }
287    }
288
289    /// Qualifies an IRI against a prefix map returning the length of the qualified string
290    /// ```
291    /// # use std::collections::HashMap;
292    /// # use prefixmap::PrefixMap;
293    /// # use prefixmap::PrefixMapError;
294    /// # use iri_s::*;
295    /// # use std::str::FromStr;
296    /// let pm = PrefixMap::from_hashmap(
297    ///   &HashMap::from([
298    ///     ("", "http://example.org/"),
299    ///     ("schema", "http://schema.org/")])
300    /// )?;
301    /// let a = IriS::from_str("http://example.org/a")?;
302    /// assert_eq!(pm.qualify(&a), ":a");
303    ///
304    /// let knows = IriS::from_str("http://schema.org/knows")?;
305    /// assert_eq!(pm.qualify(&knows), "schema:knows");
306    ///
307    /// let other = IriS::from_str("http://other.org/foo")?;
308    /// assert_eq!(pm.qualify(&other), "<http://other.org/foo>");
309    /// # Ok::<(), PrefixMapError>(())
310    /// ```
311    pub fn qualify_and_length(&self, iri: &IriS) -> (String, usize) {
312        let mut founds: Vec<_> = self
313            .map
314            .iter()
315            .filter_map(|(alias, pm_iri)| {
316                iri.as_str()
317                    .strip_prefix(pm_iri.as_str())
318                    .map(|rest| (alias, rest))
319            })
320            .collect();
321        founds.sort_by_key(|(_, iri)| iri.len());
322        let (str, length) = if let Some((alias, rest)) = founds.first() {
323            let prefix_colored = match self.qualify_prefix_color {
324                Some(color) => alias.color(color),
325                None => ColoredString::from(alias.as_str()),
326            };
327            let rest_colored = match self.qualify_localname_color {
328                Some(color) => rest.color(color),
329                None => ColoredString::from(*rest),
330            };
331            let semicolon_colored = match self.qualify_semicolon_color {
332                Some(color) => ":".color(color),
333                None => ColoredString::from(":"),
334            };
335            let length = prefix_colored.len() + 1 + rest_colored.len();
336            (
337                format!("{}{}{}", prefix_colored, semicolon_colored, rest_colored),
338                length,
339            )
340        } else {
341            let length = format!("{iri}").len();
342            (format!("<{iri}>"), length)
343        };
344        if self.hyperlink {
345            (
346                format!(
347                    "\u{1b}]8;;{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\",
348                    iri.as_str(),
349                    str
350                ),
351                length,
352            )
353        } else {
354            (str, length)
355        }
356    }
357
358    /// Qualify an IRI against a prefix map and obtains the local name
359    /// ```
360    /// # use std::collections::HashMap;
361    /// # use prefixmap::PrefixMap;
362    /// # use prefixmap::PrefixMapError;
363    /// # use iri_s::*;
364    /// # use std::str::FromStr;
365    /// let pm = PrefixMap::from_hashmap(
366    ///   &HashMap::from([
367    ///     ("", "http://example.org/"),
368    ///     ("schema", "http://schema.org/")])
369    /// )?;
370    /// let a = IriS::from_str("http://example.org/a")?;
371    /// assert_eq!(pm.qualify_local(&a), Some("a".to_string()));
372    ///
373    /// let knows = IriS::from_str("http://schema.org/knows")?;
374    /// assert_eq!(pm.qualify_local(&knows), Some("knows".to_string()));
375    ///
376    /// let other = IriS::from_str("http://other.org/foo")?;
377    /// assert_eq!(pm.qualify_local(&other), None);
378    /// # Ok::<(), PrefixMapError>(())
379    /// ```
380    pub fn qualify_local(&self, iri: &IriS) -> Option<String> {
381        let mut founds: Vec<_> = self
382            .map
383            .iter()
384            .filter_map(|(alias, pm_iri)| {
385                iri.as_str()
386                    .strip_prefix(pm_iri.as_str())
387                    .map(|rest| (alias, rest))
388            })
389            .collect();
390        founds.sort_by_key(|(_, iri)| iri.len());
391        if let Some((_alias, rest)) = founds.first() {
392            Some(rest.to_string())
393        } else {
394            None
395        }
396    }
397
398    /// Basic prefixmap with common definitions
399    pub fn basic() -> PrefixMap {
400        PrefixMap::from_hashmap(&HashMap::from([
401            ("", "http://example.org/"),
402            ("dc", "http://purl.org/dc/elements/1.1/"),
403            ("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
404            ("rdfs", "http://www.w3.org/2000/01/rdf-schema#"),
405            ("sh", "http://www.w3.org/ns/shacl#"),
406            ("xsd", "http://www.w3.org/2001/XMLSchema#"),
407        ]))
408        .unwrap()
409    }
410
411    /// Default Wikidata prefixmap
412    /// This source of this list is <https://www.mediawiki.org/wiki/Wikibase/Indexing/RDF_Dump_Format#Full_list_of_prefixes>
413    pub fn wikidata() -> PrefixMap {
414        PrefixMap::from_hashmap(&HashMap::from([
415            ("bd", "http://www.bigdata.com/rdf#"),
416            ("cc", "http://creativecommons.org/ns#"),
417            ("dct", "http://purl.org/dc/terms/"),
418            ("geo", "http://www.opengis.net/ont/geosparql#"),
419            ("hint", "http://www.bigdata.com/queryHints#"),
420            ("ontolex", "http://www.w3.org/ns/lemon/ontolex#"),
421            ("owl", "http://www.w3.org/2002/07/owl#"),
422            ("prov", "http://www.w3.org/ns/prov#"),
423            ("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
424            ("rdfs", "http://www.w3.org/2000/01/rdf-schema#"),
425            ("schema", "http://schema.org/"),
426            ("skos", "http://www.w3.org/2004/02/skos/core#"),
427            ("xsd", "http://www.w3.org/2001/XMLSchema#"),
428            ("p", "http://www.wikidata.org/prop/"),
429            ("pq", "http://www.wikidata.org/prop/qualifier/"),
430            (
431                "pqn",
432                "http://www.wikidata.org/prop/qualifier/value-normalized/",
433            ),
434            ("pqv", "http://www.wikidata.org/prop/qualifier/value/"),
435            ("pr", "http://www.wikidata.org/prop/reference/"),
436            (
437                "prn",
438                "http://www.wikidata.org/prop/reference/value-normalized/",
439            ),
440            ("prv", "http://www.wikidata.org/prop/reference/value/"),
441            ("psv", "http://www.wikidata.org/prop/statement/value/"),
442            ("ps", "http://www.wikidata.org/prop/statement/"),
443            (
444                "psn",
445                "http://www.wikidata.org/prop/statement/value-normalized/",
446            ),
447            ("wd", "http://www.wikidata.org/entity/"),
448            ("wdata", "http://www.wikidata.org/wiki/Special:EntityData/"),
449            ("wdno", "http://www.wikidata.org/prop/novalue/"),
450            ("wdref", "http://www.wikidata.org/reference/"),
451            ("wds", "http://www.wikidata.org/entity/statement/"),
452            ("wdt", "http://www.wikidata.org/prop/direct/"),
453            ("wdtn", "http://www.wikidata.org/prop/direct-normalized/"),
454            ("wdv", "http://www.wikidata.org/value/"),
455            ("wikibase", "http://wikiba.se/ontology#"),
456        ]))
457        .unwrap()
458        .without_default_colors()
459        .with_hyperlink(true)
460    }
461
462    pub fn without_colors(mut self) -> Self {
463        self.qualify_localname_color = None;
464        self.qualify_prefix_color = None;
465        self.qualify_semicolon_color = None;
466        self
467    }
468
469    pub fn without_default_colors(mut self) -> Self {
470        self.qualify_localname_color = Some(Color::Black);
471        self.qualify_prefix_color = Some(Color::Blue);
472        self.qualify_semicolon_color = Some(Color::Red);
473        self
474    }
475
476    pub fn with_hyperlink(mut self, hyperlink: bool) -> Self {
477        self.hyperlink = hyperlink;
478        self
479    }
480
481    pub fn merge(&mut self, other: PrefixMap) -> Result<(), PrefixMapError> {
482        for (alias, iri) in other.iter() {
483            self.insert(alias, iri)?
484        }
485        Ok(())
486    }
487}
488
489impl fmt::Display for PrefixMap {
490    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491        for (alias, iri) in self.map.iter() {
492            writeln!(f, "prefix {}: <{}>", &alias, &iri)?
493        }
494        Ok(())
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    #[test]
503    fn split_ex_name() {
504        assert_eq!(split("ex:name"), Some(("ex", "name")))
505    }
506
507    #[test]
508    fn prefix_map1() {
509        let mut pm = PrefixMap::new();
510        let binding = IriS::from_str("http://example.org/").unwrap();
511        pm.insert("ex", &binding).unwrap();
512        let expected = IriS::from_str("http://example.org/name").unwrap();
513        assert_eq!(pm.resolve("ex:name").unwrap(), expected);
514    }
515
516    #[test]
517    fn prefixmap_display() {
518        let mut pm = PrefixMap::new();
519        let ex_iri = IriS::from_str("http://example.org/").unwrap();
520        pm.insert("ex", &ex_iri).unwrap();
521        let ex_rdf = IriS::from_str("http://www.w3.org/1999/02/22-rdf-syntax-ns#").unwrap();
522        pm.insert("rdf", &ex_rdf).unwrap();
523        assert_eq!(
524            pm.to_string(),
525            "prefix ex: <http://example.org/>\nprefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\n"
526        );
527    }
528
529    #[test]
530    fn prefixmap_resolve() {
531        let mut pm = PrefixMap::new();
532        let ex_iri = IriS::from_str("http://example.org/").unwrap();
533        pm.insert("ex", &ex_iri).unwrap();
534        assert_eq!(
535            pm.resolve("ex:pepe").unwrap(),
536            IriS::from_str("http://example.org/pepe").unwrap()
537        );
538    }
539
540    #[test]
541    fn prefixmap_resolve_xsd() {
542        let mut pm = PrefixMap::new();
543        let ex_iri = IriS::from_str("http://www.w3.org/2001/XMLSchema#").unwrap();
544        pm.insert("xsd", &ex_iri).unwrap();
545        assert_eq!(
546            pm.resolve_prefix_local("xsd", "string").unwrap(),
547            IriS::from_str("http://www.w3.org/2001/XMLSchema#string").unwrap()
548        );
549    }
550
551    #[test]
552    fn qualify() {
553        let mut pm = PrefixMap::new();
554        pm.insert("", &IriS::from_str("http://example.org/").unwrap())
555            .unwrap();
556        pm.insert(
557            "shapes",
558            &IriS::from_str("http://example.org/shapes/").unwrap(),
559        )
560        .unwrap();
561        assert_eq!(
562            pm.qualify(&IriS::from_str("http://example.org/alice").unwrap()),
563            ":alice"
564        );
565        assert_eq!(
566            pm.qualify(&IriS::from_str("http://example.org/shapes/User").unwrap()),
567            "shapes:User"
568        );
569    }
570}