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'
= 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)
.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 inusercontext (usinginstance_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_liveindicates 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.
= Uber::Options.new(tags: Tags.new)
Evaluating Elements
If you want to evaluate a single option element, use #eval.
.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.
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_liveas 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.