Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dd9d3d3
Initial setup
Gijsreyn Nov 5, 2025
fed30d4
Fix comments
Gijsreyn Nov 5, 2025
9b05728
Re-add localization
Gijsreyn Nov 5, 2025
97cf256
Merge branch 'main' of https://github.com/Gijsreyn/operation-methods …
Gijsreyn Nov 14, 2025
acc8181
Remove dead code and add instructions
Gijsreyn Nov 14, 2025
3e019fe
Initial setup
Gijsreyn Nov 5, 2025
a81882c
Fix comments
Gijsreyn Nov 5, 2025
b180986
Re-add localization
Gijsreyn Nov 5, 2025
fc04a3b
Merge branch 'gh-57/main/add-map-lambda-function' of https://github.c…
Gijsreyn Nov 14, 2025
00f9701
Merge branch 'main' into gh-57/main/add-map-lambda-function
Gijsreyn Nov 15, 2025
3176d5b
Remove conflict
Gijsreyn Nov 15, 2025
a93f9e2
Fix test to look at output
Gijsreyn Nov 15, 2025
3139835
Merge branch 'main' into gh-57/main/add-map-lambda-function
Gijsreyn Dec 2, 2025
0869cf0
Add lambda expression support for DSC function expressions
Gijsreyn Dec 2, 2025
4d831b1
Merge branch 'main' into gh-57/main/add-map-lambda-function
Gijsreyn Dec 12, 2025
fb353f4
Add support for Lambda function arguments in the function dispatcher
Gijsreyn Dec 12, 2025
e9df293
Merge branch 'main' into gh-57/main/add-map-lambda-function
Gijsreyn Dec 13, 2025
af25cc2
Refactor argument handling
Gijsreyn Dec 13, 2025
7a1de83
Remove test
Gijsreyn Dec 13, 2025
9057146
Remove unused localization strings
Gijsreyn Dec 13, 2025
7c3c6b6
Add Lambda process mode and update function invocation logic
Gijsreyn Dec 15, 2025
f5a902d
Merge branch 'main' into gh-57/main/add-map-lambda-function
Gijsreyn Dec 16, 2025
c3d6d77
Implement filter function with lambda support and update related meta…
Gijsreyn Dec 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ fn list_functions(functions: &FunctionDispatcher, function_name: Option<&String>
let returned_types= [
(FunctionArgKind::Array, "a"),
(FunctionArgKind::Boolean, "b"),
(FunctionArgKind::Lambda, "l"),
(FunctionArgKind::Number, "n"),
(FunctionArgKind::String, "s"),
(FunctionArgKind::Object, "o"),
Expand Down
134 changes: 134 additions & 0 deletions dsc/tests/dsc_lambda.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'map() function with lambda tests' {
It 'map with simple lambda multiplies each element by 2' {
$config_yaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
numbers:
type: array
defaultValue: [1, 2, 3]
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[map(parameters('numbers'), lambda('x', mul(lambdaVariables('x'), 2)))]"
'@
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output | Should -Be @(2,4,6)
}

It 'map with lambda using index parameter' {
$config_yaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
items:
type: array
defaultValue: [10, 20, 30]
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[map(parameters('items'), lambda('val', 'i', add(lambdaVariables('val'), lambdaVariables('i'))))]"
'@
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output | Should -Be @(10,21,32)
}

It 'map with range generates array' {
$config_yaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[map(range(0, 3), lambda('x', mul(lambdaVariables('x'), 3)))]"
'@
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output | Should -Be @(0,3,6)
}

It 'map returns empty array for empty input' {
$config_yaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[map(createArray(), lambda('x', mul(lambdaVariables('x'), 2)))]"
'@
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output.Count | Should -Be 0
}
}

Describe 'filter() function with lambda tests' {
It 'filter with simple lambda filters elements greater than 2' {
$config_yaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
numbers:
type: array
defaultValue: [1, 2, 3, 4, 5]
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[filter(parameters('numbers'), lambda('x', greater(lambdaVariables('x'), 2)))]"
'@
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output | Should -Be @(3,4,5)
}

It 'filter with lambda using index parameter' {
$config_yaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
items:
type: array
defaultValue: [10, 20, 30, 40]
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[filter(parameters('items'), lambda('val', 'i', less(lambdaVariables('i'), 2)))]"
'@
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output | Should -Be @(10,20)
}

It 'filter returns empty array when no elements match' {
$config_yaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[filter(createArray(1, 2, 3), lambda('x', greater(lambdaVariables('x'), 10)))]"
'@
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output.Count | Should -Be 0
}

It 'filter returns all elements when all match' {
$config_yaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[filter(createArray(5, 6, 7), lambda('x', greater(lambdaVariables('x'), 2)))]"
'@
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output | Should -Be @(5,6,7)
}
}
25 changes: 25 additions & 0 deletions lib/dsc-lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,12 @@ argCountRequired = "Function '%{name}' requires between %{min_args} and %{max_ar
noArrayArgs = "Function '%{name}' does not accept array arguments, accepted types are: %{accepted_args_string}"
noBooleanArgs = "Function '%{name}' does not accept boolean arguments, accepted types are: %{accepted_args_string}"
noNumberArgs = "Function '%{name}' does not accept number arguments, accepted types are: %{accepted_args_string}"
noLambdaArgs = "Function '%{name}' does not accept lambda arguments, accepted types are: %{accepted_args_string}"
noNullArgs = "Function '%{name}' does not accept null arguments, accepted types are: %{accepted_args_string}"
noObjectArgs = "Function '%{name}' does not accept object arguments, accepted types are: %{accepted_args_string}"
noStringArgs = "Function '%{name}' does not accept string arguments, accepted types are: %{accepted_args_string}"
lambdaNotFound = "Function '%{name}' could not find lambda with ID '%{id}'"
lambdaTooManyParams = "Function '%{name}' requires lambda with 1 or 2 parameters (element and optional index)"

[functions.add]
description = "Adds two or more numbers together"
Expand Down Expand Up @@ -344,6 +347,11 @@ description = "Evaluates if the two values are the same"
description = "Returns the boolean value false"
invoked = "false function"

[functions.filter]
description = "Filters an array with a custom filtering function"
invoked = "filter function"
lambdaMustReturnBool = "filter() lambda must return a boolean value"

[functions.first]
description = "Returns the first element of an array or first character of a string"
invoked = "first function"
Expand Down Expand Up @@ -409,11 +417,27 @@ invalidObjectElement = "Array elements cannot be objects"
description = "Converts a valid JSON string into a JSON data type"
invalidJson = "Invalid JSON string"

[functions.lambda]
description = "Creates a lambda function with parameters and a body expression"
invoked = "lambda function"
requiresParamAndBody = "lambda() requires at least one parameter name and a body expression"
paramsMustBeStrings = "lambda() parameter names must be string literals"
bodyMustBeExpression = "lambda() body must be an expression"

[functions.lambdaVariables]
description = "Retrieves the value of a lambda parameter"
invoked = "lambdaVariables function"
notFound = "Lambda parameter '%{name}' not found in current context"

[functions.lastIndexOf]
description = "Returns the index of the last occurrence of an item in an array"
invoked = "lastIndexOf function"
invalidArrayArg = "First argument must be an array"

[functions.map]
description = "Transforms an array by applying a lambda function to each element"
invoked = "map function"

[functions.length]
description = "Returns the length of a string, array, or object"
invoked = "length function"
Expand Down Expand Up @@ -683,6 +707,7 @@ functionName = "Function name: '%{name}'"
argIsExpression = "Argument is an expression"
argIsValue = "Argument is a value: '%{value}'"
unknownArgType = "Unknown argument type '%{kind}'"
unexpectedLambda = "Lambda expressions cannot be used as function arguments directly. Use the lambda() function to create a lambda expression."

[parser]
parsingStatement = "Parsing statement: %{statement}"
Expand Down
7 changes: 7 additions & 0 deletions lib/dsc-lib/src/configure/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use super::config_doc::{DataType, RestartRequired, SecurityContextKind};
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ProcessMode {
Copy,
Lambda,
Normal,
NoExpressionEvaluation,
ParametersDefault,
Expand All @@ -25,6 +26,9 @@ pub struct Context {
pub dsc_version: Option<String>,
pub execution_type: ExecutionKind,
pub extensions: Vec<DscExtension>,
pub lambda_raw_args: std::cell::RefCell<Option<Vec<crate::parser::functions::FunctionArg>>>,
pub lambda_variables: HashMap<String, Value>,
pub lambdas: std::cell::RefCell<HashMap<String, crate::parser::functions::Lambda>>,
pub outputs: Map<String, Value>,
pub parameters: HashMap<String, (Value, DataType)>,
pub process_expressions: bool,
Expand All @@ -49,6 +53,9 @@ impl Context {
dsc_version: None,
execution_type: ExecutionKind::Actual,
extensions: Vec::new(),
lambda_raw_args: std::cell::RefCell::new(None),
lambda_variables: HashMap::new(),
lambdas: std::cell::RefCell::new(HashMap::new()),
outputs: Map::new(),
parameters: HashMap::new(),
process_expressions: true,
Expand Down
54 changes: 54 additions & 0 deletions lib/dsc-lib/src/functions/filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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 crate::functions::lambda_helpers::{get_lambda, apply_lambda_to_array};
use rust_i18n::t;
use serde_json::Value;
use tracing::debug;

#[derive(Debug, Default)]
pub struct Filter {}

impl Function for Filter {
fn get_metadata(&self) -> FunctionMetadata {
FunctionMetadata {
name: "filter".to_string(),
description: t!("functions.filter.description").to_string(),
category: vec![FunctionCategory::Array, FunctionCategory::Lambda],
min_args: 2,
max_args: 2,
accepted_arg_ordered_types: vec![
vec![FunctionArgKind::Array],
vec![FunctionArgKind::Lambda],
],
remaining_arg_accepted_types: None,
return_types: vec![FunctionArgKind::Array],
}
}

fn invoke(&self, args: &[Value], context: &Context) -> Result<Value, DscError> {
debug!("{}", t!("functions.filter.invoked"));

let array = args[0].as_array().unwrap();
let lambda_id = args[1].as_str().unwrap();

let lambdas = get_lambda(context, lambda_id, "filter")?;
let lambda = lambdas.get(lambda_id).unwrap();

let result_array = apply_lambda_to_array(array, lambda, context, |result, element| {
let Some(include) = result.as_bool() else {
return Err(DscError::Parser(t!("functions.filter.lambdaMustReturnBool").to_string()));
};
if include {
Ok(Some(element.clone()))
} else {
Ok(None)
}
})?;

Ok(Value::Array(result_array))
}
}
60 changes: 60 additions & 0 deletions lib/dsc-lib/src/functions/lambda.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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 crate::parser::functions::{FunctionArg, Lambda};
use rust_i18n::t;
use serde_json::Value;
use tracing::debug;
use uuid::Uuid;

#[derive(Debug, Default)]
pub struct LambdaFn {}

impl Function for LambdaFn {
fn get_metadata(&self) -> FunctionMetadata {
FunctionMetadata {
name: "lambda".to_string(),
description: t!("functions.lambda.description").to_string(),
category: vec![FunctionCategory::Lambda],
min_args: 0, // Args come through context.lambda_raw_args
max_args: 0,
accepted_arg_ordered_types: vec![],
remaining_arg_accepted_types: None,
return_types: vec![FunctionArgKind::Lambda],
}
}

fn invoke(&self, _args: &[Value], context: &Context) -> Result<Value, DscError> {
debug!("{}", t!("functions.lambda.invoked"));

let raw_args = context.lambda_raw_args.borrow();
let args = raw_args.as_ref()
.filter(|a| a.len() >= 2)
.ok_or_else(|| DscError::Parser(t!("functions.lambda.requiresParamAndBody").to_string()))?;

let (body_arg, param_args) = args.split_last().unwrap(); // safe: len >= 2

let parameters: Vec<String> = param_args.iter()
.map(|arg| match arg {
FunctionArg::Value(Value::String(s)) => Ok(s.clone()),
_ => Err(DscError::Parser(t!("functions.lambda.paramsMustBeStrings").to_string())),
})
.collect::<Result<_, _>>()?;

// Extract body expression
let body = match body_arg {
FunctionArg::Expression(expr) => expr.clone(),
_ => return Err(DscError::Parser(t!("functions.lambda.bodyMustBeExpression").to_string())),
};

// Create Lambda and store in Context with unique ID
let lambda = Lambda { parameters, body };
let lambda_id = format!("__lambda_{}", Uuid::new_v4());
context.lambdas.borrow_mut().insert(lambda_id.clone(), lambda);

Ok(Value::String(lambda_id))
}
}
Loading