Skip to content

Commit

Permalink
Merge d90bf33 into 6d346a7
Browse files Browse the repository at this point in the history
  • Loading branch information
jonspalmer committed Nov 19, 2019
2 parents 6d346a7 + d90bf33 commit 35ff5fc
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 25 deletions.
18 changes: 15 additions & 3 deletions lib/active_model/validations/store_model_validator.rb
Expand Up @@ -22,16 +22,28 @@ def validate_each(record, attribute, value)

case record.type_for_attribute(attribute).type
when :json
strategy.call(attribute, record.errors, value.errors) if value.invalid?
call_json_strategy(attribute, record.errors, value)
when :array
record.errors.add(attribute, :invalid) if value.select(&:invalid?).present?
call_array_strategy(attribute, record.errors, value)
end
end

private

def call_json_strategy(attribute, record_errors, value)
strategy.call(attribute, record_errors, value.errors) if value.invalid?
end

def call_array_strategy(attribute, record_errors, value)
array_strategy.call(attribute, record_errors, value) if value.select(&:invalid?).present?
end

def strategy
StoreModel::CombineErrorsStrategies.configure(options)
@strategy ||= StoreModel::CombineErrorsStrategies.configure(options)
end

def array_strategy
@array_strategy ||= StoreModel::CombineErrorsStrategies.configure_array(options)
end
end
end
Expand Down
24 changes: 23 additions & 1 deletion lib/store_model/combine_errors_strategies.rb
Expand Up @@ -2,6 +2,7 @@

require "store_model/combine_errors_strategies/mark_invalid_error_strategy"
require "store_model/combine_errors_strategies/merge_error_strategy"
require "store_model/combine_errors_strategies/merge_array_error_strategy"

module StoreModel
# Module with built-in strategies for combining errors.
Expand All @@ -16,10 +17,31 @@ module CombineErrorsStrategies
def configure(options)
configured_strategy = options[:merge_errors] || StoreModel.config.merge_errors

get_configured_strategy(
configured_strategy,
StoreModel::CombineErrorsStrategies::MergeErrorStrategy
)
end

# Finds a array strategy based on +options+ and global config.
#
# @param options [Hash]
#
# @return [Object] strategy
def configure_array(options)
configured_strategy = options[:merge_array_errors] || StoreModel.config.merge_array_errors

get_configured_strategy(
configured_strategy,
StoreModel::CombineErrorsStrategies::MergeArrayErrorStrategy
)
end

def get_configured_strategy(configured_strategy, true_strategy_class)
if configured_strategy.respond_to?(:call)
configured_strategy
elsif configured_strategy == true
StoreModel::CombineErrorsStrategies::MergeErrorStrategy.new
true_strategy_class.new
elsif configured_strategy.nil?
StoreModel::CombineErrorsStrategies::MarkInvalidErrorStrategy.new
else
Expand Down
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module StoreModel
module CombineErrorsStrategies
# +MergeArrayErrorStrategy+ copies errors from the StoreModel::Model to the parent
# record attribute errors.
class MergeArrayErrorStrategy
# Merges errors on +attribute+ from the child model with parent errors.
#
# @param attribute [String] name of the validated attribute
# @param base_errors [ActiveModel::Errors] errors object of the parent record
# @param store_models [Array] an array or store_models that have been validated
def call(attribute, base_errors, store_models)
store_models.each_with_index do |store_model, index|
store_model.errors.full_messages.each do |full_message|
base_errors.add(attribute, :invalid, message: "[#{index}] #{full_message}")
end
end
end
end
end
end
Expand Up @@ -7,16 +7,16 @@ module CombineErrorsStrategies
class MergeErrorStrategy
# Merges errors on +attribute+ from the child model with parent errors.
#
# @param _attribute [String] name of the validated attribute
# @param attribute [String] name of the validated attribute
# @param base_errors [ActiveModel::Errors] errors object of the parent record
# @param store_model_errors [ActiveModel::Errors] errors object of the StoreModel::Model
# attribute
def call(_attribute, base_errors, store_model_errors)
def call(attribute, base_errors, store_model_errors)
if Rails::VERSION::MAJOR < 6 || Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR.zero?
base_errors.copy!(store_model_errors)
else
store_model_errors.errors.each do |error|
base_errors.add(:configuration, :invalid, message: error.full_message)
base_errors.add(attribute, :invalid, message: error.full_message)
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions lib/store_model/configuration.rb
Expand Up @@ -6,5 +6,9 @@ class Configuration
# Controls usage of MergeErrorStrategy
# @return [Boolean]
attr_accessor :merge_errors

