Skip to content

Commit

Permalink
validation: fix active_model should not fail on undeclared attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
alessandro-fazzi committed Aug 13, 2024
1 parent 96b6164 commit 84a329e
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 7 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ evaluated in the context of a class. I advise to keep things simple; `attribute`
and `validates` should be all you need in order to do basic validation.
`contract` will dynamically create a class, will create the object initializing
it with the context's key/value pairs as attributes, then it will be validated.
it with the context's key/value pairs also declared as attributes in the contract
(in composition a single interactor can't be responsible to validate the whole
context, but just the data it needs), then it will be validated.
If the context is invalid it's errors messages will be copied into context's
`errors`, thus making it a failed one.
Expand Down
16 changes: 13 additions & 3 deletions lib/i_do_not_need_interactor/contract/active_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module Contract
module ActiveModel # rubocop:disable Style/Documentation
def self.included(descendant) # rubocop:disable Metrics/MethodLength
class << descendant
def contract(&block)
def contract(&block) # rubocop:disable Metrics/MethodLength
caller = self
@contract = Class.new do
set_temporary_name("#{caller.inspect.downcase}::contract")
Expand All @@ -18,15 +18,25 @@ def contract(&block)
def self.model_name
::ActiveModel::Name.new(self, nil, "temp")
end

def self.build_for_interactor_validation(ctx)
contract = new
declared_attributes = contract.attribute_names.each(&:to_sym)
context_attributes_to_validate = ctx.slice(declared_attributes)
contract.assign_attributes(context_attributes_to_validate)
contract
end
end
@contract.class_exec(&block)
end
end
end

def validate(ctx)
self.class.instance_variable_get(:@contract).new(**ctx).validate!
rescue ::ActiveModel::ValidationError, ::ActiveModel::UnknownAttributeError => e
self.class.instance_variable_get(:@contract)
.build_for_interactor_validation(ctx)
.validate!
rescue ::ActiveModel::ValidationError => e
ctx.errors << e.message
end
end
Expand Down
14 changes: 11 additions & 3 deletions test/test_i_do_not_need_interactor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,11 @@ def test_contract_active_model_integration
assert_equal ["Validation failed: Test can't be blank"], outcome.errors
end

def test_contract_active_model_integration_with_unknown_attribute
outcome = InteractorWithActiveModelContract.call(foo: 1)
def test_contract_active_model_integration_validates_only_declared_attributes
undeclared_attribute = { foo: 1 }
outcome = InteractorWithActiveModelContract.call(undeclared_attribute)

assert_equal ["unknown attribute 'foo' for interactorwithactivemodelcontract::contract."], outcome.errors
assert_equal ["Validation failed: Test can't be blank"], outcome.errors
end

def test_contract_dry_validation_integration
Expand All @@ -146,6 +147,13 @@ def test_contract_dry_validation_integration
assert_equal [{ test: ["is missing"] }], outcome.errors
end

def test_contract_dry_validation_integration_validates_only_declared_attributes
undeclared_attribute = { foo: 1 }
outcome = InteractorWithDryValidationContract.call(undeclared_attribute)

assert_equal [{ test: ["is missing"] }], outcome.errors
end

def test_manual_validation
outcome = InteractorWithManualValidation.call

Expand Down

0 comments on commit 84a329e

Please sign in to comment.