wavs_types/id/
workflow.rs1use serde::{Deserialize, Deserializer, Serialize};
2use std::{ops::Deref, str::FromStr};
3use thiserror::Error;
4#[cfg(feature = "ts-bindings")]
5use ts_rs::TS;
6use utoipa::ToSchema;
7
8#[cfg_attr(feature = "ts-bindings", derive(TS))]
10#[derive(
11 Serialize,
12 Clone,
13 Debug,
14 PartialEq,
15 Eq,
16 PartialOrd,
17 Ord,
18 Hash,
19 ToSchema,
20 bincode::Decode,
21 bincode::Encode,
22)]
23#[serde(transparent)]
24pub struct WorkflowId(String);
25
26impl WorkflowId {
27 pub fn validate(id: impl AsRef<str>) -> Result<(), WorkflowIdError> {
29 let id = id.as_ref();
30
31 if id.len() < 3 || id.len() > 36 {
32 Err(WorkflowIdError::LengthError)
33 } else if !id
34 .chars()
35 .all(|c| c.is_ascii_lowercase() || c.is_numeric() || c == '_' || c == '-')
36 {
37 Err(WorkflowIdError::CharError)
38 } else {
39 Ok(())
40 }
41 }
42 pub fn new(id: impl Into<String>) -> Result<Self, WorkflowIdError> {
46 let id = id.into();
47
48 Self::validate(&id)?;
49
50 Ok(Self(id))
51 }
52}
53
54impl<'de> Deserialize<'de> for WorkflowId {
55 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
56 where
57 D: Deserializer<'de>,
58 {
59 let s = String::deserialize(deserializer)?;
60 Self::new(s).map_err(serde::de::Error::custom)
61 }
62}
63
64impl AsRef<str> for WorkflowId {
65 fn as_ref(&self) -> &str {
66 &self.0
67 }
68}
69
70impl Deref for WorkflowId {
71 type Target = str;
72
73 fn deref(&self) -> &Self::Target {
74 &self.0
75 }
76}
77
78impl std::fmt::Display for WorkflowId {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 write!(f, "{}", self.0)
81 }
82}
83
84impl TryFrom<&str> for WorkflowId {
85 type Error = WorkflowIdError;
86
87 fn try_from(s: &str) -> Result<Self, Self::Error> {
88 Self::new(s)
89 }
90}
91
92impl FromStr for WorkflowId {
93 type Err = WorkflowIdError;
94
95 fn from_str(s: &str) -> Result<Self, Self::Err> {
96 Self::new(s.to_string())
97 }
98}
99
100impl Default for WorkflowId {
101 fn default() -> Self {
102 WorkflowId::new("default").unwrap()
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn valid_ids() {
112 WorkflowId::new("foobar").unwrap();
113 WorkflowId::new("foot123").unwrap();
114 WorkflowId::new("123foot").unwrap();
115 WorkflowId::new("two_words").unwrap();
116 WorkflowId::new("kebab-case").unwrap();
117 WorkflowId::new("pretty-1234321-long").unwrap();
118 WorkflowId::new("12345678901234567890123456789012").unwrap();
120 }
121
122 #[test]
123 fn invalid_ids() {
124 let err = WorkflowId::new("fo").unwrap_err();
126 assert_eq!(err, WorkflowIdError::LengthError);
127 let err = WorkflowId::new("1234567890123456789012345678901234567").unwrap_err();
128 assert_eq!(err, WorkflowIdError::LengthError);
129
130 let err = WorkflowId::new("with space").unwrap_err();
132 assert_eq!(err, WorkflowIdError::CharError);
133 WorkflowId::new("UPPER_SPACE").unwrap_err();
134 WorkflowId::new("Capitalized").unwrap_err();
135 WorkflowId::new("../../etc/passwd").unwrap_err();
136 WorkflowId::new("c:\\\\badfile").unwrap_err();
137 }
138
139 #[test]
140 fn invalid_id_deserialize() {
141 let id_str = "foo";
143 let id_obj: WorkflowId = serde_json::from_str(&format!("\"{id_str}\"")).unwrap();
144 assert_eq!(id_obj.to_string(), id_str);
145
146 let id_str = "THIS/IS/BAD";
148 serde_json::from_str::<WorkflowId>(&format!("\"{id_str}\"")).unwrap_err();
149 }
150
151 #[test]
152 fn proper_representation() {
153 let name = "fly2you";
154 let id = WorkflowId::new(name).unwrap();
155 assert_eq!(id.to_string(), name.to_string());
157 assert_eq!(name, id.as_ref());
159 assert_eq!(name.as_bytes(), id.as_bytes())
161 }
162}
163
164#[derive(Error, Debug, PartialEq, Eq, Clone)]
165pub enum WorkflowIdError {
166 #[error("ID must be between 3 and 36 characters")]
167 LengthError,
168 #[error("ID must be lowercase alphanumeric")]
169 CharError,
170}