Skip to content

Commit 0112a57

Browse files
authored
Merge pull request #990 from Gijsreyn/comparison-functions
Add new comparison operators
2 parents aa02f29 + 3d3b0fd commit 0112a57

File tree

7 files changed

+443
-0
lines changed

7 files changed

+443
-0
lines changed

dsc/tests/dsc_expressions.tests.ps1

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,32 @@ resources:
112112
@{ expression = "[equals('a', 'a')]"; expected = $true }
113113
@{ expression = "[equals('a', 'b')]"; expected = $false }
114114
@{ expression = "[not(equals('a', 'b'))]"; expected = $true }
115+
@{ expression = "[greater(5, 3)]"; expected = $true }
116+
@{ expression = "[greater(3, 5)]"; expected = $false }
117+
@{ expression = "[greater(5, 5)]"; expected = $false }
118+
@{ expression = "[greaterOrEquals(5, 3)]"; expected = $true }
119+
@{ expression = "[greaterOrEquals(3, 5)]"; expected = $false }
120+
@{ expression = "[greaterOrEquals(5, 5)]"; expected = $true }
121+
@{ expression = "[less(3, 5)]"; expected = $true }
122+
@{ expression = "[less(5, 3)]"; expected = $false }
123+
@{ expression = "[less(5, 5)]"; expected = $false }
124+
@{ expression = "[lessOrEquals(3, 5)]"; expected = $true }
125+
@{ expression = "[lessOrEquals(5, 3)]"; expected = $false }
126+
@{ expression = "[lessOrEquals(5, 5)]"; expected = $true }
127+
@{ expression = "[greater('b', 'a')]"; expected = $true }
128+
@{ expression = "[greater('a', 'b')]"; expected = $false }
129+
@{ expression = "[greater('A', 'a')]"; expected = $false }
130+
@{ expression = "[greaterOrEquals('b', 'a')]"; expected = $true }
131+
@{ expression = "[greaterOrEquals('a', 'b')]"; expected = $false }
132+
@{ expression = "[greaterOrEquals('a', 'a')]"; expected = $true }
133+
@{ expression = "[greaterOrEquals('Aa', 'aa')]"; expected = $false }
134+
@{ expression = "[less('a', 'b')]"; expected = $true }
135+
@{ expression = "[less('b', 'a')]"; expected = $false }
136+
@{ expression = "[less('A', 'a')]"; expected = $true }
137+
@{ expression = "[lessOrEquals('a', 'b')]"; expected = $true }
138+
@{ expression = "[lessOrEquals('b', 'a')]"; expected = $false }
139+
@{ expression = "[lessOrEquals('a', 'a')]"; expected = $true }
140+
@{ expression = "[lessOrEquals('aa', 'Aa')]"; expected = $false }
115141
@{ expression = "[and(true, true)]"; expected = $true }
116142
@{ expression = "[and(true, false)]"; expected = $false }
117143
@{ expression = "[or(false, true)]"; expected = $true }
@@ -138,4 +164,26 @@ resources:
138164
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String)
139165
$out.results[0].result.actualState.output | Should -Be $expected -Because ($out | ConvertTo-Json -Depth 10| Out-String)
140166
}
167+
168+
It 'Comparison functions handle type mismatches: <expression>' -TestCases @(
169+
@{ expression = "[greater('a', 1)]" }
170+
@{ expression = "[greaterOrEquals('5', 3)]" }
171+
@{ expression = "[less(1, 'b')]" }
172+
@{ expression = "[lessOrEquals(5, 'a')]" }
173+
) {
174+
param($expression)
175+
$yaml = @"
176+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
177+
resources:
178+
- name: echo
179+
type: Microsoft.DSC.Debug/Echo
180+
properties:
181+
output: "$expression"
182+
"@
183+
$out = dsc config get -i $yaml 2>$TestDrive/error.log
184+
$LASTEXITCODE | Should -Be 2
185+
$log = Get-Content -Path $TestDrive/error.log -Raw
186+
$log | Should -BeLike "*ERROR* Arguments must be of the same type*"
187+
188+
}
141189
}

dsc_lib/locales/en-us.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ extensionManifestSchemaDescription = "Defines the JSON Schema the extension mani
191191
[functions]
192192
invalidArgType = "Invalid argument type"
193193
invalidArguments = "Invalid argument(s)"
194+
typeMismatch = "Arguments must be of the same type (both numbers or both strings)"
194195
unknownFunction = "Unknown function '%{name}'"
195196
noArgsAccepted = "Function '%{name}' does not accept arguments"
196197
invalidArgCount = "Function '%{name}' requires exactly %{count} arguments"
@@ -248,6 +249,14 @@ description = "Evaluates if the two values are the same"
248249
description = "Returns the boolean value false"
249250
invoked = "false function"
250251

