Gem-authoring extensions for classes and modules.
Latest commit b254e0c Nov 10, 2016 @apotonick releasing 0.1.0. πŸŽ‰πŸ’ͺ🏽
*ducks away*


Gem-authoring tools like class method inheritance in modules, dynamic options and more.


Gem Version

Add this line to your application's Gemfile:

gem 'uber'

Uber runs with Ruby >= 1.9.3.

Inheritable Class Attributes

If you want inherited class attributes, this is for you. This is a mandatory mechanism for creating DSLs.

require 'uber/inheritable_attr'

class Song
  extend Uber::InheritableAttr

  inheritable_attr :properties = [:title, :track] # initialize it before using it.

Note that you have to initialize your class attribute with whatever you want - usually a hash or an array. #=> [:title, :track]

A subclass of Song will have a cloned properties class attribute.

class Hit < Song
end #=> [:title, :track]

The cool thing about the inheritance is: you can work on the inherited attribute without any restrictions. It is a copy of the original. << :number  #=> [:title, :track, :number] #=> [:title, :track]

It's similar to ActiveSupport's class_attribute but with a simpler implementation. It is less dangerous. There are no restrictions for modifying the attribute. compared to class_attribute.

Uncloneable Values

::inheritable_attr will clone values to copy them to subclasses. Uber won't attempt to clone Symbol, nil, true and false per default.

If you assign any other unclonable value you need to tell Uber that.

class Song
  extend Uber::InheritableAttr
  inheritable_attr :properties, clone: false

This won't clone but simply pass the value on to the subclass.

Dynamic Options

Implements the pattern of defining configuration options and dynamically evaluating them at run-time.

Usually DSL methods accept a number of options that can either be static values, symbolized instance method names, or blocks (lambdas/Procs).

Here's an example from Cells.

cache :show, tags: lambda { Tag.last }, expires_in: 5.mins, ttl: :time_to_live

Usually, when processing these options, you'd have to check every option for its type, evaluate the tags: lambda in a particular context, call the #time_to_live instance method, etc.

This is abstracted in Uber::Options and could be implemented like this.

require 'uber/options'

options =       lambda { Tag.last },
                            expires_in: 5.mins,
                            ttl:        :time_to_live)

Just initialize Options with your actual options hash. While this usually happens on class level at compile-time, evaluating the hash happens at run-time.

class User < ActiveRecord::Base # this could be any Ruby class.
  # .. lots of code

  def time_to_live(*args)

user = User.find(1)

options.evaluate(user, *args) #=> {tags: "hot", expires_in: 300, ttl: "n/a"}

Evaluating Dynamic Options

To evaluate the options to a real hash, the following happens:

  • The tags: lambda is executed in user context (using instance_exec). This allows accessing instance variables or calling instance methods.
  • Nothing is done with expires_in's value, it is static.
  • user.time_to_live? is called as the symbol :time_to_live indicates that this is an instance method.

The default behaviour is to treat Procs, lambdas and symbolized :method names as dynamic options, everything else is considered static. Optional arguments from the evaluate call are passed in either as block or method arguments for dynamic options.

This is a pattern well-known from Rails and other frameworks.


A third way of providing a dynamic option is using a "callable" object. This saves you the unreadable lambda syntax and gives you more flexibility.

require 'uber/callable'
class Tags
  include Uber::Callable

  def call(context, *args)

By including Uber::Callable, uber will invoke the #call method on the specified object.

Note how you simply pass an instance of the callable object into the hash instead of a lambda.

options =


Uber::Option implements the pattern of taking an option, such as a proc, instance method name, or static value, and evaluate it at runtime without knowing the option's implementation.

Creating Option instances via ::[] usually happens on class-level in DSL methods.

with_proc    = Uber::Option[ ->(options) { "proc: #{options.inspect}" } ]
with_static  = Uber::Option[ "Static value" ]
with_method  = Uber::Option[ :name_of_method ]

def name_of_method(options)
  "method: #{options.inspect}"

Use #call to evaluate the options at runtime.

with_proc.(1, 2)         #=> "proc: [1, 2]"
with_static.(1, 2)       #=> "Static value"   # arguments are ignored
with_method.(self, 1, 2) #=> "method: [1, 2]" # first arg is context

It's also possible to evaluate a callable object. It has to be marked with Uber::Callable beforehand.

class MyCallable
  include Uber::Callable

  def call(context, *args)
    "callable: #{args.inspect}, #{context}"

with_callable = Uber::Option[ ]

The context is passed as first argument.

with_callable.(Object, 1, 2) #=> "callable: [1, 2] Object"

You can also make blocks being instance_execed on the context, giving a unique API to all option types.

with_instance_proc  = Uber::Option[ ->(options) { "proc: #{options.inspect} #{self}" }, instance_exec: true ]

The first argument now becomes the context, exactly the way it works for the method and callable type.

with_instance_proc.(Object, 1, 2) #=> "proc [1, 2] Object"


Using ::delegates works exactly like the Forwardable module in Ruby, with one bonus: It creates the accessors in a module, allowing you to override and call super in a user module or class.

require 'uber/delegates'

class SongDecorator
  def initialize(song)
    @song = song
  attr_reader :song

  extend Uber::Delegates

  delegates :song, :title, :id # delegate :title and :id to #song.

  def title
    super.downcase # this calls the original delegate #title.

This creates readers #title and #id which are delegated to #song.

song = 1, title: "HELLOWEEN!")) #=> 1
song.title #=> "helloween!"

Note how #title calls the original title and then downcases the string.


Builders are good for polymorphically creating objects without having to know where that happens. You define a builder with conditions in one class, and that class takes care of creating the actual desired class.

Declarative Interface

Include Uber::Builder to leverage the ::builds method for adding builders, and ::build! to run those builders in a given context and with arbitrary options.

require "uber/builder"

class User
  include Uber::Builder

  builds do |options|
    Admin if params[:admin]

class Admin

Note that you can call builds as many times as you want per class.

Run the builders using ::build!.!(User, {})              #=> User!(User, { admin: true }) #=> Admin

The first argument is the context in which the builder blocks will be executed. This is also the default return value if all builders returned a falsey value.

All following arguments will be passed straight through to the procs.

Your API should communicate User as the only public class, since the builder hides details about computing the concrete class.

Builder: Procs

You may also use procs instead of blocks.

class User
  include Uber::Builder

  builds ->(options) do
    return SignedIn if params[:current_user]
    return Admin    if params[:admin]

Note that this allows returns in the block.

Builder: Direct API

In case you don't want the builds DSL, you can instantiate a Builders object yourself and add builders to it using #<<.

MyBuilders =
MyBuilders << ->(options) do
  return Admin if options[:admin]

Note that you can call Builders#<< multiple times per instance.

Invoke the builder using #call., {})              #=> User, { admin: true }) #=> Admin

Again, the first object is the context/default return value, all other arguments are passed to the builder procs.

Builder: Contexts

Every proc is instance_execed in the context you pass into build! (or call), allowing you to define generic, shareable builders.

MyBuilders =
MyBuilders << ->(options) do
  return self::Admin if options[:admin] # note the self:: !

class User
  class Admin

class Instructor
  class Admin

Now, depending on the context class, the builder will return different classes., {})              #=> User, { admin: true }) #=> User::Admin, {})              #=> Instructor, { admin: true }) #=> Instructor::Admin

Don't forget the self:: when writing generic builders, and write tests.


Copyright (c) 2014 by Nick Sutterer

Uber is released under the MIT License.