Skip to content

Latest commit

 

History

History
449 lines (345 loc) · 10.1 KB

README.md

File metadata and controls

449 lines (345 loc) · 10.1 KB

🔃 Zx::Result

Functional result object for Ruby

Gem Build Status

Motivation

Because in sometimes, we need to create a safe return for our objects. This gem simplify this work.

Documentation

Version Documentation
unreleased https://github.com/thadeu/zx-result/blob/main/README.md

Table of Contents

Compatibility

kind branch ruby
unreleased main >= 2.5.8, <= 3

Installation

Use bundle

bundle add zx-result

or add this line to your application's Gemfile.

gem 'zx-result'

and then, require module

require 'zx'

Configuration

Without configuration, because we use only Ruby. ❤️

Usage

You can use with many towards.

Given

result = Zx.Given { Zx.Success(5) }
result.success? #=> true
result.failure? #=> false
result.value #=> 5
result.value! #=> 5 or raise
result.error #=> nil or raises an exception
input = 5

result = Zx.Given(input)
  .then{ |number| number + 5 }
  .then{ |number| number + 5 }
  .then{ |number| number + 5 }
  .on_success{|number| number }

You can use Given to invoke other methods in the class, like this.

class User
  def self.create(name:, email:)
    new(name:, email:).create
  end

  attr_reader :name, :email

  def initialize(name:, email:)
    @name = name
    @email = email
  end

  def create
    self
  end
end

class Account
  def self.create(user:)
    new(user:).create
  end

  attr_reader :user

  def initialize(user:)
    @user = user
  end

  def create
    self
  end

  def send_welcome_email!
    true
  end
end

class NewsletterMailer
  def self.subscribe!(account:)
    new(account:).subscribe!
  end

  attr_reader :account

  def initialize(account:)
    @account = account
  end

  def subscribe!
    true
  end
end

class AccountCreation
  include Zx

  def self.deliver(input)
    new.deliver(input)
  end
  
  def self.deliver_with_sym(input)
    new.deliver_with_sym(input)
  end

  def deliver(input)
    Given(input)
      .and_then(&method(:create_user))
      .and_then(&method(:create_account))
      .and_then(&method(:subscribe_mailer))
      .and_then(&method(:send_welcome_email!))
  end
  
  def deliver_with_sym(input)
    Given(input)
      .and_then(:create_user)
      .and_then(:create_account)
      .and_then(:subscribe_mailer)
      .and_then(:send_welcome_email!)
  end

  def create_user(input)
    user = User.create(name: input[:name], email: input[:email])
    Success(user:, type: :user_created)
  end

  def create_account(user:, **)
    account = Account.create(user:)
    Success(account:, type: :account_created)
  end

  def subscribe_mailer(account:, **)
    NewsletterMailer.subscribe!(account:)
    Success(account:, type: :mailer_subscribed)
  end

  def send_welcome_email!(account:, **)
    account.send_welcome_email!
    Success(account, type: :email_sent)
  end
end

input = { name: 'Thadeu Esteves', email: '[email protected]' }

account = AccountCreation.deliver(input)
  .on(:success, :user_created) { |user| p [:user_created, user] }
  .on(:success, :account_created) { |acc| p [:account_created, acc] }
  .on(:success, :mailer_subscribed) { |acc| p [:mailer_subscribed, acc] }
  .on(:success, :email_sent) { |acc| p [:email_sent, acc] }
  .on(:failure, :user_not_created) { |error| p [:user_not_created, error] }
  .otherwise { |error| p [:otherwise, error] }

expect(account.success?).to be_truthy
expect(account.type).to eq(:email_sent)
expect(account.unwrap.user.name).to eq('Thadeu Esteves')
expect(account.unwrap.user.email).to eq('[email protected]')

Success

result = Zx.Success(5)
result.success? #=> true
result.failure? #=> false
result.value #=> 5
result.value! #=> 5 or raise
result.error #=> nil or raises an exception
result = Zx.Success(5, type: :integer)
result.success? #=> true
result.failure? #=> false
result.value #=> 5
result.value! #=> 5 or raise
result.error #=> nil or raises an exception
result.type #=> :integer

Failure

result = Zx.Failure(:fizz)
result.success? #=> false
result.failure? #=> true
result.value #=> raises an exception
result.error #=> :fizz
result.type #=> :error
result = Zx.Failure(:fizz, type: :not_found)
result.success? #=> false
result.failure? #=> true
result.value #=> raises an exception
result.error #=> :fizz
result.type #=> :not_found

Map or Then

