Skip to content

Commit 751b908

Browse files
authored
Merge pull request #1005 from SteveL-MSFT/array-functions
Add `contains()`, `union()`, `length()`, and `empty()` Array functions
2 parents 22b64f6 + 3001dad commit 751b908

File tree

7 files changed

+556
-0
lines changed

7 files changed

+556
-0
lines changed

dsc/tests/dsc_functions.tests.ps1

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,186 @@ Describe 'tests for function expressions' {
6262
$LASTEXITCODE | Should -Be 0
6363
$out.results[0].result.actualState.output | Should -BeExactly $expected
6464
}
65+
66+
It 'union function works for: <expression>' -TestCases @(
67+
@{ expression = "[union(parameters('firstArray'), parameters('secondArray'))]"; expected = @('ab', 'cd', 'ef') }
68+
@{ expression = "[union(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ one = 'a'; two = 'c'; three = 'd' } }
69+
@{ expression = "[union(parameters('secondArray'), parameters('secondArray'))]"; expected = @('cd', 'ef') }
70+
@{ expression = "[union(parameters('secondObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'c'; three = 'd' } }
71+
@{ expression = "[union(parameters('firstObject'), parameters('firstArray'))]"; isError = $true }
72+
) {
73+
param($expression, $expected, $isError)
74+
75+
$config_yaml = @"
76+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
77+
parameters:
78+
firstObject:
79+
type: object
80+
defaultValue:
81+
one: a
82+
two: b
83+
secondObject:
84+
type: object
85+
defaultValue:
86+
two: c
87+
three: d
88+
firstArray:
89+
type: array
90+
defaultValue:
91+
- ab
92+
- cd
93+
secondArray:
94+
type: array
95+
defaultValue:
96+
- cd
97+
- ef
98+
resources:
99+
- name: Echo
100+
type: Microsoft.DSC.Debug/Echo
101+
properties:
102+
output: "$expression"
103+
"@
104+
$out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
105+
if ($isError) {
106+
$LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw)
107+
(Get-Content $TestDrive/error.log -Raw) | Should -Match 'All arguments must either be arrays or objects'
108+
} else {
109+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
110+
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
111+
}
112+
}
113+
114+
It 'contain function works for: <expression>' -TestCases @(
115+
@{ expression = "[contains(parameters('array'), 'a')]" ; expected = $true }
116+
@{ expression = "[contains(parameters('array'), 2)]" ; expected = $false }
117+
@{ expression = "[contains(parameters('array'), 1)]" ; expected = $true }
118+
@{ expression = "[contains(parameters('array'), 'z')]" ; expected = $false }
119+
@{ expression = "[contains(parameters('object'), 'a')]" ; expected = $true }
120+
@{ expression = "[contains(parameters('object'), 'c')]" ; expected = $false }
121+
@{ expression = "[contains(parameters('object'), 3)]" ; expected = $true }
122+
@{ expression = "[contains(parameters('object'), parameters('object'))]" ; isError = $true }
123+
@{ expression = "[contains(parameters('array'), parameters('array'))]" ; isError = $true }
124+
@{ expression = "[contains(parameters('string'), 'not found')]" ; expected = $false }
125+
@{ expression = "[contains(parameters('string'), 'hello')]" ; expected = $true }
126+
@{ expression = "[contains(parameters('string'), 12)]" ; expected = $true }
127+
) {
128+
param($expression, $expected, $isError)
129+
130+
$config_yaml = @"
131+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
132+
parameters:
133+
array:
134+
type: array
135+
defaultValue:
136+
- a
137+
- b
138+
- 0
139+
- 1
140+
object:
141+
type: object
142+
defaultValue:
143+
a: 1
144+
b: 2
145+
3: c
146+
string:
147+
type: string
148+
defaultValue: 'hello 123 world!'
149+
resources:
150+
- name: Echo
151+
type: Microsoft.DSC.Debug/Echo
152+
properties:
153+
output: "$expression"
154+
"@
155+
$out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
156+
if ($isError) {
157+
$LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw)
158+
(Get-Content $TestDrive/error.log -Raw) | Should -Match 'Invalid item to find, must be a string or number'
159+
} else {
160+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
161+
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
162+
}
163+
}
164+
165+
It 'length function works for: <expression>' -TestCases @(
166+
@{ expression = "[length(parameters('array'))]" ; expected = 3 }
167+
@{ expression = "[length(parameters('object'))]" ; expected = 4 }
168+
@{ expression = "[length(parameters('string'))]" ; expected = 12 }
169+
@{ expression = "[length('')]"; expected = 0 }
170+
) {
171+
param($expression, $expected, $isError)
172+
173+
$config_yaml = @"
174+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
175+
parameters:
176+
array:
177+
type: array
178+
defaultValue:
179+
- a
180+
- b
181+
- c
182+
object:
183+
type: object
184+
defaultValue:
185+
one: a
186+
two: b
187+
three: c
188+
four: d
189+
string:
190+
type: string
191+
defaultValue: 'hello world!'
192+
resources:
193+
- name: Echo
194+
type: Microsoft.DSC.Debug/Echo
195+
properties:
196+
output: "$expression"
197+
"@
198+
$out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
199+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
200+
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
201+
}
202+
203+
It 'empty function works for: <expression>' -TestCases @(
204+
@{ expression = "[empty(parameters('array'))]" ; expected = $false }
205+
@{ expression = "[empty(parameters('object'))]" ; expected = $false }
206+
@{ expression = "[empty(parameters('string'))]" ; expected = $false }
207+
@{ expression = "[empty(parameters('emptyArray'))]" ; expected = $true }
208+
@{ expression = "[empty(parameters('emptyObject'))]" ; expected = $true }
209+
@{ expression = "[empty('')]" ; expected = $true }
210+
) {
211+
param($expression, $expected)
212+
213+
$config_yaml = @"
214+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
215+
parameters:
216+
array:
217+
type: array
218+
defaultValue:
219+
- a
220+
- b
221+
- c
222+
emptyArray:
223+
type: array
224+
defaultValue: []
225+
object:
226+
type: object
227+
defaultValue:
228+
one: a
229+
two: b
230+
three: c
231+
emptyObject:
232+
type: object
233+
defaultValue: {}
234+
string:
235+
type: string
236+
defaultValue: 'hello world!'
237+
resources:
238+
- name: Echo
239+
type: Microsoft.DSC.Debug/Echo
240+
properties:
241+
output: "$expression"
242+
"@
243+
$out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
244+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
245+
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
246+
}
65247
}

