diff --git a/.travis.yml b/.travis.yml index 5994a06..74b8b20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,7 @@ install: - cargo -V script: +- cargo check --verbose --no-default-features - cargo check --verbose - cargo test --verbose - cargo doc --no-deps diff --git a/Cargo.toml b/Cargo.toml index 58c4372..077b588 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,10 @@ travis-ci = { repository = "assert-rs/predicates-rs" } appveyor = { repository = "assert-rs/predicates-rs" } [dependencies] +difference = { version = "2.0", optional = true } +regex = { version="0.2", optional = true } +float-cmp = { version="0.4", optional = true } [features] -default = [] +default = ["difference", "regex", "float-cmp"] unstable = [] diff --git a/src/lib.rs b/src/lib.rs index 7621e81..31a6a46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,13 @@ #![deny(missing_docs, missing_debug_implementations)] +#[cfg(feature = "difference")] +extern crate difference; +#[cfg(feature = "float-cmp")] +extern crate float_cmp; +#[cfg(feature = "regex")] +extern crate regex; + // core `Predicate` trait pub mod predicate; pub use self::predicate::{BoxPredicate, Predicate}; diff --git a/src/predicate/float/close.rs b/src/predicate/float/close.rs new file mode 100644 index 0000000..e1c4d30 --- /dev/null +++ b/src/predicate/float/close.rs @@ -0,0 +1,109 @@ +// Copyright (c) 2018 The predicates-rs Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use float_cmp::ApproxEq; +use float_cmp::Ulps; + +use Predicate; + +/// Predicate that ensures two numbers are "close" enough, understanding that rounding errors +/// occur. +/// +/// This is created by the `predicate::float::is_close`. +#[derive(Clone, Debug)] +pub struct IsClosePredicate { + target: f64, + epsilon: f64, + ulps: ::U, +} + +impl IsClosePredicate { + /// Set the amount of error allowed. + /// + /// Values `1`-`5` should work in most cases. Sometimes more control is needed and you will + /// need to set `IsClosePredicate::epsilon` separately from `IsClosePredicate::ulps`. + /// + /// # Examples + /// + /// ``` + /// use predicates::predicate::*; + /// + /// let a = 0.15_f64 + 0.15_f64 + 0.15_f64; + /// let predicate_fn = float::is_close(a).distance(5); + /// ``` + pub fn distance(mut self, distance: ::U) -> Self { + self.epsilon = (distance as f64) * ::std::f64::EPSILON; + self.ulps = distance; + self + } + + /// Set the absolute deviation allowed. + /// + /// This is meant to handle problems near `0`. Values `1.`-`5.` epislons should work in most + /// cases. + /// + /// # Examples + /// + /// ``` + /// use predicates::predicate::*; + /// + /// let a = 0.15_f64 + 0.15_f64 + 0.15_f64; + /// let predicate_fn = float::is_close(a).epsilon(5.0 * ::std::f64::EPSILON); + /// ``` + pub fn epsilon(mut self, epsilon: f64) -> Self { + self.epsilon = epsilon; + self + } + + /// Set the relative deviation allowed. + /// + /// This is meant to handle large numbers. Values `1`-`5` should work in most cases. + /// + /// # Examples + /// + /// ``` + /// use predicates::predicate::*; + /// + /// let a = 0.15_f64 + 0.15_f64 + 0.15_f64; + /// let predicate_fn = float::is_close(a).ulps(5); + /// ``` + pub fn ulps(mut self, ulps: ::U) -> Self { + self.ulps = ulps; + self + } +} + +impl Predicate for IsClosePredicate { + type Item = f64; + + fn eval(&self, variable: &f64) -> bool { + variable.approx_eq(&self.target, self.epsilon, self.ulps) + } +} + +/// Create a new `Predicate` that ensures two numbers are "close" enough, understanding that +/// rounding errors occur. +/// +/// # Examples +/// +/// ``` +/// use predicates::predicate::*; +/// +/// let a = 0.15_f64 + 0.15_f64 + 0.15_f64; +/// let b = 0.1_f64 + 0.1_f64 + 0.25_f64; +/// let predicate_fn = float::is_close(a); +/// assert_eq!(true, predicate_fn.eval(&b)); +/// assert_eq!(false, predicate_fn.distance(0).eval(&b)); +/// ``` +pub fn is_close(target: f64) -> IsClosePredicate { + IsClosePredicate { + target, + epsilon: 2.0 * ::std::f64::EPSILON, + ulps: 2, + } +} diff --git a/src/predicate/float/mod.rs b/src/predicate/float/mod.rs new file mode 100644 index 0000000..76c3377 --- /dev/null +++ b/src/predicate/float/mod.rs @@ -0,0 +1,16 @@ +// Copyright (c) 2018 The predicates-rs Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Float Predicates +//! +//! This module contains predicates specific to string handling. + +#[cfg(feature = "float-cmp")] +mod close; +#[cfg(feature = "float-cmp")] +pub use self::close::{is_close, IsClosePredicate}; diff --git a/src/predicate/mod.rs b/src/predicate/mod.rs index 0099668..0aa8cc8 100644 --- a/src/predicate/mod.rs +++ b/src/predicate/mod.rs @@ -22,6 +22,11 @@ pub use self::ord::{eq, ge, gt, le, lt, ne, EqPredicate, OrdPredicate}; pub use self::set::{contains, contains_hashable, contains_ord, ContainsPredicate, HashableContainsPredicate, OrdContainsPredicate}; +// specialized primitive `Predicate` types +pub mod str; +pub mod path; +pub mod float; + // combinators mod boolean; mod boxed; diff --git a/src/predicate/path/existence.rs b/src/predicate/path/existence.rs new file mode 100644 index 0000000..d4b80b0 --- /dev/null +++ b/src/predicate/path/existence.rs @@ -0,0 +1,57 @@ +// Copyright (c) 2018 The predicates-rs Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::path; + +use Predicate; + +/// Predicate that checks if a file is present +/// +/// This is created by the `predicate::path::exists` and `predicate::path::missing`. +#[derive(Debug)] +pub struct ExistencePredicate { + exists: bool, +} + +impl Predicate for ExistencePredicate { + type Item = path::Path; + + fn eval(&self, path: &path::Path) -> bool { + path.exists() == self.exists + } +} + +/// Creates a new `Predicate` that ensures the path exists. +/// +/// # Examples +/// +/// ``` +/// use std::path::Path; +/// use predicates::predicate::*; +/// +/// let predicate_fn = path::exists(); +/// assert_eq!(true, predicate_fn.eval(Path::new("Cargo.toml"))); +/// ``` +pub fn exists() -> ExistencePredicate { + ExistencePredicate { exists: true } +} + +/// Creates a new `Predicate` that ensures the path doesn't exist. +/// +/// # Examples +/// +/// ``` +/// use std::path::Path; +/// use predicates::predicate::*; +/// +/// let predicate_fn = path::missing(); +/// assert_eq!(true, predicate_fn.eval(Path::new("non-existent-file.foo"))); +/// ``` +pub fn missing() -> ExistencePredicate { + ExistencePredicate { exists: false } +} diff --git a/src/predicate/path/ft.rs b/src/predicate/path/ft.rs new file mode 100644 index 0000000..e863726 --- /dev/null +++ b/src/predicate/path/ft.rs @@ -0,0 +1,125 @@ +// Copyright (c) 2018 The predicates-rs Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::path; +use std::fs; + +use Predicate; + +#[derive(Clone, Copy, Debug)] +enum FileType { + File, + Dir, + Symlink, +} + +impl FileType { + fn eval(self, ft: &fs::FileType) -> bool { + match self { + FileType::File => ft.is_file(), + FileType::Dir => ft.is_dir(), + FileType::Symlink => ft.is_symlink(), + } + } +} + +/// Predicate that checks the `std::fs::FileType`. +/// +/// This is created by the `predicate::path::is_file`, `predicate::path::is_dir`, and `predicate::path::is_symlink`. +#[derive(Debug)] +pub struct FileTypePredicate { + ft: FileType, + follow: bool, +} + +impl FileTypePredicate { + /// Follow symbolic links. + /// + /// When yes is true, symbolic links are followed as if they were normal directories and files. + /// + /// Default: disabled. + pub fn follow_links(mut self, yes: bool) -> Self { + self.follow = yes; + self + } +} + +impl Predicate for FileTypePredicate { + type Item = path::Path; + + fn eval(&self, path: &path::Path) -> bool { + let metadata = if self.follow { + path.metadata() + } else { + path.symlink_metadata() + }; + metadata + .map(|m| self.ft.eval(&m.file_type())) + .unwrap_or(false) + } +} + +/// Creates a new `Predicate` that ensures the path points to a file. +/// +/// # Examples +/// +/// ``` +/// use std::path::Path; +/// use predicates::predicate::*; +/// +/// let predicate_fn = path::is_file(); +/// assert_eq!(true, predicate_fn.eval(Path::new("Cargo.toml"))); +/// assert_eq!(false, predicate_fn.eval(Path::new("src"))); +/// assert_eq!(false, predicate_fn.eval(Path::new("non-existent-file.foo"))); +/// ``` +pub fn is_file() -> FileTypePredicate { + FileTypePredicate { + ft: FileType::File, + follow: false, + } +} + +/// Creates a new `Predicate` that ensures the path points to a directory. +/// +/// # Examples +/// +/// ``` +/// use std::path::Path; +/// use predicates::predicate::*; +/// +/// let predicate_fn = path::is_dir(); +/// assert_eq!(false, predicate_fn.eval(Path::new("Cargo.toml"))); +/// assert_eq!(true, predicate_fn.eval(Path::new("src"))); +/// assert_eq!(false, predicate_fn.eval(Path::new("non-existent-file.foo"))); +/// ``` +pub fn is_dir() -> FileTypePredicate { + FileTypePredicate { + ft: FileType::Dir, + follow: false, + } +} + +/// Creates a new `Predicate` that ensures the path points to a symlink. +/// +/// # Examples +/// +/// ``` +/// use std::path::Path; +/// use predicates::predicate::*; +/// +/// let predicate_fn = path::is_symlink(); +/// assert_eq!(false, predicate_fn.eval(Path::new("Cargo.toml"))); +/// assert_eq!(false, predicate_fn.eval(Path::new("src"))); +/// assert_eq!(false, predicate_fn.eval(Path::new("non-existent-file.foo"))); +/// ``` +pub fn is_symlink() -> FileTypePredicate { + FileTypePredicate { + ft: FileType::Symlink, + follow: false, + } +} diff --git a/src/predicate/path/mod.rs b/src/predicate/path/mod.rs new file mode 100644 index 0000000..bcc9a89 --- /dev/null +++ b/src/predicate/path/mod.rs @@ -0,0 +1,16 @@ +// Copyright (c) 2018 The predicates-rs Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Path Predicates +//! +//! This module contains predicates specific to the file system. + +mod existence; +pub use self::existence::{exists, missing, ExistencePredicate}; +mod ft; +pub use self::ft::{is_dir, is_file, is_symlink, FileTypePredicate}; diff --git a/src/predicate/str/difference.rs b/src/predicate/str/difference.rs new file mode 100644 index 0000000..6370ae5 --- /dev/null +++ b/src/predicate/str/difference.rs @@ -0,0 +1,140 @@ +// Copyright (c) 2018 The predicates-rs Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::borrow; + +use difference; + +use Predicate; + +#[derive(Clone, Copy, Debug)] +enum DistanceOp { + Similar, + Different, +} + +impl DistanceOp { + fn eval(self, limit: i32, distance: i32) -> bool { + match self { + DistanceOp::Similar => distance <= limit, + DistanceOp::Different => limit < distance, + } + } +} + +/// Predicate that diffs two strings. +/// +/// This is created by the `predicate::str::similar`. +#[derive(Clone, Debug)] +pub struct DifferencePredicate { + orig: borrow::Cow<'static, str>, + split: borrow::Cow<'static, str>, + distance: i32, + op: DistanceOp, +} + +impl DifferencePredicate { + /// The split used when identifying changes. + /// + /// Common splits include: + /// - `""` for char-level. + /// - `" "` for word-level. + /// - `"\n" for line-level. + /// + /// Default: `"\n"` + /// + /// # Examples + /// + /// ``` + /// use predicates::predicate::*; + /// + /// let predicate_fn = str::similar("Hello World").split(" "); + /// assert_eq!(true, predicate_fn.eval("Hello World")); + /// ``` + pub fn split(mut self, split: S) -> Self + where + S: Into>, + { + self.split = split.into(); + self + } + + /// The maximum allowed edit distance. + /// + /// Default: `0` + /// + /// # Examples + /// + /// ``` + /// use predicates::predicate::*; + /// + /// let predicate_fn = str::similar("Hello World!").split("").distance(1); + /// assert_eq!(true, predicate_fn.eval("Hello World!")); + /// assert_eq!(true, predicate_fn.eval("Hello World")); + /// assert_eq!(false, predicate_fn.eval("Hello World?")); + /// ``` + pub fn distance(mut self, distance: i32) -> Self { + self.distance = distance; + self + } +} + +impl Predicate for DifferencePredicate { + type Item = str; + + fn eval(&self, edit: &str) -> bool { + let change = difference::Changeset::new(&self.orig, edit, &self.split); + self.op.eval(self.distance, change.distance) + } +} + +/// Creates a new `Predicate` that diffs two strings. +/// +/// # Examples +/// +/// ``` +/// use predicates::predicate::*; +/// +/// let predicate_fn = str::diff("Hello World"); +/// assert_eq!(false, predicate_fn.eval("Hello World")); +/// assert_eq!(true, predicate_fn.eval("Goodbye World")); +/// ``` +pub fn diff(orig: S) -> DifferencePredicate +where + S: Into>, +{ + DifferencePredicate { + orig: orig.into(), + split: "\n".into(), + distance: 0, + op: DistanceOp::Different, + } +} + +/// Creates a new `Predicate` that checks strings for how similar they are. +/// +/// # Examples +/// +/// ``` +/// use predicates::predicate::*; +/// +/// let predicate_fn = str::similar("Hello World"); +/// assert_eq!(true, predicate_fn.eval("Hello World")); +/// assert_eq!(false, predicate_fn.eval("Goodbye World")); +/// ``` +pub fn similar(orig: S) -> DifferencePredicate +where + S: Into>, +{ + DifferencePredicate { + orig: orig.into(), + split: "\n".into(), + distance: 0, + op: DistanceOp::Similar, + } +} diff --git a/src/predicate/str/mod.rs b/src/predicate/str/mod.rs new file mode 100644 index 0000000..51f9ea8 --- /dev/null +++ b/src/predicate/str/mod.rs @@ -0,0 +1,21 @@ +// Copyright (c) 2018 The predicates-rs Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! String Predicates +//! +//! This module contains predicates specific to string handling. + +#[cfg(feature = "difference")] +mod difference; +#[cfg(feature = "difference")] +pub use self::difference::{diff, similar, DifferencePredicate}; + +#[cfg(feature = "regex")] +mod regex; +#[cfg(feature = "regex")] +pub use self::regex::{is_match, RegexError, RegexPredicate}; diff --git a/src/predicate/str/regex.rs b/src/predicate/str/regex.rs new file mode 100644 index 0000000..2849e69 --- /dev/null +++ b/src/predicate/str/regex.rs @@ -0,0 +1,48 @@ +// Copyright (c) 2018 The predicates-rs Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use regex; + +use Predicate; + +/// An error that occurred during parsing or compiling a regular expression. +pub type RegexError = regex::Error; + +/// Predicate that uses regex matching +/// +/// This is created by the `predicate::str::is_match`. +#[derive(Clone, Debug)] +pub struct RegexPredicate { + re: regex::Regex, +} + +impl Predicate for RegexPredicate { + type Item = str; + + fn eval(&self, variable: &str) -> bool { + self.re.is_match(variable) + } +} + +/// Creates a new `Predicate` that uses a regular expression to match the string. +/// +/// # Examples +/// +/// ``` +/// use predicates::predicate::*; +/// +/// let predicate_fn = str::is_match("^Hel.o.*$").unwrap(); +/// assert_eq!(true, predicate_fn.eval("Hello World")); +/// assert_eq!(false, predicate_fn.eval("Food World")); +/// ``` +pub fn is_match(pattern: S) -> Result +where + S: AsRef, +{ + regex::Regex::new(pattern.as_ref()).map(|re| RegexPredicate { re }) +}