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
29pub struct IdentList(SmolStr);
31
32impl IdentList {
33 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 #[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 #[must_use]
74 pub const fn new_unchecked(n: &str) -> Self {
75 IdentList(SmolStr::new_inline(n))
76 }
77
78 #[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}")]
111pub 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}