1use argentor_core::{ArgentorResult, ToolCall, ToolResult};
26use argentor_skills::skill::{Skill, SkillDescriptor};
27use async_trait::async_trait;
28use base64::{engine::general_purpose, Engine as _};
29use regex::Regex;
30
31pub struct DataValidatorSkill {
33 descriptor: SkillDescriptor,
34}
35
36impl DataValidatorSkill {
37 pub fn new() -> Self {
39 Self {
40 descriptor: SkillDescriptor {
41 name: "data_validator".to_string(),
42 description: "Validate data against common formats: email, url, ipv4, ipv6, \
43 uuid, phone, credit_card, date, datetime, hex_color, semver, \
44 json, base64, domain, mac_address."
45 .to_string(),
46 parameters_schema: serde_json::json!({
47 "type": "object",
48 "properties": {
49 "format": {
50 "type": "string",
51 "enum": [
52 "email", "url", "ipv4", "ipv6", "uuid", "phone",
53 "credit_card", "date", "datetime", "hex_color",
54 "semver", "json", "base64", "domain", "mac_address"
55 ],
56 "description": "The data format to validate against"
57 },
58 "value": {
59 "type": "string",
60 "description": "The value to validate"
61 }
62 },
63 "required": ["format", "value"]
64 }),
65 required_capabilities: vec![],
66 requires_approval: false,
67 },
68 }
69 }
70}
71
72impl Default for DataValidatorSkill {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78fn result_json(valid: bool, format: &str, details: &str) -> serde_json::Value {
84 serde_json::json!({
85 "valid": valid,
86 "format": format,
87 "details": details,
88 })
89}
90
91fn validate_email(value: &str) -> serde_json::Value {
92 let re = Regex::new(
94 r"(?i)^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$",
95 );
96 match re {
97 Ok(re) if re.is_match(value) => {
98 let parts: Vec<&str> = value.splitn(2, '@').collect();
99 let domain = parts.get(1).copied().unwrap_or("");
100 result_json(
101 true,
102 "email",
103 &format!("Valid email address (domain: {domain})"),
104 )
105 }
106 _ => result_json(false, "email", "Invalid email address format"),
107 }
108}
109
110fn validate_url(value: &str) -> serde_json::Value {
111 let re = Regex::new(r"^https?://[^\s/$.?#].[^\s]*$");
113 match re {
114 Ok(re) if re.is_match(value) => {
115 let scheme = if value.starts_with("https://") {
116 "https"
117 } else {
118 "http"
119 };
120 result_json(true, "url", &format!("Valid URL (scheme: {scheme})"))
121 }
122 _ => result_json(false, "url", "Invalid URL format (must be http or https)"),
123 }
124}
125
126fn validate_ipv4(value: &str) -> serde_json::Value {
127 let parts: Vec<&str> = value.split('.').collect();
128 if parts.len() != 4 {
129 return result_json(false, "ipv4", "IPv4 must have exactly 4 octets");
130 }
131
132 for (i, part) in parts.iter().enumerate() {
133 if part.len() > 1 && part.starts_with('0') {
135 return result_json(
136 false,
137 "ipv4",
138 &format!("Octet {i} has leading zeros: '{part}'"),
139 );
140 }
141 match part.parse::<u16>() {
142 Ok(n) if n <= 255 => {}
143 _ => {
144 return result_json(
145 false,
146 "ipv4",
147 &format!("Octet {i} is not a valid number 0-255: '{part}'"),
148 );
149 }
150 }
151 }
152
153 result_json(true, "ipv4", "Valid IPv4 address")
154}
155
156fn validate_ipv6(value: &str) -> serde_json::Value {
157 match value.parse::<std::net::Ipv6Addr>() {
159 Ok(_) => result_json(true, "ipv6", "Valid IPv6 address"),
160 Err(e) => result_json(false, "ipv6", &format!("Invalid IPv6 address: {e}")),
161 }
162}
163
164fn validate_uuid(value: &str) -> serde_json::Value {
165 let re = Regex::new(r"(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
166 match re {
167 Ok(re) if re.is_match(value) => {
168 let version_char = value.chars().nth(14).unwrap_or('0');
170 let version = match version_char {
171 '1' => "v1 (time-based)",
172 '2' => "v2 (DCE security)",
173 '3' => "v3 (MD5 namespace)",
174 '4' => "v4 (random)",
175 '5' => "v5 (SHA-1 namespace)",
176 '6' => "v6 (reordered time)",
177 '7' => "v7 (Unix epoch time)",
178 _ => "unknown version",
179 };
180 result_json(true, "uuid", &format!("Valid UUID ({version})"))
181 }
182 _ => result_json(false, "uuid", "Invalid UUID format"),
183 }
184}
185
186fn validate_phone(value: &str) -> serde_json::Value {
187 let digits: String = value.chars().filter(char::is_ascii_digit).collect();
190 let starts_valid = value.starts_with('+')
191 || value.starts_with('(')
192 || value.starts_with(|c: char| c.is_ascii_digit());
193
194 let all_valid = value
196 .chars()
197 .all(|c| c.is_ascii_digit() || "+- .()".contains(c));
198
199 if starts_valid && all_valid && (7..=15).contains(&digits.len()) {
200 result_json(
201 true,
202 "phone",
203 &format!("Valid phone number ({} digits)", digits.len()),
204 )
205 } else if digits.len() < 7 {
206 result_json(
207 false,
208 "phone",
209 &format!("Too few digits ({}, minimum 7)", digits.len()),
210 )
211 } else if digits.len() > 15 {
212 result_json(
213 false,
214 "phone",
215 &format!("Too many digits ({}, maximum 15)", digits.len()),
216 )
217 } else {
218 result_json(false, "phone", "Invalid phone number format")
219 }
220}
221
222fn validate_credit_card(value: &str) -> serde_json::Value {
223 let digits: String = value.chars().filter(char::is_ascii_digit).collect();
225
226 if digits.len() < 13 || digits.len() > 19 {
227 return result_json(
228 false,
229 "credit_card",
230 &format!("Invalid length ({} digits, expected 13-19)", digits.len()),
231 );
232 }
233
234 let mut sum: u32 = 0;
236 let mut double = false;
237
238 for ch in digits.chars().rev() {
239 let d = match ch.to_digit(10) {
240 Some(d) => d,
241 None => {
242 return result_json(false, "credit_card", "Contains non-digit characters");
243 }
244 };
245
246 let val = if double {
247 let doubled = d * 2;
248 if doubled > 9 {
249 doubled - 9
250 } else {
251 doubled
252 }
253 } else {
254 d
255 };
256
257 sum += val;
258 double = !double;
259 }
260
261 if sum % 10 == 0 {
262 let card_type = if digits.starts_with('4') {
264 "Visa"
265 } else if digits.starts_with("51")
266 || digits.starts_with("52")
267 || digits.starts_with("53")
268 || digits.starts_with("54")
269 || digits.starts_with("55")
270 {
271 "Mastercard"
272 } else if digits.starts_with("34") || digits.starts_with("37") {
273 "American Express"
274 } else if digits.starts_with("6011") || digits.starts_with("65") {
275 "Discover"
276 } else {
277 "Unknown"
278 };
279 result_json(
280 true,
281 "credit_card",
282 &format!("Valid credit card number (Luhn check passed, type: {card_type})"),
283 )
284 } else {
285 result_json(
286 false,
287 "credit_card",
288 "Invalid credit card number (Luhn check failed)",
289 )
290 }
291}
292
293fn validate_date(value: &str) -> serde_json::Value {
294 let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$");
295 match re {
296 Ok(re) if re.is_match(value) => {
297 let parts: Vec<&str> = value.split('-').collect();
299 let year: u32 = parts[0].parse().unwrap_or(0);
300 let month: u32 = parts[1].parse().unwrap_or(0);
301 let day: u32 = parts[2].parse().unwrap_or(0);
302
303 if !(1..=9999).contains(&year) {
304 return result_json(false, "date", &format!("Invalid year: {year}"));
305 }
306 if !(1..=12).contains(&month) {
307 return result_json(false, "date", &format!("Invalid month: {month}"));
308 }
309
310 let days_in_month = match month {
311 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
312 4 | 6 | 9 | 11 => 30,
313 2 => {
314 if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
315 29
316 } else {
317 28
318 }
319 }
320 _ => 0,
321 };
322
323 if day < 1 || day > days_in_month {
324 return result_json(
325 false,
326 "date",
327 &format!("Invalid day {day} for month {month} (max: {days_in_month})"),
328 );
329 }
330
331 result_json(true, "date", "Valid ISO 8601 date")
332 }
333 _ => result_json(false, "date", "Invalid date format (expected YYYY-MM-DD)"),
334 }
335}
336
337fn validate_datetime(value: &str) -> serde_json::Value {
338 let re = Regex::new(r"^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$");
340 match re {
341 Ok(re) if re.is_match(value) => {
342 let date_part = &value[..10];
344 let date_result = validate_date(date_part);
345 if date_result["valid"] == false {
346 return result_json(
347 false,
348 "datetime",
349 date_result["details"]
350 .as_str()
351 .unwrap_or("Invalid date portion"),
352 );
353 }
354
355 let time_start = 11; let time_str = &value[time_start..];
358 let time_core = if let Some(pos) = time_str.find(['.', 'Z', '+', '-']) {
359 &time_str[..pos]
360 } else {
361 time_str
362 };
363
364 let time_parts: Vec<&str> = time_core.split(':').collect();
365 if time_parts.len() == 3 {
366 let hour: u32 = time_parts[0].parse().unwrap_or(99);
367 let min: u32 = time_parts[1].parse().unwrap_or(99);
368 let sec: u32 = time_parts[2].parse().unwrap_or(99);
369
370 if hour > 23 {
371 return result_json(false, "datetime", &format!("Invalid hour: {hour}"));
372 }
373 if min > 59 {
374 return result_json(false, "datetime", &format!("Invalid minute: {min}"));
375 }
376 if sec > 59 {
377 return result_json(false, "datetime", &format!("Invalid second: {sec}"));
378 }
379 }
380
381 let has_tz =
382 value.ends_with('Z') || value.contains('+') || (value.matches('-').count() > 2);
383 let tz_info = if has_tz { " with timezone" } else { " (local)" };
384
385 result_json(
386 true,
387 "datetime",
388 &format!("Valid ISO 8601 datetime{tz_info}"),
389 )
390 }
391 _ => result_json(
392 false,
393 "datetime",
394 "Invalid datetime format (expected ISO 8601: YYYY-MM-DDThh:mm:ss[.sss][Z|+hh:mm])",
395 ),
396 }
397}
398
399fn validate_hex_color(value: &str) -> serde_json::Value {
400 let re = Regex::new(r"(?i)^#([0-9a-f]{3}|[0-9a-f]{6})$");
401 match re {
402 Ok(re) if re.is_match(value) => {
403 let kind = if value.len() == 4 {
404 "shorthand (#RGB)"
405 } else {
406 "full (#RRGGBB)"
407 };
408 result_json(true, "hex_color", &format!("Valid hex color ({kind})"))
409 }
410 _ => result_json(
411 false,
412 "hex_color",
413 "Invalid hex color (expected #RGB or #RRGGBB)",
414 ),
415 }
416}
417
418fn validate_semver(value: &str) -> serde_json::Value {
419 let re = Regex::new(
421 r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$",
422 );
423 match re {
424 Ok(re) if re.is_match(value) => {
425 let core_part = value.split('-').next().unwrap_or(value);
426 let core_part = core_part.split('+').next().unwrap_or(core_part);
427 let has_pre = value.contains('-');
428 let has_build = value.contains('+');
429 let mut details = format!("Valid semver ({core_part})");
430 if has_pre {
431 details.push_str(" with pre-release");
432 }
433 if has_build {
434 details.push_str(" with build metadata");
435 }
436 result_json(true, "semver", &details)
437 }
438 _ => result_json(
439 false,
440 "semver",
441 "Invalid semver (expected MAJOR.MINOR.PATCH[-prerelease][+build])",
442 ),
443 }
444}
445
446fn validate_json(value: &str) -> serde_json::Value {
447 match serde_json::from_str::<serde_json::Value>(value) {
448 Ok(parsed) => {
449 let kind = match &parsed {
450 serde_json::Value::Object(_) => "object",
451 serde_json::Value::Array(_) => "array",
452 serde_json::Value::String(_) => "string",
453 serde_json::Value::Number(_) => "number",
454 serde_json::Value::Bool(_) => "boolean",
455 serde_json::Value::Null => "null",
456 };
457 result_json(true, "json", &format!("Valid JSON ({kind})"))
458 }
459 Err(e) => result_json(false, "json", &format!("Invalid JSON: {e}")),
460 }
461}
462
463fn validate_base64(value: &str) -> serde_json::Value {
464 if value.is_empty() {
465 return result_json(true, "base64", "Valid base64 (empty string)");
466 }
467
468 let re = Regex::new(r"^[A-Za-z0-9+/]*={0,2}$");
470 match re {
471 Ok(re) if re.is_match(value) && value.len() % 4 == 0 => {
472 match general_purpose::STANDARD.decode(value) {
474 Ok(decoded) => result_json(
475 true,
476 "base64",
477 &format!("Valid base64 ({} bytes decoded)", decoded.len()),
478 ),
479 Err(e) => result_json(false, "base64", &format!("Invalid base64: {e}")),
480 }
481 }
482 _ => result_json(
483 false,
484 "base64",
485 "Invalid base64 encoding (bad characters or padding)",
486 ),
487 }
488}
489
490fn validate_domain(value: &str) -> serde_json::Value {
491 if value.is_empty() || value.len() > 253 {
493 return result_json(
494 false,
495 "domain",
496 "Invalid domain (empty or exceeds 253 characters)",
497 );
498 }
499
500 let re = Regex::new(r"^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)*[a-zA-Z]{2,}$");
501 match re {
502 Ok(re) if re.is_match(value) => {
503 for label in value.split('.') {
505 if label.len() > 63 {
506 return result_json(
507 false,
508 "domain",
509 &format!("Label '{}...' exceeds 63 characters", &label[..20]),
510 );
511 }
512 }
513
514 let label_count = value.split('.').count();
515 let tld = value.rsplit('.').next().unwrap_or("");
516 result_json(
517 true,
518 "domain",
519 &format!("Valid domain ({label_count} labels, TLD: {tld})"),
520 )
521 }
522 _ => result_json(false, "domain", "Invalid domain name format"),
523 }
524}
525
526fn validate_mac_address(value: &str) -> serde_json::Value {
527 let re = Regex::new(r"(?i)^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$");
529 match re {
530 Ok(re) if re.is_match(value) => {
531 let separator = if value.contains(':') { "colon" } else { "dash" };
532 result_json(
533 true,
534 "mac_address",
535 &format!("Valid MAC address ({separator}-separated)"),
536 )
537 }
538 _ => result_json(
539 false,
540 "mac_address",
541 "Invalid MAC address (expected xx:xx:xx:xx:xx:xx or xx-xx-xx-xx-xx-xx)",
542 ),
543 }
544}
545
546#[async_trait]
551impl Skill for DataValidatorSkill {
552 fn descriptor(&self) -> &SkillDescriptor {
553 &self.descriptor
554 }
555
556 async fn execute(&self, call: ToolCall) -> ArgentorResult<ToolResult> {
557 let format = call.arguments["format"]
558 .as_str()
559 .unwrap_or_default()
560 .to_string();
561
562 let value = call.arguments["value"]
563 .as_str()
564 .unwrap_or_default()
565 .to_string();
566
567 if format.is_empty() {
568 return Ok(ToolResult::error(&call.id, "Format parameter is required"));
569 }
570 if value.is_empty() {
571 return Ok(ToolResult::error(&call.id, "Value parameter is required"));
572 }
573
574 let result = match format.as_str() {
575 "email" => validate_email(&value),
576 "url" => validate_url(&value),
577 "ipv4" => validate_ipv4(&value),
578 "ipv6" => validate_ipv6(&value),
579 "uuid" => validate_uuid(&value),
580 "phone" => validate_phone(&value),
581 "credit_card" => validate_credit_card(&value),
582 "date" => validate_date(&value),
583 "datetime" => validate_datetime(&value),
584 "hex_color" => validate_hex_color(&value),
585 "semver" => validate_semver(&value),
586 "json" => validate_json(&value),
587 "base64" => validate_base64(&value),
588 "domain" => validate_domain(&value),
589 "mac_address" => validate_mac_address(&value),
590 _ => {
591 return Ok(ToolResult::error(
592 &call.id,
593 format!(
594 "Unknown format '{format}'. Supported: email, url, ipv4, ipv6, uuid, \
595 phone, credit_card, date, datetime, hex_color, semver, json, base64, \
596 domain, mac_address"
597 ),
598 ));
599 }
600 };
601
602 Ok(ToolResult::success(&call.id, result.to_string()))
603 }
604}
605
606#[cfg(test)]
611#[allow(clippy::unwrap_used, clippy::expect_used)]
612mod tests {
613 use super::*;
614
615 fn skill() -> DataValidatorSkill {
616 DataValidatorSkill::new()
617 }
618
619 fn call(format: &str, value: &str) -> ToolCall {
620 ToolCall {
621 id: "t1".to_string(),
622 name: "data_validator".to_string(),
623 arguments: serde_json::json!({"format": format, "value": value}),
624 }
625 }
626
627 fn parse_result(result: &ToolResult) -> serde_json::Value {
628 serde_json::from_str(&result.content).unwrap()
629 }
630
631 #[test]
634 fn test_descriptor() {
635 let s = skill();
636 assert_eq!(s.descriptor().name, "data_validator");
637 assert!(s.descriptor().required_capabilities.is_empty());
638 }
639
640 #[tokio::test]
643 async fn test_email_valid() {
644 let s = skill();
645 let r = s.execute(call("email", "user@example.com")).await.unwrap();
646 let v = parse_result(&r);
647 assert_eq!(v["valid"], true);
648 assert!(v["details"].as_str().unwrap().contains("example.com"));
649 }
650
651 #[tokio::test]
652 async fn test_email_valid_complex() {
653 let s = skill();
654 let r = s
655 .execute(call("email", "user.name+tag@sub.domain.co.uk"))
656 .await
657 .unwrap();
658 let v = parse_result(&r);
659 assert_eq!(v["valid"], true);
660 }
661
662 #[tokio::test]
663 async fn test_email_invalid() {
664 let s = skill();
665 for invalid in &["notanemail", "@missing.local", "user@", "user@.com"] {
666 let r = s.execute(call("email", invalid)).await.unwrap();
667 let v = parse_result(&r);
668 assert_eq!(v["valid"], false, "Expected invalid for: {invalid}");
669 }
670 }
671
672 #[tokio::test]
675 async fn test_url_valid() {
676 let s = skill();
677 let r = s
678 .execute(call("url", "https://example.com/path?q=1"))
679 .await
680 .unwrap();
681 let v = parse_result(&r);
682 assert_eq!(v["valid"], true);
683 assert!(v["details"].as_str().unwrap().contains("https"));
684 }
685
686 #[tokio::test]
687 async fn test_url_http() {
688 let s = skill();
689 let r = s.execute(call("url", "http://example.com")).await.unwrap();
690 let v = parse_result(&r);
691 assert_eq!(v["valid"], true);
692 }
693
694 #[tokio::test]
695 async fn test_url_invalid() {
696 let s = skill();
697 for invalid in &["ftp://example.com", "not a url", "://missing.scheme"] {
698 let r = s.execute(call("url", invalid)).await.unwrap();
699 let v = parse_result(&r);
700 assert_eq!(v["valid"], false, "Expected invalid for: {invalid}");
701 }
702 }
703
704 #[tokio::test]
707 async fn test_ipv4_valid() {
708 let s = skill();
709 for ip in &["192.168.1.1", "0.0.0.0", "255.255.255.255", "10.0.0.1"] {
710 let r = s.execute(call("ipv4", ip)).await.unwrap();
711 let v = parse_result(&r);
712 assert_eq!(v["valid"], true, "Expected valid for: {ip}");
713 }
714 }
715
716 #[tokio::test]
717 async fn test_ipv4_invalid() {
718 let s = skill();
719 for ip in &[
720 "256.0.0.1",
721 "1.2.3",
722 "1.2.3.4.5",
723 "01.02.03.04",
724 "abc.def.ghi.jkl",
725 ] {
726 let r = s.execute(call("ipv4", ip)).await.unwrap();
727 let v = parse_result(&r);
728 assert_eq!(v["valid"], false, "Expected invalid for: {ip}");
729 }
730 }
731
732 #[tokio::test]
735 async fn test_ipv6_valid() {
736 let s = skill();
737 for ip in &[
738 "::1",
739 "fe80::1",
740 "2001:0db8:85a3::8a2e:0370:7334",
741 "::ffff:192.0.2.1",
742 ] {
743 let r = s.execute(call("ipv6", ip)).await.unwrap();
744 let v = parse_result(&r);
745 assert_eq!(v["valid"], true, "Expected valid for: {ip}");
746 }
747 }
748
749 #[tokio::test]
750 async fn test_ipv6_invalid() {
751 let s = skill();
752 for ip in &["not-ipv6", "12345::abcde", ":::1"] {
753 let r = s.execute(call("ipv6", ip)).await.unwrap();
754 let v = parse_result(&r);
755 assert_eq!(v["valid"], false, "Expected invalid for: {ip}");
756 }
757 }
758
759 #[tokio::test]
762 async fn test_uuid_valid() {
763 let s = skill();
764 let r = s
765 .execute(call("uuid", "550e8400-e29b-41d4-a716-446655440000"))
766 .await
767 .unwrap();
768 let v = parse_result(&r);
769 assert_eq!(v["valid"], true);
770 assert!(v["details"].as_str().unwrap().contains("v4"));
771 }
772
773 #[tokio::test]
774 async fn test_uuid_invalid() {
775 let s = skill();
776 for invalid in &[
777 "not-a-uuid",
778 "550e8400-e29b-41d4-a716",
779 "ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ",
780 ] {
781 let r = s.execute(call("uuid", invalid)).await.unwrap();
782 let v = parse_result(&r);
783 assert_eq!(v["valid"], false, "Expected invalid for: {invalid}");
784 }
785 }
786
787 #[tokio::test]
790 async fn test_phone_valid() {
791 let s = skill();
792 for phone in &[
793 "+1234567890",
794 "1234567890",
795 "+44 20 7946 0958",
796 "(212) 555-1234",
797 ] {
798 let r = s.execute(call("phone", phone)).await.unwrap();
799 let v = parse_result(&r);
800 assert_eq!(v["valid"], true, "Expected valid for: {phone}");
801 }
802 }
803
804 #[tokio::test]
805 async fn test_phone_invalid() {
806 let s = skill();
807 for phone in &["123", "abcdefghij", "+1234567890123456"] {
808 let r = s.execute(call("phone", phone)).await.unwrap();
809 let v = parse_result(&r);
810 assert_eq!(v["valid"], false, "Expected invalid for: {phone}");
811 }
812 }
813
814 #[tokio::test]
817 async fn test_credit_card_valid() {
818 let s = skill();
819 for card in &["4111111111111111", "5500000000000004", "340000000000009"] {
821 let r = s.execute(call("credit_card", card)).await.unwrap();
822 let v = parse_result(&r);
823 assert_eq!(v["valid"], true, "Expected valid for: {card}");
824 }
825 }
826
827 #[tokio::test]
828 async fn test_credit_card_with_spaces() {
829 let s = skill();
830 let r = s
831 .execute(call("credit_card", "4111 1111 1111 1111"))
832 .await
833 .unwrap();
834 let v = parse_result(&r);
835 assert_eq!(v["valid"], true);
836 }
837
838 #[tokio::test]
839 async fn test_credit_card_invalid_luhn() {
840 let s = skill();
841 let r = s
842 .execute(call("credit_card", "4111111111111112"))
843 .await
844 .unwrap();
845 let v = parse_result(&r);
846 assert_eq!(v["valid"], false);
847 assert!(v["details"].as_str().unwrap().contains("Luhn"));
848 }
849
850 #[tokio::test]
853 async fn test_date_valid() {
854 let s = skill();
855 for d in &["2024-01-01", "2024-02-29", "2023-12-31"] {
856 let r = s.execute(call("date", d)).await.unwrap();
857 let v = parse_result(&r);
858 assert_eq!(v["valid"], true, "Expected valid for: {d}");
859 }
860 }
861
862 #[tokio::test]
863 async fn test_date_invalid() {
864 let s = skill();
865 for d in &["2024-13-01", "2023-02-29", "2024-00-01", "not-a-date"] {
866 let r = s.execute(call("date", d)).await.unwrap();
867 let v = parse_result(&r);
868 assert_eq!(v["valid"], false, "Expected invalid for: {d}");
869 }
870 }
871
872 #[tokio::test]
875 async fn test_datetime_valid() {
876 let s = skill();
877 for dt in &[
878 "2024-01-15T10:30:00Z",
879 "2024-01-15T10:30:00+05:30",
880 "2024-01-15T10:30:00.123Z",
881 "2024-01-15T10:30:00",
882 ] {
883 let r = s.execute(call("datetime", dt)).await.unwrap();
884 let v = parse_result(&r);
885 assert_eq!(v["valid"], true, "Expected valid for: {dt}");
886 }
887 }
888
889 #[tokio::test]
890 async fn test_datetime_invalid() {
891 let s = skill();
892 for dt in &["not-datetime", "2024-13-01T10:00:00Z", "2024-01-15"] {
893 let r = s.execute(call("datetime", dt)).await.unwrap();
894 let v = parse_result(&r);
895 assert_eq!(v["valid"], false, "Expected invalid for: {dt}");
896 }
897 }
898
899 #[tokio::test]
902 async fn test_hex_color_valid() {
903 let s = skill();
904 for color in &["#fff", "#FFF", "#aabbcc", "#AABBCC", "#123456"] {
905 let r = s.execute(call("hex_color", color)).await.unwrap();
906 let v = parse_result(&r);
907 assert_eq!(v["valid"], true, "Expected valid for: {color}");
908 }
909 }
910
911 #[tokio::test]
912 async fn test_hex_color_invalid() {
913 let s = skill();
914 for color in &["#gg0000", "ff0000", "#12345", "#1234567"] {
915 let r = s.execute(call("hex_color", color)).await.unwrap();
916 let v = parse_result(&r);
917 assert_eq!(v["valid"], false, "Expected invalid for: {color}");
918 }
919 }
920
921 #[tokio::test]
924 async fn test_semver_valid() {
925 let s = skill();
926 for sv in &[
927 "1.0.0",
928 "0.1.0",
929 "1.2.3-alpha",
930 "1.2.3-alpha.1",
931 "1.2.3+build.123",
932 ] {
933 let r = s.execute(call("semver", sv)).await.unwrap();
934 let v = parse_result(&r);
935 assert_eq!(v["valid"], true, "Expected valid for: {sv}");
936 }
937 }
938
939 #[tokio::test]
940 async fn test_semver_invalid() {
941 let s = skill();
942 for sv in &["1.0", "v1.0.0", "01.0.0", "1.0.0.0"] {
943 let r = s.execute(call("semver", sv)).await.unwrap();
944 let v = parse_result(&r);
945 assert_eq!(v["valid"], false, "Expected invalid for: {sv}");
946 }
947 }
948
949 #[tokio::test]
952 async fn test_json_valid() {
953 let s = skill();
954 for j in &[
955 r#"{"key": "value"}"#,
956 "[1,2,3]",
957 "\"hello\"",
958 "42",
959 "true",
960 "null",
961 ] {
962 let r = s.execute(call("json", j)).await.unwrap();
963 let v = parse_result(&r);
964 assert_eq!(v["valid"], true, "Expected valid for: {j}");
965 }
966 }
967
968 #[tokio::test]
969 async fn test_json_invalid() {
970 let s = skill();
971 for j in &["{missing: quotes}", "[1,2,", "undefined"] {
972 let r = s.execute(call("json", j)).await.unwrap();
973 let v = parse_result(&r);
974 assert_eq!(v["valid"], false, "Expected invalid for: {j}");
975 }
976 }
977
978 #[tokio::test]
981 async fn test_base64_valid() {
982 let s = skill();
983 for b in &["SGVsbG8=", "SGVsbG8gV29ybGQ=", "dGVzdA=="] {
984 let r = s.execute(call("base64", b)).await.unwrap();
985 let v = parse_result(&r);
986 assert_eq!(v["valid"], true, "Expected valid for: {b}");
987 }
988 }
989
990 #[tokio::test]
991 async fn test_base64_invalid() {
992 let s = skill();
993 for b in &["SGVsbG8!", "not base64 at all!!!"] {
994 let r = s.execute(call("base64", b)).await.unwrap();
995 let v = parse_result(&r);
996 assert_eq!(v["valid"], false, "Expected invalid for: {b}");
997 }
998 }
999
1000 #[tokio::test]
1003 async fn test_domain_valid() {
1004 let s = skill();
1005 for d in &["example.com", "sub.domain.co.uk", "a-b.example.org"] {
1006 let r = s.execute(call("domain", d)).await.unwrap();
1007 let v = parse_result(&r);
1008 assert_eq!(v["valid"], true, "Expected valid for: {d}");
1009 }
1010 }
1011
1012 #[tokio::test]
1013 async fn test_domain_invalid() {
1014 let s = skill();
1015 for d in &[
1016 "-invalid.com",
1017 "no_underscores.com",
1018 ".leading-dot.com",
1019 "a",
1020 ] {
1021 let r = s.execute(call("domain", d)).await.unwrap();
1022 let v = parse_result(&r);
1023 assert_eq!(v["valid"], false, "Expected invalid for: {d}");
1024 }
1025 }
1026
1027 #[tokio::test]
1030 async fn test_mac_valid() {
1031 let s = skill();
1032 for mac in &[
1033 "00:1A:2B:3C:4D:5E",
1034 "aa:bb:cc:dd:ee:ff",
1035 "AA-BB-CC-DD-EE-FF",
1036 ] {
1037 let r = s.execute(call("mac_address", mac)).await.unwrap();
1038 let v = parse_result(&r);
1039 assert_eq!(v["valid"], true, "Expected valid for: {mac}");
1040 }
1041 }
1042
1043 #[tokio::test]
1044 async fn test_mac_invalid() {
1045 let s = skill();
1046 for mac in &["00:1A:2B:3C:4D", "GG:HH:II:JJ:KK:LL", "001A2B3C4D5E"] {
1047 let r = s.execute(call("mac_address", mac)).await.unwrap();
1048 let v = parse_result(&r);
1049 assert_eq!(v["valid"], false, "Expected invalid for: {mac}");
1050 }
1051 }
1052
1053 #[tokio::test]
1056 async fn test_unknown_format() {
1057 let s = skill();
1058 let r = s.execute(call("unknown_format", "test")).await.unwrap();
1059 assert!(r.is_error);
1060 assert!(r.content.contains("Unknown format"));
1061 }
1062
1063 #[tokio::test]
1064 async fn test_empty_format() {
1065 let s = skill();
1066 let c = ToolCall {
1067 id: "t1".to_string(),
1068 name: "data_validator".to_string(),
1069 arguments: serde_json::json!({"format": "", "value": "test"}),
1070 };
1071 let r = s.execute(c).await.unwrap();
1072 assert!(r.is_error);
1073 assert!(r.content.contains("Format parameter is required"));
1074 }
1075
1076 #[tokio::test]
1077 async fn test_empty_value() {
1078 let s = skill();
1079 let c = ToolCall {
1080 id: "t1".to_string(),
1081 name: "data_validator".to_string(),
1082 arguments: serde_json::json!({"format": "email", "value": ""}),
1083 };
1084 let r = s.execute(c).await.unwrap();
1085 assert!(r.is_error);
1086 assert!(r.content.contains("Value parameter is required"));
1087 }
1088
1089 #[test]
1092 fn test_default() {
1093 let s = DataValidatorSkill::default();
1094 assert_eq!(s.descriptor().name, "data_validator");
1095 }
1096}