Skip to content

Commit b54a451

Browse files
committed
Add array(), first(), indexOf() functions
1 parent 79633e1 commit b54a451

File tree

9 files changed

+647
-152
lines changed

9 files changed

+647
-152
lines changed

dsc/tests/dsc_functions.tests.ps1

Lines changed: 214 additions & 145 deletions
Large diffs are not rendered by default.

dsc_lib/locales/en-us.toml

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

220+
[functions.array]
221+
description = "Creates an array from the given elements of mixed types"
222+
invoked = "array function"
223+
invalidArgType = "Invalid argument type, only int, string, array, or object are accepted"
224+
220225
[functions.base64]
221226
description = "Encodes a string to Base64 format"
222227

@@ -280,6 +285,13 @@ description = "Evaluates if the two values are the same"
280285
description = "Returns the boolean value false"
281286
invoked = "false function"
282287

288+
[functions.first]
289+
description = "Returns the first element of an array or first character of a string"
290+
invoked = "first function"
291+
emptyArray = "Cannot get first element of empty array"
292+
emptyString = "Cannot get first character of empty string"
293+
invalidArgType = "Invalid argument type, argument must be an array or string"
294+
283295
[functions.greater]
284296
description = "Evaluates if the first value is greater than the second value"
285297
invoked = "greater function"
@@ -307,6 +319,11 @@ parseStringError = "unable to parse string to int"
307319
castError = "unable to cast to int"
308320
parseNumError = "unable to parse number to int"
309321

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

