From 84d3ed2e8cc84a7399d7e9daaf63cb484bc1234f Mon Sep 17 00:00:00 2001 From: Rob Smith Date: Mon, 27 Nov 2023 16:56:12 -0500 Subject: [PATCH 1/4] Refactor legacy contracts. --- .rubocop.yml | 14 +- DEVELOPMENT.md | 4 +- .../collections/rspec/collection_contract.rb | 423 ------- lib/cuprum/collections/rspec/contracts.rb | 8 + .../rspec/contracts/collection_contracts.rb | 429 +++++++ .../rspec/contracts/query_contracts.rb | 1082 +++++++++++++++++ .../rspec/contracts/repository_contracts.rb | 605 +++++++++ .../rspec/find_matching_command_contract.rb | 15 +- .../rspec/query_builder_contract.rb | 92 -- .../collections/rspec/query_contract.rb | 650 ---------- .../collections/rspec/querying_contract.rb | 298 ----- .../collections/rspec/repository_contract.rb | 598 --------- .../collections/basic/collection_spec.rb | 6 +- .../collections/basic/query_builder_spec.rb | 6 +- spec/cuprum/collections/basic/query_spec.rb | 6 +- .../collections/basic/repository_spec.rb | 6 +- spec/cuprum/collections/collection_spec.rb | 8 +- spec/cuprum/collections/query_builder_spec.rb | 6 +- spec/cuprum/collections/repository_spec.rb | 7 +- 19 files changed, 2166 insertions(+), 2097 deletions(-) delete mode 100644 lib/cuprum/collections/rspec/collection_contract.rb create mode 100644 lib/cuprum/collections/rspec/contracts/collection_contracts.rb create mode 100644 lib/cuprum/collections/rspec/contracts/query_contracts.rb create mode 100644 lib/cuprum/collections/rspec/contracts/repository_contracts.rb delete mode 100644 lib/cuprum/collections/rspec/query_builder_contract.rb delete mode 100644 lib/cuprum/collections/rspec/query_contract.rb delete mode 100644 lib/cuprum/collections/rspec/querying_contract.rb delete mode 100644 lib/cuprum/collections/rspec/repository_contract.rb diff --git a/.rubocop.yml b/.rubocop.yml index 10b9205..e46184a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,13 @@ require: - rubocop-rspec +AllCops: + TargetRubyVersion: 2.7 + NewCops: enable + Exclude: + - tmp/**/* + - vendor/**/* + RSpec: Language: ExampleGroups: @@ -28,13 +35,6 @@ RSpec: - xinclude_examples - xwrap_examples -AllCops: - TargetRubyVersion: 2.7 - NewCops: enable - Exclude: - - tmp/**/* - - vendor/**/* - Layout/ArgumentAlignment: EnforcedStyle: with_fixed_indentation diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index da41cb4..2133858 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -8,8 +8,8 @@ Steps to add an operator: - Update Queries::Operators. - Update Queries::ParseBlock::Builder. -- Define context(s) in RSpec::QUERYING_CONTEXTS. -- Add test cases in RSpec::QUERYING_CONTRACT. +- Define context(s) in QueryContracts::WithQueryContexts. +- Add test cases in QueryContracts::ShouldPerformQueries. - Add implementations to Basic::QueryBuilder. ### ParseCriteria diff --git a/lib/cuprum/collections/rspec/collection_contract.rb b/lib/cuprum/collections/rspec/collection_contract.rb deleted file mode 100644 index a158055..0000000 --- a/lib/cuprum/collections/rspec/collection_contract.rb +++ /dev/null @@ -1,423 +0,0 @@ -# frozen_string_literal: true - -require 'rspec/sleeping_king_studios/contract' - -require 'cuprum/collections/rspec' -require 'cuprum/collections/rspec/contracts/relation_contracts' - -module Cuprum::Collections::RSpec - # Contract validating the behavior of a Collection. - module CollectionContract - 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. - # @param options [Hash] additional options for the contract. - # - # @option options abstract [Boolean] if true, the collection is an - # abstract base class and does not define a query or commands. - # @option options default_entity_class [Class] the default entity class - # for the collection, if any. - - contract do |**options| - include Cuprum::Collections::RSpec::Contracts::RelationContracts - - shared_examples 'should define the command' \ - do |command_name, command_class_name = nil| - next if options[:abstract] - - tools = SleepingKingStudios::Tools::Toolbelt.instance - class_name = tools.str.camelize(command_name) - command_options = %i[ - collection_name - member_name - primary_key_name - primary_key_type - ] + options.fetch(:command_options, []).map(&:intern) - - describe "::#{class_name}" do - let(:constructor_options) { defined?(super()) ? super() : {} } - let(:command_class) do - command_class_name || - "#{options[:commands_namespace]}::#{class_name}" - .then { |str| Object.const_get(str) } - end - let(:command) do - collection.const_get(class_name).new(**constructor_options) - end - let(:expected_options) do - Hash - .new { |_, key| collection.send(key) } - .merge( - collection_name: collection.name, - member_name: collection.singular_name - ) - end - - it { expect(collection).to define_constant(class_name) } - - it { expect(collection.const_get(class_name)).to be_a Class } - - it { expect(collection.const_get(class_name)).to be < command_class } - - it { expect(command.options).to be >= {} } - - command_options.each do |option_name| - it "should set the ##{option_name}" do - expect(command.send(option_name)) - .to be == expected_options[option_name] - end - end - - describe 'with options' do - let(:constructor_options) do - super().merge( - custom_option: 'value', - singular_name: 'tome' - ) - end - - it { expect(command.options).to be >= { custom_option: 'value' } } - - command_options.each do |option_name| - it "should set the ##{option_name}" do - expect(command.send(option_name)).to( - be == expected_options[option_name] - ) - end - end - end - end - - describe "##{command_name}" do - let(:constructor_options) { defined?(super()) ? super() : {} } - let(:command) do - collection.send(command_name, **constructor_options) - end - let(:expected_options) do - Hash - .new { |_, key| collection.send(key) } - .merge( - collection_name: collection.name, - member_name: collection.singular_name - ) - end - - it 'should define the command' do - expect(collection) - .to respond_to(command_name) - .with(0).arguments - .and_any_keywords - end - - it { expect(command).to be_a collection.const_get(class_name) } - - command_options.each do |option_name| - it "should set the ##{option_name}" do - expect(command.send(option_name)) - .to be == expected_options[option_name] - end - end - - describe 'with options' do - let(:constructor_options) do - super().merge( - custom_option: 'value', - singular_name: 'tome' - ) - end - - it { expect(command.options).to be >= { custom_option: 'value' } } - - command_options.each do |option_name| - it "should set the ##{option_name}" do - expect(command.send(option_name)).to( - be == expected_options[option_name] - ) - end - end - end - end - end - - include_contract 'should be a relation', - constructor: false, - default_entity_class: options[:default_entity_class] - - include_contract 'should disambiguate parameter', - :name, - as: :collection_name - - include_contract 'should disambiguate parameter', - :singular_name, - as: :member_name - - include_contract 'should define primary keys' - - include_examples 'should define the command', :assign_one - - include_examples 'should define the command', :build_one - - include_examples 'should define the command', :destroy_one - - include_examples 'should define the command', :find_many - - include_examples 'should define the command', :find_matching - - include_examples 'should define the command', :find_one - - include_examples 'should define the command', :insert_one - - include_examples 'should define the command', :update_one - - include_examples 'should define the command', :validate_one - - describe '#==' do - let(:other_options) { { name: name } } - let(:other_collection) { described_class.new(**other_options) } - - describe 'with nil' do - it { expect(collection == nil).to be false } # rubocop:disable Style/NilComparison - end - - describe 'with an object' do - it { expect(collection == Object.new.freeze).to be false } - end - - describe 'with a collection with non-matching properties' do - let(:other_options) { super().merge(custom_option: 'value') } - - it { expect(collection == other_collection).to be false } - end - - describe 'with a collection with matching properties' do - it { expect(collection == other_collection).to be true } - end - - describe 'with another type of collection' do - let(:other_collection) do - Spec::OtherCollection.new(**other_options) - end - - example_class 'Spec::OtherCollection', Cuprum::Collections::Collection - - it { expect(collection == other_collection).to be false } - end - - context 'when initialized with options' do - let(:constructor_options) do - super().merge( - qualified_name: 'spec/scoped_books', - singular_name: 'grimoire' - ) - end - - describe 'with a collection with non-matching properties' do - it { expect(collection == other_collection).to be false } - end - - describe 'with a collection with matching properties' do - let(:other_options) do - super().merge( - qualified_name: 'spec/scoped_books', - singular_name: 'grimoire' - ) - end - - it { expect(collection == other_collection).to be true } - end - end - end - - describe '#count' do - it { expect(collection).to respond_to(:count).with(0).arguments } - - it { expect(collection).to have_aliased_method(:count).as(:size) } - - next if options[:abstract] - - it { expect(collection.count).to be 0 } - - wrap_context 'when the collection has many items' do - it { expect(collection.count).to be items.count } - end - end - - describe '#matches?' do - def tools - SleepingKingStudios::Tools::Toolbelt.instance - end - - it 'should define the method' do - expect(collection) - .to respond_to(:matches?) - .with(0).arguments - .and_any_keywords - end - - describe 'with no options' do - it { expect(collection.matches?).to be true } - end - - describe 'with non-matching entity class as a Class' do - let(:other_options) { { entity_class: Grimoire } } - - it { expect(collection.matches?(**other_options)).to be false } - end - - describe 'with non-matching entity class as a String' do - let(:other_options) { { entity_class: 'Grimoire' } } - - it { expect(collection.matches?(**other_options)).to be false } - end - - describe 'with non-matching name' do - it { expect(collection.matches?(name: 'grimoires')).to be false } - end - - describe 'with non-matching primary key name' do - let(:other_options) { { primary_key_name: 'uuid' } } - - it { expect(collection.matches?(**other_options)).to be false } - end - - describe 'with non-matching primary key type' do - let(:other_options) { { primary_key_type: String } } - - it { expect(collection.matches?(**other_options)).to be false } - end - - describe 'with non-matching qualified name' do - let(:other_options) { { qualified_name: 'spec/scoped_books' } } - - it { expect(collection.matches?(**other_options)).to be false } - end - - describe 'with non-matching singular name' do - let(:other_options) { { singular_name: 'grimoire' } } - - it { expect(collection.matches?(**other_options)).to be false } - end - - describe 'with non-matching custom options' do - let(:other_options) { { custom_option: 'custom value' } } - - it { expect(collection.matches?(**other_options)).to be false } - end - - describe 'with partially-matching options' do - let(:other_options) do - { - name: name, - singular_name: 'grimoire' - } - end - - it { expect(collection.matches?(**other_options)).to be false } - end - - describe 'with matching entity class as a Class' do - let(:configured_entity_class) do - options.fetch(:default_entity_class, Book) - end - let(:other_options) { { entity_class: configured_entity_class } } - - it { expect(collection.matches?(**other_options)).to be true } - end - - describe 'with matching entity class as a String' do - let(:configured_entity_class) do - options.fetch(:default_entity_class, Book) - end - let(:other_options) { { entity_class: configured_entity_class.to_s } } - - it { expect(collection.matches?(**other_options)).to be true } - end - - describe 'with matching name' do - let(:other_options) { { collection_name: name } } - - it { expect(collection.matches?(**other_options)).to be true } - end - - describe 'with matching primary key name' do - let(:other_options) { { primary_key_name: 'id' } } - - it { expect(collection.matches?(**other_options)).to be true } - end - - describe 'with matching primary key type' do - let(:other_options) { { primary_key_type: Integer } } - - it { expect(collection.matches?(**other_options)).to be true } - end - - describe 'with matching qualified name' do - let(:other_options) { { qualified_name: name } } - - it { expect(collection.matches?(**other_options)).to be true } - end - - describe 'with matching singular name' do - let(:other_options) do - { singular_name: tools.str.singularize(name) } - end - - it { expect(collection.matches?(**other_options)).to be true } - end - - describe 'with multiple matching options' do - let(:other_options) do - { - collection_name: name, - primary_key_name: 'id', - qualified_name: name - } - end - - it { expect(collection.matches?(**other_options)).to be true } - end - end - - describe '#query' do - let(:error_message) do - "#{described_class.name} is an abstract class. Define a repository " \ - 'subclass and implement the #query method.' - end - let(:default_order) { defined?(super()) ? super() : {} } - let(:query) { collection.query } - - it { expect(collection).to respond_to(:query).with(0).arguments } - - if options[:abstract] - it 'should raise an exception' do - expect { collection.query } - .to raise_error( - described_class::AbstractCollectionError, - error_message - ) - end - else - it { expect(collection.query).to be_a query_class } - - it 'should set the query options' do - query_options.each do |option, value| - expect(collection.query.send option).to be == value - end - end - - it { expect(query.criteria).to be == [] } - - it { expect(query.limit).to be nil } - - it { expect(query.offset).to be nil } - - it { expect(query.order).to be == default_order } - end - end - end - end -end diff --git a/lib/cuprum/collections/rspec/contracts.rb b/lib/cuprum/collections/rspec/contracts.rb index ee7265b..cbdf5ac 100644 --- a/lib/cuprum/collections/rspec/contracts.rb +++ b/lib/cuprum/collections/rspec/contracts.rb @@ -5,7 +5,15 @@ module Cuprum::Collections::RSpec # Namespace for RSpec contract objects. module Contracts + autoload :AssociationContracts, + 'cuprum/collections/rspec/contracts/association_contracts' + autoload :CollectionContracts, + 'cuprum/collections/rspec/contracts/collection_contracts' + autoload :QueryContracts, + 'cuprum/collections/rspec/contracts/query_contracts' autoload :RelationContracts, 'cuprum/collections/rspec/contracts/relation_contracts' + autoload :RepositoryContracts, + 'cuprum/collections/rspec/contracts/repository_contracts' end end diff --git a/lib/cuprum/collections/rspec/contracts/collection_contracts.rb b/lib/cuprum/collections/rspec/contracts/collection_contracts.rb new file mode 100644 index 0000000..3cf9dc6 --- /dev/null +++ b/lib/cuprum/collections/rspec/contracts/collection_contracts.rb @@ -0,0 +1,429 @@ +# frozen_string_literal: true + +require 'cuprum/collections/rspec/contracts' +require 'cuprum/collections/rspec/contracts/relation_contracts' + +module Cuprum::Collections::RSpec::Contracts + # Contracts for asserting on Collection objects. + module CollectionContracts + include Cuprum::Collections::RSpec::Contracts::RelationContracts + + # Contract validating the behavior of a Collection. + module ShouldBeACollectionContract + 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. + # @param options [Hash] additional options for the contract. + # + # @option options abstract [Boolean] if true, the collection is an + # abstract base class and does not define a query or commands. + # @option options default_entity_class [Class] the default entity class + # for the collection, if any. + + contract do |**options| + shared_examples 'should define the command' \ + do |command_name, command_class_name = nil| + next if options[:abstract] + + tools = SleepingKingStudios::Tools::Toolbelt.instance + class_name = tools.str.camelize(command_name) + command_options = %i[ + collection_name + member_name + primary_key_name + primary_key_type + ] + options.fetch(:command_options, []).map(&:intern) + + describe "::#{class_name}" do + let(:constructor_options) { defined?(super()) ? super() : {} } + let(:command_class) do + command_class_name || + "#{options[:commands_namespace]}::#{class_name}" + .then { |str| Object.const_get(str) } + end + let(:command) do + collection.const_get(class_name).new(**constructor_options) + end + let(:expected_options) do + Hash + .new { |_, key| collection.send(key) } + .merge( + collection_name: collection.name, + member_name: collection.singular_name + ) + end + + it { expect(collection).to define_constant(class_name) } + + it { expect(collection.const_get(class_name)).to be_a Class } + + it 'should be an instance of the command class' do + expect(collection.const_get(class_name)).to be < command_class + end + + it { expect(command.options).to be >= {} } + + command_options.each do |option_name| + it "should set the ##{option_name}" do + expect(command.send(option_name)) + .to be == expected_options[option_name] + end + end + + describe 'with options' do + let(:constructor_options) do + super().merge( + custom_option: 'value', + singular_name: 'tome' + ) + end + + it { expect(command.options).to be >= { custom_option: 'value' } } + + command_options.each do |option_name| + it "should set the ##{option_name}" do + expect(command.send(option_name)).to( + be == expected_options[option_name] + ) + end + end + end + end + + describe "##{command_name}" do + let(:constructor_options) { defined?(super()) ? super() : {} } + let(:command) do + collection.send(command_name, **constructor_options) + end + let(:expected_options) do + Hash + .new { |_, key| collection.send(key) } + .merge( + collection_name: collection.name, + member_name: collection.singular_name + ) + end + + it 'should define the command' do + expect(collection) + .to respond_to(command_name) + .with(0).arguments + .and_any_keywords + end + + it { expect(command).to be_a collection.const_get(class_name) } + + command_options.each do |option_name| + it "should set the ##{option_name}" do + expect(command.send(option_name)) + .to be == expected_options[option_name] + end + end + + describe 'with options' do + let(:constructor_options) do + super().merge( + custom_option: 'value', + singular_name: 'tome' + ) + end + + it { expect(command.options).to be >= { custom_option: 'value' } } + + command_options.each do |option_name| + it "should set the ##{option_name}" do + expect(command.send(option_name)).to( + be == expected_options[option_name] + ) + end + end + end + end + end + + include_contract 'should be a relation', + constructor: false, + default_entity_class: options[:default_entity_class] + + include_contract 'should disambiguate parameter', + :name, + as: :collection_name + + include_contract 'should disambiguate parameter', + :singular_name, + as: :member_name + + include_contract 'should define primary keys' + + include_examples 'should define the command', :assign_one + + include_examples 'should define the command', :build_one + + include_examples 'should define the command', :destroy_one + + include_examples 'should define the command', :find_many + + include_examples 'should define the command', :find_matching + + include_examples 'should define the command', :find_one + + include_examples 'should define the command', :insert_one + + include_examples 'should define the command', :update_one + + include_examples 'should define the command', :validate_one + + describe '#==' do + let(:other_options) { { name: name } } + let(:other_collection) { described_class.new(**other_options) } + + describe 'with nil' do + it { expect(collection == nil).to be false } # rubocop:disable Style/NilComparison + end + + describe 'with an object' do + it { expect(collection == Object.new.freeze).to be false } + end + + describe 'with a collection with non-matching properties' do + let(:other_options) { super().merge(custom_option: 'value') } + + it { expect(collection == other_collection).to be false } + end + + describe 'with a collection with matching properties' do + it { expect(collection == other_collection).to be true } + end + + describe 'with another type of collection' do + let(:other_collection) do + Spec::OtherCollection.new(**other_options) + end + + example_class 'Spec::OtherCollection', + Cuprum::Collections::Collection + + it { expect(collection == other_collection).to be false } + end + + context 'when initialized with options' do + let(:constructor_options) do + super().merge( + qualified_name: 'spec/scoped_books', + singular_name: 'grimoire' + ) + end + + describe 'with a collection with non-matching properties' do + it { expect(collection == other_collection).to be false } + end + + describe 'with a collection with matching properties' do + let(:other_options) do + super().merge( + qualified_name: 'spec/scoped_books', + singular_name: 'grimoire' + ) + end + + it { expect(collection == other_collection).to be true } + end + end + end + + describe '#count' do + it { expect(collection).to respond_to(:count).with(0).arguments } + + it { expect(collection).to have_aliased_method(:count).as(:size) } + + next if options[:abstract] + + it { expect(collection.count).to be 0 } + + wrap_context 'when the collection has many items' do + it { expect(collection.count).to be items.count } + end + end + + describe '#matches?' do + def tools + SleepingKingStudios::Tools::Toolbelt.instance + end + + it 'should define the method' do + expect(collection) + .to respond_to(:matches?) + .with(0).arguments + .and_any_keywords + end + + describe 'with no options' do + it { expect(collection.matches?).to be true } + end + + describe 'with non-matching entity class as a Class' do + let(:other_options) { { entity_class: Grimoire } } + + it { expect(collection.matches?(**other_options)).to be false } + end + + describe 'with non-matching entity class as a String' do + let(:other_options) { { entity_class: 'Grimoire' } } + + it { expect(collection.matches?(**other_options)).to be false } + end + + describe 'with non-matching name' do + it { expect(collection.matches?(name: 'grimoires')).to be false } + end + + describe 'with non-matching primary key name' do + let(:other_options) { { primary_key_name: 'uuid' } } + + it { expect(collection.matches?(**other_options)).to be false } + end + + describe 'with non-matching primary key type' do + let(:other_options) { { primary_key_type: String } } + + it { expect(collection.matches?(**other_options)).to be false } + end + + describe 'with non-matching qualified name' do + let(:other_options) { { qualified_name: 'spec/scoped_books' } } + + it { expect(collection.matches?(**other_options)).to be false } + end + + describe 'with non-matching singular name' do + let(:other_options) { { singular_name: 'grimoire' } } + + it { expect(collection.matches?(**other_options)).to be false } + end + + describe 'with non-matching custom options' do + let(:other_options) { { custom_option: 'custom value' } } + + it { expect(collection.matches?(**other_options)).to be false } + end + + describe 'with partially-matching options' do + let(:other_options) do + { + name: name, + singular_name: 'grimoire' + } + end + + it { expect(collection.matches?(**other_options)).to be false } + end + + describe 'with matching entity class as a Class' do + let(:configured_entity_class) do + options.fetch(:default_entity_class, Book) + end + let(:other_options) { { entity_class: configured_entity_class } } + + it { expect(collection.matches?(**other_options)).to be true } + end + + describe 'with matching entity class as a String' do + let(:configured_entity_class) do + options.fetch(:default_entity_class, Book) + end + let(:other_options) do + { entity_class: configured_entity_class.to_s } + end + + it { expect(collection.matches?(**other_options)).to be true } + end + + describe 'with matching name' do + let(:other_options) { { collection_name: name } } + + it { expect(collection.matches?(**other_options)).to be true } + end + + describe 'with matching primary key name' do + let(:other_options) { { primary_key_name: 'id' } } + + it { expect(collection.matches?(**other_options)).to be true } + end + + describe 'with matching primary key type' do + let(:other_options) { { primary_key_type: Integer } } + + it { expect(collection.matches?(**other_options)).to be true } + end + + describe 'with matching qualified name' do + let(:other_options) { { qualified_name: name } } + + it { expect(collection.matches?(**other_options)).to be true } + end + + describe 'with matching singular name' do + let(:other_options) do + { singular_name: tools.str.singularize(name) } + end + + it { expect(collection.matches?(**other_options)).to be true } + end + + describe 'with multiple matching options' do + let(:other_options) do + { + collection_name: name, + primary_key_name: 'id', + qualified_name: name + } + end + + it { expect(collection.matches?(**other_options)).to be true } + end + end + + describe '#query' do + let(:error_message) do + "#{described_class.name} is an abstract class. Define a " \ + 'repository subclass and implement the #query method.' + end + let(:default_order) { defined?(super()) ? super() : {} } + let(:query) { collection.query } + + it { expect(collection).to respond_to(:query).with(0).arguments } + + if options[:abstract] + it 'should raise an exception' do + expect { collection.query } + .to raise_error( + described_class::AbstractCollectionError, + error_message + ) + end + else + it { expect(collection.query).to be_a query_class } + + it 'should set the query options' do + query_options.each do |option, value| + expect(collection.query.send(option)).to be == value + end + end + + it { expect(query.criteria).to be == [] } + + it { expect(query.limit).to be nil } + + it { expect(query.offset).to be nil } + + it { expect(query.order).to be == default_order } + end + end + end + end + end +end diff --git a/lib/cuprum/collections/rspec/contracts/query_contracts.rb b/lib/cuprum/collections/rspec/contracts/query_contracts.rb new file mode 100644 index 0000000..758cefe --- /dev/null +++ b/lib/cuprum/collections/rspec/contracts/query_contracts.rb @@ -0,0 +1,1082 @@ +# frozen_string_literal: true + +require 'cuprum/collections/rspec/contracts' +require 'cuprum/collections/rspec/fixtures' + +module Cuprum::Collections::RSpec::Contracts + # Contracts for asserting on Query objects. + module QueryContracts + # Contract validating the behavior of a Query implementation. + module ShouldBeAQuery + extend RSpec::SleepingKingStudios::Contract + + BOOKS_FIXTURES = Cuprum::Collections::RSpec::BOOKS_FIXTURES + private_constant :BOOKS_FIXTURES + + OPERATORS = Cuprum::Collections::Queries::Operators + private_constant :OPERATORS + + # @!method apply(example_group) + # Adds the contract to the example group. + # + # @param operators [Array] the expected operators. + contract do |operators: OPERATORS.values| + include Cuprum::Collections::RSpec::Contracts::QueryContracts + + operators = Set.new(operators.map(&:to_sym)) + + include_contract 'with query contexts' + + shared_context 'when the query has composed filters' do + let(:scoped_query) do + super() + .where { { author: 'Ursula K. LeGuin' } } + .where { { series: not_equal('Earthsea') } } + end + let(:matching_data) do + super() + .select { |item| item['author'] == 'Ursula K. LeGuin' } + .reject { |item| item['series'] == 'Earthsea' } + end + end + + let(:scoped_query) do + # :nocov: + scoped = + if filter.is_a?(Proc) + query.where(&filter) + elsif !filter.nil? + query.where(filter) + else + query + end + # :nocov: + scoped = scoped.limit(limit) if limit + scoped = scoped.offset(offset) if offset + scoped = scoped.order(order) if order + + scoped + end + + it 'should be enumerable' do + expect(described_class).to be < Enumerable + end + + describe '#count' do + let(:data) { [] } + let(:matching_data) { data } + let(:expected_data) do + defined?(super()) ? super() : matching_data + end + + it { expect(query).to respond_to(:count).with(0).arguments } + + it { expect(query.count).to be == expected_data.count } + + wrap_context 'when the query has composed filters' do + it { expect(scoped_query.count).to be == expected_data.count } + end + + context 'when the collection data changes' do + let(:item) { BOOKS_FIXTURES.first } + + before(:example) do + query.count # Cache query results. + + add_item_to_collection(item) + end + + it { expect(query.count).to be == expected_data.count } + end + + context 'when the collection has many items' do + let(:data) { BOOKS_FIXTURES } + + it { expect(query.count).to be == expected_data.count } + + wrap_context 'when the query has composed filters' do + it { expect(scoped_query.count).to be == expected_data.count } + end + + context 'when the collection data changes' do + let(:data) { BOOKS_FIXTURES[0...-1] } + let(:item) { BOOKS_FIXTURES.last } + + before(:example) do + query.count # Cache query results. + + add_item_to_collection(item) + end + + it { expect(query.count).to be == expected_data.count } + end + end + end + + describe '#criteria' do + include_examples 'should have reader', :criteria, [] + + wrap_context 'when the query has where: a simple block filter' do + let(:expected) { [['author', :equal, 'Ursula K. LeGuin']] } + + it { expect(scoped_query.criteria).to be == expected } + end + + wrap_context 'when the query has where: a complex block filter' do + let(:expected) do + [ + ['author', :equal, 'Ursula K. LeGuin'], + ['series', :not_equal, 'Earthsea'] + ] + end + + if operators.include?(OPERATORS::EQUAL) && + operators.include?(OPERATORS::NOT_EQUAL) + it { expect(scoped_query.criteria).to be == expected } + else + # :nocov: + pending + # :nocov: + end + end + + wrap_context 'when the query has composed filters' do + let(:expected) do + [ + ['author', :equal, 'Ursula K. LeGuin'], + ['series', :not_equal, 'Earthsea'] + ] + end + + it { expect(scoped_query.criteria).to be == expected } + end + + wrap_context 'when the query has where: an equal block filter' do + let(:expected) { [['author', :equal, 'Ursula K. LeGuin']] } + + if operators.include?(OPERATORS::EQUAL) + it { expect(scoped_query.criteria).to be == expected } + else + # :nocov: + pending + # :nocov: + end + end + + wrap_context 'when the query has where: a not_equal block filter' do + let(:expected) { [['author', :not_equal, 'Ursula K. LeGuin']] } + + if operators.include?(OPERATORS::NOT_EQUAL) + it { expect(scoped_query.criteria).to be == expected } + else + # :nocov: + pending + # :nocov: + end + end + end + + describe '#each' do + shared_examples 'should enumerate the matching data' do + describe 'with no arguments' do + it { expect(scoped_query.each).to be_a Enumerator } + + it { expect(scoped_query.each.count).to be == matching_data.size } + + it { expect(scoped_query.each.to_a).to deep_match expected_data } + end + + describe 'with a block' do + it 'should yield each matching item' do + expect { |block| scoped_query.each(&block) } + .to yield_successive_args(*expected_data) + end + end + end + + let(:data) { [] } + let(:matching_data) { data } + let(:expected_data) do + defined?(super()) ? super() : matching_data + end + + it { expect(query).to respond_to(:each).with(0).arguments } + + include_examples 'should enumerate the matching data' + + include_contract 'should perform queries', + block: lambda { + include_examples 'should enumerate the matching data' + }, + operators: operators + + wrap_context 'when the query has composed filters' do + include_examples 'should enumerate the matching data' + end + + context 'when the collection data changes' do + let(:item) { BOOKS_FIXTURES.first } + + before(:example) do + query.each {} # Cache query results. + + add_item_to_collection(item) + end + + include_examples 'should enumerate the matching data' + end + + context 'when the collection has many items' do + let(:data) { BOOKS_FIXTURES } + + include_examples 'should enumerate the matching data' + + include_contract 'should perform queries', + block: lambda { + include_examples 'should enumerate the matching data' + }, + operators: operators + + wrap_context 'when the query has composed filters' do + include_examples 'should enumerate the matching data' + end + + context 'when the collection data changes' do + let(:data) { BOOKS_FIXTURES[0...-1] } + let(:item) { BOOKS_FIXTURES.last } + + before(:example) do + query.each {} # Cache query results. + + add_item_to_collection(item) + end + + include_examples 'should enumerate the matching data' + end + end + end + + describe '#exists?' do + shared_examples 'should check the existence of matching data' do + it { expect(query.exists?).to be == !matching_data.empty? } + end + + let(:data) { [] } + let(:matching_data) { data } + + include_examples 'should define predicate', :exists? + + include_examples 'should check the existence of matching data' + + include_contract 'should perform queries', + block: lambda { + include_examples 'should check the existence of matching data' + }, + operators: operators + + wrap_context 'when the query has composed filters' do + include_examples 'should check the existence of matching data' + end + + context 'when the collection has many items' do + let(:data) { BOOKS_FIXTURES } + + include_examples 'should check the existence of matching data' + + include_contract 'should perform queries', + block: lambda { + include_examples 'should check the existence of matching data' + }, + operators: operators + + wrap_context 'when the query has composed filters' do + include_examples 'should check the existence of matching data' + end + end + end + + describe '#limit' do + it { expect(query).to respond_to(:limit).with(0..1).arguments } + + describe 'with no arguments' do + it { expect(query.limit).to be nil } + end + + describe 'with nil' do + let(:error_message) { 'limit must be a non-negative integer' } + + it 'should raise an exception' do + expect { query.limit nil } + .to raise_error ArgumentError, error_message + end + end + + describe 'with an object' do + let(:error_message) { 'limit must be a non-negative integer' } + + it 'should raise an exception' do + expect { query.limit Object.new.freeze } + .to raise_error ArgumentError, error_message + end + end + + describe 'with a negative integer' do + let(:error_message) { 'limit must be a non-negative integer' } + + it 'should raise an exception' do + expect { query.limit(-1) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with zero' do + it { expect(query.limit(0)).to be_a described_class } + + it { expect(query.limit(0)).not_to be query } + + it { expect(query.limit(0).limit).to be 0 } + end + + describe 'with a positive integer' do + it { expect(query.limit(3)).to be_a described_class } + + it { expect(query.limit(3)).not_to be query } + + it { expect(query.limit(3).limit).to be 3 } + end + end + + describe '#offset' do + it { expect(query).to respond_to(:offset).with(0..1).argument } + + describe 'with no arguments' do + it { expect(query.offset).to be nil } + end + + describe 'with nil' do + let(:error_message) { 'offset must be a non-negative integer' } + + it 'should raise an exception' do + expect { query.offset nil } + .to raise_error ArgumentError, error_message + end + end + + describe 'with an object' do + let(:error_message) { 'offset must be a non-negative integer' } + + it 'should raise an exception' do + expect { query.offset Object.new.freeze } + .to raise_error ArgumentError, error_message + end + end + + describe 'with a negative integer' do + let(:error_message) { 'offset must be a non-negative integer' } + + it 'should raise an exception' do + expect { query.offset(-1) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with zero' do + it { expect(query.offset(0)).to be_a described_class } + + it { expect(query.offset(0)).not_to be query } + + it { expect(query.offset(0).offset).to be 0 } + end + + describe 'with a positive integer' do + it { expect(query.offset(3)).to be_a described_class } + + it { expect(query.offset(3)).not_to be query } + + it { expect(query.offset(3).offset).to be 3 } + end + end + + describe '#order' do + let(:default_order) { defined?(super()) ? super() : {} } + let(:error_message) do + 'order must be a list of attribute names and/or a hash of ' \ + 'attribute names with values :asc or :desc' + end + + it 'should define the method' do + expect(query) + .to respond_to(:order) + .with(0).arguments + .and_unlimited_arguments + end + + it { expect(query).to have_aliased_method(:order).as(:order_by) } + + describe 'with no arguments' do + it { expect(query.order).to be == default_order } + end + + describe 'with a hash with invalid keys' do + it 'should raise an exception' do + expect { query.order({ nil => :asc }) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with a hash with empty string keys' do + it 'should raise an exception' do + expect { query.order({ '' => :asc }) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with a hash with empty symbol keys' do + it 'should raise an exception' do + expect { query.order({ '': :asc }) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with a hash with nil value' do + it 'should raise an exception' do + expect { query.order({ title: nil }) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with a hash with object value' do + it 'should raise an exception' do + expect { query.order({ title: Object.new.freeze }) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with a hash with empty value' do + it 'should raise an exception' do + expect { query.order({ title: '' }) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with a hash with invalid value' do + it 'should raise an exception' do + expect { query.order({ title: 'wibbly' }) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with a valid ordering' do + let(:expected) do + { title: :asc } + end + + it { expect(query.order(:title)).to be_a described_class } + + it { expect(query.order(:title)).not_to be query } + + it { expect(query.order(:title).order).to be == expected } + end + end + + describe '#reset' do + let(:data) { [] } + let(:matching_data) { data } + let(:expected_data) do + defined?(super()) ? super() : matching_data + end + + it { expect(query).to respond_to(:reset).with(0).arguments } + + it { expect(query.reset).to be_a query.class } + + it { expect(query.reset).not_to be query } + + it { expect(query.reset.to_a).to be == query.to_a } + + context 'when the collection data changes' do + let(:item) { BOOKS_FIXTURES.first } + let(:matching_data) { [item] } + + before(:example) do + query.to_a # Cache query results. + + add_item_to_collection(item) + end + + it { expect(query.reset.count).to be expected_data.size } + + it { expect(query.reset.to_a).to deep_match expected_data } + end + + context 'when the collection has many items' do + let(:data) { BOOKS_FIXTURES } + + it { expect(query.reset).to be_a query.class } + + it { expect(query.reset).not_to be query } + + it { expect(query.reset.to_a).to be == query.to_a } + + context 'when the collection data changes' do + let(:data) { BOOKS_FIXTURES[0...-1] } + let(:item) { BOOKS_FIXTURES.last } + let(:matching_data) { [*data, item] } + + before(:example) do + query.to_a # Cache query results. + + add_item_to_collection(item) + end + + it { expect(query.reset.count).to be expected_data.size } + + it { expect(query.reset.to_a).to deep_match expected_data } + end + end + end + + describe '#to_a' do + let(:data) { [] } + let(:matching_data) { data } + let(:expected_data) do + defined?(super()) ? super() : matching_data + end + + it { expect(query).to respond_to(:to_a).with(0).arguments } + + it { expect(query.to_a).to deep_match expected_data } + + include_contract 'should perform queries', + block: lambda { + it { expect(scoped_query.to_a).to deep_match expected_data } + }, + operators: operators + + wrap_context 'when the query has composed filters' do + it { expect(scoped_query.to_a).to deep_match expected_data } + end + + context 'when the collection data changes' do + let(:item) { BOOKS_FIXTURES.first } + + before(:example) do + query.to_a # Cache query results. + + add_item_to_collection(item) + end + + it { expect(query.to_a).to deep_match expected_data } + end + + context 'when the collection has many items' do + let(:data) { BOOKS_FIXTURES } + + it { expect(query.to_a).to deep_match expected_data } + + include_contract 'should perform queries', + block: lambda { + it { expect(scoped_query.to_a).to deep_match expected_data } + }, + operators: operators + + wrap_context 'when the query has composed filters' do + it { expect(scoped_query.to_a).to deep_match expected_data } + end + + context 'when the collection data changes' do + let(:data) { BOOKS_FIXTURES[0...-1] } + let(:item) { BOOKS_FIXTURES.last } + + before(:example) do + query.to_a # Cache query results. + + add_item_to_collection(item) + end + + it { expect(query.to_a).to deep_match expected_data } + end + end + end + + describe '#where' do + let(:block) { -> { { title: 'The Caves of Steel' } } } + + it 'should define the method' do + expect(query) + .to respond_to(:where) + .with(0..1).arguments + .and_keywords(:strategy) + .and_a_block + end + + describe 'with no arguments' do + it { expect(query.where).to be_a described_class } + + it { expect(query.where).not_to be query } + end + + describe 'with a block' do + it { expect(query.where(&block)).to be_a described_class } + + it { expect(query.where(&block)).not_to be query } + end + + describe 'with a valid strategy' do + it 'should return a query instance' do + expect(query.where(strategy: :block, &block)) + .to be_a described_class + end + + it { expect(query.where(strategy: :block, &block)).not_to be query } + end + + describe 'with parameters that do not match a strategy' do + let(:error_class) do + Cuprum::Collections::QueryBuilder::ParseError + end + let(:error_message) { 'unable to parse query with strategy nil' } + + it 'should raise an exception' do + expect { query.where(%w[ichi ni san]) } + .to raise_error error_class, error_message + end + end + + describe 'with an invalid strategy' do + let(:error_class) do + Cuprum::Collections::QueryBuilder::ParseError + end + let(:error_message) do + 'unable to parse query with strategy :random' + end + + it 'should raise an exception' do + expect { query.where(strategy: :random) } + .to raise_error error_class, error_message + end + end + + describe 'with invalid parameters for a strategy' do + let(:error_class) do + Cuprum::Collections::QueryBuilder::ParseError + end + let(:error_message) { 'unable to parse query with strategy :block' } + + it 'should raise an exception' do + expect { query.where(strategy: :block) } + .to raise_error error_class, error_message + end + end + end + end + end + + # Contract validating the behavior of a QueryBuilder implementation. + module ShouldBeAQueryBuilderContract + extend RSpec::SleepingKingStudios::Contract + + # @!method apply(example_group) + # Adds the contract to the example group. + contract do + describe '#base_query' do + include_examples 'should define reader', + :base_query, + -> { base_query } + end + + describe '#call' do + let(:criteria) { [['title', :equal, 'The Naked Sun']] } + let(:expected) { criteria } + let(:filter) { { title: 'The Naked Sun' } } + let(:strategy) { :custom } + let(:parser) do + instance_double( + Cuprum::Collections::Queries::Parse, + call: Cuprum::Result.new(value: criteria) + ) + end + let(:query) do + builder.call(strategy: strategy, where: filter) + end + + before(:example) do + allow(Cuprum::Collections::Queries::Parse) + .to receive(:new) + .and_return(parser) + end + + it 'should define the method' do + expect(builder).to respond_to(:call) + .with(0).arguments + .and_keywords(:strategy, :where) + end + + it 'should parse the criteria' do + builder.call(strategy: strategy, where: filter) + + expect(parser) + .to have_received(:call) + .with(strategy: strategy, where: filter) + end + + it { expect(query).to be_a base_query.class } + + it { expect(query).not_to be base_query } + + it { expect(query.criteria).to be == expected } + + describe 'with strategy: :unsafe' do + let(:strategy) { :unsafe } + let(:filter) { criteria } + + it 'should not parse the criteria' do + builder.call(strategy: strategy, where: filter) + + expect(parser).not_to have_received(:call) + end + + it { expect(query.criteria).to be == expected } + end + + context 'when the query has existing criteria' do + let(:old_criteria) { [['genre', :eq, 'Science Fiction']] } + let(:expected) { old_criteria + criteria } + let(:base_query) { super().send(:with_criteria, old_criteria) } + + it { expect(query.criteria).to be == expected } + end + + context 'when the parser is unable to parse the query' do + let(:error) { Cuprum::Error.new(message: 'Something went wrong.') } + let(:result) { Cuprum::Result.new(error: error) } + + before(:example) do + allow(parser).to receive(:call).and_return(result) + end + + it 'should raise an exception' do + expect do + builder.call(strategy: strategy, where: filter) + end + .to raise_error Cuprum::Collections::QueryBuilder::ParseError, + error.message + end + end + end + end + end + + # Contract validating the behavior when performing queries. + module ShouldPerformQueriesContract + extend RSpec::SleepingKingStudios::Contract + + OPERATORS = Cuprum::Collections::Queries::Operators + private_constant :OPERATORS + + # @!method apply(example_group) + # Adds the contract to the example group. + # + # @param block [Proc] the expectations for each query context. + # @param operators [Array] the expected operators. + contract do |block:, operators: OPERATORS.values| + operators = Set.new(operators.map(&:to_sym)) + + wrap_context 'when the query has limit: value' do + instance_exec(&block) + end + + wrap_context 'when the query has offset: value' do + instance_exec(&block) + end + + wrap_context 'when the query has order: a simple ordering' do + instance_exec(&block) + end + + wrap_context 'when the query has order: a complex ordering' do + instance_exec(&block) + end + + context 'when the query has where: a block filter' do + context 'with a simple filter' do + include_context 'when the query has where: a simple block filter' + + instance_exec(&block) + end + + context 'with a complex filter' do + include_context 'when the query has where: a complex block filter' + + if operators.include?(OPERATORS::EQUAL) && + operators.include?(OPERATORS::NOT_EQUAL) + instance_exec(&block) + else + # :nocov: + pending + # :nocov: + end + end + + context 'with an equals filter' do + include_context 'when the query has where: an equal block filter' + + if operators.include?(OPERATORS::EQUAL) + instance_exec(&block) + else + # :nocov: + pending + # :nocov: + end + end + + context 'with a greater_than filter' do + include_context 'when the query has where: a greater_than filter' + + if operators.include?(OPERATORS::GREATER_THAN) + instance_exec(&block) + else + # :nocov: + pending + # :nocov: + end + end + + context 'with a greater_than_or_equal_to filter' do + include_context \ + 'when the query has where: a greater_than_or_equal_to filter' + + if operators.include?(OPERATORS::GREATER_THAN_OR_EQUAL_TO) + instance_exec(&block) + else + # :nocov: + pending + # :nocov: + end + end + + context 'with a less_than filter' do + include_context 'when the query has where: a less_than filter' + + if operators.include?(OPERATORS::LESS_THAN) + instance_exec(&block) + else + # :nocov: + pending + # :nocov: + end + end + + context 'with a less_than_or_equal_to filter' do + include_context \ + 'when the query has where: a less_than_or_equal_to filter' + + if operators.include?(OPERATORS::LESS_THAN_OR_EQUAL_TO) + instance_exec(&block) + else + # :nocov: + pending + # :nocov: + end + end + + context 'with a not_equal filter' do + include_context 'when the query has where: a not_equal block filter' + + if operators.include?(OPERATORS::NOT_EQUAL) + instance_exec(&block) + else + # :nocov: + pending + # :nocov: + end + end + + context 'with a not_one_of filter' do + include_context \ + 'when the query has where: a not_one_of block filter' + + if operators.include?(OPERATORS::NOT_ONE_OF) + instance_exec(&block) + else + # :nocov: + pending + # :nocov: + end + end + + context 'with a one_of filter' do + include_context 'when the query has where: a one_of block filter' + + if operators.include?(OPERATORS::ONE_OF) + instance_exec(&block) + else + # :nocov: + pending + # :nocov: + end + end + end + + wrap_context 'when the query has multiple query options' do + instance_exec(&block) + end + end + end + + # Contract defining contexts for validating query behavior. + module WithQueryContextsContract + extend RSpec::SleepingKingStudios::Contract + + # @!method apply(example_group) + # Adds the contract to the example group. + contract do + let(:filter) { nil } + let(:strategy) { nil } + let(:limit) { nil } + let(:offset) { nil } + let(:order) { nil } + + shared_context 'when the query has limit: value' do + let(:limit) { 3 } + let(:matching_data) { super()[0...limit] } + end + + shared_context 'when the query has offset: value' do + let(:offset) { 2 } + let(:matching_data) { super()[offset..] || [] } + end + + shared_context 'when the query has order: a simple ordering' do + let(:order) { :title } + let(:matching_data) { super().sort_by { |item| item['title'] } } + end + + shared_context 'when the query has order: a complex ordering' do + let(:order) do + { + author: :asc, + title: :desc + } + end + let(:matching_data) do + super().sort do |u, v| + cmp = u['author'] <=> v['author'] + + cmp.zero? ? (v['title'] <=> u['title']) : cmp + end + end + end + + shared_context 'when the query has where: a simple block filter' do + let(:filter) { -> { { author: 'Ursula K. LeGuin' } } } + let(:matching_data) do + super().select { |item| item['author'] == 'Ursula K. LeGuin' } + end + end + + shared_context 'when the query has where: a complex block filter' do + let(:filter) do + lambda do + { + author: equals('Ursula K. LeGuin'), + series: not_equal('Earthsea') + } + end + end + let(:matching_data) do + super() + .select { |item| item['author'] == 'Ursula K. LeGuin' } + .reject { |item| item['series'] == 'Earthsea' } + end + end + + shared_context 'when the query has where: a greater_than filter' do + let(:filter) { -> { { published_at: greater_than('1970-12-01') } } } + let(:matching_data) do + super().select { |item| item['published_at'] > '1970-12-01' } + end + end + + shared_context 'when the query has where: a greater_than_or_equal_to ' \ + 'filter' \ + do + let(:filter) do + -> { { published_at: greater_than_or_equal_to('1970-12-01') } } + end + let(:matching_data) do + super().select { |item| item['published_at'] >= '1970-12-01' } + end + end + + shared_context 'when the query has where: a less_than filter' do + let(:filter) { -> { { published_at: less_than('1970-12-01') } } } + let(:matching_data) do + super().select { |item| item['published_at'] < '1970-12-01' } + end + end + + shared_context 'when the query has where: a ' \ + 'less_than_or_equal_to filter' \ + do + let(:filter) do + -> { { published_at: less_than_or_equal_to('1970-12-01') } } + end + let(:matching_data) do + super().select { |item| item['published_at'] <= '1970-12-01' } + end + end + + shared_context 'when the query has where: an equal block filter' do + let(:filter) { -> { { author: equals('Ursula K. LeGuin') } } } + let(:matching_data) do + super().select { |item| item['author'] == 'Ursula K. LeGuin' } + end + end + + shared_context 'when the query has where: a not_equal block filter' do + let(:filter) { -> { { author: not_equal('Ursula K. LeGuin') } } } + let(:matching_data) do + super().reject { |item| item['author'] == 'Ursula K. LeGuin' } + end + end + + shared_context 'when the query has where: a not_one_of block filter' do + let(:filter) do + -> { { series: not_one_of(['Earthsea', 'The Lord of the Rings']) } } + end + let(:matching_data) do + super().reject do |item| + ['Earthsea', 'The Lord of the Rings'].include?(item['series']) + end + end + end + + shared_context 'when the query has where: a one_of block filter' do + let(:filter) do + -> { { series: one_of(['Earthsea', 'The Lord of the Rings']) } } + end + let(:matching_data) do + super().select do |item| + ['Earthsea', 'The Lord of the Rings'].include?(item['series']) + end + end + end + + shared_context 'when the query has multiple query options' do + let(:filter) { -> { { author: 'Ursula K. LeGuin' } } } + let(:strategy) { nil } + let(:order) { { title: :desc } } + let(:limit) { 2 } + let(:offset) { 1 } + let(:matching_data) do + super() + .select { |item| item['author'] == 'Ursula K. LeGuin' } + .sort { |u, v| v['title'] <=> u['title'] } + .slice(1, 2) || [] + end + end + end + end + end +end diff --git a/lib/cuprum/collections/rspec/contracts/repository_contracts.rb b/lib/cuprum/collections/rspec/contracts/repository_contracts.rb new file mode 100644 index 0000000..3be704d --- /dev/null +++ b/lib/cuprum/collections/rspec/contracts/repository_contracts.rb @@ -0,0 +1,605 @@ +# frozen_string_literal: true + +require 'cuprum/collections/rspec/contracts' + +module Cuprum::Collections::RSpec::Contracts + # Contracts for asserting on Repository objects. + module RepositoryContracts + # Contract validating the behavior of a Repository. + module ShouldBeARepositoryContract + extend RSpec::SleepingKingStudios::Contract + + # @!method apply(example_group) + # Adds the contract to the example group. + # + # @param abstract [Boolean] if true, the repository is abstract and does + # not define certain methods. Defaults to false. + # @param example_group [RSpec::Core::ExampleGroup] the example group to + # which the contract is applied. + # @param options [Hash] additional options for the contract. + # + # @option options collection_class [Class, String] the expected class + # for created collections. + # @option options entity_class [Class, String] the expected entity + # class. + contract do |abstract: false, **options| + shared_examples 'should create the collection' do + let(:configured_collection_class) do + return super() if defined?(super()) + + configured = options[:collection_class] + + # :nocov: + if configured.is_a?(String) + configured = Object.const_get(configured) + end + # :nocov: + + configured + end + let(:configured_entity_class) do + return super() if defined?(super()) + + # :nocov: + expected = + if collection_options.key?(:entity_class) + collection_options[:entity_class] + elsif options.key?(:entity_class) + options[:entity_class] + else + qualified_name + .split('/') + .then { |ary| [*ary[0...-1], tools.str.singularize(ary[-1])] } + .map { |str| tools.str.camelize(str) } + .join('::') + end + # :nocov: + expected = Object.const_get(expected) if expected.is_a?(String) + + expected + end + let(:configured_member_name) do + return super() if defined?(super()) + + tools.str.singularize(collection_name.to_s.split('/').last) + end + + def tools + SleepingKingStudios::Tools::Toolbelt.instance + end + + it 'should create the collection' do + create_collection(safe: false) + + expect(repository.key?(qualified_name)).to be true + end + + it 'should return the collection' do + collection = create_collection(safe: false) + + expect(collection).to be repository[qualified_name] + end + + it { expect(collection).to be_a configured_collection_class } + + it 'should set the entity class' do + expect(collection.entity_class).to be == configured_entity_class + end + + it 'should set the collection name' do + expect(collection.name).to be == collection_name.to_s + end + + it 'should set the member name' do + expect(collection.singular_name).to be == configured_member_name + end + + it 'should set the qualified name' do + expect(collection.qualified_name).to be == qualified_name + end + + it 'should set the collection options' do + expect(collection).to have_attributes( + primary_key_name: primary_key_name, + primary_key_type: primary_key_type + ) + end + end + + describe '#[]' do + let(:error_class) do + described_class::UndefinedCollectionError + end + let(:error_message) do + "repository does not define collection #{collection_name.inspect}" + end + + it { expect(repository).to respond_to(:[]).with(1).argument } + + describe 'with nil' do + let(:collection_name) { nil } + + it 'should raise an exception' do + expect { repository[collection_name] } + .to raise_error(error_class, error_message) + end + end + + describe 'with an object' do + let(:collection_name) { Object.new.freeze } + + it 'should raise an exception' do + expect { repository[collection_name] } + .to raise_error(error_class, error_message) + end + end + + describe 'with an invalid string' do + let(:collection_name) { 'invalid_name' } + + it 'should raise an exception' do + expect { repository[collection_name] } + .to raise_error(error_class, error_message) + end + end + + describe 'with an invalid symbol' do + let(:collection_name) { :invalid_name } + + it 'should raise an exception' do + expect { repository[collection_name] } + .to raise_error(error_class, error_message) + end + end + + wrap_context 'when the repository has many collections' do + describe 'with an invalid string' do + let(:collection_name) { 'invalid_name' } + + it 'should raise an exception' do + expect { repository[collection_name] } + .to raise_error(error_class, error_message) + end + end + + describe 'with an invalid symbol' do + let(:collection_name) { :invalid_name } + + it 'should raise an exception' do + expect { repository[collection_name] } + .to raise_error(error_class, error_message) + end + end + + describe 'with a valid string' do + let(:collection) { collections.values.first } + let(:collection_name) { collections.keys.first } + + it { expect(repository[collection_name]).to be collection } + end + + describe 'with a valid symbol' do + let(:collection) { collections.values.first } + let(:collection_name) { collections.keys.first.intern } + + it { expect(repository[collection_name]).to be collection } + end + end + end + + describe '#add' do + let(:error_class) do + described_class::InvalidCollectionError + end + let(:error_message) do + "#{collection.inspect} is not a valid collection" + end + + it 'should define the method' do + expect(repository) + .to respond_to(:add) + .with(1).argument + .and_keywords(:force) + end + + it 'should alias #add as #<<' do + expect(repository.method(:<<)).to be == repository.method(:add) + end + + describe 'with nil' do + let(:collection) { nil } + + it 'should raise an exception' do + expect { repository.add(collection) } + .to raise_error(error_class, error_message) + end + end + + describe 'with an object' do + let(:collection) { Object.new.freeze } + + it 'should raise an exception' do + expect { repository.add(collection) } + .to raise_error(error_class, error_message) + end + end + + describe 'with a collection' do + it { expect(repository.add(example_collection)).to be repository } + + it 'should add the collection to the repository' do + repository.add(example_collection) + + expect(repository[example_collection.qualified_name]) + .to be example_collection + end + + describe 'with force: true' do + it 'should add the collection to the repository' do + repository.add(example_collection, force: true) + + expect(repository[example_collection.qualified_name]) + .to be example_collection + end + end + + context 'when the collection already exists' do + let(:error_message) do + "collection #{example_collection.qualified_name} already exists" + end + + before(:example) do + allow(repository) + .to receive(:key?) + .with(example_collection.qualified_name) + .and_return(true) + end + + it 'should raise an exception' do + expect { repository.add(example_collection) } + .to raise_error( + described_class::DuplicateCollectionError, + error_message + ) + end + + it 'should not update the repository' do + begin + repository.add(example_collection) + rescue described_class::DuplicateCollectionError + # Do nothing. + end + + expect { repository[example_collection.qualified_name] } + .to raise_error( + described_class::UndefinedCollectionError, + 'repository does not define collection ' \ + "#{example_collection.qualified_name.inspect}" + ) + end + + describe 'with force: true' do + it 'should add the collection to the repository' do + repository.add(example_collection, force: true) + + expect(repository[example_collection.qualified_name]) + .to be example_collection + end + end + end + end + end + + describe '#create' do + let(:collection_name) { 'books' } + let(:qualified_name) { collection_name.to_s } + let(:primary_key_name) { 'id' } + let(:primary_key_type) { Integer } + let(:collection_options) { {} } + let(:collection) do + create_collection + + repository[qualified_name] + end + let(:error_message) do + "#{described_class.name} is an abstract class. Define a " \ + 'repository subclass and implement the #build_collection method.' + end + + def create_collection(force: false, safe: true, **options) + if safe + begin + repository.create(force: force, **collection_options, **options) + rescue StandardError + # Do nothing. + end + else + repository.create(force: force, **collection_options, **options) + end + end + + it 'should define the method' do + expect(repository) + .to respond_to(:create) + .with(0).arguments + .and_keywords(:collection_name, :entity_class, :force) + .and_any_keywords + end + + if abstract + it 'should raise an exception' do + expect { create_collection(safe: false) } + .to raise_error( + described_class::AbstractRepositoryError, + error_message + ) + end + + next + end + + describe 'with entity_class: a Class' do + let(:entity_class) { Book } + let(:collection_options) do + super().merge(entity_class: entity_class) + end + + include_examples 'should create the collection' + end + + describe 'with entity_class: a String' do + let(:entity_class) { 'Book' } + let(:collection_options) do + super().merge(entity_class: entity_class) + end + + include_examples 'should create the collection' + end + + describe 'with name: a String' do + let(:collection_name) { 'books' } + let(:collection_options) do + super().merge(name: collection_name) + end + + include_examples 'should create the collection' + end + + describe 'with name: a Symbol' do + let(:collection_name) { :books } + let(:collection_options) do + super().merge(name: collection_name) + end + + include_examples 'should create the collection' + end + + describe 'with collection options' do + let(:primary_key_name) { 'uuid' } + let(:primary_key_type) { String } + let(:collection_options) do + super().merge( + name: collection_name, + primary_key_name: primary_key_name, + primary_key_type: primary_key_type + ) + end + + include_examples 'should create the collection' + end + + context 'when the collection already exists' do + let(:collection_name) { 'books' } + let(:collection_options) do + super().merge(name: collection_name) + end + let(:error_message) do + "collection #{qualified_name} already exists" + end + + before { create_collection(old: true) } + + it 'should raise an exception' do + expect { create_collection(safe: false) } + .to raise_error( + described_class::DuplicateCollectionError, + error_message + ) + end + + it 'should not update the repository' do + create_collection(old: false) + + collection = repository[qualified_name] + + expect(collection.options[:old]).to be true + end + + describe 'with force: true' do + it 'should update the repository' do + create_collection(force: true, old: false) + + collection = repository[qualified_name] + + expect(collection.options[:old]).to be false + end + end + end + end + + describe '#find_or_create' do + let(:collection_name) { 'books' } + let(:qualified_name) { collection_name.to_s } + let(:primary_key_name) { 'id' } + let(:primary_key_type) { Integer } + let(:collection_options) { {} } + let(:collection) do + create_collection + + repository[qualified_name] + end + let(:error_message) do + "#{described_class.name} is an abstract class. Define a " \ + 'repository subclass and implement the #build_collection method.' + end + + def create_collection(safe: true, **options) + if safe + begin + repository.find_or_create(**collection_options, **options) + rescue StandardError + # Do nothing. + end + else + repository.find_or_create(**collection_options, **options) + end + end + + it 'should define the method' do + expect(repository) + .to respond_to(:find_or_create) + .with(0).arguments + .and_keywords(:entity_class) + .and_any_keywords + end + + if abstract + let(:collection_options) { { name: collection_name } } + + it 'should raise an exception' do + expect { create_collection(safe: false) } + .to raise_error( + described_class::AbstractRepositoryError, + error_message + ) + end + + next + end + + describe 'with entity_class: a Class' do + let(:entity_class) { Book } + let(:collection_options) do + super().merge(entity_class: entity_class) + end + + include_examples 'should create the collection' + end + + describe 'with entity_class: a String' do + let(:entity_class) { Book } + let(:collection_options) do + super().merge(entity_class: entity_class) + end + + include_examples 'should create the collection' + end + + describe 'with name: a String' do + let(:collection_name) { 'books' } + let(:collection_options) do + super().merge(name: collection_name) + end + + include_examples 'should create the collection' + end + + describe 'with name: a Symbol' do + let(:collection_name) { :books } + let(:collection_options) do + super().merge(name: collection_name) + end + + include_examples 'should create the collection' + end + + describe 'with collection options' do + let(:primary_key_name) { 'uuid' } + let(:primary_key_type) { String } + let(:qualified_name) { 'spec/scoped_books' } + let(:collection_options) do + super().merge( + name: collection_name, + primary_key_name: primary_key_name, + primary_key_type: primary_key_type, + qualified_name: qualified_name + ) + end + + include_examples 'should create the collection' + end + + context 'when the collection already exists' do + let(:collection_name) { 'books' } + let(:collection_options) do + super().merge(name: collection_name) + end + let(:error_message) do + "collection #{qualified_name} already exists" + end + + before { create_collection(old: true) } + + describe 'with non-matching options' do + it 'should raise an exception' do + expect { create_collection(old: false, safe: false) } + .to raise_error( + described_class::DuplicateCollectionError, + error_message + ) + end + + it 'should not update the repository' do + create_collection(old: false) + + collection = repository[qualified_name] + + expect(collection.options[:old]).to be true + end + end + + describe 'with matching options' do + it 'should return the collection' do + collection = create_collection(old: true) + + expect(collection.options[:old]).to be true + end + end + end + end + + describe '#key?' do + it { expect(repository).to respond_to(:key?).with(1).argument } + + it { expect(repository.key?(nil)).to be false } + + it { expect(repository.key?(Object.new.freeze)).to be false } + + it { expect(repository.key?('invalid_name')).to be false } + + it { expect(repository.key?(:invalid_name)).to be false } + + wrap_context 'when the repository has many collections' do + it { expect(repository.key?('invalid_name')).to be false } + + it { expect(repository.key?(:invalid_name)).to be false } + + it { expect(repository.key?(collections.keys.first)).to be true } + + it 'should include the key' do + expect(repository.key?(collections.keys.first.intern)).to be true + end + end + end + + describe '#keys' do + include_examples 'should define reader', :keys, [] + + wrap_context 'when the repository has many collections' do + it { expect(repository.keys).to be == collections.keys } + end + end + end + end + end +end diff --git a/lib/cuprum/collections/rspec/find_matching_command_contract.rb b/lib/cuprum/collections/rspec/find_matching_command_contract.rb index e62ade6..43acfd9 100644 --- a/lib/cuprum/collections/rspec/find_matching_command_contract.rb +++ b/lib/cuprum/collections/rspec/find_matching_command_contract.rb @@ -4,14 +4,17 @@ require 'cuprum/collections/constraints/ordering' require 'cuprum/collections/rspec' -require 'cuprum/collections/rspec/querying_contract' +require 'cuprum/collections/rspec/contracts/query_contracts' module Cuprum::Collections::RSpec # Contract validating the behavior of a FindMatching command implementation. FIND_MATCHING_COMMAND_CONTRACT = lambda do include Stannum::RSpec::Matchers + include Cuprum::Collections::RSpec::Contracts::QueryContracts describe '#call' do + include_contract 'with query contexts' + shared_examples 'should return the matching items' do it { expect(result).to be_a_passing_result } @@ -30,8 +33,6 @@ module Cuprum::Collections::RSpec it { expect(result.value[collection_name]).to be == expected_data } end - include_contract Cuprum::Collections::RSpec::QUERYING_CONTEXTS - let(:options) do opts = {} @@ -94,7 +95,7 @@ module Cuprum::Collections::RSpec include_examples 'should return the matching items' - include_contract Cuprum::Collections::RSpec::QUERYING_CONTRACT, + include_contract 'should perform queries', block: lambda { include_examples 'should return the matching items' } @@ -115,7 +116,7 @@ module Cuprum::Collections::RSpec include_examples 'should return the wrapped items' - include_contract Cuprum::Collections::RSpec::QUERYING_CONTRACT, + include_contract 'should perform queries', block: lambda { include_examples 'should return the wrapped items' } @@ -126,7 +127,7 @@ module Cuprum::Collections::RSpec include_examples 'should return the matching items' - include_contract Cuprum::Collections::RSpec::QUERYING_CONTRACT, + include_contract 'should perform queries', block: lambda { include_examples 'should return the matching items' } @@ -136,7 +137,7 @@ module Cuprum::Collections::RSpec include_examples 'should return the wrapped items' - include_contract Cuprum::Collections::RSpec::QUERYING_CONTRACT, + include_contract 'should perform queries', block: lambda { include_examples 'should return the wrapped items' } diff --git a/lib/cuprum/collections/rspec/query_builder_contract.rb b/lib/cuprum/collections/rspec/query_builder_contract.rb deleted file mode 100644 index 37040d5..0000000 --- a/lib/cuprum/collections/rspec/query_builder_contract.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -require 'cuprum/collections/rspec' - -module Cuprum::Collections::RSpec - # Contract validating the behavior of a QueryBuilder implementation. - QUERY_BUILDER_CONTRACT = lambda do - describe '#base_query' do - include_examples 'should define reader', :base_query, -> { base_query } - end - - describe '#call' do - let(:criteria) { [['title', :equal, 'The Naked Sun']] } - let(:expected) { criteria } - let(:filter) { { title: 'The Naked Sun' } } - let(:strategy) { :custom } - let(:parser) do - instance_double( - Cuprum::Collections::Queries::Parse, - call: Cuprum::Result.new(value: criteria) - ) - end - let(:query) do - builder.call(strategy: strategy, where: filter) - end - - before(:example) do - allow(Cuprum::Collections::Queries::Parse) - .to receive(:new) - .and_return(parser) - end - - it 'should define the method' do - expect(builder).to respond_to(:call) - .with(0).arguments - .and_keywords(:strategy, :where) - end - - it 'should parse the criteria' do - builder.call(strategy: strategy, where: filter) - - expect(parser) - .to have_received(:call) - .with(strategy: strategy, where: filter) - end - - it { expect(query).to be_a base_query.class } - - it { expect(query).not_to be base_query } - - it { expect(query.criteria).to be == expected } - - describe 'with strategy: :unsafe' do - let(:strategy) { :unsafe } - let(:filter) { criteria } - - it 'should not parse the criteria' do - builder.call(strategy: strategy, where: filter) - - expect(parser).not_to have_received(:call) - end - - it { expect(query.criteria).to be == expected } - end - - context 'when the query has existing criteria' do - let(:old_criteria) { [['genre', :eq, 'Science Fiction']] } - let(:expected) { old_criteria + criteria } - let(:base_query) { super().send(:with_criteria, old_criteria) } - - it { expect(query.criteria).to be == expected } - end - - context 'when the parser is unable to parse the query' do - let(:error) { Cuprum::Error.new(message: 'Something went wrong.') } - let(:result) { Cuprum::Result.new(error: error) } - - before(:example) do - allow(parser).to receive(:call).and_return(result) - end - - it 'should raise an exception' do - expect do - builder.call(strategy: strategy, where: filter) - end - .to raise_error Cuprum::Collections::QueryBuilder::ParseError, - error.message - end - end - end - end -end diff --git a/lib/cuprum/collections/rspec/query_contract.rb b/lib/cuprum/collections/rspec/query_contract.rb deleted file mode 100644 index f6fa0bc..0000000 --- a/lib/cuprum/collections/rspec/query_contract.rb +++ /dev/null @@ -1,650 +0,0 @@ -# frozen_string_literal: true - -require 'cuprum/collections/rspec' -require 'cuprum/collections/rspec/fixtures' -require 'cuprum/collections/rspec/querying_contract' - -module Cuprum::Collections::RSpec # rubocop:disable Style/Documentation - default_operators = Cuprum::Collections::Queries::Operators.values - - # Contract validating the behavior of a Query implementation. - QUERY_CONTRACT = lambda do |operators: default_operators.freeze| - operators = Set.new(operators.map(&:to_sym)) - - include_contract Cuprum::Collections::RSpec::QUERYING_CONTEXTS - - shared_context 'when the query has composed filters' do - let(:scoped_query) do - super() - .where { { author: 'Ursula K. LeGuin' } } - .where { { series: not_equal('Earthsea') } } - end - let(:matching_data) do - super() - .select { |item| item['author'] == 'Ursula K. LeGuin' } - .reject { |item| item['series'] == 'Earthsea' } - end - end - - let(:scoped_query) do - # :nocov: - scoped = - if filter.is_a?(Proc) - query.where(&filter) - elsif !filter.nil? - query.where(filter) - else - query - end - # :nocov: - scoped = scoped.limit(limit) if limit - scoped = scoped.offset(offset) if offset - scoped = scoped.order(order) if order - - scoped - end - - it 'should be enumerable' do - expect(described_class).to be < Enumerable - end - - describe '#count' do - let(:data) { [] } - let(:matching_data) { data } - let(:expected_data) do - defined?(super()) ? super() : matching_data - end - - it { expect(query).to respond_to(:count).with(0).arguments } - - it { expect(query.count).to be == expected_data.count } - - wrap_context 'when the query has composed filters' do - it { expect(scoped_query.count).to be == expected_data.count } - end - - context 'when the collection data changes' do - let(:item) { BOOKS_FIXTURES.first } - - before(:example) do - query.count # Cache query results. - - add_item_to_collection(item) - end - - it { expect(query.count).to be == expected_data.count } - end - - context 'when the collection has many items' do - let(:data) { BOOKS_FIXTURES } - - it { expect(query.count).to be == expected_data.count } - - wrap_context 'when the query has composed filters' do - it { expect(scoped_query.count).to be == expected_data.count } - end - - context 'when the collection data changes' do - let(:data) { BOOKS_FIXTURES[0...-1] } - let(:item) { BOOKS_FIXTURES.last } - - before(:example) do - query.count # Cache query results. - - add_item_to_collection(item) - end - - it { expect(query.count).to be == expected_data.count } - end - end - end - - describe '#criteria' do - include_examples 'should have reader', :criteria, [] - - wrap_context 'when the query has where: a simple block filter' do - let(:expected) { [['author', :equal, 'Ursula K. LeGuin']] } - - it { expect(scoped_query.criteria).to be == expected } - end - - wrap_context 'when the query has where: a complex block filter' do - let(:expected) do - [ - ['author', :equal, 'Ursula K. LeGuin'], - ['series', :not_equal, 'Earthsea'] - ] - end - - if operators.include?(OPERATORS::EQUAL) && - operators.include?(OPERATORS::NOT_EQUAL) - it { expect(scoped_query.criteria).to be == expected } - else - # :nocov: - pending - # :nocov: - end - end - - wrap_context 'when the query has composed filters' do - let(:expected) do - [ - ['author', :equal, 'Ursula K. LeGuin'], - ['series', :not_equal, 'Earthsea'] - ] - end - - it { expect(scoped_query.criteria).to be == expected } - end - - wrap_context 'when the query has where: an equal block filter' do - let(:expected) { [['author', :equal, 'Ursula K. LeGuin']] } - - if operators.include?(OPERATORS::EQUAL) - it { expect(scoped_query.criteria).to be == expected } - else - # :nocov: - pending - # :nocov: - end - end - - wrap_context 'when the query has where: a not_equal block filter' do - let(:expected) { [['author', :not_equal, 'Ursula K. LeGuin']] } - - if operators.include?(OPERATORS::NOT_EQUAL) - it { expect(scoped_query.criteria).to be == expected } - else - # :nocov: - pending - # :nocov: - end - end - end - - describe '#each' do - shared_examples 'should enumerate the matching data' do - describe 'with no arguments' do - it { expect(scoped_query.each).to be_a Enumerator } - - it { expect(scoped_query.each.count).to be == matching_data.size } - - it { expect(scoped_query.each.to_a).to deep_match expected_data } - end - - describe 'with a block' do - it 'should yield each matching item' do - expect { |block| scoped_query.each(&block) } - .to yield_successive_args(*expected_data) - end - end - end - - let(:data) { [] } - let(:matching_data) { data } - let(:expected_data) do - defined?(super()) ? super() : matching_data - end - - it { expect(query).to respond_to(:each).with(0).arguments } - - include_examples 'should enumerate the matching data' - - include_contract Cuprum::Collections::RSpec::QUERYING_CONTRACT, - block: lambda { - include_examples 'should enumerate the matching data' - }, - operators: operators - - wrap_context 'when the query has composed filters' do - include_examples 'should enumerate the matching data' - end - - context 'when the collection data changes' do - let(:item) { BOOKS_FIXTURES.first } - - before(:example) do - query.each {} # Cache query results. - - add_item_to_collection(item) - end - - include_examples 'should enumerate the matching data' - end - - context 'when the collection has many items' do - let(:data) { BOOKS_FIXTURES } - - include_examples 'should enumerate the matching data' - - include_contract Cuprum::Collections::RSpec::QUERYING_CONTRACT, - block: lambda { - include_examples 'should enumerate the matching data' - }, - operators: operators - - wrap_context 'when the query has composed filters' do - include_examples 'should enumerate the matching data' - end - - context 'when the collection data changes' do - let(:data) { BOOKS_FIXTURES[0...-1] } - let(:item) { BOOKS_FIXTURES.last } - - before(:example) do - query.each {} # Cache query results. - - add_item_to_collection(item) - end - - include_examples 'should enumerate the matching data' - end - end - end - - describe '#exists?' do - shared_examples 'should check the existence of matching data' do - it { expect(query.exists?).to be == !matching_data.empty? } - end - - let(:data) { [] } - let(:matching_data) { data } - - include_examples 'should define predicate', :exists? - - include_examples 'should check the existence of matching data' - - include_contract Cuprum::Collections::RSpec::QUERYING_CONTRACT, - block: lambda { - include_examples 'should check the existence of matching data' - }, - operators: operators - - wrap_context 'when the query has composed filters' do - include_examples 'should check the existence of matching data' - end - - context 'when the collection has many items' do - let(:data) { BOOKS_FIXTURES } - - include_examples 'should check the existence of matching data' - - include_contract Cuprum::Collections::RSpec::QUERYING_CONTRACT, - block: lambda { - include_examples 'should check the existence of matching data' - }, - operators: operators - - wrap_context 'when the query has composed filters' do - include_examples 'should check the existence of matching data' - end - end - end - - describe '#limit' do - it { expect(query).to respond_to(:limit).with(0..1).arguments } - - describe 'with no arguments' do - it { expect(query.limit).to be nil } - end - - describe 'with nil' do - let(:error_message) { 'limit must be a non-negative integer' } - - it 'should raise an exception' do - expect { query.limit nil } - .to raise_error ArgumentError, error_message - end - end - - describe 'with an object' do - let(:error_message) { 'limit must be a non-negative integer' } - - it 'should raise an exception' do - expect { query.limit Object.new.freeze } - .to raise_error ArgumentError, error_message - end - end - - describe 'with a negative integer' do - let(:error_message) { 'limit must be a non-negative integer' } - - it 'should raise an exception' do - expect { query.limit(-1) } - .to raise_error ArgumentError, error_message - end - end - - describe 'with zero' do - it { expect(query.limit 0).to be_a described_class } - - it { expect(query.limit 0).not_to be query } - - it { expect(query.limit(0).limit).to be 0 } - end - - describe 'with a positive integer' do - it { expect(query.limit 3).to be_a described_class } - - it { expect(query.limit 3).not_to be query } - - it { expect(query.limit(3).limit).to be 3 } - end - end - - describe '#offset' do - it { expect(query).to respond_to(:offset).with(0..1).argument } - - describe 'with no arguments' do - it { expect(query.offset).to be nil } - end - - describe 'with nil' do - let(:error_message) { 'offset must be a non-negative integer' } - - it 'should raise an exception' do - expect { query.offset nil } - .to raise_error ArgumentError, error_message - end - end - - describe 'with an object' do - let(:error_message) { 'offset must be a non-negative integer' } - - it 'should raise an exception' do - expect { query.offset Object.new.freeze } - .to raise_error ArgumentError, error_message - end - end - - describe 'with a negative integer' do - let(:error_message) { 'offset must be a non-negative integer' } - - it 'should raise an exception' do - expect { query.offset(-1) } - .to raise_error ArgumentError, error_message - end - end - - describe 'with zero' do - it { expect(query.offset 0).to be_a described_class } - - it { expect(query.offset 0).not_to be query } - - it { expect(query.offset(0).offset).to be 0 } - end - - describe 'with a positive integer' do - it { expect(query.offset 3).to be_a described_class } - - it { expect(query.offset 3).not_to be query } - - it { expect(query.offset(3).offset).to be 3 } - end - end - - describe '#order' do - let(:default_order) { defined?(super()) ? super() : {} } - let(:error_message) do - 'order must be a list of attribute names and/or a hash of attribute ' \ - 'names with values :asc or :desc' - end - - it 'should define the method' do - expect(query) - .to respond_to(:order) - .with(0).arguments - .and_unlimited_arguments - end - - it { expect(query).to have_aliased_method(:order).as(:order_by) } - - describe 'with no arguments' do - it { expect(query.order).to be == default_order } - end - - describe 'with a hash with invalid keys' do - it 'should raise an exception' do - expect { query.order({ nil => :asc }) } - .to raise_error ArgumentError, error_message - end - end - - describe 'with a hash with empty string keys' do - it 'should raise an exception' do - expect { query.order({ '' => :asc }) } - .to raise_error ArgumentError, error_message - end - end - - describe 'with a hash with empty symbol keys' do - it 'should raise an exception' do - expect { query.order({ '': :asc }) } - .to raise_error ArgumentError, error_message - end - end - - describe 'with a hash with nil value' do - it 'should raise an exception' do - expect { query.order({ title: nil }) } - .to raise_error ArgumentError, error_message - end - end - - describe 'with a hash with object value' do - it 'should raise an exception' do - expect { query.order({ title: Object.new.freeze }) } - .to raise_error ArgumentError, error_message - end - end - - describe 'with a hash with empty value' do - it 'should raise an exception' do - expect { query.order({ title: '' }) } - .to raise_error ArgumentError, error_message - end - end - - describe 'with a hash with invalid value' do - it 'should raise an exception' do - expect { query.order({ title: 'wibbly' }) } - .to raise_error ArgumentError, error_message - end - end - - describe 'with a valid ordering' do - let(:expected) do - { title: :asc } - end - - it { expect(query.order :title).to be_a described_class } - - it { expect(query.order :title).not_to be query } - - it { expect(query.order(:title).order).to be == expected } - end - end - - describe '#reset' do - let(:data) { [] } - let(:matching_data) { data } - let(:expected_data) do - defined?(super()) ? super() : matching_data - end - - it { expect(query).to respond_to(:reset).with(0).arguments } - - it { expect(query.reset).to be_a query.class } - - it { expect(query.reset).not_to be query } - - it { expect(query.reset.to_a).to be == query.to_a } - - context 'when the collection data changes' do - let(:item) { BOOKS_FIXTURES.first } - let(:matching_data) { [item] } - - before(:example) do - query.to_a # Cache query results. - - add_item_to_collection(item) - end - - it { expect(query.reset.count).to be expected_data.size } - - it { expect(query.reset.to_a).to deep_match expected_data } - end - - context 'when the collection has many items' do - let(:data) { BOOKS_FIXTURES } - - it { expect(query.reset).to be_a query.class } - - it { expect(query.reset).not_to be query } - - it { expect(query.reset.to_a).to be == query.to_a } - - context 'when the collection data changes' do - let(:data) { BOOKS_FIXTURES[0...-1] } - let(:item) { BOOKS_FIXTURES.last } - let(:matching_data) { [*data, item] } - - before(:example) do - query.to_a # Cache query results. - - add_item_to_collection(item) - end - - it { expect(query.reset.count).to be expected_data.size } - - it { expect(query.reset.to_a).to deep_match expected_data } - end - end - end - - describe '#to_a' do - let(:data) { [] } - let(:matching_data) { data } - let(:expected_data) do - defined?(super()) ? super() : matching_data - end - - it { expect(query).to respond_to(:to_a).with(0).arguments } - - it { expect(query.to_a).to deep_match expected_data } - - include_contract Cuprum::Collections::RSpec::QUERYING_CONTRACT, - block: lambda { - it { expect(scoped_query.to_a).to deep_match expected_data } - }, - operators: operators - - wrap_context 'when the query has composed filters' do - it { expect(scoped_query.to_a).to deep_match expected_data } - end - - context 'when the collection data changes' do - let(:item) { BOOKS_FIXTURES.first } - - before(:example) do - query.to_a # Cache query results. - - add_item_to_collection(item) - end - - it { expect(query.to_a).to deep_match expected_data } - end - - context 'when the collection has many items' do - let(:data) { BOOKS_FIXTURES } - - it { expect(query.to_a).to deep_match expected_data } - - include_contract Cuprum::Collections::RSpec::QUERYING_CONTRACT, - block: lambda { - it { expect(scoped_query.to_a).to deep_match expected_data } - }, - operators: operators - - wrap_context 'when the query has composed filters' do - it { expect(scoped_query.to_a).to deep_match expected_data } - end - - context 'when the collection data changes' do - let(:data) { BOOKS_FIXTURES[0...-1] } - let(:item) { BOOKS_FIXTURES.last } - - before(:example) do - query.to_a # Cache query results. - - add_item_to_collection(item) - end - - it { expect(query.to_a).to deep_match expected_data } - end - end - end - - describe '#where' do - let(:block) { -> { { title: 'The Caves of Steel' } } } - - it 'should define the method' do - expect(query) - .to respond_to(:where) - .with(0..1).arguments - .and_keywords(:strategy) - .and_a_block - end - - describe 'with no arguments' do - it { expect(query.where).to be_a described_class } - - it { expect(query.where).not_to be query } - end - - describe 'with a block' do - it { expect(query.where(&block)).to be_a described_class } - - it { expect(query.where(&block)).not_to be query } - end - - describe 'with a valid strategy' do - it 'should return a query instance' do - expect(query.where(strategy: :block, &block)).to be_a described_class - end - - it { expect(query.where(strategy: :block, &block)).not_to be query } - end - - describe 'with parameters that do not match a strategy' do - let(:error_class) { Cuprum::Collections::QueryBuilder::ParseError } - let(:error_message) { 'unable to parse query with strategy nil' } - - it 'should raise an exception' do - expect { query.where(%w[ichi ni san]) } - .to raise_error error_class, error_message - end - end - - describe 'with an invalid strategy' do - let(:error_class) { Cuprum::Collections::QueryBuilder::ParseError } - let(:error_message) { 'unable to parse query with strategy :random' } - - it 'should raise an exception' do - expect { query.where(strategy: :random) } - .to raise_error error_class, error_message - end - end - - describe 'with invalid parameters for a strategy' do - let(:error_class) { Cuprum::Collections::QueryBuilder::ParseError } - let(:error_message) { 'unable to parse query with strategy :block' } - - it 'should raise an exception' do - expect { query.where(strategy: :block) } - .to raise_error error_class, error_message - end - end - end - end -end diff --git a/lib/cuprum/collections/rspec/querying_contract.rb b/lib/cuprum/collections/rspec/querying_contract.rb deleted file mode 100644 index 05bfc17..0000000 --- a/lib/cuprum/collections/rspec/querying_contract.rb +++ /dev/null @@ -1,298 +0,0 @@ -# frozen_string_literal: true - -require 'cuprum/collections/queries' -require 'cuprum/collections/rspec' - -module Cuprum::Collections::RSpec - OPERATORS = Cuprum::Collections::Queries::Operators - private_constant :OPERATORS - - # Shared contexts for specs that define querying behavior. - QUERYING_CONTEXTS = lambda do - let(:filter) { nil } - let(:strategy) { nil } - let(:limit) { nil } - let(:offset) { nil } - let(:order) { nil } - - shared_context 'when the query has limit: value' do - let(:limit) { 3 } - let(:matching_data) { super()[0...limit] } - end - - shared_context 'when the query has offset: value' do - let(:offset) { 2 } - let(:matching_data) { super()[offset..] || [] } - end - - shared_context 'when the query has order: a simple ordering' do - let(:order) { :title } - let(:matching_data) { super().sort_by { |item| item['title'] } } - end - - shared_context 'when the query has order: a complex ordering' do - let(:order) do - { - author: :asc, - title: :desc - } - end - let(:matching_data) do - super().sort do |u, v| - cmp = u['author'] <=> v['author'] - - cmp.zero? ? (v['title'] <=> u['title']) : cmp - end - end - end - - shared_context 'when the query has where: a simple block filter' do - let(:filter) { -> { { author: 'Ursula K. LeGuin' } } } - let(:matching_data) do - super().select { |item| item['author'] == 'Ursula K. LeGuin' } - end - end - - shared_context 'when the query has where: a complex block filter' do - let(:filter) do - lambda do - { - author: equals('Ursula K. LeGuin'), - series: not_equal('Earthsea') - } - end - end - let(:matching_data) do - super() - .select { |item| item['author'] == 'Ursula K. LeGuin' } - .reject { |item| item['series'] == 'Earthsea' } - end - end - - shared_context 'when the query has where: a greater_than filter' do - let(:filter) { -> { { published_at: greater_than('1970-12-01') } } } - let(:matching_data) do - super().select { |item| item['published_at'] > '1970-12-01' } - end - end - - shared_context 'when the query has where: a greater_than_or_equal_to ' \ - 'filter' \ - do - let(:filter) do - -> { { published_at: greater_than_or_equal_to('1970-12-01') } } - end - let(:matching_data) do - super().select { |item| item['published_at'] >= '1970-12-01' } - end - end - - shared_context 'when the query has where: a less_than filter' do - let(:filter) { -> { { published_at: less_than('1970-12-01') } } } - let(:matching_data) do - super().select { |item| item['published_at'] < '1970-12-01' } - end - end - - shared_context 'when the query has where: a less_than_or_equal_to filter' do - let(:filter) do - -> { { published_at: less_than_or_equal_to('1970-12-01') } } - end - let(:matching_data) do - super().select { |item| item['published_at'] <= '1970-12-01' } - end - end - - shared_context 'when the query has where: an equal block filter' do - let(:filter) { -> { { author: equals('Ursula K. LeGuin') } } } - let(:matching_data) do - super().select { |item| item['author'] == 'Ursula K. LeGuin' } - end - end - - shared_context 'when the query has where: a not_equal block filter' do - let(:filter) { -> { { author: not_equal('Ursula K. LeGuin') } } } - let(:matching_data) do - super().reject { |item| item['author'] == 'Ursula K. LeGuin' } - end - end - - shared_context 'when the query has where: a not_one_of block filter' do - let(:filter) do - -> { { series: not_one_of(['Earthsea', 'The Lord of the Rings']) } } - end - let(:matching_data) do - super().reject do |item| - ['Earthsea', 'The Lord of the Rings'].include?(item['series']) - end - end - end - - shared_context 'when the query has where: a one_of block filter' do - let(:filter) do - -> { { series: one_of(['Earthsea', 'The Lord of the Rings']) } } - end - let(:matching_data) do - super().select do |item| - ['Earthsea', 'The Lord of the Rings'].include?(item['series']) - end - end - end - - shared_context 'when the query has multiple query options' do - let(:filter) { -> { { author: 'Ursula K. LeGuin' } } } - let(:strategy) { nil } - let(:order) { { title: :desc } } - let(:limit) { 2 } - let(:offset) { 1 } - let(:matching_data) do - super() - .select { |item| item['author'] == 'Ursula K. LeGuin' } - .sort { |u, v| v['title'] <=> u['title'] } - .slice(1, 2) || [] - end - end - end - - # Contract validating the behavior objects that perform queries. - QUERYING_CONTRACT = lambda do |block:, operators: OPERATORS.values| - wrap_context 'when the query has limit: value' do - instance_exec(&block) - end - - wrap_context 'when the query has offset: value' do - instance_exec(&block) - end - - wrap_context 'when the query has order: a simple ordering' do - instance_exec(&block) - end - - wrap_context 'when the query has order: a complex ordering' do - instance_exec(&block) - end - - context 'when the query has where: a block filter' do - context 'with a simple filter' do - include_context 'when the query has where: a simple block filter' - - instance_exec(&block) - end - - context 'with a complex filter' do - include_context 'when the query has where: a complex block filter' - - if operators.include?(OPERATORS::EQUAL) && - operators.include?(OPERATORS::NOT_EQUAL) - instance_exec(&block) - else - # :nocov: - pending - # :nocov: - end - end - - context 'with an equals filter' do - include_context 'when the query has where: an equal block filter' - - if operators.include?(OPERATORS::EQUAL) - instance_exec(&block) - else - # :nocov: - pending - # :nocov: - end - end - - context 'with a greater_than filter' do - include_context 'when the query has where: a greater_than filter' - - if operators.include?(OPERATORS::GREATER_THAN) - instance_exec(&block) - else - # :nocov: - pending - # :nocov: - end - end - - context 'with a greater_than_or_equal_to filter' do - include_context \ - 'when the query has where: a greater_than_or_equal_to filter' - - if operators.include?(OPERATORS::GREATER_THAN_OR_EQUAL_TO) - instance_exec(&block) - else - # :nocov: - pending - # :nocov: - end - end - - context 'with a less_than filter' do - include_context 'when the query has where: a less_than filter' - - if operators.include?(OPERATORS::LESS_THAN) - instance_exec(&block) - else - # :nocov: - pending - # :nocov: - end - end - - context 'with a less_than_or_equal_to filter' do - include_context \ - 'when the query has where: a less_than_or_equal_to filter' - - if operators.include?(OPERATORS::LESS_THAN_OR_EQUAL_TO) - instance_exec(&block) - else - # :nocov: - pending - # :nocov: - end - end - - context 'with a not_equal filter' do - include_context 'when the query has where: a not_equal block filter' - - if operators.include?(OPERATORS::NOT_EQUAL) - instance_exec(&block) - else - # :nocov: - pending - # :nocov: - end - end - - context 'with a not_one_of filter' do - include_context 'when the query has where: a not_one_of block filter' - - if operators.include?(OPERATORS::NOT_ONE_OF) - instance_exec(&block) - else - # :nocov: - pending - # :nocov: - end - end - - context 'with a one_of filter' do - include_context 'when the query has where: a one_of block filter' - - if operators.include?(OPERATORS::ONE_OF) - instance_exec(&block) - else - # :nocov: - pending - # :nocov: - end - end - end - - wrap_context 'when the query has multiple query options' do - instance_exec(&block) - end - end -end diff --git a/lib/cuprum/collections/rspec/repository_contract.rb b/lib/cuprum/collections/rspec/repository_contract.rb deleted file mode 100644 index 9e32ea4..0000000 --- a/lib/cuprum/collections/rspec/repository_contract.rb +++ /dev/null @@ -1,598 +0,0 @@ -# frozen_string_literal: true - -require 'rspec/sleeping_king_studios/contract' - -require 'cuprum/collections/rspec' - -module Cuprum::Collections::RSpec - # Contract validating the behavior of a Repository. - module RepositoryContract - extend RSpec::SleepingKingStudios::Contract - - # @!method apply(example_group) - # Adds the contract to the example group. - # - # @param abstract [Boolean] if true, the repository is abstract and does - # not define certain methods. Defaults to false. - # @param example_group [RSpec::Core::ExampleGroup] the example group to - # which the contract is applied. - # @param options [Hash] additional options for the contract. - # - # @option options collection_class [Class, String] the expected class for - # created collections. - # @option options entity_class [Class, String] the expected entity class. - contract do |abstract: false, **options| - shared_examples 'should create the collection' do - let(:configured_collection_class) do - return super() if defined?(super()) - - configured = options[:collection_class] - configured = Object.const_get(configured) if configured.is_a?(String) - - configured - end - let(:configured_entity_class) do - return super() if defined?(super()) - - # :nocov: - expected = - if collection_options.key?(:entity_class) - collection_options[:entity_class] - elsif options.key?(:entity_class) - options[:entity_class] - else - qualified_name - .split('/') - .then { |ary| [*ary[0...-1], tools.str.singularize(ary[-1])] } - .map { |str| tools.str.camelize(str) } - .join('::') - end - # :nocov: - expected = Object.const_get(expected) if expected.is_a?(String) - - expected - end - let(:configured_member_name) do - return super() if defined?(super()) - - tools.str.singularize(collection_name.to_s.split('/').last) - end - - def tools - SleepingKingStudios::Tools::Toolbelt.instance - end - - it 'should create the collection' do - create_collection(safe: false) - - expect(repository.key?(qualified_name)).to be true - end - - it 'should return the collection' do - collection = create_collection(safe: false) - - expect(collection).to be repository[qualified_name] - end - - it { expect(collection).to be_a configured_collection_class } - - it 'should set the entity class' do - expect(collection.entity_class).to be == configured_entity_class - end - - it 'should set the collection name' do - expect(collection.name).to be == collection_name.to_s - end - - it 'should set the member name' do - expect(collection.singular_name).to be == configured_member_name - end - - it 'should set the qualified name' do - expect(collection.qualified_name).to be == qualified_name - end - - it 'should set the collection options' do - expect(collection).to have_attributes( - primary_key_name: primary_key_name, - primary_key_type: primary_key_type - ) - end - end - - describe '#[]' do - let(:error_class) do - described_class::UndefinedCollectionError - end - let(:error_message) do - "repository does not define collection #{collection_name.inspect}" - end - - it { expect(repository).to respond_to(:[]).with(1).argument } - - describe 'with nil' do - let(:collection_name) { nil } - - it 'should raise an exception' do - expect { repository[collection_name] } - .to raise_error(error_class, error_message) - end - end - - describe 'with an object' do - let(:collection_name) { Object.new.freeze } - - it 'should raise an exception' do - expect { repository[collection_name] } - .to raise_error(error_class, error_message) - end - end - - describe 'with an invalid string' do - let(:collection_name) { 'invalid_name' } - - it 'should raise an exception' do - expect { repository[collection_name] } - .to raise_error(error_class, error_message) - end - end - - describe 'with an invalid symbol' do - let(:collection_name) { :invalid_name } - - it 'should raise an exception' do - expect { repository[collection_name] } - .to raise_error(error_class, error_message) - end - end - - wrap_context 'when the repository has many collections' do - describe 'with an invalid string' do - let(:collection_name) { 'invalid_name' } - - it 'should raise an exception' do - expect { repository[collection_name] } - .to raise_error(error_class, error_message) - end - end - - describe 'with an invalid symbol' do - let(:collection_name) { :invalid_name } - - it 'should raise an exception' do - expect { repository[collection_name] } - .to raise_error(error_class, error_message) - end - end - - describe 'with a valid string' do - let(:collection) { collections.values.first } - let(:collection_name) { collections.keys.first } - - it { expect(repository[collection_name]).to be collection } - end - - describe 'with a valid symbol' do - let(:collection) { collections.values.first } - let(:collection_name) { collections.keys.first.intern } - - it { expect(repository[collection_name]).to be collection } - end - end - end - - describe '#add' do - let(:error_class) do - described_class::InvalidCollectionError - end - let(:error_message) do - "#{collection.inspect} is not a valid collection" - end - - it 'should define the method' do - expect(repository) - .to respond_to(:add) - .with(1).argument - .and_keywords(:force) - end - - it 'should alias #add as #<<' do - expect(repository.method(:<<)).to be == repository.method(:add) - end - - describe 'with nil' do - let(:collection) { nil } - - it 'should raise an exception' do - expect { repository.add(collection) } - .to raise_error(error_class, error_message) - end - end - - describe 'with an object' do - let(:collection) { Object.new.freeze } - - it 'should raise an exception' do - expect { repository.add(collection) } - .to raise_error(error_class, error_message) - end - end - - describe 'with a collection' do - it { expect(repository.add(example_collection)).to be repository } - - it 'should add the collection to the repository' do - repository.add(example_collection) - - expect(repository[example_collection.qualified_name]) - .to be example_collection - end - - describe 'with force: true' do - it 'should add the collection to the repository' do - repository.add(example_collection, force: true) - - expect(repository[example_collection.qualified_name]) - .to be example_collection - end - end - - context 'when the collection already exists' do - let(:error_message) do - "collection #{example_collection.qualified_name} already exists" - end - - before(:example) do - allow(repository) - .to receive(:key?) - .with(example_collection.qualified_name) - .and_return(true) - end - - it 'should raise an exception' do - expect { repository.add(example_collection) } - .to raise_error( - described_class::DuplicateCollectionError, - error_message - ) - end - - it 'should not update the repository' do - begin - repository.add(example_collection) - rescue described_class::DuplicateCollectionError - # Do nothing. - end - - expect { repository[example_collection.qualified_name] } - .to raise_error( - described_class::UndefinedCollectionError, - 'repository does not define collection ' \ - "#{example_collection.qualified_name.inspect}" - ) - end - - describe 'with force: true' do - it 'should add the collection to the repository' do - repository.add(example_collection, force: true) - - expect(repository[example_collection.qualified_name]) - .to be example_collection - end - end - end - end - end - - describe '#create' do - let(:collection_name) { 'books' } - let(:qualified_name) { collection_name.to_s } - let(:primary_key_name) { 'id' } - let(:primary_key_type) { Integer } - let(:collection_options) { {} } - let(:collection) do - create_collection - - repository[qualified_name] - end - let(:error_message) do - "#{described_class.name} is an abstract class. Define a repository " \ - 'subclass and implement the #build_collection method.' - end - - def create_collection(force: false, safe: true, **options) - if safe - begin - repository.create(force: force, **collection_options, **options) - rescue StandardError - # Do nothing. - end - else - repository.create(force: force, **collection_options, **options) - end - end - - it 'should define the method' do - expect(repository) - .to respond_to(:create) - .with(0).arguments - .and_keywords(:collection_name, :entity_class, :force) - .and_any_keywords - end - - if abstract - it 'should raise an exception' do - expect { create_collection(safe: false) } - .to raise_error( - described_class::AbstractRepositoryError, - error_message - ) - end - - next - end - - describe 'with entity_class: a Class' do - let(:entity_class) { Book } - let(:collection_options) do - super().merge(entity_class: entity_class) - end - - include_examples 'should create the collection' - end - - describe 'with entity_class: a String' do - let(:entity_class) { 'Book' } - let(:collection_options) do - super().merge(entity_class: entity_class) - end - - include_examples 'should create the collection' - end - - describe 'with name: a String' do - let(:collection_name) { 'books' } - let(:collection_options) do - super().merge(name: collection_name) - end - - include_examples 'should create the collection' - end - - describe 'with name: a Symbol' do - let(:collection_name) { :books } - let(:collection_options) do - super().merge(name: collection_name) - end - - include_examples 'should create the collection' - end - - describe 'with collection options' do - let(:primary_key_name) { 'uuid' } - let(:primary_key_type) { String } - let(:collection_options) do - super().merge( - name: collection_name, - primary_key_name: primary_key_name, - primary_key_type: primary_key_type - ) - end - - include_examples 'should create the collection' - end - - context 'when the collection already exists' do - let(:collection_name) { 'books' } - let(:collection_options) do - super().merge(name: collection_name) - end - let(:error_message) do - "collection #{qualified_name} already exists" - end - - before { create_collection(old: true) } - - it 'should raise an exception' do - expect { create_collection(safe: false) } - .to raise_error( - described_class::DuplicateCollectionError, - error_message - ) - end - - it 'should not update the repository' do - create_collection(old: false) - - collection = repository[qualified_name] - - expect(collection.options[:old]).to be true - end - - describe 'with force: true' do - it 'should update the repository' do - create_collection(force: true, old: false) - - collection = repository[qualified_name] - - expect(collection.options[:old]).to be false - end - end - end - end - - describe '#find_or_create' do - let(:collection_name) { 'books' } - let(:qualified_name) { collection_name.to_s } - let(:primary_key_name) { 'id' } - let(:primary_key_type) { Integer } - let(:collection_options) { {} } - let(:collection) do - create_collection - - repository[qualified_name] - end - let(:error_message) do - "#{described_class.name} is an abstract class. Define a repository " \ - 'subclass and implement the #build_collection method.' - end - - def create_collection(safe: true, **options) - if safe - begin - repository.find_or_create(**collection_options, **options) - rescue StandardError - # Do nothing. - end - else - repository.find_or_create(**collection_options, **options) - end - end - - it 'should define the method' do - expect(repository) - .to respond_to(:find_or_create) - .with(0).arguments - .and_keywords(:entity_class) - .and_any_keywords - end - - if abstract - let(:collection_options) { { name: collection_name } } - - it 'should raise an exception' do - expect { create_collection(safe: false) } - .to raise_error( - described_class::AbstractRepositoryError, - error_message - ) - end - - next - end - - describe 'with entity_class: a Class' do - let(:entity_class) { Book } - let(:collection_options) do - super().merge(entity_class: entity_class) - end - - include_examples 'should create the collection' - end - - describe 'with entity_class: a String' do - let(:entity_class) { Book } - let(:collection_options) do - super().merge(entity_class: entity_class) - end - - include_examples 'should create the collection' - end - - describe 'with name: a String' do - let(:collection_name) { 'books' } - let(:collection_options) do - super().merge(name: collection_name) - end - - include_examples 'should create the collection' - end - - describe 'with name: a Symbol' do - let(:collection_name) { :books } - let(:collection_options) do - super().merge(name: collection_name) - end - - include_examples 'should create the collection' - end - - describe 'with collection options' do - let(:primary_key_name) { 'uuid' } - let(:primary_key_type) { String } - let(:qualified_name) { 'spec/scoped_books' } - let(:collection_options) do - super().merge( - name: collection_name, - primary_key_name: primary_key_name, - primary_key_type: primary_key_type, - qualified_name: qualified_name - ) - end - - include_examples 'should create the collection' - end - - context 'when the collection already exists' do - let(:collection_name) { 'books' } - let(:collection_options) do - super().merge(name: collection_name) - end - let(:error_message) do - "collection #{qualified_name} already exists" - end - - before { create_collection(old: true) } - - describe 'with non-matching options' do - it 'should raise an exception' do - expect { create_collection(old: false, safe: false) } - .to raise_error( - described_class::DuplicateCollectionError, - error_message - ) - end - - it 'should not update the repository' do - create_collection(old: false) - - collection = repository[qualified_name] - - expect(collection.options[:old]).to be true - end - end - - describe 'with matching options' do - it 'should return the collection' do - collection = create_collection(old: true) - - expect(collection.options[:old]).to be true - end - end - end - end - - describe '#key?' do - it { expect(repository).to respond_to(:key?).with(1).argument } - - it { expect(repository.key? nil).to be false } - - it { expect(repository.key? Object.new.freeze).to be false } - - it { expect(repository.key? 'invalid_name').to be false } - - it { expect(repository.key? :invalid_name).to be false } - - wrap_context 'when the repository has many collections' do - it { expect(repository.key? 'invalid_name').to be false } - - it { expect(repository.key? :invalid_name).to be false } - - it { expect(repository.key? collections.keys.first).to be true } - - it 'should include the key' do - expect(repository.key? collections.keys.first.intern).to be true - end - end - end - - describe '#keys' do - include_examples 'should define reader', :keys, [] - - wrap_context 'when the repository has many collections' do - it { expect(repository.keys).to be == collections.keys } - end - end - end - end -end diff --git a/spec/cuprum/collections/basic/collection_spec.rb b/spec/cuprum/collections/basic/collection_spec.rb index e1d4aaf..78b1054 100644 --- a/spec/cuprum/collections/basic/collection_spec.rb +++ b/spec/cuprum/collections/basic/collection_spec.rb @@ -2,7 +2,7 @@ require 'cuprum/collections/basic/collection' require 'cuprum/collections/basic/commands' -require 'cuprum/collections/rspec/collection_contract' +require 'cuprum/collections/rspec/contracts/collection_contracts' require 'cuprum/collections/rspec/fixtures' require 'support/book' @@ -10,6 +10,8 @@ require 'support/scoped_book' RSpec.describe Cuprum::Collections::Basic::Collection do + include Cuprum::Collections::RSpec::Contracts::CollectionContracts + subject(:collection) do described_class.new( data: data, @@ -38,7 +40,7 @@ end end - include_contract Cuprum::Collections::RSpec::CollectionContract, + include_contract 'should be a collection', command_options: %i[data default_contract], commands_namespace: 'Cuprum::Collections::Basic::Commands', default_entity_class: Hash diff --git a/spec/cuprum/collections/basic/query_builder_spec.rb b/spec/cuprum/collections/basic/query_builder_spec.rb index a68d0f5..2fcaed7 100644 --- a/spec/cuprum/collections/basic/query_builder_spec.rb +++ b/spec/cuprum/collections/basic/query_builder_spec.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true require 'cuprum/collections/basic/query_builder' -require 'cuprum/collections/rspec/query_builder_contract' +require 'cuprum/collections/rspec/contracts/query_contracts' RSpec.describe Cuprum::Collections::Basic::QueryBuilder do + include Cuprum::Collections::RSpec::Contracts::QueryContracts + shared_context 'when the query has criteria' do let(:base_query) do super().where do @@ -24,7 +26,7 @@ it { expect(described_class).to respond_to(:new).with(1).argument } end - include_contract Cuprum::Collections::RSpec::QUERY_BUILDER_CONTRACT + include_contract 'should be a query builder' describe '#call' do def match_item(expected) diff --git a/spec/cuprum/collections/basic/query_spec.rb b/spec/cuprum/collections/basic/query_spec.rb index b9a63bf..c871fb7 100644 --- a/spec/cuprum/collections/basic/query_spec.rb +++ b/spec/cuprum/collections/basic/query_spec.rb @@ -3,9 +3,11 @@ require 'sleeping_king_studios/tools/string_tools' require 'cuprum/collections/basic/query' -require 'cuprum/collections/rspec/query_contract' +require 'cuprum/collections/rspec/contracts/query_contracts' RSpec.describe Cuprum::Collections::Basic::Query do + include Cuprum::Collections::RSpec::Contracts::QueryContracts + subject(:query) { described_class.new(stringify_data(data)) } let(:data) { [] } @@ -27,5 +29,5 @@ def stringify_data(data) it { expect(described_class).to respond_to(:new).with(1).argument } end - include_contract Cuprum::Collections::RSpec::QUERY_CONTRACT + include_contract 'should be a query' end diff --git a/spec/cuprum/collections/basic/repository_spec.rb b/spec/cuprum/collections/basic/repository_spec.rb index 2267430..120d567 100644 --- a/spec/cuprum/collections/basic/repository_spec.rb +++ b/spec/cuprum/collections/basic/repository_spec.rb @@ -3,14 +3,14 @@ require 'cuprum/collections/basic/collection' require 'cuprum/collections/basic/repository' require 'cuprum/collections/rspec/fixtures' -require 'cuprum/collections/rspec/repository_contract' +require 'cuprum/collections/rspec/contracts/repository_contracts' require 'support/book' require 'support/grimoire' require 'support/scoped_book' RSpec.describe Cuprum::Collections::Basic::Repository do - include Cuprum::Collections::RSpec + include Cuprum::Collections::RSpec::Contracts::RepositoryContracts subject(:repository) { described_class.new(**constructor_options) } @@ -66,7 +66,7 @@ end end - include_contract Cuprum::Collections::RSpec::RepositoryContract, + include_contract 'should be a repository', collection_class: Cuprum::Collections::Basic::Collection, entity_class: Hash diff --git a/spec/cuprum/collections/collection_spec.rb b/spec/cuprum/collections/collection_spec.rb index 2f63e23..def3b6f 100644 --- a/spec/cuprum/collections/collection_spec.rb +++ b/spec/cuprum/collections/collection_spec.rb @@ -1,15 +1,14 @@ # frozen_string_literal: true require 'cuprum/collections/collection' -require 'cuprum/collections/rspec/collection_contract' -require 'cuprum/collections/rspec/contracts/relation_contracts' +require 'cuprum/collections/rspec/contracts/collection_contracts' require 'support/book' require 'support/grimoire' require 'support/scoped_book' RSpec.describe Cuprum::Collections::Collection do - include Cuprum::Collections::RSpec::Contracts::RelationContracts + include Cuprum::Collections::RSpec::Contracts::CollectionContracts subject(:collection) do described_class.new(**constructor_options) @@ -55,6 +54,5 @@ def call_method(**parameters) include_contract 'should validate the parameters' end - include_contract Cuprum::Collections::RSpec::CollectionContract, - abstract: true + include_contract 'should be a collection', abstract: true end diff --git a/spec/cuprum/collections/query_builder_spec.rb b/spec/cuprum/collections/query_builder_spec.rb index 4293c0d..48251bb 100644 --- a/spec/cuprum/collections/query_builder_spec.rb +++ b/spec/cuprum/collections/query_builder_spec.rb @@ -2,9 +2,11 @@ require 'cuprum/collections/query' require 'cuprum/collections/query_builder' -require 'cuprum/collections/rspec/query_builder_contract' +require 'cuprum/collections/rspec/contracts/query_contracts' RSpec.describe Cuprum::Collections::QueryBuilder do + include Cuprum::Collections::RSpec::Contracts::QueryContracts + subject(:builder) { described_class.new(base_query) } let(:base_query) { Cuprum::Collections::Query.new } @@ -19,5 +21,5 @@ it { expect(described_class).to respond_to(:new).with(1).argument } end - include_contract Cuprum::Collections::RSpec::QUERY_BUILDER_CONTRACT + include_contract 'should be a query builder' end diff --git a/spec/cuprum/collections/repository_spec.rb b/spec/cuprum/collections/repository_spec.rb index 96c2302..7c8c66b 100644 --- a/spec/cuprum/collections/repository_spec.rb +++ b/spec/cuprum/collections/repository_spec.rb @@ -2,12 +2,12 @@ require 'cuprum/collections/basic/collection' require 'cuprum/collections/repository' -require 'cuprum/collections/rspec/repository_contract' +require 'cuprum/collections/rspec/contracts/repository_contracts' require 'support/book' RSpec.describe Cuprum::Collections::Repository do - include Cuprum::Collections::RSpec + include Cuprum::Collections::RSpec::Contracts::RepositoryContracts subject(:repository) { described_class.new } @@ -101,6 +101,5 @@ it { expect(described_class).to respond_to(:new).with(0).arguments } end - include_contract Cuprum::Collections::RSpec::RepositoryContract, - abstract: true + include_contract 'should be a repository', abstract: true end From fced2c5f25355c3c5b43db916f1fa12f5986176d Mon Sep 17 00:00:00 2001 From: Rob Smith Date: Mon, 27 Nov 2023 21:57:56 -0500 Subject: [PATCH 2/4] Refactor legacy command contracts. --- .rubocop.yml | 2 + .../basic/rspec/command_contract.rb | 392 ----- .../rspec/assign_one_command_contract.rb | 168 -- .../rspec/build_one_command_contract.rb | 93 -- lib/cuprum/collections/rspec/contracts.rb | 4 + .../rspec/contracts/association_contracts.rb | 9 + .../collections/rspec/contracts/basic.rb | 11 + .../contracts/basic/command_contracts.rb | 484 ++++++ .../rspec/contracts/collection_contracts.rb | 2 +- .../rspec/contracts/command_contracts.rb | 1462 +++++++++++++++++ .../rspec/contracts/query_contracts.rb | 15 +- .../rspec/contracts/relation_contracts.rb | 17 +- .../rspec/contracts/repository_contracts.rb | 2 +- .../rspec/destroy_one_command_contract.rb | 108 -- .../rspec/find_many_command_contract.rb | 407 ----- .../rspec/find_matching_command_contract.rb | 195 --- .../rspec/find_one_command_contract.rb | 157 -- .../rspec/insert_one_command_contract.rb | 87 - .../rspec/update_one_command_contract.rb | 83 - .../rspec/validate_one_command_contract.rb | 96 -- spec/cuprum/collections/basic/command_spec.rb | 6 +- .../basic/commands/assign_one_spec.rb | 19 +- .../basic/commands/build_one_spec.rb | 15 +- .../basic/commands/destroy_one_spec.rb | 17 +- .../basic/commands/find_many_spec.rb | 17 +- .../basic/commands/find_matching_spec.rb | 15 +- .../basic/commands/find_one_spec.rb | 17 +- .../basic/commands/insert_one_spec.rb | 17 +- .../basic/commands/update_one_spec.rb | 17 +- .../basic/commands/validate_one_spec.rb | 17 +- spec/spec_helper.rb | 9 +- .../examples/basic_command_examples.rb | 72 - 32 files changed, 2094 insertions(+), 1938 deletions(-) delete mode 100644 lib/cuprum/collections/basic/rspec/command_contract.rb delete mode 100644 lib/cuprum/collections/rspec/assign_one_command_contract.rb delete mode 100644 lib/cuprum/collections/rspec/build_one_command_contract.rb create mode 100644 lib/cuprum/collections/rspec/contracts/basic.rb create mode 100644 lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb create mode 100644 lib/cuprum/collections/rspec/contracts/command_contracts.rb delete mode 100644 lib/cuprum/collections/rspec/destroy_one_command_contract.rb delete mode 100644 lib/cuprum/collections/rspec/find_many_command_contract.rb delete mode 100644 lib/cuprum/collections/rspec/find_matching_command_contract.rb delete mode 100644 lib/cuprum/collections/rspec/find_one_command_contract.rb delete mode 100644 lib/cuprum/collections/rspec/insert_one_command_contract.rb delete mode 100644 lib/cuprum/collections/rspec/update_one_command_contract.rb delete mode 100644 lib/cuprum/collections/rspec/validate_one_command_contract.rb delete mode 100644 spec/support/examples/basic_command_examples.rb diff --git a/.rubocop.yml b/.rubocop.yml index e46184a..89aeabe 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -24,6 +24,8 @@ RSpec: - fdescribe - fwrap_context Includes: + Contexts: + - with_contract Examples: - finclude_contract - finclude_examples diff --git a/lib/cuprum/collections/basic/rspec/command_contract.rb b/lib/cuprum/collections/basic/rspec/command_contract.rb deleted file mode 100644 index 1e5dd84..0000000 --- a/lib/cuprum/collections/basic/rspec/command_contract.rb +++ /dev/null @@ -1,392 +0,0 @@ -# frozen_string_literal: true - -require 'cuprum/collections/basic/rspec' - -module Cuprum::Collections::Basic::RSpec - # Contract validating the behavior of a basic command implementation. - COMMAND_CONTRACT = lambda do - describe '.subclass' do - let(:subclass) { described_class.subclass } - let(:constructor_options) do - { - collection_name: 'books', - data: data, - optional_key: 'optional value' - } - end - - it 'should define the class method' do - expect(described_class) - .to respond_to(:subclass) - .with(0).arguments - .and_any_keywords - end - - it { expect(subclass).to be_a Class } - - it { expect(subclass).to be < described_class } - - it 'should define the constructor' do - expect(subclass) - .to respond_to(:new) - .with(0).arguments - .and_any_keywords - end - - it 'should return the collection name' do - expect(subclass.new(**constructor_options).collection_name) - .to be collection_name - end - - it 'should return the data' do - expect(subclass.new(**constructor_options).data) - .to be data - end - - it 'should return the options' do - expect(subclass.new(**constructor_options).options) - .to be == { optional_key: 'optional value' } - end - - describe 'with options' do - let(:default_options) do - { - collection_name: 'books', - custom_key: 'custom value' - } - end - let(:constructor_options) do - { - data: data, - optional_key: 'optional value' - } - end - let(:subclass) { described_class.subclass(**default_options) } - - it { expect(subclass).to be_a Class } - - it { expect(subclass).to be < described_class } - - it 'should define the constructor' do - expect(subclass) - .to respond_to(:new) - .with(0).arguments - .and_any_keywords - end - - it 'should return the collection name' do - expect(subclass.new(**constructor_options).collection_name) - .to be collection_name - end - - it 'should return the data' do - expect(subclass.new(**constructor_options).data) - .to be data - end - - it 'should return the options' do - expect(subclass.new(**constructor_options).options) - .to be == { - custom_key: 'custom value', - optional_key: 'optional value' - } - end - end - end - - describe '#collection_name' do - include_examples 'should have reader', - :collection_name, - -> { collection_name } - - context 'when initialized with collection_name: symbol' do - let(:collection_name) { :books } - - it { expect(command.collection_name).to be == collection_name.to_s } - end - end - - describe '#data' do - include_examples 'should define reader', :data, -> { data } - end - - describe '#default_contract' do - include_examples 'should define reader', :default_contract, nil - - context 'when initialized with a default contract' do - let(:default_contract) { Stannum::Contract.new } - let(:constructor_options) do - super().merge(default_contract: default_contract) - end - - it { expect(command.default_contract).to be default_contract } - end - end - - describe '#member_name' do - def tools - SleepingKingStudios::Tools::Toolbelt.instance - end - - include_examples 'should have reader', - :member_name, - -> { tools.str.singularize(collection_name) } - - context 'when initialized with collection_name: value' do - let(:collection_name) { :books } - - it 'should return the singular collection name' do - expect(command.member_name) - .to be == tools.str.singularize(collection_name.to_s) - end - end - - context 'when initialized with member_name: string' do - let(:member_name) { 'tome' } - let(:constructor_options) { super().merge(member_name: member_name) } - - it 'should return the singular collection name' do - expect(command.member_name).to be member_name - end - end - - context 'when initialized with member_name: symbol' do - let(:member_name) { :tome } - let(:constructor_options) { super().merge(member_name: member_name) } - - it 'should return the singular collection name' do - expect(command.member_name).to be == member_name.to_s - end - end - end - - describe '#options' do - let(:expected_options) do - defined?(super()) ? super() : constructor_options - end - - include_examples 'should define reader', - :options, - -> { be == expected_options } - - context 'when initialized with options' do - let(:constructor_options) { super().merge({ key: 'value' }) } - let(:expected_options) { super().merge({ key: 'value' }) } - - it { expect(command.options).to be == expected_options } - end - end - - describe '#primary_key_name' do - include_examples 'should define reader', :primary_key_name, :id - - context 'when initialized with a primary key name' do - let(:primary_key_name) { :uuid } - let(:constructor_options) do - super().merge({ primary_key_name: primary_key_name }) - end - - it { expect(command.primary_key_name).to be == primary_key_name } - end - end - - describe '#primary_key_type' do - include_examples 'should define reader', :primary_key_type, Integer - - context 'when initialized with a primary key type' do - let(:primary_key_type) { String } - let(:constructor_options) do - super().merge({ primary_key_type: primary_key_type }) - end - - it { expect(command.primary_key_type).to be == primary_key_type } - end - end - - describe '#validate_primary_key' do - let(:primary_key_type) { Integer } - let(:expected_error) do - type = primary_key_type - contract = Stannum::Contracts::ParametersContract.new do - keyword :primary_key, type - end - errors = contract.errors_for( - { - arguments: [], - block: nil, - keywords: { primary_key: nil } - } - ) - - Cuprum::Collections::Errors::InvalidParameters.new( - command: command, - errors: errors - ) - end - - it 'should define the private method' do - expect(command) - .to respond_to(:validate_primary_key, true) - .with(1).argument - end - - describe 'with nil' do - it 'should return a failing result' do - expect(command.send(:validate_primary_key, nil)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with an Object' do - it 'should return a failing result' do - expect(command.send(:validate_primary_key, Object.new.freeze)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with a String' do - it 'should return a failing result' do - expect(command.send(:validate_primary_key, '12345')) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with an Integer' do - it 'should not return a result' do - expect(command.send(:validate_primary_key, 12_345)).not_to be_a_result - end - end - - context 'when initialized with a primary key type' do - let(:primary_key_type) { String } - let(:constructor_options) do - super().merge({ primary_key_type: primary_key_type }) - end - - describe 'with an Integer' do - it 'should return a failing result' do - expect(command.send(:validate_primary_key, 12_345)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with a String' do - it 'should not return a result' do - expect(command.send(:validate_primary_key, '12345')) - .not_to be_a_result - end - end - end - end - - describe '#validate_primary_keys' do - let(:primary_keys) { nil } - let(:primary_key_type) { Integer } - let(:expected_error) do - type = primary_key_type - contract = Stannum::Contracts::ParametersContract.new do - keyword :primary_keys, - Stannum::Constraints::Types::ArrayType.new(item_type: type) - end - errors = contract.errors_for( - { - arguments: [], - block: nil, - keywords: { primary_keys: primary_keys } - } - ) - - Cuprum::Collections::Errors::InvalidParameters.new( - command: command, - errors: errors - ) - end - - it 'should define the private method' do - expect(command) - .to respond_to(:validate_primary_keys, true) - .with(1).argument - end - - describe 'with nil' do - it 'should return a failing result' do - expect(command.send(:validate_primary_keys, nil)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with an Object' do - it 'should return a failing result' do - expect(command.send(:validate_primary_keys, Object.new.freeze)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with a String' do - it 'should return a failing result' do - expect(command.send(:validate_primary_keys, '12345')) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with an Integer' do - it 'should return a failing result' do - expect(command.send(:validate_primary_keys, 12_345)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with an empty Array' do - it 'should not return a result' do - expect(command.send(:validate_primary_keys, [])) - .not_to be_a_result - end - end - - describe 'with an Array with nil values' do - let(:primary_keys) { Array.new(3, nil) } - - it 'should return a failing result' do - expect(command.send(:validate_primary_keys, primary_keys)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with an Array with Object values' do - let(:primary_keys) { Array.new(3) { Object.new.freeze } } - - it 'should return a failing result' do - expect(command.send(:validate_primary_keys, primary_keys)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with an Array with String values' do - let(:primary_keys) { %w[ichi ni san] } - - it 'should return a failing result' do - expect(command.send(:validate_primary_keys, primary_keys)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with an Array with Integer values' do - it 'should not return a result' do - expect(command.send(:validate_primary_keys, [0, 1, 2])) - .not_to be_a_result - end - end - end - end -end diff --git a/lib/cuprum/collections/rspec/assign_one_command_contract.rb b/lib/cuprum/collections/rspec/assign_one_command_contract.rb deleted file mode 100644 index e4afa0f..0000000 --- a/lib/cuprum/collections/rspec/assign_one_command_contract.rb +++ /dev/null @@ -1,168 +0,0 @@ -# frozen_string_literal: true - -require 'stannum/constraints/presence' -require 'stannum/constraints/types/hash_with_indifferent_keys' -require 'stannum/rspec/validate_parameter' - -require 'cuprum/collections/rspec' - -module Cuprum::Collections::RSpec - # Contract validating the behavior of an Assign command implementation. - ASSIGN_ONE_COMMAND_CONTRACT = lambda do |allow_extra_attributes:| - describe '#call' do - shared_examples 'should assign the attributes' do - it { expect(result).to be_a_passing_result } - - it { expect(result.value).to be_a entity.class } - - it { expect(result.value).to be == expected_value } - end - - let(:attributes) { {} } - let(:result) { command.call(attributes: attributes, entity: entity) } - let(:expected_attributes) do - initial_attributes.merge(attributes) - end - let(:expected_value) do - defined?(super()) ? super() : expected_attributes - end - - it 'should validate the :attributes keyword' do - expect(command) - .to validate_parameter(:call, :attributes) - .using_constraint( - Stannum::Constraints::Types::HashWithIndifferentKeys.new - ) - end - - it 'should validate the :entity keyword' do - expect(command) - .to validate_parameter(:call, :entity) - .using_constraint(entity_type) - .with_parameters(attributes: {}, entity: nil) - end - - describe 'with an empty attributes hash' do - let(:attributes) { {} } - - include_examples 'should assign the attributes' - end - - describe 'with an attributes hash with partial attributes' do - let(:attributes) { { title: 'Gideon the Ninth' } } - - include_examples 'should assign the attributes' - end - - describe 'with an attributes hash with full attributes' do - let(:attributes) do - { - title: 'Gideon the Ninth', - author: 'Tamsyn Muir', - series: 'The Locked Tomb', - category: 'Horror' - } - end - - include_examples 'should assign the attributes' - end - - describe 'with an attributes hash with extra attributes' do - let(:attributes) do - { - title: 'The Book of Lost Tales', - audiobook: true - } - end - - if allow_extra_attributes - include_examples 'should assign the attributes' - else - # :nocov: - let(:valid_attributes) do - defined?(super()) ? super() : expected_attributes.keys - end - let(:expected_error) do - Cuprum::Collections::Errors::ExtraAttributes.new( - entity_class: entity.class, - extra_attributes: %w[audiobook], - valid_attributes: valid_attributes - ) - end - - it 'should return a failing result' do - expect(result).to be_a_failing_result.with_error(expected_error) - end - # :nocov: - end - end - - context 'when the entity has existing attributes' do - let(:initial_attributes) do - # :nocov: - if defined?(super()) - super().merge(fixtures_data.first) - else - fixtures_data.first - end - # :nocov: - end - - describe 'with an empty attributes hash' do - let(:attributes) { {} } - - include_examples 'should assign the attributes' - end - - describe 'with an attributes hash with partial attributes' do - let(:attributes) { { title: 'Gideon the Ninth' } } - - include_examples 'should assign the attributes' - end - - describe 'with an attributes hash with full attributes' do - let(:attributes) do - { - title: 'Gideon the Ninth', - author: 'Tamsyn Muir', - series: 'The Locked Tomb', - category: 'Horror' - } - end - - include_examples 'should assign the attributes' - end - - describe 'with an attributes hash with extra attributes' do - let(:attributes) do - { - title: 'The Book of Lost Tales', - audiobook: true - } - end - - if allow_extra_attributes - include_examples 'should assign the attributes' - else - # :nocov: - let(:valid_attributes) do - defined?(super()) ? super() : expected_attributes.keys - end - let(:expected_error) do - Cuprum::Collections::Errors::ExtraAttributes.new( - entity_class: entity.class, - extra_attributes: %w[audiobook], - valid_attributes: valid_attributes - ) - end - - it 'should return a failing result' do - expect(result).to be_a_failing_result.with_error(expected_error) - end - # :nocov: - end - end - end - end - end -end diff --git a/lib/cuprum/collections/rspec/build_one_command_contract.rb b/lib/cuprum/collections/rspec/build_one_command_contract.rb deleted file mode 100644 index d6a225e..0000000 --- a/lib/cuprum/collections/rspec/build_one_command_contract.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -require 'stannum/constraints/types/hash_with_indifferent_keys' -require 'stannum/rspec/validate_parameter' - -require 'cuprum/collections/rspec' - -module Cuprum::Collections::RSpec - # Contract validating the behavior of a Build command implementation. - BUILD_ONE_COMMAND_CONTRACT = lambda do |allow_extra_attributes:| - include Stannum::RSpec::Matchers - - describe '#call' do - shared_examples 'should build the entity' do - it { expect(result).to be_a_passing_result } - - it { expect(result.value).to be == expected_value } - end - - let(:attributes) { {} } - let(:result) { command.call(attributes: attributes) } - let(:expected_attributes) do - attributes - end - let(:expected_value) do - defined?(super()) ? super() : attributes - end - - it 'should validate the :attributes keyword' do - expect(command) - .to validate_parameter(:call, :attributes) - .using_constraint( - Stannum::Constraints::Types::HashWithIndifferentKeys.new - ) - end - - describe 'with an empty attributes hash' do - let(:attributes) { {} } - - include_examples 'should build the entity' - end - - describe 'with an attributes hash with partial attributes' do - let(:attributes) { { title: 'Gideon the Ninth' } } - - include_examples 'should build the entity' - end - - describe 'with an attributes hash with full attributes' do - let(:attributes) do - { - title: 'Gideon the Ninth', - author: 'Tamsyn Muir', - series: 'The Locked Tomb', - category: 'Horror' - } - end - - include_examples 'should build the entity' - end - - describe 'with an attributes hash with extra attributes' do - let(:attributes) do - { - title: 'The Book of Lost Tales', - audiobook: true - } - end - - if allow_extra_attributes - include_examples 'should build the entity' - else - # :nocov: - let(:valid_attributes) do - defined?(super()) ? super() : expected_attributes.keys - end - let(:expected_error) do - Cuprum::Collections::Errors::ExtraAttributes.new( - entity_class: entity_type, - extra_attributes: %w[audiobook], - valid_attributes: valid_attributes - ) - end - - it 'should return a failing result' do - expect(result).to be_a_failing_result.with_error(expected_error) - end - # :nocov: - end - end - end - end -end diff --git a/lib/cuprum/collections/rspec/contracts.rb b/lib/cuprum/collections/rspec/contracts.rb index cbdf5ac..8aff0ac 100644 --- a/lib/cuprum/collections/rspec/contracts.rb +++ b/lib/cuprum/collections/rspec/contracts.rb @@ -7,8 +7,12 @@ module Cuprum::Collections::RSpec module Contracts autoload :AssociationContracts, 'cuprum/collections/rspec/contracts/association_contracts' + autoload :Basic, + 'cuprum/collections/rspec/contracts/basic' autoload :CollectionContracts, 'cuprum/collections/rspec/contracts/collection_contracts' + autoload :CommandContracts, + 'cuprum/collections/rspec/contracts/command_contracts' autoload :QueryContracts, 'cuprum/collections/rspec/contracts/query_contracts' autoload :RelationContracts, diff --git a/lib/cuprum/collections/rspec/contracts/association_contracts.rb b/lib/cuprum/collections/rspec/contracts/association_contracts.rb index 4721a7d..ec634a5 100644 --- a/lib/cuprum/collections/rspec/contracts/association_contracts.rb +++ b/lib/cuprum/collections/rspec/contracts/association_contracts.rb @@ -12,6 +12,9 @@ module ShouldBeAnAssociationContract # @!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 include Cuprum::Collections::RSpec::Contracts::RelationContracts @@ -453,6 +456,9 @@ module ShouldBeABelongsToAssociationContract # @!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 include Cuprum::Collections::RSpec::Contracts::RelationContracts @@ -1154,6 +1160,9 @@ module ShouldBeAHasAssociationContract # @!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 include Cuprum::Collections::RSpec::Contracts::RelationContracts diff --git a/lib/cuprum/collections/rspec/contracts/basic.rb b/lib/cuprum/collections/rspec/contracts/basic.rb new file mode 100644 index 0000000..aa36071 --- /dev/null +++ b/lib/cuprum/collections/rspec/contracts/basic.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'cuprum/collections/rspec/contracts' + +module Cuprum::Collections::RSpec::Contracts + # Namespace for RSpec contract objects for Basic collections. + module Basic + autoload :CommandContracts, + 'cuprum/collections/rspec/contracts/basic/command_contracts' + end +end diff --git a/lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb b/lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb new file mode 100644 index 0000000..de3f7d9 --- /dev/null +++ b/lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb @@ -0,0 +1,484 @@ +# frozen_string_literal: true + +require 'cuprum/collections/rspec/contracts/basic' + +module Cuprum::Collections::RSpec::Contracts::Basic + # Contracts for asserting on Basic::Command objects. + module CommandContracts + # Contract validating the behavior of a basic command implementation. + module ShouldBeABasicCommandContract + 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 + describe '.subclass' do + let(:subclass) { described_class.subclass } + let(:constructor_options) do + { + collection_name: 'books', + data: data, + optional_key: 'optional value' + } + end + + it 'should define the class method' do + expect(described_class) + .to respond_to(:subclass) + .with(0).arguments + .and_any_keywords + end + + it { expect(subclass).to be_a Class } + + it { expect(subclass).to be < described_class } + + it 'should define the constructor' do + expect(subclass) + .to respond_to(:new) + .with(0).arguments + .and_any_keywords + end + + it 'should return the collection name' do + expect(subclass.new(**constructor_options).collection_name) + .to be collection_name + end + + it 'should return the data' do + expect(subclass.new(**constructor_options).data) + .to be data + end + + it 'should return the options' do + expect(subclass.new(**constructor_options).options) + .to be == { optional_key: 'optional value' } + end + + describe 'with options' do + let(:default_options) do + { + collection_name: 'books', + custom_key: 'custom value' + } + end + let(:constructor_options) do + { + data: data, + optional_key: 'optional value' + } + end + let(:subclass) { described_class.subclass(**default_options) } + + it { expect(subclass).to be_a Class } + + it { expect(subclass).to be < described_class } + + it 'should define the constructor' do + expect(subclass) + .to respond_to(:new) + .with(0).arguments + .and_any_keywords + end + + it 'should return the collection name' do + expect(subclass.new(**constructor_options).collection_name) + .to be collection_name + end + + it 'should return the data' do + expect(subclass.new(**constructor_options).data) + .to be data + end + + it 'should return the options' do + expect(subclass.new(**constructor_options).options) + .to be == { + custom_key: 'custom value', + optional_key: 'optional value' + } + end + end + end + + describe '#collection_name' do + include_examples 'should have reader', + :collection_name, + -> { collection_name } + + context 'when initialized with collection_name: symbol' do + let(:collection_name) { :books } + + it { expect(command.collection_name).to be == collection_name.to_s } + end + end + + describe '#data' do + include_examples 'should define reader', :data, -> { data } + end + + describe '#default_contract' do + include_examples 'should define reader', :default_contract, nil + + context 'when initialized with a default contract' do + let(:default_contract) { Stannum::Contract.new } + let(:constructor_options) do + super().merge(default_contract: default_contract) + end + + it { expect(command.default_contract).to be default_contract } + end + end + + describe '#member_name' do + def tools + SleepingKingStudios::Tools::Toolbelt.instance + end + + include_examples 'should have reader', + :member_name, + -> { tools.str.singularize(collection_name) } + + context 'when initialized with collection_name: value' do + let(:collection_name) { :books } + + it 'should return the singular collection name' do + expect(command.member_name) + .to be == tools.str.singularize(collection_name.to_s) + end + end + + context 'when initialized with member_name: string' do + let(:member_name) { 'tome' } + let(:constructor_options) do + super().merge(member_name: member_name) + end + + it 'should return the singular collection name' do + expect(command.member_name).to be member_name + end + end + + context 'when initialized with member_name: symbol' do + let(:member_name) { :tome } + let(:constructor_options) do + super().merge(member_name: member_name) + end + + it 'should return the singular collection name' do + expect(command.member_name).to be == member_name.to_s + end + end + end + + describe '#options' do + let(:expected_options) do + defined?(super()) ? super() : constructor_options + end + + include_examples 'should define reader', + :options, + -> { be == expected_options } + + context 'when initialized with options' do + let(:constructor_options) { super().merge({ key: 'value' }) } + let(:expected_options) { super().merge({ key: 'value' }) } + + it { expect(command.options).to be == expected_options } + end + end + + describe '#primary_key_name' do + include_examples 'should define reader', :primary_key_name, :id + + context 'when initialized with a primary key name' do + let(:primary_key_name) { :uuid } + let(:constructor_options) do + super().merge({ primary_key_name: primary_key_name }) + end + + it { expect(command.primary_key_name).to be == primary_key_name } + end + end + + describe '#primary_key_type' do + include_examples 'should define reader', :primary_key_type, Integer + + context 'when initialized with a primary key type' do + let(:primary_key_type) { String } + let(:constructor_options) do + super().merge({ primary_key_type: primary_key_type }) + end + + it { expect(command.primary_key_type).to be == primary_key_type } + end + end + + describe '#validate_primary_key' do + let(:primary_key_type) { Integer } + let(:expected_error) do + type = primary_key_type + contract = Stannum::Contracts::ParametersContract.new do + keyword :primary_key, type + end + errors = contract.errors_for( + { + arguments: [], + block: nil, + keywords: { primary_key: nil } + } + ) + + Cuprum::Collections::Errors::InvalidParameters.new( + command: command, + errors: errors + ) + end + + it 'should define the private method' do + expect(command) + .to respond_to(:validate_primary_key, true) + .with(1).argument + end + + describe 'with nil' do + it 'should return a failing result' do + expect(command.send(:validate_primary_key, nil)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with an Object' do + it 'should return a failing result' do + expect(command.send(:validate_primary_key, Object.new.freeze)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with a String' do + it 'should return a failing result' do + expect(command.send(:validate_primary_key, '12345')) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with an Integer' do + it 'should not return a result' do + expect(command.send(:validate_primary_key, 12_345)) + .not_to be_a_result + end + end + + context 'when initialized with a primary key type' do + let(:primary_key_type) { String } + let(:constructor_options) do + super().merge({ primary_key_type: primary_key_type }) + end + + describe 'with an Integer' do + it 'should return a failing result' do + expect(command.send(:validate_primary_key, 12_345)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with a String' do + it 'should not return a result' do + expect(command.send(:validate_primary_key, '12345')) + .not_to be_a_result + end + end + end + end + + describe '#validate_primary_keys' do + let(:primary_keys) { nil } + let(:primary_key_type) { Integer } + let(:expected_error) do + type = primary_key_type + contract = Stannum::Contracts::ParametersContract.new do + keyword :primary_keys, + Stannum::Constraints::Types::ArrayType.new(item_type: type) + end + errors = contract.errors_for( + { + arguments: [], + block: nil, + keywords: { primary_keys: primary_keys } + } + ) + + Cuprum::Collections::Errors::InvalidParameters.new( + command: command, + errors: errors + ) + end + + it 'should define the private method' do + expect(command) + .to respond_to(:validate_primary_keys, true) + .with(1).argument + end + + describe 'with nil' do + it 'should return a failing result' do + expect(command.send(:validate_primary_keys, nil)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with an Object' do + it 'should return a failing result' do + expect(command.send(:validate_primary_keys, Object.new.freeze)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with a String' do + it 'should return a failing result' do + expect(command.send(:validate_primary_keys, '12345')) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with an Integer' do + it 'should return a failing result' do + expect(command.send(:validate_primary_keys, 12_345)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with an empty Array' do + it 'should not return a result' do + expect(command.send(:validate_primary_keys, [])) + .not_to be_a_result + end + end + + describe 'with an Array with nil values' do + let(:primary_keys) { Array.new(3, nil) } + + it 'should return a failing result' do + expect(command.send(:validate_primary_keys, primary_keys)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with an Array with Object values' do + let(:primary_keys) { Array.new(3) { Object.new.freeze } } + + it 'should return a failing result' do + expect(command.send(:validate_primary_keys, primary_keys)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with an Array with String values' do + let(:primary_keys) { %w[ichi ni san] } + + it 'should return a failing result' do + expect(command.send(:validate_primary_keys, primary_keys)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with an Array with Integer values' do + it 'should not return a result' do + expect(command.send(:validate_primary_keys, [0, 1, 2])) + .not_to be_a_result + end + end + end + end + end + + # Contract defining contexts for validating basic commands. + module WithBasicCommandContextsContract + 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 parameters for a basic contract' do + let(:collection_name) { 'books' } + let(:data) { [] } + let(:mapped_data) { data } + let(:constructor_options) { {} } + let(:expected_options) { {} } + let(:primary_key_name) { :id } + let(:primary_key_type) { Integer } + let(:entity_type) do + Stannum::Constraints::Types::HashWithStringKeys.new + end + let(:fixtures_data) do + Cuprum::Collections::RSpec::BOOKS_FIXTURES.dup + end + let(:query) do + Cuprum::Collections::Basic::Query.new(mapped_data) + end + let(:scope) do + Cuprum::Collections::Basic::Query + .new(mapped_data).where(scope_filter) + end + end + + shared_context 'with a custom primary key' do + let(:primary_key_name) { :uuid } + let(:primary_key_type) { String } + let(:constructor_options) do + super().merge( + primary_key_name: primary_key_name, + primary_key_type: primary_key_type + ) + end + let(:mapped_data) do + data.map do |item| + item.dup.tap do |hsh| + value = hsh.delete('id').to_s.rjust(12, '0') + + hsh['uuid'] = "00000000-0000-0000-0000-#{value}" + end + end + end + let(:invalid_primary_key_value) do + '00000000-0000-0000-0000-000000000100' + end + let(:valid_primary_key_value) do + '00000000-0000-0000-0000-000000000000' + end + let(:invalid_primary_key_values) do + %w[ + 00000000-0000-0000-0000-000000000100 + 00000000-0000-0000-0000-000000000101 + 00000000-0000-0000-0000-000000000102 + ] + end + let(:valid_primary_key_values) do + %w[ + 00000000-0000-0000-0000-000000000000 + 00000000-0000-0000-0000-000000000001 + 00000000-0000-0000-0000-000000000002 + ] + end + end + end + end + end +end diff --git a/lib/cuprum/collections/rspec/contracts/collection_contracts.rb b/lib/cuprum/collections/rspec/contracts/collection_contracts.rb index 3cf9dc6..a27b65a 100644 --- a/lib/cuprum/collections/rspec/contracts/collection_contracts.rb +++ b/lib/cuprum/collections/rspec/contracts/collection_contracts.rb @@ -15,7 +15,7 @@ module ShouldBeACollectionContract # @!method apply(example_group) # Adds the contract to the example group. # - # @param example_group [RSpec::Core::ExampleGroup] The example group to + # @param example_group [RSpec::Core::ExampleGroup] the example group to # which the contract is applied. # @param options [Hash] additional options for the contract. # diff --git a/lib/cuprum/collections/rspec/contracts/command_contracts.rb b/lib/cuprum/collections/rspec/contracts/command_contracts.rb new file mode 100644 index 0000000..28a7291 --- /dev/null +++ b/lib/cuprum/collections/rspec/contracts/command_contracts.rb @@ -0,0 +1,1462 @@ +# frozen_string_literal: true + +require 'cuprum/collections/rspec/contracts' + +module Cuprum::Collections::RSpec::Contracts + # Contracts for asserting on Command objects. + module CommandContracts + # Contract validating the behavior of an AssignOne command implementation. + module ShouldBeAnAssignOneCommandContract + extend RSpec::SleepingKingStudios::Contract + + # @!method apply(example_group, allow_extra_attributes:) + # Adds the contract to the example group. + # + # @param example_group [RSpec::Core::ExampleGroup] the example group to + # which the contract is applied. + # @param allow_extra_attributes [Boolean] if false, the command should + # fail if given attributes not defined for the entity. + contract do |allow_extra_attributes:| + describe '#call' do + shared_examples 'should assign the attributes' do + it { expect(result).to be_a_passing_result } + + it { expect(result.value).to be_a entity.class } + + it { expect(result.value).to be == expected_value } + end + + let(:attributes) { {} } + let(:result) do + command.call(attributes: attributes, entity: entity) + end + let(:expected_attributes) do + initial_attributes.merge(attributes) + end + let(:expected_value) do + defined?(super()) ? super() : expected_attributes + end + + it 'should validate the :attributes keyword' do + expect(command) + .to validate_parameter(:call, :attributes) + .using_constraint( + Stannum::Constraints::Types::HashWithIndifferentKeys.new + ) + end + + it 'should validate the :entity keyword' do + expect(command) + .to validate_parameter(:call, :entity) + .using_constraint(entity_type) + .with_parameters(attributes: {}, entity: nil) + end + + describe 'with an empty attributes hash' do + let(:attributes) { {} } + + include_examples 'should assign the attributes' + end + + describe 'with an attributes hash with partial attributes' do + let(:attributes) { { title: 'Gideon the Ninth' } } + + include_examples 'should assign the attributes' + end + + describe 'with an attributes hash with full attributes' do + let(:attributes) do + { + title: 'Gideon the Ninth', + author: 'Tamsyn Muir', + series: 'The Locked Tomb', + category: 'Horror' + } + end + + include_examples 'should assign the attributes' + end + + describe 'with an attributes hash with extra attributes' do + let(:attributes) do + { + title: 'The Book of Lost Tales', + audiobook: true + } + end + + if allow_extra_attributes + include_examples 'should assign the attributes' + else + # :nocov: + let(:valid_attributes) do + defined?(super()) ? super() : expected_attributes.keys + end + let(:expected_error) do + Cuprum::Collections::Errors::ExtraAttributes.new( + entity_class: entity.class, + extra_attributes: %w[audiobook], + valid_attributes: valid_attributes + ) + end + + it 'should return a failing result' do + expect(result).to be_a_failing_result.with_error(expected_error) + end + # :nocov: + end + end + + context 'when the entity has existing attributes' do + let(:initial_attributes) do + # :nocov: + if defined?(super()) + super().merge(fixtures_data.first) + else + fixtures_data.first + end + # :nocov: + end + + describe 'with an empty attributes hash' do + let(:attributes) { {} } + + include_examples 'should assign the attributes' + end + + describe 'with an attributes hash with partial attributes' do + let(:attributes) { { title: 'Gideon the Ninth' } } + + include_examples 'should assign the attributes' + end + + describe 'with an attributes hash with full attributes' do + let(:attributes) do + { + title: 'Gideon the Ninth', + author: 'Tamsyn Muir', + series: 'The Locked Tomb', + category: 'Horror' + } + end + + include_examples 'should assign the attributes' + end + + describe 'with an attributes hash with extra attributes' do + let(:attributes) do + { + title: 'The Book of Lost Tales', + audiobook: true + } + end + + if allow_extra_attributes + include_examples 'should assign the attributes' + else + # :nocov: + let(:valid_attributes) do + defined?(super()) ? super() : expected_attributes.keys + end + let(:expected_error) do + Cuprum::Collections::Errors::ExtraAttributes.new( + entity_class: entity.class, + extra_attributes: %w[audiobook], + valid_attributes: valid_attributes + ) + end + + it 'should return a failing result' do + expect(result) + .to be_a_failing_result + .with_error(expected_error) + end + # :nocov: + end + end + end + end + end + end + + # Contract validating the behavior of a Build command implementation. + module ShouldBeABuildOneCommandContract + extend RSpec::SleepingKingStudios::Contract + + # @!method apply(example_group, allow_extra_attributes:) + # Adds the contract to the example group. + # + # @param example_group [RSpec::Core::ExampleGroup] the example group to + # which the contract is applied. + # @param allow_extra_attributes [Boolean] if false, the command should + # fail if given attributes not defined for the entity. + contract do |allow_extra_attributes:| + include Stannum::RSpec::Matchers + + describe '#call' do + shared_examples 'should build the entity' do + it { expect(result).to be_a_passing_result } + + it { expect(result.value).to be == expected_value } + end + + let(:attributes) { {} } + let(:result) { command.call(attributes: attributes) } + let(:expected_attributes) do + attributes + end + let(:expected_value) do + defined?(super()) ? super() : attributes + end + + it 'should validate the :attributes keyword' do + expect(command) + .to validate_parameter(:call, :attributes) + .using_constraint( + Stannum::Constraints::Types::HashWithIndifferentKeys.new + ) + end + + describe 'with an empty attributes hash' do + let(:attributes) { {} } + + include_examples 'should build the entity' + end + + describe 'with an attributes hash with partial attributes' do + let(:attributes) { { title: 'Gideon the Ninth' } } + + include_examples 'should build the entity' + end + + describe 'with an attributes hash with full attributes' do + let(:attributes) do + { + title: 'Gideon the Ninth', + author: 'Tamsyn Muir', + series: 'The Locked Tomb', + category: 'Horror' + } + end + + include_examples 'should build the entity' + end + + describe 'with an attributes hash with extra attributes' do + let(:attributes) do + { + title: 'The Book of Lost Tales', + audiobook: true + } + end + + if allow_extra_attributes + include_examples 'should build the entity' + else + # :nocov: + let(:valid_attributes) do + defined?(super()) ? super() : expected_attributes.keys + end + let(:expected_error) do + Cuprum::Collections::Errors::ExtraAttributes.new( + entity_class: entity_type, + extra_attributes: %w[audiobook], + valid_attributes: valid_attributes + ) + end + + it 'should return a failing result' do + expect(result).to be_a_failing_result.with_error(expected_error) + end + # :nocov: + end + end + end + end + end + + # Contract validating the behavior of a FindOne command implementation. + module ShouldBeADestroyOneCommandContract + 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 + describe '#call' do + let(:mapped_data) do + defined?(super()) ? super() : data + end + let(:primary_key_name) { defined?(super()) ? super() : 'id' } + let(:primary_key_type) { defined?(super()) ? super() : Integer } + let(:invalid_primary_key_value) do + defined?(super()) ? super() : 100 + end + let(:valid_primary_key_value) do + defined?(super()) ? super() : 0 + end + + it 'should validate the :primary_key keyword' do + expect(command) + .to validate_parameter(:call, :primary_key) + .using_constraint(primary_key_type) + end + + describe 'with an invalid primary key' do + let(:primary_key) { invalid_primary_key_value } + let(:expected_error) do + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: collection_name, + primary_key: true + ) + end + + it 'should return a failing result' do + expect(command.call(primary_key: primary_key)) + .to be_a_failing_result + .with_error(expected_error) + end + + it 'should not remove an entity from the collection' do + expect { command.call(primary_key: primary_key) } + .not_to(change { query.reset.count }) + end + end + + context 'when the collection has many items' do + let(:data) { fixtures_data } + let(:matching_data) do + mapped_data.find do |item| + item[primary_key_name.to_s] == primary_key + end + end + let!(:expected_data) do + defined?(super()) ? super() : matching_data + end + + describe 'with an invalid primary key' do + let(:primary_key) { invalid_primary_key_value } + let(:expected_error) do + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: collection_name, + primary_key: true + ) + end + + it 'should return a failing result' do + expect(command.call(primary_key: primary_key)) + .to be_a_failing_result + .with_error(expected_error) + end + + it 'should not remove an entity from the collection' do + expect { command.call(primary_key: primary_key) } + .not_to(change { query.reset.count }) + end + end + + describe 'with a valid primary key' do + let(:primary_key) { valid_primary_key_value } + + it 'should return a passing result' do + expect(command.call(primary_key: primary_key)) + .to be_a_passing_result + .with_value(expected_data) + end + + it 'should remove an entity from the collection' do + expect { command.call(primary_key: primary_key) } + .to( + change { query.reset.count }.by(-1) + ) + end + + it 'should remove the entity from the collection' do + command.call(primary_key: primary_key) + + expect(query.map { |item| item[primary_key_name.to_s] }) + .not_to include primary_key + end + end + end + end + end + end + + # Contract validating the behavior of a FindMany command implementation. + module ShouldBeAFindManyCommandContract + 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 + describe '#call' do + let(:mapped_data) do + defined?(super()) ? super() : data + end + let(:primary_key_name) { defined?(super()) ? super() : 'id' } + let(:primary_key_type) { defined?(super()) ? super() : Integer } + let(:primary_keys_contract) do + Stannum::Constraints::Types::ArrayType + .new(item_type: primary_key_type) + end + let(:invalid_primary_key_values) do + defined?(super()) ? super() : [100, 101, 102] + end + let(:valid_primary_key_values) do + defined?(super()) ? super() : [0, 1, 2] + end + + it 'should validate the :allow_partial keyword' do + expect(command) + .to validate_parameter(:call, :allow_partial) + .using_constraint(Stannum::Constraints::Boolean.new) + end + + it 'should validate the :envelope keyword' do + expect(command) + .to validate_parameter(:call, :envelope) + .using_constraint(Stannum::Constraints::Boolean.new) + end + + it 'should validate the :primary_keys keyword' do + expect(command) + .to validate_parameter(:call, :primary_keys) + .using_constraint(Array) + end + + it 'should validate the :primary_keys keyword items' do + expect(command) + .to validate_parameter(:call, :primary_keys) + .with_value([nil]) + .using_constraint(primary_keys_contract) + end + + it 'should validate the :scope keyword' do + expect(command) + .to validate_parameter(:call, :scope) + .using_constraint( + Stannum::Constraints::Type.new(query.class, optional: true) + ) + .with_value(Object.new.freeze) + end + + describe 'with an array of invalid primary keys' do + let(:primary_keys) { invalid_primary_key_values } + let(:expected_error) do + Cuprum::Errors::MultipleErrors.new( + errors: primary_keys.map do |primary_key| + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: command.collection_name, + primary_key: true + ) + end + ) + end + + it 'should return a failing result' do + expect(command.call(primary_keys: primary_keys)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + context 'when the collection has many items' do + let(:data) { fixtures_data } + let(:matching_data) do + primary_keys + .map do |key| + mapped_data.find { |item| item[primary_key_name.to_s] == key } + end + end + let(:expected_data) do + defined?(super()) ? super() : matching_data + end + + describe 'with an array of invalid primary keys' do + let(:primary_keys) { invalid_primary_key_values } + let(:expected_error) do + Cuprum::Errors::MultipleErrors.new( + errors: primary_keys.map do |primary_key| + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: command.collection_name, + primary_key: true + ) + end + ) + end + + it 'should return a failing result' do + expect(command.call(primary_keys: primary_keys)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with a partially valid array of primary keys' do + let(:primary_keys) do + invalid_primary_key_values + valid_primary_key_values + end + let(:expected_error) do + Cuprum::Errors::MultipleErrors.new( + errors: primary_keys.map do |primary_key| + unless invalid_primary_key_values.include?(primary_key) + next nil + end + + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: command.collection_name, + primary_key: true + ) + end + ) + end + + it 'should return a failing result' do + expect(command.call(primary_keys: primary_keys)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with a valid array of primary keys' do + let(:primary_keys) { valid_primary_key_values } + + it 'should return a passing result' do + expect(command.call(primary_keys: primary_keys)) + .to be_a_passing_result + .with_value(expected_data) + end + + describe 'with an ordered array of primary keys' do + let(:primary_keys) { valid_primary_key_values.reverse } + + it 'should return a passing result' do + expect(command.call(primary_keys: primary_keys)) + .to be_a_passing_result + .with_value(expected_data) + end + end + end + + describe 'with allow_partial: true' do + describe 'with an array of invalid primary keys' do + let(:primary_keys) { invalid_primary_key_values } + let(:expected_error) do + Cuprum::Errors::MultipleErrors.new( + errors: invalid_primary_key_values.map do |primary_key| + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: command.collection_name, + primary_key: true + ) + end + ) + end + + it 'should return a failing result' do + expect(command.call(primary_keys: primary_keys)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with a partially valid array of primary keys' do + let(:primary_keys) do + invalid_primary_key_values + valid_primary_key_values + end + let(:expected_error) do + Cuprum::Errors::MultipleErrors.new( + errors: primary_keys.map do |primary_key| + unless invalid_primary_key_values.include?(primary_key) + next nil + end + + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: command.collection_name, + primary_key: true + ) + end + ) + end + + it 'should return a passing result' do + expect( + command.call( + primary_keys: primary_keys, + allow_partial: true + ) + ) + .to be_a_passing_result + .with_value(expected_data) + .and_error(expected_error) + end + end + + describe 'with a valid array of primary keys' do + let(:primary_keys) { valid_primary_key_values } + + it 'should return a passing result' do + expect( + command.call( + primary_keys: primary_keys, + allow_partial: true + ) + ) + .to be_a_passing_result + .with_value(expected_data) + end + + describe 'with an ordered array of primary keys' do + let(:primary_keys) { valid_primary_key_values.reverse } + + it 'should return a passing result' do + expect( + command.call( + primary_keys: primary_keys, + allow_partial: true + ) + ) + .to be_a_passing_result + .with_value(expected_data) + end + end + end + end + + describe 'with envelope: true' do + describe 'with a valid array of primary keys' do + let(:primary_keys) { valid_primary_key_values } + + it 'should return a passing result' do + expect( + command.call(primary_keys: primary_keys, envelope: true) + ) + .to be_a_passing_result + .with_value({ collection_name => expected_data }) + end + + describe 'with an ordered array of primary keys' do + let(:primary_keys) { valid_primary_key_values.reverse } + + it 'should return a passing result' do + expect( + command.call(primary_keys: primary_keys, envelope: true) + ) + .to be_a_passing_result + .with_value({ collection_name => expected_data }) + end + end + end + end + + describe 'with scope: query' do + let(:scope_filter) { -> { {} } } + + describe 'with an array of invalid primary keys' do + let(:primary_keys) { invalid_primary_key_values } + let(:expected_error) do + Cuprum::Errors::MultipleErrors.new( + errors: primary_keys.map do |primary_key| + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: command.collection_name, + primary_key: true + ) + end + ) + end + + it 'should return a failing result' do + expect(command.call(primary_keys: primary_keys, scope: scope)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with a scope that does not match any keys' do + let(:scope_filter) { -> { { author: 'Ursula K. LeGuin' } } } + + describe 'with a valid array of primary keys' do + let(:primary_keys) { valid_primary_key_values } + let(:expected_error) do + Cuprum::Errors::MultipleErrors.new( + errors: primary_keys.map do |primary_key| + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: command.collection_name, + primary_key: true + ) + end + ) + end + + it 'should return a failing result' do + expect( + command.call(primary_keys: primary_keys, scope: scope) + ) + .to be_a_failing_result + .with_error(expected_error) + end + end + end + + describe 'with a scope that matches some keys' do + let(:scope_filter) { -> { { series: nil } } } + let(:matching_data) do + super().map do |item| + next nil unless item['series'].nil? + + item + end + end + + describe 'with a valid array of primary keys' do + let(:primary_keys) { valid_primary_key_values } + let(:expected_error) do + found_keys = + matching_data + .compact + .map { |item| item[primary_key_name.to_s] } + + Cuprum::Errors::MultipleErrors.new( + errors: primary_keys.map do |primary_key| + next if found_keys.include?(primary_key) + + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: command.collection_name, + primary_key: true + ) + end + ) + end + + it 'should return a failing result' do + expect( + command.call(primary_keys: primary_keys, scope: scope) + ) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with allow_partial: true' do + describe 'with a valid array of primary keys' do + let(:primary_keys) { valid_primary_key_values } + let(:expected_error) do + found_keys = + matching_data + .compact + .map { |item| item[primary_key_name.to_s] } + + Cuprum::Errors::MultipleErrors.new( + errors: primary_keys.map do |primary_key| + next if found_keys.include?(primary_key) + + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: command.collection_name, + primary_key: true + ) + end + ) + end + + it 'should return a passing result' do + expect( + command.call( + allow_partial: true, + primary_keys: primary_keys, + scope: scope + ) + ) + .to be_a_passing_result + .with_value(expected_data) + .and_error(expected_error) + end + end + end + end + + describe 'with a scope that matches all keys' do + let(:scope_filter) { -> { { author: 'J.R.R. Tolkien' } } } + + describe 'with a valid array of primary keys' do + let(:primary_keys) { valid_primary_key_values } + + it 'should return a passing result' do + expect(command.call(primary_keys: primary_keys)) + .to be_a_passing_result + .with_value(expected_data) + end + end + end + end + end + end + end + end + + # Contract validating the behavior of a FindMatching command implementation. + module ShouldBeAFindMatchingCommandContract + 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 + include Stannum::RSpec::Matchers + include Cuprum::Collections::RSpec::Contracts::QueryContracts + + describe '#call' do + include_contract 'with query contexts' + + shared_examples 'should return the matching items' do + it { expect(result).to be_a_passing_result } + + it { expect(result.value).to be_a Enumerator } + + it { expect(result.value.to_a).to be == expected_data } + end + + shared_examples 'should return the wrapped items' do + it { expect(result).to be_a_passing_result } + + it { expect(result.value).to be_a Hash } + + it { expect(result.value.keys).to be == [collection_name] } + + it { expect(result.value[collection_name]).to be == expected_data } + end + + let(:options) do + opts = {} + + opts[:limit] = limit if limit + opts[:offset] = offset if offset + opts[:order] = order if order + opts[:where] = filter unless filter.nil? || filter.is_a?(Proc) + + opts + end + let(:block) { filter.is_a?(Proc) ? filter : nil } + let(:result) { command.call(**options, &block) } + let(:data) { [] } + let(:matching_data) { data } + let(:expected_data) do + defined?(super()) ? super() : matching_data + end + + it 'should validate the :envelope keyword' do + expect(command) + .to validate_parameter(:call, :envelope) + .using_constraint(Stannum::Constraints::Boolean.new) + end + + it 'should validate the :limit keyword' do + expect(command) + .to validate_parameter(:call, :limit) + .with_value(Object.new) + .using_constraint(Integer, required: false) + end + + it 'should validate the :offset keyword' do + expect(command) + .to validate_parameter(:call, :offset) + .with_value(Object.new) + .using_constraint(Integer, required: false) + end + + it 'should validate the :order keyword' do + constraint = Cuprum::Collections::Constraints::Ordering.new + + expect(command) + .to validate_parameter(:call, :order) + .with_value(Object.new) + .using_constraint(constraint, required: false) + end + + it 'should validate the :scope keyword' do + expect(command) + .to validate_parameter(:call, :scope) + .using_constraint( + Stannum::Constraints::Type.new(query.class, optional: true) + ) + .with_value(Object.new.freeze) + end + + it 'should validate the :where keyword' do + expect(command).to validate_parameter(:call, :where) + end + + include_examples 'should return the matching items' + + include_contract 'should perform queries', + block: lambda { + include_examples 'should return the matching items' + } + + describe 'with an invalid filter block' do + let(:block) { -> {} } + let(:expected_error) do + an_instance_of(Cuprum::Collections::Errors::InvalidQuery) + end + + it 'should return a failing result' do + expect(result).to be_a_failing_result.with_error(expected_error) + end + end + + describe 'with envelope: true' do + let(:options) { super().merge(envelope: true) } + + include_examples 'should return the wrapped items' + + include_contract 'should perform queries', + block: lambda { + include_examples 'should return the wrapped items' + } + end + + context 'when the collection has many items' do + let(:data) { fixtures_data } + + include_examples 'should return the matching items' + + include_contract 'should perform queries', + block: lambda { + include_examples 'should return the matching items' + } + + describe 'with envelope: true' do + let(:options) { super().merge(envelope: true) } + + include_examples 'should return the wrapped items' + + include_contract 'should perform queries', + block: lambda { + include_examples 'should return the wrapped items' + } + end + + describe 'with scope: query' do + let(:scope_filter) { -> { {} } } + let(:options) { super().merge(scope: scope) } + + describe 'with a scope that does not match any values' do + let(:scope_filter) { -> { { series: 'Mistborn' } } } + let(:matching_data) { [] } + + include_examples 'should return the matching items' + end + + describe 'with a scope that matches some values' do + let(:scope_filter) { -> { { series: nil } } } + let(:matching_data) do + super().select { |item| item['series'].nil? } + end + + include_examples 'should return the matching items' + + describe 'with a where filter' do + let(:filter) { -> { { author: 'Ursula K. LeGuin' } } } + let(:options) { super().merge(where: filter) } + let(:matching_data) do + super() + .select { |item| item['author'] == 'Ursula K. LeGuin' } + end + + include_examples 'should return the matching items' + end + end + + describe 'with a scope that matches all values' do + let(:scope_filter) { -> { { id: not_equal(nil) } } } + + include_examples 'should return the matching items' + + describe 'with a where filter' do + let(:filter) { -> { { author: 'Ursula K. LeGuin' } } } + let(:options) { super().merge(where: filter) } + let(:matching_data) do + super() + .select { |item| item['author'] == 'Ursula K. LeGuin' } + end + + include_examples 'should return the matching items' + end + end + end + end + end + end + end + + # Contract validating the behavior of a FindOne command implementation. + module ShouldBeAFindOneCommandContract + 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 + describe '#call' do + let(:mapped_data) do + defined?(super()) ? super() : data + end + let(:primary_key_name) { defined?(super()) ? super() : 'id' } + let(:primary_key_type) { defined?(super()) ? super() : Integer } + let(:invalid_primary_key_value) do + defined?(super()) ? super() : 100 + end + let(:valid_primary_key_value) do + defined?(super()) ? super() : 0 + end + + def tools + SleepingKingStudios::Tools::Toolbelt.instance + end + + it 'should validate the :envelope keyword' do + expect(command) + .to validate_parameter(:call, :envelope) + .using_constraint(Stannum::Constraints::Boolean.new) + end + + it 'should validate the :primary_key keyword' do + expect(command) + .to validate_parameter(:call, :primary_key) + .using_constraint(primary_key_type) + end + + it 'should validate the :scope keyword' do + expect(command) + .to validate_parameter(:call, :scope) + .using_constraint( + Stannum::Constraints::Type.new(query.class, optional: true) + ) + .with_value(Object.new.freeze) + end + + describe 'with an invalid primary key' do + let(:primary_key) { invalid_primary_key_value } + let(:expected_error) do + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: command.collection_name, + primary_key: true + ) + end + + it 'should return a failing result' do + expect(command.call(primary_key: primary_key)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + context 'when the collection has many items' do + let(:data) { fixtures_data } + let(:matching_data) do + mapped_data + .find { |item| item[primary_key_name.to_s] == primary_key } + end + let(:expected_data) do + defined?(super()) ? super() : matching_data + end + + describe 'with an invalid primary key' do + let(:primary_key) { invalid_primary_key_value } + let(:expected_error) do + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: command.collection_name, + primary_key: true + ) + end + + it 'should return a failing result' do + expect(command.call(primary_key: primary_key)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + describe 'with a valid primary key' do + let(:primary_key) { valid_primary_key_value } + + it 'should return a passing result' do + expect(command.call(primary_key: primary_key)) + .to be_a_passing_result + .with_value(expected_data) + end + end + + describe 'with envelope: true' do + let(:member_name) { tools.str.singularize(collection_name) } + + describe 'with a valid primary key' do + let(:primary_key) { valid_primary_key_value } + + it 'should return a passing result' do + expect(command.call(primary_key: primary_key, envelope: true)) + .to be_a_passing_result + .with_value({ member_name => expected_data }) + end + end + end + + describe 'with scope: query' do + let(:scope_filter) { -> { {} } } + + describe 'with a scope that does not match the key' do + let(:scope_filter) { -> { { author: 'Ursula K. LeGuin' } } } + + describe 'with an valid primary key' do + let(:primary_key) { valid_primary_key_value } + let(:expected_error) do + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: primary_key, + collection_name: command.collection_name, + primary_key: true + ) + end + + it 'should return a failing result' do + expect(command.call(primary_key: primary_key, scope: scope)) + .to be_a_failing_result + .with_error(expected_error) + end + end + end + + describe 'with a scope that matches the key' do + let(:scope_filter) { -> { { author: 'J.R.R. Tolkien' } } } + + describe 'with a valid primary key' do + let(:primary_key) { valid_primary_key_value } + + it 'should return a passing result' do + expect(command.call(primary_key: primary_key)) + .to be_a_passing_result + .with_value(expected_data) + end + end + end + end + end + end + end + end + + # Contract validating the behavior of an InsertOne command implementation. + module ShouldBeAnInsertOneCommandContract + 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 + describe '#call' do + let(:matching_data) { attributes } + let(:expected_data) do + defined?(super()) ? super() : matching_data + end + let(:primary_key_name) do + defined?(super()) ? super() : 'id' + end + let(:primary_key_type) do + defined?(super()) ? super() : Integer + end + let(:scoped) do + key = primary_key_name + value = entity[primary_key_name.to_s] + + query.where { { key => value } } + end + + it 'should validate the :entity keyword' do + expect(command) + .to validate_parameter(:call, :entity) + .using_constraint(entity_type) + end + + context 'when the item does not exist in the collection' do + it 'should return a passing result' do + expect(command.call(entity: entity)) + .to be_a_passing_result + .with_value(be == expected_data) + end + + it 'should append an item to the collection' do + expect { command.call(entity: entity) } + .to( + change { query.reset.count } + .by(1) + ) + end + + it 'should add the entity to the collection' do + expect { command.call(entity: entity) } + .to change(scoped, :exists?) + .to be true + end + + it 'should set the attributes' do + command.call(entity: entity) + + expect(scoped.to_a.first).to be == expected_data + end + end + + context 'when the item exists in the collection' do + let(:data) { fixtures_data } + let(:expected_error) do + Cuprum::Collections::Errors::AlreadyExists.new( + attribute_name: primary_key_name, + attribute_value: attributes.fetch( + primary_key_name.to_s, + attributes[primary_key_name.intern] + ), + collection_name: collection_name, + primary_key: true + ) + end + + it 'should return a failing result' do + expect(command.call(entity: entity)) + .to be_a_failing_result + .with_error(expected_error) + end + + it 'should not append an item to the collection' do + expect { command.call(entity: entity) } + .not_to(change { query.reset.count }) + end + end + end + end + end + + # Contract validating the behavior of an UpdateOne command implementation. + module ShouldBeAnUpdateOneCommandContract + 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 + describe '#call' do + let(:mapped_data) do + defined?(super()) ? super() : data + end + let(:matching_data) { attributes } + let(:expected_data) do + defined?(super()) ? super() : matching_data + end + let(:primary_key_name) do + defined?(super()) ? super() : 'id' + end + let(:scoped) do + key = primary_key_name + value = entity[primary_key_name.to_s] + + query.where { { key => value } } + end + + it 'should validate the :entity keyword' do + expect(command) + .to validate_parameter(:call, :entity) + .using_constraint(entity_type) + end + + context 'when the item does not exist in the collection' do + let(:expected_error) do + Cuprum::Collections::Errors::NotFound.new( + attribute_name: primary_key_name, + attribute_value: attributes.fetch( + primary_key_name.to_s, + attributes[primary_key_name.intern] + ), + collection_name: collection_name, + primary_key: true + ) + end + let(:matching_data) { mapped_data.first } + + it 'should return a failing result' do + expect(command.call(entity: entity)) + .to be_a_failing_result + .with_error(expected_error) + end + + it 'should not append an item to the collection' do + expect { command.call(entity: entity) } + .not_to(change { query.reset.count }) + end + end + + context 'when the item exists in the collection' do + let(:data) { fixtures_data } + let(:matching_data) do + mapped_data.first.merge(super()) + end + + it 'should return a passing result' do + expect(command.call(entity: entity)) + .to be_a_passing_result + .with_value(be == expected_data) + end + + it 'should not append an item to the collection' do + expect { command.call(entity: entity) } + .not_to(change { query.reset.count }) + end + + it 'should set the attributes' do + command.call(entity: entity) + + expect(scoped.to_a.first).to be == expected_data + end + end + end + end + end + + # Contract validating the behavior of a ValidateOne command implementation. + module ShouldBeAValidateOneCommandContract + extend RSpec::SleepingKingStudios::Contract + + # @!method apply(example_group, default_contract:) + # Adds the contract to the example group. + # + # @param default_contract [Boolean] if true, the command defines a + # default contract. + # @param example_group [RSpec::Core::ExampleGroup] the example group to + # which the contract is applied. + contract do |default_contract:| + describe '#call' do + it 'should validate the :contract keyword' do + expect(command) + .to validate_parameter(:call, :contract) + .with_value(Object.new.freeze) + .using_constraint(Stannum::Constraints::Base, optional: true) + end + + it 'should validate the :entity keyword' do + expect(command) + .to validate_parameter(:call, :entity) + .with_value(Object.new.freeze) + .using_constraint(entity_type) + end + + describe 'with contract: nil' do + if default_contract + context 'when the entity does not match the default contract' do + let(:attributes) { invalid_default_attributes } + let(:expected_error) do + Cuprum::Collections::Errors::FailedValidation.new( + entity_class: entity.class, + errors: expected_errors + ) + end + + it 'should return a failing result' do + expect(command.call(entity: entity)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + context 'when the entity matches the default contract' do + let(:attributes) { valid_default_attributes } + + it 'should return a passing result' do + expect(command.call(entity: entity)) + .to be_a_passing_result + .with_value(entity) + end + end + else + let(:attributes) { valid_attributes } + let(:expected_error) do + Cuprum::Collections::Errors::MissingDefaultContract.new( + entity_class: entity.class + ) + end + + it 'should return a failing result' do + expect(command.call(entity: entity)) + .to be_a_failing_result + .with_error(expected_error) + end + end + end + + describe 'with contract: value' do + context 'when the entity does not match the contract' do + let(:attributes) { invalid_attributes } + let(:errors) { contract.errors_for(entity) } + let(:expected_error) do + Cuprum::Collections::Errors::FailedValidation.new( + entity_class: entity.class, + errors: errors + ) + end + + it 'should return a failing result' do + expect(command.call(contract: contract, entity: entity)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + context 'when the entity matches the contract' do + let(:attributes) { valid_attributes } + + it 'should return a passing result' do + expect(command.call(contract: contract, entity: entity)) + .to be_a_passing_result + .with_value(entity) + end + end + end + end + end + end + end +end diff --git a/lib/cuprum/collections/rspec/contracts/query_contracts.rb b/lib/cuprum/collections/rspec/contracts/query_contracts.rb index 758cefe..4595d19 100644 --- a/lib/cuprum/collections/rspec/contracts/query_contracts.rb +++ b/lib/cuprum/collections/rspec/contracts/query_contracts.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'cuprum/collections/queries' require 'cuprum/collections/rspec/contracts' require 'cuprum/collections/rspec/fixtures' @@ -16,9 +17,11 @@ module ShouldBeAQuery OPERATORS = Cuprum::Collections::Queries::Operators private_constant :OPERATORS - # @!method apply(example_group) + # @!method apply(example_group, operators:) # Adds the contract to the example group. # + # @param example_group [RSpec::Core::ExampleGroup] the example group to + # which the contract is applied. # @param operators [Array] the expected operators. contract do |operators: OPERATORS.values| include Cuprum::Collections::RSpec::Contracts::QueryContracts @@ -678,6 +681,9 @@ module ShouldBeAQueryBuilderContract # @!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 describe '#base_query' do include_examples 'should define reader', @@ -774,9 +780,11 @@ module ShouldPerformQueriesContract OPERATORS = Cuprum::Collections::Queries::Operators private_constant :OPERATORS - # @!method apply(example_group) + # @!method apply(example_group, block:, operators:) # Adds the contract to the example group. # + # @param example_group [RSpec::Core::ExampleGroup] the example group to + # which the contract is applied. # @param block [Proc] the expectations for each query context. # @param operators [Array] the expected operators. contract do |block:, operators: OPERATORS.values| @@ -930,6 +938,9 @@ module WithQueryContextsContract # @!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 let(:filter) { nil } let(:strategy) { nil } diff --git a/lib/cuprum/collections/rspec/contracts/relation_contracts.rb b/lib/cuprum/collections/rspec/contracts/relation_contracts.rb index afdf412..885eb1d 100644 --- a/lib/cuprum/collections/rspec/contracts/relation_contracts.rb +++ b/lib/cuprum/collections/rspec/contracts/relation_contracts.rb @@ -9,9 +9,11 @@ module RelationContracts module ShouldDisambiguateParameter extend RSpec::SleepingKingStudios::Contract - # @!method apply(example_group) + # @!method apply(example_group, key, as:, value:) # Adds the contract to the example group. # + # @param example_group [RSpec::Core::ExampleGroup] the example group to + # which the contract is applied. # @param key [Symbol] the original parameter key. # @param as [Symbol, Array] the aliased key or keys. # @param value [Object] the custom value for the property. @@ -129,6 +131,9 @@ module ShouldValidateTheParametersContract # @!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 describe 'with no parameters' do let(:error_message) { "name or entity class can't be blank" } @@ -408,9 +413,11 @@ module ShouldValidateTheParametersContract module ShouldBeARelationContract extend RSpec::SleepingKingStudios::Contract - # @!method apply(example_group) + # @!method apply(example_group, **options) # Adds the contract to the example group. # + # @param example_group [RSpec::Core::ExampleGroup] the example group to + # which the contract is applied. # @param options [Hash] additional options for the contract. # # @option options cardinality [Boolean] true if the relation accepts @@ -1177,6 +1184,9 @@ module ShouldDefineCardinalityContract # @!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 describe '.new' do describe 'with plural: an Object' do @@ -1309,6 +1319,9 @@ module ShouldDefinePrimaryKeysContract # @!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 describe '#primary_key_name' do let(:expected_primary_key_name) do diff --git a/lib/cuprum/collections/rspec/contracts/repository_contracts.rb b/lib/cuprum/collections/rspec/contracts/repository_contracts.rb index 3be704d..c0e2948 100644 --- a/lib/cuprum/collections/rspec/contracts/repository_contracts.rb +++ b/lib/cuprum/collections/rspec/contracts/repository_contracts.rb @@ -9,7 +9,7 @@ module RepositoryContracts module ShouldBeARepositoryContract extend RSpec::SleepingKingStudios::Contract - # @!method apply(example_group) + # @!method apply(example_group, abstract:, **options) # Adds the contract to the example group. # # @param abstract [Boolean] if true, the repository is abstract and does diff --git a/lib/cuprum/collections/rspec/destroy_one_command_contract.rb b/lib/cuprum/collections/rspec/destroy_one_command_contract.rb deleted file mode 100644 index 61f45cc..0000000 --- a/lib/cuprum/collections/rspec/destroy_one_command_contract.rb +++ /dev/null @@ -1,108 +0,0 @@ -# frozen_string_literal: true - -require 'cuprum/collections/rspec' - -module Cuprum::Collections::RSpec - # Contract validating the behavior of a FindOne command implementation. - DESTROY_ONE_COMMAND_CONTRACT = lambda do - describe '#call' do - let(:mapped_data) do - defined?(super()) ? super() : data - end - let(:primary_key_name) { defined?(super()) ? super() : 'id' } - let(:primary_key_type) { defined?(super()) ? super() : Integer } - let(:invalid_primary_key_value) do - defined?(super()) ? super() : 100 - end - let(:valid_primary_key_value) do - defined?(super()) ? super() : 0 - end - - it 'should validate the :primary_key keyword' do - expect(command) - .to validate_parameter(:call, :primary_key) - .using_constraint(primary_key_type) - end - - describe 'with an invalid primary key' do - let(:primary_key) { invalid_primary_key_value } - let(:expected_error) do - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: collection_name, - primary_key: true - ) - end - - it 'should return a failing result' do - expect(command.call(primary_key: primary_key)) - .to be_a_failing_result - .with_error(expected_error) - end - - it 'should not remove an entity from the collection' do - expect { command.call(primary_key: primary_key) } - .not_to(change { query.reset.count }) - end - end - - context 'when the collection has many items' do - let(:data) { fixtures_data } - let(:matching_data) do - mapped_data.find { |item| item[primary_key_name.to_s] == primary_key } - end - let!(:expected_data) do - defined?(super()) ? super() : matching_data - end - - describe 'with an invalid primary key' do - let(:primary_key) { invalid_primary_key_value } - let(:expected_error) do - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: collection_name, - primary_key: true - ) - end - - it 'should return a failing result' do - expect(command.call(primary_key: primary_key)) - .to be_a_failing_result - .with_error(expected_error) - end - - it 'should not remove an entity from the collection' do - expect { command.call(primary_key: primary_key) } - .not_to(change { query.reset.count }) - end - end - - describe 'with a valid primary key' do - let(:primary_key) { valid_primary_key_value } - - it 'should return a passing result' do - expect(command.call(primary_key: primary_key)) - .to be_a_passing_result - .with_value(expected_data) - end - - it 'should remove an entity from the collection' do - expect { command.call(primary_key: primary_key) } - .to( - change { query.reset.count }.by(-1) - ) - end - - it 'should remove the entity from the collection' do - command.call(primary_key: primary_key) - - expect(query.map { |item| item[primary_key_name.to_s] }) - .not_to include primary_key - end - end - end - end - end -end diff --git a/lib/cuprum/collections/rspec/find_many_command_contract.rb b/lib/cuprum/collections/rspec/find_many_command_contract.rb deleted file mode 100644 index bc81094..0000000 --- a/lib/cuprum/collections/rspec/find_many_command_contract.rb +++ /dev/null @@ -1,407 +0,0 @@ -# frozen_string_literal: true - -require 'cuprum/collections/rspec' - -module Cuprum::Collections::RSpec - # Contract validating the behavior of a FindMany command implementation. - FIND_MANY_COMMAND_CONTRACT = lambda do - describe '#call' do - let(:mapped_data) do - defined?(super()) ? super() : data - end - let(:primary_key_name) { defined?(super()) ? super() : 'id' } - let(:primary_key_type) { defined?(super()) ? super() : Integer } - let(:primary_keys_contract) do - Stannum::Constraints::Types::ArrayType.new(item_type: primary_key_type) - end - let(:invalid_primary_key_values) do - defined?(super()) ? super() : [100, 101, 102] - end - let(:valid_primary_key_values) do - defined?(super()) ? super() : [0, 1, 2] - end - - it 'should validate the :allow_partial keyword' do - expect(command) - .to validate_parameter(:call, :allow_partial) - .using_constraint(Stannum::Constraints::Boolean.new) - end - - it 'should validate the :envelope keyword' do - expect(command) - .to validate_parameter(:call, :envelope) - .using_constraint(Stannum::Constraints::Boolean.new) - end - - it 'should validate the :primary_keys keyword' do - expect(command) - .to validate_parameter(:call, :primary_keys) - .using_constraint(Array) - end - - it 'should validate the :primary_keys keyword items' do - expect(command) - .to validate_parameter(:call, :primary_keys) - .with_value([nil]) - .using_constraint(primary_keys_contract) - end - - it 'should validate the :scope keyword' do - expect(command) - .to validate_parameter(:call, :scope) - .using_constraint( - Stannum::Constraints::Type.new(query.class, optional: true) - ) - .with_value(Object.new.freeze) - end - - describe 'with an array of invalid primary keys' do - let(:primary_keys) { invalid_primary_key_values } - let(:expected_error) do - Cuprum::Errors::MultipleErrors.new( - errors: primary_keys.map do |primary_key| - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: command.collection_name, - primary_key: true - ) - end - ) - end - - it 'should return a failing result' do - expect(command.call(primary_keys: primary_keys)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - context 'when the collection has many items' do - let(:data) { fixtures_data } - let(:matching_data) do - primary_keys - .map do |key| - mapped_data.find { |item| item[primary_key_name.to_s] == key } - end - end - let(:expected_data) do - defined?(super()) ? super() : matching_data - end - - describe 'with an array of invalid primary keys' do - let(:primary_keys) { invalid_primary_key_values } - let(:expected_error) do - Cuprum::Errors::MultipleErrors.new( - errors: primary_keys.map do |primary_key| - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: command.collection_name, - primary_key: true - ) - end - ) - end - - it 'should return a failing result' do - expect(command.call(primary_keys: primary_keys)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with a partially valid array of primary keys' do - let(:primary_keys) do - invalid_primary_key_values + valid_primary_key_values - end - let(:expected_error) do - Cuprum::Errors::MultipleErrors.new( - errors: primary_keys.map do |primary_key| - next nil unless invalid_primary_key_values.include?(primary_key) - - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: command.collection_name, - primary_key: true - ) - end - ) - end - - it 'should return a failing result' do - expect(command.call(primary_keys: primary_keys)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with a valid array of primary keys' do - let(:primary_keys) { valid_primary_key_values } - - it 'should return a passing result' do - expect(command.call(primary_keys: primary_keys)) - .to be_a_passing_result - .with_value(expected_data) - end - - describe 'with an ordered array of primary keys' do - let(:primary_keys) { valid_primary_key_values.reverse } - - it 'should return a passing result' do - expect(command.call(primary_keys: primary_keys)) - .to be_a_passing_result - .with_value(expected_data) - end - end - end - - describe 'with allow_partial: true' do - describe 'with an array of invalid primary keys' do - let(:primary_keys) { invalid_primary_key_values } - let(:expected_error) do - Cuprum::Errors::MultipleErrors.new( - errors: invalid_primary_key_values.map do |primary_key| - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: command.collection_name, - primary_key: true - ) - end - ) - end - - it 'should return a failing result' do - expect(command.call(primary_keys: primary_keys)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with a partially valid array of primary keys' do - let(:primary_keys) do - invalid_primary_key_values + valid_primary_key_values - end - let(:expected_error) do - Cuprum::Errors::MultipleErrors.new( - errors: primary_keys.map do |primary_key| - unless invalid_primary_key_values.include?(primary_key) - next nil - end - - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: command.collection_name, - primary_key: true - ) - end - ) - end - - it 'should return a passing result' do - expect( - command.call(primary_keys: primary_keys, allow_partial: true) - ) - .to be_a_passing_result - .with_value(expected_data) - .and_error(expected_error) - end - end - - describe 'with a valid array of primary keys' do - let(:primary_keys) { valid_primary_key_values } - - it 'should return a passing result' do - expect( - command.call(primary_keys: primary_keys, allow_partial: true) - ) - .to be_a_passing_result - .with_value(expected_data) - end - - describe 'with an ordered array of primary keys' do - let(:primary_keys) { valid_primary_key_values.reverse } - - it 'should return a passing result' do - expect( - command.call(primary_keys: primary_keys, allow_partial: true) - ) - .to be_a_passing_result - .with_value(expected_data) - end - end - end - end - - describe 'with envelope: true' do - describe 'with a valid array of primary keys' do - let(:primary_keys) { valid_primary_key_values } - - it 'should return a passing result' do - expect(command.call(primary_keys: primary_keys, envelope: true)) - .to be_a_passing_result - .with_value({ collection_name => expected_data }) - end - - describe 'with an ordered array of primary keys' do - let(:primary_keys) { valid_primary_key_values.reverse } - - it 'should return a passing result' do - expect(command.call(primary_keys: primary_keys, envelope: true)) - .to be_a_passing_result - .with_value({ collection_name => expected_data }) - end - end - end - end - - describe 'with scope: query' do - let(:scope_filter) { -> { {} } } - - describe 'with an array of invalid primary keys' do - let(:primary_keys) { invalid_primary_key_values } - let(:expected_error) do - Cuprum::Errors::MultipleErrors.new( - errors: primary_keys.map do |primary_key| - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: command.collection_name, - primary_key: true - ) - end - ) - end - - it 'should return a failing result' do - expect(command.call(primary_keys: primary_keys, scope: scope)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with a scope that does not match any keys' do - let(:scope_filter) { -> { { author: 'Ursula K. LeGuin' } } } - - describe 'with a valid array of primary keys' do - let(:primary_keys) { valid_primary_key_values } - let(:expected_error) do - Cuprum::Errors::MultipleErrors.new( - errors: primary_keys.map do |primary_key| - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: command.collection_name, - primary_key: true - ) - end - ) - end - - it 'should return a failing result' do - expect(command.call(primary_keys: primary_keys, scope: scope)) - .to be_a_failing_result - .with_error(expected_error) - end - end - end - - describe 'with a scope that matches some keys' do - let(:scope_filter) { -> { { series: nil } } } - let(:matching_data) do - super().map do |item| - next nil unless item['series'].nil? - - item - end - end - - describe 'with a valid array of primary keys' do - let(:primary_keys) { valid_primary_key_values } - let(:expected_error) do - found_keys = - matching_data - .compact - .map { |item| item[primary_key_name.to_s] } - - Cuprum::Errors::MultipleErrors.new( - errors: primary_keys.map do |primary_key| - next if found_keys.include?(primary_key) - - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: command.collection_name, - primary_key: true - ) - end - ) - end - - it 'should return a failing result' do - expect(command.call(primary_keys: primary_keys, scope: scope)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with allow_partial: true' do - describe 'with a valid array of primary keys' do - let(:primary_keys) { valid_primary_key_values } - let(:expected_error) do - found_keys = - matching_data - .compact - .map { |item| item[primary_key_name.to_s] } - - Cuprum::Errors::MultipleErrors.new( - errors: primary_keys.map do |primary_key| - next if found_keys.include?(primary_key) - - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: command.collection_name, - primary_key: true - ) - end - ) - end - - it 'should return a passing result' do - expect( - command.call( - allow_partial: true, - primary_keys: primary_keys, - scope: scope - ) - ) - .to be_a_passing_result - .with_value(expected_data) - .and_error(expected_error) - end - end - end - end - - describe 'with a scope that matches all keys' do - let(:scope_filter) { -> { { author: 'J.R.R. Tolkien' } } } - - describe 'with a valid array of primary keys' do - let(:primary_keys) { valid_primary_key_values } - - it 'should return a passing result' do - expect(command.call(primary_keys: primary_keys)) - .to be_a_passing_result - .with_value(expected_data) - end - end - end - end - end - end - end -end diff --git a/lib/cuprum/collections/rspec/find_matching_command_contract.rb b/lib/cuprum/collections/rspec/find_matching_command_contract.rb deleted file mode 100644 index 43acfd9..0000000 --- a/lib/cuprum/collections/rspec/find_matching_command_contract.rb +++ /dev/null @@ -1,195 +0,0 @@ -# frozen_string_literal: true - -require 'stannum/rspec/validate_parameter' - -require 'cuprum/collections/constraints/ordering' -require 'cuprum/collections/rspec' -require 'cuprum/collections/rspec/contracts/query_contracts' - -module Cuprum::Collections::RSpec - # Contract validating the behavior of a FindMatching command implementation. - FIND_MATCHING_COMMAND_CONTRACT = lambda do - include Stannum::RSpec::Matchers - include Cuprum::Collections::RSpec::Contracts::QueryContracts - - describe '#call' do - include_contract 'with query contexts' - - shared_examples 'should return the matching items' do - it { expect(result).to be_a_passing_result } - - it { expect(result.value).to be_a Enumerator } - - it { expect(result.value.to_a).to be == expected_data } - end - - shared_examples 'should return the wrapped items' do - it { expect(result).to be_a_passing_result } - - it { expect(result.value).to be_a Hash } - - it { expect(result.value.keys).to be == [collection_name] } - - it { expect(result.value[collection_name]).to be == expected_data } - end - - let(:options) do - opts = {} - - opts[:limit] = limit if limit - opts[:offset] = offset if offset - opts[:order] = order if order - opts[:where] = filter unless filter.nil? || filter.is_a?(Proc) - - opts - end - let(:block) { filter.is_a?(Proc) ? filter : nil } - let(:result) { command.call(**options, &block) } - let(:data) { [] } - let(:matching_data) { data } - let(:expected_data) do - defined?(super()) ? super() : matching_data - end - - it 'should validate the :envelope keyword' do - expect(command) - .to validate_parameter(:call, :envelope) - .using_constraint(Stannum::Constraints::Boolean.new) - end - - it 'should validate the :limit keyword' do - expect(command) - .to validate_parameter(:call, :limit) - .with_value(Object.new) - .using_constraint(Integer, required: false) - end - - it 'should validate the :offset keyword' do - expect(command) - .to validate_parameter(:call, :offset) - .with_value(Object.new) - .using_constraint(Integer, required: false) - end - - it 'should validate the :order keyword' do - constraint = Cuprum::Collections::Constraints::Ordering.new - - expect(command) - .to validate_parameter(:call, :order) - .with_value(Object.new) - .using_constraint(constraint, required: false) - end - - it 'should validate the :scope keyword' do - expect(command) - .to validate_parameter(:call, :scope) - .using_constraint( - Stannum::Constraints::Type.new(query.class, optional: true) - ) - .with_value(Object.new.freeze) - end - - it 'should validate the :where keyword' do - expect(command).to validate_parameter(:call, :where) - end - - include_examples 'should return the matching items' - - include_contract 'should perform queries', - block: lambda { - include_examples 'should return the matching items' - } - - describe 'with an invalid filter block' do - let(:block) { -> {} } - let(:expected_error) do - an_instance_of(Cuprum::Collections::Errors::InvalidQuery) - end - - it 'should return a failing result' do - expect(result).to be_a_failing_result.with_error(expected_error) - end - end - - describe 'with envelope: true' do - let(:options) { super().merge(envelope: true) } - - include_examples 'should return the wrapped items' - - include_contract 'should perform queries', - block: lambda { - include_examples 'should return the wrapped items' - } - end - - context 'when the collection has many items' do - let(:data) { fixtures_data } - - include_examples 'should return the matching items' - - include_contract 'should perform queries', - block: lambda { - include_examples 'should return the matching items' - } - - describe 'with envelope: true' do - let(:options) { super().merge(envelope: true) } - - include_examples 'should return the wrapped items' - - include_contract 'should perform queries', - block: lambda { - include_examples 'should return the wrapped items' - } - end - - describe 'with scope: query' do - let(:scope_filter) { -> { {} } } - let(:options) { super().merge(scope: scope) } - - describe 'with a scope that does not match any values' do - let(:scope_filter) { -> { { series: 'Mistborn' } } } - let(:matching_data) { [] } - - include_examples 'should return the matching items' - end - - describe 'with a scope that matches some values' do - let(:scope_filter) { -> { { series: nil } } } - let(:matching_data) do - super().select { |item| item['series'].nil? } - end - - include_examples 'should return the matching items' - - describe 'with a where filter' do - let(:filter) { -> { { author: 'Ursula K. LeGuin' } } } - let(:options) { super().merge(where: filter) } - let(:matching_data) do - super().select { |item| item['author'] == 'Ursula K. LeGuin' } - end - - include_examples 'should return the matching items' - end - end - - describe 'with a scope that matches all values' do - let(:scope_filter) { -> { { id: not_equal(nil) } } } - - include_examples 'should return the matching items' - - describe 'with a where filter' do - let(:filter) { -> { { author: 'Ursula K. LeGuin' } } } - let(:options) { super().merge(where: filter) } - let(:matching_data) do - super().select { |item| item['author'] == 'Ursula K. LeGuin' } - end - - include_examples 'should return the matching items' - end - end - end - end - end - end -end diff --git a/lib/cuprum/collections/rspec/find_one_command_contract.rb b/lib/cuprum/collections/rspec/find_one_command_contract.rb deleted file mode 100644 index f96192d..0000000 --- a/lib/cuprum/collections/rspec/find_one_command_contract.rb +++ /dev/null @@ -1,157 +0,0 @@ -# frozen_string_literal: true - -require 'cuprum/collections/rspec' - -module Cuprum::Collections::RSpec - # Contract validating the behavior of a FindOne command implementation. - FIND_ONE_COMMAND_CONTRACT = lambda do - describe '#call' do - let(:mapped_data) do - defined?(super()) ? super() : data - end - let(:primary_key_name) { defined?(super()) ? super() : 'id' } - let(:primary_key_type) { defined?(super()) ? super() : Integer } - let(:invalid_primary_key_value) do - defined?(super()) ? super() : 100 - end - let(:valid_primary_key_value) do - defined?(super()) ? super() : 0 - end - - def tools - SleepingKingStudios::Tools::Toolbelt.instance - end - - it 'should validate the :envelope keyword' do - expect(command) - .to validate_parameter(:call, :envelope) - .using_constraint(Stannum::Constraints::Boolean.new) - end - - it 'should validate the :primary_key keyword' do - expect(command) - .to validate_parameter(:call, :primary_key) - .using_constraint(primary_key_type) - end - - it 'should validate the :scope keyword' do - expect(command) - .to validate_parameter(:call, :scope) - .using_constraint( - Stannum::Constraints::Type.new(query.class, optional: true) - ) - .with_value(Object.new.freeze) - end - - describe 'with an invalid primary key' do - let(:primary_key) { invalid_primary_key_value } - let(:expected_error) do - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: command.collection_name, - primary_key: true - ) - end - - it 'should return a failing result' do - expect(command.call(primary_key: primary_key)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - context 'when the collection has many items' do - let(:data) { fixtures_data } - let(:matching_data) do - mapped_data.find { |item| item[primary_key_name.to_s] == primary_key } - end - let(:expected_data) do - defined?(super()) ? super() : matching_data - end - - describe 'with an invalid primary key' do - let(:primary_key) { invalid_primary_key_value } - let(:expected_error) do - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: command.collection_name, - primary_key: true - ) - end - - it 'should return a failing result' do - expect(command.call(primary_key: primary_key)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - describe 'with a valid primary key' do - let(:primary_key) { valid_primary_key_value } - - it 'should return a passing result' do - expect(command.call(primary_key: primary_key)) - .to be_a_passing_result - .with_value(expected_data) - end - end - - describe 'with envelope: true' do - let(:member_name) { tools.str.singularize(collection_name) } - - describe 'with a valid primary key' do - let(:primary_key) { valid_primary_key_value } - - it 'should return a passing result' do - expect(command.call(primary_key: primary_key, envelope: true)) - .to be_a_passing_result - .with_value({ member_name => expected_data }) - end - end - end - - describe 'with scope: query' do - let(:scope_filter) { -> { {} } } - - describe 'with a scope that does not match the key' do - let(:scope_filter) { -> { { author: 'Ursula K. LeGuin' } } } - - describe 'with an valid primary key' do - let(:primary_key) { valid_primary_key_value } - let(:expected_error) do - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: primary_key, - collection_name: command.collection_name, - primary_key: true - ) - end - - it 'should return a failing result' do - expect(command.call(primary_key: primary_key, scope: scope)) - .to be_a_failing_result - .with_error(expected_error) - end - end - end - - describe 'with a scope that matches the key' do - let(:scope_filter) { -> { { author: 'J.R.R. Tolkien' } } } - - describe 'with a valid primary key' do - let(:primary_key) { valid_primary_key_value } - - it 'should return a passing result' do - expect(command.call(primary_key: primary_key)) - .to be_a_passing_result - .with_value(expected_data) - end - end - end - end - end - end - end -end diff --git a/lib/cuprum/collections/rspec/insert_one_command_contract.rb b/lib/cuprum/collections/rspec/insert_one_command_contract.rb deleted file mode 100644 index 8febef8..0000000 --- a/lib/cuprum/collections/rspec/insert_one_command_contract.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -require 'cuprum/collections/rspec' - -module Cuprum::Collections::RSpec - # Contract validating the behavior of an InsertOne command implementation. - INSERT_ONE_COMMAND_CONTRACT = lambda do - describe '#call' do - let(:matching_data) { attributes } - let(:expected_data) do - defined?(super()) ? super() : matching_data - end - let(:primary_key_name) do - defined?(super()) ? super() : 'id' - end - let(:primary_key_type) do - defined?(super()) ? super() : Integer - end - let(:scoped) do - key = primary_key_name - value = entity[primary_key_name.to_s] - - query.where { { key => value } } - end - - it 'should validate the :entity keyword' do - expect(command) - .to validate_parameter(:call, :entity) - .using_constraint(entity_type) - end - - context 'when the item does not exist in the collection' do - it 'should return a passing result' do - expect(command.call(entity: entity)) - .to be_a_passing_result - .with_value(be == expected_data) - end - - it 'should append an item to the collection' do - expect { command.call(entity: entity) } - .to( - change { query.reset.count } - .by(1) - ) - end - - it 'should add the entity to the collection' do - expect { command.call(entity: entity) } - .to change(scoped, :exists?) - .to be true - end - - it 'should set the attributes' do - command.call(entity: entity) - - expect(scoped.to_a.first).to be == expected_data - end - end - - context 'when the item exists in the collection' do - let(:data) { fixtures_data } - let(:expected_error) do - Cuprum::Collections::Errors::AlreadyExists.new( - attribute_name: primary_key_name, - attribute_value: attributes.fetch( - primary_key_name.to_s, - attributes[primary_key_name.intern] - ), - collection_name: collection_name, - primary_key: true - ) - end - - it 'should return a failing result' do - expect(command.call(entity: entity)) - .to be_a_failing_result - .with_error(expected_error) - end - - it 'should not append an item to the collection' do - expect { command.call(entity: entity) } - .not_to(change { query.reset.count }) - end - end - end - end -end diff --git a/lib/cuprum/collections/rspec/update_one_command_contract.rb b/lib/cuprum/collections/rspec/update_one_command_contract.rb deleted file mode 100644 index 2df4225..0000000 --- a/lib/cuprum/collections/rspec/update_one_command_contract.rb +++ /dev/null @@ -1,83 +0,0 @@ -# frozen_string_literal: true - -require 'cuprum/collections/rspec' - -module Cuprum::Collections::RSpec - # Contract validating the behavior of an UpdateOne command implementation. - UPDATE_ONE_COMMAND_CONTRACT = lambda do - describe '#call' do - let(:mapped_data) do - defined?(super()) ? super() : data - end - let(:matching_data) { attributes } - let(:expected_data) do - defined?(super()) ? super() : matching_data - end - let(:primary_key_name) do - defined?(super()) ? super() : 'id' - end - let(:scoped) do - key = primary_key_name - value = entity[primary_key_name.to_s] - - query.where { { key => value } } - end - - it 'should validate the :entity keyword' do - expect(command) - .to validate_parameter(:call, :entity) - .using_constraint(entity_type) - end - - context 'when the item does not exist in the collection' do - let(:expected_error) do - Cuprum::Collections::Errors::NotFound.new( - attribute_name: primary_key_name, - attribute_value: attributes.fetch( - primary_key_name.to_s, - attributes[primary_key_name.intern] - ), - collection_name: collection_name, - primary_key: true - ) - end - let(:matching_data) { mapped_data.first } - - it 'should return a failing result' do - expect(command.call(entity: entity)) - .to be_a_failing_result - .with_error(expected_error) - end - - it 'should not append an item to the collection' do - expect { command.call(entity: entity) } - .not_to(change { query.reset.count }) - end - end - - context 'when the item exists in the collection' do - let(:data) { fixtures_data } - let(:matching_data) do - mapped_data.first.merge(super()) - end - - it 'should return a passing result' do - expect(command.call(entity: entity)) - .to be_a_passing_result - .with_value(be == expected_data) - end - - it 'should not append an item to the collection' do - expect { command.call(entity: entity) } - .not_to(change { query.reset.count }) - end - - it 'should set the attributes' do - command.call(entity: entity) - - expect(scoped.to_a.first).to be == expected_data - end - end - end - end -end diff --git a/lib/cuprum/collections/rspec/validate_one_command_contract.rb b/lib/cuprum/collections/rspec/validate_one_command_contract.rb deleted file mode 100644 index ab52cd2..0000000 --- a/lib/cuprum/collections/rspec/validate_one_command_contract.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -require 'cuprum/collections/rspec' - -module Cuprum::Collections::RSpec - # Contract validating the behavior of a ValidateOne command implementation. - VALIDATE_ONE_COMMAND_CONTRACT = lambda do |default_contract:| - describe '#call' do - it 'should validate the :contract keyword' do - expect(command) - .to validate_parameter(:call, :contract) - .with_value(Object.new.freeze) - .using_constraint(Stannum::Constraints::Base, optional: true) - end - - it 'should validate the :entity keyword' do - expect(command) - .to validate_parameter(:call, :entity) - .with_value(Object.new.freeze) - .using_constraint(entity_type) - end - - describe 'with contract: nil' do - if default_contract - context 'when the entity does not match the default contract' do - let(:attributes) { invalid_default_attributes } - let(:expected_error) do - Cuprum::Collections::Errors::FailedValidation.new( - entity_class: entity.class, - errors: expected_errors - ) - end - - it 'should return a failing result' do - expect(command.call(entity: entity)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - context 'when the entity matches the default contract' do - let(:attributes) { valid_default_attributes } - - it 'should return a passing result' do - expect(command.call(entity: entity)) - .to be_a_passing_result - .with_value(entity) - end - end - else - let(:attributes) { valid_attributes } - let(:expected_error) do - Cuprum::Collections::Errors::MissingDefaultContract.new( - entity_class: entity.class - ) - end - - it 'should return a failing result' do - expect(command.call(entity: entity)) - .to be_a_failing_result - .with_error(expected_error) - end - end - end - - describe 'with contract: value' do - context 'when the entity does not match the contract' do - let(:attributes) { invalid_attributes } - let(:errors) { contract.errors_for(entity) } - let(:expected_error) do - Cuprum::Collections::Errors::FailedValidation.new( - entity_class: entity.class, - errors: errors - ) - end - - it 'should return a failing result' do - expect(command.call(contract: contract, entity: entity)) - .to be_a_failing_result - .with_error(expected_error) - end - end - - context 'when the entity matches the contract' do - let(:attributes) { valid_attributes } - - it 'should return a passing result' do - expect(command.call(contract: contract, entity: entity)) - .to be_a_passing_result - .with_value(entity) - end - end - end - end - end -end diff --git a/spec/cuprum/collections/basic/command_spec.rb b/spec/cuprum/collections/basic/command_spec.rb index 6720a62..ed8dcfe 100644 --- a/spec/cuprum/collections/basic/command_spec.rb +++ b/spec/cuprum/collections/basic/command_spec.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true require 'cuprum/collections/basic/command' -require 'cuprum/collections/basic/rspec/command_contract' +require 'cuprum/collections/rspec/contracts/basic/command_contracts' require 'cuprum/collections/rspec/fixtures' RSpec.describe Cuprum::Collections::Basic::Command do + include Cuprum::Collections::RSpec::Contracts::Basic::CommandContracts + subject(:command) do described_class.new( collection_name: collection_name, @@ -27,7 +29,7 @@ end end - include_contract Cuprum::Collections::Basic::RSpec::COMMAND_CONTRACT + include_contract 'should be a basic command' describe '#call' do it 'should define the method' do diff --git a/spec/cuprum/collections/basic/commands/assign_one_spec.rb b/spec/cuprum/collections/basic/commands/assign_one_spec.rb index 2b6a69d..9264319 100644 --- a/spec/cuprum/collections/basic/commands/assign_one_spec.rb +++ b/spec/cuprum/collections/basic/commands/assign_one_spec.rb @@ -3,13 +3,14 @@ require 'stannum/constraints/types/hash_with_string_keys' require 'cuprum/collections/basic/commands/assign_one' -require 'cuprum/collections/basic/rspec/command_contract' -require 'cuprum/collections/rspec/assign_one_command_contract' - -require 'support/examples/basic_command_examples' +require 'cuprum/collections/rspec/contracts/basic/command_contracts' +require 'cuprum/collections/rspec/contracts/command_contracts' RSpec.describe Cuprum::Collections::Basic::Commands::AssignOne do - include Spec::Support::Examples::BasicCommandExamples + include Cuprum::Collections::RSpec::Contracts::Basic::CommandContracts + include Cuprum::Collections::RSpec::Contracts::CommandContracts + + with_contract 'with basic command contexts' include_context 'with parameters for a basic contract' @@ -21,8 +22,8 @@ ) end - let(:initial_attributes) { {} } - let(:entity) { initial_attributes } + let(:initial_attributes) { {} } + let(:entity) { initial_attributes } let(:expected_value) do SleepingKingStudios::Tools::HashTools .instance @@ -39,8 +40,8 @@ end end - include_contract Cuprum::Collections::Basic::RSpec::COMMAND_CONTRACT + include_contract 'should be a basic command' - include_contract Cuprum::Collections::RSpec::ASSIGN_ONE_COMMAND_CONTRACT, + include_contract 'should be an assign one command', allow_extra_attributes: true end diff --git a/spec/cuprum/collections/basic/commands/build_one_spec.rb b/spec/cuprum/collections/basic/commands/build_one_spec.rb index c9fd381..920edb4 100644 --- a/spec/cuprum/collections/basic/commands/build_one_spec.rb +++ b/spec/cuprum/collections/basic/commands/build_one_spec.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true require 'cuprum/collections/basic/commands/build_one' -require 'cuprum/collections/basic/rspec/command_contract' -require 'cuprum/collections/rspec/build_one_command_contract' - -require 'support/examples/basic_command_examples' +require 'cuprum/collections/rspec/contracts/basic/command_contracts' +require 'cuprum/collections/rspec/contracts/command_contracts' RSpec.describe Cuprum::Collections::Basic::Commands::BuildOne do - include Spec::Support::Examples::BasicCommandExamples + include Cuprum::Collections::RSpec::Contracts::Basic::CommandContracts + include Cuprum::Collections::RSpec::Contracts::CommandContracts + + with_contract 'with basic command contexts' include_context 'with parameters for a basic contract' @@ -35,8 +36,8 @@ end end - include_contract Cuprum::Collections::Basic::RSpec::COMMAND_CONTRACT + include_contract 'should be a basic command' - include_contract Cuprum::Collections::RSpec::BUILD_ONE_COMMAND_CONTRACT, + include_contract 'should be a build one command', allow_extra_attributes: true end diff --git a/spec/cuprum/collections/basic/commands/destroy_one_spec.rb b/spec/cuprum/collections/basic/commands/destroy_one_spec.rb index d21956f..1474e76 100644 --- a/spec/cuprum/collections/basic/commands/destroy_one_spec.rb +++ b/spec/cuprum/collections/basic/commands/destroy_one_spec.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true require 'cuprum/collections/basic/commands/destroy_one' -require 'cuprum/collections/basic/rspec/command_contract' -require 'cuprum/collections/rspec/destroy_one_command_contract' - -require 'support/examples/basic_command_examples' +require 'cuprum/collections/rspec/contracts/basic/command_contracts' +require 'cuprum/collections/rspec/contracts/command_contracts' RSpec.describe Cuprum::Collections::Basic::Commands::DestroyOne do - include Spec::Support::Examples::BasicCommandExamples + include Cuprum::Collections::RSpec::Contracts::Basic::CommandContracts + include Cuprum::Collections::RSpec::Contracts::CommandContracts + + with_contract 'with basic command contexts' include_context 'with parameters for a basic contract' @@ -33,11 +34,11 @@ end end - include_contract Cuprum::Collections::Basic::RSpec::COMMAND_CONTRACT + include_contract 'should be a basic command' - include_contract Cuprum::Collections::RSpec::DESTROY_ONE_COMMAND_CONTRACT + include_contract 'should be a destroy one command' wrap_context 'with a custom primary key' do - include_contract Cuprum::Collections::RSpec::DESTROY_ONE_COMMAND_CONTRACT + include_contract 'should be a destroy one command' end end diff --git a/spec/cuprum/collections/basic/commands/find_many_spec.rb b/spec/cuprum/collections/basic/commands/find_many_spec.rb index 3f43460..ceebfd9 100644 --- a/spec/cuprum/collections/basic/commands/find_many_spec.rb +++ b/spec/cuprum/collections/basic/commands/find_many_spec.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true require 'cuprum/collections/basic/commands/find_many' -require 'cuprum/collections/basic/rspec/command_contract' -require 'cuprum/collections/rspec/find_many_command_contract' - -require 'support/examples/basic_command_examples' +require 'cuprum/collections/rspec/contracts/basic/command_contracts' +require 'cuprum/collections/rspec/contracts/command_contracts' RSpec.describe Cuprum::Collections::Basic::Commands::FindMany do - include Spec::Support::Examples::BasicCommandExamples + include Cuprum::Collections::RSpec::Contracts::Basic::CommandContracts + include Cuprum::Collections::RSpec::Contracts::CommandContracts + + with_contract 'with basic command contexts' include_context 'with parameters for a basic contract' @@ -38,11 +39,11 @@ end end - include_contract Cuprum::Collections::Basic::RSpec::COMMAND_CONTRACT + include_contract 'should be a basic command' - include_contract Cuprum::Collections::RSpec::FIND_MANY_COMMAND_CONTRACT + include_contract 'should be a find many command' wrap_context 'with a custom primary key' do - include_contract Cuprum::Collections::RSpec::FIND_MANY_COMMAND_CONTRACT + include_contract 'should be a find many command' end end diff --git a/spec/cuprum/collections/basic/commands/find_matching_spec.rb b/spec/cuprum/collections/basic/commands/find_matching_spec.rb index b187e28..b0b6954 100644 --- a/spec/cuprum/collections/basic/commands/find_matching_spec.rb +++ b/spec/cuprum/collections/basic/commands/find_matching_spec.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true require 'cuprum/collections/basic/commands/find_matching' -require 'cuprum/collections/basic/rspec/command_contract' -require 'cuprum/collections/rspec/find_matching_command_contract' - -require 'support/examples/basic_command_examples' +require 'cuprum/collections/rspec/contracts/basic/command_contracts' +require 'cuprum/collections/rspec/contracts/command_contracts' RSpec.describe Cuprum::Collections::Basic::Commands::FindMatching do - include Spec::Support::Examples::BasicCommandExamples + include Cuprum::Collections::RSpec::Contracts::Basic::CommandContracts + include Cuprum::Collections::RSpec::Contracts::CommandContracts + + with_contract 'with basic command contexts' include_context 'with parameters for a basic contract' @@ -29,7 +30,7 @@ end end - include_contract Cuprum::Collections::Basic::RSpec::COMMAND_CONTRACT + include_contract 'should be a basic command' - include_contract Cuprum::Collections::RSpec::FIND_MATCHING_COMMAND_CONTRACT + include_contract 'should be a find matching command' end diff --git a/spec/cuprum/collections/basic/commands/find_one_spec.rb b/spec/cuprum/collections/basic/commands/find_one_spec.rb index 11b5313..564c858 100644 --- a/spec/cuprum/collections/basic/commands/find_one_spec.rb +++ b/spec/cuprum/collections/basic/commands/find_one_spec.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true require 'cuprum/collections/basic/commands/find_one' -require 'cuprum/collections/basic/rspec/command_contract' -require 'cuprum/collections/rspec/find_one_command_contract' - -require 'support/examples/basic_command_examples' +require 'cuprum/collections/rspec/contracts/basic/command_contracts' +require 'cuprum/collections/rspec/contracts/command_contracts' RSpec.describe Cuprum::Collections::Basic::Commands::FindOne do - include Spec::Support::Examples::BasicCommandExamples + include Cuprum::Collections::RSpec::Contracts::Basic::CommandContracts + include Cuprum::Collections::RSpec::Contracts::CommandContracts + + with_contract 'with basic command contexts' include_context 'with parameters for a basic contract' @@ -33,11 +34,11 @@ end end - include_contract Cuprum::Collections::Basic::RSpec::COMMAND_CONTRACT + include_contract 'should be a basic command' - include_contract Cuprum::Collections::RSpec::FIND_ONE_COMMAND_CONTRACT + include_contract 'should be a find one command' wrap_context 'with a custom primary key' do - include_contract Cuprum::Collections::RSpec::FIND_ONE_COMMAND_CONTRACT + include_contract 'should be a find one command' end end diff --git a/spec/cuprum/collections/basic/commands/insert_one_spec.rb b/spec/cuprum/collections/basic/commands/insert_one_spec.rb index f22147f..2b4f272 100644 --- a/spec/cuprum/collections/basic/commands/insert_one_spec.rb +++ b/spec/cuprum/collections/basic/commands/insert_one_spec.rb @@ -2,13 +2,14 @@ require 'cuprum/collections/basic/commands/insert_one' require 'cuprum/collections/basic/query' -require 'cuprum/collections/basic/rspec/command_contract' -require 'cuprum/collections/rspec/insert_one_command_contract' - -require 'support/examples/basic_command_examples' +require 'cuprum/collections/rspec/contracts/basic/command_contracts' +require 'cuprum/collections/rspec/contracts/command_contracts' RSpec.describe Cuprum::Collections::Basic::Commands::InsertOne do - include Spec::Support::Examples::BasicCommandExamples + include Cuprum::Collections::RSpec::Contracts::Basic::CommandContracts + include Cuprum::Collections::RSpec::Contracts::CommandContracts + + with_contract 'with basic command contexts' include_context 'with parameters for a basic contract' @@ -51,9 +52,9 @@ def tools end end - include_contract Cuprum::Collections::Basic::RSpec::COMMAND_CONTRACT + include_contract 'should be a basic command' - include_contract Cuprum::Collections::RSpec::INSERT_ONE_COMMAND_CONTRACT + include_contract 'should be an insert one command' wrap_context 'with a custom primary key' do let(:attributes) do @@ -62,6 +63,6 @@ def tools .merge(uuid: '00000000-0000-0000-0000-000000000000') end - include_contract Cuprum::Collections::RSpec::INSERT_ONE_COMMAND_CONTRACT + include_contract 'should be an insert one command' end end diff --git a/spec/cuprum/collections/basic/commands/update_one_spec.rb b/spec/cuprum/collections/basic/commands/update_one_spec.rb index a363729..9b44414 100644 --- a/spec/cuprum/collections/basic/commands/update_one_spec.rb +++ b/spec/cuprum/collections/basic/commands/update_one_spec.rb @@ -2,13 +2,14 @@ require 'cuprum/collections/basic/commands/update_one' require 'cuprum/collections/basic/query' -require 'cuprum/collections/basic/rspec/command_contract' -require 'cuprum/collections/rspec/update_one_command_contract' - -require 'support/examples/basic_command_examples' +require 'cuprum/collections/rspec/contracts/basic/command_contracts' +require 'cuprum/collections/rspec/contracts/command_contracts' RSpec.describe Cuprum::Collections::Basic::Commands::UpdateOne do - include Spec::Support::Examples::BasicCommandExamples + include Cuprum::Collections::RSpec::Contracts::Basic::CommandContracts + include Cuprum::Collections::RSpec::Contracts::CommandContracts + + with_contract 'with basic command contexts' include_context 'with parameters for a basic contract' @@ -51,9 +52,9 @@ def tools end end - include_contract Cuprum::Collections::Basic::RSpec::COMMAND_CONTRACT + include_contract 'should be a basic command' - include_contract Cuprum::Collections::RSpec::UPDATE_ONE_COMMAND_CONTRACT + include_contract 'should be an update one command' wrap_context 'with a custom primary key' do let(:attributes) do @@ -62,6 +63,6 @@ def tools .merge(uuid: '00000000-0000-0000-0000-000000000000') end - include_contract Cuprum::Collections::RSpec::UPDATE_ONE_COMMAND_CONTRACT + include_contract 'should be an update one command' end end diff --git a/spec/cuprum/collections/basic/commands/validate_one_spec.rb b/spec/cuprum/collections/basic/commands/validate_one_spec.rb index 1e5c6a3..9ac96c9 100644 --- a/spec/cuprum/collections/basic/commands/validate_one_spec.rb +++ b/spec/cuprum/collections/basic/commands/validate_one_spec.rb @@ -4,13 +4,14 @@ require 'stannum/contracts/hash_contract' require 'cuprum/collections/basic/commands/validate_one' -require 'cuprum/collections/basic/rspec/command_contract' -require 'cuprum/collections/rspec/validate_one_command_contract' - -require 'support/examples/basic_command_examples' +require 'cuprum/collections/rspec/contracts/basic/command_contracts' +require 'cuprum/collections/rspec/contracts/command_contracts' RSpec.describe Cuprum::Collections::Basic::Commands::ValidateOne do - include Spec::Support::Examples::BasicCommandExamples + include Cuprum::Collections::RSpec::Contracts::Basic::CommandContracts + include Cuprum::Collections::RSpec::Contracts::CommandContracts + + with_contract 'with basic command contexts' include_context 'with parameters for a basic contract' @@ -47,9 +48,9 @@ def tools end end - include_contract Cuprum::Collections::Basic::RSpec::COMMAND_CONTRACT + include_contract 'should be a basic command' - include_contract Cuprum::Collections::RSpec::VALIDATE_ONE_COMMAND_CONTRACT, + include_contract 'should be a validate one command', default_contract: false context 'when the collection has a default contract' do @@ -73,7 +74,7 @@ def tools } end - include_contract Cuprum::Collections::RSpec::VALIDATE_ONE_COMMAND_CONTRACT, + include_contract 'should be a validate one command', default_contract: true end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0aeacb2..ebc0e49 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -24,7 +24,13 @@ ) # Isolated namespace for defining spec-only or transient objects. -module Spec; end +module Spec + module WithContract + include RSpec::SleepingKingStudios::Concerns::IncludeContract + + alias with_contract include_contract + end +end require 'support/error_messages' @@ -38,6 +44,7 @@ module Spec; end config.extend RSpec::SleepingKingStudios::Concerns::IncludeContract config.extend RSpec::SleepingKingStudios::Concerns::WrapExamples config.include RSpec::SleepingKingStudios::Examples::PropertyExamples + config.extend Spec::WithContract config.disable_monkey_patching! diff --git a/spec/support/examples/basic_command_examples.rb b/spec/support/examples/basic_command_examples.rb deleted file mode 100644 index e140045..0000000 --- a/spec/support/examples/basic_command_examples.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -require 'rspec/sleeping_king_studios/concerns/shared_example_group' - -require 'cuprum/collections/basic/query' -require 'cuprum/collections/rspec/fixtures' - -require 'support/examples' - -module Spec::Support::Examples - module BasicCommandExamples - extend RSpec::SleepingKingStudios::Concerns::SharedExampleGroup - - shared_context 'with parameters for a basic contract' do - let(:collection_name) { 'books' } - let(:data) { [] } - let(:mapped_data) { data } - let(:constructor_options) { {} } - let(:expected_options) { {} } - let(:primary_key_name) { :id } - let(:primary_key_type) { Integer } - let(:entity_type) do - Stannum::Constraints::Types::HashWithStringKeys.new - end - let(:fixtures_data) do - Cuprum::Collections::RSpec::BOOKS_FIXTURES.dup - end - let(:query) do - Cuprum::Collections::Basic::Query.new(mapped_data) - end - let(:scope) do - Cuprum::Collections::Basic::Query.new(mapped_data).where(scope_filter) - end - end - - shared_context 'with a custom primary key' do - let(:primary_key_name) { :uuid } - let(:primary_key_type) { String } - let(:constructor_options) do - super().merge( - primary_key_name: primary_key_name, - primary_key_type: primary_key_type - ) - end - let(:mapped_data) do - data.map do |item| - item.dup.tap do |hsh| - value = hsh.delete('id').to_s.rjust(12, '0') - - hsh['uuid'] = "00000000-0000-0000-0000-#{value}" - end - end - end - let(:invalid_primary_key_value) { '00000000-0000-0000-0000-000000000100' } - let(:valid_primary_key_value) { '00000000-0000-0000-0000-000000000000' } - let(:invalid_primary_key_values) do - %w[ - 00000000-0000-0000-0000-000000000100 - 00000000-0000-0000-0000-000000000101 - 00000000-0000-0000-0000-000000000102 - ] - end - let(:valid_primary_key_values) do - %w[ - 00000000-0000-0000-0000-000000000000 - 00000000-0000-0000-0000-000000000001 - 00000000-0000-0000-0000-000000000002 - ] - end - end - end -end From 9d1a784bc070723b10076c05dbdedcd45c7b8c19 Mon Sep 17 00:00:00 2001 From: Rob Smith Date: Mon, 27 Nov 2023 22:08:47 -0500 Subject: [PATCH 3/4] Refactor fixtures. --- lib/cuprum/collections/rspec.rb | 1 + .../contracts/basic/command_contracts.rb | 2 +- .../rspec/contracts/query_contracts.rb | 2 +- lib/cuprum/collections/rspec/fixtures.rb | 167 +++++++++--------- .../collections/basic/collection_spec.rb | 2 +- spec/cuprum/collections/basic/command_spec.rb | 4 +- .../collections/basic/repository_spec.rb | 8 +- .../commands/find_one_matching_spec.rb | 2 +- spec/integration/commands/create_spec.rb | 4 +- spec/integration/commands/destroy_spec.rb | 4 +- spec/integration/commands/index_spec.rb | 4 +- spec/integration/commands/show_spec.rb | 4 +- spec/integration/commands/update_spec.rb | 4 +- 13 files changed, 112 insertions(+), 96 deletions(-) diff --git a/lib/cuprum/collections/rspec.rb b/lib/cuprum/collections/rspec.rb index 838d592..4465f5a 100644 --- a/lib/cuprum/collections/rspec.rb +++ b/lib/cuprum/collections/rspec.rb @@ -6,5 +6,6 @@ module Cuprum::Collections # Namespace for RSpec contracts, which validate collection implementations. module RSpec autoload :Contracts, 'cuprum/collections/rspec/contracts' + autoload :Fixtures, 'cuprum/collections/rspec/fixtures' end end diff --git a/lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb b/lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb index de3f7d9..bb7f0ff 100644 --- a/lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb +++ b/lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb @@ -428,7 +428,7 @@ module WithBasicCommandContextsContract Stannum::Constraints::Types::HashWithStringKeys.new end let(:fixtures_data) do - Cuprum::Collections::RSpec::BOOKS_FIXTURES.dup + Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES.dup end let(:query) do Cuprum::Collections::Basic::Query.new(mapped_data) diff --git a/lib/cuprum/collections/rspec/contracts/query_contracts.rb b/lib/cuprum/collections/rspec/contracts/query_contracts.rb index 4595d19..341de34 100644 --- a/lib/cuprum/collections/rspec/contracts/query_contracts.rb +++ b/lib/cuprum/collections/rspec/contracts/query_contracts.rb @@ -11,7 +11,7 @@ module QueryContracts module ShouldBeAQuery extend RSpec::SleepingKingStudios::Contract - BOOKS_FIXTURES = Cuprum::Collections::RSpec::BOOKS_FIXTURES + BOOKS_FIXTURES = Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES private_constant :BOOKS_FIXTURES OPERATORS = Cuprum::Collections::Queries::Operators diff --git a/lib/cuprum/collections/rspec/fixtures.rb b/lib/cuprum/collections/rspec/fixtures.rb index afeb9d8..7fc189b 100644 --- a/lib/cuprum/collections/rspec/fixtures.rb +++ b/lib/cuprum/collections/rspec/fixtures.rb @@ -4,86 +4,89 @@ module Cuprum::Collections::RSpec # Sample data for validating collection implementations. - BOOKS_FIXTURES = [ - { - 'id' => 0, - 'title' => 'The Hobbit', - 'author' => 'J.R.R. Tolkien', - 'series' => nil, - 'category' => 'Science Fiction and Fantasy', - 'published_at' => '1937-09-21' - }, - { - 'id' => 1, - 'title' => 'The Silmarillion', - 'author' => 'J.R.R. Tolkien', - 'series' => nil, - 'category' => 'Science Fiction and Fantasy', - 'published_at' => '1977-09-15' - }, - { - 'id' => 2, - 'title' => 'The Fellowship of the Ring', - 'author' => 'J.R.R. Tolkien', - 'series' => 'The Lord of the Rings', - 'category' => 'Science Fiction and Fantasy', - 'published_at' => '1954-07-29' - }, - { - 'id' => 3, - 'title' => 'The Two Towers', - 'author' => 'J.R.R. Tolkien', - 'series' => 'The Lord of the Rings', - 'category' => 'Science Fiction and Fantasy', - 'published_at' => '1954-11-11' - }, - { - 'id' => 4, - 'title' => 'The Return of the King', - 'author' => 'J.R.R. Tolkien', - 'series' => 'The Lord of the Rings', - 'category' => 'Science Fiction and Fantasy', - 'published_at' => '1955-10-20' - }, - { - 'id' => 5, - 'title' => 'The Word for World is Forest', - 'author' => 'Ursula K. LeGuin', - 'series' => nil, - 'category' => 'Science Fiction and Fantasy', - 'published_at' => '1972-03-13' - }, - { - 'id' => 6, - 'title' => 'The Ones Who Walk Away From Omelas', - 'author' => 'Ursula K. LeGuin', - 'series' => nil, - 'category' => 'Science Fiction and Fantasy', - 'published_at' => '1973-10-01' - }, - { - 'id' => 7, - 'title' => 'A Wizard of Earthsea', - 'author' => 'Ursula K. LeGuin', - 'series' => 'Earthsea', - 'category' => 'Science Fiction and Fantasy', - 'published_at' => '1968-11-01' - }, - { - 'id' => 8, - 'title' => 'The Tombs of Atuan', - 'author' => 'Ursula K. LeGuin', - 'series' => 'Earthsea', - 'category' => 'Science Fiction and Fantasy', - 'published_at' => '1970-12-01' - }, - { - 'id' => 9, - 'title' => 'The Farthest Shore', - 'author' => 'Ursula K. LeGuin', - 'series' => 'Earthsea', - 'category' => 'Science Fiction and Fantasy', - 'published_at' => '1972-09-01' - } - ].map(&:freeze).freeze + module Fixtures + # Sample data for Book objects. + BOOKS_FIXTURES = [ + { + 'id' => 0, + 'title' => 'The Hobbit', + 'author' => 'J.R.R. Tolkien', + 'series' => nil, + 'category' => 'Science Fiction and Fantasy', + 'published_at' => '1937-09-21' + }, + { + 'id' => 1, + 'title' => 'The Silmarillion', + 'author' => 'J.R.R. Tolkien', + 'series' => nil, + 'category' => 'Science Fiction and Fantasy', + 'published_at' => '1977-09-15' + }, + { + 'id' => 2, + 'title' => 'The Fellowship of the Ring', + 'author' => 'J.R.R. Tolkien', + 'series' => 'The Lord of the Rings', + 'category' => 'Science Fiction and Fantasy', + 'published_at' => '1954-07-29' + }, + { + 'id' => 3, + 'title' => 'The Two Towers', + 'author' => 'J.R.R. Tolkien', + 'series' => 'The Lord of the Rings', + 'category' => 'Science Fiction and Fantasy', + 'published_at' => '1954-11-11' + }, + { + 'id' => 4, + 'title' => 'The Return of the King', + 'author' => 'J.R.R. Tolkien', + 'series' => 'The Lord of the Rings', + 'category' => 'Science Fiction and Fantasy', + 'published_at' => '1955-10-20' + }, + { + 'id' => 5, + 'title' => 'The Word for World is Forest', + 'author' => 'Ursula K. LeGuin', + 'series' => nil, + 'category' => 'Science Fiction and Fantasy', + 'published_at' => '1972-03-13' + }, + { + 'id' => 6, + 'title' => 'The Ones Who Walk Away From Omelas', + 'author' => 'Ursula K. LeGuin', + 'series' => nil, + 'category' => 'Science Fiction and Fantasy', + 'published_at' => '1973-10-01' + }, + { + 'id' => 7, + 'title' => 'A Wizard of Earthsea', + 'author' => 'Ursula K. LeGuin', + 'series' => 'Earthsea', + 'category' => 'Science Fiction and Fantasy', + 'published_at' => '1968-11-01' + }, + { + 'id' => 8, + 'title' => 'The Tombs of Atuan', + 'author' => 'Ursula K. LeGuin', + 'series' => 'Earthsea', + 'category' => 'Science Fiction and Fantasy', + 'published_at' => '1970-12-01' + }, + { + 'id' => 9, + 'title' => 'The Farthest Shore', + 'author' => 'Ursula K. LeGuin', + 'series' => 'Earthsea', + 'category' => 'Science Fiction and Fantasy', + 'published_at' => '1972-09-01' + } + ].map(&:freeze).freeze + end end diff --git a/spec/cuprum/collections/basic/collection_spec.rb b/spec/cuprum/collections/basic/collection_spec.rb index 78b1054..9d4368f 100644 --- a/spec/cuprum/collections/basic/collection_spec.rb +++ b/spec/cuprum/collections/basic/collection_spec.rb @@ -20,7 +20,7 @@ end shared_context 'when the collection has many items' do - let(:data) { Cuprum::Collections::RSpec::BOOKS_FIXTURES } + let(:data) { Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES.dup } let(:items) { data } end diff --git a/spec/cuprum/collections/basic/command_spec.rb b/spec/cuprum/collections/basic/command_spec.rb index ed8dcfe..b890e0d 100644 --- a/spec/cuprum/collections/basic/command_spec.rb +++ b/spec/cuprum/collections/basic/command_spec.rb @@ -15,8 +15,10 @@ ) end + let(:data) do + Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES.dup + end let(:collection_name) { 'books' } - let(:data) { Cuprum::Collections::RSpec::BOOKS_FIXTURES } let(:constructor_options) { {} } describe '.new' do diff --git a/spec/cuprum/collections/basic/repository_spec.rb b/spec/cuprum/collections/basic/repository_spec.rb index 120d567..6852b4b 100644 --- a/spec/cuprum/collections/basic/repository_spec.rb +++ b/spec/cuprum/collections/basic/repository_spec.rb @@ -94,7 +94,7 @@ describe 'with data: an Array' do let(:data) do - Cuprum::Collections::RSpec::BOOKS_FIXTURES + Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES.dup end let(:collection_options) { super().merge(data: data) } @@ -103,7 +103,7 @@ context 'when initialized with data: value' do let(:data) do - { 'books' => Cuprum::Collections::RSpec::BOOKS_FIXTURES } + { 'books' => Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES.dup } end let(:constructor_options) { super().merge(data: data) } @@ -141,7 +141,7 @@ describe 'with data: an Array' do let(:data) do - Cuprum::Collections::RSpec::BOOKS_FIXTURES + Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES.dup end let(:collection_options) { super().merge(data: data) } @@ -150,7 +150,7 @@ context 'when initialized with data: value' do let(:data) do - { 'books' => Cuprum::Collections::RSpec::BOOKS_FIXTURES } + { 'books' => Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES.dup } end let(:constructor_options) { super().merge(data: data) } diff --git a/spec/cuprum/collections/commands/find_one_matching_spec.rb b/spec/cuprum/collections/commands/find_one_matching_spec.rb index ba109c1..ccf8bb7 100644 --- a/spec/cuprum/collections/commands/find_one_matching_spec.rb +++ b/spec/cuprum/collections/commands/find_one_matching_spec.rb @@ -71,7 +71,7 @@ end context 'when there are many entities' do - let(:data) { Cuprum::Collections::RSpec::BOOKS_FIXTURES } + let(:data) { Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES.dup } describe 'with attributes: a Hash that does not match any entities' do let(:attributes) { { 'author' => 'Jules Verne' } } diff --git a/spec/integration/commands/create_spec.rb b/spec/integration/commands/create_spec.rb index 192085d..75bae2d 100644 --- a/spec/integration/commands/create_spec.rb +++ b/spec/integration/commands/create_spec.rb @@ -11,8 +11,10 @@ RSpec.describe Spec::Support::Commands::Create do subject(:command) { described_class.new(collection) } + let(:data) do + Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES.dup + end let(:collection_name) { 'books' } - let(:data) { Cuprum::Collections::RSpec::BOOKS_FIXTURES.dup } let(:collection_options) do { name: collection_name, diff --git a/spec/integration/commands/destroy_spec.rb b/spec/integration/commands/destroy_spec.rb index 7c3f7a7..5736ee3 100644 --- a/spec/integration/commands/destroy_spec.rb +++ b/spec/integration/commands/destroy_spec.rb @@ -8,8 +8,10 @@ RSpec.describe Spec::Support::Commands::Destroy do subject(:command) { described_class.new(collection) } + let(:data) do + Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES.dup + end let(:collection_name) { 'books' } - let(:data) { Cuprum::Collections::RSpec::BOOKS_FIXTURES.dup } let(:collection_options) do { name: collection_name, diff --git a/spec/integration/commands/index_spec.rb b/spec/integration/commands/index_spec.rb index 9a1b9c5..76f00d8 100644 --- a/spec/integration/commands/index_spec.rb +++ b/spec/integration/commands/index_spec.rb @@ -8,8 +8,10 @@ RSpec.describe Spec::Support::Commands::Index do subject(:command) { described_class.new(collection) } + let(:data) do + Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES.dup + end let(:collection_name) { 'books' } - let(:data) { Cuprum::Collections::RSpec::BOOKS_FIXTURES.dup } let(:collection_options) do { name: collection_name, diff --git a/spec/integration/commands/show_spec.rb b/spec/integration/commands/show_spec.rb index a07e996..9fd723b 100644 --- a/spec/integration/commands/show_spec.rb +++ b/spec/integration/commands/show_spec.rb @@ -8,8 +8,10 @@ RSpec.describe Spec::Support::Commands::Show do subject(:command) { described_class.new(collection) } + let(:data) do + Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES.dup + end let(:collection_name) { 'books' } - let(:data) { Cuprum::Collections::RSpec::BOOKS_FIXTURES.dup } let(:collection_options) do { name: collection_name, diff --git a/spec/integration/commands/update_spec.rb b/spec/integration/commands/update_spec.rb index e6e48bc..7ea5610 100644 --- a/spec/integration/commands/update_spec.rb +++ b/spec/integration/commands/update_spec.rb @@ -11,8 +11,10 @@ RSpec.describe Spec::Support::Commands::Update do subject(:command) { described_class.new(collection) } + let(:data) do + Cuprum::Collections::RSpec::Fixtures::BOOKS_FIXTURES.dup + end let(:collection_name) { 'books' } - let(:data) { Cuprum::Collections::RSpec::BOOKS_FIXTURES.dup } let(:collection_options) do { name: collection_name, From ecb1fb466a5248d493692ee638f571b849b97235 Mon Sep 17 00:00:00 2001 From: Rob Smith Date: Mon, 27 Nov 2023 22:35:28 -0500 Subject: [PATCH 4/4] Update documentation. --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83f8d9e..9b695a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,19 +7,26 @@ Implemented `Cuprum::Collections::Association`, which represents an association between entity types. - Implemented `Cuprum::Collections::Associations::BelongsTo`. +- Implemented `Cuprum::Collections::Associations::HasMany`. +- Implemented `Cuprum::Collections::Associations::HasOne`. ### Collections Defined standard interface for collections. - Implemented `Cuprum::Collections::Collection`. -- Collections can now be initialized with any combination of collection name and entity class. +- Collections can now be initialized with any combination of collection name, entity class, and qualified name. Updated `Cuprum::Collections::Basic::Collection`. - Implemented `#count` method. - Implemented `#qualified_name` method. +Deprecated certain collection methods and corresponding constructor keywords: + +- `#collection_name`: Use `#name`. +- `#member_name`: Use `#singular_name`. + ### Commands Implemented built-in Commands, which take a `:collection` parameter: @@ -49,6 +56,10 @@ Defined standard interface for repositories. Defined `Cuprum::Collections::Resource`, representing a singular or plural resource of entities. +### RSpec + +- **(Breaking Change)** Contracts have been refactored to use `RSpec::SleepingKingStudios::Contract`. Contract names and filenames have changed. + ## 0.2.0 Implemented `Cuprum::Collections::Repository`.