Permalink
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...
1 parent c4f8dba commit ddd793d385e1a6af1f98ea972cb62e8dc93811c7 @jnunemaker jnunemaker committed Oct 11, 2012
@@ -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
@@ -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
@@ -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')
Oops, something went wrong.

0 comments on commit ddd793d

Please sign in to comment.