diff --git a/CHANGELOG.md b/CHANGELOG.md index a1f6b30..9cff4ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ### Removed ### Added +- Added a new conditional piece of logic for filtering whilst using `#find_by`. This allows you to filter using "AND" logic or "OR" logic + - Filters using "AND" logic will require all attributes to match the criteria specified in order for an entity to be returned + - Filters using "OR" logic will require at least one attribute to match the criteria specified in order for an entity to be returned + - By default, all filters will use "AND" logic, but you can specify "OR" logic by using the `:logic :or` keyword argument when defining a filter that uses `#find_by` ### Changed - Exposed `.find_by` as a public method for more complex querying of models, and to allow people to write @@ -12,6 +16,7 @@ their own custom filters on top of this method ruby conventions and to prevent issues with helper generation - `.with_email` filter now supports `email_address`, `email` and `email-address` key names for better flexibility when filtering on email addresses +- Refactored `.find_by` to use more ruby-like methods to improve performance rather than repeated iterations and selections ### Fixed - Hyphenated keys are not permitted for the `primary_key` setting as this will cause issues with ruby diff --git a/lib/testing_record/dsl.rb b/lib/testing_record/dsl.rb index 6279b95..4c99339 100644 --- a/lib/testing_record/dsl.rb +++ b/lib/testing_record/dsl.rb @@ -1,4 +1,6 @@ # frozen_string_literal: true -require_relative 'dsl/builder' +# Must be loaded before builder as some validations are needed during build phase require_relative 'dsl/validation' + +require_relative 'dsl/builder' diff --git a/lib/testing_record/dsl/builder/filters.rb b/lib/testing_record/dsl/builder/filters.rb index 6ff4a48..52c2336 100644 --- a/lib/testing_record/dsl/builder/filters.rb +++ b/lib/testing_record/dsl/builder/filters.rb @@ -6,6 +6,8 @@ module Builder # [TestingRecord::DSL::Builder::Filters] # Ways in which we can filter our collection to find specific models module Filters + include DSL::Validation::Input + # Checks to see whether an entity exists with the provided attributes # # @return [Boolean] @@ -14,16 +16,19 @@ def exists?(attributes) end # Finds all entities that match specified attribute values + # attributes (Hash) -> The attributes you wish to filter on, each is iterated through sequentially + # :logic (Symbol) -> Whether to use `and` (Intersection), or `or` (Union), logic to combine each key in attributes hash # # @return [Array] - def find_by(attributes) - pool = all - attributes.each do |key, value| - TestingRecord.logger.debug("Current user pool size: #{pool.length}") - TestingRecord.logger.debug("Filtering User list by #{key}: #{value}") - pool = pool.select { |entity| entity.attributes[key] == value } + def find_by(attributes, logic: :and) + raise Error::InvalidArgumentError, 'Invalid filtering logic option, must be `:and` or `:or`' unless filter_logic_valid?(logic) + + TestingRecord.logger.debug("Filtering Entity: '#{self}' list by #{attributes}. Logic: '#{logic}'") + if logic == :and + find_by_and(attributes) + else + find_by_or(attributes) end - pool end # Finds an entity with the provided email address @@ -68,6 +73,24 @@ def with_primary_key?(primary_key) def with_primary_key(primary_key) find_by({ __primary_key => primary_key })&.first&.tap { |entity| entity.class.current = entity } end + + private + + def find_by_and(attributes) + all.select do |entity| + attributes.all? do |key, value| + entity.attributes[key] == value + end + end + end + + def find_by_or(attributes) + all.select do |entity| + attributes.any? do |key, value| + entity.attributes[key] == value + end + end + end end end end diff --git a/lib/testing_record/dsl/builder/settings.rb b/lib/testing_record/dsl/builder/settings.rb index 0675f20..0d25084 100644 --- a/lib/testing_record/dsl/builder/settings.rb +++ b/lib/testing_record/dsl/builder/settings.rb @@ -24,7 +24,7 @@ def __primary_key # # @return [Symbol] def caching(option) - raise Error::InvalidConfigurationError, 'Invalid caching option, must be :enabled or :disabled' unless caching_valid?(option) + raise Error::InvalidConfigurationError, 'Invalid caching option, must be `:enabled` or `:disabled`' unless caching_valid?(option) return unless option == :enabled instance_variable_set(:@all, []) diff --git a/lib/testing_record/dsl/validation/input.rb b/lib/testing_record/dsl/validation/input.rb index 4b01a82..27673ed 100644 --- a/lib/testing_record/dsl/validation/input.rb +++ b/lib/testing_record/dsl/validation/input.rb @@ -10,12 +10,12 @@ module Input # # @return [Boolean] def caching_valid?(input) - enabled_or_disabled.include?(input) + %i[enabled disabled].include?(input) end - private - - def enabled_or_disabled = %i[enabled disabled] + def filter_logic_valid?(input) + %i[and or].include?(input) + end end end end diff --git a/lib/testing_record/error.rb b/lib/testing_record/error.rb index 349cdf8..72efe01 100644 --- a/lib/testing_record/error.rb +++ b/lib/testing_record/error.rb @@ -5,5 +5,6 @@ module Error class AttributeError < StandardError; end class EntityError < StandardError; end class InvalidConfigurationError < StandardError; end + class InvalidArgumentError < StandardError; end end end diff --git a/lib/testing_record/model.rb b/lib/testing_record/model.rb index 25fd59b..aa7887d 100644 --- a/lib/testing_record/model.rb +++ b/lib/testing_record/model.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'automation_helpers/extensions/string' + require_relative 'dsl' module TestingRecord @@ -11,8 +12,6 @@ class Model extend DSL::Builder::Helpers extend DSL::Builder::Settings - attr_reader :attributes - class << self attr_reader :current @@ -110,6 +109,8 @@ def ensure_primary_key_presence(attributes) end end + attr_reader :attributes + def initialize(attributes = {}) @attributes = attributes end diff --git a/spec/testing_record/dsl/builder/filters_spec.rb b/spec/testing_record/dsl/builder/filters_spec.rb index 9a3624c..980cc00 100644 --- a/spec/testing_record/dsl/builder/filters_spec.rb +++ b/spec/testing_record/dsl/builder/filters_spec.rb @@ -75,7 +75,7 @@ def self.name end end - describe '.find_by' do + describe '.find_by - `:and` logic' do let(:foo_entity) { model_klazz.create({ email_address: 'foo@foo.com', foo: 3, other: :foo }) } let(:bar_entity) { model_klazz.create({ email_address: 'bar@bar.com', foo: 3, other: :bar }) } let(:baz_entity) { model_klazz.create({ email_address: 'baz@baz.com', foo: 3, other: :baz }) } @@ -107,6 +107,48 @@ def self.name end end + describe '.find_by - `:or` logic' do + let(:foo_entity) { model_klazz.create({ email_address: 'foo@foo.com', foo: 3, other: :foo }) } + let(:bar_entity) { model_klazz.create({ email_address: 'bar@bar.com', foo: 3, other: :bar }) } + let(:baz_entity) { model_klazz.create({ email_address: 'baz@baz.com', foo: 3, other: :baz }) } + + before do + foo_entity + bar_entity + baz_entity + end + + context 'with a simple 1 attribute query' do + it 'returns a collection of entities that match the query - behaving the same as `:and` logic' do + expect(model_klazz.find_by({ foo: 3 }, logic: :or)).to eq([foo_entity, bar_entity, baz_entity]) + end + + it 'returns a blank collection when no entities match the query - behaving the same as `:and` logic' do + expect(model_klazz.find_by({ foo: 4 }, logic: :or)).to eq([]) + end + end + + context 'with a more complex set of attributes as a query' do + it 'returns a collection of entities that match any of the query attributes' do + expect(model_klazz.find_by({ other: :bar, email_address: 'foo@foo.com' }, logic: :or)).to eq([foo_entity, bar_entity]) + end + + it 'returns a blank collection when no entities match any of the query attributes' do + expect(model_klazz.find_by({ foo: 55, other: :jeff, email_address: 'jeff@foo.com' }, logic: :or)).to eq([]) + end + end + end + + describe '.find_by - invalid logic' do + let(:foo_entity) { model_klazz.create({ email_address: 'foo@foo.com', foo: 3, other: :foo }) } + + it 'raises an error that the logic is not valid' do + expect { model_klazz.find_by({ foo: 3 }, logic: :foo) } + .to raise_error(TestingRecord::Error::InvalidArgumentError) + .with_message('Invalid filtering logic option, must be `:and` or `:or`') + end + end + describe '.with_id' do before { model_klazz.primary_key :id } after { model_klazz.primary_key :email_address } diff --git a/spec/testing_record/dsl/validation/input_spec.rb b/spec/testing_record/dsl/validation/input_spec.rb index be93f42..ee5fe08 100644 --- a/spec/testing_record/dsl/validation/input_spec.rb +++ b/spec/testing_record/dsl/validation/input_spec.rb @@ -8,11 +8,11 @@ end describe '.caching_valid?' do - it 'is `true` when the type is enabled' do + it 'is `true` when the type is :enabled' do expect(klazz.caching_valid?(:enabled)).to be true end - it 'is `true` when the type is disabled' do + it 'is `true` when the type is :disabled' do expect(klazz.caching_valid?(:disabled)).to be true end @@ -20,4 +20,18 @@ expect(klazz.caching_valid?(:foo)).to be false end end + + describe '.filter_logic_valid??' do + it 'is `true` when the type is :and' do + expect(klazz.filter_logic_valid?(:and)).to be true + end + + it 'is `true` when the type is :or' do + expect(klazz.filter_logic_valid?(:or)).to be true + end + + it 'is `false` for all other types' do + expect(klazz.filter_logic_valid?(:foo)).to be false + end + end end diff --git a/spec/testing_record/model_spec.rb b/spec/testing_record/model_spec.rb index 7a61449..dfe870d 100644 --- a/spec/testing_record/model_spec.rb +++ b/spec/testing_record/model_spec.rb @@ -86,6 +86,14 @@ end end + context 'with an invalid caching setting' do + it 'does not permit the class to be configured with an invalid caching setting' do + expect { FakeModel.caching :foo } + .to raise_error(TestingRecord::Error::InvalidConfigurationError) + .with_message('Invalid caching option, must be `:enabled` or `:disabled`') + end + end + context 'with any hyphenated keys' do before do FakeModel.caching :enabled