Skip to content

Commit 930e4fd

Browse files
authored
Add support for $nor to the Mongoid matcher. (#4552)
1 parent 48673d7 commit 930e4fd

File tree

5 files changed

+282
-1
lines changed

5 files changed

+282
-1
lines changed

lib/mongoid/matchable.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
require "mongoid/matchable/ne"
1212
require "mongoid/matchable/nin"
1313
require "mongoid/matchable/or"
14+
require "mongoid/matchable/nor"
1415
require "mongoid/matchable/size"
1516
require "mongoid/matchable/elem_match"
1617
require "mongoid/matchable/regexp"
@@ -40,6 +41,7 @@ module Matchable
4041
"$ne" => Ne,
4142
"$nin" => Nin,
4243
"$or" => Or,
44+
"$nor" => Nor,
4345
"$size" => Size
4446
}.with_indifferent_access.freeze
4547

@@ -124,6 +126,7 @@ def matcher(document, key, value)
124126
case key.to_s
125127
when "$or" then Or.new(value, document)
126128
when "$and" then And.new(value, document)
129+
when "$nor" then Nor.new(value, document)
127130
else Default.new(extract_attribute(document, key))
128131
end
129132
end

lib/mongoid/matchable/nor.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
# encoding: utf-8
3+
module Mongoid
4+
module Matchable
5+
6+
# Defines behavior for handling $nor expressions in embedded documents.
7+
class Nor < Default
8+
9+
# Does the supplied query match the attribute?
10+
#
11+
# Note: an empty array as an argument to $nor is prohibited by
12+
# MongoDB server. Mongoid does allow this and returns false in this case.
13+
#
14+
# @example Does this match?
15+
# matcher._matches?("$nor" => [ { field => value } ])
16+
#
17+
# @param [ Array ] conditions The or expression.
18+
#
19+
# @return [ true, false ] True if matches, false if not.
20+
#
21+
# @since 6.4.2/7.0.2/7.1.0
22+
def _matches?(conditions)
23+
if conditions.length == 0
24+
# MongoDB does not allow $nor array to be empty, but
25+
# Mongoid accepts an empty array for $or which MongoDB also
26+
# prohibits
27+
return false
28+
end
29+
conditions.none? do |condition|
30+
condition.all? do |key, value|
31+
document._matches?(key => value)
32+
end
33+
end
34+
end
35+
end
36+
end
37+
end

spec/app/models/array_field.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
class ArrayField
4+
include Mongoid::Document
5+
6+
field :af, type: Array
7+
end

