diff --git a/dsc/tests/dsc_expressions.tests.ps1 b/dsc/tests/dsc_expressions.tests.ps1 index f3f54d293..6a673aa25 100644 --- a/dsc/tests/dsc_expressions.tests.ps1 +++ b/dsc/tests/dsc_expressions.tests.ps1 @@ -112,6 +112,32 @@ resources: @{ expression = "[equals('a', 'a')]"; expected = $true } @{ expression = "[equals('a', 'b')]"; expected = $false } @{ expression = "[not(equals('a', 'b'))]"; expected = $true } + @{ expression = "[greater(5, 3)]"; expected = $true } + @{ expression = "[greater(3, 5)]"; expected = $false } + @{ expression = "[greater(5, 5)]"; expected = $false } + @{ expression = "[greaterOrEquals(5, 3)]"; expected = $true } + @{ expression = "[greaterOrEquals(3, 5)]"; expected = $false } + @{ expression = "[greaterOrEquals(5, 5)]"; expected = $true } + @{ expression = "[less(3, 5)]"; expected = $true } + @{ expression = "[less(5, 3)]"; expected = $false } + @{ expression = "[less(5, 5)]"; expected = $false } + @{ expression = "[lessOrEquals(3, 5)]"; expected = $true } + @{ expression = "[lessOrEquals(5, 3)]"; expected = $false } + @{ expression = "[lessOrEquals(5, 5)]"; expected = $true } + @{ expression = "[greater('b', 'a')]"; expected = $true } + @{ expression = "[greater('a', 'b')]"; expected = $false } + @{ expression = "[greater('A', 'a')]"; expected = $false } + @{ expression = "[greaterOrEquals('b', 'a')]"; expected = $true } + @{ expression = "[greaterOrEquals('a', 'b')]"; expected = $false } + @{ expression = "[greaterOrEquals('a', 'a')]"; expected = $true } + @{ expression = "[greaterOrEquals('Aa', 'aa')]"; expected = $false } + @{ expression = "[less('a', 'b')]"; expected = $true } + @{ expression = "[less('b', 'a')]"; expected = $false } + @{ expression = "[less('A', 'a')]"; expected = $true } + @{ expression = "[lessOrEquals('a', 'b')]"; expected = $true } + @{ expression = "[lessOrEquals('b', 'a')]"; expected = $false } + @{ expression = "[lessOrEquals('a', 'a')]"; expected = $true } + @{ expression = "[lessOrEquals('aa', 'Aa')]"; expected = $false } @{ expression = "[and(true, true)]"; expected = $true } @{ expression = "[and(true, false)]"; expected = $false } @{ expression = "[or(false, true)]"; expected = $true } @@ -138,4 +164,26 @@ resources: $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String) $out.results[0].result.actualState.output | Should -Be $expected -Because ($out | ConvertTo-Json -Depth 10| Out-String) } + + It 'Comparison functions handle type mismatches: ' -TestCases @( + @{ expression = "[greater('a', 1)]" } + @{ expression = "[greaterOrEquals('5', 3)]" } + @{ expression = "[less(1, 'b')]" } + @{ expression = "[lessOrEquals(5, 'a')]" } + ) { + param($expression) + $yaml = @" +`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc config get -i $yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 2 + $log = Get-Content -Path $TestDrive/error.log -Raw + $log | Should -BeLike "*ERROR* Arguments must be of the same type*" + + } } diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index 895c02e26..743b99a5c 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -191,6 +191,7 @@ extensionManifestSchemaDescription = "Defines the JSON Schema the extension mani [functions] invalidArgType = "Invalid argument type" invalidArguments = "Invalid argument(s)" +typeMismatch = "Arguments must be of the same type (both numbers or both strings)" unknownFunction = "Unknown function '%{name}'" noArgsAccepted = "Function '%{name}' does not accept arguments" invalidArgCount = "Function '%{name}' requires exactly %{count} arguments" @@ -248,6 +249,14 @@ description = "Evaluates if the two values are the same" description = "Returns the boolean value false" invoked = "false function" +[functions.greater] +description = "Evaluates if the first value is greater than the second value" +invoked = "greater function" + +[functions.greaterOrEquals] +description = "Evaluates if the first value is greater than or equal to the second value" +invoked = "greaterOrEquals function" + [functions.format] description = "Formats a string using the given arguments" experimental = "`format()` function is experimental" @@ -267,6 +276,14 @@ parseStringError = "unable to parse string to int" castError = "unable to cast to int" parseNumError = "unable to parse number to int" +[functions.less] +description = "Evaluates if the first value is less than the second value" +invoked = "less function" + +[functions.lessOrEquals] +description = "Evaluates if the first value is less than or equal to the second value" +invoked = "lessOrEquals function" + [functions.max] description = "Returns the largest number from a list of numbers" emptyArray = "Array cannot be empty" diff --git a/dsc_lib/src/functions/greater.rs b/dsc_lib/src/functions/greater.rs new file mode 100644 index 000000000..e367c326e --- /dev/null +++ b/dsc_lib/src/functions/greater.rs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{AcceptedArgKind, Function, FunctionCategory}; +use rust_i18n::t; +use serde_json::Value; +use tracing::debug; + +#[derive(Debug, Default)] +pub struct Greater {} + +impl Function for Greater { + fn description(&self) -> String { + t!("functions.greater.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Comparison + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::Number, AcceptedArgKind::String] + } + + fn min_args(&self) -> usize { + 2 + } + + fn max_args(&self) -> usize { + 2 + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.greater.invoked")); + + let first = &args[0]; + let second = &args[1]; + + if let (Some(num1), Some(num2)) = (first.as_i64(), second.as_i64()) { + return Ok(Value::Bool(num1 > num2)); + } + + if let (Some(str1), Some(str2)) = (first.as_str(), second.as_str()) { + return Ok(Value::Bool(str1 > str2)); + } + + Err(DscError::Parser(t!("functions.typeMismatch").to_string())) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn number_greater() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[greater(2,1)]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + #[test] + fn number_not_greater() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[greater(1,2)]", &Context::new()).unwrap(); + assert_eq!(result, false); + } + + #[test] + fn number_equal() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[greater(1,1)]", &Context::new()).unwrap(); + assert_eq!(result, false); + } + + #[test] + fn string_greater() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[greater('b','a')]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + #[test] + fn type_mismatch_string_number() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[greater('5', 3)]", &Context::new()); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Arguments must be of the same type")); + } +} diff --git a/dsc_lib/src/functions/greater_or_equals.rs b/dsc_lib/src/functions/greater_or_equals.rs new file mode 100644 index 000000000..82fec7547 --- /dev/null +++ b/dsc_lib/src/functions/greater_or_equals.rs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{AcceptedArgKind, Function, FunctionCategory}; +use rust_i18n::t; +use serde_json::Value; +use tracing::debug; + +#[derive(Debug, Default)] +pub struct GreaterOrEquals {} + +impl Function for GreaterOrEquals { + fn description(&self) -> String { + t!("functions.greaterOrEquals.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Comparison + } + + fn min_args(&self) -> usize { + 2 + } + + fn max_args(&self) -> usize { + 2 + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::Number, AcceptedArgKind::String] + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.greaterOrEquals.invoked")); + + let first = &args[0]; + let second = &args[1]; + + if let (Some(num1), Some(num2)) = (first.as_i64(), second.as_i64()) { + return Ok(Value::Bool(num1 >= num2)); + } + + if let (Some(str1), Some(str2)) = (first.as_str(), second.as_str()) { + return Ok(Value::Bool(str1 >= str2)); + } + + Err(DscError::Parser(t!("functions.typeMismatch").to_string())) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn number_greater_or_equals() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[greaterOrEquals(5,3)]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + #[test] + fn number_not_greater_or_equals() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[greaterOrEquals(3,5)]", &Context::new()).unwrap(); + assert_eq!(result, false); + } + + #[test] + fn number_equal() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[greaterOrEquals(5,5)]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + #[test] + fn string_greater_or_equals() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[greaterOrEquals('b','a')]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + #[test] + fn type_mismatch_string_number() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[greaterOrEquals('5', 3)]", &Context::new()); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Arguments must be of the same type")); + } +} diff --git a/dsc_lib/src/functions/less.rs b/dsc_lib/src/functions/less.rs new file mode 100644 index 000000000..0c6db133a --- /dev/null +++ b/dsc_lib/src/functions/less.rs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{AcceptedArgKind, Function, FunctionCategory}; +use rust_i18n::t; +use serde_json::Value; +use tracing::debug; + +#[derive(Debug, Default)] +pub struct Less {} + +impl Function for Less { + fn description(&self) -> String { + t!("functions.less.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Comparison + } + + fn min_args(&self) -> usize { + 2 + } + + fn max_args(&self) -> usize { + 2 + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::Number, AcceptedArgKind::String] + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.less.invoked")); + + let first = &args[0]; + let second = &args[1]; + + if let (Some(num1), Some(num2)) = (first.as_i64(), second.as_i64()) { + return Ok(Value::Bool(num1 < num2)); + } + + if let (Some(str1), Some(str2)) = (first.as_str(), second.as_str()) { + return Ok(Value::Bool(str1 < str2)); + } + + Err(DscError::Parser(t!("functions.typeMismatch").to_string())) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn number_less() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[less(3,5)]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + #[test] + fn number_not_less() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[less(5,3)]", &Context::new()).unwrap(); + assert_eq!(result, false); + } + + #[test] + fn number_equal() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[less(5,5)]", &Context::new()).unwrap(); + assert_eq!(result, false); + } + + #[test] + fn string_less() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[less('a','b')]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + fn type_mismatch_string_number() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[lessOrEquals('5', 3)]", &Context::new()); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Arguments must be of the same type")); + } +} diff --git a/dsc_lib/src/functions/less_or_equals.rs b/dsc_lib/src/functions/less_or_equals.rs new file mode 100644 index 000000000..9927159bc --- /dev/null +++ b/dsc_lib/src/functions/less_or_equals.rs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{AcceptedArgKind, Function, FunctionCategory}; +use rust_i18n::t; +use serde_json::Value; +use tracing::debug; + +#[derive(Debug, Default)] +pub struct LessOrEquals {} + +impl Function for LessOrEquals { + fn description(&self) -> String { + t!("functions.lessOrEquals.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Comparison + } + + fn min_args(&self) -> usize { + 2 + } + + fn max_args(&self) -> usize { + 2 + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::Number, AcceptedArgKind::String] + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.lessOrEquals.invoked")); + + let first = &args[0]; + let second = &args[1]; + + if let (Some(num1), Some(num2)) = (first.as_i64(), second.as_i64()) { + return Ok(Value::Bool(num1 <= num2)); + } + + if let (Some(str1), Some(str2)) = (first.as_str(), second.as_str()) { + return Ok(Value::Bool(str1 <= str2)); + } + + Err(DscError::Parser(t!("functions.typeMismatch").to_string())) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn number_less_or_equals() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[lessOrEquals(3,5)]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + #[test] + fn number_not_less_or_equals() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[lessOrEquals(5,3)]", &Context::new()).unwrap(); + assert_eq!(result, false); + } + + #[test] + fn number_equal() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[lessOrEquals(5,5)]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + #[test] + fn string_less_or_equals() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[lessOrEquals('a','b')]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + fn type_mismatch_string_number() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[lessOrEquals('5', 3)]", &Context::new()); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Arguments must be of the same type")); + } +} diff --git a/dsc_lib/src/functions/mod.rs b/dsc_lib/src/functions/mod.rs index f2f75d292..dbb140867 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -20,8 +20,12 @@ pub mod create_array; pub mod div; pub mod envvar; pub mod equals; +pub mod greater; +pub mod greater_or_equals; pub mod r#if; pub mod r#false; +pub mod less; +pub mod less_or_equals; pub mod format; pub mod int; pub mod max; @@ -92,9 +96,13 @@ impl FunctionDispatcher { functions.insert("envvar".to_string(), Box::new(envvar::Envvar{})); functions.insert("equals".to_string(), Box::new(equals::Equals{})); functions.insert("false".to_string(), Box::new(r#false::False{})); + functions.insert("greater".to_string(), Box::new(greater::Greater{})); + functions.insert("greaterOrEquals".to_string(), Box::new(greater_or_equals::GreaterOrEquals{})); functions.insert("if".to_string(), Box::new(r#if::If{})); functions.insert("format".to_string(), Box::new(format::Format{})); functions.insert("int".to_string(), Box::new(int::Int{})); + functions.insert("less".to_string(), Box::new(less::Less{})); + functions.insert("lessOrEquals".to_string(), Box::new(less_or_equals::LessOrEquals{})); functions.insert("max".to_string(), Box::new(max::Max{})); functions.insert("min".to_string(), Box::new(min::Min{})); functions.insert("mod".to_string(), Box::new(mod_function::Mod{}));