Uber

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

Installation

Add this line to your application's Gemfile:

gem 'uber'

Ready?

Inheritable Class Attributes

This is for you if you want class attributes to be inherited, which 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.
end

Note that you have to initialize your attribute which whatever you want - usually a hash or an array.

You can now use that attribute on the class level.

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

Inheriting from Song will result in the properties object being cloned to the sub-class.

class Hit < Song
end

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

The cool thing about the inheritance is: you can work on the inherited attribute without any restrictions, as 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 resulting in a less dangerous potential. Also, there is no restriction about the way you modify the attribute as found in class_attribute.

This module is very popular amongst numerous 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)
    "n/a"
  end
end

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.

Uber::Callable

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)
    [:comment]
  end
end

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.

Performance

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

Delegates

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

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.

Version

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 thou it is not a symbol.

License

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

Roar is released under the MIT License.