252+
[functions.greater]
253+
description = "Evaluates if the first value is greater than the second value"
254+
invoked = "greater function"
255+
256+
[functions.greaterOrEquals]
257+
description = "Evaluates if the first value is greater than or equal to the second value"
258+
invoked = "greaterOrEquals function"
259+
251260
[functions.format]
252261
description = "Formats a string using the given arguments"
253262
experimental = "`format()` function is experimental"
@@ -267,6 +276,14 @@ parseStringError = "unable to parse string to int"
267276
castError = "unable to cast to int"
268277
parseNumError = "unable to parse number to int"
269278

279+
[functions.less]
280+
description = "Evaluates if the first value is less than the second value"
281+
invoked = "less function"
282+
283+
[functions.lessOrEquals]
284+
description = "Evaluates if the first value is less than or equal to the second value"
285+
invoked = "lessOrEquals function"
286+
270287
[functions.max]
271288
description = "Returns the largest number from a list of numbers"
272289
emptyArray = "Array cannot be empty"

dsc_lib/src/functions/greater.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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 Greater {}
13+
14+
impl Function for Greater {
15+
fn description(&self) -> String {
16+
t!("functions.greater.description").to_string()
17+
}
18+
19+
fn category(&self) -> FunctionCategory {
20+
FunctionCategory::Comparison
21+
}
22+
23+
fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
24+
vec![AcceptedArgKind::Number, AcceptedArgKind::String]
25+
}
26+
27+
fn min_args(&self) -> usize {
28+
2
29+
}
30+
31+
fn max_args(&self) -> usize {
32+
2
33+
}
34+
35+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
36+
debug!("{}", t!("functions.greater.invoked"));
37+
38+
let first = &args[0];
39+
let second = &args[1];
40+
41+
if let (Some(num1), Some(num2)) = (first.as_i64(), second.as_i64()) {
42+
return Ok(Value::Bool(num1 > num2));
43+
}
44+
45+
if let (Some(str1), Some(str2)) = (first.as_str(), second.as_str()) {
46+
return Ok(Value::Bool(str1 > str2));
47+
}
48+
49+
Err(DscError::Parser(t!("functions.typeMismatch").to_string()))
50+
}
51+
}
52+
53+
#[cfg(test)]
54+
mod tests {
55+
use crate::configure::context::Context;
56+
use crate::parser::Statement;
57+
58+
#[test]
59+
fn number_greater() {
60+
let mut parser = Statement::new().unwrap();
61+
let result = parser.parse_and_execute("[greater(2,1)]", &Context::new()).unwrap();
62+
assert_eq!(result, true);
63+
}
64+
65+
#[test]
66+
fn number_not_greater() {
67+
let mut parser = Statement::new().unwrap();
68+
let result = parser.parse_and_execute("[greater(1,2)]", &Context::new()).unwrap();
69+
assert_eq!(result, false);
70+
}
71+
72+
#[test]
73+
fn number_equal() {
74+
let mut parser = Statement::new().unwrap();
75+
let result = parser.parse_and_execute("[greater(1,1)]", &Context::new()).unwrap();
76+
assert_eq!(result, false);
77+
}
78+
79+
#[test]
80+
fn string_greater() {
81+
let mut parser = Statement::new().unwrap();
82+
let result = parser.parse_and_execute("[greater('b','a')]", &Context::new()).unwrap();
83+
assert_eq!(result, true);
84+
}
85+
86+
#[test]
87+
fn type_mismatch_string_number() {
88+
let mut parser = Statement::new().unwrap();
89+
let result = parser.parse_and_execute("[greater('5', 3)]", &Context::new());
90+
assert!(result.is_err());
91+
assert!(result.unwrap_err().to_string().contains("Arguments must be of the same type"));
92+
}
93+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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 GreaterOrEquals {}
13+
14+
impl Function for GreaterOrEquals {
15+
fn description(&self) -> String {
16+
t!("functions.greaterOrEquals.description").to_string()
17+
}
18+
19+
fn category(&self) -> FunctionCategory {
20+
FunctionCategory::Comparison
21+
}
22+
23+
fn min_args(&self) -> usize {
24+
2
25+
}
26+
27+
fn max_args(&self) -> usize {
28+
2
29+
}
30+
31+
fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
32+
vec![AcceptedArgKind::Number, AcceptedArgKind::String]
33+
}
34+
35+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
36+
debug!("{}", t!("functions.greaterOrEquals.invoked"));
37+
38+
let first = &args[0];
39+
let second = &args[1];
40+
41+
if let (Some(num1), Some(num2)) = (first.as_i64(), second.as_i64()) {
42+
return Ok(Value::Bool(num1 >= num2));
43+
}
44+
45+
if let (Some(str1), Some(str2)) = (first.as_str(), second.as_str()) {
46+
return Ok(Value::Bool(str1 >= str2));
47+
}
48+
49+
Err(DscError::Parser(t!("functions.typeMismatch").to_string()))
50+
}
51+
}
52+
53+
#[cfg(test)]
54+
mod tests {
55+
use crate::configure::context::Context;
56+
use crate::parser::Statement;
57+
58+
#[test]
59+
fn number_greater_or_equals() {
60+
let mut parser = Statement::new().unwrap();
61+
let result = parser.parse_and_execute("[greaterOrEquals(5,3)]", &Context::new()).unwrap();
62+
assert_eq!(result, true);
63+
}
64+
65+
#[test]
66+
fn number_not_greater_or_equals() {
67+
let mut parser = Statement::new().unwrap();
68+
let result = parser.parse_and_execute("[greaterOrEquals(3,5)]", &Context::new()).unwrap();
69+
assert_eq!(result, false);
70+
}
71+
72+
#[test]
73+
fn number_equal() {
74+
let mut parser = Statement::new().unwrap();
75+
let result = parser.parse_and_execute("[greaterOrEquals(5,5)]", &Context::new()).unwrap();
76+
assert_eq!(result, true);
77+
}
78+
79+
#[test]
80+
fn string_greater_or_equals() {
81+
let mut parser = Statement::new().unwrap();
82+
let result = parser.parse_and_execute("[greaterOrEquals('b','a')]", &Context::new()).unwrap();
83+
assert_eq!(result, true);
84+
}
85+
86+
#[test]
87+
fn type_mismatch_string_number() {
88+
let mut parser = Statement::new().unwrap();
89+
let result = parser.parse_and_execute("[greaterOrEquals('5', 3)]", &Context::new());
90+
assert!(result.is_err());
91+
assert!(result.unwrap_err().to_string().contains("Arguments must be of the same type"));
92+
}
93+
}

