diff --git a/CHANGELOG.md b/CHANGELOG.md index a494c31..accadb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,10 @@ 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; +- Changed depreacate `Result#type` method. ## 0.2.0 ### Added 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 diff --git a/README.md b/README.md index caeb792..2ccdc8a 100644 --- a/README.md +++ b/README.md @@ -58,17 +58,17 @@ 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.valid? - Success(:created, data: user) + if user.save + Success(:success, :created, data: user) else Failure(:creation_failed, data: user.errors) end @@ -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. @@ -229,10 +235,10 @@ 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 } -# => # +# => # ``` `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 @@ -251,10 +257,10 @@ class IHateEvenNumbers < FService::Base end IHateEvenNumbers.call -# => # +# => # IHateEvenNumbers.call -# => #, @type=:rand_int> +# => #, @types=[:rand_int]> ``` ## Testing @@ -278,19 +284,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(User::Create, result: :failure, types: [: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(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 ``` 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/base.rb b/lib/f_service/base.rb index 1d80a93..fed442c 100644 --- a/lib/f_service/base.rb +++ b/lib/f_service/base.rb @@ -109,62 +109,62 @@ 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. - # 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. # 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,34 +172,34 @@ def Failure(type = nil, 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. # 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(type = nil, 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/lib/f_service/result/base.rb b/lib/f_service/result/base.rb index 43b68ad..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,17 @@ 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 + + # Implements old attribute type. Its deprecated in favor of using types. + def type + FService.deprecate!(name: "#{self.class}##{__method__}", alternative: '#types', from: caller[0]) + + types.size == 1 ? types.first : Array(@matching_types).first end # This hook runs if the result is successful. @@ -59,6 +70,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 @@ -108,6 +120,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 @@ -122,7 +135,7 @@ def on_failure(*target_types, unhandled: false) def to_ary data = successful? ? value : error - [data, type] + [data, @matching_types.first] end private @@ -136,7 +149,11 @@ def unhandled? end def expected_type?(target_types, unhandled:) - target_types.empty? || unhandled || target_types.include?(type) + target_types.empty? || unhandled || target_types.any? { |target_type| types.include?(target_type) } + end + + def match_types(target_types) + @matching_types = target_types.empty? ? types : target_types & types end end end diff --git a/lib/f_service/result/failure.rb b/lib/f_service/result/failure.rb index 7061277..2c82502 100644 --- a/lib/f_service/result/failure.rb +++ b/lib/f_service/result/failure.rb @@ -10,20 +10,19 @@ 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 # 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) - super() + def initialize(error, types = []) + super(types) @error = error - @type = type end # Returns false. diff --git a/lib/f_service/result/success.rb b/lib/f_service/result/success.rb index 0a295fd..9b1028e 100644 --- a/lib/f_service/result/success.rb +++ b/lib/f_service/result/success.rb @@ -9,20 +9,19 @@ 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 # 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) - super() + def initialize(value, types = []) + super(types) @value = value - @type = type end # Returns true. @@ -79,7 +78,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/lib/f_service/rspec/support/helpers/result.rb b/lib/f_service/rspec/support/helpers/result.rb index 6329174..ed16c98 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 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 " diff --git a/spec/f_service/base_spec.rb b/spec/f_service/base_spec.rb index b0fa1a0..2b4c8f8 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 @@ -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 @@ -34,31 +34,31 @@ 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 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 be_empty } + 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 be_empty } it { expect(response.error).to eq('that is an error') } end end @@ -67,14 +67,14 @@ 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 } } 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 @@ -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 be_empty } + 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/base_spec.rb b/spec/f_service/result/base_spec.rb index 05e55b2..abfefd2 100644 --- a/spec/f_service/result/base_spec.rb +++ b/spec/f_service/result/base_spec.rb @@ -3,18 +3,51 @@ 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) } + describe '#type' do + 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 + + 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 "raises error on '#{method_name}' call" do - expect { method_call }.to raise_error NotImplementedError, "called #{method_name} on class Result::Base" + it 'deprecates this method', :aggregate_failures do + expect(test_class.new.type).to eq(:ok) + expect(FService).to have_received(:deprecate!) end end end diff --git a/spec/f_service/result/failure_spec.rb b/spec/f_service/result/failure_spec.rb index c73c1b7..320fdf7 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 @@ -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 @@ -33,11 +33,11 @@ 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) { [] } - let(:failure) { described_class.new(array, :error) } + let(:failure) { described_class.new(array, [:error]) } before { allow(FService).to receive(:deprecate!) } @@ -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 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