Skip to content
Merged
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
## [Unreleased]
### Breaking Changes
- Set a default primary key of `:id` to all models
- All models require a primary key attribute to be present on each created entity
- All entities are now deduplicated on their primary key value - but only when `caching` is enabled on the model

### Removed

### Added
- Add ability to change the default primary key for all models by using `TestingRecord.default_primary_key = :my_id`
- Added ability to filter model on `primary_key` attribute
- `.with_primary_key?` -> An entity exists with the `primary_key` value specified
- `.with_primary_key` -> Returns the entity with the `primary_key` value specified

### Changed
- Deletion calls will now also purge `.current` if the deleted entity is the current entity when caching is enabled
Expand Down Expand Up @@ -40,7 +45,7 @@ add all helpers to all attributes on a model
## [0.6] - 2026-02-09
### Added
- Added ability to filter model on `id` attribute
- `.with_id?` -> An entity exists that with the id specified
- `.with_id?` -> An entity exists with the id specified
- `.with_id` -> Returns the entity with the id specified
- Added ability to delete models where caching is enabled
- When updating models, the cache is also updated to reflect the new values
Expand Down
15 changes: 15 additions & 0 deletions lib/testing_record/dsl/builder/filters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ def with_id(id)
find_by({ id: })&.first&.tap { |entity| entity.class.current = entity }
end

# Checks to see whether an entity exists with the provided primary_key
#
# @return [Boolean]
def with_primary_key?(primary_key)
!with_primary_key(primary_key).nil?
end

# Finds an entity with the provided primary_key
# If one is found, set it as the current entity
#
# @return [TestingRecord::Model, nil]
def with_primary_key(primary_key)
find_by({ __primary_key => primary_key })&.first&.tap { |entity| entity.class.current = entity }
end

private

# Finds all entities that match specified attribute values
Expand Down
2 changes: 2 additions & 0 deletions lib/testing_record/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

module TestingRecord
module Error
class AttributeError < StandardError; end
class EntityError < StandardError; end
class InvalidConfigurationError < StandardError; end
end
end
53 changes: 44 additions & 9 deletions lib/testing_record/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,24 @@ class << self
attr_reader :current

# Creates an instance of the model
# -> Creating iVar values for each attribute that was provided
# -> Adding it to the cache if caching is enabled
# -> Keeping a track of all originally supplied attributes in symbolized format in the `attributes` iVar
# -> Ensures that the primary key is specified in the attribute payload
# -> Validates that a duplicate entity has not been made (If caching is enabled)
# -> Creates iVar values (Symbol format) and attr_reader's for each attribute that was provided
# -> Adds helper methods (If the model has been configured to include helpers)
# -> Adds it to the cache (If caching is enabled)
#
# @return [TestingRecord::Model]
def create(attributes)
new(attributes.transform_keys(&:to_sym)).tap do |entity|
configure_data(entity, attributes)
add_helpers(attributes) if entity.class.instance_variable_get(:@include_helpers)
cache_entity(entity)
if respond_to?(:all)
create_with_caching(attributes)
else
create_without_caching(attributes)
end
end

# Sets the current entity instance to the one supplied (Or removes it if supplied `nil`)
#
# @return [TestingRecord::Model, nil]
def current=(entity)
if entity
TestingRecord.logger.info("Switching current user from #{@current} to #{entity}")
Expand Down Expand Up @@ -58,9 +63,25 @@ def delete_by_id(id)

private

def cache_entity(entity)
return unless respond_to?(:all)
def create_with_caching(attributes)
ensure_primary_key_presence(attributes)
ensure_deduplication(attributes)
new(attributes.transform_keys(&:to_sym)).tap do |entity|
configure_data(entity, attributes)
add_helpers(attributes) if entity.class.instance_variable_get(:@include_helpers)
cache_entity(entity)
end
end

def create_without_caching(attributes)
ensure_primary_key_presence(attributes)
new(attributes.transform_keys(&:to_sym)).tap do |entity|
configure_data(entity, attributes)
add_helpers(attributes) if entity.class.instance_variable_get(:@include_helpers)
end
end

def cache_entity(entity)
self.current = entity
all << entity
TestingRecord.logger.debug("Entity: #{entity} added to cache")
Expand All @@ -72,6 +93,20 @@ def configure_data(entity, attributes)
entity.class.attr_reader attribute_key
end
end

def ensure_deduplication(attributes)
pk_value = attributes[__primary_key]
return unless with_primary_key?(pk_value)

TestingRecord.logger.error("#{name} entity already exists with primary key: #{pk_value}")
raise Error::EntityError, "#{name} entity already exists with primary key: #{pk_value}"
end

def ensure_primary_key_presence(attributes)
return if attributes.key?(__primary_key)

raise Error::AttributeError, "#{name} entity has not been supplied with the primary key: #{__primary_key}"
end
end

def initialize(attributes = {})
Expand Down
51 changes: 51 additions & 0 deletions spec/testing_record/dsl/builder/filters_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
subject(:model_klazz) do
Class.new(TestingRecord::Model) do
caching :enabled
primary_key :email_address
end
end