dsc_lib/src/functions/less.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 Less {}
13+
14+
impl Function for Less {
15+
fn description(&self) -> String {
16+
t!("functions.less.description").to_string()
17+
}
18+
19+
fn category(&self) -> FunctionCategory {
20+
FunctionCategory::Comparison
21+
}
22+
23+
fn min_args(&self) -> usize {
24+
2
25+
}
26+
27+
fn max_args(&self) -> usize {
28+
2
29+
}
30+
31+
fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
32+
vec![AcceptedArgKind::Number, AcceptedArgKind::String]
33+
}
34+
35+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
36+
debug!("{}", t!("functions.less.invoked"));
37+
38+
let first = &args[0];
39+
let second = &args[1];
40+
41+
if let (Some(num1), Some(num2)) = (first.as_i64(), second.as_i64()) {
42+
return Ok(Value::Bool(num1 < num2));
43+
}
44+
45+
if let (Some(str1), Some(str2)) = (first.as_str(), second.as_str()) {
46+
return Ok(Value::Bool(str1 < str2));
47+
}
48+
49+
Err(DscError::Parser(t!("functions.typeMismatch").to_string()))
50+
}
51+
}
52+
53+
#[cfg(test)]
54+
mod tests {
55+
use crate::configure::context::Context;
56+
use crate::parser::Statement;
57+
58+
#[test]
59+
fn number_less() {
60+
let mut parser = Statement::new().unwrap();
61+
let result = parser.parse_and_execute("[less(3,5)]", &Context::new()).unwrap();
62+
assert_eq!(result, true);
63+
}
64+
65+
#[test]
66+
fn number_not_less() {
67+
let mut parser = Statement::new().unwrap();
68+
let result = parser.parse_and_execute("[less(5,3)]", &Context::new()).unwrap();
69+
assert_eq!(result, false);
70+
}
71+
72+
#[test]
73+
fn number_equal() {
74+
let mut parser = Statement::new().unwrap();
75+
let result = parser.parse_and_execute("[less(5,5)]", &Context::new()).unwrap();
76+
assert_eq!(result, false);
77+
}
78+
79+
#[test]
80+
fn string_less() {
81+
let mut parser = Statement::new().unwrap();
82+
let result = parser.parse_and_execute("[less('a','b')]", &Context::new()).unwrap();
83+
assert_eq!(result, true);
84+
}
85+
86+
fn type_mismatch_string_number() {
87+
let mut parser = Statement::new().unwrap();
88+
let result = parser.parse_and_execute("[lessOrEquals('5', 3)]", &Context::new());
89+
assert!(result.is_err());
90+
assert!(result.unwrap_err().to_string().contains("Arguments must be of the same type"));
91+
}
92+
}

0 commit comments

Comments
 (0)