Skip to content

Commit 9d70166

Browse files
committed
Add intersection() function
1 parent f3abfc1 commit 9d70166

File tree

4 files changed

+232
-0
lines changed

4 files changed

+232
-0
lines changed

dsc/tests/dsc_functions.tests.ps1

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,76 @@ Describe 'tests for function expressions' {
111111
}
112112
}
113113

114+
It 'intersection function works for: <expression>' -TestCases @(
115+
@{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'))]"; expected = @('cd') }
116+
@{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'b' } }
117+
@{ expression = "[intersection(parameters('thirdArray'), parameters('fourthArray'))]"; expected = @('ef', 'gh') }
118+
@{ expression = "[intersection(parameters('thirdObject'), parameters('fourthObject'))]"; expected = [pscustomobject]@{ three = 'd' } }
119+
@{ expression = "[intersection(parameters('firstArray'), parameters('thirdArray'))]"; expected = @() }
120+
@{ expression = "[intersection(parameters('firstObject'), parameters('firstArray'))]"; isError = $true }
121+
) {
122+
param($expression, $expected, $isError)
123+
124+
$config_yaml = @"
125+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
126+
parameters:
127+
firstObject:
128+
type: object
129+
defaultValue:
130+
one: a
131+
two: b
132+
secondObject:
133+
type: object
134+
defaultValue:
135+
two: b
136+
three: d
137+
thirdObject:
138+
type: object
139+
defaultValue:
140+
two: c
141+
three: d
142+
fourthObject:
143+
type: object
144+
defaultValue:
145+
three: d
146+
four: e
147+
firstArray:
148+
type: array
149+
defaultValue:
150+
- ab
151+
- cd
152+
secondArray:
153+
type: array
154+
defaultValue:
155+
- cd
156+
- ef
157+
thirdArray:
158+
type: array
159+
defaultValue:
160+
- ef
161+
- gh
162+
fourthArray:
163+
type: array
164+
defaultValue:
165+
- gh
166+
- ef
167+
- ij
168+
resources:
169+
- name: Echo
170+
type: Microsoft.DSC.Debug/Echo
171+
properties:
172+
output: "$expression"
173+
"@
174+
$out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
175+
if ($isError) {
176+
$LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw)
177+
(Get-Content $TestDrive/error.log -Raw) | Should -Match 'All arguments must either be arrays or objects'
178+
} else {
179+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
180+
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
181+
}
182+
}
183+
114184
It 'contain function works for: <expression>' -TestCases @(
115185
@{ expression = "[contains(parameters('array'), 'a')]" ; expected = $true }
116186
@{ expression = "[contains(parameters('array'), 2)]" ; expected = $false }

dsc_lib/locales/en-us.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,11 @@ parseStringError = "unable to parse string to int"
353353
castError = "unable to cast to int"
354354
parseNumError = "unable to parse number to int"
355355

356+
[functions.intersection]
357+
description = "Returns a single array or object with the common elements from the parameters"
358+
invoked = "intersection function"
359+
invalidArgType = "All arguments must either be arrays or objects"
360+
356361
[functions.indexOf]
357362
description = "Returns the index of the first occurrence of an item in an array"
358363
invoked = "indexOf function"
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::DscError;
5+
use crate::configure::context::Context;
6+
use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata};
7+
use rust_i18n::t;
8+
use serde_json::{Map, Value};
9+
use tracing::debug;
10+
11+
#[derive(Debug, Default)]
12+
pub struct Intersection {}
13+
14+
impl Function for Intersection {
15+
fn get_metadata(&self) -> FunctionMetadata {
16+
FunctionMetadata {
17+
name: "intersection".to_string(),
18+
description: t!("functions.intersection.description").to_string(),
19+
category: FunctionCategory::Array,
20+
min_args: 2,
21+
max_args: usize::MAX,
22+
accepted_arg_ordered_types: vec![
23+
vec![FunctionArgKind::Array, FunctionArgKind::Object],
24+
vec![FunctionArgKind::Array, FunctionArgKind::Object],
25+
],
26+
remaining_arg_accepted_types: Some(vec![FunctionArgKind::Array, FunctionArgKind::Object]),
27+
return_types: vec![FunctionArgKind::Array, FunctionArgKind::Object],
28+
}
29+
}
30+
31+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
32+
debug!("{}", t!("functions.intersection.invoked"));
33+
34+
if args[0].is_array() {
35+
let first_array = args[0].as_array().unwrap();
36+
let mut result = Vec::new();
37+
38+
for item in first_array {
39+
let mut found_in_all = true;
40+
41+
for arg in &args[1..] {
42+
if let Some(array) = arg.as_array() {
43+
if !array.contains(item) {
44+
found_in_all = false;
45+
break;
46+
}
47+
} else {
48+
return Err(DscError::Parser(t!("functions.intersection.invalidArgType").to_string()));
49+
}
50+
}
51+
52+
if found_in_all && !result.contains(item) {
53+
result.push(item.clone());
54+
}
55+
}
56+
57+
return Ok(Value::Array(result));
58+
}
59+
60+
if args[0].is_object() {
61+
let first_object = args[0].as_object().unwrap();
62+
let mut result = Map::new();
63+
64+
for (key, value) in first_object {
65+
let mut found_in_all = true;
66+
67+
for arg in &args[1..] {
68+
if let Some(object) = arg.as_object() {
69+
if let Some(other_value) = object.get(key) {
70+
if other_value != value {
71+
found_in_all = false;
72+
break;
73+
}
74+
} else {
75+
found_in_all = false;
76+
break;
77+
}
78+
} else {
79+
return Err(DscError::Parser(t!("functions.intersection.invalidArgType").to_string()));
80+
}
81+
}
82+
83+
if found_in_all {
84+
result.insert(key.clone(), value.clone());
85+
}
86+
}
87+
88+
return Ok(Value::Object(result));
89+
}
90+
91+
Err(DscError::Parser(t!("functions.intersection.invalidArgType").to_string()))
92+
}
93+
}
94+
95+
#[cfg(test)]
96+
mod tests {
97+
use crate::configure::context::Context;
98+
use crate::parser::Statement;
99+
100+
#[test]
101+
fn array_intersection() {
102+
let mut parser = Statement::new().unwrap();
103+
let result = parser.parse_and_execute("[intersection(createArray(1, 2, 3), createArray(2, 3, 4))]", &Context::new()).unwrap();
104+
assert_eq!(result, serde_json::json!([2, 3]));
105+
}
106+
107+
#[test]
108+
fn array_intersection_three_arrays() {
109+
let mut parser = Statement::new().unwrap();
110+
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();
111+
assert_eq!(result, serde_json::json!([3, 4]));
112+
}
113+
114+
#[test]
115+
fn array_intersection_no_common_elements() {
116+
let mut parser = Statement::new().unwrap();
117+
let result = parser.parse_and_execute("[intersection(createArray(1, 2), createArray(3, 4))]", &Context::new()).unwrap();
118+
assert_eq!(result, serde_json::json!([]));
119+
}
120+
121+
#[test]
122+
fn array_intersection_with_duplicates() {
123+
let mut parser = Statement::new().unwrap();
124+
let result = parser.parse_and_execute("[intersection(createArray(1, 2, 2, 3), createArray(2, 2, 3, 4))]", &Context::new()).unwrap();
125+
assert_eq!(result, serde_json::json!([2, 3]));
126+
}
127+
128+
#[test]
129+
fn object_intersection() {
130+
let mut parser = Statement::new().unwrap();
131+
let result = parser.parse_and_execute("[intersection(createObject('a', 1, 'b', 2), createObject('b', 2, 'c', 3))]", &Context::new()).unwrap();
132+
assert_eq!(result, serde_json::json!({"b": 2}));
133+
}
134+
135+
#[test]
136+
fn object_intersection_different_values() {
137+
let mut parser = Statement::new().unwrap();
138+
let result = parser.parse_and_execute("[intersection(createObject('a', 1, 'b', 2), createObject('a', 2, 'b', 2))]", &Context::new()).unwrap();
139+
assert_eq!(result, serde_json::json!({"b": 2}));
140+
}
141+
142+
#[test]
143+
fn object_intersection_no_common_keys() {
144+
let mut parser = Statement::new().unwrap();
145+
let result = parser.parse_and_execute("[intersection(createObject('a', 1), createObject('b', 2))]", &Context::new()).unwrap();
146+
assert_eq!(result, serde_json::json!({}));
147+
}
148+
149+
#[test]
150+
fn mixed_types_error() {
151+
let mut parser = Statement::new().unwrap();
152+
let result = parser.parse_and_execute("[intersection(createArray(1, 2), createObject('a', 1))]", &Context::new());
153+
assert!(result.is_err());
154+
}
155+
}

dsc_lib/src/functions/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub mod less_or_equals;
4040
pub mod format;
4141
pub mod int;
4242
pub mod index_of;
43+
pub mod intersection;
4344
pub mod join;
4445
pub mod last_index_of;
4546
pub mod max;
@@ -155,6 +156,7 @@ impl FunctionDispatcher {
155156
Box::new(format::Format{}),
156157
Box::new(int::Int{}),
157158
Box::new(index_of::IndexOf{}),
159+
Box::new(intersection::Intersection{}),
158160
Box::new(join::Join{}),
159161
Box::new(last_index_of::LastIndexOf{}),
160162
Box::new(max::Max{}),

0 commit comments

Comments
 (0)