Skip to content

Commit

Permalink
Clean up scope composition.
Browse files Browse the repository at this point in the history
  • Loading branch information
sleepingkingstudios committed Jan 17, 2024
1 parent cbddfa7 commit b609691
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 40 deletions.
148 changes: 148 additions & 0 deletions lib/cuprum/collections/rspec/contracts/scopes/composition_contracts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,72 @@ module ShouldComposeScopesContract

include_examples 'should combine the scopes with logical NAND'
end

describe 'with a conjunction scope' do
let(:expected_first) do
operators = Cuprum::Collections::Queries::Operators

[
[
'title',
operators::EQUAL,
'A Wizard of Earthsea'
]
]
end
let(:expected_second) do
operators = Cuprum::Collections::Queries::Operators

[
[
'category',
operators::EQUAL,
'Science Fiction and Fantasy'
]
]
end
let(:original) do
wrapped_first =
Cuprum::Collections::Scopes::CriteriaScope
.new(criteria: expected_first)
wrapped_second =
Cuprum::Collections::Scopes::CriteriaScope
.new(criteria: expected_second)

Cuprum::Collections::Scopes::ConjunctionScope
.new(scopes: [wrapped_first, wrapped_second])
end
let(:outer) { subject.not(original) }
let(:invert) { outer.scopes.last }
let(:inner_one) { invert.scopes.first }
let(:inner_two) { invert.scopes.last }

it { expect(outer).to be_a Cuprum::Collections::Scopes::Base }

it { expect(outer.type).to be :conjunction }

it { expect(outer.scopes.size).to be 2 }

it { expect(outer.scopes.first).to be subject }

it { expect(invert).to be_a Cuprum::Collections::Scopes::Base }

it { expect(invert.type).to be :negation }

it { expect(invert.scopes.size).to be 2 }

it { expect(inner_one).to be_a Cuprum::Collections::Scopes::Base }

it { expect(inner_one.type).to be :criteria }

it { expect(inner_one.criteria).to be == expected_first }

it { expect(inner_two).to be_a Cuprum::Collections::Scopes::Base }

it { expect(inner_two.type).to be :criteria }

it { expect(inner_two.criteria).to be == expected_second }
end
end

describe '#or' do
Expand Down Expand Up @@ -427,6 +493,61 @@ module ShouldComposeScopesForConjunctionContract
end
end

describe 'with a conjunction scope' do
let(:original) do
wrapped =
Cuprum::Collections::Scopes::CriteriaScope
.new(criteria: expected)

Cuprum::Collections::Scopes::ConjunctionScope
.new(scopes: [wrapped])
end
let(:outer) { subject.not(original) }
let(:invert) { outer.scopes.last }
let(:inner) { invert.scopes.first }
let(:inner) { invert.scopes.last }

it { expect(outer).to be_a Cuprum::Collections::Scopes::Base }

it { expect(outer.type).to be :conjunction }

it { expect(outer.scopes.size).to be scopes.size + 1 }

it { expect(invert).to be_a Cuprum::Collections::Scopes::Base }

it { expect(invert.type).to be :negation }

it { expect(invert.scopes.size).to be 1 }

it { expect(inner).to be_a Cuprum::Collections::Scopes::Base }

it { expect(inner.type).to be :criteria }

it { expect(inner.criteria).to be == expected }

wrap_context 'when the scope has many child scopes' do
it { expect(outer).to be_a Cuprum::Collections::Scopes::Base }

it { expect(outer.type).to be :conjunction }

it { expect(outer.scopes.size).to be scopes.size + 1 }

it { expect(outer.scopes[0...scopes.size]).to be == scopes }

it { expect(invert).to be_a Cuprum::Collections::Scopes::Base }

it { expect(invert.type).to be :negation }

it { expect(invert.scopes.size).to be 1 }

it { expect(inner).to be_a Cuprum::Collections::Scopes::Base }

it { expect(inner.type).to be :criteria }

it { expect(inner.criteria).to be == expected }
end
end

describe 'with a scope' do
let(:original) do
Cuprum::Collections::Scopes::CriteriaScope.new(criteria: expected)
Expand Down Expand Up @@ -522,6 +643,33 @@ module ShouldComposeScopesForCriteriaContract
end
end

describe 'with a conjunction scope' do
let(:other_scope) do
wrapped =
Cuprum::Collections::Scopes::CriteriaScope
.new(criteria: expected)

Cuprum::Collections::Scopes::ConjunctionScope
.new(scopes: [wrapped])
end
let(:outer) { subject.and(other_scope) }
let(:inner) { outer.scopes.last }

it { expect(outer).to be_a(Cuprum::Collections::Scopes::Base) }

it { expect(outer.type).to be :conjunction }

it { expect(outer.scopes.size).to be 2 }

it { expect(outer.scopes.first).to be subject }

it { expect(inner).to be_a(Cuprum::Collections::Scopes::Base) }

it { expect(inner.type).to be :criteria }

it { expect(inner.criteria).to be == expected }
end

describe 'with a criteria scope' do
let(:other_scope) do
Cuprum::Collections::Scopes::CriteriaScope
Expand Down
51 changes: 47 additions & 4 deletions lib/cuprum/collections/scopes/composition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ module Composition
#
# @override and(scope)
# Combines with the current scope using a logical AND.
def and(...)
scope = builder.build(...)
def and(*args, &block)
return and_conjunction_scope(args.first) if conjunction_scope?(args.first)

scope = builder.build(*args, &block)

