1use std::{borrow::Borrow, convert::Infallible, str::FromStr};
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6fn validate_node_id(id: &str) -> Result<(), InvalidId> {
18 if id.is_empty() {
19 return Err(InvalidId("identifier must not be empty".into()));
20 }
21 if id.starts_with('.') {
22 return Err(InvalidId(format!(
23 "identifier '{id}' must not start with '.' \
24 (dot-segments such as '.' and '..' can traverse parent directories)"
25 )));
26 }
27 if let Some(ch) = id
28 .chars()
29 .find(|c| !c.is_ascii_alphanumeric() && *c != '_' && *c != '-' && *c != '.')
30 {
31 return Err(InvalidId(format!(
32 "identifier contains invalid character '{ch}' -- only [a-zA-Z0-9_.-] are allowed"
33 )));
34 }
35 Ok(())
36}
37
38fn validate_data_id(id: &str) -> Result<(), InvalidId> {
45 if id.is_empty() {
46 return Err(InvalidId("identifier must not be empty".into()));
47 }
48 if let Some(ch) = id
49 .chars()
50 .find(|c| !c.is_ascii_alphanumeric() && *c != '_' && *c != '-' && *c != '.' && *c != '/')
51 {
52 return Err(InvalidId(format!(
53 "identifier contains invalid character '{ch}' -- only [a-zA-Z0-9_./-] are allowed"
54 )));
55 }
56 if id.starts_with('/') || id.ends_with('/') || id.contains("//") {
61 return Err(InvalidId(format!(
62 "identifier '{id}' has empty path segment -- \
63 leading, trailing, or consecutive '/' are not allowed"
64 )));
65 }
66 Ok(())
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct InvalidId(pub String);
71
72impl std::fmt::Display for InvalidId {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 write!(f, "{}", self.0)
75 }
76}
77
78impl std::error::Error for InvalidId {}
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, JsonSchema)]
81pub struct NodeId(pub(crate) String);
82
83impl<'de> Deserialize<'de> for NodeId {
84 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
85 where
86 D: serde::Deserializer<'de>,
87 {
88 let s = String::deserialize(deserializer)?;
89 validate_node_id(&s).map_err(serde::de::Error::custom)?;
90 Ok(NodeId(s))
91 }
92}
93
94impl FromStr for NodeId {
95 type Err = InvalidId;
96
97 fn from_str(s: &str) -> Result<Self, Self::Err> {
98 validate_node_id(s)?;
99 Ok(Self(s.to_owned()))
100 }
101}
102
103impl From<String> for NodeId {
115 fn from(id: String) -> Self {
116 if let Err(e) = validate_node_id(&id) {
117 panic!("invalid NodeId '{id}': {e}");
118 }
119 Self(id)
120 }
121}
122
123impl std::fmt::Display for NodeId {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 std::fmt::Display::fmt(&self.0, f)
126 }
127}
128
129impl AsRef<str> for NodeId {
130 fn as_ref(&self) -> &str {
131 &self.0
132 }
133}
134
135#[derive(
136 Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
137)]
138pub struct OperatorId(String);
139
140impl FromStr for OperatorId {
141 type Err = Infallible;
142
143 fn from_str(s: &str) -> Result<Self, Self::Err> {
144 Ok(Self(s.to_owned()))
145 }
146}
147
148impl From<String> for OperatorId {
149 fn from(id: String) -> Self {
150 Self(id)
151 }
152}
153
154impl std::fmt::Display for OperatorId {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 std::fmt::Display::fmt(&self.0, f)
157 }
158}
159
160impl AsRef<str> for OperatorId {
161 fn as_ref(&self) -> &str {
162 &self.0
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, JsonSchema)]
167pub struct DataId(String);
168
169impl<'de> Deserialize<'de> for DataId {
170 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
171 where
172 D: serde::Deserializer<'de>,
173 {
174 let s = String::deserialize(deserializer)?;
175 validate_data_id(&s).map_err(serde::de::Error::custom)?;
176 Ok(DataId(s))
177 }
178}
179
180impl FromStr for DataId {
181 type Err = InvalidId;
182
183 fn from_str(s: &str) -> Result<Self, Self::Err> {
184 validate_data_id(s)?;
185 Ok(Self(s.to_owned()))
186 }
187}
188
189impl From<DataId> for String {
190 fn from(id: DataId) -> Self {
191 id.0
192 }
193}
194
195impl From<String> for DataId {
207 fn from(id: String) -> Self {
208 if let Err(e) = validate_data_id(&id) {
209 panic!("invalid DataId '{id}': {e}");
210 }
211 Self(id)
212 }
213}
214
215impl From<&str> for DataId {
220 fn from(id: &str) -> Self {
221 id.to_owned().into()
222 }
223}
224
225impl std::fmt::Display for DataId {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 std::fmt::Display::fmt(&self.0, f)
228 }
229}
230
231impl std::ops::Deref for DataId {
232 type Target = String;
233
234 fn deref(&self) -> &Self::Target {
235 &self.0
236 }
237}
238
239impl AsRef<String> for DataId {
240 fn as_ref(&self) -> &String {
241 &self.0
242 }
243}
244
245impl AsRef<str> for DataId {
246 fn as_ref(&self) -> &str {
247 &self.0
248 }
249}
250
251impl Borrow<String> for DataId {
252 fn borrow(&self) -> &String {
253 &self.0
254 }
255}
256
257impl Borrow<str> for DataId {
258 fn borrow(&self) -> &str {
259 &self.0
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn valid_node_ids() {
269 assert!(validate_node_id("my-node").is_ok());
270 assert!(validate_node_id("my_node").is_ok());
271 assert!(validate_node_id("MyNode123").is_ok());
272 assert!(validate_node_id("node.v2").is_ok());
273 assert!(validate_node_id("a").is_ok());
274 }
275
276 #[test]
277 fn invalid_node_ids() {
278 assert!(validate_node_id("").is_err());
279 assert!(validate_node_id("node/output").is_err());
280 assert!(validate_node_id("node name").is_err());
281 assert!(validate_node_id("node;rm").is_err());
282 assert!(validate_node_id("node\0").is_err());
283 }
284
285 #[test]
286 fn node_id_rejects_dot_segments() {
287 assert!(validate_node_id(".").is_err(), ". must be rejected");
289 assert!(validate_node_id("..").is_err(), ".. must be rejected");
290 assert!(
292 validate_node_id(".hidden").is_err(),
293 ".hidden must be rejected"
294 );
295 assert!(
296 validate_node_id(".config").is_err(),
297 ".config must be rejected"
298 );
299 assert!(
301 validate_node_id("node.v2").is_ok(),
302 "embedded dot is allowed"
303 );
304 assert!(
305 validate_node_id("a.b.c").is_ok(),
306 "multiple embedded dots are allowed"
307 );
308 }
309
310 #[test]
311 fn data_id_allows_slash_for_operators() {
312 assert!(validate_data_id("rust-operator/status").is_ok());
314 assert!(validate_data_id("op/output").is_ok());
315 assert!(validate_data_id("simple").is_ok());
316 }
317
318 #[test]
319 fn data_id_rejects_other_specials() {
320 assert!(validate_data_id("").is_err());
321 assert!(validate_data_id("has space").is_err());
322 assert!(validate_data_id("semi;colon").is_err());
323 }
324
325 #[test]
326 fn data_id_rejects_malformed_slash_patterns() {
327 assert!(validate_data_id("op/").is_err(), "trailing slash");
328 assert!(validate_data_id("/out").is_err(), "leading slash");
329 assert!(validate_data_id("a//b").is_err(), "double slash");
330 assert!(validate_data_id("/").is_err(), "bare slash");
331 }
332
333 #[test]
334 fn node_id_from_str_rejects_invalid() {
335 assert!(NodeId::from_str("hello").is_ok());
336 assert!(NodeId::from_str("hello/world").is_err());
337 assert!(NodeId::from_str("hello world").is_err());
338 assert!(NodeId::from_str("").is_err());
339 }
340
341 #[test]
342 #[should_panic(expected = "invalid NodeId")]
343 fn node_id_from_string_panics_on_invalid() {
344 let _id: NodeId = "bad/id".to_string().into();
345 }
346
347 #[test]
348 fn node_id_parse_rejects_invalid() {
349 assert!("hello".parse::<NodeId>().is_ok());
350 assert!("bad/id".parse::<NodeId>().is_err());
351 assert!("".parse::<NodeId>().is_err());
352 }
353
354 #[test]
355 fn data_id_parse_rejects_invalid() {
356 assert!("output".parse::<DataId>().is_ok());
357 assert!("bad;id".parse::<DataId>().is_err());
358 }
359
360 #[test]
361 fn node_id_deserialize_rejects_invalid() {
362 let result: Result<NodeId, _> = serde_json::from_str("\"bad/id\"");
363 assert!(result.is_err());
364 }
365
366 #[test]
367 fn data_id_deserialize_rejects_invalid() {
368 let result: Result<DataId, _> = serde_json::from_str("\"bad;id\"");
369 assert!(result.is_err());
370 }
371}