From 4724339530e93e9d9513bd50dbaac0065f550c78 Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Fri, 10 Feb 2023 18:05:09 -0300 Subject: [PATCH 01/20] Add multiple types to success case --- README.md | 8 +-- lib/f_service/base.rb | 16 +++--- lib/f_service/result/base.rb | 20 ++++++- lib/f_service/result/success.rb | 12 ++--- spec/f_service/base_spec.rb | 14 ++--- spec/f_service/result/success_spec.rb | 77 ++++++++++++++++++++++----- 6 files changed, 106 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index caeb792..942c4d1 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ class User::Create < FService::Base return Failure(:no_name) if @name.nil? user = UserRepository.create(name: @name) - if user.valid? - Success(:created, data: user) + if user.save + Success(:success, :created, data: user) else Failure(:creation_failed, data: user.errors) end @@ -229,7 +229,7 @@ You can use `Check` to converts a boolean to a Result, truthy values map to `Suc ```ruby Check(:math_works) { 1 < 2 } -# => # +# => # Check(:math_works) { 1 > 2 } # => # @@ -251,7 +251,7 @@ class IHateEvenNumbers < FService::Base end IHateEvenNumbers.call -# => # +# => # IHateEvenNumbers.call # => #, @type=:rand_int> diff --git a/lib/f_service/base.rb b/lib/f_service/base.rb index 1d80a93..6542b1a 100644 --- a/lib/f_service/base.rb +++ b/lib/f_service/base.rb @@ -109,30 +109,30 @@ def success(data = nil) end # Returns a successful result. - # You can optionally specify a type and a value for your result. + # You can optionally specify a list of types and a value for your result. # You'll probably want to return this inside {#run}. # # # @example # def run # Success() - # # => # + # # => # # # Success(:ok) - # # => # + # # => # # # Success(data: 10) - # # => # + # # => # # # Success(:ok, data: 10) - # # => # + # # => # # end # - # @param type the Result type + # @param types the Result types # @param data the result value # @return [Result::Success] a successful result - def Success(type = nil, data: nil) - Result::Success.new(data, type) + def Success(*types, data: nil) + Result::Success.new(data, types) end # Returns a failed result. diff --git a/lib/f_service/result/base.rb b/lib/f_service/result/base.rb index 43b68ad..3fe8248 100644 --- a/lib/f_service/result/base.rb +++ b/lib/f_service/result/base.rb @@ -16,6 +16,7 @@ class Base # You usually shouldn't call this directly. See {FService::Base#Failure} and {FService::Base#Success}. def initialize @handled = false + @matching_types = [] end # This hook runs if the result is successful. @@ -59,6 +60,7 @@ def initialize # @api public def on_success(*target_types, unhandled: false) if successful? && unhandled? && expected_type?(target_types, unhandled: unhandled) + match_types(target_types) yield(*to_ary) @handled = true freeze @@ -122,7 +124,7 @@ def on_failure(*target_types, unhandled: false) def to_ary data = successful? ? value : error - [data, type] + [data, respond_to?(:type) ? type : @matching_types.first] end private @@ -136,7 +138,21 @@ def unhandled? end def expected_type?(target_types, unhandled:) - target_types.empty? || unhandled || target_types.include?(type) + if respond_to?(:type) + target_types.empty? || unhandled || target_types.include?(type) + else + target_types.empty? || unhandled || target_types.any? { |target_type| types.include?(target_type) } + end + end + + def match_types(target_types) + @matching_types = if target_types.empty? + [] + elsif respond_to?(:type) + type + else + target_types & types + end end end end diff --git a/lib/f_service/result/success.rb b/lib/f_service/result/success.rb index 0a295fd..ffd2f40 100644 --- a/lib/f_service/result/success.rb +++ b/lib/f_service/result/success.rb @@ -9,20 +9,20 @@ module Result # # @!attribute [r] value # @return [Object] the provided value for the result - # @!attribute [r] type - # @return [Object] the provided type for the result. Defaults to nil. + # @!attribute [r] types + # @return [Object] the provided types for the result. Defaults to nil. # @api public class Success < Result::Base - attr_reader :value, :type + attr_reader :value, :types # Creates a successful operation. # You usually shouldn't call this directly. See {FService::Base#Success}. # # @param value [Object] success value. - def initialize(value, type = nil) + def initialize(value, types = []) super() @value = value - @type = type + @types = types end # Returns true. @@ -79,7 +79,7 @@ def error # end # # @yieldparam value pass {#value} to a block - # @yieldparam type pass {#type} to a block + # @yieldparam types pass {#types} to a block def and_then yield(*to_ary) end diff --git a/spec/f_service/base_spec.rb b/spec/f_service/base_spec.rb index b0fa1a0..1afb3d6 100644 --- a/spec/f_service/base_spec.rb +++ b/spec/f_service/base_spec.rb @@ -12,7 +12,7 @@ describe '#Success' do subject(:response) { described_class.new.Success(:ok, data: 'yay!') } - it { expect(response.type).to eq(:ok) } + it { expect(response.types).to contain_exactly(:ok) } it { expect(response.value).to eq('yay!') } end @@ -34,8 +34,8 @@ subject(:response) { described_class.new.Check(:math_works) { 1 < 2 } } it { expect(response).to be_successful } - it { expect(response.type).to eq(:math_works) } - it { expect(response.value!).to eq(true) } + it { expect(response.types).to contain_exactly(:math_works) } + it { expect(response.value!).to be_truthy } end context 'when block evaluates to false' do @@ -67,8 +67,8 @@ subject(:response) { described_class.new.Try(:division) { 0 / 1 } } it { expect(response).to be_successful } - it { expect(response.type).to eq(:division) } - it { expect(response.value!).to eq(0) } + it { expect(response.types).to contain_exactly(:division) } + it { expect(response.value!).to be_zero } context 'when some exception is raised' do subject(:response) { described_class.new.Try(:division) { 1 / 0 } } @@ -82,8 +82,8 @@ subject(:response) { described_class.new.Try { 0 / 1 } } it { expect(response).to be_successful } - it { expect(response.type).to eq(nil) } - it { expect(response.value!).to eq 0 } + it { expect(response.types).to contain_exactly(nil) } + it { expect(response.value!).to be_zero } end context 'when raised exception does not match specified exception' do diff --git a/spec/f_service/result/success_spec.rb b/spec/f_service/result/success_spec.rb index 9103e2b..345b745 100644 --- a/spec/f_service/result/success_spec.rb +++ b/spec/f_service/result/success_spec.rb @@ -10,14 +10,14 @@ it { expect(success).to be_successful } it { expect(success).not_to be_failed } it { expect(success.value).to eq('Yay!') } - it { expect(success.type).to eq(nil) } - it { expect(success.error).to eq(nil) } + it { expect(success.types).to be_empty } + it { expect(success.error).to be_nil } it { expect(success.value!).to eq('Yay!') } - context 'when defining a type' do - subject(:success) { described_class.new('Yay!', :ok) } + context 'when defining types' do + subject(:success) { described_class.new('Yay!', %i[ok success]) } - it { expect(success.type).to eq(:ok) } + it { expect(success.types).to contain_exactly(:ok, :success) } end end @@ -25,7 +25,7 @@ describe 'return' do subject(:on_success_callback) { success.on_success(unhandled: true) { 'some recovering' } } - let(:success) { described_class.new([], :ok) } + let(:success) { described_class.new([], [:ok]) } it 'returns itself' do expect(on_success_callback).to eq success @@ -37,7 +37,7 @@ subject(:on_success_callback) { success.on_success { |array| array << 'It works!' } } let(:array) { [] } - let(:success) { described_class.new(array, :error) } + let(:success) { described_class.new(array, [:error]) } before { allow(FService).to receive(:deprecate!) } @@ -54,7 +54,7 @@ end let(:array) { [] } - let(:success) { described_class.new(array, type) } + let(:success) { described_class.new(array, [type]) } context 'and no type matches with success type' do let(:type) { :unknow_success } @@ -84,6 +84,45 @@ end end end + + context 'when multiple types are specified' do + subject(:on_success_callback) do + success.on_success(:ok, :second_ok) { |value, type| value << type } + .on_success(:still_ok) { |value| value << 3 } + .on_success(unhandled: true) { |value| value << 'one more time' } + end + + let(:array) { [] } + let(:success) { described_class.new(array, types) } + + context 'and no type matches with success type' do + let(:types) { [:unknow_success] } + + it 'freezes the result' do + expect(on_success_callback).to be_frozen + end + + it 'evaluates the block wich matches without specifying success' do + on_success_callback + + expect(array).to eq ['one more time'] + end + end + + context 'and first type matches with success type' do + let(:types) { %i[first_ok second_ok] } + + it 'freezes the result' do + expect(on_success_callback).to be_frozen + end + + it 'evaluates only the first given block on failure' do + on_success_callback + + expect(array).to eq [:second_ok] + end + end + end end end @@ -96,7 +135,7 @@ end let(:array) { [] } - let(:success) { described_class.new(array, :ok) } + let(:success) { described_class.new(array, [:ok]) } it 'returns itself' do expect(on_failure_callback).to eq success @@ -114,7 +153,7 @@ end describe '#and_then' do - subject(:success) { described_class.new('Pax', :ok) } + subject(:success) { described_class.new('Pax', [:ok]) } context 'when a block is given' do it 'returns the given block result' do @@ -132,7 +171,7 @@ end describe '#then' do - subject(:success) { described_class.new('Pax', :ok) } + subject(:success) { described_class.new('Pax', [:ok]) } before { allow(FService).to receive(:deprecate!) } @@ -154,7 +193,7 @@ end describe '#or_else' do - subject(:success) { described_class.new('Pax', :ok) } + subject(:success) { described_class.new('Pax', [:ok]) } it 'does not yields the block' do expect { |block| success.or_else(&block) }.not_to yield_control @@ -166,8 +205,18 @@ end describe '#to_s' do - subject(:success) { described_class.new('Yay!') } + subject(:success) { described_class.new(value) } + + context 'when result does not have a value' do + let(:value) { nil } - it { expect(success.to_s).to eq 'Success("Yay!")' } + it { expect(success.to_s).to eq 'Success()' } + end + + context 'when result has a value' do + let(:value) { 'Yay!' } + + it { expect(success.to_s).to eq 'Success("Yay!")' } + end end end From 3c0d46c675241101f9777fb35078f2c13e5041c4 Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 09:24:53 -0300 Subject: [PATCH 02/20] Add multiple types to failure case --- lib/f_service/base.rb | 18 ++++---- lib/f_service/result/base.rb | 1 + lib/f_service/result/failure.rb | 10 ++--- spec/f_service/base_spec.rb | 14 +++--- spec/f_service/result/failure_spec.rb | 62 ++++++++++++++++++++++----- 5 files changed, 73 insertions(+), 32 deletions(-) diff --git a/lib/f_service/base.rb b/lib/f_service/base.rb index 6542b1a..7286644 100644 --- a/lib/f_service/base.rb +++ b/lib/f_service/base.rb @@ -125,7 +125,7 @@ def success(data = nil) # # => # # # Success(:ok, data: 10) - # # => # + # # => # # end # # @param types the Result types @@ -136,30 +136,30 @@ def Success(*types, data: nil) end # Returns a failed result. - # You can optionally specify a type and a value for your result. + # You can optionally specify types and a value for your result. # You'll probably want to return this inside {#run}. # # # @example # def run # Failure() - # # => # + # # => # # # Failure(:not_a_number) - # # => # + # # => # # # Failure(data: "10") - # # => # + # # => # # # Failure(:not_a_number, data: "10") - # # => # + # # => # # end # - # @param type the Result type + # @param types the Result types # @param data the result value # @return [Result::Failure] a failed result - def Failure(type = nil, data: nil) - Result::Failure.new(data, type) + def Failure(*types, data: nil) + Result::Failure.new(data, types) end # Converts a boolean to a Result. diff --git a/lib/f_service/result/base.rb b/lib/f_service/result/base.rb index 3fe8248..fa04664 100644 --- a/lib/f_service/result/base.rb +++ b/lib/f_service/result/base.rb @@ -110,6 +110,7 @@ def on_success(*target_types, unhandled: false) # @api public def on_failure(*target_types, unhandled: false) if failed? && unhandled? && expected_type?(target_types, unhandled: unhandled) + match_types(target_types) yield(*to_ary) @handled = true freeze diff --git a/lib/f_service/result/failure.rb b/lib/f_service/result/failure.rb index 7061277..951e636 100644 --- a/lib/f_service/result/failure.rb +++ b/lib/f_service/result/failure.rb @@ -10,20 +10,20 @@ module Result # # @!attribute [r] error # @return [Object] the provided error for the result - # @!attribute [r] type - # @return [Object] the provided type for the result. Defaults to nil. + # @!attribute [r] types + # @return [Object] the provided types for the result. Defaults to nil. # @api public class Failure < Result::Base - attr_reader :error, :type + attr_reader :error, :types # Creates a failed operation. # You usually shouldn't call this directly. See {FService::Base#Failure}. # # @param error [Object] failure value. - def initialize(error, type = nil) + def initialize(error, types = []) super() @error = error - @type = type + @types = types end # Returns false. diff --git a/spec/f_service/base_spec.rb b/spec/f_service/base_spec.rb index 1afb3d6..5b4dc8a 100644 --- a/spec/f_service/base_spec.rb +++ b/spec/f_service/base_spec.rb @@ -25,7 +25,7 @@ describe '#Failure' do subject(:response) { described_class.new.Failure(:error, data: 'Whoops!') } - it { expect(response.type).to eq(:error) } + it { expect(response.types).to contain_exactly(:error) } it { expect(response.error).to eq('Whoops!') } end @@ -42,23 +42,23 @@ subject(:response) { described_class.new.Check(:math_works) { 1 > 2 } } it { expect(response).to be_failed } - it { expect(response.type).to eq(:math_works) } - it { expect(response.error).to eq(false) } + it { expect(response.types).to contain_exactly(:math_works) } + it { expect(response.error).to be_falsy } end context 'when type is not specified' do subject(:response) { described_class.new.Check { 1 > 2 } } it { expect(response).to be_failed } - it { expect(response.type).to eq(nil) } - it { expect(response.error).to eq(false) } + it { expect(response.types).to contain_exactly(nil) } + it { expect(response.error).to be_falsy } end context 'when data is passed' do subject(:response) { described_class.new.Check(data: 'that is an error') { 1 > 2 } } it { expect(response).to be_failed } - it { expect(response.type).to eq(nil) } + it { expect(response.types).to contain_exactly(nil) } it { expect(response.error).to eq('that is an error') } end end @@ -74,7 +74,7 @@ subject(:response) { described_class.new.Try(:division) { 1 / 0 } } it { expect(response).to be_failed } - it { expect(response.type).to eq(:division) } + it { expect(response.types).to contain_exactly(:division) } it { expect(response.error).to be_a ZeroDivisionError } end diff --git a/spec/f_service/result/failure_spec.rb b/spec/f_service/result/failure_spec.rb index c73c1b7..23e12bc 100644 --- a/spec/f_service/result/failure_spec.rb +++ b/spec/f_service/result/failure_spec.rb @@ -10,14 +10,14 @@ it { expect(failure).to be_failed } it { expect(failure).not_to be_successful } it { expect(failure.error).to eq('Whoops!') } - it { expect(failure.type).to eq(nil) } - it { expect(failure.value).to eq(nil) } + it { expect(failure.types).to be_empty } + it { expect(failure.value).to be_nil } it { expect { failure.value! }.to raise_error FService::Result::Error } - context 'when defining a type' do - subject(:failure) { described_class.new('Whoops!', :error) } + context 'when defining a types' do + subject(:failure) { described_class.new('Whoops!', %i[error]) } - it { expect(failure.type).to eq(:error) } + it { expect(failure.types).to contain_exactly(:error) } end end @@ -33,7 +33,7 @@ end describe 'callback matching' do - context 'when no type is especified' do + context 'when no types are especified' do subject(:on_failure_callback) { failure.on_failure { |array| array << "That's no moon" } } let(:array) { [] } @@ -46,7 +46,7 @@ end end - context 'when type is especified' do + context 'when some types are especified' do subject(:on_failure_callback) do failure .on_failure(:error) { |array, type| array << type } @@ -55,10 +55,10 @@ end let(:array) { [] } - let(:failure) { described_class.new(array, type) } + let(:failure) { described_class.new(array, types) } - context 'and no type matches with error type' do - let(:type) { :unknown_error } + context 'and no types matches with error types' do + let(:types) { %i[unknown_error] } it 'evaluates the block wich matches without specifying error' do on_failure_callback @@ -72,7 +72,7 @@ end context 'and some type matches with error type' do - let(:type) { :error } + let(:types) { %i[error] } it 'freezes the result' do expect(on_failure_callback).to be_frozen @@ -85,6 +85,46 @@ end end end + + context 'when multiple types are especified' do + subject(:on_failure_callback) do + failure + .on_failure(:unprocessable_entity) { |array, type| array << type } + .on_failure(:client_error) { |array| array << 3 } + .on_failure(unhandled: true) { |array| array << "That's no moon" } + end + + let(:array) { [] } + let(:failure) { described_class.new(array, types) } + + context 'and no types matches with error types' do + let(:types) { %i[unknown_error server_error] } + + it 'evaluates the block wich matches without specifying error' do + on_failure_callback + + expect(array).to eq ["That's no moon"] + end + + it 'freezes the result' do + expect(on_failure_callback).to be_frozen + end + end + + context 'and some type matches with error types' do + let(:types) { %i[not_found client_error] } + + it 'freezes the result' do + expect(on_failure_callback).to be_frozen + end + + it 'evaluates only the first given block on failure' do + on_failure_callback + + expect(array).to eq [3] + end + end + end end end From b17c77797f4c1f1c3db8c45647f12b384b696ef1 Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 10:12:40 -0300 Subject: [PATCH 03/20] Check uses multiple failures and successes --- lib/f_service/base.rb | 16 ++++++++-------- spec/f_service/base_spec.rb | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/f_service/base.rb b/lib/f_service/base.rb index 7286644..caf4991 100644 --- a/lib/f_service/base.rb +++ b/lib/f_service/base.rb @@ -164,7 +164,7 @@ def Failure(*types, data: nil) # Converts a boolean to a Result. # Truthy values map to Success, and falsey values map to Failures. - # You can optionally provide a type for the result. + # You can optionally provide a types for the result. # The result value defaults as the evaluated value of the given block. # If you want another value you can pass it through the `data:` argument. # @@ -172,28 +172,28 @@ def Failure(*types, data: nil) # class CheckMathWorks < FService::Base # def run # Check(:math_works) { 1 < 2 } - # # => # + # # => # # # Check(:math_works) { 1 > 2 } - # # => # + # # => # # # Check(:math_works, data: 1 + 2) { 1 > 2 } - # # => # + # # => # # end # # Check(:math_works, data: 1 + 2) { 1 < 2 } - # # => # + # # => # # end # end # - # @param type the Result type + # @param types the Result types # @return [Result::Success, Result::Failure] a Result from the boolean expression - def Check(type = nil, data: nil) + def Check(*types, data: nil) res = yield final_data = data || res - res ? Success(type, data: final_data) : Failure(type, data: final_data) + res ? Success(*types, data: final_data) : Failure(*types, data: final_data) end # If the given block raises an exception, it wraps it in a Failure. diff --git a/spec/f_service/base_spec.rb b/spec/f_service/base_spec.rb index 5b4dc8a..4832952 100644 --- a/spec/f_service/base_spec.rb +++ b/spec/f_service/base_spec.rb @@ -50,7 +50,7 @@ subject(:response) { described_class.new.Check { 1 > 2 } } it { expect(response).to be_failed } - it { expect(response.types).to contain_exactly(nil) } + it { expect(response.types).to be_empty } it { expect(response.error).to be_falsy } end @@ -58,7 +58,7 @@ subject(:response) { described_class.new.Check(data: 'that is an error') { 1 > 2 } } it { expect(response).to be_failed } - it { expect(response.types).to contain_exactly(nil) } + it { expect(response.types).to be_empty } it { expect(response.error).to eq('that is an error') } end end From 10d0414637e4d9458ef592ed2bb7c54bc522f717 Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 10:19:31 -0300 Subject: [PATCH 04/20] Check uses multiple types for Try --- lib/f_service/base.rb | 28 ++++++++++++++-------------- spec/f_service/base_spec.rb | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/f_service/base.rb b/lib/f_service/base.rb index caf4991..fed442c 100644 --- a/lib/f_service/base.rb +++ b/lib/f_service/base.rb @@ -143,16 +143,16 @@ def Success(*types, data: nil) # @example # def run # Failure() - # # => # + # # => # # # Failure(:not_a_number) - # # => # + # # => # # # Failure(data: "10") - # # => # + # # => # # # Failure(:not_a_number, data: "10") - # # => # + # # => # # end # # @param types the Result types @@ -172,17 +172,17 @@ def Failure(*types, data: nil) # class CheckMathWorks < FService::Base # def run # Check(:math_works) { 1 < 2 } - # # => # + # # => # # # Check(:math_works) { 1 > 2 } - # # => # + # # => # # # Check(:math_works, data: 1 + 2) { 1 > 2 } # # => # # end # # Check(:math_works, data: 1 + 2) { 1 < 2 } - # # => # + # # => # # end # end # @@ -199,7 +199,7 @@ def Check(*types, data: nil) # If the given block raises an exception, it wraps it in a Failure. # Otherwise, maps the block value in a Success object. # You can specify which exceptions to watch for. - # It's possible to provide a type for the result too. + # It's possible to provide a types for the result too. # # @example # class IHateEvenNumbers < FService::Base @@ -214,20 +214,20 @@ def Check(*types, data: nil) # end # # IHateEvenNumbers.call - # # => # + # # => # # # IHateEvenNumbers.call - # # => #, @type=:rand_int> + # # => #, @types=[:rand_int]> # - # @param type the Result type + # @param types the Result types # @param catch the exception list to catch # @return [Result::Success, Result::Failure] a result from the boolean expression - def Try(type = nil, catch: StandardError) + def Try(*types, catch: StandardError) res = yield - Success(type, data: res) + Success(*types, data: res) rescue *catch => e - Failure(type, data: e) + Failure(*types, data: e) end # Returns a failed operation. diff --git a/spec/f_service/base_spec.rb b/spec/f_service/base_spec.rb index 4832952..2673d4b 100644 --- a/spec/f_service/base_spec.rb +++ b/spec/f_service/base_spec.rb @@ -82,7 +82,7 @@ subject(:response) { described_class.new.Try { 0 / 1 } } it { expect(response).to be_successful } - it { expect(response.types).to contain_exactly(nil) } + it { expect(response.types).to be_blank } it { expect(response.value!).to be_zero } end From d18133c0e51a85ef6bb33515b3bd0ff8e7fe47a3 Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 10:25:55 -0300 Subject: [PATCH 05/20] Adjust README to new way --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 942c4d1..02572e2 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,13 @@ end The next step is writing the `#run` method, which is where the work should be done. Use the methods `#Success` and `#Failure` to handle your return values. -You can optionally specify a type and a value for your result. +You can optionally specify a list of types which represents that result and a value for your result. ```ruby class User::Create < FService::Base # ... def run - return Failure(:no_name) if @name.nil? + return Failure(:no_name, :invalid_attribute) if @name.nil? user = UserRepository.create(name: @name) if user.save @@ -232,7 +232,7 @@ Check(:math_works) { 1 < 2 } # => # Check(:math_works) { 1 > 2 } -# => # +# => # ``` `Try` transforms an exception into a `Failure` if some exception is raised for the given block. You can specify which exception class to watch for @@ -254,7 +254,7 @@ IHateEvenNumbers.call # => # IHateEvenNumbers.call -# => #, @type=:rand_int> +# => #, @types=[:rand_int]> ``` ## Testing From bbfcb7beb7a7ea3c7ebba615b6cc79d8ccb8c4e1 Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 11:54:21 -0300 Subject: [PATCH 06/20] Fix: Remove rails method matcher --- spec/f_service/base_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/f_service/base_spec.rb b/spec/f_service/base_spec.rb index 2673d4b..2b4c8f8 100644 --- a/spec/f_service/base_spec.rb +++ b/spec/f_service/base_spec.rb @@ -82,7 +82,7 @@ subject(:response) { described_class.new.Try { 0 / 1 } } it { expect(response).to be_successful } - it { expect(response.types).to be_blank } + it { expect(response.types).to be_empty } it { expect(response.value!).to be_zero } end From f0d4c61e6b29a371ff331700675cd8957b962293 Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 11:59:39 -0300 Subject: [PATCH 07/20] Add: mock_service using types and deprecating type argument --- README.md | 8 ++++---- lib/f_service.rb | 12 ++++++++++++ lib/f_service/rspec/support/helpers/result.rb | 19 ++++++++++++++----- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 02572e2..4b8fc64 100644 --- a/README.md +++ b/README.md @@ -278,19 +278,19 @@ mock_service(Uer::Create) mock_service(Uer::Create, result: :success) # => Mocks a successful result with all values nil -mock_service(Uer::Create, result: :success, type: :created) +mock_service(Uer::Create, result: :success, types: [:created, :success]) # => Mocks a successful result with type created -mock_service(Uer::Create, result: :success, type: :created, value: instance_spy(User)) +mock_service(Uer::Create, result: :success, types: :created, value: instance_spy(User)) # => Mocks a successful result with type created and a value mock_service(Uer::Create, result: :failure) # => Mocs a failure with all nil values -mock_service(Uer::Create, result: :failure, type: :invalid_attributes) +mock_service(Uer::Create, result: :failure, type: [:unprocessable_entity, :client_error]) # => Mocs a failure with a failure type -mock_service(Uer::Create, result: :failure, type: :invalid_attributes, value: { name: ["can't be blank"] }) +mock_service(Uer::Create, result: :failure, type: [:unprocessable_entity, :client_error], value: { name: ["can't be blank"] }) # => Mocs a failure with a failure type and an error value ``` diff --git a/lib/f_service.rb b/lib/f_service.rb index be74968..552efcb 100644 --- a/lib/f_service.rb +++ b/lib/f_service.rb @@ -17,4 +17,16 @@ def self.deprecate!(name:, alternative:, from: nil) warn warn_message.join("\n") end + + # Marks an argument as deprecated + # + # @api private + def self.deprecate_argument_name(name:, argument_name:, alternative:, from: nil) + warn_message = ["\n[DEPRECATED] #{name} passing #{argument_name.inspect} is deprecated; "] + warn_message << ["called from #{from}; "] unless from.nil? + warn_message << "use #{alternative} instead. " + warn_message << 'It will be removed on the next release.' + + warn warn_message.join("\n") + end end diff --git a/lib/f_service/rspec/support/helpers/result.rb b/lib/f_service/rspec/support/helpers/result.rb index 6329174..328f0ca 100644 --- a/lib/f_service/rspec/support/helpers/result.rb +++ b/lib/f_service/rspec/support/helpers/result.rb @@ -3,17 +3,26 @@ # Methods to mock a FService result from a service call. module FServiceResultHelpers # Create an Fservice result Success or Failure. - def f_service_result(result, value = nil, type = nil) + def f_service_result(result, value = nil, types = []) if result == :success - FService::Result::Success.new(value, type) + FService::Result::Success.new(value, *Array(types)) else - FService::Result::Failure.new(value, type) + FService::Result::Failure.new(value, *Array(types)) end end # Mock a Fservice service call returning a result. - def mock_service(service, result: :success, value: nil, type: nil) - service_result = f_service_result(result, value, type) + def mock_service(service, result: :success, value: nil, type: :not_passed, types: []) + result_types = Array(types) + + if type != :not_passed + alternative = "mock_service(..., types: [#{type.inspect}])" + name = 'mock_service' + FService.deprecate_argument_name(name: name, argument_name: :type, alternative: alternative, from: caller[0]) + result_types = Array(type) + end + + service_result = f_service_result(result, value, result_types) allow(service).to receive(:call).and_return(service_result) end end From 50f146d5ff4f699dccad90e4b8685a5bc6c7f17f Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 12:09:57 -0300 Subject: [PATCH 08/20] Add: rspec matchers matching a list of types --- lib/f_service/rspec/support/matchers/result.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/f_service/rspec/support/matchers/result.rb b/lib/f_service/rspec/support/matchers/result.rb index 0e7b900..28b7153 100644 --- a/lib/f_service/rspec/support/matchers/result.rb +++ b/lib/f_service/rspec/support/matchers/result.rb @@ -2,9 +2,9 @@ require 'rspec/expectations' -RSpec::Matchers.define :have_failed_with do |expected| +RSpec::Matchers.define :have_failed_with do |*expected_types| match do |actual| - matched = actual.is_a?(FService::Result::Failure) && actual.type == expected + matched = actual.is_a?(FService::Result::Failure) && actual.types == expected_types matched &&= actual.error == @expected_error if defined?(@expected_error) @@ -17,7 +17,7 @@ failure_message do |actual| if actual.is_a?(FService::Result::Failure) - message = "expected failure's type '#{actual.type.inspect}' to be equal '#{expected.inspect}'" + message = "expected failure's types '#{actual.types.inspect}' to be equal '#{expected_types.inspect}'" if defined?(@expected_error) has_description = @expected_error.respond_to?(:description) message += " and error '#{actual.error.inspect}' to be " @@ -31,9 +31,9 @@ end end -RSpec::Matchers.define :have_succeed_with do |expected| +RSpec::Matchers.define :have_succeed_with do |*expected_types| match do |actual| - matched = actual.is_a?(FService::Result::Success) && actual.type == expected + matched = actual.is_a?(FService::Result::Success) && actual.types == expected_types matched &&= values_match?(@expected_value, actual.value) if defined?(@expected_value) @@ -46,7 +46,7 @@ failure_message do |actual| if actual.is_a?(FService::Result::Success) - message = "expected success's type '#{actual.type.inspect}' to be equal '#{expected.inspect}'" + message = "expected success's types '#{actual.types.inspect}' to be equal '#{expected_types.inspect}'" if defined?(@expected_value) has_description = @expected_value.respond_to?(:description) message += " and value '#{actual.value.inspect}' to be " From 79a689214c1aed37aeffa9319779708c9c967eae Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 12:26:39 -0300 Subject: [PATCH 09/20] Refactory: Remove code added to support developing in separated --- lib/f_service/result/base.rb | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/f_service/result/base.rb b/lib/f_service/result/base.rb index fa04664..430293f 100644 --- a/lib/f_service/result/base.rb +++ b/lib/f_service/result/base.rb @@ -125,7 +125,7 @@ def on_failure(*target_types, unhandled: false) def to_ary data = successful? ? value : error - [data, respond_to?(:type) ? type : @matching_types.first] + [data, @matching_types.first] end private @@ -139,18 +139,12 @@ def unhandled? end def expected_type?(target_types, unhandled:) - if respond_to?(:type) - target_types.empty? || unhandled || target_types.include?(type) - else - target_types.empty? || unhandled || target_types.any? { |target_type| types.include?(target_type) } - end + target_types.empty? || unhandled || target_types.any? { |target_type| types.include?(target_type) } end def match_types(target_types) @matching_types = if target_types.empty? [] - elsif respond_to?(:type) - type else target_types & types end From 104e1c3c825a4e598b1af1a84eccc7a18d2937fa Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 12:27:47 -0300 Subject: [PATCH 10/20] Add: binding pry to debugging --- lib/f_service/rspec/support/matchers/result.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/f_service/rspec/support/matchers/result.rb b/lib/f_service/rspec/support/matchers/result.rb index 28b7153..6cf197d 100644 --- a/lib/f_service/rspec/support/matchers/result.rb +++ b/lib/f_service/rspec/support/matchers/result.rb @@ -33,6 +33,7 @@ RSpec::Matchers.define :have_succeed_with do |*expected_types| match do |actual| + binding.pry matched = actual.is_a?(FService::Result::Success) && actual.types == expected_types matched &&= values_match?(@expected_value, actual.value) if defined?(@expected_value) From 3f976c7cd938a65438b325dd80671527e0b2fa66 Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 13:40:52 -0300 Subject: [PATCH 11/20] Fix: Keep array format when create a mock result --- lib/f_service/rspec/support/helpers/result.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/f_service/rspec/support/helpers/result.rb b/lib/f_service/rspec/support/helpers/result.rb index 328f0ca..ed16c98 100644 --- a/lib/f_service/rspec/support/helpers/result.rb +++ b/lib/f_service/rspec/support/helpers/result.rb @@ -5,9 +5,9 @@ module FServiceResultHelpers # Create an Fservice result Success or Failure. def f_service_result(result, value = nil, types = []) if result == :success - FService::Result::Success.new(value, *Array(types)) + FService::Result::Success.new(value, Array(types)) else - FService::Result::Failure.new(value, *Array(types)) + FService::Result::Failure.new(value, Array(types)) end end From 0dc6d9332b65185686f069b35a4e2bd6cf46bfe1 Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 13:42:17 -0300 Subject: [PATCH 12/20] Revert "Add: binding pry to debugging" This reverts commit 104e1c3c825a4e598b1af1a84eccc7a18d2937fa. --- lib/f_service/rspec/support/matchers/result.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/f_service/rspec/support/matchers/result.rb b/lib/f_service/rspec/support/matchers/result.rb index 6cf197d..28b7153 100644 --- a/lib/f_service/rspec/support/matchers/result.rb +++ b/lib/f_service/rspec/support/matchers/result.rb @@ -33,7 +33,6 @@ RSpec::Matchers.define :have_succeed_with do |*expected_types| match do |actual| - binding.pry matched = actual.is_a?(FService::Result::Success) && actual.types == expected_types matched &&= values_match?(@expected_value, actual.value) if defined?(@expected_value) From ab65e587ef80045a1008b811e412caee1eeeb69c Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 13:56:22 -0300 Subject: [PATCH 13/20] Update changelog to new success formats --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a494c31..f75fb87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- ### Added - -- Rspec Helper mock_service; -- Rspec Matcher have_succeed_with and have_failed_with +- Add Rspec Helper mock_service; +- Add Rspec Matcher have_succeed_with and have_failed_with; +- Add `Success()`, `Failure()`, `Check()`, `Try()` now can be multipe types; ## 0.2.0 ### Added From 2dc9282f105655ae45c9cdeca1a2af173f20f254 Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 14:38:49 -0300 Subject: [PATCH 14/20] Update nokogiri dependencies --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8addf73..6b11576 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,8 +16,8 @@ GEM jaro_winkler (1.5.4) maruku (0.7.3) method_source (1.0.0) - mini_portile2 (2.8.0) - nokogiri (1.13.10) + mini_portile2 (2.8.1) + nokogiri (1.14.1) mini_portile2 (~> 2.8.0) racc (~> 1.4) parallel (1.19.1) @@ -28,7 +28,7 @@ GEM method_source (~> 1.0) pry-nav (1.0.0) pry (>= 0.9.10, < 0.15) - racc (1.6.1) + racc (1.6.2) rainbow (3.0.0) rake (13.0.1) reverse_markdown (1.4.0) @@ -100,4 +100,4 @@ DEPENDENCIES yard BUNDLED WITH - 2.1.4 + 2.2.32 From 5ee58f5f0c31cf810417b380e79af9c6099e913d Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Mon, 13 Feb 2023 16:20:45 -0300 Subject: [PATCH 15/20] PR Suggestions: Fix: TYPO added to docs Co-authored-by: Matheus Batista --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b8fc64..090d865 100644 --- a/README.md +++ b/README.md @@ -287,10 +287,10 @@ mock_service(Uer::Create, result: :success, types: :created, value: instance_spy mock_service(Uer::Create, result: :failure) # => Mocs a failure with all nil values -mock_service(Uer::Create, result: :failure, type: [:unprocessable_entity, :client_error]) +mock_service(User::Create, result: :failure, types: [:unprocessable_entity, :client_error]) # => Mocs a failure with a failure type -mock_service(Uer::Create, result: :failure, type: [:unprocessable_entity, :client_error], value: { name: ["can't be blank"] }) +mock_service(User::Create, result: :failure, types: [:unprocessable_entity, :client_error], value: { name: ["can't be blank"] }) # => Mocs a failure with a failure type and an error value ``` From be25fdbd604594a6c579132d09ccda82894ed7fd Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Tue, 14 Feb 2023 08:39:17 -0300 Subject: [PATCH 16/20] Fix: Matching all types when no error types are specified --- lib/f_service/result/base.rb | 6 +----- spec/f_service/result/failure_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/f_service/result/base.rb b/lib/f_service/result/base.rb index 430293f..a8b6242 100644 --- a/lib/f_service/result/base.rb +++ b/lib/f_service/result/base.rb @@ -143,11 +143,7 @@ def expected_type?(target_types, unhandled:) end def match_types(target_types) - @matching_types = if target_types.empty? - [] - else - target_types & types - end + @matching_types = target_types.empty? ? types : target_types & types end end end diff --git a/spec/f_service/result/failure_spec.rb b/spec/f_service/result/failure_spec.rb index 23e12bc..320fdf7 100644 --- a/spec/f_service/result/failure_spec.rb +++ b/spec/f_service/result/failure_spec.rb @@ -25,7 +25,7 @@ describe 'return' do subject(:on_failure_callback) { failure.on_failure(unhandled: true) { 'some recovering' } } - let(:failure) { described_class.new([], :error) } + let(:failure) { described_class.new([], [:error]) } it 'returns itself' do expect(on_failure_callback).to eq failure @@ -37,7 +37,7 @@ subject(:on_failure_callback) { failure.on_failure { |array| array << "That's no moon" } } let(:array) { [] } - let(:failure) { described_class.new(array, :error) } + let(:failure) { described_class.new(array, [:error]) } before { allow(FService).to receive(:deprecate!) } From d0e11d8bd832ca318096325e6b69de43d8010b3f Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Thu, 16 Feb 2023 14:21:05 -0300 Subject: [PATCH 17/20] PR Suggestion: Return a type implemntation and deprecate it --- lib/f_service/result/base.rb | 7 +++++++ spec/f_service/result/base_spec.rb | 33 ++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/lib/f_service/result/base.rb b/lib/f_service/result/base.rb index a8b6242..0eb6ac2 100644 --- a/lib/f_service/result/base.rb +++ b/lib/f_service/result/base.rb @@ -19,6 +19,13 @@ def initialize @matching_types = [] end + # Implements old attribute type. Its deprecated in favor of using types. + def type + FService.deprecate!(name: "#{self.class}##{__method__}", alternative: '#types', from: caller[0]) + + Array(@matching_types).first + end + # This hook runs if the result is successful. # Can receive one or more types to be checked before running the given block. # diff --git a/spec/f_service/result/base_spec.rb b/spec/f_service/result/base_spec.rb index 05e55b2..0937b05 100644 --- a/spec/f_service/result/base_spec.rb +++ b/spec/f_service/result/base_spec.rb @@ -3,19 +3,34 @@ require 'spec_helper' RSpec.describe FService::Result::Base do - let(:test_class) do - Class.new(described_class) do - def initialize; end + describe 'not implmented methods' do + let(:test_class) { Class.new(described_class) } + + %i[and_then successful? failed? value value! error].each do |method_name| + context 'when subclasses do not override methods' do + subject(:method_call) { test_class.new.public_send(method_name) } + + it "raises error on '#{method_name}' call" do + expect { method_call }.to raise_error NotImplementedError, "called #{method_name} on class Result::Base" + end + end end end - %i[and_then successful? failed? value value! error].each do |method_name| - context 'when subclasses do not override methods' do - subject(:method_call) { test_class.new.public_send(method_name) } - - it "raises error on '#{method_name}' call" do - expect { method_call }.to raise_error NotImplementedError, "called #{method_name} on class Result::Base" + describe '#type' do + let(:test_class) do + Class.new(described_class) do + def initialize + @matching_types = %i[ok success] + end end end + + before { allow(FService).to receive(:deprecate!) } + + it 'deprecates this method', :aggregate_failures do + expect(test_class.new.type).to eq(:ok) + expect(FService).to have_received(:deprecate!) + end end end From 01b3265a3bf5631d104de89e2e08773f288b73d1 Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Thu, 16 Feb 2023 14:28:22 -0300 Subject: [PATCH 18/20] PR Suggestion - Add a documentation about types precedence --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 090d865..2ccdc8a 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,12 @@ class UsersController < BaseController end ``` +### Types precedence + +FService matches types from left to right, from more specific to more generic. +Example (:unprocessable_entity, :client_error, :http_response) +Then, result will match first :unprocessable_entity, after :client_error, after :http_response, then not matched. + ### Chaining services Since all services return Results, you can chain service calls making a data pipeline. From 55d299cf0492d81dbf9993f519b07c80c8176771 Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Thu, 16 Feb 2023 14:29:19 -0300 Subject: [PATCH 19/20] Add: deprecation to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f75fb87..accadb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add Rspec Helper mock_service; - Add Rspec Matcher have_succeed_with and have_failed_with; - Add `Success()`, `Failure()`, `Check()`, `Try()` now can be multipe types; +- Changed depreacate `Result#type` method. ## 0.2.0 ### Added From d1cac387723b652764e76fa0e715f662cf89d6b8 Mon Sep 17 00:00:00 2001 From: Bruno Vicenzo Date: Wed, 22 Feb 2023 08:58:20 -0300 Subject: [PATCH 20/20] Change deprecated method time to work as before --- lib/f_service/result/base.rb | 7 ++++-- lib/f_service/result/failure.rb | 5 ++--- lib/f_service/result/success.rb | 5 ++--- spec/f_service/result/base_spec.rb | 34 +++++++++++++++++++++++------- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/lib/f_service/result/base.rb b/lib/f_service/result/base.rb index 0eb6ac2..d8898f1 100644 --- a/lib/f_service/result/base.rb +++ b/lib/f_service/result/base.rb @@ -7,6 +7,8 @@ module Result # # @abstract class Base + attr_reader :types + %i[and_then successful? failed? value value! error].each do |method_name| define_method(method_name) do |*_args| raise NotImplementedError, "called #{method_name} on class Result::Base" @@ -14,8 +16,9 @@ class Base end # You usually shouldn't call this directly. See {FService::Base#Failure} and {FService::Base#Success}. - def initialize + def initialize(types = []) @handled = false + @types = types @matching_types = [] end @@ -23,7 +26,7 @@ def initialize def type FService.deprecate!(name: "#{self.class}##{__method__}", alternative: '#types', from: caller[0]) - Array(@matching_types).first + types.size == 1 ? types.first : Array(@matching_types).first end # This hook runs if the result is successful. diff --git a/lib/f_service/result/failure.rb b/lib/f_service/result/failure.rb index 951e636..2c82502 100644 --- a/lib/f_service/result/failure.rb +++ b/lib/f_service/result/failure.rb @@ -14,16 +14,15 @@ module Result # @return [Object] the provided types for the result. Defaults to nil. # @api public class Failure < Result::Base - attr_reader :error, :types + attr_reader :error # Creates a failed operation. # You usually shouldn't call this directly. See {FService::Base#Failure}. # # @param error [Object] failure value. def initialize(error, types = []) - super() + super(types) @error = error - @types = types end # Returns false. diff --git a/lib/f_service/result/success.rb b/lib/f_service/result/success.rb index ffd2f40..9b1028e 100644 --- a/lib/f_service/result/success.rb +++ b/lib/f_service/result/success.rb @@ -13,16 +13,15 @@ module Result # @return [Object] the provided types for the result. Defaults to nil. # @api public class Success < Result::Base - attr_reader :value, :types + attr_reader :value # Creates a successful operation. # You usually shouldn't call this directly. See {FService::Base#Success}. # # @param value [Object] success value. def initialize(value, types = []) - super() + super(types) @value = value - @types = types end # Returns true. diff --git a/spec/f_service/result/base_spec.rb b/spec/f_service/result/base_spec.rb index 0937b05..abfefd2 100644 --- a/spec/f_service/result/base_spec.rb +++ b/spec/f_service/result/base_spec.rb @@ -18,19 +18,37 @@ end describe '#type' do - let(:test_class) do - Class.new(described_class) do - def initialize - @matching_types = %i[ok success] + before { allow(FService).to receive(:deprecate!) } + + context 'when types has just one type' do + let(:test_class) do + Class.new(described_class) do + def initialize + @types = %i[success] + end end end + + it 'deprecates this method', :aggregate_failures do + expect(test_class.new.type).to eq(:success) + expect(FService).to have_received(:deprecate!) + end end - before { allow(FService).to receive(:deprecate!) } + context 'when types has multiple values' do + let(:test_class) do + Class.new(described_class) do + def initialize + @types = %i[ok success http_response] + @matching_types = %i[ok success] + end + end + end - it 'deprecates this method', :aggregate_failures do - expect(test_class.new.type).to eq(:ok) - expect(FService).to have_received(:deprecate!) + it 'deprecates this method', :aggregate_failures do + expect(test_class.new.type).to eq(:ok) + expect(FService).to have_received(:deprecate!) + end end end end