Skip to content

Commit 87b8bc4

Browse files
committed
Merge branch 'main' of https://github.com/Gijsreyn/operation-methods into gh-57/main/add-uri-functions
2 parents bfa0115 + 735d319 commit 87b8bc4

File tree

9 files changed

+241
-16
lines changed

9 files changed

+241
-16
lines changed

dsc/tests/dsc_expressions.tests.ps1

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ Describe 'Expressions tests' {
1212
@{ text = "[parameters('test').objectArray[1].value[1].name]"; expected = 'three' }
1313
@{ text = "[parameters('test').index]"; expected = '1' }
1414
@{ text = "[parameters('test').objectArray[parameters('test').index].name]"; expected = 'two' }
15+
@{ text = "[parameters('test')['hello']"; expected = '@{world=there}' }
16+
@{ text = "[parameters('test')['hello']['world']]"; expected = 'there' }
17+
@{ text = "[parameters('test')['array'][1][0]]"; expected = 'two' }
18+
@{ text = "[parameters('test')['objectArray'][0]['name']]"; expected = 'one' }
19+
@{ text = "[parameters('test')['objectArray'][1]['value'][1]['name']]"; expected = 'three' }
20+
@{ text = "[parameters('test')[parameters('propertyName')]]"; expected = '@{world=there}' }
1521
) {
1622
param($text, $expected)
1723
$yaml = @"
@@ -35,6 +41,9 @@ Describe 'Expressions tests' {
3541
- nestedObject:
3642
name: three
3743
value: 3
44+
propertyName:
45+
type: string
46+
defaultValue: hello
3847
resources:
3948
- name: echo
4049
type: Microsoft.DSC.Debug/Echo
@@ -185,7 +194,7 @@ resources:
185194
if ($out.results[0].result.actualState.output.$key -is [psobject]) {
186195
$out.results[0].result.actualState.output.$key.psobject.properties.value | Should -Be $expected.$key.values -Because ($out | ConvertTo-Json -Depth 10 | Out-String)
187196
} else {
188-
$out.results[0].result.actualState.output.$key | Should -Be $expected.$key -Because ($out | ConvertTo-Json -Depth 10 | Out-String)
197+
$out.results[0].result.actualState.output.$key | Should -Be $expected.$key -Because ($out | ConvertTo-Json -Depth 10 | Out-String)
189198
}
190199
}
191200
}
@@ -240,7 +249,7 @@ resources:
240249
$LASTEXITCODE | Should -Be 2
241250
$log = Get-Content -Path $TestDrive/error.log -Raw
242251
$log | Should -BeLike "*ERROR* Arguments must be of the same type*"
243-
252+
244253
}
245254

246255
Context 'Resource name expression evaluation' {

dsc/tests/dsc_functions.tests.ps1

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1031,4 +1031,33 @@ Describe 'tests for function expressions' {
10311031
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
10321032
$out.results[0].result.actualState.output | Should -BeExactly $expected
10331033
}
1034-
}
1034+
1035+
It 'tryGet() function works for: <expression>' -TestCases @(
1036+
@{ expression = "[tryGet(createObject('a', 1, 'b', 2), 'a')]"; expected = 1 }
1037+
@{ expression = "[tryGet(createObject('a', 1, 'b', 2), 'c')]"; expected = $null }
1038+
@{ expression = "[tryGet(createObject('key', 'value'), 'key')]"; expected = 'value' }
1039+
@{ expression = "[tryGet(createObject('nested', createObject('x', 10)), 'nested')]"; expected = [pscustomobject]@{ x = 10 } }
1040+
@{ expression = "[tryGet(createObject('nested', createObject('x', 10)), 'missing')]"; expected = $null }
1041+
@{ expression = "[tryGet(createArray(1,2,3), 0)]"; expected = 1 }
1042+
@{ expression = "[tryGet(createArray(1,2,3), 3)]"; expected = $null }
1043+
@{ expression = "[tryGet(createArray(1,2,3), -3)]"; expected = $null }
1044+
) {
1045+
param($expression, $expected)
1046+
1047+
$escapedExpression = $expression -replace "'", "''"
1048+
$config_yaml = @"
1049+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1050+
resources:
1051+
- name: Echo
1052+
type: Microsoft.DSC.Debug/Echo
1053+
properties:
1054+
output: '$escapedExpression'
1055+
"@
1056+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
1057+
if ($expected -is [pscustomobject]) {
1058+
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
1059+
} else {
1060+
$out.results[0].result.actualState.output | Should -BeExactly $expected
1061+
}
1062+
}
1063+
}

