diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index c7453e0bd..aeaaffaa8 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -2,18 +2,18 @@ # Licensed under the MIT License. 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', 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==' } - ) { - param($text, $expected) + It 'function works: ' -TestCases @( + @{ text = "[concat('a', 'b')]"; expected = 'ab' } + @{ text = "[concat('a', 'b', 'c')]"; expected = 'abc' } + @{ 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==' } + ) { + param($text, $expected) - $escapedText = $text -replace "'", "''" - $config_yaml = @" + $escapedText = $text -replace "'", "''" + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Echo @@ -21,17 +21,17 @@ Describe 'tests for function expressions' { properties: output: '$escapedText' "@ - $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -Be $expected - } + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } - It 'path() works' -TestCases @( - @{ path = "systemRoot(), 'a'"; expected = "$PSHOME$([System.IO.Path]::DirectorySeparatorChar)a" } - @{ path = "'a', 'b', 'c'"; expected = "a$([System.IO.Path]::DirectorySeparatorChar)b$([System.IO.Path]::DirectorySeparatorChar)c" } - ) { - param($path, $expected) + It 'path() works' -TestCases @( + @{ path = "systemRoot(), 'a'"; expected = "$PSHOME$([System.IO.Path]::DirectorySeparatorChar)a" } + @{ path = "'a', 'b', 'c'"; expected = "a$([System.IO.Path]::DirectorySeparatorChar)b$([System.IO.Path]::DirectorySeparatorChar)c" } + ) { + param($path, $expected) - $config_yaml = @" + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Echo @@ -39,12 +39,12 @@ Describe 'tests for function expressions' { properties: output: "[path($path)]" "@ - $out = $config_yaml | dsc config --system-root $PSHOME get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -BeExactly $expected - } + $out = $config_yaml | dsc config --system-root $PSHOME get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -BeExactly $expected + } - It 'default systemRoot() is correct for the OS' { - $config_yaml = @' + It 'default systemRoot() is correct for the OS' { + $config_yaml = @' $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Echo @@ -53,26 +53,26 @@ Describe 'tests for function expressions' { output: "[systemRoot()]" '@ - $expected = if ($IsWindows) { - $env:SYSTEMDRIVE + '\' - } else { - '/' - } - $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 - $out.results[0].result.actualState.output | Should -BeExactly $expected + $expected = if ($IsWindows) { + $env:SYSTEMDRIVE + '\' + } else { + '/' } + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.actualState.output | Should -BeExactly $expected + } - It 'union function works for: ' -TestCases @( - @{ expression = "[union(parameters('firstArray'), parameters('secondArray'))]"; expected = @('ab', 'cd', 'ef') } - @{ expression = "[union(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ one = 'a'; two = 'c'; three = 'd' } } - @{ expression = "[union(parameters('secondArray'), parameters('secondArray'))]"; expected = @('cd', 'ef') } - @{ expression = "[union(parameters('secondObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'c'; three = 'd' } } - @{ expression = "[union(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } - ) { - param($expression, $expected, $isError) + It 'union function works for: ' -TestCases @( + @{ expression = "[union(parameters('firstArray'), parameters('secondArray'))]"; expected = @('ab', 'cd', 'ef') } + @{ expression = "[union(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ one = 'a'; two = 'c'; three = 'd' } } + @{ expression = "[union(parameters('secondArray'), parameters('secondArray'))]"; expected = @('cd', 'ef') } + @{ expression = "[union(parameters('secondObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'c'; three = 'd' } } + @{ expression = "[union(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } + ) { + param($expression, $expected, $isError) - $config_yaml = @" + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json parameters: firstObject: @@ -101,33 +101,33 @@ Describe 'tests for function expressions' { properties: output: "$expression" "@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - if ($isError) { - $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) - (Get-Content $TestDrive/error.log -Raw) | Should -Match 'All arguments must either be arrays or objects' - } else { - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + if ($isError) { + $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) + (Get-Content $TestDrive/error.log -Raw) | Should -Match 'All arguments must either be arrays or objects' + } else { + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) } + } - It 'contain function works for: ' -TestCases @( - @{ expression = "[contains(parameters('array'), 'a')]" ; expected = $true } - @{ expression = "[contains(parameters('array'), 2)]" ; expected = $false } - @{ expression = "[contains(parameters('array'), 1)]" ; expected = $true } - @{ expression = "[contains(parameters('array'), 'z')]" ; expected = $false } - @{ expression = "[contains(parameters('object'), 'a')]" ; expected = $true } - @{ expression = "[contains(parameters('object'), 'c')]" ; expected = $false } - @{ expression = "[contains(parameters('object'), 3)]" ; expected = $true } - @{ expression = "[contains(parameters('object'), parameters('object'))]" ; isError = $true } - @{ expression = "[contains(parameters('array'), parameters('array'))]" ; isError = $true } - @{ expression = "[contains(parameters('string'), 'not found')]" ; expected = $false } - @{ expression = "[contains(parameters('string'), 'hello')]" ; expected = $true } - @{ expression = "[contains(parameters('string'), 12)]" ; expected = $true } - ) { - param($expression, $expected, $isError) + It 'contain function works for: ' -TestCases @( + @{ expression = "[contains(parameters('array'), 'a')]" ; expected = $true } + @{ expression = "[contains(parameters('array'), 2)]" ; expected = $false } + @{ expression = "[contains(parameters('array'), 1)]" ; expected = $true } + @{ expression = "[contains(parameters('array'), 'z')]" ; expected = $false } + @{ expression = "[contains(parameters('object'), 'a')]" ; expected = $true } + @{ expression = "[contains(parameters('object'), 'c')]" ; expected = $false } + @{ expression = "[contains(parameters('object'), 3)]" ; expected = $true } + @{ expression = "[contains(parameters('object'), parameters('object'))]" ; isError = $true } + @{ expression = "[contains(parameters('array'), parameters('array'))]" ; isError = $true } + @{ expression = "[contains(parameters('string'), 'not found')]" ; expected = $false } + @{ expression = "[contains(parameters('string'), 'hello')]" ; expected = $true } + @{ expression = "[contains(parameters('string'), 12)]" ; expected = $true } + ) { + param($expression, $expected, $isError) - $config_yaml = @" + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json parameters: array: @@ -152,25 +152,25 @@ Describe 'tests for function expressions' { properties: output: "$expression" "@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - if ($isError) { - $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) - (Get-Content $TestDrive/error.log -Raw) | Should -Match 'accepted types are: String, Number' - } else { - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + if ($isError) { + $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) + (Get-Content $TestDrive/error.log -Raw) | Should -Match 'accepted types are: String, Number' + } else { + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) } + } - It 'length function works for: ' -TestCases @( - @{ expression = "[length(parameters('array'))]" ; expected = 3 } - @{ expression = "[length(parameters('object'))]" ; expected = 4 } - @{ expression = "[length(parameters('string'))]" ; expected = 12 } - @{ expression = "[length('')]"; expected = 0 } - ) { - param($expression, $expected, $isError) + It 'length function works for: ' -TestCases @( + @{ expression = "[length(parameters('array'))]" ; expected = 3 } + @{ expression = "[length(parameters('object'))]" ; expected = 4 } + @{ expression = "[length(parameters('string'))]" ; expected = 12 } + @{ expression = "[length('')]"; expected = 0 } + ) { + param($expression, $expected, $isError) - $config_yaml = @" + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json parameters: array: @@ -195,22 +195,22 @@ Describe 'tests for function expressions' { properties: output: "$expression" "@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } - It 'empty function works for: ' -TestCases @( - @{ expression = "[empty(parameters('array'))]" ; expected = $false } - @{ expression = "[empty(parameters('object'))]" ; expected = $false } - @{ expression = "[empty(parameters('string'))]" ; expected = $false } - @{ expression = "[empty(parameters('emptyArray'))]" ; expected = $true } - @{ expression = "[empty(parameters('emptyObject'))]" ; expected = $true } - @{ expression = "[empty('')]" ; expected = $true } - ) { - param($expression, $expected) + It 'empty function works for: ' -TestCases @( + @{ expression = "[empty(parameters('array'))]" ; expected = $false } + @{ expression = "[empty(parameters('object'))]" ; expected = $false } + @{ expression = "[empty(parameters('string'))]" ; expected = $false } + @{ expression = "[empty(parameters('emptyArray'))]" ; expected = $true } + @{ expression = "[empty(parameters('emptyObject'))]" ; expected = $true } + @{ expression = "[empty('')]" ; expected = $true } + ) { + param($expression, $expected) - $config_yaml = @" + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json parameters: array: @@ -240,31 +240,31 @@ Describe 'tests for function expressions' { properties: output: "$expression" "@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } - It 'utcNow function works for: utcNow()' -TestCases @( - @{ format = $null} - @{ format = "yyyy-MM-dd"} - @{ format = "yyyy-MM-ddTHH"} - @{ format = "yyyy-MM-ddTHHZ"} - @{ format = "MMM dd, yyyy HH"} - @{ format = "yy-MMMM-dddd tt H" } - @{ format = "MMM ddd zzz" } - @{ format = "YY YYYY MM MMM MMMM" } - ) { - param($format) + It 'utcNow function works for: utcNow()' -TestCases @( + @{ format = $null } + @{ format = "yyyy-MM-dd" } + @{ format = "yyyy-MM-ddTHH" } + @{ format = "yyyy-MM-ddTHHZ" } + @{ format = "MMM dd, yyyy HH" } + @{ format = "yy-MMMM-dddd tt H" } + @{ format = "MMM ddd zzz" } + @{ format = "YY YYYY MM MMM MMMM" } + ) { + param($format) - if ($null -eq $format) { - $expected = (Get-Date -AsUTC).ToString("o") - } else { - $expected = (Get-Date -AsUTC).ToString($format) - $format = "'$format'" - } + if ($null -eq $format) { + $expected = (Get-Date -AsUTC).ToString("o") + } else { + $expected = (Get-Date -AsUTC).ToString($format) + $format = "'$format'" + } - $config_yaml = @" + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json parameters: test: @@ -276,19 +276,19 @@ Describe 'tests for function expressions' { properties: output: "[parameters('test')]" "@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - # ConvertFrom-Json will convert the date to a DateTime object, so we use regex to capture the string - $out -match '"output":"(?.*?)"' | Should -BeTrue -Because "Output should contain a date" - $actual = $matches['date'] - # since the datetimes might slightly differ, we remove the seconds and milliseconds - $expected = $expected -replace ':\d+\.\d+Z$', 'Z' - $actual = $actual -replace ':\d+\.\d+Z$', 'Z' - $actual | Should -BeExactly $expected -Because "Expected: '$expected', Actual: '$actual'" - } + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + # ConvertFrom-Json will convert the date to a DateTime object, so we use regex to capture the string + $out -match '"output":"(?.*?)"' | Should -BeTrue -Because "Output should contain a date" + $actual = $matches['date'] + # since the datetimes might slightly differ, we remove the seconds and milliseconds + $expected = $expected -replace ':\d+\.\d+Z$', 'Z' + $actual = $actual -replace ':\d+\.\d+Z$', 'Z' + $actual | Should -BeExactly $expected -Because "Expected: '$expected', Actual: '$actual'" + } - It 'utcNow errors if used not as a parameter default' { - $config_yaml = @" + It 'utcNow errors if used not as a parameter default' { + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Echo @@ -296,15 +296,83 @@ Describe 'tests for function expressions' { properties: output: "[utcNow()]" "@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) - (Get-Content $TestDrive/error.log -Raw) | Should -Match 'utcNow function can only be used as a parameter default' + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) + (Get-Content $TestDrive/error.log -Raw) | Should -Match 'utcNow function can only be used as a parameter default' + } + + It 'uniqueString function works for: ' -TestCases @( + @{ expression = "[uniqueString('a')]" ; expected = 'cfvwxu6sc4lqo' } + @{ expression = "[uniqueString('a', 'b', 'c')]" ; expected = 'bhw7m6t6ntwd6' } + @{ expression = "[uniqueString('a', 'b', 'c', 'd')]" ; expected = 'yxzg7ur4qetcy' } + ) { + param($expression, $expected) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + $out.results[0].result.actualState.output | Should -BeExactly $expected + } + + It 'string function works for: ' -TestCases @( + @{ expression = "[string('hello')]"; expected = 'hello' } + @{ expression = "[string(123)]"; expected = '123' } + @{ expression = "[string(true)]"; expected = 'true' } + @{ expression = "[string(null())]"; expected = 'null' } + @{ expression = "[string(createArray('a', 'b'))]"; expected = '["a","b"]' } + @{ expression = "[string(createObject('a', 1))]"; expected = '{"a":1}' } + ) { + param($expression, $expected) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } + + It 'array function works for: ' -TestCases @( + @{ expression = "[array('hello', 42)]"; expected = @('hello', 42) } + @{ expression = "[array('a', 'b', 'c')]"; expected = @('a', 'b', 'c') } + @{ expression = "[array(1, 2, 3)]"; expected = @(1, 2, 3) } + @{ expression = "[array()]"; expected = @() } + @{ expression = "[array('string', 123, createObject('key', 'value'))]"; expected = @('string', 123, [pscustomobject]@{ key = 'value' }) } + @{ expression = "[array(createArray('a', 'b'), 'string')]"; expected = @(@('a', 'b'), 'string') } + ) { + param($expression, $expected) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) } - It 'uniqueString function works for: ' -TestCases @( - @{ expression = "[uniqueString('a')]" ; expected = 'cfvwxu6sc4lqo' } - @{ expression = "[uniqueString('a', 'b', 'c')]" ; expected = 'bhw7m6t6ntwd6' } - @{ expression = "[uniqueString('a', 'b', 'c', 'd')]" ; expected = 'yxzg7ur4qetcy' } + It 'first function works for: ' -TestCases @( + @{ expression = "[first(createArray('hello', 'world'))]"; expected = 'hello' } + @{ expression = "[first(createArray(1, 2, 3))]"; expected = 1 } + @{ expression = "[first('hello')]"; expected = 'h' } + @{ expression = "[first('a')]"; expected = 'a' } + @{ expression = "[first(array('mixed', 42))]"; expected = 'mixed' } ) { param($expression, $expected) @@ -318,16 +386,17 @@ Describe 'tests for function expressions' { "@ $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - $out.results[0].result.actualState.output | Should -BeExactly $expected + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) } - It 'string function works for: ' -TestCases @( - @{ expression = "[string('hello')]"; expected = 'hello' } - @{ expression = "[string(123)]"; expected = '123' } - @{ expression = "[string(true)]"; expected = 'true' } - @{ expression = "[string(null())]"; expected = 'null' } - @{ expression = "[string(createArray('a', 'b'))]"; expected = '["a","b"]' } - @{ expression = "[string(createObject('a', 1))]"; expected = '{"a":1}' } + It 'indexOf function works for: ' -TestCases @( + @{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'banana')]"; expected = 1 } + @{ expression = "[indexOf(createArray(10, 20, 30), 20)]"; expected = 1 } + @{ expression = "[indexOf(createArray('a', 'b', 'a', 'c'), 'a')]"; expected = 0 } + @{ expression = "[indexOf(createArray('apple', 'banana'), 'orange')]"; expected = -1 } + @{ expression = "[indexOf(createArray('Apple', 'Banana'), 'apple')]"; expected = -1 } + @{ expression = "[indexOf(createArray(), 'test')]"; expected = -1 } + @{ expression = "[indexOf(array(createArray('a', 'b'), createArray('c', 'd')), createArray('c', 'd'))]"; expected = 1 } ) { param($expression, $expected) diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index e5ccb3cbf..a588fed0c 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -217,6 +217,11 @@ invoked = "add function" description = "Evaluates if all arguments are true" invoked = "and function" +[functions.array] +description = "Creates an array from the given elements of mixed types" +invoked = "array function" +invalidArgType = "Invalid argument type, only int, string, array, or object are accepted" + [functions.base64] description = "Encodes a string to Base64 format" @@ -280,6 +285,13 @@ description = "Evaluates if the two values are the same" description = "Returns the boolean value false" invoked = "false function" +[functions.first] +description = "Returns the first element of an array or first character of a string" +invoked = "first function" +emptyArray = "Cannot get first element of empty array" +emptyString = "Cannot get first character of empty string" +invalidArgType = "Invalid argument type, argument must be an array or string" + [functions.greater] description = "Evaluates if the first value is greater than the second value" invoked = "greater function" @@ -307,6 +319,11 @@ parseStringError = "unable to parse string to int" castError = "unable to cast to int" parseNumError = "unable to parse number to int" +[functions.indexOf] +description = "Returns the index of the first occurrence of an item in an array" +invoked = "indexOf function" +invalidArrayArg = "First argument must be an array" + [functions.length] description = "Returns the length of a string, array, or object" invoked = "length function" diff --git a/dsc_lib/src/functions/array.rs b/dsc_lib/src/functions/array.rs new file mode 100644 index 000000000..190477998 --- /dev/null +++ b/dsc_lib/src/functions/array.rs @@ -0,0 +1,103 @@ +// 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 Array {} + +impl Function for Array { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "array".to_string(), + description: t!("functions.array.description").to_string(), + category: FunctionCategory::Array, + min_args: 0, + max_args: usize::MAX, + accepted_arg_ordered_types: vec![], + remaining_arg_accepted_types: Some(vec![ + FunctionArgKind::String, + FunctionArgKind::Number, + FunctionArgKind::Object, + FunctionArgKind::Array, + ]), + return_types: vec![FunctionArgKind::Array], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.array.invoked")); + let mut array_result = Vec::::new(); + + for value in args { + // Only accept int, string, array, or object as specified + if value.is_number() || value.is_string() || value.is_array() || value.is_object() { + array_result.push(value.clone()); + } else { + return Err(DscError::Parser(t!("functions.array.invalidArgType").to_string())); + } + } + + Ok(Value::Array(array_result)) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn mixed_types() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[array('hello', 42)]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), r#"["hello",42]"#); + } + + #[test] + fn strings_only() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[array('a', 'b', 'c')]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), r#"["a","b","c"]"#); + } + + #[test] + fn numbers_only() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[array(1, 2, 3)]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "[1,2,3]"); + } + + #[test] + fn arrays_and_objects() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[array(createArray('a','b'), createObject('key', 'value'))]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), r#"[["a","b"],{"key":"value"}]"#); + } + + #[test] + fn empty_array() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[array()]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "[]"); + } + + #[test] + fn invalid_type_boolean() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[array(true)]", &Context::new()); + assert!(result.is_err()); + } + + #[test] + fn invalid_type_null() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[array(null())]", &Context::new()); + assert!(result.is_err()); + } +} diff --git a/dsc_lib/src/functions/first.rs b/dsc_lib/src/functions/first.rs new file mode 100644 index 000000000..d53b32129 --- /dev/null +++ b/dsc_lib/src/functions/first.rs @@ -0,0 +1,116 @@ +// 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 First {} + +impl Function for First { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "first".to_string(), + description: t!("functions.first.description").to_string(), + category: FunctionCategory::Array, + min_args: 1, + max_args: 1, + accepted_arg_ordered_types: vec![vec![FunctionArgKind::Array, FunctionArgKind::String]], + remaining_arg_accepted_types: None, + return_types: vec![FunctionArgKind::String, FunctionArgKind::Number, FunctionArgKind::Array, FunctionArgKind::Object], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.first.invoked")); + + if let Some(array) = args[0].as_array() { + if array.is_empty() { + return Err(DscError::Parser(t!("functions.first.emptyArray").to_string())); + } + return Ok(array[0].clone()); + } + + if let Some(string) = args[0].as_str() { + if string.is_empty() { + return Err(DscError::Parser(t!("functions.first.emptyString").to_string())); + } + return Ok(Value::String(string.chars().next().unwrap().to_string())); + } + + Err(DscError::Parser(t!("functions.first.invalidArgType").to_string())) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn array_of_strings() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[first(createArray('hello', 'world'))]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "\"hello\""); + } + + #[test] + fn array_of_numbers() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[first(createArray(1, 2, 3))]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "1"); + } + + #[test] + fn array_of_mixed() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[first(array('hello', 42))]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "\"hello\""); + } + + #[test] + fn string_input() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[first('hello')]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "\"h\""); + } + + #[test] + fn single_character_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[first('a')]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "\"a\""); + } + + #[test] + fn empty_array() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[first(createArray())]", &Context::new()); + assert!(result.is_err()); + } + + #[test] + fn empty_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[first('')]", &Context::new()); + assert!(result.is_err()); + } + + #[test] + fn invalid_type_object() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[first(createObject('key', 'value'))]", &Context::new()); + assert!(result.is_err()); + } + + #[test] + fn invalid_type_number() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[first(42)]", &Context::new()); + assert!(result.is_err()); + } +} diff --git a/dsc_lib/src/functions/index_of.rs b/dsc_lib/src/functions/index_of.rs new file mode 100644 index 000000000..f1e3e415d --- /dev/null +++ b/dsc_lib/src/functions/index_of.rs @@ -0,0 +1,130 @@ +// 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 IndexOf {} + +impl Function for IndexOf { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "indexOf".to_string(), + description: t!("functions.indexOf.description").to_string(), + category: FunctionCategory::Array, + min_args: 2, + max_args: 2, + accepted_arg_ordered_types: vec![ + vec![FunctionArgKind::Array], + vec![FunctionArgKind::String, FunctionArgKind::Number, FunctionArgKind::Array, FunctionArgKind::Object], + ], + remaining_arg_accepted_types: None, + return_types: vec![FunctionArgKind::Number], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.indexOf.invoked")); + + let array = args[0].as_array().ok_or_else(|| { + DscError::Parser(t!("functions.indexOf.invalidArrayArg").to_string()) + })?; + + let item_to_find = &args[1]; + + for (index, item) in array.iter().enumerate() { + let matches = match (item_to_find, item) { + // String comparison (case-sensitive) + (Value::String(find_str), Value::String(item_str)) => find_str == item_str, + (Value::Number(find_num), Value::Number(item_num)) => find_num == item_num, + (Value::Array(find_arr), Value::Array(item_arr)) => find_arr == item_arr, + (Value::Object(find_obj), Value::Object(item_obj)) => find_obj == item_obj, + _ => false, + }; + + if matches { + let index_i64 = i64::try_from(index).map_err(|_| { + DscError::Parser("Array index too large to represent as integer".to_string()) + })?; + return Ok(Value::Number(index_i64.into())); + } + } + + // Not found is -1 + Ok(Value::Number((-1i64).into())) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn find_string_in_array() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[indexOf(createArray('apple', 'banana', 'cherry'), 'banana')]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "1"); + } + + #[test] + fn find_number_in_array() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[indexOf(createArray(10, 20, 30), 20)]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "1"); + } + + #[test] + fn find_first_occurrence() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[indexOf(createArray('a', 'b', 'a', 'c'), 'a')]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "0"); + } + + #[test] + fn item_not_found() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[indexOf(createArray('apple', 'banana'), 'orange')]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "-1"); + } + + #[test] + fn case_sensitive_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[indexOf(createArray('Apple', 'Banana'), 'apple')]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "-1"); + } + + #[test] + fn find_array_in_array() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[indexOf(array(createArray('a', 'b'), createArray('c', 'd')), createArray('c', 'd'))]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "1"); + } + + #[test] + fn find_object_in_array() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[indexOf(array(createObject('name', 'John'), createObject('name', 'Jane')), createObject('name', 'Jane'))]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "1"); + } + + #[test] + fn empty_array() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[indexOf(createArray(), 'test')]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "-1"); + } + + #[test] + fn invalid_array_arg() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[indexOf('not_an_array', 'test')]", &Context::new()); + assert!(result.is_err()); + } +} diff --git a/dsc_lib/src/functions/mod.rs b/dsc_lib/src/functions/mod.rs index cdcb4c928..b02db0167 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -13,6 +13,7 @@ use std::fmt::Display; pub mod add; pub mod and; +pub mod array; pub mod base64; pub mod bool; pub mod coalesce; @@ -29,11 +30,13 @@ pub mod greater; pub mod greater_or_equals; pub mod r#if; pub mod r#false; +pub mod first; pub mod length; pub mod less; pub mod less_or_equals; pub mod format; pub mod int; +pub mod index_of; pub mod max; pub mod min; pub mod mod_function; @@ -119,6 +122,7 @@ impl FunctionDispatcher { let function_list : Vec> = vec![ Box::new(add::Add{}), Box::new(and::And{}), + Box::new(array::Array{}), Box::new(base64::Base64{}), Box::new(bool::Bool{}), Box::new(coalesce::Coalesce{}), @@ -135,11 +139,13 @@ impl FunctionDispatcher { Box::new(greater_or_equals::GreaterOrEquals{}), Box::new(r#if::If{}), Box::new(r#false::False{}), + Box::new(first::First{}), Box::new(length::Length{}), Box::new(less::Less{}), Box::new(less_or_equals::LessOrEquals{}), Box::new(format::Format{}), Box::new(int::Int{}), + Box::new(index_of::IndexOf{}), Box::new(max::Max{}), Box::new(min::Min{}), Box::new(mod_function::Mod{}), diff --git a/tree-sitter-dscexpression/.editorconfig b/tree-sitter-dscexpression/.editorconfig new file mode 100644 index 000000000..65330c40c --- /dev/null +++ b/tree-sitter-dscexpression/.editorconfig @@ -0,0 +1,46 @@ +root = true + +[*] +charset = utf-8 + +[*.{json,toml,yml,gyp}] +indent_style = space +indent_size = 2 + +[*.js] +indent_style = space +indent_size = 2 + +[*.scm] +indent_style = space +indent_size = 2 + +[*.{c,cc,h}] +indent_style = space +indent_size = 4 + +[*.rs] +indent_style = space +indent_size = 4 + +[*.{py,pyi}] +indent_style = space +indent_size = 4 + +[*.swift] +indent_style = space +indent_size = 4 + +[*.go] +indent_style = tab +indent_size = 8 + +[Makefile] +indent_style = tab +indent_size = 8 + +[parser.c] +indent_size = 2 + +[{alloc,array,parser}.h] +indent_size = 2 diff --git a/tree-sitter-dscexpression/.gitattributes b/tree-sitter-dscexpression/.gitattributes index ffb52abec..5f0a73219 100644 --- a/tree-sitter-dscexpression/.gitattributes +++ b/tree-sitter-dscexpression/.gitattributes @@ -9,3 +9,7 @@ binding.gyp linguist-generated setup.py linguist-generated Makefile linguist-generated Package.swift linguist-generated + +# Zig bindings +build.zig linguist-generated +build.zig.zon linguist-generated diff --git a/tree-sitter-dscexpression/.gitignore b/tree-sitter-dscexpression/.gitignore index 17aa2089c..87a0c80c2 100644 --- a/tree-sitter-dscexpression/.gitignore +++ b/tree-sitter-dscexpression/.gitignore @@ -1,18 +1,18 @@ # Rust artifacts target/ +Cargo.lock # Node artifacts build/ prebuilds/ node_modules/ -*.tgz +package-lock.json # Swift artifacts .build/ -Package.swift +Package.resolved # Go artifacts -go.sum _obj/ # Python artifacts @@ -20,8 +20,6 @@ _obj/ dist/ *.egg-info *.whl -pyproject.toml -setup.py # C artifacts *.a @@ -30,7 +28,13 @@ setup.py *.dylib *.dll *.pc -Makefile +*.exp +*.lib + +# Zig artifacts +.zig-cache/ +zig-cache/ +zig-out/ # Example dirs /examples/*/ @@ -40,4 +44,7 @@ Makefile *.obj *.o -.editorconfig +# Archives +*.tar.gz +*.tgz +*.zip