Skip to content

Commit 8e16b51

Browse files
committed
Add join() function
1 parent 5b9d420 commit 8e16b51

File tree

5 files changed

+237
-0
lines changed

5 files changed

+237
-0
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
description: Reference for the 'join' DSC configuration document function
3+
ms.date: 08/29/2025
4+
ms.topic: reference
5+
title: join
6+
---
7+
8+
## Synopsis
9+
10+
Joins a string array into a single string, separated using a delimiter.
11+
12+
## Syntax
13+
14+
```Syntax
15+
join(inputArray, delimiter)
16+
```
17+
18+
## Description
19+
20+
The `join()` function takes either an array or a string and a delimiter.
21+
22+
- If `inputArray` is an array, each element is converted to a string and
23+
concatenated with the delimiter between elements.
24+
- If `inputArray` is a string, its characters are joined with the delimiter
25+
between each character.
26+
27+
The `delimiter` can be any value; it is converted to a string.
28+
29+
## Examples
30+
31+
### Example 1 - Join array of strings
32+
33+
```yaml
34+
# join.example.1.dsc.config.yaml
35+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
36+
resources:
37+
- name: Echo
38+
type: Microsoft.DSC.Debug/Echo
39+
properties:
40+
output: "[join(createArray('a','b','c'), '-')]"
41+
```
42+
43+
```bash
44+
dsc config get --file join.example.1.dsc.config.yaml
45+
```
46+
47+
```yaml
48+
results:
49+
- name: Echo
50+
type: Microsoft.DSC.Debug/Echo
51+
result:
52+
actualState:
53+
output: a-b-c
54+
messages: []
55+
hadErrors: false
56+
```
57+
58+
### Example 2 - Join characters of a string
59+
60+
```yaml
61+
# join.example.2.dsc.config.yaml
62+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
63+
resources:
64+
- name: Echo
65+
type: Microsoft.DSC.Debug/Echo
66+
properties:
67+
output: "[join('abc', '-')]"
68+
```
69+
70+
```bash
71+
dsc config get --file join.example.2.dsc.config.yaml
72+
```
73+
74+
```yaml
75+
results:
76+
- name: Echo
77+
type: Microsoft.DSC.Debug/Echo
78+
result:
79+
actualState:
80+
output: a-b-c
81+
messages: []
82+
hadErrors: false
83+
```
84+
85+
## Parameters
86+
87+
### inputArray
88+
89+
An array or a string.
90+
91+
```yaml
92+
Type: array | string
93+
Required: true
94+
Position: 1
95+
```
96+
97+
### delimiter
98+
99+
Any value used between elements/characters. Converted to a string.
100+
101+
```yaml
102+
Type: any
103+
Required: true
104+
Position: 2
105+
```
106+
107+
## Output
108+
109+
Returns a string containing the joined result.
110+
111+
```yaml
112+
Type: string
113+
```
114+
115+
## Related functions
116+
117+
- [`concat()`][00] - Concatenates strings together
118+
- [`string()`][01] - Converts values to strings
119+
120+
<!-- Link reference definitions -->
121+
[00]: ./concat.md
122+
[01]: ./string.md

dsc/tests/dsc_functions.tests.ps1

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,4 +408,25 @@ Describe 'tests for function expressions' {
408408
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
409409
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
410410
}
411+
412+
It 'join function works for: <expression>' -TestCases @(
413+
@{ expression = "[join(createArray('a','b','c'), '-')]"; expected = 'a-b-c' }
414+
@{ expression = "[join('abc', '-')]"; expected = 'a-b-c' }
415+
@{ expression = "[join(createArray(), '-')]"; expected = '' }
416+
@{ expression = "[join('', '-')]"; expected = '' }
417+
) {
418+
param($expression, $expected)
419+
420+
$config_yaml = @"
421+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
422+
resources:
423+
- name: Echo
424+
type: Microsoft.DSC.Debug/Echo
425+
properties:
426+
output: "$expression"
427+
"@
428+
$out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
429+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
430+
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
431+
}
411432
}

dsc_lib/locales/en-us.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,11 @@ description = "Returns the index of the first occurrence of an item in an array"
323323
invoked = "indexOf function"
324324
invalidArrayArg = "First argument must be an array"
325325