dsc_lib/locales/en-us.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,12 @@ argsMustBeStrings = "Arguments must all be strings"
234234
argsMustBeArrays = "Arguments must all be arrays"
235235
onlyArraysOfStrings = "Arguments must all be arrays of strings"
236236

237+
[functions.contains]
238+
description = "Checks if an array contains a specific item"
239+
invoked = "contains function"
240+
invalidItemToFind = "Invalid item to find, must be a string or number"
241+
invalidArgType = "Invalid argument type, first argument must be an array, object, or string"
242+
237243
[functions.createArray]
238244
description = "Creates an array from the given elements"
239245
invoked = "createArray function"
@@ -253,6 +259,11 @@ description = "Divides the first number by the second"
253259
invoked = "div function"
254260
divideByZero = "Cannot divide by zero"
255261

262+
[functions.empty]
263+
description = "Checks if an array, object, or string is empty"
264+
invoked = "empty function"
265+
invalidArgType = "Invalid argument type, argument must be an array, object, or string"
266+
256267
[functions.envvar]
257268
description = "Retrieves the value of an environment variable"
258269
notFound = "Environment variable not found"
@@ -291,6 +302,11 @@ parseStringError = "unable to parse string to int"
291302
castError = "unable to cast to int"
292303
parseNumError = "unable to parse number to int"
293304

305+
[functions.length]
306+
description = "Returns the length of a string, array, or object"
307+
invoked = "length function"
308+
invalidArgType = "Invalid argument type, argument must be a string, array, or object"
309+
294310
[functions.less]
295311
description = "Evaluates if the first value is less than the second value"
296312
invoked = "less function"
@@ -376,6 +392,11 @@ invoked = "systemRoot function"
376392
description = "Returns the boolean value true"
377393
invoked = "true function"
378394

