Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion lib/testing_record/dsl.rb
Original file line number Diff line number Diff line change
@@ -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'
37 changes: 30 additions & 7 deletions lib/testing_record/dsl/builder/filters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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<TestingRecord::Model>]
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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/testing_record/dsl/builder/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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, [])
Expand Down
8 changes: 4 additions & 4 deletions lib/testing_record/dsl/validation/input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/testing_record/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ module Error
class AttributeError < StandardError; end
class EntityError < StandardError; end
class InvalidConfigurationError < StandardError; end
class InvalidArgumentError < StandardError; end
end
end
5 changes: 3 additions & 2 deletions lib/testing_record/model.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'automation_helpers/extensions/string'

require_relative 'dsl'

module TestingRecord
Expand All @@ -11,8 +12,6 @@ class Model
extend DSL::Builder::Helpers
extend DSL::Builder::Settings

attr_reader :attributes

class << self
attr_reader :current

Expand Down Expand Up @@ -110,6 +109,8 @@ def ensure_primary_key_presence(attributes)
end
end

attr_reader :attributes

def initialize(attributes = {})
@attributes = attributes
end
Expand Down
44 changes: 43 additions & 1 deletion spec/testing_record/dsl/builder/filters_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) }
Expand Down Expand Up @@ -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 }
Expand Down
18 changes: 16 additions & 2 deletions spec/testing_record/dsl/validation/input_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,30 @@
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

it 'is `false` for all other types' do
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
8 changes: 8 additions & 0 deletions spec/testing_record/model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down