Skip to content

Commit affa724

Browse files
committed
Add array index syntax support to expressions
1 parent 5c1b34b commit affa724

File tree

11 files changed

+1796
-392
lines changed

11 files changed

+1796
-392
lines changed

dsc/src/subcommand.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub fn config_get(configurator: &mut Configurator, format: &Option<OutputFormat>
4949
}
5050
},
5151
Err(err) => {
52-
error!("Error: {err}");
52+
error!("{err}");
5353
exit(EXIT_DSC_ERROR);
5454
}
5555
}

dsc/tests/dsc_expressions.tests.ps1

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'Expressions tests' {
5+
It 'Accessors work: <text>' -TestCases @(
6+
@{ text = "[parameters('test').hello]"; expected = '@{world=there}' }
7+
@{ text = "[parameters('test').hello.world]"; expected = 'there' }
8+
) {
9+
param($text, $expected)
10+
$yaml = @"
11+
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
12+
parameters:
13+
test:
14+
type: object
15+
defaultValue:
16+
hello:
17+
world: there
18+
array:
19+
- one
20+
- [ 'two', 'three' ]
21+
objectArray:
22+
- name: one
23+
value: 1
24+
- name: two
25+
value:
26+
- 2
27+
- nestedObject:
28+
name: three
29+
value: 3
30+
resources:
31+
- name: echo
32+
type: Test/Echo
33+
properties:
34+
output: "$text"
35+
"@
36+
$debug = $yaml | dsc -l debug config get -f yaml 2>&1 | Out-String
37+
$out = $yaml | dsc config get | ConvertFrom-Json
38+
$LASTEXITCODE | Should -Be 0
39+
$out.results[0].result.actualState.output | Should -Be $expected -Because $debug
40+
}
41+
42+
It 'Invalid expressions: <expression>' -TestCases @(
43+
@{ expression = "[concat('A','B')].hello" }
44+
@{ expression = "[concat('A','B')](0)" }
45+
@{ expression = "[concat('a','b').hello]" }
46+
@{ expression = "[concat('a','b')[0]]" }
47+
) {
48+
param($expression)
49+
$yaml = @"
50+
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
51+
resources:
52+
- name: echo
53+
type: Test/Echo
54+
properties:
55+
output: "$expression"
56+
"@
57+
$out = dsc config get -d $yaml 2>&1
58+
$LASTEXITCODE | Should -Be 2
59+
$out | Should -BeLike "*ERROR*"
60+
}
61+
}

dsc_lib/src/parser/expressions.rs

Lines changed: 92 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ use crate::dscerror::DscError;
1010
use crate::functions::FunctionDispatcher;
1111
use crate::parser::functions::Function;
1212

13+
#[derive(Clone)]
14+
pub enum Accessor {
15+
Member(String),
16+
Index(Value),
17+
}
18+
1319
#[derive(Clone)]
1420
pub struct Expression {
1521
function: Function,
16-
member_access: Option<Vec<String>>,
22+
accessors: Vec<Accessor>,
1723
}
1824

