From 2cd25d8c692d86cca7e95473123f89671834ed79 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 24 Jul 2025 14:41:27 -0700 Subject: [PATCH 1/8] Add `contains()` and `union()` Array functions --- dsc/tests/dsc_functions.tests.ps1 | 97 +++++++++++++++++++++++++ dsc_lib/locales/en-us.toml | 10 +++ dsc_lib/src/functions/contains.rs | 115 ++++++++++++++++++++++++++++++ dsc_lib/src/functions/mod.rs | 4 ++ dsc_lib/src/functions/union.rs | 71 ++++++++++++++++++ 5 files changed, 297 insertions(+) create mode 100644 dsc_lib/src/functions/contains.rs create mode 100644 dsc_lib/src/functions/union.rs diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 148d26f14..9230b682a 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -62,4 +62,101 @@ Describe 'tests for function expressions' { $LASTEXITCODE | Should -Be 0 $out.results[0].result.actualState.output | Should -BeExactly $expected } + + It 'union function works for: ' -TestCases @( + @{ expression = "[union(parameters('firstArray'), parameters('secondArray'))]"; expected = @('ab', 'cd', 'ef') } + @{ expression = "[union(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ one = 'a'; two = 'c'; three = 'd' } } + @{ expression = "[union(parameters('secondArray'), parameters('secondArray'))]"; expected = @('cd', 'ef') } + @{ expression = "[union(parameters('secondObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'c'; three = 'd' } } + @{ expression = "[union(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } + ) { + param($expression, $expected, $isError) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + firstObject: + type: object + defaultValue: + one: a + two: b + secondObject: + type: object + defaultValue: + two: c + three: d + firstArray: + type: array + defaultValue: + - ab + - cd + secondArray: + type: array + defaultValue: + - cd + - ef + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + if ($isError) { + $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) + (Get-Content $TestDrive/error.log -Raw) | Should -Match 'All arguments must either be arrays or objects' + } else { + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } + } + + It 'contain function works for: ' -TestCases @( + @{ expression = "[contains(parameters('array'), 'a')]" ; expected = $true } + @{ expression = "[contains(parameters('array'), 2)]" ; expected = $false } + @{ expression = "[contains(parameters('array'), 1)]" ; expected = $true } + @{ expression = "[contains(parameters('object'), 'a')]" ; expected = $true } + @{ expression = "[contains(parameters('object'), 'c')]" ; expected = $false } + @{ expression = "[contains(parameters('object'), 3)]" ; expected = $true } + @{ expression = "[contains(parameters('object'), parameters('object'))]" ; isError = $true } + @{ expression = "[contains(parameters('array'), parameters('array'))]" ; isError = $true } + @{ expression = "[contains(parameters('string'), 'not found')]" ; expected = $false } + @{ expression = "[contains(parameters('string'), 'hello')]" ; expected = $true } + @{ expression = "[contains(parameters('string'), 12)]" ; expected = $true } + ) { + param($expression, $expected, $isError) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + array: + type: array + defaultValue: + - a + - b + - 1 + object: + type: object + defaultValue: + a: 1 + b: 2 + 3: c + string: + type: string + defaultValue: 'hello 123 world!' + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + if ($isError) { + $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) + (Get-Content $TestDrive/error.log -Raw) | Should -Match 'Invalid item to find, must be a string or number' + } else { + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } + } } diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index 6a757c2ee..f822e7494 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -234,6 +234,11 @@ argsMustBeStrings = "Arguments must all be strings" argsMustBeArrays = "Arguments must all be arrays" onlyArraysOfStrings = "Arguments must all be arrays of strings" +[functions.contains] +description = "Checks if an array contains a specific item" +invoked = "contains function" +invalidItemToFind = "Invalid item to find, must be a string or number" + [functions.createArray] description = "Creates an array from the given elements" invoked = "createArray function" @@ -376,6 +381,11 @@ invoked = "systemRoot function" description = "Returns the boolean value true" invoked = "true function" +[functions.union] +description = "Returns a single array or object with all elements from the parameters" +invoked = "union function" +invalidArgType = "All arguments must either be arrays or objects" + [functions.variables] description = "Retrieves the value of a variable" invoked = "variables function" diff --git a/dsc_lib/src/functions/contains.rs b/dsc_lib/src/functions/contains.rs new file mode 100644 index 000000000..88c684689 --- /dev/null +++ b/dsc_lib/src/functions/contains.rs @@ -0,0 +1,115 @@ +// 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 Contains {} + +impl Function for Contains { + fn description(&self) -> String { + t!("functions.contains.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Array + } + + fn min_args(&self) -> usize { + 2 + } + + fn max_args(&self) -> usize { + usize::MAX + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::Array, AcceptedArgKind::Object, AcceptedArgKind::String, AcceptedArgKind::Number] + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.contains.invoked")); + let mut found = false; + + let (string_to_find, number_to_find) = if let Some(string) = args[1].as_str() { + (string.to_string(), 0) + } else if let Some(number) = args[1].as_i64() { + (number.to_string(), number) + } else { + return Err(DscError::Parser(t!("functions.contains.invalidItemToFind").to_string())); + }; + + // for array, we check if the string or number exists + if let Some(array) = args[0].as_array() { + for item in array { + if let Some(item_str) = item.as_str() { + if item_str == string_to_find { + found = true; + break; + } + } else if let Some(item_num) = item.as_i64() { + if item_num == number_to_find { + found = true; + break; + } + } + } + return Ok(Value::Bool(found)); + } + + // for object, we check if the key exists + if let Some(object) = args[0].as_object() { + // see if key exists + for key in object.keys() { + if key == &string_to_find { + found = true; + break; + } + } + return Ok(Value::Bool(found)); + } + + // for string, we check if the string contains the substring or number + if let Some(str) = args[0].as_str() { + if str.contains(&string_to_find) { + found = true; + } + return Ok(Value::Bool(found)); + } + + Err(DscError::Parser(t!("functions.contains.invalidArgType").to_string())) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn string_contains_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[contains('hello', 'lo')]", &Context::new()).unwrap(); + assert_eq!(result, true); + } + + #[test] + fn string_does_not_contain_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[contains('hello', 'world')]", &Context::new()).unwrap(); + assert_eq!(result, false); + } + + #[test] + fn string_contains_number() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[contains('hello123', 123)]", &Context::new()).unwrap(); + assert_eq!(result, true); + } +} + diff --git a/dsc_lib/src/functions/mod.rs b/dsc_lib/src/functions/mod.rs index 5770d3ad1..998979c69 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -17,6 +17,7 @@ pub mod base64; pub mod bool; pub mod coalesce; pub mod concat; +pub mod contains; pub mod create_array; pub mod create_object; pub mod div; @@ -45,6 +46,7 @@ pub mod secret; pub mod sub; pub mod system_root; pub mod r#true; +pub mod union; pub mod variables; /// The kind of argument that a function accepts. @@ -95,6 +97,7 @@ impl FunctionDispatcher { functions.insert("bool".to_string(), Box::new(bool::Bool{})); functions.insert("coalesce".to_string(), Box::new(coalesce::Coalesce{})); functions.insert("concat".to_string(), Box::new(concat::Concat{})); + functions.insert("contains".to_string(), Box::new(contains::Contains{})); functions.insert("createArray".to_string(), Box::new(create_array::CreateArray{})); functions.insert("createObject".to_string(), Box::new(create_object::CreateObject{})); functions.insert("div".to_string(), Box::new(div::Div{})); @@ -123,6 +126,7 @@ impl FunctionDispatcher { 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("union".to_string(), Box::new(union::Union{})); functions.insert("variables".to_string(), Box::new(variables::Variables{})); Self { functions, diff --git a/dsc_lib/src/functions/union.rs b/dsc_lib/src/functions/union.rs new file mode 100644 index 000000000..809a423e3 --- /dev/null +++ b/dsc_lib/src/functions/union.rs @@ -0,0 +1,71 @@ +// 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::{Map, Value}; +use tracing::debug; + +#[derive(Debug, Default)] +pub struct Union {} + +impl Function for Union { + fn description(&self) -> String { + t!("functions.union.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Array + } + + fn min_args(&self) -> usize { + 2 + } + + fn max_args(&self) -> usize { + usize::MAX + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::Array, AcceptedArgKind::Object] + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.union.invoked")); + if args[0].is_array() { + let mut result = Vec::new(); + // iterate through array and skip elements that are already in result + for arg in args { + if let Some(array) = arg.as_array() { + for item in array { + if !result.contains(item) { + result.push(item.clone()); + } + } + } else { + return Err(DscError::Parser(t!("functions.union.invalidArgType").to_string())); + } + } + return Ok(Value::Array(result)); + } + + if args[0].is_object() { + let mut result = Map::new(); + // iterate through objects, duplicate keys are overwritten + for arg in args { + if let Some(object) = arg.as_object() { + for (key, value) in object { + result.insert(key.clone(), value.clone()); + } + } else { + return Err(DscError::Parser(t!("functions.union.invalidArgType").to_string())); + } + } + return Ok(Value::Object(result)); + } + + Err(DscError::Parser(t!("functions.union.invalidArgType").to_string())) + } +} From 06b8a4df88143a65b89773e7e4486cb09abbaff4 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 24 Jul 2025 16:27:55 -0700 Subject: [PATCH 2/8] add missing string --- dsc_lib/locales/en-us.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index f822e7494..637696406 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -238,6 +238,7 @@ onlyArraysOfStrings = "Arguments must all be arrays of strings" description = "Checks if an array contains a specific item" invoked = "contains function" invalidItemToFind = "Invalid item to find, must be a string or number" +invalidArgType = "Invalid argument type, first argument must be an array, object, or string" [functions.createArray] description = "Creates an array from the given elements" From d64d584567c3c544e114f9cbc687c1f4c8674cc1 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 25 Jul 2025 10:25:45 -0700 Subject: [PATCH 3/8] add empty and length functions --- dsc/tests/dsc_functions.tests.ps1 | 83 +++++++++++++++++++++++++++++++ dsc_lib/locales/en-us.toml | 10 ++++ dsc_lib/src/functions/empty.rs | 75 ++++++++++++++++++++++++++++ dsc_lib/src/functions/length.rs | 75 ++++++++++++++++++++++++++++ dsc_lib/src/functions/mod.rs | 4 ++ 5 files changed, 247 insertions(+) create mode 100644 dsc_lib/src/functions/empty.rs create mode 100644 dsc_lib/src/functions/length.rs diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 9230b682a..a9b7641c9 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -159,4 +159,87 @@ Describe 'tests for function expressions' { ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) } } + + It 'length function works for: ' -TestCases @( + @{ expression = "[length(parameters('array'))]" ; expected = 3 } + @{ expression = "[length(parameters('object'))]" ; expected = 4 } + @{ expression = "[length(parameters('string'))]" ; expected = 12 } + @{ expression = "[length('')]"; expected = 0 } + ) { + param($expression, $expected, $isError) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + array: + type: array + defaultValue: + - a + - b + - c + object: + type: object + defaultValue: + one: a + two: b + three: c + four: d + string: + type: string + defaultValue: 'hello world!' + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } + + It 'empty function works for: ' -TestCases @( + @{ expression = "[empty(parameters('array'))]" ; expected = $false } + @{ expression = "[empty(parameters('object'))]" ; expected = $false } + @{ expression = "[empty(parameters('string'))]" ; expected = $false } + @{ expression = "[empty(parameters('emptyArray'))]" ; expected = $true } + @{ expression = "[empty(parameters('emptyObject'))]" ; expected = $true } + @{ expression = "[empty('')]" ; expected = $true } + ) { + param($expression, $expected) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + array: + type: array + defaultValue: + - a + - b + - c + emptyArray: + type: array + defaultValue: [] + object: + type: object + defaultValue: + one: a + two: b + three: c + emptyObject: + type: object + defaultValue: {} + string: + type: string + defaultValue: 'hello world!' + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } } diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index 637696406..d0a11e736 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -259,6 +259,11 @@ description = "Divides the first number by the second" invoked = "div function" divideByZero = "Cannot divide by zero" +[functions.empty] +description = "Checks if an array, object, or string is empty" +invoked = "empty function" +invalidArgType = "Invalid argument type, argument must be an array, object, or string" + [functions.envvar] description = "Retrieves the value of an environment variable" notFound = "Environment variable not found" @@ -297,6 +302,11 @@ parseStringError = "unable to parse string to int" castError = "unable to cast to int" parseNumError = "unable to parse number to int" +[functions.length] +description = "Returns the length of a string, array, or object" +invoked = "length function" +invalidArgType = "Invalid argument type, argument must be a string, array, or object" + [functions.less] description = "Evaluates if the first value is less than the second value" invoked = "less function" diff --git a/dsc_lib/src/functions/empty.rs b/dsc_lib/src/functions/empty.rs new file mode 100644 index 000000000..ee6306a9d --- /dev/null +++ b/dsc_lib/src/functions/empty.rs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use core::option::Option::Some; + +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 Empty {} + +impl Function for Empty { + fn description(&self) -> String { + t!("functions.empty.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Array + } + + fn min_args(&self) -> usize { + 1 + } + + fn max_args(&self) -> usize { + 1 + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::Array, AcceptedArgKind::Object, AcceptedArgKind::String] + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.empty.invoked")); + if let Some(array) = args[0].as_array() { + return Ok(Value::Bool(array.is_empty())); + } + + if let Some(object) = args[0].as_object() { + return Ok(Value::Bool(object.keys().len() == 0)); + } + + if let Some(string) = args[0].as_str() { + return Ok(Value::Bool(string.is_empty())); + } + + Err(DscError::Parser(t!("functions.empty.invalidArgType").to_string())) + } +} + + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + use serde_json::Value; + + #[test] + fn empty_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[empty('')]", &Context::new()).unwrap(); + assert_eq!(result, Value::Bool(true)); + } + + #[test] + fn not_empty_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[empty('foo')]", &Context::new()).unwrap(); + assert_eq!(result, Value::Bool(false)); + } +} diff --git a/dsc_lib/src/functions/length.rs b/dsc_lib/src/functions/length.rs new file mode 100644 index 000000000..980530045 --- /dev/null +++ b/dsc_lib/src/functions/length.rs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use core::option::Option::Some; + +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 Length {} + +impl Function for Length { + fn description(&self) -> String { + t!("functions.length.description").to_string() + } + + fn category(&self) -> FunctionCategory { + FunctionCategory::Array + } + + fn min_args(&self) -> usize { + 1 + } + + fn max_args(&self) -> usize { + 1 + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::Array, AcceptedArgKind::Object, AcceptedArgKind::String] + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.length.invoked")); + if let Some(array) = args[0].as_array() { + return Ok(Value::Number(array.len().into())); + } + + if let Some(object) = args[0].as_object() { + return Ok(Value::Number(object.keys().len().into())); + } + + if let Some(string) = args[0].as_str() { + return Ok(Value::Number(string.len().into())); + } + + Err(DscError::Parser(t!("functions.length.invalidArgType").to_string())) + } +} + + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + use serde_json::Value; + + #[test] + fn empty_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[length('')]", &Context::new()).unwrap(); + assert_eq!(result, Value::Number(0.into())); + } + + #[test] + fn not_empty_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[length('foo')]", &Context::new()).unwrap(); + assert_eq!(result, Value::Number(3.into())); + } +} diff --git a/dsc_lib/src/functions/mod.rs b/dsc_lib/src/functions/mod.rs index 998979c69..f95135a3b 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -21,12 +21,14 @@ pub mod contains; pub mod create_array; pub mod create_object; pub mod div; +pub mod empty; pub mod envvar; pub mod equals; pub mod greater; pub mod greater_or_equals; pub mod r#if; pub mod r#false; +pub mod length; pub mod less; pub mod less_or_equals; pub mod format; @@ -101,6 +103,7 @@ impl FunctionDispatcher { functions.insert("createArray".to_string(), Box::new(create_array::CreateArray{})); functions.insert("createObject".to_string(), Box::new(create_object::CreateObject{})); functions.insert("div".to_string(), Box::new(div::Div{})); + functions.insert("empty".to_string(), Box::new(empty::Empty{})); 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{})); @@ -109,6 +112,7 @@ impl FunctionDispatcher { 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("length".to_string(), Box::new(length::Length{})); 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{})); From 17610e93c341e92366d0124adc71b582d8f6a260 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 25 Jul 2025 12:14:31 -0700 Subject: [PATCH 4/8] Update dsc_lib/src/functions/contains.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dsc_lib/src/functions/contains.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc_lib/src/functions/contains.rs b/dsc_lib/src/functions/contains.rs index 88c684689..8674fe416 100644 --- a/dsc_lib/src/functions/contains.rs +++ b/dsc_lib/src/functions/contains.rs @@ -25,7 +25,7 @@ impl Function for Contains { } fn max_args(&self) -> usize { - usize::MAX + 2 } fn accepted_arg_types(&self) -> Vec { From fa2df493ce413d701b3c6c306367e97065ed1cc6 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 25 Jul 2025 12:15:00 -0700 Subject: [PATCH 5/8] Update dsc_lib/src/functions/empty.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dsc_lib/src/functions/empty.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc_lib/src/functions/empty.rs b/dsc_lib/src/functions/empty.rs index ee6306a9d..d0e1c1b00 100644 --- a/dsc_lib/src/functions/empty.rs +++ b/dsc_lib/src/functions/empty.rs @@ -41,7 +41,7 @@ impl Function for Empty { } if let Some(object) = args[0].as_object() { - return Ok(Value::Bool(object.keys().len() == 0)); + return Ok(Value::Bool(object.is_empty())); } if let Some(string) = args[0].as_str() { From 6d60e79c72e4f258967149f2c1ea74b616573398 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 25 Jul 2025 12:15:06 -0700 Subject: [PATCH 6/8] Update dsc_lib/src/functions/length.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dsc_lib/src/functions/length.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/dsc_lib/src/functions/length.rs b/dsc_lib/src/functions/length.rs index 980530045..1e193bf15 100644 --- a/dsc_lib/src/functions/length.rs +++ b/dsc_lib/src/functions/length.rs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use core::option::Option::Some; - use crate::DscError; use crate::configure::context::Context; use crate::functions::{AcceptedArgKind, Function, FunctionCategory}; From e9a33d6fe1f3c130e726a4fb3ba50501f1379263 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 25 Jul 2025 12:15:11 -0700 Subject: [PATCH 7/8] Update dsc_lib/src/functions/empty.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dsc_lib/src/functions/empty.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/dsc_lib/src/functions/empty.rs b/dsc_lib/src/functions/empty.rs index d0e1c1b00..932a15f52 100644 --- a/dsc_lib/src/functions/empty.rs +++ b/dsc_lib/src/functions/empty.rs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use core::option::Option::Some; - use crate::DscError; use crate::configure::context::Context; use crate::functions::{AcceptedArgKind, Function, FunctionCategory}; From 3001dad6868c4e0f84f1debd3f3e693d87bd35f9 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 5 Aug 2025 13:07:27 -0700 Subject: [PATCH 8/8] fix logic for checking string or number --- dsc/tests/dsc_functions.tests.ps1 | 2 ++ dsc_lib/src/functions/contains.rs | 39 ++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index a9b7641c9..ab29802a8 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -115,6 +115,7 @@ Describe 'tests for function expressions' { @{ expression = "[contains(parameters('array'), 'a')]" ; expected = $true } @{ expression = "[contains(parameters('array'), 2)]" ; expected = $false } @{ expression = "[contains(parameters('array'), 1)]" ; expected = $true } + @{ expression = "[contains(parameters('array'), 'z')]" ; expected = $false } @{ expression = "[contains(parameters('object'), 'a')]" ; expected = $true } @{ expression = "[contains(parameters('object'), 'c')]" ; expected = $false } @{ expression = "[contains(parameters('object'), 3)]" ; expected = $true } @@ -134,6 +135,7 @@ Describe 'tests for function expressions' { defaultValue: - a - b + - 0 - 1 object: type: object diff --git a/dsc_lib/src/functions/contains.rs b/dsc_lib/src/functions/contains.rs index 8674fe416..6cc1013e2 100644 --- a/dsc_lib/src/functions/contains.rs +++ b/dsc_lib/src/functions/contains.rs @@ -37,9 +37,9 @@ impl Function for Contains { let mut found = false; let (string_to_find, number_to_find) = if let Some(string) = args[1].as_str() { - (string.to_string(), 0) + (Some(string.to_string()), None) } else if let Some(number) = args[1].as_i64() { - (number.to_string(), number) + (None, Some(number)) } else { return Err(DscError::Parser(t!("functions.contains.invalidItemToFind").to_string())); }; @@ -48,14 +48,18 @@ impl Function for Contains { if let Some(array) = args[0].as_array() { for item in array { if let Some(item_str) = item.as_str() { - if item_str == string_to_find { - found = true; - break; + if let Some(string) = &string_to_find { + if item_str == string { + found = true; + break; + } } } else if let Some(item_num) = item.as_i64() { - if item_num == number_to_find { - found = true; - break; + if let Some(number) = number_to_find { + if item_num == number { + found = true; + break; + } } } } @@ -66,9 +70,16 @@ impl Function for Contains { if let Some(object) = args[0].as_object() { // see if key exists for key in object.keys() { - if key == &string_to_find { - found = true; - break; + if let Some(string) = &string_to_find { + if key == string { + found = true; + break; + } + } else if let Some(number) = number_to_find { + if key == &number.to_string() { + found = true; + break; + } } } return Ok(Value::Bool(found)); @@ -76,8 +87,10 @@ impl Function for Contains { // for string, we check if the string contains the substring or number if let Some(str) = args[0].as_str() { - if str.contains(&string_to_find) { - found = true; + if let Some(string) = &string_to_find { + found = str.contains(string); + } else if let Some(number) = number_to_find { + found = str.contains(&number.to_string()); } return Ok(Value::Bool(found)); }