Skip to content

Commit

Permalink
子モデルのバリデーションエラーをcascadeするバリデータを追加する
Browse files Browse the repository at this point in the history
  • Loading branch information
kakeru-one committed Aug 18, 2024
1 parent 14b8e38 commit 71bd401
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 0 deletions.
79 changes: 79 additions & 0 deletions backend/app/validators/cascade_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

class CascadeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
Validator
.call(parent: self, record: record, attribute: attribute, value: value)
end

class Validator
private_class_method :new

class << self
def call(parent:, record:, attribute:, value:)
new(
parent: parent, record: record, attribute: attribute, value: value
).call
end
end

def initialize(parent:, record:, attribute:, value:)
@parent = parent
@record = record
@attribute = attribute
@value = value
end

def call
return if value.nil? || valid?

add_error!
end

private

attr_reader :parent
attr_reader :record
attr_reader :attribute
attr_reader :value

delegate :options, to: :parent

def associations
@associations ||= \
if value.class.include?(Enumerable) && !value.is_a?(Struct)
value
else
[value]
end.select { _1.respond_to?(:valid?) }
end

def valid?
invalid_associations.empty?
end

def invalid_associations
@invalid_associations ||= \
associations.select { _1.invalid?(context) }
end

def invalid_message
invalid_associations
.map { _1.errors.full_messages.to_sentence }
.to_sentence
end

def context = options.try(:[], :context)

def add_error!
record
.errors
.add(
attribute,
:invalid_association,
association: invalid_message
)
end
end
private_constant :Validator
end
109 changes: 109 additions & 0 deletions backend/spec/validators/cascade_validator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# frozen_string_literal: true

RSpec.describe CascadeValidator do
let(:child_klass) do
child_test_class = Class.new do
attr_accessor :name, :age

include ActiveModel::Validations
validates :name, presence: true
validates :age, presence: true, inclusion: { in: 18..65 }
end

stub_const('Child', child_test_class)
end

context '対象が配列ではないとき' do
let(:klass) do
test_class = Class.new do
attr_accessor :child

include ActiveModel::Validations

validates :child, cascade: true
end

stub_const('TestClass', test_class)
end

let(:model) do
klass.new.tap do |instance|
instance.child = child_klass.new
end
end

it 'validatesに渡されたメソッドに対してvalid?による検証を行う' do
aggregate_failures do
expect(model.valid?).to be false
model.child.name = 'hoge'
model.child.age = 18
expect(model.valid?).to be true
model.child.age = 11
expect(model.valid?).to be false
end
end
end

context 'valueがnilを返却する時' do
let(:klass) do
test_class = Class.new do
attr_accessor :child

include ActiveModel::Validations

validates :child, cascade: true
end

stub_const('TestClass', test_class)
end

let(:model) do
klass.new.tap do |instance|
instance.child = child_klass.new
end
end

before { model.child = nil }

it 'cascadeは実行されないため評価はスキップされる。' do
expect(model.valid?).to be true
end
end

context '対象が配列のとき' do
let(:klass) do
children_test_class = Class.new do
attr_accessor :children

include ActiveModel::Validations

validates :children, cascade: true
end

stub_const('TestClass', children_test_class)
end

let(:model) do
klass.new.tap do |instance|
instance.children = Array.new(3) { child_klass.new }
end
end

it 'validatesに渡されたメソッドに対してvalid?による検証を行う' do
aggregate_failures do
expect(model.valid?).to be false
model.children.first.name = 'hoge'
model.children.first.age = 18
expect(model.valid?).to be false
model.children.second.name = 'hoge'
model.children.second.age = 18
expect(model.valid?).to be false
model.children.third.name = 'hoge'
model.children.third.age = 18
expect(model.valid?).to be true
model.children.second.age = 11
expect(model.valid?).to be false
end
end
end
end

0 comments on commit 71bd401

Please sign in to comment.