Skip to content

olegantonyan/typerb

Repository files navigation

Gem Version CI RSpec & Rubocop

Typerb

Proof of concept type-checking library for Ruby 2.6. Works with previous versions too with some limitation (see below).

class A
  using Typerb

  def call(some_arg)
    some_arg.type!(String, Symbol)
  end

  def call_with_respond_checks(some_arg)
    some_arg.respond_to!(:strip)
  end

  def call_with_enum(arg)
    arg.enum!(:one, :two)
  end

  def call_with_subset(arg)
    arg.subset_of!([:one, :two])
  end
end

A.new.call(1) #=> TypeError: `some_arg` should be String or Symbol, not Integer
A.new.call_with_respond_checks(1) #=> TypeError: 'Integer should respond to all methods: strip'
A.new.call_with_enum(:three) #=> TypeError: 'Symbol (`arg`) should be one of: [one, two], not three'
A.new.call_with_subset([:one, :three]) #=> TypeError: 'Array (`arg`) should be subset of: [:one, :two], not [:one, :three]'

This is equivalent to:

class A
  def call(some_arg)
    raise TypeError, "`some_arg` should be String or Symbol, not #{some_arg.class}" unless [String, Symbol].include?(some_arg.class)
  end

  def call_with_respond_checks(some_arg)
    raise TypeError, "#{some_arg.class} should respond to all methods: strip" unless [:strip].all{|meth| some_arg.respond_to?(meth)}
  end
end

But without boilerplate.

It also has not_nil! method, similar to Crystal language.

class A
  using Typerb

  def call(some_arg)
    some_arg.not_nil!
  end
end

A.new.call(nil) #=> TypeError: expected not nil, but got nil

Why?

  1. Catch error as early as possible (especially nils);
  2. Additional documentation: you're telling other people more about interfaces.

Installation

Add this line to your application's Gemfile:

gem 'typerb'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install typerb

If this fails with error

ERROR:  Error installing typerb:
        There are no versions of typerb (>= 0) compatible with your Ruby & RubyGems
        typerb requires Ruby version >= 2.6.0.pre.preview3. The current ruby version is 2.6.0.

even when you have Ruby 2.6.0-preview3 installed, then try installing it through Gemfile from git:

gem 'typerb', github: 'olegantonyan/typerb'

Usage

  1. Add using Typerb to a class where you want to have type check.
  2. Call .type!() on any object to assert its type.
  3. PROFIT! No more "NoMethodError for nil" 10 methods up the stack. You'll know exactly where this nil came from.
class A
  using Typerb

  attr_reader :param, :another_param

  def initialize(param, another_param)
    @param = param.type!(String)
    @another_param = another_param.not_nil!
  end
end

If you're unfamiliar with using keyword - this is refinement - a relatively new feature in Ruby (since 2.0). It's kind of monkey-patch, but with strict scope. Learn more about refinements.

This refinement adds type!() and not_nil! methods to BasicObject class so you can call it on any object.

The method will raise an exception if self is not an instance of one of the classes passed as arguments. The tricky part, however, is to get the variable name on which it's called. You need this to get a nice error message telling you exactly which variable has wrong type, not just an abstract TypeError. That's why we need Ruby 2.6 with its new RubyVM::AST (https://ruby-doc.org/core-2.6.0.preview3/RubyVM/AST.html).

Limitations

Full functionality Ruby 2.6.0-preview3. Relies on RubyVM::AST which may change in release version. So, expect breaking changes in Ruby. Previous versions also supported, but without variable name in exception message.

Known limitations:

  1. Multi-line method call:
class A
  using Typerb

  def call(some_arg)
    some_arg.
            type!(String)
    # this won't work. type!() call must be on the same line with the variable it's called on - raise error message without variable name
    # some_arg.    type!(String) is ok though
  end
end
  1. Method defined in console:
[1] pry(main)> class A
[1] pry(main)*   using Typerb
[1] pry(main)*   def call(a)
[1] pry(main)*     a.type!(Hash)
[1] pry(main)*   end
[1] pry(main)* end
[2] pry(main)> A.new.call(1)
TypeError: expected Hash, got Integer
# here we cannot get the source code for a line containing "a.type!(Hash)", so cannot see the variable name
  1. Multiple arguments on the same line:
class A
  using Typerb

  def initialize(arg1, arg2)
    arg1.type!(Integer); arg2.type!(String)
    # no way to tell the variable - raise error message without variable name
    # same error will be raised on Ruby < 2.6.0 because there is no RubyVM::AST
  end
end

These limitations shouldn't be a problem in any case. Please, file an issue if you know a scenario where one of these could be a real problem.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec 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 exec rake 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/olegantonyan/typerb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

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

Code of Conduct

Everyone interacting in the Typerb project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

About

Strong type checking (assertion) for Ruby

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published