Skip to content

Commit

Permalink
Add nested form errors support
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezhny committed Mar 23, 2020
1 parent 9944a40 commit ff2cfe6
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 55 deletions.
1 change: 1 addition & 0 deletions lib/rectify.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require "active_record"

require "rectify/version"
require "rectify/nested_form_helpers"
require "rectify/form"
require "rectify/form_attribute"
require "rectify/format_attributes_hash"
Expand Down
80 changes: 50 additions & 30 deletions lib/rectify/form.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module Rectify
class Form
class Form # rubocop:disable Metrics/ClassLength
include Virtus.model
include ActiveModel::Validations
include NestedFormHelpers

attr_reader :context

Expand Down Expand Up @@ -69,14 +70,14 @@ def persisted?
def valid?(options = {})
before_validation

options = {} if options.blank?
context = options[:context]
validations = [super(context)]
options = {} if options.blank?
context = options[:context]

validations << form_attributes_valid? unless options[:exclude_nested]
validations << array_attributes_valid? unless options[:exclude_arrays]
super(context)
validate_nested_attributes(options) unless options[:exclude_nested]
validate_nested_array_attributes(options) unless options[:exclude_arrays]

validations.all?
errors.empty?
end

def invalid?(options = {})
Expand Down Expand Up @@ -121,41 +122,60 @@ def with_context(new_context)
new_context
end

attributes_that_respond_to(:with_context)
.each { |f| f.with_context(context) }

array_attributes_that_respond_to(:with_context)
.each { |f| f.with_context(context) }
nested_values.each_value { |form| form.with_context(context) }
nested_array_values.each_value do |forms|
forms.each { |form| form.with_context(context) }
end

self
end

private

def form_attributes_valid?
attributes_that_respond_to(:valid?)
.map(&:valid?)
.all?
def validate_nested_attributes(options)
nested_values.each do |attribute, form|
next if form.valid?(options)

merge_error_messages(attribute, form, options)
end
end

def array_attributes_valid?
array_attributes_that_respond_to(:valid?)
.map(&:valid?)
.all?
def validate_nested_array_attributes(options)
nested_array_values.each do |attribute, forms|
forms.each_with_index do |form, index|
next if form.valid?(options)

merge_error_messages(attribute, form, options, index)
end
end
end

def attributes_that_respond_to(message)
attributes
.each_value
.select { |f| f.respond_to?(message) }
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
def merge_error_messages(attribute, form, options, index = nil)
indexed_attribute = !index.nil? && options[:index_errors]
form_errors = form.errors

form_errors.details.each do |nested_attribute, details|
error_attribute =
normalize_error_attribute(indexed_attribute, attribute, index, nested_attribute).to_sym

form_errors[nested_attribute].each do |message|
errors[error_attribute].push(message).uniq!
end

details.each do |error|
errors.details[error_attribute].push(error).uniq!
end
end
end
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize

def array_attributes_that_respond_to(message)
attributes
.each_value
.select { |a| a.is_a?(Array) }
.flatten
.select { |f| f.respond_to?(message) }
def normalize_error_attribute(indexed_attribute, attribute, index, nested_attribute)
if indexed_attribute
"#{attribute}[#{index}].#{nested_attribute}"
else
"#{attribute}.#{nested_attribute}"
end
end
end
end
16 changes: 5 additions & 11 deletions lib/rectify/format_attributes_hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

module Rectify
class FormatAttributesHash
include NestedFormHelpers

def initialize(attribute_set)
@attribute_set = attribute_set
end

def format(params)
convert_indexed_hashes_to_arrays_for_nested_form_attributes(params)
convert_indexed_hashes_to_arrays_for_nested_attributes(params)
convert_indexed_hashes_to_arrays_for_array_attributes(params)
convert_hash_keys(params)
end
Expand All @@ -29,8 +31,8 @@ def convert_indexed_hashes_to_arrays_for_array_attributes(attributes_hash)
end
end

def convert_indexed_hashes_to_arrays_for_nested_form_attributes(attributes_hash)
nested_form_attributes.each do |nested_form_attribute|
def convert_indexed_hashes_to_arrays_for_nested_attributes(attributes_hash)
nested_attributes.each do |nested_form_attribute|
name = nested_form_attribute.name
attribute = attributes_hash[name]
next unless attribute.is_a?(Hash)
Expand All @@ -48,14 +50,6 @@ def transform_values_for_type(values, element_type)
end
end

def nested_form_attributes
attribute_set.select { |attribute| attribute.primitive < ::Rectify::Form }
end

def array_attributes
attribute_set.select { |attribute| attribute.primitive == Array }
end

def convert_hash_keys(value)
case value
when Array
Expand Down
43 changes: 43 additions & 0 deletions lib/rectify/nested_form_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module Rectify
module NestedFormHelpers
private

def nested_values
nested_attributes.each_with_object({}) do |attribute, attributes|
attribute_name = attribute.name
next unless (value = __send__(attribute_name))

attributes[attribute_name] = value
end
end

def nested_array_values
nested_array_attributes.each_with_object({}) do |attribute, attributes|
attribute_name = attribute.name
next if (value = __send__(attribute_name)).empty?

attributes[attribute_name] = value
end
end

def nested_attributes
@nested_attributes ||= attribute_set.select do |attribute|
attribute.primitive < ::Rectify::Form
end
end

def array_attributes
@array_attributes ||= attribute_set.select do |attribute|
attribute.primitive.eql?(Array)
end
end

def nested_array_attributes
@nested_array_attributes ||= array_attributes.select do |attribute|
attribute.member_type.primitive < ::Rectify::Form
end
end
end
end
2 changes: 1 addition & 1 deletion spec/lib/rectify/command_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def something_private
@private = true
end

private :something_private
private :something_private # rubocop:disable Style/AccessModifierDeclarations

it "calls public methods on the caller" do
@success = false
Expand Down
Loading

0 comments on commit ff2cfe6

Please sign in to comment.