Skip to main content

knowledge_base_models/
identifiers.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5#[derive(Clone, Debug, Eq, PartialEq)]
6pub struct IdentifierParseError {
7    value: String,
8    prefix: &'static str,
9}
10
11impl fmt::Display for IdentifierParseError {
12    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
13        write!(
14            formatter,
15            "invalid identifier {:?}; expected canonical {}<positive integer> syntax",
16            self.value, self.prefix
17        )
18    }
19}
20
21impl std::error::Error for IdentifierParseError {}
22
23macro_rules! identifier {
24    ($name:ident, $prefix:literal) => {
25        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
26        #[serde(transparent)]
27        pub struct $name(String);
28
29        impl $name {
30            pub fn as_str(&self) -> &str {
31                &self.0
32            }
33
34            pub fn number(&self) -> u64 {
35                self.0[1..].parse().expect("validated identifiers contain a u64")
36            }
37        }
38
39        impl fmt::Display for $name {
40            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41                formatter.write_str(&self.0)
42            }
43        }
44
45        impl FromStr for $name {
46            type Err = IdentifierParseError;
47
48            fn from_str(value: &str) -> Result<Self, Self::Err> {
49                let digits = value.strip_prefix($prefix).ok_or_else(|| IdentifierParseError {
50                    value: value.to_owned(),
51                    prefix: $prefix,
52                })?;
53                let canonical = !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()) && !digits.starts_with('0') && digits.parse::<u64>().is_ok();
54                if !canonical {
55                    return Err(IdentifierParseError {
56                        value: value.to_owned(),
57                        prefix: $prefix,
58                    });
59                }
60                Ok(Self(value.to_owned()))
61            }
62        }
63
64        impl<'de> Deserialize<'de> for $name {
65            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66            where
67                D: Deserializer<'de>,
68            {
69                String::deserialize(deserializer)?.parse().map_err(serde::de::Error::custom)
70            }
71        }
72    };
73}
74
75identifier!(EntityId, "Q");
76identifier!(PropertyId, "P");
77identifier!(ReferenceId, "R");
78identifier!(StatementId, "S");
79identifier!(EntityTypeId, "T");
80
81#[cfg(test)]
82mod tests {
83    use super::{EntityId, EntityTypeId, PropertyId, ReferenceId, StatementId};
84    use serde::de::DeserializeOwned;
85
86    fn parses<T: DeserializeOwned>(value: &str) -> bool {
87        serde_yaml::from_str::<T>(value).is_ok()
88    }
89
90    #[test]
91    fn typed_identifiers_accept_their_canonical_forms() {
92        assert!(parses::<EntityId>("Q1"));
93        assert!(parses::<PropertyId>("P2"));
94        assert!(parses::<ReferenceId>("R3"));
95        assert!(parses::<StatementId>("S4"));
96        assert!(parses::<EntityTypeId>("T5"));
97    }
98
99    #[test]
100    fn typed_identifiers_reject_noncanonical_forms() {
101        for value in ["Q0", "Q01", "Q-1", "Q", "P1", "q1", "1"] {
102            assert!(!parses::<EntityId>(value), "{value} unexpectedly parsed as an entity identifier");
103        }
104    }
105
106    #[test]
107    fn typed_identifiers_parse_from_strings() {
108        assert_eq!("Q42".parse::<EntityId>().expect("valid identifier").as_str(), "Q42");
109
110        for value in ["Q0", "Q01", "P1", "../Q1", "Q1.yaml"] {
111            assert!(value.parse::<EntityId>().is_err(), "{value} unexpectedly parsed as an entity identifier");
112        }
113    }
114}