-
Notifications
You must be signed in to change notification settings - Fork 55
Add lambda expression and map() + filter() functions with ARM syntax
#1238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 22 commits
dd9d3d3
fed30d4
9b05728
97cf256
acc8181
3e019fe
a81882c
b180986
fc04a3b
00f9701
3176d5b
a93f9e2
3139835
0869cf0
4d831b1
fb353f4
e9df293
af25cc2
7a1de83
9057146
7c3c6b6
f5a902d
c3d6d77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| # 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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // 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::Value; | ||
|
|
||
|
|
||
| /// The lambda() function is special - it's not meant to be invoked directly | ||
| /// through the normal function dispatcher path. Instead, it's caught in the | ||
| /// Function::invoke method and handled specially via invoke_lambda(). | ||
| /// | ||
| /// This struct exists for metadata purposes and to signal errors if someone | ||
| /// tries to invoke lambda() as a regular function (which shouldn't happen). | ||
| #[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: 2, | ||
| max_args: 10, // Up to 9 parameters + 1 body | ||
| accepted_arg_ordered_types: vec![], | ||
| remaining_arg_accepted_types: None, | ||
| return_types: vec![FunctionArgKind::Object], // Lambda is represented as a special object | ||
| } | ||
| } | ||
|
|
||
| fn invoke(&self, _args: &[Value], _context: &Context) -> Result<Value, DscError> { | ||
| // This should never be called - lambda() is handled specially in Function::invoke | ||
| Err(DscError::Parser(t!("functions.lambda.cannotInvokeDirectly").to_string())) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| // 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::Value; | ||
| use tracing::debug; | ||
|
|
||
| #[derive(Debug, Default)] | ||
| pub struct LambdaVariables {} | ||
|
|
||
| impl Function for LambdaVariables { | ||
| fn get_metadata(&self) -> FunctionMetadata { | ||
| FunctionMetadata { | ||
| name: "lambdaVariables".to_string(), | ||
| description: t!("functions.lambdaVariables.description").to_string(), | ||
| category: vec![FunctionCategory::Lambda], | ||
| min_args: 1, | ||
| max_args: 1, | ||
| accepted_arg_ordered_types: vec![vec![FunctionArgKind::String]], | ||
| remaining_arg_accepted_types: None, | ||
| return_types: vec![ | ||
| FunctionArgKind::String, | ||
| FunctionArgKind::Number, | ||
| FunctionArgKind::Boolean, | ||
| FunctionArgKind::Array, | ||
| FunctionArgKind::Object, | ||
| FunctionArgKind::Null, | ||
| ], | ||
| } | ||
| } | ||
|
|
||
| fn invoke(&self, args: &[Value], context: &Context) -> Result<Value, DscError> { | ||
| debug!("{}", t!("functions.lambdaVariables.invoked")); | ||
|
|
||
| let var_name = args[0].as_str().unwrap(); | ||
|
|
||
| // Look up the variable in the lambda context | ||
| if let Some(value) = context.lambda_variables.get(var_name) { | ||
| Ok(value.clone()) | ||
| } else { | ||
| Err(DscError::Parser(t!("functions.lambdaVariables.notFound", name = var_name).to_string())) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use serde_json::json; | ||
|
|
||
| #[test] | ||
| fn lookup_existing_variable() { | ||
| let mut context = Context::new(); | ||
| context.lambda_variables.insert("x".to_string(), json!(42)); | ||
|
|
||
| let func = LambdaVariables {}; | ||
| let result = func.invoke(&[Value::String("x".to_string())], &context).unwrap(); | ||
| assert_eq!(result, json!(42)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn lookup_nonexistent_variable() { | ||
| let context = Context::new(); | ||
| let func = LambdaVariables {}; | ||
| let result = func.invoke(&[Value::String("x".to_string())], &context); | ||
| assert!(result.is_err()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| use crate::DscError; | ||
| use crate::configure::context::Context; | ||
| use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata, FunctionDispatcher}; | ||
| use rust_i18n::t; | ||
| use serde_json::Value; | ||
| use tracing::debug; | ||
|
|
||
| #[derive(Debug, Default)] | ||
| pub struct Map {} | ||
|
|
||
| impl Function for Map { | ||
| fn get_metadata(&self) -> FunctionMetadata { | ||
| FunctionMetadata { | ||
| name: "map".to_string(), | ||
| description: t!("functions.map.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.map.invoked")); | ||
|
|
||
| let array = args[0].as_array().unwrap(); | ||
| let lambda_id = args[1].as_str().unwrap(); | ||
|
|
||
| // Retrieve the lambda from context | ||
| let lambdas = context.lambdas.borrow(); | ||
| let Some(lambda) = lambdas.get(lambda_id) else { | ||
| return Err(DscError::Parser(t!("functions.map.lambdaNotFound", id = lambda_id).to_string())); | ||
| }; | ||
|
|
||
| // Validate parameter count (1 or 2 parameters) | ||
| if lambda.parameters.is_empty() || lambda.parameters.len() > 2 { | ||
| return Err(DscError::Parser(t!("functions.map.lambdaMustHave1Or2Params").to_string())); | ||
| } | ||
|
|
||
| // Create function dispatcher for evaluating lambda body | ||
| let dispatcher = FunctionDispatcher::new(); | ||
| let mut result_array = Vec::new(); | ||
|
|
||
| // Iterate through array and evaluate lambda for each element | ||
| for (index, element) in array.iter().enumerate() { | ||
| // Create a new context with lambda variables bound | ||
| let mut lambda_context = context.clone(); | ||
Gijsreyn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Bind first parameter to array element | ||
| lambda_context.lambda_variables.insert( | ||
| lambda.parameters[0].clone(), | ||
| element.clone() | ||
| ); | ||
|
|
||
| // Bind second parameter to index if provided | ||
| if lambda.parameters.len() == 2 { | ||
| lambda_context.lambda_variables.insert( | ||
| lambda.parameters[1].clone(), | ||
| Value::Number(serde_json::Number::from(index)) | ||
| ); | ||
| } | ||
|
|
||
| // Evaluate lambda body with bound variables | ||
| let result = lambda.body.invoke(&dispatcher, &lambda_context)?; | ||
| result_array.push(result); | ||
| } | ||
|
||
|
|
||
| Ok(Value::Array(result_array)) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.