Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
- name: Test jmespath
run: cargo test

- name: Test jmespath with let-expr feature
run: cargo test --features let-expr

- name: Test jmespath with specialized feature (nightly only)
if: matrix.rust == 'nightly'
run: cargo +nightly test --features specialized
Expand Down
4 changes: 4 additions & 0 deletions jmespath/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ sync = []
# however at time of writing it is unstable & so requires a nightly compiler.
# See https://github.com/rust-lang/rust/issues/31844 for the latest status.
specialized = []
# `let-expr` enables JEP-18 lexical scoping with let expressions.
# Syntax: let $var = expr in body
# See: https://github.com/jmespath/jmespath.jep/blob/main/proposals/0018-lexical-scope.md
let-expr = []
18 changes: 18 additions & 0 deletions jmespath/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,24 @@ pub enum Ast {
/// Right hand side of the expression.
rhs: Box<Ast>,
},
#[cfg(feature = "let-expr")]
/// Variable reference (JEP-18): $name
VariableRef {
/// Approximate absolute position in the parsed expression.
offset: usize,
/// Variable name (without the $ prefix)
name: String,
},
#[cfg(feature = "let-expr")]
/// Let expression (JEP-18): let $var = expr, ... in body
Let {
/// Approximate absolute position in the parsed expression.
offset: usize,
/// Variable bindings: name -> expression
bindings: Vec<(String, Ast)>,
/// Body expression to evaluate with bindings in scope
expr: Box<Ast>,
},
}

impl fmt::Display for Ast {
Expand Down
40 changes: 40 additions & 0 deletions jmespath/src/interpreter.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Interprets JMESPath expressions.

use std::collections::BTreeMap;
#[cfg(feature = "let-expr")]
use std::collections::HashMap;

use super::Context;
use super::ast::Ast;
Expand Down Expand Up @@ -181,5 +183,43 @@ pub fn interpret(data: &Rcvar, node: &Ast, ctx: &mut Context<'_>) -> SearchResul
}
}
}
// JEP-18: Variable reference - look up variable in scope stack
#[cfg(feature = "let-expr")]
Ast::VariableRef { ref name, offset } => match ctx.get_variable(name) {
Some(value) => Ok(value),
None => {
ctx.offset = offset;
let reason = ErrorReason::Runtime(RuntimeError::UnknownFunction(format!(
"Undefined variable: ${}",
name
)));
Err(JmespathError::from_ctx(ctx, reason))
}
},
// JEP-18: Let expression - evaluate bindings and body with new scope
#[cfg(feature = "let-expr")]
Ast::Let {
ref bindings,
ref expr,
..
} => {
// Evaluate all bindings and create a new scope
let mut scope = HashMap::new();
for (name, binding_expr) in bindings {
let value = interpret(data, binding_expr, ctx)?;
scope.insert(name.clone(), value);
}

// Push the new scope
ctx.push_scope(scope);

// Evaluate the body expression
let result = interpret(data, expr, ctx);

// Pop the scope (even if there was an error)
ctx.pop_scope();

result
}
}
}
50 changes: 47 additions & 3 deletions jmespath/src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ pub enum Token {
Lbrace,
Rbrace,
Eof,
#[cfg(feature = "let-expr")]
/// Assignment operator for let bindings (JEP-18): =
Assign,
#[cfg(feature = "let-expr")]
/// Variable reference (JEP-18): $name
Variable(String),
}

impl Token {
Expand Down Expand Up @@ -126,8 +132,14 @@ impl<'a> Lexer<'a> {
'"' => tokens.push_back((pos, self.consume_quoted_identifier(pos)?)),
'\'' => tokens.push_back((pos, self.consume_raw_string(pos)?)),
'`' => tokens.push_back((pos, self.consume_literal(pos)?)),
'=' => match self.iter.next() {
Some((_, '=')) => tokens.push_back((pos, Eq)),
'=' => match self.iter.peek() {
Some(&(_, '=')) => {
self.iter.next();
tokens.push_back((pos, Eq))
}
#[cfg(feature = "let-expr")]
_ => tokens.push_back((pos, Assign)),
#[cfg(not(feature = "let-expr"))]
_ => {
let message = "'=' is not valid. Did you mean '=='?";
let reason = ErrorReason::Parse(message.to_owned());
Expand All @@ -139,6 +151,8 @@ impl<'a> Lexer<'a> {
'!' => tokens.push_back((pos, self.alt('=', Ne, Not))),
'0'..='9' => tokens.push_back((pos, self.consume_number(pos, ch, false)?)),
'-' => tokens.push_back((pos, self.consume_negative_number(pos)?)),
#[cfg(feature = "let-expr")]
'$' => tokens.push_back((pos, self.consume_variable(pos)?)),
// Skip whitespace tokens
' ' | '\n' | '\t' | '\r' => {}
c => {
Expand Down Expand Up @@ -232,6 +246,27 @@ impl<'a> Lexer<'a> {
}
}

// Consumes a variable reference: $identifier (JEP-18)
#[cfg(feature = "let-expr")]
#[inline]
fn consume_variable(&mut self, pos: usize) -> Result<Token, JmespathError> {
// Peek at the first character to start the identifier
match self.iter.peek() {
Some(&(_, 'a'..='z' | 'A'..='Z' | '_')) => {
let name = self.consume_while(
String::new(),
|c| matches!(c, 'a'..='z' | '_' | 'A'..='Z' | '0'..='9'),
);
Ok(Variable(name))
}
_ => {
let reason =
ErrorReason::Parse("'$' must be followed by a valid identifier".to_owned());
Err(JmespathError::new(self.expr, pos, reason))
}
}
}

// Consumes tokens inside of a closing character. The closing character
// can be escaped using a "\" character.
#[inline]
Expand Down Expand Up @@ -378,7 +413,16 @@ mod tests {
}

#[test]
fn ensures_eq_valid() {
#[cfg(feature = "let-expr")]
fn ensures_eq_valid_with_let_expr() {
// With JEP-18, single = is now valid (Assign token for let bindings)
assert!(tokenize("=").is_ok());
}

#[test]
#[cfg(not(feature = "let-expr"))]
fn ensures_eq_invalid_without_let_expr() {
// Without JEP-18, single = is an error
assert!(tokenize("=").is_err());
}

Expand Down
Loading