diff --git a/docs/reference/schemas/config/functions/last.md b/docs/reference/schemas/config/functions/last.md new file mode 100644 index 000000000..83f6ebfa5 --- /dev/null +++ b/docs/reference/schemas/config/functions/last.md @@ -0,0 +1,231 @@ +--- +description: Reference for the 'last' DSC configuration document function +ms.date: 01/25/2025 +ms.topic: reference +title: last +--- + +## Synopsis + +Returns the last element of an array, or the last character of a string. + +## Syntax + +```Syntax +last(arg) +``` + +## Description + +The `last()` function returns the final element from an array or the final +character from a string. This is useful when you need to access the most recent +item in a sequence, the final stage in a deployment pipeline, or the last +character in a configuration value. + +For arrays, it returns the element at index `length - 1`. For strings, it +returns the last character as a string. + +## Examples + +### Example 1 - Extract the final deployment stage (array of strings) + +Use `last()` to retrieve the final stage in a multi-stage deployment pipeline. +This helps you identify which environment or phase should receive special +handling, such as extended health checks or manual approval gates. This example +uses [`createArray()`][01] to build the deployment stages. + +```yaml +# last.example.1.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Deployment Pipeline + type: Microsoft.DSC.Debug/Echo + properties: + output: + finalStage: "[last(createArray('dev', 'test', 'staging', 'production'))]" + requiresApproval: true +``` + +```bash +dsc config get --file last.example.1.dsc.config.yaml +``` + +```yaml +results: +- name: Deployment Pipeline + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + finalStage: production + requiresApproval: true +messages: [] +hadErrors: false +``` + +This identifies `production` as the final stage, allowing you to apply +production-specific policies or validations. + +### Example 2 - Get the last character of a configuration string + +Use `last()` to extract the final character from a string value. This is useful +for parsing identifiers, checking suffixes, or validating format conventions +like version numbers or region codes. + +```yaml +# last.example.2.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Region Identifier + type: Microsoft.DSC.Debug/Echo + properties: + output: + regionCode: us-west-2 + zoneSuffix: "[last('us-west-2')]" + description: "Zone suffix extracted from region code" +``` + +```bash +dsc config get --file last.example.2.dsc.config.yaml +``` + +```yaml +results: +- name: Region Identifier + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + regionCode: us-west-2 + zoneSuffix: '2' + description: Zone suffix extracted from region code +messages: [] +hadErrors: false +``` + +The function returns `'2'` as a single-character string, representing the zone +suffix in the region identifier. + +### Example 3 - Identify the most recent backup (array of numbers) + +Use `last()` with numerical arrays to find the most recent timestamp or version +number. This example shows how to select the latest backup from a sorted list +of timestamps. This example uses [`createArray()`][01] to build the backup +timestamps. + +```yaml +# last.example.3.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Backup Selection + type: Microsoft.DSC.Debug/Echo + properties: + output: + availableBackups: "[createArray(1704067200, 1704153600, 1704240000, 1704326400)]" + latestBackup: "[last(createArray(1704067200, 1704153600, 1704240000, 1704326400))]" + description: "Most recent backup timestamp (Unix epoch)" +``` + +```bash +dsc config get --file last.example.3.dsc.config.yaml +``` + +```yaml +results: +- name: Backup Selection + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + availableBackups: + - 1704067200 + - 1704153600 + - 1704240000 + - 1704326400 + latestBackup: 1704326400 + description: Most recent backup timestamp (Unix epoch) +messages: [] +hadErrors: false +``` + +The function returns `1704326400`, which represents the most recent backup in +the chronologically sorted array. + +### Example 4 - Combine with other functions for complex logic + +Use `last()` together with [`split()`][02] to extract file extensions or path +components. This example demonstrates parsing a filename to get its extension. + +```yaml +# last.example.4.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: File Extension Parser + type: Microsoft.DSC.Debug/Echo + properties: + output: + filename: config.production.yaml + extension: "[last(split('config.production.yaml', '.'))]" +``` + +```bash +dsc config get --file last.example.4.dsc.config.yaml +``` + +```yaml +results: +- name: File Extension Parser + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + filename: config.production.yaml + extension: yaml +messages: [] +hadErrors: false +``` + +By combining `split()` and `last()`, you can extract the `yaml` extension from +the full filename. + +## Parameters + +### arg + +The array or string to get the last element or character from. Required. + +```yaml +Type: array | string +Required: true +Position: 1 +``` + +## Output + +Returns the last element of the array (preserving its original type) or the +last character as a string. For arrays, the return type matches the element +type. For strings, returns a single-character string. + +If the input is an empty array, the function returns `null`. If the input is an +empty string, the function returns an empty string. + +```yaml +Type: any | string | null +``` + +## Errors + +The function returns an error in the following cases: + +- **Invalid type**: The argument is not an array or string + +## Related functions + +- [`first()`][00] - Returns the first element of an array or character of a string +- [`split()`][02] - Splits a string into an array +- [`createArray()`][01] - Creates an array from provided values + + +[00]: ./first.md +[01]: ./createArray.md +[02]: ./split.md diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 4123a6865..feb047d94 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -504,6 +504,30 @@ Describe 'tests for function expressions' { ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) } + It 'last function works for: ' -TestCases @( + @{ expression = "[last(createArray('hello', 'world'))]"; expected = 'world' } + @{ expression = "[last(createArray(1, 2, 3))]"; expected = 3 } + @{ expression = "[last('hello')]"; expected = 'o' } + @{ expression = "[last('a')]"; expected = 'a' } + @{ expression = "[last(array('mixed'))]"; expected = 'mixed' } + @{ expression = "[last(createArray())]"; expected = $null } + @{ expression = "[last('')]"; expected = '' } + ) { + 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 'indexOf function works for: ' -TestCases @( @{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'banana')]"; expected = 1 } @{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'cherry')]"; expected = 2 } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index ad410767c..54a1eead7 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -343,6 +343,11 @@ 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.last] +description = "Returns the last element of an array or last character of a string" +invoked = "last function" +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" diff --git a/lib/dsc-lib/src/functions/last.rs b/lib/dsc-lib/src/functions/last.rs new file mode 100644 index 000000000..7b8fb471e --- /dev/null +++ b/lib/dsc-lib/src/functions/last.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 Last {} + +impl Function for Last { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "last".to_string(), + description: t!("functions.last.description").to_string(), + category: vec![FunctionCategory::Array, FunctionCategory::String], + 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, FunctionArgKind::Null], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.last.invoked")); + + if let Some(array) = args[0].as_array() { + if array.is_empty() { + return Ok(Value::Null); + } + return Ok(array[array.len() - 1].clone()); + } + + if let Some(string) = args[0].as_str() { + if string.is_empty() { + return Ok(Value::String(String::new())); + } + return Ok(Value::String(string.chars().last().unwrap().to_string())); + } + + Err(DscError::Parser(t!("functions.last.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("[last(createArray('hello', 'world'))]", &Context::new()).unwrap(); + assert_eq!(result.as_str(), Some("world")); + } + + #[test] + fn array_of_numbers() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[last(createArray(1, 2, 3))]", &Context::new()).unwrap(); + assert_eq!(result.to_string(), "3"); + } + + #[test] + fn array_of_single_element() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[last(array('hello'))]", &Context::new()).unwrap(); + assert_eq!(result.as_str(), Some("hello")); + } + + #[test] + fn string_input() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[last('hello')]", &Context::new()).unwrap(); + assert_eq!(result.as_str(), Some("o")); + } + + #[test] + fn single_character_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[last('a')]", &Context::new()).unwrap(); + assert_eq!(result.as_str(), Some("a")); + } + + #[test] + fn invalid_type_object() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[last(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("[last(42)]", &Context::new()); + assert!(result.is_err()); + } + + #[test] + fn array_of_multiple_strings() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[last(createArray('text', 'middle', 'last'))]", &Context::new()).unwrap(); + assert_eq!(result.as_str(), Some("last")); + } + + #[test] + fn unicode_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[last('Hello๐ŸŒ')]", &Context::new()).unwrap(); + assert_eq!(result.as_str(), Some("๐ŸŒ")); + } +} diff --git a/lib/dsc-lib/src/functions/mod.rs b/lib/dsc-lib/src/functions/mod.rs index 01133987e..81ee74455 100644 --- a/lib/dsc-lib/src/functions/mod.rs +++ b/lib/dsc-lib/src/functions/mod.rs @@ -35,6 +35,7 @@ pub mod greater_or_equals; pub mod r#if; pub mod r#false; pub mod first; +pub mod last; pub mod length; pub mod less; pub mod less_or_equals; @@ -163,6 +164,7 @@ impl FunctionDispatcher { Box::new(r#if::If{}), Box::new(r#false::False{}), Box::new(first::First{}), + Box::new(last::Last{}), Box::new(length::Length{}), Box::new(less::Less{}), Box::new(less_or_equals::LessOrEquals{}),