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