From eabd88c2bd03573c8330eafd5c22e9ab77e2f517 Mon Sep 17 00:00:00 2001 From: Rob Smith Date: Thu, 18 Jan 2024 03:42:43 -0500 Subject: [PATCH] Implement Scopes::NullScope. - Implement Scopes::Null. - Implement Basic::Scopes::NullScope. --- lib/cuprum/collections/basic/scopes.rb | 2 + .../collections/basic/scopes/null_scope.rb | 19 ++ .../rspec/contracts/scope_contracts.rb | 212 ++++++++++++++++++ lib/cuprum/collections/scopes.rb | 2 + lib/cuprum/collections/scopes/null.rb | 49 ++++ lib/cuprum/collections/scopes/null_scope.rb | 12 + .../basic/scopes/null_scope_spec.rb | 66 ++++++ .../collections/scopes/null_scope_spec.rb | 19 ++ spec/cuprum/collections/scopes/null_spec.rb | 18 ++ 9 files changed, 399 insertions(+) create mode 100644 lib/cuprum/collections/basic/scopes/null_scope.rb create mode 100644 lib/cuprum/collections/scopes/null.rb create mode 100644 lib/cuprum/collections/scopes/null_scope.rb create mode 100644 spec/cuprum/collections/basic/scopes/null_scope_spec.rb create mode 100644 spec/cuprum/collections/scopes/null_scope_spec.rb create mode 100644 spec/cuprum/collections/scopes/null_spec.rb diff --git a/lib/cuprum/collections/basic/scopes.rb b/lib/cuprum/collections/basic/scopes.rb index 8f578ce..bb6ff8a 100644 --- a/lib/cuprum/collections/basic/scopes.rb +++ b/lib/cuprum/collections/basic/scopes.rb @@ -17,5 +17,7 @@ module Scopes 'cuprum/collections/basic/scopes/disjunction_scope' autoload :NegationScope, 'cuprum/collections/basic/scopes/negation_scope' + autoload :NullScope, + 'cuprum/collections/basic/scopes/null_scope' end end diff --git a/lib/cuprum/collections/basic/scopes/null_scope.rb b/lib/cuprum/collections/basic/scopes/null_scope.rb new file mode 100644 index 0000000..9e10c8a --- /dev/null +++ b/lib/cuprum/collections/basic/scopes/null_scope.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'cuprum/collections/basic/scopes' +require 'cuprum/collections/basic/scopes/base' +require 'cuprum/collections/scopes/null' + +module Cuprum::Collections::Basic::Scopes + # Scope for returning unfiltered data. + class NullScope < Cuprum::Collections::Basic::Scopes::Base + include Cuprum::Collections::Scopes::Null + + # Filters the provided data. + def call(data:) + raise ArgumentError, 'data must be an Array' unless data.is_a?(Array) + + data + end + end +end diff --git a/lib/cuprum/collections/rspec/contracts/scope_contracts.rb b/lib/cuprum/collections/rspec/contracts/scope_contracts.rb index 565d390..0bdab26 100644 --- a/lib/cuprum/collections/rspec/contracts/scope_contracts.rb +++ b/lib/cuprum/collections/rspec/contracts/scope_contracts.rb @@ -2,6 +2,7 @@ require 'cuprum/collections/rspec/contracts' require 'cuprum/collections/rspec/fixtures' +require 'cuprum/collections/scope' module Cuprum::Collections::RSpec::Contracts # Contracts for asserting on scope objects. @@ -82,5 +83,216 @@ module ShouldBeAContainerScopeContract end end end + + # Contract validating the behavior of a Null scope implementation. + module ShouldBeANullScopeContract + extend RSpec::SleepingKingStudios::Contract + + # @!method apply(example_group, abstract: false) + # Adds the contract to the example group. + # + # @param example_group [RSpec::Core::ExampleGroup] the example group to + # which the contract is applied. + # @param abstract [Boolean] if true, the scope is abstract and does not + # define a #call implementation. Defaults to false. + contract do |abstract: false| + 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) + .with(0..1).arguments + .and_a_block + end + + it { expect(subject).to have_aliased_method(:and).as(:where) } + + describe 'with a block' do + let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } + let(:outer) { subject.and(&block) } + + include_examples 'should return the scope' + end + + describe 'with a hash' do + let(:value) { { 'title' => 'A Wizard of Earthsea' } } + let(:outer) { subject.and(value) } + + include_examples 'should return the scope' + end + + describe 'with a scope' do + let(:value) do + Cuprum::Collections::Scope + .new({ 'title' => 'A Wizard of Earthsea' }) + end + let(:outer) { subject.and(value) } + + include_examples 'should return the scope' + end + end + + describe '#call' do + shared_context 'with data' do + let(:data) do + Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES + end + end + + next if abstract + + describe 'with empty data' do + let(:data) { [] } + let(:expected) { data } + + it { expect(filtered_data).to be == expected } + end + + wrap_context 'with data' do + let(:expected) { data } + + it { expect(filtered_data).to be == expected } + end + 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) + .with(0..1).arguments + .and_a_block + end + + describe 'with a block' do + let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } + let(:outer) { subject.or(&block) } + + include_examples 'should return the scope' + end + + describe 'with a hash' do + let(:value) { { 'title' => 'A Wizard of Earthsea' } } + let(:outer) { subject.or(value) } + + include_examples 'should return the scope' + end + + describe 'with a scope' do + let(:value) do + Cuprum::Collections::Scope + .new({ 'title' => 'A Wizard of Earthsea' }) + end + let(:outer) { subject.or(value) } + + include_examples 'should return the scope' + end + end + + describe '#not' do + shared_examples 'should invert and return the scope' do + it { expect(outer).to be_a Cuprum::Collections::Scopes::Base } + + it { expect(outer.type).to be :negation } + + it { expect(outer.scopes.size).to be 1 } + + it { expect(inner).to be_a Cuprum::Collections::Scopes::Base } + + it { expect(inner.type).to be :criteria } + + it { expect(inner.criteria).to be == expected } + end + + 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(:not) + .with(0..1).arguments + .and_a_block + end + + describe 'with a block' do + let(:block) { -> { { 'title' => 'A Wizard of Earthsea' } } } + let(:outer) { subject.not(&block) } + let(:inner) { outer.scopes.first } + + include_examples 'should invert and return the scope' + end + + describe 'with a hash' do + let(:value) { { 'title' => 'A Wizard of Earthsea' } } + let(:outer) { subject.not(value) } + let(:inner) { outer.scopes.first } + + include_examples 'should invert and return the scope' + end + + describe 'with a scope' do + let(:value) do + Cuprum::Collections::Scope + .new({ 'title' => 'A Wizard of Earthsea' }) + end + let(:outer) { subject.not(value) } + let(:inner) { outer.scopes.first } + + include_examples 'should invert and return the scope' + end + end + + describe '#type' do + include_examples 'should define reader', :type, :null + end + end + end end end diff --git a/lib/cuprum/collections/scopes.rb b/lib/cuprum/collections/scopes.rb index 2e12385..ee75c05 100644 --- a/lib/cuprum/collections/scopes.rb +++ b/lib/cuprum/collections/scopes.rb @@ -17,5 +17,7 @@ module Scopes autoload :DisjunctionScope, 'cuprum/collections/scopes/disjunction_scope' autoload :Negation, 'cuprum/collections/scopes/negation' autoload :NegationScope, 'cuprum/collections/scopes/negation_scope' + autoload :Null, 'cuprum/collections/scopes/null' + autoload :NullScope, 'cuprum/collections/scopes/null_scope' end end diff --git a/lib/cuprum/collections/scopes/null.rb b/lib/cuprum/collections/scopes/null.rb new file mode 100644 index 0000000..f40d749 --- /dev/null +++ b/lib/cuprum/collections/scopes/null.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'cuprum/collections/scopes' + +module Cuprum::Collections::Scopes + # Functionality for implementing a null scope. + module Null + # @override and(hash = nil, &block) + # Parses the hash or block and returns the parsed scope. + # + # @see Cuprum::Collections::Scopes::Criteria::Parser#parse. + # + # @override and(scope) + # Returns the given scope. + def and(...) + builder.build(...) + end + alias where and + + # @override or(hash = nil, &block) + # Parses the hash or block and returns the parsed scope. + # + # @see Cuprum::Collections::Scopes::Criteria::Parser#parse. + # + # @override or(scope) + # Returns the given scope. + def or(...) + builder.build(...) + end + + # @override not(hash = nil, &block) + # Parses and inverts the hash or block and returns the inverted scope. + # + # @see Cuprum::Collections::Scopes::Criteria::Parser#parse. + # + # @override not(scope) + # Inverts and returns the given scope. + def not(...) + scope = builder.build(...) + + builder.build_negation_scope(scopes: [scope], safe: false) + end + + # (see Cuprum::Collections::Scopes::Base#type) + def type + :null + end + end +end diff --git a/lib/cuprum/collections/scopes/null_scope.rb b/lib/cuprum/collections/scopes/null_scope.rb new file mode 100644 index 0000000..64bec4f --- /dev/null +++ b/lib/cuprum/collections/scopes/null_scope.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'cuprum/collections/scopes' +require 'cuprum/collections/scopes/base' +require 'cuprum/collections/scopes/null' + +module Cuprum::Collections::Scopes + # Generic scope class for defining collection-independent null scopes. + class NullScope < Cuprum::Collections::Scopes::Base + include Cuprum::Collections::Scopes::Null + end +end diff --git a/spec/cuprum/collections/basic/scopes/null_scope_spec.rb b/spec/cuprum/collections/basic/scopes/null_scope_spec.rb new file mode 100644 index 0000000..e3edda1 --- /dev/null +++ b/spec/cuprum/collections/basic/scopes/null_scope_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'cuprum/collections/rspec/contracts/scope_contracts' +require 'cuprum/collections/basic/scopes/null_scope' + +RSpec.describe Cuprum::Collections::Basic::Scopes::NullScope do + include Cuprum::Collections::RSpec::Contracts::ScopeContracts + + subject(:scope) { described_class.new } + + let(:data) { [] } + + def filtered_data + scope.call(data: data) + end + + describe '.new' do + it 'should define the constructor' do + expect(described_class) + .to be_constructible + .with(0).arguments + .and_any_keywords + end + end + + include_contract 'should be a null scope' + + describe '#match' do + let(:item) { {} } + + it 'should define the method' do + expect(scope).to respond_to(:match?).with(0).arguments.and_keywords(:item) + end + + it 'should alias the method' do + expect(scope).to have_aliased_method(:match?).as(:matches?) + end + + describe 'with nil' do + let(:error_message) { 'item must be a Hash' } + + it 'should raise an exception' do + expect { scope.match?(item: nil) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with an Object' do + let(:error_message) { 'item must be a Hash' } + + it 'should raise an exception' do + expect { scope.match?(item: Object.new.freeze) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with an item' do + let(:item) do + Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES + .find { |book| book['title'] == 'The Silmarillion' } + end + + it { expect(scope.match?(item: item)).to be true } + end + end +end diff --git a/spec/cuprum/collections/scopes/null_scope_spec.rb b/spec/cuprum/collections/scopes/null_scope_spec.rb new file mode 100644 index 0000000..920b109 --- /dev/null +++ b/spec/cuprum/collections/scopes/null_scope_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'cuprum/collections/rspec/contracts/scope_contracts' +require 'cuprum/collections/scopes/null_scope' + +RSpec.describe Cuprum::Collections::Scopes::NullScope do + include Cuprum::Collections::RSpec::Contracts::ScopeContracts + + describe '.new' do + it 'should define the constructor' do + expect(described_class) + .to be_constructible + .with(0).arguments + .and_any_keywords + end + end + + include_contract 'should be a null scope', abstract: true +end diff --git a/spec/cuprum/collections/scopes/null_spec.rb b/spec/cuprum/collections/scopes/null_spec.rb new file mode 100644 index 0000000..ea03d38 --- /dev/null +++ b/spec/cuprum/collections/scopes/null_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'cuprum/collections/rspec/contracts/scope_contracts' +require 'cuprum/collections/scopes/base' +require 'cuprum/collections/scopes/null' + +RSpec.describe Cuprum::Collections::Scopes::Null do + include Cuprum::Collections::RSpec::Contracts::ScopeContracts + + let(:described_class) { Spec::ExampleScope } + + example_class 'Spec::ExampleScope', Cuprum::Collections::Scopes::Base \ + do |klass| + klass.include Cuprum::Collections::Scopes::Null # rubocop:disable RSpec/DescribedClass + end + + include_contract 'should be a null scope', abstract: true +end