diff --git a/lib/mongoid/contextual/atomic.rb b/lib/mongoid/contextual/atomic.rb index 1587c4aca7..8ab4911504 100644 --- a/lib/mongoid/contextual/atomic.rb +++ b/lib/mongoid/contextual/atomic.rb @@ -169,17 +169,14 @@ def set(sets) # @example Unset the field on the matches. # context.unset(:name) # - # @param [ [ String | Symbol | Array | Hash ]... ] *args - # The name(s) of the field(s) to unset. - # If a Hash is specified, its keys will be used irrespective of what - # each key's value is, even if the value is nil or false. + # @param [ [ String | Symbol | Array | Hash ]... ] *unsets + # The name(s) of the field(s) to unset. If a Hash is specified, + # its keys will be used irrespective of value, even if the value + # is nil or false. # # @return [ nil ] Nil. - def unset(*args) - fields = args.map { |a| a.is_a?(Hash) ? a.keys : a } - .__find_args__ - .map { |f| [database_field_name(f), true] } - view.update_many("$unset" => Hash[fields]) + def unset(*unsets) + view.update_many('$unset' => collect_unset_operations(unsets)) end # Performs an atomic $min update operation on the given field or fields. @@ -247,6 +244,25 @@ def collect_each_operations(ops) operations[database_field_name(field)] = { "$each" => Array.wrap(value).mongoize } end end + + # Builds the selector an atomic $unset operation from arguments. + # + # @example Prepare selector from array. + # context.collect_unset_operations([:name, :age]) + # #=> { "name" => true, "age" => true } + # + # @example Prepare selector from hash. + # context.collect_unset_operations({ name: 1 }, { age: 1 }) + # #=> { "name" => true, "age" => true } + # + # @param [ String | Symbol | Array | Hash ] ops + # The name(s) of the field(s) to unset. + # + # @return [ Hash ] The selector for the atomic $unset operation. + def collect_unset_operations(ops) + ops.map { |op| op.is_a?(Hash) ? op.keys : op }.flatten + .map { |field| [database_field_name(field), true] }.to_h + end end end end diff --git a/lib/mongoid/criteria/findable.rb b/lib/mongoid/criteria/findable.rb index 2f116dc353..527821a631 100644 --- a/lib/mongoid/criteria/findable.rb +++ b/lib/mongoid/criteria/findable.rb @@ -41,7 +41,7 @@ def execute_or_raise(ids, multi) # # @return [ Document | Array ] The matching document(s). def find(*args) - ids = args.__find_args__ + ids = prepare_ids_for_find(args) raise_invalid if ids.any?(&:nil?) for_ids(ids).execute_or_raise(ids, multi_args?(args)) end @@ -134,6 +134,27 @@ def mongoize_ids(ids) end end + # Convert args to the +#find+ method into a flat array of ids. + # + # @example Get the ids. + # prepare_ids_for_find([ 1, [ 2, 3 ] ]) + # + # @param [ Array ] args The arguments. + # + # @return [ Array ] The array of ids. + def prepare_ids_for_find(args) + args.flat_map do |arg| + case arg + when Array, Set + prepare_ids_for_find(arg) + when Range + arg.begin&.numeric? && arg.end&.numeric? ? arg.to_a : arg + else + arg + end + end.uniq(&:to_s) + end + # Indicates whether the given arguments array is a list of values. # Used by the +find+ method to determine whether to return an array # or single value. diff --git a/lib/mongoid/extensions/array.rb b/lib/mongoid/extensions/array.rb index 7cf774c754..072a1d42b4 100644 --- a/lib/mongoid/extensions/array.rb +++ b/lib/mongoid/extensions/array.rb @@ -23,9 +23,11 @@ def __evolve_object_id__ # [ 1, 2, 3 ].__find_args__ # # @return [ Array ] The array of args. + # @deprecated def __find_args__ flat_map{ |a| a.__find_args__ }.uniq{ |a| a.to_s } end + Mongoid.deprecate(self, :__find_args__) # Mongoize the array into an array of object ids. # diff --git a/lib/mongoid/extensions/object.rb b/lib/mongoid/extensions/object.rb index 98e480c912..c14661bbcb 100644 --- a/lib/mongoid/extensions/object.rb +++ b/lib/mongoid/extensions/object.rb @@ -24,9 +24,11 @@ def __evolve_object_id__ # object.__find_args__ # # @return [ Object ] self. + # @deprecated def __find_args__ self end + Mongoid.deprecate(self, :__find_args__) # Mongoize a plain object into a time. # diff --git a/lib/mongoid/extensions/range.rb b/lib/mongoid/extensions/range.rb index a804dc2da6..e5dc8e89c3 100644 --- a/lib/mongoid/extensions/range.rb +++ b/lib/mongoid/extensions/range.rb @@ -3,9 +3,11 @@ module Mongoid module Extensions - # Adds type-casting behavior to Range class. module Range + def self.included(base) + base.extend(ClassMethods) + end # Get the range as arguments for a find. # @@ -13,9 +15,11 @@ module Range # range.__find_args__ # # @return [ Array ] The range as an array. + # @deprecated def __find_args__ to_a end + Mongoid.deprecate(self, :__find_args__) # Turn the object from the ruby type we deal with to a Mongo friendly # type. @@ -39,7 +43,6 @@ def resizable? end module ClassMethods - # Convert the object from its mongo friendly ruby type to this type. # # @example Demongoize the object. @@ -107,5 +110,4 @@ def __mongoize_range__(object) end end -::Range.__send__(:include, Mongoid::Extensions::Range) -::Range.extend(Mongoid::Extensions::Range::ClassMethods) +Range.include(Mongoid::Extensions::Range) diff --git a/lib/mongoid/extensions/set.rb b/lib/mongoid/extensions/set.rb index 21a887a5f6..574723f845 100644 --- a/lib/mongoid/extensions/set.rb +++ b/lib/mongoid/extensions/set.rb @@ -6,7 +6,6 @@ module Extensions # Adds type-casting behavior to Set class. module Set - # Turn the object from the ruby type we deal with to a Mongo friendly # type. # @@ -18,8 +17,17 @@ def mongoize ::Set.mongoize(self) end - module ClassMethods + # Returns whether the object's size can be changed. + # + # @example Is the object resizable? + # object.resizable? + # + # @return [ true ] true. + def resizable? + true + end + module ClassMethods # Convert the object from its mongo friendly ruby type to this type. # # @example Demongoize the object. diff --git a/spec/mongoid/criteria/findable_spec.rb b/spec/mongoid/criteria/findable_spec.rb index 9715fe1ae3..1468fc5f80 100644 --- a/spec/mongoid/criteria/findable_spec.rb +++ b/spec/mongoid/criteria/findable_spec.rb @@ -260,6 +260,196 @@ end end + context "when providing nested arrays of ids" do + + let!(:band_two) do + Band.create!(name: "Tool") + end + + context "when all ids match" do + + let(:found) do + Band.find([ [ band.id ], [ [ band_two.id ] ] ]) + end + + it "contains the first match" do + expect(found).to include(band) + end + + it "contains the second match" do + expect(found).to include(band_two) + end + + context "when ids are duplicates" do + + let(:found) do + Band.find([ [ band.id ], [ [ band.id ] ] ]) + end + + it "contains only the first match" do + expect(found).to eq([band]) + end + end + end + + context "when any id does not match" do + + context "when raising a not found error" do + config_override :raise_not_found_error, true + + let(:found) do + Band.find([ [ band.id ], [ [ BSON::ObjectId.new ] ] ]) + end + + it "raises an error" do + expect { + found + }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) + end + end + + context "when raising no error" do + config_override :raise_not_found_error, false + + let(:found) do + Band.find([ [ band.id ], [ [ BSON::ObjectId.new ] ] ]) + end + + it "returns only the matching documents" do + expect(found).to eq([ band ]) + end + end + end + end + + context "when providing a Set of ids" do + + let!(:band_two) do + Band.create!(name: "Tool") + end + + context "when all ids match" do + let(:found) do + Band.find(Set[ band.id, band_two.id ]) + end + + it "contains the first match" do + expect(found).to include(band) + end + + it "contains the second match" do + expect(found).to include(band_two) + end + end + + context "when any id does not match" do + + context "when raising a not found error" do + config_override :raise_not_found_error, true + + let(:found) do + Band.find(Set[ band.id, BSON::ObjectId.new ]) + end + + it "raises an error" do + expect { + found + }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) + end + end + + context "when raising no error" do + config_override :raise_not_found_error, false + + let(:found) do + Band.find(Set[ band.id, BSON::ObjectId.new ]) + end + + it "returns only the matching documents" do + expect(found).to eq([ band ]) + end + end + end + end + + context "when providing a Range of ids" do + + let!(:band_two) do + Band.create!(name: "Tool") + end + + context "when all ids match" do + let(:found) do + Band.find(band.id.to_s..band_two.id.to_s) + end + + it "contains the first match" do + expect(found).to include(band) + end + + it "contains the second match" do + expect(found).to include(band_two) + end + + + context "when any id does not match" do + + context "when raising a not found error" do + config_override :raise_not_found_error, true + + let(:found) do + Band.find(band_two.id.to_s..BSON::ObjectId.new) + end + + it "does not raise error and returns only the matching documents" do + expect(found).to eq([ band_two ]) + end + end + + context "when raising no error" do + config_override :raise_not_found_error, false + + let(:found) do + Band.find(band_two.id.to_s..BSON::ObjectId.new) + end + + it "returns only the matching documents" do + expect(found).to eq([ band_two ]) + end + end + end + end + + context "when all ids do not match" do + + context "when raising a not found error" do + config_override :raise_not_found_error, true + + let(:found) do + Band.find(BSON::ObjectId.new..BSON::ObjectId.new) + end + + it "raises an error" do + expect { + found + }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) + end + end + + context "when raising no error" do + config_override :raise_not_found_error, false + + let(:found) do + Band.find(BSON::ObjectId.new..BSON::ObjectId.new) + end + + it "returns only the matching documents" do + expect(found).to eq([]) + end + end + end + end + context "when providing a single id as extended json" do context "when the id matches" do diff --git a/spec/mongoid/extensions/object_spec.rb b/spec/mongoid/extensions/object_spec.rb index 0e95c16caf..4b2e294ded 100644 --- a/spec/mongoid/extensions/object_spec.rb +++ b/spec/mongoid/extensions/object_spec.rb @@ -16,13 +16,6 @@ end end - describe "#__find_args__" do - - it "returns self" do - expect(object.__find_args__).to eq(object) - end - end - describe "#__mongoize_object_id__" do it "returns self" do diff --git a/spec/mongoid/extensions/range_spec.rb b/spec/mongoid/extensions/range_spec.rb index 572f076fb2..58ea85c9ff 100644 --- a/spec/mongoid/extensions/range_spec.rb +++ b/spec/mongoid/extensions/range_spec.rb @@ -5,17 +5,6 @@ describe Mongoid::Extensions::Range do - describe "#__find_args__" do - - let(:range) do - 1..3 - end - - it "returns the range as an array" do - expect(range.__find_args__).to eq([ 1, 2, 3 ]) - end - end - describe ".demongoize" do subject { Range.demongoize(hash) }