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}