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.
@@ -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
@@ -0,0 +1,12 @@
1
+ module Decorum
2
+ module Examples
3
+ class SugarDecorator < Decorum::Decorator
4
+ share :sugar_level
5
+
6
+ def add_sugar
7
+ self.sugar_level = sugar_level.to_i + 1
8
+ decorated_tail(sugar_level) { decoratee.add_cube }
9
+ end
10
+ end
11
+ end
12
+ 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,5 @@
1
+ module Decorum
2
+ class BareParticular < Decorum::SuperHash
3
+ include Decorum::Decorations
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ module Decorum
2
+ class ChainStop
3
+ def method_missing(*args, &block)
4
+ throw :chain_stop, self
5
+ end
6
+ end
7
+ end
@@ -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,3 @@
1
+ module Decorum
2
+ VERSION = "0.0.1"
3
+ 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
@@ -0,0 +1,4 @@
1
+ require './lib/decorum.rb'
2
+
3
+ Dir.glob("./spec/support/**/*.rb").each { |path| require path }
4
+ Dir.glob("./examples/**/*.rb").each { |path| require path }
@@ -0,0 +1,15 @@
1
+ module Decorum
2
+ module Spec
3
+ module DecoratedState
4
+ class SharedStateStub
5
+ def marker=(*args)
6
+ "assigned"
7
+ end
8
+
9
+ def marker
10
+ "retrieved"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Decorum
2
+ module Spec
3
+ module Decorations
4
+ class DecoratedObjectStub
5
+ include Decorum::Decorations
6
+
7
+ def respect_previously_defined_methods?
8
+ true
9
+ end
10
+
11
+ def undecorated_method
12
+ true
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -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