Last active
August 18, 2024 20:58
-
-
Save elias19r/2cd63bfbf21606cba5e5d453709931f4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
class Result | |
extend ActiveModel::Naming # Required dependency for ActiveModel::Errors | |
GENERIC_ERROR = :generic_error | |
attr_reader :errors, :data | |
alias attributes data | |
def self.success(data = {}) | |
new(nil, data) | |
end | |
def self.failure(error_values = GENERIC_ERROR, data = {}) | |
# Ensure errors presence when creating a Result by calling .failure | |
error_values = error_values.compact_blank if error_values.is_a?(Array) || error_values.is_a?(Hash) | |
error_values = GENERIC_ERROR if error_values.blank? | |
new(error_values, data) | |
end | |
def initialize(error_values = {}, data = {}) | |
@errors = ActiveModel::Errors.new(self) | |
@data = data.with_indifferent_access | |
add_errors(error_values) | |
end | |
def success? | |
errors.empty? | |
end | |
def failure? | |
!success? | |
end | |
def add_errors(error_values) | |
case error_values | |
when Hash | |
add_errors_from_hash(error_values) | |
when Array | |
add_errors_from_array(:base, error_values) | |
when Symbol, String, Numeric | |
add_errors_from_literal(:base, error_values) | |
when ActiveModel::Errors | |
add_errors_from_active_model_errors(error_values) | |
when true | |
add_errors_from_literal(:base, GENERIC_ERROR) | |
when nil, false | |
# Nothing | |
else | |
raise "Could not build_active_model_errors for Result: invalid error_values.class: #{error_values.class.name}" | |
end | |
end | |
# The following methods are needed for ActiveModel::Errors | |
def read_attribute_for_validation(attr) | |
attr | |
end | |
def self.human_attribute_name(attr, _options = {}) | |
attr | |
end | |
def self.lookup_ancestors | |
[self] | |
end | |
private | |
def add_errors_from_hash(hash) | |
hash.each do |key, value| | |
if value.is_a?(Array) | |
add_errors_from_array(key, value) | |
else | |
add_errors_from_literal(key, value) | |
end | |
end | |
end | |
def add_errors_from_array(key, array) | |
array.each do |value| | |
add_errors_from_literal(key, value) | |
end | |
end | |
def add_errors_from_literal(key, value) | |
return if value.blank? | |
case value | |
when Symbol | |
errors.add(key.to_sym, value) | |
when String, Numeric | |
errors.add(key.to_sym, value.to_s) | |
end | |
end | |
def add_errors_from_active_model_errors(active_model_errors) | |
errors.merge!(active_model_errors) | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
en: | |
errors: | |
messages: | |
generic_error: "An error has occurred" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
require 'rails_helper' | |
RSpec.describe Result do | |
describe '#initialize' do | |
it 'has default values for errors and data' do | |
result = described_class.new | |
expect(result.errors).to be_empty | |
expect(result.errors).to be_a(ActiveModel::Errors) | |
expect(result.data).to be_empty | |
expect(result.data).to be_a(Hash) | |
expect(result.data.object_id).to eq(result.attributes.object_id) | |
end | |
describe 'setting errors' do | |
it 'sets errors from Hash with Symbol, String, Number or Array values, and skips blanks' do | |
result = described_class.new( | |
{ | |
error_code_1: 'error message', | |
error_code_2: :invalid, | |
error_code_3: 123456, | |
error_code_4: ' ', | |
error_code_5: false, | |
error_code_6: nil, | |
error_code_7: [], | |
error_code_8: ['error message', :invalid, 123456, ' ', false, nil] | |
} | |
) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ | |
error_code_1: ['error message'], | |
error_code_2: ['is invalid'], | |
error_code_3: ['123456'], | |
error_code_8: ['error message', 'is invalid', '123456'], | |
} | |
) | |
expect(result.errors.of_kind?(:error_code_1, 'error message')).to be(true) | |
expect(result.errors.of_kind?(:error_code_2, :invalid)).to be(true) | |
expect(result.errors.of_kind?(:error_code_3, '123456')).to be(true) | |
expect(result.errors.of_kind?(:error_code_8, 'error message')).to be(true) | |
expect(result.errors.of_kind?(:error_code_8, :invalid)).to be(true) | |
expect(result.errors.of_kind?(:error_code_8, '123456')).to be(true) | |
end | |
it 'sets errors from Array, and skips blanks' do | |
result = described_class.new( | |
['error message', :invalid, 123456, ' ', false, nil] | |
) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ base: ['error message', 'is invalid', '123456'] }, | |
) | |
expect(result.errors.of_kind?(:base, 'error message')).to be(true) | |
expect(result.errors.of_kind?(:base, :invalid)).to be(true) | |
expect(result.errors.of_kind?(:base, '123456')).to be(true) | |
end | |
it 'sets errors from String, Symbol, Numeric, and skips blanks' do | |
[ | |
['error message', 'error message', { base: ['error message'] }], | |
[:invalid, :invalid, { base: ['is invalid'] }], | |
[123456, '123456', { base: ['123456'] }], | |
[1234.56, '1234.56', { base: ['1234.56'] }], | |
].each do |error_value, expected_type, expected_messages| | |
result = described_class.new(error_value) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq(expected_messages) | |
expect(result.errors.of_kind?(:base, expected_type)).to be(true) | |
end | |
[' ', '', :'', false, nil].each do |blank_error_value| | |
result = described_class.new(blank_error_value) | |
expect(result.errors).to be_empty | |
end | |
end | |
it 'sets errors from ActiveModel::Errors' do | |
active_model_errors = ActiveModel::Errors.new(generic_model_class.new) | |
active_model_errors.add(:name, 'error message') | |
active_model_errors.add(:description, :invalid) | |
result = described_class.new(active_model_errors) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ | |
name: ['error message'], | |
description: ['is invalid'], | |
} | |
) | |
expect(result.errors.of_kind?(:name, 'error message')).to be(true) | |
expect(result.errors.of_kind?(:description, :invalid)).to be(true) | |
end | |
it 'sets errors from true with a generic message' do | |
result = described_class.new(true) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq({ base: ['An error has occurred'] }) | |
expect(result.errors.of_kind?(:base, :generic_error)).to be(true) | |
end | |
it 'does not set errors from nil, false' do | |
[nil, false].each do |nil_or_false_error_value| | |
result = described_class.new(nil_or_false_error_value) | |
expect(result.errors).to be_empty | |
end | |
end | |
it 'raises an exception when errors class is invalid' do | |
expect do | |
described_class.new(Time.current) | |
end.to raise_exception( | |
/Could not build_active_model_errors for Result: invalid error_values\.class/ | |
) | |
end | |
end | |
describe 'setting data' do | |
it 'uses with_indifferent_access' do | |
result = described_class.new( | |
nil, | |
{ | |
attr: 'value', | |
'another attr' => 'another value', | |
42 => 'forty two' | |
} | |
) | |
expect(result.data['attr']).to eq('value') | |
expect(result.data[:'another attr']).to eq('another value') | |
expect(result.data[42]).to eq('forty two') | |
expect(result.data).to eq( | |
{ | |
'attr' => 'value', | |
'another attr' => 'another value', | |
42 => 'forty two' | |
} | |
) | |
end | |
end | |
end | |
describe '#success?' do | |
it 'returns true when #errors is empty' do | |
result = described_class.new(nil) | |
expect(result.errors).to be_empty | |
expect(result.success?).to eq(true) | |
end | |
it 'returns false when #errors is not empty' do | |
result = described_class.new('error message') | |
expect(result.errors).not_to be_empty | |
expect(result.success?).to eq(false) | |
end | |
end | |
describe '#failure?' do | |
it 'returns true when #errors is not empty' do | |
result = described_class.new('error message') | |
expect(result.errors).not_to be_empty | |
expect(result.failure?).to eq(true) | |
end | |
it 'returns false when #errors is empty' do | |
result = described_class.new(nil) | |
expect(result.errors).to be_empty | |
expect(result.failure?).to eq(false) | |
end | |
end | |
describe '#add_errors' do | |
it 'adds to errors from Hash with Symbol, String, Number or Array values, and skips blanks' do | |
result = described_class.new('existing error message') | |
result.add_errors( | |
{ | |
error_code_1: 'error message', | |
error_code_2: :invalid, | |
error_code_3: 123456, | |
error_code_4: ' ', | |
error_code_5: false, | |
error_code_6: nil, | |
error_code_7: [], | |
error_code_8: ['error message', :invalid, 123456, ' ', false, nil] | |
} | |
) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ | |
base: ['existing error message'], | |
error_code_1: ['error message'], | |
error_code_2: ['is invalid'], | |
error_code_3: ['123456'], | |
error_code_8: ['error message', 'is invalid', '123456'], | |
} | |
) | |
expect(result.errors.of_kind?(:base, 'existing error message')).to be(true) | |
expect(result.errors.of_kind?(:error_code_1, 'error message')).to be(true) | |
expect(result.errors.of_kind?(:error_code_2, :invalid)).to be(true) | |
expect(result.errors.of_kind?(:error_code_3, '123456')).to be(true) | |
expect(result.errors.of_kind?(:error_code_8, 'error message')).to be(true) | |
expect(result.errors.of_kind?(:error_code_8, :invalid)).to be(true) | |
expect(result.errors.of_kind?(:error_code_8, '123456')).to be(true) | |
end | |
it 'adds to errors from Array, and skips blanks' do | |
result = described_class.new('existing error message') | |
result.add_errors( | |
['error message', :invalid, 123456, ' ', false, nil] | |
) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ base: ['existing error message', 'error message', 'is invalid', '123456'] }, | |
) | |
expect(result.errors.of_kind?(:base, 'existing error message')).to be(true) | |
expect(result.errors.of_kind?(:base, 'error message')).to be(true) | |
expect(result.errors.of_kind?(:base, :invalid)).to be(true) | |
expect(result.errors.of_kind?(:base, '123456')).to be(true) | |
end | |
it 'adds to errors from String, Symbol, Numeric, and skips blanks' do | |
[ | |
['error message', ['existing error message', 'error message'], { base: ['existing error message', 'error message'] }], | |
[:invalid, ['existing error message', :invalid], { base: ['existing error message', 'is invalid'] }], | |
[123456, ['existing error message', '123456'], { base: ['existing error message', '123456'] }], | |
[1234.56, ['existing error message', '1234.56'], { base: ['existing error message', '1234.56'] }], | |
].each do |error_value, expected_error_types, expected_error_messages| | |
result = described_class.new('existing error message') | |
result.add_errors(error_value) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq(expected_error_messages) | |
expected_error_types.each do |expected_type| | |
expect(result.errors.of_kind?(:base, expected_type)).to be(true) | |
end | |
end | |
[' ', '', :'', false, nil].each do |blank_error_value| | |
result = described_class.new('existing error message') | |
result.add_errors(blank_error_value) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ base: ['existing error message'] } | |
) | |
expect(result.errors.of_kind?(:base, 'existing error message')).to be(true) | |
end | |
end | |
it 'adds to errors from ActiveModel::Errors' do | |
active_model_errors = ActiveModel::Errors.new(generic_model_class.new) | |
active_model_errors.add(:name, 'error message') | |
active_model_errors.add(:description, :invalid) | |
result = described_class.new('existing error message') | |
result.add_errors(active_model_errors) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ | |
base: ['existing error message'], | |
name: ['error message'], | |
description: ['is invalid'], | |
} | |
) | |
expect(result.errors.of_kind?(:base, 'existing error message')).to be(true) | |
expect(result.errors.of_kind?(:name, 'error message')).to be(true) | |
expect(result.errors.of_kind?(:description, :invalid)).to be(true) | |
end | |
it 'adds to errors from true with a generic message' do | |
result = described_class.new('existing error message') | |
result.add_errors(true) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ base: ['existing error message', 'An error has occurred'] } | |
) | |
expect(result.errors.of_kind?(:base, 'existing error message')).to be(true) | |
expect(result.errors.of_kind?(:base, :generic_error)).to be(true) | |
end | |
it 'does not add to errors from nil, false' do | |
[nil, false].each do |nil_or_false_error_value| | |
result = described_class.new('existing error message') | |
result.add_errors(nil_or_false_error_value) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ base: ['existing error message'] } | |
) | |
expect(result.errors.of_kind?(:base, 'existing error message')).to be(true) | |
end | |
end | |
it 'raises an error when errors class is invalid' do | |
expect do | |
result = described_class.new('existing error message') | |
result.add_errors(Time.current) | |
end.to raise_error( | |
/Could not build_active_model_errors for Result: invalid error_values\.class/ | |
) | |
end | |
end | |
describe '.success' do | |
it 'creates an instance with empty errors and empty data' do | |
result = described_class.success | |
expect(result.errors).to be_empty | |
expect(result.data).to be_empty | |
end | |
it 'accepts only data' do | |
result = described_class.success(attr: 'value') | |
expect(result.errors).to be_empty | |
expect(result.data).to eq( | |
{ 'attr' => 'value' } | |
) | |
end | |
end | |
describe '.failure' do | |
it 'ensures a Result instance is created with errors, defaults to a generic error' do | |
result = described_class.failure(nil) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ base: ['An error has occurred'] } | |
) | |
expect(result.errors.of_kind?(:base, :generic_error)).to be(true) | |
end | |
it 'accepts errors and data' do | |
result = described_class.failure('error message', attr: 'value') | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ base: ['error message'] } | |
) | |
expect(result.data).to eq( | |
{ 'attr' => 'value' } | |
) | |
expect(result.errors.of_kind?(:base, 'error message')).to be(true) | |
end | |
end | |
def generic_model_class | |
Class.new do | |
include ActiveModel::Model | |
include ActiveModel::Attributes | |
def self.name | |
'GenericModelClass' | |
end | |
attribute :name | |
attribute :description | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment