netidx_bscript/stdfn/
str.rs

1use crate::{
2    deftype, err,
3    expr::Expr,
4    stdfn::{CachedArgs, CachedVals, EvalCached},
5    Ctx, ExecCtx, UserEvent,
6};
7use arcstr::{literal, ArcStr};
8use netidx::{path::Path, subscriber::Value, utils};
9use netidx_netproto::valarray::ValArray;
10use smallvec::SmallVec;
11use std::cell::RefCell;
12
13#[derive(Debug, Default)]
14struct StartsWithEv;
15
16impl EvalCached for StartsWithEv {
17    const NAME: &str = "starts_with";
18    deftype!("str", "fn(#pfx:string, string) -> bool");
19
20    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
21        match (&from.0[0], &from.0[1]) {
22            (Some(Value::String(pfx)), Some(Value::String(val))) => {
23                if val.starts_with(&**pfx) {
24                    Some(Value::Bool(true))
25                } else {
26                    Some(Value::Bool(false))
27                }
28            }
29            (None, _) | (_, None) => None,
30            _ => err!("starts_with string arguments"),
31        }
32    }
33}
34
35type StartsWith = CachedArgs<StartsWithEv>;
36
37#[derive(Debug, Default)]
38struct EndsWithEv;
39
40impl EvalCached for EndsWithEv {
41    const NAME: &str = "ends_with";
42    deftype!("str", "fn(#sfx:string, string) -> bool");
43
44    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
45        match (&from.0[0], &from.0[1]) {
46            (Some(Value::String(sfx)), Some(Value::String(val))) => {
47                if val.ends_with(&**sfx) {
48                    Some(Value::Bool(true))
49                } else {
50                    Some(Value::Bool(false))
51                }
52            }
53            (None, _) | (_, None) => None,
54            _ => err!("ends_with string arguments"),
55        }
56    }
57}
58
59type EndsWith = CachedArgs<EndsWithEv>;
60
61#[derive(Debug, Default)]
62struct ContainsEv;
63
64impl EvalCached for ContainsEv {
65    const NAME: &str = "contains";
66    deftype!("str", "fn(#part:string, string) -> bool");
67
68    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
69        match (&from.0[0], &from.0[1]) {
70            (Some(Value::String(chs)), Some(Value::String(val))) => {
71                if val.contains(&**chs) {
72                    Some(Value::Bool(true))
73                } else {
74                    Some(Value::Bool(false))
75                }
76            }
77            (None, _) | (_, None) => None,
78            _ => err!("contains expected string"),
79        }
80    }
81}
82
83type Contains = CachedArgs<ContainsEv>;
84
85#[derive(Debug, Default)]
86struct StripPrefixEv;
87
88impl EvalCached for StripPrefixEv {
89    const NAME: &str = "strip_prefix";
90    deftype!("str", "fn(#pfx:string, string) -> [string, null]");
91
92    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
93        match (&from.0[0], &from.0[1]) {
94            (Some(Value::String(pfx)), Some(Value::String(val))) => val
95                .strip_prefix(&**pfx)
96                .map(|s| Value::String(s.into()))
97                .or(Some(Value::Null)),
98            (None, _) | (_, None) => None,
99            _ => err!("strip_prefix expected string"),
100        }
101    }
102}
103
104type StripPrefix = CachedArgs<StripPrefixEv>;
105
106#[derive(Debug, Default)]
107struct StripSuffixEv;
108
109impl EvalCached for StripSuffixEv {
110    const NAME: &str = "strip_suffix";
111    deftype!("str", "fn(#sfx:string, string) -> [string, null]");
112
113    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
114        match (&from.0[0], &from.0[1]) {
115            (Some(Value::String(sfx)), Some(Value::String(val))) => val
116                .strip_suffix(&**sfx)
117                .map(|s| Value::String(s.into()))
118                .or(Some(Value::Null)),
119            (None, _) | (_, None) => None,
120            _ => err!("strip_suffix expected string"),
121        }
122    }
123}
124
125type StripSuffix = CachedArgs<StripSuffixEv>;
126
127#[derive(Debug, Default)]
128struct TrimEv;
129
130impl EvalCached for TrimEv {
131    const NAME: &str = "trim";
132    deftype!("str", "fn(string) -> string");
133
134    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
135        match &from.0[0] {
136            Some(Value::String(val)) => Some(Value::String(val.trim().into())),
137            None => None,
138            _ => err!("trim expected string"),
139        }
140    }
141}
142
143type Trim = CachedArgs<TrimEv>;
144
145#[derive(Debug, Default)]
146struct TrimStartEv;
147
148impl EvalCached for TrimStartEv {
149    const NAME: &str = "trim_start";
150    deftype!("str", "fn(string) -> string");
151
152    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
153        match &from.0[0] {
154            Some(Value::String(val)) => Some(Value::String(val.trim_start().into())),
155            None => None,
156            _ => err!("trim_start expected string"),
157        }
158    }
159}
160
161type TrimStart = CachedArgs<TrimStartEv>;
162
163#[derive(Debug, Default)]
164struct TrimEndEv;
165
166impl EvalCached for TrimEndEv {
167    const NAME: &str = "trim_end";
168    deftype!("str", "fn(string) -> string");
169
170    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
171        match &from.0[0] {
172            Some(Value::String(val)) => Some(Value::String(val.trim_end().into())),
173            None => None,
174            _ => err!("trim_start expected string"),
175        }
176    }
177}
178
179type TrimEnd = CachedArgs<TrimEndEv>;
180
181#[derive(Debug, Default)]
182struct ReplaceEv;
183
184impl EvalCached for ReplaceEv {
185    const NAME: &str = "replace";
186    deftype!("str", "fn(#pat:string, #rep:string, string) -> string");
187
188    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
189        match (&from.0[0], &from.0[1], &from.0[2]) {
190            (
191                Some(Value::String(pat)),
192                Some(Value::String(rep)),
193                Some(Value::String(val)),
194            ) => Some(Value::String(val.replace(&**pat, &**rep).into())),
195            (None, _, _) | (_, None, _) | (_, _, None) => None,
196            _ => err!("replace expected string"),
197        }
198    }
199}
200
201type Replace = CachedArgs<ReplaceEv>;
202
203#[derive(Debug, Default)]
204struct DirnameEv;
205
206impl EvalCached for DirnameEv {
207    const NAME: &str = "dirname";
208    deftype!("str", "fn(string) -> [null, string]");
209
210    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
211        match &from.0[0] {
212            Some(Value::String(path)) => match Path::dirname(path) {
213                None => Some(Value::Null),
214                Some(dn) => Some(Value::String(dn.into())),
215            },
216            None => None,
217            _ => err!("dirname expected string"),
218        }
219    }
220}
221
222type Dirname = CachedArgs<DirnameEv>;
223
224#[derive(Debug, Default)]
225struct BasenameEv;
226
227impl EvalCached for BasenameEv {
228    const NAME: &str = "basename";
229    deftype!("str", "fn(string) -> [null, string]");
230
231    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
232        match &from.0[0] {
233            Some(Value::String(path)) => match Path::basename(path) {
234                None => Some(Value::Null),
235                Some(dn) => Some(Value::String(dn.into())),
236            },
237            None => None,
238            _ => err!("basename expected string"),
239        }
240    }
241}
242
243type Basename = CachedArgs<BasenameEv>;
244
245#[derive(Debug, Default)]
246struct StringJoinEv;
247
248impl EvalCached for StringJoinEv {
249    const NAME: &str = "string_join";
250    deftype!("str", "fn(#sep:string, @args: [string, Array<string>]) -> string");
251
252    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
253        thread_local! {
254            static BUF: RefCell<String> = RefCell::new(String::new());
255        }
256        match &from.0[..] {
257            [_] | [] => None,
258            [None, ..] => None,
259            [Some(sep), parts @ ..] => {
260                // this is fairly common, so we check it before doing any real work
261                for p in parts {
262                    if p.is_none() {
263                        return None;
264                    }
265                }
266                let sep = match sep {
267                    Value::String(c) => c.clone(),
268                    sep => match sep.clone().cast_to::<ArcStr>().ok() {
269                        Some(c) => c,
270                        None => return err!("string_join, separator must be a string"),
271                    },
272                };
273                BUF.with_borrow_mut(|buf| {
274                    macro_rules! push {
275                        ($c:expr) => {
276                            if buf.is_empty() {
277                                buf.push_str($c.as_str());
278                            } else {
279                                buf.push_str(sep.as_str());
280                                buf.push_str($c.as_str());
281                            }
282                        };
283                    }
284                    buf.clear();
285                    for p in parts {
286                        match p.as_ref().unwrap() {
287                            Value::String(c) => push!(c),
288                            Value::Array(a) => {
289                                for v in a.iter() {
290                                    match v {
291                                        Value::String(c) => push!(c),
292                                        _ => {
293                                            return err!(
294                                                "string_join, components must be strings"
295                                            )
296                                        }
297                                    }
298                                }
299                            }
300                            _ => return err!("string_join, components must be strings"),
301                        }
302                    }
303                    Some(Value::String(buf.as_str().into()))
304                })
305            }
306        }
307    }
308}
309
310type StringJoin = CachedArgs<StringJoinEv>;
311
312#[derive(Debug, Default)]
313struct StringConcatEv;
314
315impl EvalCached for StringConcatEv {
316    const NAME: &str = "string_concat";
317    deftype!("str", "fn(@args: [string, Array<string>]) -> string");
318
319    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
320        thread_local! {
321            static BUF: RefCell<String> = RefCell::new(String::new());
322        }
323        let parts = &from.0[..];
324        // this is a fairly common case, so we check it before doing any real work
325        for p in parts {
326            if p.is_none() {
327                return None;
328            }
329        }
330        BUF.with_borrow_mut(|buf| {
331            buf.clear();
332            for p in parts {
333                match p.as_ref().unwrap() {
334                    Value::String(c) => buf.push_str(c.as_ref()),
335                    Value::Array(a) => {
336                        for v in a.iter() {
337                            match v {
338                                Value::String(c) => buf.push_str(c.as_ref()),
339                                _ => {
340                                    return err!(
341                                        "string_concat: arguments must be strings"
342                                    )
343                                }
344                            }
345                        }
346                    }
347                    _ => return err!("string_concat: arguments must be strings"),
348                }
349            }
350            Some(Value::String(buf.as_str().into()))
351        })
352    }
353}
354
355type StringConcat = CachedArgs<StringConcatEv>;
356
357#[derive(Debug, Default)]
358struct StringEscapeEv {
359    to_escape: SmallVec<[char; 8]>,
360}
361
362impl EvalCached for StringEscapeEv {
363    const NAME: &str = "string_escape";
364    deftype!("str", "fn(?#to_escape:string, ?#escape:string, string) -> [string, error]");
365
366    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
367        // this is a fairly common case, so we check it before doing any real work
368        for p in &from.0[..] {
369            if p.is_none() {
370                return None;
371            }
372        }
373        match &from.0[0] {
374            Some(Value::String(s)) => {
375                self.to_escape.clear();
376                self.to_escape.extend(s.chars());
377            }
378            _ => return err!("escape: expected a string"),
379        }
380        let ec = match &from.0[1] {
381            Some(Value::String(s)) if s.len() == 1 => s.chars().next().unwrap(),
382            _ => return err!("escape: expected a single escape char"),
383        };
384        match &from.0[2] {
385            Some(Value::String(s)) => {
386                Some(Value::String(utils::escape(s, ec, &self.to_escape).into()))
387            }
388            _ => err!("escape: expected a string"),
389        }
390    }
391}
392
393type StringEscape = CachedArgs<StringEscapeEv>;
394
395#[derive(Debug, Default)]
396struct StringUnescapeEv;
397
398impl EvalCached for StringUnescapeEv {
399    const NAME: &str = "string_unescape";
400    deftype!("str", "fn(?#escape:string, string) -> string");
401
402    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
403        // this is a fairly common case, so we check it before doing any real work
404        for p in &from.0[..] {
405            if p.is_none() {
406                return None;
407            }
408        }
409        let ec = match &from.0[0] {
410            Some(Value::String(s)) if s.len() == 1 => s.chars().next().unwrap(),
411            _ => return err!("escape: expected a single escape char"),
412        };
413        match &from.0[1] {
414            Some(Value::String(s)) => Some(Value::String(utils::unescape(s, ec).into())),
415            _ => err!("escape: expected a string"),
416        }
417    }
418}
419
420type StringUnescape = CachedArgs<StringUnescapeEv>;
421
422#[derive(Debug, Default)]
423struct StringSplitEv;
424
425impl EvalCached for StringSplitEv {
426    const NAME: &str = "string_split";
427    deftype!("str", "fn(#pat:string, string) -> Array<string>");
428
429    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
430        // this is a fairly common case, so we check it before doing any real work
431        for p in &from.0[..] {
432            if p.is_none() {
433                return None;
434            }
435        }
436        let pat = match &from.0[0] {
437            Some(Value::String(s)) => s,
438            _ => return err!("split: expected string"),
439        };
440        match &from.0[1] {
441            Some(Value::String(s)) => Some(Value::Array(ValArray::from_iter(
442                s.split(&**pat).map(|s| Value::String(ArcStr::from(s))),
443            ))),
444            _ => err!("split: expected a string"),
445        }
446    }
447}
448
449type StringSplit = CachedArgs<StringSplitEv>;
450
451#[derive(Debug, Default)]
452struct StringSplitOnceEv;
453
454impl EvalCached for StringSplitOnceEv {
455    const NAME: &str = "string_split_once";
456    deftype!("str", "fn(#pat:string, string) -> [null, (string, string)]");
457
458    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
459        // this is a fairly common case, so we check it before doing any real work
460        for p in &from.0[..] {
461            if p.is_none() {
462                return None;
463            }
464        }
465        let pat = match &from.0[0] {
466            Some(Value::String(s)) => s,
467            _ => return err!("split_once: expected string"),
468        };
469        match &from.0[1] {
470            Some(Value::String(s)) => match s.split_once(&**pat) {
471                None => Some(Value::Null),
472                Some((s0, s1)) => Some(Value::Array(ValArray::from([
473                    Value::String(s0.into()),
474                    Value::String(s1.into()),
475                ]))),
476            },
477            _ => err!("split_once: expected a string"),
478        }
479    }
480}
481
482type StringSplitOnce = CachedArgs<StringSplitOnceEv>;
483
484#[derive(Debug, Default)]
485struct StringRSplitOnceEv;
486
487impl EvalCached for StringRSplitOnceEv {
488    const NAME: &str = "string_rsplit_once";
489    deftype!("str", "fn(#pat:string, string) -> [null, (string, string)]");
490
491    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
492        // this is a fairly common case, so we check it before doing any real work
493        for p in &from.0[..] {
494            if p.is_none() {
495                return None;
496            }
497        }
498        let pat = match &from.0[0] {
499            Some(Value::String(s)) => s,
500            _ => return err!("split_once: expected string"),
501        };
502        match &from.0[1] {
503            Some(Value::String(s)) => match s.rsplit_once(&**pat) {
504                None => Some(Value::Null),
505                Some((s0, s1)) => Some(Value::Array(ValArray::from([
506                    Value::String(s0.into()),
507                    Value::String(s1.into()),
508                ]))),
509            },
510            _ => err!("split_once: expected a string"),
511        }
512    }
513}
514
515type StringRSplitOnce = CachedArgs<StringRSplitOnceEv>;
516
517#[derive(Debug, Default)]
518struct StringToLowerEv;
519
520impl EvalCached for StringToLowerEv {
521    const NAME: &str = "string_to_lower";
522    deftype!("str", "fn(string) -> string");
523
524    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
525        match &from.0[0] {
526            None => None,
527            Some(Value::String(s)) => Some(Value::String(s.to_lowercase().into())),
528            _ => err!("to_lower: expected a string"),
529        }
530    }
531}
532
533type StringToLower = CachedArgs<StringToLowerEv>;
534
535#[derive(Debug, Default)]
536struct StringToUpperEv;
537
538impl EvalCached for StringToUpperEv {
539    const NAME: &str = "string_to_upper";
540    deftype!("str", "fn(string) -> string");
541
542    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
543        match &from.0[0] {
544            None => None,
545            Some(Value::String(s)) => Some(Value::String(s.to_uppercase().into())),
546            _ => err!("to_upper: expected a string"),
547        }
548    }
549}
550
551type StringToUpper = CachedArgs<StringToUpperEv>;
552
553const MOD: &str = r#"
554pub mod str {
555    /// return true if s starts with #pfx, otherwise return false
556    pub let starts_with = |#pfx, s| 'starts_with;
557
558    /// return true if s ends with #sfx otherwise return false
559    pub let ends_with = |#sfx, s| 'ends_with;
560
561    /// return true if s contains #part, otherwise return false
562    pub let contains = |#part, s| 'contains;
563
564    /// if s starts with #pfx then return s with #pfx stripped otherwise return null
565    pub let strip_prefix = |#pfx, s| 'strip_prefix;
566
567    /// if s ends with #sfx then return s with #sfx stripped otherwise return null
568    pub let strip_suffix = |#sfx, s| 'strip_suffix;
569
570    /// return s with leading and trailing whitespace removed
571    pub let trim = |s| 'trim;
572
573    /// return s with leading whitespace removed
574    pub let trim_start = |s| 'trim_start;
575
576    /// return s with trailing whitespace removed
577    pub let trim_end = |s| 'trim_end;
578
579    /// replace all instances of #pat in s with #rep and return s
580    pub let replace = |#pat, #rep, s| 'replace;
581
582    /// return the parent path of s, or null if s does not have a parent path
583    pub let dirname = |path| 'dirname;
584
585    /// return the leaf path of s, or null if s is not a path. e.g. /foo/bar -> bar
586    pub let basename = |path| 'basename;
587
588    /// return a single string with the arguments concatenated and separated by #sep
589    pub let join = |#sep, @args| 'string_join;
590
591    /// concatenate the specified strings into a single string
592    pub let concat = |@args| 'string_concat;
593
594    /// escape all the charachters in #to_escape in s with the escape charachter #escape.
595    /// The escape charachter must appear in #to_escape
596    pub let escape = |#to_escape = "/", #escape = "\\", s| 'string_escape;
597
598    /// unescape all the charachters in s escaped by the specified #escape charachter
599    pub let unescape = |#escape = "\\", s| 'string_unescape;
600
601    /// split the string by the specified #pat and return an array of each part
602    pub let split = |#pat, s| 'string_split;
603
604    /// split the string once from the beginning by #pat and return a
605    /// tuple of strings, or return null if #pat was not found in the string
606    pub let split_once = |#pat, s| 'string_split_once;
607
608    /// split the string once from the end by #pat and return a tuple of strings
609    /// or return null if #pat was not found in the string    
610    pub let rsplit_once = |#pat, s| 'string_rsplit_once;
611
612    /// change the string to lowercase
613    pub let to_lower = |s| 'string_to_lower;
614
615    /// change the string to uppercase
616    pub let to_upper = |s| 'string_to_upper
617}
618"#;
619
620pub fn register<C: Ctx, E: UserEvent>(ctx: &mut ExecCtx<C, E>) -> Expr {
621    ctx.register_builtin::<StartsWith>().unwrap();
622    ctx.register_builtin::<EndsWith>().unwrap();
623    ctx.register_builtin::<Contains>().unwrap();
624    ctx.register_builtin::<StripPrefix>().unwrap();
625    ctx.register_builtin::<StripSuffix>().unwrap();
626    ctx.register_builtin::<Trim>().unwrap();
627    ctx.register_builtin::<TrimStart>().unwrap();
628    ctx.register_builtin::<TrimEnd>().unwrap();
629    ctx.register_builtin::<Replace>().unwrap();
630    ctx.register_builtin::<Dirname>().unwrap();
631    ctx.register_builtin::<Basename>().unwrap();
632    ctx.register_builtin::<StringJoin>().unwrap();
633    ctx.register_builtin::<StringConcat>().unwrap();
634    ctx.register_builtin::<StringEscape>().unwrap();
635    ctx.register_builtin::<StringUnescape>().unwrap();
636    ctx.register_builtin::<StringSplit>().unwrap();
637    ctx.register_builtin::<StringSplitOnce>().unwrap();
638    ctx.register_builtin::<StringRSplitOnce>().unwrap();
639    ctx.register_builtin::<StringToLower>().unwrap();
640    ctx.register_builtin::<StringToUpper>().unwrap();
641    MOD.parse().unwrap()
642}