Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture just one callback per result #30

Merged
merged 14 commits into from
Mar 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,17 @@ Style/DocumentationMethod:
Naming/MethodName:
Exclude:
- lib/f_service/base.rb

bvicenzo marked this conversation as resolved.
Show resolved Hide resolved
RSpec/ContextWording:
Prefixes:
- and
- but
- when
- with
- without

RSpec/ExampleLength:
Max: 20

RSpec/NestedGroups:
Enabled: false
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ source 'https://rubygems.org'
gemspec

group :development, :test do
gem 'pry'
gem 'pry-nav'
gem 'rake', '~> 13.0.0'
gem 'rubocop', '~> 0.82.0', require: false
gem 'rubocop-rspec', require: false
Expand Down
9 changes: 9 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,25 @@ GEM
ast (2.4.0)
backport (1.1.2)
benchmark (0.1.0)
coderay (1.1.3)
diff-lcs (1.3)
docile (1.3.5)
e2mmap (0.1.0)
jaro_winkler (1.5.4)
maruku (0.7.3)
method_source (1.0.0)
mini_portile2 (2.8.0)
nokogiri (1.13.3)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
parallel (1.19.1)
parser (2.7.1.1)
ast (~> 2.4.0)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-nav (1.0.0)
pry (>= 0.9.10, < 0.15)
racc (1.6.0)
rainbow (3.0.0)
rake (13.0.1)
Expand Down Expand Up @@ -82,6 +89,8 @@ PLATFORMS
DEPENDENCIES
bundler (~> 2.0)
f_service!
pry
pry-nav
rake (~> 13.0.0)
rspec (~> 3.0)
rubocop (~> 0.82.0)
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,29 @@ class UsersController < BaseController
end
```

Or else it is possible to specify an unhandled option to ensure that the callback will process that message anyway the
error.

```ruby
class UsersController < BaseController
def create
User::Create.(user_params)
.on_success(unhandled: true) { |value| return json_success(value) }
.on_failure(unhandled: true) { |error| return json_error(error) }
j133y marked this conversation as resolved.
Show resolved Hide resolved
end
end
```

```ruby
class UsersController < BaseController
def create
User::Create.(user_params)
.on_success { |value| return json_success(value) }
.on_failure { |error| return json_error(error) }
end
end
```

> You can ignore any of the callbacks, if you want to.

Going further, you can match the Result type, in case you want to handle them differently:
Expand Down
11 changes: 7 additions & 4 deletions lib/f_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ module FService
# Marks a method as deprecated
#
# @api private
def self.deprecate!(name:, alternative:)
warn "[DEPRECATED] #{name} is deprecated; " \
"use #{alternative} instead. " \
'It will be removed on the next release.'
def self.deprecate!(name:, alternative:, from: nil)
warn_message = ["[DEPRECATED] #{name} 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
end
end
65 changes: 59 additions & 6 deletions lib/f_service/result/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ class Base
end
end

# You usually shouldn't call this directly. See {FService::Base#Failure} and {FService::Base#Success}.
def initialize
@handled = false
end

# "Pattern matching"-like method for results.
# It will run the success path if Result is a Success.
# Otherwise, it will run the failure path.
Expand Down Expand Up @@ -70,12 +75,32 @@ def on(success:, failure:)
# end
# end
#
# @example
# class UsersController < BaseController
# def update
# User::Update.(user: user)
# .on_success(:type, :type2) { return json_success({ status: :ok }) } # run only if type matches
# .on_success(unhandled: true) { |value| return json_success(value) }
# .on_failure(unhandled: true) { |error| return json_error(error) } # this won't run
# end
#
# private
#
# def user
# @user ||= User.find_by!(slug: params[:slug])
# end
# end
#
# @yieldparam value value of the failure object
# @yieldparam type type of the failure object
# @return [Success, Failure] the original Result object
# @api public
def on_success(*target_types)
yield(*to_ary) if successful? && expected_type?(target_types)
def on_success(*target_types, unhandled: false)
if successful? && unhandled? && expected_type?(target_types, unhandled: unhandled)
yield(*to_ary)
@handled = true
freeze
end

self
end
Expand All @@ -99,12 +124,32 @@ def on_success(*target_types)
# end
# end
#
# @example
# class UsersController < BaseController
# def update
# User::Update.(user: user)
# .on_success(:unhandled: true) { |value| return json_success(value) } # this won't run
j133y marked this conversation as resolved.
Show resolved Hide resolved
# .on_failure(:type, :type2) { |error| return json_error(error) } # runs only if type matches
# .on_failure(:unhandled: true) { |error| return json_error(error) }
# end
#
# private
#
# def user
# @user ||= User.find_by!(slug: params[:slug])
# end
# end
#
# @yieldparam value value of the failure object
# @yieldparam type type of the failure object
# @return [Success, Failure] the original Result object
# @api public
def on_failure(*target_types)
yield(*to_ary) if failed? && expected_type?(target_types)
def on_failure(*target_types, unhandled: false)
if failed? && unhandled? && expected_type?(target_types, unhandled: unhandled)
yield(*to_ary)
@handled = true
freeze
end

self
end
Expand All @@ -120,8 +165,16 @@ def to_ary

private

def expected_type?(target_types)
target_types.include?(type) || target_types.empty?
def handled?
@handled
end

def unhandled?
!handled?
end

def expected_type?(target_types, unhandled:)
target_types.empty? || unhandled || target_types.include?(type)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/f_service/result/failure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ class Failure < Result::Base
#
# @param error [Object] failure value.
def initialize(error, type = nil)
super()
@error = error
@type = type
bvicenzo marked this conversation as resolved.
Show resolved Hide resolved
freeze
end

# Returns false.
Expand Down
2 changes: 1 addition & 1 deletion lib/f_service/result/success.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ class Success < Result::Base
#
# @param value [Object] success value.
def initialize(value, type = nil)
super()
@value = value
@type = type
freeze
end

# Returns true.
Expand Down
4 changes: 0 additions & 4 deletions spec/f_service/result/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ def initialize; end
end
end

it 'raises error on .new call' do
expect { described_class.new }.to raise_error NotImplementedError, 'called initialize on class Result::Base'
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) }
Expand Down
101 changes: 84 additions & 17 deletions spec/f_service/result/failure_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,34 +79,77 @@
end

describe '#on_failure' do
subject(:on_failure_callback) do
failure.on_failure { |value| value << 1 }
.on_failure(:error) { |value, type| value << type }
.on_failure(:other_error) { |value| value << 3 }
.on_failure(:error, :other_error, :another_error) do |value|
value << "That's no moon"
end
end
describe 'return' do
subject(:on_failure_callback) { failure.on_failure(unhandled: true) { 'some recovering' } }

let(:array) { [] }
let(:failure) { described_class.new(array, :error) }
let(:failure) { described_class.new([], :error) }

it 'returns itself' do
expect(on_failure_callback).to eq failure
it 'returns itself' do
expect(on_failure_callback).to eq failure
end
end

it 'evaluates the given block on failure' do
on_failure_callback
describe 'callback matching' do
context 'when no type is especified' do
subject(:on_failure_callback) { failure.on_failure { |array| array << "That's no moon" } }

let(:array) { [] }
let(:failure) { described_class.new(array, :error) }

before { allow(FService).to receive(:deprecate!) }

it 'handles the error' do
expect { on_failure_callback }.to change { array }.from([]).to(["That's no moon"])
end
end

context 'when type is especified' do
subject(:on_failure_callback) do
failure
.on_failure(:error) { |array, type| array << type }
.on_failure(:other_error) { |array| array << 3 }
.on_failure(unhandled: true) { |array| array << "That's no moon" }
end

let(:array) { [] }
let(:failure) { described_class.new(array, type) }

context 'and no type matches with error type' do
let(:type) { :unknown_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 type' do
let(:type) { :error }

expect(array).to eq [1, :error, "That's no moon"]
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 [:error]
end
end
end
end
end

describe '#on_success' do
subject(:on_success_callback) do
failure.on_success { |value| value << 1 }
failure.on_success(unhandled: true) { |value| value << 1 }
.on_success(:error) { |value| value << 2 }
.on_success { raise "This won't ever run" }
.on_success(unhandled: true) { raise "This won't ever run" }
.on_success(:error, :other_error) { raise 'Chewbacca is a Wookie warrior' }
end

Expand All @@ -117,13 +160,37 @@
expect(on_success_callback).to eq failure
end

it 'keeps the result unfreeze' do
expect(on_success_callback).not_to be_frozen
end

it 'does not evaluate blocks on success' do
on_success_callback

expect(array).to eq []
end
end

describe '#or_else' do
subject(:failure) { described_class.new('User not found', :error) }

it 'returns the given block result' do
expect(failure.or_else { |error| "Failure: #{error}" }).to eq('Failure: User not found')
end
end

describe '#and_then' do
subject(:failure) { described_class.new('Pax', :ok) }

it 'does not yields the block' do
expect { |block| failure.and_then(&block) }.not_to yield_control
end

it 'returns itself' do
expect(failure.and_then { 'an error happened' }).to eq(failure)
end
end

describe '#to_s' do
it { expect(failure.to_s).to eq 'Failure("Whoops!")' }
end
Expand Down
Loading