1use crate::compiler::prelude::*;
2use std::borrow::Cow;
3
4fn join(array: Value, separator: Option<Value>) -> Resolved {
5 let array = array.try_array()?;
6 let string_vec = array
7 .iter()
8 .map(|s| s.try_bytes_utf8_lossy().map_err(Into::into))
9 .collect::<ExpressionResult<Vec<Cow<'_, str>>>>()
10 .map_err(|_| "all array items must be strings")?;
11 let separator: String = separator
12 .map(Value::try_bytes)
13 .transpose()?
14 .map_or_else(String::new, |s| String::from_utf8_lossy(&s).to_string());
15 let joined = string_vec.join(&separator);
16 Ok(Value::from(joined))
17}
18
19#[derive(Clone, Copy, Debug)]
20pub struct Join;
21
22impl Function for Join {
23 fn identifier(&self) -> &'static str {
24 "join"
25 }
26
27 fn usage(&self) -> &'static str {
28 "Joins each string in the `value` array into a single string, with items optionally separated from one another by a `separator`."
29 }
30
31 fn category(&self) -> &'static str {
32 Category::String.as_ref()
33 }
34
35 fn return_kind(&self) -> u16 {
36 kind::BYTES
37 }
38
39 fn parameters(&self) -> &'static [Parameter] {
40 const PARAMETERS: &[Parameter] = &[
41 Parameter::required(
42 "value",
43 kind::ARRAY,
44 "The array of strings to join together.",
45 ),
46 Parameter::optional(
47 "separator",
48 kind::BYTES,
49 "The string separating each original element when joined.",
50 ),
51 ];
52 PARAMETERS
53 }
54
55 fn compile(
56 &self,
57 _state: &state::TypeState,
58 _ctx: &mut FunctionCompileContext,
59 arguments: ArgumentList,
60 ) -> Compiled {
61 let value = arguments.required("value");
62 let separator = arguments.optional("separator");
63
64 Ok(JoinFn { value, separator }.as_expr())
65 }
66
67 fn examples(&self) -> &'static [Example] {
68 &[
69 example! {
70 title: "Join array (no separator)",
71 source: r#"join!(["bring", "us", "together"])"#,
72 result: Ok("bringustogether"),
73 },
74 example! {
75 title: "Join array (comma separator)",
76 source: r#"join!(["sources", "transforms", "sinks"], separator: ", ")"#,
77 result: Ok("sources, transforms, sinks"),
78 },
79 ]
80 }
81}
82
83#[derive(Clone, Debug)]
84struct JoinFn {
85 value: Box<dyn Expression>,
86 separator: Option<Box<dyn Expression>>,
87}
88
89impl FunctionExpression for JoinFn {
90 fn resolve(&self, ctx: &mut Context) -> Resolved {
91 let array = self.value.resolve(ctx)?;
92 let separator = self
93 .separator
94 .as_ref()
95 .map(|s| s.resolve(ctx))
96 .transpose()?;
97
98 join(array, separator)
99 }
100
101 fn type_def(&self, _: &state::TypeState) -> TypeDef {
102 TypeDef::bytes().fallible()
103 }
104}
105
106#[cfg(test)]
107mod test {
108 use super::*;
109 use crate::value;
110 test_function![
111 join => Join;
112
113 with_comma_separator {
114 args: func_args![value: value!(["one", "two", "three"]), separator: ", "],
115 want: Ok(value!("one, two, three")),
116 tdef: TypeDef::bytes().fallible(),
117 }
118
119 with_space_separator {
120 args: func_args![value: value!(["one", "two", "three"]), separator: " "],
121 want: Ok(value!("one two three")),
122 tdef: TypeDef::bytes().fallible(),
123 }
124
125 without_separator {
126 args: func_args![value: value!(["one", "two", "three"])],
127 want: Ok(value!("onetwothree")),
128 tdef: TypeDef::bytes().fallible(),
129 }
130
131 non_string_array_item_throws_error {
132 args: func_args![value: value!(["one", "two", 3])],
133 want: Err("all array items must be strings"),
134 tdef: TypeDef::bytes().fallible(),
135 }
136 ];
137}