spec/mongoid/matchable/nor_spec.rb

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
describe Mongoid::Matchable::Nor do
6+
7+
let(:target) do
8+
Person.new
9+
end
10+
11+
let(:matcher) do
12+
described_class.new("value", target)
13+
end
14+
15+
describe "#_matches?" do
16+
17+
context "when provided a simple expression" do
18+
19+
context "when one of the hashes does not match model" do
20+
21+
let(:matches) do
22+
matcher._matches?(
23+
[ { title: "Sir" }, { title: "King" } ]
24+
)
25+
end
26+
27+
let(:target) do
28+
Person.new(title: 'Queen')
29+
end
30+
31+
it "returns true" do
32+
expect(matches).to be true
33+
end
34+
end
35+
36+
context "when all of the hashes match different fields in model" do
37+
let(:matches) do
38+
matcher._matches?(
39+
[ { age: 10 }, { title: "King" } ]
40+
)
41+
end
42+
43+
let(:target) do
44+
Person.new(title: 'King', age: 10)
45+
end
46+
47+
it "returns false" do
48+
expect(matches).to be false
49+
end
50+
end
51+
52+
context "when one of the hashes matches an array field in model" do
53+
let(:matches) do
54+
matcher._matches?(
55+
[ { af: "Sir" }, { af: "King" } ]
56+
)
57+
end
58+
59+
let(:target) do
60+
ArrayField.new(af: ['King'])
61+
end
62+
63+
it "returns false" do
64+
expect(matches).to be false
65+
end
66+
end
67+
68+
context "when none of the hashes matches an array field in model" do
69+
let(:matches) do
70+
matcher._matches?(
71+
[ { af: "Sir" }, { af: "King" } ]
72+
)
73+
end
74+
75+
let(:target) do
76+
ArrayField.new(af: ['Boo'])
77+
end
78+
79+
it "returns true" do
80+
expect(matches).to be true
81+
end
82+
end
83+
84+
context "when there are no criteria" do
85+
86+
it "returns false" do
87+
expect(matcher._matches?([])).to be false
88+
end
89+
end
90+
91+
# $nor with $not is a double negation.
92+
# Whatever the argument of $not is is what the overall condition
93+
# is looking for.
94+
context "when the expression is a $not" do
95+
96+
let(:matches) do
97+
matcher._matches?([ { title: {:$not => /Foobar/ } }])
98+
end
99+
100+
context "when the value does not match $not argument" do
101+
102+
let(:target) do
103+
Person.new(title: 'test')
104+
end
105+
106+
it "returns false" do
107+
expect(matches).to be false
108+
end
109+
end
110+
111+
context "when the value matches $not argument" do
112+
113+
let(:target) do
114+
Person.new(title: 'Foobar baz')
115+
end
116+
117+
it "returns true" do
118+
expect(matches).to be true
119+
end
120+
end
121+
end
122+
end
123+
124+
context "when provided a complex expression" do
125+
126+
context "when none of the model values match criteria values" do
127+
128+
let(:matches) do
129+
matcher._matches?(
130+
[
131+
{ title: { "$in" => [ "Sir", "Madam" ] } },
132+
{ title: "King" }
133+
]
134+
)
135+
end
136+
137+
let(:target) do
138+
Person.new(title: 'Queen')
139+
end
140+
141+
it "returns true" do
142+
expect(matches).to be true
143+
end
144+
end
145+
146+
context "when there is a matching value" do
147+
148+
let(:matches) do
149+
matcher._matches?(
150+
[
151+
{ title: { "$in" => [ "Prince", "Madam" ] } },
152+
{ title: "King" }
153+
]
154+
)
155+
end
156+
157+
let(:target) do
158+
Person.new(title: 'Prince')
159+
end
160+
161+
it "returns false" do
162+
expect(matches).to be false
163+
end
164+
end
165+
166+
context "when expression contain multiple fields" do
167+
168+
let(:matches) do
169+
matcher._matches?(
170+
[
171+
{ title: "Sir", age: 23 },
172+
{ title: "King", age: 100 }
173+
]
174+
)
175+
end
176+
177+
context 'and model has different values in all of the fields' do
178+
let(:target) do
179+
Person.new(title: 'Queen', age: 10)
180+
end
181+
182+
it "returns true" do
183+
expect(matches).to be true
184+
end
185+
end
186+
187+
context 'and model has identical value in one of the fields' do
188+
let(:target) do
189+
Person.new(title: 'Queen', age: 23)
190+
end
191+
192+
it "returns true" do
193+
expect(matches).to be true
194+
end
195+
end
196+
197+
context 'and model has identical values in all of the fields' do
198+
let(:target) do
199+
Person.new(title: 'Sir', age: 23)
200+
end
201+
202+
it "returns false" do
203+
expect(matches).to be false
204+
end
205+
end
206+
end
207+
end
208+
end
209+
end

spec/mongoid/matchable_spec.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
let(:selector) do
8787
{ "occupants.0" => "Tim" }
8888
end
89-
89+
9090
it "returns true" do
9191
expect(document.locations.first._matches?(selector)).to be true
9292
end
@@ -774,6 +774,31 @@
774774
end
775775
end
776776

777+
context "with an $nor selector" do
778+
779+
context "when the attributes match" do
780+
781+
let(:selector) do
782+
{ "$nor" => [ { number: 10 }, { number: { "$gt" => 199 } } ] }
783+
end
784+
785+
it "returns true" do
786+
expect(document._matches?(selector)).to be true
787+
end
788+
end
789+
790+
context "when the attributes do not match" do
791+
792+
let(:selector) do
793+
{ "$nor" => [ { number: 10 }, { number: { "$gt" => 99 } } ] }
794+
end
795+
796+
it "returns false" do
797+
expect(document._matches?(selector)).to be false
798+
end
799+
end
800+
end
801+
777802
context "with a $size selector" do
778803

779804
context "when the attributes match" do

0 commit comments

Comments
 (0)