wikidata/
ids.rs

1//! Various ID types used by Wikidata.
2
3use serde::{Deserialize, Serialize};
4use std::{fmt, num::ParseIntError, str::FromStr};
5
6pub mod consts;
7
8/// Three main types of IDs entities can have.
9///
10/// EntitySchemas (with E IDs) are currently unsupported.
11#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[non_exhaustive]
13pub enum WikiId {
14    /// A Qid, representing an entity.
15    EntityId(Qid),
16    /// A Pid, representing a property.
17    PropertyId(Pid),
18    /// An Lid, representing a lexeme.
19    LexemeId(Lid),
20}
21
22impl FromStr for WikiId {
23    type Err = IdParseError;
24
25    /// Parse the identifier from a string.
26    fn from_str(x: &str) -> Result<Self, Self::Err> {
27        match x.chars().next() {
28            Some('Q') => Qid::from_str(x).map(WikiId::EntityId),
29            Some('P') => Pid::from_str(x).map(WikiId::PropertyId),
30            Some('L') => Lid::from_str(x).map(WikiId::LexemeId),
31            _ => Err(IdParseError::InvalidPrefix),
32        }
33    }
34}
35
36/// An error parsing an ID.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum IdParseError {
39    /// The number couldn't be parsed.
40    UnparseableNumber(ParseIntError),
41    /// The ID had an invalid prefix letter.
42    InvalidPrefix,
43    /// The ID had too many parts seperated by dashes
44    ///
45    /// ## Example
46    /// ```
47    /// use std::str::FromStr;
48    /// use wikidata::{Fid, IdParseError};
49    /// assert_eq!(Fid::from_str("L3-F2-S6"), Err(IdParseError::TooManyParts));
50    /// ```
51    TooManyParts,
52    /// The ID had too few parts seperated by dashes
53    ///
54    /// ## Example
55    /// ```
56    /// use std::str::FromStr;
57    /// use wikidata::{Fid, IdParseError};
58    /// assert_eq!(Fid::from_str("L3"), Err(IdParseError::TooFewParts));
59    /// ```
60    TooFewParts,
61}
62
63macro_rules! id_def {
64    ($name:ident, $full_name:expr, $letter:expr, $khar:expr) => {
65        #[derive(
66            Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
67        )]
68        #[doc = "A Wikidata"]
69        #[doc = $full_name]
70        pub struct $name(pub u64);
71
72        impl $name {
73            /// Get the URL to access data about the claim on Wikidata.
74            #[must_use]
75            pub fn json_url(&self) -> String {
76                format!(
77                    concat!(
78                        "https://www.wikidata.org/wiki/Special:EntityData/",
79                        $letter,
80                        "{}.json"
81                    ),
82                    self.0
83                )
84            }
85        }
86        impl FromStr for $name {
87            type Err = IdParseError;
88
89            /// Parse the identifier from a string.
90            fn from_str(x: &str) -> Result<Self, Self::Err> {
91                if x.chars().next() != Some($khar) {
92                    return Err(IdParseError::InvalidPrefix);
93                }
94                let num_str = &x[1..];
95                match num_str.parse() {
96                    Ok(num) => Ok(Self(num)),
97                    Err(e) => Err(IdParseError::UnparseableNumber(e)),
98                }
99            }
100        }
101        impl fmt::Display for $name {
102            /// Display the ID as it would be in a URI.
103            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104                write!(f, concat!($letter, "{}"), self.0)
105            }
106        }
107    };
108}
109
110id_def!(Qid, "entity ID", "Q", 'Q');
111id_def!(Pid, "property ID", "P", 'P');
112id_def!(Lid, "lexeme ID", "L", 'L');
113
114macro_rules! lexeme_subid_def {
115    ($name:ident, $full_name:expr, $letter:expr, $khar:expr) => {
116        /// A lexeme ID and associated
117        #[doc = $full_name]
118        #[derive(
119            Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
120        )]
121        pub struct $name(pub Lid, pub u16);
122
123        impl fmt::Display for $name {
124            /// Display the ID as it would be in a URI.
125            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126                write!(f, "{}-{}{}", self.0, $khar, self.1)
127            }
128        }
129
130        impl FromStr for $name {
131            type Err = IdParseError;
132
133            /// Parse the identifier from a string.
134            fn from_str(x: &str) -> Result<Self, Self::Err> {
135                if x.chars().next() != Some('L') {
136                    return Err(IdParseError::InvalidPrefix);
137                }
138                let mut parts = x[1..].split('-');
139                let lid = parts
140                    .next()
141                    .ok_or(IdParseError::TooFewParts)?
142                    .parse()
143                    .map_err(IdParseError::UnparseableNumber)?;
144                let part2 = parts.next().ok_or(IdParseError::TooFewParts)?;
145                if part2.chars().next() != Some($khar) {
146                    return Err(IdParseError::InvalidPrefix);
147                }
148                let id = part2[1..]
149                    .parse()
150                    .map_err(IdParseError::UnparseableNumber)?;
151                if parts.next().is_some() {
152                    Err(IdParseError::TooManyParts)
153                } else {
154                    Ok(Self(Lid(lid), id))
155                }
156            }
157        }
158    };
159}
160
161lexeme_subid_def!(Fid, "form ID", "F", 'F');
162lexeme_subid_def!(Sid, "sense ID", "S", 'S');
163
164#[cfg(test)]
165pub mod test {
166    use super::*;
167
168    #[test]
169    fn json_url() {
170        assert_eq!(
171            Qid(42).json_url(),
172            "https://www.wikidata.org/wiki/Special:EntityData/Q42.json"
173        );
174        assert_eq!(
175            Pid(31).json_url(),
176            "https://www.wikidata.org/wiki/Special:EntityData/P31.json"
177        );
178        assert_eq!(
179            Lid(1).json_url(),
180            "https://www.wikidata.org/wiki/Special:EntityData/L1.json"
181        )
182    }
183
184    #[test]
185    fn to_string() {
186        let entity = Qid(42);
187        assert_eq!(format!("{}", entity), "Q42");
188
189        let prop = Pid(6);
190        assert_eq!(format!("{}", prop), "P6");
191
192        let lexeme = Lid(2);
193        assert_eq!(format!("{}", lexeme), "L2");
194
195        let sense = Sid(Lid(5), 9);
196        assert_eq!(format!("{}", sense), "L5-S9");
197
198        let form = Fid(Lid(3), 11);
199        assert_eq!(format!("{}", form), "L3-F11");
200    }
201
202    #[test]
203    fn from_str() {
204        assert_eq!(Qid::from_str("Q42").unwrap(), Qid(42));
205        assert_eq!(Lid::from_str("L944114").unwrap(), Lid(944114));
206        assert_eq!(Pid::from_str("P1341").unwrap(), Pid(1341));
207        assert_eq!(Pid::from_str("Q1341"), Err(IdParseError::InvalidPrefix));
208        assert_eq!(Pid::from_str("1341"), Err(IdParseError::InvalidPrefix));
209        assert!(Qid::from_str("Q").is_err());
210        assert_eq!(Sid::from_str("S1341"), Err(IdParseError::InvalidPrefix));
211        assert_eq!(Sid::from_str("L1341"), Err(IdParseError::TooFewParts));
212        assert_eq!(
213            Sid::from_str("L1341-A123"),
214            Err(IdParseError::InvalidPrefix)
215        );
216        assert_eq!(Sid::from_str("L1341-S123").unwrap(), Sid(Lid(1341), 123));
217        assert!(Lid::from_str("L1341-S123").is_err());
218        assert!(Lid::from_str("L1341-F123").is_err());
219        assert!(WikiId::from_str("L1341-F123").is_err());
220        assert!(WikiId::from_str("L1341-S123").is_err());
221        assert_eq!(WikiId::from_str("A123"), Err(IdParseError::InvalidPrefix));
222        assert_eq!(
223            WikiId::from_str("L1341").unwrap(),
224            WikiId::LexemeId(Lid(1341))
225        );
226        assert_eq!(
227            WikiId::from_str("Q1341").unwrap(),
228            WikiId::EntityId(Qid(1341))
229        );
230        assert_eq!(
231            WikiId::from_str("P1341").unwrap(),
232            WikiId::PropertyId(Pid(1341))
233        );
234    }
235
236    #[test]
237    fn unit_suffix() {
238        assert_eq!(consts::unit_suffix(consts::METRE).unwrap(), " m");
239        assert_eq!(consts::unit_suffix(consts::DEGREE).unwrap(), "°");
240    }
241}