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]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Add support for safe access (`?.`) in simplexpr (By: oldwomanjosiah)
|
||||||
|
|
||||||
## [0.4.0] (04.09.2022)
|
## [0.4.0] (04.09.2022)
|
||||||
|
|
||||||
### BREAKING CHANGES
|
### BREAKING CHANGES
|
||||||
|
|
|
@ -33,6 +33,13 @@ pub enum UnaryOp {
|
||||||
Negative,
|
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)]
|
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum SimplExpr {
|
pub enum SimplExpr {
|
||||||
Literal(DynVal),
|
Literal(DynVal),
|
||||||
|
@ -43,7 +50,7 @@ pub enum SimplExpr {
|
||||||
BinOp(Span, Box<SimplExpr>, BinOp, Box<SimplExpr>),
|
BinOp(Span, Box<SimplExpr>, BinOp, Box<SimplExpr>),
|
||||||
UnaryOp(Span, UnaryOp, Box<SimplExpr>),
|
UnaryOp(Span, UnaryOp, Box<SimplExpr>),
|
||||||
IfElse(Span, Box<SimplExpr>, Box<SimplExpr>, 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>),
|
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::BinOp(_, l, op, r) => write!(f, "({} {} {})", l, op, r),
|
||||||
SimplExpr::UnaryOp(_, op, x) => write!(f, "{}{}", op, x),
|
SimplExpr::UnaryOp(_, op, x) => write!(f, "{}{}", op, x),
|
||||||
SimplExpr::IfElse(_, a, b, c) => write!(f, "({} ? {} : {})", a, b, c),
|
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) => {
|
SimplExpr::FunctionCall(_, function_name, args) => {
|
||||||
write!(f, "{}({})", function_name, args.iter().join(", "))
|
write!(f, "{}({})", function_name, args.iter().join(", "))
|
||||||
}
|
}
|
||||||
|
@ -101,7 +109,7 @@ impl SimplExpr {
|
||||||
Literal(_) => false,
|
Literal(_) => false,
|
||||||
Concat(_, x) | FunctionCall(_, _, x) | JsonArray(_, x) => x.iter().any(|x| x.references_var(var)),
|
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)),
|
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),
|
UnaryOp(_, _, x) => x.references_var(var),
|
||||||
IfElse(_, a, b, c) => a.references_var(var) || b.references_var(var) || c.references_var(var),
|
IfElse(_, a, b, c) => a.references_var(var) || b.references_var(var) || c.references_var(var),
|
||||||
VarRef(_, x) => x == var,
|
VarRef(_, x) => x == var,
|
||||||
|
@ -113,7 +121,7 @@ impl SimplExpr {
|
||||||
match self {
|
match self {
|
||||||
VarRef(_, x) => dest.push(x.clone()),
|
VarRef(_, x) => dest.push(x.clone()),
|
||||||
UnaryOp(_, _, x) => x.as_ref().collect_var_refs_into(dest),
|
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);
|
a.as_ref().collect_var_refs_into(dest);
|
||||||
b.as_ref().collect_var_refs_into(dest);
|
b.as_ref().collect_var_refs_into(dest);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::{BinOp, SimplExpr, UnaryOp},
|
ast::{AccessType, BinOp, SimplExpr, UnaryOp},
|
||||||
dynval::{ConversionError, DynVal},
|
dynval::{ConversionError, DynVal},
|
||||||
};
|
};
|
||||||
use eww_shared_util::{Span, Spanned, VarName};
|
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, 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)?)
|
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) => {
|
||||||
FunctionCall(span, name, args.into_iter().map(|x| x.try_map_var_refs(f)).collect::<Result<_, _>>()?)
|
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(),
|
Literal(..) => Vec::new(),
|
||||||
VarRef(span, name) => vec![(*span, name)],
|
VarRef(span, name) => vec![(*span, name)],
|
||||||
Concat(_, elems) => elems.iter().flat_map(|x| x.var_refs_with_span().into_iter()).collect(),
|
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();
|
let mut refs = a.var_refs_with_span();
|
||||||
refs.extend(b.var_refs_with_span().iter());
|
refs.extend(b.var_refs_with_span().iter());
|
||||||
refs
|
refs
|
||||||
|
@ -195,8 +197,14 @@ impl SimplExpr {
|
||||||
BinOp::LT => DynVal::from(a.as_f64()? < b.as_f64()?),
|
BinOp::LT => DynVal::from(a.as_f64()? < b.as_f64()?),
|
||||||
BinOp::GE => 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()?),
|
BinOp::LE => DynVal::from(a.as_f64()? <= b.as_f64()?),
|
||||||
#[allow(clippy::useless_conversion)]
|
BinOp::Elvis => {
|
||||||
BinOp::Elvis => DynVal::from(if a.0.is_empty() { b } else { a }),
|
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 => {
|
BinOp::RegexMatch => {
|
||||||
let regex = regex::Regex::new(&b.as_string()?)?;
|
let regex = regex::Regex::new(&b.as_string()?)?;
|
||||||
DynVal::from(regex.is_match(&a.as_string()?))
|
DynVal::from(regex.is_match(&a.as_string()?))
|
||||||
|
@ -218,9 +226,12 @@ impl SimplExpr {
|
||||||
no.eval(values)
|
no.eval(values)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SimplExpr::JsonAccess(span, val, index) => {
|
SimplExpr::JsonAccess(span, safe, val, index) => {
|
||||||
let val = val.eval(values)?;
|
let val = val.eval(values)?;
|
||||||
let index = index.eval(values)?;
|
let index = index.eval(values)?;
|
||||||
|
|
||||||
|
let is_safe = *safe == AccessType::Safe;
|
||||||
|
|
||||||
match val.as_json_value()? {
|
match val.as_json_value()? {
|
||||||
serde_json::Value::Array(val) => {
|
serde_json::Value::Array(val) => {
|
||||||
let index = index.as_i32()?;
|
let index = index.as_i32()?;
|
||||||
|
@ -234,6 +245,10 @@ impl SimplExpr {
|
||||||
.unwrap_or(&serde_json::Value::Null);
|
.unwrap_or(&serde_json::Value::Null);
|
||||||
Ok(DynVal::from(indexed_value).at(*span))
|
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)),
|
_ => 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())),
|
_ => 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,
|
GT,
|
||||||
LT,
|
LT,
|
||||||
Elvis,
|
Elvis,
|
||||||
|
SafeAccess,
|
||||||
RegexMatch,
|
RegexMatch,
|
||||||
|
|
||||||
Not,
|
Not,
|
||||||
|
@ -88,6 +89,7 @@ regex_rules! {
|
||||||
r">" => |_| Token::GT,
|
r">" => |_| Token::GT,
|
||||||
r"<" => |_| Token::LT,
|
r"<" => |_| Token::LT,
|
||||||
r"\?:" => |_| Token::Elvis,
|
r"\?:" => |_| Token::Elvis,
|
||||||
|
r"\?\." => |_| Token::SafeAccess,
|
||||||
r"=~" => |_| Token::RegexMatch,
|
r"=~" => |_| Token::RegexMatch,
|
||||||
|
|
||||||
r"!" => |_| Token::Not,
|
r"!" => |_| Token::Not,
|
||||||
|
@ -318,5 +320,6 @@ mod test {
|
||||||
"${ {"hi": "ho"}.hi }".hi
|
"${ {"hi": "ho"}.hi }".hi
|
||||||
"#),
|
"#),
|
||||||
empty_interpolation => v!(r#""${}""#),
|
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]",
|
"foo.bar[2 + 2] * asdf[foo.bar]",
|
||||||
r#"[1, 2, 3 + 4, "bla", [blub, blo]]"#,
|
r#"[1, 2, 3 + 4, "bla", [blub, blo]]"#,
|
||||||
r#"{ "key": "value", 5: 1+2, true: false }"#,
|
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 eww_shared_util::{Span, VarName};
|
||||||
use crate::parser::lexer::{Token, LexicalError, StrLitSegment, Sp};
|
use crate::parser::lexer::{Token, LexicalError, StrLitSegment, Sp};
|
||||||
use crate::parser::lalrpop_helpers::*;
|
use crate::parser::lalrpop_helpers::*;
|
||||||
|
@ -25,6 +25,7 @@ extern {
|
||||||
">" => Token::GT,
|
">" => Token::GT,
|
||||||
"<" => Token::LT,
|
"<" => Token::LT,
|
||||||
"?:" => Token::Elvis,
|
"?:" => Token::Elvis,
|
||||||
|
"?." => Token::SafeAccess,
|
||||||
"=~" => Token::RegexMatch,
|
"=~" => Token::RegexMatch,
|
||||||
|
|
||||||
"!" => Token::Not,
|
"!" => Token::Not,
|
||||||
|
@ -75,10 +76,16 @@ pub Expr: SimplExpr = {
|
||||||
|
|
||||||
#[precedence(level="1")] #[assoc(side="right")]
|
#[precedence(level="1")] #[assoc(side="right")]
|
||||||
<l:@L> <ident:"identifier"> "(" <args: Comma<ExprReset>> ")" <r:@R> => FunctionCall(Span(l, r, fid), ident, args),
|
<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> => {
|
<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")]
|
#[precedence(level="2")] #[assoc(side="right")]
|
||||||
|
|
|
@ -24,7 +24,14 @@ Supported currently are the following features:
|
||||||
- comparisons (`==`, `!=`, `>`, `<`, `<=`, `>=`)
|
- comparisons (`==`, `!=`, `>`, `<`, `<=`, `>=`)
|
||||||
- boolean operations (`||`, `&&`, `!`)
|
- boolean operations (`||`, `&&`, `!`)
|
||||||
- elvis operator (`?:`)
|
- 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'`)
|
- conditionals (`condition ? 'value' : 'other value'`)
|
||||||
- numbers, strings, booleans and variable references (`12`, `'hi'`, `true`, `some_variable`)
|
- numbers, strings, booleans and variable references (`12`, `'hi'`, `true`, `some_variable`)
|
||||||
- json access (`object.field`, `array[12]`, `object["field"]`)
|
- json access (`object.field`, `array[12]`, `object["field"]`)
|
||||||
|
|
Loading…
Add table
Reference in a new issue