Skip to main content

prefixmap/
map.rs

1use crate::IriRef;
2use crate::error::PrefixMapError;
3use colored::*;
4use indexmap::IndexMap;
5use iri_s::*;
6use serde::{Deserialize, Serialize};
7use std::fmt::Display;
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, Eq, 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
35/// Methods for [`PrefixMap`] manipulation
36impl PrefixMap {
37    /// Creates an empty map
38    pub fn new() -> PrefixMap {
39        PrefixMap::default()
40    }
41
42    /// Returns the number of prefix associations in the [`PrefixMap`]
43    pub fn len(&self) -> usize {
44        self.map.len()
45    }
46
47    /// Returns `true` if the [`PrefixMap`] is empty
48    pub fn is_empty(&self) -> bool {
49        self.map.is_empty()
50    }
51
52    /// Inserts an alias association to an IRI
53    ///
54    // Returns an [`PrefixMapError`] if the alias already exists.
55    pub fn add_prefix<A, I>(&mut self, alias: A, iri: I) -> Result<(), PrefixMapError>
56    where
57        A: AsRef<str>,
58        I: Into<IriS>,
59    {
60        let key = alias.as_ref();
61        // if self.map.contains_key(key) {
62        //     return Err(PrefixMapError::AliasAlreadyExists {
63        //         prefix: key.to_string(),
64        //         value: self.map.get(key).unwrap().to_string(),
65        //     });
66        // }
67        self.map.insert(key.to_string(), iri.into());
68        Ok(())
69    }
70
71    /// Finds an IRI associated with a given alias
72    pub fn find(&self, str: &str) -> Option<&IriS> {
73        self.map.get(str)
74    }
75
76    /// Creates a [`PrefixMap`] from a [`HashMap`]
77    ///
78    /// Returns an error if any of the IRIs in the [`HashMap`] are invalid.
79    pub fn from_hashmap(hm: HashMap<&str, &str>) -> Result<PrefixMap, PrefixMapError> {
80        let mut pm = PrefixMap::new();
81        for (a, s) in hm.iter() {
82            let iri = IriS::from_str(s)?;
83            pm.add_prefix(a, iri)?;
84        }
85        Ok(pm)
86    }
87
88    /// Merges another [`PrefixMap`] into this one.
89    ///
90    /// Returns an error if any of the aliases in the other [`PrefixMap`] already exist in this one.
91    pub fn merge(&mut self, other: PrefixMap) -> Result<(), PrefixMapError> {
92        for (alias, iri) in other.into_iter() {
93            self.add_prefix(alias, iri)?
94        }
95        Ok(())
96    }
97
98    /// Returns an iterator over the aliases in the [`PrefixMap`]
99    pub fn aliases(&self) -> impl Iterator<Item = &String> {
100        self.map.keys()
101    }
102}
103
104/// Formatting for [`PrefixMap`] outputs
105impl PrefixMap {
106    /// Disable all colors when qualifying IRIs
107    pub fn without_colors(self) -> Self {
108        self.with_qualify_prefix_color(None)
109            .with_qualify_localname_color(None)
110            .with_qualify_semicolon_color(None)
111    }
112
113    /// Use default colors when qualifying IRIs
114    pub fn without_default_colors(mut self) -> Self {
115        self.qualify_localname_color = Some(Color::Black);
116        self.qualify_prefix_color = Some(Color::Blue);
117        self.qualify_semicolon_color = Some(Color::Red);
118        self
119    }
120
121    /// Enable or disable hyperlinking when qualifying IRIs
122    pub fn with_hyperlink(mut self, hyperlink: bool) -> Self {
123        self.hyperlink = hyperlink;
124        self
125    }
126
127    /// Color the alias when qualifying an IRI
128    fn alias_color(&self, alias: &str) -> ColoredString {
129        match self.qualify_prefix_color {
130            Some(color) => alias.color(color),
131            None => ColoredString::from(alias),
132        }
133    }
134
135    /// Color the local name when qualifying an IRI
136    fn local_color(&self, rest: &str) -> ColoredString {
137        match self.qualify_localname_color {
138            Some(color) => rest.color(color),
139            None => ColoredString::from(rest),
140        }
141    }
142
143    /// Color the semicolon when qualifying an IRI
144    fn semicolon_color(&self) -> ColoredString {
145        match self.qualify_semicolon_color {
146            Some(color) => ":".color(color),
147            None => ColoredString::from(":"),
148        }
149    }
150
151    /// Format a qualified IRI with colors
152    fn format_colored(&self, alias: &str, rest: &str) -> String {
153        let prefix_colored = self.alias_color(alias);
154        let rest_colored = self.local_color(rest);
155        let semicolon_colored = self.semicolon_color();
156
157        format!("{prefix_colored}{semicolon_colored}{rest_colored}")
158    }
159
160    /// Change color when qualifying a IRI
161    pub fn with_qualify_prefix_color(mut self, color: Option<Color>) -> Self {
162        self.qualify_prefix_color = color;
163        self
164    }
165
166    /// Change color of localname when qualifying a IRI
167    pub fn with_qualify_localname_color(mut self, color: Option<Color>) -> Self {
168        self.qualify_localname_color = color;
169        self
170    }
171
172    /// Change color of semicolon when qualifying a IRI
173    pub fn with_qualify_semicolon_color(mut self, color: Option<Color>) -> Self {
174        self.qualify_semicolon_color = color;
175        self
176    }
177
178    /// Disable all rich qualifying (colors and hyperlinks)
179    pub fn without_rich_qualifying(self) -> Self {
180        self.with_hyperlink(false).without_colors()
181    }
182}
183
184/// Common predefined prefix maps
185impl PrefixMap {
186    /// Basic prefixmap with common definitions.
187    /// This includes:
188    /// - `default`
189    /// - `dc`
190    /// - `rdf`
191    /// - `rdfs`
192    /// - `sh`
193    /// - `xsd`
194    pub fn basic() -> PrefixMap {
195        PrefixMap::from_hashmap(HashMap::from([
196            ("", "https://example.org/"),
197            ("dc", "https://purl.org/dc/elements/1.1/"),
198            ("rdf", "https://www.w3.org/1999/02/22-rdf-syntax-ns#"),
199            ("rdfs", "https://www.w3.org/2000/01/rdf-schema#"),
200            ("sh", "https://www.w3.org/ns/shacl#"),
201            ("xsd", "https://www.w3.org/2001/XMLSchema#"),
202        ]))
203        .unwrap()
204    }
205
206    /// Default Wikidata prefix map
207    /// This source of this list is <https://www.mediawiki.org/wiki/Wikibase/Indexing/RDF_Dump_Format#Full_list_of_prefixes>
208    pub fn wikidata() -> PrefixMap {
209        PrefixMap::from_hashmap(HashMap::from([
210            ("bd", "https://www.bigdata.com/rdf#"),
211            ("cc", "https://creativecommons.org/ns#"),
212            ("dct", "https://purl.org/dc/terms/"),
213            ("geo", "https://www.opengis.net/ont/geosparql#"),
214            ("hint", "https://www.bigdata.com/queryHints#"),
215            ("ontolex", "https://www.w3.org/ns/lemon/ontolex#"),
216            ("owl", "https://www.w3.org/2002/07/owl#"),
217            ("prov", "https://www.w3.org/ns/prov#"),
218            ("rdf", "https://www.w3.org/1999/02/22-rdf-syntax-ns#"),
219            ("rdfs", "https://www.w3.org/2000/01/rdf-schema#"),
220            ("schema", "https://schema.org/"),
221            ("skos", "https://www.w3.org/2004/02/skos/core#"),
222            ("xsd", "https://www.w3.org/2001/XMLSchema#"),
223            ("p", "https://www.wikidata.org/prop/"),
224            ("pq", "https://www.wikidata.org/prop/qualifier/"),
225            ("pqn", "https://www.wikidata.org/prop/qualifier/value-normalized/"),
226            ("pqv", "https://www.wikidata.org/prop/qualifier/value/"),
227            ("pr", "https://www.wikidata.org/prop/reference/"),
228            ("prn", "https://www.wikidata.org/prop/reference/value-normalized/"),
229            ("prv", "https://www.wikidata.org/prop/reference/value/"),
230            ("psv", "https://www.wikidata.org/prop/statement/value/"),
231            ("ps", "https://www.wikidata.org/prop/statement/"),
232            ("psn", "https://www.wikidata.org/prop/statement/value-normalized/"),
233            ("wd", "https://www.wikidata.org/entity/"),
234            ("wdata", "https://www.wikidata.org/wiki/Special:EntityData/"),
235            ("wdno", "https://www.wikidata.org/prop/novalue/"),
236            ("wdref", "https://www.wikidata.org/reference/"),
237            ("wds", "https://www.wikidata.org/entity/statement/"),
238            ("wdt", "https://www.wikidata.org/prop/direct/"),
239            ("wdtn", "https://www.wikidata.org/prop/direct-normalized/"),
240            ("wdv", "https://www.wikidata.org/value/"),
241            ("wikibase", "https://wikiba.se/ontology#"),
242        ]))
243        .unwrap()
244        .without_default_colors()
245        .with_hyperlink(true)
246    }
247}
248
249/// Qualifying IRIs against a [`PrefixMap`]
250impl PrefixMap {
251    /// Qualifies an IRI against a [`PrefixMap`]
252    ///
253    /// If it can't qualify the IRI, it returns the iri between `<` and `>`
254    /// ```
255    /// # use std::collections::HashMap;
256    /// # use prefixmap::PrefixMap;
257    /// # use prefixmap::error::PrefixMapError;
258    /// # use iri_s::*;
259    /// # use std::str::FromStr;
260    /// let pm = PrefixMap::from_hashmap(
261    ///   HashMap::from([
262    ///     ("", "https://example.org/"),
263    ///     ("schema", "https://schema.org/")])
264    /// )?;
265    /// let a = IriS::from_str("https://example.org/a")?;
266    /// assert_eq!(pm.qualify(&a), ":a");
267    ///
268    /// let knows = IriS::from_str("https://schema.org/knows")?;
269    /// assert_eq!(pm.qualify(&knows), "schema:knows");
270    ///
271    /// let other = IriS::from_str("https://other.org/foo")?;
272    /// assert_eq!(pm.qualify(&other), "<https://other.org/foo>");
273    /// # Ok::<(), PrefixMapError>(())
274    /// ```
275    pub fn qualify(&self, iri: &IriS) -> String {
276        self.qualify_optional(iri).unwrap_or_else(|| format!("<{iri}>"))
277    }
278
279    /// Qualifies an IRI against a [`PrefixMap`]
280    ///
281    /// If it can't qualify the IRI, returns [`None`]
282    ///
283    /// ```
284    /// # use std::collections::HashMap;
285    /// # use prefixmap::PrefixMap;
286    /// # use prefixmap::error::PrefixMapError;
287    /// # use iri_s::*;
288    /// # use std::str::FromStr;
289    /// let pm = PrefixMap::from_hashmap(
290    ///   HashMap::from([
291    ///     ("", "https://example.org/"),
292    ///     ("schema", "https://schema.org/")])
293    /// )?;
294    /// let a = IriS::from_str("https://example.org/a")?;
295    /// assert_eq!(pm.qualify_optional(&a), Some(":a".to_string()));
296    ///
297    /// let knows = IriS::from_str("https://schema.org/knows")?;
298    /// assert_eq!(pm.qualify_optional(&knows), Some("schema:knows".to_string()));
299    ///
300    /// let other = IriS::from_str("https://other.org/foo")?;
301    /// assert_eq!(pm.qualify_optional(&other), None);
302    /// # Ok::<(), PrefixMapError>(())
303    /// ```
304    pub fn qualify_optional(&self, iri: &IriS) -> Option<String> {
305        let (alias, rest) = self.longest_prefix_match(iri)?;
306        let s = self.format_colored(alias, rest);
307
308        if self.hyperlink {
309            Some(format!("\u{1b}]8;;{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\", iri.as_str(), s))
310        } else {
311            Some(s)
312        }
313    }
314
315    /// Qualifies an IRI against a [`PrefixMap`], returning the length of the qualified string
316    ///
317    /// ```
318    /// # use std::collections::HashMap;
319    /// # use prefixmap::PrefixMap;
320    /// # use prefixmap::error::PrefixMapError;
321    /// # use iri_s::*;
322    /// # use std::str::FromStr;
323    /// let pm = PrefixMap::from_hashmap(
324    ///   HashMap::from([
325    ///     ("", "https://example.org/"),
326    ///     ("schema", "https://schema.org/")])
327    /// )?;
328    /// let a = IriS::from_str("https://example.org/a")?;
329    /// assert_eq!(pm.qualify_and_length(&a), (":a".to_string(), 2));
330    ///
331    /// let knows = IriS::from_str("https://schema.org/knows")?;
332    /// assert_eq!(pm.qualify_and_length(&knows), ("schema:knows".to_string(),12));
333    ///
334    /// let other = IriS::from_str("https://other.org/foo")?;
335    /// assert_eq!(pm.qualify_and_length(&other), ("<https://other.org/foo>".to_string(), 23));
336    /// # Ok::<(), PrefixMapError>(())
337    /// ```
338    pub fn qualify_and_length(&self, iri: &IriS) -> (String, usize) {
339        let (s, length) = if let Some((alias, rest)) = self.longest_prefix_match(iri) {
340            let s = self.format_colored(alias, rest);
341            let length = alias.len() + 1 + rest.len();
342            (s, length)
343        } else {
344            let s = format!("<{iri}>");
345            let length = iri.as_str().len() + 2;
346            (s, length)
347        };
348
349        if self.hyperlink {
350            let s_hyperlink = format!("\u{1b}]8;;{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\", iri.as_str(), s);
351            (s_hyperlink, length)
352        } else {
353            (s, length)
354        }
355    }
356
357    /// Qualify an IRI against a [`PrefixMap`] and obtains the local name.
358    ///
359    /// Returns [`None`] if it can't qualify the IRI.
360    ///
361    /// ```
362    /// # use std::collections::HashMap;
363    /// # use prefixmap::PrefixMap;
364    /// # use prefixmap::error::PrefixMapError;
365    /// # use iri_s::*;
366    /// # use std::str::FromStr;
367    /// let pm = PrefixMap::from_hashmap(
368    ///   HashMap::from([
369    ///     ("", "https://example.org/"),
370    ///     ("schema", "https://schema.org/")])
371    /// )?;
372    /// let a = IriS::from_str("https://example.org/a")?;
373    /// assert_eq!(pm.qualify_local(&a), Some("a".to_string()));
374    ///
375    /// let knows = IriS::from_str("https://schema.org/knows")?;
376    /// assert_eq!(pm.qualify_local(&knows), Some("knows".to_string()));
377    ///
378    /// let other = IriS::from_str("https://other.org/foo")?;
379    /// assert_eq!(pm.qualify_local(&other), None);
380    /// # Ok::<(), PrefixMapError>(())
381    /// ```
382    pub fn qualify_local(&self, iri: &IriS) -> Option<String> {
383        self.longest_prefix_match(iri).map(|(_, rest)| rest.to_string())
384    }
385
386    /// Finds the longest prefix match for a given IRI in the [`PrefixMap`]
387    fn longest_prefix_match<'a>(&'a self, iri: &'a IriS) -> Option<(&'a str, &'a str)> {
388        self.map
389            .iter()
390            .filter_map(|(alias, pm_iri)| {
391                iri.as_str()
392                    .strip_prefix(pm_iri.as_str())
393                    .map(|rest| (alias.as_str(), rest))
394            })
395            .max_by_key(|(_, rest)| iri.as_str().len() - rest.len())
396    }
397}
398
399/// Resolving strings and IRI references against a [`PrefixMap`]
400impl PrefixMap {
401    /// Resolves a string against a prefix map
402    ///
403    /// Returns an error if the prefix is not found in the prefix map or if the `string` is not a valid IRI.
404    ///
405    /// Example:
406    /// Given a string like "ex:a" and a prefixmap that has alias "ex" with value "https://example.org/", the result will be "https://example.org/a"
407    /// ```
408    /// # use std::collections::HashMap;
409    /// # use prefixmap::PrefixMap;
410    /// # use prefixmap::error::PrefixMapError;
411    /// # use iri_s::*;
412    /// # use std::str::FromStr;
413    ///
414    /// let pm: PrefixMap = PrefixMap::from_hashmap(
415    ///   HashMap::from([
416    ///     ("", "https://example.org/"),
417    ///     ("schema", "https://schema.org/")])
418    /// )?;
419    ///
420    /// let a = pm.resolve(":a")?;
421    /// let a_resolved = IriS::from_str("https://example.org/a")?;
422    /// assert_eq!(a, a_resolved);
423    ///
424    /// let knows = pm.resolve("schema:knows")?;
425    /// let knows_resolved = IriS::from_str("https://schema.org/knows")?;
426    /// assert_eq!(knows, knows_resolved);
427    /// # Ok::<(), PrefixMapError>(())
428    /// ```
429    pub fn resolve(&self, str: &str) -> Result<IriS, PrefixMapError> {
430        match str.rsplit_once(':') {
431            Some((prefix, local)) => Ok(self.resolve_prefix_local(prefix, local)?),
432            None => Ok(IriS::from_str(str)?),
433        }
434    }
435
436    /// Resolves an [`IriRef`] against a [`PrefixMap`]
437    pub fn resolve_iriref(&self, iri_ref: IriRef) -> Result<IriS, PrefixMapError> {
438        match iri_ref {
439            IriRef::Prefixed { prefix, local } => Ok(self.resolve_prefix_local(prefix, local)?),
440            IriRef::Iri(iri) => Ok(iri),
441        }
442    }
443
444    /// Resolves a prefixed alias and a local name in a prefix map to obtain the full IRI
445    ///
446    /// Returns an error if:
447    /// - the prefix is not found in the prefix map.
448    /// - the resulting IRI is invalid.
449    ///
450    /// ```
451    /// # use std::collections::HashMap;
452    /// # use prefixmap::PrefixMap;
453    /// # use prefixmap::error::PrefixMapError;
454    /// # use iri_s::*;
455    /// # use std::str::FromStr;
456    ///
457    /// let pm = PrefixMap::from_hashmap(
458    ///   HashMap::from([
459    ///     ("", "https://example.org/"),
460    ///     ("schema", "https://schema.org/"),
461    ///     ("xsd", "https://www.w3.org/2001/XMLSchema#")
462    /// ]))?;
463    ///
464    /// let a = pm.resolve_prefix_local("", "a")?;
465    /// let a_resolved = IriS::from_str("https://example.org/a")?;
466    /// assert_eq!(a, a_resolved);
467    ///
468    /// let knows = pm.resolve_prefix_local("schema","knows")?;
469    /// let knows_resolved = IriS::from_str("https://schema.org/knows")?;
470    /// assert_eq!(knows, knows_resolved);
471    ///
472    /// let xsd_string = pm.resolve_prefix_local("xsd","string")?;
473    /// let xsd_string_resolved = IriS::from_str("https://www.w3.org/2001/XMLSchema#string")?;
474    /// assert_eq!(xsd_string, xsd_string_resolved);
475    /// # Ok::<(), PrefixMapError>(())
476    /// ```
477    pub fn resolve_prefix_local<S: Into<String>>(&self, prefix: S, local: S) -> Result<IriS, PrefixMapError> {
478        let prefix = prefix.into();
479        let local = local.into();
480
481        match self.find(prefix.as_str()) {
482            Some(iri) => {
483                if local.is_empty() {
484                    return Ok(iri.clone());
485                }
486                let new_iri = iri.extend(local.as_str())?;
487                Ok(new_iri)
488            },
489            None => Err(PrefixMapError::PrefixNotFound {
490                prefix,
491                prefixmap: self.clone(),
492            }),
493        }
494    }
495}
496
497impl Display for PrefixMap {
498    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499        for (alias, iri) in self.map.iter() {
500            writeln!(f, "prefix {}: <{}>", &alias, &iri)?
501        }
502        Ok(())
503    }
504}
505
506impl Iterator for PrefixMap {
507    type Item = (String, IriS);
508
509    fn next(&mut self) -> Option<Self::Item> {
510        match self.map.is_empty() {
511            true => None,
512            false => {
513                let (k, v) = self.map.shift_remove_index(0).unwrap();
514                Some((k, v))
515            },
516        }
517    }
518}