result = Zx.Success(5, type: :integer)
  .fmap{ |number| number + 5 }
  .fmap{ |number| number + 5 }
  .fmap{ |number| number + 5 }
  .on_success(:integer) {|number| puts number } #=> 20
  .on(:success, :integer) {|number| puts number } #=> 20
  .on_success {|number| puts number } #=> 20

result.success? #=> true
result.failure? #=> false
result.value #=> 20
result.value! #=> 20 or raise
result.error #=> nil or raises a  n exception
result.type #=> :integer
result = Zx.Success(5, type: :integer)
  .then{ |number| number + 5 }
  .then{ |number| number + 5 }
  .then{ |number| number + 5 }
  .on_success{|number| puts number } #=> 20

result.success? #=> true
result.failure? #=> false
result.value #=> 20
result.value! #=> 20 or raise
result.error #=> nil or raises an exception
result.type #=> :integer

Step or Check

result = Zx.Success(5, type: :integer)
  .step{ |number| number + 5 }
  .on_success(:integer) {|number| puts number } #=> 10
  .on(:success, :integer) {|number| puts number } #=> 10
  .on_success {|number| puts number } #=> 10

result.success? #=> true
result.failure? #=> false
result.value #=> 10
result.value! #=> 10 or raise
result.error #=> nil or raises a  n exception
result.type #=> :integer
result = Zx.Success(5, type: :integer)
  .step{ |number| number + 5 }
  .check { |number| number == 10 }
  .on_success{|number| puts number } #=> 10

result.success? #=> true
result.failure? #=> false
result.value #=> 10
result.value! #=> 10 or raise
result.error #=> nil or raises an exception
result.type #=> :integer
result = Zx.Success(5, type: :integer)
  .step{ |number| number + 5 }
  .check(:number_valid) { |number| number == 15 }
  .on_failure { |error| puts error } #=> 10

Try

result = Zx.Try { Zx.Success(5) }
  .step { |number| number + 5 }
  .check(:number_invalid) { |number| number == 15 }
  .on_failure{ |error, (type)| puts [error, type] } # failure! because, number == 10, right?
  .on_success{ |number| puts number }
result = Zx.Try { Zx.Success(10) }
  .step { |number| number + 1 }
  .then { |number| number + 1 }
  .and_then { |number| number + 1 }
  .fmap { |number| number + 1 }
  .check(:number_invalid) { |number| number == 15 }
  .on_failure{ |error, (type)| puts [:failure, error, type] }
  .on_success{ |number| puts [:success, number] }
result = Zx.Try { Zx.Failure(10) }
  .step { |number| number + 1 }
  .then { |number| number + 1 }
  .and_then { |number| number + 1 }
  .fmap { |number| number + 1 }
  .check(:number_invalid) { |number| number == 15 }
  .on_failure{ |error, (type)| puts [:failure, error, type] }
  .on_success{ |number| puts [:success, number] }

You can use one or multiples listeners in your result. We see some use cases.

Simple composition

class AsIncluded
  include Zx

  def pass(...)
    Success(...)
  end

  def passthrough(value)
    Success[value]
  end

  def failed(error)
    Failure[error, type: :error]
  end
end

result = AsIncluded.new.pass('save record!')

result
  .on(:success, :success) { expect(_1).to eq(a: 1) }
  .on(:success, :mailer) { expect(_1).to eq(a: 1) }
  .on(:success, :persisted) { expect(_1).to eq('save record!') }
  .on(:success) { |value, (type)| expect([value, type]).to eq(['save record!', :persisted]) }
  .on(:failure, :error) { expect(_1).to eq('on error') }
  .on(:failure, :record_not_found) { expect(_1).to eq('not found user') }

Match

result = AsIncluded.new.pass('save record!')

result.match(
  Ok: ->(v) { [:ok, v] },
  Err: ->(err) { raise [:err, err] }
)

result.match(
  Ok: -> { expect(_1).to eq(2) },
  Err: -> { raise [:err, _1] }
)

Otherwise

result = Zx.Failure('invalid type tagged')

result
  .on_success(:jump_this) { [:jump_this, _1] }
  .on_failure(:jump_this1) { [:jump_this1, _1] }
  .on_failure(:jump_this2) { [:jump_this2, _1] }
  .otherwise { p [:otherwise, _1] }

You can use directly methods, for example:

Zx.Success(relation)

# or

Zx::Success[relation]
Zx:.Failure('error', type: :invalid)

# or

Zx::Failure['user was not found', { type: :invalid_user }]

⬆️  Back to Top

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bundle exec rspec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/thadeu/zx-result. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.