nika_engine/binding/
validate.rs1use crate::error::NikaError;
14
15pub fn validate_task_id(id: &str) -> Result<(), NikaError> {
33 if id.is_empty() {
35 return Err(NikaError::InvalidTaskId {
36 id: id.to_string(),
37 reason: "cannot be empty".into(),
38 });
39 }
40
41 let first = id.as_bytes()[0];
43 if !first.is_ascii_lowercase() {
44 return Err(NikaError::InvalidTaskId {
45 id: id.to_string(),
46 reason: "must start with lowercase letter (a-z), then lowercase letters, digits, or underscores".into(),
47 });
48 }
49
50 for &byte in &id.as_bytes()[1..] {
52 if !byte.is_ascii_lowercase() && !byte.is_ascii_digit() && byte != b'_' {
53 return Err(NikaError::InvalidTaskId {
54 id: id.to_string(),
55 reason: "must start with lowercase letter (a-z), then lowercase letters, digits, or underscores".into(),
56 });
57 }
58 }
59
60 Ok(())
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
72 fn valid_simple() {
73 assert!(validate_task_id("weather").is_ok());
74 assert!(validate_task_id("w").is_ok());
75 }
76
77 #[test]
78 fn valid_with_underscore() {
79 assert!(validate_task_id("get_data").is_ok());
80 assert!(validate_task_id("fetch_api").is_ok());
81 assert!(validate_task_id("my_task_name").is_ok());
82 assert!(validate_task_id("a_").is_ok());
83 assert!(validate_task_id("a__b").is_ok());
84 }
85
86 #[test]
87 fn valid_with_numbers() {
88 assert!(validate_task_id("task123").is_ok());
89 assert!(validate_task_id("step2").is_ok());
90 assert!(validate_task_id("v2_parser").is_ok());
91 assert!(validate_task_id("a0").is_ok());
92 assert!(validate_task_id("a123456789").is_ok());
93 }
94
95 #[test]
96 fn valid_single_letter() {
97 assert!(validate_task_id("a").is_ok());
98 assert!(validate_task_id("x").is_ok());
99 assert!(validate_task_id("z").is_ok());
100 }
101
102 #[test]
103 fn valid_all_lowercase_boundaries() {
104 assert!(validate_task_id("abcdefghijklmnopqrstuvwxyz").is_ok());
105 assert!(validate_task_id("a0123456789").is_ok());
106 }
107
108 #[test]
113 fn reject_empty() {
114 let err = validate_task_id("").unwrap_err();
115 assert!(err.to_string().contains("NIKA-055"));
116 assert!(err.to_string().contains("cannot be empty"));
117 }
118
119 #[test]
120 fn reject_number_start() {
121 let err = validate_task_id("123task").unwrap_err();
122 assert!(err.to_string().contains("NIKA-055"));
123 assert!(err.to_string().contains("start with lowercase letter"));
124 }
125
126 #[test]
127 fn reject_uppercase_start() {
128 let err = validate_task_id("Task").unwrap_err();
129 assert!(err.to_string().contains("NIKA-055"));
130 }
131
132 #[test]
133 fn reject_all_uppercase() {
134 let err = validate_task_id("TASK").unwrap_err();
135 assert!(err.to_string().contains("NIKA-055"));
136 }
137
138 #[test]
139 fn reject_uppercase_middle() {
140 let err = validate_task_id("myTask").unwrap_err();
141 assert!(err.to_string().contains("NIKA-055"));
142 }
143
144 #[test]
145 fn reject_underscore_start() {
146 let err = validate_task_id("_private").unwrap_err();
147 assert!(err.to_string().contains("NIKA-055"));
148 assert!(err.to_string().contains("start with lowercase letter"));
149 }
150
151 #[test]
152 fn reject_dash() {
153 let err = validate_task_id("fetch-api").unwrap_err();
154 assert!(err.to_string().contains("NIKA-055"));
155 }
157
158 #[test]
159 fn reject_dot() {
160 let err = validate_task_id("weather.api").unwrap_err();
161 assert!(err.to_string().contains("NIKA-055"));
162 }
164
165 #[test]
166 fn reject_spaces() {
167 assert!(validate_task_id("my task").is_err());
168 assert!(validate_task_id(" weather").is_err());
169 assert!(validate_task_id("weather ").is_err());
170 assert!(validate_task_id("my task").is_err());
171 }
172
173 #[test]
174 fn reject_special_chars() {
175 assert!(validate_task_id("task!").is_err());
176 assert!(validate_task_id("task@name").is_err());
177 assert!(validate_task_id("task#1").is_err());
178 assert!(validate_task_id("task$").is_err());
179 assert!(validate_task_id("task%name").is_err());
180 assert!(validate_task_id("task&more").is_err());
181 assert!(validate_task_id("task(x)").is_err());
182 assert!(validate_task_id("task=value").is_err());
183 assert!(validate_task_id("task+more").is_err());
184 assert!(validate_task_id("task[0]").is_err());
185 assert!(validate_task_id("task{x}").is_err());
186 assert!(validate_task_id("task|pipe").is_err());
187 assert!(validate_task_id("task\\slash").is_err());
188 assert!(validate_task_id("task;semicolon").is_err());
189 assert!(validate_task_id("task:colon").is_err());
190 assert!(validate_task_id("task'quote").is_err());
191 assert!(validate_task_id("task\u{00a0}nbsp").is_err()); }
193
194 #[test]
195 fn reject_emoji_and_unicode() {
196 assert!(validate_task_id("task😀").is_err());
197 assert!(validate_task_id("tâche").is_err()); assert!(validate_task_id("任務").is_err()); }
200
201 #[test]
202 fn error_message_contains_nika_code() {
203 let result = validate_task_id("invalid-name");
204 let err = result.unwrap_err();
205 assert!(err.to_string().contains("NIKA-055"));
206 }
207
208 #[test]
209 fn error_message_includes_invalid_id() {
210 let invalid_id = "my-invalid-task";
211 let result = validate_task_id(invalid_id);
212 let err = result.unwrap_err();
213 assert!(err.to_string().contains(invalid_id));
214 }
215}