use crate::limit::conditions::{ErrorType, Literal, SyntaxError, Token, TokenType};
use serde::{Deserialize, Serialize, Serializer};
use std::cmp::Ordering;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::hash::{Hash, Hasher};
#[cfg(feature = "lenient_conditions")]
mod deprecated {
use std::sync::atomic::{AtomicBool, Ordering};
static DEPRECATED_SYNTAX: AtomicBool = AtomicBool::new(false);
pub fn check_deprecated_syntax_usages_and_reset() -> bool {
match DEPRECATED_SYNTAX.compare_exchange(true, false, Ordering::Relaxed, Ordering::Relaxed)
{
Ok(previous) => previous,
Err(previous) => previous,
}
}
pub fn deprecated_syntax_used() {
DEPRECATED_SYNTAX.fetch_or(true, Ordering::SeqCst);
}
}
#[cfg(feature = "lenient_conditions")]
pub use deprecated::check_deprecated_syntax_usages_and_reset;
#[derive(Debug, Hash, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct Namespace(String);
impl From<&str> for Namespace {
fn from(s: &str) -> Namespace {
Self(s.into())
}
}
impl AsRef<str> for Namespace {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl From<String> for Namespace {
fn from(s: String) -> Self {
Self(s)
}
}
#[derive(Eq, Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Limit {
namespace: Namespace,
#[serde(skip_serializing, default)]
max_value: i64,
seconds: u64,
#[serde(skip_serializing, default)]
name: Option<String>,
#[serde(serialize_with = "ordered_condition_set")]
conditions: HashSet<Condition>,
#[serde(serialize_with = "ordered_set")]
variables: HashSet<String>,
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Hash)]
#[serde(try_from = "String", into = "String")]
pub struct Condition {
var_name: String,
predicate: Predicate,
operand: String,
}
#[derive(Debug)]
pub struct ConditionParsingError {
error: SyntaxError,
pub tokens: Vec<Token>,
condition: String,
}
impl Display for ConditionParsingError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{} of condition \"{}\"", self.error, self.condition)
}
}
impl Error for ConditionParsingError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.error)
}
}
impl TryFrom<&str> for Condition {
type Error = ConditionParsingError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.to_owned().try_into()
}
}
impl TryFrom<String> for Condition {
type Error = ConditionParsingError;
fn try_from(value: String) -> Result<Self, Self::Error> {
match conditions::Scanner::scan(value.clone()) {
Ok(tokens) => match tokens.len().cmp(&(3_usize)) {
Ordering::Equal => {
match (
&tokens[0].token_type,
&tokens[1].token_type,
&tokens[2].token_type,
) {
(
TokenType::Identifier,
TokenType::EqualEqual | TokenType::NotEqual,
TokenType::String,
) => {
if let (
Some(Literal::Identifier(var_name)),
Some(Literal::String(operand)),
) = (&tokens[0].literal, &tokens[2].literal)
{
let predicate = match &tokens[1].token_type {
TokenType::EqualEqual => Predicate::Equal,
TokenType::NotEqual => Predicate::NotEqual,
_ => unreachable!(),
};
Ok(Condition {
var_name: var_name.clone(),
predicate,
operand: operand.clone(),
})
} else {
panic!(
"Unexpected state {:?} returned from Scanner for: `{}`",
tokens, value
)
}
}
(
TokenType::String,
TokenType::EqualEqual | TokenType::NotEqual,
TokenType::Identifier,
) => {
if let (
Some(Literal::String(operand)),
Some(Literal::Identifier(var_name)),
) = (&tokens[0].literal, &tokens[2].literal)
{
let predicate = match &tokens[1].token_type {
TokenType::EqualEqual => Predicate::Equal,
TokenType::NotEqual => Predicate::NotEqual,
_ => unreachable!(),
};
Ok(Condition {
var_name: var_name.clone(),
predicate,
operand: operand.clone(),
})
} else {
panic!(
"Unexpected state {:?} returned from Scanner for: `{}`",
tokens, value
)
}
}
#[cfg(feature = "lenient_conditions")]
(TokenType::Identifier, TokenType::EqualEqual, TokenType::Identifier) => {
if let (
Some(Literal::Identifier(var_name)),
Some(Literal::Identifier(operand)),
) = (&tokens[0].literal, &tokens[2].literal)
{
deprecated::deprecated_syntax_used();
Ok(Condition {
var_name: var_name.clone(),
predicate: Predicate::Equal,
operand: operand.clone(),
})
} else {
panic!(
"Unexpected state {:?} returned from Scanner for: `{}`",
tokens, value
)
}
}
#[cfg(feature = "lenient_conditions")]
(TokenType::Identifier, TokenType::EqualEqual, TokenType::Number) => {
if let (
Some(Literal::Identifier(var_name)),
Some(Literal::Number(operand)),
) = (&tokens[0].literal, &tokens[2].literal)
{
deprecated::deprecated_syntax_used();
Ok(Condition {
var_name: var_name.clone(),
predicate: Predicate::Equal,
operand: operand.to_string(),
})
} else {
panic!(
"Unexpected state {:?} returned from Scanner for: `{}`",
tokens, value
)
}
}
(t1, t2, _) => {
let faulty = match (t1, t2) {
(
TokenType::Identifier | TokenType::String,
TokenType::EqualEqual | TokenType::NotEqual,
) => 2,
(TokenType::Identifier | TokenType::String, _) => 1,
(_, _) => 0,
};
Err(ConditionParsingError {
error: SyntaxError {
pos: tokens[faulty].pos,
error: ErrorType::UnexpectedToken(tokens[faulty].clone()),
},
tokens,
condition: value,
})
}
}
}
Ordering::Less => Err(ConditionParsingError {
error: SyntaxError {
pos: value.len(),
error: ErrorType::MissingToken,
},
tokens,
condition: value,
}),
Ordering::Greater => Err(ConditionParsingError {
error: SyntaxError {
pos: tokens[3].pos,
error: ErrorType::UnexpectedToken(tokens[3].clone()),
},
tokens,
condition: value,
}),
},
Err(err) => Err(ConditionParsingError {
error: err,
tokens: Vec::new(),
condition: value,
}),
}
}
}
impl From<Condition> for String {
fn from(condition: Condition) -> Self {
let p = &condition.predicate;
let predicate: String = p.clone().into();
let quotes = if condition.operand.contains('"') {
'\''
} else {
'"'
};
format!(
"{} {} {}{}{}",
condition.var_name, predicate, quotes, condition.operand, quotes
)
}
}
#[derive(PartialEq, Eq, Debug, Clone, Hash)]
pub enum Predicate {
Equal,
NotEqual,
}
impl Predicate {
fn test(&self, lhs: &str, rhs: &str) -> bool {
match self {
Predicate::Equal => lhs == rhs,
Predicate::NotEqual => lhs != rhs,
}
}
}
impl From<Predicate> for String {
fn from(op: Predicate) -> Self {
match op {
Predicate::Equal => "==".to_string(),
Predicate::NotEqual => "!=".to_string(),
}
}
}
fn ordered_condition_set<S>(value: &HashSet<Condition>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let ordered: BTreeSet<String> = value.iter().map(|c| c.clone().into()).collect();
ordered.serialize(serializer)
}
fn ordered_set<S>(value: &HashSet<String>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let ordered: BTreeSet<_> = value.iter().collect();
ordered.serialize(serializer)
}
impl Limit {
pub fn new<N: Into<Namespace>, T: TryInto<Condition>>(
namespace: N,
max_value: i64,
seconds: u64,
conditions: impl IntoIterator<Item = T>,
variables: impl IntoIterator<Item = impl Into<String>>,
) -> Self
where
<N as TryInto<Namespace>>::Error: core::fmt::Debug,
<T as TryInto<Condition>>::Error: core::fmt::Debug,
{
Self {
namespace: namespace.into(),
max_value,
seconds,
name: None,
conditions: conditions
.into_iter()
.map(|cond| cond.try_into().expect("Invalid condition"))
.collect(),
variables: variables.into_iter().map(|var| var.into()).collect(),
}
}
pub fn namespace(&self) -> &Namespace {
&self.namespace
}
pub fn max_value(&self) -> i64 {
self.max_value
}
pub fn seconds(&self) -> u64 {
self.seconds
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn set_name(&mut self, name: String) {
self.name = Some(name)
}
pub fn set_max_value(&mut self, value: i64) {
self.max_value = value;
}
pub fn conditions(&self) -> HashSet<String> {
self.conditions
.iter()
.map(|cond| cond.clone().into())
.collect()
}
pub fn variables(&self) -> HashSet<String> {
self.variables.iter().map(|var| var.into()).collect()
}
pub fn has_variable(&self, var: &str) -> bool {
self.variables.contains(var)
}
pub fn applies(&self, values: &HashMap<String, String>) -> bool {
let all_conditions_apply = self
.conditions
.iter()
.all(|cond| Self::condition_applies(cond, values));
let all_vars_are_set = self.variables.iter().all(|var| values.contains_key(var));
all_conditions_apply && all_vars_are_set
}
fn condition_applies(condition: &Condition, values: &HashMap<String, String>) -> bool {
let left_operand = condition.var_name.as_str();
let right_operand = condition.operand.as_str();
match values.get(left_operand) {
Some(val) => condition.predicate.test(val, right_operand),
None => false,
}
}
}
impl Hash for Limit {
fn hash<H: Hasher>(&self, state: &mut H) {
self.namespace.hash(state);
self.seconds.hash(state);
let mut encoded_conditions = self
.conditions
.iter()
.map(|c| c.clone().into())
.collect::<Vec<String>>();
encoded_conditions.sort();
encoded_conditions.hash(state);
let mut encoded_vars = self
.variables
.iter()
.map(|c| c.to_string())
.collect::<Vec<String>>();
encoded_vars.sort();
encoded_vars.hash(state);
}
}
impl PartialEq for Limit {
fn eq(&self, other: &Self) -> bool {
self.namespace == other.namespace
&& self.seconds == other.seconds
&& self.conditions == other.conditions
&& self.variables == other.variables
}
}
mod conditions {
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::num::IntErrorKind;
#[derive(Debug)]
pub struct SyntaxError {
pub pos: usize,
pub error: ErrorType,
}
#[derive(Debug, Eq, PartialEq)]
pub enum ErrorType {
UnexpectedToken(Token),
MissingToken,
InvalidCharacter(char),
InvalidNumber,
UnclosedStringLiteral(char),
}
impl Display for SyntaxError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self.error {
ErrorType::UnexpectedToken(token) => write!(
f,
"SyntaxError: Unexpected token `{}` at offset {}",
token, self.pos
),
ErrorType::InvalidCharacter(char) => write!(
f,
"SyntaxError: Invalid character `{}` at offset {}",
char, self.pos
),
ErrorType::InvalidNumber => {
write!(f, "SyntaxError: Invalid number at offset {}", self.pos)
}
ErrorType::MissingToken => {
write!(f, "SyntaxError: Expected token at offset {}", self.pos)
}
ErrorType::UnclosedStringLiteral(char) => {
write!(f, "SyntaxError: Missing closing `{}` for string literal starting at offset {}", char, self.pos)
}
}
}
}
impl Error for SyntaxError {}
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum TokenType {
EqualEqual,
NotEqual,
Identifier,
String,
Number,
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum Literal {
Identifier(String),
String(String),
Number(i64),
}
impl Display for Literal {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Literal::Identifier(id) => write!(f, "{}", id),
Literal::String(string) => write!(f, "'{}'", string),
Literal::Number(number) => write!(f, "{}", number),
}
}
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Token {
pub token_type: TokenType,
pub literal: Option<Literal>,
pub pos: usize,
}
impl Display for Token {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.token_type {
TokenType::EqualEqual => write!(f, "Equality (==)"),
TokenType::NotEqual => write!(f, "Unequal (!=)"),
TokenType::Identifier => {
write!(f, "Identifier: {}", self.literal.as_ref().unwrap())
}
TokenType::String => {
write!(f, "String literal: {}", self.literal.as_ref().unwrap())
}
TokenType::Number => {
write!(f, "Number literal: {}", self.literal.as_ref().unwrap())
}
}
}
}
pub struct Scanner {
input: Vec<char>,
pos: usize,
}
impl Scanner {
pub fn scan(condition: String) -> Result<Vec<Token>, SyntaxError> {
let mut tokens: Vec<Token> = Vec::with_capacity(3);
let mut scanner = Scanner {
input: condition.chars().collect(),
pos: 0,
};
while !scanner.done() {
match scanner.next_token() {
Ok(token) => {
if let Some(token) = token {
tokens.push(token)
}
}
Err(err) => {
return Err(err);
}
}
}
Ok(tokens)
}
fn next_token(&mut self) -> Result<Option<Token>, SyntaxError> {
let character = self.advance();
match character {
'=' => {
if self.next_matches('=') {
Ok(Some(Token {
token_type: TokenType::EqualEqual,
literal: None,
pos: self.pos - 1,
}))
} else {
Err(SyntaxError {
pos: self.pos,
error: ErrorType::InvalidCharacter(self.input[self.pos - 1]),
})
}
}
'!' => {
if self.next_matches('=') {
Ok(Some(Token {
token_type: TokenType::NotEqual,
literal: None,
pos: self.pos - 1,
}))
} else {
Err(SyntaxError {
pos: self.pos,
error: ErrorType::InvalidCharacter(self.input[self.pos - 1]),
})
}
}
'"' | '\'' => self.scan_string(character).map(Some),
' ' | '\n' | '\r' | '\t' => Ok(None),
_ => {
if character.is_alphabetic() {
self.scan_identifier().map(Some)
} else if character.is_numeric() {
self.scan_number().map(Some)
} else {
Err(SyntaxError {
pos: self.pos,
error: ErrorType::InvalidCharacter(character),
})
}
}
}
}
fn scan_identifier(&mut self) -> Result<Token, SyntaxError> {
let start = self.pos;
while !self.done() && self.valid_id_char() {
self.advance();
}
Ok(Token {
token_type: TokenType::Identifier,
literal: Some(Literal::Identifier(
self.input[start - 1..self.pos].iter().collect(),
)),
pos: start,
})
}
fn valid_id_char(&mut self) -> bool {
let char = self.input[self.pos];
char.is_alphanumeric() || char == '.' || char == '_'
}
fn scan_string(&mut self, until: char) -> Result<Token, SyntaxError> {
let start = self.pos;
loop {
if self.done() {
return Err(SyntaxError {
pos: start,
error: ErrorType::UnclosedStringLiteral(until),
});
}
if self.advance() == until {
return Ok(Token {
token_type: TokenType::String,
literal: Some(Literal::String(
self.input[start..self.pos - 1].iter().collect(),
)),
pos: start,
});
}
}
}
fn scan_number(&mut self) -> Result<Token, SyntaxError> {
let start = self.pos;
while !self.done() && self.input[self.pos].is_numeric() {
self.advance();
}
let number_str = self.input[start - 1..self.pos].iter().collect::<String>();
match number_str.parse::<i64>() {
Ok(number) => Ok(Token {
token_type: TokenType::Number,
literal: Some(Literal::Number(number)),
pos: start,
}),
Err(err) => {
let syntax_error = match err.kind() {
IntErrorKind::Empty => {
unreachable!("This means a bug in the scanner!")
}
IntErrorKind::Zero => {
unreachable!("We're parsing Numbers as i64, so 0 should always work!")
}
_ => SyntaxError {
pos: start,
error: ErrorType::InvalidNumber,
},
};
Err(syntax_error)
}
}
}
fn advance(&mut self) -> char {
let char = self.input[self.pos];
self.pos += 1;
char
}
fn next_matches(&mut self, c: char) -> bool {
if self.done() || self.input[self.pos] != c {
return false;
}
self.pos += 1;
true
}
fn done(&self) -> bool {
self.pos >= self.input.len()
}
}
#[cfg(test)]
mod tests {
use crate::limit::conditions::Literal::Identifier;
use crate::limit::conditions::{ErrorType, Literal, Scanner, Token, TokenType};
#[test]
fn test_scanner() {
let mut tokens =
Scanner::scan("foo=='bar '".to_owned()).expect("Should parse alright!");
assert_eq!(tokens.len(), 3);
assert_eq!(
tokens[0],
Token {
token_type: TokenType::Identifier,
literal: Some(Identifier("foo".to_owned())),
pos: 1,
}
);
assert_eq!(
tokens[1],
Token {
token_type: TokenType::EqualEqual,
literal: None,
pos: 4,
}
);
assert_eq!(
tokens[2],
Token {
token_type: TokenType::String,
literal: Some(Literal::String("bar ".to_owned())),
pos: 6,
}
);
tokens[1].pos += 1;
tokens[2].pos += 2;
assert_eq!(
tokens,
Scanner::scan("foo == 'bar '".to_owned()).expect("Should parse alright!")
);
tokens[0].pos += 2;
tokens[1].pos += 2;
tokens[2].pos += 2;
assert_eq!(
tokens,
Scanner::scan(" foo == 'bar ' ".to_owned()).expect("Should parse alright!")
);
tokens[1].pos += 2;
tokens[2].pos += 4;
assert_eq!(
tokens,
Scanner::scan(" foo == 'bar ' ".to_owned()).expect("Should parse alright!")
);
}
#[test]
fn test_number_literal() {
let tokens = Scanner::scan("var == 42".to_owned()).expect("Should parse alright!");
assert_eq!(tokens.len(), 3);
assert_eq!(
tokens[0],
Token {
token_type: TokenType::Identifier,
literal: Some(Identifier("var".to_owned())),
pos: 1,
}
);
assert_eq!(
tokens[1],
Token {
token_type: TokenType::EqualEqual,
literal: None,
pos: 5,
}
);
assert_eq!(
tokens[2],
Token {
token_type: TokenType::Number,
literal: Some(Literal::Number(42)),
pos: 8,
}
);
}
#[test]
fn test_charset() {
let tokens =
Scanner::scan(" 変数 == ' 💖 '".to_owned()).expect("Should parse alright!");
assert_eq!(tokens.len(), 3);
assert_eq!(
tokens[0],
Token {
token_type: TokenType::Identifier,
literal: Some(Identifier("変数".to_owned())),
pos: 2,
}
);
assert_eq!(
tokens[1],
Token {
token_type: TokenType::EqualEqual,
literal: None,
pos: 5,
}
);
assert_eq!(
tokens[2],
Token {
token_type: TokenType::String,
literal: Some(Literal::String(" 💖 ".to_owned())),
pos: 8,
}
);
}
#[test]
fn unclosed_string_literal() {
let error = Scanner::scan("foo == 'ba".to_owned())
.err()
.expect("Should fail!");
assert_eq!(error.pos, 8);
assert_eq!(error.error, ErrorType::UnclosedStringLiteral('\''));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn limit_can_have_an_optional_name() {
let mut limit = Limit::new("test_namespace", 10, 60, vec!["x == \"5\""], vec!["y"]);
assert!(limit.name.is_none());
let name = "Test Limit";
limit.set_name(name.to_string());
assert_eq!(name, limit.name.unwrap())
}
#[test]
fn limit_applies() {
let limit = Limit::new("test_namespace", 10, 60, vec!["x == \"5\""], vec!["y"]);
let mut values: HashMap<String, String> = HashMap::new();
values.insert("x".into(), "5".into());
values.insert("y".into(), "1".into());
assert!(limit.applies(&values))
}
#[test]
fn limit_does_not_apply_when_cond_is_false() {
let limit = Limit::new("test_namespace", 10, 60, vec!["x == \"5\""], vec!["y"]);
let mut values: HashMap<String, String> = HashMap::new();
values.insert("x".into(), "1".into());
values.insert("y".into(), "1".into());
assert!(!limit.applies(&values))
}
#[test]
#[cfg(feature = "lenient_conditions")]
fn limit_does_not_apply_when_cond_is_false_deprecated_style() {
let limit = Limit::new("test_namespace", 10, 60, vec!["x == 5"], vec!["y"]);
let mut values: HashMap<String, String> = HashMap::new();
values.insert("x".into(), "1".into());
values.insert("y".into(), "1".into());
assert!(!limit.applies(&values));
assert!(check_deprecated_syntax_usages_and_reset());
assert!(!check_deprecated_syntax_usages_and_reset());
let limit = Limit::new("test_namespace", 10, 60, vec!["x == foobar"], vec!["y"]);
let mut values: HashMap<String, String> = HashMap::new();
values.insert("x".into(), "foobar".into());
values.insert("y".into(), "1".into());
assert!(limit.applies(&values));
assert!(check_deprecated_syntax_usages_and_reset());
assert!(!check_deprecated_syntax_usages_and_reset());
}
#[test]
fn limit_does_not_apply_when_cond_var_is_not_set() {
let limit = Limit::new("test_namespace", 10, 60, vec!["x == \"5\""], vec!["y"]);
let mut values: HashMap<String, String> = HashMap::new();
values.insert("a".into(), "1".into());
values.insert("y".into(), "1".into());
assert!(!limit.applies(&values))
}
#[test]
fn limit_does_not_apply_when_var_not_set() {
let limit = Limit::new("test_namespace", 10, 60, vec!["x == \"5\""], vec!["y"]);
let mut values: HashMap<String, String> = HashMap::new();
values.insert("x".into(), "5".into());
assert!(!limit.applies(&values))
}
#[test]
fn limit_applies_when_all_its_conditions_apply() {
let limit = Limit::new(
"test_namespace",
10,
60,
vec!["x == \"5\"", "y == \"2\""],
vec!["z"],
);
let mut values: HashMap<String, String> = HashMap::new();
values.insert("x".into(), "5".into());
values.insert("y".into(), "2".into());
values.insert("z".into(), "1".into());
assert!(limit.applies(&values))
}
#[test]
fn limit_does_not_apply_if_one_cond_doesnt() {
let limit = Limit::new(
"test_namespace",
10,
60,
vec!["x == \"5\"", "y == \"2\""],
vec!["z"],
);
let mut values: HashMap<String, String> = HashMap::new();
values.insert("x".into(), "3".into());
values.insert("y".into(), "2".into());
values.insert("z".into(), "1".into());
assert!(!limit.applies(&values))
}
#[test]
fn valid_condition_literal_parsing() {
let result: Condition = serde_json::from_str(r#""x == '5'""#).expect("Should deserialize");
assert_eq!(
result,
Condition {
var_name: "x".to_string(),
predicate: Predicate::Equal,
operand: "5".to_string(),
}
);
let result: Condition =
serde_json::from_str(r#"" foobar=='ok' ""#).expect("Should deserialize");
assert_eq!(
result,
Condition {
var_name: "foobar".to_string(),
predicate: Predicate::Equal,
operand: "ok".to_string(),
}
);
let result: Condition =
serde_json::from_str(r#"" foobar == 'ok' ""#).expect("Should deserialize");
assert_eq!(
result,
Condition {
var_name: "foobar".to_string(),
predicate: Predicate::Equal,
operand: "ok".to_string(),
}
);
}
#[test]
#[cfg(not(feature = "lenient_conditions"))]
fn invalid_deprecated_condition_parsing() {
let _result = serde_json::from_str::<Condition>(r#""x == 5""#)
.err()
.expect("Should fail!");
}
#[test]
fn invalid_condition_parsing() {
let result = serde_json::from_str::<Condition>(r#""x != 5 && x > 12""#)
.err()
.expect("should fail parsing");
assert_eq!(
result.to_string(),
"SyntaxError: Invalid character `&` at offset 8 of condition \"x != 5 && x > 12\""
.to_string()
);
}
#[test]
fn condition_serialization() {
let condition = Condition {
var_name: "foobar".to_string(),
predicate: Predicate::Equal,
operand: "ok".to_string(),
};
let result = serde_json::to_string(&condition).expect("Should serialize");
assert_eq!(result, r#""foobar == \"ok\"""#.to_string());
}
}