Skip to content

Commit 4a8c8ca

Browse files
authored
Merge pull request #1211 from Gijsreyn/gh-57/main/add-last-function
Add `last()` function
2 parents 33d2f64 + 25c8e36 commit 4a8c8ca

File tree

5 files changed

+378
-0
lines changed

5 files changed

+378
-0
lines changed
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
---
2+
description: Reference for the 'last' DSC configuration document function
3+
ms.date: 01/25/2025
4+
ms.topic: reference
5+
title: last
6+
---
7+
8+
## Synopsis
9+
10+
Returns the last element of an array, or the last character of a string.
11+
12+
## Syntax
13+
14+
```Syntax
15+
last(arg)
16+
```
17+
18+
## Description
19+
20+
The `last()` function returns the final element from an array or the final
21+
character from a string. This is useful when you need to access the most recent
22+
item in a sequence, the final stage in a deployment pipeline, or the last
23+
character in a configuration value.
24+
25+
For arrays, it returns the element at index `length - 1`. For strings, it
26+
returns the last character as a string.
27+
28+
## Examples
29+
30+
### Example 1 - Extract the final deployment stage (array of strings)
31+
32+
Use `last()` to retrieve the final stage in a multi-stage deployment pipeline.
33+
This helps you identify which environment or phase should receive special
34+
handling, such as extended health checks or manual approval gates. This example
35+
uses [`createArray()`][01] to build the deployment stages.
36+
37+
```yaml
38+
# last.example.1.dsc.config.yaml
39+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
40+
resources:
41+
- name: Deployment Pipeline
42+
type: Microsoft.DSC.Debug/Echo
43+
properties:
44+
output:
45+
finalStage: "[last(createArray('dev', 'test', 'staging', 'production'))]"
46+
requiresApproval: true
47+
```
48+
49+
```bash
50+
dsc config get --file last.example.1.dsc.config.yaml
51+
```
52+
53+
```yaml
54+
results:
55+
- name: Deployment Pipeline
56+
type: Microsoft.DSC.Debug/Echo
57+
result:
58+
actualState:
59+
output:
60+
finalStage: production
61+
requiresApproval: true
62+
messages: []
63+
hadErrors: false
64+
```
65+
66+
This identifies `production` as the final stage, allowing you to apply
67+
production-specific policies or validations.
68+
69+
### Example 2 - Get the last character of a configuration string
70+
71+
Use `last()` to extract the final character from a string value. This is useful
72+
for parsing identifiers, checking suffixes, or validating format conventions
73+
like version numbers or region codes.
74+
75+
```yaml
76+
# last.example.2.dsc.config.yaml
77+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
78+
resources:
79+
- name: Region Identifier
80+
type: Microsoft.DSC.Debug/Echo
81+
properties:
82+
output:
83+
regionCode: us-west-2
84+
zoneSuffix: "[last('us-west-2')]"
85+
description: "Zone suffix extracted from region code"
86+
```
87+
88+
```bash
89+
dsc config get --file last.example.2.dsc.config.yaml
90+
```
91+
92+
```yaml
93+
results:
94+
- name: Region Identifier
95+
type: Microsoft.DSC.Debug/Echo
96+
result:
97+
actualState:
98+
output:
99+
regionCode: us-west-2
100+
zoneSuffix: '2'
101+
description: Zone suffix extracted from region code
102+
messages: []
103+
hadErrors: false
104+
```
105+
106+
The function returns `'2'` as a single-character string, representing the zone
107+
suffix in the region identifier.
108+
109+
### Example 3 - Identify the most recent backup (array of numbers)
110+
111+
Use `last()` with numerical arrays to find the most recent timestamp or version
112+
number. This example shows how to select the latest backup from a sorted list
113+
of timestamps. This example uses [`createArray()`][01] to build the backup
114+
timestamps.
115+
116+
```yaml
117+
# last.example.3.dsc.config.yaml
118+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
119+
resources:
120+
- name: Backup Selection
121+
type: Microsoft.DSC.Debug/Echo
122+
properties:
123+
output:
124+
availableBackups: "[createArray(1704067200, 1704153600, 1704240000, 1704326400)]"
125+
latestBackup: "[last(createArray(1704067200, 1704153600, 1704240000, 1704326400))]"
126+
description: "Most recent backup timestamp (Unix epoch)"
127+
```
128+
129+
```bash
130+
dsc config get --file last.example.3.dsc.config.yaml
131+
```
132+
133+
```yaml
134+
results:
135+
- name: Backup Selection
136+
type: Microsoft.DSC.Debug/Echo
137+
result:
138+
actualState:
139+
output:
140+
availableBackups:
141+
- 1704067200
142+
- 1704153600
143+
- 1704240000
144+
- 1704326400
145+
latestBackup: 1704326400
146+
description: Most recent backup timestamp (Unix epoch)
147+
messages: []
148+
hadErrors: false
149+
```
150+
151+
The function returns `1704326400`, which represents the most recent backup in
152+
the chronologically sorted array.
153+
154+
### Example 4 - Combine with other functions for complex logic
155+
156+
Use `last()` together with [`split()`][02] to extract file extensions or path
157+
components. This example demonstrates parsing a filename to get its extension.
158+
159+
```yaml
160+
# last.example.4.dsc.config.yaml
161+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
162+
resources:
163+
- name: File Extension Parser
164+
type: Microsoft.DSC.Debug/Echo
165+
properties:
166+
output:
167+
filename: config.production.yaml
168+
extension: "[last(split('config.production.yaml', '.'))]"
169+
```
170+
171+
```bash
172+
dsc config get --file last.example.4.dsc.config.yaml
173+
```
174+
175+
```yaml
176+
results:
177+
- name: File Extension Parser
178+
type: Microsoft.DSC.Debug/Echo
179+
result:
180+
actualState:
181+
output:
182+
filename: config.production.yaml
183+
extension: yaml
184+
messages: []
185+
hadErrors: false
186+
```
187+
188+
By combining `split()` and `last()`, you can extract the `yaml` extension from
189+
the full filename.
190+
191+
## Parameters
192+
193+
### arg
194+
195+
The array or string to get the last element or character from. Required.
196+
197+
```yaml
198+
Type: array | string
199+
Required: true
200+
Position: 1
201+
```
202+
203+
## Output
204+
205+
Returns the last element of the array (preserving its original type) or the
206+
last character as a string. For arrays, the return type matches the element
207+
type. For strings, returns a single-character string.
208+
209+
If the input is an empty array, the function returns `null`. If the input is an
210+
empty string, the function returns an empty string.
211+
212+
```yaml
213+
Type: any | string | null
214+
```
215+
216+
## Errors
217+
218+
The function returns an error in the following cases:
219+
220+
- **Invalid type**: The argument is not an array or string
221+
222+
## Related functions
223+
224+
- [`first()`][00] - Returns the first element of an array or character of a string
225+
- [`split()`][02] - Splits a string into an array
226+
- [`createArray()`][01] - Creates an array from provided values
227+
228+
<!-- Link reference definitions -->
229+
[00]: ./first.md
230+
[01]: ./createArray.md
231+
[02]: ./split.md