# Controls usage of MergeArrayErrorStrategy
# @return [Boolean]
attr_accessor :merge_array_errors
end
end
142 changes: 124 additions & 18 deletions spec/active_model/validations/store_model_validator_spec.rb
Expand Up @@ -74,58 +74,164 @@
context "with store_model validator" do
let(:custom_product_class) do
build_custom_product_class do
attribute :configuration, Configuration.to_array_type
validates :configuration, store_model: true
attribute :configurations, Configuration.to_array_type
validates :configurations, store_model: true
end
end

context "when array is empty" do
let(:attributes) { { configurations: [] } }

it { is_expected.to be_valid }
end

context "when array member is invalid" do
let(:attributes) { { configuration: [Configuration.new, Configuration.new] } }
context "when all array members are valid" do
let(:attributes) do
{
configurations: [
Configuration.new(color: "red"),
Configuration.new(color: "blue")
]
}
end

it { is_expected.to be_valid }

it "returns correct error messages" do
expect(subject.errors.messages).to be_empty
expect(subject.errors.full_messages).to be_empty

expect(subject.configurations.first.errors.messages).to be_empty
expect(subject.configurations.first.errors.full_messages).to be_empty

expect(subject.configurations.second.errors.messages).to be_empty
expect(subject.configurations.second.errors.full_messages).to be_empty
end
end

context "when some array members are invalid" do
let(:attributes) do
{
configurations:
[
Configuration.new(color: "red"),
Configuration.new
]
}
end

it { is_expected.to be_invalid }

it "returns correct error messages" do
expect(subject.errors.messages).to eq(configurations: ["is invalid"])
expect(subject.errors.full_messages).to eq(["Configurations is invalid"])

expect(subject.configurations.second.errors.messages).to eq(color: ["can't be blank"])
expect(subject.configurations.second.errors.full_messages).to eq(["Color can't be blank"])
end
end

context "when more than one array member is invalid" do
let(:attributes) { { configurations: [Configuration.new, Configuration.new] } }

it { is_expected.to be_invalid }

it "returns correct error messages" do
expect(subject.errors.messages).to eq(configuration: ["is invalid"])
expect(subject.errors.full_messages).to eq(["Configuration is invalid"])
expect(subject.errors.messages).to eq(configurations: ["is invalid"])
expect(subject.errors.full_messages).to eq(["Configurations is invalid"])

expect(subject.configuration.first.errors.messages).to eq(color: ["can't be blank"])
expect(subject.configuration.first.errors.full_messages).to eq(["Color can't be blank"])
expect(subject.configurations.first.errors.messages).to eq(color: ["can't be blank"])
expect(subject.configurations.first.errors.full_messages).to eq(["Color can't be blank"])

expect(subject.configuration.second.errors.messages).to eq(color: ["can't be blank"])
expect(subject.configuration.second.errors.full_messages).to eq(["Color can't be blank"])
expect(subject.configurations.second.errors.messages).to eq(color: ["can't be blank"])
expect(subject.configurations.second.errors.full_messages).to eq(["Color can't be blank"])
end
end

context "when store_model value is nil" do
let(:attributes) { { configuration: nil } }
context "with merge_array_errors: true" do
let(:custom_product_class) do
build_custom_product_class do
attribute :configurations, Configuration.to_array_type
validates :configurations, store_model: { merge_array_errors: true }
end
end

context "when more than one array member is invalid" do
let(:attributes) do
{
configurations: [
Configuration.new(color: "red"),
Configuration.new
]
}
end

it { is_expected.to be_invalid }

