Skip to content

Commit bc5e1ae

Browse files
author
Steve Lee (POWERSHELL HE/HIM) (from Dev Box)
committed
Add contains() and union() Array functions
1 parent 493d75b commit bc5e1ae

File tree

5 files changed

+297
-0
lines changed

5 files changed

+297
-0
lines changed

dsc/tests/dsc_functions.tests.ps1

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,101 @@ 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('object'), 'a')]" ; expected = $true }
119+
@{ expression = "[contains(parameters('object'), 'c')]" ; expected = $false }
120+
@{ expression = "[contains(parameters('object'), 3)]" ; expected = $true }
121+
@{ expression = "[contains(parameters('object'), parameters('object'))]" ; isError = $true }
122+
@{ expression = "[contains(parameters('array'), parameters('array'))]" ; isError = $true }
123+
@{ expression = "[contains(parameters('string'), 'not found')]" ; expected = $false }
124+
@{ expression = "[contains(parameters('string'), 'hello')]" ; expected = $true }
125+
@{ expression = "[contains(parameters('string'), 12)]" ; expected = $true }
126+
) {
127+
param($expression, $expected, $isError)
128+
129+
$config_yaml = @"
130+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
131+
parameters:
132+
array:
133+
type: array
134+
defaultValue:
135+
- a
136+
- b
137+
- 1
138+
object:
139+
type: object
140+
defaultValue:
141+
a: 1
142+
b: 2
143+
3: c
144+
string:
145+
type: string
146+
defaultValue: 'hello 123 world!'
147+
resources:
148+
- name: Echo
149+
type: Microsoft.DSC.Debug/Echo
150+
properties:
151+
output: "$expression"
152+
"@
153+
$out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
154+
if ($isError) {
155+
$LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw)
156+
(Get-Content $TestDrive/error.log -Raw) | Should -Match 'Invalid item to find, must be a string or number'
157+
} else {
158+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
159+
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
160+
}
161+
}
65162
}

dsc_lib/locales/en-us.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@ argsMustBeStrings = "Arguments must all be strings"
229229
argsMustBeArrays = "Arguments must all be arrays"
230230
onlyArraysOfStrings = "Arguments must all be arrays of strings"
231231

232+
[functions.contains]
233+
description = "Checks if an array contains a specific item"
234+
invoked = "contains function"
235+
invalidItemToFind = "Invalid item to find, must be a string or number"
236+
232237
[functions.createArray]
233238
description = "Creates an array from the given elements"
234239
invoked = "createArray function"
@@ -361,6 +366,11 @@ invoked = "systemRoot function"
361366
description = "Returns the boolean value true"
362367
invoked = "true function"
363368

369+
[functions.union]
370+
description = "Returns a single array or object with all elements from the parameters"
371+
invoked = "union function"
372+
invalidArgType = "All arguments must either be arrays or objects"
373+
364374
[functions.variables]
365375
description = "Retrieves the value of a variable"
366376
invoked = "variables function"

dsc_lib/src/functions/contains.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
usize::MAX
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+
(string.to_string(), 0)
41+
} else if let Some(number) = args[1].as_i64() {
42+
(number.to_string(), 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 item_str == string_to_find {
52+
found = true;
53+
break;
54+
}
55+
} else if let Some(item_num) = item.as_i64() {
56+
if item_num == number_to_find {
57+
found = true;
58+
break;
59+
}
60+
}
61+
}
62+
return Ok(Value::Bool(found));
63+
}
64+
65+
// for object, we check if the key exists
66+
if let Some(object) = args[0].as_object() {
67+
// see if key exists
68+
for key in object.keys() {
69+
if key == &string_to_find {
70+
found = true;
71+
break;
72+
}
73+
}
74+
return Ok(Value::Bool(found));
75+
}
76+
77+
// for string, we check if the string contains the substring or number
78+
if let Some(str) = args[0].as_str() {
79+
if str.contains(&string_to_find) {
80+
found = true;
81+
}
82+
return Ok(Value::Bool(found));
83+
}
84+
85+
Err(DscError::Parser(t!("functions.contains.invalidArgType").to_string()))
86+
}
87+
}
88+
89+
#[cfg(test)]
90+
mod tests {
91+
use crate::configure::context::Context;
92+
use crate::parser::Statement;
93+
94+
#[test]
95+
fn string_contains_string() {
96+
let mut parser = Statement::new().unwrap();
97+
let result = parser.parse_and_execute("[contains('hello', 'lo')]", &Context::new()).unwrap();
98+
assert_eq!(result, true);
99+
}
100+
101+
#[test]
102+
fn string_does_not_contain_string() {
103+
let mut parser = Statement::new().unwrap();
104+
let result = parser.parse_and_execute("[contains('hello', 'world')]", &Context::new()).unwrap();
105+
assert_eq!(result, false);
106+
}
107+
108+
#[test]
109+
fn string_contains_number() {
110+
let mut parser = Statement::new().unwrap();
111+
let result = parser.parse_and_execute("[contains('hello123', 123)]", &Context::new()).unwrap();
112+
assert_eq!(result, true);
113+
}
114+
}
115+

