Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions docs/reference/schemas/config/functions/join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
---
description: Reference for the 'join' DSC configuration document function
ms.date: 08/29/2025
ms.topic: reference
title: join
---

## Synopsis

Joins an array into a single string, separated using a delimiter.

## Syntax

```Syntax
join(inputArray, delimiter)
```

## Description

The `join()` function takes an array and a delimiter.

- Each array element is converted to a string and concatenated with the
delimiter between elements.

The `delimiter` can be any value; it’s converted to a string.

## Examples

### Example 1 - Produce a list of servers

Create a comma-separated string from a list of host names to pass to tools or
APIs that accept CSV input. This example uses [`createArray()`][02] to build
the server list and joins with ", ".

```yaml
# join.example.1.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[join(createArray('web01','web02','web03'), ', ')]"
```

```bash
dsc config get --file join.example.1.dsc.config.yaml
```

```yaml
results:
- name: Echo
type: Microsoft.DSC.Debug/Echo
result:
actualState:
output: web01, web02, web03
messages: []
hadErrors: false
```

### Example 2 - Build a file system path from segments

Join path segments into a single path string. This is useful when composing
paths dynamically from parts.

```yaml
# join.example.2.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[join(createArray('/etc','nginx','sites-enabled'), '/')]"
```

```bash
dsc config get --file join.example.2.dsc.config.yaml
```

```yaml
results:
- name: Echo
type: Microsoft.DSC.Debug/Echo
result:
actualState:
output: /etc/nginx/sites-enabled
messages: []
hadErrors: false
```

### Example 3 - Format a version string from numeric parts

Convert version components (numbers) into a dotted version string. Non-string
elements are converted to strings automatically.

```yaml
# join.example.3.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[join(createArray(1,2,3), '.')]"
```

```bash
dsc config get --file join.example.3.dsc.config.yaml
```

```yaml
results:
- name: Echo
type: Microsoft.DSC.Debug/Echo
result:
actualState:
output: 1.2.3
messages: []
hadErrors: false
```

## Parameters

### inputArray

The array whose elements will be concatenated.

```yaml
Type: array
Required: true
Position: 1
```

### delimiter

Any value used between elements. Converted to a string.

```yaml
Type: any
Required: true
Position: 2
```

## Output

Returns a string containing the joined result.

```yaml
Type: string
```

## Related functions

- [`concat()`][00] - Concatenates strings together
- [`string()`][01] - Converts values to strings

<!-- Link reference definitions -->
[00]: ./concat.md
[01]: ./string.md
26 changes: 23 additions & 3 deletions dsc/tests/dsc_functions.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -409,15 +409,35 @@ Describe 'tests for function expressions' {
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
}

It 'join function works for: <expression>' -TestCases @(
@{ expression = "[join(createArray('a','b','c'), '-')]"; expected = 'a-b-c' }
@{ expression = "[join(createArray(), '-')]"; expected = '' }
@{ expression = "[join(createArray(1,2,3), ',')]"; expected = '1,2,3' }
) {
param($expression, $expected)

$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "$expression"
"@
$out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
}

It 'skip function works for: <expression>' -TestCases @(
@{ expression = "[skip(createArray('a','b','c','d'), 2)]"; expected = @('c','d') }
@{ expression = "[skip(createArray('a','b','c','d'), 2)]"; expected = @('c', 'd') }
@{ expression = "[skip('hello', 2)]"; expected = 'llo' }
@{ expression = "[skip(createArray('a','b'), 0)]"; expected = @('a','b') }
@{ expression = "[skip(createArray('a','b'), 0)]"; expected = @('a', 'b') }
@{ expression = "[skip('abc', 0)]"; expected = 'abc' }
@{ expression = "[skip(createArray('a','b'), 5)]"; expected = @() }
@{ expression = "[skip('', 1)]"; expected = '' }
# Negative counts are treated as zero
@{ expression = "[skip(createArray('x','y'), -3)]"; expected = @('x','y') }
@{ expression = "[skip(createArray('x','y'), -3)]"; expected = @('x', 'y') }
@{ expression = "[skip('xy', -1)]"; expected = 'xy' }
) {
param($expression, $expected)
Expand Down
8 changes: 8 additions & 0 deletions dsc_lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,14 @@ description = "Returns the index of the first occurrence of an item in an array"
invoked = "indexOf function"
invalidArrayArg = "First argument must be an array"

[functions.join]
description = "Joins the elements of an array into a single string, separated using a delimiter."
invoked = "join function"
invalidArrayArg = "First argument must be an array"
invalidNullElement = "Array elements cannot be null"
invalidArrayElement = "Array elements cannot be arrays"
invalidObjectElement = "Array elements cannot be objects"

[functions.lastIndexOf]
description = "Returns the index of the last occurrence of an item in an array"
invoked = "lastIndexOf function"
Expand Down
128 changes: 128 additions & 0 deletions dsc_lib/src/functions/join.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::DscError;
use crate::configure::context::Context;
use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata};
use rust_i18n::t;
use serde_json::Value;
use tracing::debug;

