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