Skip to content

Commit f07aa9b

Browse files
committed
Add tryGet() function
1 parent e57d7e2 commit f07aa9b

File tree

4 files changed

+154
-0
lines changed

4 files changed

+154
-0
lines changed

dsc/tests/dsc_functions.tests.ps1

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,4 +901,33 @@ Describe 'tests for function expressions' {
901901
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
902902
$out.results[0].result.actualState.output | Should -Be $expected
903903
}
904+
905+
It 'tryGet() function works for: <expression>' -TestCases @(
906+
@{ expression = "[tryGet(createObject('a', 1, 'b', 2), 'a')]"; expected = 1 }
907+
@{ expression = "[tryGet(createObject('a', 1, 'b', 2), 'c')]"; expected = $null }
908+
@{ expression = "[tryGet(createObject('key', 'value'), 'key')]"; expected = 'value' }
909+
@{ expression = "[tryGet(createObject('nested', createObject('x', 10)), 'nested')]"; expected = [pscustomobject]@{ x = 10 } }
910+
@{ expression = "[tryGet(createObject('nested', createObject('x', 10)), 'missing')]"; expected = $null }
911+
@{ expression = "[tryGet(createArray(1,2,3), 0)]"; expected = 1 }
912+
@{ expression = "[tryGet(createArray(1,2,3), 3)]"; expected = $null }
913+
@{ expression = "[tryGet(createArray(1,2,3), -3)]"; expected = $null }
914+
) {
915+
param($expression, $expected)
916+
917+
$escapedExpression = $expression -replace "'", "''"
918+
$config_yaml = @"
919+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
920+
resources:
921+
- name: Echo
922+
type: Microsoft.DSC.Debug/Echo
923+
properties:
924+
output: '$escapedExpression'
925+
"@
926+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
927+
if ($expected -is [pscustomobject]) {
928+
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
929+
} else {
930+
$out.results[0].result.actualState.output | Should -BeExactly $expected
931+
}
932+
}
904933
}

lib/dsc-lib/locales/en-us.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,12 @@ description = "Removes all leading and trailing white-space characters from the
523523
description = "Returns the boolean value true"
524524
invoked = "true function"
525525

526+
[functions.tryGet]
527+
description = "Attempts to retrieve a value from an array (by index) or object (by key). Null is returned if the key or index does not exist."
528+
invoked = "tryGet function"
529+
invalidKeyType = "Invalid key type, must be a string"
530+
invalidIndexType = "Invalid index type, must be an integer"
531+
526532
[functions.union]
527533
description = "Returns a single array or object with all elements from the parameters"
528534
invoked = "union function"

