diff --git a/docs/reference/schemas/config/functions/overview.md b/docs/reference/schemas/config/functions/overview.md index f4c8fdb0d..bc1d7635f 100644 --- a/docs/reference/schemas/config/functions/overview.md +++ b/docs/reference/schemas/config/functions/overview.md @@ -589,6 +589,7 @@ The following list of functions operate on arrays and collections: - [min()][min] - Return the smallest integer value from an array of integers. - [range()][range] - Create an array of integers within a specified range. - [skip()][skip] - Return an array or string with elements skipped from the beginning. +- [take()][take] - Return an array or string with the specified number of elements from the start. - [tryGet()][tryGet] - Safely retrieve a value from an array by index or an object by key without throwing an error. - [tryIndexFromEnd()][tryIndexFromEnd] - Safely retrieve a value from an array by counting backward from the end. - [union()][union] - Return a single array or object with all unique elements from the parameters. @@ -679,6 +680,7 @@ The following list of functions are for manipulating strings: - [length()][length] - Return the number of elements in an array, characters in a string, or top-level properties in an object. - [skip()][skip] - Return an array or string with elements skipped from the beginning. - [startsWith()][startsWith] - Check if a string starts with a specified prefix. +- [take()][take] - Return an array or string with the specified number of elements from the start. - [string()][string] - Convert a value to its string representation. - [substring()][substring] - Extract a portion of a string starting at a specified position. - [toLower()][toLower] - Convert a string to lowercase. @@ -765,6 +767,7 @@ The following list of functions create or convert values of a given type: [skip]: ./skip.md [startsWith]: ./startsWith.md [string]: ./string.md +[take]: ./take.md [sub]: ./sub.md [substring]: ./substring.md [systemRoot]: ./systemRoot.md diff --git a/docs/reference/schemas/config/functions/take.md b/docs/reference/schemas/config/functions/take.md new file mode 100644 index 000000000..ea8e23108 --- /dev/null +++ b/docs/reference/schemas/config/functions/take.md @@ -0,0 +1,324 @@ +--- +description: Reference for the 'take' DSC configuration document function +ms.date: 11/01/2025 +ms.topic: reference +title: take +--- + +## Synopsis + +Returns an array with the specified number of elements from the start of an +array, or a string with the specified number of characters from the start of a +string. + +## Syntax + +```Syntax +take(originalValue, numberToTake) +``` + +## Description + +The `take()` function extracts a specified number of elements from the beginning +of an array or characters from the beginning of a string. This is useful for +limiting results, implementing pagination, or extracting prefixes from larger +datasets. + +- For arrays: returns a new array containing the first `n` elements +- For strings: returns a new string containing the first `n` characters + +Both parameters are required. The `originalValue` must be an array or a string. +The `numberToTake` must be an integer. If the number is zero or negative, an +empty array or empty string is returned. If the number is larger than the length +of the array or string, all elements or characters are returned. + +This function is particularly useful when you need to: + +- Limit the number of items processed from a list +- Extract a fixed-length prefix from identifiers or paths +- Implement top-N selections without complex filtering +- Create pagination or batch processing logic + +## Examples + +### Example 1 - Limit deployment to top priority servers + +Deploy configuration changes to only the highest priority servers first, limiting +risk during rollout. The `take()` function extracts the first N servers from +your priority list. This example uses [`parameters()`][00] to reference the +server list. + +```yaml +# take.example.1.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + allServers: + type: array + defaultValue: + - prod-web-01 + - prod-web-02 + - prod-web-03 + - prod-web-04 + - prod-web-05 +resources: +- name: Priority Deployment + type: Microsoft.DSC.Debug/Echo + properties: + output: + allServers: "[parameters('allServers')]" + deployToFirst: "[take(parameters('allServers'), 2)]" + description: Deploy to first 2 servers in priority order +``` + +```bash +dsc config get --file take.example.1.dsc.config.yaml +``` + +```yaml +results: +- name: Priority Deployment + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + allServers: + - prod-web-01 + - prod-web-02 + - prod-web-03 + - prod-web-04 + - prod-web-05 + deployToFirst: + - prod-web-01 + - prod-web-02 + description: Deploy to first 2 servers in priority order +messages: [] +hadErrors: false +``` + +The function returns only the first two servers from the list, allowing you to +implement a staged rollout strategy. + +### Example 2 - Extract environment prefix from resource names + +When working with standardized naming conventions, extracting prefixes helps +with categorization and routing logic. This example shows how to use `take()` to +get environment codes from resource identifiers. This example uses +[`createArray()`][01] to build the resource name list. + +```yaml +# take.example.2.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Resource Prefixes + type: Microsoft.DSC.Debug/Echo + properties: + output: + resources: "[createArray('prod-db-east-01', 'dev-api-west-02', 'test-cache-central')]" + prodPrefix: "[take('prod-db-east-01', 4)]" + devPrefix: "[take('dev-api-west-02', 3)]" + testPrefix: "[take('test-cache-central', 4)]" +``` + +```bash +dsc config get --file take.example.2.dsc.config.yaml +``` + +```yaml +results: +- name: Resource Prefixes + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + resources: + - prod-db-east-01 + - dev-api-west-02 + - test-cache-central + prodPrefix: prod + devPrefix: dev + testPrefix: test +messages: [] +hadErrors: false +``` + +The function extracts the environment prefix from each resource name, enabling +environment-specific configuration logic. + +### Example 3 - Implement batch processing with size limits + +Processing items in controlled batches prevents resource exhaustion when dealing +with large datasets. By using `take()`, you can limit the number of items +processed in each run. This example uses [`parameters()`][00] to reference the +pending jobs array and [`length()`][02] to show the total count. + +```yaml +# take.example.3.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + pendingJobs: + type: array + defaultValue: + - job-001 + - job-002 + - job-003 + - job-004 + - job-005 + - job-006 + - job-007 + - job-008 +resources: +- name: Batch Processing + type: Microsoft.DSC.Debug/Echo + properties: + output: + totalPending: "[length(parameters('pendingJobs'))]" + currentBatch: "[take(parameters('pendingJobs'), 3)]" + batchSize: 3 + description: Process first 3 jobs from queue +``` + +```bash +dsc config get --file take.example.3.dsc.config.yaml +``` + +```yaml +results: +- name: Batch Processing + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + totalPending: 8 + currentBatch: + - job-001 + - job-002 + - job-003 + batchSize: 3 + description: Process first 3 jobs from queue +messages: [] +hadErrors: false +``` + +The function returns the first three jobs for processing, allowing you to +implement controlled batch processing with predictable resource usage. + +### Example 4 - Select top-N log entries for monitoring + +Pagination-style access to log entries or event streams can be implemented by +combining `take()` with [`skip()`][03]. This example shows how to get the most +recent entries while demonstrating the complementary relationship between these +functions. This example uses [`parameters()`][00] to reference the log entries +array. + +```yaml +# take.example.4.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + recentLogs: + type: array + defaultValue: + - 2025-11-01 10:00:00 - System started + - 2025-11-01 10:05:32 - User login: admin + - 2025-11-01 10:07:15 - Config updated + - 2025-11-01 10:12:48 - Service restarted + - 2025-11-01 10:15:03 - Backup completed + - 2025-11-01 10:20:17 - Health check passed + - 2025-11-01 10:25:44 - Cache cleared +resources: +- name: Log Monitoring + type: Microsoft.DSC.Debug/Echo + properties: + output: + topThree: "[take(parameters('recentLogs'), 3)]" + nextThree: "[take(skip(parameters('recentLogs'), 3), 3)]" + description: Show first 3 and next 3 log entries +``` + +```bash +dsc config get --file take.example.4.dsc.config.yaml +``` + +```yaml +results: +- name: Log Monitoring + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + topThree: + - 2025-11-01 10:00:00 - System started + - 2025-11-01 10:05:32 - User login: admin + - 2025-11-01 10:07:15 - Config updated + nextThree: + - 2025-11-01 10:12:48 - Service restarted + - 2025-11-01 10:15:03 - Backup completed + - 2025-11-01 10:20:17 - Health check passed + description: Show first 3 and next 3 log entries +messages: [] +hadErrors: false +``` + +By combining `take()` and `skip()`, you can implement pagination logic to +process logs or events in manageable chunks. + +## Parameters + +### originalValue + +The array or string to take elements from. Required. + +```yaml +Type: array | string +Required: true +Position: 1 +``` + +### numberToTake + +The number of elements or characters to take from the start. Must be an integer. +If this value is 0 or less, an empty array or string is returned. If it's larger +than the length of the given array or string, all elements or characters are +returned. Required. + +```yaml +Type: integer +Required: true +Position: 2 +``` + +## Output + +Returns the same type as `originalValue`: + +- If `originalValue` is an array, returns an array with up to `numberToTake` + elements from the start +- If `originalValue` is a string, returns a string with up to `numberToTake` + characters from the start + +```yaml +Type: array | string +``` + +## Errors + +The function returns an error in the following cases: + +- **Invalid original value type**: The first argument is not an array or string +- **Invalid number type**: The second argument is not an integer + +## Related functions + +- [`skip()`][03] - Returns an array or string with elements skipped from the start +- [`first()`][04] - Returns the first element of an array or first character of a string +- [`last()`][05] - Returns the last element of an array or last character of a string +- [`length()`][02] - Returns the number of elements in an array or characters in a string +- [`createArray()`][01] - Creates an array from provided values +- [`parameters()`][00] - Returns the value of a specified configuration parameter + + +[00]: ./parameters.md +[01]: ./createArray.md +[02]: ./length.md +[03]: ./skip.md +[04]: ./first.md +[05]: ./last.md diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index bc7b830fa..70df91fa0 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -599,6 +599,37 @@ Describe 'tests for function expressions' { ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) } + It 'take function works for: ' -TestCases @( + @{ expression = "[take(createArray('a','b','c','d'), 2)]"; expected = @('a', 'b') } + @{ expression = "[take('hello', 2)]"; expected = 'he' } + @{ expression = "[take(createArray('a','b'), 0)]"; expected = @() } + @{ expression = "[take('abc', 0)]"; expected = '' } + @{ expression = "[take(createArray('a','b'), 5)]"; expected = @('a', 'b') } + @{ expression = "[take('hi', 10)]"; expected = 'hi' } + @{ expression = "[take('', 1)]"; expected = '' } + @{ expression = "[take(createArray(), 2)]"; expected = @() } + # Negative and zero counts return empty + @{ expression = "[take(createArray('x','y','z'), -1)]"; expected = @() } + @{ expression = "[take('hello', -2)]"; expected = '' } + # Take all elements + @{ expression = "[take(createArray('x','y','z'), 3)]"; expected = @('x', 'y', 'z') } + @{ expression = "[take('test', 4)]"; expected = 'test' } + ) { + 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 'lastIndexOf function works for: ' -TestCases @( @{ expression = "[lastIndexOf(createArray('a', 'b', 'a', 'c'), 'a')]"; expected = 2 } @{ expression = "[lastIndexOf(createArray(10, 20, 30, 20), 20)]"; expected = 3 } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 72eed3367..fd2de8f84 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -528,6 +528,12 @@ invoked = "sub function" description = "Returns the system root path" invoked = "systemRoot function" +[functions.take] +description = "Returns an array with the specified number of elements from the start, or a string with the specified number of characters from the start" +invoked = "take function" +invalidNumberToTake = "Second argument must be an integer" +invalidOriginalValue = "First argument must be an array or string" + [functions.toLower] description = "Converts the specified string to lower case" diff --git a/lib/dsc-lib/src/functions/mod.rs b/lib/dsc-lib/src/functions/mod.rs index 39e58631f..94d008202 100644 --- a/lib/dsc-lib/src/functions/mod.rs +++ b/lib/dsc-lib/src/functions/mod.rs @@ -63,6 +63,7 @@ pub mod secret; pub mod skip; pub mod starts_with; pub mod string; +pub mod take; pub mod sub; pub mod substring; pub mod system_root; @@ -194,6 +195,7 @@ impl FunctionDispatcher { Box::new(starts_with::StartsWith{}), Box::new(string::StringFn{}), Box::new(sub::Sub{}), + Box::new(take::Take{}), Box::new(substring::Substring{}), Box::new(system_root::SystemRoot{}), Box::new(to_lower::ToLower{}), diff --git a/lib/dsc-lib/src/functions/take.rs b/lib/dsc-lib/src/functions/take.rs new file mode 100644 index 000000000..3f21cc51c --- /dev/null +++ b/lib/dsc-lib/src/functions/take.rs @@ -0,0 +1,152 @@ +// 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 Take {} + +impl Function for Take { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "take".to_string(), + description: t!("functions.take.description").to_string(), + category: vec![FunctionCategory::Array, FunctionCategory::String], + min_args: 2, + max_args: 2, + accepted_arg_ordered_types: vec![ + vec![FunctionArgKind::Array, FunctionArgKind::String], + vec![FunctionArgKind::Number], + ], + remaining_arg_accepted_types: None, + return_types: vec![FunctionArgKind::Array, FunctionArgKind::String], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.take.invoked")); + + if let Some(count_i64) = args[1].as_i64() { + let count: usize = if count_i64 <= 0 { + 0 + } else { + count_i64.try_into().unwrap_or(usize::MAX) + }; + + if let Some(array) = args[0].as_array() { + let take_count = count.min(array.len()); + let taken = array.iter().take(take_count).cloned().collect::>(); + return Ok(Value::Array(taken)); + } + + if let Some(s) = args[0].as_str() { + let result: String = s.chars().take(count).collect(); + return Ok(Value::String(result)); + } + + return Err(DscError::Parser(t!("functions.take.invalidOriginalValue").to_string())); + } + + Err(DscError::Parser(t!("functions.take.invalidNumberToTake").to_string())) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + use serde_json::Value; + + #[test] + fn take_array_basic() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[take(createArray('a','b','c','d'), 2)]", &Context::new()).unwrap(); + assert_eq!(result, Value::Array(vec![Value::String("a".into()), Value::String("b".into())])); + } + + #[test] + fn take_string_basic() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[take('hello', 2)]", &Context::new()).unwrap(); + assert_eq!(result, Value::String("he".into())); + } + + #[test] + fn take_more_than_length() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[take(createArray('a','b'), 5)]", &Context::new()).unwrap(); + assert_eq!(result, Value::Array(vec![Value::String("a".into()), Value::String("b".into())])); + } + + #[test] + fn take_string_more_than_length() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[take('hi', 10)]", &Context::new()).unwrap(); + assert_eq!(result, Value::String("hi".into())); + } + + #[test] + fn take_array_zero() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[take(createArray('a','b'), 0)]", &Context::new()).unwrap(); + assert_eq!(result, Value::Array(vec![])); + } + + #[test] + fn take_string_zero() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[take('hello', 0)]", &Context::new()).unwrap(); + assert_eq!(result, Value::String("".into())); + } + + #[test] + fn take_array_negative_is_empty() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[take(createArray('a','b','c'), -1)]", &Context::new()).unwrap(); + assert_eq!(result, Value::Array(vec![])); + } + + #[test] + fn take_string_negative_is_empty() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[take('hello', -2)]", &Context::new()).unwrap(); + assert_eq!(result, Value::String("".into())); + } + + #[test] + fn take_empty_array() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[take(createArray(), 2)]", &Context::new()).unwrap(); + assert_eq!(result, Value::Array(vec![])); + } + + #[test] + fn take_empty_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[take('', 1)]", &Context::new()).unwrap(); + assert_eq!(result, Value::String("".into())); + } + + #[test] + fn take_array_all_elements() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[take(createArray('x','y','z'), 3)]", &Context::new()).unwrap(); + assert_eq!(result, Value::Array(vec![ + Value::String("x".into()), + Value::String("y".into()), + Value::String("z".into()), + ])); + } + + #[test] + fn take_string_all_characters() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[take('test', 4)]", &Context::new()).unwrap(); + assert_eq!(result, Value::String("test".into())); + } +}