diff --git a/lib/cuprum/collections/rspec/contracts/scope_contracts.rb b/lib/cuprum/collections/rspec/contracts/scope_contracts.rb index 7ba44c2..8678574 100644 --- a/lib/cuprum/collections/rspec/contracts/scope_contracts.rb +++ b/lib/cuprum/collections/rspec/contracts/scope_contracts.rb @@ -273,26 +273,6 @@ module ShouldBeANullScopeContract end describe '#and' do - shared_examples 'should return the scope' do - it { expect(outer).to be_a Cuprum::Collections::Scopes::Base } - - it { expect(outer.type).to be :criteria } - - it { expect(outer.criteria).to be == expected } - end - - let(:expected) do - operators = Cuprum::Collections::Queries::Operators - - [ - [ - 'title', - operators::EQUAL, - 'A Wizard of Earthsea' - ] - ] - end - it 'should define the method' do expect(subject) .to respond_to(:and) @@ -304,26 +284,108 @@ module ShouldBeANullScopeContract describe 'with a block' do let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } - let(:outer) { subject.and(&block) } + let(:expected) do + Cuprum::Collections::Scope.new(&block) + end - include_examples 'should return the scope' + it { expect(subject.and(&block)).to be == expected } end describe 'with a hash' do let(:value) { { 'title' => 'A Wizard of Earthsea' } } - let(:outer) { subject.and(value) } + let(:expected) do + Cuprum::Collections::Scope.new(value) + end + + it { expect(subject.and(value)).to be == expected } + end + + describe 'with an empty conjunction scope' do + let(:original) do + Cuprum::Collections::Scopes::ConjunctionScope.new(scopes: []) + end + + it { expect(subject.and(original)).to be subject } + end - include_examples 'should return the scope' + describe 'with an empty criteria scope' do + let(:original) do + Cuprum::Collections::Scopes::CriteriaScope.new(criteria: []) + end + + it { expect(subject.and(original)).to be subject } end - describe 'with a scope' do - let(:value) do + describe 'with an empty disjunction scope' do + let(:original) do + Cuprum::Collections::Scopes::DisjunctionScope.new(scopes: []) + end + + it { expect(subject.and(original)).to be subject } + end + + describe 'with an empty negation scope' do + let(:original) do + Cuprum::Collections::Scopes::NegationScope.new(scopes: []) + end + + it { expect(subject.and(original)).to be subject } + end + + describe 'with a non-empty conjunction scope' do + let(:original) do + wrapped = + Cuprum::Collections::Scope + .new({ 'title' => 'A Wizard of Earthsea' }) + + Cuprum::Collections::Scopes::ConjunctionScope + .new(scopes: [wrapped]) + end + + it { expect(subject.and(original)).to be == original } + end + + describe 'with a non-empty criteria scope' do + let(:original) do Cuprum::Collections::Scope .new({ 'title' => 'A Wizard of Earthsea' }) end - let(:outer) { subject.and(value) } - include_examples 'should return the scope' + it { expect(subject.and(original)).to be == original } + end + + describe 'with a non-empty disjunction scope' do + let(:original) do + wrapped = + Cuprum::Collections::Scope + .new({ 'title' => 'A Wizard of Earthsea' }) + + Cuprum::Collections::Scopes::DisjunctionScope + .new(scopes: [wrapped]) + end + + it { expect(subject.and(original)).to be == original } + end + + describe 'with a non-empty negation scope' do + let(:original) do + wrapped = + Cuprum::Collections::Scope + .new({ 'title' => 'A Wizard of Earthsea' }) + + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [wrapped]) + end + + it { expect(subject.and(original)).to be == original } + end + + describe 'with a null scope' do + let(:original) do + Cuprum::Collections::Scopes::NullScope.new + end + + it { expect(subject.and(original)).to be subject } end end @@ -355,26 +417,6 @@ module ShouldBeANullScopeContract end describe '#or' do - shared_examples 'should return the scope' do - it { expect(outer).to be_a Cuprum::Collections::Scopes::Base } - - it { expect(outer.type).to be :criteria } - - it { expect(outer.criteria).to be == expected } - end - - let(:expected) do - operators = Cuprum::Collections::Queries::Operators - - [ - [ - 'title', - operators::EQUAL, - 'A Wizard of Earthsea' - ] - ] - end - it 'should define the method' do expect(subject) .to respond_to(:or) @@ -384,56 +426,112 @@ module ShouldBeANullScopeContract describe 'with a block' do let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } - let(:outer) { subject.or(&block) } + let(:expected) do + Cuprum::Collections::Scope.new(&block) + end - include_examples 'should return the scope' + it { expect(subject.or(&block)).to be == expected } end describe 'with a hash' do let(:value) { { 'title' => 'A Wizard of Earthsea' } } - let(:outer) { subject.or(value) } + let(:expected) do + Cuprum::Collections::Scope.new(value) + end + + it { expect(subject.or(value)).to be == expected } + end + + describe 'with an empty conjunction scope' do + let(:original) do + Cuprum::Collections::Scopes::ConjunctionScope.new(scopes: []) + end + + it { expect(subject.or(original)).to be subject } + end + + describe 'with an empty criteria scope' do + let(:original) do + Cuprum::Collections::Scopes::CriteriaScope.new(criteria: []) + end + + it { expect(subject.or(original)).to be subject } + end + + describe 'with an empty disjunction scope' do + let(:original) do + Cuprum::Collections::Scopes::DisjunctionScope.new(scopes: []) + end + + it { expect(subject.or(original)).to be subject } + end + + describe 'with an empty negation scope' do + let(:original) do + Cuprum::Collections::Scopes::NegationScope.new(scopes: []) + end - include_examples 'should return the scope' + it { expect(subject.or(original)).to be subject } end - describe 'with a scope' do - let(:value) do + describe 'with a non-empty conjunction scope' do + let(:original) do + wrapped = + Cuprum::Collections::Scope + .new({ 'title' => 'A Wizard of Earthsea' }) + + Cuprum::Collections::Scopes::ConjunctionScope + .new(scopes: [wrapped]) + end + + it { expect(subject.or(original)).to be == original } + end + + describe 'with a non-empty criteria scope' do + let(:original) do Cuprum::Collections::Scope .new({ 'title' => 'A Wizard of Earthsea' }) end - let(:outer) { subject.or(value) } - include_examples 'should return the scope' + it { expect(subject.or(original)).to be == original } end - end - describe '#not' do - shared_examples 'should invert and return the scope' do - it { expect(outer).to be_a Cuprum::Collections::Scopes::Base } + describe 'with a non-empty disjunction scope' do + let(:original) do + wrapped = + Cuprum::Collections::Scope + .new({ 'title' => 'A Wizard of Earthsea' }) - it { expect(outer.type).to be :negation } + Cuprum::Collections::Scopes::DisjunctionScope + .new(scopes: [wrapped]) + end - it { expect(outer.scopes.size).to be 1 } + it { expect(subject.or(original)).to be == original } + end - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + describe 'with a non-empty negation scope' do + let(:original) do + wrapped = + Cuprum::Collections::Scope + .new({ 'title' => 'A Wizard of Earthsea' }) - it { expect(inner.type).to be :criteria } + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [wrapped]) + end - it { expect(inner.criteria).to be == expected } + it { expect(subject.or(original)).to be == original } end - let(:expected) do - operators = Cuprum::Collections::Queries::Operators + describe 'with a null scope' do + let(:original) do + Cuprum::Collections::Scopes::NullScope.new + end - [ - [ - 'title', - operators::EQUAL, - 'A Wizard of Earthsea' - ] - ] + it { expect(subject.or(original)).to be subject } end + end + describe '#not' do it 'should define the method' do expect(subject) .to respond_to(:not) @@ -443,29 +541,142 @@ module ShouldBeANullScopeContract describe 'with a block' do let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } - let(:outer) { subject.not(&block) } - let(:inner) { outer.scopes.first } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(&block) + + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [wrapped]) + end - include_examples 'should invert and return the scope' + it { expect(subject.not(&block)).to be == expected } end describe 'with a hash' do let(:value) { { 'title' => 'A Wizard of Earthsea' } } - let(:outer) { subject.not(value) } - let(:inner) { outer.scopes.first } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(value) + + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [wrapped]) + end + + it { expect(subject.not(value)).to be == expected } + end + + describe 'with an empty conjunction scope' do + let(:original) do + Cuprum::Collections::Scopes::ConjunctionScope.new(scopes: []) + end + + it { expect(subject.not(original)).to be subject } + end + + describe 'with an empty criteria scope' do + let(:original) do + Cuprum::Collections::Scopes::CriteriaScope.new(criteria: []) + end + + it { expect(subject.not(original)).to be subject } + end - include_examples 'should invert and return the scope' + describe 'with an empty disjunction scope' do + let(:original) do + Cuprum::Collections::Scopes::DisjunctionScope.new(scopes: []) + end + + it { expect(subject.not(original)).to be subject } end - describe 'with a scope' do - let(:value) do + describe 'with an empty negation scope' do + let(:original) do + Cuprum::Collections::Scopes::NegationScope.new(scopes: []) + end + + it { expect(subject.not(original)).to be subject } + end + + describe 'with a non-empty conjunction scope' do + let(:original) do + wrapped = + Cuprum::Collections::Scope + .new({ 'title' => 'A Wizard of Earthsea' }) + + Cuprum::Collections::Scopes::ConjunctionScope + .new(scopes: [wrapped]) + end + let(:expected) do + Cuprum::Collections::Scopes::NegationScope + .new(scopes: original.scopes) + end + + it { expect(subject.not(original)).to be == expected } + end + + describe 'with a non-empty criteria scope' do + let(:original) do Cuprum::Collections::Scope .new({ 'title' => 'A Wizard of Earthsea' }) end - let(:outer) { subject.not(value) } - let(:inner) { outer.scopes.first } + let(:expected) do + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [original]) + end + + it { expect(subject.not(original)).to be == expected } + end + + describe 'with a non-empty disjunction scope' do + let(:original) do + wrapped = + Cuprum::Collections::Scope + .new({ 'title' => 'A Wizard of Earthsea' }) + + Cuprum::Collections::Scopes::DisjunctionScope + .new(scopes: [wrapped]) + end + let(:expected) do + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [original]) + end + + it { expect(subject.not(original)).to be == expected } + end + + describe 'with a negation scope with one child scope' do + let(:original) do + wrapped = + Cuprum::Collections::Scope + .new({ 'title' => 'A Wizard of Earthsea' }) + + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [wrapped]) + end + it { expect(subject.not(original)).to be == original.scopes.first } + end + + describe 'with a negation scope with many child scopes' do + let(:original) do + wrapped = Array.new(3) do + Cuprum::Collections::Scope.new({ 'ok' => true }) + end + + Cuprum::Collections::Scopes::NegationScope + .new(scopes: wrapped) + end + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope + .new(scopes: original.scopes) + end + + it { expect(subject.not(original)).to be == expected } + end + + describe 'with a null scope' do + let(:original) do + Cuprum::Collections::Scopes::NullScope.new + end - include_examples 'should invert and return the scope' + it { expect(subject.not(original)).to be subject } end end diff --git a/lib/cuprum/collections/rspec/contracts/scopes/composition_contracts.rb b/lib/cuprum/collections/rspec/contracts/scopes/composition_contracts.rb index 847bd17..066f423 100644 --- a/lib/cuprum/collections/rspec/contracts/scopes/composition_contracts.rb +++ b/lib/cuprum/collections/rspec/contracts/scopes/composition_contracts.rb @@ -24,35 +24,103 @@ module ShouldComposeScopesContract # @param except [Array] names of composition methods where the # scope defines custom behavior. contract do |except: []| - describe '#and' do - shared_examples 'should combine the scopes with logical AND' do - it { expect(outer).to be_a Cuprum::Collections::Scopes::Base } + shared_context 'with an empty conjunction scope' do + let(:original) do + Cuprum::Collections::Scopes::ConjunctionScope.new(scopes: []) + end + end - it { expect(outer.type).to be :conjunction } + shared_context 'with an empty criteria scope' do + let(:original) do + Cuprum::Collections::Scopes::CriteriaScope.new(criteria: []) + end + end - it { expect(outer.scopes.size).to be 2 } + shared_context 'with an empty disjunction scope' do + let(:original) do + Cuprum::Collections::Scopes::DisjunctionScope.new(scopes: []) + end + end - it { expect(outer.scopes.first).to be subject } + shared_context 'with an empty negation scope' do + let(:original) do + Cuprum::Collections::Scopes::NegationScope.new(scopes: []) + end + end + + shared_context 'with a non-empty conjunction scope' do + let(:original) do + operators = Cuprum::Collections::Queries::Operators + criteria = [ + [ + 'category', + operators::EQUAL, + 'Science Fiction and Fantasy' + ] + ] + wrapped = + Cuprum::Collections::Scopes::CriteriaScope.new(criteria: criteria) - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + Cuprum::Collections::Scopes::ConjunctionScope.new(scopes: [wrapped]) + end + end - it { expect(inner.type).to be :criteria } + shared_examples 'with a non-empty criteria scope' do + let(:original) do + operators = Cuprum::Collections::Queries::Operators + criteria = [ + [ + 'category', + operators::EQUAL, + 'Science Fiction and Fantasy' + ] + ] - it { expect(inner.criteria).to be == expected } + Cuprum::Collections::Scopes::CriteriaScope.new(criteria: criteria) end + end - let(:expected) do + shared_context 'with a non-empty disjunction scope' do + let(:original) do operators = Cuprum::Collections::Queries::Operators + criteria = [ + [ + 'category', + operators::EQUAL, + 'Science Fiction and Fantasy' + ] + ] + wrapped = + Cuprum::Collections::Scopes::CriteriaScope.new(criteria: criteria) - [ + Cuprum::Collections::Scopes::DisjunctionScope.new(scopes: [wrapped]) + end + end + + shared_context 'with a non-empty negation scope' do + let(:original) do + operators = Cuprum::Collections::Queries::Operators + criteria = [ [ - 'title', + 'category', operators::EQUAL, - 'A Wizard of Earthsea' + 'Science Fiction and Fantasy' ] ] + wrapped = + Cuprum::Collections::Scopes::CriteriaScope.new(criteria: criteria) + + Cuprum::Collections::Scopes::NegationScope.new(scopes: [wrapped]) + end + end + + shared_context 'with a null scope' do + let(:original) do + Cuprum::Collections::Scopes::NullScope.new end + end + describe '#and' do it 'should define the method' do expect(subject) .to respond_to(:and) @@ -66,66 +134,92 @@ module ShouldComposeScopesContract describe 'with a block' do let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } - let(:outer) { subject.and(&block) } - let(:inner) { outer.scopes.last } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(&block) - include_examples 'should combine the scopes with logical AND' + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, wrapped] + ) + end + + it { expect(subject.and(&block)).to be == expected } end describe 'with a hash' do let(:value) { { 'title' => 'A Wizard of Earthsea' } } - let(:outer) { subject.and(value) } - let(:inner) { outer.scopes.last } - - include_examples 'should combine the scopes with logical AND' - end + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(value) - describe 'with a scope' do - let(:original) do - Cuprum::Collections::Scopes::CriteriaScope.new(criteria: expected) + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, wrapped] + ) end - let(:outer) { subject.and(original) } - let(:inner) { outer.scopes.last } - include_examples 'should combine the scopes with logical AND' + it { expect(subject.and(value)).to be == expected } end - end - describe '#not' do - shared_examples 'should combine the scopes with logical NAND' do - it { expect(outer).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'with an empty conjunction scope' do + it { expect(subject.and(original)).to be subject } + end - it { expect(outer.type).to be :conjunction } + wrap_context 'with an empty criteria scope' do + it { expect(subject.and(original)).to be subject } + end - it { expect(outer.scopes.size).to be 2 } + wrap_context 'with an empty disjunction scope' do + it { expect(subject.and(original)).to be subject } + end - it { expect(outer.scopes.first).to be subject } + wrap_context 'with an empty negation scope' do + it { expect(subject.and(original)).to be subject } + end - it { expect(invert).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'with a non-empty conjunction scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, *original.scopes] + ) + end - it { expect(invert.type).to be :negation } + it { expect(subject.and(original)).to be == expected } + end - it { expect(invert.scopes.size).to be 1 } + wrap_context 'with a non-empty criteria scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, original] + ) + end - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + it { expect(subject.and(original)).to be == expected } + end - it { expect(inner.type).to be :criteria } + wrap_context 'with a non-empty disjunction scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, original] + ) + end - it { expect(inner.criteria).to be == expected } + it { expect(subject.and(original)).to be == expected } end - let(:expected) do - operators = Cuprum::Collections::Queries::Operators + wrap_context 'with a non-empty negation scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, original] + ) + end - [ - [ - 'title', - operators::EQUAL, - 'A Wizard of Earthsea' - ] - ] + it { expect(subject.and(original)).to be == expected } + end + + wrap_context 'with a null scope' do + it { expect(subject.and(original)).to be subject } end + end + describe '#not' do it 'should define the method' do expect(subject) .to respond_to(:not) @@ -136,163 +230,204 @@ module ShouldComposeScopesContract next if except.include?(:not) describe 'with a block' do - let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } - let(:outer) { subject.not(&block) } - let(:invert) { outer.scopes.last } - let(:inner) { invert.scopes.first } + let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(&block) + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [wrapped]) + + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, inverted] + ) + end - include_examples 'should combine the scopes with logical NAND' + it { expect(subject.not(&block)).to be == expected } end describe 'with a hash' do let(:value) { { 'title' => 'A Wizard of Earthsea' } } - let(:outer) { subject.not(value) } - let(:invert) { outer.scopes.last } - let(:inner) { invert.scopes.first } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(value) + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [wrapped]) + + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, inverted] + ) + end - include_examples 'should combine the scopes with logical NAND' + it { expect(subject.not(value)).to be == expected } end - describe 'with a scope' do - let(:original) do - Cuprum::Collections::Scopes::CriteriaScope.new(criteria: expected) - end - let(:outer) { subject.not(original) } - let(:invert) { outer.scopes.last } - let(:inner) { invert.scopes.last } + wrap_context 'with an empty conjunction scope' do + it { expect(subject.not(original)).to be subject } + end - include_examples 'should combine the scopes with logical NAND' + wrap_context 'with an empty criteria scope' do + it { expect(subject.not(original)).to be subject } end - describe 'with a conjunction scope' do - let(:expected_first) do - operators = Cuprum::Collections::Queries::Operators + wrap_context 'with an empty disjunction scope' do + it { expect(subject.not(original)).to be subject } + end - [ - [ - 'title', - operators::EQUAL, - 'A Wizard of Earthsea' - ] - ] - end - let(:expected_second) do - operators = Cuprum::Collections::Queries::Operators + wrap_context 'with an empty negation scope' do + it { expect(subject.not(original)).to be subject } + end - [ - [ - '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) + wrap_context 'with a non-empty conjunction scope' do + let(:expected) do + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: original.scopes) - Cuprum::Collections::Scopes::ConjunctionScope - .new(scopes: [wrapped_first, wrapped_second]) + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, inverted] + ) 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(subject.not(original)).to be == expected } + end - it { expect(invert).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'with a non-empty criteria scope' do + let(:expected) do + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [original]) - it { expect(invert.type).to be :negation } + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, inverted] + ) + end - it { expect(invert.scopes.size).to be 2 } + it { expect(subject.not(original)).to be == expected } + end - it { expect(inner_one).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'with a non-empty disjunction scope' do + let(:expected) do + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [original]) - it { expect(inner_one.type).to be :criteria } + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, inverted] + ) + end - it { expect(inner_one.criteria).to be == expected_first } + it { expect(subject.not(original)).to be == expected } + end - it { expect(inner_two).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'with a non-empty negation scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, *original.scopes] + ) + end - it { expect(inner_two.type).to be :criteria } + it { expect(subject.not(original)).to be == expected } + end - it { expect(inner_two.criteria).to be == expected_second } + wrap_context 'with a null scope' do + it { expect(subject.not(original)).to be subject } end end describe '#or' do - shared_examples 'should combine the scopes with logical OR' do - it { expect(outer).to be_a Cuprum::Collections::Scopes::Base } + it 'should define the method' do + expect(subject) + .to respond_to(:or) + .with(0..1).arguments + .and_a_block + end - it { expect(outer.type).to be :disjunction } + next if except.include?(:or) - it { expect(outer.scopes.size).to be 2 } + describe 'with a block' do + let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(&block) - it { expect(outer.scopes.first).to be subject } + Cuprum::Collections::Scopes::DisjunctionScope.new( + scopes: [subject, wrapped] + ) + end - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + it { expect(subject.or(&block)).to be == expected } + end - it { expect(inner.type).to be :criteria } + describe 'with a hash' do + let(:value) { { 'title' => 'A Wizard of Earthsea' } } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(value) + + Cuprum::Collections::Scopes::DisjunctionScope.new( + scopes: [subject, wrapped] + ) + end - it { expect(inner.criteria).to be == expected } + it { expect(subject.or(value)).to be == expected } end - let(:expected) do - operators = Cuprum::Collections::Queries::Operators + wrap_context 'with an empty conjunction scope' do + it { expect(subject.or(original)).to be subject } + end - [ - [ - 'title', - operators::EQUAL, - 'A Wizard of Earthsea' - ] - ] + wrap_context 'with an empty criteria scope' do + it { expect(subject.or(original)).to be subject } end - it 'should define the method' do - expect(subject) - .to respond_to(:or) - .with(0..1).arguments - .and_a_block + wrap_context 'with an empty disjunction scope' do + it { expect(subject.or(original)).to be subject } end - next if except.include?(:or) + wrap_context 'with an empty negation scope' do + it { expect(subject.or(original)).to be subject } + end - describe 'with a block' do - let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } - let(:outer) { subject.or(&block) } - let(:inner) { outer.scopes.last } + wrap_context 'with a non-empty conjunction scope' do + let(:expected) do + Cuprum::Collections::Scopes::DisjunctionScope.new( + scopes: [subject, original] + ) + end - include_examples 'should combine the scopes with logical OR' + it { expect(subject.or(original)).to be == expected } end - describe 'with a hash' do - let(:value) { { 'title' => 'A Wizard of Earthsea' } } - let(:outer) { subject.or(value) } - let(:inner) { outer.scopes.last } + wrap_context 'with a non-empty criteria scope' do + let(:expected) do + Cuprum::Collections::Scopes::DisjunctionScope.new( + scopes: [subject, original] + ) + end - include_examples 'should combine the scopes with logical OR' + it { expect(subject.or(original)).to be == expected } end - describe 'with a scope' do - let(:original) do - Cuprum::Collections::Scopes::CriteriaScope.new(criteria: expected) + wrap_context 'with a non-empty disjunction scope' do + let(:expected) do + Cuprum::Collections::Scopes::DisjunctionScope.new( + scopes: [subject, *original.scopes] + ) + end + + it { expect(subject.or(original)).to be == expected } + end + + wrap_context 'with a non-empty negation scope' do + let(:expected) do + Cuprum::Collections::Scopes::DisjunctionScope.new( + scopes: [subject, original] + ) end - let(:outer) { subject.or(original) } - let(:inner) { outer.scopes.last } - include_examples 'should combine the scopes with logical OR' + it { expect(subject.or(original)).to be == expected } + end + + wrap_context 'with a null scope' do + it { expect(subject.or(original)).to be subject } end end end @@ -324,246 +459,245 @@ module ShouldComposeScopesForConjunctionContract include_contract 'should compose scopes', except: %i[and not] describe '#and' do - shared_examples 'should combine the scopes with logical AND' 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(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.and(&block) } - let(:inner) { outer.scopes.last } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(&block) - include_examples 'should combine the scopes with logical AND' + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [*subject.scopes, wrapped] + ) + end - wrap_context 'when the scope has many child scopes' do - include_examples 'should combine the scopes with logical AND' + it { expect(subject.and(&block)).to be == expected } - it { expect(outer.scopes[0...scopes.size]).to be == scopes } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.and(&block)).to be == expected } end end describe 'with a hash' do let(:value) { { 'title' => 'A Wizard of Earthsea' } } - let(:outer) { subject.and(value) } - let(:inner) { outer.scopes.last } - - include_examples 'should combine the scopes with logical AND' + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(value) - wrap_context 'when the scope has many child scopes' do - include_examples 'should combine the scopes with logical AND' - - it { expect(outer.scopes[0...scopes.size]).to be == scopes } + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [*subject.scopes, wrapped] + ) end - end - describe 'with a conjunction scope' do - let(:original) do - wrapped = - Cuprum::Collections::Scopes::CriteriaScope - .new(criteria: expected) + it { expect(subject.and(value)).to be == expected } - Cuprum::Collections::Scopes::ConjunctionScope - .new(scopes: [wrapped]) + wrap_context 'when the scope has many child scopes' do + it { expect(subject.and(value)).to be == expected } end - let(:outer) { subject.and(original) } - let(:inner) { outer.scopes.last } + end - it { expect(outer).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'with an empty conjunction scope' do + it { expect(subject.and(original)).to be subject } + end - it { expect(outer.type).to be :conjunction } + wrap_context 'with an empty criteria scope' do + it { expect(subject.and(original)).to be subject } + end - it { expect(outer.scopes.size).to be scopes.size + 1 } + wrap_context 'with an empty disjunction scope' do + it { expect(subject.and(original)).to be subject } + end - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'with an empty negation scope' do + it { expect(subject.and(original)).to be subject } + end - it { expect(inner.type).to be :criteria } + wrap_context 'with a non-empty conjunction scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [*subject.scopes, *original.scopes] + ) + end - it { expect(inner.criteria).to be == expected } + it { expect(subject.and(original)).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 } + it { expect(subject.and(original)).to be == expected } end end - describe 'with a non-conjunction scope' do - let(:original) do - Cuprum::Collections::Scopes::CriteriaScope.new(criteria: expected) + wrap_context 'with a non-empty criteria scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [*subject.scopes, original] + ) end - let(:outer) { subject.and(original) } - let(:inner) { outer.scopes.last } - include_examples 'should combine the scopes with logical AND' + it { expect(subject.and(original)).to be == expected } wrap_context 'when the scope has many child scopes' do - include_examples 'should combine the scopes with logical AND' - - it { expect(outer.scopes[0...scopes.size]).to be == scopes } + it { expect(subject.and(original)).to be == expected } end end - end - - describe '#not' do - shared_examples 'should combine the scopes with logical NAND' 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(invert).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'with a non-empty disjunction scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [*subject.scopes, original] + ) + end - it { expect(invert.type).to be :negation } + it { expect(subject.and(original)).to be == expected } - it { expect(invert.scopes.size).to be 1 } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.and(original)).to be == expected } + end + end - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'with a non-empty negation scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [*subject.scopes, original] + ) + end - it { expect(inner.type).to be :criteria } + it { expect(subject.and(original)).to be == expected } - it { expect(inner.criteria).to be == expected } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.and(original)).to be == expected } + end end - let(:expected) do - operators = Cuprum::Collections::Queries::Operators - - [ - [ - 'title', - operators::EQUAL, - 'A Wizard of Earthsea' - ] - ] + wrap_context 'with a null scope' do + it { expect(subject.and(original)).to be subject } end + end + describe '#not' do describe 'with a block' do - let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } - let(:outer) { subject.not(&block) } - let(:invert) { outer.scopes.last } - let(:inner) { invert.scopes.first } + let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(&block) + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [wrapped]) + + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [*subject.scopes, inverted] + ) + end - include_examples 'should combine the scopes with logical NAND' + it { expect(subject.not(&block)).to be == expected } wrap_context 'when the scope has many child scopes' do - include_examples 'should combine the scopes with logical NAND' - - it { expect(outer.scopes[0...scopes.size]).to be == scopes } + it { expect(subject.not(&block)).to be == expected } end end describe 'with a hash' do - let(:value) { { 'title' => 'A Wizard of Earthsea' } } - let(:outer) { subject.not(value) } - let(:invert) { outer.scopes.last } - let(:inner) { invert.scopes.first } + let(:value) { { 'title' => 'A Wizard of Earthsea' } } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(value) + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [wrapped]) + + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [*subject.scopes, inverted] + ) + end - include_examples 'should combine the scopes with logical NAND' + it { expect(subject.not(value)).to be == expected } wrap_context 'when the scope has many child scopes' do - include_examples 'should combine the scopes with logical NAND' - - it { expect(outer.scopes[0...scopes.size]).to be == scopes } + it { expect(subject.not(value)).to be == expected } 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 } + wrap_context 'with an empty conjunction scope' do + it { expect(subject.not(original)).to be subject } + end - it { expect(invert).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'with an empty criteria scope' do + it { expect(subject.not(original)).to be subject } + end - it { expect(invert.type).to be :negation } + wrap_context 'with an empty disjunction scope' do + it { expect(subject.not(original)).to be subject } + end - it { expect(invert.scopes.size).to be 1 } + wrap_context 'with an empty negation scope' do + it { expect(subject.not(original)).to be subject } + end - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'with a non-empty conjunction scope' do + let(:expected) do + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: original.scopes) - it { expect(inner.type).to be :criteria } + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [*subject.scopes, inverted] + ) + end - it { expect(inner.criteria).to be == expected } + it { expect(subject.not(original)).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(subject.not(original)).to be == expected } + end + end - it { expect(outer.scopes.size).to be scopes.size + 1 } + wrap_context 'with a non-empty criteria scope' do + let(:expected) do + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [original]) - it { expect(outer.scopes[0...scopes.size]).to be == scopes } + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [*subject.scopes, inverted] + ) + end - it { expect(invert).to be_a Cuprum::Collections::Scopes::Base } + it { expect(subject.not(original)).to be == expected } - it { expect(invert.type).to be :negation } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.not(original)).to be == expected } + end + end - it { expect(invert.scopes.size).to be 1 } + wrap_context 'with a non-empty disjunction scope' do + let(:expected) do + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [original]) - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [*subject.scopes, inverted] + ) + end - it { expect(inner.type).to be :criteria } + it { expect(subject.not(original)).to be == expected } - it { expect(inner.criteria).to be == expected } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.not(original)).to be == expected } end end - describe 'with a scope' do - let(:original) do - Cuprum::Collections::Scopes::CriteriaScope.new(criteria: expected) + wrap_context 'with a non-empty negation scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [*subject.scopes, *original.scopes] + ) end - let(:outer) { subject.not(original) } - let(:invert) { outer.scopes.last } - let(:inner) { invert.scopes.last } - include_examples 'should combine the scopes with logical NAND' + it { expect(subject.not(original)).to be == expected } wrap_context 'when the scope has many child scopes' do - include_examples 'should combine the scopes with logical NAND' - - it { expect(outer.scopes[0...scopes.size]).to be == scopes } + it { expect(subject.not(original)).to be == expected } end end + + wrap_context 'with a null scope' do + it { expect(subject.not(original)).to be subject } + end end end end @@ -601,120 +735,128 @@ module ShouldComposeScopesForCriteriaContract include_contract 'should compose scopes', except: %i[and] describe '#and' do - shared_examples 'should merge the criteria' do - it { expect(outer).to be_a Cuprum::Collections::Scopes::Base } - - it { expect(outer.type).to be :criteria } - - it { expect(outer.criteria).to be == [*criteria, *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.and(&block) } + let(:expected) do + operators = Cuprum::Collections::Queries::Operators + criteria = [ + [ + 'title', + operators::EQUAL, + 'A Wizard of Earthsea' + ] + ] + + Cuprum::Collections::Scopes::CriteriaScope.new( + criteria: [*self.criteria, *criteria] + ) + end - include_examples 'should merge the criteria' + it { expect(subject.and(&block)).to be == expected } wrap_context 'when the scope has multiple criteria' do - include_examples 'should merge the criteria' + it { expect(subject.and(&block)).to be == expected } end end describe 'with a hash' do let(:value) { { 'title' => 'A Wizard of Earthsea' } } - let(:outer) { subject.and(value) } - - include_examples 'should merge the criteria' + let(:expected) do + operators = Cuprum::Collections::Queries::Operators + criteria = [ + [ + 'title', + operators::EQUAL, + 'A Wizard of Earthsea' + ] + ] - wrap_context 'when the scope has multiple criteria' do - include_examples 'should merge the criteria' + Cuprum::Collections::Scopes::CriteriaScope.new( + criteria: [*self.criteria, *criteria] + ) end - end - describe 'with a conjunction scope' do - let(:other_scope) do - wrapped = - Cuprum::Collections::Scopes::CriteriaScope - .new(criteria: expected) + it { expect(subject.and(value)).to be == expected } - Cuprum::Collections::Scopes::ConjunctionScope - .new(scopes: [wrapped]) + wrap_context 'when the scope has multiple criteria' do + it { expect(subject.and(value)).to be == expected } 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 } + end - it { expect(outer.scopes.first).to be subject } + wrap_context 'with an empty conjunction scope' do + it { expect(subject.and(original)).to be subject } + end - it { expect(inner).to be_a(Cuprum::Collections::Scopes::Base) } + wrap_context 'with an empty criteria scope' do + it { expect(subject.and(original)).to be subject } + end - it { expect(inner.type).to be :criteria } + wrap_context 'with an empty disjunction scope' do + it { expect(subject.and(original)).to be subject } + end - it { expect(inner.criteria).to be == expected } + wrap_context 'with an empty negation scope' do + it { expect(subject.and(original)).to be subject } end - describe 'with a criteria scope' do - let(:other_scope) do - Cuprum::Collections::Scopes::CriteriaScope - .new(criteria: expected) + wrap_context 'with a non-empty conjunction scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, *original.scopes] + ) end - let(:outer) { subject.and(other_scope) } - include_examples 'should merge the criteria' + it { expect(subject.and(original)).to be == expected } wrap_context 'when the scope has multiple criteria' do - include_examples 'should merge the criteria' + it { expect(subject.and(original)).to be == expected } end end - describe 'with a non-criteria scope' do - let(:other_scope) do - wrapped = - Cuprum::Collections::Scopes::CriteriaScope - .new(criteria: expected) - - Cuprum::Collections::Scopes::NegationScope.new(scopes: [wrapped]) + wrap_context 'with a non-empty criteria scope' do + let(:expected) do + Cuprum::Collections::Scopes::CriteriaScope.new( + criteria: [*subject.criteria, *original.criteria] + ) end - let(:outer) { subject.and(other_scope) } - let(:invert) { outer.scopes.last } - let(:inner) { invert.scopes.last } - it { expect(outer).to be_a Cuprum::Collections::Scopes::Base } + it { expect(subject.and(original)).to be == expected } - it { expect(outer.type).to be :conjunction } - - it { expect(outer.scopes.size).to be 2 } + wrap_context 'when the scope has multiple criteria' do + it { expect(subject.and(original)).to be == expected } + end + end - it { expect(outer.scopes.first).to be subject } + wrap_context 'with a non-empty disjunction scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, original] + ) + end - it { expect(invert).to be_a Cuprum::Collections::Scopes::Base } + it { expect(subject.and(original)).to be == expected } - it { expect(invert.type).to be :negation } + wrap_context 'when the scope has multiple criteria' do + it { expect(subject.and(original)).to be == expected } + end + end - it { expect(invert.scopes.size).to be 1 } + wrap_context 'with a non-empty negation scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, original] + ) + end - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + it { expect(subject.and(original)).to be == expected } - it { expect(inner.type).to be :criteria } + wrap_context 'when the scope has multiple criteria' do + it { expect(subject.and(original)).to be == expected } + end + end - it { expect(inner.criteria).to be == expected } + wrap_context 'with a null scope' do + it { expect(subject.and(original)).to be subject } end end end @@ -746,110 +888,115 @@ module ShouldComposeScopesForDisjunctionContract 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 } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(&block) + + Cuprum::Collections::Scopes::DisjunctionScope.new( + scopes: [*subject.scopes, wrapped] + ) + end - include_examples 'should combine the scopes with logical OR' + it { expect(subject.or(&block)).to be == expected } 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 } + it { expect(subject.or(&block)).to be == expected } end end describe 'with a hash' do let(:value) { { 'title' => 'A Wizard of Earthsea' } } - let(:outer) { subject.or(value) } - let(:inner) { outer.scopes.last } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(value) - include_examples 'should combine the scopes with logical OR' + Cuprum::Collections::Scopes::DisjunctionScope.new( + scopes: [*subject.scopes, wrapped] + ) + end - wrap_context 'when the scope has many child scopes' do - include_examples 'should combine the scopes with logical OR' + it { expect(subject.or(value)).to be == expected } - it { expect(outer.scopes[0...scopes.size]).to be == scopes } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.or(value)).to be == expected } end end - describe 'with a disjunction scope' do - let(:original) do - wrapped = - Cuprum::Collections::Scopes::CriteriaScope - .new(criteria: expected) + wrap_context 'with an empty conjunction scope' do + it { expect(subject.or(original)).to be subject } + end - Cuprum::Collections::Scopes::DisjunctionScope - .new(scopes: [wrapped]) - end - let(:outer) { subject.or(original) } - let(:inner) { outer.scopes.last } + wrap_context 'with an empty criteria scope' do + it { expect(subject.or(original)).to be subject } + end - it { expect(outer).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'with an empty disjunction scope' do + it { expect(subject.or(original)).to be subject } + end - it { expect(outer.type).to be :disjunction } + wrap_context 'with an empty negation scope' do + it { expect(subject.or(original)).to be subject } + end - it { expect(outer.scopes.size).to be scopes.size + 1 } + wrap_context 'with a non-empty conjunction scope' do + let(:expected) do + Cuprum::Collections::Scopes::DisjunctionScope.new( + scopes: [*subject.scopes, original] + ) + end - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + it { expect(subject.or(original)).to be == expected } - it { expect(inner.type).to be :criteria } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.or(original)).to be == expected } + end + end + + wrap_context 'with a non-empty criteria scope' do + let(:expected) do + Cuprum::Collections::Scopes::DisjunctionScope.new( + scopes: [*subject.scopes, original] + ) + end - it { expect(inner.criteria).to be == expected } + it { expect(subject.or(original)).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(subject.or(original)).to be == expected } + end + end - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'with a non-empty disjunction scope' do + let(:expected) do + Cuprum::Collections::Scopes::DisjunctionScope.new( + scopes: [*subject.scopes, *original.scopes] + ) + end - it { expect(inner.type).to be :criteria } + it { expect(subject.or(original)).to be == expected } - it { expect(inner.criteria).to be == expected } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.or(original)).to be == expected } end end - describe 'with a non-disjunction scope' do - let(:original) do - Cuprum::Collections::Scopes::CriteriaScope.new(criteria: expected) + wrap_context 'with a non-empty negation scope' do + let(:expected) do + Cuprum::Collections::Scopes::DisjunctionScope.new( + scopes: [*subject.scopes, original] + ) end - let(:outer) { subject.or(original) } - let(:inner) { outer.scopes.last } - include_examples 'should combine the scopes with logical OR' + it { expect(subject.or(original)).to be == expected } 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 } + it { expect(subject.or(original)).to be == expected } end end + + wrap_context 'with a null scope' do + it { expect(subject.or(original)).to be subject } + end end end end @@ -880,256 +1027,296 @@ module ShouldComposeScopesForNegationContract include_contract 'should compose scopes', except: %i[and not] describe '#and' do - shared_examples 'should combine the scopes with logical AND' 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 2 } - - it { expect(outer.scopes.first).to be subject } + describe 'with a block' do + let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(&block) - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, wrapped] + ) + end - it { expect(inner.type).to be :criteria } + it { expect(subject.and(&block)).to be == expected } - it { expect(inner.criteria).to be == expected } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.and(&block)).to be == expected } + end end - shared_examples 'should merge the scopes' do - it { expect(outer).to be_a Cuprum::Collections::Scopes::Base } + describe 'with a hash' do + let(:value) { { 'title' => 'A Wizard of Earthsea' } } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(value) - it { expect(outer.type).to be :negation } + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, wrapped] + ) + end - it { expect(outer.scopes.size).to be scopes.size + 1 } + it { expect(subject.and(value)).to be == expected } - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.and(value)).to be == expected } + end + end - it { expect(inner.type).to be :criteria } + wrap_context 'with an empty conjunction scope' do + it { expect(subject.and(original)).to be subject } + end - it { expect(inner.criteria).to be == expected } + wrap_context 'with an empty criteria scope' do + it { expect(subject.and(original)).to be subject } end - let(:expected) do - operators = Cuprum::Collections::Queries::Operators + wrap_context 'with an empty disjunction scope' do + it { expect(subject.and(original)).to be subject } + end - [ - [ - 'title', - operators::EQUAL, - 'A Wizard of Earthsea' - ] - ] + wrap_context 'with an empty negation scope' do + it { expect(subject.and(original)).to be subject } end - describe 'with a block' do - let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } - let(:outer) { subject.and(&block) } - let(:inner) { outer.scopes.last } + wrap_context 'with a non-empty conjunction scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, *original.scopes] + ) + end - include_examples 'should combine the scopes with logical AND' + it { expect(subject.and(original)).to be == expected } wrap_context 'when the scope has many child scopes' do - include_examples 'should combine the scopes with logical AND' + it { expect(subject.and(original)).to be == expected } end end - describe 'with a hash' do - let(:value) { { 'title' => 'A Wizard of Earthsea' } } - let(:outer) { subject.and(value) } - let(:inner) { outer.scopes.last } + wrap_context 'with a non-empty criteria scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, original] + ) + end - include_examples 'should combine the scopes with logical AND' + it { expect(subject.and(original)).to be == expected } wrap_context 'when the scope has many child scopes' do - include_examples 'should combine the scopes with logical AND' + it { expect(subject.and(original)).to be == expected } end end - describe 'with a negation scope' do - let(:original) do - wrapped = - Cuprum::Collections::Scopes::CriteriaScope - .new(criteria: expected) - - Cuprum::Collections::Scopes::NegationScope.new(scopes: [wrapped]) + wrap_context 'with a non-empty disjunction scope' do + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, original] + ) end - let(:outer) { subject.and(original) } - let(:inner) { outer.scopes.last } - include_examples 'should merge the scopes' + it { expect(subject.and(original)).to be == expected } wrap_context 'when the scope has many child scopes' do - include_examples 'should merge the scopes' - - it { expect(outer.scopes[0...scopes.size]).to be == scopes } + it { expect(subject.and(original)).to be == expected } end end - describe 'with a non-negation scope' do - let(:original) do - Cuprum::Collections::Scopes::CriteriaScope.new(criteria: expected) + wrap_context 'with a non-empty negation scope' do + let(:expected) do + Cuprum::Collections::Scopes::NegationScope.new( + scopes: [*subject.scopes, *original.scopes] + ) end - let(:outer) { subject.and(original) } - let(:inner) { outer.scopes.last } - include_examples 'should combine the scopes with logical AND' + it { expect(subject.and(original)).to be == expected } wrap_context 'when the scope has many child scopes' do - include_examples 'should combine the scopes with logical AND' + it { expect(subject.and(original)).to be == expected } end end + + wrap_context 'with a null scope' do + it { expect(subject.and(original)).to be subject } + end end describe '#not' do - shared_examples 'should combine the scopes with logical NAND' 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 2 } + describe 'with a block' do + let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(&block) + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [wrapped]) + + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, inverted] + ) + end - it { expect(outer.scopes.first).to be subject } + it { expect(subject.not(&block)).to be == expected } - it { expect(invert).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.not(&block)).to be == expected } + end + end - it { expect(invert.type).to be :negation } + describe 'with a hash' do + let(:value) { { 'title' => 'A Wizard of Earthsea' } } + let(:expected) do + wrapped = Cuprum::Collections::Scope.new(value) + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [wrapped]) + + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, inverted] + ) + end - it { expect(invert.scopes.size).to be 1 } + it { expect(subject.not(value)).to be == expected } - it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.not(value)).to be == expected } + end + end - it { expect(inner.type).to be :criteria } + wrap_context 'with an empty conjunction scope' do + it { expect(subject.not(original)).to be subject } + end - it { expect(inner.criteria).to be == expected } + wrap_context 'with an empty criteria scope' do + it { expect(subject.not(original)).to be subject } end - let(:expected) do - operators = Cuprum::Collections::Queries::Operators + wrap_context 'with an empty disjunction scope' do + it { expect(subject.not(original)).to be subject } + end - [ - [ - 'title', - operators::EQUAL, - 'A Wizard of Earthsea' - ] - ] + wrap_context 'with an empty negation scope' do + it { expect(subject.not(original)).to be subject } end - describe 'with a block' do - let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } - let(:outer) { subject.not(&block) } - let(:invert) { outer.scopes.last } - let(:inner) { invert.scopes.last } + wrap_context 'with a non-empty conjunction scope' do + let(:expected) do + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: original.scopes) + + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, inverted] + ) + end - include_examples 'should combine the scopes with logical NAND' + it { expect(subject.not(original)).to be == expected } wrap_context 'when the scope has many child scopes' do - include_examples 'should combine the scopes with logical NAND' + it { expect(subject.not(original)).to be == expected } end end - describe 'with a hash' do - let(:value) { { 'title' => 'A Wizard of Earthsea' } } - let(:outer) { subject.not(value) } - let(:invert) { outer.scopes.last } - let(:inner) { invert.scopes.last } + wrap_context 'with a non-empty criteria scope' do + let(:expected) do + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [original]) + + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, inverted] + ) + end - include_examples 'should combine the scopes with logical NAND' + it { expect(subject.not(original)).to be == expected } wrap_context 'when the scope has many child scopes' do - include_examples 'should combine the scopes with logical NAND' + it { expect(subject.not(original)).to be == expected } end end - describe 'with a negation scope with one child scope' do - let(:original) do - wrapped = - Cuprum::Collections::Scopes::CriteriaScope - .new(criteria: expected) + wrap_context 'with a non-empty disjunction scope' do + let(:expected) do + inverted = + Cuprum::Collections::Scopes::NegationScope + .new(scopes: [original]) - Cuprum::Collections::Scopes::NegationScope.new(scopes: [wrapped]) + Cuprum::Collections::Scopes::ConjunctionScope.new( + scopes: [subject, inverted] + ) end - let(:outer) { subject.not(original) } - it { expect(outer).to be_a Cuprum::Collections::Scopes::Base } + it { expect(subject.not(original)).to be == expected } - it { expect(outer.type).to be :criteria } - - it { expect(outer.criteria).to be == expected } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.not(original)).to be == expected } + end end - describe 'with a negation scope with many child scopes' do - let(:expected_first) do - operators = Cuprum::Collections::Queries::Operators - - [ - [ - 'title', - operators::EQUAL, - 'A Wizard of Earthsea' - ] - ] - end - let(:expected_second) do + describe 'with a negation scope with one child scope' do + let(:original) do operators = Cuprum::Collections::Queries::Operators - - [ + criteria = [ [ 'category', operators::EQUAL, 'Science Fiction and Fantasy' ] ] - end - let(:original) do - wrapped_first = - Cuprum::Collections::Scopes::CriteriaScope - .new(criteria: expected_first) - wrapped_second = + wrapped = Cuprum::Collections::Scopes::CriteriaScope - .new(criteria: expected_second) + .new(criteria: criteria) - Cuprum::Collections::Scopes::NegationScope - .new(scopes: [wrapped_first, wrapped_second]) + Cuprum::Collections::Scopes::NegationScope.new(scopes: [wrapped]) + end + let(:expected) do + Cuprum::Collections::Scopes::CriteriaScope + .new(criteria: original.scopes.first.criteria) end - let(:outer) { subject.not(original) } - let(:inner_one) { outer.scopes.first } - let(:inner_two) { 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(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(subject.not(original)).to be == expected } - it { expect(inner_two.criteria).to be == expected_second } + wrap_context 'when the scope has many child scopes' do + it { expect(subject.not(original)).to be == expected } + end end - describe 'with a non-negation scope' do + describe 'with a negation scope with many child scopes' do let(:original) do - Cuprum::Collections::Scopes::CriteriaScope.new(criteria: expected) + operators = Cuprum::Collections::Queries::Operators + criteria = [ + [ + [ + 'category', + operators::EQUAL, + 'Science Fiction and Fantasy' + ] + ], + [ + [ + 'title', + operators::EQUAL, + 'A Wizard of Earthsea' + ] + ] + ] + wrapped = criteria.map do |ary| + Cuprum::Collections::Scopes::CriteriaScope.new(criteria: ary) + end + + Cuprum::Collections::Scopes::NegationScope.new(scopes: wrapped) + end + let(:expected) do + Cuprum::Collections::Scopes::ConjunctionScope + .new(scopes: original.scopes) end - let(:outer) { subject.not(original) } - let(:invert) { outer.scopes.last } - let(:inner) { invert.scopes.last } - include_examples 'should combine the scopes with logical NAND' + it { expect(subject.not(original)).to be == expected } wrap_context 'when the scope has many child scopes' do - include_examples 'should combine the scopes with logical NAND' + it { expect(subject.not(original)).to be == expected } end end + + wrap_context 'with a null scope' do + it { expect(subject.not(original)).to be subject } + end end end end diff --git a/lib/cuprum/collections/scopes/base.rb b/lib/cuprum/collections/scopes/base.rb index 8bf5bb8..0f68d73 100644 --- a/lib/cuprum/collections/scopes/base.rb +++ b/lib/cuprum/collections/scopes/base.rb @@ -20,6 +20,16 @@ def ==(other) other.type == type end + # :nocov: + + # @private + # + # Generates a string representation of the scope. + def debug + debug_class_name(self) + end + # :nocov: + # @return [Boolean] false. def empty? false @@ -35,6 +45,16 @@ def type def builder Cuprum::Collections::Scopes::Builder.instance end + + # :nocov: + def debug_class_name(scope) + name = scope.class.name.sub(/\ACuprum::Collections::/, '') + segments = + name.split(/(::)?Scopes(::)?/).reject { |s| s.empty? || s == '::' } + + segments.join('::') + end + # :nocov: end end diff --git a/lib/cuprum/collections/scopes/composition.rb b/lib/cuprum/collections/scopes/composition.rb index c410f33..b26eb14 100644 --- a/lib/cuprum/collections/scopes/composition.rb +++ b/lib/cuprum/collections/scopes/composition.rb @@ -12,7 +12,11 @@ module Composition # # @override and(scope) # Combines with the current scope using a logical AND. + # + # Returns self if the given scope is empty. def and(*args, &block) + return self if empty_scope?(args.first) + return and_conjunction_scope(args.first) if conjunction_scope?(args.first) scope = builder.build(*args, &block) @@ -30,9 +34,15 @@ def and(*args, &block) # # @override not(scope) # Inverts and combines with the current scope using a logical AND. + # + # Returns self if the given scope is empty. def not(*args, &block) + return self if empty_scope?(args.first) + return not_conjunction_scope(args.first) if conjunction_scope?(args.first) + return not_negation_scope(args.first) if negation_scope?(args.first) + scope = builder.build(*args, &block) inverted = builder.build_negation_scope(scopes: [scope], safe: false) @@ -48,8 +58,14 @@ def not(*args, &block) # # @override and(scope) # Combines with the current scope using a logical OR. - def or(...) - scope = builder.build(...) + # + # Returns self if the given scope is empty. + def or(*args, &block) + return self if empty_scope?(args.first) + + return or_disjunction_scope(args.first) if disjunction_scope?(args.first) + + scope = builder.build(*args, &block) # We control the current and generated scopes, so we can skip validation # and transformation. @@ -78,6 +94,10 @@ def disjunction_scope?(value) scope?(value) && value.type == :disjunction end + def empty_scope?(value) + scope?(value) && value.empty? + end + def negation_scope?(value) scope?(value) && value.type == :negation end @@ -91,6 +111,22 @@ def not_conjunction_scope(scope) builder.build_conjunction_scope(scopes: [self, inverted], safe: false) end + def not_negation_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 or_disjunction_scope(scope) + scopes = scope.scopes.map do |inner| + builder.transform_scope(scope: inner) + end + + builder.build_disjunction_scope(scopes: [self, *scopes], safe: false) + end + def scope?(value) value.is_a?(Cuprum::Collections::Scopes::Base) end diff --git a/lib/cuprum/collections/scopes/conjunction.rb b/lib/cuprum/collections/scopes/conjunction.rb index c61f58a..6fc64b6 100644 --- a/lib/cuprum/collections/scopes/conjunction.rb +++ b/lib/cuprum/collections/scopes/conjunction.rb @@ -10,6 +10,8 @@ module Conjunction # (see Cuprum::Collections::Scopes::Composition#and) def and(*args, &block) + return self if empty_scope?(args.first) + return and_conjunction_scope(args.first) if conjunction_scope?(args.first) with_scopes([*scopes, builder.build(*args, &block)]) @@ -18,8 +20,12 @@ def and(*args, &block) # (see Cuprum::Collections::Scopes::Composition#not) def not(*args, &block) + return self if empty_scope?(args.first) + return not_conjunction_scope(args.first) if conjunction_scope?(args.first) + return not_negation_scope(args.first) if negation_scope?(args.first) + scope = builder.build(*args, &block) inverted = builder.build_negation_scope(scopes: [scope], safe: false) @@ -49,5 +55,13 @@ def not_conjunction_scope(scope) with_scopes([*self.scopes, inverted]) end + + def not_negation_scope(scope) + scopes = scope.scopes.map do |inner| + builder.transform_scope(scope: inner) + end + + with_scopes([*self.scopes, *scopes]) + end end end diff --git a/lib/cuprum/collections/scopes/container.rb b/lib/cuprum/collections/scopes/container.rb index 882a0c9..378b753 100644 --- a/lib/cuprum/collections/scopes/container.rb +++ b/lib/cuprum/collections/scopes/container.rb @@ -26,6 +26,17 @@ def ==(other) other.scopes == scopes end + # @private + def debug + message = "#{super} (#{scopes.count})" + + return message if empty? + + scopes.reduce("#{message}:") do |str, scope| + str + "\n- #{scope.debug.gsub("\n", "\n ")}" + end + end + # @return [Boolean] true if the scope has no child scopes; otherwise false. def empty? @scopes.empty? diff --git a/lib/cuprum/collections/scopes/criteria.rb b/lib/cuprum/collections/scopes/criteria.rb index cdd188c..8a3af1c 100644 --- a/lib/cuprum/collections/scopes/criteria.rb +++ b/lib/cuprum/collections/scopes/criteria.rb @@ -253,6 +253,8 @@ def ==(other) # (see Cuprum::Collections::Scopes::Composition#and) def and(*args, &block) + return self if empty_scope?(args.first) + return and_criteria_scope(args.first) if criteria_scope?(args.first) return super if scope?(args.first) @@ -261,6 +263,17 @@ def and(*args, &block) end alias where and + # @private + def debug + message = "#{super} (#{criteria.count})" + + return message if empty? + + criteria.reduce("#{message}:") do |str, (attribute, operator, value)| + str + "\n- #{attribute.inspect} #{operator} #{value.inspect}" + end + end + # @return [Boolean] true if the scope has no criteria; otherwise false. def empty? @criteria.empty? diff --git a/lib/cuprum/collections/scopes/disjunction.rb b/lib/cuprum/collections/scopes/disjunction.rb index be6f655..5e5bbfe 100644 --- a/lib/cuprum/collections/scopes/disjunction.rb +++ b/lib/cuprum/collections/scopes/disjunction.rb @@ -10,7 +10,9 @@ module Disjunction # (see Cuprum::Collections::Scopes::Composition#or) def or(*args, &block) - return or_disjuncton_scope(args.first) if disjunction_scope?(args.first) + return self if empty_scope?(args.first) + + return or_disjunction_scope(args.first) if disjunction_scope?(args.first) with_scopes([*scopes, builder.build(*args, &block)]) end @@ -22,7 +24,7 @@ def type private - def or_disjuncton_scope(scope) + def or_disjunction_scope(scope) scopes = scope.scopes.map do |inner| builder.transform_scope(scope: inner) end diff --git a/lib/cuprum/collections/scopes/negation.rb b/lib/cuprum/collections/scopes/negation.rb index 280bc8c..c8de8fe 100644 --- a/lib/cuprum/collections/scopes/negation.rb +++ b/lib/cuprum/collections/scopes/negation.rb @@ -10,6 +10,8 @@ module Negation # (see Cuprum::Collections::Scopes::Composition#and) def and(*args, &block) + return self if empty_scope?(args.first) + return super unless negation_scope?(args.first) scopes = args.first.scopes.map do |scope| @@ -22,6 +24,8 @@ def and(*args, &block) # (see Cuprum::Collections::Scopes::Composition#not) def not(*args, &block) + return self if empty_scope?(args.first) + return super unless negation_scope?(args.first) scopes = args.first.scopes.map do |scope| diff --git a/lib/cuprum/collections/scopes/null.rb b/lib/cuprum/collections/scopes/null.rb index dbc4191..38a3ba0 100644 --- a/lib/cuprum/collections/scopes/null.rb +++ b/lib/cuprum/collections/scopes/null.rb @@ -12,8 +12,10 @@ module Null # # @override and(scope) # Returns the given scope. - def and(...) - builder.build(...) + def and(*args, &block) + return self if empty_scope?(args.first) + + builder.build(*args, &block) end alias where and @@ -29,8 +31,10 @@ def empty? # # @override or(scope) # Returns the given scope. - def or(...) - builder.build(...) + def or(*args, &block) + return self if empty_scope?(args.first) + + builder.build(*args, &block) end # @override not(hash = nil, &block) @@ -40,8 +44,14 @@ def or(...) # # @override not(scope) # Inverts and returns the given scope. - def not(...) - scope = builder.build(...) + def not(*args, &block) + return self if empty_scope?(args.first) + + return not_conjunction_scope(args.first) if conjunction_scope?(args.first) + + return not_negation_scope(args.first) if negation_scope?(args.first) + + scope = builder.build(*args, &block) builder.build_negation_scope(scopes: [scope], safe: false) end @@ -50,5 +60,25 @@ def not(...) def type :null end + + private + + def not_conjunction_scope(scope) + scopes = scope.scopes.map do |inner| + builder.transform_scope(scope: inner) + end + + builder.build_negation_scope(scopes: scopes, safe: false) + end + + def not_negation_scope(scope) + scopes = scope.scopes.map do |inner| + builder.transform_scope(scope: inner) + end + + return scopes.first if scopes.size == 1 + + builder.build_conjunction_scope(scopes: scopes, safe: false) + end end end diff --git a/spec/integration/basic/scopes_spec.rb b/spec/integration/basic/scopes_spec.rb index 8531994..739cf8a 100644 --- a/spec/integration/basic/scopes_spec.rb +++ b/spec/integration/basic/scopes_spec.rb @@ -16,46 +16,12 @@ .not({ 'series' => nil }) end - def inspect_scope(scope) # rubocop:disable Metrics:AbcSize, Metrics/MethodLength - str = scope_class_name(scope) - - if scope.respond_to?(:criteria) - str += " (#{scope.criteria.count})" - str += ':' unless scope.criteria.empty? - - scope.criteria.reduce(str) do |memo, (attr, op, value)| - memo + "\n- #{attr.inspect} #{op} #{value.inspect}" - end - elsif scope.respond_to?(:scopes) - str += " (#{scope.scopes.count})" - str += ':' unless scope.scopes.empty? - - scope.scopes.reduce(str) do |memo, scope| - inspected = inspect_scope(scope) - - memo + "\n- #{tools.str.indent(inspected, 2).sub(/\A /, '')}" - end - end - end - - def scope_class_name(scope) - name = scope.class.name.sub(/\ACuprum::Collections::/, '') - segments = - name.split(/(::)?Scopes(::)?/).reject { |s| s.empty? || s == '::' } - - segments.join('::') - end - - def tools - SleepingKingStudios::Tools::Toolbelt.instance - end - describe 'with an empty scope' do let(:scope) { described_class::CriteriaScope.new(criteria: []) } let(:inspected) { 'Basic::CriteriaScope (0)' } let(:matching) { data } - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } @@ -72,7 +38,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -90,7 +56,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -108,7 +74,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -132,7 +98,7 @@ def tools .reject { |book| book['series'].nil? } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -149,7 +115,7 @@ def tools TEXT end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -166,7 +132,7 @@ def tools TEXT end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -183,7 +149,7 @@ def tools TEXT end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -203,7 +169,7 @@ def tools TEXT end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -224,7 +190,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -245,7 +211,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -266,7 +232,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -291,7 +257,7 @@ def tools end end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -320,7 +286,7 @@ def tools data.select { |book| book['author'] == 'Ursula K. LeGuin' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } @@ -338,7 +304,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -357,7 +323,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -378,7 +344,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -403,7 +369,7 @@ def tools .reject { |book| book['series'].nil? } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -421,7 +387,7 @@ def tools TEXT end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -439,7 +405,7 @@ def tools TEXT end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -457,7 +423,7 @@ def tools TEXT end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -478,7 +444,7 @@ def tools TEXT end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -500,7 +466,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -522,7 +488,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -544,7 +510,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -570,7 +536,7 @@ def tools end end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -599,7 +565,7 @@ def tools .reject { |book| book['title'] == 'The Ones Who Walk Away From Omelas' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } @@ -622,7 +588,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -646,7 +612,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -670,7 +636,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -700,7 +666,7 @@ def tools # :nocov: end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -725,7 +691,7 @@ def tools [*super(), *data.select { |book| book['series'] == 'Earthsea' }].uniq end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -750,7 +716,7 @@ def tools [*super(), *data.select { |book| book['series'] == 'Earthsea' }].uniq end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -775,7 +741,7 @@ def tools [*super(), *data.select { |book| book['series'] == 'Earthsea' }].uniq end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -809,7 +775,7 @@ def tools ].uniq end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -834,7 +800,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -859,7 +825,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -884,7 +850,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -913,7 +879,7 @@ def tools end end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -943,7 +909,7 @@ def tools end end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } @@ -966,7 +932,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -990,7 +956,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -1014,7 +980,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -1042,7 +1008,7 @@ def tools .reject { |book| book['series'].nil? } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -1062,7 +1028,7 @@ def tools TEXT end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -1082,7 +1048,7 @@ def tools TEXT end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -1102,7 +1068,7 @@ def tools TEXT end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -1133,7 +1099,7 @@ def tools ].uniq end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -1158,7 +1124,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -1183,7 +1149,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -1208,7 +1174,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -1237,7 +1203,7 @@ def tools end end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -1272,7 +1238,7 @@ def tools .reject { |book| book['author'] == 'J.R.R. Tolkien' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } @@ -1293,7 +1259,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -1315,7 +1281,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -1337,7 +1303,7 @@ def tools super().select { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -1363,7 +1329,7 @@ def tools end end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -1388,7 +1354,7 @@ def tools ].uniq end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -1413,7 +1379,7 @@ def tools ].uniq end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -1438,7 +1404,7 @@ def tools ].uniq end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to match_array(matching) } end @@ -1468,7 +1434,7 @@ def tools ].uniq end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -1491,7 +1457,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -1514,7 +1480,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -1537,7 +1503,7 @@ def tools super().reject { |book| book['series'] == 'Earthsea' } end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end @@ -1564,7 +1530,251 @@ def tools end end - it { expect(inspect_scope(scope)).to be == inspected } + it { expect(scope.debug).to be == inspected } + + it { expect(scope.call(data: data)).to be == matching } + end + end + + describe 'with a null scope' do + let(:scope) { described_class::NullScope.new } + let(:inspected) { 'Basic::NullScope' } + let(:matching) { data } + + it { expect(scope.debug).to be == inspected } + + it { expect(scope.call(data: data)).to be == matching } + + describe '#and a block' do + let(:block) { -> { { 'series' => 'Earthsea' } } } + let(:scope) { super().and(&block) } + let(:inspected) do + <<~TEXT.strip + Basic::CriteriaScope (1): + - "series" equal "Earthsea" + TEXT + end + let(:matching) do + super().select { |book| book['series'] == 'Earthsea' } + end + + it { expect(scope.debug).to be == inspected } + + it { expect(scope.call(data: data)).to be == matching } + end + + describe '#and a hash' do + let(:value) { { 'series' => 'Earthsea' } } + let(:scope) { super().and(value) } + let(:inspected) do + <<~TEXT.strip + Basic::CriteriaScope (1): + - "series" equal "Earthsea" + TEXT + end + let(:matching) do + super().select { |book| book['series'] == 'Earthsea' } + end + + it { expect(scope.debug).to be == inspected } + + it { expect(scope.call(data: data)).to be == matching } + end + + describe '#and a basic scope' do + let(:value) { Cuprum::Collections::Scope.new({ 'series' => 'Earthsea' }) } + let(:scope) { super().and(value) } + let(:inspected) do + <<~TEXT.strip + Basic::CriteriaScope (1): + - "series" equal "Earthsea" + TEXT + end + let(:matching) do + super().select { |book| book['series'] == 'Earthsea' } + end + + it { expect(scope.debug).to be == inspected } + + it { expect(scope.call(data: data)).to be == matching } + end + + describe '#and a complex scope' do + let(:scope) { super().and(complex_scope) } + let(:inspected) do + <<~TEXT.strip + Basic::ConjunctionScope (2): + - Basic::CriteriaScope (1): + - "published_at" greater_than "1973-01-01" + - Basic::NegationScope (1): + - Basic::CriteriaScope (1): + - "series" equal nil + TEXT + end + let(:matching) do + super() + .select { |book| book['published_at'] > '1973-01-01' } + .reject { |book| book['series'].nil? } + end + + it { expect(scope.debug).to be == inspected } + + it { expect(scope.call(data: data)).to be == matching } + end + + describe '#or a block' do + let(:block) { -> { { 'series' => 'Earthsea' } } } + let(:scope) { super().or(&block) } + let(:inspected) do + <<~TEXT.strip + Basic::CriteriaScope (1): + - "series" equal "Earthsea" + TEXT + end + let(:matching) do + super().select { |book| book['series'] == 'Earthsea' } + end + + it { expect(scope.debug).to be == inspected } + + it { expect(scope.call(data: data)).to be == matching } + end + + describe '#or a hash' do + let(:value) { { 'series' => 'Earthsea' } } + let(:scope) { super().or(value) } + let(:inspected) do + <<~TEXT.strip + Basic::CriteriaScope (1): + - "series" equal "Earthsea" + TEXT + end + let(:matching) do + super().select { |book| book['series'] == 'Earthsea' } + end + + it { expect(scope.debug).to be == inspected } + + it { expect(scope.call(data: data)).to be == matching } + end + + describe '#or a basic scope' do + let(:value) { Cuprum::Collections::Scope.new({ 'series' => 'Earthsea' }) } + let(:scope) { super().or(value) } + let(:inspected) do + <<~TEXT.strip + Basic::CriteriaScope (1): + - "series" equal "Earthsea" + TEXT + end + let(:matching) do + super().select { |book| book['series'] == 'Earthsea' } + end + + it { expect(scope.debug).to be == inspected } + + it { expect(scope.call(data: data)).to be == matching } + end + + describe '#or a complex scope' do + let(:scope) { super().or(complex_scope) } + let(:inspected) do + <<~TEXT.strip + Basic::ConjunctionScope (2): + - Basic::CriteriaScope (1): + - "published_at" greater_than "1973-01-01" + - Basic::NegationScope (1): + - Basic::CriteriaScope (1): + - "series" equal nil + TEXT + end + let(:matching) do + super() + .select { |book| book['published_at'] > '1973-01-01' } + .reject { |book| book['series'].nil? } + end + + it { expect(scope.debug).to be == inspected } + + it { expect(scope.call(data: data)).to be == matching } + end + + describe '#not a block' do + let(:block) { -> { { 'series' => 'Earthsea' } } } + let(:scope) { super().not(&block) } + let(:inspected) do + <<~TEXT.strip + Basic::NegationScope (1): + - Basic::CriteriaScope (1): + - "series" equal "Earthsea" + TEXT + end + let(:matching) do + super().reject { |book| book['series'] == 'Earthsea' } + end + + it { expect(scope.debug).to be == inspected } + + it { expect(scope.call(data: data)).to be == matching } + end + + describe '#not a hash' do + let(:value) { { 'series' => 'Earthsea' } } + let(:scope) { super().not(value) } + let(:inspected) do + <<~TEXT.strip + Basic::NegationScope (1): + - Basic::CriteriaScope (1): + - "series" equal "Earthsea" + TEXT + end + let(:matching) do + super().reject { |book| book['series'] == 'Earthsea' } + end + + it { expect(scope.debug).to be == inspected } + + it { expect(scope.call(data: data)).to be == matching } + end + + describe '#not a basic scope' do + let(:value) { Cuprum::Collections::Scope.new({ 'series' => 'Earthsea' }) } + let(:scope) { super().not(value) } + let(:inspected) do + <<~TEXT.strip + Basic::NegationScope (1): + - Basic::CriteriaScope (1): + - "series" equal "Earthsea" + TEXT + end + let(:matching) do + super().reject { |book| book['series'] == 'Earthsea' } + end + + it { expect(scope.debug).to be == inspected } + + it { expect(scope.call(data: data)).to be == matching } + end + + describe '#not a complex scope' do + let(:scope) { super().not(complex_scope) } + let(:inspected) do + <<~TEXT.strip + Basic::NegationScope (2): + - Basic::CriteriaScope (1): + - "published_at" greater_than "1973-01-01" + - Basic::NegationScope (1): + - Basic::CriteriaScope (1): + - "series" equal nil + TEXT + end + let(:matching) do + super().reject do |book| + book['published_at'] > '1973-01-01' && !book['series'].nil? + end + end + + it { expect(scope.debug).to be == inspected } it { expect(scope.call(data: data)).to be == matching } end