diff --git a/dsc/tests/dsc_expressions.tests.ps1 b/dsc/tests/dsc_expressions.tests.ps1 index 7b33366d8..f3f54d293 100644 --- a/dsc/tests/dsc_expressions.tests.ps1 +++ b/dsc/tests/dsc_expressions.tests.ps1 @@ -107,4 +107,35 @@ resources: $LASTEXITCODE | Should -Be 0 $out.results[0].result[1].result.actualState.output.family | Should -BeExactly $out.results[0].result[0].result.actualState.family } + + It 'Logical functions work: ' -TestCases @( + @{ expression = "[equals('a', 'a')]"; expected = $true } + @{ expression = "[equals('a', 'b')]"; expected = $false } + @{ expression = "[not(equals('a', 'b'))]"; expected = $true } + @{ expression = "[and(true, true)]"; expected = $true } + @{ expression = "[and(true, false)]"; expected = $false } + @{ expression = "[or(false, true)]"; expected = $true } + @{ expression = "[or(false, false)]"; expected = $false } + @{ expression = "[not(true)]"; expected = $false } + @{ expression = "[not(or(true, false))]"; expected = $false } + @{ expression = "[bool('TRUE')]" ; expected = $true } + @{ expression = "[bool('False')]" ; expected = $false } + @{ expression = "[bool(1)]" ; expected = $true } + @{ expression = "[not(bool(0))]" ; expected = $true } + @{ expression = "[true()]" ; expected = $true } + @{ expression = "[false()]" ; expected = $false } + ) { + param($expression, $expected) + $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 | ConvertFrom-Json + $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) + } } diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index 449fb6a4c..ad38f1ee5 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -204,9 +204,17 @@ noStringArgs = "Function '%{name}' does not accept string arguments, accepted ty description = "Adds two or more numbers together" invoked = "add function" +[functions.and] +description = "Evaluates if all arguments are true" +invoked = "and function" + [functions.base64] description = "Encodes a string to Base64 format" +[functions.bool] +description = "Converts a string or number to a boolean" +invoked = "bool function" + [functions.concat] description = "Concatenates two or more strings or arrays" invoked = "concat function" @@ -234,6 +242,10 @@ notFound = "Environment variable not found" [functions.equals] description = "Evaluates if the two values are the same" +[functions.false] +description = "Returns the boolean value false" +invoked = "false function" + [functions.format] description = "Formats a string using the given arguments" experimental = "`format()` function is experimental" @@ -274,6 +286,14 @@ divideByZero = "Cannot divide by zero" description = "Multiplies two or more numbers together" invoked = "mul function" +[functions.not] +description = "Negates a boolean value" +invoked = "not function" + +[functions.or] +description = "Evaluates if any arguments are true" +invoked = "or function" + [functions.parameters] description = "Retrieves parameters from the configuration" invoked = "parameters function" @@ -314,6 +334,10 @@ invoked = "sub function" description = "Returns the system root path" invoked = "systemRoot function" +[functions.true] +description = "Returns the boolean value true" +invoked = "true function" + [functions.variables] description = "Retrieves the value of a variable" invoked = "variables function" diff --git a/dsc_lib/src/functions/and.rs b/dsc_lib/src/functions/and.rs new file mode 100644 index 000000000..4541b10d9 --- /dev/null +++ b/dsc_lib/src/functions/and.rs @@ -0,0 +1,82 @@ +// 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 And {} + +impl Function for And { + fn description(&self) -> String { + t!("functions.and.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Logical + } + + fn min_args(&self) -> usize { + 2 + } + + fn max_args(&self) -> usize { + usize::MAX + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::Boolean] + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.and.invoked")); + for arg in args { + if let Some(value) = arg.as_bool() { + if !value { + return Ok(Value::Bool(false)); + } + } else { + return Err(DscError::Parser(t!("functions.invalidArguments").to_string())); + } + } + Ok(Value::Bool(true)) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn two_values() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[and(true, false)]", &Context::new()).unwrap(); + assert_eq!(result, false); + } + + #[test] + fn multiple_values() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[and(true, false, true)]", &Context::new()).unwrap(); + assert_eq!(result, false); + } + + #[test] + fn all_false() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[and(false, false)]", &Context::new()).unwrap(); + assert_eq!(result, false); + } + + #[test] + fn all_true() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[and(true, true)]", &Context::new()).unwrap(); + assert_eq!(result, true); + } +} diff --git a/dsc_lib/src/functions/bool.rs b/dsc_lib/src/functions/bool.rs new file mode 100644 index 000000000..e85f3cb47 --- /dev/null +++ b/dsc_lib/src/functions/bool.rs @@ -0,0 +1,83 @@ +// 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 Bool {} + +impl Function for Bool { + fn description(&self) -> String { + t!("functions.bool.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Logical + } + + fn min_args(&self) -> usize { + 1 + } + + fn max_args(&self) -> usize { + 1 + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::String, AcceptedArgKind::Number] + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.bool.invoked")); + if let Some(arg) = args[0].as_str() { + match arg.to_lowercase().as_str() { + "true" => Ok(Value::Bool(true)), + "false" => Ok(Value::Bool(false)), + _ => Err(DscError::Parser(t!("functions.invalidArguments").to_string())), + } + } else if let Some(num) = args[0].as_i64() { + Ok(Value::Bool(num != 0)) + } else { + Err(DscError::Parser(t!("functions.invalidArguments").to_string())) + } + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn true_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[bool('true')]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + #[test] + fn false_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[bool('false')]", &Context::new()).unwrap(); + assert_eq!(result, false); + } + + #[test] + fn number_1() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[bool(1)]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + #[test] + fn number_0() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[bool(0)]", &Context::new()).unwrap(); + assert_eq!(result, false); + } +} diff --git a/dsc_lib/src/functions/false.rs b/dsc_lib/src/functions/false.rs new file mode 100644 index 000000000..dd5e3f204 --- /dev/null +++ b/dsc_lib/src/functions/false.rs @@ -0,0 +1,52 @@ +// 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 False {} + +impl Function for False { + fn description(&self) -> String { + t!("functions.false.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Logical + } + + fn min_args(&self) -> usize { + 0 + } + + fn max_args(&self) -> usize { + 0 + } + + fn accepted_arg_types(&self) -> Vec { + vec![] + } + + fn invoke(&self, _args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.false.invoked")); + Ok(Value::Bool(false)) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn false_function() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[false()]", &Context::new()).unwrap(); + assert_eq!(result, false); + } +} diff --git a/dsc_lib/src/functions/mod.rs b/dsc_lib/src/functions/mod.rs index 31ce88f6d..f2f75d292 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -12,19 +12,24 @@ use serde_json::Value; use std::fmt::Display; pub mod add; +pub mod and; pub mod base64; +pub mod bool; pub mod concat; pub mod create_array; pub mod div; pub mod envvar; pub mod equals; pub mod r#if; +pub mod r#false; pub mod format; pub mod int; pub mod max; pub mod min; pub mod mod_function; pub mod mul; +pub mod not; +pub mod or; pub mod parameters; pub mod path; pub mod reference; @@ -32,6 +37,7 @@ pub mod resource_id; pub mod secret; pub mod sub; pub mod system_root; +pub mod r#true; pub mod variables; /// The kind of argument that a function accepts. @@ -77,12 +83,15 @@ impl FunctionDispatcher { pub fn new() -> Self { let mut functions: HashMap> = HashMap::new(); functions.insert("add".to_string(), Box::new(add::Add{})); + functions.insert("and".to_string(), Box::new(and::And{})); functions.insert("base64".to_string(), Box::new(base64::Base64{})); + functions.insert("bool".to_string(), Box::new(bool::Bool{})); functions.insert("concat".to_string(), Box::new(concat::Concat{})); functions.insert("createArray".to_string(), Box::new(create_array::CreateArray{})); functions.insert("div".to_string(), Box::new(div::Div{})); 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("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{})); @@ -90,6 +99,8 @@ impl FunctionDispatcher { functions.insert("min".to_string(), Box::new(min::Min{})); functions.insert("mod".to_string(), Box::new(mod_function::Mod{})); functions.insert("mul".to_string(), Box::new(mul::Mul{})); + functions.insert("not".to_string(), Box::new(not::Not{})); + functions.insert("or".to_string(), Box::new(or::Or{})); functions.insert("parameters".to_string(), Box::new(parameters::Parameters{})); functions.insert("path".to_string(), Box::new(path::Path{})); functions.insert("reference".to_string(), Box::new(reference::Reference{})); @@ -97,6 +108,7 @@ impl FunctionDispatcher { functions.insert("secret".to_string(), Box::new(secret::Secret{})); functions.insert("sub".to_string(), Box::new(sub::Sub{})); functions.insert("systemRoot".to_string(), Box::new(system_root::SystemRoot{})); + functions.insert("true".to_string(), Box::new(r#true::True{})); functions.insert("variables".to_string(), Box::new(variables::Variables{})); Self { functions, diff --git a/dsc_lib/src/functions/not.rs b/dsc_lib/src/functions/not.rs new file mode 100644 index 000000000..f89094364 --- /dev/null +++ b/dsc_lib/src/functions/not.rs @@ -0,0 +1,63 @@ +// 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 Not {} + +impl Function for Not { + fn description(&self) -> String { + t!("functions.not.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Logical + } + + fn min_args(&self) -> usize { + 1 + } + + fn max_args(&self) -> usize { + 1 + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::Boolean] + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.not.invoked")); + if let Some(arg1) = args[0].as_bool() { + Ok(Value::Bool(!arg1)) + } else { + Err(DscError::Parser(t!("functions.invalidArguments").to_string())) + } + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn not_true() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[not(true)]", &Context::new()).unwrap(); + assert_eq!(result, false); + } + + #[test] + fn not_false() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[not(false)]", &Context::new()).unwrap(); + assert_eq!(result, true); + } +} diff --git a/dsc_lib/src/functions/or.rs b/dsc_lib/src/functions/or.rs new file mode 100644 index 000000000..17091a5a8 --- /dev/null +++ b/dsc_lib/src/functions/or.rs @@ -0,0 +1,75 @@ +// 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 Or {} + +impl Function for Or { + fn description(&self) -> String { + t!("functions.or.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Logical + } + + fn min_args(&self) -> usize { + 2 + } + + fn max_args(&self) -> usize { + usize::MAX + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::Boolean] + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.or.invoked")); + for arg in args { + if let Some(value) = arg.as_bool() { + if value { + return Ok(Value::Bool(true)); + } + } else { + return Err(DscError::Parser(t!("functions.invalidArguments").to_string())); + } + } + Ok(Value::Bool(false)) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn two_values() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[or(true, false)]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + #[test] + fn multiple_values() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[or(true, false, true)]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + #[test] + fn all_false() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[or(false, false)]", &Context::new()).unwrap(); + assert_eq!(result, false); + } +} diff --git a/dsc_lib/src/functions/true.rs b/dsc_lib/src/functions/true.rs new file mode 100644 index 000000000..b3dfb6547 --- /dev/null +++ b/dsc_lib/src/functions/true.rs @@ -0,0 +1,52 @@ +// 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 True {} + +impl Function for True { + fn description(&self) -> String { + t!("functions.true.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Logical + } + + fn min_args(&self) -> usize { + 0 + } + + fn max_args(&self) -> usize { + 0 + } + + fn accepted_arg_types(&self) -> Vec { + vec![] + } + + fn invoke(&self, _args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.true.invoked")); + Ok(Value::Bool(true)) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn true_function() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[true()]", &Context::new()).unwrap(); + assert_eq!(result, true); + } +}