it "returns correct error messages" do
expect(subject.errors.messages).to eq(
configurations: [
"[1] Color can't be blank"
]
)
expect(subject.errors.full_messages).to eq(
[
"Configurations [1] Color can't be blank"
]
)
end
end

context "when more than one array member is invalid" do
let(:attributes) { { configurations: [Configuration.new, Configuration.new] } }

it { is_expected.to be_invalid }

it "returns correct error messages" do
expect(subject.errors.messages).to eq(
configurations: [
"[0] Color can't be blank",
"[1] Color can't be blank"
]
)
expect(subject.errors.full_messages).to eq(
[
"Configurations [0] Color can't be blank",
"Configurations [1] Color can't be blank"
]
)
end
end
end

context "when array is nil" do
let(:attributes) { { configurations: nil } }

it { is_expected.to be_invalid }

it "is invalid because configuration is blank" do
expect(subject.errors.messages).to eq(configuration: ["can't be blank"])
expect(subject.errors.full_messages).to eq(["Configuration can't be blank"])
expect(subject.errors.messages).to eq(configurations: ["can't be blank"])
expect(subject.errors.full_messages).to eq(["Configurations can't be blank"])
end
end
end

context "with allow_nil: true" do
let(:custom_product_class) do
build_custom_product_class do
attribute :configuration, Configuration.to_array_type
validates :configuration, allow_nil: true, store_model: true
attribute :configurations, Configuration.to_array_type
validates :configurations, allow_nil: true, store_model: true
end
end

context "when array is empty" do
let(:attributes) { { configurations: [] } }

it { is_expected.to be_valid }
end

context "when store_model value is nil" do
let(:attributes) { { configuration: nil } }
context "when array is nil" do
let(:attributes) { { configurations: nil } }

it { is_expected.to be_valid }
end
Expand Down
@@ -0,0 +1,34 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe StoreModel::CombineErrorsStrategies::MergeArrayErrorStrategy do
let(:custom_product_class) do
build_custom_product_class do
attribute :configurations, Configuration.to_array_type
validates :configurations, store_model: true
end
end

let(:record) do
product = custom_product_class.new(
configurations:
[
Configuration.new(color: "red"),
Configuration.new
]
)
product.configurations.each(&:validate)
product
end

it "adds message that associated object is invalid" do
described_class.new.call(:configurations, record.errors, record.configurations)

expect(record.errors.messages).to eq(configurations: ["[1] Color can't be blank"])
expect(record.errors.full_messages).to eq(["Configurations [1] Color can't be blank"])

expect(record.configurations.second.errors.messages).to eq(color: ["can't be blank"])
expect(record.configurations.second.errors.full_messages).to eq(["Color can't be blank"])
end
end
39 changes: 39 additions & 0 deletions spec/store_model/combine_error_strategies_spec.rb
Expand Up @@ -41,4 +41,43 @@
it { is_expected.to eq(LAMBDA_FHTAGN_STRATEGY) }
end
end

describe ".configure_array" do
LAMBDA_FHTAGN_STRATEGY =
lambda do |attribute, base_errors, _store_model|
base_errors.add(attribute, "cthulhu fhtagn")
end

subject { described_class.configure_array(options) }

context "when empty hash is passed" do
let(:options) { {} }

it { is_expected.to be_a(StoreModel::CombineErrorsStrategies::MarkInvalidErrorStrategy) }
end

context "when true is passed" do
let(:options) { { merge_array_errors: true } }

it { is_expected.to be_a(StoreModel::CombineErrorsStrategies::MergeArrayErrorStrategy) }
end

context "when custom strategy class name is passed" do
let(:options) { { merge_array_errors: :fhtagn_error_strategy } }

it { is_expected.to be_a(FhtagnErrorStrategy) }
end

context "when instance of custom strategy class is passed" do
let(:options) { { merge_array_errors: FhtagnErrorStrategy.new } }

it { is_expected.to be_a(FhtagnErrorStrategy) }
end

context "when labmda is passed" do
let(:options) { { merge_array_errors: LAMBDA_FHTAGN_STRATEGY } }

it { is_expected.to eq(LAMBDA_FHTAGN_STRATEGY) }
end
end
end

0 comments on commit 35ff5fc

Please sign in to comment.