326+
[functions.join]
327+
description = "Joins a string array into a single string, separated using a delimiter."
328+
invoked = "join function"
329+
invalidInputType = "The inputArray must be an array or a string."
330+
326331
[functions.length]
327332
description = "Returns the length of a string, array, or object"
328333
invoked = "length function"

dsc_lib/src/functions/join.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::DscError;
5+
use crate::configure::context::Context;
6+
use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata};
7+
use rust_i18n::t;
8+
use serde_json::Value;
9+
use tracing::debug;
10+
11+
#[derive(Debug, Default)]
12+
pub struct Join {}
13+
14+
fn stringify_value(v: &Value) -> String {
15+
if let Some(s) = v.as_str() { return s.to_string(); }
16+
if let Some(n) = v.as_i64() { return n.to_string(); }
17+
if let Some(b) = v.as_bool() { return b.to_string(); }
18+
if v.is_null() { return "null".to_string(); }
19+
// Fallback to JSON for arrays/objects or other numbers
20+
serde_json::to_string(v).unwrap_or_default()
21+
}
22+
23+
impl Function for Join {
24+
fn get_metadata(&self) -> FunctionMetadata {
25+
FunctionMetadata {
26+
name: "join".to_string(),
27+
description: t!("functions.join.description").to_string(),
28+
category: FunctionCategory::String,
29+
min_args: 2,
30+
max_args: 2,
31+
accepted_arg_ordered_types: vec![
32+
vec![FunctionArgKind::Array, FunctionArgKind::String],
33+
// delimiter: accept any type (no validation), convert to string
34+
vec![
35+
FunctionArgKind::Array,
36+
FunctionArgKind::Boolean,
37+
FunctionArgKind::Null,
38+
FunctionArgKind::Number,
39+
FunctionArgKind::Object,
40+
FunctionArgKind::String,
41+
],
42+
],
43+
remaining_arg_accepted_types: None,
44+
return_types: vec![FunctionArgKind::String],
45+
}
46+
}
47+
48+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
49+
debug!("{}", t!("functions.join.invoked"));
50+
51+
let delimiter = stringify_value(&args[1]);
52+
53+
if let Some(array) = args[0].as_array() {
54+
let items: Vec<String> = array.iter().map(stringify_value).collect();
55+
return Ok(Value::String(items.join(&delimiter)));
56+
}
57+
58+
if let Some(s) = args[0].as_str() {
59+
// Edge case: empty string => empty string
60+
if s.is_empty() { return Ok(Value::String(String::new())); }
61+
let items: Vec<String> = s.chars().map(|c| c.to_string()).collect();
62+
return Ok(Value::String(items.join(&delimiter)));
63+
}
64+
65+
Err(DscError::Parser(t!("functions.join.invalidInputType").to_string()))
66+
}
67+
}
68+
69+
#[cfg(test)]
70+
mod tests {
71+
use crate::configure::context::Context;
72+
use crate::parser::Statement;
73+
74+
#[test]
75+
fn join_array_of_strings() {
76+
let mut parser = Statement::new().unwrap();
77+
let result = parser.parse_and_execute("[join(createArray('a','b','c'), '-')]", &Context::new()).unwrap();
78+
assert_eq!(result, "a-b-c");
79+
}
80+
81+
#[test]
82+
fn join_string_chars() {
83+
let mut parser = Statement::new().unwrap();
84+
let result = parser.parse_and_execute("[join('abc', '-')]", &Context::new()).unwrap();
85+
assert_eq!(result, "a-b-c");
86+
}
87+
}

dsc_lib/src/functions/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub mod less_or_equals;
3737
pub mod format;
3838
pub mod int;
3939
pub mod index_of;
40+
pub mod join;
4041
pub mod max;
4142
pub mod min;
4243
pub mod mod_function;
@@ -146,6 +147,7 @@ impl FunctionDispatcher {
146147
Box::new(format::Format{}),
147148
Box::new(int::Int{}),
148149
Box::new(index_of::IndexOf{}),
150+
Box::new(join::Join{}),
149151
Box::new(max::Max{}),
150152
Box::new(min::Min{}),
151153
Box::new(mod_function::Mod{}),

0 commit comments

Comments
 (0)