dsc/tests/dsc_functions.tests.ps1

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,30 @@ Describe 'tests for function expressions' {
504504
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
505505
}
506506

507+
It 'last function works for: <expression>' -TestCases @(
508+
@{ expression = "[last(createArray('hello', 'world'))]"; expected = 'world' }
509+
@{ expression = "[last(createArray(1, 2, 3))]"; expected = 3 }
510+
@{ expression = "[last('hello')]"; expected = 'o' }
511+
@{ expression = "[last('a')]"; expected = 'a' }
512+
@{ expression = "[last(array('mixed'))]"; expected = 'mixed' }
513+
@{ expression = "[last(createArray())]"; expected = $null }
514+
@{ expression = "[last('')]"; expected = '' }
515+
) {
516+
param($expression, $expected)
517+
518+
$config_yaml = @"
519+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
520+
resources:
521+
- name: Echo
522+
type: Microsoft.DSC.Debug/Echo
523+
properties:
524+
output: "$expression"
525+
"@
526+
$out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
527+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
528+
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
529+
}
530+
507531
It 'indexOf function works for: <expression>' -TestCases @(
508532
@{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'banana')]"; expected = 1 }
509533
@{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'cherry')]"; expected = 2 }

lib/dsc-lib/locales/en-us.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,11 @@ emptyArray = "Cannot get first element of empty array"
343343
emptyString = "Cannot get first character of empty string"
344344
invalidArgType = "Invalid argument type, argument must be an array or string"
345345

