From cb2e295d1473e4ec95cbda7015afc9bac15509db Mon Sep 17 00:00:00 2001 From: Rob Smith Date: Wed, 13 Nov 2024 06:23:14 -0500 Subject: [PATCH] Migrate Relation contracts to deferred examples. --- .../rspec/contracts/association_contracts.rb | 3 + .../rspec/contracts/collection_contracts.rb | 3 + .../rspec/contracts/relation_contracts.rb | 3 + .../rspec/deferred/association_examples.rb | 2098 +++++++++++++++++ .../rspec/deferred/collection_examples.rb | 372 +++ .../rspec/deferred/relation_examples.rb | 1213 ++++++++++ .../rspec/deferred/resource_examples.rb | 24 + spec/cuprum/collections/association_spec.rb | 10 +- .../associations/belongs_to_spec.rb | 6 +- .../collections/associations/has_many_spec.rb | 6 +- .../collections/associations/has_one_spec.rb | 6 +- .../collections/basic/collection_spec.rb | 10 +- spec/cuprum/collections/collection_spec.rb | 12 +- spec/cuprum/collections/relation_spec.rb | 12 +- spec/cuprum/collections/resource_spec.rb | 11 +- 15 files changed, 3751 insertions(+), 38 deletions(-) create mode 100644 lib/cuprum/collections/rspec/deferred/association_examples.rb create mode 100644 lib/cuprum/collections/rspec/deferred/collection_examples.rb create mode 100644 lib/cuprum/collections/rspec/deferred/relation_examples.rb create mode 100644 lib/cuprum/collections/rspec/deferred/resource_examples.rb diff --git a/lib/cuprum/collections/rspec/contracts/association_contracts.rb b/lib/cuprum/collections/rspec/contracts/association_contracts.rb index 7cc7670..690216f 100644 --- a/lib/cuprum/collections/rspec/contracts/association_contracts.rb +++ b/lib/cuprum/collections/rspec/contracts/association_contracts.rb @@ -5,6 +5,9 @@ module Cuprum::Collections::RSpec::Contracts # Contracts for asserting on Association objects. + # + # @deprecated 0.5.0 Association contracts are deprecated. Use + # Deferred::AssociationExamples instead. module AssociationContracts # Contract validating the behavior of an Association. module ShouldBeAnAssociationContract diff --git a/lib/cuprum/collections/rspec/contracts/collection_contracts.rb b/lib/cuprum/collections/rspec/contracts/collection_contracts.rb index 26e98ed..6575e5a 100644 --- a/lib/cuprum/collections/rspec/contracts/collection_contracts.rb +++ b/lib/cuprum/collections/rspec/contracts/collection_contracts.rb @@ -5,6 +5,9 @@ module Cuprum::Collections::RSpec::Contracts # Contracts for asserting on Collection objects. + # + # @deprecated 0.5.0 Collection contracts are deprecated. Use + # Deferred::CollectionExamples instead. module CollectionContracts include Cuprum::Collections::RSpec::Contracts::RelationContracts diff --git a/lib/cuprum/collections/rspec/contracts/relation_contracts.rb b/lib/cuprum/collections/rspec/contracts/relation_contracts.rb index 612d9b4..76d6ecf 100644 --- a/lib/cuprum/collections/rspec/contracts/relation_contracts.rb +++ b/lib/cuprum/collections/rspec/contracts/relation_contracts.rb @@ -4,6 +4,9 @@ module Cuprum::Collections::RSpec::Contracts # Contracts for asserting on Relation objects. + # + # @deprecated 0.5.0 Relation contracts are deprecated. Use + # Deferred::RelationExamples instead. module RelationContracts # Contract asserting that the method validates the required parameters. module ShouldValidateTheParametersContract diff --git a/lib/cuprum/collections/rspec/deferred/association_examples.rb b/lib/cuprum/collections/rspec/deferred/association_examples.rb new file mode 100644 index 0000000..8f3ef0d --- /dev/null +++ b/lib/cuprum/collections/rspec/deferred/association_examples.rb @@ -0,0 +1,2098 @@ +# frozen_string_literal: true + +require 'rspec/sleeping_king_studios/deferred' + +require 'cuprum/collections/rspec/deferred' +require 'cuprum/collections/rspec/deferred/relation_examples' + +module Cuprum::Collections::RSpec::Deferred + # Deferred examples for testing associations. + module AssociationExamples + include RSpec::SleepingKingStudios::Deferred::Provider + + deferred_examples 'should be an Association' do + include Cuprum::Collections::RSpec::Deferred::RelationExamples + + example_class 'Author' + example_class 'Chapter' + example_class 'Writer' + + include_deferred 'should be a Relation' + + include_deferred 'should define Relation primary key' + + describe '#build_entities_query' do + it 'should define the method' do + expect(association) + .to respond_to(:build_entities_query) + .with_unlimited_arguments + .and_keywords(:allow_nil, :deduplicate) + end + end + + describe '#build_keys_query' do + it 'should define the method' do + expect(association) + .to respond_to(:build_keys_query) + .with_unlimited_arguments + .and_keywords(:allow_nil, :deduplicate) + end + end + + describe '#foreign_key_name' do + include_examples 'should define reader', :foreign_key_name + end + + describe '#inverse' do + include_examples 'should define reader', :inverse, nil + + context 'when initialized with inverse: value' do + let(:inverse) { described_class.new(name: 'authors') } + let(:constructor_options) do + super().merge(inverse:) + end + + it { expect(subject.inverse).to be == inverse } + end + + context 'with a copy with assigned inverse' do + subject { super().with_inverse(new_inverse) } + + let(:new_inverse) { described_class.new(name: 'chapters') } + + it { expect(subject.inverse).to be == new_inverse } + + context 'when initialized with inverse: value' do + let(:inverse) { described_class.new(name: 'authors') } + let(:constructor_options) do + super().merge(inverse:) + end + + it { expect(subject.inverse).to be == new_inverse } + end + end + end + + describe '#inverse_class' do + include_examples 'should define reader', :inverse_class, nil + + context 'when initialized with inverse: value' do + let(:inverse) { described_class.new(name: 'authors') } + let(:constructor_options) do + super().merge(inverse:) + end + + it { expect(subject.inverse_class).to be == Author } + + context 'when initialized with inverse_class: a Class' do + let(:constructor_options) do + super().merge(inverse_class: Writer) + end + + it { expect(subject.inverse_class).to be == Writer } + end + + context 'when initialized with inverse_class: a String' do + let(:constructor_options) do + super().merge(inverse_class: 'Writer') + end + + it { expect(subject.inverse_class).to be == Writer } + end + end + + context 'when initialized with inverse_class: a Class' do + let(:constructor_options) do + super().merge(inverse_class: Writer) + end + + it { expect(subject.inverse_class).to be == Writer } + end + + context 'when initialized with inverse_class: a String' do + let(:constructor_options) do + super().merge(inverse_class: 'Writer') + end + + it { expect(subject.inverse_class).to be == Writer } + end + + context 'with a copy with assigned inverse' do + subject do + super().tap(&:inverse_class).with_inverse(new_inverse) + end + + let(:new_inverse) { described_class.new(name: 'chapters') } + + it { expect(subject.inverse_class).to be == Chapter } + + context 'when initialized with inverse_class: a Class' do + let(:constructor_options) do + super().merge(inverse_class: Writer) + end + + it { expect(subject.inverse_class).to be == Writer } + end + + context 'when initialized with inverse_class: a String' do + let(:constructor_options) do + super().merge(inverse_class: 'Writer') + end + + it { expect(subject.inverse_class).to be == Writer } + end + + context 'when initialized with inverse: value' do + let(:inverse) { described_class.new(name: 'authors') } + let(:constructor_options) do + super().merge(inverse:) + end + + it { expect(subject.inverse_class).to be == Chapter } + end + end + end + + describe '#inverse_key_name' do + include_examples 'should define reader', :inverse_key_name + end + + describe '#inverse_name' do + include_examples 'should define reader', :inverse_name, nil + + context 'when initialized with inverse: value' do + let(:inverse) { described_class.new(name: 'authors') } + let(:constructor_options) do + super().merge(inverse:) + end + + it { expect(subject.inverse_name).to be == 'authors' } + + context 'when initialized with inverse_name: a String' do + let(:constructor_options) do + super().merge(inverse_name: 'writers') + end + + it { expect(subject.inverse_name).to be == 'writers' } + end + + context 'when initialized with inverse_name: a Symbol' do + let(:constructor_options) do + super().merge(inverse_name: :writers) + end + + it { expect(subject.inverse_name).to be == 'writers' } + end + end + + context 'when initialized with inverse_name: a String' do + let(:constructor_options) do + super().merge(inverse_name: 'writers') + end + + it { expect(subject.inverse_name).to be == 'writers' } + end + + context 'when initialized with inverse_name: a Symbol' do + let(:constructor_options) do + super().merge(inverse_name: :writers) + end + + it { expect(subject.inverse_name).to be == 'writers' } + end + + context 'with a copy with assigned inverse' do + subject do + super().tap(&:inverse_name).with_inverse(new_inverse) + end + + let(:new_inverse) { described_class.new(name: 'chapters') } + + it { expect(subject.inverse_name).to be == 'chapters' } + + context 'when initialized with inverse: value' do + let(:inverse) { described_class.new(name: 'authors') } + let(:constructor_options) do + super().merge(inverse:) + end + + it { expect(subject.inverse_name).to be == 'chapters' } + end + + context 'when initialized with inverse_name: a String' do + let(:constructor_options) do + super().merge(inverse_name: 'writers') + end + + it { expect(subject.inverse_name).to be == 'writers' } + end + + context 'when initialized with inverse_name: a Symbol' do + let(:constructor_options) do + super().merge(inverse_name: :writers) + end + + it { expect(subject.inverse_name).to be == 'writers' } + end + end + end + + describe '#map_entities_to_keys' do + it 'should define the method' do + expect(subject) + .to respond_to(:map_entities_to_keys) + .with_unlimited_arguments + .and_keywords(:allow_nil, :deduplicate, :strict) + end + end + + describe '#options' do + context 'when initialized with inverse: value' do + let(:inverse) { described_class.new(name: 'authors') } + let(:constructor_options) do + super().merge(inverse:) + end + + it { expect(subject.options).to be == {} } + end + end + + describe '#plural?' do + include_examples 'should define predicate', :plural? + end + + describe '#primary_key_query?' do + include_examples 'should define predicate', :primary_key_query? + end + + describe '#query_key_name' do + include_examples 'should define reader', :query_key_name + end + + describe '#singular_inverse_name' do + include_examples 'should define reader', :singular_inverse_name, nil + + context 'when initialized with inverse: value' do + let(:inverse) { described_class.new(name: 'authors') } + let(:constructor_options) do + super().merge(inverse:) + end + + it { expect(subject.singular_inverse_name).to be == 'author' } + + context 'when initialized with singular_inverse_name: a String' do + let(:singular_inverse_name) { 'writer' } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.singular_inverse_name).to be == 'writer' } + end + + context 'when initialized with singular_inverse_name: a Symbol' do + let(:singular_inverse_name) { :writer } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.singular_inverse_name).to be == 'writer' } + end + end + + context 'when initialized with inverse_name: value' do + let(:inverse_name) { 'authors' } + let(:constructor_options) do + super().merge(inverse_name:) + end + + it { expect(subject.singular_inverse_name).to be == 'author' } + + context 'when initialized with singular_inverse_name: a String' do + let(:singular_inverse_name) { 'writer' } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.singular_inverse_name).to be == 'writer' } + end + + context 'when initialized with singular_inverse_name: a Symbol' do + let(:singular_inverse_name) { :writer } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.singular_inverse_name).to be == 'writer' } + end + end + + context 'when initialized with singular_inverse_name: a String' do + let(:singular_inverse_name) { 'writer' } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.singular_inverse_name).to be == 'writer' } + end + + context 'when initialized with singular_inverse_name: a Symbol' do + let(:singular_inverse_name) { :writer } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.singular_inverse_name).to be == 'writer' } + end + + context 'with a copy with assigned inverse' do + subject do + super().tap(&:singular_inverse_name).with_inverse(new_inverse) + end + + let(:new_inverse) { described_class.new(name: 'chapters') } + + it { expect(subject.singular_inverse_name).to be == 'chapter' } + + context 'when initialized with inverse: value' do + let(:inverse) { described_class.new(name: 'authors') } + let(:constructor_options) do + super().merge(inverse:) + end + + it { expect(subject.singular_inverse_name).to be == 'chapter' } + end + + context 'when initialized with inverse_name: value' do + let(:inverse_name) { 'authors' } + let(:constructor_options) do + super().merge(inverse_name:) + end + + it { expect(subject.singular_inverse_name).to be == 'chapter' } + + context 'when initialized with singular_inverse_name: a String' do + let(:singular_inverse_name) { 'writer' } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.singular_inverse_name).to be == 'writer' } + end + + context 'when initialized with singular_inverse_name: a Symbol' do + let(:singular_inverse_name) { :writer } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.singular_inverse_name).to be == 'writer' } + end + end + + context 'when initialized with singular_inverse_name: a String' do + let(:singular_inverse_name) { 'writer' } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.singular_inverse_name).to be == 'writer' } + end + + context 'when initialized with singular_inverse_name: a Symbol' do + let(:singular_inverse_name) { :writer } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.singular_inverse_name).to be == 'writer' } + end + end + end + + describe '#singular?' do + include_examples 'should define predicate', :singular? + end + + describe '#with_inverse' do + it 'should define the method' do + expect(association).to respond_to(:with_inverse).with(1).argument + end + + context 'with a copy with assigned inverse' do + let(:new_inverse) { described_class.new(name: 'chapters') } + let(:copy) { subject.with_inverse(new_inverse) } + + it { expect(copy).to be_a described_class } + + it { expect(copy.inverse).to be == new_inverse } + + it { expect(subject.inverse).to be nil } + end + end + end + + deferred_examples 'should be a belongs to Association' do + include Cuprum::Collections::RSpec::Deferred::AssociationExamples + + include_deferred 'should be an Association' + + describe '#build_entities_query' do + let(:key) { subject.foreign_key_name } + let(:entities) { [] } + let(:options) { {} } + let(:query) do + association.build_entities_query(*entities, **options) + end + let(:evaluated) do + query.call(Spec::QueryBuilder.new) + end + + example_class 'Spec::Entity' do |klass| + klass.define_method(:initialize) do |**attributes| + attributes.each do |key, value| + instance_variable_set(:"@#{key}", value) + end + end + + klass.attr_reader :book_id + end + + example_class 'Spec::QueryBuilder' do |klass| + klass.define_method(:one_of) { |values| { 'one_of' => values } } + end + + describe 'with no entities' do + let(:entities) { [] } + + it { expect(query).to be_a Proc } + + it { expect(evaluated).to be == {} } + end + + describe 'with one nil entity' do + let(:entities) { [nil] } + + it { expect(evaluated).to be == {} } + end + + describe 'with one invalid entity' do + let(:entities) { [Object.new.freeze] } + let(:error_message) do + "undefined method :[] or :#{key} for #{entities.first.inspect}" + end + + it 'should raise an exception' do + expect { association.build_entities_query(*entities) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with one entity that responds to #[] and key: nil' do + let(:entities) { [{ key => nil }] } + + it { expect(evaluated).to be == {} } + + describe 'with allow_nil: true' do + let(:options) { super().merge(allow_nil: true) } + + it { expect(evaluated).to be == { 'id' => nil } } + end + end + + describe 'with one entity that responds to #[] and key: value' do + let(:entities) { [{ key => 0 }] } + + it { expect(evaluated).to be == { 'id' => 0 } } + end + + describe 'with one entity that responds to #id and key: nil' do + let(:entities) { [Spec::Entity.new(key => nil)] } + + it { expect(evaluated).to be == {} } + + describe 'with allow_nil: true' do + let(:options) { super().merge(allow_nil: true) } + + it { expect(evaluated).to be == { 'id' => nil } } + end + end + + describe 'with one entity that responds to #id and key: value' do + let(:entities) { [Spec::Entity.new(key => 0)] } + + it { expect(evaluated).to be == { 'id' => 0 } } + end + + describe 'with multiple entities' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => 2) + ] + end + let(:expected) do + { 'id' => { 'one_of' => [0, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + end + + describe 'with multiple entities including nil' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + nil, + Spec::Entity.new(key => 1), + nil, + Spec::Entity.new(key => 2) + ] + end + let(:expected) do + { 'id' => { 'one_of' => [0, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + end + + describe 'with multiple entities including nil ids' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + Spec::Entity.new(key => nil), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => nil), + Spec::Entity.new(key => 2) + ] + end + let(:expected) do + { 'id' => { 'one_of' => [0, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + + describe 'with allow_nil: true' do + let(:options) { super().merge(allow_nil: true) } + let(:expected) do + { 'id' => { 'one_of' => [0, nil, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + end + end + + describe 'with multiple entities including duplicate ids' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => 0), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => 2) + ] + end + let(:expected) do + { 'id' => { 'one_of' => [0, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + + describe 'with deduplicate: false' do + let(:options) { super().merge(deduplicate: false) } + let(:expected) do + { 'id' => { 'one_of' => [0, 1, 0, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + end + end + end + + describe '#build_keys_query' do + let(:keys) { [] } + let(:options) { {} } + let(:query) do + association.build_keys_query(*keys, **options) + end + let(:evaluated) do + query.call(Spec::QueryBuilder.new) + end + + example_class 'Spec::QueryBuilder' do |klass| + klass.define_method(:one_of) { |values| { 'one_of' => values } } + end + + describe 'with no keys' do + let(:keys) { [] } + + it { expect(query).to be_a Proc } + + it { expect(evaluated).to be == {} } + end + + describe 'with one nil key' do + let(:keys) { [nil] } + + it { expect(evaluated).to be == {} } + + describe 'with allow_nil: true' do + let(:options) { { allow_nil: true } } + + it { expect(evaluated).to be == { 'id' => nil } } + end + end + + describe 'with one non-nil key' do + let(:keys) { [0] } + + it { expect(evaluated).to be == { 'id' => 0 } } + end + + describe 'with many keys' do + let(:keys) { [0, 1, 2] } + let(:expected) { { 'id' => { 'one_of' => keys } } } + + it { expect(evaluated).to be == expected } + end + + describe 'with many keys including nil' do + let(:keys) { [0, nil, 2] } + let(:expected) { { 'id' => { 'one_of' => [0, 2] } } } + + it { expect(evaluated).to be == expected } + + describe 'with allow_nil: true' do + let(:options) { { allow_nil: true } } + let(:expected) do + { 'id' => { 'one_of' => [0, nil, 2] } } + end + + it { expect(evaluated).to be == expected } + end + end + + describe 'with many non-unique keys' do + let(:keys) { [0, 1, 2, 1, 2] } + let(:expected) { { 'id' => { 'one_of' => keys.uniq } } } + + it { expect(evaluated).to be == expected } + + describe 'with deduplicate: false' do + let(:options) { super().merge(deduplicate: false) } + let(:expected) do + { 'id' => { 'one_of' => [0, 1, 2, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + end + end + end + + describe '#foreign_key_name' do + let(:expected) { "#{tools.str.singularize(name)}_id" } + + def tools + SleepingKingStudios::Tools::Toolbelt.instance + end + + it { expect(subject.foreign_key_name).to be == expected } + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + + context 'when initialized with singular_name: value' do + let(:singular_name) { 'author' } + let(:constructor_options) do + super().merge(singular_name:) + end + + it { expect(subject.foreign_key_name).to be == 'author_id' } + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + end + end + + describe '#inverse_key_name' do + let(:expected) { "#{tools.str.singularize(name)}_id" } + + def tools + SleepingKingStudios::Tools::Toolbelt.instance + end + + it { expect(subject.inverse_key_name).to be == expected } + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.inverse_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.inverse_key_name).to be == 'writer_id' } + end + + context 'when initialized with singular_name: value' do + let(:singular_name) { 'author' } + let(:constructor_options) do + super().merge(singular_name:) + end + + it { expect(subject.inverse_key_name).to be == 'author_id' } + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.inverse_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.inverse_key_name).to be == 'writer_id' } + end + end + end + + describe '#map_entities_to_keys' do + let(:key) { subject.foreign_key_name } + let(:entities) { [] } + let(:options) { {} } + let(:keys) do + association.map_entities_to_keys(*entities, **options) + end + + example_class 'Spec::Entity' do |klass| + klass.define_method(:initialize) do |**attributes| + attributes.each do |key, value| + instance_variable_set(:"@#{key}", value) + end + end + + klass.attr_reader :book_id + end + + describe 'with no entities' do + let(:entities) { [] } + + it { expect(keys).to be == [] } + end + + describe 'with one nil entity' do + let(:entities) { [nil] } + + it { expect(keys).to be == [] } + end + + describe 'with one invalid entity' do + let(:entities) { [Object.new.freeze] } + let(:error_message) do + "undefined method :[] or :#{key} for #{entities.first.inspect}" + end + + it 'should raise an exception' do + expect { association.map_entities_to_keys(*entities) } + .to raise_error ArgumentError, error_message + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect do + association.map_entities_to_keys(*entities, strict: false) + end + .to raise_error ArgumentError, error_message + end + end + end + + describe 'with one Integer' do + let(:entities) { [0] } + let(:error_message) do + "undefined method :[] or :#{key} for #{entities.first.inspect}" + end + + it 'should raise an exception' do + expect { association.map_entities_to_keys(*entities) } + .to raise_error ArgumentError, error_message + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect( + association.map_entities_to_keys(*entities, strict: false) + ) + .to be == entities + end + end + + context 'when initialized with primary_key_type: String' do + let(:constructor_options) do + super().merge(primary_key_type: String) + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect do + association.map_entities_to_keys(*entities, strict: false) + end + .to raise_error ArgumentError, error_message + end + end + end + end + + describe 'with one String' do + let(:entities) { %w[0] } + let(:error_message) do + "undefined method :[] or :#{key} for #{entities.first.inspect}" + end + + it 'should raise an exception' do + expect { association.map_entities_to_keys(*entities) } + .to raise_error ArgumentError, error_message + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect do + association.map_entities_to_keys(*entities, strict: false) + end + .to raise_error ArgumentError, error_message + end + end + + context 'when initialized with primary_key_type: String' do + let(:constructor_options) do + super().merge(primary_key_type: String) + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect( + association.map_entities_to_keys(*entities, strict: false) + ) + .to be == entities + end + end + end + end + + describe 'with one entity that responds to #[] and key: nil' do + let(:entities) { [{ key => nil }] } + + it { expect(keys).to be == [] } + + describe 'with allow_nil: true' do + let(:options) { super().merge(allow_nil: true) } + + it { expect(keys).to be == [nil] } + end + end + + describe 'with one entity that responds to #[] and key: value' do + let(:entities) { [{ key => 0 }] } + + it { expect(keys).to be == [0] } + end + + describe 'with one entity that responds to #id and key: nil' do + let(:entities) { [Spec::Entity.new(key => nil)] } + + it { expect(keys).to be == [] } + + describe 'with allow_nil: true' do + let(:options) { super().merge(allow_nil: true) } + + it { expect(keys).to be == [nil] } + end + end + + describe 'with one entity that responds to #id and key: value' do + let(:entities) { [Spec::Entity.new(key => 0)] } + + it { expect(keys).to be == [0] } + end + + describe 'with multiple entities' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => 2) + ] + end + + it { expect(keys).to be == [0, 1, 2] } + end + + describe 'with multiple entities including nil' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + nil, + Spec::Entity.new(key => 1), + nil, + Spec::Entity.new(key => 2) + ] + end + + it { expect(keys).to be == [0, 1, 2] } + end + + describe 'with multiple entities including nil ids' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + Spec::Entity.new(key => nil), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => nil), + Spec::Entity.new(key => 2) + ] + end + + it { expect(keys).to be == [0, 1, 2] } + + describe 'with allow_nil: true' do + let(:options) { super().merge(allow_nil: true) } + + it { expect(keys).to be == [0, nil, 1, 2] } + end + end + + describe 'with multiple entities including duplicate ids' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => 0), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => 2) + ] + end + + it { expect(keys).to be == [0, 1, 2] } + + describe 'with deduplicate: false' do + let(:options) { super().merge(deduplicate: false) } + + it { expect(keys).to be == [0, 1, 0, 1, 2] } + end + end + + describe 'with multiple Integers' do + let(:entities) { [0, 1, 2] } + let(:error_message) do + "undefined method :[] or :#{key} for #{entities.first.inspect}" + end + + it 'should raise an exception' do + expect { association.map_entities_to_keys(*entities) } + .to raise_error ArgumentError, error_message + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect( + association.map_entities_to_keys(*entities, strict: false) + ) + .to be == entities + end + end + + context 'when initialized with primary_key_type: String' do + let(:constructor_options) do + super().merge(primary_key_type: String) + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect do + association.map_entities_to_keys(*entities, strict: false) + end + .to raise_error ArgumentError, error_message + end + end + end + end + + describe 'with multiple Strings' do + let(:entities) { %w[0 1 2] } + let(:error_message) do + "undefined method :[] or :#{key} for #{entities.first.inspect}" + end + + it 'should raise an exception' do + expect { association.map_entities_to_keys(*entities) } + .to raise_error ArgumentError, error_message + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect do + association.map_entities_to_keys(*entities, strict: false) + end + .to raise_error ArgumentError, error_message + end + end + + context 'when initialized with primary_key_type: String' do + let(:constructor_options) do + super().merge(primary_key_type: String) + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect( + association.map_entities_to_keys(*entities, strict: false) + ) + .to be == entities + end + end + end + end + end + + describe '#primary_key_query?' do + it { expect(subject.primary_key_query?).to be true } + end + + describe '#query_key_name' do + it { expect(subject.query_key_name).to be == 'id' } + + context 'when initialized with primary_key_name: a String' do + let(:primary_key_name) { 'uuid' } + let(:constructor_options) do + super().merge(primary_key_name:) + end + + it { expect(subject.query_key_name).to be == primary_key_name } + end + + context 'when initialized with primary_key_name: a Symbol' do + let(:primary_key_name) { :uuid } + let(:constructor_options) do + super().merge(primary_key_name:) + end + + it 'should set the primary key name' do + expect(subject.query_key_name).to be == primary_key_name.to_s + end + end + end + end + + deferred_examples 'should be a has Association' do + include Cuprum::Collections::RSpec::Deferred::AssociationExamples + + include_deferred 'should be an Association' + + describe '#build_entities_query' do + let(:key) { subject.primary_key_name } + let(:entities) { [] } + let(:options) { {} } + let(:query) do + association.build_entities_query(*entities, **options) + end + let(:evaluated) do + query.call(Spec::QueryBuilder.new) + end + + example_class 'Spec::Entity' do |klass| + klass.define_method(:initialize) do |**attributes| + attributes.each do |key, value| + instance_variable_set(:"@#{key}", value) + end + end + + klass.attr_reader :id + end + + example_class 'Spec::QueryBuilder' do |klass| + klass.define_method(:one_of) { |values| { 'one_of' => values } } + end + + context 'when the foreign key name is blank' do + let(:error_message) do + "foreign key name can't be blank" + end + + it 'should raise an exception' do + expect { association.build_entities_query(*entities) } + .to raise_error ArgumentError, error_message + end + end + + context 'when initialized with foreign_key_name: value' do + let(:foreign_key_name) { 'author_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + describe 'with no entities' do + let(:entities) { [] } + + it { expect(query).to be_a Proc } + + it { expect(evaluated).to be == {} } + end + + describe 'with one nil entity' do + let(:entities) { [nil] } + + it { expect(evaluated).to be == {} } + end + + describe 'with one invalid entity' do + let(:entities) { [Object.new.freeze] } + let(:error_message) do + "undefined method :[] or :#{key} for #{entities.first.inspect}" + end + + it 'should raise an exception' do + expect { association.build_entities_query(*entities) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with one entity that responds to #[] and key: nil' do + let(:entities) { [{ key => nil }] } + + it { expect(evaluated).to be == {} } + + describe 'with allow_nil: true' do + let(:options) { super().merge(allow_nil: true) } + + it { expect(evaluated).to be == { 'author_id' => nil } } + end + end + + describe 'with one entity that responds to #[] and key: value' do + let(:entities) { [{ key => 0 }] } + + it { expect(evaluated).to be == { 'author_id' => 0 } } + end + + describe 'with one entity that responds to #id and key: nil' do + let(:entities) { [Spec::Entity.new(key => nil)] } + + it { expect(evaluated).to be == {} } + + describe 'with allow_nil: true' do + let(:options) { super().merge(allow_nil: true) } + + it { expect(evaluated).to be == { 'author_id' => nil } } + end + end + + describe 'with one entity that responds to #id and key: value' do + let(:entities) { [Spec::Entity.new(key => 0)] } + + it { expect(evaluated).to be == { 'author_id' => 0 } } + end + + describe 'with multiple entities' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => 2) + ] + end + let(:expected) do + { 'author_id' => { 'one_of' => [0, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + end + + describe 'with multiple entities including nil' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + nil, + Spec::Entity.new(key => 1), + nil, + Spec::Entity.new(key => 2) + ] + end + let(:expected) do + { 'author_id' => { 'one_of' => [0, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + end + + describe 'with multiple entities including nil ids' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + Spec::Entity.new(key => nil), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => nil), + Spec::Entity.new(key => 2) + ] + end + let(:expected) do + { 'author_id' => { 'one_of' => [0, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + + describe 'with allow_nil: true' do + let(:options) { super().merge(allow_nil: true) } + let(:expected) do + { 'author_id' => { 'one_of' => [0, nil, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + end + end + + describe 'with multiple entities including duplicate ids' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => 0), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => 2) + ] + end + let(:expected) do + { 'author_id' => { 'one_of' => [0, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + + describe 'with deduplicate: false' do + let(:options) { super().merge(deduplicate: false) } + let(:expected) do + { 'author_id' => { 'one_of' => [0, 1, 0, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + end + end + end + end + + describe '#build_keys_query' do + let(:keys) { [] } + let(:options) { {} } + let(:query) do + association.build_keys_query(*keys, **options) + end + let(:evaluated) do + query.call(Spec::QueryBuilder.new) + end + + example_class 'Spec::QueryBuilder' do |klass| + klass.define_method(:one_of) { |values| { 'one_of' => values } } + end + + context 'when the foreign key name is blank' do + let(:error_message) do + "foreign key name can't be blank" + end + + it 'should raise an exception' do + expect { association.build_keys_query(*keys) } + .to raise_error ArgumentError, error_message + end + end + + context 'when initialized with foreign_key_name: value' do + let(:foreign_key_name) { 'author_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + describe 'with no keys' do + let(:keys) { [] } + + it { expect(query).to be_a Proc } + + it { expect(evaluated).to be == {} } + end + + describe 'with one nil key' do + let(:keys) { [nil] } + + it { expect(evaluated).to be == {} } + + describe 'with allow_nil: true' do + let(:options) { { allow_nil: true } } + + it { expect(evaluated).to be == { 'author_id' => nil } } + end + end + + describe 'with one non-nil key' do + let(:keys) { [0] } + + it { expect(evaluated).to be == { 'author_id' => 0 } } + end + + describe 'with many keys' do + let(:keys) { [0, 1, 2] } + let(:expected) { { 'author_id' => { 'one_of' => keys } } } + + it { expect(evaluated).to be == expected } + end + + describe 'with many keys including nil' do + let(:keys) { [0, nil, 2] } + let(:expected) { { 'author_id' => { 'one_of' => [0, 2] } } } + + it { expect(evaluated).to be == expected } + + describe 'with allow_nil: true' do + let(:options) { { allow_nil: true } } + let(:expected) do + { 'author_id' => { 'one_of' => [0, nil, 2] } } + end + + it { expect(evaluated).to be == expected } + end + end + + describe 'with many non-unique keys' do + let(:keys) { [0, 1, 2, 1, 2] } + let(:expected) { { 'author_id' => { 'one_of' => keys.uniq } } } + + it { expect(evaluated).to be == expected } + + describe 'with deduplicate: false' do + let(:options) { super().merge(deduplicate: false) } + let(:expected) do + { 'author_id' => { 'one_of' => [0, 1, 2, 1, 2] } } + end + + it { expect(evaluated).to be == expected } + end + end + end + end + + describe '#foreign_key_name' do + it { expect(subject.foreign_key_name).to be nil } + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + + context 'when initialized with inverse: value' do + let(:inverse) { described_class.new(name: 'authors') } + let(:constructor_options) do + super().merge(inverse:) + end + + it { expect(subject.foreign_key_name).to be == 'author_id' } + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + + context 'when initialized with inverse_name: value' do + let(:inverse_name) { 'writers' } + let(:constructor_options) do + super().merge(inverse_name:) + end + + it { expect(subject.foreign_key_name).to be == 'author_id' } + end + + context 'when initialized with singular_inverse_name: value' do + let(:singular_inverse_name) { 'writer' } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + end + + context 'when initialized with inverse_name: value' do + let(:inverse_name) { 'authors' } + let(:constructor_options) do + super().merge(inverse_name:) + end + + it { expect(subject.foreign_key_name).to be == 'author_id' } + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + end + + context 'when initialized with singular_inverse_name: value' do + let(:singular_inverse_name) { 'author' } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.foreign_key_name).to be == 'author_id' } + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + end + + context 'with a copy with assigned inverse' do + subject do + super().tap(&:foreign_key_name).with_inverse(new_inverse) + end + + let(:new_inverse) { described_class.new(name: 'chapters') } + + it { expect(subject.foreign_key_name).to be == 'chapter_id' } + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.foreign_key_name).to be == 'writer_id' } + end + + context 'when initialized with inverse: value' do + let(:inverse) { described_class.new(name: 'authors') } + let(:constructor_options) do + super().merge(inverse:) + end + + it { expect(subject.foreign_key_name).to be == 'chapter_id' } + end + + context 'when initialized with inverse_name: value' do + let(:inverse_name) { 'authors' } + let(:constructor_options) do + super().merge(inverse_name:) + end + + it { expect(subject.foreign_key_name).to be == 'chapter_id' } + end + + context 'when initialized with singular_inverse_name: value' do + let(:singular_inverse_name) { 'author' } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.foreign_key_name).to be == 'author_id' } + end + end + end + + describe '#inverse_key_name' do + it { expect(subject.inverse_key_name).to be == 'id' } + + context 'when initialized with primary_key_name: a String' do + let(:primary_key_name) { 'uuid' } + let(:constructor_options) do + super().merge(primary_key_name:) + end + + it { expect(subject.inverse_key_name).to be == primary_key_name } + end + + context 'when initialized with primary_key_name: a Symbol' do + let(:primary_key_name) { :uuid } + let(:constructor_options) do + super().merge(primary_key_name:) + end + + it 'should set the primary key name' do + expect(subject.inverse_key_name).to be == primary_key_name.to_s + end + end + end + + describe '#map_entities_to_keys' do + let(:key) { subject.primary_key_name } + let(:entities) { [] } + let(:options) { {} } + let(:keys) { subject.map_entities_to_keys(*entities, **options) } + + example_class 'Spec::Entity' do |klass| + klass.define_method(:initialize) do |**attributes| + attributes.each do |key, value| + instance_variable_set(:"@#{key}", value) + end + end + + klass.attr_reader :id + end + + describe 'with no entities' do + let(:entities) { [] } + + it { expect(keys).to be == [] } + end + + describe 'with one nil entity' do + let(:entities) { [nil] } + + it { expect(keys).to be == [] } + end + + describe 'with one invalid entity' do + let(:entities) { [Object.new.freeze] } + let(:error_message) do + "undefined method :[] or :#{key} for #{entities.first.inspect}" + end + + it 'should raise an exception' do + expect { association.map_entities_to_keys(*entities) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with one Integer' do + let(:entities) { [0] } + let(:error_message) do + "undefined method :[] or :#{key} for #{entities.first.inspect}" + end + + it 'should raise an exception' do + expect { association.map_entities_to_keys(*entities) } + .to raise_error ArgumentError, error_message + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect( + association.map_entities_to_keys(*entities, strict: false) + ) + .to be == entities + end + end + + context 'when initialized with primary_key_type: String' do + let(:constructor_options) do + super().merge(primary_key_type: String) + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect do + association.map_entities_to_keys(*entities, strict: false) + end + .to raise_error ArgumentError, error_message + end + end + end + end + + describe 'with one String' do + let(:entities) { %w[0] } + let(:error_message) do + "undefined method :[] or :#{key} for #{entities.first.inspect}" + end + + it 'should raise an exception' do + expect { association.map_entities_to_keys(*entities) } + .to raise_error ArgumentError, error_message + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect do + association.map_entities_to_keys(*entities, strict: false) + end + .to raise_error ArgumentError, error_message + end + end + + context 'when initialized with primary_key_type: String' do + let(:constructor_options) do + super().merge(primary_key_type: String) + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect( + association.map_entities_to_keys(*entities, strict: false) + ) + .to be == entities + end + end + end + end + + describe 'with one entity that responds to #[] and key: nil' do + let(:entities) { [{ key => nil }] } + + it { expect(keys).to be == [] } + + describe 'with allow_nil: true' do + let(:options) { super().merge(allow_nil: true) } + + it { expect(keys).to be == [nil] } + end + end + + describe 'with one entity that responds to #[] and key: value' do + let(:entities) { [{ key => 0 }] } + + it { expect(keys).to be == [0] } + end + + describe 'with one entity that responds to #id and key: nil' do + let(:entities) { [Spec::Entity.new(key => nil)] } + + it { expect(keys).to be == [] } + + describe 'with allow_nil: true' do + let(:options) { super().merge(allow_nil: true) } + + it { expect(keys).to be == [nil] } + end + end + + describe 'with one entity that responds to #id and key: value' do + let(:entities) { [Spec::Entity.new(key => 0)] } + + it { expect(keys).to be == [0] } + end + + describe 'with multiple entities' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => 2) + ] + end + + it { expect(keys).to be == [0, 1, 2] } + end + + describe 'with multiple entities including nil' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + nil, + Spec::Entity.new(key => 1), + nil, + Spec::Entity.new(key => 2) + ] + end + + it { expect(keys).to be == [0, 1, 2] } + end + + describe 'with multiple entities including nil ids' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + Spec::Entity.new(key => nil), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => nil), + Spec::Entity.new(key => 2) + ] + end + + it { expect(keys).to be == [0, 1, 2] } + + describe 'with allow_nil: true' do + let(:options) { super().merge(allow_nil: true) } + + it { expect(keys).to be == [0, nil, 1, 2] } + end + end + + describe 'with multiple entities including duplicate ids' do + let(:entities) do + [ + Spec::Entity.new(key => 0), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => 0), + Spec::Entity.new(key => 1), + Spec::Entity.new(key => 2) + ] + end + + it { expect(keys).to be == [0, 1, 2] } + + describe 'with deduplicate: false' do + let(:options) { super().merge(deduplicate: false) } + + it { expect(keys).to be == [0, 1, 0, 1, 2] } + end + end + + describe 'with multiple Integers' do + let(:entities) { [0, 1, 2] } + let(:error_message) do + "undefined method :[] or :#{key} for #{entities.first.inspect}" + end + + it 'should raise an exception' do + expect { association.map_entities_to_keys(*entities) } + .to raise_error ArgumentError, error_message + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect( + association.map_entities_to_keys(*entities, strict: false) + ) + .to be == entities + end + end + + context 'when initialized with primary_key_type: String' do + let(:constructor_options) do + super().merge(primary_key_type: String) + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect do + association.map_entities_to_keys(*entities, strict: false) + end + .to raise_error ArgumentError, error_message + end + end + end + end + + describe 'with multiple Strings' do + let(:entities) { %w[0 1 2] } + let(:error_message) do + "undefined method :[] or :#{key} for #{entities.first.inspect}" + end + + it 'should raise an exception' do + expect { association.map_entities_to_keys(*entities) } + .to raise_error ArgumentError, error_message + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect do + association.map_entities_to_keys(*entities, strict: false) + end + .to raise_error ArgumentError, error_message + end + end + + context 'when initialized with primary_key_type: String' do + let(:constructor_options) do + super().merge(primary_key_type: String) + end + + describe 'with strict: false' do + it 'should raise an exception' do + expect( + association.map_entities_to_keys(*entities, strict: false) + ) + .to be == entities + end + end + end + end + end + + describe '#primary_key_query?' do + it { expect(subject.primary_key_query?).to be false } + end + + describe '#query_key_name' do + context 'when the foreign key name is blank' do + let(:error_message) do + "foreign key name can't be blank" + end + + it 'should raise an exception' do + expect { association.query_key_name } + .to raise_error ArgumentError, error_message + end + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.query_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.query_key_name).to be == 'writer_id' } + end + + context 'when initialized with inverse: value' do + let(:inverse) { described_class.new(name: 'authors') } + let(:constructor_options) do + super().merge(inverse:) + end + + it { expect(subject.query_key_name).to be == 'author_id' } + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.query_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.query_key_name).to be == 'writer_id' } + end + + context 'when initialized with inverse_name: value' do + let(:inverse_name) { 'writers' } + let(:constructor_options) do + super().merge(inverse_name:) + end + + it { expect(subject.query_key_name).to be == 'author_id' } + end + + context 'when initialized with singular_inverse_name: value' do + let(:singular_inverse_name) { 'writer' } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.query_key_name).to be == 'writer_id' } + end + end + + context 'when initialized with inverse_name: value' do + let(:inverse_name) { 'authors' } + let(:constructor_options) do + super().merge(inverse_name:) + end + + it { expect(subject.query_key_name).to be == 'author_id' } + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.query_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.query_key_name).to be == 'writer_id' } + end + end + + context 'when initialized with singular_inverse_name: value' do + let(:singular_inverse_name) { 'author' } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.query_key_name).to be == 'author_id' } + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.query_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.query_key_name).to be == 'writer_id' } + end + end + + context 'with a copy with assigned inverse' do + subject do + super().tap(&:foreign_key_name).with_inverse(new_inverse) + end + + let(:new_inverse) { described_class.new(name: 'chapters') } + + it { expect(subject.query_key_name).to be == 'chapter_id' } + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { 'writer_id' } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.query_key_name).to be == 'writer_id' } + end + + context 'when initialized with foreign_key_name: a String' do + let(:foreign_key_name) { :writer_id } + let(:constructor_options) do + super().merge(foreign_key_name:) + end + + it { expect(subject.query_key_name).to be == 'writer_id' } + end + + context 'when initialized with inverse: value' do + let(:inverse) { described_class.new(name: 'authors') } + let(:constructor_options) do + super().merge(inverse:) + end + + it { expect(subject.query_key_name).to be == 'chapter_id' } + end + + context 'when initialized with inverse_name: value' do + let(:inverse_name) { 'authors' } + let(:constructor_options) do + super().merge(inverse_name:) + end + + it { expect(subject.query_key_name).to be == 'chapter_id' } + end + + context 'when initialized with singular_inverse_name: value' do + let(:singular_inverse_name) { 'author' } + let(:constructor_options) do + super().merge(singular_inverse_name:) + end + + it { expect(subject.query_key_name).to be == 'author_id' } + end + end + end + end + end +end diff --git a/lib/cuprum/collections/rspec/deferred/collection_examples.rb b/lib/cuprum/collections/rspec/deferred/collection_examples.rb new file mode 100644 index 0000000..b93ab24 --- /dev/null +++ b/lib/cuprum/collections/rspec/deferred/collection_examples.rb @@ -0,0 +1,372 @@ +# frozen_string_literal: true + +require 'rspec/sleeping_king_studios/deferred' + +require 'cuprum/collections/rspec/deferred' +require 'cuprum/collections/rspec/deferred/relation_examples' + +module Cuprum::Collections::RSpec::Deferred + # Deferred examples for testing collections. + module CollectionExamples + include RSpec::SleepingKingStudios::Deferred::Provider + include Cuprum::Collections::RSpec::Deferred::RelationExamples + + deferred_context 'when initialized with a scope' do + let(:initial_scope) do + Cuprum::Collections::Scope.new({ 'ok' => true }) + end + let(:constructor_options) do + super().merge(scope: initial_scope) + end + end + + deferred_examples 'should be a Collection' do |**options| + include Cuprum::Collections::RSpec::Deferred::RelationExamples + + deferred_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) + + describe "##{command_name}" do + let(:constructor_options) { defined?(super()) ? super() : {} } + let(:command) { build_command(collection) } + let(:command_class) do + ( + command_class_name || + "#{options[:commands_namespace]}::#{class_name}" + ) + .then { |str| Object.const_get(str) } + end + + define_method(:build_command) do |collection| + collection.send(command_name, **constructor_options) + 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 command_class } + + it { expect(command.collection).to be subject } + end + end + + include_deferred 'should be a Relation', + constructor: false, + default_entity_class: options[:default_entity_class] + + include_deferred 'should define Relation primary key' + + include_deferred 'should define the command', :assign_one + + include_deferred 'should define the command', :build_one + + include_deferred 'should define the command', :destroy_one + + include_deferred 'should define the command', :find_many + + include_deferred 'should define the command', :find_matching + + include_deferred 'should define the command', :find_one + + include_deferred 'should define the command', :insert_one + + include_deferred 'should define the command', :update_one + + include_deferred 'should define the command', :validate_one + + describe '#==' do + let(:other_options) { { 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:, + 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) { { 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 + { + 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 " \ + 'collection 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.limit).to be nil } + + it { expect(query.offset).to be nil } + + it { expect(query.order).to be == default_order } + + it { expect(query.scope).to be == subject.scope } + + wrap_deferred 'when initialized with a scope' do + it { expect(query.scope).to be == subject.scope } + end + end + end + + describe '#scope' do + let(:expected) do + Cuprum::Collections::Scopes::AllScope.new + end + + include_examples 'should define reader', :scope, -> { be == expected } + + wrap_deferred 'when initialized with a scope' do + it { expect(subject.scope).to be == initial_scope } + end + end + + describe '#with_scope' do + let(:other_scope) do + Cuprum::Collections::Scope.new({ 'secret' => '12345' }) + end + let(:copy) { subject.with_scope(other_scope) } + + it { expect(subject).to respond_to(:with_scope).with(1).argument } + + it { expect(copy).to be_a described_class } + + it { expect(copy).not_to be subject } + + it { expect(copy.scope).to be == other_scope } + + wrap_deferred 'when initialized with a scope' do + let(:expected) do + initial_scope.and(other_scope) + end + + it { expect(copy.scope).to be == expected } + end + end + end + end +end diff --git a/lib/cuprum/collections/rspec/deferred/relation_examples.rb b/lib/cuprum/collections/rspec/deferred/relation_examples.rb new file mode 100644 index 0000000..201be4a --- /dev/null +++ b/lib/cuprum/collections/rspec/deferred/relation_examples.rb @@ -0,0 +1,1213 @@ +# frozen_string_literal: true + +require 'rspec/sleeping_king_studios/deferred' + +require 'cuprum/collections/rspec/deferred' + +module Cuprum::Collections::RSpec::Deferred + # Deferred examples for testing relations. + module RelationExamples + include RSpec::SleepingKingStudios::Deferred::Provider + + deferred_examples 'should be a Relation' do |**options| + if options.fetch(:constructor, true) + describe '.new' do + let(:expected_keywords) do + keywords = %i[ + entity_class + name + plural_name + qualified_name + singular_name + ] + + keywords += %i[plural singular] if options[:cardinality] + + keywords + options.fetch(:expected_keywords, []) + end + + def call_method(**parameters) + described_class.new(**parameters) + end + + it 'should define the constructor' do + expect(described_class) + .to be_constructible + .with(0).arguments + .and_keywords(*expected_keywords) + .and_any_keywords + end + + include_deferred 'should validate the Relation parameters' + end + end + + describe '#entity_class' do + include_examples 'should define reader', :entity_class + + context 'when initialized with entity_class: a Class' do + let(:entity_class) { Grimoire } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.entity_class).to be Grimoire } + end + + context 'when initialized with entity_class: a scoped Class' do + let(:entity_class) { Spec::ScopedBook } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.entity_class).to be Spec::ScopedBook } + end + + context 'when initialized with entity_class: a String' do + let(:entity_class) { 'Grimoire' } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.entity_class).to be Grimoire } + end + + context 'when initialized with entity_class: a scoped String' do + let(:entity_class) { 'Spec::ScopedBook' } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.entity_class).to be Spec::ScopedBook } + end + + context 'when initialized with name: a String' do + let(:name) { 'books' } + let(:constructor_options) do + super().merge(name:) + end + let(:expected) { options[:default_entity_class] || Book } + + it { expect(subject.entity_class).to be expected } + + context 'when initialized with entity_class: value' do + let(:entity_class) { Grimoire } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.entity_class).to be Grimoire } + end + + context 'when initialized with qualified_name: value' do + let(:qualified_name) { 'spec/scoped_books' } + let(:constructor_options) do + super().merge(qualified_name:) + end + let(:expected) do + options[:default_entity_class] || Spec::ScopedBook + end + + it { expect(subject.entity_class).to be expected } + + context 'when initialized with entity_class: value' do + let(:entity_class) { Grimoire } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.entity_class).to be Grimoire } + end + end + end + + context 'when initialized with name: a Symbol' do + let(:name) { 'books' } + let(:constructor_options) do + super().merge(name:) + end + let(:expected) { options[:default_entity_class] || Book } + + it { expect(subject.entity_class).to be expected } + + context 'when initialized with entity_class: value' do + let(:entity_class) { Grimoire } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.entity_class).to be Grimoire } + end + + context 'when initialized with qualified_name: value' do + let(:qualified_name) { 'spec/scoped_books' } + let(:constructor_options) do + super().merge(qualified_name:) + end + let(:expected) do + options[:default_entity_class] || Spec::ScopedBook + end + + it { expect(subject.entity_class).to be expected } + + context 'when initialized with entity_class: value' do + let(:entity_class) { Grimoire } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.entity_class).to be Grimoire } + end + end + end + + context 'when initialized with qualified_name: a String' do + let(:qualified_name) { 'spec/scoped_books' } + let(:constructor_options) do + super().merge(qualified_name:) + end + let(:expected) do + options[:default_entity_class] || Spec::ScopedBook + end + + it { expect(subject.entity_class).to be expected } + + context 'when initialized with entity_class: value' do + let(:entity_class) { Grimoire } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.entity_class).to be Grimoire } + end + end + + context 'when initialized with qualified_name: a Symbol' do + let(:qualified_name) { :'spec/scoped_books' } + let(:constructor_options) do + super().merge(qualified_name:) + end + let(:expected) do + options[:default_entity_class] || Spec::ScopedBook + end + + it { expect(subject.entity_class).to be expected } + + context 'when initialized with entity_class: value' do + let(:entity_class) { Grimoire } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.entity_class).to be Grimoire } + end + end + end + + describe '#name' do + include_examples 'should define reader', :name + + context 'when initialized with entity_class: a Class' do + let(:entity_class) { Grimoire } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.name).to be == 'grimoires' } + + context 'when initialized with name: value' do + let(:name) { 'books' } + let(:constructor_options) do + super().merge(name:) + end + + it { expect(subject.name).to be == name } + end + end + + context 'when initialized with entity_class: a scoped Class' do + let(:entity_class) { Spec::ScopedBook } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.name).to be == 'scoped_books' } + + context 'when initialized with name: value' do + let(:name) { 'books' } + let(:constructor_options) do + super().merge(name:) + end + + it { expect(subject.name).to be == name } + end + end + + context 'when initialized with entity_class: a String' do + let(:entity_class) { 'Grimoire' } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.name).to be == 'grimoires' } + + context 'when initialized with name: value' do + let(:name) { 'books' } + let(:constructor_options) do + super().merge(name:) + end + + it { expect(subject.name).to be == name } + end + end + + context 'when initialized with entity_class: a scoped String' do + let(:entity_class) { 'Spec::ScopedBook' } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.name).to be == 'scoped_books' } + + context 'when initialized with name: value' do + let(:name) { 'books' } + let(:constructor_options) do + super().merge(name:) + end + + it { expect(subject.name).to be == name } + end + end + + context 'when initialized with name: a String' do + let(:name) { 'books' } + let(:constructor_options) do + super().merge(name:) + end + + it { expect(subject.name).to be == name } + end + + context 'when initialized with name: a Symbol' do + let(:name) { :books } + let(:constructor_options) do + super().merge(name:) + end + + it { expect(subject.name).to be == name.to_s } + end + end + + describe '#options' do + include_examples 'should define reader', :options, -> { {} } + + context 'when initialized with entity_class: value' do + let(:entity_class) { Grimoire } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.options).to be == {} } + end + + context 'when initialized with singular_name: value' do + let(:singular_name) { 'book' } + let(:constructor_options) do + super().merge(singular_name:) + end + + it { expect(subject.options).to be == {} } + end + + context 'when initialized with name: value' do + let(:name) { 'books' } + let(:constructor_options) do + super().merge(name:) + end + + it { expect(subject.options).to be == {} } + end + + context 'when initialized with qualified_name: value' do + let(:qualified_name) { 'spec/scoped_books' } + let(:constructor_options) do + super().merge(qualified_name:) + end + + it { expect(subject.options).to be == {} } + end + + context 'when initialized with options' do + let(:options) { { custom_option: 'custom value' } } + let(:constructor_options) do + super().merge(options) + end + + it { expect(subject.options).to be == options } + end + end + + describe '#plural_name' do + include_examples 'should define reader', :plural_name + + context 'when initialized with entity_class: a Class' do + let(:entity_class) { Grimoire } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.plural_name).to be == 'grimoires' } + + context 'when initialized with plural_name: value' do + let(:plural_name) { 'books' } + let(:constructor_options) do + super().merge(plural_name:) + end + + it { expect(subject.plural_name).to be == plural_name } + end + end + + context 'when initialized with entity_class: a scoped Class' do + let(:entity_class) { Spec::ScopedBook } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.plural_name).to be == 'scoped_books' } + + context 'when initialized with plural_name: value' do + let(:plural_name) { 'books' } + let(:constructor_options) do + super().merge(plural_name:) + end + + it { expect(subject.plural_name).to be == plural_name } + end + end + + context 'when initialized with entity_class: a String' do + let(:entity_class) { 'Grimoire' } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.plural_name).to be == 'grimoires' } + + context 'when initialized with plural_name: value' do + let(:plural_name) { 'books' } + let(:constructor_options) do + super().merge(plural_name:) + end + + it { expect(subject.plural_name).to be == plural_name } + end + end + + context 'when initialized with entity_class: a scoped String' do + let(:entity_class) { 'Spec::ScopedBook' } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.plural_name).to be == 'scoped_books' } + + context 'when initialized with plural_name: value' do + let(:plural_name) { 'books' } + let(:constructor_options) do + super().merge(plural_name:) + end + + it { expect(subject.plural_name).to be == plural_name } + end + end + + context 'when initialized with name: a String' do + let(:name) { 'grimoires' } + let(:constructor_options) do + super().merge(name:) + end + + it { expect(subject.plural_name).to be == 'grimoires' } + + context 'when initialized with plural_name: value' do + let(:plural_name) { 'books' } + let(:constructor_options) do + super().merge(plural_name:) + end + + it { expect(subject.plural_name).to be == plural_name } + end + end + + context 'when initialized with name: a Symbol' do + let(:name) { :grimoires } + let(:constructor_options) do + super().merge(name:) + end + + it { expect(subject.plural_name).to be == 'grimoires' } + + context 'when initialized with plural_name: value' do + let(:plural_name) { 'books' } + let(:constructor_options) do + super().merge(plural_name:) + end + + it { expect(subject.plural_name).to be == plural_name } + end + end + end + + describe '#qualified_name' do + include_examples 'should define reader', :qualified_name + + context 'when initialized with entity_class: a Class' do + let(:entity_class) { Grimoire } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.qualified_name).to be == 'grimoires' } + + context 'when initialized with qualified_name: value' do + let(:qualified_name) { 'path/to/books' } + let(:constructor_options) do + super().merge(qualified_name:) + end + + it { expect(subject.qualified_name).to be == qualified_name } + end + end + + context 'when initialized with entity_class: a scoped Class' do + let(:entity_class) { Spec::ScopedBook } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.qualified_name).to be == 'spec/scoped_books' } + + context 'when initialized with qualified_name: value' do + let(:qualified_name) { 'path/to/books' } + let(:constructor_options) do + super().merge(qualified_name:) + end + + it { expect(subject.qualified_name).to be == qualified_name } + end + end + + context 'when initialized with entity_class: a String' do + let(:entity_class) { 'Grimoire' } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.qualified_name).to be == 'grimoires' } + + context 'when initialized with qualified_name: value' do + let(:qualified_name) { 'path/to/books' } + let(:constructor_options) do + super().merge(qualified_name:) + end + + it { expect(subject.qualified_name).to be == qualified_name } + end + end + + context 'when initialized with entity_class: a scoped String' do + let(:entity_class) { 'Spec::ScopedBook' } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.qualified_name).to be == 'spec/scoped_books' } + + context 'when initialized with qualified_name: value' do + let(:qualified_name) { 'path/to/books' } + let(:constructor_options) do + super().merge(qualified_name:) + end + + it { expect(subject.qualified_name).to be == qualified_name } + end + end + + context 'when initialized with name: a String' do + let(:name) { 'books' } + let(:constructor_options) do + super().merge(name:) + end + + it { expect(subject.qualified_name).to be == name } + + context 'when initialized with qualified_name: value' do + let(:qualified_name) { 'path/to/books' } + let(:constructor_options) do + super().merge(qualified_name:) + end + + it { expect(subject.qualified_name).to be == qualified_name } + end + end + + context 'when initialized with name: a Symbol' do + let(:name) { :books } + let(:constructor_options) do + super().merge(name:) + end + + it { expect(subject.qualified_name).to be == name.to_s } + + context 'when initialized with qualified_name: value' do + let(:qualified_name) { 'path/to/books' } + let(:constructor_options) do + super().merge(qualified_name:) + end + + it { expect(subject.qualified_name).to be == qualified_name } + end + end + + context 'when initialized with qualified_name: a String' do + let(:qualified_name) { 'path/to/books' } + let(:constructor_options) do + super().merge(qualified_name:) + end + + it { expect(subject.qualified_name).to be == qualified_name } + end + + context 'when initialized with qualified_name: a Symbol' do + let(:qualified_name) { :'path/to/books' } + let(:constructor_options) do + super().merge(qualified_name:) + end + + it { expect(subject.qualified_name).to be == qualified_name.to_s } + end + end + + describe '#singular_name' do + include_examples 'should define reader', :singular_name + + context 'when initialized with entity_class: a Class' do + let(:entity_class) { Grimoire } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.singular_name).to be == 'grimoire' } + + context 'when initialized with singular_name: value' do + let(:singular_name) { 'book' } + let(:constructor_options) do + super().merge(singular_name:) + end + + it { expect(subject.singular_name).to be == singular_name } + end + end + + context 'when initialized with entity_class: a scoped Class' do + let(:entity_class) { Spec::ScopedBook } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.singular_name).to be == 'scoped_book' } + + context 'when initialized with singular_name: value' do + let(:singular_name) { 'book' } + let(:constructor_options) do + super().merge(singular_name:) + end + + it { expect(subject.singular_name).to be == singular_name } + end + end + + context 'when initialized with entity_class: a String' do + let(:entity_class) { 'Grimoire' } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.singular_name).to be == 'grimoire' } + + context 'when initialized with singular_name: value' do + let(:singular_name) { 'book' } + let(:constructor_options) do + super().merge(singular_name:) + end + + it { expect(subject.singular_name).to be == singular_name } + end + end + + context 'when initialized with entity_class: a scoped String' do + let(:entity_class) { 'Spec::ScopedBook' } + let(:constructor_options) do + super() + .tap { |hsh| hsh.delete(:name) } + .merge(entity_class:) + end + + it { expect(subject.singular_name).to be == 'scoped_book' } + + context 'when initialized with singular_name: value' do + let(:singular_name) { 'book' } + let(:constructor_options) do + super().merge(singular_name:) + end + + it { expect(subject.singular_name).to be == singular_name } + end + end + + context 'when initialized with name: a String' do + let(:name) { 'grimoires' } + let(:constructor_options) do + super().merge(name:) + end + + it { expect(subject.singular_name).to be == 'grimoire' } + + context 'when initialized with singular_name: value' do + let(:singular_name) { 'book' } + let(:constructor_options) do + super().merge(singular_name:) + end + + it { expect(subject.singular_name).to be == singular_name } + end + end + + context 'when initialized with name: a Symbol' do + let(:name) { :grimoires } + let(:constructor_options) do + super().merge(name:) + end + + it { expect(subject.singular_name).to be == 'grimoire' } + + context 'when initialized with singular_name: value' do + let(:singular_name) { 'book' } + let(:constructor_options) do + super().merge(singular_name:) + end + + it { expect(subject.singular_name).to be == singular_name } + end + end + end + end + + deferred_examples 'should define Relation cardinality' do + describe '.new' do + describe 'with plural: an Object' do + let(:constructor_options) do + super().merge(plural: Object.new.freeze) + end + let(:error_message) do + 'plural must be true or false' + end + + it 'should raise an exception' do + expect { described_class.new(**constructor_options) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with singular: an Object' do + let(:constructor_options) do + super().merge(singular: Object.new.freeze) + end + let(:error_message) do + 'singular must be true or false' + end + + it 'should raise an exception' do + expect { described_class.new(**constructor_options) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with singular: nil and plural: value' do + let(:constructor_options) do + super().merge(plural: true, singular: nil) + end + + it 'should not raise an exception' do + expect { described_class.new(**constructor_options) } + .not_to raise_error + end + end + + describe 'with singular: value and plural: nil' do + let(:constructor_options) do + super().merge(plural: nil, singular: true) + end + + it 'should not raise an exception' do + expect { described_class.new(**constructor_options) } + .not_to raise_error + end + end + + describe 'with singular: value and plural: value' do + let(:constructor_options) do + super().merge(singular: true, plural: false) + end + let(:error_message) do + 'ambiguous cardinality: initialized with parameters ' \ + 'plural: false and singular: true' + end + + it 'should raise an exception' do + expect { described_class.new(**constructor_options) } + .to raise_error ArgumentError, error_message + end + end + end + + describe '#plural?' do + include_examples 'should define predicate', :plural?, true + + context 'when initialized with plural: false' do + let(:constructor_options) { super().merge(plural: false) } + + it { expect(subject.plural?).to be false } + end + + context 'when initialized with plural: true' do + let(:constructor_options) { super().merge(plural: true) } + + it { expect(subject.plural?).to be true } + end + + context 'when initialized with singular: false' do + let(:constructor_options) { super().merge(singular: false) } + + it { expect(subject.plural?).to be true } + end + + context 'when initialized with singular: true' do + let(:constructor_options) { super().merge(singular: true) } + + it { expect(subject.plural?).to be false } + end + end + + describe '#singular?' do + include_examples 'should define predicate', :singular?, false + + context 'when initialized with plural: false' do + let(:constructor_options) { super().merge(plural: false) } + + it { expect(subject.singular?).to be true } + end + + context 'when initialized with plural: true' do + let(:constructor_options) { super().merge(plural: true) } + + it { expect(subject.singular?).to be false } + end + + context 'when initialized with singular: false' do + let(:constructor_options) { super().merge(singular: false) } + + it { expect(subject.singular?).to be false } + end + + context 'when initialized with singular: true' do + let(:constructor_options) { super().merge(singular: true) } + + it { expect(subject.singular?).to be true } + end + end + end + + deferred_examples 'should define Relation primary key' do + describe '#primary_key_name' do + let(:expected_primary_key_name) do + return super() if defined?(super()) + + constructor_options.fetch(:primary_key_name, 'id') + end + + include_examples 'should define reader', + :primary_key_name, + -> { expected_primary_key_name } + + context 'when initialized with primary_key_name: a String' do + let(:primary_key_name) { 'uuid' } + let(:constructor_options) do + super().merge(primary_key_name:) + end + + it { expect(subject.primary_key_name).to be == primary_key_name } + end + + context 'when initialized with primary_key_name: a Symbol' do + let(:primary_key_name) { :uuid } + let(:constructor_options) do + super().merge(primary_key_name:) + end + + it 'should set the primary key name' do + expect(subject.primary_key_name).to be == primary_key_name.to_s + end + end + end + + describe '#primary_key_type' do + let(:expected_primary_key_type) do + return super() if defined?(super()) + + constructor_options.fetch(:primary_key_type, Integer) + end + + include_examples 'should define reader', + :primary_key_type, + -> { expected_primary_key_type } + + context 'when initialized with primary_key_type: value' do + let(:primary_key_type) { String } + let(:constructor_options) do + super().merge(primary_key_type:) + end + + it { expect(subject.primary_key_type).to be == primary_key_type } + end + end + end + + deferred_examples 'should validate the Relation parameters' do + describe 'with no parameters' do + let(:error_message) { "name or entity class can't be blank" } + + it 'should raise an exception' do + expect { call_method } + .to raise_error ArgumentError, error_message + end + end + + describe 'with entity_class: nil' do + let(:error_message) { "name or entity class can't be blank" } + + it 'should raise an exception' do + expect { call_method(entity_class: nil) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with entity_class: an Object' do + let(:error_message) do + 'entity class is not a Class, a String or a Symbol' + end + + it 'should raise an exception' do + expect do + call_method(entity_class: Object.new.freeze) + end + .to raise_error ArgumentError, error_message + end + end + + describe 'with entity_class: an empty String' do + let(:error_message) { "entity class can't be blank" } + + it 'should raise an exception' do + expect { call_method(entity_class: '') } + .to raise_error ArgumentError, error_message + end + end + + describe 'with entity_class: an empty Symbol' do + let(:error_message) { "entity class can't be blank" } + + it 'should raise an exception' do + expect { call_method(entity_class: :'') } + .to raise_error ArgumentError, error_message + end + end + + describe 'with name: nil' do + let(:error_message) { "name or entity class can't be blank" } + + it 'should raise an exception' do + expect { call_method(name: nil) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with name: nil and entity_class: value' do + it 'should not raise an exception' do + expect do + call_method(entity_class: 'Book', name: nil) + end + .not_to raise_error + end + end + + describe 'with name: an Object' do + let(:error_message) { 'name is not a String or a Symbol' } + + it 'should raise an exception' do + expect { call_method(name: Object.new.freeze) } + .to raise_error ArgumentError, error_message + end + end + + describe 'with name: an empty String' do + let(:error_message) { "name can't be blank" } + + it 'should raise an exception' do + expect { call_method(name: '') } + .to raise_error ArgumentError, error_message + end + end + + describe 'with name: an empty Symbol' do + let(:error_message) { "name can't be blank" } + + it 'should raise an exception' do + expect { call_method(name: '') } + .to raise_error ArgumentError, error_message + end + end + + describe 'with name: value and entity_class: nil' do + it 'should not raise an exception' do + expect do + call_method(entity_class: 'Book', name: nil) + end + .not_to raise_error + end + end + + describe 'with name: value and entity_class: an Object' do + let(:error_message) do + 'entity class is not a Class, a String or a Symbol' + end + + it 'should raise an exception' do + expect do + call_method( + entity_class: Object.new.freeze, + name: 'books' + ) + end + .to raise_error ArgumentError, error_message + end + end + + describe 'with name: value and entity_class: an empty String' do + let(:error_message) { "entity class can't be blank" } + + it 'should raise an exception' do + expect { call_method(entity_class: '', name: 'books') } + .to raise_error ArgumentError, error_message + end + end + + describe 'with name: value and entity_class: an empty Symbol' do + let(:error_message) { "entity class can't be blank" } + + it 'should raise an exception' do + expect { call_method(entity_class: :'', name: 'books') } + .to raise_error ArgumentError, error_message + end + end + + describe 'with plural_name: an Object' do + let(:error_message) { 'plural name is not a String or a Symbol' } + + it 'should raise an exception' do + expect do + call_method( + name: 'books', + plural_name: Object.new.freeze + ) + end + .to raise_error ArgumentError, error_message + end + end + + describe 'with plural_name: an empty String' do + let(:error_message) { "plural name can't be blank" } + + it 'should raise an exception' do + expect do + call_method( + name: 'books', + plural_name: '' + ) + end + .to raise_error ArgumentError, error_message + end + end + + describe 'with plural_name: an empty Symbol' do + let(:error_message) { "plural name can't be blank" } + + it 'should raise an exception' do + expect do + call_method( + name: 'books', + plural_name: :'' + ) + end + .to raise_error ArgumentError, error_message + end + end + + describe 'with qualified_name: an Object' do + let(:error_message) { 'qualified name is not a String or a Symbol' } + + it 'should raise an exception' do + expect do + call_method( + name: 'books', + qualified_name: Object.new.freeze + ) + end + .to raise_error ArgumentError, error_message + end + end + + describe 'with qualified_name: an empty String' do + let(:error_message) { "qualified name can't be blank" } + + it 'should raise an exception' do + expect do + call_method( + name: 'books', + qualified_name: '' + ) + end + .to raise_error ArgumentError, error_message + end + end + + describe 'with qualified_name: an empty Symbol' do + let(:error_message) { "qualified name can't be blank" } + + it 'should raise an exception' do + expect do + call_method( + name: 'books', + qualified_name: :'' + ) + end + .to raise_error ArgumentError, error_message + end + end + + describe 'with qualified_name: value and name, entity_class: nil' do + it 'should not raise an exception' do + expect do + call_method(entity_class: nil, name: nil, qualified_name: 'books') + end + .not_to raise_error + end + end + + describe 'with singular_name: an Object' do + let(:error_message) { 'singular name is not a String or a Symbol' } + + it 'should raise an exception' do + expect do + call_method( + name: 'books', + singular_name: Object.new.freeze + ) + end + .to raise_error ArgumentError, error_message + end + end + + describe 'with singular_name: an empty String' do + let(:error_message) { "singular name can't be blank" } + + it 'should raise an exception' do + expect do + call_method( + name: 'books', + singular_name: '' + ) + end + .to raise_error ArgumentError, error_message + end + end + + describe 'with singular_name: an empty Symbol' do + let(:error_message) { "singular name can't be blank" } + + it 'should raise an exception' do + expect do + call_method( + name: 'books', + singular_name: :'' + ) + end + .to raise_error ArgumentError, error_message + end + end + end + end +end diff --git a/lib/cuprum/collections/rspec/deferred/resource_examples.rb b/lib/cuprum/collections/rspec/deferred/resource_examples.rb new file mode 100644 index 0000000..d4e7229 --- /dev/null +++ b/lib/cuprum/collections/rspec/deferred/resource_examples.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rspec/sleeping_king_studios/deferred' + +require 'cuprum/collections/rspec/deferred' +require 'cuprum/collections/rspec/deferred/relation_examples' + +module Cuprum::Collections::RSpec::Deferred + # Deferred examples for testing resources. + module ResourceExamples + include RSpec::SleepingKingStudios::Deferred::Provider + + deferred_examples 'should be a Resource' do + include Cuprum::Collections::RSpec::Deferred::RelationExamples + + include_deferred 'should be a Relation', + cardinality: true + + include_deferred 'should define Relation cardinality' + + include_deferred 'should define Relation primary key' + end + end +end diff --git a/spec/cuprum/collections/association_spec.rb b/spec/cuprum/collections/association_spec.rb index 3ec24df..9a932d1 100644 --- a/spec/cuprum/collections/association_spec.rb +++ b/spec/cuprum/collections/association_spec.rb @@ -1,21 +1,23 @@ # frozen_string_literal: true require 'cuprum/collections/association' -require 'cuprum/collections/rspec/contracts/association_contracts' +require 'cuprum/collections/rspec/deferred/association_examples' +require 'cuprum/collections/rspec/deferred/relation_examples' require 'support/book' require 'support/grimoire' require 'support/scoped_book' RSpec.describe Cuprum::Collections::Association do - include Cuprum::Collections::RSpec::Contracts::AssociationContracts + include Cuprum::Collections::RSpec::Deferred::AssociationExamples + include Cuprum::Collections::RSpec::Deferred::RelationExamples subject(:association) { described_class.new(**constructor_options) } let(:name) { 'books' } let(:constructor_options) { { name: } } - include_contract 'should be a has association' + include_deferred 'should be a has Association' - include_contract 'should define cardinality' + include_deferred 'should define Relation cardinality' end diff --git a/spec/cuprum/collections/associations/belongs_to_spec.rb b/spec/cuprum/collections/associations/belongs_to_spec.rb index c7b1671..45a26c8 100644 --- a/spec/cuprum/collections/associations/belongs_to_spec.rb +++ b/spec/cuprum/collections/associations/belongs_to_spec.rb @@ -1,21 +1,21 @@ # frozen_string_literal: true require 'cuprum/collections/associations/belongs_to' -require 'cuprum/collections/rspec/contracts/association_contracts' +require 'cuprum/collections/rspec/deferred/association_examples' require 'support/book' require 'support/grimoire' require 'support/scoped_book' RSpec.describe Cuprum::Collections::Associations::BelongsTo do - include Cuprum::Collections::RSpec::Contracts::AssociationContracts + include Cuprum::Collections::RSpec::Deferred::AssociationExamples subject(:association) { described_class.new(**constructor_options) } let(:name) { 'books' } let(:constructor_options) { { name: } } - include_contract 'should be a belongs to association' + include_deferred 'should be a belongs to Association' describe '#plural?' do it { expect(association.plural?).to be false } diff --git a/spec/cuprum/collections/associations/has_many_spec.rb b/spec/cuprum/collections/associations/has_many_spec.rb index 2f69675..5cb5112 100644 --- a/spec/cuprum/collections/associations/has_many_spec.rb +++ b/spec/cuprum/collections/associations/has_many_spec.rb @@ -1,21 +1,21 @@ # frozen_string_literal: true require 'cuprum/collections/associations/has_many' -require 'cuprum/collections/rspec/contracts/association_contracts' +require 'cuprum/collections/rspec/deferred/association_examples' require 'support/book' require 'support/grimoire' require 'support/scoped_book' RSpec.describe Cuprum::Collections::Associations::HasMany do - include Cuprum::Collections::RSpec::Contracts::AssociationContracts + include Cuprum::Collections::RSpec::Deferred::AssociationExamples subject(:association) { described_class.new(**constructor_options) } let(:name) { 'books' } let(:constructor_options) { { name: } } - include_contract 'should be a has association' + include_deferred 'should be a has Association' describe '#plural?' do it { expect(association.plural?).to be true } diff --git a/spec/cuprum/collections/associations/has_one_spec.rb b/spec/cuprum/collections/associations/has_one_spec.rb index d2913c1..72739ba 100644 --- a/spec/cuprum/collections/associations/has_one_spec.rb +++ b/spec/cuprum/collections/associations/has_one_spec.rb @@ -1,21 +1,21 @@ # frozen_string_literal: true require 'cuprum/collections/associations/has_one' -require 'cuprum/collections/rspec/contracts/association_contracts' +require 'cuprum/collections/rspec/deferred/association_examples' require 'support/book' require 'support/grimoire' require 'support/scoped_book' RSpec.describe Cuprum::Collections::Associations::HasOne do - include Cuprum::Collections::RSpec::Contracts::AssociationContracts + include Cuprum::Collections::RSpec::Deferred::AssociationExamples subject(:association) { described_class.new(**constructor_options) } let(:name) { 'book' } let(:constructor_options) { { name: } } - include_contract 'should be a has association' + include_deferred 'should be a has Association' describe '#plural?' do it { expect(association.plural?).to be false } diff --git a/spec/cuprum/collections/basic/collection_spec.rb b/spec/cuprum/collections/basic/collection_spec.rb index f68a296..431badb 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/contracts/collection_contracts' +require 'cuprum/collections/rspec/deferred/collection_examples' require 'cuprum/collections/rspec/fixtures' require 'support/book' @@ -10,7 +10,7 @@ require 'support/scoped_book' RSpec.describe Cuprum::Collections::Basic::Collection do - include Cuprum::Collections::RSpec::Contracts::CollectionContracts + include Cuprum::Collections::RSpec::Deferred::CollectionExamples subject(:collection) do described_class.new( @@ -40,7 +40,7 @@ end end - include_contract 'should be a collection', + include_deferred 'should be a Collection', commands_namespace: 'Cuprum::Collections::Basic::Commands', default_entity_class: Hash @@ -74,7 +74,7 @@ .to be_a Cuprum::Collections::Basic::Scopes::AllScope end - wrap_context 'when initialized with a scope' do + wrap_deferred 'when initialized with a scope' do it 'should transform the scope' do expect(collection.scope) .to be_a Cuprum::Collections::Basic::Scopes::CriteriaScope @@ -93,7 +93,7 @@ .to be_a Cuprum::Collections::Basic::Scopes::CriteriaScope end - wrap_context 'when initialized with a scope' do + wrap_deferred 'when initialized with a scope' do it 'should transform the scope' do expect(copy.scope) .to be_a Cuprum::Collections::Basic::Scopes::CriteriaScope diff --git a/spec/cuprum/collections/collection_spec.rb b/spec/cuprum/collections/collection_spec.rb index fef314d..6e5642f 100644 --- a/spec/cuprum/collections/collection_spec.rb +++ b/spec/cuprum/collections/collection_spec.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true require 'cuprum/collections/collection' -require 'cuprum/collections/rspec/contracts/collection_contracts' +require 'cuprum/collections/rspec/deferred/collection_examples' require 'support/book' require 'support/grimoire' require 'support/scoped_book' RSpec.describe Cuprum::Collections::Collection do - include Cuprum::Collections::RSpec::Contracts::CollectionContracts + include Cuprum::Collections::RSpec::Deferred::CollectionExamples subject(:collection) do described_class.new(**constructor_options) @@ -52,17 +52,17 @@ def call_method(**parameters) .and_any_keywords end - include_contract 'should validate the parameters' + include_deferred 'should validate the Relation parameters' end - include_contract 'should be a collection', abstract: true + include_deferred 'should be a Collection', abstract: true describe '#scope' do it 'should define the default scope' do expect(collection.scope).to be_a Cuprum::Collections::Scopes::AllScope end - wrap_context 'when initialized with a scope' do + wrap_deferred 'when initialized with a scope' do it 'should transform the scope' do expect(collection.scope) .to be_a Cuprum::Collections::Scopes::CriteriaScope @@ -80,7 +80,7 @@ def call_method(**parameters) expect(copy.scope).to be_a Cuprum::Collections::Scopes::CriteriaScope end - wrap_context 'when initialized with a scope' do + wrap_deferred 'when initialized with a scope' do it 'should transform the scope' do expect(copy.scope).to be_a Cuprum::Collections::Scopes::CriteriaScope end diff --git a/spec/cuprum/collections/relation_spec.rb b/spec/cuprum/collections/relation_spec.rb index 37c6485..3586de2 100644 --- a/spec/cuprum/collections/relation_spec.rb +++ b/spec/cuprum/collections/relation_spec.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true require 'cuprum/collections/relation' -require 'cuprum/collections/rspec/contracts/relation_contracts' +require 'cuprum/collections/rspec/deferred/relation_examples' require 'support/book' require 'support/grimoire' require 'support/scoped_book' RSpec.describe Cuprum::Collections::Relation do - include Cuprum::Collections::RSpec::Contracts::RelationContracts + include Cuprum::Collections::RSpec::Deferred::RelationExamples subject(:relation) { described_class.new(**constructor_options) } @@ -29,7 +29,7 @@ end end - include_contract 'should define cardinality' + include_deferred 'should define Relation cardinality' end describe '::Parameters' do @@ -53,7 +53,7 @@ def call_method(**parameters) .with(1).argument end - include_contract 'should validate the parameters' + include_deferred 'should validate the Relation parameters' describe 'with entity_class: a Class' do let(:entity_class) { Book } @@ -1233,8 +1233,8 @@ def call_method(**parameters) klass.attr_reader :options end - include_contract 'should define primary keys' + include_deferred 'should define Relation primary key' end - include_contract 'should be a relation' + include_deferred 'should be a Relation' end diff --git a/spec/cuprum/collections/resource_spec.rb b/spec/cuprum/collections/resource_spec.rb index 318cbbc..272efe3 100644 --- a/spec/cuprum/collections/resource_spec.rb +++ b/spec/cuprum/collections/resource_spec.rb @@ -1,24 +1,19 @@ # frozen_string_literal: true require 'cuprum/collections/resource' -require 'cuprum/collections/rspec/contracts/relation_contracts' +require 'cuprum/collections/rspec/deferred/resource_examples' require 'support/book' require 'support/grimoire' require 'support/scoped_book' RSpec.describe Cuprum::Collections::Resource do - include Cuprum::Collections::RSpec::Contracts::RelationContracts + include Cuprum::Collections::RSpec::Deferred::ResourceExamples subject(:resource) { described_class.new(**constructor_options) } let(:name) { 'books' } let(:constructor_options) { { name: } } - include_contract 'should be a relation', - cardinality: true - - include_contract 'should define primary keys' - - include_contract 'should define cardinality' + include_deferred 'should be a Resource' end