Project

zx-result

0.0
The project is in a healthy, maintained state
Expose an methods to create result object flow
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 1.14
~> 10.0
~> 3.0
~> 0.70
 Project Readme

🔃 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

  • Installation
  • Usage
    • Success
    • Failure
    • Try
    • Given

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: 'tadeuu@gmail.com' }

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('tadeuu@gmail.com')

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.