dsc_lib/src/functions/array.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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: 0,
21+
max_args: usize::MAX,
22+
accepted_arg_ordered_types: vec![],
23+
remaining_arg_accepted_types: Some(vec![
24+
FunctionArgKind::String,
25+
FunctionArgKind::Number,
26+
FunctionArgKind::Object,
27+
FunctionArgKind::Array,
28+
]),
29+
return_types: vec![FunctionArgKind::Array],
30+
}
31+
}
32+
33+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
34+
debug!("{}", t!("functions.array.invoked"));
35+
let mut array_result = Vec::<Value>::new();
36+
37+
for value in args {
38+
// Only accept int, string, array, or object as specified
39+
if value.is_number() || value.is_string() || value.is_array() || value.is_object() {
40+
array_result.push(value.clone());
41+
} else {
42+
return Err(DscError::Parser(t!("functions.array.invalidArgType").to_string()));
43+
}
44+
}
45+
46+
Ok(Value::Array(array_result))
47+
}
48+
}
49+
50+
#[cfg(test)]
51+
mod tests {
52+
use crate::configure::context::Context;
53+
use crate::parser::Statement;
54+
55+
#[test]
56+
fn mixed_types() {
57+
let mut parser = Statement::new().unwrap();
58+
let result = parser.parse_and_execute("[array('hello', 42)]", &Context::new()).unwrap();
59+
assert_eq!(result.to_string(), r#"["hello",42]"#);
60+
}
61+
62+
#[test]
63+
fn strings_only() {
64+
let mut parser = Statement::new().unwrap();
65+
let result = parser.parse_and_execute("[array('a', 'b', 'c')]", &Context::new()).unwrap();
66+
assert_eq!(result.to_string(), r#"["a","b","c"]"#);
67+
}
68+
69+
#[test]
70+
fn numbers_only() {
71+
let mut parser = Statement::new().unwrap();
72+
let result = parser.parse_and_execute("[array(1, 2, 3)]", &Context::new()).unwrap();
73+
assert_eq!(result.to_string(), "[1,2,3]");
74+
}
75+
76+
#[test]
77+
fn arrays_and_objects() {
78+
let mut parser = Statement::new().unwrap();
79+
let result = parser.parse_and_execute("[array(createArray('a','b'), createObject('key', 'value'))]", &Context::new()).unwrap();
80+
assert_eq!(result.to_string(), r#"[["a","b"],{"key":"value"}]"#);
81+
}
82+
83+
#[test]
84+
fn empty_array() {
85+
let mut parser = Statement::new().unwrap();
86+
let result = parser.parse_and_execute("[array()]", &Context::new()).unwrap();
87+
assert_eq!(result.to_string(), "[]");
88+
}
89+
90+
#[test]
91+
fn invalid_type_boolean() {
92+
let mut parser = Statement::new().unwrap();
93+
let result = parser.parse_and_execute("[array(true)]", &Context::new());
94+
assert!(result.is_err());
95+
}
96+
97+
#[test]
98+
fn invalid_type_null() {
99+
let mut parser = Statement::new().unwrap();
100+
let result = parser.parse_and_execute("[array(null())]", &Context::new());
101+
assert!(result.is_err());
102+
}
103+
}

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.to_string(), "\"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_mixed() {
70+
let mut parser = Statement::new().unwrap();
71+
let result = parser.parse_and_execute("[first(array('hello', 42))]", &Context::new()).unwrap();
72+
assert_eq!(result.to_string(), "\"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.to_string(), "\"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.to_string(), "\"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: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 array = args[0].as_array().ok_or_else(|| {
35+
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+
let matches = match (item_to_find, item) {
42+
// String comparison (case-sensitive)
43+
(Value::String(find_str), Value::String(item_str)) => find_str == item_str,
44+
(Value::Number(find_num), Value::Number(item_num)) => find_num == item_num,
45+
(Value::Array(find_arr), Value::Array(item_arr)) => find_arr == item_arr,
46+
(Value::Object(find_obj), Value::Object(item_obj)) => find_obj == item_obj,
47+
_ => false,
48+
};
49+
50+
if matches {
51+
return Ok(Value::Number((index as i64).into()));
52+
}
53+
}
54+
55+
// Not found is -1
56+
Ok(Value::Number((-1i64).into()))
57+
}
58+
}
59+
60+
#[cfg(test)]
61+
mod tests {
62+
use crate::configure::context::Context;
63+
use crate::parser::Statement;
64+
65+
#[test]
66+
fn find_string_in_array() {
67+
let mut parser = Statement::new().unwrap();
68+
let result = parser.parse_and_execute("[indexOf(createArray('apple', 'banana', 'cherry'), 'banana')]", &Context::new()).unwrap();
69+
assert_eq!(result.to_string(), "1");
70+
}
71+
72+
#[test]
73+
fn find_number_in_array() {
74+
let mut parser = Statement::new().unwrap();
75+
let result = parser.parse_and_execute("[indexOf(createArray(10, 20, 30), 20)]", &Context::new()).unwrap();
76+
assert_eq!(result.to_string(), "1");
77+
}
78+
79+
#[test]
80+
fn find_first_occurrence() {
81+
let mut parser = Statement::new().unwrap();
82+
let result = parser.parse_and_execute("[indexOf(createArray('a', 'b', 'a', 'c'), 'a')]", &Context::new()).unwrap();
83+
assert_eq!(result.to_string(), "0");
84+
}
85+
86+
#[test]
87+
fn item_not_found() {
88+
let mut parser = Statement::new().unwrap();
89+
let result = parser.parse_and_execute("[indexOf(createArray('apple', 'banana'), 'orange')]", &Context::new()).unwrap();
90+
assert_eq!(result.to_string(), "-1");
91+
}
92+
93+
#[test]
94+
fn case_sensitive_string() {
95+
let mut parser = Statement::new().unwrap();
96+
let result = parser.parse_and_execute("[indexOf(createArray('Apple', 'Banana'), 'apple')]", &Context::new()).unwrap();
97+
assert_eq!(result.to_string(), "-1");
98+
}
99+
100+
#[test]
101+
fn find_array_in_array() {
102+
let mut parser = Statement::new().unwrap();
103+
let result = parser.parse_and_execute("[indexOf(array(createArray('a', 'b'), createArray('c', 'd')), createArray('c', 'd'))]", &Context::new()).unwrap();
104+
assert_eq!(result.to_string(), "1");
105+
}
106+
107+
#[test]
108+
fn find_object_in_array() {
109+
let mut parser = Statement::new().unwrap();
110+
let result = parser.parse_and_execute("[indexOf(array(createObject('name', 'John'), createObject('name', 'Jane')), createObject('name', 'Jane'))]", &Context::new()).unwrap();
111+
assert_eq!(result.to_string(), "1");
112+
}
113+
114+
#[test]
115+
fn empty_array() {
116+
let mut parser = Statement::new().unwrap();
117+
let result = parser.parse_and_execute("[indexOf(createArray(), 'test')]", &Context::new()).unwrap();
118+
assert_eq!(result.to_string(), "-1");
119+
}
120+
121+
#[test]
122+
fn invalid_array_arg() {
123+
let mut parser = Statement::new().unwrap();
124+
let result = parser.parse_and_execute("[indexOf('not_an_array', 'test')]", &Context::new());
125+
assert!(result.is_err());
126+
}
127+
}

0 commit comments

Comments
 (0)