Skip to content

Commit

Permalink
Implement Scopes::Composition::Disjunction.
Browse files Browse the repository at this point in the history
  • Loading branch information
sleepingkingstudios committed Jan 16, 2024
1 parent f5d37bb commit 2a5059f
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 1 deletion.
2 changes: 2 additions & 0 deletions lib/cuprum/collections/basic/scopes/disjunction_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

require 'cuprum/collections/basic/scopes'
require 'cuprum/collections/basic/scopes/base'
require 'cuprum/collections/scopes/composition/disjunction'
require 'cuprum/collections/scopes/container'

module Cuprum::Collections::Basic::Scopes
# Scope for filtering data matching any of the given scopes.
class DisjunctionScope < Cuprum::Collections::Basic::Scopes::Base
include Cuprum::Collections::Scopes::Container
include Cuprum::Collections::Scopes::Composition::Disjunction

# Returns true if the provided item matches any of the configured scopes.
def match?(item:)
Expand Down
145 changes: 145 additions & 0 deletions lib/cuprum/collections/rspec/contracts/scopes/composition_contracts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require 'cuprum/collections/scopes/builder'
require 'cuprum/collections/scopes/conjunction_scope'
require 'cuprum/collections/scopes/criteria_scope'
require 'cuprum/collections/scopes/disjunction_scope'
require 'cuprum/collections/scopes/negation_scope'

module Cuprum::Collections::RSpec::Contracts::Scopes
Expand Down Expand Up @@ -334,6 +335,16 @@ module ShouldComposeScopesForConjunctionContract
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.scopes.size).to be scopes.size + 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 non-conjunction scope' do
Expand Down Expand Up @@ -560,5 +571,139 @@ module ShouldComposeScopesForCriteriaContract
end
end
end

# Contract validating scope composition for disjunction scopes.
module ShouldComposeScopesForDisjunctionContract
extend RSpec::SleepingKingStudios::Contract
include Cuprum::Collections::RSpec::Contracts::Scopes::CompositionContracts # rubocop:disable Layout/LineLength

# @!method apply(example_group)
# Adds the contract to the example group.
#
# @param example_group [RSpec::Core::ExampleGroup] the example group to
# which the contract is applied.
contract do
shared_context 'when the scope has many child scopes' do
let(:scopes) do
[
build_scope({ 'author' => 'J.R.R. Tolkien' }),
build_scope({ 'series' => 'The Lord of the Rings' }),
build_scope do
{ 'published_at' => less_than('1955-01-01') }
end
]
end
end

include_contract 'should compose scopes', except: %i[or]

describe '#or' do
shared_examples 'should combine the scopes with logical OR' do
it { expect(outer).to be_a Cuprum::Collections::Scopes::Base }

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

it { expect(outer.scopes.size).to be scopes.size + 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

let(:expected) do
operators = Cuprum::Collections::Queries::Operators

[
[
'title',
operators::EQUAL,
'A Wizard of Earthsea'
]
]
end

describe 'with a block' do
let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } }
let(:outer) { subject.or(&block) }
let(:inner) { outer.scopes.last }

include_examples 'should combine the scopes with logical OR'

wrap_context 'when the scope has many child scopes' do
include_examples 'should combine the scopes with logical OR'

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

describe 'with a hash' do
let(:value) { { 'title' => 'A Wizard of Earthsea' } }
let(:outer) { subject.or(value) }
let(:inner) { outer.scopes.last }

include_examples 'should combine the scopes with logical OR'

wrap_context 'when the scope has many child scopes' do
include_examples 'should combine the scopes with logical OR'

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

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

Cuprum::Collections::Scopes::DisjunctionScope
.new(scopes: [wrapped])
end
let(:outer) { subject.or(original) }
let(:inner) { outer.scopes.last }

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

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

it { expect(outer.scopes.size).to be scopes.size + 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.scopes.size).to be scopes.size + 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 non-disjunction scope' do
let(:original) do
Cuprum::Collections::Scopes::CriteriaScope.new(criteria: expected)
end
let(:outer) { subject.or(original) }
let(:inner) { outer.scopes.last }