grammars/tree-sitter-dscexpression/grammar.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ module.exports = grammar({
4242
memberAccess: $ => seq('.', field('name', $.memberName)),
4343
memberName: $ => /[a-zA-Z0-9_-]+/,
4444

45-
index: $ => seq('[', field('indexValue', choice($.expression, $.number)), ']'),
45+
propertyName: $ => seq('\'', field('string', $.string), '\''),
46+
index: $ => seq('[', field('indexValue', choice($.expression, $.number, $.propertyName)), ']'),
4647
}
4748

4849
});

grammars/tree-sitter-dscexpression/test/corpus/invalid_expressions.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,12 @@ String with un-escaped single-quote
206206
(arguments
207207
(string))
208208
(ERROR))))
209+
210+
=====
211+
Object index without quotes
212+
=====
213+
[myObject[foo]]
214+
---
215+
216+
(ERROR
217+
(functionName))

grammars/tree-sitter-dscexpression/test/corpus/valid_expressions.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,24 @@ Array index
160160
(index
161161
(number)))))
162162

163+
=====
164+
Object index
165+
=====
166+
[createObject('a',1)['a']]
167+
---
168+
169+
(statement
170+
(expression
171+
(function
172+
(functionName)
173+
(arguments
174+
(string)
175+
(number)))
176+
(accessor
177+
(index
178+
(propertyName
179+
(string))))))
180+
163181
=====
164182
Multiple array indexes
165183
=====

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

Lines changed: 15 additions & 5 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"
@@ -558,14 +564,17 @@ unavailableInUserFunction = "The 'variables()' function is not available in user
558564

559565
[parser.expression]
560566
functionNodeNotFound = "Function node not found"
561-
parsingFunction = "Parsing function '%{name}'"
562-
parsingAccessor = "Parsing accessor '%{name}'"
567+
parsingFunction = "Parsing function: %{name}"
568+
parsingAccessor = "Parsing accessor: %{name}"
563569
accessorParsingError = "Error parsing accessor"
564-
parsingMemberAccessor = "Parsing member accessor '%{name}'"
570+
parsingMemberAccessor = "Parsing member accessor: %{name}"
565571
memberNotFound = "Member name not found"
566-
parsingIndexAccessor = "Parsing index accessor '%{index}'"
572+
parsingIndexAccessor = "Parsing index accessor: %{index}"
567573
indexNotFound = "Index value not found"
568-
invalidAccessorKind = "Invalid accessor kind: '%{kind}'"
574+
indexValue = "Index value: %{value} with kind %{kind}"
575+
propertyNameValue = "Property name value: %{value}"
576+
invalidIndexValueKind = "Invalid index value kind: %{kind}"
577+
invalidAccessorKind = "Invalid accessor kind: %{kind}"
569578
functionResult = "Function results: %{results}"
570579
functionResultSecure = "Function result is secure"
571580
evalAccessors = "Evaluating accessors"
@@ -576,6 +585,7 @@ indexNotValid = "Index is not a valid number"
576585
indexOutOfBounds = "Index is out of bounds"
577586
indexOnNonArray = "Index access on non-array value"
578587
invalidIndexType = "Invalid index type"
588+
propertyNameNotString = "Property name is not a string"
579589

580590
[parser.functions]
581591
foundErrorNode = "Found error node parsing function"

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

Lines changed: 3 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 uri_component;
@@ -193,6 +194,8 @@ impl FunctionDispatcher {
193194
Box::new(to_upper::ToUpper{}),
194195
Box::new(trim::Trim{}),
195196
Box::new(r#true::True{}),
197+
Box::new(try_get::TryGet{}),
198+
Box::new(utc_now::UtcNow{}),
196199
Box::new(union::Union{}),
197200
Box::new(unique_string::UniqueString{}),
198201
Box::new(uri_component::UriComponent{}),
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::Array, FunctionCategory::Object],
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!(null));
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+
}

lib/dsc-lib/src/parser/expressions.rs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ pub struct Expression {
2525
accessors: Vec<Accessor>,
2626
}
2727

