diff --git a/dsc/Cargo.toml b/dsc/Cargo.toml index 7426b268..37ed68b0 100644 --- a/dsc/Cargo.toml +++ b/dsc/Cargo.toml @@ -24,6 +24,6 @@ serde_json = { version = "1.0", features = ["preserve_order"] } serde_yaml = { version = "0.9.3" } syntect = { version = "5.0", features = ["default-fancy"], default-features = false } sysinfo = { version = "0.29.10" } -thiserror = "1.0" +thiserror = "1.0.52" tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17", features = ["ansi", "env-filter", "json"] } diff --git a/dsc/tests/dsc_config_get.tests.ps1 b/dsc/tests/dsc_config_get.tests.ps1 index 2bf907c9..b2604193 100644 --- a/dsc/tests/dsc_config_get.tests.ps1 +++ b/dsc/tests/dsc_config_get.tests.ps1 @@ -38,7 +38,7 @@ Describe 'dsc config get tests' { - name: Echo type: Test/Echo properties: - text: hello + output: hello "@ $null = $config_yaml | dsc config get --format pretty-json | Out-String $LASTEXITCODE | Should -Be 0 diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index c4c6b33b..a305575d 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -5,7 +5,7 @@ Describe 'tests for function expressions' { It 'function works: ' -TestCases @( @{ text = "[concat('a', 'b')]"; expected = 'ab' } @{ text = "[concat('a', 'b', 'c')]"; expected = 'abc' } - @{ text = "[concat('a', 1, concat(2, 'b'))]"; expected = 'a12b' } + @{ text = "[concat('a', concat('b', 'c'))]"; expected = 'abc' } @{ text = "[base64('ab')]"; expected = 'YWI=' } @{ text = "[base64(concat('a','b'))]"; expected = 'YWI=' } @{ text = "[base64(base64(concat('a','b')))]"; expected = 'WVdJPQ==' } @@ -19,9 +19,9 @@ Describe 'tests for function expressions' { - name: Echo type: Test/Echo properties: - text: '$escapedText' + output: '$escapedText' "@ $out = $config_yaml | dsc config get | ConvertFrom-Json - $out.results[0].result.actualState.text | Should -Be $expected + $out.results[0].result.actualState.output | Should -Be $expected } } diff --git a/dsc/tests/dsc_parameters.tests.ps1 b/dsc/tests/dsc_parameters.tests.ps1 index 29829aac..f08b5fae 100644 --- a/dsc/tests/dsc_parameters.tests.ps1 +++ b/dsc/tests/dsc_parameters.tests.ps1 @@ -17,7 +17,7 @@ Describe 'Parameters tests' { - name: Echo type: Test/Echo properties: - text: '[parameters(''param1'')]' + output: '[parameters(''param1'')]' "@ $params_json = @{ parameters = @{ param1 = 'hello' }} | ConvertTo-Json @@ -31,16 +31,16 @@ Describe 'Parameters tests' { } $LASTEXITCODE | Should -Be 0 - $out.results[0].result.actualState.text | Should -BeExactly 'hello' + $out.results[0].result.actualState.output | Should -BeExactly 'hello' } It 'Input is ' -TestCases @( - @{ type = 'string'; value = 'hello'; expected = 'hello' } - @{ type = 'int'; value = 42; expected = 42 } - @{ type = 'bool'; value = $true; expected = $true } - @{ type = 'array'; value = @('hello', 'world'); expected = '["hello","world"]' } + @{ type = 'string'; value = 'hello' } + @{ type = 'int'; value = 42} + @{ type = 'bool'; value = $true} + @{ type = 'array'; value = @('hello', 'world')} ) { - param($type, $value, $expected) + param($type, $value) $config_yaml = @" `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json @@ -51,13 +51,13 @@ Describe 'Parameters tests' { - name: Echo type: Test/Echo properties: - text: '[parameters(''param1'')]' + output: '[parameters(''param1'')]' "@ $params_json = @{ parameters = @{ param1 = $value }} | ConvertTo-Json $out = $config_yaml | dsc config -p $params_json get | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - $out.results[0].result.actualState.text | Should -BeExactly $expected + $out.results[0].result.actualState.output | Should -BeExactly $value } It 'Input is incorrect type ' -TestCases @( @@ -77,7 +77,7 @@ Describe 'Parameters tests' { - name: Echo type: Test/Echo properties: - text: '[parameters(''param1'')]' + output: '[parameters(''param1'')]' "@ $params_json = @{ parameters = @{ param1 = $value }} | ConvertTo-Json @@ -104,7 +104,7 @@ Describe 'Parameters tests' { - name: Echo type: Test/Echo properties: - text: '[parameters(''param1'')]' + output: '[parameters(''param1'')]' "@ $params_json = @{ parameters = @{ param1 = $value }} | ConvertTo-Json @@ -130,7 +130,7 @@ Describe 'Parameters tests' { - name: Echo type: Test/Echo properties: - text: '[parameters(''param1'')]' + output: '[parameters(''param1'')]' "@ $params_json = @{ parameters = @{ param1 = $value }} | ConvertTo-Json @@ -154,7 +154,7 @@ Describe 'Parameters tests' { - name: Echo type: Test/Echo properties: - text: '[parameters(''param1'')]' + output: '[parameters(''param1'')]' "@ $params_json = @{ parameters = @{ param1 = $value }} | ConvertTo-Json @@ -180,7 +180,7 @@ Describe 'Parameters tests' { - name: Echo type: Test/Echo properties: - text: '[parameters(''param1'')]' + output: '[parameters(''param1'')]' "@ $params_json = @{ parameters = @{ param1 = $value }} | ConvertTo-Json @@ -192,28 +192,43 @@ Describe 'Parameters tests' { $config_yaml = @" `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json parameters: - param1: + paramString: type: string defaultValue: 'hello' - param2: + paramInt: type: int defaultValue: 7 - param3: + paramBool: type: bool defaultValue: false - param4: + paramArray: type: array defaultValue: ['hello', 'world'] resources: - - name: Echo + - name: String + type: Test/Echo + properties: + output: '[parameters(''paramString'')]' + - name: Int + type: Test/Echo + properties: + output: '[parameters(''paramInt'')]' + - name: Bool + type: Test/Echo + properties: + output: '[parameters(''paramBool'')]' + - name: Array type: Test/Echo properties: - text: '[concat(parameters(''param1''),'','',parameters(''param2''),'','',parameters(''param3''),'','',parameters(''param4''))]' + output: '[parameters(''paramArray'')]' "@ $out = $config_yaml | dsc config get | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - $out.results[0].result.actualState.text | Should -BeExactly 'hello,7,false,["hello","world"]' + $out.results[0].result.actualState.output | Should -BeExactly 'hello' + $out.results[1].result.actualState.output | Should -BeExactly 7 + $out.results[2].result.actualState.output | Should -BeExactly $false + $out.results[3].result.actualState.output | Should -BeExactly @('hello', 'world') } It 'property value uses parameter value' { diff --git a/dsc/tests/dsc_test.tests.ps1 b/dsc/tests/dsc_test.tests.ps1 index da3e5149..09fdb72c 100644 --- a/dsc/tests/dsc_test.tests.ps1 +++ b/dsc/tests/dsc_test.tests.ps1 @@ -56,7 +56,7 @@ Describe 'config test tests' { } It 'can accept the use of --format as a subcommand' { - $null = "text: hello" | dsc resource test -r Test/Echo --format pretty-json + $null = "output: hello" | dsc resource test -r Test/Echo --format pretty-json $LASTEXITCODE | Should -Be 0 } } diff --git a/dsc_lib/src/configure/depends_on.rs b/dsc_lib/src/configure/depends_on.rs index 67c732cd..0d7e123e 100644 --- a/dsc_lib/src/configure/depends_on.rs +++ b/dsc_lib/src/configure/depends_on.rs @@ -33,7 +33,10 @@ pub fn get_resource_invocation_order(config: &Configuration, parser: &mut Statem if let Some(depends_on) = resource.depends_on.clone() { for dependency in depends_on { let statement = parser.parse_and_execute(&dependency, context)?; - let (resource_type, resource_name) = get_type_and_name(&statement)?; + let Some(string_result) = statement.as_str() else { + return Err(DscError::Validation(format!("'dependsOn' syntax is incorrect: {dependency}"))); + }; + let (resource_type, resource_name) = get_type_and_name(string_result)?; // find the resource by name let Some(dependency_resource) = config.resources.iter().find(|r| r.name.eq(resource_name)) else { @@ -64,7 +67,10 @@ pub fn get_resource_invocation_order(config: &Configuration, parser: &mut Statem let resource_index = order.iter().position(|r| r.name == resource.name && r.resource_type == resource.resource_type).ok_or(DscError::Validation("Resource not found in order".to_string()))?; for dependency in depends_on { let statement = parser.parse_and_execute(dependency, context)?; - let (resource_type, resource_name) = get_type_and_name(&statement)?; + let Some(string_result) = statement.as_str() else { + return Err(DscError::Validation(format!("'dependsOn' syntax is incorrect: {dependency}"))); + }; + let (resource_type, resource_name) = get_type_and_name(string_result)?; let dependency_index = order.iter().position(|r| r.name == resource_name && r.resource_type == resource_type).ok_or(DscError::Validation("Dependency not found in order".to_string()))?; if resource_index < dependency_index { return Err(DscError::Validation(format!("Circular dependency detected for resource named '{0}'", resource.name))); diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 6e953958..cb763684 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -14,7 +14,7 @@ use self::config_result::{ConfigurationGetResult, ConfigurationSetResult, Config use self::contraints::{check_length, check_number_limits, check_allowed_values}; use serde_json::{Map, Value}; use std::collections::{HashMap, HashSet}; -use tracing::debug; +use tracing::{debug, trace}; pub mod context; pub mod config_doc; @@ -73,6 +73,7 @@ pub fn add_resource_export_results_to_configuration(resource: &DscResource, prov // for values returned by resources, they may look like expressions, so we make sure to escape them in case // they are re-used to apply configuration fn escape_property_values(properties: &Map) -> Result>, DscError> { + debug!("Escape returned property values"); let mut result: Map = Map::new(); for (name, value) in properties { match value { @@ -400,6 +401,7 @@ impl Configurator { } fn invoke_property_expressions(&mut self, properties: &Option>) -> Result>, DscError> { + debug!("Invoke property expressions"); if properties.is_none() { return Ok(None); } @@ -407,6 +409,7 @@ impl Configurator { let mut result: Map = Map::new(); if let Some(properties) = properties { for (name, value) in properties { + trace!("Invoke property expression for {name}: {value}"); match value { Value::Object(object) => { let value = self.invoke_property_expressions(&Some(object.clone()))?; @@ -431,7 +434,10 @@ impl Configurator { return Err(DscError::Parser("Array element could not be transformed as string".to_string())); }; let statement_result = self.statement_parser.parse_and_execute(statement, &self.context)?; - result_array.push(Value::String(statement_result)); + let Some(string_result) = statement_result.as_str() else { + return Err(DscError::Parser("Array element could not be transformed as string".to_string())); + }; + result_array.push(Value::String(string_result.to_string())); } _ => { result_array.push(element.clone()); @@ -446,7 +452,11 @@ impl Configurator { return Err(DscError::Parser(format!("Property value '{value}' could not be transformed as string"))); }; let statement_result = self.statement_parser.parse_and_execute(statement, &self.context)?; - result.insert(name.clone(), Value::String(statement_result)); + if let Some(string_result) = statement_result.as_str() { + result.insert(name.clone(), Value::String(string_result.to_string())); + } else { + result.insert(name.clone(), statement_result); + }; }, _ => { result.insert(name.clone(), value.clone()); diff --git a/dsc_lib/src/functions/base64.rs b/dsc_lib/src/functions/base64.rs index 136f1cac..de5f3638 100644 --- a/dsc_lib/src/functions/base64.rs +++ b/dsc_lib/src/functions/base64.rs @@ -5,8 +5,9 @@ use base64::{Engine as _, engine::general_purpose}; use crate::DscError; use crate::configure::context::Context; -use crate::parser::functions::{FunctionArg, FunctionResult}; -use super::{Function, AcceptedArgKind}; +use crate::functions::AcceptedArgKind; +use serde_json::Value; +use super::Function; #[derive(Debug, Default)] pub struct Base64 {} @@ -24,11 +25,8 @@ impl Function for Base64 { 1 } - fn invoke(&self, args: &[FunctionArg], _context: &Context) -> Result { - let FunctionArg::String(arg) = args.first().unwrap() else { - return Err(DscError::Parser("Invalid argument type".to_string())); - }; - Ok(FunctionResult::String(general_purpose::STANDARD.encode(arg))) + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + Ok(Value::String(general_purpose::STANDARD.encode(args[0].as_str().unwrap_or_default()))) } } diff --git a/dsc_lib/src/functions/concat.rs b/dsc_lib/src/functions/concat.rs index c6ab2415..6c475bb2 100644 --- a/dsc_lib/src/functions/concat.rs +++ b/dsc_lib/src/functions/concat.rs @@ -3,7 +3,8 @@ use crate::DscError; use crate::configure::context::Context; -use crate::functions::{Function, FunctionArg, FunctionResult, AcceptedArgKind}; +use crate::functions::{AcceptedArgKind, Function}; +use serde_json::Value; use tracing::debug; #[derive(Debug, Default)] @@ -19,26 +20,59 @@ impl Function for Concat { } fn accepted_arg_types(&self) -> Vec { - vec![AcceptedArgKind::String, AcceptedArgKind::Integer] + vec![AcceptedArgKind::String, AcceptedArgKind::Array] } - fn invoke(&self, args: &[FunctionArg], _context: &Context) -> Result { - let mut result = String::new(); - for arg in args { - match arg { - FunctionArg::String(value) => { - result.push_str(value); - }, - FunctionArg::Integer(value) => { - result.push_str(&value.to_string()); - }, - _ => { - return Err(DscError::Parser("Invalid argument type".to_string())); + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("concat function"); + let mut string_result = String::new(); + let mut array_result: Vec = Vec::new(); + let mut input_type : Option = None; + for value in args { + if value.is_string() { + if input_type.is_none() { + input_type = Some(AcceptedArgKind::String); + } else if input_type != Some(AcceptedArgKind::String) { + return Err(DscError::Parser("Arguments must all be strings".to_string())); } + + string_result.push_str(value.as_str().unwrap_or_default()); + } else if value.is_array() { + if input_type.is_none() { + input_type = Some(AcceptedArgKind::Array); + } else if input_type != Some(AcceptedArgKind::Array) { + return Err(DscError::Parser("Arguments must all be arrays".to_string())); + } + + if let Some(array) = value.as_array() { + for arg in array { + if arg.is_string() { + if arg.as_str().is_some() { + array_result.push(arg.as_str().unwrap().to_string()); + } else { + array_result.push(String::new()); + } + } else { + return Err(DscError::Parser("Only arrays of strings are valid".to_string())); + } + } + } + } else { + return Err(DscError::Parser("Invalid argument type".to_string())); + } + } + + match input_type { + Some(AcceptedArgKind::String) => { + Ok(Value::String(string_result)) + }, + Some(AcceptedArgKind::Array) => { + Ok(Value::Array(array_result.into_iter().map(Value::String).collect())) + }, + _ => { + Err(DscError::Parser("Invalid argument type".to_string())) } } - debug!("concat result: {result}"); - Ok(FunctionResult::String(result)) } } @@ -62,17 +96,17 @@ mod tests { } #[test] - fn numbers() { + fn arrays() { let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[concat(1, 2)]", &Context::new()).unwrap(); - assert_eq!(result, "12"); + let result = parser.parse_and_execute("[concat(createArray('a','b'), createArray('c','d'))]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), r#"["a","b","c","d"]"#); } #[test] fn string_and_numbers() { let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[concat('a', 1, 'b', 2)]", &Context::new()).unwrap(); - assert_eq!(result, "a1b2"); + let result = parser.parse_and_execute("[concat('a', 1)]", &Context::new()); + assert!(result.is_err()); } #[test] @@ -88,4 +122,18 @@ mod tests { let result = parser.parse_and_execute("[concat('a')]", &Context::new()); assert!(result.is_err()); } + + #[test] + fn string_and_array() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[concat('a', createArray('b','c'))]", &Context::new()); + assert!(result.is_err()); + } + + #[test] + fn array_and_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[concat(createArray('a','b'), 'c')]", &Context::new()); + assert!(result.is_err()); + } } diff --git a/dsc_lib/src/functions/create_array.rs b/dsc_lib/src/functions/create_array.rs new file mode 100644 index 00000000..595baf3a --- /dev/null +++ b/dsc_lib/src/functions/create_array.rs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{AcceptedArgKind, Function}; +use serde_json::Value; +use tracing::debug; + +#[derive(Debug, Default)] +pub struct CreateArray {} + +impl Function for CreateArray { + fn min_args(&self) -> usize { + 0 + } + + fn max_args(&self) -> usize { + usize::MAX + } + + fn accepted_arg_types(&self) -> Vec { + vec![AcceptedArgKind::String, AcceptedArgKind::Number, AcceptedArgKind::Object, AcceptedArgKind::Array] + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("createArray function"); + let mut array_result = Vec::::new(); + let mut input_type : Option = None; + for value in args { + if value.is_array() { + if input_type.is_none() { + input_type = Some(AcceptedArgKind::Array); + } else if input_type != Some(AcceptedArgKind::Array) { + return Err(DscError::Parser("Arguments must all be arrays".to_string())); + } + } else if value.is_number() { + if input_type.is_none() { + input_type = Some(AcceptedArgKind::Number); + } else if input_type != Some(AcceptedArgKind::Number) { + return Err(DscError::Parser("Arguments must all be integers".to_string())); + } + } else if value.is_object() { + if input_type.is_none() { + input_type = Some(AcceptedArgKind::Object); + } else if input_type != Some(AcceptedArgKind::Object) { + return Err(DscError::Parser("Arguments must all be objects".to_string())); + } + } else if value.is_string() { + if input_type.is_none() { + input_type = Some(AcceptedArgKind::String); + } else if input_type != Some(AcceptedArgKind::String) { + return Err(DscError::Parser("Arguments must all be strings".to_string())); + } + } else { + return Err(DscError::Parser("Invalid argument type".to_string())); + } + array_result.push(value.clone()); + } + + Ok(Value::Array(array_result)) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn strings() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[createArray('a', 'b')]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), r#"["a","b"]"#); + } + + #[test] + fn integers() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[createArray(1,2,3)]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "[1,2,3]"); + } + + #[test] + fn arrays() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[createArray(createArray('a','b'), createArray('c','d'))]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), r#"[["a","b"],["c","d"]]"#); + } + + #[test] + fn objects() { + // TODO + } + + #[test] + fn mixed_types() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[createArray(1,'a')]", &Context::new()); + assert!(result.is_err()); + } + + #[test] + fn empty() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[createArray()]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "[]"); + } +} diff --git a/dsc_lib/src/functions/envvar.rs b/dsc_lib/src/functions/envvar.rs index d2610092..b0278da5 100644 --- a/dsc_lib/src/functions/envvar.rs +++ b/dsc_lib/src/functions/envvar.rs @@ -3,8 +3,9 @@ use crate::DscError; use crate::configure::context::Context; -use crate::parser::functions::{FunctionArg, FunctionResult}; -use super::{Function, AcceptedArgKind}; +use crate::functions::AcceptedArgKind; +use super::Function; +use serde_json::Value; use std::env; #[derive(Debug, Default)] @@ -23,12 +24,8 @@ impl Function for Envvar { 1 } - fn invoke(&self, args: &[FunctionArg], _context: &Context) -> Result { - let FunctionArg::String(arg) = args.first().unwrap() else { - return Err(DscError::Parser("Invalid argument type".to_string())); - }; - - let val = env::var(arg).unwrap_or_default(); - Ok(FunctionResult::String(val)) + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + let val = env::var(args[0].as_str().unwrap_or_default()).unwrap_or_default(); + Ok(Value::String(val)) } } diff --git a/dsc_lib/src/functions/mod.rs b/dsc_lib/src/functions/mod.rs index 80f5a9d1..400b8293 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -2,14 +2,14 @@ // Licensed under the MIT License. use std::collections::HashMap; -use tracing::debug; use crate::DscError; use crate::configure::context::Context; -use crate::parser::functions::{FunctionArg, FunctionResult}; +use serde_json::Value; pub mod base64; pub mod concat; +pub mod create_array; pub mod envvar; pub mod parameters; pub mod resource_id; @@ -17,8 +17,10 @@ pub mod resource_id; /// The kind of argument that a function accepts. #[derive(Debug, PartialEq)] pub enum AcceptedArgKind { + Array, Boolean, - Integer, + Number, + Object, String, } @@ -39,7 +41,7 @@ pub trait Function { /// # Errors /// /// This function will return an error if the function fails to execute. - fn invoke(&self, args: &[FunctionArg], context: &Context) -> Result; + fn invoke(&self, args: &[Value], context: &Context) -> Result; } /// A dispatcher for functions. @@ -54,6 +56,7 @@ impl FunctionDispatcher { let mut functions: HashMap> = HashMap::new(); functions.insert("base64".to_string(), Box::new(base64::Base64{})); functions.insert("concat".to_string(), Box::new(concat::Concat{})); + functions.insert("createArray".to_string(), Box::new(create_array::CreateArray{})); functions.insert("envvar".to_string(), Box::new(envvar::Envvar{})); functions.insert("parameters".to_string(), Box::new(parameters::Parameters{})); functions.insert("resourceId".to_string(), Box::new(resource_id::ResourceId{})); @@ -72,7 +75,7 @@ impl FunctionDispatcher { /// # Errors /// /// This function will return an error if the function fails to execute. - pub fn invoke(&self, name: &str, args: &Vec, context: &Context) -> Result { + pub fn invoke(&self, name: &str, args: &Vec, context: &Context) -> Result { let Some(function) = self.functions.get(name) else { return Err(DscError::Parser(format!("Unknown function '{name}'"))); }; @@ -96,27 +99,17 @@ impl FunctionDispatcher { // check if arg types are valid let accepted_arg_types = function.accepted_arg_types(); let accepted_args_string = accepted_arg_types.iter().map(|x| format!("{x:?}")).collect::>().join(", "); - for arg in args { - match arg { - FunctionArg::String(_) => { - if !accepted_arg_types.contains(&AcceptedArgKind::String) { - return Err(DscError::Parser(format!("Function '{name}' does not accept string argument, accepted types are: {accepted_args_string}"))); - } - }, - FunctionArg::Integer(_) => { - if !accepted_arg_types.contains(&AcceptedArgKind::Integer) { - return Err(DscError::Parser(format!("Function '{name}' does not accept integer arguments, accepted types are: {accepted_args_string}"))); - } - }, - FunctionArg::Boolean(_) => { - if !accepted_arg_types.contains(&AcceptedArgKind::Boolean) { - return Err(DscError::Parser(format!("Function '{name}' does not accept boolean arguments, accepted types are: {accepted_args_string}"))); - } - }, - FunctionArg::Expression(_) => { - debug!("An expression was not resolved before invoking a function"); - return Err(DscError::Parser("Error in parsing".to_string())); - } + for value in args { + if value.is_array() && !accepted_arg_types.contains(&AcceptedArgKind::Array) { + return Err(DscError::Parser(format!("Function '{name}' does not accept array arguments, accepted types are: {accepted_args_string}"))); + } else if value.is_boolean() && !accepted_arg_types.contains(&AcceptedArgKind::Boolean) { + return Err(DscError::Parser(format!("Function '{name}' does not accept boolean arguments, accepted types are: {accepted_args_string}"))); + } else if value.is_number() && !accepted_arg_types.contains(&AcceptedArgKind::Number) { + return Err(DscError::Parser(format!("Function '{name}' does not accept number arguments, accepted types are: {accepted_args_string}"))); + } else if value.is_object() && !accepted_arg_types.contains(&AcceptedArgKind::Object) { + return Err(DscError::Parser(format!("Function '{name}' does not accept object arguments, accepted types are: {accepted_args_string}"))); + } else if value.is_string() && !accepted_arg_types.contains(&AcceptedArgKind::String) { + return Err(DscError::Parser(format!("Function '{name}' does not accept string argument, accepted types are: {accepted_args_string}"))); } } diff --git a/dsc_lib/src/functions/parameters.rs b/dsc_lib/src/functions/parameters.rs index 210c5304..de01ffe0 100644 --- a/dsc_lib/src/functions/parameters.rs +++ b/dsc_lib/src/functions/parameters.rs @@ -3,8 +3,9 @@ use crate::DscError; use crate::configure::context::Context; -use crate::functions::{Function, FunctionArg, FunctionResult, AcceptedArgKind}; -use tracing::debug; +use crate::functions::{AcceptedArgKind, Function}; +use serde_json::Value; +use tracing::{debug, trace}; #[derive(Debug, Default)] pub struct Parameters {} @@ -22,24 +23,18 @@ impl Function for Parameters { vec![AcceptedArgKind::String] } - fn invoke(&self, args: &[FunctionArg], context: &Context) -> Result { - let FunctionArg::String(key) = &args[0] else { - return Err(DscError::Parser("Invalid argument type".to_string())); - }; - debug!("parameters key: {key}"); - if context.parameters.contains_key(key) { - let value = &context.parameters[key]; - // we have to check if it's a string as a to_string() will put the string in quotes as part of the value - if value.is_string() { - if let Some(value) = value.as_str() { - return Ok(FunctionResult::String(value.to_string())); - } + fn invoke(&self, args: &[Value], context: &Context) -> Result { + debug!("Invoke parameters function"); + if let Some(key) = args[0].as_str() { + trace!("parameters key: {key}"); + if context.parameters.contains_key(key) { + Ok(context.parameters[key].clone()) } - - Ok(FunctionResult::Object(context.parameters[key].clone())) - } - else { - Err(DscError::Parser(format!("Parameter '{key}' not found in context"))) + else { + Err(DscError::Parser(format!("Parameter '{key}' not found in context"))) + } + } else { + Err(DscError::Parser("Invalid argument type".to_string())) } } } diff --git a/dsc_lib/src/functions/resource_id.rs b/dsc_lib/src/functions/resource_id.rs index 4ddcf15c..f893f127 100644 --- a/dsc_lib/src/functions/resource_id.rs +++ b/dsc_lib/src/functions/resource_id.rs @@ -3,7 +3,8 @@ use crate::DscError; use crate::configure::context::Context; -use crate::functions::{Function, FunctionArg, FunctionResult, AcceptedArgKind}; +use crate::functions::{AcceptedArgKind, Function}; +use serde_json::Value; #[derive(Debug, Default)] pub struct ResourceId {} @@ -21,38 +22,34 @@ impl Function for ResourceId { vec![AcceptedArgKind::String] } - fn invoke(&self, args: &[FunctionArg], _context: &Context) -> Result { + fn invoke(&self, args: &[Value], _context: &Context) -> Result { let mut result = String::new(); // first argument is the type and must contain only 1 slash - match &args[0] { - FunctionArg::String(value) => { - let slash_count = value.chars().filter(|c| *c == '/').count(); - if slash_count != 1 { - return Err(DscError::Function("resourceId".to_string(), "Type argument must contain exactly one slash".to_string())); - } - result.push_str(value); - }, - _ => { - return Err(DscError::Parser("Invalid argument type".to_string())); + let resource_type = &args[0]; + if let Some(value) = resource_type.as_str() { + let slash_count = value.chars().filter(|c| *c == '/').count(); + if slash_count != 1 { + return Err(DscError::Function("resourceId".to_string(), "Type argument must contain exactly one slash".to_string())); } + result.push_str(value); + } else { + return Err(DscError::Parser("Invalid argument type for first parameter".to_string())); } // ARM uses a slash separator, but here we use a colon which is not allowed for the type nor name result.push(':'); // second argument is the name and must contain no slashes - match &args[1] { - FunctionArg::String(value) => { - if value.contains('/') { - return Err(DscError::Function("resourceId".to_string(), "Name argument cannot contain a slash".to_string())); - } - - result.push_str(value); - }, - _ => { - return Err(DscError::Parser("Invalid argument type".to_string())); + let resource_name = &args[1]; + if let Some(value) = resource_name.as_str() { + if value.contains('/') { + return Err(DscError::Function("resourceId".to_string(), "Name argument cannot contain a slash".to_string())); } + + result.push_str(value); + } else { + return Err(DscError::Parser("Invalid argument type for second parameter".to_string())); } - Ok(FunctionResult::String(result)) + Ok(Value::String(result)) } } diff --git a/dsc_lib/src/parser/expressions.rs b/dsc_lib/src/parser/expressions.rs index 29b6e0d8..3610fc16 100644 --- a/dsc_lib/src/parser/expressions.rs +++ b/dsc_lib/src/parser/expressions.rs @@ -1,12 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use serde_json::Value; +use tracing::{debug, trace}; use tree_sitter::Node; use crate::configure::context::Context; use crate::dscerror::DscError; use crate::functions::FunctionDispatcher; -use crate::parser::functions::{Function, FunctionResult}; +use crate::parser::functions::Function; #[derive(Clone)] pub struct Expression { @@ -60,30 +62,34 @@ impl Expression { /// # Errors /// /// This function will return an error if the expression fails to execute. - pub fn invoke(&self, function_dispatcher: &FunctionDispatcher, context: &Context) -> Result { + pub fn invoke(&self, function_dispatcher: &FunctionDispatcher, context: &Context) -> Result { let result = self.function.invoke(function_dispatcher, context)?; + trace!("Function result: '{:?}'", result); if let Some(member_access) = &self.member_access { - match result { - FunctionResult::String(_) => { - Err(DscError::Parser("Member access on string not supported".to_string())) - }, - FunctionResult::Object(object) => { - let mut value = object; - if !value.is_object() { - return Err(DscError::Parser(format!("Member access on non-object value '{value}'"))); - } - for member in member_access { - value = value[member].clone(); + debug!("Evaluating member access '{:?}'", member_access); + if !result.is_object() { + return Err(DscError::Parser("Member access on non-object value".to_string())); + } + + let mut value = result; + for member in member_access { + if !value.is_object() { + return Err(DscError::Parser(format!("Member access '{member}' on non-object value"))); + } + + if let Some(object) = value.as_object() { + if !object.contains_key(member) { + return Err(DscError::Parser(format!("Member '{member}' not found"))); } - Ok(value.to_string()) + + value = object[member].clone(); } } + + Ok(value) } else { - match result { - FunctionResult::String(value) => Ok(value), - FunctionResult::Object(object) => Ok(object.to_string()), - } + Ok(result) } } } diff --git a/dsc_lib/src/parser/functions.rs b/dsc_lib/src/parser/functions.rs index 1a6c3ff4..c4b11ada 100644 --- a/dsc_lib/src/parser/functions.rs +++ b/dsc_lib/src/parser/functions.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use serde_json::Value; +use serde_json::{Number, Value}; use tree_sitter::Node; use crate::DscError; @@ -19,18 +19,10 @@ pub struct Function { #[derive(Clone)] pub enum FunctionArg { - String(String), - Integer(i32), - Boolean(bool), + Value(Value), Expression(Expression), } -#[derive(Debug, PartialEq)] -pub enum FunctionResult { - String(String), - Object(Value), -} - impl Function { /// Create a new `Function` instance. /// @@ -59,18 +51,18 @@ impl Function { /// # Errors /// /// This function will return an error if the function fails to execute. - pub fn invoke(&self, function_dispatcher: &FunctionDispatcher, context: &Context) -> Result { + pub fn invoke(&self, function_dispatcher: &FunctionDispatcher, context: &Context) -> Result { // if any args are expressions, we need to invoke those first - let mut resolved_args: Vec = vec![]; + let mut resolved_args: Vec = vec![]; if let Some(args) = &self.args { for arg in args { match arg { FunctionArg::Expression(expression) => { let value = expression.invoke(function_dispatcher, context)?; - resolved_args.push(FunctionArg::String(value)); + resolved_args.push(value.clone()); }, - _ => { - resolved_args.push(arg.clone()); + FunctionArg::Value(value) => { + resolved_args.push(value.clone()); } } } @@ -90,15 +82,15 @@ fn convert_args_node(statement_bytes: &[u8], args: &Option) -> Result { let value = arg.utf8_text(statement_bytes)?; - result.push(FunctionArg::String(value.to_string())); + result.push(FunctionArg::Value(Value::String(value.to_string()))); }, "number" => { let value = arg.utf8_text(statement_bytes)?; - result.push(FunctionArg::Integer(value.parse::()?)); + result.push(FunctionArg::Value(Value::Number(Number::from(value.parse::()?)))); }, "boolean" => { let value = arg.utf8_text(statement_bytes)?; - result.push(FunctionArg::Boolean(value.parse::()?)); + result.push(FunctionArg::Value(Value::Bool(value.parse::()?))); }, "expression" => { // TODO: this is recursive, we may want to stop at a specific depth diff --git a/dsc_lib/src/parser/mod.rs b/dsc_lib/src/parser/mod.rs index 5cc3c30e..1bccaf60 100644 --- a/dsc_lib/src/parser/mod.rs +++ b/dsc_lib/src/parser/mod.rs @@ -2,6 +2,8 @@ // Licensed under the MIT License. use expressions::Expression; +use serde_json::Value; +use tracing::debug; use tree_sitter::Parser; use crate::configure::context::Context; @@ -41,7 +43,8 @@ impl Statement { /// # Errors /// /// This function will return an error if the statement fails to parse or execute. - pub fn parse_and_execute(&mut self, statement: &str, context: &Context) -> Result { + pub fn parse_and_execute(&mut self, statement: &str, context: &Context) -> Result { + debug!("Parsing statement: {0}", statement); let Some(tree) = &mut self.parser.parse(statement, None) else { return Err(DscError::Parser(format!("Error parsing statement: {statement}"))); }; @@ -66,14 +69,14 @@ impl Statement { let Ok(value) = child_node.utf8_text(statement_bytes) else { return Err(DscError::Parser("Error parsing string literal".to_string())); }; - Ok(value.to_string()) + Ok(Value::String(value.to_string())) }, "escapedStringLiteral" => { // need to remove the first character: [[ => [ let Ok(value) = child_node.utf8_text(statement_bytes) else { return Err(DscError::Parser("Error parsing escaped string literal".to_string())); }; - Ok(value[1..].to_string()) + Ok(Value::String(value[1..].to_string())) }, "expression" => { let expression = Expression::new(statement_bytes, &child_node)?; diff --git a/tools/dsctest/src/echo.rs b/tools/dsctest/src/echo.rs index e822648a..64d76259 100644 --- a/tools/dsctest/src/echo.rs +++ b/tools/dsctest/src/echo.rs @@ -3,9 +3,23 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(untagged)] +pub enum Output { + #[serde(rename = "array")] + Array(Vec), + #[serde(rename = "bool")] + Bool(bool), + #[serde(rename = "number")] + Number(i64), + #[serde(rename = "string")] + String(String), +} #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Echo { - pub text: String, + pub output: Output, }