395+
[functions.union]
396+
description = "Returns a single array or object with all elements from the parameters"
397+
invoked = "union function"
398+
invalidArgType = "All arguments must either be arrays or objects"
399+
379400
[functions.variables]
380401
description = "Retrieves the value of a variable"
381402
invoked = "variables function"

dsc_lib/src/functions/contains.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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::{AcceptedArgKind, Function, FunctionCategory};
7+
use rust_i18n::t;
8+
use serde_json::Value;
9+
use tracing::debug;
10+
11+
#[derive(Debug, Default)]
12+
pub struct Contains {}
13+
14+
impl Function for Contains {
15+
fn description(&self) -> String {
16+
t!("functions.contains.description").to_string()
17+
}
18+
19+
fn category(&self) -> FunctionCategory {
20+
FunctionCategory::Array
21+
}
22+
23+
fn min_args(&self) -> usize {
24+
2
25+
}
26+
27+
fn max_args(&self) -> usize {
28+
2
29+
}
30+
31+
fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
32+
vec![AcceptedArgKind::Array, AcceptedArgKind::Object, AcceptedArgKind::String, AcceptedArgKind::Number]
33+
}
34+
35+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
36+
debug!("{}", t!("functions.contains.invoked"));
37+
let mut found = false;
38+
39+
let (string_to_find, number_to_find) = if let Some(string) = args[1].as_str() {
40+
(Some(string.to_string()), None)
41+
} else if let Some(number) = args[1].as_i64() {
42+
(None, Some(number))
43+
} else {
44+
return Err(DscError::Parser(t!("functions.contains.invalidItemToFind").to_string()));
45+
};
46+
47+
// for array, we check if the string or number exists
48+
if let Some(array) = args[0].as_array() {
49+
for item in array {
50+
if let Some(item_str) = item.as_str() {
51+
if let Some(string) = &string_to_find {
52+
if item_str == string {
53+
found = true;
54+
break;
55+
}
56+
}
57+
} else if let Some(item_num) = item.as_i64() {
58+
if let Some(number) = number_to_find {
59+
if item_num == number {
60+
found = true;
61+
break;
62+
}
63+
}
64+
}
65+
}
66+
return Ok(Value::Bool(found));
67+
}
68+
69+
// for object, we check if the key exists
70+
if let Some(object) = args[0].as_object() {
71+
// see if key exists
72+
for key in object.keys() {
73+
if let Some(string) = &string_to_find {
74+
if key == string {
75+
found = true;
76+
break;
77+
}
78+
} else if let Some(number) = number_to_find {
79+
if key == &number.to_string() {
80+
found = true;
81+
break;
82+
}
83+
}
84+
}
85+
return Ok(Value::Bool(found));
86+
}
87+
88+
// for string, we check if the string contains the substring or number
89+
if let Some(str) = args[0].as_str() {
90+
if let Some(string) = &string_to_find {
91+
found = str.contains(string);
92+
} else if let Some(number) = number_to_find {
93+
found = str.contains(&number.to_string());
94+
}
95+
return Ok(Value::Bool(found));
96+
}
97+
98+
Err(DscError::Parser(t!("functions.contains.invalidArgType").to_string()))
99+
}
100+
}
101+
102+
#[cfg(test)]
103+
mod tests {
104+
use crate::configure::context::Context;
105+
use crate::parser::Statement;
106+
107+
#[test]
108+
fn string_contains_string() {
109+
let mut parser = Statement::new().unwrap();
110+
let result = parser.parse_and_execute("[contains('hello', 'lo')]", &Context::new()).unwrap();
111+
assert_eq!(result, true);
112+
}
113+
114+
#[test]
115+
fn string_does_not_contain_string() {
116+
let mut parser = Statement::new().unwrap();
117+
let result = parser.parse_and_execute("[contains('hello', 'world')]", &Context::new()).unwrap();
118+
assert_eq!(result, false);
119+
}
120+
121+
#[test]
122+
fn string_contains_number() {
123+
let mut parser = Statement::new().unwrap();
124+
let result = parser.parse_and_execute("[contains('hello123', 123)]", &Context::new()).unwrap();
125+
assert_eq!(result, true);
126+
}
127+
}
128+

0 commit comments

Comments
 (0)