nextest_runner/config/core/
identifier.rs1use crate::{
5 config::utils::{InvalidIdentifierKind, is_valid_identifier_unicode},
6 errors::{InvalidIdentifier, InvalidToolName},
7};
8use smol_str::SmolStr;
9use std::fmt;
10use unicode_normalization::{IsNormalized, UnicodeNormalization, is_nfc_quick};
11
12#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
20#[serde(transparent)]
21pub struct ConfigIdentifier(SmolStr);
22
23impl ConfigIdentifier {
24 pub fn new(identifier: SmolStr) -> Result<Self, InvalidIdentifier> {
26 let identifier = if is_nfc_quick(identifier.chars()) == IsNormalized::Yes {
27 identifier
28 } else {
29 identifier.nfc().collect::<SmolStr>()
30 };
31
32 if identifier.is_empty() {
33 return Err(InvalidIdentifier::Empty);
34 }
35
36 if let Some(suffix) = identifier.strip_prefix("@tool:") {
39 let mut parts = suffix.splitn(2, ':');
40 let tool_name = parts
41 .next()
42 .expect("at least one identifier should be returned.");
43 let tool_identifier = match parts.next() {
44 Some(tool_identifier) => tool_identifier,
45 None => return Err(InvalidIdentifier::ToolIdentifierInvalidFormat(identifier)),
46 };
47
48 for x in [tool_name, tool_identifier] {
49 is_valid_identifier_unicode(x).map_err(|error| match error {
50 InvalidIdentifierKind::Empty => {
51 InvalidIdentifier::ToolComponentEmpty(identifier.clone())
52 }
53 InvalidIdentifierKind::InvalidXid => {
54 InvalidIdentifier::ToolIdentifierInvalidXid(identifier.clone())
55 }
56 })?;
57 }
58 } else {
59 is_valid_identifier_unicode(&identifier).map_err(|error| match error {
61 InvalidIdentifierKind::Empty => InvalidIdentifier::Empty,
62 InvalidIdentifierKind::InvalidXid => {
63 InvalidIdentifier::InvalidXid(identifier.clone())
64 }
65 })?;
66 }
67
68 Ok(Self(identifier))
69 }
70
71 pub fn is_tool_identifier(&self) -> bool {
73 self.0.starts_with("@tool:")
74 }
75
76 pub fn tool_components(&self) -> Option<(&str, &str)> {
78 self.0.strip_prefix("@tool:").map(|suffix| {
79 let mut parts = suffix.splitn(2, ':');
80 let tool_name = parts
81 .next()
82 .expect("identifier was checked to have 2 components above");
83 let tool_identifier = parts
84 .next()
85 .expect("identifier was checked to have 2 components above");
86 (tool_name, tool_identifier)
87 })
88 }
89
90 #[inline]
92 pub fn as_str(&self) -> &str {
93 &self.0
94 }
95}
96
97impl fmt::Display for ConfigIdentifier {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 write!(f, "{}", self.0)
100 }
101}
102
103impl<'de> serde::Deserialize<'de> for ConfigIdentifier {
104 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105 where
106 D: serde::Deserializer<'de>,
107 {
108 let identifier = SmolStr::deserialize(deserializer)?;
109 ConfigIdentifier::new(identifier).map_err(serde::de::Error::custom)
110 }
111}
112
113#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
122pub struct ToolName(SmolStr);
123
124impl ToolName {
125 pub fn new(name: SmolStr) -> Result<Self, InvalidToolName> {
127 let name = if is_nfc_quick(name.chars()) == IsNormalized::Yes {
128 name
129 } else {
130 name.nfc().collect::<SmolStr>()
131 };
132
133 if name.is_empty() {
134 return Err(InvalidToolName::Empty);
135 }
136
137 if name.starts_with("@tool") {
140 return Err(InvalidToolName::StartsWithToolPrefix(name));
141 }
142
143 is_valid_identifier_unicode(&name).map_err(|error| match error {
145 InvalidIdentifierKind::Empty => InvalidToolName::Empty,
146 InvalidIdentifierKind::InvalidXid => InvalidToolName::InvalidXid(name.clone()),
147 })?;
148
149 Ok(Self(name))
150 }
151
152 #[inline]
154 pub fn as_str(&self) -> &str {
155 &self.0
156 }
157}
158
159impl fmt::Display for ToolName {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 self.0.fmt(f)
162 }
163}
164
165impl<'de> serde::Deserialize<'de> for ToolName {
166 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167 where
168 D: serde::Deserializer<'de>,
169 {
170 let name = SmolStr::deserialize(deserializer)?;
171 ToolName::new(name).map_err(serde::de::Error::custom)
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use serde::Deserialize;
179
180 #[derive(Deserialize, Debug, PartialEq, Eq)]
181 struct TestDeserialize {
182 identifier: ConfigIdentifier,
183 }
184
185 fn make_json(identifier: &str) -> String {
186 format!(r#"{{ "identifier": "{identifier}" }}"#)
187 }
188
189 #[test]
190 fn test_valid() {
191 let valid_inputs = ["foo", "foo-bar", "Δabc"];
192
193 for &input in &valid_inputs {
194 let identifier = ConfigIdentifier::new(input.into()).unwrap();
195 assert_eq!(identifier.as_str(), input);
196 assert!(!identifier.is_tool_identifier());
197
198 serde_json::from_str::<TestDeserialize>(&make_json(input)).unwrap();
199 }
200
201 let valid_tool_inputs = ["@tool:foo:bar", "@tool:Δabc_def-ghi:foo-bar"];
202
203 for &input in &valid_tool_inputs {
204 let identifier = ConfigIdentifier::new(input.into()).unwrap();
205 assert_eq!(identifier.as_str(), input);
206 assert!(identifier.is_tool_identifier());
207
208 serde_json::from_str::<TestDeserialize>(&make_json(input)).unwrap();
209 }
210 }
211
212 #[test]
213 fn test_invalid() {
214 let identifier = ConfigIdentifier::new("".into());
215 assert_eq!(identifier.unwrap_err(), InvalidIdentifier::Empty);
216
217 let invalid_xid = ["foo bar", "_", "-foo", "_foo", "@foo", "@tool"];
218
219 for &input in &invalid_xid {
220 let identifier = ConfigIdentifier::new(input.into());
221 assert_eq!(
222 identifier.unwrap_err(),
223 InvalidIdentifier::InvalidXid(input.into())
224 );
225
226 serde_json::from_str::<TestDeserialize>(&make_json(input)).unwrap_err();
227 }
228
229 let tool_component_empty = ["@tool::", "@tool:foo:", "@tool::foo"];
230
231 for &input in &tool_component_empty {
232 let identifier = ConfigIdentifier::new(input.into());
233 assert_eq!(
234 identifier.unwrap_err(),
235 InvalidIdentifier::ToolComponentEmpty(input.into())
236 );
237
238 serde_json::from_str::<TestDeserialize>(&make_json(input)).unwrap_err();
239 }
240
241 let tool_identifier_invalid_format = ["@tool:", "@tool:foo"];
242
243 for &input in &tool_identifier_invalid_format {
244 let identifier = ConfigIdentifier::new(input.into());
245 assert_eq!(
246 identifier.unwrap_err(),
247 InvalidIdentifier::ToolIdentifierInvalidFormat(input.into())
248 );
249
250 serde_json::from_str::<TestDeserialize>(&make_json(input)).unwrap_err();
251 }
252
253 let tool_identifier_invalid_xid = ["@tool:_foo:bar", "@tool:foo:#bar", "@tool:foo:bar:baz"];
254
255 for &input in &tool_identifier_invalid_xid {
256 let identifier = ConfigIdentifier::new(input.into());
257 assert_eq!(
258 identifier.unwrap_err(),
259 InvalidIdentifier::ToolIdentifierInvalidXid(input.into())
260 );
261
262 serde_json::from_str::<TestDeserialize>(&make_json(input)).unwrap_err();
263 }
264 }
265
266 #[derive(Deserialize, Debug, PartialEq, Eq)]
267 struct TestToolNameDeserialize {
268 tool_name: ToolName,
269 }
270
271 fn make_tool_name_json(name: &str) -> String {
272 format!(r#"{{ "tool_name": "{name}" }}"#)
273 }
274
275 #[test]
276 fn test_tool_name_valid() {
277 let valid_inputs = ["foo", "foo-bar", "Δabc", "my-tool", "myTool123"];
278
279 for &input in &valid_inputs {
280 let tool_name = ToolName::new(input.into()).unwrap();
281 assert_eq!(tool_name.as_str(), input);
282
283 serde_json::from_str::<TestToolNameDeserialize>(&make_tool_name_json(input)).unwrap();
284 }
285 }
286
287 #[test]
288 fn test_tool_name_invalid() {
289 let tool_name = ToolName::new("".into());
290 assert_eq!(tool_name.unwrap_err(), InvalidToolName::Empty);
291
292 let invalid_xid = ["foo bar", "_", "-foo", "_foo", "@foo"];
293
294 for &input in &invalid_xid {
295 let tool_name = ToolName::new(input.into());
296 assert_eq!(
297 tool_name.unwrap_err(),
298 InvalidToolName::InvalidXid(input.into())
299 );
300
301 serde_json::from_str::<TestToolNameDeserialize>(&make_tool_name_json(input))
302 .unwrap_err();
303 }
304
305 let starts_with_tool_prefix = ["@tool", "@tool:foo:bar", "@tool:test:id", "@toolname"];
307
308 for &input in &starts_with_tool_prefix {
309 let tool_name = ToolName::new(input.into());
310 assert_eq!(
311 tool_name.unwrap_err(),
312 InvalidToolName::StartsWithToolPrefix(input.into()),
313 "for input {input:?}"
314 );
315
316 serde_json::from_str::<TestToolNameDeserialize>(&make_tool_name_json(input))
317 .unwrap_err();
318 }
319 }
320}