#[derive(Debug, Default)]
pub struct Join {}

fn stringify_value(v: &Value) -> Result<String, DscError> {
match v {
Value::String(s) => Ok(s.clone()),
Value::Number(n) => Ok(n.to_string()),
Value::Bool(b) => Ok(b.to_string()),
Value::Null => Err(DscError::Parser(t!("functions.join.invalidNullElement").to_string())),
Value::Array(_) => Err(DscError::Parser(t!("functions.join.invalidArrayElement").to_string())),
Value::Object(_) => Err(DscError::Parser(t!("functions.join.invalidObjectElement").to_string())),
}
}

impl Function for Join {
fn get_metadata(&self) -> FunctionMetadata {
FunctionMetadata {
name: "join".to_string(),
description: t!("functions.join.description").to_string(),
category: FunctionCategory::String,
min_args: 2,
max_args: 2,
accepted_arg_ordered_types: vec![
vec![FunctionArgKind::Array],
vec![FunctionArgKind::String],
],
remaining_arg_accepted_types: None,
return_types: vec![FunctionArgKind::String],
}
}

fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
debug!("{}", t!("functions.join.invoked"));

let delimiter = args[1].as_str().unwrap();

if let Some(array) = args[0].as_array() {
let items: Result<Vec<String>, DscError> = array.iter().map(stringify_value).collect();
let items = items?;
return Ok(Value::String(items.join(delimiter)));
}

Err(DscError::Parser(t!("functions.join.invalidArrayArg").to_string()))
}
}

#[cfg(test)]
mod tests {
use crate::configure::context::Context;
use crate::parser::Statement;
use super::Join;
use crate::functions::Function;

#[test]
fn join_array_of_strings() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[join(createArray('a','b','c'), '-')]", &Context::new()).unwrap();
assert_eq!(result, "a-b-c");
}

#[test]
fn join_empty_array_returns_empty() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[join(createArray(), '-')]", &Context::new()).unwrap();
assert_eq!(result, "");
}

#[test]
fn join_array_of_integers() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[join(createArray(1,2,3), ',')]", &Context::new()).unwrap();
assert_eq!(result, "1,2,3");
}

#[test]
fn join_array_with_null_fails() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[join(createArray('a', null()), ',')]", &Context::new());
assert!(result.is_err());
// The error comes from argument validation, not our function
assert!(result.unwrap_err().to_string().contains("does not accept null arguments"));
}

#[test]
fn join_array_with_array_fails() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[join(createArray('a', createArray('b')), ',')]", &Context::new());
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Arguments must all be arrays") || error_msg.contains("mixed types"));
}

#[test]
fn join_array_with_object_fails() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[join(createArray('a', createObject('key', 'value')), ',')]", &Context::new());
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Arguments must all be") || error_msg.contains("mixed types"));
}

#[test]
fn join_direct_test_with_mixed_array() {
use serde_json::json;
use crate::configure::context::Context;

let join_fn = Join::default();
let args = vec![
json!(["hello", {"key": "value"}]), // Array with string and object
json!(",")
];
let result = join_fn.invoke(&args, &Context::new());

assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Array elements cannot be objects"));
}
}
2 changes: 2 additions & 0 deletions dsc_lib/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub mod less_or_equals;
pub mod format;
pub mod int;
pub mod index_of;
pub mod join;
pub mod last_index_of;
pub mod max;
pub mod min;
Expand Down Expand Up @@ -148,6 +149,7 @@ impl FunctionDispatcher {
Box::new(format::Format{}),
Box::new(int::Int{}),
Box::new(index_of::IndexOf{}),
Box::new(join::Join{}),
Box::new(last_index_of::LastIndexOf{}),
Box::new(max::Max{}),
Box::new(min::Min{}),
Expand Down
Loading