hugr_core/hugr/
ident.rs

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