include_examples 'should combine the scopes with logical OR'

wrap_context 'when the scope has many child scopes' do
include_examples 'should combine the scopes with logical OR'

it { expect(outer.scopes[0...scopes.size]).to be == scopes }
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ module ShouldBeADisjunctionScopeContract
contract do |abstract: false|
include_contract 'should be a container scope'

include_contract 'should compose scopes for disjunction'

describe '#call' do
next if abstract

Expand Down
1 change: 1 addition & 0 deletions lib/cuprum/collections/scopes/composition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module Cuprum::Collections::Scopes
module Composition
autoload :Conjunction, 'cuprum/collections/scopes/composition/conjunction'
autoload :Criteria, 'cuprum/collections/scopes/composition/criteria'
autoload :Disjunction, 'cuprum/collections/scopes/composition/disjunction'

# @override and(hash = nil, &block)
# Parses the hash or block and combines using a logical AND.
Expand Down
23 changes: 23 additions & 0 deletions lib/cuprum/collections/scopes/composition/disjunction.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

require 'cuprum/collections/scopes/composition'

module Cuprum::Collections::Scopes::Composition
# Defines composition behavior for disjunction scopes.
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

with_scopes([*self.scopes, *scopes])
end
end
end
2 changes: 2 additions & 0 deletions lib/cuprum/collections/scopes/disjunction_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

require 'cuprum/collections/scopes'
require 'cuprum/collections/scopes/base'
require 'cuprum/collections/scopes/composition/disjunction'
require 'cuprum/collections/scopes/container'

module Cuprum::Collections::Scopes
# Generic scope class for defining collection-independent logical OR scopes.
class DisjunctionScope < Cuprum::Collections::Scopes::Base
include Cuprum::Collections::Scopes::Container
include Cuprum::Collections::Scopes::Composition::Disjunction

# (see Cuprum::Collections::Scopes::Base#type)
def type
Expand Down
36 changes: 36 additions & 0 deletions spec/cuprum/collections/scopes/composition/disjunction_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require 'cuprum/collections/rspec/contracts/scopes/composition_contracts'
require 'cuprum/collections/scopes/base'
require 'cuprum/collections/scopes/composition/disjunction'
require 'cuprum/collections/scopes/container'
require 'cuprum/collections/scopes/criteria_scope'

RSpec.describe Cuprum::Collections::Scopes::Composition::Disjunction do
include Cuprum::Collections::RSpec::Contracts::Scopes::CompositionContracts

subject(:scope) { described_class.new(scopes: scopes) }

let(:described_class) { Spec::ExampleScope }
let(:scopes) { [] }

example_class 'Spec::ExampleScope', Cuprum::Collections::Scopes::Base \
do |klass|
klass.include Cuprum::Collections::Scopes::Container
klass.include Cuprum::Collections::Scopes::Composition::Disjunction # rubocop:disable RSpec/DescribedClass

klass.define_method(:type) { :disjunction }
end

def build_scope(filters = nil, &block)
scope_class = Cuprum::Collections::Basic::Scopes::CriteriaScope

if block_given?
scope_class.build(&block)
else
scope_class.build(filters)
end
end

include_contract 'should compose scopes for disjunction'
end
13 changes: 12 additions & 1 deletion spec/cuprum/collections/scopes/disjunction_scope_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# frozen_string_literal: true

require 'cuprum/collections/scopes/disjunction_scope'
require 'cuprum/collections/rspec/contracts/scopes/logical_contracts'
require 'cuprum/collections/scopes/criteria_scope'
require 'cuprum/collections/scopes/disjunction_scope'

RSpec.describe Cuprum::Collections::Scopes::DisjunctionScope do
include Cuprum::Collections::RSpec::Contracts::Scopes::LogicalContracts
Expand All @@ -10,5 +11,15 @@

let(:scopes) { [] }

def build_scope(filters = nil, &block)
scope_class = Cuprum::Collections::Basic::Scopes::CriteriaScope

if block_given?
scope_class.build(&block)
else
scope_class.build(filters)
end
end

include_contract 'should be a disjunction scope', abstract: true
end

0 comments on commit 2a5059f

Please sign in to comment.