Last active
June 29, 2023 20:17
-
-
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 = 'An error has occurred' | |
attr_reader :errors | |
attr_reader :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 String, Symbol, 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) | |
errors.add(key.to_sym, value.to_s) if value.present? | |
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
# 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 String or Array values, and skips blanks' do | |
result = described_class.new( | |
{ | |
error_code_1: 'error message 1', | |
error_code_2: ' ', | |
error_code_3: false, | |
error_code_4: nil, | |
error_code_5: [], | |
error_code_6: ['error message 1', ' ', false, nil, 'error message 2'] | |
} | |
) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to include( | |
{ error_code_1: ['error message 1'] }, | |
{ error_code_6: ['error message 1', 'error message 2'] }, | |
) | |
expect(result.errors.messages).not_to include( | |
:error_code_2, | |
:error_code_3, | |
:error_code_4, | |
:error_code_5, | |
) | |
end | |
it 'sets errors from Array, and skips blanks' do | |
result = described_class.new( | |
['error message 1', ' ', false, nil, 'error message 2'] | |
) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ base: ['error message 1', 'error message 2'] }, | |
) | |
end | |
it 'sets errors from String, Symbol, Numeric, and skips blanks' do | |
[ | |
['error message 1', { base: ['error message 1'] }], | |
[:error_message_1, { base: ['error_message_1'] }], | |
[12345, { base: ['12345'] }], | |
[123.45, { base: ['123.45'] }], | |
].each do |error_value, expected_error_messages| | |
result = described_class.new(error_value) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq(expected_error_messages) | |
end | |
[' ', '', :'', 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(nil) | |
active_model_errors.add(:error_code, 'some error message') | |
result = described_class.new(active_model_errors) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ error_code: ['some error message'] } | |
) | |
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'] }) | |
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 { described_class.new(Time.current) } | |
.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, | |
{ | |
some_attr: 'some value', | |
'another attr' => 'another value', | |
42 => 'forty two' | |
} | |
) | |
expect(result.data['some_attr']).to eq('some value') | |
expect(result.data[:'another attr']).to eq('another value') | |
expect(result.data[42]).to eq('forty two') | |
expect(result.data).to eq( | |
{ | |
'some_attr' => 'some 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('some 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('some 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 String or Array values, and skips blanks' do | |
result = described_class.new('some existing error message') | |
result.add_errors( | |
{ | |
error_code_1: 'error message 1', | |
error_code_2: ' ', | |
error_code_3: false, | |
error_code_4: nil, | |
error_code_5: [], | |
error_code_6: ['error message 1', ' ', false, nil, 'error message 2'] | |
} | |
) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to include( | |
{ base: ['some existing error message'] }, | |
{ error_code_1: ['error message 1'] }, | |
{ error_code_6: ['error message 1', 'error message 2'] }, | |
) | |
expect(result.errors.messages).not_to include( | |
:error_code_2, | |
:error_code_3, | |
:error_code_4, | |
:error_code_5, | |
) | |
end | |
it 'adds to errors from Array, and skips blanks' do | |
result = described_class.new('some existing error message') | |
result.add_errors( | |
['error message 1', ' ', false, nil, 'error message 2'] | |
) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ base: ['some existing error message', 'error message 1', 'error message 2'] }, | |
) | |
end | |
it 'adds to errors from String, Symbol, Numeric, and skips blanks' do | |
[ | |
['error message 1', { base: ['some existing error message', 'error message 1'] }], | |
[:error_message_1, { base: ['some existing error message', 'error_message_1'] }], | |
[12345, { base: ['some existing error message', '12345'] }], | |
[123.45, { base: ['some existing error message', '123.45'] }], | |
].each do |error_value, expected_error_messages| | |
result = described_class.new('some existing error message') | |
result.add_errors(error_value) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
expected_error_messages | |
) | |
end | |
[' ', '', :'', nil].each do |blank_error_value| | |
result = described_class.new('some existing error message') | |
result.add_errors(blank_error_value) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ base: ['some existing error message'] } | |
) | |
end | |
end | |
it 'adds to errors from ActiveModel::Errors' do | |
active_model_errors = ActiveModel::Errors.new(nil) | |
active_model_errors.add(:error_code, 'some error message') | |
result = described_class.new('some existing error message') | |
result.add_errors(active_model_errors) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ | |
base: ['some existing error message'], | |
error_code: ['some error message'] | |
} | |
) | |
end | |
it 'adds to errors from true with a generic message' do | |
result = described_class.new('some existing error message') | |
result.add_errors(true) | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ base: ['some existing error message', 'An error has occurred'] } | |
) | |
end | |
it 'does not add to errors from nil, false' do | |
[nil, false].each do |nil_or_false_error_value| | |
result = described_class.new('some 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: ['some existing error message'] } | |
) | |
end | |
end | |
it 'raises an error when errors class is invalid' do | |
expect do | |
result = described_class.new('some 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 data only' do | |
result = described_class.success(some_attr: 'some value') | |
expect(result.errors).to be_empty | |
expect(result.data).to eq( | |
{ 'some_attr' => 'some 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'] } | |
) | |
end | |
it 'accepts errors and data' do | |
result = described_class.failure('some error message', some_attr: 'some value') | |
expect(result.errors).not_to be_empty | |
expect(result.errors.messages).to eq( | |
{ base: ['some error message'] } | |
) | |
expect(result.data).to eq( | |
{ 'some_attr' => 'some value' } | |
) | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment