Skip to content

Commit

Permalink
[webui] Make PrettyNestedErrors work for n-level associations.
Browse files Browse the repository at this point in the history
Before PrettyNestedErrors only works for nested associations which are
2 levels deep, now it can work for n-levels deep.
  • Loading branch information
Evan Rolfe committed Feb 5, 2018
1 parent 5652298 commit f6dc7f7
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 58 deletions.
83 changes: 27 additions & 56 deletions src/api/lib/pretty_nested_errors/key_and_messages_parser.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
module PrettyNestedErrors
class KeyAndMessagesParser
DOUBLE_NESTED_ERROR_REGEX = /(\w+)\[(\d+)\]\.(\w+)\[(\d+)\]\.(\w+)/
NESTED_ERROR_REGEX = /(\w+)\[(\d+)\]\.(\w+)/
NESTED_ERROR_REGEX = /(\w+\[\d+\]\.)+(\w+)/
HAS_ONE_ERROR_REGEX = /(\w+)\.(\w+)/

def initialize(base_model, key, messages, nested_error_messages, nested_error_groupings)
Expand All @@ -12,29 +11,25 @@ def initialize(base_model, key, messages, nested_error_messages, nested_error_gr
@nested_error_groupings = nested_error_groupings
end

# This method hard codes the behaviour for validations errors on 4 different types of associations:
# 1. errors on a double nested has_many association
# 2. errors on a nested has_many association
# 3. errors on a has_one associations
# 4. errors on the base model
# TODO: Make it so that the method uses regex to recursivley parse the validation key (i.e.
# "package_groups[0].packages[0].name") so that it can handle n-level nested assoications
# Parse the errors on the model into a nested hash based with keys based on the lambda
# set in the base model for each association
def parse
# Matches an error on a has_many nested resource of a has_many nested resource
# like: {:"package_groups[0].packages[0].name"=>["can't be blank"]}
if @key.match(DOUBLE_NESTED_ERROR_REGEX)
parse_error_message_for_double_nested
if @key.match(NESTED_ERROR_REGEX)
parsed_key = @key.match(NESTED_ERROR_REGEX)

# Matches an error on a has_many nested resource
# like {:"repositories[0].source_path"=>["can't be blank"]}
elsif @key.match(NESTED_ERROR_REGEX)
parse_error_message_for_nested
association_invalid_column = parsed_key.to_a.last
nested_resources_and_indexes = @key.to_s.scan(/(\w+)\[(\d+)\]\./)

parse_error_message_for_nested(nested_resources_and_indexes, association_invalid_column)

# Matches an error on a has_one nested resource like: {:"preference.type_containerconfig_tag"=>["can't be blank"]}
elsif @key.match(HAS_ONE_ERROR_REGEX)
parse_error_message_for_has_one
parsed_key = @key.match(HAS_ONE_ERROR_REGEX)

association_name = parsed_key[1].to_sym
association_invalid_column = parsed_key[2].to_sym

parse_error_message_for_has_one(association_name, association_invalid_column)

# Matches an error on the base resource like: {:"name"=>["can't be blank"]}
else
parse_error_message_for_base
end
Expand All @@ -44,50 +39,26 @@ def parse

private

def parse_error_message_for_double_nested
parsed_key = @key.match(DOUBLE_NESTED_ERROR_REGEX)

association_name = parsed_key[1].to_sym
association_index = parsed_key[2].to_i
sub_association_name = parsed_key[3].to_sym
sub_association_index = parsed_key[4].to_i
association_invalid_column = parsed_key[5]

# Find the association record
record = @base_model.send(association_name)[association_index].send(sub_association_name)[sub_association_index]

# Call the lambda method to determine the grouping
group_by = @nested_error_groupings["#{association_name}_#{sub_association_name}".to_sym].call(record)

@nested_error_messages[group_by] ||= []
@nested_error_messages[group_by] +=
@messages.map { |message| @base_model.errors.full_message(association_invalid_column, message) }
end

def parse_error_message_for_nested
parsed_key = @key.match(NESTED_ERROR_REGEX)

association_name = parsed_key[1].to_sym
association_index = parsed_key[2].to_i
association_invalid_column = parsed_key[3]
def parse_error_message_for_nested(nested_resources_and_indexes, association_invalid_column)
record = @base_model
group_by_key = ''
i = 1

# Find the association record
record = @base_model.send(association_name)[association_index]
nested_resources_and_indexes.each do |association_name, association_index|
record = record.send(association_name.to_sym)[association_index.to_i]
group_by_key += association_name
group_by_key += '_' unless i == nested_resources_and_indexes.count
i += 1
end

# Call the lambda method to determine the grouping
group_by = @nested_error_groupings[association_name].call(record)
group_by = @nested_error_groupings[group_by_key.to_sym].call(record)

@nested_error_messages[group_by] ||= []
@nested_error_messages[group_by] +=
@messages.map { |message| @base_model.errors.full_message(association_invalid_column, message) }
end

def parse_error_message_for_has_one
parsed_key = @key.match(HAS_ONE_ERROR_REGEX)

association_name = parsed_key[1].to_sym
association_invalid_column = parsed_key[2].to_sym

def parse_error_message_for_has_one(association_name, association_invalid_column)
# Call the lambda method to determine the grouping
group_by = @nested_error_groupings[association_name].call

Expand Down
81 changes: 81 additions & 0 deletions src/api/spec/lib/pretty_nested_errors_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
require 'rails_helper'
require 'pretty_nested_errors'

class Bicycle < ApplicationRecord
include PrettyNestedErrors

has_many :wheels, index_errors: true
accepts_nested_attributes_for :wheels
nest_errors_for :wheels, by: ->(wheel) { "Wheel: #{wheel.name}" }
nest_errors_for :wheels_spokes, by: ->(spoke) { "Spoke ##{spoke.number}" }
end

class Wheel < ApplicationRecord
belongs_to :bicycle
has_many :spokes, index_errors: true
accepts_nested_attributes_for :spokes

validates :name, presence: true
end

class Spoke < ApplicationRecord
validates :tension, presence: true
validates :number, presence: true

belongs_to :wheel
end

RSpec.describe PrettyNestedErrors do
let(:bicycle) do
Bicycle.new(
name: 'My Favorite Bicycle',
wheels_attributes: [
{ name: nil },
{
name: 'Rear wheel',
spokes_attributes: [
{ tension: nil, number: 1 },
{ tension: 1.34, number: 2 },
{ tension: 1.34, number: 3 },
{ tension: 1.34, number: 4 },
{ tension: 1.34, number: 5 }
]
}
]
)
end

before do
ActiveRecord::Base.connection.create_table(:bicycles) do |t|
t.string :name
end

ActiveRecord::Base.connection.create_table(:wheels) do |t|
t.string :name
t.integer :bicycle_id
end

ActiveRecord::Base.connection.create_table(:spokes) do |t|
t.float :tension
t.integer :number
t.integer :wheel_id
end
end

after do
ActiveRecord::Base.connection.drop_table(:spokes)
ActiveRecord::Base.connection.drop_table(:wheels)
ActiveRecord::Base.connection.drop_table(:bicycles)
end

subject! { bicycle.valid? }

it { is_expected.to be_falsey }

it 'generates a nested error hash' do
expect(bicycle.nested_error_messages).to eq(
'Wheel: ' => ["Name can't be blank"],
'Spoke #1' => ["Tension can't be blank"]
)
end
end
8 changes: 6 additions & 2 deletions src/api/spec/models/kiwi/image_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -498,16 +498,20 @@
'Order is not a number',
'Replaceable has to be a boolean'
],
'Package: ' => [
"Name can't be blank"
],
'Image Errors:' => [
'Multiple package groups with same type are not allowed'
"Name can't be blank"
]
}
end

before do
kiwi_image.name = nil
kiwi_image.repositories << Kiwi::Repository.new(alias: 'example')
kiwi_image.package_groups << create(:kiwi_package_group_non_empty, kiwi_type: :image)
kiwi_image.package_groups << create(:kiwi_package_group_non_empty, kiwi_type: :image)
kiwi_image.package_groups[0].packages[0].name = nil
kiwi_image.valid?
end

Expand Down

0 comments on commit f6dc7f7

Please sign in to comment.