Skip to content

Commit bdc62b1

Browse files
authored
Merge pull request #1041 from Gijsreyn/add-array-functions
Add array(), first(), indexOf() functions
2 parents 12a5faf + 564f985 commit bdc62b1

File tree

9 files changed

+643
-163
lines changed

9 files changed

+643
-163
lines changed

dsc/tests/dsc_functions.tests.ps1

Lines changed: 224 additions & 156 deletions
Large diffs are not rendered by default.

dsc_lib/locales/en-us.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ invoked = "add function"
217217
description = "Evaluates if all arguments are true"
218218
invoked = "and function"
219219

220+
[functions.array]
221+
description = "Convert the value to an array"
222+
invoked = "array function"
223+
220224
[functions.base64]
221225
description = "Encodes a string to Base64 format"
222226

@@ -280,6 +284,13 @@ description = "Evaluates if the two values are the same"
280284
description = "Returns the boolean value false"
281285
invoked = "false function"
282286

287+
[functions.first]
288+
description = "Returns the first element of an array or first character of a string"
289+
invoked = "first function"
290+
emptyArray = "Cannot get first element of empty array"
291+
emptyString = "Cannot get first character of empty string"
292+
invalidArgType = "Invalid argument type, argument must be an array or string"
293+
283294
[functions.greater]
284295
description = "Evaluates if the first value is greater than the second value"
285296
invoked = "greater function"
@@ -307,6 +318,11 @@ parseStringError = "unable to parse string to int"
307318
castError = "unable to cast to int"
308319
parseNumError = "unable to parse number to int"
309320

321+
[functions.indexOf]
322+
description = "Returns the index of the first occurrence of an item in an array"
323+
invoked = "indexOf function"
324+
invalidArrayArg = "First argument must be an array"
325+
310326
[functions.length]
311327
description = "Returns the length of a string, array, or object"
312328
invoked = "length function"

