Safe Access Operator (#577)
Co-authored-by: elkowar <5300871+elkowar@users.noreply.github.com>
This commit is contained in:
parent
91d55cb305
commit
37fc231761
9 changed files with 122 additions and 14 deletions
|
@ -5,6 +5,9 @@ All notable changes to eww will be listed here, starting at changes since versio
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Features
|
||||
- Add support for safe access (`?.`) in simplexpr (By: oldwomanjosiah)
|
||||
|
||||
## [0.4.0] (04.09.2022)
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
|
|
@ -33,6 +33,13 @@ pub enum UnaryOp {
|
|||
Negative,
|
||||
}
|
||||
|
||||
/// Differenciates between regular field access (`foo.bar`) and null-safe field access (`foo?.bar`)
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AccessType {
|
||||
Normal,
|
||||
Safe,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SimplExpr {
|
||||
Literal(DynVal),
|
||||
|
@ -43,7 +50,7 @@ pub enum SimplExpr {
|
|||
BinOp(Span, Box<SimplExpr>, BinOp, Box<SimplExpr>),
|
||||
UnaryOp(Span, UnaryOp, Box<SimplExpr>),
|
||||
IfElse(Span, Box<SimplExpr>, Box<SimplExpr>, Box<SimplExpr>),
|
||||
JsonAccess(Span, Box<SimplExpr>, Box<SimplExpr>),
|
||||
JsonAccess(Span, AccessType, Box<SimplExpr>, Box<SimplExpr>),
|
||||
FunctionCall(Span, String, Vec<SimplExpr>),
|
||||
}
|
||||
|
||||
|
@ -65,7 +72,8 @@ impl std::fmt::Display for SimplExpr {
|
|||
SimplExpr::BinOp(_, l, op, r) => write!(f, "({} {} {})", l, op, r),
|
||||
SimplExpr::UnaryOp(_, op, x) => write!(f, "{}{}", op, x),
|
||||
SimplExpr::IfElse(_, a, b, c) => write!(f, "({} ? {} : {})", a, b, c),
|
||||
SimplExpr::JsonAccess(_, value, index) => write!(f, "{}[{}]", value, index),
|
||||
SimplExpr::JsonAccess(_, AccessType::Normal, value, index) => write!(f, "{}[{}]", value, index),
|
||||
SimplExpr::JsonAccess(_, AccessType::Safe, value, index) => write!(f, "{}?.[{}]", value, index),
|
||||
SimplExpr::FunctionCall(_, function_name, args) => {
|
||||
write!(f, "{}({})", function_name, args.iter().join(", "))
|
||||
}
|
||||
|
@ -101,7 +109,7 @@ impl SimplExpr {
|
|||
Literal(_) => false,
|
||||
Concat(_, x) | FunctionCall(_, _, x) | JsonArray(_, x) => x.iter().any(|x| x.references_var(var)),
|
||||
JsonObject(_, x) => x.iter().any(|(k, v)| k.references_var(var) || v.references_var(var)),
|
||||
JsonAccess(_, a, b) | BinOp(_, a, _, b) => a.references_var(var) || b.references_var(var),
|
||||
JsonAccess(_, _, a, b) | BinOp(_, a, _, b) => a.references_var(var) || b.references_var(var),
|
||||
UnaryOp(_, _, x) => x.references_var(var),
|
||||
IfElse(_, a, b, c) => a.references_var(var) || b.references_var(var) || c.references_var(var),
|
||||
VarRef(_, x) => x == var,
|
||||
|
@ -113,7 +121,7 @@ impl SimplExpr {
|
|||
match self {
|
||||
VarRef(_, x) => dest.push(x.clone()),
|
||||
UnaryOp(_, _, x) => x.as_ref().collect_var_refs_into(dest),
|
||||
BinOp(_, a, _, b) | JsonAccess(_, a, b) => {
|
||||
BinOp(_, a, _, b) | JsonAccess(_, _, a, b) => {
|
||||
a.as_ref().collect_var_refs_into(dest);
|
||||
b.as_ref().collect_var_refs_into(dest);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use itertools::Itertools;
|
||||
|
||||
use crate::{
|
||||
ast::{BinOp, SimplExpr, UnaryOp},
|
||||
ast::{AccessType, BinOp, SimplExpr, UnaryOp},
|
||||
dynval::{ConversionError, DynVal},
|
||||
};
|
||||
use eww_shared_util::{Span, Spanned, VarName};
|
||||
|
@ -75,7 +75,9 @@ impl SimplExpr {
|
|||
IfElse(span, box a, box b, box c) => {
|
||||
IfElse(span, box a.try_map_var_refs(f)?, box b.try_map_var_refs(f)?, box c.try_map_var_refs(f)?)
|
||||
}
|
||||
JsonAccess(span, box a, box b) => JsonAccess(span, box a.try_map_var_refs(f)?, box b.try_map_var_refs(f)?),
|
||||
JsonAccess(span, safe, box a, box b) => {
|
||||
JsonAccess(span, safe, box a.try_map_var_refs(f)?, box b.try_map_var_refs(f)?)
|
||||
}
|
||||
FunctionCall(span, name, args) => {
|
||||
FunctionCall(span, name, args.into_iter().map(|x| x.try_map_var_refs(f)).collect::<Result<_, _>>()?)
|
||||
}
|
||||
|
@ -124,7 +126,7 @@ impl SimplExpr {
|
|||
Literal(..) => Vec::new(),
|
||||
VarRef(span, name) => vec![(*span, name)],
|
||||
Concat(_, elems) => elems.iter().flat_map(|x| x.var_refs_with_span().into_iter()).collect(),
|
||||
BinOp(_, box a, _, box b) | JsonAccess(_, box a, box b) => {
|
||||
BinOp(_, box a, _, box b) | JsonAccess(_, _, box a, box b) => {
|
||||
let mut refs = a.var_refs_with_span();
|
||||
refs.extend(b.var_refs_with_span().iter());
|
||||
refs
|
||||
|
@ -195,8 +197,14 @@ impl SimplExpr {
|
|||
BinOp::LT => DynVal::from(a.as_f64()? < b.as_f64()?),
|
||||
BinOp::GE => DynVal::from(a.as_f64()? >= b.as_f64()?),
|
||||
BinOp::LE => DynVal::from(a.as_f64()? <= b.as_f64()?),
|
||||
#[allow(clippy::useless_conversion)]
|
||||
BinOp::Elvis => DynVal::from(if a.0.is_empty() { b } else { a }),
|
||||
BinOp::Elvis => {
|
||||
let is_null = matches!(serde_json::from_str(&a.0), Ok(serde_json::Value::Null));
|
||||
if a.0.is_empty() || is_null {
|
||||
b
|
||||
} else {
|
||||
a
|
||||
}
|
||||
}
|
||||
BinOp::RegexMatch => {
|
||||
let regex = regex::Regex::new(&b.as_string()?)?;
|
||||
DynVal::from(regex.is_match(&a.as_string()?))
|
||||
|
@ -218,9 +226,12 @@ impl SimplExpr {
|
|||
no.eval(values)
|
||||
}
|
||||
}
|
||||
SimplExpr::JsonAccess(span, val, index) => {
|
||||
SimplExpr::JsonAccess(span, safe, val, index) => {
|
||||
let val = val.eval(values)?;
|
||||
let index = index.eval(values)?;
|
||||
|
||||
let is_safe = *safe == AccessType::Safe;
|
||||
|
||||
match val.as_json_value()? {
|
||||
serde_json::Value::Array(val) => {
|
||||
let index = index.as_i32()?;
|
||||
|
@ -234,6 +245,10 @@ impl SimplExpr {
|
|||
.unwrap_or(&serde_json::Value::Null);
|
||||
Ok(DynVal::from(indexed_value).at(*span))
|
||||
}
|
||||
serde_json::Value::String(val) if val.is_empty() && is_safe => {
|
||||
Ok(DynVal::from(&serde_json::Value::Null).at(*span))
|
||||
}
|
||||
serde_json::Value::Null if is_safe => Ok(DynVal::from(&serde_json::Value::Null).at(*span)),
|
||||
_ => Err(EvalError::CannotIndex(format!("{}", val)).at(*span)),
|
||||
}
|
||||
}
|
||||
|
@ -330,3 +345,55 @@ fn call_expr_function(name: &str, args: Vec<DynVal>) -> Result<DynVal, EvalError
|
|||
_ => Err(EvalError::UnknownFunction(name.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::dynval::DynVal;
|
||||
|
||||
macro_rules! evals_as {
|
||||
($name:ident($simplexpr:expr) => $expected:expr $(,)?) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let expected: Result<$crate::dynval::DynVal, $crate::eval::EvalError> = $expected;
|
||||
|
||||
let parsed = match $crate::parser::parse_string(0, 0, $simplexpr.into()) {
|
||||
Ok(it) => it,
|
||||
Err(e) => {
|
||||
panic!("Could not parse input as SimpleExpr\nInput: {}\nReason: {}", stringify!($simplexpr), e);
|
||||
}
|
||||
};
|
||||
|
||||
eprintln!("Parsed as {parsed:#?}");
|
||||
|
||||
let output = parsed.eval_no_vars();
|
||||
|
||||
match expected {
|
||||
Ok(expected) => {
|
||||
let actual = output.expect("Output was not Ok(_)");
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
Err(expected) => {
|
||||
let actual = output.expect_err("Output was not Err(_)").to_string();
|
||||
let expected = expected.to_string();
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident($simplexpr:expr) => $expected:expr, $($tt:tt)+) => {
|
||||
evals_as!($name($simplexpr) => $expected);
|
||||
evals_as!($($tt)*);
|
||||
}
|
||||
}
|
||||
|
||||
evals_as! {
|
||||
string_to_string(r#""Hello""#) => Ok(DynVal::from("Hello".to_string())),
|
||||
safe_access_to_existing(r#"{ "a": { "b": 2 } }.a?.b"#) => Ok(DynVal::from(2)),
|
||||
safe_access_to_missing(r#"{ "a": { "b": 2 } }.b?.b"#) => Ok(DynVal::from(&serde_json::Value::Null)),
|
||||
normal_access_to_existing(r#"{ "a": { "b": 2 } }.a.b"#) => Ok(DynVal::from(2)),
|
||||
normal_access_to_missing(r#"{ "a": { "b": 2 } }.b.b"#) => Err(super::EvalError::CannotIndex("null".to_string())),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ pub enum Token {
|
|||
GT,
|
||||
LT,
|
||||
Elvis,
|
||||
SafeAccess,
|
||||
RegexMatch,
|
||||
|
||||
Not,
|
||||
|
@ -88,6 +89,7 @@ regex_rules! {
|
|||
r">" => |_| Token::GT,
|
||||
r"<" => |_| Token::LT,
|
||||
r"\?:" => |_| Token::Elvis,
|
||||
r"\?\." => |_| Token::SafeAccess,
|
||||
r"=~" => |_| Token::RegexMatch,
|
||||
|
||||
r"!" => |_| Token::Not,
|
||||
|
@ -318,5 +320,6 @@ mod test {
|
|||
"${ {"hi": "ho"}.hi }".hi
|
||||
"#),
|
||||
empty_interpolation => v!(r#""${}""#),
|
||||
safe_interpolation => v!(r#""${ { "key": "value" }.key1?.key2 ?: "Recovery" }""#),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ mod tests {
|
|||
"foo.bar[2 + 2] * asdf[foo.bar]",
|
||||
r#"[1, 2, 3 + 4, "bla", [blub, blo]]"#,
|
||||
r#"{ "key": "value", 5: 1+2, true: false }"#,
|
||||
r#"{ "key": "value" }?.key?.does_not_exist"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
source: crates/simplexpr/src/parser/lexer.rs
|
||||
expression: "v!(r#\"\"${ { \"key\": \"value\" }.key1?.key2 ?: \"Recovery\" }\"\"#)"
|
||||
---
|
||||
(0, StringLit([(0, Literal(""), 3), (3, Interp([(4, LCurl, 5), (6, StringLit([(6, Literal("key"), 11)]), 11), (11, Colon, 12), (13, StringLit([(13, Literal("value"), 20)]), 20), (21, RCurl, 22), (22, Dot, 23), (23, Ident("key1"), 27), (27, SafeAccess, 29), (29, Ident("key2"), 33), (34, Elvis, 36), (37, StringLit([(37, Literal("Recovery"), 47)]), 47)]), 48), (48, Literal(""), 50)]), 50)
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
source: crates/simplexpr/src/parser/mod.rs
|
||||
expression: "p.parse(0, Lexer::new(0, 0, r#\"{ \"key\": \"value\" }?.key?.does_not_exist\"#))"
|
||||
---
|
||||
Ok(
|
||||
{"key": "value"}?.["key"]?.["does_not_exist"],
|
||||
)
|
|
@ -1,4 +1,4 @@
|
|||
use crate::ast::{SimplExpr::{self, *}, BinOp::*, UnaryOp::*};
|
||||
use crate::ast::{SimplExpr::{self, *}, BinOp::*, UnaryOp::*, AccessType};
|
||||
use eww_shared_util::{Span, VarName};
|
||||
use crate::parser::lexer::{Token, LexicalError, StrLitSegment, Sp};
|
||||
use crate::parser::lalrpop_helpers::*;
|
||||
|
@ -25,6 +25,7 @@ extern {
|
|||
">" => Token::GT,
|
||||
"<" => Token::LT,
|
||||
"?:" => Token::Elvis,
|
||||
"?." => Token::SafeAccess,
|
||||
"=~" => Token::RegexMatch,
|
||||
|
||||
"!" => Token::Not,
|
||||
|
@ -75,10 +76,16 @@ pub Expr: SimplExpr = {
|
|||
|
||||
#[precedence(level="1")] #[assoc(side="right")]
|
||||
<l:@L> <ident:"identifier"> "(" <args: Comma<ExprReset>> ")" <r:@R> => FunctionCall(Span(l, r, fid), ident, args),
|
||||
<l:@L> <value:Expr> "[" <index: ExprReset> "]" <r:@R> => JsonAccess(Span(l, r, fid), b(value), b(index)),
|
||||
<l:@L> <value:Expr> "[" <index: ExprReset> "]" <r:@R> => {
|
||||
JsonAccess(Span(l, r, fid), AccessType::Normal, b(value), b(index))
|
||||
},
|
||||
|
||||
<l:@L> <value:Expr> "." <lit_l:@L> <index:"identifier"> <r:@R> => {
|
||||
JsonAccess(Span(l, r, fid), b(value), b(Literal(index.into())))
|
||||
JsonAccess(Span(l, r, fid), AccessType::Normal, b(value), b(Literal(index.into())))
|
||||
},
|
||||
|
||||
<l:@L> <value:Expr> "?." <lit_l:@L> <index:"identifier"> <r:@R> => {
|
||||
JsonAccess(Span(l, r, fid), AccessType::Safe, b(value), b(Literal(index.into())))
|
||||
},
|
||||
|
||||
#[precedence(level="2")] #[assoc(side="right")]
|
||||
|
|
|
@ -24,7 +24,14 @@ Supported currently are the following features:
|
|||
- comparisons (`==`, `!=`, `>`, `<`, `<=`, `>=`)
|
||||
- boolean operations (`||`, `&&`, `!`)
|
||||
- elvis operator (`?:`)
|
||||
- if the left side is `""`, then returns the right side, otherwise evaluates to the left side.
|
||||
- if the left side is `""` or a JSON `null`, then returns the right side,
|
||||
otherwise evaluates to the left side.
|
||||
- Safe Access operator (`?.`)
|
||||
- if the left side is `""` or a JSON `null`, then return `null`. Otherwise,
|
||||
attempt to index.
|
||||
- This can still cause an error to occur if the left hand side exists but is
|
||||
not an object.
|
||||
(`Number` or `String`).
|
||||
- conditionals (`condition ? 'value' : 'other value'`)
|
||||
- numbers, strings, booleans and variable references (`12`, `'hi'`, `true`, `some_variable`)
|
||||
- json access (`object.field`, `array[12]`, `object["field"]`)
|
||||
|
|
Loading…
Add table
Reference in a new issue