Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Move criteria hash value normalization to separate class.

Also allowing it to be injected so it can be changed at runtime.
  • Loading branch information...
commit ddd793d385e1a6af1f98ea972cb62e8dc93811c7 1 parent c4f8dba
@jnunemaker jnunemaker authored
View
71 lib/plucky/criteria_hash.rb
@@ -1,5 +1,6 @@
# encoding: UTF-8
require 'set'
+require 'plucky/normalizers/criteria_hash_value'
module Plucky
class CriteriaHash
@@ -16,10 +17,6 @@ class CriteriaHash
# criteria hash is simple.
SimpleQueryMaxSize = [SimpleIdQueryKeys.size, SimpleIdAndTypeQueryKeys.size].max
- # Internal: Used by normalized_value to determine if we need to run the
- # value through another criteria hash to normalize it.
- NestingOperators = [:$or, :$and, :$nor]
-
def initialize(hash={}, options={})
@source, @options = {}, options
hash.each { |key, value| self[key] = value }
@@ -122,55 +119,29 @@ def simple?
key_set == SimpleIdQueryKeys || key_set == SimpleIdAndTypeQueryKeys
end
- private
- def method_missing(method, *args, &block)
- @source.send(method, *args, &block)
- end
-
- def object_id?(key)
- object_ids.include?(key.to_sym)
- end
+ def method_missing(method, *args, &block)
+ @source.send(method, *args, &block)
+ end
- def normalized_key(key)
- key = key.to_sym if key.respond_to?(:to_sym)
- return normalized_key(key.field) if key.respond_to?(:field)
- return :_id if key == :id
- key
- end
+ def object_id?(key)
+ object_ids.include?(key.to_sym)
+ end
- def normalized_value(parent_key, key, value)
- case value
- when Array, Set
- if object_id?(parent_key)
- value = value.map { |v| Plucky.to_object_id(v) }
- end
+ def normalized_key(key)
+ key = key.to_sym if key.respond_to?(:to_sym)
+ return normalized_key(key.field) if key.respond_to?(:field)
+ return :_id if key == :id
+ key
+ end
- if nesting_operator?(key)
- value.map { |v| CriteriaHash.new(v, options).to_hash }
- elsif parent_key == key
- # we're not nested and not the value for a symbol operator
- {'$in' => value.to_a}
- else
- # we are a value for a symbol operator or nested hash
- value.to_a
- end
- when Time
- value.utc
- when String
- return Plucky.to_object_id(value) if object_id?(key)
- value
- when Hash
- value.each { |k, v| value[k] = normalized_value(key, k, v) }
- value
- when Regexp
- Regexp.new(value)
- else
- value
- end
- end
+ def normalized_value(parent_key, key, value)
+ value_normalizer.call(parent_key, key, value)
+ end
- def nesting_operator?(key)
- NestingOperators.include?(key)
- end
+ def value_normalizer
+ @value_normalizer ||= options.fetch(:value_normalizer) {
+ Normalizers::CriteriaHashValue.new(self)
+ }
+ end
end
end
View
77 lib/plucky/normalizers/criteria_hash_value.rb
@@ -0,0 +1,77 @@
+module Plucky
+ module Normalizers
+ class CriteriaHashValue
+
+ # Internal: Used by normalized_value to determine if we need to run the
+ # value through another criteria hash to normalize it.
+ NestingOperators = [:$or, :$and, :$nor]
+
+ def initialize(criteria_hash)
+ @criteria_hash = criteria_hash
+ end
+
+ # Public: Returns value normalized for Mongo
+ #
+ # parent_key - The parent key if nested, otherwise same as key
+ # key - The key we are currently normalizing
+ # value - The value that should be normalized
+ def call(parent_key, key, value)
+ case value
+ when Array, Set
+ if object_id?(parent_key)
+ value = value.map { |v| to_object_id(v) }
+ end
+
+ if nesting_operator?(key)
+ value.map { |v| criteria_hash_class.new(v, options).to_hash }
+ elsif parent_key == key
+ # we're not nested and not the value for a symbol operator
+ {'$in' => value.to_a}
+ else
+ # we are a value for a symbol operator or nested hash
+ value.to_a
+ end
+ when Time
+ value.utc
+ when String
+ if object_id?(key)
+ return to_object_id(value)
+ end
+ value
+ when Hash
+ value.each { |k, v| value[k] = call(key, k, v) }
+ value
+ when Regexp
+ Regexp.new(value)
+ else
+ value
+ end
+ end
+
+ # Private: Ensures value is object id if possible
+ def to_object_id(value)
+ Plucky.to_object_id(value)
+ end
+
+ # Private: Returns class of provided criteria hash
+ def criteria_hash_class
+ @criteria_hash.class
+ end
+
+ # Private: Returns options of provided criteria hash
+ def options
+ @criteria_hash.options
+ end
+
+ # Private: Returns true or false if key should be converted to object id
+ def object_id?(key)
+ @criteria_hash.object_id?(key)
+ end
+
+ # Private: Returns true or false if key is a nesting operator
+ def nesting_operator?(key)
+ NestingOperators.include?(key)
+ end
+ end
+ end
+end
View
173 spec/plucky/criteria_hash_spec.rb
@@ -21,52 +21,6 @@
}
end
- context "nested clauses" do
- context "::NestingOperators" do
- it "returns array of operators that take nested queries" do
- described_class::NestingOperators.should == [:$or, :$and, :$nor]
- end
- end
-
- described_class::NestingOperators.each do |operator|
- context "#{operator}" do
- it "works with symbol operators" do
- nested1 = {:age.gt => 12, :age.lt => 20}
- translated1 = {:age => {'$gt' => 12, '$lt' => 20 }}
- nested2 = {:type.nin => ['friend', 'enemy']}
- translated2 = {:type => {'$nin' => ['friend', 'enemy']}}
-
- given = {operator.to_s => [nested1, nested2]}
-
- described_class.new(given)[operator].should == [translated1, translated2]
- end
-
- it "honors criteria hash options" do
- nested = {:post_id => '4f5ead6378fca23a13000001'}
- translated = {:post_id => BSON::ObjectId.from_string('4f5ead6378fca23a13000001')}
- given = {operator.to_s => [nested]}
-
- described_class.new(given, :object_ids => [:post_id])[operator].should == [translated]
- end
- end
- end
-
- context "doubly nested" do
- it "works with symbol operators" do
- nested1 = {:age.gt => 12, :age.lt => 20}
- translated1 = {:age => {'$gt' => 12, '$lt' => 20}}
- nested2 = {:type.nin => ['friend', 'enemy']}
- translated2 = {:type => {'$nin' => ['friend', 'enemy']}}
- nested3 = {'$and' => [nested2]}
- translated3 = {:$and => [translated2]}
-
- given = {'$or' => [nested1, nested3]}
-
- described_class.new(given)[:$or].should == [translated1, translated3]
- end
- end
- end
-
context "#initialize_copy" do
before do
@original = described_class.new({
@@ -110,33 +64,6 @@
end
context "#[]=" do
- it "leaves string values for string keys alone" do
- criteria = described_class.new
- criteria[:foo] = 'bar'
- criteria[:foo].should == 'bar'
- end
-
- it "converts string values to object ids for object id keys" do
- id = BSON::ObjectId.new
- criteria = described_class.new({}, :object_ids => [:_id])
- criteria[:_id] = id.to_s
- criteria[:_id].should == id
- end
-
- it "converts sets to arrays" do
- criteria = described_class.new
- criteria[:foo] = [1, 2].to_set
- criteria[:foo].should == {'$in' => [1, 2]}
- end
-
- it "converts times to utc" do
- time = Time.now
- criteria = described_class.new
- criteria[:foo] = time
- criteria[:foo].should be_utc
- criteria[:foo].should == time.utc
- end
-
it "converts :id to :_id" do
criteria = described_class.new
criteria[:id] = 1
@@ -174,106 +101,6 @@
end
end
- context "with time value" do
- it "converts to utc if not utc" do
- described_class.new(:created_at => Time.now)[:created_at].utc?.should be(true)
- end
-
- it "leaves utc alone" do
- described_class.new(:created_at => Time.now.utc)[:created_at].utc?.should be(true)
- end
- end
-
- context "with array value" do
- it "defaults to $in" do
- described_class.new(:numbers => [1,2,3])[:numbers].should == {'$in' => [1,2,3]}
- end
-
- it "uses existing modifier if present" do
- described_class.new(:numbers => {'$all' => [1,2,3]})[:numbers].should == {'$all' => [1,2,3]}
- described_class.new(:numbers => {'$any' => [1,2,3]})[:numbers].should == {'$any' => [1,2,3]}
- end
-
- it "does not turn value to $in with $or key" do
- described_class.new(:$or => [{:numbers => 1}, {:numbers => 2}] )[:$or].should == [{:numbers=>1}, {:numbers=>2}]
- end
-
- it "does not turn value to $in with $and key" do
- described_class.new(:$and => [{:numbers => 1}, {:numbers => 2}] )[:$and].should == [{:numbers=>1}, {:numbers=>2}]
- end
-
- it "does not turn value to $in with $nor key" do
- described_class.new(:$nor => [{:numbers => 1}, {:numbers => 2}] )[:$nor].should == [{:numbers=>1}, {:numbers=>2}]
- end
-
- it "defaults to $in even with ObjectId keys" do
- described_class.new({:mistake_id => [1,2,3]}, :object_ids => [:mistake_id])[:mistake_id].should == {'$in' => [1,2,3]}
- end
- end
-
- context "with set value" do
- it "defaults to $in and convert to array" do
- described_class.new(:numbers => [1,2,3].to_set)[:numbers].should == {'$in' => [1,2,3]}
- end
-
- it "uses existing modifier if present and convert to array" do
- described_class.new(:numbers => {'$all' => [1,2,3].to_set})[:numbers].should == {'$all' => [1,2,3]}
- described_class.new(:numbers => {'$any' => [1,2,3].to_set})[:numbers].should == {'$any' => [1,2,3]}
- end
- end
-
- context "with string ids for string keys" do
- before do
- @id = BSON::ObjectId.new
- @room_id = BSON::ObjectId.new
- @criteria = described_class.new(:_id => @id.to_s, :room_id => @room_id.to_s)
- end
-
- it "leaves string ids as strings" do
- @criteria[:_id].should == @id.to_s
- @criteria[:room_id].should == @room_id.to_s
- @criteria[:_id].should be_instance_of(String)
- @criteria[:room_id].should be_instance_of(String)
- end
- end
-
- context "with string ids for object id keys" do
- before do
- @id = BSON::ObjectId.new
- @room_id = BSON::ObjectId.new
- end
-
- it "converts strings to object ids" do
- criteria = described_class.new({:_id => @id.to_s, :room_id => @room_id.to_s}, :object_ids => [:_id, :room_id])
- criteria[:_id].should == @id
- criteria[:room_id].should == @room_id
- criteria[:_id].should be_instance_of(BSON::ObjectId)
- criteria[:room_id].should be_instance_of(BSON::ObjectId)
- end
-
- it "converts :id with string value to object id value" do
- criteria = described_class.new({:id => @id.to_s}, :object_ids => [:_id])
- criteria[:_id].should == @id
- end
- end
-
- context "with string ids for object id keys (nested)" do
- before do
- @id1 = BSON::ObjectId.new
- @id2 = BSON::ObjectId.new
- @ids = [@id1.to_s, @id2.to_s]
- @criteria = described_class.new({:_id => {'$in' => @ids}}, :object_ids => [:_id])
- end
-
- it "converts strings to object ids" do
- @criteria[:_id].should == {'$in' => [@id1, @id2]}
- end
-
- it "does not modify original array of string ids" do
- @ids.should == [@id1.to_s, @id2.to_s]
- end
- end
-
context "#merge" do
it "works when no keys match" do
c1 = described_class.new(:foo => 'bar')
View
187 spec/plucky/normalizers/criteria_hash_value_spec.rb
@@ -0,0 +1,187 @@
+require 'helper'
+
+describe Plucky::Normalizers::CriteriaHashValue do
+ let(:criteria_hash) { Plucky::CriteriaHash.new }
+
+ subject {
+ described_class.new(criteria_hash)
+ }
+
+ context "with a string" do
+ it "leaves string values for string keys alone" do
+ subject.call(:foo, :foo, 'bar').should eq('bar')
+ end
+
+ context "that is actually an object id" do
+ it "converts string values to object ids for object id keys" do
+ criteria_hash.object_ids = [:_id]
+ id = BSON::ObjectId.new
+ subject.call(:_id, :_id, id.to_s).should eq(id)
+ end
+ end
+ end
+
+ context "with a time" do
+ it "converts times to utc" do
+ time = Time.now
+ actual = time
+ expected = time.utc
+ result = subject.call(:foo, :foo, actual)
+ result.should be_utc
+ result.should eq(expected)
+ end
+
+ it "leaves utc times alone" do
+ time = Time.now
+ actual = time.utc
+ expected = time.utc
+ result = subject.call(:foo, :foo, actual)
+ result.should be_utc
+ result.should eq(expected)
+ end
+ end
+
+ context "with an array" do
+ it "defaults to $in" do
+ actual = [1,2,3]
+ expected = {'$in' => [1,2,3]}
+ subject.call(:foo, :foo, actual).should eq(expected)
+ end
+
+ it "uses existing modifier if present" do
+ actual = {'$all' => [1,2,3]}
+ expected = {'$all' => [1,2,3]}
+ subject.call(:foo, :foo, actual).should eq(expected)
+
+ actual = {'$any' => [1,2,3]}
+ expected = {'$any' => [1,2,3]}
+ subject.call(:foo, :foo, actual).should eq(expected)
+ end
+
+ it "does not turn value to $in with $or key" do
+ actual = [{:numbers => 1}, {:numbers => 2}]
+ expected = [{:numbers => 1}, {:numbers => 2}]
+ subject.call(:$or, :$or, actual).should eq(expected)
+ end
+
+ it "does not turn value to $in with $and key" do
+ actual = [{:numbers => 1}, {:numbers => 2}]
+ expected = [{:numbers => 1}, {:numbers => 2}]
+ subject.call(:$and, :$and, actual).should eq(expected)
+ end
+
+ it "does not turn value to $in with $nor key" do
+ actual = [{:numbers => 1}, {:numbers => 2}]
+ expected = [{:numbers => 1}, {:numbers => 2}]
+ subject.call(:$nor, :$nor, actual).should eq(expected)
+ end
+
+ it "defaults to $in even with ObjectId keys" do
+ actual = [1,2,3]
+ expected = {'$in' => [1,2,3]}
+ criteria_hash.object_ids = [:mistake_id]
+ subject.call(:mistake_id, :mistake_id, actual).should eq(expected)
+ end
+ end
+
+ context "with a set" do
+ it "defaults to $in and convert to array" do
+ actual = [1,2,3].to_set
+ expected = {'$in' => [1,2,3]}
+ subject.call(:numbers, :numbers, actual).should eq(expected)
+ end
+
+ it "uses existing modifier if present and convert to array" do
+ actual = {'$all' => [1,2,3].to_set}
+ expected = {'$all' => [1,2,3]}
+ subject.call(:foo, :foo, actual).should eq(expected)
+
+ actual = {'$any' => [1,2,3].to_set}
+ expected = {'$any' => [1,2,3]}
+ subject.call(:foo, :foo, actual).should eq(expected)
+ end
+ end
+
+ context "with string object ids for string keys" do
+ let(:object_id) { BSON::ObjectId.new }
+
+ it "leaves string ids as strings" do
+ subject.call(:_id, :_id, object_id.to_s).should eq(object_id.to_s)
+ subject.call(:room_id, :room_id, object_id.to_s).should eq(object_id.to_s)
+ end
+ end
+
+ context "with string object ids for object id keys" do
+ let(:object_id) { BSON::ObjectId.new }
+
+ before do
+ criteria_hash.object_ids = [:_id, :room_id]
+ end
+
+ it "converts strings to object ids" do
+ subject.call(:_id, :_id, object_id.to_s).should eq(object_id)
+ subject.call(:room_id, :room_id, object_id.to_s).should eq(object_id)
+ end
+
+ context "nested with modifier" do
+ let(:oid1) { BSON::ObjectId.new }
+ let(:oid2) { BSON::ObjectId.new }
+ let(:oids) { [oid1.to_s, oid2.to_s] }
+
+ it "converts strings to object ids" do
+ actual = {'$in' => oids}
+ expected = {'$in' => [oid1, oid2]}
+ subject.call(:_id, :_id, actual).should eq(expected)
+ end
+
+ it "does not modify original array of string ids" do
+ subject.call(:_id, :_id, {'$in' => oids})
+ oids.should == [oid1.to_s, oid2.to_s]
+ end
+ end
+ end
+
+ context "nested clauses" do
+ it "knows constant array of operators that take nested queries" do
+ described_class::NestingOperators.should == [:$or, :$and, :$nor]
+ end
+
+ described_class::NestingOperators.each do |operator|
+ context "with #{operator}" do
+ it "works with symbol operators" do
+ nested1 = {:age.gt => 12, :age.lt => 20}
+ translated1 = {:age => {'$gt' => 12, '$lt' => 20 }}
+ nested2 = {:type.nin => ['friend', 'enemy']}
+ translated2 = {:type => {'$nin' => ['friend', 'enemy']}}
+ value = [nested1, nested2]
+ expected = [translated1, translated2]
+
+ subject.call(operator, operator, value).should eq(expected)
+ end
+
+ it "honors criteria hash options" do
+ nested = [{:post_id => '4f5ead6378fca23a13000001'}]
+ translated = [{:post_id => BSON::ObjectId.from_string('4f5ead6378fca23a13000001')}]
+ given = {operator.to_s => [nested]}
+
+ criteria_hash.object_ids = [:post_id]
+ subject.call(operator, operator, nested).should eq(translated)
+ end
+ end
+ end
+
+ context "doubly nested" do
+ it "works with symbol operators" do
+ nested1 = {:age.gt => 12, :age.lt => 20}
+ translated1 = {:age => {'$gt' => 12, '$lt' => 20}}
+ nested2 = {:type.nin => ['friend', 'enemy']}
+ translated2 = {:type => {'$nin' => ['friend', 'enemy']}}
+ nested3 = {'$and' => [nested2]}
+ translated3 = {:$and => [translated2]}
+ expected = [translated1, translated3]
+
+ subject.call(:$or, :$or, [nested1, nested3]).should eq(expected)
+ end
+ end
+ end
+end

0 comments on commit ddd793d

Please sign in to comment.
Something went wrong with that request. Please try again.