Skip to content

Instantly share code, notes, and snippets.

@mattgoldman
Last active December 26, 2015 08:59
Show Gist options
  • Save mattgoldman/7125774 to your computer and use it in GitHub Desktop.
Save mattgoldman/7125774 to your computer and use it in GitHub Desktop.
Ruby module to evaluate a Hash against a set of filters. i.e. Does `{name: "John", age: 21}` pass the following filter?: `age > 20`
module HashFilter
# Runs filters on Hash and determines whether or not it shall pass
#
# ==== Attributes
#
# * +hash+ - +Hash+ to be filtered
# * +filters+ - +Array+ of objects containing the following keys for comparison
# <tt>:hash_key</tt> - Dot-notated, nestable hash key
# <tt>:comparison_operator</tt> - Any of the following comparison operators: +==+, +!=+, +<=+, +>=+, +<+, +>+
# <tt>:value</tt> - Desired value to compare for
def self.valid_hash?(hash, filters = [])
hash = hash.with_indifferent_access
filters.all? do |filter|
# No false filters allowed (everything must pass)
raise InvalidFilter.new("Invalid comparison operator. All filters must use one of the following operators: ==, !=, <=, >=, <, >") unless filter[:comparison_operator].in? ['==', '!=', '<=', '>=', '<', '>']
raise InvalidFilter.new("Invalid hash key. All hash keys must consist of only a-z, A-z, 0-9, -, _ characters") unless /^[a-zA-Z0-9\-_\.]+$/ === filter[:hash_key]
hash_value = HashFilter.fetch_value(filter[:hash_key], hash)
if hash_value.kind_of?(String) && !HashFilter.integer?(hash_value)
raise InvalidFilter.new("Invalid comparison operator. You cannot use <=, >=, <, or > on a non-numerical String value") unless filter[:comparison_operator].in? ['==', '!=']
end
if hash_value.kind_of? Integer
hash_value.send(filter[:comparison_operator].to_sym, filter[:value].to_i)
else # String
hash_value.send(filter[:comparison_operator].to_sym, filter[:value])
end
end
end
# Uses dot-notation to fetch a nested Hash key's value. Returns +nil+ if the key doesn't exist
#
# ==== Attributes
#
# * +dot_key+ - Nested hash key lookup i.e. person.address.zip_code
def self.fetch_value(dot_key, hash)
hash_path = dot_key.split('.')
hash_path.reduce(hash) do |memo, key|
memo[key.to_s] if memo
end
end
private
def self.integer?(string)
begin
!!Integer(string)
rescue ArgumentError, TypeError
false
end
end
class InvalidFilter < StandardError; end
end
require 'spec_helper'
# Todo: Handle arrays, booleans, floats, dates
describe HashFilter do
before :each do
@hash = {
"person" => {
"name" => {
"first_name" => "John",
"last_name" => "Doe"
},
"age" => 21
},
"address" => "1234 Imaginationland",
"amount" => "740"
}
end
describe "valid_hash?" do
context "hash key type" do
before :each do
@passing_filter = [
{
hash_key: 'address',
comparison_operator: '==',
value: '1234 Imaginationland'
}
]
@failing_filter = [
{
hash_key: 'address',
comparison_operator: '!=',
value: '1234 Imaginationland'
}
]
end
it "accepts hashes with string keys" do
expect(HashFilter.valid_hash?(@hash, @passing_filter)).to be_true
expect(HashFilter.valid_hash?(@hash, @failing_filter)).to be_false
end
it "accepts hashes with symbol keys" do
hash_with_symbols = {
person: {
name: {
first_name: "John",
last_name: "Doe"
},
age: 21
},
address: "1234 Imaginationland"
}
expect(HashFilter.valid_hash?(hash_with_symbols, @passing_filter)).to be_true
expect(HashFilter.valid_hash?(hash_with_symbols, @failing_filter)).to be_false
end
end
describe "number of passing/failing filters" do
it "validates with no filters" do
filters = []
expect(HashFilter.valid_hash?(@hash, filters)).to be_true
end
it "validates with only 1 passing filter" do
filters = [
{
hash_key: 'address',
comparison_operator: '==',
value: '1234 Imaginationland'
}
]
expect(HashFilter.valid_hash?(@hash, filters)).to be_true
end
it "validates with only 1 failing filter" do
filters = [
{
hash_key: 'address',
comparison_operator: '!=',
value: '1234 Imaginationland'
}
]
expect(HashFilter.valid_hash?(@hash, filters)).to be_false
end
it "validates with one failing of many filters" do
filters = [
{
hash_key: 'address',
comparison_operator: '!=',
value: '1234 Imaginationland'
},
{
hash_key: 'person.age',
comparison_operator: '==',
value: '21'
}
]
expect(HashFilter.valid_hash?(@hash, filters)).to be_false
end
it "validates with multiple failing filters" do
filters = [
{
hash_key: 'address',
comparison_operator: '!=',
value: '1234 Imaginationland'
},
{
hash_key: 'person.age',
comparison_operator: '>',
value: '21'
},
{
hash_key: 'person.name.first_name',
comparison_operator: '!=',
value: 'Sarah'
}
]
expect(HashFilter.valid_hash?(@hash, filters)).to be_false
end
it "validates with all failing filters" do
filters = [
{
hash_key: 'address',
comparison_operator: '!=',
value: '1234 Imaginationland'
},
{
hash_key: 'person.age',
comparison_operator: '>',
value: '21'
}
]
expect(HashFilter.valid_hash?(@hash, filters)).to be_false
end
it "validates with all passing filters" do
filters = [
{
hash_key: 'address',
comparison_operator: '==',
value: '1234 Imaginationland'
},
{
hash_key: 'person.age',
comparison_operator: '>=',
value: '21'
}
]
expect(HashFilter.valid_hash?(@hash, filters)).to be_true
end
end
describe "hash keys" do
it "rejects empty keys" do
bad_hash1 = {"" => "John"}
bad_hash2 = {nil => "John"}
filters1 = [
{
hash_key: '',
comparison_operator: '==',
value: 'John'
}
]
filters2 = [
{
hash_key: nil,
comparison_operator: '==',
value: 'John'
}
]
expect{HashFilter.valid_hash?(bad_hash1, filters1)}.to raise_error{HashFilter::InvalidFilter.new}
expect{HashFilter.valid_hash?(bad_hash2, filters2)}.to raise_error{HashFilter::InvalidFilter.new}
end
it "rejects non-[a-zA-Z0-9.-_]" do
bad_hash = {"person" => "John"}
filters = [
{
hash_key: 'pers*on',
comparison_operator: '==',
value: 'John'
}
]
expect{HashFilter.valid_hash?(bad_hash, filters)}.to raise_error{HashFilter::InvalidFilter.new}
end
end
describe "comparison operators" do
context "strings" do
context "number in string" do
it "accepts ==" do
filters1 = [{hash_key: 'amount', comparison_operator: '==', value: '740'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_true
filters2 = [{hash_key: 'amount', comparison_operator: '==', value: '800'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_false
end
it "accepts !=" do
filters1 = [{hash_key: 'amount', comparison_operator: '!=', value: '740'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_false
filters2 = [{hash_key: 'amount', comparison_operator: '!=', value: '800'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_true
end
it "accepts <=" do
filters1 = [{hash_key: 'amount', comparison_operator: '<=', value: '740'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_true
filters2 = [{hash_key: 'amount', comparison_operator: '<=', value: '800'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_true
filters3 = [{hash_key: 'amount', comparison_operator: '<=', value: '200'}]
expect(HashFilter.valid_hash?(@hash, filters3)).to be_false
end
it "accepts >=" do
filters1 = [{hash_key: 'amount', comparison_operator: '>=', value: '740'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_true
filters2 = [{hash_key: 'amount', comparison_operator: '>=', value: '500'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_true
filters3 = [{hash_key: 'amount', comparison_operator: '>=', value: '800'}]
expect(HashFilter.valid_hash?(@hash, filters3)).to be_false
end
it "accepts <" do
filters1 = [{hash_key: 'amount', comparison_operator: '<', value: '800'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_true
filters2 = [{hash_key: 'amount', comparison_operator: '<', value: '0'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_false
end
it "accepts >" do
filters1 = [{hash_key: 'amount', comparison_operator: '>', value: '800'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_false
filters2 = [{hash_key: 'amount', comparison_operator: '>', value: '0'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_true
end
end
context "non-number in string" do
it "accepts ==" do
filters1 = [{hash_key: 'address', comparison_operator: '==', value: '1234 Imaginationland'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_true
filters2 = [{hash_key: 'address', comparison_operator: '==', value: '5678 Dream Avenue'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_false
end
it "accepts !=" do
filters1 = [{hash_key: 'address', comparison_operator: '!=', value: '1234 Imaginationland'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_false
filters2 = [{hash_key: 'address', comparison_operator: '!=', value: '5678 Dream Avenue'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_true
end
it "rejects <=" do
filters = [{hash_key: 'address', comparison_operator: '<=', value: '56'}]
expect{HashFilter.valid_hash?(@hash, filters)}.to raise_error{HashFilter::InvalidFilter.new}
end
it "rejects >=" do
filters = [{hash_key: 'address', comparison_operator: '>=', value: '56'}]
expect{HashFilter.valid_hash?(@hash, filters)}.to raise_error{HashFilter::InvalidFilter.new}
end
it "rejects <" do
filters = [{hash_key: 'address', comparison_operator: '<', value: '56'}]
expect{HashFilter.valid_hash?(@hash, filters)}.to raise_error{HashFilter::InvalidFilter.new}
end
it "rejects >" do
filters = [{hash_key: 'address', comparison_operator: '>', value: '56'}]
expect{HashFilter.valid_hash?(@hash, filters)}.to raise_error{HashFilter::InvalidFilter.new}
end
end
it "rejects ===" do
filters = [{hash_key: 'address', comparison_operator: '===', value: '987 South Park Road'}]
expect{HashFilter.valid_hash?(@hash, filters)}.to raise_error{HashFilter::InvalidFilter.new}
end
it "rejects random non-sense like 'less than'" do
filters = [{hash_key: 'address', comparison_operator: 'less than', value: '987 South Park Road'}]
expect{HashFilter.valid_hash?(@hash, filters)}.to raise_error{HashFilter::InvalidFilter.new}
end
end
context "integers" do
it "accepts ==" do
filters1 = [{hash_key: 'person.age', comparison_operator: '==', value: '21'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_true
filters2 = [{hash_key: 'person.age', comparison_operator: '==', value: '25'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_false
end
it "accepts !=" do
filters1 = [{hash_key: 'person.age', comparison_operator: '!=', value: '21'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_false
filters2 = [{hash_key: 'person.age', comparison_operator: '!=', value: '25'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_true
end
it "accepts <=" do
filters1 = [{hash_key: 'person.age', comparison_operator: '<=', value: '21'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_true
filters2 = [{hash_key: 'person.age', comparison_operator: '<=', value: '25'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_true
filters3 = [{hash_key: 'person.age', comparison_operator: '<=', value: '17'}]
expect(HashFilter.valid_hash?(@hash, filters3)).to be_false
end
it "accepts >=" do
filters1 = [{hash_key: 'person.age', comparison_operator: '<=', value: '21'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_true
filters2 = [{hash_key: 'person.age', comparison_operator: '<=', value: '25'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_true
filters3 = [{hash_key: 'person.age', comparison_operator: '<=', value: '17'}]
expect(HashFilter.valid_hash?(@hash, filters3)).to be_false
end
it "accepts <" do
filters1 = [{hash_key: 'person.age', comparison_operator: '<', value: '25'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_true
filters2 = [{hash_key: 'person.age', comparison_operator: '<', value: '0'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_false
end
it "accepts >" do
filters1 = [{hash_key: 'person.age', comparison_operator: '>', value: '25'}]
expect(HashFilter.valid_hash?(@hash, filters1)).to be_false
filters2 = [{hash_key: 'person.age', comparison_operator: '>', value: '0'}]
expect(HashFilter.valid_hash?(@hash, filters2)).to be_true
end
it "rejects ===" do
filters = [{hash_key: 'person.age', comparison_operator: '===', value: '25'}]
expect{HashFilter.valid_hash?(@hash, filters)}.to raise_error{HashFilter::InvalidFilter.new}
end
it "rejects random non-sense like 'less than'" do
filters = [{hash_key: 'person.age', comparison_operator: 'less than', value: '25'}]
expect{HashFilter.valid_hash?(@hash, filters)}.to raise_error{HashFilter::InvalidFilter.new}
end
end
end
describe "values" do
it "compares integers" do
passing_integer_filter = [{
hash_key: 'person.age',
comparison_operator: '>',
value: 20
}]
failing_integer_filter = [{
hash_key: 'person.age',
comparison_operator: '<=',
value: 12
}]
expect(HashFilter.valid_hash?(@hash, passing_integer_filter)).to be_true
expect(HashFilter.valid_hash?(@hash, failing_integer_filter)).to be_false
end
it "compares numbers in strings" do
passing_integer_filter = [{
hash_key: 'person.age',
comparison_operator: '>',
value: '20'
}]
failing_integer_filter = [{
hash_key: 'person.age',
comparison_operator: '<=',
value: '12'
}]
expect(HashFilter.valid_hash?(@hash, passing_integer_filter)).to be_true
expect(HashFilter.valid_hash?(@hash, failing_integer_filter)).to be_false
end
it "compares strings" do
passing_string_filter = [{
hash_key: 'address',
comparison_operator: '==',
value: '1234 Imaginationland'
}]
failing_string_filter = [{
hash_key: 'address',
comparison_operator: '!=',
value: '1234 Imaginationland'
}]
expect(HashFilter.valid_hash?(@hash, passing_string_filter)).to be_true
expect(HashFilter.valid_hash?(@hash, failing_string_filter)).to be_false
end
end
end
describe "fetch_value(dot_key, hash)" do
context "key exists" do
it "fetches top-level key values" do
expect(HashFilter.fetch_value("address", @hash)).to eql "1234 Imaginationland"
end
it "fetches second-level key values" do
expect(HashFilter.fetch_value("person.age", @hash)).to eql 21
end
it "fetches third-level key values" do
expect(HashFilter.fetch_value("person.name.first_name", @hash)).to eql "John"
end
end
context "key doesn't exist" do
it "returns nil for top-level keys" do
expect(HashFilter.fetch_value("dog", @hash)).to eql nil
end
it "returns nil for deeply-nested keys" do
expect(HashFilter.fetch_value("person.name.middle_name", @hash)).to eql nil
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment