Skip to content

Commit ef989ce

Browse files
authored
Merge pull request #1219 from Gijsreyn/gh-57/main/add-tryindexfromend-function
Add `tryIndexFromEnd()` function
2 parents ff9a762 + 6dcbb96 commit ef989ce

File tree

5 files changed

+513
-0
lines changed

5 files changed

+513
-0
lines changed
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
---
2+
description: Reference for the 'tryIndexFromEnd' DSC configuration document function
3+
ms.date: 01/29/2025
4+
ms.topic: reference
5+
title: tryIndexFromEnd
6+
---
7+
8+
## Synopsis
9+
10+
Safely retrieves a value from an array by counting backward from the end without
11+
throwing an error if the index is out of range.
12+
13+
## Syntax
14+
15+
```Syntax
16+
tryIndexFromEnd(sourceArray, reverseIndex)
17+
```
18+
19+
## Description
20+
21+
The `tryIndexFromEnd()` function provides a safe way to access array elements by
22+
counting backward from the end using a one-based index. Unlike standard array
23+
indexing that might fail with out-of-bounds errors, this function returns `null`
24+
when the index is invalid or out of range.
25+
26+
This is particularly useful when working with dynamic arrays where the length
27+
isn't known in advance, or when implementing fallback logic that needs to handle
28+
missing data gracefully. The function uses a one-based index, meaning `1`
29+
refers to the last element, `2` to the second-to-last, and so on.
30+
31+
The function returns `null` in the following cases:
32+
33+
- The reverse index is greater than the array length
34+
- The reverse index is zero or negative
35+
- The array is empty
36+
37+
## Examples
38+
39+
### Example 1 - Access recent deployment history safely
40+
41+
Use `tryIndexFromEnd()` to access recent deployment records when you're not
42+
certain how many deployments have occurred. This is useful for rollback
43+
scenarios where you want to retrieve the previous deployment without causing
44+
errors if the history is empty or shorter than expected. This example uses
45+
[`createArray()`][05] to build the deployment history and [`last()`][00] to get
46+
the current deployment.
47+
48+
```yaml
49+
# tryIndexFromEnd.example.1.dsc.config.yaml
50+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
51+
resources:
52+
- name: Deployment Rollback
53+
type: Microsoft.DSC.Debug/Echo
54+
properties:
55+
output:
56+
currentDeployment: "[last(createArray('v1.0.0', 'v1.1.0', 'v1.2.0'))]"
57+
previousDeployment: "[tryIndexFromEnd(createArray('v1.0.0', 'v1.1.0', 'v1.2.0'), 2)]"
58+
fallbackDeployment: "[tryIndexFromEnd(createArray('v1.0.0', 'v1.1.0', 'v1.2.0'), 10)]"
59+
```
60+
61+
```bash
62+
dsc config get --file tryIndexFromEnd.example.1.dsc.config.yaml
63+
```
64+
65+
```yaml
66+
results:
67+
- name: Deployment Rollback
68+
type: Microsoft.DSC.Debug/Echo
69+
result:
70+
actualState:
71+
output:
72+
currentDeployment: v1.2.0
73+
previousDeployment: v1.1.0
74+
fallbackDeployment: null
75+
messages: []
76+
hadErrors: false
77+
```
78+
79+
The function returns `v1.1.0` for the second-to-last deployment, and `null` for
80+
the non-existent 10th-from-last deployment, allowing your configuration to
81+
handle missing data gracefully.
82+
83+
### Example 2 - Select backup retention with safe defaults
84+
85+
Use `tryIndexFromEnd()` to implement flexible backup retention policies that
86+
adapt to available backups without failing when fewer backups exist than
87+
expected. This example retrieves the third-most-recent backup if available. This
88+
example uses [`parameters()`][06] to reference the backup timestamps array.
89+
90+
```yaml
91+
# tryIndexFromEnd.example.2.dsc.config.yaml
92+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
93+
parameters:
94+
backupTimestamps:
95+
type: array
96+
defaultValue:
97+
- 20250101
98+
- 20250108
99+
- 20250115
100+
- 20250122
101+
- 20250129
102+
resources:
103+
- name: Backup Retention
104+
type: Microsoft.DSC.Debug/Echo
105+
properties:
106+
output:
107+
backups: "[parameters('backupTimestamps')]"
108+
retainAfter: "[tryIndexFromEnd(parameters('backupTimestamps'), 3)]"
109+
description: "Retain backups newer than the third-most-recent"
110+
```
111+
112+
```bash
113+
dsc config get --file tryIndexFromEnd.example.2.dsc.config.yaml
114+
```
115+
116+
```yaml
117+
results:
118+
- name: Backup Retention
119+
type: Microsoft.DSC.Debug/Echo
120+
result:
121+
actualState:
122+
output:
123+
backups:
124+
- 20250101
125+
- 20250108
126+
- 20250115
127+
- 20250122
128+
- 20250129
129+
retainAfter: 20250115
130+
description: Retain backups newer than the third-most-recent
131+
messages: []
132+
hadErrors: false
133+
```
134+
135+
The function safely returns `20250115` (the third-from-last backup), allowing
136+
you to implement a retention policy that keeps the three most recent backups.
137+
138+
### Example 3 - Parse log levels from configuration arrays
139+
140+
Use `tryIndexFromEnd()` to access configuration values from arrays of varying
141+
lengths. This is useful when configuration arrays might have different numbers
142+
of elements across environments. This example uses [`parameters()`][06] to
143+
reference the log level arrays.
144+
145+
```yaml
146+
# tryIndexFromEnd.example.3.dsc.config.yaml
147+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
148+
parameters:
149+
productionLevels:
150+
type: array
151+
defaultValue: [ERROR, WARN, INFO]
152+
devLevels:
153+
type: array
154+
defaultValue: [ERROR, WARN, INFO, DEBUG, TRACE]
155+
resources:
156+
- name: Log Configuration
157+
type: Microsoft.DSC.Debug/Echo
158+
properties:
159+
output:
160+
productionLevels: "[parameters('productionLevels')]"
161+
devLevels: "[parameters('devLevels')]"
162+
prodThirdLevel: "[tryIndexFromEnd(parameters('productionLevels'), 3)]"
163+
devThirdLevel: "[tryIndexFromEnd(parameters('devLevels'), 3)]"
164+
prodFifthLevel: "[tryIndexFromEnd(parameters('productionLevels'), 5)]"
165+
```
166+
167+
```bash
168+
dsc config get --file tryIndexFromEnd.example.3.dsc.config.yaml
169+
```
170+
171+
```yaml
172+
results:
173+
- name: Log Configuration
174+
type: Microsoft.DSC.Debug/Echo
175+
result:
176+
actualState:
177+
output:
178+
productionLevels:
179+
- ERROR
180+
- WARN
181+
- INFO
182+
devLevels:
183+
- ERROR
184+
- WARN
185+
- INFO
186+
- DEBUG
187+
- TRACE
188+
prodThirdLevel: ERROR
189+
devThirdLevel: INFO
190+
prodFifthLevel: null
191+
messages: []
192+
hadErrors: false
193+
```
194+
195+
The function safely handles arrays of different lengths, returning the
196+
appropriate log level or `null` without throwing errors.
197+
198+
### Example 4 - Access region-specific configuration with fallback
199+
200+
Use `tryIndexFromEnd()` with [`coalesce()`][02] to implement fallback logic when
201+
accessing configuration values from arrays that might have different lengths
202+
across regions. This example shows how to safely access regional endpoints with
203+
a default fallback. This example uses [`createArray()`][05] to build the
204+
regional endpoint arrays.
205+
206+
```yaml
207+
# tryIndexFromEnd.example.4.dsc.config.yaml
208+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
209+
resources:
210+
- name: Regional Endpoints
211+
type: Microsoft.DSC.Debug/Echo
212+
properties:
213+
output:
214+
primaryRegion: "[createArray('us-east-1', 'us-west-2', 'eu-west-1')]"
215+
secondaryRegion: "[createArray('us-west-1')]"
216+
preferredPrimary: "[coalesce(tryIndexFromEnd(createArray('us-east-1', 'us-west-2', 'eu-west-1'), 2), 'us-east-1')]"
217+
preferredSecondary: "[coalesce(tryIndexFromEnd(createArray('us-west-1'), 2), 'us-west-1')]"
218+
```
219+
220+
```bash
221+
dsc config get --file tryIndexFromEnd.example.4.dsc.config.yaml
222+
```
223+
224+
```yaml
225+
results:
226+
- name: Regional Endpoints
227+
type: Microsoft.DSC.Debug/Echo
228+
result:
229+
actualState:
230+
output:
231+
primaryRegion:
232+
- us-east-1
233+
- us-west-2
234+
- eu-west-1
235+
secondaryRegion:
236+
- us-west-1
237+
preferredPrimary: us-west-2
238+
preferredSecondary: us-west-1
239+
messages: []
240+
hadErrors: false
241+
```
242+
243+
By combining `tryIndexFromEnd()` with `coalesce()`, you get robust fallback
244+
behavior: `preferredPrimary` returns `us-west-2` (the second-to-last region),
245+
while `preferredSecondary` falls back to the default `us-west-1` when the
246+
second-to-last element doesn't exist.
247+
248+
## Parameters
249+
250+
### sourceArray
251+
252+
The array to retrieve the element from by counting backward from the end.
253+
Required.
254+
255+
```yaml
256+
Type: array
257+
Required: true
258+
Position: 1
259+
```
260+
261+
### reverseIndex
262+
263+
The one-based index from the end of the array. Must be a positive integer where
264+
`1` refers to the last element, `2` to the second-to-last, and so on. Required.
265+
266+
```yaml
267+
Type: integer
268+
Required: true
269+
Position: 2
270+
Minimum: 1
271+
```
272+
273+
## Output
274+
275+
Returns the array element at the specified reverse index if the index is valid
276+
(within array bounds). Returns `null` if the index is out of range, zero,
277+
negative, or if the array is empty.
278+
279+
The return type matches the type of the element in the array.
280+
281+
```yaml
282+
Type: any | null
283+
```
284+
285+
## Errors
286+
287+
The function returns an error in the following cases:
288+
289+
- **Invalid source type**: The first argument is not an array
290+
- **Invalid index type**: The second argument is not an integer
291+
292+
## Related functions
293+
294+
- [`last()`][00] - Returns the last element of an array (throws error if empty)
295+
- [`first()`][01] - Returns the first element of an array or character of a string
296+
- [`coalesce()`][02] - Returns the first non-null value from a list
297+
- [`equals()`][03] - Compares two values for equality
298+
- [`not()`][04] - Inverts a boolean value
299+
- [`createArray()`][05] - Creates an array from provided values
300+
- [`parameters()`][06] - Returns the value of a specified configuration parameter
301+
302+
<!-- Link reference definitions -->
303+
[00]: ./last.md
304+
[01]: ./first.md
305+
[02]: ./coalesce.md
306+
[03]: ./equals.md
307+
[04]: ./not.md
308+
[05]: ./createArray.md
309+
[06]: ./parameters.md