346+
[functions.last]
347+
description = "Returns the last element of an array or last character of a string"
348+
invoked = "last function"
349+
invalidArgType = "Invalid argument type, argument must be an array or string"
350+
346351
[functions.greater]
347352
description = "Evaluates if the first value is greater than the second value"
348353
invoked = "greater function"

lib/dsc-lib/src/functions/last.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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 Last {}
13+
14+
impl Function for Last {
15+
fn get_metadata(&self) -> FunctionMetadata {
16+
FunctionMetadata {
17+
name: "last".to_string(),
18+
description: t!("functions.last.description").to_string(),
19+
category: vec![FunctionCategory::Array, FunctionCategory::String],
20+
min_args: 1,
21+
max_args: 1,
22+
accepted_arg_ordered_types: vec![vec![FunctionArgKind::Array, FunctionArgKind::String]],
23+
remaining_arg_accepted_types: None,
24+
return_types: vec![FunctionArgKind::String, FunctionArgKind::Number, FunctionArgKind::Array, FunctionArgKind::Object, FunctionArgKind::Null],
25+
}
26+
}
27+
28+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
29+
debug!("{}", t!("functions.last.invoked"));
30+
31+
if let Some(array) = args[0].as_array() {
32+
if array.is_empty() {
33+
return Ok(Value::Null);
34+
}
35+
return Ok(array[array.len() - 1].clone());
36+
}
37+
38+
if let Some(string) = args[0].as_str() {
39+
if string.is_empty() {
40+
return Ok(Value::String(String::new()));
41+
}
42+
return Ok(Value::String(string.chars().last().unwrap().to_string()));
43+
}
44+
45+
Err(DscError::Parser(t!("functions.last.invalidArgType").to_string()))
46+
}
47+
}
48+
49+
#[cfg(test)]
50+
mod tests {
51+
use crate::configure::context::Context;
52+
use crate::parser::Statement;
53+
54+
#[test]
55+
fn array_of_strings() {
56+
let mut parser = Statement::new().unwrap();
57+
let result = parser.parse_and_execute("[last(createArray('hello', 'world'))]", &Context::new()).unwrap();
58+
assert_eq!(result.as_str(), Some("world"));
59+
}
60+
61+
#[test]
62+
fn array_of_numbers() {
63+
let mut parser = Statement::new().unwrap();
64+
let result = parser.parse_and_execute("[last(createArray(1, 2, 3))]", &Context::new()).unwrap();
65+
assert_eq!(result.to_string(), "3");
66+
}
67+
68+
#[test]
69+
fn array_of_single_element() {
70+
let mut parser = Statement::new().unwrap();
71+
let result = parser.parse_and_execute("[last(array('hello'))]", &Context::new()).unwrap();
72+
assert_eq!(result.as_str(), Some("hello"));
73+
}
74+
75+
#[test]
76+
fn string_input() {
77+
let mut parser = Statement::new().unwrap();
78+
let result = parser.parse_and_execute("[last('hello')]", &Context::new()).unwrap();
79+
assert_eq!(result.as_str(), Some("o"));
80+
}
81+
82+
#[test]
83+
fn single_character_string() {
84+
let mut parser = Statement::new().unwrap();
85+
let result = parser.parse_and_execute("[last('a')]", &Context::new()).unwrap();
86+
assert_eq!(result.as_str(), Some("a"));
87+
}
88+
89+
#[test]
90+
fn invalid_type_object() {
91+
let mut parser = Statement::new().unwrap();
92+
let result = parser.parse_and_execute("[last(createObject('key', 'value'))]", &Context::new());
93+
assert!(result.is_err());
94+
}
95+
96+
#[test]
97+
fn invalid_type_number() {
98+
let mut parser = Statement::new().unwrap();
99+
let result = parser.parse_and_execute("[last(42)]", &Context::new());
100+
assert!(result.is_err());
101+
}
102+
103+
#[test]
104+
fn array_of_multiple_strings() {
105+
let mut parser = Statement::new().unwrap();
106+
let result = parser.parse_and_execute("[last(createArray('text', 'middle', 'last'))]", &Context::new()).unwrap();
107+
assert_eq!(result.as_str(), Some("last"));
108+
}
109+
110+
#[test]
111+
fn unicode_string() {
112+
let mut parser = Statement::new().unwrap();
113+
let result = parser.parse_and_execute("[last('Hello🌍')]", &Context::new()).unwrap();
114+
assert_eq!(result.as_str(), Some("🌍"));
115+
}
116+
}

0 commit comments

Comments
 (0)