synapse-rpc 0.1.20

RPC data structures used by synapse
Documentation
use std::f32;

use regex::{self, Regex};
use chrono::{DateTime, Utc};

use resource::ResourceKind;

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Criterion {
    pub field: String,
    pub op: Operation,
    pub value: Value,
}

#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub enum Operation {
    #[serde(rename = "==")]
    Eq,
    #[serde(rename = "!=")]
    Neq,
    #[serde(rename = ">")]
    GT,
    #[serde(rename = ">=")]
    GTE,
    #[serde(rename = "<")]
    LT,
    #[serde(rename = "<=")]
    LTE,
    #[serde(rename = "like")]
    Like,
    #[serde(rename = "ilike")]
    ILike,
    #[serde(rename = "in")]
    In,
    #[serde(rename = "!in")]
    NotIn,
    #[serde(rename = "has")]
    Has,
    #[serde(rename = "!has")]
    NotHas,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
#[serde(deny_unknown_fields)]
pub enum Value {
    B(bool),
    S(String),
    N(i64),
    F(f32),
    D(DateTime<Utc>),
    E(Option<()>),
    V(Vec<Value>),
}

#[derive(Debug, Clone, PartialEq)]
pub enum Field<'a> {
    B(bool),
    S(&'a str),
    N(i64),
    F(f32),
    D(DateTime<Utc>),
    E(Option<()>),
    V(Vec<Field<'a>>),
}

pub const FNULL: Field<'static> = Field::E(None);

pub trait Queryable {
    fn field(&self, field: &str) -> Option<Field>;
}

impl Criterion {
    pub fn matches<Q: Queryable>(&self, q: &Q) -> bool {
        if let Some(f) = q.field(&self.field) {
            self.match_field(&f, self.op, &self.value)
        } else {
            false
        }
    }

    fn match_field(&self, field: &Field, op: Operation, value: &Value) -> bool {
        match (field, value) {
            (&Field::V(ref items), &Value::V(ref vals)) => match op {
                Operation::Eq => items
                    .iter()
                    .zip(vals)
                    .all(|(f, v)| self.match_field(f, Operation::Eq, v)),
                Operation::Neq => items
                    .iter()
                    .zip(vals)
                    .any(|(f, v)| self.match_field(f, Operation::Neq, v)),
                _ => false,
            },
            (&Field::V(ref items), v) => match op {
                Operation::Has => items.iter().any(|f| {
                    self.match_field(f, Operation::Eq, v)
                        || self.match_field(f, Operation::ILike, v)
                }),
                Operation::NotHas => items.iter().all(|f| {
                    self.match_field(f, Operation::Neq, v)
                        && !self.match_field(f, Operation::ILike, v)
                }),
                _ => false,
            },
            (f, &Value::V(ref v)) => match op {
                Operation::In => v.iter()
                    .any(|item| self.match_field(f, Operation::Eq, item)),
                Operation::NotIn => v.iter()
                    .all(|item| self.match_field(f, Operation::Neq, item)),
                _ => false,
            },
            (&Field::B(f), &Value::B(v)) => match op {
                Operation::Eq => f == v,
                Operation::Neq => f != v,
                _ => false,
            },
            (&Field::S(ref f), &Value::S(ref v)) => match op {
                Operation::Eq => f == v,
                Operation::Neq => f != v,
                Operation::Like => match_like(v, f),
                Operation::ILike => match_ilike(v, f),
                _ => false,
            },
            (&Field::N(f), &Value::N(v)) => match op {
                Operation::Eq => f == v,
                Operation::Neq => f != v,
                Operation::GTE => f >= v,
                Operation::GT => f > v,
                Operation::LTE => f <= v,
                Operation::LT => f < v,
                _ => false,
            },
            (&Field::N(f), &Value::F(v)) => match op {
                Operation::Eq => f as f32 - v <= f32::EPSILON,
                Operation::Neq => f as f32 - v > f32::EPSILON,
                Operation::GTE => f as f32 >= v,
                Operation::GT => f as f32 > v,
                Operation::LTE => f as f32 <= v,
                Operation::LT => (f as f32) < v,
                _ => false,
            },
            (&Field::F(f), &Value::N(v)) => match op {
                Operation::Eq => f - v as f32 <= f32::EPSILON,
                Operation::Neq => f - v as f32 > f32::EPSILON,
                Operation::GTE => f >= v as f32,
                Operation::GT => f > v as f32,
                Operation::LTE => f <= v as f32,
                Operation::LT => f < v as f32,
                _ => false,
            },
            (&Field::F(f), &Value::F(v)) => match op {
                Operation::Eq => f - v <= f32::EPSILON,
                Operation::Neq => f - v > f32::EPSILON,
                Operation::GTE => f >= v,
                Operation::GT => f > v,
                Operation::LTE => f <= v,
                Operation::LT => f < v,
                _ => false,
            },
            (&Field::D(f), &Value::D(v)) => match op {
                Operation::Eq => f == v,
                Operation::Neq => f != v,
                Operation::GTE => f >= v,
                Operation::GT => f > v,
                Operation::LTE => f <= v,
                Operation::LT => f < v,
                _ => false,
            },
            (&Field::E(_), &Value::E(_)) => match op {
                Operation::Eq => true,
                _ => false,
            },
            (&Field::E(_), _) => match op {
                Operation::Neq => true,
                _ => false,
            },
            _ => match op {
                Operation::Neq => true,
                _ => false,
            },
        }
    }
}

impl Default for ResourceKind {
    fn default() -> ResourceKind {
        ResourceKind::Torrent
    }
}

fn match_like(pat: &str, s: &str) -> bool {
    let mut p = regex::escape(pat);
    p = p.replace("%", ".*");
    p = p.replace("_", ".");
    if let Ok(re) = Regex::new(&p) {
        re.is_match(s)
    } else {
        false
    }
}

fn match_ilike(pat: &str, s: &str) -> bool {
    match_like(&pat.to_lowercase(), &s.to_lowercase())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_like() {
        assert!(match_like("hello", "hello"));
        assert!(match_like("hello %", "hello world"));
        assert!(match_like("%world", "hello world"));
        assert!(!match_like("% world", "helloworld"));
        assert!(match_like("%", "foo bar"));
        assert!(match_like("fo%", "foo"));
    }

    struct Q;
    impl Queryable for Q {
        fn field(&self, f: &str) -> Option<Field> {
            match f {
                "s" => Some(Field::S("foo")),
                "n" => Some(Field::N(1)),
                "ob" => Some(Field::B(true)),
                "on" => Some(Field::E(None)),
                _ => None,
            }
        }
    }

    #[test]
    fn test_match_bad_field() {
        let c = Criterion {
            field: "asdf".to_owned(),
            op: Operation::Like,
            value: Value::S("fo%".to_owned()),
        };

        let q = Q;
        assert_eq!(q.field("asdf"), None);
        assert!(!c.matches(&q));
    }

    #[test]
    fn test_match() {
        let c = Criterion {
            field: "s".to_owned(),
            op: Operation::Like,
            value: Value::S("fo%".to_owned()),
        };

        let q = Q;
        assert!(c.matches(&q));
    }

    #[test]
    fn test_match_none() {
        let c = Criterion {
            field: "on".to_owned(),
            op: Operation::Eq,
            value: Value::E(None),
        };

        let q = Q;
        assert!(c.matches(&q));
    }

    #[test]
    fn test_match_some() {
        let c = Criterion {
            field: "ob".to_owned(),
            op: Operation::Eq,
            value: Value::B(true),
        };

        let q = Q;
        assert!(c.matches(&q));
    }

    #[test]
    fn test_match_none_in() {
        let c = Criterion {
            field: "on".to_owned(),
            op: Operation::In,
            value: Value::V(vec![Value::B(false), Value::E(None)]),
        };

        let q = Q;
        assert!(c.matches(&q));
    }

    #[test]
    fn test_match_none_not_in() {
        let c = Criterion {
            field: "on".to_owned(),
            op: Operation::NotIn,
            value: Value::V(vec![Value::B(false), Value::B(true)]),
        };

        let q = Q;
        assert!(c.matches(&q));
    }
}