Skip to content

Commit

Permalink
Merge pull request mongodb#4300 from jonhyman/feature/elemMatch-matches
Browse files Browse the repository at this point in the history
Add support for $elemMatch in Matchable#matches?
  • Loading branch information
estolfo authored Jul 22, 2016
2 parents 3804ba2 + 2cf3104 commit 745a124
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 1 deletion.
8 changes: 7 additions & 1 deletion lib/mongoid/matchable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
require "mongoid/matchable/nin"
require "mongoid/matchable/or"
require "mongoid/matchable/size"
require "mongoid/matchable/elem_match"

module Mongoid

Expand All @@ -27,6 +28,7 @@ module Matchable
# @since 1.0.0
MATCHERS = {
"$all" => All,
"$elemMatch" => ElemMatch,
"$and" => And,
"$exists" => Exists,
"$gt" => Gt,
Expand Down Expand Up @@ -149,7 +151,11 @@ def extract_attribute(document, key)
end
end
else
document.attributes[key_string]
if document.is_a?(Hash)
document[key_string]
else
document.attributes[key_string]
end
end
end
end
Expand Down
28 changes: 28 additions & 0 deletions lib/mongoid/matchable/elem_match.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# encoding: utf-8
module Mongoid
module Matchable

class ElemMatch < Default

# Return true if a given predicate matches a sub document entirely
#
# @example Do the values match?
# matcher.matches?({"$elemMatch" => {"a" => 1, "b" => 2}})
#
# @param [ Hash ] value The values to check.
#
# @return [ true, false ] If the values match.
def matches?(value)
if !@attribute.is_a?(Array) || !value.kind_of?(Hash) || !value["$elemMatch"].kind_of?(Hash)
return false
end

return @attribute.any? do |sub_document|
value["$elemMatch"].all? do |k, v|
Matchable.matcher(sub_document, k, v).matches?(v)
end
end
end
end
end
end
86 changes: 86 additions & 0 deletions spec/mongoid/matchable/elem_match_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
require "spec_helper"

describe Mongoid::Matchable::ElemMatch do

let(:attribute) {[{"a" => 1, "b" => 2}, {"a" => 2, "b" => 2}, {"a" => 3, "b" => 1}]}
let(:matcher) do
described_class.new(attribute)
end

describe "#matches?" do

context "when the attribute is not an array" do

let(:attribute) {"string"}

it "returns false" do
expect(matcher.matches?("$elemMatch" => {"a" => 1})).to be false
end
end

context "when the $elemMatch is not a hash" do

let(:attribute) {"string"}

it "returns false" do
expect(matcher.matches?("$elemMatch" => [])).to be false
end
end

context "when there is a sub document that matches the criteria" do

it "returns true" do
expect(matcher.matches?("$elemMatch" => {"a" => 1})).to be true
end

context "when evaluating multiple fields of the subdocument" do

it "returns true" do
expect(matcher.matches?("$elemMatch" => {"a" => 1, "b" => 2})).to be true
end

context "when the $elemMatch document keys are out of order" do

it "returns true" do
expect(matcher.matches?("$elemMatch" => {"b" => 2, "a" => 1})).to be true
end
end
end

context "when using other operators that match" do

it "returns true" do
expect(matcher.matches?("$elemMatch" => {"a" => {"$in" => [1]}, "b" => {"$gt" => 1}})).to be true
end
end
end

context "when there is not a sub document that matches the criteria" do

it "returns false" do
expect(matcher.matches?("$elemMatch" => {"a" => 10})).to be false
end

context "when evaluating multiple fields of the subdocument" do

it "returns false" do
expect(matcher.matches?("$elemMatch" => {"a" => 1, "b" => 3})).to be false
end
end

context "when using other operators that do not match" do

it "returns true" do
expect(matcher.matches?("$elemMatch" => {"a" => {"$in" => [1]}, "b" => {"$gt" => 10}})).to be false
end
end
end

context "when using a criteria that matches partially but not a single sub document" do

it "returns false" do
expect(matcher.matches?("$elemMatch" => {"a" => 3, "b" => 2})).to be false
end
end
end
end
120 changes: 120 additions & 0 deletions spec/mongoid/matchable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,126 @@
end
end

context "when using $elemMatch" do

context "one predicate" do

let(:selector) do
{
"occupants" => {"$elemMatch" => {"name" => "Tim"}}
}
end

context "and there is a matching sub document" do

it "returns true" do
expect(document.locations.first.matches?(selector)).to be true
end

context "using $in" do
let(:selector) do
{
"occupants" => {"$elemMatch" => {"name" => {"$in" => ["Tim", "Alice"]}}}
}
end

it "returns true" do
expect(document.locations.first.matches?(selector)).to be true
end
end
end

context "and there is not a matching sub document" do

let(:selector) do
{
"occupants" => {"$elemMatch" => {"name" => "Lyle"}}
}
end

it "returns false" do
expect(document.locations.first.matches?(selector)).to be false
end

context "using $in" do
let(:selector) do
{
"occupants" => {"$elemMatch" => {"name" => {"$in" => ["Lyfe", "Alice"]}}}
}
end

it "returns false" do
expect(document.locations.first.matches?(selector)).to be false
end
end
end
end

context "multiple predicates" do

before do
document.locations = [
Location.new(
name: 'No.1',
info: { 'door' => 'Red'},
occupants: [{'name' => 'Tim', 'eye_color' => 'brown'}, {'name' => 'Alice', 'eye_color' => 'blue'}]
)
]
end

let(:selector) do
{
"occupants" => {"$elemMatch" => {"name" => "Tim", "eye_color" => "brown"}}
}
end

context "and there is a matching sub document" do

it "returns true" do
expect(document.locations.first.matches?(selector)).to be true
end

context "using $in and $ne in the $elemMatch to include the element" do

let(:selector) do
{
"occupants" => {"$elemMatch" => {"name" => {"$in" => ["Tim"]}, "eye_color" => {"$ne" => "blue"}}}
}
end

it "returns true" do
expect(document.locations.first.matches?(selector)).to be true
end
end
end

context "and there is not a matching sub document" do

let(:selector) do
{
"occupants" => {"$elemMatch" => {"name" => "Tim", "eye_color" => "blue"}}
}
end

it "returns false" do
expect(document.locations.first.matches?(selector)).to be false
end

context "using $ne in the $elemMatch to exclude the element" do

let(:selector) do
{
"occupants" => {"$elemMatch" => {"name" => {"$in" => ["Tim"]}, "eye_color" => {"$ne" => "brown"}}}
}
end

it "returns false" do
expect(document.locations.first.matches?(selector)).to be false
end
end
end
end
end
end

context "when performing simple matching" do
Expand Down

0 comments on commit 745a124

Please sign in to comment.