diff --git a/lib/cuprum/collections/commands/associations/find_many.rb b/lib/cuprum/collections/commands/associations/find_many.rb index ff42567..df5402d 100644 --- a/lib/cuprum/collections/commands/associations/find_many.rb +++ b/lib/cuprum/collections/commands/associations/find_many.rb @@ -5,6 +5,48 @@ module Cuprum::Collections::Commands::Associations # Command for querying entities by association. class FindMany < Cuprum::Command + PERMITTED_KEYWORDS = Set.new(%i[entities entity key keys]).freeze + private_constant :PERMITTED_KEYWORDS + + # @!method call(**params) + # @overload call(key:) + # Finds the association values for the given key. + # + # @param key [Object] the primary or foreign key for querying the + # association. + # + # @return [Object, nil] the association value or nil, if the association + # is singular. + # @return [Array] the association values, if the association is + # plural. + # + # @overload call(keys:) + # Finds the association values for the given Array of keys. + # @return [Array] the association values. + # + # @param keys [Array] the primary or foreign keys for querying + # the association. + # + # @return [Array] the association values. + # + # @overload call(entity:) + # Finds the association values for the given entity. + # + # @param entity [Object] the base entity for querying the association. + # + # @return [Object, nil] the association value or nil, if the association + # is singular. + # @return [Array] the association values, if the association is + # plural. + # + # @overload call(entities:) + # Finds the association values for the given Array of entities. + # + # @param entity [Array] the base entities for querying the + # association. + # + # @return [Array] the association values. + # @param association [Cuprum::Collections::Association] the association to # query. # @param repository [Cuprum::Collections::Repository] the repository to @@ -38,29 +80,78 @@ def collection ) end - def perform_query(association:, expected_keys:) + def extract_keys(association, hsh) + return hsh[:key] if hsh.key?(:key) + return hsh[:keys] if hsh.key?(:keys) + + values = hsh.fetch(:entity) { hsh[:entities] } + + if values.is_a?(Array) + association.map_entities_to_keys(*values) + else + association.map_entities_to_keys(values).first + end + end + + def handle_ambiguous_keys(hsh) + return if hsh.keys.size == 1 + + raise ArgumentError, + "ambiguous keywords #{hsh.each_key.map(&:inspect).join(', ')} " \ + '- must provide exactly one parameter' + end + + def handle_extra_keys(hsh) + return if hsh.keys.all? { |key| PERMITTED_KEYWORDS.include?(key) } + + extra_keys = hsh.keys - PERMITTED_KEYWORDS.to_a + + raise ArgumentError, + "invalid keywords #{extra_keys.map(&:inspect).join(', ')}" + end + + def handle_missing_keys(hsh) + return unless hsh.empty? + + raise ArgumentError, 'missing keyword :entity, :entities, :key, or :keys' + end + + def perform_query(association:, expected_keys:, **) query = association.build_keys_query(*expected_keys) find_command = collection.find_matching find_command.call(&query) end - def process(*entities_or_keys) - association = @association.with_inverse(resource) - expected_keys = - association.map_entities_to_keys(*entities_or_keys, strict: false) + def process(**params) # rubocop:disable Metrics/MethodLength + association = @association.with_inverse(resource) + expected_keys, plural = resolve_keys(association, **params) + plural ||= association.plural? - return singular? ? nil : [] if expected_keys.empty? + return plural ? [] : nil if expected_keys.empty? values = step do - perform_query(association: association, expected_keys: expected_keys) + perform_query( + association: association, + expected_keys: expected_keys, + plural: plural + ) end - singular? ? values.first : values.to_a + plural ? values.to_a : values.first end - def singular? - association.singular? && resource.singular? + def resolve_keys(association, **params) + handle_missing_keys(params) + handle_extra_keys(params) + handle_ambiguous_keys(params) + + keys = extract_keys(association, params) + plural = keys.is_a?(Array) + keys = [keys] unless plural + keys = keys.compact.uniq + + [keys, plural] end def tools diff --git a/lib/cuprum/collections/commands/associations/require_many.rb b/lib/cuprum/collections/commands/associations/require_many.rb index e5914d1..01d0983 100644 --- a/lib/cuprum/collections/commands/associations/require_many.rb +++ b/lib/cuprum/collections/commands/associations/require_many.rb @@ -17,16 +17,19 @@ def map_entity_keys(entities:) entities.map { |entity| entity[association.query_key_name] } end - def missing_keys_error(missing_keys:) + def missing_keys_error(missing_keys:, plural:) + attribute_value = + !plural && missing_keys.is_a?(Array) ? missing_keys.first : missing_keys + Cuprum::Collections::Errors::Associations::NotFound.new( attribute_name: association.query_key_name, - attribute_value: singular? ? missing_keys.first : missing_keys, + attribute_value: attribute_value, collection_name: association.name, primary_key: association.primary_key_query? ) end - def perform_query(association:, expected_keys:) + def perform_query(association:, expected_keys:, plural:, **) entities = step { super } missing_keys = find_missing_keys( entities: entities, @@ -35,7 +38,9 @@ def perform_query(association:, expected_keys:) return success(entities) if missing_keys.empty? - error = missing_keys_error(missing_keys: missing_keys) + missing_keys = missing_keys.first unless plural + error = + missing_keys_error(missing_keys: missing_keys, plural: plural) failure(error) end diff --git a/spec/cuprum/collections/commands/associations/find_many_spec.rb b/spec/cuprum/collections/commands/associations/find_many_spec.rb index 4061e18..41d25b5 100644 --- a/spec/cuprum/collections/commands/associations/find_many_spec.rb +++ b/spec/cuprum/collections/commands/associations/find_many_spec.rb @@ -33,19 +33,144 @@ end describe '#call' do + shared_examples 'should find the plural association for one entity' do + context 'when there is are no matching entities' do + it 'should return a passing result with no results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value([]) + end + end + + context 'when there is one matching entity' do + let(:matching) { values[0..0] } + + it 'should return a passing result with the matching results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value(matching) + end + end + + context 'when there are multiple matching entities' do + let(:matching) { values[0..2] } + + it 'should return a passing result with the matching results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value(matching) + end + end + end + + shared_examples 'should find the plural association for many entities' do + context 'when there is are no matching entities' do + it 'should return a passing result with no results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value([]) + end + end + + context 'when there are multiple matching entities' do + let(:matching) { values } + + it 'should return a passing result with the matching results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value(matching) + end + end + end + + shared_examples 'should find the singular association for one entity' do + context 'when there is no matching entity' do + it 'should return a passing result with no results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value(nil) + end + end + + context 'when there is one matching entity' do + let(:matching) { values[0..0] } + + it 'should return a passing result with the matching results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value(matching.first) + end + end + end + + shared_examples 'should find the singular association for many entities' do + context 'when there is are no matching entities' do + it 'should return a passing result with no results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value([]) + end + end + + context 'when there are multiple matching entities' do + let(:matching) { values } + + it 'should return a passing result with the matching results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value(matching) + end + end + end + let(:collection) do repository.find_or_create( name: tools.str.pluralize(association.name), qualified_name: association.qualified_name ) end - let(:matching) { [] } let(:non_matching) do { 'author_id' => 4, 'title' => 'Dead Witch Walking' } end + let(:entities) do + [ + { 'id' => 0, 'name' => 'Tammsyn Muir' }, + { 'id' => 1, 'name' => 'Ursula K. LeGuin' }, + { 'id' => 2, 'name' => 'Seanan McGuire' } + ] + end + let(:values) do + [ + { + 'id' => 0, + 'author_id' => 0, + 'title' => 'Gideon the Ninth' + }, + { + 'id' => 1, + 'author_id' => 0, + 'title' => 'Harrow the Ninth' + }, + { + 'id' => 2, + 'author_id' => 0, + 'title' => 'Nona the Ninth' + }, + { + 'id' => 4, + 'author_id' => 1, + 'title' => 'The Word For World Is Forest' + } + ] + end + let(:inverse_key_name) do + association.with_inverse(resource).inverse_key_name + end + let(:params) { {} } + let(:matching) { [] } def tools SleepingKingStudios::Tools::Toolbelt.instance @@ -57,258 +182,112 @@ def tools collection.insert_one.call(entity: non_matching) end - it { expect(command).to be_callable.with_unlimited_arguments } + it 'should define the method' do + expect(command) + .to be_callable + .with(0).arguments + .and_keywords(:entities, :entity, :key, :keys) + end describe 'with no arguments' do - it 'should return a passing result' do - expect(command.call) - .to be_a_passing_result - .with_value([]) + let(:error_message) do + 'missing keyword :entity, :entities, :key, or :keys' end - end - describe 'with one entity' do - let(:entities) do - [ - { 'id' => 0, 'name' => 'Tammsyn Muir' } - ] + it 'should raise an exception' do + expect { command.call(**params) } + .to raise_error ArgumentError, error_message end + end - it 'should return a passing result with no results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value([]) + describe 'with invalid keywords' do + let(:params) { { key: nil, custom: 'value', other: 'value' } } + let(:error_message) do + 'invalid keywords :custom, :other' end - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end + it 'should raise an exception' do + expect { command.call(**params) } + .to raise_error ArgumentError, error_message end + end - context 'when there are multiple matching entities' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - }, - { - 'id' => 1, - 'author_id' => 0, - 'title' => 'Harrow the Ninth' - }, - { - 'id' => 2, - 'author_id' => 0, - 'title' => 'Nona the Ninth' - } - ] - end + describe 'with ambiguous keywords' do + let(:params) { { key: nil, keys: [] } } + let(:error_message) do + 'ambiguous keywords :key, :keys - must provide exactly one parameter' + end - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end + it 'should raise an exception' do + expect { command.call(**params) } + .to raise_error ArgumentError, error_message end end - describe 'with one key' do - let(:keys) { [0] } + describe 'with entity: nil' do + let(:params) { { entity: nil } } it 'should return a passing result with no results' do - expect(command.call(*keys)) + expect(command.call(**params)) .to be_a_passing_result .with_value([]) end + end - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end - end - - context 'when there are multiple matching entities' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - }, - { - 'id' => 1, - 'author_id' => 0, - 'title' => 'Harrow the Ninth' - }, - { - 'id' => 2, - 'author_id' => 0, - 'title' => 'Nona the Ninth' - } - ] - end + describe 'with entity: an Object' do + let(:params) { { entity: entities.first } } - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end - end + include_examples 'should find the plural association for one entity' end - describe 'with many entities' do - let(:entities) do - [ - { 'id' => 0, 'name' => 'Tammsyn Muir' }, - { 'id' => 1, 'name' => 'Ursula K. LeGuin' }, - { 'id' => 2, 'name' => 'Seanan McGuire' } - ] - end + describe 'with entities: an empty Array' do + let(:params) { { entities: [] } } it 'should return a passing result with no results' do - expect(command.call(*entities)) + expect(command.call(**params)) .to be_a_passing_result .with_value([]) end + end - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - } - ] - end + describe 'with entities: an Array of Objects' do + let(:params) { { entities: entities } } - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end - end + include_examples 'should find the plural association for many entities' + end - context 'when there are multiple matching entities' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - }, - { - 'id' => 1, - 'author_id' => 0, - 'title' => 'Harrow the Ninth' - }, - { - 'id' => 2, - 'author_id' => 0, - 'title' => 'Nona the Ninth' - }, - { - 'id' => 4, - 'author_id' => 1, - 'title' => 'The Word For World Is Forest' - } - ] - end + describe 'with key: nil' do + let(:params) { { key: nil } } - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end + it 'should return a passing result with no results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value([]) end end - describe 'with many keys' do - let(:keys) { [0, 1, 2] } + describe 'with key: an Integer' do + let(:params) { { key: entities.first[inverse_key_name] } } + + include_examples 'should find the plural association for one entity' + end + + describe 'with keys: an empty Array' do + let(:params) { { keys: [] } } it 'should return a passing result with no results' do - expect(command.call(*keys)) + expect(command.call(**params)) .to be_a_passing_result .with_value([]) end + end - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end + describe 'with keys: an Array of Integers' do + let(:params) do + { keys: entities.map { |entity| entity[inverse_key_name] } } end - context 'when there are multiple matching entities' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - }, - { - 'id' => 1, - 'author_id' => 0, - 'title' => 'Harrow the Ninth' - }, - { - 'id' => 2, - 'author_id' => 0, - 'title' => 'Nona the Ninth' - }, - { - 'id' => 4, - 'author_id' => 1, - 'title' => 'The Word For World Is Forest' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end - end + include_examples 'should find the plural association for many entities' end context 'when initialized with a belongs_to association' do @@ -322,177 +301,56 @@ def tools 'name' => 'Kim Harrison' } end - - describe 'with no arguments' do - it 'should return a passing result' do - expect(command.call) - .to be_a_passing_result - .with_value([]) - end + let(:entities) do + [ + { 'id' => 0, 'author_id' => 0, 'title' => 'Gideon the Ninth' }, + { 'id' => 1, 'author_id' => 0, 'title' => 'Harrow the Ninth' }, + { + 'id' => 2, + 'author_id' => 1, + 'title' => 'The Word For World Is Forest' + } + ] end - - describe 'with one entity' do - let(:entities) do - [ - { 'id' => 0, 'author_id' => 0, 'title' => 'Gideon the Ninth' } - ] - end - - it 'should return a passing result with no results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value([]) - end - - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end - end + let(:values) do + [ + { + 'id' => 0, + 'name' => 'Tammsyn Muir' + }, + { + 'id' => 1, + 'name' => 'Ursula K. LeGuin' + } + ] end - describe 'with one key' do - let(:keys) { [0] } - - it 'should return a passing result with no results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value([]) - end + describe 'with entity: an Object' do + let(:params) { { entity: entities.first } } - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end - end + include_examples 'should find the singular association for one entity' end - describe 'with many entities' do - let(:entities) do - [ - { 'id' => 0, 'author_id' => 0, 'title' => 'Gideon the Ninth' }, - { 'id' => 1, 'author_id' => 0, 'title' => 'Harrow the Ninth' }, - { - 'id' => 2, - 'author_id' => 1, - 'title' => 'The Word For World Is Forest' - } - ] - end - - it 'should return a passing result with no results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value([]) - end - - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end - end + describe 'with entities: an Array of Objects' do + let(:params) { { entities: entities } } - context 'when there are many matching entities' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - }, - { - 'id' => 1, - 'name' => 'Ursula K. LeGuin' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end - end + include_examples \ + 'should find the singular association for many entities' end - describe 'with many keys' do - let(:keys) { [0, 1, 2] } + describe 'with key: an Integer' do + let(:params) { { key: entities.first[inverse_key_name] } } - it 'should return a passing result with no results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value([]) - end + include_examples 'should find the singular association for one entity' + end - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end + describe 'with keys: an Array of Integers' do + let(:params) do + { keys: entities.map { |entity| entity[inverse_key_name] } } end - context 'when there are many matching entities' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - }, - { - 'id' => 1, - 'name' => 'Ursula K. LeGuin' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end - end + include_examples \ + 'should find the singular association for many entities' end context 'when initialized with a singular resource' do @@ -500,70 +358,16 @@ def tools Cuprum::Collections::Resource.new(name: 'book', singular: true) end - describe 'with no arguments' do - it 'should return a passing result' do - expect(command.call) - .to be_a_passing_result - .with_value(nil) - end - end + describe 'with entity: an Object' do + let(:params) { { entity: entities.first } } - describe 'with one entity' do - let(:entities) do - [ - { 'id' => 0, 'author_id' => 0, 'title' => 'Gideon the Ninth' } - ] - end - - it 'should return a passing result with no results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(nil) - end - - context 'when there is one matching entity' do # rubocop:disable RSpec/NestedGroups - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching.first) - end - end + include_examples 'should find the singular association for one entity' end - describe 'with one key' do - let(:keys) { [0] } - - it 'should return a passing result with no results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(nil) - end - - context 'when there is one matching entity' do # rubocop:disable RSpec/NestedGroups - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching.first) - end - end + describe 'with key: an Integer' do + let(:params) { { key: entities.first[inverse_key_name] } } + + include_examples 'should find the singular association for one entity' end end end @@ -578,181 +382,47 @@ def tools 'name' => 'Jane C. Agent' } end - - describe 'with no arguments' do - it 'should return a passing result' do - expect(command.call) - .to be_a_passing_result - .with_value([]) - end + let(:values) do + [ + { + 'id' => 0, + 'author_id' => 0, + 'title' => 'Jane A. Agent' + }, + { + 'id' => 1, + 'author_id' => 1, + 'title' => 'John B. Agent' + } + ] end - describe 'with one entity' do - let(:entities) do - [ - { 'id' => 0, 'name' => 'Tammsyn Muir' } - ] - end + describe 'with entity: an Object' do + let(:params) { { entity: entities.first } } - it 'should return a passing result with no results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value([]) - end - - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Jane A. Agent' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end - end + include_examples 'should find the singular association for one entity' end - describe 'with one key' do - let(:keys) { [0] } - - it 'should return a passing result with no results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value([]) - end + describe 'with entities: an Array of Objects' do + let(:params) { { entities: entities } } - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Jane A. Agent' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end - end + include_examples \ + 'should find the singular association for many entities' end - describe 'with many entities' do - let(:entities) do - [ - { 'id' => 0, 'name' => 'Tammsyn Muir' }, - { 'id' => 1, 'name' => 'Ursula K. LeGuin' }, - { 'id' => 2, 'name' => 'Seanan McGuire' } - ] - end - - it 'should return a passing result with no results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value([]) - end - - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Jane A. Agent' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end - end + describe 'with key: an Integer' do + let(:params) { { key: entities.first[inverse_key_name] } } - context 'when there are many matching entities' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Jane A. Agent' - }, - { - 'id' => 1, - 'author_id' => 1, - 'title' => 'John B. Agent' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end - end + include_examples 'should find the singular association for one entity' end - describe 'with many keys' do - let(:keys) { [0, 1, 2] } - - it 'should return a passing result with no results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value([]) - end - - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Jane A. Agent' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end + describe 'with keys: an Array of Integers' do + let(:params) do + { keys: entities.map { |entity| entity[inverse_key_name] } } end - context 'when there are many matching entities' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Jane A. Agent' - }, - { - 'id' => 1, - 'author_id' => 1, - 'title' => 'John B. Agent' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end - end + include_examples \ + 'should find the singular association for many entities' end context 'when initialized with a singular resource' do @@ -760,72 +430,16 @@ def tools Cuprum::Collections::Resource.new(name: 'author', singular: true) end - describe 'with no arguments' do - it 'should return a passing result' do - expect(command.call) - .to be_a_passing_result - .with_value(nil) - end - end + describe 'with key: an Integer' do + let(:params) { { key: entities.first[inverse_key_name] } } - describe 'with one entity' do - let(:entities) do - [ - { 'id' => 0, 'name' => 'Tammsyn Muir' } - ] - end - - it 'should return a passing result with no results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(nil) - end - - context 'when there is one matching entity' do # rubocop:disable RSpec/NestedGroups - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching.first) - end - end + include_examples 'should find the singular association for one entity' end - describe 'with one key' do - let(:keys) { [0] } - - it 'should return a passing result with no results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(nil) - end - - context 'when there is one matching entity' do # rubocop:disable RSpec/NestedGroups - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching.first) - end - end + describe 'with entity: an Object' do + let(:params) { { entity: entities.first } } + + include_examples 'should find the singular association for one entity' end end end @@ -835,128 +449,16 @@ def tools Cuprum::Collections::Resource.new(name: 'author', singular: true) end - describe 'with no arguments' do - it 'should return a passing result' do - expect(command.call) - .to be_a_passing_result - .with_value([]) - end - end - - describe 'with one entity' do - let(:entities) do - [ - { 'id' => 0, 'name' => 'Tammsyn Muir' } - ] - end - - it 'should return a passing result with no results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value([]) - end - - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end - end + describe 'with entity: an Object' do + let(:params) { { entity: entities.first } } - context 'when there are multiple matching entities' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - }, - { - 'id' => 1, - 'author_id' => 0, - 'title' => 'Harrow the Ninth' - }, - { - 'id' => 2, - 'author_id' => 0, - 'title' => 'Nona the Ninth' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end - end + include_examples 'should find the plural association for one entity' end - describe 'with one key' do - let(:keys) { [0] } + describe 'with key: an Integer' do + let(:params) { { key: entities.first[inverse_key_name] } } - it 'should return a passing result with no results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value([]) - end - - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end - end - - context 'when there are multiple matching entities' do - let(:matching) do - [ - { - 'id' => 0, - 'author_id' => 0, - 'title' => 'Gideon the Ninth' - }, - { - 'id' => 1, - 'author_id' => 0, - 'title' => 'Harrow the Ninth' - }, - { - 'id' => 2, - 'author_id' => 0, - 'title' => 'Nona the Ninth' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end - end + include_examples 'should find the plural association for one entity' end end end diff --git a/spec/cuprum/collections/commands/associations/require_many_spec.rb b/spec/cuprum/collections/commands/associations/require_many_spec.rb index 20d38c9..bfe91b0 100644 --- a/spec/cuprum/collections/commands/associations/require_many_spec.rb +++ b/spec/cuprum/collections/commands/associations/require_many_spec.rb @@ -34,19 +34,122 @@ end describe '#call' do + shared_examples 'should require the association for one entity' do + context 'when there is no matching entity' do + let(:expected_error) do + Cuprum::Collections::Errors::Associations::NotFound.new( + attribute_name: association.query_key_name, + attribute_value: 0, + collection_name: association.name, + primary_key: association.primary_key_query? + ) + end + + it 'should return a failing result' do + expect(command.call(**params)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + context 'when there is one matching entity' do + let(:matching) { values[0..0] } + + it 'should return a passing result with the matching results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value(matching.first) + end + end + end + + shared_examples 'should require the association for many entities' do + context 'when there are no matching entities' do + let(:expected_error) do + Cuprum::Collections::Errors::Associations::NotFound.new( + attribute_name: association.query_key_name, + attribute_value: [0, 1], + collection_name: association.name, + primary_key: association.primary_key_query? + ) + end + + it 'should return a failing result' do + expect(command.call(**params)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + context 'when there are some matching entities' do + let(:matching) { values[0..0] } + let(:expected_error) do + Cuprum::Collections::Errors::Associations::NotFound.new( + attribute_name: association.query_key_name, + attribute_value: [1], + collection_name: association.name, + primary_key: association.primary_key_query? + ) + end + + it 'should return a failing result' do + expect(command.call(**params)) + .to be_a_failing_result + .with_error(expected_error) + end + end + + context 'when there are many matching entities' do + let(:matching) { values } + + it 'should return a passing result with the matching results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value(matching) + end + end + end + let(:collection) do repository.find_or_create( name: tools.str.pluralize(association.name), qualified_name: association.qualified_name ) end - let(:matching) { [] } let(:non_matching) do { 'id' => 3, 'name' => 'Kim Harrison' } end + let(:entities) do + [ + { 'id' => 0, 'author_id' => 0, 'title' => 'Gideon the Ninth' }, + { 'id' => 1, 'author_id' => 0, 'title' => 'Harrow the Ninth' }, + { + 'id' => 2, + 'author_id' => 1, + 'title' => 'The Word For World Is Forest' + } + ] + end + let(:values) do + [ + { + 'id' => 0, + 'name' => 'Tammsyn Muir' + }, + { + 'id' => 1, + 'name' => 'Ursula K. LeGuin' + } + ] + end + let(:inverse_key_name) do + association.with_inverse(resource).inverse_key_name + end + let(:params) { {} } + let(:matching) { [] } def tools SleepingKingStudios::Tools::Toolbelt.instance @@ -58,226 +161,112 @@ def tools collection.insert_one.call(entity: non_matching) end - it { expect(command).to be_callable.with_unlimited_arguments } + it 'should define the method' do + expect(command) + .to be_callable + .with(0).arguments + .and_keywords(:entities, :entity, :key, :keys) + end describe 'with no arguments' do - it 'should return a passing result' do - expect(command.call) - .to be_a_passing_result - .with_value([]) + let(:error_message) do + 'missing keyword :entity, :entities, :key, or :keys' end - end - describe 'with one entity' do - let(:entities) do - [ - { 'id' => 0, 'author_id' => 0, 'title' => 'Gideon the Ninth' } - ] - end - let(:expected_error) do - Cuprum::Collections::Errors::Associations::NotFound.new( - attribute_name: association.query_key_name, - attribute_value: [0], - collection_name: association.name, - primary_key: association.primary_key_query? - ) + it 'should raise an exception' do + expect { command.call(**params) } + .to raise_error ArgumentError, error_message end + end - it 'should return a failing result' do - expect(command.call(*entities)) - .to be_a_failing_result - .with_error(expected_error) + describe 'with invalid keywords' do + let(:params) { { key: nil, custom: 'value', other: 'value' } } + let(:error_message) do + 'invalid keywords :custom, :other' end - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end + it 'should raise an exception' do + expect { command.call(**params) } + .to raise_error ArgumentError, error_message end end - describe 'with one key' do - let(:keys) { [0] } - let(:expected_error) do - Cuprum::Collections::Errors::Associations::NotFound.new( - attribute_name: association.query_key_name, - attribute_value: [0], - collection_name: association.name, - primary_key: association.primary_key_query? - ) + describe 'with ambiguous keywords' do + let(:params) { { key: nil, keys: [] } } + let(:error_message) do + 'ambiguous keywords :key, :keys - must provide exactly one parameter' end - it 'should return a failing result' do - expect(command.call(*keys)) - .to be_a_failing_result - .with_error(expected_error) + it 'should raise an exception' do + expect { command.call(**params) } + .to raise_error ArgumentError, error_message end + end - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - } - ] - end + describe 'with entity: nil' do + let(:params) { { entity: nil } } - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end + it 'should return a passing result with no results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value(nil) end end - describe 'with many entities' do - let(:entities) do - [ - { 'id' => 0, 'author_id' => 0, 'title' => 'Gideon the Ninth' }, - { 'id' => 1, 'author_id' => 0, 'title' => 'Harrow the Ninth' }, - { - 'id' => 2, - 'author_id' => 1, - 'title' => 'The Word For World Is Forest' - } - ] - end - let(:expected_error) do - Cuprum::Collections::Errors::Associations::NotFound.new( - attribute_name: association.query_key_name, - attribute_value: [0, 1], - collection_name: association.name, - primary_key: association.primary_key_query? - ) - end + describe 'with entity: an Object' do + let(:params) { { entity: entities.first } } - it 'should return a failing result' do - expect(command.call(*entities)) - .to be_a_failing_result - .with_error(expected_error) - end + include_examples 'should require the association for one entity' + end - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - } - ] - end - let(:expected_error) do - Cuprum::Collections::Errors::Associations::NotFound.new( - attribute_name: association.query_key_name, - attribute_value: [1], - collection_name: association.name, - primary_key: association.primary_key_query? - ) - end + describe 'with entities: an empty Array' do + let(:params) { { entities: [] } } - it 'should return a failing result' do - expect(command.call(*entities)) - .to be_a_failing_result - .with_error(expected_error) - end + it 'should return a passing result with no results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value([]) end + end - context 'when there are many matching entities' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - }, - { - 'id' => 1, - 'name' => 'Ursula K. LeGuin' - } - ] - end + describe 'with entities: an Array of Objects' do + let(:params) { { entities: entities } } - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching) - end - end + include_examples 'should require the association for many entities' end - describe 'with many keys' do - let(:keys) { [0, 1] } - let(:expected_error) do - Cuprum::Collections::Errors::Associations::NotFound.new( - attribute_name: association.query_key_name, - attribute_value: [0, 1], - collection_name: association.name, - primary_key: association.primary_key_query? - ) - end + describe 'with key: nil' do + let(:params) { { key: nil } } - it 'should return a failing result' do - expect(command.call(*keys)) - .to be_a_failing_result - .with_error(expected_error) + it 'should return a passing result with no results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value(nil) end + end - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - } - ] - end - let(:expected_error) do - Cuprum::Collections::Errors::Associations::NotFound.new( - attribute_name: association.query_key_name, - attribute_value: [1], - collection_name: association.name, - primary_key: association.primary_key_query? - ) - end + describe 'with key: an Integer' do + let(:params) { { key: entities.first[inverse_key_name] } } - it 'should return a failing result' do - expect(command.call(*keys)) - .to be_a_failing_result - .with_error(expected_error) - end - end + include_examples 'should require the association for one entity' + end - context 'when there are many matching entities' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - }, - { - 'id' => 1, - 'name' => 'Ursula K. LeGuin' - } - ] - end + describe 'with keys: an empty Array' do + let(:params) { { keys: [] } } - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching) - end + it 'should return a passing result with no results' do + expect(command.call(**params)) + .to be_a_passing_result + .with_value([]) + end + end + + describe 'with keys: an Array of Integers' do + let(:params) do + { keys: entities.map { |entity| entity[inverse_key_name] } } end + + include_examples 'should require the association for many entities' end context 'when initialized with a singular resource' do @@ -285,86 +274,16 @@ def tools Cuprum::Collections::Resource.new(name: 'book', singular: true) end - describe 'with no arguments' do - it 'should return a passing result' do - expect(command.call) - .to be_a_passing_result - .with_value(nil) - end - end + describe 'with entity: an Object' do + let(:params) { { entity: entities.first } } - describe 'with one entity' do - let(:entities) do - [ - { 'id' => 0, 'author_id' => 0, 'title' => 'Gideon the Ninth' } - ] - end - let(:expected_error) do - Cuprum::Collections::Errors::Associations::NotFound.new( - attribute_name: association.query_key_name, - attribute_value: 0, - collection_name: association.name, - primary_key: association.primary_key_query? - ) - end - - it 'should return a failing result' do - expect(command.call(*entities)) - .to be_a_failing_result - .with_error(expected_error) - end - - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*entities)) - .to be_a_passing_result - .with_value(matching.first) - end - end + include_examples 'should require the association for one entity' end - describe 'with one key' do - let(:keys) { [0] } - let(:expected_error) do - Cuprum::Collections::Errors::Associations::NotFound.new( - attribute_name: association.query_key_name, - attribute_value: 0, - collection_name: association.name, - primary_key: association.primary_key_query? - ) - end - - it 'should return a failing result' do - expect(command.call(*keys)) - .to be_a_failing_result - .with_error(expected_error) - end + describe 'with key: an Integer' do + let(:params) { { key: entities.first[inverse_key_name] } } - context 'when there is one matching entity' do - let(:matching) do - [ - { - 'id' => 0, - 'name' => 'Tammsyn Muir' - } - ] - end - - it 'should return a passing result with the matching results' do - expect(command.call(*keys)) - .to be_a_passing_result - .with_value(matching.first) - end - end + include_examples 'should require the association for one entity' end end end