diff --git a/backend/app/validators/cascade_validator.rb b/backend/app/validators/cascade_validator.rb new file mode 100644 index 0000000..a46139e --- /dev/null +++ b/backend/app/validators/cascade_validator.rb @@ -0,0 +1,76 @@ +# 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, :record, :attribute, :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 diff --git a/backend/spec/validators/cascade_validator_spec.rb b/backend/spec/validators/cascade_validator_spec.rb new file mode 100644 index 0000000..4bb248f --- /dev/null +++ b/backend/spec/validators/cascade_validator_spec.rb @@ -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