Gem Version Build Status License

TL;DR

Tron is a minimalistic combination of a monad and value object, implemented in a few lines of code.

Return Tron.success(:it_worked) or Tron.failure(:aww_too_bad) from a method to explain why and how it succeded/failed. That returns an immutable Struct (value object) that responds to result.success? and result.failure?.

The reason is accessible in result.success #=> :it_worked. You can add more metadata as a second argument: Tron.failure(:nopes, error_code: 404) which you can access like a Struct: result.error_code #=> 404.

Chaining can make your code cleaner: result.on_success { download }.on_failure { show_message }

Introduction

Imagine you have a class like this:

class User
  def self.delete(id)
    @users.delete id
  end
end

It's not clear from the code what this method returns. true?, a User?, a user ID?. What if a network error occurs, how does anyone calling User.delete 42 know what happened?

Indeed, it is not even clear what "successful" means in the context of this message - if there is no user and you try to delete one, is that considered a "failure"?

Let's rewrite the method using Tron:

class User
  def self.delete(id)
    return Tron.failure(:id_missing) unless id
    return Tron.failure(:invalid_id, id: id) unless id.match /[a-f]{8}/

    user = @users[id]
    if @users.delete id
      Tron.success :user_deleted, user: user
    else
      Tron.success :deletion_failed, id: id
    end

  rescue ConnectionError
    Tron.failure :deletion_failed_badly, id: id
  end
end

One could even take it a step further and write it like this:

class User
  def self.delete(id)
    check_id_syntax(id).on_success { delete_user(id) }
                       .on_success { send_sms }
                       .on_success { redirect }
  end

  def self.check_id_syntax(id)
    return Tron.failure(:id_missing) unless id
    return Tron.failure(:invalid_id, id: id) unless id.match /[a-f]{8}/

    Tron.success :id_looks_good
  end

  def self.delete_user(id)
    user = @users[id]

    if @users.delete id
      Tron.success :user_deleted, user: user
    else
      Tron.success :deletion_failed, id: id
    end

  rescue ConnectionError => ex
    Tron.failure :deletion_failed_badly, id: id, message: ex.message
  end
end

So what are the benefits?

1. It will give you robust and predictable code

Tron will give you this consistent, implementation-unaware, programming convention:

result = User.delete 42

if result.success?
  puts "It worked! You deleted the user #{result.user.first_name}"
else
  puts "Aw, couldn't delete User with ID #{result.id} because #{result.failure}"
end

The result is just a Struct:

result = User.delete 42

# Query whether it worked
result.success? # => false
result.failure? # => true

# Query why and how
result.success # => nil
result.failure # => :deletion_failed_badly

# Access immutable metadata
result.message # => "..."
result.inspect # => "#<struct failure=:alright, user_id=42, message='...'>"

result.message.upcase! # => modification raises an exception

2. If will give you better tests

How would you test this code?

class Product
  def self.delete(id)
    return false if id.blank?
    return false unless product = Products.find(id)
    return false unless permission?
    api.update(id, attributes)
  end

  def self.permission?
    Date.today.sunday?
  end
end

You cannot simply test for the false as expected return value because it could mean anything. Tron helps you to check the response objects for every case.

3. It gives you documentation

While the code you're writing becomes slightly more verbose, that verbosity translates directly into documenation. You see immediately what each line is doing.

Upgrading from 1.x.x to 2.0.0

  • 1.2.0 and 2.0.0 are identical, except that all deprecations have been removed and don't work any more.

Upgrading from 0.x.x to 1.x.x

  • Don't use include Tron, it is not useful any more. There are no subclasses you might want to access.
  • Replace Tron::Success.call with Tron.success (same for failure). The syntax is identical.
  • The result object is now a Struct and has no #meta and #metadata methods anymore.
  • The result object does not respond to #code any more. Instead use #success and #failure respectively. This is so that you can use #code as metadata, and also so that you can query the code via #success immediately, without first having to check #success?.

Background

Tron is a complete rewrite of its predecessor operation. I got inspired by the deterministic gem, which is the follow-up of the monadic gem. There are some complicated structs so I got inspired by this robust implementation and simplified it even more.

Requirements

  • Ruby >= 2.5.0

MIT 2015-2019 halo. See MIT-LICENSE.

Caveats

  • There are no setter methods in the returned Struct, so you cannot overwrite the metadata. The values are also frozen, so you don't accidentally modify the attributes in-place. However, they are not deep-frozen, so an object may still be modified if you're ignorant.