File: README.md

package info (click to toggle)
ruby-uber 0.1.0-1.1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, sid
  • size: 188 kB
  • sloc: ruby: 466; makefile: 3
file content (355 lines) | stat: -rw-r--r-- 9,701 bytes parent folder | download | duplicates (3)
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).