1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
|
# Uber
_Gem-authoring tools like class method inheritance in modules, dynamic options and more._
## Installation
[![Gem Version](https://badge.fury.io/rb/uber.svg)](http://badge.fury.io/rb/uber)
Add this line to your application's Gemfile:
```ruby
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.
```ruby
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 class attribute with whatever you want - usually a hash or an array.
```ruby
Song.properties #=> [:title, :track]
```
A subclass of `Song` will have a `clone`d `properties` class attribute.
```ruby
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. It is a _copy_ of the original.
```ruby
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`](http://apidock.com/rails/v4.0.2/Class/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.
```ruby
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.
```ruby
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.
```ruby
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.
```ruby
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 `Proc`s, 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.
```ruby
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.
```ruby
options = Uber::Options.new(tags: Tags.new)
```
## Option
`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.
```ruby
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}"
end
```
Use `#call` to evaluate the options at runtime.
```ruby
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.
```ruby
class MyCallable
include Uber::Callable
def call(context, *args)
"callable: #{args.inspect}, #{context}"
end
end
with_callable = Uber::Option[ MyCallable.new ]
```
The context is passed as first argument.
```ruby
with_callable.(Object, 1, 2) #=> "callable: [1, 2] Object"
```
You can also make blocks being `instance_exec`ed on the context, giving a unique API to all option types.
```ruby
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.
```ruby
with_instance_proc.(Object, 1, 2) #=> "proc [1, 2] Object"
```
# 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.
```ruby
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`.
```ruby
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.
# Builder
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.
```ruby
require "uber/builder"
class User
include Uber::Builder
builds do |options|
Admin if params[:admin]
end
end
class Admin
end
```
Note that you can call `builds` as many times as you want per class.
Run the builders using `::build!`.
```ruby
User.build!(User, {}) #=> User
User.build!(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.
```ruby
class User
include Uber::Builder
builds ->(options) do
return SignedIn if params[:current_user]
return Admin if params[:admin]
Anonymous
end
end
```
Note that this allows `return`s 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 `#<<`.
```ruby
MyBuilders = Uber::Builder::Builders.new
MyBuilders << ->(options) do
return Admin if options[:admin]
end
```
Note that you can call `Builders#<<` multiple times per instance.
Invoke the builder using `#call`.
```ruby
MyBuilders.call(User, {}) #=> User
MyBuilders.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_exec`ed in the context you pass into `build!` (or `call`), allowing you to define generic, shareable builders.
```ruby
MyBuilders = Uber::Builder::Builders.new
MyBuilders << ->(options) do
return self::Admin if options[:admin] # note the self:: !
end
class User
class Admin
end
end
class Instructor
class Admin
end
end
```
Now, depending on the context class, the builder will return different classes.
```ruby
MyBuilders.call(User, {}) #=> User
MyBuilders.call(User, { admin: true }) #=> User::Admin
MyBuilders.call(Instructor, {}) #=> Instructor
MyBuilders.call(Instructor, { admin: true }) #=> Instructor::Admin
```
Don't forget the `self::` when writing generic builders, and write tests.
# License
Copyright (c) 2014 by Nick Sutterer <apotonick@gmail.com>
Uber is released under the [MIT License](http://www.opensource.org/licenses/MIT).
|