Skip to content

Commit dd9d3d3

Browse files
committed
Initial setup
1 parent 007090d commit dd9d3d3

File tree

8 files changed

+387
-0
lines changed

8 files changed

+387
-0
lines changed

dsc/tests/dsc_map_lambda.tests.ps1

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'map() function with lambda tests' {
5+
It 'map with simple lambda multiplies each element by 2' {
6+
$config_yaml = @'
7+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
8+
parameters:
9+
numbers:
10+
type: array
11+
defaultValue: [1, 2, 3]
12+
resources:
13+
- name: Echo
14+
type: Microsoft.DSC.Debug/Echo
15+
properties:
16+
output: "[map(parameters('numbers'), lambda('x', mul(lambdaVariables('x'), 2)))]"
17+
'@
18+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
19+
$LASTEXITCODE | Should -Be 0
20+
$out.results[0].result.actualState.output | Should -Be @(2,4,6)
21+
}
22+
23+
It 'map with lambda using index parameter' {
24+
$config_yaml = @'
25+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
26+
parameters:
27+
items:
28+
type: array
29+
defaultValue: [10, 20, 30]
30+
resources:
31+
- name: Echo
32+
type: Microsoft.DSC.Debug/Echo
33+
properties:
34+
output: "[map(parameters('items'), lambda('val', 'i', add(lambdaVariables('val'), lambdaVariables('i'))))]"
35+
'@
36+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
37+
$LASTEXITCODE | Should -Be 0
38+
$out.results[0].result.actualState.output | Should -Be @(10,21,32)
39+
}
40+
41+
It 'map with range generates array' {
42+
$config_yaml = @'
43+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
44+
resources:
45+
- name: Echo
46+
type: Microsoft.DSC.Debug/Echo
47+
properties:
48+
output: "[map(range(0, 3), lambda('x', mul(lambdaVariables('x'), 3)))]"
49+
'@
50+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
51+
$LASTEXITCODE | Should -Be 0
52+
$out.results[0].result.actualState.output | Should -Be @(0,3,6)
53+
}
54+
55+
It 'map returns empty array for empty input' {
56+
$config_yaml = @'
57+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
58+
resources:
59+
- name: Echo
60+
type: Microsoft.DSC.Debug/Echo
61+
properties:
62+
output: "[map(createArray(), lambda('x', mul(lambdaVariables('x'), 2)))]"
63+
'@
64+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
65+
$LASTEXITCODE | Should -Be 0
66+
$out.results[0].result.actualState.output | Should -Be $null
67+
}
68+
}

lib/dsc-lib/locales/en-us.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,11 +401,33 @@ invalidObjectElement = "Array elements cannot be objects"
401401
description = "Converts a valid JSON string into a JSON data type"
402402
invalidJson = "Invalid JSON string"
403403

404+
[functions.lambda]
405+
description = "Creates a lambda function with parameters and a body expression"
406+
cannotInvokeDirectly = "lambda() should not be invoked directly"
407+
requiresArgs = "lambda() requires at least 2 arguments"
408+
requiresParamAndBody = "lambda() requires at least one parameter name and a body expression"
409+
paramsMustBeStrings = "lambda() parameter names must be string literals"
410+
bodyMustBeExpression = "lambda() body must be an expression"
411+
412+
[functions.lambdaVariables]
413+
description = "Retrieves the value of a lambda parameter"
414+
invoked = "lambdaVariables function"
415+
paramNameMustBeString = "lambdaVariables() parameter name must be a string"
416+
notFound = "Lambda parameter '%{name}' not found in current context"
417+
404418
[functions.lastIndexOf]
405419
description = "Returns the index of the last occurrence of an item in an array"
406420
invoked = "lastIndexOf function"
407421
invalidArrayArg = "First argument must be an array"
408422

423+
[functions.map]
424+
description = "Transforms an array by applying a lambda function to each element"
425+
invoked = "map function"
426+
firstArgMustBeArray = "map() first argument must be an array"
427+
secondArgMustBeLambda = "map() second argument must be a lambda function"
428+
lambdaNotFound = "Lambda function with ID '%{id}' not found"
429+
lambdaMustHave1Or2Params = "map() lambda must have 1 or 2 parameters (element and optional index)"
430+
409431
[functions.length]
410432
description = "Returns the length of a string, array, or object"
411433
invoked = "length function"

