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.
Evaluating Elements
If you wanna 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
When writing gems against other gems 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.