declarative-builder 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +10 -0
- data/CHANGES.md +3 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.md +355 -0
- data/Rakefile +13 -0
- data/declarative-builder.gemspec +22 -0
- data/lib/declarative-builder.rb +1 -0
- data/lib/declarative/builder.rb +44 -0
- data/lib/declarative/builder/version.rb +5 -0
- data/test/builder_test.rb +147 -0
- data/test/test_helper.rb +1 -0
- metadata +101 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 8a7a21a5da3c82bf9edcbfc53a0b493e3c886ad0
|
|
4
|
+
data.tar.gz: 62bb129cb86b262e81792e59bb83ec841ec8df7e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 574cc0bb1eb623eb61c57b84d156470e400f864f4fc840a1c391826191399f4716a136c4e84de11f666c63397d8217447360efe1d5198c4202e1a5dd69b11e57
|
|
7
|
+
data.tar.gz: 67a36117a847c5069ac2131eadcb75e9a1116ae1e59a486df79917934dcbbe945b4af59bc8f32b348c9451fd0ee5b0212ed24cd7ecb1d2c869a85b75a6a59f8d
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGES.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2012 Nick Sutterer
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# Uber
|
|
2
|
+
|
|
3
|
+
_Gem-authoring tools like class method inheritance in modules, dynamic options and more._
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
[](http://badge.fury.io/rb/uber)
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'uber'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Uber runs with Ruby >= 1.9.3.
|
|
16
|
+
|
|
17
|
+
# Inheritable Class Attributes
|
|
18
|
+
|
|
19
|
+
If you want inherited class attributes, this is for you.
|
|
20
|
+
This is a mandatory mechanism for creating DSLs.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require 'uber/inheritable_attr'
|
|
24
|
+
|
|
25
|
+
class Song
|
|
26
|
+
extend Uber::InheritableAttr
|
|
27
|
+
|
|
28
|
+
inheritable_attr :properties
|
|
29
|
+
self.properties = [:title, :track] # initialize it before using it.
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Note that you have to initialize your class attribute with whatever you want - usually a hash or an array.
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
Song.properties #=> [:title, :track]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
A subclass of `Song` will have a `clone`d `properties` class attribute.
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class Hit < Song
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Hit.properties #=> [:title, :track]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The cool thing about the inheritance is: you can work on the inherited attribute without any restrictions. It is a _copy_ of the original.
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
Hit.properties << :number
|
|
52
|
+
|
|
53
|
+
Hit.properties #=> [:title, :track, :number]
|
|
54
|
+
Song.properties #=> [:title, :track]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
It's similar to ActiveSupport's `class_attribute` but with a simpler implementation.
|
|
58
|
+
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).
|
|
59
|
+
|
|
60
|
+
## Uncloneable Values
|
|
61
|
+
|
|
62
|
+
`::inheritable_attr` will `clone` values to copy them to subclasses. Uber won't attempt to clone `Symbol`, `nil`, `true` and `false` per default.
|
|
63
|
+
|
|
64
|
+
If you assign any other unclonable value you need to tell Uber that.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
class Song
|
|
68
|
+
extend Uber::InheritableAttr
|
|
69
|
+
inheritable_attr :properties, clone: false
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This won't `clone` but simply pass the value on to the subclass.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Dynamic Options
|
|
76
|
+
|
|
77
|
+
Implements the pattern of defining configuration options and dynamically evaluating them at run-time.
|
|
78
|
+
|
|
79
|
+
Usually DSL methods accept a number of options that can either be static values, symbolized instance method names, or blocks (lambdas/Procs).
|
|
80
|
+
|
|
81
|
+
Here's an example from Cells.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
cache :show, tags: lambda { Tag.last }, expires_in: 5.mins, ttl: :time_to_live
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
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.
|
|
88
|
+
|
|
89
|
+
This is abstracted in `Uber::Options` and could be implemented like this.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
require 'uber/options'
|
|
93
|
+
|
|
94
|
+
options = Uber::Options.new(tags: lambda { Tag.last },
|
|
95
|
+
expires_in: 5.mins,
|
|
96
|
+
ttl: :time_to_live)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
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.
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
class User < ActiveRecord::Base # this could be any Ruby class.
|
|
103
|
+
# .. lots of code
|
|
104
|
+
|
|
105
|
+
def time_to_live(*args)
|
|
106
|
+
"n/a"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
user = User.find(1)
|
|
111
|
+
|
|
112
|
+
options.evaluate(user, *args) #=> {tags: "hot", expires_in: 300, ttl: "n/a"}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Evaluating Dynamic Options
|
|
116
|
+
|
|
117
|
+
To evaluate the options to a real hash, the following happens:
|
|
118
|
+
|
|
119
|
+
* The `tags:` lambda is executed in `user` context (using `instance_exec`). This allows accessing instance variables or calling instance methods.
|
|
120
|
+
* Nothing is done with `expires_in`'s value, it is static.
|
|
121
|
+
* `user.time_to_live?` is called as the symbol `:time_to_live` indicates that this is an instance method.
|
|
122
|
+
|
|
123
|
+
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.
|
|
124
|
+
|
|
125
|
+
This is a pattern well-known from Rails and other frameworks.
|
|
126
|
+
|
|
127
|
+
## Uber::Callable
|
|
128
|
+
|
|
129
|
+
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.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
require 'uber/callable'
|
|
133
|
+
class Tags
|
|
134
|
+
include Uber::Callable
|
|
135
|
+
|
|
136
|
+
def call(context, *args)
|
|
137
|
+
[:comment]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
By including `Uber::Callable`, uber will invoke the `#call` method on the specified object.
|
|
143
|
+
|
|
144
|
+
Note how you simply pass an instance of the callable object into the hash instead of a lambda.
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
options = Uber::Options.new(tags: Tags.new)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Option
|
|
151
|
+
|
|
152
|
+
`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.
|
|
153
|
+
|
|
154
|
+
Creating `Option` instances via `::[]` usually happens on class-level in DSL methods.
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
with_proc = Uber::Option[ ->(options) { "proc: #{options.inspect}" } ]
|
|
158
|
+
with_static = Uber::Option[ "Static value" ]
|
|
159
|
+
with_method = Uber::Option[ :name_of_method ]
|
|
160
|
+
|
|
161
|
+
def name_of_method(options)
|
|
162
|
+
"method: #{options.inspect}"
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Use `#call` to evaluate the options at runtime.
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
with_proc.(1, 2) #=> "proc: [1, 2]"
|
|
170
|
+
with_static.(1, 2) #=> "Static value" # arguments are ignored
|
|
171
|
+
with_method.(self, 1, 2) #=> "method: [1, 2]" # first arg is context
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
It's also possible to evaluate a callable object. It has to be marked with `Uber::Callable` beforehand.
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
class MyCallable
|
|
178
|
+
include Uber::Callable
|
|
179
|
+
|
|
180
|
+
def call(context, *args)
|
|
181
|
+
"callable: #{args.inspect}, #{context}"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
with_callable = Uber::Option[ MyCallable.new ]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The context is passed as first argument.
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
with_callable.(Object, 1, 2) #=> "callable: [1, 2] Object"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
You can also make blocks being `instance_exec`ed on the context, giving a unique API to all option types.
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
with_instance_proc = Uber::Option[ ->(options) { "proc: #{options.inspect} #{self}" }, instance_exec: true ]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The first argument now becomes the context, exactly the way it works for the method and callable type.
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
with_instance_proc.(Object, 1, 2) #=> "proc [1, 2] Object"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# Delegates
|
|
208
|
+
|
|
209
|
+
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.
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
require 'uber/delegates'
|
|
213
|
+
|
|
214
|
+
class SongDecorator
|
|
215
|
+
def initialize(song)
|
|
216
|
+
@song = song
|
|
217
|
+
end
|
|
218
|
+
attr_reader :song
|
|
219
|
+
|
|
220
|
+
extend Uber::Delegates
|
|
221
|
+
|
|
222
|
+
delegates :song, :title, :id # delegate :title and :id to #song.
|
|
223
|
+
|
|
224
|
+
def title
|
|
225
|
+
super.downcase # this calls the original delegate #title.
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
This creates readers `#title` and `#id` which are delegated to `#song`.
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
song = SongDecorator.new(Song.create(id: 1, title: "HELLOWEEN!"))
|
|
234
|
+
|
|
235
|
+
song.id #=> 1
|
|
236
|
+
song.title #=> "helloween!"
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Note how `#title` calls the original title and then downcases the string.
|
|
240
|
+
|
|
241
|
+
# Builder
|
|
242
|
+
|
|
243
|
+
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.
|
|
244
|
+
|
|
245
|
+
## Declarative Interface
|
|
246
|
+
|
|
247
|
+
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.
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
require "uber/builder"
|
|
252
|
+
|
|
253
|
+
class User
|
|
254
|
+
include Uber::Builder
|
|
255
|
+
|
|
256
|
+
builds do |options|
|
|
257
|
+
Admin if params[:admin]
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
class Admin
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Note that you can call `builds` as many times as you want per class.
|
|
266
|
+
|
|
267
|
+
Run the builders using `::build!`.
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
User.build!(User, {}) #=> User
|
|
271
|
+
User.build!(User, { admin: true }) #=> Admin
|
|
272
|
+
```
|
|
273
|
+
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.
|
|
274
|
+
|
|
275
|
+
All following arguments will be passed straight through to the procs.
|
|
276
|
+
|
|
277
|
+
Your API should communicate `User` as the only public class, since the builder hides details about computing the concrete class.
|
|
278
|
+
|
|
279
|
+
### Builder: Procs
|
|
280
|
+
|
|
281
|
+
You may also use procs instead of blocks.
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
class User
|
|
285
|
+
include Uber::Builder
|
|
286
|
+
|
|
287
|
+
builds ->(options) do
|
|
288
|
+
return SignedIn if params[:current_user]
|
|
289
|
+
return Admin if params[:admin]
|
|
290
|
+
Anonymous
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Note that this allows `return`s in the block.
|
|
296
|
+
|
|
297
|
+
## Builder: Direct API
|
|
298
|
+
|
|
299
|
+
In case you don't want the `builds` DSL, you can instantiate a `Builders` object yourself and add builders to it using `#<<`.
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
MyBuilders = Uber::Builder::Builders.new
|
|
303
|
+
MyBuilders << ->(options) do
|
|
304
|
+
return Admin if options[:admin]
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Note that you can call `Builders#<<` multiple times per instance.
|
|
309
|
+
|
|
310
|
+
Invoke the builder using `#call`.
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
MyBuilders.call(User, {}) #=> User
|
|
314
|
+
MyBuilders.call(User, { admin: true }) #=> Admin
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Again, the first object is the context/default return value, all other arguments are passed to the builder procs.
|
|
318
|
+
|
|
319
|
+
## Builder: Contexts
|
|
320
|
+
|
|
321
|
+
Every proc is `instance_exec`ed in the context you pass into `build!` (or `call`), allowing you to define generic, shareable builders.
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
MyBuilders = Uber::Builder::Builders.new
|
|
325
|
+
MyBuilders << ->(options) do
|
|
326
|
+
return self::Admin if options[:admin] # note the self:: !
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
class User
|
|
330
|
+
class Admin
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
class Instructor
|
|
335
|
+
class Admin
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Now, depending on the context class, the builder will return different classes.
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
MyBuilders.call(User, {}) #=> User
|
|
344
|
+
MyBuilders.call(User, { admin: true }) #=> User::Admin
|
|
345
|
+
MyBuilders.call(Instructor, {}) #=> Instructor
|
|
346
|
+
MyBuilders.call(Instructor, { admin: true }) #=> Instructor::Admin
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Don't forget the `self::` when writing generic builders, and write tests.
|
|
350
|
+
|
|
351
|
+
# License
|
|
352
|
+
|
|
353
|
+
Copyright (c) 2014 by Nick Sutterer <apotonick@gmail.com>
|
|
354
|
+
|
|
355
|
+
Uber is released under the [MIT License](http://www.opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env rake
|
|
2
|
+
require "bundler/gem_tasks"
|
|
3
|
+
|
|
4
|
+
require 'rake/testtask'
|
|
5
|
+
|
|
6
|
+
desc 'Test the representable gem.'
|
|
7
|
+
task :default => :test
|
|
8
|
+
|
|
9
|
+
Rake::TestTask.new(:test) do |test|
|
|
10
|
+
test.libs << 'test'
|
|
11
|
+
test.test_files = FileList['test/*_test.rb']
|
|
12
|
+
test.verbose = true
|
|
13
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require File.expand_path('../lib/declarative/builder/version', __FILE__)
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |gem|
|
|
4
|
+
gem.authors = ["Nick Sutterer"]
|
|
5
|
+
gem.email = ["apotonick@gmail.com"]
|
|
6
|
+
gem.description = %q{Generic builder pattern.}
|
|
7
|
+
gem.summary = %q{Generic builder pattern.}
|
|
8
|
+
gem.homepage = "https://github.com/apotonick/declarative-builder"
|
|
9
|
+
gem.license = "MIT"
|
|
10
|
+
|
|
11
|
+
gem.files = `git ls-files`.split($\)
|
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
14
|
+
gem.name = "declarative-builder"
|
|
15
|
+
gem.require_paths = ["lib"]
|
|
16
|
+
gem.version = Declarative::Builder::VERSION
|
|
17
|
+
|
|
18
|
+
gem.add_dependency "declarative-option", "< 0.2.0"
|
|
19
|
+
|
|
20
|
+
gem.add_development_dependency "rake"
|
|
21
|
+
gem.add_development_dependency "minitest"
|
|
22
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "declarative/builder"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "declarative/option"
|
|
2
|
+
|
|
3
|
+
module Declarative
|
|
4
|
+
module Builder
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend DSL
|
|
7
|
+
base.extend Build
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class Builders < Array
|
|
11
|
+
def call(context, *args)
|
|
12
|
+
each do |block|
|
|
13
|
+
klass = block.(context, *args) and return klass # Declarative::Option#call()
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def <<(proc)
|
|
20
|
+
super Declarative::Option( proc, instance_exec: true ) # lambdas are always instance_exec'ed.
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module DSL
|
|
25
|
+
def builders
|
|
26
|
+
@builders ||= Builders.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def builds(proc=nil, &block)
|
|
30
|
+
builders << (proc || block)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module Build
|
|
35
|
+
# Call this from your class to compute the concrete target class.
|
|
36
|
+
def build!(context, *args)
|
|
37
|
+
builders.(context, *args)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Declarative::Builder(->(options) { options[:current_user] ? Bla : Blubb })
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
require "declarative/builder"
|
|
3
|
+
|
|
4
|
+
class BuilderTest < MiniTest::Spec
|
|
5
|
+
Evergreen = Struct.new(:title)
|
|
6
|
+
Hit = Struct.new(:title)
|
|
7
|
+
|
|
8
|
+
class Song
|
|
9
|
+
include Declarative::Builder
|
|
10
|
+
|
|
11
|
+
builds do |options|
|
|
12
|
+
if options[:evergreen]
|
|
13
|
+
Evergreen
|
|
14
|
+
elsif options[:hit]
|
|
15
|
+
Hit
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.build(options)
|
|
20
|
+
build!(self, options).new
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# building class if no block matches
|
|
25
|
+
it { Song.build({}).must_be_instance_of Song }
|
|
26
|
+
|
|
27
|
+
it { Song.build({evergreen: true}).must_be_instance_of Evergreen }
|
|
28
|
+
it { Song.build({hit: true}).must_be_instance_of Hit }
|
|
29
|
+
|
|
30
|
+
# test chained builds.
|
|
31
|
+
class Track
|
|
32
|
+
include Declarative::Builder
|
|
33
|
+
|
|
34
|
+
builds do |options|
|
|
35
|
+
Evergreen if options[:evergreen]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
builds do |options|
|
|
39
|
+
Hit if options[:hit]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.build(options)
|
|
43
|
+
build!(self, options).new
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it { Track.build({}).must_be_instance_of Track }
|
|
48
|
+
it { Track.build({evergreen: true}).must_be_instance_of Evergreen }
|
|
49
|
+
it { Track.build({hit: true}).must_be_instance_of Hit }
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# test inheritance. builder do not inherit.
|
|
53
|
+
class Play < Song
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it { Play.build({}).must_be_instance_of Play }
|
|
57
|
+
it { Play.build({evergreen: true}).must_be_instance_of Play }
|
|
58
|
+
it { Play.build({hit: true}).must_be_instance_of Play }
|
|
59
|
+
|
|
60
|
+
# test return from builds
|
|
61
|
+
class Boomerang
|
|
62
|
+
include Declarative::Builder
|
|
63
|
+
|
|
64
|
+
builds ->(options) do
|
|
65
|
+
return Song if options[:hit]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.build(options)
|
|
69
|
+
build!(self, options).new
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it { Boomerang.build({}).must_be_instance_of Boomerang }
|
|
74
|
+
it { Boomerang.build({hit: true}).must_be_instance_of Song }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BuilderScopeTest < MiniTest::Spec
|
|
79
|
+
def self.builder_method(options)
|
|
80
|
+
options[:from_builder_method] and return self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class Hit; end
|
|
84
|
+
|
|
85
|
+
class Song
|
|
86
|
+
class Hit
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
include Declarative::Builder
|
|
90
|
+
|
|
91
|
+
builds :builder_method # i get called first.
|
|
92
|
+
builds ->(options) do # and i second.
|
|
93
|
+
self::Hit
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.build(context, options={})
|
|
97
|
+
build!(context, options)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it do
|
|
102
|
+
Song.build(self.class).must_equal BuilderScopeTest::Hit
|
|
103
|
+
|
|
104
|
+
# this runs BuilderScopeTest::builder_method and returns self.
|
|
105
|
+
Song.build(self.class, from_builder_method: true).must_equal BuilderScopeTest
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class Evergreen
|
|
109
|
+
class Hit
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
include Declarative::Builder
|
|
113
|
+
|
|
114
|
+
class << self
|
|
115
|
+
attr_writer :builders
|
|
116
|
+
end
|
|
117
|
+
self.builders = Song.builders
|
|
118
|
+
|
|
119
|
+
def self.build(context, options={})
|
|
120
|
+
build!(context, options)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.builder_method(options)
|
|
124
|
+
options[:from_builder_method] and return self
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it do
|
|
129
|
+
# running the "copied" block in Evergreen will reference the correct @context.
|
|
130
|
+
Evergreen.build(Evergreen).must_equal BuilderScopeTest::Evergreen::Hit
|
|
131
|
+
|
|
132
|
+
Evergreen.build(Evergreen, from_builder_method: true).must_equal BuilderScopeTest::Evergreen
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
#---
|
|
136
|
+
# Builders API
|
|
137
|
+
# Builders#call
|
|
138
|
+
# Builders#<<
|
|
139
|
+
A = Class.new
|
|
140
|
+
MyBuilders = Declarative::Builder::Builders.new
|
|
141
|
+
MyBuilders << ->(options) do
|
|
142
|
+
return Song if options[:hit]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it { MyBuilders.call(A, {}).new.must_be_instance_of A }
|
|
146
|
+
it { MyBuilders.call(A, { hit: true }).new.must_be_instance_of Song }
|
|
147
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "minitest/autorun"
|
metadata
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: declarative-builder
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nick Sutterer
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2017-01-28 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: declarative-option
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "<"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.2.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "<"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.2.0
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: minitest
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
description: Generic builder pattern.
|
|
56
|
+
email:
|
|
57
|
+
- apotonick@gmail.com
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- ".gitignore"
|
|
63
|
+
- ".travis.yml"
|
|
64
|
+
- CHANGES.md
|
|
65
|
+
- Gemfile
|
|
66
|
+
- LICENSE
|
|
67
|
+
- README.md
|
|
68
|
+
- Rakefile
|
|
69
|
+
- declarative-builder.gemspec
|
|
70
|
+
- lib/declarative-builder.rb
|
|
71
|
+
- lib/declarative/builder.rb
|
|
72
|
+
- lib/declarative/builder/version.rb
|
|
73
|
+
- test/builder_test.rb
|
|
74
|
+
- test/test_helper.rb
|
|
75
|
+
homepage: https://github.com/apotonick/declarative-builder
|
|
76
|
+
licenses:
|
|
77
|
+
- MIT
|
|
78
|
+
metadata: {}
|
|
79
|
+
post_install_message:
|
|
80
|
+
rdoc_options: []
|
|
81
|
+
require_paths:
|
|
82
|
+
- lib
|
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: '0'
|
|
93
|
+
requirements: []
|
|
94
|
+
rubyforge_project:
|
|
95
|
+
rubygems_version: 2.6.3
|
|
96
|
+
signing_key:
|
|
97
|
+
specification_version: 4
|
|
98
|
+
summary: Generic builder pattern.
|
|
99
|
+
test_files:
|
|
100
|
+
- test/builder_test.rb
|
|
101
|
+
- test/test_helper.rb
|