lib/dsc-lib/src/configure/context.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ pub struct Context {
2525
pub dsc_version: Option<String>,
2626
pub execution_type: ExecutionKind,
2727
pub extensions: Vec<DscExtension>,
28+
pub lambda_variables: HashMap<String, Value>,
29+
pub lambdas: std::cell::RefCell<HashMap<String, crate::parser::functions::Lambda>>,
2830
pub outputs: Map<String, Value>,
2931
pub parameters: HashMap<String, (Value, DataType)>,
3032
pub process_expressions: bool,
@@ -48,6 +50,8 @@ impl Context {
4850
dsc_version: None,
4951
execution_type: ExecutionKind::Actual,
5052
extensions: Vec::new(),
53+
lambda_variables: HashMap::new(),
54+
lambdas: std::cell::RefCell::new(HashMap::new()),
5155
outputs: Map::new(),
5256
parameters: HashMap::new(),
5357
process_expressions: true,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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::Value;
9+
10+
11+
/// The lambda() function is special - it's not meant to be invoked directly
12+
/// through the normal function dispatcher path. Instead, it's caught in the
13+
/// Function::invoke method and handled specially via invoke_lambda().
14+
///
15+
/// This struct exists for metadata purposes and to signal errors if someone
16+
/// tries to invoke lambda() as a regular function (which shouldn't happen).
17+
#[derive(Debug, Default)]
18+
pub struct LambdaFn {}
19+
20+
impl Function for LambdaFn {
21+
fn get_metadata(&self) -> FunctionMetadata {
22+
FunctionMetadata {
23+
name: "lambda".to_string(),
24+
description: t!("functions.lambda.description").to_string(),
25+
category: vec![FunctionCategory::Lambda],
26+
min_args: 2,
27+
max_args: 10, // Up to 9 parameters + 1 body
28+
accepted_arg_ordered_types: vec![],
29+
remaining_arg_accepted_types: None,
30+
return_types: vec![FunctionArgKind::Object], // Lambda is represented as a special object
31+
}
32+
}
33+
34+
fn invoke(&self, _args: &[Value], _context: &Context) -> Result<Value, DscError> {
35+
// This should never be called - lambda() is handled specially in Function::invoke
36+
Err(DscError::Parser(t!("functions.lambda.cannotInvokeDirectly").to_string()))
37+
}
38+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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::Value;
9+
use tracing::debug;
10+
11+
#[derive(Debug, Default)]
12+
pub struct LambdaVariables {}
13+
14+
impl Function for LambdaVariables {
15+
fn get_metadata(&self) -> FunctionMetadata {
16+
FunctionMetadata {
17+
name: "lambdaVariables".to_string(),
18+
description: t!("functions.lambdaVariables.description").to_string(),
19+
category: vec![FunctionCategory::Lambda],
20+
min_args: 1,
21+
max_args: 1,
22+
accepted_arg_ordered_types: vec![vec![FunctionArgKind::String]],
23+
remaining_arg_accepted_types: None,
24+
return_types: vec![
25+
FunctionArgKind::String,
26+
FunctionArgKind::Number,
27+
FunctionArgKind::Boolean,
28+
FunctionArgKind::Array,
29+
FunctionArgKind::Object,
30+
FunctionArgKind::Null,
31+
],
32+
}
33+
}
34+
35+
fn invoke(&self, args: &[Value], context: &Context) -> Result<Value, DscError> {
36+
debug!("{}", t!("functions.lambdaVariables.invoked"));
37+
38+
if args.len() != 1 {
39+
return Err(DscError::Parser(t!("functions.invalidArgCount", name = "lambdaVariables", count = 1).to_string()));
40+
}
41+
42+
let Some(var_name) = args[0].as_str() else {
43+
return Err(DscError::Parser(t!("functions.lambdaVariables.paramNameMustBeString").to_string()));
44+
};
45+
46+
// Look up the variable in the lambda context
47+
if let Some(value) = context.lambda_variables.get(var_name) {
48+
Ok(value.clone())
49+
} else {
50+
Err(DscError::Parser(t!("functions.lambdaVariables.notFound", name = var_name).to_string()))
51+
}
52+
}
53+
}
54+
55+
#[cfg(test)]
56+
mod tests {
57+
use super::*;
58+
use serde_json::json;
59+
60+
#[test]
61+
fn lookup_existing_variable() {
62+
let mut context = Context::new();
63+
context.lambda_variables.insert("x".to_string(), json!(42));
64+
65+
let func = LambdaVariables {};
66+
let result = func.invoke(&[Value::String("x".to_string())], &context).unwrap();
67+
assert_eq!(result, json!(42));
68+
}
69+
70+
#[test]
71+
fn lookup_nonexistent_variable() {
72+
let context = Context::new();
73+
let func = LambdaVariables {};
74+
let result = func.invoke(&[Value::String("x".to_string())], &context);
75+
assert!(result.is_err());
76+
}
77+
}

lib/dsc-lib/src/functions/map.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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, FunctionDispatcher};
7+
use rust_i18n::t;
8+
use serde_json::Value;
9+
use tracing::debug;
10+
11+
#[derive(Debug, Default)]
12+
pub struct Map {}
13+
14+
impl Function for Map {
15+
fn get_metadata(&self) -> FunctionMetadata {
16+
FunctionMetadata {
17+
name: "map".to_string(),
18+
description: t!("functions.map.description").to_string(),
19+
category: vec![FunctionCategory::Array, FunctionCategory::Lambda],
20+
min_args: 2,
21+
max_args: 2,
22+
accepted_arg_ordered_types: vec![
23+
vec![FunctionArgKind::Array],
24+
vec![FunctionArgKind::String], // Lambda ID as string
25+
],
26+
remaining_arg_accepted_types: None,
27+
return_types: vec![FunctionArgKind::Array],
28+
}
29+
}
30+
31+
fn invoke(&self, args: &[Value], context: &Context) -> Result<Value, DscError> {
32+
debug!("{}", t!("functions.map.invoked"));
33+
34+
if args.len() != 2 {
35+
return Err(DscError::Parser(t!("functions.invalidArgCount", name = "map", count = 2).to_string()));
36+
}
37+
38+
let Some(array) = args[0].as_array() else {
39+
return Err(DscError::Parser(t!("functions.map.firstArgMustBeArray").to_string()));
40+
};
41+
42+
let Some(lambda_id) = args[1].as_str() else {
43+
return Err(DscError::Parser(t!("functions.map.secondArgMustBeLambda").to_string()));
44+
};
45+
46+
// Retrieve the lambda from context
47+
let lambdas = context.lambdas.borrow();
48+
let Some(lambda) = lambdas.get(lambda_id) else {
49+
return Err(DscError::Parser(t!("functions.map.lambdaNotFound", id = lambda_id).to_string()));
50+
};
51+
52+
// Validate parameter count (1 or 2 parameters)
53+
if lambda.parameters.is_empty() || lambda.parameters.len() > 2 {
54+
return Err(DscError::Parser(t!("functions.map.lambdaMustHave1Or2Params").to_string()));
55+
}
56+
57+
// Create function dispatcher for evaluating lambda body
58+
let dispatcher = FunctionDispatcher::new();
59+
let mut result_array = Vec::new();
60+
61+
// Iterate through array and evaluate lambda for each element
62+
for (index, element) in array.iter().enumerate() {
63+
// Create a new context with lambda variables bound
64+
let mut lambda_context = context.clone();
65+
66+
// Bind first parameter to array element
67+
lambda_context.lambda_variables.insert(
68+
lambda.parameters[0].clone(),
69+
element.clone()
70+
);
71+
72+
// Bind second parameter to index if provided
73+
if lambda.parameters.len() == 2 {
74+
lambda_context.lambda_variables.insert(
75+
lambda.parameters[1].clone(),
76+
Value::Number(serde_json::Number::from(index))
77+
);
78+
}
79+
80+
// Evaluate lambda body with bound variables
81+
let result = lambda.body.invoke(&dispatcher, &lambda_context)?;
82+
result_array.push(result);
83+
}
84+
85+
Ok(Value::Array(result_array))
86+
}
87+
}
88+
89+
#[cfg(test)]
90+
mod tests {
91+
use super::*;
92+
93+
#[test]
94+
fn requires_two_args() {
95+
let func = Map {};
96+
let result = func.invoke(&[], &Context::new());
97+
assert!(result.is_err());
98+
}
99+
}

0 commit comments

Comments
 (0)