Skip to main content

pepl_stdlib/modules/
string.rs

1//! The `string` module — 20 functions.
2//!
3//! | Function           | Signature                                              | Description                      |
4//! |--------------------|--------------------------------------------------------|----------------------------------|
5//! | `string.length`    | `(s: string) -> number`                                | Number of characters             |
6//! | `string.concat`    | `(a: string, b: string) -> string`                     | Concatenate two strings          |
7//! | `string.contains`  | `(haystack: string, needle: string) -> bool`           | True if needle found             |
8//! | `string.slice`     | `(s: string, start: number, end: number) -> string`   | Substring \[start, end)          |
9//! | `string.trim`      | `(s: string) -> string`                                | Remove leading/trailing WS       |
10//! | `string.split`     | `(s: string, delimiter: string) -> list<string>`       | Split by delimiter               |
11//! | `string.to_upper`  | `(s: string) -> string`                                | Uppercase                        |
12//! | `string.to_lower`  | `(s: string) -> string`                                | Lowercase                        |
13//! | `string.starts_with` | `(s: string, prefix: string) -> bool`                | Prefix check                     |
14//! | `string.ends_with` | `(s: string, suffix: string) -> bool`                  | Suffix check                     |
15//! | `string.replace`   | `(s: string, old: string, new: string) -> string`     | Replace first occurrence         |
16//! | `string.replace_all` | `(s: string, old: string, new: string) -> string`   | Replace all occurrences          |
17//! | `string.pad_start` | `(s: string, length: number, pad: string) -> string`  | Left-pad to target length        |
18//! | `string.pad_end`   | `(s: string, length: number, pad: string) -> string`  | Right-pad to target length       |
19//! | `string.repeat`    | `(s: string, count: number) -> string`                 | Repeat string N times            |
20//! | `string.join`      | `(items: list<string>, separator: string) -> string`   | Join list with separator         |
21//! | `string.format`    | `(template: string, values: record) -> string`        | `{key}` placeholder replacement  |
22//! | `string.from`      | `(value: any) -> string`                               | Any value to string              |
23//! | `string.is_empty`  | `(s: string) -> bool`                                  | True if zero length              |
24//! | `string.index_of`  | `(s: string, sub: string) -> number`                   | Index of sub, or -1              |
25
26use crate::error::StdlibError;
27use crate::module::StdlibModule;
28use crate::value::Value;
29
30/// The `string` stdlib module.
31pub struct StringModule;
32
33impl StringModule {
34    pub fn new() -> Self {
35        Self
36    }
37}
38
39impl Default for StringModule {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45impl StdlibModule for StringModule {
46    fn name(&self) -> &'static str {
47        "string"
48    }
49
50    fn has_function(&self, function: &str) -> bool {
51        matches!(
52            function,
53            "length"
54                | "concat"
55                | "contains"
56                | "slice"
57                | "trim"
58                | "split"
59                | "to_upper"
60                | "to_lower"
61                | "starts_with"
62                | "ends_with"
63                | "replace"
64                | "replace_all"
65                | "pad_start"
66                | "pad_end"
67                | "repeat"
68                | "join"
69                | "format"
70                | "from"
71                | "is_empty"
72                | "index_of"
73        )
74    }
75
76    fn call(&self, function: &str, args: Vec<Value>) -> Result<Value, StdlibError> {
77        match function {
78            "length" => self.length(args),
79            "concat" => self.concat(args),
80            "contains" => self.contains(args),
81            "slice" => self.slice(args),
82            "trim" => self.trim(args),
83            "split" => self.split(args),
84            "to_upper" => self.to_upper(args),
85            "to_lower" => self.to_lower(args),
86            "starts_with" => self.starts_with(args),
87            "ends_with" => self.ends_with(args),
88            "replace" => self.replace(args),
89            "replace_all" => self.replace_all(args),
90            "pad_start" => self.pad_start(args),
91            "pad_end" => self.pad_end(args),
92            "repeat" => self.repeat(args),
93            "join" => self.join(args),
94            "format" => self.format(args),
95            "from" => self.value_to_string(args),
96            "is_empty" => self.is_empty(args),
97            "index_of" => self.index_of(args),
98            _ => Err(StdlibError::unknown_function("string", function)),
99        }
100    }
101}
102
103// ── Helpers ───────────────────────────────────────────────────────────────────
104
105/// Extract a single string argument.
106fn expect_one_string(fn_name: &str, args: &[Value]) -> Result<String, StdlibError> {
107    if args.len() != 1 {
108        return Err(StdlibError::wrong_args(fn_name, 1, args.len()));
109    }
110    match &args[0] {
111        Value::String(s) => Ok(s.clone()),
112        other => Err(StdlibError::type_mismatch(
113            fn_name,
114            1,
115            "string",
116            other.type_name(),
117        )),
118    }
119}
120
121/// Extract two string arguments.
122fn expect_two_strings(fn_name: &str, args: &[Value]) -> Result<(String, String), StdlibError> {
123    if args.len() != 2 {
124        return Err(StdlibError::wrong_args(fn_name, 2, args.len()));
125    }
126    let a = match &args[0] {
127        Value::String(s) => s.clone(),
128        other => {
129            return Err(StdlibError::type_mismatch(
130                fn_name,
131                1,
132                "string",
133                other.type_name(),
134            ));
135        }
136    };
137    let b = match &args[1] {
138        Value::String(s) => s.clone(),
139        other => {
140            return Err(StdlibError::type_mismatch(
141                fn_name,
142                2,
143                "string",
144                other.type_name(),
145            ));
146        }
147    };
148    Ok((a, b))
149}
150
151/// Extract three string arguments.
152fn expect_three_strings(
153    fn_name: &str,
154    args: &[Value],
155) -> Result<(String, String, String), StdlibError> {
156    if args.len() != 3 {
157        return Err(StdlibError::wrong_args(fn_name, 3, args.len()));
158    }
159    let a = match &args[0] {
160        Value::String(s) => s.clone(),
161        other => {
162            return Err(StdlibError::type_mismatch(
163                fn_name,
164                1,
165                "string",
166                other.type_name(),
167            ));
168        }
169    };
170    let b = match &args[1] {
171        Value::String(s) => s.clone(),
172        other => {
173            return Err(StdlibError::type_mismatch(
174                fn_name,
175                2,
176                "string",
177                other.type_name(),
178            ));
179        }
180    };
181    let c = match &args[2] {
182        Value::String(s) => s.clone(),
183        other => {
184            return Err(StdlibError::type_mismatch(
185                fn_name,
186                3,
187                "string",
188                other.type_name(),
189            ));
190        }
191    };
192    Ok((a, b, c))
193}
194
195// ── Function implementations ──────────────────────────────────────────────────
196
197impl StringModule {
198    /// `string.length(s: string) -> number`
199    ///
200    /// Returns the number of Unicode characters (not bytes).
201    fn length(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
202        let s = expect_one_string("string.length", &args)?;
203        Ok(Value::Number(s.chars().count() as f64))
204    }
205
206    /// `string.concat(a: string, b: string) -> string`
207    fn concat(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
208        let (a, b) = expect_two_strings("string.concat", &args)?;
209        Ok(Value::String(format!("{a}{b}")))
210    }
211
212    /// `string.contains(haystack: string, needle: string) -> bool`
213    fn contains(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
214        let (haystack, needle) = expect_two_strings("string.contains", &args)?;
215        Ok(Value::Bool(haystack.contains(&needle)))
216    }
217
218    /// `string.slice(s: string, start: number, end: number) -> string`
219    ///
220    /// Substring from start (inclusive) to end (exclusive).
221    /// Indices are character-based (not byte-based).
222    /// Clamps out-of-range indices to valid bounds.
223    fn slice(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
224        if args.len() != 3 {
225            return Err(StdlibError::wrong_args("string.slice", 3, args.len()));
226        }
227        let s = match &args[0] {
228            Value::String(s) => s.clone(),
229            other => {
230                return Err(StdlibError::type_mismatch(
231                    "string.slice",
232                    1,
233                    "string",
234                    other.type_name(),
235                ));
236            }
237        };
238        let start = match &args[1] {
239            Value::Number(n) => *n,
240            other => {
241                return Err(StdlibError::type_mismatch(
242                    "string.slice",
243                    2,
244                    "number",
245                    other.type_name(),
246                ));
247            }
248        };
249        let end = match &args[2] {
250            Value::Number(n) => *n,
251            other => {
252                return Err(StdlibError::type_mismatch(
253                    "string.slice",
254                    3,
255                    "number",
256                    other.type_name(),
257                ));
258            }
259        };
260
261        let len = s.chars().count() as isize;
262        let start = (start as isize).clamp(0, len) as usize;
263        let end = (end as isize).clamp(0, len) as usize;
264
265        if start >= end {
266            return Ok(Value::String(String::new()));
267        }
268
269        let result: String = s.chars().skip(start).take(end - start).collect();
270        Ok(Value::String(result))
271    }
272
273    /// `string.trim(s: string) -> string`
274    fn trim(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
275        let s = expect_one_string("string.trim", &args)?;
276        Ok(Value::String(s.trim().to_string()))
277    }
278
279    /// `string.split(s: string, delimiter: string) -> list<string>`
280    fn split(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
281        let (s, delimiter) = expect_two_strings("string.split", &args)?;
282        let parts: Vec<Value> = if delimiter.is_empty() {
283            // Split on empty delimiter → each character becomes an element
284            s.chars().map(|c| Value::String(c.to_string())).collect()
285        } else {
286            s.split(&delimiter)
287                .map(|part| Value::String(part.to_string()))
288                .collect()
289        };
290        Ok(Value::List(parts))
291    }
292
293    /// `string.to_upper(s: string) -> string`
294    fn to_upper(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
295        let s = expect_one_string("string.to_upper", &args)?;
296        Ok(Value::String(s.to_uppercase()))
297    }
298
299    /// `string.to_lower(s: string) -> string`
300    fn to_lower(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
301        let s = expect_one_string("string.to_lower", &args)?;
302        Ok(Value::String(s.to_lowercase()))
303    }
304
305    /// `string.starts_with(s: string, prefix: string) -> bool`
306    fn starts_with(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
307        let (s, prefix) = expect_two_strings("string.starts_with", &args)?;
308        Ok(Value::Bool(s.starts_with(&prefix)))
309    }
310
311    /// `string.ends_with(s: string, suffix: string) -> bool`
312    fn ends_with(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
313        let (s, suffix) = expect_two_strings("string.ends_with", &args)?;
314        Ok(Value::Bool(s.ends_with(&suffix)))
315    }
316
317    /// `string.replace(s: string, old: string, new: string) -> string`
318    ///
319    /// Replace first occurrence only.
320    fn replace(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
321        let (s, old, new) = expect_three_strings("string.replace", &args)?;
322        if old.is_empty() {
323            // Replacing empty string → return original (no-op)
324            return Ok(Value::String(s));
325        }
326        let result = if let Some(pos) = s.find(&old) {
327            format!("{}{new}{}", &s[..pos], &s[pos + old.len()..])
328        } else {
329            s
330        };
331        Ok(Value::String(result))
332    }
333
334    /// `string.replace_all(s: string, old: string, new: string) -> string`
335    fn replace_all(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
336        let (s, old, new) = expect_three_strings("string.replace_all", &args)?;
337        if old.is_empty() {
338            return Ok(Value::String(s));
339        }
340        Ok(Value::String(s.replace(&old, &new)))
341    }
342
343    /// `string.pad_start(s: string, length: number, pad: string) -> string`
344    ///
345    /// Pad string on the left to reach target length. If already >= length,
346    /// returns original. The pad string is repeated/truncated as needed.
347    fn pad_start(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
348        if args.len() != 3 {
349            return Err(StdlibError::wrong_args("string.pad_start", 3, args.len()));
350        }
351        let s = match &args[0] {
352            Value::String(s) => s.clone(),
353            other => {
354                return Err(StdlibError::type_mismatch(
355                    "string.pad_start",
356                    1,
357                    "string",
358                    other.type_name(),
359                ));
360            }
361        };
362        let target_len = match &args[1] {
363            Value::Number(n) => *n,
364            other => {
365                return Err(StdlibError::type_mismatch(
366                    "string.pad_start",
367                    2,
368                    "number",
369                    other.type_name(),
370                ));
371            }
372        };
373        let pad = match &args[2] {
374            Value::String(s) => s.clone(),
375            other => {
376                return Err(StdlibError::type_mismatch(
377                    "string.pad_start",
378                    3,
379                    "string",
380                    other.type_name(),
381                ));
382            }
383        };
384
385        let current_len = s.chars().count();
386        let target_len = target_len as usize;
387
388        if current_len >= target_len || pad.is_empty() {
389            return Ok(Value::String(s));
390        }
391
392        let needed = target_len - current_len;
393        let padding: String = pad.chars().cycle().take(needed).collect();
394        Ok(Value::String(format!("{padding}{s}")))
395    }
396
397    /// `string.pad_end(s: string, length: number, pad: string) -> string`
398    ///
399    /// Pad string on the right to reach target length.
400    fn pad_end(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
401        if args.len() != 3 {
402            return Err(StdlibError::wrong_args("string.pad_end", 3, args.len()));
403        }
404        let s = match &args[0] {
405            Value::String(s) => s.clone(),
406            other => {
407                return Err(StdlibError::type_mismatch(
408                    "string.pad_end",
409                    1,
410                    "string",
411                    other.type_name(),
412                ));
413            }
414        };
415        let target_len = match &args[1] {
416            Value::Number(n) => *n,
417            other => {
418                return Err(StdlibError::type_mismatch(
419                    "string.pad_end",
420                    2,
421                    "number",
422                    other.type_name(),
423                ));
424            }
425        };
426        let pad = match &args[2] {
427            Value::String(s) => s.clone(),
428            other => {
429                return Err(StdlibError::type_mismatch(
430                    "string.pad_end",
431                    3,
432                    "string",
433                    other.type_name(),
434                ));
435            }
436        };
437
438        let current_len = s.chars().count();
439        let target_len = target_len as usize;
440
441        if current_len >= target_len || pad.is_empty() {
442            return Ok(Value::String(s));
443        }
444
445        let needed = target_len - current_len;
446        let padding: String = pad.chars().cycle().take(needed).collect();
447        Ok(Value::String(format!("{s}{padding}")))
448    }
449
450    /// `string.repeat(s: string, count: number) -> string`
451    fn repeat(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
452        if args.len() != 2 {
453            return Err(StdlibError::wrong_args("string.repeat", 2, args.len()));
454        }
455        let s = match &args[0] {
456            Value::String(s) => s.clone(),
457            other => {
458                return Err(StdlibError::type_mismatch(
459                    "string.repeat",
460                    1,
461                    "string",
462                    other.type_name(),
463                ));
464            }
465        };
466        let count = match &args[1] {
467            Value::Number(n) => *n,
468            other => {
469                return Err(StdlibError::type_mismatch(
470                    "string.repeat",
471                    2,
472                    "number",
473                    other.type_name(),
474                ));
475            }
476        };
477
478        if count < 0.0 || count.fract() != 0.0 {
479            return Err(StdlibError::RuntimeError(
480                "string.repeat: count must be a non-negative integer".to_string(),
481            ));
482        }
483
484        Ok(Value::String(s.repeat(count as usize)))
485    }
486
487    /// `string.join(items: list<string>, separator: string) -> string`
488    fn join(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
489        if args.len() != 2 {
490            return Err(StdlibError::wrong_args("string.join", 2, args.len()));
491        }
492        let items = match &args[0] {
493            Value::List(l) => l.clone(),
494            other => {
495                return Err(StdlibError::type_mismatch(
496                    "string.join",
497                    1,
498                    "list",
499                    other.type_name(),
500                ));
501            }
502        };
503        let separator = match &args[1] {
504            Value::String(s) => s.clone(),
505            other => {
506                return Err(StdlibError::type_mismatch(
507                    "string.join",
508                    2,
509                    "string",
510                    other.type_name(),
511                ));
512            }
513        };
514
515        let mut parts = Vec::with_capacity(items.len());
516        for (i, item) in items.iter().enumerate() {
517            match item {
518                Value::String(s) => parts.push(s.clone()),
519                other => {
520                    return Err(StdlibError::TypeMismatch {
521                        function: "string.join".to_string(),
522                        position: i + 1,
523                        expected: "string".to_string(),
524                        got: other.type_name().to_string(),
525                    });
526                }
527            }
528        }
529
530        Ok(Value::String(parts.join(&separator)))
531    }
532
533    /// `string.format(template: string, values: record) -> string`
534    ///
535    /// Replace `{key}` placeholders in template with values from the record.
536    /// Unrecognized placeholders are left as-is.
537    fn format(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
538        if args.len() != 2 {
539            return Err(StdlibError::wrong_args("string.format", 2, args.len()));
540        }
541        let template = match &args[0] {
542            Value::String(s) => s.clone(),
543            other => {
544                return Err(StdlibError::type_mismatch(
545                    "string.format",
546                    1,
547                    "string",
548                    other.type_name(),
549                ));
550            }
551        };
552        let fields = match &args[1] {
553            Value::Record { fields, .. } => fields.clone(),
554            other => {
555                return Err(StdlibError::type_mismatch(
556                    "string.format",
557                    2,
558                    "record",
559                    other.type_name(),
560                ));
561            }
562        };
563
564        let mut result = template;
565        for (key, val) in &fields {
566            let placeholder = format!("{{{key}}}");
567            let replacement = format!("{val}");
568            result = result.replace(&placeholder, &replacement);
569        }
570
571        Ok(Value::String(result))
572    }
573
574    /// `string.from(value: any) -> string`
575    ///
576    /// Convert any value to its string representation. Uses Display impl.
577    fn value_to_string(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
578        if args.len() != 1 {
579            return Err(StdlibError::wrong_args("string.from", 1, args.len()));
580        }
581        Ok(Value::String(format!("{}", args[0])))
582    }
583
584    /// `string.is_empty(s: string) -> bool`
585    fn is_empty(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
586        let s = expect_one_string("string.is_empty", &args)?;
587        Ok(Value::Bool(s.is_empty()))
588    }
589
590    /// `string.index_of(s: string, sub: string) -> number`
591    ///
592    /// Returns the character index of the first occurrence of `sub` in `s`,
593    /// or -1 if not found. Index is character-based (not byte-based).
594    fn index_of(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
595        let (s, sub) = expect_two_strings("string.index_of", &args)?;
596        if sub.is_empty() {
597            return Ok(Value::Number(0.0));
598        }
599        // Find byte position, then convert to char index
600        match s.find(&sub) {
601            Some(byte_pos) => {
602                let char_index = s[..byte_pos].chars().count();
603                Ok(Value::Number(char_index as f64))
604            }
605            None => Ok(Value::Number(-1.0)),
606        }
607    }
608}