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


Add this line to your application's Gemfile:

gem 'uber'


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
  self.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.

Song.properties #=> [:title, :track]

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

class Hit < Song

Hit.properties #=> [: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.

Hit.properties << :number

Hit.properties  #=> [:title, :track, :number]
Song.properties #=> [: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.

This module is used across several other gems like Cells, Representable, Roar and Reform.

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 = Uber::Options.new(tags:       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::Options.new(tags: Tags.new)

Evaluating Elements

If you want to evaluate a single option element, use #eval.

options.eval(:ttl, user) #=> "n/a"

Single Values

Sometimes you don't need an entire hash but a dynamic value, only.

value = Uber::Options::Value.new(lambda { |volume| volume < 0 ? 0 : volume })

value.evaluate(context, -122.18) #=> 0

Use Options::Value#evaluate to handle single values.


Evaluating an options hash can be time-consuming. When Options contains static elements only, it behaves and performs like an ordinary hash.


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 = SongDecorator.new(Song.create(id: 1, title: "HELLOWEEN!"))

song.id #=> 1
song.title #=> "helloween!"

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


When included, Builder allows to add builder instructions on the class level. These can then be evaluated when instantiating the class to conditionally build (sub-)classes based on the incoming parameters.

class Listener
  include Uber::Builder

  builds do |params|
    SignedIn if params[:current_user]

class SignedIn

The class then has to use the builder to compute a class name using the build blocks you defined.

class Listener
  def self.build(params)

As you can see, it's still up to you to instantiate the object, the builder only helps you computing the concrete class.

Listener.build({}) #=> Listener
Listener.build({current_user: @current_user}) #=> SignedIn

Note that builders are not inherited to subclasses. This allows instantiating subclasses directly without running builders.

This pattern is used in Cells, Trailblazer and soon Reform and Representable/Roar, too.


Writing gems against other gems often involves checking for versions and loading appropriate version strategies - e.g. "is Rails >= 4.0?". Uber gives you Version for easy, semantic version deciders.

version = Uber::Version.new("1.2.3")

The API currently gives you #>= and #~.

version >= "1.1" #=> true
version >= "1.3" #=> false

The ~ method does a semantic check (currently on major and minor level, only).

version.~ "1.1" #=> false
version.~ "1.2" #=> true
version.~ "1.3" #=> false

Accepting a list of versions, it makes it simple to check for multiple minor versions.

version.~ "1.1", "1.0" #=> false
version.~ "1.1", "1.2" #=> true

Undocumented Features

(Please don't read this!)

  • You can enforce treating values as dynamic (or not): Uber::Options::Value.new("time_to_live", dynamic: true) will always run #time_to_live as an instance method on the context, even though it is not a symbol.


Copyright (c) 2014 by Nick Sutterer [email protected]

Uber is released under the MIT License.