Expand Down Expand Up @@ -48,6 +49,9 @@
end

describe '.with_id' do
before { model_klazz.primary_key :id }
after { model_klazz.primary_key :email_address }

context 'when entity does not exist' do
it 'does not find a model' do
expect(model_klazz.with_id(1)).to be_nil
Expand All @@ -67,6 +71,9 @@
end

describe '.with_id?' do
before { model_klazz.primary_key :id }
after { model_klazz.primary_key :email_address }

context 'when entity does not exist' do
it 'is `false`' do
expect(model_klazz.with_id?(1)).to be false
Expand All @@ -84,4 +91,48 @@
end
end
end

describe '.with_primary_key' do
before { model_klazz.primary_key :peekay }
after { model_klazz.primary_key :email_address }

context 'when entity does not exist' do
it 'does not find a model' do
expect(model_klazz.with_primary_key(1)).to be_nil
end
end

context 'when entity exists' do
before do
model_klazz.create({ peekay: 1 })
model_klazz.create({ peekay: 2 })
end

it 'finds the first matching model' do
expect(model_klazz.with_primary_key(1)).to be_a TestingRecord::Model
end
end
end

describe '.with_primary_key?' do
before { model_klazz.primary_key :peekay }
after { model_klazz.primary_key :email_address }

context 'when entity does not exist' do
it 'is `false`' do
expect(model_klazz.with_primary_key?(1)).to be false
end
end

context 'when entity exists' do
before do
model_klazz.create({ peekay: 1 })
model_klazz.create({ peekay: 2 })
end

it 'is `true`' do
expect(model_klazz.with_primary_key?(1)).to be true
end
end
end
end
1 change: 1 addition & 0 deletions spec/testing_record/dsl/builder/helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
subject(:klazz) do
Class.new(TestingRecord::Model) do
include_helpers
primary_key :foo
end
end

Expand Down
53 changes: 51 additions & 2 deletions spec/testing_record/model_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# frozen_string_literal: true

RSpec.describe TestingRecord::Model do
let(:primary_model_entity) { FakeModel.create({ id: 1, foo: :foo, bar: :bar }) }
let(:secondary_model_entity) { FakeOtherModel.create({ id: 1, foo: :foo, bar: :bar }) }
let(:primary_model_entity) { FakeModel.create(model_attributes) }
let(:model_attributes) { { id: 1, foo: :foo, bar: :bar } }
let(:secondary_model_entity) { FakeOtherModel.create(model_attributes) }

before do
silence_logger!
Expand All @@ -14,10 +15,38 @@
context 'with caching enabled' do
before { FakeModel.caching :enabled }

it 'ensures that the primary key attribute was set in the attributes hash' do
expect(FakeModel).to receive(:ensure_primary_key_presence).with(model_attributes)

primary_model_entity
end

it 'does not permit an entity to be created without a primary key' do
expect { FakeModel.create({ foo: :no_primary_key }) }
.to raise_error(TestingRecord::Error::AttributeError)
.with_message('FakeModel entity has not been supplied with the primary key: id')
end

it 'does not permit an entity to be created with the same primary key' do
primary_model_entity

expect { FakeModel.create({ id: 1, other_key: :whatever }) }
.to raise_error(TestingRecord::Error::EntityError)
.with_message('FakeModel entity already exists with primary key: 1')
end

it 'generates a new instance of the entity' do
expect(primary_model_entity).to be_a FakeModel
end

it 'creates a model with all the supplied attributes as iVars' do
expect(primary_model_entity.instance_variables).to contain_exactly(:@attributes, :@id, :@foo, :@bar)
end

it 'creates a model with all the supplied attributes as attr_readers' do
expect(primary_model_entity).to respond_to(:attributes, :id, :foo, :bar)
end

it 'adds the entity to the cache' do
expect { primary_model_entity }.to change(FakeModel.all, :length).by(1)
end
Expand All @@ -32,9 +61,29 @@
context 'without caching enabled' do
before { FakeModel.caching :disabled }

it 'ensures that the primary key attribute was set in the attributes hash' do
expect(FakeModel).to receive(:ensure_primary_key_presence).with(model_attributes)

primary_model_entity
end

it 'does not permit an entity to be created without a primary key' do
expect { FakeModel.create({ foo: :no_primary_key }) }
.to raise_error(TestingRecord::Error::AttributeError)
.with_message('FakeModel entity has not been supplied with the primary key: id')
end

it 'generates a new instance of the entity' do
expect(primary_model_entity).to be_a FakeModel
end

it 'creates a model with all the supplied attributes as iVars' do
expect(primary_model_entity.instance_variables).to contain_exactly(:@attributes, :@id, :@foo, :@bar)
end

it 'creates a model with all the supplied attributes as attr_readers' do
expect(primary_model_entity).to respond_to(:attributes, :id, :foo, :bar)
end
end
end

Expand Down
Loading