dsc_lib/src/functions/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod and;
1616
pub mod base64;
1717
pub mod bool;
1818
pub mod concat;
19+
pub mod contains;
1920
pub mod create_array;
2021
pub mod div;
2122
pub mod envvar;
@@ -42,6 +43,7 @@ pub mod secret;
4243
pub mod sub;
4344
pub mod system_root;
4445
pub mod r#true;
46+
pub mod union;
4547
pub mod variables;
4648

4749
/// The kind of argument that a function accepts.
@@ -91,6 +93,7 @@ impl FunctionDispatcher {
9193
functions.insert("base64".to_string(), Box::new(base64::Base64{}));
9294
functions.insert("bool".to_string(), Box::new(bool::Bool{}));
9395
functions.insert("concat".to_string(), Box::new(concat::Concat{}));
96+
functions.insert("contains".to_string(), Box::new(contains::Contains{}));
9497
functions.insert("createArray".to_string(), Box::new(create_array::CreateArray{}));
9598
functions.insert("div".to_string(), Box::new(div::Div{}));
9699
functions.insert("envvar".to_string(), Box::new(envvar::Envvar{}));
@@ -117,6 +120,7 @@ impl FunctionDispatcher {
117120
functions.insert("sub".to_string(), Box::new(sub::Sub{}));
118121
functions.insert("systemRoot".to_string(), Box::new(system_root::SystemRoot{}));
119122
functions.insert("true".to_string(), Box::new(r#true::True{}));
123+
functions.insert("union".to_string(), Box::new(union::Union{}));
120124
functions.insert("variables".to_string(), Box::new(variables::Variables{}));
121125
Self {
122126
functions,

dsc_lib/src/functions/union.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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::{Map, Value};
9+
use tracing::debug;
10+
11+
#[derive(Debug, Default)]
12+
pub struct Union {}
13+
14+
impl Function for Union {
15+
fn description(&self) -> String {
16+
t!("functions.union.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+
usize::MAX
29+
}
30+
31+
fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
32+
vec![AcceptedArgKind::Array, AcceptedArgKind::Object]
33+
}
34+
35+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
36+
debug!("{}", t!("functions.union.invoked"));
37+
if args[0].is_array() {
38+
let mut result = Vec::new();
39+
// iterate through array and skip elements that are already in result
40+
for arg in args {
41+
if let Some(array) = arg.as_array() {
42+
for item in array {
43+
if !result.contains(item) {
44+
result.push(item.clone());
45+
}
46+
}
47+
} else {
48+
return Err(DscError::Parser(t!("functions.union.invalidArgType").to_string()));
49+
}
50+
}
51+
return Ok(Value::Array(result));
52+
}
53+
54+
if args[0].is_object() {
55+
let mut result = Map::new();
56+
// iterate through objects, duplicate keys are overwritten
57+
for arg in args {
58+
if let Some(object) = arg.as_object() {
59+
for (key, value) in object {
60+
result.insert(key.clone(), value.clone());
61+
}
62+
} else {
63+
return Err(DscError::Parser(t!("functions.union.invalidArgType").to_string()));
64+
}
65+
}
66+
return Ok(Value::Object(result));
67+
}
68+
69+
Err(DscError::Parser(t!("functions.union.invalidArgType").to_string()))
70+
}
71+
}

0 commit comments

Comments
 (0)