pooly 0.2.1

A protobuf to Postgres adapter + connection pooling middleware.
Documentation
use serde::{Deserialize, Serialize};

use crate::models::errors::WildcardPatternError;

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(try_from = "String", into = "String")]
pub enum WildcardPattern {

    Any,
    Contains {
        pattern: String,
        contains: String
    },
    EndsWith {
        pattern: String,
        ends_with: String
    },
    StartsWith {
        pattern: String,
        starts_with: String
    },
    StartsAndEndsWith{
        pattern: String,
        starts_with: String,
        ends_with: String
    }

}

const STAR: &str = "*";

impl WildcardPattern {

    pub fn matches(&self, target: &str) -> bool {
        match self {
            WildcardPattern::Any => true,
            WildcardPattern::Contains { pattern: _, contains } =>
                target.contains(contains),
            WildcardPattern::EndsWith { pattern: _, ends_with } =>
                target.ends_with(ends_with),
            WildcardPattern::StartsWith { pattern: _, starts_with } =>
                target.starts_with(starts_with),
            WildcardPattern::StartsAndEndsWith { pattern: _, starts_with, ends_with } =>
                target.starts_with(starts_with) && target.ends_with(ends_with)
        }
    }

    pub fn parse(value: &str) -> Result<WildcardPattern, WildcardPatternError> {
        if value == STAR {
            return Ok(WildcardPattern::Any);
        }

        let num_stars = value.matches(STAR).count();

        match num_stars {
            0 => Err(WildcardPatternError::NoStars),
            1 => Ok(WildcardPattern::parse_one_star(value)),
            2 => WildcardPattern::parse_two_star(value),
            _ => Err(WildcardPatternError::TooManyStars)
        }
    }

    fn parse_one_star(value: &str) -> WildcardPattern {
        if value.ends_with(STAR) {
            return WildcardPattern::StartsWith {
                pattern: value.into(),
                starts_with: value.replace(STAR, "")
            };
        }

        if value.starts_with(STAR) {
            return WildcardPattern::EndsWith {
                pattern: value.into(),
                ends_with: value.replace(STAR, "")
            };
        }

        let split: Vec<&str> = value.split(STAR).collect();

        WildcardPattern::StartsAndEndsWith {
            pattern: value.into(),
            starts_with: split[0].into(),
            ends_with: split[1].into()
        }
    }

    fn parse_two_star(value: &str) -> Result<WildcardPattern, WildcardPatternError> {
        if value.starts_with(STAR) && value.ends_with(STAR) {
            return Ok(
                WildcardPattern::Contains {
                    pattern: value.into(),
                    contains: value.replacen(STAR, "", 2)
                }
            );
        }

        Err(WildcardPatternError::UnsupportedPattern)
    }

}

impl TryFrom<String> for WildcardPattern {
    type Error = WildcardPatternError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        WildcardPattern::parse(&value)
    }
}

impl Into<String> for WildcardPattern {
    fn into(self) -> String {
        match self {
            WildcardPattern::Any => STAR.into(),
            WildcardPattern::Contains { pattern, contains: _ } => pattern,
            WildcardPattern::EndsWith { pattern, ends_with: _ } => pattern,
            WildcardPattern::StartsWith { pattern, starts_with: _ } => pattern,
            WildcardPattern::StartsAndEndsWith { pattern, starts_with: _, ends_with: _ } => pattern
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::models::errors::WildcardPatternError;
    use crate::models::utils::wildcards::WildcardPattern;

    #[test]
    fn test_matches_any_correctly() {
        check(
            &WildcardPattern::Any,
            vec!["alf-loves-cats", "alfcats", "alf+cats", "", "something"],
            vec![]);
    }

    #[test]
    fn test_matches_starts_and_ends_with_correctly() {
        check(
            &WildcardPattern::StartsAndEndsWith {
                pattern: "alf*cats".into(),
                starts_with: "alf".into(),
                ends_with: "cats".into()
            },
            vec!["alf-loves-cats", "alfcats", "alf+cats"],
            vec!["alf_loves_ats", "", "cats", "not-only-alf-loves-cats"]);
    }

    #[test]
    fn test_matches_ends_with_correctly() {
        check(
            &WildcardPattern::EndsWith {
                pattern: "*alf".into(),
                ends_with: "alf".into()
            },
            vec!["cats_dont_like_alf", "alf-loves-alf", "alf"],
            vec!["alf_loves_cats", "", "cats", "not-only-alf-loves-cats"]);
    }

    #[test]
    fn test_matches_starts_with_correctly() {
        check(
            &WildcardPattern::StartsWith {
                pattern: "alf*".into(),
                starts_with: "alf".into()
            },
            vec!["alf_loves_cats", "alf-loves-eating-cats", "alf"],
            vec!["al", "", "cats", "not-only-alf-loves-cats"]);
    }

    #[test]
    fn test_matches_contains_correctly() {
        check(
            &WildcardPattern::Contains {
                pattern: "*alf*".into(),
                contains: "alf".into()
            },
            vec!["alf_loves_cats", "not-only-alf-loves-cats", "alf"],
            vec!["al", "", "cats"]);
    }

    #[test]
    fn test_builds_wildcard_patterns_correctly() {
        check_ok("*", WildcardPattern::Any);
        check_ok("alf_loves*cats", WildcardPattern::StartsAndEndsWith {
            pattern: "alf_loves*cats".into(),
            starts_with: "alf_loves".into(),
            ends_with: "cats".into()
        });
        check_ok("*alf*", WildcardPattern::Contains {
            pattern: "*alf*".into(),
            contains: "alf".into()
        });
        check_ok("alf*", WildcardPattern::StartsWith {
            pattern: "alf*".into(),
            starts_with: "alf".into()
        });
        check_ok("*alf", WildcardPattern::EndsWith {
            pattern: "*alf".into(),
            ends_with: "alf".into()
        });
    }

    #[test]
    fn test_returns_err_on_illegal_patterns() {
        check_err("alf_loves_cats", WildcardPatternError::NoStars);

        check_err("*alf*loves*cats*", WildcardPatternError::TooManyStars);
        check_err("*alf*loves*cats", WildcardPatternError::TooManyStars);

        check_err("*alf*loves_cats", WildcardPatternError::UnsupportedPattern);
        check_err("**alf", WildcardPatternError::UnsupportedPattern);
    }

    fn check_err(value: &str,
                 expected_err: WildcardPatternError) {
        assert!(matches!(WildcardPattern::parse(value), Err(err) if err == expected_err));
    }

    fn check_ok(value: &str, expected: WildcardPattern) {
        assert!(matches!(WildcardPattern::parse(value), Ok(ok) if ok == expected));
    }

    fn check(pattern: &WildcardPattern,
             should_match: Vec<&str>,
             should_not_match: Vec<&str>) {
        for val in should_match {
            assert!(pattern.matches(val));
        }

        for val in should_not_match {
            assert!(!pattern.matches(val));
        }
    }

}