Skip to content

Commit e4df4dd

Browse files
committed
Add create_object and null()
1 parent 68ac724 commit e4df4dd

File tree

6 files changed

+326
-16
lines changed

6 files changed

+326
-16
lines changed

dsc/tests/dsc_expressions.tests.ps1

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,7 @@ resources:
108108
$out.results[0].result[1].result.actualState.output.family | Should -BeExactly $out.results[0].result[0].result.actualState.family
109109
}
110110

111-
It 'Logical functions work: <expression>' -TestCases @(
112-
@{ expression = "[equals('a', 'a')]"; expected = $true }
113-
@{ expression = "[equals('a', 'b')]"; expected = $false }
114-
@{ expression = "[not(equals('a', 'b'))]"; expected = $true }
111+
It 'Comparison functions work: <expression>' -TestCases @(
115112
@{ expression = "[greater(5, 3)]"; expected = $true }
116113
@{ expression = "[greater(3, 5)]"; expected = $false }
117114
@{ expression = "[greater(5, 5)]"; expected = $false }
@@ -138,6 +135,53 @@ resources:
138135
@{ expression = "[lessOrEquals('b', 'a')]"; expected = $false }
139136
@{ expression = "[lessOrEquals('a', 'a')]"; expected = $true }
140137
@{ expression = "[lessOrEquals('aa', 'Aa')]"; expected = $false }
138+
@{ expression = "[coalesce('hello', 'world')]" ; expected = 'hello' }
139+
@{ expression = "[coalesce(42, 'fallback')]" ; expected = 42 }
140+
@{ expression = "[coalesce(true, false)]" ; expected = $true }
141+
@{ expression = "[coalesce('first', 'second')]" ; expected = 'first' }
142+
@{ expression = "[coalesce(createArray('a', 'b'), createArray('c', 'd'))]" ; expected = @('a', 'b') }
143+
) {
144+
param($expression, $expected)
145+
$yaml = @"
146+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
147+
resources:
148+
- name: echo
149+
type: Microsoft.DSC.Debug/Echo
150+
properties:
151+
output: "$expression"
152+
"@
153+
$out = dsc config get -i $yaml 2>$TestDrive/error.log | ConvertFrom-Json
154+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String)
155+
$out.results[0].result.actualState.output | Should -Be $expected -Because ($out | ConvertTo-Json -Depth 10| Out-String)
156+
}
157+
158+
It 'Object functions work: <expression>' -TestCases @(
159+
@{ expression = "[createObject('name', 'test')]" ; expected = @{name='test'} }
160+
@{ expression = "[createObject('key1', 'value1', 'key2', 42)]" ; expected = @{key1='value1'; key2=42} }
161+
@{ expression = "[createObject()]" ; expected = @{} }
162+
@{ expression = "[null()]" ; expected = $null }
163+
@{ expression = "[createObject('key', null())]" ; expected = @{key=$null} }
164+
) {
165+
param($expression, $expected)
166+
$yaml = @"
167+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
168+
resources:
169+
- name: echo
170+
type: Microsoft.DSC.Debug/Echo
171+
properties:
172+
output: "$expression"
173+
"@
174+
$out = dsc config get -i $yaml 2>$TestDrive/error.log | ConvertFrom-Json
175+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String)
176+
foreach ($key in $out.results[0].result.actualState.output.psobject.properties.Name) {
177+
$out.results[0].result.actualState.output.$key | Should -Be $expected.$key -Because ($out | ConvertTo-Json -Depth 10| Out-String)
178+
}
179+
}
180+
181+
It 'Logical functions work: <expression>' -TestCases @(
182+
@{ expression = "[equals('a', 'a')]"; expected = $true }
183+
@{ expression = "[equals('a', 'b')]"; expected = $false }
184+
@{ expression = "[not(equals('a', 'b'))]"; expected = $true }
141185
@{ expression = "[and(true, true)]"; expected = $true }
142186
@{ expression = "[and(true, false)]"; expected = $false }
143187
@{ expression = "[or(false, true)]"; expected = $true }
@@ -148,10 +192,6 @@ resources:
148192
@{ expression = "[bool('False')]" ; expected = $false }
149193
@{ expression = "[bool(1)]" ; expected = $true }
150194
@{ expression = "[not(bool(0))]" ; expected = $true }
151-
@{ expression = "[coalesce('hello', 'world')]" ; expected = 'hello' }
152-
@{ expression = "[coalesce(42, 'fallback')]" ; expected = 42 }
153-
@{ expression = "[coalesce(true, false)]" ; expected = $true }
154-
@{ expression = "[coalesce('first', 'second')]" ; expected = 'first' }
155195
@{ expression = "[true()]" ; expected = $true }
156196
@{ expression = "[false()]" ; expected = $false }
157197
) {

dsc_lib/locales/en-us.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ argsMustAllBeIntegers = "Arguments must all be integers"
241241
argsMustAllBeObjects = "Arguments must all be objects"
242242
argsMustAllBeStrings = "Arguments must all be strings"
243243

244+
[functions.createObject]
245+
description = "Creates an object from the given key-value pairs"
246+
invoked = "createObject function"
247+
argsMustBePairs = "Arguments must be provided in key-value pairs"
248+
keyMustBeString = "Object keys must be strings"
249+
244250
[functions.div]
245251
description = "Divides the first number by the second"
246252
invoked = "div function"
@@ -317,6 +323,10 @@ invoked = "mul function"
317323
description = "Negates a boolean value"
318324
invoked = "not function"
319325

326+
[functions.null]
327+
description = "Returns a null value"
328+
invoked = "null function"
329+
320330
[functions.or]
321331
description = "Evaluates if any arguments are true"
322332
invoked = "or function"

dsc_lib/src/functions/coalesce.rs

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ impl Function for Coalesce {
1717
}
1818

1919
fn category(&self) -> FunctionCategory {
20-
FunctionCategory::Logical
20+
FunctionCategory::Comparison
2121
}
2222

2323
fn min_args(&self) -> usize {
@@ -56,13 +56,6 @@ mod tests {
5656
use crate::configure::context::Context;
5757
use crate::parser::Statement;
5858
use super::*;
59-
// TODO: Add tests for direct function calls with nulls and mixed types if the parser accept it
60-
// #[test]
61-
// fn all_null_returns_null() {
62-
// let mut parser = Statement::new().unwrap();
63-
// let result = parser.parse_and_execute("[coalesce(null, null, null)]", &Context::new()).unwrap();
64-
// assert_eq!(result, serde_json::Value::Null);
65-
// }
6659

6760
#[test]
6861
fn direct_function_call_with_nulls() {
@@ -96,6 +89,56 @@ mod tests {
9689
assert_eq!(result, Value::Bool(true));
9790
}
9891

92+
#[test]
93+
fn direct_function_call_with_arrays() {
94+
let coalesce = Coalesce {};
95+
let context = Context::new();
96+
97+
let first_array = serde_json::json!(["a", "b", "c"]);
98+
let second_array = serde_json::json!(["x", "y", "z"]);
99+
100+
let args = vec![Value::Null, first_array.clone(), second_array];
101+
let result = coalesce.invoke(&args, &context).unwrap();
102+
assert_eq!(result, first_array);
103+
104+
let args = vec![Value::Null, Value::Null, serde_json::json!([1, 2, 3])];
105+
let result = coalesce.invoke(&args, &context).unwrap();
106+
assert_eq!(result, serde_json::json!([1, 2, 3]));
107+
}
108+
109+
#[test]
110+
fn direct_function_call_with_objects() {
111+
let coalesce = Coalesce {};
112+
let context = Context::new();
113+
114+
let first_obj = serde_json::json!({"name": "test", "value": 42});
115+
let second_obj = serde_json::json!({"name": "fallback", "value": 0});
116+
117+
let args = vec![Value::Null, first_obj.clone(), second_obj];
118+
let result = coalesce.invoke(&args, &context).unwrap();
119+
assert_eq!(result, first_obj);
120+
121+
let args = vec![Value::Null, Value::Null, serde_json::json!({"key": "value"})];
122+
let result = coalesce.invoke(&args, &context).unwrap();
123+
assert_eq!(result, serde_json::json!({"key": "value"}));
124+
}
125+
126+
#[test]
127+
fn direct_function_call_with_empty_collections() {
128+
let coalesce = Coalesce {};
129+
let context = Context::new();
130+
131+
let empty_array = serde_json::json!([]);
132+
let args = vec![Value::Null, empty_array.clone(), Value::String("fallback".to_string())];
133+
let result = coalesce.invoke(&args, &context).unwrap();
134+
assert_eq!(result, empty_array);
135+
136+
let empty_obj = serde_json::json!({});
137+
let args = vec![Value::Null, empty_obj.clone(), Value::String("fallback".to_string())];
138+
let result = coalesce.invoke(&args, &context).unwrap();
139+
assert_eq!(result, empty_obj);
140+
}
141+
99142
#[test]
100143
fn parser_with_values() {
101144
let mut parser = Statement::new().unwrap();
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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 CreateObject {}
13+
14+
impl Function for CreateObject {
15+
fn description(&self) -> String {
16+
t!("functions.createObject.description").to_string()
17+
}
18+
19+
fn category(&self) -> FunctionCategory {
20+
FunctionCategory::Object
21+
}
22+
23+
fn min_args(&self) -> usize {
24+
0
25+
}
26+
27+
fn max_args(&self) -> usize {
28+
usize::MAX
29+
}
30+
31+
fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
32+
vec![
33+
AcceptedArgKind::Array,
34+
AcceptedArgKind::Boolean,
35+
AcceptedArgKind::Number,
36+
AcceptedArgKind::Object,
37+
AcceptedArgKind::String,
38+
]
39+
}
40+
41+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
42+
debug!("{}", t!("functions.createObject.invoked"));
43+
44+
if args.len() % 2 != 0 {
45+
return Err(DscError::Parser(t!("functions.createObject.argsMustBePairs").to_string()));
46+
}
47+
48+
let mut object_result = Map::<String, Value>::new();
49+
50+
for chunk in args.chunks(2) {
51+
let key = &chunk[0];
52+
let value = &chunk[1];
53+
54+
if !key.is_string() {
55+
return Err(DscError::Parser(t!("functions.createObject.keyMustBeString").to_string()));
56+
}
57+
58+
let key_str = key.as_str().unwrap().to_string();
59+
object_result.insert(key_str, value.clone());
60+
}
61+
62+
Ok(Value::Object(object_result))
63+
}
64+
}
65+
66+
#[cfg(test)]
67+
mod tests {
68+
use crate::configure::context::Context;
69+
use crate::parser::Statement;
70+
71+
#[test]
72+
fn simple_object() {
73+
let mut parser = Statement::new().unwrap();
74+
let result = parser.parse_and_execute("[createObject('name', 'test')]", &Context::new()).unwrap();
75+
assert_eq!(result.to_string(), r#"{"name":"test"}"#);
76+
}
77+
78+
#[test]
79+
fn multiple_properties() {
80+
let mut parser = Statement::new().unwrap();
81+
let result = parser.parse_and_execute("[createObject('name', 'test', 'value', 42)]", &Context::new()).unwrap();
82+
assert_eq!(result.to_string(), r#"{"name":"test","value":42}"#);
83+
}
84+
85+
#[test]
86+
fn mixed_value_types() {
87+
let mut parser = Statement::new().unwrap();
88+
let result = parser.parse_and_execute("[createObject('string', 'hello', 'number', 123, 'boolean', true)]", &Context::new()).unwrap();
89+
90+
let json: serde_json::Value = serde_json::from_str(&result.to_string()).unwrap();
91+
assert_eq!(json["string"], "hello");
92+
assert_eq!(json["number"], 123);
93+
assert_eq!(json["boolean"], true);
94+
}
95+
96+
#[test]
97+
fn nested_objects() {
98+
let mut parser = Statement::new().unwrap();
99+
let result = parser.parse_and_execute("[createObject('outer', createObject('inner', 'value'))]", &Context::new()).unwrap();
100+
assert_eq!(result.to_string(), r#"{"outer":{"inner":"value"}}"#);
101+
}
102+
103+
#[test]
104+
fn with_arrays() {
105+
let mut parser = Statement::new().unwrap();
106+
let result = parser.parse_and_execute("[createObject('items', createArray('a', 'b', 'c'))]", &Context::new()).unwrap();
107+
assert_eq!(result.to_string(), r#"{"items":["a","b","c"]}"#);
108+
}
109+
110+
#[test]
111+
fn odd_number_of_args() {
112+
let mut parser = Statement::new().unwrap();
113+
let result = parser.parse_and_execute("[createObject('name')]", &Context::new());
114+
assert!(result.is_err());
115+
}
116+
117+
#[test]
118+
fn non_string_key() {
119+
let mut parser = Statement::new().unwrap();
120+
let result = parser.parse_and_execute("[createObject(123, 'value')]", &Context::new());
121+
assert!(result.is_err());
122+
}
123+
124+
#[test]
125+
fn empty() {
126+
let mut parser = Statement::new().unwrap();
127+
let result = parser.parse_and_execute("[createObject()]", &Context::new()).unwrap();
128+
assert_eq!(result.to_string(), "{}");
129+
}
130+
}

dsc_lib/src/functions/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod bool;
1818
pub mod coalesce;
1919
pub mod concat;
2020
pub mod create_array;
21+
pub mod create_object;
2122
pub mod div;
2223
pub mod envvar;
2324
pub mod equals;
@@ -34,6 +35,7 @@ pub mod min;
3435
pub mod mod_function;
3536
pub mod mul;
3637
pub mod not;
38+
pub mod null;
3739
pub mod or;
3840
pub mod parameters;
3941
pub mod path;
@@ -94,6 +96,7 @@ impl FunctionDispatcher {
9496
functions.insert("coalesce".to_string(), Box::new(coalesce::Coalesce{}));
9597
functions.insert("concat".to_string(), Box::new(concat::Concat{}));
9698
functions.insert("createArray".to_string(), Box::new(create_array::CreateArray{}));
99+
functions.insert("createObject".to_string(), Box::new(create_object::CreateObject{}));
97100
functions.insert("div".to_string(), Box::new(div::Div{}));
98101
functions.insert("envvar".to_string(), Box::new(envvar::Envvar{}));
99102
functions.insert("equals".to_string(), Box::new(equals::Equals{}));
@@ -110,6 +113,7 @@ impl FunctionDispatcher {
110113
functions.insert("mod".to_string(), Box::new(mod_function::Mod{}));
111114
functions.insert("mul".to_string(), Box::new(mul::Mul{}));
112115
functions.insert("not".to_string(), Box::new(not::Not{}));
116+
functions.insert("null".to_string(), Box::new(null::Null{}));
113117
functions.insert("or".to_string(), Box::new(or::Or{}));
114118
functions.insert("parameters".to_string(), Box::new(parameters::Parameters{}));
115119
functions.insert("path".to_string(), Box::new(path::Path{}));

0 commit comments

Comments
 (0)