lib/dsc-lib/src/functions/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ pub mod to_lower;
6868
pub mod to_upper;
6969
pub mod trim;
7070
pub mod r#true;
71+
pub mod try_get;
7172
pub mod union;
7273
pub mod unique_string;
7374
pub mod user_function;
@@ -191,6 +192,7 @@ impl FunctionDispatcher {
191192
Box::new(to_upper::ToUpper{}),
192193
Box::new(trim::Trim{}),
193194
Box::new(r#true::True{}),
195+
Box::new(try_get::TryGet{}),
194196
Box::new(utc_now::UtcNow{}),
195197
Box::new(union::Union{}),
196198
Box::new(unique_string::UniqueString{}),
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::DscError;
5+
use crate::configure::context::Context;
6+
use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata};
7+
use rust_i18n::t;
8+
use serde_json::Value;
9+
use tracing::debug;
10+
11+
#[derive(Debug, Default)]
12+
pub struct TryGet {}
13+
14+
impl Function for TryGet {
15+
fn get_metadata(&self) -> FunctionMetadata {
16+
FunctionMetadata {
17+
name: "tryGet".to_string(),
18+
description: t!("functions.tryGet.description").to_string(),
19+
category: vec![FunctionCategory::Logical],
20+
min_args: 2,
21+
max_args: 2,
22+
accepted_arg_ordered_types: vec![
23+
vec![FunctionArgKind::Array, FunctionArgKind::Object],
24+
vec![FunctionArgKind::Number, FunctionArgKind::String],
25+
],
26+
remaining_arg_accepted_types: None,
27+
return_types: vec![FunctionArgKind::Array, FunctionArgKind::Boolean, FunctionArgKind::Null, FunctionArgKind::Number, FunctionArgKind::Object, FunctionArgKind::String],
28+
}
29+
}
30+
31+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
32+
debug!("{}", t!("functions.tryGet.invoked"));
33+
34+
if let Some(object) = args[0].as_object() {
35+
if let Some(key) = args[1].as_str() {
36+
if let Some(value) = object.get(key) {
37+
return Ok(value.clone());
38+
}
39+
return Ok(Value::Null);
40+
}
41+
return Err(DscError::Parser(t!("functions.tryGet.invalidKeyType").to_string()));
42+
}
43+
44+
if let Some(array) = args[0].as_array() {
45+
if let Some(index) = args[1].as_i64() {
46+
let Ok(index) = usize::try_from(index) else {
47+
// handle negative index
48+
return Ok(Value::Null);
49+
};
50+
let index = if index >= array.len() {
51+
return Ok(Value::Null);
52+
} else {
53+
index
54+
};
55+
return Ok(array[index].clone());
56+
}
57+
return Err(DscError::Parser(t!("functions.tryGet.invalidIndexType").to_string()));
58+
}
59+
60+
Err(DscError::Parser(t!("functions.invalidArgType").to_string()))
61+
}
62+
}
63+
64+
#[cfg(test)]
65+
mod tests {
66+
use crate::configure::context::Context;
67+
use crate::parser::Statement;
68+
69+
#[test]
70+
fn try_get_object() {
71+
let mut parser = Statement::new().unwrap();
72+
let result = parser.parse_and_execute("[tryGet(createObject('key1', 'value1'), 'key1')]", &Context::new()).unwrap();
73+
assert_eq!(result, serde_json::json!(["value1"]));
74+
}
75+
76+
#[test]
77+
fn try_get_object_not_found() {
78+
let mut parser = Statement::new().unwrap();
79+
let result = parser.parse_and_execute("[tryGet(createObject('key1', 'value1'), 'key2')]", &Context::new()).unwrap();
80+
assert_eq!(result, serde_json::json!([null]));
81+
}
82+
83+
#[test]
84+
fn try_get_array() {
85+
let mut parser = Statement::new().unwrap();
86+
let result = parser.parse_and_execute("[tryGet(createArray('value1', 'value2'), 1)]", &Context::new()).unwrap();
87+
assert_eq!(result, serde_json::json!(["value2"]));
88+
}
89+
90+
#[test]
91+
fn try_get_array_negative_index() {
92+
let mut parser = Statement::new().unwrap();
93+
let result = parser.parse_and_execute("[tryGet(createArray('value1', 'value2'), -1)]", &Context::new()).unwrap();
94+
assert_eq!(result, serde_json::json!(["value2"]));
95+
}
96+
97+
#[test]
98+
fn try_get_array_index_not_found() {
99+
let mut parser = Statement::new().unwrap();
100+
let result = parser.parse_and_execute("[tryGet(createArray('value1', 'value2'), 2)]", &Context::new()).unwrap();
101+
assert_eq!(result, serde_json::json!([null]));
102+
}
103+
104+
#[test]
105+
fn try_get_object_invalid_key_type() {
106+
let mut parser = Statement::new().unwrap();
107+
let result = parser.parse_and_execute("[tryGet(createObject('key1', 'value1'), 1)]", &Context::new());
108+
assert!(result.is_err());
109+
}
110+
111+
#[test]
112+
fn try_get_array_invalid_index_type() {
113+
let mut parser = Statement::new().unwrap();
114+
let result = parser.parse_and_execute("[tryGet(createArray('value1', 'value2'), '1')]", &Context::new());
115+
assert!(result.is_err());
116+
}
117+
}

0 commit comments

Comments
 (0)