diff --git a/lib/cuprum/collections.rb b/lib/cuprum/collections.rb index 7bacc25..e6d1bc1 100644 --- a/lib/cuprum/collections.rb +++ b/lib/cuprum/collections.rb @@ -15,6 +15,8 @@ module Collections autoload :Relation, 'cuprum/collections/relation' autoload :Repository, 'cuprum/collections/repository' autoload :Resource, 'cuprum/collections/resource' + autoload :Scope, 'cuprum/collections/scope' + autoload :Scopes, 'cuprum/collections/scopes' # @return [String] the absolute path to the gem directory. def self.gem_path diff --git a/lib/cuprum/collections/rspec/contracts.rb b/lib/cuprum/collections/rspec/contracts.rb index 8aff0ac..725c303 100644 --- a/lib/cuprum/collections/rspec/contracts.rb +++ b/lib/cuprum/collections/rspec/contracts.rb @@ -19,5 +19,7 @@ module Contracts 'cuprum/collections/rspec/contracts/relation_contracts' autoload :RepositoryContracts, 'cuprum/collections/rspec/contracts/repository_contracts' + autoload :Scopes, + 'cuprum/collections/rspec/contracts/scopes' end end diff --git a/lib/cuprum/collections/rspec/contracts/scope_contracts.rb b/lib/cuprum/collections/rspec/contracts/scope_contracts.rb new file mode 100644 index 0000000..20e551e --- /dev/null +++ b/lib/cuprum/collections/rspec/contracts/scope_contracts.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'cuprum/collections/rspec/contracts' + +module Cuprum::Collections::RSpec::Contracts + # Contracts for asserting on scope objects. + module ScopeContracts + # Contract validating the behavior of a Criteria scope implementation. + module ShouldBeACriteriaScopeContract + extend RSpec::SleepingKingStudios::Contract + + # @!method apply(example_group) + # Adds the contract to the example group. + # + # @param example_group [RSpec::Core::ExampleGroup] the example group to + # which the contract is applied. + contract do + shared_context 'with criteria' do + let(:criteria) do + [ + ['title', 'eq', 'Gideon the Ninth'], + ['author', 'eq', 'Tamsyn Muir'] + ] + end + end + + let(:criteria) { [] } + + describe '.new' do + it 'should define the constructor' do + expect(described_class) + .to be_constructible + .with(0).arguments + .and_keywords(:criteria) + .and_any_keywords + end + end + + describe '#criteria' do + include_examples 'should define reader', :criteria, -> { criteria } + + wrap_context 'with criteria' do + it { expect(scope.criteria).to be == criteria } + end + end + + describe '#with_criteria' do + let(:new_criteria) { ['author', 'eq', 'Ursula K. LeGuin'] } + + it { expect(scope).to respond_to(:with_criteria).with(1).argument } + + it 'should return a scope' do + expect(scope.with_criteria(new_criteria)).to be_a described_class + end + + it "should not change the original scope's criteria" do + expect { scope.with_criteria(new_criteria) } + .not_to change(scope, :criteria) + end + + it "should set the copied scope's criteria" do + expect(scope.with_criteria(new_criteria).criteria) + .to be == new_criteria + end + + wrap_context 'with criteria' do + it "should not change the original scope's criteria" do + expect { scope.with_criteria(new_criteria) } + .not_to change(scope, :criteria) + end + + it "should set the copied scope's criteria" do + expect(scope.with_criteria(new_criteria).criteria) + .to be == new_criteria + end + end + end + end + end + end +end diff --git a/lib/cuprum/collections/scope.rb b/lib/cuprum/collections/scope.rb new file mode 100644 index 0000000..bbfa5a0 --- /dev/null +++ b/lib/cuprum/collections/scope.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'cuprum/collections' + +module Cuprum::Collections + # Abstract class representing a set of filters for a query. + class Scope; end # rubocop:disable Lint/EmptyClass +end diff --git a/lib/cuprum/collections/scopes.rb b/lib/cuprum/collections/scopes.rb new file mode 100644 index 0000000..572623a --- /dev/null +++ b/lib/cuprum/collections/scopes.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'cuprum/collections' + +module Cuprum::Collections + # Namespace for scope functionality, which filters query data. + module Scopes + autoload :Criteria, 'cuprum/collections/scopes/criteria' + end +end diff --git a/lib/cuprum/collections/scopes/criteria.rb b/lib/cuprum/collections/scopes/criteria.rb new file mode 100644 index 0000000..0d3ed19 --- /dev/null +++ b/lib/cuprum/collections/scopes/criteria.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'cuprum/collections/scopes' + +module Cuprum::Collections::Scopes + # Functionality for implementing a criteria scope. + module Criteria + # @param criteria [Array] the criteria used for filtering query data. + def initialize(criteria:, **options) + super(**options) + + @criteria = criteria + end + + # @return [Array] the criteria used for filtering query data. + attr_reader :criteria + + # Creates a copy of the scope with the given criteria. + # + # @param criteria [Array] the criteria used for filtering query data. + # + # @return [Scope] the copied scope. + def with_criteria(criteria) + dup.tap { |copy| copy.criteria = criteria } + end + + protected + + attr_writer :criteria + end +end diff --git a/spec/cuprum/collections/scopes/criteria_spec.rb b/spec/cuprum/collections/scopes/criteria_spec.rb new file mode 100644 index 0000000..b52d00c --- /dev/null +++ b/spec/cuprum/collections/scopes/criteria_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'cuprum/collections/scope' +require 'cuprum/collections/scopes/criteria' +require 'cuprum/collections/rspec/contracts/scope_contracts' + +RSpec.describe Cuprum::Collections::Scopes::Criteria do + include Cuprum::Collections::RSpec::Contracts::ScopeContracts + + subject(:scope) { described_class.new(criteria: criteria) } + + let(:described_class) { Spec::ExampleScope } + + example_class 'Spec::ExampleScope', Cuprum::Collections::Scope do |klass| + klass.include Cuprum::Collections::Scopes::Criteria # rubocop:disable RSpec/DescribedClass + end + + include_contract 'should be a criteria scope' +end