decorum 0.3.0 → 0.4.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.
- data/CHANGELOG.md +5 -0
- data/README.md +39 -6
- data/lib/decorum/decorations.rb +49 -4
- data/lib/decorum/version.rb +1 -1
- data/spec/support/decorations/class_specified_decorator_one.rb +21 -0
- data/spec/unit/decorations_spec.rb +68 -2
- metadata +4 -2
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -24,10 +24,16 @@ end
|
|
24
24
|
|
25
25
|
bp = BirthdayParty.new
|
26
26
|
bp.respond_to?(:shoot_confetti) # ==> false
|
27
|
+
bp.is_decorated? # ==> false
|
27
28
|
bp.decorate(Confetti)
|
29
|
+
bp.is_decorated? # ==> true
|
28
30
|
bp.shoot_confetti # ==> "boom, yay"
|
29
31
|
```
|
30
32
|
|
33
|
+
## New in 0.4.0
|
34
|
+
- Decorators and their attributes may now be specified in class definitions, and loaded with `#load_decorators_from_class`
|
35
|
+
- `#is_decorated?`
|
36
|
+
|
31
37
|
## New in 0.3.0
|
32
38
|
- Methods may be called directly on decorators
|
33
39
|
- Decorator namespaces
|
@@ -196,7 +202,7 @@ original definition is found.)
|
|
196
202
|
A sizeable amount of the world's total Ruby functionality is implemented
|
197
203
|
by overriding `#method_missing`, so decorators shouldn't get in the way.
|
198
204
|
Because Decorum intercepts messages in the objects eigenclass, it also
|
199
|
-
respects existing overrides. If the decorator chain doesn't claim the
|
205
|
+
respects existing class-level overrides. If the decorator chain doesn't claim the
|
200
206
|
message, `super` is called and lookup proceeds normally.
|
201
207
|
|
202
208
|
#### Unloadable decorators
|
@@ -205,10 +211,10 @@ Decorators can be unloaded, if necessary:
|
|
205
211
|
|
206
212
|
```ruby
|
207
213
|
@bob.decorate(IsTheMole)
|
208
|
-
@bob.revoke_security_clearances
|
209
214
|
|
210
|
-
|
211
|
-
|
215
|
+
# meanwhile:
|
216
|
+
class IsTheMole < Decorum::Decorator
|
217
|
+
# hook method
|
212
218
|
def post_decorate
|
213
219
|
object.revoke_security_clearances
|
214
220
|
end
|
@@ -219,6 +225,8 @@ class IsTheMole < Decorum::Decorators
|
|
219
225
|
end
|
220
226
|
end
|
221
227
|
```
|
228
|
+
This example also shows the use of the `#post_decorate` hook.
|
229
|
+
|
222
230
|
|
223
231
|
### Implementation
|
224
232
|
|
@@ -243,7 +251,7 @@ by including Decorum::Decorations, or for a single object, by extending it.
|
|
243
251
|
Decorum::Decorations at whatever point(s) of the class hierarchy you
|
244
252
|
feel appropriate.
|
245
253
|
|
246
|
-
###
|
254
|
+
### Using Decorators
|
247
255
|
The decorated object is accessible as either `#root` or `#object`. A helper method:
|
248
256
|
|
249
257
|
```ruby
|
@@ -299,6 +307,31 @@ object; this shared state can be used for a number of purposes. Finally,
|
|
299
307
|
`default_attributes` lets you set class-level defaults; these will be
|
300
308
|
preempted by options passed to the constructor.
|
301
309
|
|
310
|
+
#### Specifying decorators in classes
|
311
|
+
|
312
|
+
Decoration may be performed by the instance method `#decorate` above,
|
313
|
+
or you can include default decorators and arguments in your class
|
314
|
+
definition:
|
315
|
+
|
316
|
+
```ruby
|
317
|
+
class Coffee
|
318
|
+
include Decorum::Decorations
|
319
|
+
# two bovine dairy no sugar by default, please:
|
320
|
+
decorators Milk, { animal: "cow" }, Milk, { animal: "cow" }
|
321
|
+
...
|
322
|
+
end
|
323
|
+
|
324
|
+
c = Coffee.new
|
325
|
+
c.load_decorators_from_class
|
326
|
+
c.add_milk
|
327
|
+
c.milk_level # ==> 2
|
328
|
+
```
|
329
|
+
|
330
|
+
Note that this usage does _not_ automatically include decorators on new
|
331
|
+
objects. That would require invasive procedures on your object initialization.
|
332
|
+
Instead, Decorum provides `#load_decorators_from_class`, which you can call
|
333
|
+
in your initializations, or later.
|
334
|
+
|
302
335
|
As a side note, you can disable another decorators methods thus:
|
303
336
|
|
304
337
|
```ruby
|
@@ -334,7 +367,7 @@ other things:
|
|
334
367
|
|
335
368
|
And so on.
|
336
369
|
|
337
|
-
###
|
370
|
+
### #decorated_tail
|
338
371
|
|
339
372
|
How exactly did the first MilkDecorator pass `#add_milk`
|
340
373
|
down the chain instead of returning? In general, the decision
|
data/lib/decorum/decorations.rb
CHANGED
@@ -1,5 +1,36 @@
|
|
1
1
|
module Decorum
|
2
2
|
module Decorations
|
3
|
+
def self.included(modyool)
|
4
|
+
# class method to declare default decorators
|
5
|
+
def modyool.decorators(*args)
|
6
|
+
# set first-listed priority
|
7
|
+
if args[0] == :reverse
|
8
|
+
return @_decorum_stack_reverse = true
|
9
|
+
end
|
10
|
+
|
11
|
+
@_decorum_stack ||= []
|
12
|
+
|
13
|
+
if !args.empty?
|
14
|
+
args.each do |arg|
|
15
|
+
if (arg.is_a?(Class) && arg.ancestors.include?(Decorum::Decorator)) || arg.is_a?(Hash)
|
16
|
+
next if arg.is_a?(Hash)
|
17
|
+
klass = arg
|
18
|
+
next_arg = args[args.index(arg) + 1]
|
19
|
+
options = next_arg.is_a?(Hash) ? next_arg : {}
|
20
|
+
@_decorum_stack << [klass, options]
|
21
|
+
else
|
22
|
+
raise ArgumentError, "invalid argument to #{self.to_s}.decorate_with: #{arg.to_s}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
else
|
26
|
+
@_decorum_stack_reverse ? @_decorum_stack.reverse : @_decorum_stack
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# public instance methods
|
32
|
+
|
33
|
+
|
3
34
|
def decorate(klass, options={})
|
4
35
|
if namespace_method = options.delete(:namespace)
|
5
36
|
decorator = nil
|
@@ -11,7 +42,7 @@ module Decorum
|
|
11
42
|
end
|
12
43
|
namespace.decorate(klass, options) { |d| decorator = d }
|
13
44
|
else
|
14
|
-
namespace
|
45
|
+
namespace = Decorum::DecoratorNamespace.new(self)
|
15
46
|
namespace.decorate(klass, options) { |d| decorator = d }
|
16
47
|
instance_variable_set(:"@_decorum_#{namespace_method}", namespace)
|
17
48
|
m = Module.new do
|
@@ -20,6 +51,7 @@ module Decorum
|
|
20
51
|
end
|
21
52
|
end
|
22
53
|
extend m
|
54
|
+
_decorator_namespaces << namespace_method
|
23
55
|
end
|
24
56
|
yield CallableDecorator.new(decorator) if block_given?
|
25
57
|
else
|
@@ -35,13 +67,22 @@ module Decorum
|
|
35
67
|
remove_from_decorator_chain(target)
|
36
68
|
self
|
37
69
|
end
|
38
|
-
|
39
70
|
|
71
|
+
def is_decorated?
|
72
|
+
![ decorators, _decorator_namespaces.map { |ns| send(ns).decorators } ].flatten.empty?
|
73
|
+
end
|
74
|
+
|
40
75
|
# returns callable decorators---use this
|
41
76
|
def decorators
|
42
77
|
_decorators.map { |d| CallableDecorator.new(d) }
|
43
78
|
end
|
44
79
|
|
80
|
+
# leaving it to you to, say, call this from #initialize
|
81
|
+
def load_decorators_from_class
|
82
|
+
self.class.decorators.each { |decorator_class, options| decorate(decorator_class, options) }
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
45
86
|
# returns raw decorators---don't use this unless
|
46
87
|
# you know what you're doing
|
47
88
|
def _decorators
|
@@ -75,6 +116,10 @@ module Decorum
|
|
75
116
|
@_decorated_state
|
76
117
|
end
|
77
118
|
end
|
119
|
+
|
120
|
+
def _decorator_namespaces
|
121
|
+
@_decorator_namespaces ||= []
|
122
|
+
end
|
78
123
|
|
79
124
|
module Decorum::Decorations::Intercept
|
80
125
|
def method_missing(message, *args, &block)
|
@@ -90,11 +135,11 @@ module Decorum
|
|
90
135
|
end
|
91
136
|
end
|
92
137
|
|
93
|
-
|
138
|
+
private
|
94
139
|
|
95
140
|
def add_to_decorator_chain(klass, options)
|
96
141
|
unless klass.ancestors.include?(Decorum::Decorator)
|
97
|
-
raise
|
142
|
+
raise TypeError, "decorator chain needs a Decorator"
|
98
143
|
end
|
99
144
|
|
100
145
|
if options[:decorator_handle]
|
data/lib/decorum/version.rb
CHANGED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Decorum
|
2
|
+
module Spec
|
3
|
+
module Decorations
|
4
|
+
class ClassSpecifiedDecoratorOne < Decorum::Decorator
|
5
|
+
attr_accessor :passed_option
|
6
|
+
|
7
|
+
def one
|
8
|
+
"one"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class ClassSpecifiedDecoratorTwo < ClassSpecifiedDecoratorOne
|
13
|
+
attr_accessor :passed_option
|
14
|
+
|
15
|
+
def two
|
16
|
+
"two"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -6,6 +6,55 @@ describe Decorum::Decorations do
|
|
6
6
|
let(:deco_class_2) { Decorum::Spec::Decorations::SecondDecorator }
|
7
7
|
let(:deco_class_3) { Decorum::Spec::Decorations::ThirdDecorator }
|
8
8
|
|
9
|
+
context 'loading decorators from class defaults' do
|
10
|
+
let(:klass) do
|
11
|
+
Class.new do
|
12
|
+
include Decorum::Decorations
|
13
|
+
decorators Decorum::Spec::Decorations::ClassSpecifiedDecoratorOne, { passed_option: "one" },
|
14
|
+
Decorum::Spec::Decorations::ClassSpecifiedDecoratorTwo, { passed_option: "two" }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '.decorators' do
|
19
|
+
it 'stores decorators in own state correctly' do
|
20
|
+
expect(klass.decorators.map { |d| d[0].ancestors.include?(Decorum::Decorator) }.inject(:&)).to be_true
|
21
|
+
end
|
22
|
+
|
23
|
+
# the instance method #load_decorators_from_class will insert them in the order given:
|
24
|
+
|
25
|
+
it 'normally gives first priority to last listed' do
|
26
|
+
expect(klass.decorators.map { |d| d[1] } == [{ passed_option: "one" }, { passed_option: "two" }]).to be_true
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'reverses order for first-specfied priority' do
|
30
|
+
klass.decorators :reverse
|
31
|
+
expect(klass.decorators.map { |d| d[1] } == [{ passed_option: "two" }, { passed_option: "one" }]).to be_true
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'rejects malformed options' do
|
35
|
+
expect { klass.decorators(Decorum::Spec::Decorations::FirstDecorator, "invalid decorator argument") }.to raise_error
|
36
|
+
end
|
37
|
+
|
38
|
+
# this probably belongs with other instance methods below, but we've got the
|
39
|
+
# support objects built for it here...
|
40
|
+
describe '#decorators' do
|
41
|
+
let(:obj) { klass.new.load_decorators_from_class }
|
42
|
+
it 'returns self' do
|
43
|
+
# sort of
|
44
|
+
expect(obj.is_a?(klass)).to be_true
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'loads all decorators given by .decorators' do
|
48
|
+
expect(obj.one == "one" && obj.two == "two").to be_true
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'loads decorators in the order given by .decorators' do
|
52
|
+
expect(obj.passed_option == "two").to be_true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
9
58
|
context 'as-yet-undecorated' do
|
10
59
|
# assert some basic assumptions
|
11
60
|
it 'is decoratable' do
|
@@ -20,6 +69,12 @@ describe Decorum::Decorations do
|
|
20
69
|
expect(decorated.undecorated_method).to be_true
|
21
70
|
end
|
22
71
|
|
72
|
+
describe '#is_decorated?' do
|
73
|
+
it 'is false' do
|
74
|
+
expect(decorated.is_decorated?).to be_false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
23
78
|
describe '#decorated_state' do
|
24
79
|
it 'returns a hash' do
|
25
80
|
blank_state = decorated.decorated_state
|
@@ -41,7 +96,7 @@ describe Decorum::Decorations do
|
|
41
96
|
expect(decorated.undecorate(:symbol_arg)).to be_equal(decorated)
|
42
97
|
end
|
43
98
|
|
44
|
-
it 'returns self on spurious
|
99
|
+
it 'returns self on spurious decorator' do
|
45
100
|
expect(decorated.undecorate(deco_class_1)).to be_equal(decorated)
|
46
101
|
end
|
47
102
|
end
|
@@ -71,6 +126,17 @@ describe Decorum::Decorations do
|
|
71
126
|
decorated.decorate(deco_class_1, decorator_options)
|
72
127
|
end
|
73
128
|
|
129
|
+
describe '#is_decorated?' do
|
130
|
+
it 'returns true' do
|
131
|
+
expect(decorated.is_decorated?).to be_true
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'returns false after unloading' do
|
135
|
+
decorated.undecorate(decorated.decorators.first)
|
136
|
+
expect(decorated.is_decorated?).to be_false
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
74
140
|
it 'installs intercept' do
|
75
141
|
expect(decorated.is_a?(Decorum::Decorations::Intercept)).to be_true
|
76
142
|
end
|
@@ -127,7 +193,7 @@ describe Decorum::Decorations do
|
|
127
193
|
|
128
194
|
context 'failure' do
|
129
195
|
it 'rejects classes that are not Decorators' do
|
130
|
-
expect { decorated.decorate(Class,{}) }.to raise_error(
|
196
|
+
expect { decorated.decorate(Class,{}) }.to raise_error(TypeError)
|
131
197
|
end
|
132
198
|
|
133
199
|
it 'rejects non unique decorator handles' do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: decorum
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-
|
12
|
+
date: 2014-12-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -95,6 +95,7 @@ files:
|
|
95
95
|
- spec/integration/immediate_methods_spec.rb
|
96
96
|
- spec/spec_helper.rb
|
97
97
|
- spec/support/decorated_state/shared_state_stub.rb
|
98
|
+
- spec/support/decorations/class_specified_decorator_one.rb
|
98
99
|
- spec/support/decorations/decorated_object_stub.rb
|
99
100
|
- spec/support/decorations/first_decorator.rb
|
100
101
|
- spec/support/decorations/second_decorator.rb
|
@@ -144,6 +145,7 @@ test_files:
|
|
144
145
|
- spec/integration/immediate_methods_spec.rb
|
145
146
|
- spec/spec_helper.rb
|
146
147
|
- spec/support/decorated_state/shared_state_stub.rb
|
148
|
+
- spec/support/decorations/class_specified_decorator_one.rb
|
147
149
|
- spec/support/decorations/decorated_object_stub.rb
|
148
150
|
- spec/support/decorations/first_decorator.rb
|
149
151
|
- spec/support/decorations/second_decorator.rb
|