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