dsc/tests/dsc_functions.tests.ps1

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,40 @@ Describe 'tests for function expressions' {
956956
}
957957
}
958958

959+
It 'tryIndexFromEnd() function works for: <expression>' -TestCases @(
960+
@{ expression = "[tryIndexFromEnd(createArray('a', 'b', 'c'), 1)]"; expected = 'c' }
961+
@{ expression = "[tryIndexFromEnd(createArray('a', 'b', 'c'), 2)]"; expected = 'b' }
962+
@{ expression = "[tryIndexFromEnd(createArray('a', 'b', 'c'), 3)]"; expected = 'a' }
963+
@{ expression = "[tryIndexFromEnd(createArray('a', 'b', 'c'), 4)]"; expected = $null }
964+
@{ expression = "[tryIndexFromEnd(createArray('a', 'b', 'c'), 0)]"; expected = $null }
965+
@{ expression = "[tryIndexFromEnd(createArray('a', 'b', 'c'), -1)]"; expected = $null }
966+
@{ expression = "[tryIndexFromEnd(createArray('only'), 1)]"; expected = 'only' }
967+
@{ expression = "[tryIndexFromEnd(createArray(10, 20, 30, 40), 2)]"; expected = 30 }
968+
@{ expression = "[tryIndexFromEnd(createArray(createObject('k', 'v1'), createObject('k', 'v2')), 1)]"; expected = [pscustomobject]@{ k = 'v2' } }
969+
@{ expression = "[tryIndexFromEnd(createArray(createArray(1, 2), createArray(3, 4)), 1)]"; expected = @(3, 4) }
970+
@{ expression = "[tryIndexFromEnd(createArray(), 1)]"; expected = $null }
971+
@{ expression = "[tryIndexFromEnd(createArray('x', 'y'), 1000)]"; expected = $null }
972+
) {
973+
param($expression, $expected)
974+
975+
$config_yaml = @"
976+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
977+
resources:
978+
- name: Echo
979+
type: Microsoft.DSC.Debug/Echo
980+
properties:
981+
output: "$expression"
982+
"@
983+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
984+
if ($expected -is [pscustomobject]) {
985+
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
986+
} elseif ($expected -is [array]) {
987+
($out.results[0].result.actualState.output | ConvertTo-Json -Compress) | Should -BeExactly ($expected | ConvertTo-Json -Compress)
988+
} else {
989+
$out.results[0].result.actualState.output | Should -BeExactly $expected
990+
}
991+
}
992+
959993
It 'uriComponent function works for: <testInput>' -TestCases @(
960994
@{ testInput = 'hello world' }
961995
@{ testInput = 'hello@example.com' }

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,12 @@ invoked = "tryGet function"
547547
invalidKeyType = "Invalid key type, must be a string"
548548
invalidIndexType = "Invalid index type, must be an integer"
549549

550+
[functions.tryIndexFromEnd]
551+
description = "Retrieves a value from an array by counting backward from the end. Returns null if the index is out of range."
552+
invoked = "tryIndexFromEnd function"
553+
invalidSourceType = "Invalid source type, must be an array"
554+
invalidIndexType = "Invalid index type, must be an integer"
555+
550556
[functions.union]
551557
description = "Returns a single array or object with all elements from the parameters"
552558
invoked = "union function"

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ pub mod to_upper;
7171
pub mod trim;
7272
pub mod r#true;
7373
pub mod try_get;
74+
pub mod try_index_from_end;
7475
pub mod union;
7576
pub mod unique_string;
7677
pub mod uri;
@@ -200,6 +201,7 @@ impl FunctionDispatcher {
200201
Box::new(trim::Trim{}),
201202
Box::new(r#true::True{}),
202203
Box::new(try_get::TryGet{}),
204+
Box::new(try_index_from_end::TryIndexFromEnd{}),
203205
Box::new(union::Union{}),
204206
Box::new(unique_string::UniqueString{}),
205207
Box::new(uri::Uri{}),

0 commit comments

Comments
 (0)