1use argentor_core::{ArgentorResult, ToolCall, ToolResult};
2use argentor_skills::skill::{Skill, SkillDescriptor};
3use async_trait::async_trait;
4
5pub struct TextTransformSkill {
9 descriptor: SkillDescriptor,
10}
11
12impl TextTransformSkill {
13 pub fn new() -> Self {
15 Self {
16 descriptor: SkillDescriptor {
17 name: "text_transform".to_string(),
18 description: "Perform text manipulation operations: case conversion, trimming, \
19 splitting, joining, replacing, padding, truncation, counting, searching, \
20 and case-style conversions."
21 .to_string(),
22 parameters_schema: serde_json::json!({
23 "type": "object",
24 "properties": {
25 "operation": {
26 "type": "string",
27 "description": "The text operation to perform",
28 "enum": [
29 "uppercase", "lowercase", "title_case", "capitalize",
30 "trim", "trim_start", "trim_end",
31 "reverse", "slug",
32 "split", "join", "replace",
33 "pad_left", "pad_right",
34 "truncate",
35 "word_count", "char_count", "line_count",
36 "repeat",
37 "contains", "starts_with", "ends_with",
38 "extract_between",
39 "camel_case", "snake_case", "kebab_case"
40 ]
41 },
42 "text": {
43 "type": "string",
44 "description": "The input text to transform"
45 },
46 "delimiter": {
47 "type": "string",
48 "description": "Delimiter for split/join operations"
49 },
50 "values": {
51 "type": "array",
52 "items": { "type": "string" },
53 "description": "Array of strings for join operation"
54 },
55 "pattern": {
56 "type": "string",
57 "description": "Pattern to search for in replace operation"
58 },
59 "replacement": {
60 "type": "string",
61 "description": "Replacement string for replace operation"
62 },
63 "width": {
64 "type": "integer",
65 "description": "Target width for pad_left/pad_right"
66 },
67 "char": {
68 "type": "string",
69 "description": "Single padding character (default: space)"
70 },
71 "max_length": {
72 "type": "integer",
73 "description": "Maximum length for truncate operation"
74 },
75 "suffix": {
76 "type": "string",
77 "description": "Suffix appended when truncating (default: \"...\")"
78 },
79 "count": {
80 "type": "integer",
81 "description": "Repetition count for repeat operation (max 1000)"
82 },
83 "substring": {
84 "type": "string",
85 "description": "Substring to search for in contains operation"
86 },
87 "prefix": {
88 "type": "string",
89 "description": "Prefix to check in starts_with operation"
90 },
91 "start_marker": {
92 "type": "string",
93 "description": "Start marker for extract_between"
94 },
95 "end_marker": {
96 "type": "string",
97 "description": "End marker for extract_between"
98 }
99 },
100 "required": ["operation"]
101 }),
102 required_capabilities: vec![],
103 requires_approval: false,
104 },
105 }
106 }
107}
108
109impl Default for TextTransformSkill {
110 fn default() -> Self {
111 Self::new()
112 }
113}
114
115#[async_trait]
116impl Skill for TextTransformSkill {
117 fn descriptor(&self) -> &SkillDescriptor {
118 &self.descriptor
119 }
120
121 async fn execute(&self, call: ToolCall) -> ArgentorResult<ToolResult> {
122 let args = &call.arguments;
123
124 let operation = match args["operation"].as_str() {
125 Some(op) => op,
126 None => {
127 return Ok(ToolResult::error(
128 &call.id,
129 r#"{"error":"Missing required parameter: operation"}"#,
130 ));
131 }
132 };
133
134 let result = match operation {
135 "uppercase" => op_uppercase(args),
136 "lowercase" => op_lowercase(args),
137 "title_case" => op_title_case(args),
138 "capitalize" => op_capitalize(args),
139 "trim" => op_trim(args),
140 "trim_start" => op_trim_start(args),
141 "trim_end" => op_trim_end(args),
142 "reverse" => op_reverse(args),
143 "slug" => op_slug(args),
144 "split" => op_split(args),
145 "join" => op_join(args),
146 "replace" => op_replace(args),
147 "pad_left" => op_pad_left(args),
148 "pad_right" => op_pad_right(args),
149 "truncate" => op_truncate(args),
150 "word_count" => op_word_count(args),
151 "char_count" => op_char_count(args),
152 "line_count" => op_line_count(args),
153 "repeat" => op_repeat(args),
154 "contains" => op_contains(args),
155 "starts_with" => op_starts_with(args),
156 "ends_with" => op_ends_with(args),
157 "extract_between" => op_extract_between(args),
158 "camel_case" => op_camel_case(args),
159 "snake_case" => op_snake_case(args),
160 "kebab_case" => op_kebab_case(args),
161 _ => Err(format!("Unknown operation: {operation}")),
162 };
163
164 match result {
165 Ok(value) => Ok(ToolResult::success(
166 &call.id,
167 serde_json::json!({ "result": value }).to_string(),
168 )),
169 Err(e) => Ok(ToolResult::error(
170 &call.id,
171 serde_json::json!({ "error": e }).to_string(),
172 )),
173 }
174 }
175}
176
177fn require_text(args: &serde_json::Value) -> Result<&str, String> {
181 args["text"]
182 .as_str()
183 .ok_or_else(|| "Missing required parameter: text".to_string())
184}
185
186fn op_uppercase(args: &serde_json::Value) -> Result<serde_json::Value, String> {
191 let text = require_text(args)?;
192 Ok(serde_json::Value::String(text.to_uppercase()))
193}
194
195fn op_lowercase(args: &serde_json::Value) -> Result<serde_json::Value, String> {
196 let text = require_text(args)?;
197 Ok(serde_json::Value::String(text.to_lowercase()))
198}
199
200fn op_title_case(args: &serde_json::Value) -> Result<serde_json::Value, String> {
201 let text = require_text(args)?;
202 let result = text
203 .split_whitespace()
204 .map(|word| {
205 let mut chars = word.chars();
206 match chars.next() {
207 Some(c) => {
208 let upper: String = c.to_uppercase().collect();
209 let lower: String = chars.as_str().to_lowercase();
210 format!("{upper}{lower}")
211 }
212 None => String::new(),
213 }
214 })
215 .collect::<Vec<_>>()
216 .join(" ");
217 Ok(serde_json::Value::String(result))
218}
219
220fn op_capitalize(args: &serde_json::Value) -> Result<serde_json::Value, String> {
221 let text = require_text(args)?;
222 if text.is_empty() {
223 return Ok(serde_json::Value::String(String::new()));
224 }
225 let mut chars = text.chars();
226 let first: String = chars
227 .next()
228 .map(|c| c.to_uppercase().collect())
229 .unwrap_or_default();
230 let rest: String = chars.collect();
231 Ok(serde_json::Value::String(format!("{first}{rest}")))
232}
233
234fn op_trim(args: &serde_json::Value) -> Result<serde_json::Value, String> {
235 let text = require_text(args)?;
236 Ok(serde_json::Value::String(text.trim().to_string()))
237}
238
239fn op_trim_start(args: &serde_json::Value) -> Result<serde_json::Value, String> {
240 let text = require_text(args)?;
241 Ok(serde_json::Value::String(text.trim_start().to_string()))
242}
243
244fn op_trim_end(args: &serde_json::Value) -> Result<serde_json::Value, String> {
245 let text = require_text(args)?;
246 Ok(serde_json::Value::String(text.trim_end().to_string()))
247}
248
249fn op_reverse(args: &serde_json::Value) -> Result<serde_json::Value, String> {
250 let text = require_text(args)?;
251 let reversed: String = text.chars().rev().collect();
252 Ok(serde_json::Value::String(reversed))
253}
254
255fn op_slug(args: &serde_json::Value) -> Result<serde_json::Value, String> {
256 let text = require_text(args)?;
257 let slug: String = text
258 .to_lowercase()
259 .chars()
260 .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
261 .collect();
262 let collapsed = collapse_hyphens(&slug);
264 Ok(serde_json::Value::String(collapsed))
265}
266
267fn collapse_hyphens(s: &str) -> String {
268 let mut result = String::with_capacity(s.len());
269 let mut prev_hyphen = false;
270 for c in s.chars() {
271 if c == '-' {
272 if !prev_hyphen {
273 result.push('-');
274 }
275 prev_hyphen = true;
276 } else {
277 prev_hyphen = false;
278 result.push(c);
279 }
280 }
281 result.trim_matches('-').to_string()
282}
283
284fn op_split(args: &serde_json::Value) -> Result<serde_json::Value, String> {
285 let text = require_text(args)?;
286 let delimiter = args["delimiter"].as_str().unwrap_or(",");
287 let parts: Vec<serde_json::Value> = text
288 .split(delimiter)
289 .map(|s| serde_json::Value::String(s.to_string()))
290 .collect();
291 Ok(serde_json::Value::Array(parts))
292}
293
294fn op_join(args: &serde_json::Value) -> Result<serde_json::Value, String> {
295 let values = args["values"]
296 .as_array()
297 .ok_or_else(|| "Missing required parameter: values (array)".to_string())?;
298 let delimiter = args["delimiter"].as_str().unwrap_or(",");
299 let strings: Vec<&str> = values.iter().filter_map(|v| v.as_str()).collect();
300 Ok(serde_json::Value::String(strings.join(delimiter)))
301}
302
303fn op_replace(args: &serde_json::Value) -> Result<serde_json::Value, String> {
304 let text = require_text(args)?;
305 let pattern = args["pattern"]
306 .as_str()
307 .ok_or_else(|| "Missing required parameter: pattern".to_string())?;
308 let replacement = args["replacement"].as_str().unwrap_or("");
309 Ok(serde_json::Value::String(
310 text.replace(pattern, replacement),
311 ))
312}
313
314fn op_pad_left(args: &serde_json::Value) -> Result<serde_json::Value, String> {
315 let text = require_text(args)?;
316 let width = args["width"]
317 .as_u64()
318 .ok_or_else(|| "Missing required parameter: width".to_string())? as usize;
319 let pad_char = extract_pad_char(args)?;
320 let current_len = text.chars().count();
321 if current_len >= width {
322 return Ok(serde_json::Value::String(text.to_string()));
323 }
324 let padding: String = std::iter::repeat(pad_char)
325 .take(width - current_len)
326 .collect();
327 Ok(serde_json::Value::String(format!("{padding}{text}")))
328}
329
330fn op_pad_right(args: &serde_json::Value) -> Result<serde_json::Value, String> {
331 let text = require_text(args)?;
332 let width = args["width"]
333 .as_u64()
334 .ok_or_else(|| "Missing required parameter: width".to_string())? as usize;
335 let pad_char = extract_pad_char(args)?;
336 let current_len = text.chars().count();
337 if current_len >= width {
338 return Ok(serde_json::Value::String(text.to_string()));
339 }
340 let padding: String = std::iter::repeat(pad_char)
341 .take(width - current_len)
342 .collect();
343 Ok(serde_json::Value::String(format!("{text}{padding}")))
344}
345
346fn extract_pad_char(args: &serde_json::Value) -> Result<char, String> {
347 match args["char"].as_str() {
348 Some(s) => {
349 let mut chars = s.chars();
350 match (chars.next(), chars.next()) {
351 (Some(c), None) => Ok(c),
352 _ => Err("Parameter 'char' must be a single character".to_string()),
353 }
354 }
355 None => Ok(' '),
356 }
357}
358
359fn op_truncate(args: &serde_json::Value) -> Result<serde_json::Value, String> {
360 let text = require_text(args)?;
361 let max_length = args["max_length"]
362 .as_u64()
363 .ok_or_else(|| "Missing required parameter: max_length".to_string())?
364 as usize;
365 let suffix = args["suffix"].as_str().unwrap_or("...");
366
367 let char_count = text.chars().count();
368 if char_count <= max_length {
369 return Ok(serde_json::Value::String(text.to_string()));
370 }
371
372 let suffix_len = suffix.chars().count();
373 if max_length <= suffix_len {
374 let truncated: String = text.chars().take(max_length).collect();
376 return Ok(serde_json::Value::String(truncated));
377 }
378
379 let keep = max_length - suffix_len;
380 let truncated: String = text.chars().take(keep).collect();
381 Ok(serde_json::Value::String(format!("{truncated}{suffix}")))
382}
383
384fn op_word_count(args: &serde_json::Value) -> Result<serde_json::Value, String> {
385 let text = require_text(args)?;
386 let count = text.split_whitespace().count();
387 Ok(serde_json::json!(count))
388}
389
390fn op_char_count(args: &serde_json::Value) -> Result<serde_json::Value, String> {
391 let text = require_text(args)?;
392 let count = text.chars().count();
393 Ok(serde_json::json!(count))
394}
395
396fn op_line_count(args: &serde_json::Value) -> Result<serde_json::Value, String> {
397 let text = require_text(args)?;
398 let count = if text.is_empty() {
399 0
400 } else {
401 text.lines().count()
402 };
403 Ok(serde_json::json!(count))
404}
405
406fn op_repeat(args: &serde_json::Value) -> Result<serde_json::Value, String> {
407 let text = require_text(args)?;
408 let count = args["count"]
409 .as_u64()
410 .ok_or_else(|| "Missing required parameter: count".to_string())?;
411 if count > 1000 {
412 return Err("count must not exceed 1000".to_string());
413 }
414 Ok(serde_json::Value::String(text.repeat(count as usize)))
415}
416
417fn op_contains(args: &serde_json::Value) -> Result<serde_json::Value, String> {
418 let text = require_text(args)?;
419 let substring = args["substring"]
420 .as_str()
421 .ok_or_else(|| "Missing required parameter: substring".to_string())?;
422 Ok(serde_json::Value::Bool(text.contains(substring)))
423}
424
425fn op_starts_with(args: &serde_json::Value) -> Result<serde_json::Value, String> {
426 let text = require_text(args)?;
427 let prefix = args["prefix"]
428 .as_str()
429 .ok_or_else(|| "Missing required parameter: prefix".to_string())?;
430 Ok(serde_json::Value::Bool(text.starts_with(prefix)))
431}
432
433fn op_ends_with(args: &serde_json::Value) -> Result<serde_json::Value, String> {
434 let text = require_text(args)?;
435 let suffix = args["suffix"]
436 .as_str()
437 .ok_or_else(|| "Missing required parameter: suffix".to_string())?;
438 Ok(serde_json::Value::Bool(text.ends_with(suffix)))
439}
440
441fn op_extract_between(args: &serde_json::Value) -> Result<serde_json::Value, String> {
442 let text = require_text(args)?;
443 let start_marker = args["start_marker"]
444 .as_str()
445 .ok_or_else(|| "Missing required parameter: start_marker".to_string())?;
446 let end_marker = args["end_marker"]
447 .as_str()
448 .ok_or_else(|| "Missing required parameter: end_marker".to_string())?;
449
450 let start_pos = text
451 .find(start_marker)
452 .ok_or_else(|| format!("Start marker '{start_marker}' not found in text"))?;
453 let after_start = start_pos + start_marker.len();
454 let end_pos = text[after_start..]
455 .find(end_marker)
456 .ok_or_else(|| format!("End marker '{end_marker}' not found after start marker"))?;
457
458 let extracted = &text[after_start..after_start + end_pos];
459 Ok(serde_json::Value::String(extracted.to_string()))
460}
461
462fn split_into_words(text: &str) -> Vec<String> {
464 let mut words = Vec::new();
465 let mut current = String::new();
466
467 for c in text.chars() {
468 if c.is_alphanumeric() {
469 current.push(c);
470 } else if !current.is_empty() {
471 words.push(current.clone());
472 current.clear();
473 }
474 }
475 if !current.is_empty() {
476 words.push(current);
477 }
478
479 let mut result = Vec::new();
481 for word in words {
482 let mut sub = String::new();
483 let chars: Vec<char> = word.chars().collect();
484 for i in 0..chars.len() {
485 if i > 0 && chars[i].is_uppercase() && chars[i - 1].is_lowercase() {
486 result.push(sub.clone());
487 sub.clear();
488 }
489 sub.push(chars[i]);
490 }
491 if !sub.is_empty() {
492 result.push(sub);
493 }
494 }
495
496 result
497}
498
499fn op_camel_case(args: &serde_json::Value) -> Result<serde_json::Value, String> {
500 let text = require_text(args)?;
501 let words = split_into_words(text);
502 let mut result = String::new();
503 for (i, word) in words.iter().enumerate() {
504 if i == 0 {
505 result.push_str(&word.to_lowercase());
506 } else {
507 let mut chars = word.chars();
508 if let Some(c) = chars.next() {
509 let upper: String = c.to_uppercase().collect();
510 result.push_str(&upper);
511 result.push_str(&chars.as_str().to_lowercase());
512 }
513 }
514 }
515 Ok(serde_json::Value::String(result))
516}
517
518fn op_snake_case(args: &serde_json::Value) -> Result<serde_json::Value, String> {
519 let text = require_text(args)?;
520 let words = split_into_words(text);
521 let result: String = words
522 .iter()
523 .map(|w| w.to_lowercase())
524 .collect::<Vec<_>>()
525 .join("_");
526 Ok(serde_json::Value::String(result))
527}
528
529fn op_kebab_case(args: &serde_json::Value) -> Result<serde_json::Value, String> {
530 let text = require_text(args)?;
531 let words = split_into_words(text);
532 let result: String = words
533 .iter()
534 .map(|w| w.to_lowercase())
535 .collect::<Vec<_>>()
536 .join("-");
537 Ok(serde_json::Value::String(result))
538}
539
540#[cfg(test)]
545#[allow(clippy::unwrap_used, clippy::expect_used)]
546mod tests {
547 use super::*;
548 use serde_json::json;
549
550 fn make_call(args: serde_json::Value) -> ToolCall {
551 ToolCall {
552 id: "test".to_string(),
553 name: "text_transform".to_string(),
554 arguments: args,
555 }
556 }
557
558 async fn exec(args: serde_json::Value) -> ToolResult {
559 let skill = TextTransformSkill::new();
560 skill.execute(make_call(args)).await.unwrap()
561 }
562
563 fn parse_result(result: &ToolResult) -> serde_json::Value {
564 serde_json::from_str(&result.content).unwrap()
565 }
566
567 #[test]
570 fn test_descriptor() {
571 let skill = TextTransformSkill::new();
572 let desc = skill.descriptor();
573 assert_eq!(desc.name, "text_transform");
574 assert!(desc.required_capabilities.is_empty());
575 assert!(desc.parameters_schema["properties"]["operation"].is_object());
576 }
577
578 #[test]
579 fn test_default_trait() {
580 let skill = TextTransformSkill::default();
581 assert_eq!(skill.descriptor().name, "text_transform");
582 }
583
584 #[tokio::test]
587 async fn test_missing_operation() {
588 let r = exec(json!({"text": "hello"})).await;
589 assert!(r.is_error);
590 assert!(r.content.contains("Missing required parameter: operation"));
591 }
592
593 #[tokio::test]
594 async fn test_unknown_operation() {
595 let r = exec(json!({"operation": "foobar", "text": "hello"})).await;
596 assert!(r.is_error);
597 assert!(r.content.contains("Unknown operation: foobar"));
598 }
599
600 #[tokio::test]
603 async fn test_uppercase() {
604 let r = exec(json!({"operation": "uppercase", "text": "hello World"})).await;
605 assert!(!r.is_error);
606 let v = parse_result(&r);
607 assert_eq!(v["result"], "HELLO WORLD");
608 }
609
610 #[tokio::test]
611 async fn test_lowercase() {
612 let r = exec(json!({"operation": "lowercase", "text": "Hello WORLD"})).await;
613 let v = parse_result(&r);
614 assert_eq!(v["result"], "hello world");
615 }
616
617 #[tokio::test]
618 async fn test_uppercase_missing_text() {
619 let r = exec(json!({"operation": "uppercase"})).await;
620 assert!(r.is_error);
621 assert!(r.content.contains("Missing required parameter: text"));
622 }
623
624 #[tokio::test]
627 async fn test_title_case() {
628 let r = exec(json!({"operation": "title_case", "text": "hello world foo"})).await;
629 let v = parse_result(&r);
630 assert_eq!(v["result"], "Hello World Foo");
631 }
632
633 #[tokio::test]
634 async fn test_title_case_mixed() {
635 let r = exec(json!({"operation": "title_case", "text": "hELLO wORLD"})).await;
636 let v = parse_result(&r);
637 assert_eq!(v["result"], "Hello World");
638 }
639
640 #[tokio::test]
643 async fn test_capitalize() {
644 let r = exec(json!({"operation": "capitalize", "text": "hello world"})).await;
645 let v = parse_result(&r);
646 assert_eq!(v["result"], "Hello world");
647 }
648
649 #[tokio::test]
650 async fn test_capitalize_empty() {
651 let r = exec(json!({"operation": "capitalize", "text": ""})).await;
652 let v = parse_result(&r);
653 assert_eq!(v["result"], "");
654 }
655
656 #[tokio::test]
659 async fn test_trim() {
660 let r = exec(json!({"operation": "trim", "text": " hello "})).await;
661 let v = parse_result(&r);
662 assert_eq!(v["result"], "hello");
663 }
664
665 #[tokio::test]
666 async fn test_trim_start() {
667 let r = exec(json!({"operation": "trim_start", "text": " hello "})).await;
668 let v = parse_result(&r);
669 assert_eq!(v["result"], "hello ");
670 }
671
672 #[tokio::test]
673 async fn test_trim_end() {
674 let r = exec(json!({"operation": "trim_end", "text": " hello "})).await;
675 let v = parse_result(&r);
676 assert_eq!(v["result"], " hello");
677 }
678
679 #[tokio::test]
682 async fn test_reverse() {
683 let r = exec(json!({"operation": "reverse", "text": "abcde"})).await;
684 let v = parse_result(&r);
685 assert_eq!(v["result"], "edcba");
686 }
687
688 #[tokio::test]
689 async fn test_reverse_unicode() {
690 let r = exec(json!({"operation": "reverse", "text": "hola"})).await;
691 let v = parse_result(&r);
692 assert_eq!(v["result"], "aloh");
693 }
694
695 #[tokio::test]
698 async fn test_slug_basic() {
699 let r = exec(json!({"operation": "slug", "text": "Hello World!"})).await;
700 let v = parse_result(&r);
701 assert_eq!(v["result"], "hello-world");
702 }
703
704 #[tokio::test]
705 async fn test_slug_special_chars() {
706 let r = exec(json!({"operation": "slug", "text": " Foo Bar & Baz!! "})).await;
707 let v = parse_result(&r);
708 assert_eq!(v["result"], "foo-bar-baz");
709 }
710
711 #[tokio::test]
714 async fn test_split_default_delimiter() {
715 let r = exec(json!({"operation": "split", "text": "a,b,c"})).await;
716 let v = parse_result(&r);
717 assert_eq!(v["result"], json!(["a", "b", "c"]));
718 }
719
720 #[tokio::test]
721 async fn test_split_custom_delimiter() {
722 let r = exec(json!({"operation": "split", "text": "a|b|c", "delimiter": "|"})).await;
723 let v = parse_result(&r);
724 assert_eq!(v["result"], json!(["a", "b", "c"]));
725 }
726
727 #[tokio::test]
730 async fn test_join_default_delimiter() {
731 let r = exec(json!({"operation": "join", "values": ["a", "b", "c"]})).await;
732 let v = parse_result(&r);
733 assert_eq!(v["result"], "a,b,c");
734 }
735
736 #[tokio::test]
737 async fn test_join_custom_delimiter() {
738 let r = exec(json!({"operation": "join", "values": ["x", "y"], "delimiter": " - "})).await;
739 let v = parse_result(&r);
740 assert_eq!(v["result"], "x - y");
741 }
742
743 #[tokio::test]
744 async fn test_join_missing_values() {
745 let r = exec(json!({"operation": "join"})).await;
746 assert!(r.is_error);
747 assert!(r.content.contains("values"));
748 }
749
750 #[tokio::test]
753 async fn test_replace() {
754 let r = exec(json!({
755 "operation": "replace",
756 "text": "hello world",
757 "pattern": "world",
758 "replacement": "rust"
759 }))
760 .await;
761 let v = parse_result(&r);
762 assert_eq!(v["result"], "hello rust");
763 }
764
765 #[tokio::test]
766 async fn test_replace_no_replacement() {
767 let r = exec(json!({
768 "operation": "replace",
769 "text": "hello world",
770 "pattern": " world"
771 }))
772 .await;
773 let v = parse_result(&r);
774 assert_eq!(v["result"], "hello");
775 }
776
777 #[tokio::test]
778 async fn test_replace_missing_pattern() {
779 let r = exec(json!({"operation": "replace", "text": "hello"})).await;
780 assert!(r.is_error);
781 assert!(r.content.contains("pattern"));
782 }
783
784 #[tokio::test]
787 async fn test_pad_left_default_char() {
788 let r = exec(json!({"operation": "pad_left", "text": "hi", "width": 5})).await;
789 let v = parse_result(&r);
790 assert_eq!(v["result"], " hi");
791 }
792
793 #[tokio::test]
794 async fn test_pad_left_custom_char() {
795 let r = exec(json!({"operation": "pad_left", "text": "42", "width": 5, "char": "0"})).await;
796 let v = parse_result(&r);
797 assert_eq!(v["result"], "00042");
798 }
799
800 #[tokio::test]
801 async fn test_pad_right_default_char() {
802 let r = exec(json!({"operation": "pad_right", "text": "hi", "width": 5})).await;
803 let v = parse_result(&r);
804 assert_eq!(v["result"], "hi ");
805 }
806
807 #[tokio::test]
808 async fn test_pad_no_change_when_longer() {
809 let r = exec(json!({"operation": "pad_left", "text": "hello", "width": 3})).await;
810 let v = parse_result(&r);
811 assert_eq!(v["result"], "hello");
812 }
813
814 #[tokio::test]
815 async fn test_pad_invalid_char() {
816 let r = exec(json!({"operation": "pad_left", "text": "x", "width": 5, "char": "ab"})).await;
817 assert!(r.is_error);
818 assert!(r.content.contains("single character"));
819 }
820
821 #[tokio::test]
822 async fn test_pad_missing_width() {
823 let r = exec(json!({"operation": "pad_left", "text": "x"})).await;
824 assert!(r.is_error);
825 assert!(r.content.contains("width"));
826 }
827
828 #[tokio::test]
831 async fn test_truncate_with_default_suffix() {
832 let r =
833 exec(json!({"operation": "truncate", "text": "hello world", "max_length": 8})).await;
834 let v = parse_result(&r);
835 assert_eq!(v["result"], "hello...");
836 }
837
838 #[tokio::test]
839 async fn test_truncate_custom_suffix() {
840 let r = exec(json!({
841 "operation": "truncate",
842 "text": "hello world",
843 "max_length": 8,
844 "suffix": ".."
845 }))
846 .await;
847 let v = parse_result(&r);
848 assert_eq!(v["result"], "hello ..");
849 }
850
851 #[tokio::test]
852 async fn test_truncate_no_truncation_needed() {
853 let r = exec(json!({"operation": "truncate", "text": "hi", "max_length": 10})).await;
854 let v = parse_result(&r);
855 assert_eq!(v["result"], "hi");
856 }
857
858 #[tokio::test]
859 async fn test_truncate_very_short_max() {
860 let r = exec(json!({"operation": "truncate", "text": "hello", "max_length": 2})).await;
861 let v = parse_result(&r);
862 assert_eq!(v["result"], "he");
863 }
864
865 #[tokio::test]
866 async fn test_truncate_missing_max_length() {
867 let r = exec(json!({"operation": "truncate", "text": "hello"})).await;
868 assert!(r.is_error);
869 assert!(r.content.contains("max_length"));
870 }
871
872 #[tokio::test]
875 async fn test_word_count() {
876 let r = exec(json!({"operation": "word_count", "text": "hello world foo"})).await;
877 let v = parse_result(&r);
878 assert_eq!(v["result"], 3);
879 }
880
881 #[tokio::test]
882 async fn test_word_count_empty() {
883 let r = exec(json!({"operation": "word_count", "text": ""})).await;
884 let v = parse_result(&r);
885 assert_eq!(v["result"], 0);
886 }
887
888 #[tokio::test]
889 async fn test_char_count() {
890 let r = exec(json!({"operation": "char_count", "text": "hello"})).await;
891 let v = parse_result(&r);
892 assert_eq!(v["result"], 5);
893 }
894
895 #[tokio::test]
896 async fn test_char_count_unicode() {
897 let r = exec(json!({"operation": "char_count", "text": "cafe\u{0301}"})).await;
898 let v = parse_result(&r);
899 assert_eq!(v["result"], 5);
901 }
902
903 #[tokio::test]
904 async fn test_line_count() {
905 let r = exec(json!({"operation": "line_count", "text": "a\nb\nc"})).await;
906 let v = parse_result(&r);
907 assert_eq!(v["result"], 3);
908 }
909
910 #[tokio::test]
911 async fn test_line_count_empty() {
912 let r = exec(json!({"operation": "line_count", "text": ""})).await;
913 let v = parse_result(&r);
914 assert_eq!(v["result"], 0);
915 }
916
917 #[tokio::test]
918 async fn test_line_count_single_line() {
919 let r = exec(json!({"operation": "line_count", "text": "no newline"})).await;
920 let v = parse_result(&r);
921 assert_eq!(v["result"], 1);
922 }
923
924 #[tokio::test]
927 async fn test_repeat() {
928 let r = exec(json!({"operation": "repeat", "text": "ab", "count": 3})).await;
929 let v = parse_result(&r);
930 assert_eq!(v["result"], "ababab");
931 }
932
933 #[tokio::test]
934 async fn test_repeat_zero() {
935 let r = exec(json!({"operation": "repeat", "text": "ab", "count": 0})).await;
936 let v = parse_result(&r);
937 assert_eq!(v["result"], "");
938 }
939
940 #[tokio::test]
941 async fn test_repeat_exceeds_max() {
942 let r = exec(json!({"operation": "repeat", "text": "x", "count": 1001})).await;
943 assert!(r.is_error);
944 assert!(r.content.contains("1000"));
945 }
946
947 #[tokio::test]
948 async fn test_repeat_missing_count() {
949 let r = exec(json!({"operation": "repeat", "text": "x"})).await;
950 assert!(r.is_error);
951 assert!(r.content.contains("count"));
952 }
953
954 #[tokio::test]
957 async fn test_contains_true() {
958 let r = exec(json!({"operation": "contains", "text": "hello world", "substring": "world"}))
959 .await;
960 let v = parse_result(&r);
961 assert_eq!(v["result"], true);
962 }
963
964 #[tokio::test]
965 async fn test_contains_false() {
966 let r =
967 exec(json!({"operation": "contains", "text": "hello world", "substring": "xyz"})).await;
968 let v = parse_result(&r);
969 assert_eq!(v["result"], false);
970 }
971
972 #[tokio::test]
973 async fn test_contains_missing_substring() {
974 let r = exec(json!({"operation": "contains", "text": "hello"})).await;
975 assert!(r.is_error);
976 assert!(r.content.contains("substring"));
977 }
978
979 #[tokio::test]
980 async fn test_starts_with_true() {
981 let r = exec(json!({"operation": "starts_with", "text": "hello world", "prefix": "hello"}))
982 .await;
983 let v = parse_result(&r);
984 assert_eq!(v["result"], true);
985 }
986
987 #[tokio::test]
988 async fn test_starts_with_false() {
989 let r = exec(json!({"operation": "starts_with", "text": "hello world", "prefix": "world"}))
990 .await;
991 let v = parse_result(&r);
992 assert_eq!(v["result"], false);
993 }
994
995 #[tokio::test]
996 async fn test_ends_with_true() {
997 let r =
998 exec(json!({"operation": "ends_with", "text": "hello world", "suffix": "world"})).await;
999 let v = parse_result(&r);
1000 assert_eq!(v["result"], true);
1001 }
1002
1003 #[tokio::test]
1004 async fn test_ends_with_false() {
1005 let r =
1006 exec(json!({"operation": "ends_with", "text": "hello world", "suffix": "hello"})).await;
1007 let v = parse_result(&r);
1008 assert_eq!(v["result"], false);
1009 }
1010
1011 #[tokio::test]
1014 async fn test_extract_between() {
1015 let r = exec(json!({
1016 "operation": "extract_between",
1017 "text": "foo [bar] baz",
1018 "start_marker": "[",
1019 "end_marker": "]"
1020 }))
1021 .await;
1022 let v = parse_result(&r);
1023 assert_eq!(v["result"], "bar");
1024 }
1025
1026 #[tokio::test]
1027 async fn test_extract_between_html() {
1028 let r = exec(json!({
1029 "operation": "extract_between",
1030 "text": "<title>My Page</title>",
1031 "start_marker": "<title>",
1032 "end_marker": "</title>"
1033 }))
1034 .await;
1035 let v = parse_result(&r);
1036 assert_eq!(v["result"], "My Page");
1037 }
1038
1039 #[tokio::test]
1040 async fn test_extract_between_start_not_found() {
1041 let r = exec(json!({
1042 "operation": "extract_between",
1043 "text": "hello world",
1044 "start_marker": "<<",
1045 "end_marker": ">>"
1046 }))
1047 .await;
1048 assert!(r.is_error);
1049 assert!(r.content.contains("Start marker"));
1050 }
1051
1052 #[tokio::test]
1053 async fn test_extract_between_end_not_found() {
1054 let r = exec(json!({
1055 "operation": "extract_between",
1056 "text": "hello << world",
1057 "start_marker": "<<",
1058 "end_marker": ">>"
1059 }))
1060 .await;
1061 assert!(r.is_error);
1062 assert!(r.content.contains("End marker"));
1063 }
1064
1065 #[tokio::test]
1068 async fn test_camel_case_from_spaces() {
1069 let r = exec(json!({"operation": "camel_case", "text": "hello world foo"})).await;
1070 let v = parse_result(&r);
1071 assert_eq!(v["result"], "helloWorldFoo");
1072 }
1073
1074 #[tokio::test]
1075 async fn test_camel_case_from_snake() {
1076 let r = exec(json!({"operation": "camel_case", "text": "my_variable_name"})).await;
1077 let v = parse_result(&r);
1078 assert_eq!(v["result"], "myVariableName");
1079 }
1080
1081 #[tokio::test]
1082 async fn test_camel_case_from_kebab() {
1083 let r = exec(json!({"operation": "camel_case", "text": "my-component-name"})).await;
1084 let v = parse_result(&r);
1085 assert_eq!(v["result"], "myComponentName");
1086 }
1087
1088 #[tokio::test]
1089 async fn test_snake_case_from_camel() {
1090 let r = exec(json!({"operation": "snake_case", "text": "myVariableName"})).await;
1091 let v = parse_result(&r);
1092 assert_eq!(v["result"], "my_variable_name");
1093 }
1094
1095 #[tokio::test]
1096 async fn test_snake_case_from_spaces() {
1097 let r = exec(json!({"operation": "snake_case", "text": "Hello World Foo"})).await;
1098 let v = parse_result(&r);
1099 assert_eq!(v["result"], "hello_world_foo");
1100 }
1101
1102 #[tokio::test]
1103 async fn test_kebab_case_from_camel() {
1104 let r = exec(json!({"operation": "kebab_case", "text": "myComponentName"})).await;
1105 let v = parse_result(&r);
1106 assert_eq!(v["result"], "my-component-name");
1107 }
1108
1109 #[tokio::test]
1110 async fn test_kebab_case_from_snake() {
1111 let r = exec(json!({"operation": "kebab_case", "text": "my_variable_name"})).await;
1112 let v = parse_result(&r);
1113 assert_eq!(v["result"], "my-variable-name");
1114 }
1115
1116 #[tokio::test]
1119 async fn test_empty_text_uppercase() {
1120 let r = exec(json!({"operation": "uppercase", "text": ""})).await;
1121 let v = parse_result(&r);
1122 assert_eq!(v["result"], "");
1123 }
1124
1125 #[tokio::test]
1126 async fn test_split_empty_text() {
1127 let r = exec(json!({"operation": "split", "text": ""})).await;
1128 let v = parse_result(&r);
1129 assert_eq!(v["result"], json!([""]));
1130 }
1131
1132 #[tokio::test]
1133 async fn test_repeat_at_boundary() {
1134 let r = exec(json!({"operation": "repeat", "text": "x", "count": 1000})).await;
1135 assert!(!r.is_error);
1136 let v = parse_result(&r);
1137 assert_eq!(v["result"].as_str().unwrap().len(), 1000);
1138 }
1139
1140 #[tokio::test]
1141 async fn test_slug_already_clean() {
1142 let r = exec(json!({"operation": "slug", "text": "already-clean"})).await;
1143 let v = parse_result(&r);
1144 assert_eq!(v["result"], "already-clean");
1145 }
1146
1147 #[tokio::test]
1148 async fn test_replace_multiple_occurrences() {
1149 let r = exec(json!({
1150 "operation": "replace",
1151 "text": "aaa bbb aaa",
1152 "pattern": "aaa",
1153 "replacement": "xxx"
1154 }))
1155 .await;
1156 let v = parse_result(&r);
1157 assert_eq!(v["result"], "xxx bbb xxx");
1158 }
1159
1160 #[tokio::test]
1161 async fn test_join_empty_array() {
1162 let r = exec(json!({"operation": "join", "values": []})).await;
1163 let v = parse_result(&r);
1164 assert_eq!(v["result"], "");
1165 }
1166
1167 #[tokio::test]
1168 async fn test_pad_right_custom_char() {
1169 let r =
1170 exec(json!({"operation": "pad_right", "text": "hi", "width": 6, "char": "."})).await;
1171 let v = parse_result(&r);
1172 assert_eq!(v["result"], "hi....");
1173 }
1174
1175 #[tokio::test]
1176 async fn test_truncate_exact_length() {
1177 let r = exec(json!({"operation": "truncate", "text": "hello", "max_length": 5})).await;
1178 let v = parse_result(&r);
1179 assert_eq!(v["result"], "hello");
1180 }
1181
1182 #[tokio::test]
1183 async fn test_word_count_extra_whitespace() {
1184 let r = exec(json!({"operation": "word_count", "text": " hello world "})).await;
1185 let v = parse_result(&r);
1186 assert_eq!(v["result"], 2);
1187 }
1188
1189 #[tokio::test]
1190 async fn test_camel_case_single_word() {
1191 let r = exec(json!({"operation": "camel_case", "text": "hello"})).await;
1192 let v = parse_result(&r);
1193 assert_eq!(v["result"], "hello");
1194 }
1195
1196 #[tokio::test]
1197 async fn test_snake_case_single_word() {
1198 let r = exec(json!({"operation": "snake_case", "text": "Hello"})).await;
1199 let v = parse_result(&r);
1200 assert_eq!(v["result"], "hello");
1201 }
1202
1203 #[tokio::test]
1204 async fn test_kebab_case_with_numbers() {
1205 let r = exec(json!({"operation": "kebab_case", "text": "version 2 release"})).await;
1206 let v = parse_result(&r);
1207 assert_eq!(v["result"], "version-2-release");
1208 }
1209
1210 #[tokio::test]
1211 async fn test_extract_between_empty_content() {
1212 let r = exec(json!({
1213 "operation": "extract_between",
1214 "text": "[]",
1215 "start_marker": "[",
1216 "end_marker": "]"
1217 }))
1218 .await;
1219 let v = parse_result(&r);
1220 assert_eq!(v["result"], "");
1221 }
1222
1223 #[tokio::test]
1224 async fn test_contains_empty_substring() {
1225 let r = exec(json!({"operation": "contains", "text": "hello", "substring": ""})).await;
1226 let v = parse_result(&r);
1227 assert_eq!(v["result"], true);
1228 }
1229
1230 #[tokio::test]
1231 async fn test_reverse_empty() {
1232 let r = exec(json!({"operation": "reverse", "text": ""})).await;
1233 let v = parse_result(&r);
1234 assert_eq!(v["result"], "");
1235 }
1236}