nextest_runner/config/
identifier.rs1use crate::errors::InvalidIdentifier;
5use smol_str::SmolStr;
6use std::fmt;
7use unicode_normalization::{IsNormalized, UnicodeNormalization, is_nfc_quick};
8
9#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct ConfigIdentifier(SmolStr);
18
19impl ConfigIdentifier {
20 pub fn new(identifier: SmolStr) -> Result<Self, InvalidIdentifier> {
22 let identifier = if is_nfc_quick(identifier.chars()) == IsNormalized::Yes {
23 identifier
24 } else {
25 identifier.nfc().collect::<SmolStr>()
26 };
27
28 if identifier.is_empty() {
29 return Err(InvalidIdentifier::Empty);
30 }
31
32 if let Some(suffix) = identifier.strip_prefix("@tool:") {
35 let mut parts = suffix.splitn(2, ':');
36 let tool_name = parts
37 .next()
38 .expect("at least one identifier should be returned.");
39 let tool_identifier = match parts.next() {
40 Some(tool_identifier) => tool_identifier,
41 None => return Err(InvalidIdentifier::ToolIdentifierInvalidFormat(identifier)),
42 };
43
44 for x in [tool_name, tool_identifier] {
45 Self::is_valid_unicode(x).map_err(|error| match error {
46 InvalidIdentifierKind::Empty => {
47 InvalidIdentifier::ToolComponentEmpty(identifier.clone())
48 }
49 InvalidIdentifierKind::InvalidXid => {
50 InvalidIdentifier::ToolIdentifierInvalidXid(identifier.clone())
51 }
52 })?;
53 }
54 } else {
55 Self::is_valid_unicode(&identifier).map_err(|error| match error {
57 InvalidIdentifierKind::Empty => InvalidIdentifier::Empty,
58 InvalidIdentifierKind::InvalidXid => {
59 InvalidIdentifier::InvalidXid(identifier.clone())
60 }
61 })?;
62 }
63
64 Ok(Self(identifier))
65 }
66
67 pub fn is_tool_identifier(&self) -> bool {
69 self.0.starts_with("@tool:")
70 }
71
72 pub fn tool_components(&self) -> Option<(&str, &str)> {
74 self.0.strip_prefix("@tool:").map(|suffix| {
75 let mut parts = suffix.splitn(2, ':');
76 let tool_name = parts
77 .next()
78 .expect("identifier was checked to have 2 components above");
79 let tool_identifier = parts
80 .next()
81 .expect("identifier was checked to have 2 components above");
82 (tool_name, tool_identifier)
83 })
84 }
85
86 #[inline]
88 pub fn as_str(&self) -> &str {
89 &self.0
90 }
91
92 fn is_valid_unicode(identifier: &str) -> Result<(), InvalidIdentifierKind> {
93 if identifier.is_empty() {
94 return Err(InvalidIdentifierKind::Empty);
95 }
96
97 let mut first = true;
98 for ch in identifier.chars() {
99 if first {
100 if !unicode_ident::is_xid_start(ch) {
101 return Err(InvalidIdentifierKind::InvalidXid);
102 }
103 first = false;
104 } else if !(ch == '-' || unicode_ident::is_xid_continue(ch)) {
105 return Err(InvalidIdentifierKind::InvalidXid);
106 }
107 }
108 Ok(())
109 }
110}
111
112impl fmt::Display for ConfigIdentifier {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 write!(f, "{}", self.0)
115 }
116}
117
118impl<'de> serde::Deserialize<'de> for ConfigIdentifier {
119 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
120 where
121 D: serde::Deserializer<'de>,
122 {
123 let identifier = SmolStr::deserialize(deserializer)?;
124 ConfigIdentifier::new(identifier).map_err(serde::de::Error::custom)
125 }
126}
127
128#[derive(Clone, Copy, Debug)]
129enum InvalidIdentifierKind {
130 Empty,
131 InvalidXid,
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use serde::Deserialize;
138
139 #[derive(Deserialize, Debug, PartialEq, Eq)]
140 struct TestDeserialize {
141 identifier: ConfigIdentifier,
142 }
143
144 fn make_json(identifier: &str) -> String {
145 format!(r#"{{ "identifier": "{identifier}" }}"#)
146 }
147
148 #[test]
149 fn test_valid() {
150 let valid_inputs = ["foo", "foo-bar", "Δabc"];
151
152 for &input in &valid_inputs {
153 let identifier = ConfigIdentifier::new(input.into()).unwrap();
154 assert_eq!(identifier.as_str(), input);
155 assert!(!identifier.is_tool_identifier());
156
157 serde_json::from_str::<TestDeserialize>(&make_json(input)).unwrap();
158 }
159
160 let valid_tool_inputs = ["@tool:foo:bar", "@tool:Δabc_def-ghi:foo-bar"];
161
162 for &input in &valid_tool_inputs {
163 let identifier = ConfigIdentifier::new(input.into()).unwrap();
164 assert_eq!(identifier.as_str(), input);
165 assert!(identifier.is_tool_identifier());
166
167 serde_json::from_str::<TestDeserialize>(&make_json(input)).unwrap();
168 }
169 }
170
171 #[test]
172 fn test_invalid() {
173 let identifier = ConfigIdentifier::new("".into());
174 assert_eq!(identifier.unwrap_err(), InvalidIdentifier::Empty);
175
176 let invalid_xid = ["foo bar", "_", "-foo", "_foo", "@foo", "@tool"];
177
178 for &input in &invalid_xid {
179 let identifier = ConfigIdentifier::new(input.into());
180 assert_eq!(
181 identifier.unwrap_err(),
182 InvalidIdentifier::InvalidXid(input.into())
183 );
184
185 serde_json::from_str::<TestDeserialize>(&make_json(input)).unwrap_err();
186 }
187
188 let tool_component_empty = ["@tool::", "@tool:foo:", "@tool::foo"];
189
190 for &input in &tool_component_empty {
191 let identifier = ConfigIdentifier::new(input.into());
192 assert_eq!(
193 identifier.unwrap_err(),
194 InvalidIdentifier::ToolComponentEmpty(input.into())
195 );
196
197 serde_json::from_str::<TestDeserialize>(&make_json(input)).unwrap_err();
198 }
199
200 let tool_identifier_invalid_format = ["@tool:", "@tool:foo"];
201
202 for &input in &tool_identifier_invalid_format {
203 let identifier = ConfigIdentifier::new(input.into());
204 assert_eq!(
205 identifier.unwrap_err(),
206 InvalidIdentifier::ToolIdentifierInvalidFormat(input.into())
207 );
208
209 serde_json::from_str::<TestDeserialize>(&make_json(input)).unwrap_err();
210 }
211
212 let tool_identifier_invalid_xid = ["@tool:_foo:bar", "@tool:foo:#bar", "@tool:foo:bar:baz"];
213
214 for &input in &tool_identifier_invalid_xid {
215 let identifier = ConfigIdentifier::new(input.into());
216 assert_eq!(
217 identifier.unwrap_err(),
218 InvalidIdentifier::ToolIdentifierInvalidXid(input.into())
219 );
220
221 serde_json::from_str::<TestDeserialize>(&make_json(input)).unwrap_err();
222 }
223 }
224}