28+
fn node_to_string(node: &Node, statement_bytes: &[u8]) -> Result<String, DscError> {
29+
let text = node.utf8_text(statement_bytes)?;
30+
Ok(text.to_string())
31+
}
32+
2833
impl Expression {
2934
/// Create a new `Expression` instance.
3035
///
@@ -41,11 +46,11 @@ impl Expression {
4146
let Some(function) = expression.child_by_field_name("function") else {
4247
return Err(DscError::Parser(t!("parser.expression.functionNodeNotFound").to_string()));
4348
};
44-
debug!("{}", t!("parser.expression.parsingFunction", name = function : {:?}));
49+
debug!("{}", t!("parser.expression.parsingFunction", name = node_to_string(&function, statement_bytes)? : {:?}));
4550
let function = Function::new(statement_bytes, &function)?;
4651
let mut accessors = Vec::<Accessor>::new();
4752
if let Some(accessor) = expression.child_by_field_name("accessor") {
48-
debug!("{}", t!("parser.expression.parsingAccessor", name = accessor : {:?}));
53+
debug!("{}", t!("parser.expression.parsingAccessor", name = node_to_string(&accessor, statement_bytes)? : {:?}));
4954
if accessor.is_error() {
5055
return Err(DscError::Parser(t!("parser.expression.accessorParsingError").to_string()));
5156
}
@@ -57,30 +62,39 @@ impl Expression {
5762
let accessor_kind = accessor.kind();
5863
let value = match accessor_kind {
5964
"memberAccess" => {
60-
debug!("{}", t!("parser.expression.parsingMemberAccessor", name = accessor : {:?}));
65+
debug!("{}", t!("parser.expression.parsingMemberAccessor", name = node_to_string(&accessor, statement_bytes)? : {:?}));
6166
let Some(member_name) = accessor.child_by_field_name("name") else {
6267
return Err(DscError::Parser(t!("parser.expression.memberNotFound").to_string()));
6368
};
6469
let member = member_name.utf8_text(statement_bytes)?;
6570
Accessor::Member(member.to_string())
6671
},
6772
"index" => {
68-
debug!("{}", t!("parser.expression.parsingIndexAccessor", index = accessor : {:?}));
73+
debug!("{}", t!("parser.expression.parsingIndexAccessor", index = node_to_string(&accessor, statement_bytes)? : {:?}));
6974
let Some(index_value) = accessor.child_by_field_name("indexValue") else {
7075
return Err(DscError::Parser(t!("parser.expression.indexNotFound").to_string()));
7176
};
77+
debug!("{}", t!("parser.expression.indexValue", value = node_to_string(&index_value, statement_bytes)? : {:?}, kind = index_value.kind()));
7278
match index_value.kind() {
7379
"number" => {
7480
let value = index_value.utf8_text(statement_bytes)?;
75-
let value = serde_json::from_str(value)?;
76-
Accessor::Index(value)
81+
let number: i64 = value.parse().map_err(|_| DscError::Parser(t!("parser.expression.indexNotValid").to_string()))?;
82+
Accessor::Index(Value::Number(number.into()))
83+
},
84+
"propertyName" => {
85+
let Some(string_node) = index_value.child_by_field_name("string") else {
86+
return Err(DscError::Parser(t!("parser.expression.propertyNameNotString").to_string()));
87+
};
88+
let value = string_node.utf8_text(statement_bytes)?;
89+
debug!("{}", t!("parser.expression.propertyNameValue", value = value : {:?}));
90+
Accessor::Index(Value::String(value.to_string()))
7791
},
7892
"expression" => {
7993
let expression = Expression::new(statement_bytes, &index_value)?;
8094
Accessor::IndexExpression(expression)
8195
},
8296
_ => {
83-
return Err(DscError::Parser(t!("parser.expression.invalidAccessorKind", kind = accessor_kind).to_string()));
97+
return Err(DscError::Parser(t!("parser.expression.invalidIndexValueKind", kind = index_value.kind()).to_string()));
8498
},
8599
}
86100
},
@@ -186,6 +200,21 @@ impl Expression {
186200
return Err(DscError::Parser(t!("parser.expression.indexOnNonArray").to_string()));
187201
}
188202
}
203+
else if index.is_string() {
204+
let index = index.as_str().ok_or_else(|| DscError::Parser(t!("parser.expression.indexNotValid").to_string()))?;
205+
if let Some(object) = value.as_object() {
206+
if !object.contains_key(index) {
207+
return Err(DscError::Parser(t!("parser.expression.memberNameNotFound", member = index).to_string()));
208+
}
209+
if is_secure {
210+
value = convert_to_secure(&object[index]);
211+
} else {
212+
value = object[index].clone();
213+
}
214+
} else {
215+
return Err(DscError::Parser(t!("parser.expression.accessOnNonObject").to_string()));
216+
}
217+
}
189218
else if !index.is_null() {
190219
return Err(DscError::Parser(t!("parser.expression.invalidIndexType").to_string()));
191220
}

0 commit comments

Comments
 (0)