# We control the current and generated scopes, so we can skip validation
# and transformation.
Expand All @@ -33,8 +35,10 @@ def and(...)
#
# @override not(scope)
# Inverts and combines with the current scope using a logical AND.
def not(...)
scope = builder.build(...)
def not(*args, &block)
return not_conjunction_scope(args.first) if conjunction_scope?(args.first)

scope = builder.build(*args, &block)
inverted = builder.build_negation_scope(scopes: [scope], safe: false)

# We control the current and generated scopes, so we can skip validation
Expand All @@ -56,5 +60,44 @@ def or(...)
# and transformation.
builder.build_disjunction_scope(scopes: [self, scope], safe: false)
end

private

def and_conjunction_scope(scope)
scopes = scope.scopes.map do |inner|
builder.transform_scope(scope: inner)
end

builder.build_conjunction_scope(scopes: [self, *scopes], safe: false)
end

def conjunction_scope?(value)
scope?(value) && value.type == :conjunction
end

def criteria_scope?(value)
scope?(value) && value.type == :criteria
end

def disjunction_scope?(value)
scope?(value) && value.type == :disjunction
end

def negation_scope?(value)
scope?(value) && value.type == :negation
end

def not_conjunction_scope(scope)
scopes = scope.scopes.map do |inner|
builder.transform_scope(scope: inner)
end
inverted = builder.build_negation_scope(scopes: scopes, safe: false)

builder.build_conjunction_scope(scopes: [self, inverted], safe: false)
end

def scope?(value)
value.is_a?(Cuprum::Collections::Scopes::Base)
end
end
end
39 changes: 26 additions & 13 deletions lib/cuprum/collections/scopes/composition/conjunction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,39 @@ module Conjunction

# (see Cuprum::Collections::Scopes::Composition#and)
def and(*args, &block)
scopes =
if args.first.is_a?(Cuprum::Collections::Scopes::Base) &&
args.first.type == :conjunction
args.first.scopes.map do |scope|
builder.transform_scope(scope: scope)
end
else
[builder.build(*args, &block)]
end
return and_conjunction_scope(args.first) if conjunction_scope?(args.first)

with_scopes([*self.scopes, *scopes])
with_scopes([*scopes, builder.build(*args, &block)])
end
alias where and

# (see Cuprum::Collections::Scopes::Composition#not)
def not(...)
scope = builder.build(...)
inverted = builder.build_negation_scope(scopes: [scope])
def not(*args, &block)
return not_conjunction_scope(args.first) if conjunction_scope?(args.first)

scope = builder.build(*args, &block)
inverted = builder.build_negation_scope(scopes: [scope], safe: false)

with_scopes([*scopes, inverted])
end

private

def and_conjunction_scope(scope)
scopes = scope.scopes.map do |inner|
builder.transform_scope(scope: inner)
end

with_scopes([*self.scopes, *scopes])
end

def not_conjunction_scope(scope)
scopes = scope.scopes.map do |inner|
builder.transform_scope(scope: inner)
end
inverted = builder.build_negation_scope(scopes: scopes, safe: false)

with_scopes([*self.scopes, inverted])
end
end
end
17 changes: 9 additions & 8 deletions lib/cuprum/collections/scopes/composition/criteria.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ module Criteria

# (see Cuprum::Collections::Scopes::Composition#and)
def and(*args, &block)
criteria =
if args.first.is_a?(Cuprum::Collections::Scopes::Base)
return super if args.first.type != :criteria
return and_criteria_scope(args.first) if criteria_scope?(args.first)

args.first.criteria
else
self.class.parse(*args, &block)
end
return super if scope?(args.first)

with_criteria([*self.criteria, *criteria])
with_criteria([*criteria, *self.class.parse(*args, &block)])
end
alias where and

private

def and_criteria_scope(scope)
with_criteria([*criteria, *scope.criteria])
end
end
end
20 changes: 11 additions & 9 deletions lib/cuprum/collections/scopes/composition/disjunction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ module Cuprum::Collections::Scopes::Composition
module Disjunction
# (see Cuprum::Collections::Scopes::Composition#or)
def or(*args, &block)
scopes =
if args.first.is_a?(Cuprum::Collections::Scopes::Base) &&
args.first.type == :disjunction
args.first.scopes.map do |scope|
builder.transform_scope(scope: scope)
end
else
[builder.build(*args, &block)]
end
return or_disjuncton_scope(args.first) if disjunction_scope?(args.first)

with_scopes([*scopes, builder.build(*args, &block)])
end

private

def or_disjuncton_scope(scope)
scopes = scope.scopes.map do |inner|
builder.transform_scope(scope: inner)
end

with_scopes([*self.scopes, *scopes])
end
Expand Down
8 changes: 2 additions & 6 deletions lib/cuprum/collections/scopes/composition/negation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ module Negation

# (see Cuprum::Collections::Scopes::Composition#and)
def and(*args, &block)
return super unless args.first.is_a?(Cuprum::Collections::Scopes::Base)

return super unless args.first.type == :negation
return super unless negation_scope?(args.first)

scopes = args.first.scopes.map do |scope|
builder.transform_scope(scope: scope)
Expand All @@ -23,9 +21,7 @@ def and(*args, &block)

# (see Cuprum::Collections::Scopes::Composition#not)
def not(*args, &block)
return super unless args.first.is_a?(Cuprum::Collections::Scopes::Base)

return super unless args.first.type == :negation
return super unless negation_scope?(args.first)

scopes = args.first.scopes.map do |scope|
builder.transform_scope(scope: scope)
Expand Down

0 comments on commit b609691

Please sign in to comment.