Skip to content

Commit 7edd993

Browse files
committed
Add objectKeys function
1 parent 055fd3d commit 7edd993

File tree

4 files changed

+183
-0
lines changed

4 files changed

+183
-0
lines changed

dsc/tests/dsc_functions.tests.ps1

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,46 @@ Describe 'tests for function expressions' {
958958
$out.results[0].result.actualState.output | Should -Be $expected
959959
}
960960

961+
It 'objectKeys function returns array of keys: <expression>' -TestCases @(
962+
@{ expression = "[length(objectKeys(createObject('a', 1, 'b', 2, 'c', 3)))]"; expected = 3 }
963+
@{ expression = "[length(objectKeys(createObject()))]"; expected = 0 }
964+
@{ expression = "[objectKeys(createObject('name', 'John'))[0]]"; expected = 'name' }
965+
@{ expression = "[length(objectKeys(createObject('x', 1, 'y', 2)))]"; expected = 2 }
966+
) {
967+
param($expression, $expected)
968+
969+
$escapedExpression = $expression -replace "'", "''"
970+
$config_yaml = @"
971+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
972+
resources:
973+
- name: Echo
974+
type: Microsoft.DSC.Debug/Echo
975+
properties:
976+
output: '$escapedExpression'
977+
"@
978+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
979+
$out.results[0].result.actualState.output | Should -Be $expected
980+
}
981+
982+
It 'objectKeys function works with nested objects: <expression>' -TestCases @(
983+
@{ expression = "[objectKeys(createObject('person', createObject('name', 'John')))[0]]"; expected = 'person' }
984+
@{ expression = "[length(objectKeys(createObject('a', createArray(1,2,3), 'b', 'text')))]"; expected = 2 }
985+
) {
986+
param($expression, $expected)
987+
988+
$escapedExpression = $expression -replace "'", "''"
989+
$config_yaml = @"
990+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
991+
resources:
992+
- name: Echo
993+
type: Microsoft.DSC.Debug/Echo
994+
properties:
995+
output: '$escapedExpression'
996+
"@
997+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
998+
$out.results[0].result.actualState.output | Should -Be $expected
999+
}
1000+
9611001
It 'tryGet() function works for: <expression>' -TestCases @(
9621002
@{ expression = "[tryGet(createObject('a', 1, 'b', 2), 'a')]"; expected = 1 }
9631003
@{ expression = "[tryGet(createObject('a', 1, 'b', 2), 'c')]"; expected = $null }

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,10 @@ invoked = "not function"
448448
description = "Returns a null value"
449449
invoked = "null function"
450450

451+
[functions.objectKeys]
452+
description = "Returns the keys from an object, where an object is a collection of key-value pairs"
453+
notObject = "Argument must be an object"
454+
451455
[functions.or]
452456
description = "Evaluates if any arguments are true"
453457
invoked = "or function"

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub mod mod_function;
5555
pub mod mul;
5656
pub mod not;
5757
pub mod null;
58+
pub mod object_keys;
5859
pub mod or;
5960
pub mod parameters;
6061
pub mod parse_cidr;
@@ -189,6 +190,7 @@ impl FunctionDispatcher {
189190
Box::new(mul::Mul{}),
190191
Box::new(not::Not{}),
191192
Box::new(null::Null{}),
193+
Box::new(object_keys::ObjectKeys{}),
192194
Box::new(or::Or{}),
193195
Box::new(parameters::Parameters{}),
194196
Box::new(parse_cidr::ParseCidr{}),
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
10+
#[derive(Debug, Default)]
11+
pub struct ObjectKeys {}
12+
13+
impl Function for ObjectKeys {
14+
fn get_metadata(&self) -> FunctionMetadata {
15+
FunctionMetadata {
16+
name: "objectKeys".to_string(),
17+
description: t!("functions.objectKeys.description").to_string(),
18+
category: vec![FunctionCategory::Object],
19+
min_args: 1,
20+
max_args: 1,
21+
accepted_arg_ordered_types: vec![
22+
vec![FunctionArgKind::Object],
23+
],
24+
remaining_arg_accepted_types: None,
25+
return_types: vec![FunctionArgKind::Array],
26+
}
27+
}
28+
29+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
30+
let Some(obj) = args[0].as_object() else {
31+
return Err(DscError::Parser(t!("functions.objectKeys.notObject").to_string()));
32+
};
33+
34+
// Extract all keys from the object and return as an array of strings
35+
let keys: Vec<Value> = obj
36+
.keys()
37+
.map(|key| Value::String(key.clone()))
38+
.collect();
39+
40+
Ok(Value::Array(keys))
41+
}
42+
}
43+
44+
#[cfg(test)]
45+
mod tests {
46+
use crate::configure::context::Context;
47+
use crate::parser::Statement;
48+
use serde_json::{json, Value};
49+
50+
#[test]
51+
fn object_keys_basic() {
52+
let mut parser = Statement::new().unwrap();
53+
let result = parser.parse_and_execute("[objectKeys(createObject('a', 1, 'b', 2, 'c', 3))]", &Context::new()).unwrap();
54+
55+
let arr = result.as_array().unwrap();
56+
assert_eq!(arr.len(), 3);
57+
58+
for key in arr {
59+
assert!(key.is_string());
60+
}
61+
62+
let keys: Vec<&str> = arr.iter().filter_map(Value::as_str).collect();
63+
assert!(keys.contains(&"a"));
64+
assert!(keys.contains(&"b"));
65+
assert!(keys.contains(&"c"));
66+
}
67+
68+
#[test]
69+
fn object_keys_empty_object() {
70+
let mut parser = Statement::new().unwrap();
71+
let result = parser.parse_and_execute("[objectKeys(createObject())]", &Context::new()).unwrap();
72+
assert_eq!(result, json!([]));
73+
}
74+
75+
#[test]
76+
fn object_keys_single_key() {
77+
let mut parser = Statement::new().unwrap();
78+
let result = parser.parse_and_execute("[objectKeys(createObject('name', 'John'))]", &Context::new()).unwrap();
79+
80+
let arr = result.as_array().unwrap();
81+
assert_eq!(arr.len(), 1);
82+
assert_eq!(arr[0].as_str(), Some("name"));
83+
}
84+
85+
#[test]
86+
fn object_keys_nested_values() {
87+
let mut parser = Statement::new().unwrap();
88+
let result = parser.parse_and_execute("[objectKeys(createObject('person', createObject('name', 'John', 'age', 30)))]", &Context::new()).unwrap();
89+
90+
let arr = result.as_array().unwrap();
91+
assert_eq!(arr.len(), 1);
92+
assert_eq!(arr[0].as_str(), Some("person"));
93+
}
94+
95+
#[test]
96+
fn object_keys_mixed_value_types() {
97+
let mut parser = Statement::new().unwrap();
98+
let result = parser.parse_and_execute("[objectKeys(createObject('str', 'text', 'num', 42, 'bool', true(), 'arr', createArray(1,2,3)))]", &Context::new()).unwrap();
99+
100+
let arr = result.as_array().unwrap();
101+
assert_eq!(arr.len(), 4);
102+
103+
let keys: Vec<&str> = arr.iter().filter_map(Value::as_str).collect();
104+
assert!(keys.contains(&"str"));
105+
assert!(keys.contains(&"num"));
106+
assert!(keys.contains(&"bool"));
107+
assert!(keys.contains(&"arr"));
108+
}
109+
110+
#[test]
111+
fn object_keys_can_be_used_with_length() {
112+
let mut parser = Statement::new().unwrap();
113+
let result = parser.parse_and_execute("[length(objectKeys(createObject('a', 1, 'b', 2, 'c', 3)))]", &Context::new()).unwrap();
114+
assert_eq!(result, json!(3));
115+
}
116+
117+
#[test]
118+
fn object_keys_not_object_error() {
119+
let mut parser = Statement::new().unwrap();
120+
let result = parser.parse_and_execute("[objectKeys('not an object')]", &Context::new());
121+
assert!(result.is_err());
122+
}
123+
124+
#[test]
125+
fn object_keys_array_error() {
126+
let mut parser = Statement::new().unwrap();
127+
let result = parser.parse_and_execute("[objectKeys(createArray('a', 'b', 'c'))]", &Context::new());
128+
assert!(result.is_err());
129+
}
130+
131+
#[test]
132+
fn object_keys_number_error() {
133+
let mut parser = Statement::new().unwrap();
134+
let result = parser.parse_and_execute("[objectKeys(42)]", &Context::new());
135+
assert!(result.is_err());
136+
}
137+
}

0 commit comments

Comments
 (0)