decorum 0.0.1
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/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +356 -0
- data/Rakefile +1 -0
- data/decorum.gemspec +24 -0
- data/examples/coffee.rb +7 -0
- data/examples/fibonacci_decorator.rb +24 -0
- data/examples/milk_decorator.rb +29 -0
- data/examples/sugar_decorator.rb +12 -0
- data/lib/decorum.rb +14 -0
- data/lib/decorum/bare_particular.rb +5 -0
- data/lib/decorum/chain_stop.rb +7 -0
- data/lib/decorum/decorated_state.rb +29 -0
- data/lib/decorum/decorations.rb +101 -0
- data/lib/decorum/decorator.rb +85 -0
- data/lib/decorum/version.rb +3 -0
- data/spec/integration/coffee_spec.rb +41 -0
- data/spec/integration/fibonacci_spec.rb +20 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support/decorated_state/shared_state_stub.rb +15 -0
- data/spec/support/decorations/decorated_object_stub.rb +17 -0
- data/spec/support/decorations/first_decorator.rb +20 -0
- data/spec/support/decorations/second_decorator.rb +20 -0
- data/spec/support/decorations/third_decorator.rb +20 -0
- data/spec/support/decorator/basic_decorator.rb +20 -0
- data/spec/support/decorator/decorated_object_stub.rb +17 -0
- data/spec/support/decorator/decorated_state_stub.rb +11 -0
- data/spec/support/decorator/decorator_stub.rb +11 -0
- data/spec/unit/bare_particular_spec.rb +13 -0
- data/spec/unit/chain_stop_spec.rb +12 -0
- data/spec/unit/decorated_state_spec.rb +31 -0
- data/spec/unit/decorations_spec.rb +233 -0
- data/spec/unit/decorator_spec.rb +166 -0
- metadata +146 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
module Decorum
|
2
|
+
module Examples
|
3
|
+
class MilkDecorator < Decorum::Decorator
|
4
|
+
share :milk_level, :calories
|
5
|
+
attr_accessor :animal, :milk_type
|
6
|
+
|
7
|
+
# recursive function to allow all MilkDecorators to add
|
8
|
+
# some milk; note the tail call is wrapped in decorated_tail
|
9
|
+
def add_milk
|
10
|
+
self.milk_level = milk_level.to_i + 1
|
11
|
+
decorated_tail(milk_level) { next_link.add_milk }
|
12
|
+
end
|
13
|
+
|
14
|
+
# used with non-shared attributes, (e.g., "animal")
|
15
|
+
# decorated tail can also be used to defer a
|
16
|
+
# call to the _first_ decorator in the chain
|
17
|
+
# that responds to the method
|
18
|
+
def first_animal
|
19
|
+
decorated_tail(animal) { next_link.first_animal }
|
20
|
+
end
|
21
|
+
|
22
|
+
# for goodness' sake what has bob been putting in his coffee
|
23
|
+
def all_animals(animals=[])
|
24
|
+
animals << animal
|
25
|
+
decorated_tail(animals) { next_link.all_animals(animals) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/decorum.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Decorum
|
4
|
+
# or Hashr, or whatever---just run the test suite
|
5
|
+
class SuperHash < OpenStruct
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require "decorum/version"
|
10
|
+
require 'decorum/decorations'
|
11
|
+
require 'decorum/decorator'
|
12
|
+
require 'decorum/decorated_state'
|
13
|
+
require 'decorum/chain_stop'
|
14
|
+
require 'decorum/bare_particular'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Decorum
|
2
|
+
class DecoratedState
|
3
|
+
def initialize(options={})
|
4
|
+
@shared_state = Decorum::SuperHash.new(options)
|
5
|
+
end
|
6
|
+
|
7
|
+
# this is one of two areas---the other being
|
8
|
+
# loading/unloading of decorators---where i
|
9
|
+
# suspect it isn't threadsafe now, but could
|
10
|
+
# pretty easily be made to be, e.g., in the
|
11
|
+
# writer forwarder below:
|
12
|
+
#
|
13
|
+
# lock = Monitor.new
|
14
|
+
# lock.synchronize do
|
15
|
+
# @state.send(message, *args)
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# more on this some other time.
|
19
|
+
def method_missing(message, *args)
|
20
|
+
if message =~ /=$/
|
21
|
+
# writer, in case we want to do something different here
|
22
|
+
@shared_state.send(message, *args)
|
23
|
+
else
|
24
|
+
# reader
|
25
|
+
@shared_state.send(message, *args)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Decorum
|
2
|
+
module Decorations
|
3
|
+
def decorate(klass, options={})
|
4
|
+
extend Decorum::Decorations::Intercept
|
5
|
+
decorator = add_to_decorator_chain(klass, options)
|
6
|
+
yield decorator if block_given?
|
7
|
+
self
|
8
|
+
end
|
9
|
+
|
10
|
+
def undecorate(target)
|
11
|
+
remove_from_decorator_chain(target)
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def decorators
|
16
|
+
if @_decorators
|
17
|
+
return @_decorators
|
18
|
+
elsif !@_decorator_chain
|
19
|
+
return []
|
20
|
+
end
|
21
|
+
|
22
|
+
decorator = @_decorator_chain
|
23
|
+
@_decorators = []
|
24
|
+
until decorator.is_a?(Decorum::ChainStop)
|
25
|
+
@_decorators << decorator
|
26
|
+
decorator = decorator.next_link
|
27
|
+
end
|
28
|
+
@_decorators
|
29
|
+
end
|
30
|
+
|
31
|
+
def decorated_state(klass=nil)
|
32
|
+
@_decorated_state ||= {}
|
33
|
+
|
34
|
+
if klass
|
35
|
+
@_decorated_state[klass]
|
36
|
+
else
|
37
|
+
@_decorated_state
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module Decorum::Decorations::Intercept
|
42
|
+
def method_missing(message, *args, &block)
|
43
|
+
response = catch :chain_stop do
|
44
|
+
@_decorator_chain.send(message, *args, &block)
|
45
|
+
end
|
46
|
+
response.is_a?(Decorum::ChainStop) ? super : response
|
47
|
+
end
|
48
|
+
|
49
|
+
def respond_to_missing?(message, include_private = false)
|
50
|
+
decorators.each { |d| return true if d.respond_to?(message) }
|
51
|
+
super
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def add_to_decorator_chain(klass, options)
|
58
|
+
unless klass.ancestors.include?(Decorum::Decorator)
|
59
|
+
raise RuntimeError.new("decorator chain needs a Decorator")
|
60
|
+
end
|
61
|
+
|
62
|
+
if options[:decorator_handle]
|
63
|
+
current_names = decorators.map { |d| d.decorator_handle.to_sym }.compact
|
64
|
+
if current_names.include?(options[:decorator_handle].to_sym)
|
65
|
+
# is this a little harsh?
|
66
|
+
raise RuntimeError.new("decorator names must be unique over an object")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
unless decorated_state(klass)
|
71
|
+
@_decorated_state[klass] = Decorum::DecoratedState.new
|
72
|
+
end
|
73
|
+
|
74
|
+
base = @_decorator_chain || Decorum::ChainStop.new
|
75
|
+
@_decorator_chain = klass.new(base, self, options)
|
76
|
+
decorators!
|
77
|
+
@_decorator_chain
|
78
|
+
end
|
79
|
+
|
80
|
+
def remove_from_decorator_chain(decorator)
|
81
|
+
return nil unless decorators.include?(decorator)
|
82
|
+
|
83
|
+
if decorator == @_decorator_chain
|
84
|
+
@_decorator_chain = decorator.next_link
|
85
|
+
else
|
86
|
+
previous_decorator = decorators[decorators.index(decorator) - 1]
|
87
|
+
previous_decorator.next_link = decorator.next_link
|
88
|
+
end
|
89
|
+
|
90
|
+
unless decorators!.map { |d| d.class }.include?(decorator.class)
|
91
|
+
@_decorated_state[decorator.class] = nil
|
92
|
+
end
|
93
|
+
decorators
|
94
|
+
end
|
95
|
+
|
96
|
+
def decorators!
|
97
|
+
@_decorators = nil
|
98
|
+
decorators
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Decorum
|
2
|
+
class Decorator
|
3
|
+
attr_accessor :next_link
|
4
|
+
attr_reader :root, :decorator_handle
|
5
|
+
alias_method :object, :root
|
6
|
+
|
7
|
+
def initialize(next_link, root, options)
|
8
|
+
@passed_options = options
|
9
|
+
@next_link = next_link
|
10
|
+
@root = root
|
11
|
+
@decorator_handle = options[:decorator_handle]
|
12
|
+
|
13
|
+
defaults = self.class.get_default_attributes
|
14
|
+
attribute_options = defaults ? defaults.merge(options) : options
|
15
|
+
|
16
|
+
attribute_options.each do |key, value|
|
17
|
+
setter = :"#{key}="
|
18
|
+
if respond_to?(setter)
|
19
|
+
send setter, value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# a superhash of shared state between Decorators
|
25
|
+
# of the same class
|
26
|
+
def decorated_state
|
27
|
+
root.decorated_state(self.class)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Decorators that want stackable or cumulative
|
31
|
+
# behavior can do so with tail recursion. Wrap
|
32
|
+
# the tail call in #decorated_tail to catch the
|
33
|
+
# end of the chain and return the accumulator
|
34
|
+
def decorated_tail(current_value=nil, &block)
|
35
|
+
response = catch :chain_stop do
|
36
|
+
yield
|
37
|
+
end
|
38
|
+
response.is_a?(Decorum::ChainStop) ? current_value : response
|
39
|
+
end
|
40
|
+
|
41
|
+
# delegate to next_link
|
42
|
+
# note that we are not faking #respond_to? because
|
43
|
+
# we want that to reflect what the decorator can
|
44
|
+
# respond to locally
|
45
|
+
def method_missing(*args, &block)
|
46
|
+
@next_link.send(*args, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
# class methods
|
50
|
+
class << self
|
51
|
+
# allow Decorator classes to share state among
|
52
|
+
# their instances
|
53
|
+
def share(*args)
|
54
|
+
args.each do |getter|
|
55
|
+
getter = getter.to_sym
|
56
|
+
define_method(getter) { self.decorated_state.send(getter) }
|
57
|
+
|
58
|
+
boolean = :"#{getter}?"
|
59
|
+
define_method(boolean) { self.decorated_state.send(getter) ? true : false }
|
60
|
+
|
61
|
+
setter = :"#{getter}="
|
62
|
+
define_method(setter) { |value| self.decorated_state.send(setter, value) }
|
63
|
+
|
64
|
+
resetter = :"reset_#{getter}"
|
65
|
+
define_method(resetter) { self.decorated_state.send(setter, nil) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# hint as to how one might use these attributes
|
70
|
+
alias_method :accumulator, :share
|
71
|
+
|
72
|
+
# allow Decorator classes to provide attribute defaults (not shared)
|
73
|
+
def default_attributes(attrs)
|
74
|
+
@default_attributes = attrs
|
75
|
+
attrs.keys.each do |attr|
|
76
|
+
attr_accessor attr.to_sym
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def get_default_attributes
|
81
|
+
@default_attributes || {}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "when Bob is ordering a cup of coffee" do
|
4
|
+
let(:coffee) { Decorum::Examples::Coffee.new }
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
coffee.decorate(Decorum::Examples::MilkDecorator, animal: "cow", milk_type: "2 percent")
|
8
|
+
coffee.decorate(Decorum::Examples::MilkDecorator, animal: "cow", milk_type: "2 percent")
|
9
|
+
coffee.decorate(Decorum::Examples::SugarDecorator)
|
10
|
+
coffee.add_milk
|
11
|
+
coffee.add_sugar
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'adds up to two milks' do
|
15
|
+
expect(coffee.milk_level).to be(2)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'has a sugar too' do
|
19
|
+
expect(coffee.sugar_level).to be(1)
|
20
|
+
end
|
21
|
+
|
22
|
+
context "things get interesting" do
|
23
|
+
before(:each) do
|
24
|
+
["bear", "man", "pig"].each do |critter|
|
25
|
+
coffee.decorate(Decorum::Examples::MilkDecorator, animal: critter)
|
26
|
+
end
|
27
|
+
coffee.add_milk
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'gets another squirt from those original two cow milks' do
|
31
|
+
# just so we're clear on how this works---you have to clear
|
32
|
+
# the shared state yourself
|
33
|
+
expect(coffee.milk_level).to be(7)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'details Bob\'s dairy proclivities' do
|
37
|
+
expected_animals = ["pig", "man", "bear", "cow", "cow"]
|
38
|
+
expect(coffee.all_animals).to eq(expected_animals)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
'require spec_helper'
|
2
|
+
|
3
|
+
describe 'a Fibonacci sequence implemented in Decorators' do
|
4
|
+
let(:fibber) { Decorum::BareParticular.new }
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
100.times do
|
8
|
+
fibber.decorate(Decorum::Examples::FibonacciDecorator)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'returns the 100th term' do
|
13
|
+
expect(fibber.fib).to eq(927372692193078999176)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'stores the sequence' do
|
17
|
+
fibber.fib
|
18
|
+
expect(fibber.sequence.length).to eq(100)
|
19
|
+
end
|
20
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Decorum
|
2
|
+
module Spec
|
3
|
+
module Decorations
|
4
|
+
class FirstDecorator < Decorum::Decorator
|
5
|
+
share :shared_attribute
|
6
|
+
attr_accessor :local_attribute
|
7
|
+
|
8
|
+
def first_decorator_method
|
9
|
+
"first"
|
10
|
+
end
|
11
|
+
|
12
|
+
alias_method :current_decorator_method, :first_decorator_method
|
13
|
+
|
14
|
+
def respect_previously_defined_methods?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Decorum
|
2
|
+
module Spec
|
3
|
+
module Decorations
|
4
|
+
class SecondDecorator < Decorum::Decorator
|
5
|
+
share :shared_attribute
|
6
|
+
attr_accessor :local_attribute
|
7
|
+
|
8
|
+
def second_decorator_method
|
9
|
+
"second"
|
10
|
+
end
|
11
|
+
|
12
|
+
alias_method :current_decorator_method, :second_decorator_method
|
13
|
+
|
14
|
+
def respect_previously_defined_methods?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|