dsc_lib/src/functions/array.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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 Array {}
13+
14+
impl Function for Array {
15+
fn get_metadata(&self) -> FunctionMetadata {
16+
FunctionMetadata {
17+
name: "array".to_string(),
18+
description: t!("functions.array.description").to_string(),
19+
category: FunctionCategory::Array,
20+
min_args: 1,
21+
max_args: 1,
22+
accepted_arg_ordered_types: vec![
23+
vec![FunctionArgKind::String, FunctionArgKind::Number, FunctionArgKind::Object, FunctionArgKind::Array],
24+
],
25+
remaining_arg_accepted_types: None,
26+
return_types: vec![FunctionArgKind::Array],
27+
}
28+
}
29+
30+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
31+
debug!("{}", t!("functions.array.invoked"));
32+
33+
Ok(Value::Array(vec![args[0].clone()]))
34+
}
35+
}
36+
37+
#[cfg(test)]
38+
mod tests {
39+
use crate::configure::context::Context;
40+
use crate::parser::Statement;
41+
42+
#[test]
43+
fn single_string() {
44+
let mut parser = Statement::new().unwrap();
45+
let result = parser.parse_and_execute("[array('hello')]", &Context::new()).unwrap();
46+
assert_eq!(result.to_string(), r#"["hello"]"#);
47+
}
48+
49+
#[test]
50+
fn single_number() {
51+
let mut parser = Statement::new().unwrap();
52+
let result = parser.parse_and_execute("[array(42)]", &Context::new()).unwrap();
53+
assert_eq!(result.to_string(), "[42]");
54+
}
55+
56+
#[test]
57+
fn single_object() {
58+
let mut parser = Statement::new().unwrap();
59+
let result = parser.parse_and_execute("[array(createObject('key', 'value'))]", &Context::new()).unwrap();
60+
assert_eq!(result.to_string(), r#"[{"key":"value"}]"#);
61+
}
62+
63+
#[test]
64+
fn single_array() {
65+
let mut parser = Statement::new().unwrap();
66+
let result = parser.parse_and_execute("[array(createArray('a','b'))]", &Context::new()).unwrap();
67+
assert_eq!(result.to_string(), r#"[["a","b"]]"#);
68+
}
69+
70+
#[test]
71+
fn empty_array_not_allowed() {
72+
let mut parser = Statement::new().unwrap();
73+
let result = parser.parse_and_execute("[array()]", &Context::new());
74+
assert!(result.is_err());
75+
}
76+
77+
#[test]
78+
fn multiple_args_not_allowed() {
79+
let mut parser = Statement::new().unwrap();
80+
let result = parser.parse_and_execute("[array('hello', 42)]", &Context::new());
81+
assert!(result.is_err());
82+
}
83+
84+
#[test]
85+
fn invalid_type_boolean() {
86+
let mut parser = Statement::new().unwrap();
87+
let result = parser.parse_and_execute("[array(true)]", &Context::new());
88+
assert!(result.is_err());
89+
}
90+
91+
#[test]
92+
fn invalid_type_null() {
93+
let mut parser = Statement::new().unwrap();
94+
let result = parser.parse_and_execute("[array(null())]", &Context::new());
95+
assert!(result.is_err());
96+
}
97+
}

dsc_lib/src/functions/first.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 First {}
13+
14+
impl Function for First {
15+
fn get_metadata(&self) -> FunctionMetadata {
16+
FunctionMetadata {
17+
name: "first".to_string(),
18+
description: t!("functions.first.description").to_string(),
19+
category: FunctionCategory::Array,
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],
25+
}
26+
}
27+
28+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
29+
debug!("{}", t!("functions.first.invoked"));
30+
31+
if let Some(array) = args[0].as_array() {
32+
if array.is_empty() {
33+
return Err(DscError::Parser(t!("functions.first.emptyArray").to_string()));
34+
}
35+
return Ok(array[0].clone());
36+
}
37+
38+
if let Some(string) = args[0].as_str() {
39+
if string.is_empty() {
40+
return Err(DscError::Parser(t!("functions.first.emptyString").to_string()));
41+
}
42+
return Ok(Value::String(string.chars().next().unwrap().to_string()));
43+
}
44+
45+
Err(DscError::Parser(t!("functions.first.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("[first(createArray('hello', 'world'))]", &Context::new()).unwrap();
58+
assert_eq!(result.as_str(), Some("hello"));
59+
}
60+
61+
#[test]
62+
fn array_of_numbers() {
63+
let mut parser = Statement::new().unwrap();
64+
let result = parser.parse_and_execute("[first(createArray(1, 2, 3))]", &Context::new()).unwrap();
65+
assert_eq!(result.to_string(), "1");
66+
}
67+
68+
#[test]
69+
fn array_of_single_element() {
70+
let mut parser = Statement::new().unwrap();
71+
let result = parser.parse_and_execute("[first(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("[first('hello')]", &Context::new()).unwrap();
79+
assert_eq!(result.as_str(), Some("h"));
80+
}
81+
82+
#[test]
83+
fn single_character_string() {
84+
let mut parser = Statement::new().unwrap();
85+
let result = parser.parse_and_execute("[first('a')]", &Context::new()).unwrap();
86+
assert_eq!(result.as_str(), Some("a"));
87+
}
88+
89+
#[test]
90+
fn empty_array() {
91+
let mut parser = Statement::new().unwrap();
92+
let result = parser.parse_and_execute("[first(createArray())]", &Context::new());
93+
assert!(result.is_err());
94+
}
95+
96+
#[test]
97+
fn empty_string() {
98+
let mut parser = Statement::new().unwrap();
99+
let result = parser.parse_and_execute("[first('')]", &Context::new());
100+
assert!(result.is_err());
101+
}
102+
103+
#[test]
104+
fn invalid_type_object() {
105+
let mut parser = Statement::new().unwrap();
106+
let result = parser.parse_and_execute("[first(createObject('key', 'value'))]", &Context::new());
107+
assert!(result.is_err());
108+
}
109+
110+
#[test]
111+
fn invalid_type_number() {
112+
let mut parser = Statement::new().unwrap();
113+
let result = parser.parse_and_execute("[first(42)]", &Context::new());
114+
assert!(result.is_err());
115+
}
116+
}

dsc_lib/src/functions/index_of.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 IndexOf {}
13+
14+
impl Function for IndexOf {
15+
fn get_metadata(&self) -> FunctionMetadata {
16+
FunctionMetadata {
17+
name: "indexOf".to_string(),
18+
description: t!("functions.indexOf.description").to_string(),
19+
category: FunctionCategory::Array,
20+
min_args: 2,
21+
max_args: 2,
22+
accepted_arg_ordered_types: vec![
23+
vec![FunctionArgKind::Array],
24+
vec![FunctionArgKind::String, FunctionArgKind::Number, FunctionArgKind::Array, FunctionArgKind::Object],
25+
],
26+
remaining_arg_accepted_types: None,
27+
return_types: vec![FunctionArgKind::Number],
28+
}
29+
}
30+
31+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
32+
debug!("{}", t!("functions.indexOf.invoked"));
33+
34+
let Some(array) = args[0].as_array() else {
35+
return Err(DscError::Parser(t!("functions.indexOf.invalidArrayArg").to_string()));
36+
};
37+
38+
let item_to_find = &args[1];
39+
40+
for (index, item) in array.iter().enumerate() {
41+
if item == item_to_find {
42+
let index_i64 = i64::try_from(index).map_err(|_| {
43+
DscError::Parser("Array index too large to represent as integer".to_string())
44+
})?;
45+
return Ok(Value::Number(index_i64.into()));
46+
}
47+
}
48+
49+
// Not found is -1
50+
Ok(Value::Number((-1i64).into()))
51+
}
52+
}
53+
54+
#[cfg(test)]
55+
mod tests {
56+
use crate::configure::context::Context;
57+
use crate::parser::Statement;
58+
59+
#[test]
60+
fn find_string_in_array() {
61+
let mut parser = Statement::new().unwrap();
62+
let result = parser.parse_and_execute("[indexOf(createArray('apple', 'banana', 'cherry'), 'banana')]", &Context::new()).unwrap();
63+
assert_eq!(result, 1);
64+
}
65+
66+
#[test]
67+
fn find_number_in_array() {
68+
let mut parser = Statement::new().unwrap();
69+
let result = parser.parse_and_execute("[indexOf(createArray(10, 20, 30), 20)]", &Context::new()).unwrap();
70+
assert_eq!(result, 1);
71+
}
72+
73+
#[test]
74+
fn find_first_occurrence() {
75+
let mut parser = Statement::new().unwrap();
76+
let result = parser.parse_and_execute("[indexOf(createArray('a', 'b', 'a', 'c'), 'a')]", &Context::new()).unwrap();
77+
assert_eq!(result, 0);
78+
}
79+
80+
#[test]
81+
fn item_not_found() {
82+
let mut parser = Statement::new().unwrap();
83+
let result = parser.parse_and_execute("[indexOf(createArray('apple', 'banana'), 'orange')]", &Context::new()).unwrap();
84+
assert_eq!(result, -1);
85+
}
86+
87+
#[test]
88+
fn case_sensitive_string() {
89+
let mut parser = Statement::new().unwrap();
90+
let result = parser.parse_and_execute("[indexOf(createArray('Apple', 'Banana'), 'apple')]", &Context::new()).unwrap();
91+
assert_eq!(result, -1);
92+
}
93+
94+
#[test]
95+
fn find_array_in_array() {
96+
let mut parser = Statement::new().unwrap();
97+
let result = parser.parse_and_execute("[indexOf(createArray(createArray('a', 'b'), createArray('c', 'd')), createArray('c', 'd'))]", &Context::new()).unwrap();
98+
assert_eq!(result, 1);
99+
}
100+
101+
#[test]
102+
fn find_object_in_array() {
103+
let mut parser = Statement::new().unwrap();
104+
let result = parser.parse_and_execute("[indexOf(createArray(createObject('name', 'John'), createObject('name', 'Jane')), createObject('name', 'Jane'))]", &Context::new()).unwrap();
105+
assert_eq!(result, 1);
106+
}
107+
108+
#[test]
109+
fn empty_array() {
110+
let mut parser = Statement::new().unwrap();
111+
let result = parser.parse_and_execute("[indexOf(createArray(), 'test')]", &Context::new()).unwrap();
112+
assert_eq!(result, -1);
113+
}
114+
115+
#[test]
116+
fn invalid_array_arg() {
117+
let mut parser = Statement::new().unwrap();
118+
let result = parser.parse_and_execute("[indexOf('not_an_array', 'test')]", &Context::new());
119+
assert!(result.is_err());
120+
}
121+
}

0 commit comments

Comments
 (0)