From af2ac818d4ca7851c72c3f1fdcf4a589adb7a003 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Fri, 26 Sep 2025 14:23:01 +0200 Subject: [PATCH 1/9] Add intersection() function --- dsc/tests/dsc_functions.tests.ps1 | 70 ++++++++++++ dsc_lib/locales/en-us.toml | 5 + dsc_lib/src/functions/intersection.rs | 155 ++++++++++++++++++++++++++ dsc_lib/src/functions/mod.rs | 2 + 4 files changed, 232 insertions(+) create mode 100644 dsc_lib/src/functions/intersection.rs diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 6538c9c7e..b1583ebb2 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -111,6 +111,76 @@ Describe 'tests for function expressions' { } } + It 'intersection function works for: ' -TestCases @( + @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'))]"; expected = @('cd') } + @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'b' } } + @{ expression = "[intersection(parameters('thirdArray'), parameters('fourthArray'))]"; expected = @('ef', 'gh') } + @{ expression = "[intersection(parameters('thirdObject'), parameters('fourthObject'))]"; expected = [pscustomobject]@{ three = 'd' } } + @{ expression = "[intersection(parameters('firstArray'), parameters('thirdArray'))]"; expected = @() } + @{ expression = "[intersection(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: b + three: d + thirdObject: + type: object + defaultValue: + two: c + three: d + fourthObject: + type: object + defaultValue: + three: d + four: e + firstArray: + type: array + defaultValue: + - ab + - cd + secondArray: + type: array + defaultValue: + - cd + - ef + thirdArray: + type: array + defaultValue: + - ef + - gh + fourthArray: + type: array + defaultValue: + - gh + - ef + - ij + 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 } diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index ada29a34c..8f0d903de 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -353,6 +353,11 @@ parseStringError = "unable to parse string to int" castError = "unable to cast to int" parseNumError = "unable to parse number to int" +[functions.intersection] +description = "Returns a single array or object with the common elements from the parameters" +invoked = "intersection function" +invalidArgType = "All arguments must either be arrays or objects" + [functions.indexOf] description = "Returns the index of the first occurrence of an item in an array" invoked = "indexOf function" diff --git a/dsc_lib/src/functions/intersection.rs b/dsc_lib/src/functions/intersection.rs new file mode 100644 index 000000000..f8ceceaf1 --- /dev/null +++ b/dsc_lib/src/functions/intersection.rs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata}; +use rust_i18n::t; +use serde_json::{Map, Value}; +use tracing::debug; + +#[derive(Debug, Default)] +pub struct Intersection {} + +impl Function for Intersection { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "intersection".to_string(), + description: t!("functions.intersection.description").to_string(), + category: FunctionCategory::Array, + min_args: 2, + max_args: usize::MAX, + accepted_arg_ordered_types: vec![ + vec![FunctionArgKind::Array, FunctionArgKind::Object], + vec![FunctionArgKind::Array, FunctionArgKind::Object], + ], + remaining_arg_accepted_types: Some(vec![FunctionArgKind::Array, FunctionArgKind::Object]), + return_types: vec![FunctionArgKind::Array, FunctionArgKind::Object], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.intersection.invoked")); + + if args[0].is_array() { + let first_array = args[0].as_array().unwrap(); + let mut result = Vec::new(); + + for item in first_array { + let mut found_in_all = true; + + for arg in &args[1..] { + if let Some(array) = arg.as_array() { + if !array.contains(item) { + found_in_all = false; + break; + } + } else { + return Err(DscError::Parser(t!("functions.intersection.invalidArgType").to_string())); + } + } + + if found_in_all && !result.contains(item) { + result.push(item.clone()); + } + } + + return Ok(Value::Array(result)); + } + + if args[0].is_object() { + let first_object = args[0].as_object().unwrap(); + let mut result = Map::new(); + + for (key, value) in first_object { + let mut found_in_all = true; + + for arg in &args[1..] { + if let Some(object) = arg.as_object() { + if let Some(other_value) = object.get(key) { + if other_value != value { + found_in_all = false; + break; + } + } else { + found_in_all = false; + break; + } + } else { + return Err(DscError::Parser(t!("functions.intersection.invalidArgType").to_string())); + } + } + + if found_in_all { + result.insert(key.clone(), value.clone()); + } + } + + return Ok(Value::Object(result)); + } + + Err(DscError::Parser(t!("functions.intersection.invalidArgType").to_string())) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn array_intersection() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createArray(1, 2, 3), createArray(2, 3, 4))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!([2, 3])); + } + + #[test] + fn array_intersection_three_arrays() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createArray(1, 2, 3, 4), createArray(2, 3, 4, 5), createArray(3, 4, 5, 6))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!([3, 4])); + } + + #[test] + fn array_intersection_no_common_elements() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createArray(1, 2), createArray(3, 4))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!([])); + } + + #[test] + fn array_intersection_with_duplicates() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createArray(1, 2, 2, 3), createArray(2, 2, 3, 4))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!([2, 3])); + } + + #[test] + fn object_intersection() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createObject('a', 1, 'b', 2), createObject('b', 2, 'c', 3))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!({"b": 2})); + } + + #[test] + fn object_intersection_different_values() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createObject('a', 1, 'b', 2), createObject('a', 2, 'b', 2))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!({"b": 2})); + } + + #[test] + fn object_intersection_no_common_keys() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createObject('a', 1), createObject('b', 2))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!({})); + } + + #[test] + fn mixed_types_error() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createArray(1, 2), createObject('a', 1))]", &Context::new()); + assert!(result.is_err()); + } +} \ No newline at end of file diff --git a/dsc_lib/src/functions/mod.rs b/dsc_lib/src/functions/mod.rs index e881acc3e..a28395eef 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -40,6 +40,7 @@ pub mod less_or_equals; pub mod format; pub mod int; pub mod index_of; +pub mod intersection; pub mod join; pub mod last_index_of; pub mod max; @@ -155,6 +156,7 @@ impl FunctionDispatcher { Box::new(format::Format{}), Box::new(int::Int{}), Box::new(index_of::IndexOf{}), + Box::new(intersection::Intersection{}), Box::new(join::Join{}), Box::new(last_index_of::LastIndexOf{}), Box::new(max::Max{}), From d5c3f86b781178049929f0a21182fba4715b70d8 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Fri, 26 Sep 2025 14:27:39 +0200 Subject: [PATCH 2/9] Add documentation --- .../schemas/config/functions/intersection.md | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 docs/reference/schemas/config/functions/intersection.md diff --git a/docs/reference/schemas/config/functions/intersection.md b/docs/reference/schemas/config/functions/intersection.md new file mode 100644 index 000000000..5e2066772 --- /dev/null +++ b/docs/reference/schemas/config/functions/intersection.md @@ -0,0 +1,236 @@ +--- +description: Reference for the 'intersection' DSC configuration document function +ms.date: 09/26/2025 +ms.topic: reference +title: intersection +--- + +## Synopsis + +Returns a single array or object with the common elements from the parameters. + +## Syntax + +```Syntax +intersection(value1, value2, ...) +``` + +## Description + +The `intersection()` function takes two or more arrays or objects and returns +only the elements that exist in all of them. For arrays, it returns elements +that appear in every array. For objects, it returns key-value pairs where both +the key and value match across all objects. + +All parameters must be the same type - either all arrays or all objects. +Results are deduplicated, meaning each element appears only once in the output. + +Supported types: + +- Arrays (elements compared by value) +- Objects (key-value pairs compared by deep equality) + +## Examples + +### Example 1 - Find common security groups across environments (arrays) + +Use `intersection()` to identify security groups that are consistently applied +across development, staging, and production environments. This helps ensure +security policies are uniformly enforced. This example uses +[`createArray()`][01] to build the security group lists. + +```yaml +# intersection.example.1.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Common Security Groups + type: Microsoft.DSC.Debug/Echo + properties: + output: + commonGroups: "[intersection(createArray('admin-access', 'monitoring', 'backup'), createArray('monitoring', 'backup', 'web-access'), createArray('backup', 'monitoring', 'database'))]" + twoEnvCommon: "[intersection(createArray('admin-access', 'monitoring'), createArray('monitoring', 'audit-log'))]" +``` + +```bash +dsc config get --file intersection.example.1.dsc.config.yaml +``` + +```yaml +results: +- name: Common Security Groups + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + commonGroups: + - monitoring + - backup + twoEnvCommon: + - monitoring +messages: [] +hadErrors: false +``` + +### Example 2 - Identify shared configuration properties (objects) + +Find configuration settings that are identical across multiple service +instances. This is useful for extracting common configuration into shared +templates or validating consistency. This example uses [`createObject()`][02] +to build configuration objects. + +```yaml +# intersection.example.2.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Shared Config Properties + type: Microsoft.DSC.Debug/Echo + properties: + output: + commonSettings: "[intersection(createObject('timeout', 30, 'retries', 3, 'region', 'us-east'), createObject('retries', 3, 'ssl', true, 'region', 'us-east'), createObject('region', 'us-east', 'retries', 3, 'logging', 'info'))]" +``` + +```bash +dsc config get --file intersection.example.2.dsc.config.yaml +``` + +```yaml +results: +- name: Shared Config Properties + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + commonSettings: + region: us-east + retries: 3 +messages: [] +hadErrors: false +``` + +### Example 3 - Find overlapping server capabilities (arrays with no matches) + +Sometimes environments have no common elements, which is valuable information +for infrastructure planning. This example shows how `intersection()` handles +arrays with no shared elements. + +```yaml +# intersection.example.3.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Server Capabilities + type: Microsoft.DSC.Debug/Echo + properties: + output: + noOverlap: "[intersection(createArray('windows-iis', 'dotnet-core'), createArray('linux-apache', 'php', 'mysql'))]" + someOverlap: "[intersection(createArray('docker', 'kubernetes', 'monitoring'), createArray('monitoring', 'logging', 'docker'))]" +``` + +```bash +dsc config get --file intersection.example.3.dsc.config.yaml +``` + +```yaml +results: +- name: Server Capabilities + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + noOverlap: [] + someOverlap: + - docker + - monitoring +messages: [] +hadErrors: false +``` + +### Example 4 - Validate compliance across teams (objects) + +Use `intersection()` to verify that critical compliance settings are identical +across different team configurations. Only settings with matching values will +appear in the result. + +```yaml +# intersection.example.4.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Compliance Check + type: Microsoft.DSC.Debug/Echo + properties: + output: + sharedCompliance: "[intersection(createObject('encryption', true, 'backup', 'daily', 'audit', true), createObject('audit', true, 'encryption', true, 'access', 'restricted'), createObject('encryption', true, 'audit', true, 'monitoring', 'enabled'))]" +``` + +```bash +dsc config get --file intersection.example.4.dsc.config.yaml +``` + +```yaml +results: +- name: Compliance Check + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + sharedCompliance: + audit: true + encryption: true +messages: [] +hadErrors: false +``` + +## Parameters + +### value1 + +The first array or object to compare. Required. + +```yaml +Type: array | object +Required: true +Position: 1 +``` + +### value2 + +The second array or object to compare. Must be the same type as value1. +Required. + +```yaml +Type: array | object +Required: true +Position: 2 +``` + +### Additional values + +Additional arrays or objects to include in the intersection. All must be the +same type. Optional. + +```yaml +Type: array | object +Required: false +Position: 3+ +``` + +## Output + +Returns an array or object containing only the common elements. The return type +matches the input type. + +```yaml +Type: array | object +``` + +## Related functions + +- [`union()`][00] - Combines all elements from multiple arrays or objects +- [`contains()`][03] - Checks for presence in arrays/objects/strings +- [`createArray()`][01] - Creates an array from individual values +- [`createObject()`][02] - Creates an object from key-value pairs + + +[00]: ./union.md +[01]: ./createArray.md +[02]: ./createObject.md +[03]: ./contains.md From 2caf08e1730d6bc751758e3790e724a4e48ef220 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 27 Sep 2025 01:49:44 +0200 Subject: [PATCH 3/9] Resolve remarks --- dsc/tests/dsc_functions.tests.ps1 | 54 +++++++++++++++++++++++++++ dsc_lib/src/functions/intersection.rs | 6 +-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index b1583ebb2..7b3dce608 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -118,6 +118,19 @@ Describe 'tests for function expressions' { @{ expression = "[intersection(parameters('thirdObject'), parameters('fourthObject'))]"; expected = [pscustomobject]@{ three = 'd' } } @{ expression = "[intersection(parameters('firstArray'), parameters('thirdArray'))]"; expected = @() } @{ expression = "[intersection(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } + # Test with 3 arrays - should find common elements across all three + @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'), parameters('fifthArray'))]"; expected = @('cd') } + # Test with 3 objects - should find properties with matching key-value pairs across all three + @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'), parameters('sixthObject'))]"; expected = [pscustomobject]@{ two = 'b' } } + # Test with nested objects - should match deep equality + @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'))]"; expected = [pscustomobject]@{ + shared = [pscustomobject]@{ value = 42; flag = $true } + level = 1 + } } + # Test with nested objects - no common nested properties + @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject3'))]"; expected = [pscustomobject]@{ level = 1 } } + # Test with 3 nested objects + @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'), parameters('nestedObject4'))]"; expected = [pscustomobject]@{ level = 1 } } ) { param($expression, $expected, $isError) @@ -144,6 +157,42 @@ Describe 'tests for function expressions' { defaultValue: three: d four: e + sixthObject: + type: object + defaultValue: + two: b + five: f + nestedObject1: + type: object + defaultValue: + shared: + value: 42 + flag: true + level: 1 + unique1: test + nestedObject2: + type: object + defaultValue: + shared: + value: 42 + flag: true + level: 1 + unique2: test + nestedObject3: + type: object + defaultValue: + shared: + value: 24 + flag: true + level: 1 + unique3: test + nestedObject4: + type: object + defaultValue: + level: 1 + different: + value: 100 + flag: false firstArray: type: array defaultValue: @@ -165,6 +214,11 @@ Describe 'tests for function expressions' { - gh - ef - ij + fifthArray: + type: array + defaultValue: + - cd + - kl resources: - name: Echo type: Microsoft.DSC.Debug/Echo diff --git a/dsc_lib/src/functions/intersection.rs b/dsc_lib/src/functions/intersection.rs index f8ceceaf1..6c0dd4a9a 100644 --- a/dsc_lib/src/functions/intersection.rs +++ b/dsc_lib/src/functions/intersection.rs @@ -31,8 +31,7 @@ impl Function for Intersection { fn invoke(&self, args: &[Value], _context: &Context) -> Result { debug!("{}", t!("functions.intersection.invoked")); - if args[0].is_array() { - let first_array = args[0].as_array().unwrap(); + if let Some(first_array) = args[0].as_array() { let mut result = Vec::new(); for item in first_array { @@ -57,8 +56,7 @@ impl Function for Intersection { return Ok(Value::Array(result)); } - if args[0].is_object() { - let first_object = args[0].as_object().unwrap(); + if let Some(first_object) = args[0].as_object() { let mut result = Map::new(); for (key, value) in first_object { From 4ce01ed45c756aa7faa4b44e522c2cd73fcf1d31 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 27 Sep 2025 01:50:29 +0200 Subject: [PATCH 4/9] Remove comments --- dsc/tests/dsc_functions.tests.ps1 | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 7b3dce608..75d193002 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -118,18 +118,13 @@ Describe 'tests for function expressions' { @{ expression = "[intersection(parameters('thirdObject'), parameters('fourthObject'))]"; expected = [pscustomobject]@{ three = 'd' } } @{ expression = "[intersection(parameters('firstArray'), parameters('thirdArray'))]"; expected = @() } @{ expression = "[intersection(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } - # Test with 3 arrays - should find common elements across all three @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'), parameters('fifthArray'))]"; expected = @('cd') } - # Test with 3 objects - should find properties with matching key-value pairs across all three @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'), parameters('sixthObject'))]"; expected = [pscustomobject]@{ two = 'b' } } - # Test with nested objects - should match deep equality @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'))]"; expected = [pscustomobject]@{ shared = [pscustomobject]@{ value = 42; flag = $true } level = 1 } } - # Test with nested objects - no common nested properties @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject3'))]"; expected = [pscustomobject]@{ level = 1 } } - # Test with 3 nested objects @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'), parameters('nestedObject4'))]"; expected = [pscustomobject]@{ level = 1 } } ) { param($expression, $expected, $isError) From 9d70166acfac59366fbbde471749fc442120ff40 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Fri, 26 Sep 2025 14:23:01 +0200 Subject: [PATCH 5/9] Add intersection() function --- dsc/tests/dsc_functions.tests.ps1 | 70 ++++++++++++ dsc_lib/locales/en-us.toml | 5 + dsc_lib/src/functions/intersection.rs | 155 ++++++++++++++++++++++++++ dsc_lib/src/functions/mod.rs | 2 + 4 files changed, 232 insertions(+) create mode 100644 dsc_lib/src/functions/intersection.rs diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 6538c9c7e..b1583ebb2 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -111,6 +111,76 @@ Describe 'tests for function expressions' { } } + It 'intersection function works for: ' -TestCases @( + @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'))]"; expected = @('cd') } + @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'b' } } + @{ expression = "[intersection(parameters('thirdArray'), parameters('fourthArray'))]"; expected = @('ef', 'gh') } + @{ expression = "[intersection(parameters('thirdObject'), parameters('fourthObject'))]"; expected = [pscustomobject]@{ three = 'd' } } + @{ expression = "[intersection(parameters('firstArray'), parameters('thirdArray'))]"; expected = @() } + @{ expression = "[intersection(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: b + three: d + thirdObject: + type: object + defaultValue: + two: c + three: d + fourthObject: + type: object + defaultValue: + three: d + four: e + firstArray: + type: array + defaultValue: + - ab + - cd + secondArray: + type: array + defaultValue: + - cd + - ef + thirdArray: + type: array + defaultValue: + - ef + - gh + fourthArray: + type: array + defaultValue: + - gh + - ef + - ij + 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 } diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index ada29a34c..8f0d903de 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -353,6 +353,11 @@ parseStringError = "unable to parse string to int" castError = "unable to cast to int" parseNumError = "unable to parse number to int" +[functions.intersection] +description = "Returns a single array or object with the common elements from the parameters" +invoked = "intersection function" +invalidArgType = "All arguments must either be arrays or objects" + [functions.indexOf] description = "Returns the index of the first occurrence of an item in an array" invoked = "indexOf function" diff --git a/dsc_lib/src/functions/intersection.rs b/dsc_lib/src/functions/intersection.rs new file mode 100644 index 000000000..f8ceceaf1 --- /dev/null +++ b/dsc_lib/src/functions/intersection.rs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata}; +use rust_i18n::t; +use serde_json::{Map, Value}; +use tracing::debug; + +#[derive(Debug, Default)] +pub struct Intersection {} + +impl Function for Intersection { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "intersection".to_string(), + description: t!("functions.intersection.description").to_string(), + category: FunctionCategory::Array, + min_args: 2, + max_args: usize::MAX, + accepted_arg_ordered_types: vec![ + vec![FunctionArgKind::Array, FunctionArgKind::Object], + vec![FunctionArgKind::Array, FunctionArgKind::Object], + ], + remaining_arg_accepted_types: Some(vec![FunctionArgKind::Array, FunctionArgKind::Object]), + return_types: vec![FunctionArgKind::Array, FunctionArgKind::Object], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.intersection.invoked")); + + if args[0].is_array() { + let first_array = args[0].as_array().unwrap(); + let mut result = Vec::new(); + + for item in first_array { + let mut found_in_all = true; + + for arg in &args[1..] { + if let Some(array) = arg.as_array() { + if !array.contains(item) { + found_in_all = false; + break; + } + } else { + return Err(DscError::Parser(t!("functions.intersection.invalidArgType").to_string())); + } + } + + if found_in_all && !result.contains(item) { + result.push(item.clone()); + } + } + + return Ok(Value::Array(result)); + } + + if args[0].is_object() { + let first_object = args[0].as_object().unwrap(); + let mut result = Map::new(); + + for (key, value) in first_object { + let mut found_in_all = true; + + for arg in &args[1..] { + if let Some(object) = arg.as_object() { + if let Some(other_value) = object.get(key) { + if other_value != value { + found_in_all = false; + break; + } + } else { + found_in_all = false; + break; + } + } else { + return Err(DscError::Parser(t!("functions.intersection.invalidArgType").to_string())); + } + } + + if found_in_all { + result.insert(key.clone(), value.clone()); + } + } + + return Ok(Value::Object(result)); + } + + Err(DscError::Parser(t!("functions.intersection.invalidArgType").to_string())) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn array_intersection() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createArray(1, 2, 3), createArray(2, 3, 4))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!([2, 3])); + } + + #[test] + fn array_intersection_three_arrays() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createArray(1, 2, 3, 4), createArray(2, 3, 4, 5), createArray(3, 4, 5, 6))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!([3, 4])); + } + + #[test] + fn array_intersection_no_common_elements() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createArray(1, 2), createArray(3, 4))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!([])); + } + + #[test] + fn array_intersection_with_duplicates() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createArray(1, 2, 2, 3), createArray(2, 2, 3, 4))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!([2, 3])); + } + + #[test] + fn object_intersection() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createObject('a', 1, 'b', 2), createObject('b', 2, 'c', 3))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!({"b": 2})); + } + + #[test] + fn object_intersection_different_values() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createObject('a', 1, 'b', 2), createObject('a', 2, 'b', 2))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!({"b": 2})); + } + + #[test] + fn object_intersection_no_common_keys() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createObject('a', 1), createObject('b', 2))]", &Context::new()).unwrap(); + assert_eq!(result, serde_json::json!({})); + } + + #[test] + fn mixed_types_error() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[intersection(createArray(1, 2), createObject('a', 1))]", &Context::new()); + assert!(result.is_err()); + } +} \ No newline at end of file diff --git a/dsc_lib/src/functions/mod.rs b/dsc_lib/src/functions/mod.rs index 413d49e4d..d16c1d5ce 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -40,6 +40,7 @@ pub mod less_or_equals; pub mod format; pub mod int; pub mod index_of; +pub mod intersection; pub mod join; pub mod last_index_of; pub mod max; @@ -155,6 +156,7 @@ impl FunctionDispatcher { Box::new(format::Format{}), Box::new(int::Int{}), Box::new(index_of::IndexOf{}), + Box::new(intersection::Intersection{}), Box::new(join::Join{}), Box::new(last_index_of::LastIndexOf{}), Box::new(max::Max{}), From ba7b614a394b5c315b56a469216e7eace8f99ae5 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Fri, 26 Sep 2025 14:27:39 +0200 Subject: [PATCH 6/9] Add documentation --- .../schemas/config/functions/intersection.md | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 docs/reference/schemas/config/functions/intersection.md diff --git a/docs/reference/schemas/config/functions/intersection.md b/docs/reference/schemas/config/functions/intersection.md new file mode 100644 index 000000000..5e2066772 --- /dev/null +++ b/docs/reference/schemas/config/functions/intersection.md @@ -0,0 +1,236 @@ +--- +description: Reference for the 'intersection' DSC configuration document function +ms.date: 09/26/2025 +ms.topic: reference +title: intersection +--- + +## Synopsis + +Returns a single array or object with the common elements from the parameters. + +## Syntax + +```Syntax +intersection(value1, value2, ...) +``` + +## Description + +The `intersection()` function takes two or more arrays or objects and returns +only the elements that exist in all of them. For arrays, it returns elements +that appear in every array. For objects, it returns key-value pairs where both +the key and value match across all objects. + +All parameters must be the same type - either all arrays or all objects. +Results are deduplicated, meaning each element appears only once in the output. + +Supported types: + +- Arrays (elements compared by value) +- Objects (key-value pairs compared by deep equality) + +## Examples + +### Example 1 - Find common security groups across environments (arrays) + +Use `intersection()` to identify security groups that are consistently applied +across development, staging, and production environments. This helps ensure +security policies are uniformly enforced. This example uses +[`createArray()`][01] to build the security group lists. + +```yaml +# intersection.example.1.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Common Security Groups + type: Microsoft.DSC.Debug/Echo + properties: + output: + commonGroups: "[intersection(createArray('admin-access', 'monitoring', 'backup'), createArray('monitoring', 'backup', 'web-access'), createArray('backup', 'monitoring', 'database'))]" + twoEnvCommon: "[intersection(createArray('admin-access', 'monitoring'), createArray('monitoring', 'audit-log'))]" +``` + +```bash +dsc config get --file intersection.example.1.dsc.config.yaml +``` + +```yaml +results: +- name: Common Security Groups + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + commonGroups: + - monitoring + - backup + twoEnvCommon: + - monitoring +messages: [] +hadErrors: false +``` + +### Example 2 - Identify shared configuration properties (objects) + +Find configuration settings that are identical across multiple service +instances. This is useful for extracting common configuration into shared +templates or validating consistency. This example uses [`createObject()`][02] +to build configuration objects. + +```yaml +# intersection.example.2.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Shared Config Properties + type: Microsoft.DSC.Debug/Echo + properties: + output: + commonSettings: "[intersection(createObject('timeout', 30, 'retries', 3, 'region', 'us-east'), createObject('retries', 3, 'ssl', true, 'region', 'us-east'), createObject('region', 'us-east', 'retries', 3, 'logging', 'info'))]" +``` + +```bash +dsc config get --file intersection.example.2.dsc.config.yaml +``` + +```yaml +results: +- name: Shared Config Properties + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + commonSettings: + region: us-east + retries: 3 +messages: [] +hadErrors: false +``` + +### Example 3 - Find overlapping server capabilities (arrays with no matches) + +Sometimes environments have no common elements, which is valuable information +for infrastructure planning. This example shows how `intersection()` handles +arrays with no shared elements. + +```yaml +# intersection.example.3.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Server Capabilities + type: Microsoft.DSC.Debug/Echo + properties: + output: + noOverlap: "[intersection(createArray('windows-iis', 'dotnet-core'), createArray('linux-apache', 'php', 'mysql'))]" + someOverlap: "[intersection(createArray('docker', 'kubernetes', 'monitoring'), createArray('monitoring', 'logging', 'docker'))]" +``` + +```bash +dsc config get --file intersection.example.3.dsc.config.yaml +``` + +```yaml +results: +- name: Server Capabilities + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + noOverlap: [] + someOverlap: + - docker + - monitoring +messages: [] +hadErrors: false +``` + +### Example 4 - Validate compliance across teams (objects) + +Use `intersection()` to verify that critical compliance settings are identical +across different team configurations. Only settings with matching values will +appear in the result. + +```yaml +# intersection.example.4.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Compliance Check + type: Microsoft.DSC.Debug/Echo + properties: + output: + sharedCompliance: "[intersection(createObject('encryption', true, 'backup', 'daily', 'audit', true), createObject('audit', true, 'encryption', true, 'access', 'restricted'), createObject('encryption', true, 'audit', true, 'monitoring', 'enabled'))]" +``` + +```bash +dsc config get --file intersection.example.4.dsc.config.yaml +``` + +```yaml +results: +- name: Compliance Check + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + sharedCompliance: + audit: true + encryption: true +messages: [] +hadErrors: false +``` + +## Parameters + +### value1 + +The first array or object to compare. Required. + +```yaml +Type: array | object +Required: true +Position: 1 +``` + +### value2 + +The second array or object to compare. Must be the same type as value1. +Required. + +```yaml +Type: array | object +Required: true +Position: 2 +``` + +### Additional values + +Additional arrays or objects to include in the intersection. All must be the +same type. Optional. + +```yaml +Type: array | object +Required: false +Position: 3+ +``` + +## Output + +Returns an array or object containing only the common elements. The return type +matches the input type. + +```yaml +Type: array | object +``` + +## Related functions + +- [`union()`][00] - Combines all elements from multiple arrays or objects +- [`contains()`][03] - Checks for presence in arrays/objects/strings +- [`createArray()`][01] - Creates an array from individual values +- [`createObject()`][02] - Creates an object from key-value pairs + + +[00]: ./union.md +[01]: ./createArray.md +[02]: ./createObject.md +[03]: ./contains.md From 1401cabcc7ee46df6d4c62d4efc69e5392ede031 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 27 Sep 2025 01:49:44 +0200 Subject: [PATCH 7/9] Resolve remarks --- dsc/tests/dsc_functions.tests.ps1 | 54 +++++++++++++++++++++++++++ dsc_lib/src/functions/intersection.rs | 6 +-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index b1583ebb2..7b3dce608 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -118,6 +118,19 @@ Describe 'tests for function expressions' { @{ expression = "[intersection(parameters('thirdObject'), parameters('fourthObject'))]"; expected = [pscustomobject]@{ three = 'd' } } @{ expression = "[intersection(parameters('firstArray'), parameters('thirdArray'))]"; expected = @() } @{ expression = "[intersection(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } + # Test with 3 arrays - should find common elements across all three + @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'), parameters('fifthArray'))]"; expected = @('cd') } + # Test with 3 objects - should find properties with matching key-value pairs across all three + @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'), parameters('sixthObject'))]"; expected = [pscustomobject]@{ two = 'b' } } + # Test with nested objects - should match deep equality + @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'))]"; expected = [pscustomobject]@{ + shared = [pscustomobject]@{ value = 42; flag = $true } + level = 1 + } } + # Test with nested objects - no common nested properties + @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject3'))]"; expected = [pscustomobject]@{ level = 1 } } + # Test with 3 nested objects + @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'), parameters('nestedObject4'))]"; expected = [pscustomobject]@{ level = 1 } } ) { param($expression, $expected, $isError) @@ -144,6 +157,42 @@ Describe 'tests for function expressions' { defaultValue: three: d four: e + sixthObject: + type: object + defaultValue: + two: b + five: f + nestedObject1: + type: object + defaultValue: + shared: + value: 42 + flag: true + level: 1 + unique1: test + nestedObject2: + type: object + defaultValue: + shared: + value: 42 + flag: true + level: 1 + unique2: test + nestedObject3: + type: object + defaultValue: + shared: + value: 24 + flag: true + level: 1 + unique3: test + nestedObject4: + type: object + defaultValue: + level: 1 + different: + value: 100 + flag: false firstArray: type: array defaultValue: @@ -165,6 +214,11 @@ Describe 'tests for function expressions' { - gh - ef - ij + fifthArray: + type: array + defaultValue: + - cd + - kl resources: - name: Echo type: Microsoft.DSC.Debug/Echo diff --git a/dsc_lib/src/functions/intersection.rs b/dsc_lib/src/functions/intersection.rs index f8ceceaf1..6c0dd4a9a 100644 --- a/dsc_lib/src/functions/intersection.rs +++ b/dsc_lib/src/functions/intersection.rs @@ -31,8 +31,7 @@ impl Function for Intersection { fn invoke(&self, args: &[Value], _context: &Context) -> Result { debug!("{}", t!("functions.intersection.invoked")); - if args[0].is_array() { - let first_array = args[0].as_array().unwrap(); + if let Some(first_array) = args[0].as_array() { let mut result = Vec::new(); for item in first_array { @@ -57,8 +56,7 @@ impl Function for Intersection { return Ok(Value::Array(result)); } - if args[0].is_object() { - let first_object = args[0].as_object().unwrap(); + if let Some(first_object) = args[0].as_object() { let mut result = Map::new(); for (key, value) in first_object { From acaf936b911b07b12974e47bbc4d735b41d145c1 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 27 Sep 2025 01:50:29 +0200 Subject: [PATCH 8/9] Remove comments --- dsc/tests/dsc_functions.tests.ps1 | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 7b3dce608..75d193002 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -118,18 +118,13 @@ Describe 'tests for function expressions' { @{ expression = "[intersection(parameters('thirdObject'), parameters('fourthObject'))]"; expected = [pscustomobject]@{ three = 'd' } } @{ expression = "[intersection(parameters('firstArray'), parameters('thirdArray'))]"; expected = @() } @{ expression = "[intersection(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } - # Test with 3 arrays - should find common elements across all three @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'), parameters('fifthArray'))]"; expected = @('cd') } - # Test with 3 objects - should find properties with matching key-value pairs across all three @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'), parameters('sixthObject'))]"; expected = [pscustomobject]@{ two = 'b' } } - # Test with nested objects - should match deep equality @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'))]"; expected = [pscustomobject]@{ shared = [pscustomobject]@{ value = 42; flag = $true } level = 1 } } - # Test with nested objects - no common nested properties @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject3'))]"; expected = [pscustomobject]@{ level = 1 } } - # Test with 3 nested objects @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'), parameters('nestedObject4'))]"; expected = [pscustomobject]@{ level = 1 } } ) { param($expression, $expected, $isError) From 5bcd2040fa1697f3f0e2733025078bbc06f7b2b9 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 27 Sep 2025 02:20:01 +0200 Subject: [PATCH 9/9] Fix vec --- dsc_lib/src/functions/intersection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc_lib/src/functions/intersection.rs b/dsc_lib/src/functions/intersection.rs index 6c0dd4a9a..292529ea3 100644 --- a/dsc_lib/src/functions/intersection.rs +++ b/dsc_lib/src/functions/intersection.rs @@ -16,7 +16,7 @@ impl Function for Intersection { FunctionMetadata { name: "intersection".to_string(), description: t!("functions.intersection.description").to_string(), - category: FunctionCategory::Array, + category: vec![FunctionCategory::Array, FunctionCategory::Object], min_args: 2, max_args: usize::MAX, accepted_arg_ordered_types: vec![