diff --git a/docs/reference/schemas/config/functions/objectKeys.md b/docs/reference/schemas/config/functions/objectKeys.md new file mode 100644 index 000000000..6de9bc83d --- /dev/null +++ b/docs/reference/schemas/config/functions/objectKeys.md @@ -0,0 +1,300 @@ +--- +description: Reference for the 'objectKeys' DSC configuration document function +ms.date: 11/14/2025 +ms.topic: reference +title: objectKeys +--- + +## Synopsis + +Returns an array containing all the keys from an object. + +## Syntax + +```Syntax +objectKeys() +``` + +## Description + +The `objectKeys()` function extracts all property names from an object and returns them as +an array of strings. This function is useful for: + +- Iterating over object properties when you only need the keys +- Counting the number of properties in an object +- Checking if specific keys exist in an object +- Converting object keys for further processing + +The function only returns the top-level keys of the object. For nested objects, only the +outer keys are included in the result. + +This function is similar to [`items()`][00], which returns both keys and values, while +`objectKeys()` returns only the keys. + +## Examples + +### Example 1 - Extract keys from simple object + +The following example extracts all keys from a simple object. + +```yaml +# objectKeys.example.1.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[objectKeys(createObject('firstName', 'John', 'lastName', 'Doe', 'age', 30))]" +``` + +```bash +dsc config get --file objectKeys.example.1.dsc.config.yaml +``` + +```yaml +results: +- name: Echo + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + - firstName + - lastName + - age +messages: [] +hadErrors: false +``` + +### Example 2 - Count object properties + +The following example uses `objectKeys()` with [`length()`][01] to count the number of +properties in an object. + +```yaml +# objectKeys.example.2.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[length(objectKeys(createObject('a', 1, 'b', 2, 'c', 3)))]" +``` + +```bash +dsc config get --file objectKeys.example.2.dsc.config.yaml +``` + +```yaml +results: +- name: Echo + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: 3 +messages: [] +hadErrors: false +``` + +### Example 3 - Check if key exists + +The following example uses `objectKeys()` with [`contains()`][02] to check if a specific +key exists in an object. + +```yaml +# objectKeys.example.3.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + config: + type: object + defaultValue: + enabled: true + timeout: 30 +resources: +- name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: + hasEnabled: "[contains(objectKeys(parameters('config')), 'enabled')]" + hasDebug: "[contains(objectKeys(parameters('config')), 'debug')]" +``` + +```bash +dsc config get --file objectKeys.example.3.dsc.config.yaml +``` + +```yaml +results: +- name: Echo + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + hasEnabled: true + hasDebug: false +messages: [] +hadErrors: false +``` + +### Example 4 - Iterate over keys with copy loop + +The following example uses `objectKeys()` to iterate over object properties using the +[`copy`][03] feature. + +```yaml +# objectKeys.example.4.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + settings: + type: object + defaultValue: + debug: false + logLevel: info + maxRetries: 3 +resources: +- name: "[format('Setting-{0}', copyIndex())]" + copy: + name: settingsLoop + count: "[length(objectKeys(parameters('settings')))]" + type: Microsoft.DSC.Debug/Echo + properties: + output: "[objectKeys(parameters('settings'))[copyIndex()]]" +``` + +```bash +dsc config get --file objectKeys.example.4.dsc.config.yaml +``` + +```yaml +results: +- name: Setting-0 + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: debug +- name: Setting-1 + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: logLevel +- name: Setting-2 + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: maxRetries +messages: [] +hadErrors: false +``` + +### Example 5 - Top-level keys only + +The following example demonstrates that `objectKeys()` only returns top-level keys, even +when the object contains nested objects. + +```yaml +# objectKeys.example.5.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[objectKeys(createObject('user', createObject('name', 'John', 'age', 30), 'role', 'admin'))]" +``` + +```bash +dsc config get --file objectKeys.example.5.dsc.config.yaml +``` + +```yaml +results: +- name: Echo + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + - user + - role +messages: [] +hadErrors: false +``` + +### Example 6 - Empty object + +The following example shows that `objectKeys()` returns an empty array for an empty object. + +```yaml +# objectKeys.example.6.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: + keys: "[objectKeys(createObject())]" + isEmpty: "[equals(length(objectKeys(createObject())), 0)]" +``` + +```bash +dsc config get --file objectKeys.example.6.dsc.config.yaml +``` + +```yaml +results: +- name: Echo + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + keys: [] + isEmpty: true +messages: [] +hadErrors: false +``` + +## Parameters + +### inputObject + +The object from which to extract the keys. + +```yaml +Type: object +Required: true +Position: 1 +``` + +## Output + +Returns an array of strings, where each string is a property name (key) from the input +object. The array contains only the top-level keys. + +```yaml +Type: array +``` + +## Error conditions + +The function will return an error in the following cases: + +- **Not an object**: The input is not an object (e.g., string, number, array, null) + +## Notes + +- The function only returns top-level keys; nested object keys are not included +- For empty objects, the function returns an empty array +- The order of keys in the returned array follows JSON object property ordering +- Key names are always returned as strings +- To get both keys and values, use [`items()`][00] instead + +## Related functions + +- [`items()`][00] - Converts an object to an array of key-value pairs +- [`createObject()`][04] - Creates an object from key-value pairs +- [`length()`][01] - Returns the number of elements in an array +- [`contains()`][02] - Checks if an array contains a specific value + + +[00]: ./items.md +[01]: ./length.md +[02]: ./contains.md +[03]: ../document/copy.md +[04]: ./createObject.md diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 5ee2bacd9..4a9223d07 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -958,6 +958,46 @@ Describe 'tests for function expressions' { $out.results[0].result.actualState.output | Should -Be $expected } + It 'objectKeys function returns array of keys: ' -TestCases @( + @{ expression = "[length(objectKeys(createObject('a', 1, 'b', 2, 'c', 3)))]"; expected = 3 } + @{ expression = "[length(objectKeys(createObject()))]"; expected = 0 } + @{ expression = "[objectKeys(createObject('name', 'John'))[0]]"; expected = 'name' } + @{ expression = "[length(objectKeys(createObject('x', 1, 'y', 2)))]"; expected = 2 } + ) { + param($expression, $expected) + + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } + + It 'objectKeys function works with nested objects: ' -TestCases @( + @{ expression = "[objectKeys(createObject('person', createObject('name', 'John')))[0]]"; expected = 'person' } + @{ expression = "[length(objectKeys(createObject('a', createArray(1,2,3), 'b', 'text')))]"; expected = 2 } + ) { + param($expression, $expected) + + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } + It 'tryGet() function works for: ' -TestCases @( @{ expression = "[tryGet(createObject('a', 1, 'b', 2), 'a')]"; expected = 1 } @{ expression = "[tryGet(createObject('a', 1, 'b', 2), 'c')]"; expected = $null } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 5061ef205..a5e4a9e94 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -448,6 +448,9 @@ invoked = "not function" description = "Returns a null value" invoked = "null function" +[functions.objectKeys] +description = "Returns the keys from an object, where an object is a collection of key-value pairs" + [functions.or] description = "Evaluates if any arguments are true" invoked = "or function" diff --git a/lib/dsc-lib/src/functions/mod.rs b/lib/dsc-lib/src/functions/mod.rs index 6cd7810f7..e109d77da 100644 --- a/lib/dsc-lib/src/functions/mod.rs +++ b/lib/dsc-lib/src/functions/mod.rs @@ -55,6 +55,7 @@ pub mod mod_function; pub mod mul; pub mod not; pub mod null; +pub mod object_keys; pub mod or; pub mod parameters; pub mod parse_cidr; @@ -189,6 +190,7 @@ impl FunctionDispatcher { Box::new(mul::Mul{}), Box::new(not::Not{}), Box::new(null::Null{}), + Box::new(object_keys::ObjectKeys{}), Box::new(or::Or{}), Box::new(parameters::Parameters{}), Box::new(parse_cidr::ParseCidr{}), diff --git a/lib/dsc-lib/src/functions/object_keys.rs b/lib/dsc-lib/src/functions/object_keys.rs new file mode 100644 index 000000000..6e7e8a755 --- /dev/null +++ b/lib/dsc-lib/src/functions/object_keys.rs @@ -0,0 +1,134 @@ +// 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; + +#[derive(Debug, Default)] +pub struct ObjectKeys {} + +impl Function for ObjectKeys { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "objectKeys".to_string(), + description: t!("functions.objectKeys.description").to_string(), + category: vec![FunctionCategory::Object], + min_args: 1, + max_args: 1, + accepted_arg_ordered_types: vec![ + vec![FunctionArgKind::Object], + ], + remaining_arg_accepted_types: None, + return_types: vec![FunctionArgKind::Array], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + let obj = args[0].as_object().unwrap(); + + let keys: Vec = obj + .keys() + .map(|key| Value::String(key.clone())) + .collect(); + + Ok(Value::Array(keys)) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + use serde_json::{json, Value}; + + #[test] + fn object_keys_basic() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[objectKeys(createObject('a', 1, 'b', 2, 'c', 3))]", &Context::new()).unwrap(); + + let arr = result.as_array().unwrap(); + assert_eq!(arr.len(), 3); + + for key in arr { + assert!(key.is_string()); + } + + let keys: Vec<&str> = arr.iter().filter_map(Value::as_str).collect(); + assert!(keys.contains(&"a")); + assert!(keys.contains(&"b")); + assert!(keys.contains(&"c")); + } + + #[test] + fn object_keys_empty_object() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[objectKeys(createObject())]", &Context::new()).unwrap(); + assert_eq!(result, json!([])); + } + + #[test] + fn object_keys_single_key() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[objectKeys(createObject('name', 'John'))]", &Context::new()).unwrap(); + + let arr = result.as_array().unwrap(); + assert_eq!(arr.len(), 1); + assert_eq!(arr[0].as_str(), Some("name")); + } + + #[test] + fn object_keys_nested_values() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[objectKeys(createObject('person', createObject('name', 'John', 'age', 30)))]", &Context::new()).unwrap(); + + let arr = result.as_array().unwrap(); + assert_eq!(arr.len(), 1); + assert_eq!(arr[0].as_str(), Some("person")); + } + + #[test] + fn object_keys_mixed_value_types() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[objectKeys(createObject('str', 'text', 'num', 42, 'bool', true(), 'arr', createArray(1,2,3)))]", &Context::new()).unwrap(); + + let arr = result.as_array().unwrap(); + assert_eq!(arr.len(), 4); + + let keys: Vec<&str> = arr.iter().filter_map(Value::as_str).collect(); + assert!(keys.contains(&"str")); + assert!(keys.contains(&"num")); + assert!(keys.contains(&"bool")); + assert!(keys.contains(&"arr")); + } + + #[test] + fn object_keys_can_be_used_with_length() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[length(objectKeys(createObject('a', 1, 'b', 2, 'c', 3)))]", &Context::new()).unwrap(); + assert_eq!(result, json!(3)); + } + + #[test] + fn object_keys_not_object_error() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[objectKeys('not an object')]", &Context::new()); + assert!(result.is_err()); + } + + #[test] + fn object_keys_array_error() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[objectKeys(createArray('a', 'b', 'c'))]", &Context::new()); + assert!(result.is_err()); + } + + #[test] + fn object_keys_number_error() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[objectKeys(42)]", &Context::new()); + assert!(result.is_err()); + } +}