2023-10-19 17:26:52 +00:00
|
|
|
use crate::SharedString;
|
2023-10-21 15:52:47 +00:00
|
|
|
use anyhow::{anyhow, Context, Result};
|
2023-10-19 17:03:10 +00:00
|
|
|
use collections::{HashMap, HashSet};
|
2023-11-08 03:58:37 +00:00
|
|
|
use lazy_static::lazy_static;
|
|
|
|
use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
|
2023-10-21 15:52:47 +00:00
|
|
|
use serde::Deserialize;
|
2023-11-09 16:54:05 +00:00
|
|
|
use std::any::{type_name, Any, TypeId};
|
2023-10-19 17:26:52 +00:00
|
|
|
|
2023-11-08 04:26:51 +00:00
|
|
|
/// Actions are used to implement keyboard-driven UI.
|
|
|
|
/// When you declare an action, you can bind keys to the action in the keymap and
|
|
|
|
/// listeners for that action in the element tree.
|
|
|
|
///
|
|
|
|
/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit struct
|
|
|
|
/// action for each listed action name.
|
|
|
|
/// ```rust
|
|
|
|
/// actions!(MoveUp, MoveDown, MoveLeft, MoveRight, Newline);
|
|
|
|
/// ```
|
|
|
|
/// More complex data types can also be actions. If you annotate your type with the `#[action]` proc macro,
|
|
|
|
/// it will automatically
|
|
|
|
/// ```
|
|
|
|
/// #[action]
|
|
|
|
/// pub struct SelectNext {
|
|
|
|
/// pub replace_newest: bool,
|
|
|
|
/// }
|
|
|
|
///
|
|
|
|
/// Any type A that satisfies the following bounds is automatically an action:
|
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
|
|
|
|
/// ```
|
|
|
|
///
|
|
|
|
/// The `#[action]` annotation will derive these implementations for your struct automatically. If you
|
|
|
|
/// want to control them manually, you can use the lower-level `#[register_action]` macro, which only
|
|
|
|
/// generates the code needed to register your action before `main`. Then you'll need to implement all
|
|
|
|
/// the traits manually.
|
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// #[gpui::register_action]
|
|
|
|
/// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::fmt::Debug)]
|
|
|
|
/// pub struct Paste {
|
|
|
|
/// pub content: SharedString,
|
|
|
|
/// }
|
|
|
|
///
|
|
|
|
/// impl std::default::Default for Paste {
|
|
|
|
/// fn default() -> Self {
|
|
|
|
/// Self {
|
|
|
|
/// content: SharedString::from("🍝"),
|
|
|
|
/// }
|
|
|
|
/// }
|
|
|
|
/// }
|
|
|
|
/// ```
|
2023-11-07 16:00:30 +00:00
|
|
|
pub trait Action: std::fmt::Debug + 'static {
|
2023-10-21 15:52:47 +00:00
|
|
|
fn qualified_name() -> SharedString
|
|
|
|
where
|
|
|
|
Self: Sized;
|
|
|
|
fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
|
|
|
|
where
|
|
|
|
Self: Sized;
|
|
|
|
|
2023-10-20 13:13:53 +00:00
|
|
|
fn partial_eq(&self, action: &dyn Action) -> bool;
|
2023-10-19 17:26:52 +00:00
|
|
|
fn boxed_clone(&self) -> Box<dyn Action>;
|
|
|
|
fn as_any(&self) -> &dyn Any;
|
|
|
|
}
|
2023-10-19 17:03:10 +00:00
|
|
|
|
2023-11-08 04:26:51 +00:00
|
|
|
// Types become actions by satisfying a list of trait bounds.
|
|
|
|
impl<A> Action for A
|
|
|
|
where
|
|
|
|
A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
|
|
|
|
{
|
|
|
|
fn qualified_name() -> SharedString {
|
|
|
|
// todo!() remove the 2 replacement when migration is done
|
|
|
|
type_name::<A>().replace("2::", "::").into()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
|
|
|
let action = if let Some(params) = params {
|
|
|
|
serde_json::from_value(params).context("failed to deserialize action")?
|
|
|
|
} else {
|
|
|
|
Self::default()
|
|
|
|
};
|
|
|
|
Ok(Box::new(action))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn partial_eq(&self, action: &dyn Action) -> bool {
|
|
|
|
action
|
|
|
|
.as_any()
|
|
|
|
.downcast_ref::<Self>()
|
|
|
|
.map_or(false, |a| self == a)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn boxed_clone(&self) -> Box<dyn Action> {
|
|
|
|
Box::new(self.clone())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn as_any(&self) -> &dyn Any {
|
|
|
|
self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-09 16:54:05 +00:00
|
|
|
impl dyn Action {
|
|
|
|
pub fn type_id(&self) -> TypeId {
|
|
|
|
self.as_any().type_id()
|
|
|
|
}
|
|
|
|
}
|
2023-11-08 03:23:02 +00:00
|
|
|
type ActionBuilder = fn(json: Option<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
|
|
|
|
|
2023-11-08 03:58:37 +00:00
|
|
|
lazy_static! {
|
|
|
|
static ref ACTION_REGISTRY: RwLock<ActionRegistry> = RwLock::default();
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
struct ActionRegistry {
|
|
|
|
builders_by_name: HashMap<SharedString, ActionBuilder>,
|
|
|
|
all_names: Vec<SharedString>, // So we can return a static slice.
|
|
|
|
}
|
2023-11-08 03:23:02 +00:00
|
|
|
|
|
|
|
/// Register an action type to allow it to be referenced in keymaps.
|
|
|
|
pub fn register_action<A: Action>() {
|
2023-11-08 03:58:37 +00:00
|
|
|
let name = A::qualified_name();
|
|
|
|
let mut lock = ACTION_REGISTRY.write();
|
|
|
|
lock.builders_by_name.insert(name.clone(), A::build);
|
|
|
|
lock.all_names.push(name);
|
2023-11-08 03:23:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
|
|
|
|
pub fn build_action(name: &str, params: Option<serde_json::Value>) -> Result<Box<dyn Action>> {
|
2023-11-08 03:58:37 +00:00
|
|
|
let lock = ACTION_REGISTRY.read();
|
2023-11-08 20:49:09 +00:00
|
|
|
|
2023-11-08 03:23:02 +00:00
|
|
|
let build_action = lock
|
2023-11-08 03:58:37 +00:00
|
|
|
.builders_by_name
|
2023-11-08 03:23:02 +00:00
|
|
|
.get(name)
|
|
|
|
.ok_or_else(|| anyhow!("no action type registered for {}", name))?;
|
|
|
|
(build_action)(params)
|
|
|
|
}
|
|
|
|
|
2023-11-08 03:58:37 +00:00
|
|
|
pub fn all_action_names() -> MappedRwLockReadGuard<'static, [SharedString]> {
|
|
|
|
let lock = ACTION_REGISTRY.read();
|
|
|
|
RwLockReadGuard::map(lock, |registry: &ActionRegistry| {
|
|
|
|
registry.all_names.as_slice()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-11-08 04:48:47 +00:00
|
|
|
/// Defines unit structs that can be used as actions.
|
|
|
|
/// To use more complex data types as actions, annotate your type with the #[action] macro.
|
2023-11-07 16:00:30 +00:00
|
|
|
#[macro_export]
|
|
|
|
macro_rules! actions {
|
|
|
|
() => {};
|
|
|
|
|
|
|
|
( $name:ident ) => {
|
2023-11-08 03:23:02 +00:00
|
|
|
#[gpui::register_action]
|
2023-11-07 16:00:30 +00:00
|
|
|
#[derive(::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq, $crate::serde::Deserialize)]
|
2023-11-07 17:48:08 +00:00
|
|
|
pub struct $name;
|
2023-11-07 16:00:30 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
( $name:ident, $($rest:tt)* ) => {
|
|
|
|
actions!($name);
|
|
|
|
actions!($($rest)*);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-10-19 17:03:10 +00:00
|
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
2023-10-19 19:28:20 +00:00
|
|
|
pub struct DispatchContext {
|
2023-10-19 17:26:52 +00:00
|
|
|
set: HashSet<SharedString>,
|
|
|
|
map: HashMap<SharedString, SharedString>,
|
2023-10-19 17:03:10 +00:00
|
|
|
}
|
|
|
|
|
2023-10-20 09:44:19 +00:00
|
|
|
impl<'a> TryFrom<&'a str> for DispatchContext {
|
|
|
|
type Error = anyhow::Error;
|
|
|
|
|
|
|
|
fn try_from(value: &'a str) -> Result<Self> {
|
|
|
|
Self::parse(value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-19 19:28:20 +00:00
|
|
|
impl DispatchContext {
|
2023-10-20 09:44:19 +00:00
|
|
|
pub fn parse(source: &str) -> Result<Self> {
|
|
|
|
let mut context = Self::default();
|
|
|
|
let source = skip_whitespace(source);
|
|
|
|
Self::parse_expr(&source, &mut context)?;
|
|
|
|
Ok(context)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
|
|
|
|
if source.is_empty() {
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
let key = source
|
|
|
|
.chars()
|
2023-10-20 10:12:06 +00:00
|
|
|
.take_while(|c| is_identifier_char(*c))
|
2023-10-20 09:44:19 +00:00
|
|
|
.collect::<String>();
|
|
|
|
source = skip_whitespace(&source[key.len()..]);
|
|
|
|
if let Some(suffix) = source.strip_prefix('=') {
|
|
|
|
source = skip_whitespace(suffix);
|
|
|
|
let value = source
|
|
|
|
.chars()
|
2023-10-20 10:12:06 +00:00
|
|
|
.take_while(|c| is_identifier_char(*c))
|
2023-10-20 09:44:19 +00:00
|
|
|
.collect::<String>();
|
|
|
|
source = skip_whitespace(&source[value.len()..]);
|
|
|
|
context.set(key, value);
|
|
|
|
} else {
|
|
|
|
context.insert(key);
|
2023-10-19 17:03:10 +00:00
|
|
|
}
|
2023-10-20 09:44:19 +00:00
|
|
|
|
|
|
|
Self::parse_expr(source, context)
|
2023-10-19 17:03:10 +00:00
|
|
|
}
|
|
|
|
|
2023-10-20 09:08:24 +00:00
|
|
|
pub fn is_empty(&self) -> bool {
|
|
|
|
self.set.is_empty() && self.map.is_empty()
|
|
|
|
}
|
|
|
|
|
2023-10-19 17:03:10 +00:00
|
|
|
pub fn clear(&mut self) {
|
|
|
|
self.set.clear();
|
|
|
|
self.map.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn extend(&mut self, other: &Self) {
|
|
|
|
for v in &other.set {
|
|
|
|
self.set.insert(v.clone());
|
|
|
|
}
|
|
|
|
for (k, v) in &other.map {
|
|
|
|
self.map.insert(k.clone(), v.clone());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-20 09:44:19 +00:00
|
|
|
pub fn insert<I: Into<SharedString>>(&mut self, identifier: I) {
|
2023-10-19 17:03:10 +00:00
|
|
|
self.set.insert(identifier.into());
|
|
|
|
}
|
|
|
|
|
2023-10-20 09:44:19 +00:00
|
|
|
pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
|
2023-10-19 17:03:10 +00:00
|
|
|
self.map.insert(key.into(), value.into());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
2023-10-20 09:08:24 +00:00
|
|
|
pub enum DispatchContextPredicate {
|
2023-10-19 17:26:52 +00:00
|
|
|
Identifier(SharedString),
|
|
|
|
Equal(SharedString, SharedString),
|
|
|
|
NotEqual(SharedString, SharedString),
|
2023-10-20 09:08:24 +00:00
|
|
|
Child(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
|
|
|
|
Not(Box<DispatchContextPredicate>),
|
|
|
|
And(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
|
|
|
|
Or(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
|
2023-10-19 17:03:10 +00:00
|
|
|
}
|
|
|
|
|
2023-10-20 09:08:24 +00:00
|
|
|
impl DispatchContextPredicate {
|
2023-10-19 17:03:10 +00:00
|
|
|
pub fn parse(source: &str) -> Result<Self> {
|
2023-10-20 09:44:19 +00:00
|
|
|
let source = skip_whitespace(source);
|
2023-10-19 17:03:10 +00:00
|
|
|
let (predicate, rest) = Self::parse_expr(source, 0)?;
|
|
|
|
if let Some(next) = rest.chars().next() {
|
|
|
|
Err(anyhow!("unexpected character {next:?}"))
|
|
|
|
} else {
|
|
|
|
Ok(predicate)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-19 19:28:20 +00:00
|
|
|
pub fn eval(&self, contexts: &[&DispatchContext]) -> bool {
|
2023-10-20 10:12:06 +00:00
|
|
|
let Some(context) = contexts.last() else {
|
2023-10-19 17:03:10 +00:00
|
|
|
return false;
|
|
|
|
};
|
|
|
|
match self {
|
2023-10-21 15:52:47 +00:00
|
|
|
Self::Identifier(name) => context.set.contains(name),
|
2023-10-19 17:03:10 +00:00
|
|
|
Self::Equal(left, right) => context
|
|
|
|
.map
|
2023-10-21 15:52:47 +00:00
|
|
|
.get(left)
|
2023-10-19 17:03:10 +00:00
|
|
|
.map(|value| value == right)
|
|
|
|
.unwrap_or(false),
|
|
|
|
Self::NotEqual(left, right) => context
|
|
|
|
.map
|
2023-10-21 15:52:47 +00:00
|
|
|
.get(left)
|
2023-10-19 17:03:10 +00:00
|
|
|
.map(|value| value != right)
|
|
|
|
.unwrap_or(true),
|
|
|
|
Self::Not(pred) => !pred.eval(contexts),
|
2023-10-20 10:12:06 +00:00
|
|
|
Self::Child(parent, child) => {
|
|
|
|
parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts)
|
|
|
|
}
|
2023-10-19 17:03:10 +00:00
|
|
|
Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
|
|
|
|
Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
|
2023-10-20 09:08:24 +00:00
|
|
|
type Op = fn(
|
|
|
|
DispatchContextPredicate,
|
|
|
|
DispatchContextPredicate,
|
|
|
|
) -> Result<DispatchContextPredicate>;
|
2023-10-19 17:03:10 +00:00
|
|
|
|
|
|
|
let (mut predicate, rest) = Self::parse_primary(source)?;
|
|
|
|
source = rest;
|
|
|
|
|
|
|
|
'parse: loop {
|
|
|
|
for (operator, precedence, constructor) in [
|
|
|
|
(">", PRECEDENCE_CHILD, Self::new_child as Op),
|
|
|
|
("&&", PRECEDENCE_AND, Self::new_and as Op),
|
|
|
|
("||", PRECEDENCE_OR, Self::new_or as Op),
|
|
|
|
("==", PRECEDENCE_EQ, Self::new_eq as Op),
|
|
|
|
("!=", PRECEDENCE_EQ, Self::new_neq as Op),
|
|
|
|
] {
|
|
|
|
if source.starts_with(operator) && precedence >= min_precedence {
|
2023-10-20 09:44:19 +00:00
|
|
|
source = skip_whitespace(&source[operator.len()..]);
|
2023-10-19 17:03:10 +00:00
|
|
|
let (right, rest) = Self::parse_expr(source, precedence + 1)?;
|
|
|
|
predicate = constructor(predicate, right)?;
|
|
|
|
source = rest;
|
|
|
|
continue 'parse;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok((predicate, source))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
|
|
|
|
let next = source
|
|
|
|
.chars()
|
|
|
|
.next()
|
|
|
|
.ok_or_else(|| anyhow!("unexpected eof"))?;
|
|
|
|
match next {
|
|
|
|
'(' => {
|
2023-10-20 09:44:19 +00:00
|
|
|
source = skip_whitespace(&source[1..]);
|
2023-10-19 17:03:10 +00:00
|
|
|
let (predicate, rest) = Self::parse_expr(source, 0)?;
|
|
|
|
if rest.starts_with(')') {
|
2023-10-20 09:44:19 +00:00
|
|
|
source = skip_whitespace(&rest[1..]);
|
2023-10-19 17:03:10 +00:00
|
|
|
Ok((predicate, source))
|
|
|
|
} else {
|
|
|
|
Err(anyhow!("expected a ')'"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
'!' => {
|
2023-10-20 09:44:19 +00:00
|
|
|
let source = skip_whitespace(&source[1..]);
|
2023-10-19 17:03:10 +00:00
|
|
|
let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
|
2023-10-20 09:08:24 +00:00
|
|
|
Ok((DispatchContextPredicate::Not(Box::new(predicate)), source))
|
2023-10-19 17:03:10 +00:00
|
|
|
}
|
2023-10-20 10:12:06 +00:00
|
|
|
_ if is_identifier_char(next) => {
|
2023-10-19 17:03:10 +00:00
|
|
|
let len = source
|
2023-10-20 10:12:06 +00:00
|
|
|
.find(|c: char| !is_identifier_char(c))
|
2023-10-19 17:03:10 +00:00
|
|
|
.unwrap_or(source.len());
|
|
|
|
let (identifier, rest) = source.split_at(len);
|
2023-10-20 09:44:19 +00:00
|
|
|
source = skip_whitespace(rest);
|
2023-10-19 17:03:10 +00:00
|
|
|
Ok((
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::Identifier(identifier.to_string().into()),
|
2023-10-19 17:03:10 +00:00
|
|
|
source,
|
|
|
|
))
|
|
|
|
}
|
|
|
|
_ => Err(anyhow!("unexpected character {next:?}")),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn new_or(self, other: Self) -> Result<Self> {
|
|
|
|
Ok(Self::Or(Box::new(self), Box::new(other)))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn new_and(self, other: Self) -> Result<Self> {
|
|
|
|
Ok(Self::And(Box::new(self), Box::new(other)))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn new_child(self, other: Self) -> Result<Self> {
|
|
|
|
Ok(Self::Child(Box::new(self), Box::new(other)))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn new_eq(self, other: Self) -> Result<Self> {
|
|
|
|
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
|
|
|
|
Ok(Self::Equal(left, right))
|
|
|
|
} else {
|
|
|
|
Err(anyhow!("operands must be identifiers"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn new_neq(self, other: Self) -> Result<Self> {
|
|
|
|
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
|
|
|
|
Ok(Self::NotEqual(left, right))
|
|
|
|
} else {
|
|
|
|
Err(anyhow!("operands must be identifiers"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const PRECEDENCE_CHILD: u32 = 1;
|
|
|
|
const PRECEDENCE_OR: u32 = 2;
|
|
|
|
const PRECEDENCE_AND: u32 = 3;
|
|
|
|
const PRECEDENCE_EQ: u32 = 4;
|
|
|
|
const PRECEDENCE_NOT: u32 = 5;
|
|
|
|
|
2023-10-20 10:12:06 +00:00
|
|
|
fn is_identifier_char(c: char) -> bool {
|
|
|
|
c.is_alphanumeric() || c == '_' || c == '-'
|
|
|
|
}
|
|
|
|
|
2023-10-20 09:44:19 +00:00
|
|
|
fn skip_whitespace(source: &str) -> &str {
|
|
|
|
let len = source
|
|
|
|
.find(|c: char| !c.is_whitespace())
|
|
|
|
.unwrap_or(source.len());
|
|
|
|
&source[len..]
|
|
|
|
}
|
|
|
|
|
2023-10-19 17:03:10 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2023-10-20 09:44:19 +00:00
|
|
|
use super::*;
|
2023-11-08 03:23:02 +00:00
|
|
|
use crate as gpui;
|
2023-10-20 09:44:19 +00:00
|
|
|
use DispatchContextPredicate::*;
|
|
|
|
|
2023-11-07 16:00:30 +00:00
|
|
|
#[test]
|
|
|
|
fn test_actions_definition() {
|
|
|
|
{
|
2023-11-08 04:48:47 +00:00
|
|
|
actions!(A, B, C, D, E, F, G);
|
2023-11-07 16:00:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
actions!(
|
|
|
|
A,
|
2023-11-08 04:48:47 +00:00
|
|
|
B,
|
2023-11-07 16:00:30 +00:00
|
|
|
C,
|
|
|
|
D,
|
|
|
|
E,
|
2023-11-08 04:48:47 +00:00
|
|
|
F,
|
2023-11-07 16:00:30 +00:00
|
|
|
G, // Don't wrap, test the trailing comma
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-20 09:44:19 +00:00
|
|
|
#[test]
|
|
|
|
fn test_parse_context() {
|
|
|
|
let mut expected = DispatchContext::default();
|
|
|
|
expected.set("foo", "bar");
|
|
|
|
expected.insert("baz");
|
|
|
|
assert_eq!(DispatchContext::parse("baz foo=bar").unwrap(), expected);
|
|
|
|
assert_eq!(DispatchContext::parse("foo = bar baz").unwrap(), expected);
|
|
|
|
assert_eq!(
|
|
|
|
DispatchContext::parse(" baz foo = bar baz").unwrap(),
|
|
|
|
expected
|
|
|
|
);
|
|
|
|
assert_eq!(DispatchContext::parse(" foo = bar baz").unwrap(), expected);
|
|
|
|
}
|
2023-10-19 17:03:10 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse_identifiers() {
|
|
|
|
// Identifiers
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse("abc12").unwrap(),
|
2023-10-19 17:03:10 +00:00
|
|
|
Identifier("abc12".into())
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse("_1a").unwrap(),
|
2023-10-19 17:03:10 +00:00
|
|
|
Identifier("_1a".into())
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse_negations() {
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse("!abc").unwrap(),
|
2023-10-19 17:03:10 +00:00
|
|
|
Not(Box::new(Identifier("abc".into())))
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse(" ! ! abc").unwrap(),
|
2023-10-19 17:03:10 +00:00
|
|
|
Not(Box::new(Not(Box::new(Identifier("abc".into())))))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse_equality_operators() {
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse("a == b").unwrap(),
|
2023-10-19 17:03:10 +00:00
|
|
|
Equal("a".into(), "b".into())
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse("c!=d").unwrap(),
|
2023-10-19 17:03:10 +00:00
|
|
|
NotEqual("c".into(), "d".into())
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse("c == !d")
|
2023-10-19 17:03:10 +00:00
|
|
|
.unwrap_err()
|
|
|
|
.to_string(),
|
|
|
|
"operands must be identifiers"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse_boolean_operators() {
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse("a || b").unwrap(),
|
2023-10-19 17:03:10 +00:00
|
|
|
Or(
|
|
|
|
Box::new(Identifier("a".into())),
|
|
|
|
Box::new(Identifier("b".into()))
|
|
|
|
)
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse("a || !b && c").unwrap(),
|
2023-10-19 17:03:10 +00:00
|
|
|
Or(
|
|
|
|
Box::new(Identifier("a".into())),
|
|
|
|
Box::new(And(
|
|
|
|
Box::new(Not(Box::new(Identifier("b".into())))),
|
|
|
|
Box::new(Identifier("c".into()))
|
|
|
|
))
|
|
|
|
)
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse("a && b || c&&d").unwrap(),
|
2023-10-19 17:03:10 +00:00
|
|
|
Or(
|
|
|
|
Box::new(And(
|
|
|
|
Box::new(Identifier("a".into())),
|
|
|
|
Box::new(Identifier("b".into()))
|
|
|
|
)),
|
|
|
|
Box::new(And(
|
|
|
|
Box::new(Identifier("c".into())),
|
|
|
|
Box::new(Identifier("d".into()))
|
|
|
|
))
|
|
|
|
)
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse("a == b && c || d == e && f").unwrap(),
|
2023-10-19 17:03:10 +00:00
|
|
|
Or(
|
|
|
|
Box::new(And(
|
|
|
|
Box::new(Equal("a".into(), "b".into())),
|
|
|
|
Box::new(Identifier("c".into()))
|
|
|
|
)),
|
|
|
|
Box::new(And(
|
|
|
|
Box::new(Equal("d".into(), "e".into())),
|
|
|
|
Box::new(Identifier("f".into()))
|
|
|
|
))
|
|
|
|
)
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse("a && b && c && d").unwrap(),
|
2023-10-19 17:03:10 +00:00
|
|
|
And(
|
|
|
|
Box::new(And(
|
|
|
|
Box::new(And(
|
|
|
|
Box::new(Identifier("a".into())),
|
|
|
|
Box::new(Identifier("b".into()))
|
|
|
|
)),
|
|
|
|
Box::new(Identifier("c".into())),
|
|
|
|
)),
|
|
|
|
Box::new(Identifier("d".into()))
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse_parenthesized_expressions() {
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse("a && (b == c || d != e)").unwrap(),
|
2023-10-19 17:03:10 +00:00
|
|
|
And(
|
|
|
|
Box::new(Identifier("a".into())),
|
|
|
|
Box::new(Or(
|
|
|
|
Box::new(Equal("b".into(), "c".into())),
|
|
|
|
Box::new(NotEqual("d".into(), "e".into())),
|
|
|
|
)),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2023-10-20 09:08:24 +00:00
|
|
|
DispatchContextPredicate::parse(" ( a || b ) ").unwrap(),
|
2023-10-19 17:03:10 +00:00
|
|
|
Or(
|
|
|
|
Box::new(Identifier("a".into())),
|
|
|
|
Box::new(Identifier("b".into())),
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|