hugr_core/hugr/
ident.rs

1use std::borrow::Borrow;
2
3use derive_more::Display;
4use lazy_static::lazy_static;
5use regex::Regex;
6use smol_str::SmolStr;
7use thiserror::Error;
8
9pub static PATH_COMPONENT_REGEX_STR: &str = r"[\w--\d]\w*";
10lazy_static! {
11    pub static ref PATH_REGEX: Regex = Regex::new(&format!(
12        r"^{PATH_COMPONENT_REGEX_STR}(\.{PATH_COMPONENT_REGEX_STR})*$"
13    ))
14    .unwrap();
15}
16
17#[derive(
18    Clone,
19    Debug,
20    Display,
21    PartialEq,
22    Eq,
23    Hash,
24    PartialOrd,
25    Ord,
26    serde::Serialize,
27    serde::Deserialize,
28)]
29
30/// A non-empty dot-separated list of valid identifiers
31pub struct IdentList(SmolStr);
32
33impl IdentList {
34    /// Makes an `IdentList`, checking the supplied string is well-formed
35    pub fn new(n: impl Into<SmolStr>) -> Result<Self, InvalidIdentifier> {
36        let n: SmolStr = n.into();
37        if PATH_REGEX.is_match(n.as_str()) {
38            Ok(IdentList(n))
39        } else {
40            Err(InvalidIdentifier(n))
41        }
42    }
43
44    /// Split off the last component of the path, returning the prefix and suffix.
45    ///
46    /// # Example
47    ///
48    /// ```
49    /// # use hugr_core::hugr::IdentList;
50    /// assert_eq!(
51    ///     IdentList::new("foo.bar.baz").unwrap().split_last(),
52    ///     Some((IdentList::new_unchecked("foo.bar"), "baz".into()))
53    /// );
54    /// assert_eq!(
55    ///    IdentList::new("foo").unwrap().split_last(),
56    ///    None
57    /// );
58    /// ```
59    #[must_use]
60    pub fn split_last(&self) -> Option<(IdentList, SmolStr)> {
61        let (prefix, suffix) = self.0.rsplit_once('.')?;
62        let prefix = Self(SmolStr::new(prefix));
63        let suffix = suffix.into();
64        Some((prefix, suffix))
65    }
66
67    /// Create a new [`IdentList`] *without* doing the well-formedness check.
68    /// This is a backdoor to be used sparingly, as we rely upon callers to
69    /// validate names themselves. In tests, instead the [`crate::const_extension_ids`]
70    /// macro is strongly encouraged as this ensures the name validity check
71    /// is done properly.
72    ///
73    /// Panics if the string is longer than 23 characters.
74    #[must_use]
75    pub const fn new_unchecked(n: &str) -> Self {
76        IdentList(SmolStr::new_inline(n))
77    }
78
79    /// Create a new [`IdentList`] *without* doing the well-formedness check.
80    /// The same caveats apply as for [`Self::new_unchecked`], except that strings
81    /// are not constrained in length.
82    #[must_use]
83    pub const fn new_static_unchecked(n: &'static str) -> Self {
84        IdentList(SmolStr::new_static(n))
85    }
86}
87
88impl Borrow<str> for IdentList {
89    fn borrow(&self) -> &str {
90        self.0.borrow()
91    }
92}
93
94impl std::ops::Deref for IdentList {
95    type Target = str;
96
97    fn deref(&self) -> &str {
98        &self.0
99    }
100}
101
102impl TryInto<IdentList> for &str {
103    type Error = InvalidIdentifier;
104
105    fn try_into(self) -> Result<IdentList, InvalidIdentifier> {
106        IdentList::new(SmolStr::new(self))
107    }
108}
109
110#[derive(Clone, Debug, PartialEq, Eq, Error)]
111#[error("Invalid identifier {0}")]
112/// Error indicating a string was not valid as an [`IdentList`]
113pub struct InvalidIdentifier(SmolStr);
114
115#[cfg(test)]
116mod test {
117
118    mod proptest {
119        use crate::hugr::ident::IdentList;
120        use ::proptest::prelude::*;
121        impl Arbitrary for super::IdentList {
122            type Parameters = ();
123            type Strategy = BoxedStrategy<Self>;
124            fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
125                use crate::proptest::any_ident_string;
126                use proptest::collection::vec;
127                vec(any_ident_string(), 1..2)
128                    .prop_map(|vs| {
129                        IdentList::new(itertools::intersperse(vs, ".".into()).collect::<String>())
130                            .unwrap()
131                    })
132                    .boxed()
133            }
134        }
135        proptest! {
136            #[test]
137            fn arbitrary_identlist_valid((IdentList(ident_list)): IdentList) {
138                assert!(IdentList::new(ident_list).is_ok());
139            }
140        }
141    }
142
143    use super::IdentList;
144
145    #[test]
146    fn test_idents() {
147        IdentList::new("foo").unwrap();
148        IdentList::new("_foo").unwrap();
149        IdentList::new("Bar_xyz67").unwrap();
150        IdentList::new("foo.bar").unwrap();
151        IdentList::new("foo.bar.baz").unwrap();
152
153        IdentList::new("42").unwrap_err();
154        IdentList::new("foo.42").unwrap_err();
155        IdentList::new("xyz-5").unwrap_err();
156        IdentList::new("foo..bar").unwrap_err();
157        IdentList::new(".foo").unwrap_err();
158    }
159}