1925
impl Expression {
@@ -32,57 +38,110 @@ impl Expression {
3238
let Some(function) = expression.child_by_field_name("function") else {
3339
return Err(DscError::Parser("Function node not found".to_string()));
3440
};
41+
debug!("Parsing function '{:?}'", function);
3542
let function = Function::new(statement_bytes, &function)?;
36-
let member_access = if let Some(members) = expression.child_by_field_name("members") {
37-
if members.is_error() {
38-
return Err(DscError::Parser("Error parsing dot-notation".to_string()));
43+
let mut accessors = Vec::<Accessor>::new();
44+
if let Some(accessor) = expression.child_by_field_name("accessor") {
45+
debug!("Parsing accessor '{:?}'", accessor);
46+
if accessor.is_error() {
47+
return Err(DscError::Parser("Error parsing accessor".to_string()));
3948
}
40-
let mut result = vec![];
41-
let mut cursor = members.walk();
42-
for member in members.named_children(&mut cursor) {
43-
if member.is_error() {
44-
return Err(DscError::Parser("Error parsing dot-notation member".to_string()));
49+
let mut cursor = accessor.walk();
50+
for accessor in accessor.named_children(&mut cursor) {
51+
if accessor.is_error() {
52+
return Err(DscError::Parser("Error parsing accessor".to_string()));
4553
}
46-
let value = member.utf8_text(statement_bytes)?;
47-
result.push(value.to_string());
54+
let accessor_kind = accessor.kind();
55+
let value = match accessor_kind {
56+
"memberAccess" => {
57+
debug!("Parsing member accessor '{:?}'", accessor);
58+
let Some(member_name) = accessor.child_by_field_name("name") else {
59+
return Err(DscError::Parser("Member name not found".to_string()));
60+
};
61+
let member = member_name.utf8_text(statement_bytes)?;
62+
Accessor::Member(member.to_string())
63+
},
64+
"index" => {
65+
debug!("Parsing index accessor '{:?}'", accessor);
66+
let Some(index_value) = accessor.child_by_field_name("indexValue") else {
67+
return Err(DscError::Parser("Index value not found".to_string()));
68+
};
69+
match index_value.kind() {
70+
"number" => {
71+
let value = index_value.utf8_text(statement_bytes)?;
72+
let value = serde_json::from_str(value)?;
73+
Accessor::Index(value)
74+
},
75+
"expression" => {
76+
return Err(DscError::Parser("Expression index not supported".to_string()));
77+
},
78+
_ => {
79+
return Err(DscError::Parser(format!("Invalid accessor kind: '{:?}'", accessor_kind)));
80+
},
81+
}
82+
},
83+
_ => {
84+
return Err(DscError::Parser(format!("Invalid accessor kind: '{:?}'", accessor_kind)));
85+
},
86+
};
87+
accessors.push(value);
4888
}
49-
Some(result)
5089
}
51-
else {
52-
None
53-
};
90+
5491
Ok(Expression {
5592
function,
56-
member_access,
93+
accessors,
5794
})
5895
}
5996

6097
/// Invoke the expression.
6198
///
99+
/// # Arguments
100+
///
101+
/// * `function_dispatcher` - The function dispatcher to use.
102+
/// * `context` - The context to use.
103+
///
104+
/// # Returns
105+
///
106+
/// The result of the expression.
107+
///
62108
/// # Errors
63109
///
64110
/// This function will return an error if the expression fails to execute.
65111
pub fn invoke(&self, function_dispatcher: &FunctionDispatcher, context: &Context) -> Result<Value, DscError> {
66112
let result = self.function.invoke(function_dispatcher, context)?;
67113
trace!("Function result: '{:?}'", result);
68-
if let Some(member_access) = &self.member_access {
69-
debug!("Evaluating member access '{:?}'", member_access);
70-
if !result.is_object() {
71-
return Err(DscError::Parser("Member access on non-object value".to_string()));
72-
}
73-
114+
if self.accessors.len() > 0 {
115+
debug!("Evaluating accessors");
74116
let mut value = result;
75-
for member in member_access {
76-
if !value.is_object() {
77-
return Err(DscError::Parser(format!("Member access '{member}' on non-object value")));
78-
}
79-
80-
if let Some(object) = value.as_object() {
81-
if !object.contains_key(member) {
82-
return Err(DscError::Parser(format!("Member '{member}' not found")));
83-
}
84-
85-
value = object[member].clone();
117+
for accessor in &self.accessors {
118+
match accessor {
119+
Accessor::Member(member) => {
120+
if !value.is_object() {
121+
return Err(DscError::Parser("Member access on non-object value".to_string()));
122+
}
123+
if let Some(object) = value.as_object() {
124+
if !object.contains_key(member) {
125+
return Err(DscError::Parser(format!("Member '{:?}' not found", member)));
126+
}
127+
value = object[member].clone();
128+
}
129+
},
130+
Accessor::Index(index) => {
131+
if !value.is_array() {
132+
return Err(DscError::Parser("Index access on non-array value".to_string()));
133+
}
134+
if let Some(array) = value.as_array() {
135+
if !index.is_number() {
136+
return Err(DscError::Parser("Index is not a number".to_string()));
137+
}
138+
let index = index.as_u64().unwrap() as usize;
139+
if index >= array.len() {
140+
return Err(DscError::Parser("Index out of bounds".to_string()));
141+
}
142+
value = array[index].clone();
143+
}
144+
},
86145
}
87146
}
88147

dsc_lib/src/parser/mod.rs

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -52,43 +52,45 @@ impl Statement {
5252
if root_node.is_error() {
5353
return Err(DscError::Parser(format!("Error parsing statement root: {statement}")));
5454
}
55-
let root_node_kind = root_node.kind();
56-
if root_node_kind != "statement" {
55+
if root_node.kind() != "statement" {
5756
return Err(DscError::Parser(format!("Invalid statement: {statement}")));
5857
}
59-
let Some(child_node) = root_node.named_child(0) else {
60-
return Err(DscError::Parser("Child node not found".to_string()));
61-
};
62-
if child_node.is_error() {
63-
return Err(DscError::Parser("Error parsing statement child".to_string()));
64-
}
65-
let kind = child_node.kind();
6658
let statement_bytes = statement.as_bytes();
67-
match kind {
68-
"stringLiteral" | "bracketInStringLiteral" => {
69-
let Ok(value) = child_node.utf8_text(statement_bytes) else {
70-
return Err(DscError::Parser("Error parsing string literal".to_string()));
71-
};
72-
debug!("Parsing string literal: {0}", value.to_string());
73-
Ok(Value::String(value.to_string()))
74-
},
75-
"escapedStringLiteral" => {
76-
// need to remove the first character: [[ => [
77-
let Ok(value) = child_node.utf8_text(statement_bytes) else {
78-
return Err(DscError::Parser("Error parsing escaped string literal".to_string()));
79-
};
80-
debug!("Parsing escaped string literal: {0}", value[1..].to_string());
81-
Ok(Value::String(value[1..].to_string()))
82-
},
83-
"expression" => {
84-
debug!("Parsing expression");
85-
let expression = Expression::new(statement_bytes, &child_node)?;
86-
Ok(expression.invoke(&self.function_dispatcher, context)?)
87-
},
88-
_ => {
89-
Err(DscError::Parser(format!("Unknown expression type {0}", child_node.kind())))
59+
let mut cursor = root_node.walk();
60+
let mut return_value = Value::Null;
61+
for child_node in root_node.named_children(&mut cursor) {
62+
if child_node.is_error() {
63+
return Err(DscError::Parser(format!("Error parsing statement: {statement}")));
64+
}
65+
66+
match child_node.kind() {
67+
"stringLiteral" => {
68+
let Ok(value) = child_node.utf8_text(statement_bytes) else {
69+
return Err(DscError::Parser("Error parsing string literal".to_string()));
70+
};
71+
debug!("Parsing string literal: {0}", value.to_string());
72+
return_value = Value::String(value.to_string());
73+
},
74+
"escapedStringLiteral" => {
75+
// need to remove the first character: [[ => [
76+
let Ok(value) = child_node.utf8_text(statement_bytes) else {
77+
return Err(DscError::Parser("Error parsing escaped string literal".to_string()));
78+
};
79+
debug!("Parsing escaped string literal: {0}", value[1..].to_string());
80+
return_value = Value::String(value[1..].to_string());
81+
},
82+
"expression" => {
83+
debug!("Parsing expression");
84+
let expression = Expression::new(statement_bytes, &child_node)?;
85+
return_value = expression.invoke(&self.function_dispatcher, context)?;
86+
},
87+
_ => {
88+
return Err(DscError::Parser(format!("Unknown expression type {0}", child_node.kind())));
89+
}
9090
}
9191
}
92+
93+
Ok(return_value)
9294
}
9395
}
9496

@@ -113,8 +115,8 @@ mod tests {
113115
#[test]
114116
fn bracket_in_string() {
115117
let mut parser = Statement::new().unwrap();
116-
let result = parser.parse_and_execute("[this] is a string", &Context::new()).unwrap();
117-
assert_eq!(result, "[this] is a string");
118+
let result = parser.parse_and_execute("[this] is a string", &Context::new());
119+
assert!(result.is_err());
118120
}
119121

120122
#[test]

tree-sitter-dscexpression/build.ps1

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ function Invoke-NativeCommand($cmd) {
1010
}
1111
}
1212

13+
$env:TREE_SITTER_VERBOSE=1
14+
1315
if ($null -eq (Get-Command npm -ErrorAction Ignore)) {
1416
Write-Host 'Installing Node'
1517

@@ -30,5 +32,5 @@ if ($LASTEXITCODE -ne 0) {
3032
npm ci tree-sitter-cli --omit=optional
3133
}
3234

33-
Invoke-NativeCommand 'npx tree-sitter generate'
35+
Invoke-NativeCommand 'npx tree-sitter generate --build'
3436
Invoke-NativeCommand 'npx tree-sitter test'

0 commit comments

Comments
 (0)