bandy-dci 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 30ad5219182fae98adeba5f1eb7880047078001f
4
+ data.tar.gz: 2284100d33e40d98f2ed53b5fa9b1647f9178ae8
5
+ SHA512:
6
+ metadata.gz: 6d033acd944220f29b6c200200c84a49b44f0897f26c8e3d4dd48d663c334671b80cce044cac713bb8a6864b7acfb4ab8e2a40de1e8f920960ccc2157f9e6561
7
+ data.tar.gz: b6b61532643781f831dc46dffd1bff8482e629a2624435810a030de9423eac256ebf2483af4b1e8ed503a6dc48354f03e945fea5e3f584c631c1d5b94f109f2a
@@ -0,0 +1,4 @@
1
+ module DCI
2
+ end
3
+
4
+ require 'dci/context'
@@ -0,0 +1,49 @@
1
+ require 'dci/context'
2
+
3
+ module DCI
4
+ module Castable
5
+ def self.module_method_rebinding?
6
+ sample_method = Enumerable.instance_method(:to_a)
7
+ begin
8
+ !!sample_method.bind(Object.new)
9
+ rescue TypeError
10
+ false
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ if module_method_rebinding?
17
+ def delegated_method(role, method)
18
+ role.instance_method(method)
19
+ end
20
+ else
21
+ def delegated_method(role, method)
22
+ clone.extend(role).method(method).unbind
23
+ end
24
+ end
25
+
26
+ def method_missing(method, *arguments, &block)
27
+ role = participating_role_with_method(method)
28
+ role ? delegated_method(role, method).bind(self).call(*arguments, &block) : super
29
+ end
30
+
31
+ def participating_role_with_method(method)
32
+ context = DCI::Context.current
33
+ return unless context
34
+
35
+ roles = context[self]
36
+ return unless roles
37
+
38
+ roles.find do |role|
39
+ role.public_instance_methods.include?(method) ||
40
+ role.protected_instance_methods.include?(method) ||
41
+ role.private_instance_methods.include?(method)
42
+ end
43
+ end
44
+
45
+ def respond_to_missing?(method, include_private)
46
+ !!participating_role_with_method(method) || super
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,99 @@
1
+ require 'dci/castable'
2
+
3
+ module DCI
4
+ module Context
5
+ # The currently executing context
6
+ def self.current
7
+ Thread.current[:'DCI::Context.current']
8
+ end
9
+
10
+ def self.included(calling_module)
11
+ calling_module.extend(DSL)
12
+ end
13
+
14
+ def cast(actor, roles)
15
+ actor.extend(Castable) unless actor.is_a?(Castable)
16
+
17
+ @roles ||= {}
18
+ @roles[actor] ||= []
19
+ @roles[actor] |= roles.values
20
+
21
+ actor
22
+ end
23
+
24
+ def [](actor)
25
+ @roles && @roles[actor]
26
+ end
27
+
28
+ module DSL
29
+ private
30
+
31
+ # Replace an existing method with a wrapper that advertises the current context
32
+ def define_entry_using_method(name)
33
+ method = instance_method(name)
34
+ define_method(name) do |*arguments, &block|
35
+ begin
36
+ # Swap out the currently executing context
37
+ Thread.current[:'DCI::Context.current'], old_context = self, Thread.current[:'DCI::Context.current']
38
+ method.bind(self).call(*arguments, &block)
39
+ ensure
40
+ # Reinstate the previously executing context
41
+ Thread.current[:'DCI::Context.current'] = old_context
42
+ end
43
+ end
44
+ end
45
+
46
+ # Create a method that executes the provided definition while advertising the current context
47
+ def define_entry_using_proc(name, definition)
48
+ define_method(name, &definition)
49
+ define_entry_using_method(name)
50
+ end
51
+
52
+ # Define a context entry point
53
+ def entry(name, proc = nil, &block)
54
+ if block_given?
55
+ define_entry_using_proc(name, block)
56
+ elsif proc.respond_to?(:to_proc)
57
+ define_entry_using_proc(name, proc)
58
+ elsif method_defined?(name)
59
+ define_entry_using_method(name)
60
+ else
61
+ @entries ||= []
62
+ @entries |= [name]
63
+ end
64
+ end
65
+
66
+ # Listen for new methods that are intended to be entry points
67
+ def method_added(name)
68
+ if @entries && @entries.delete(name)
69
+ define_entry_using_method(name)
70
+ end
71
+ end
72
+
73
+ # Define a context role
74
+ def role(name, *args, &block)
75
+ attr_reader name
76
+ public name
77
+
78
+ if block_given?
79
+ role_module = Module.new
80
+ role_module.module_eval(&block)
81
+
82
+ define_method("#{name}=") do |actor|
83
+ instance_variable_set("@#{name}", cast(actor, :as => role_module))
84
+ end
85
+ else
86
+ attr_writer name
87
+ end
88
+
89
+ protected "#{name}="
90
+ end
91
+
92
+ def trigger(name, delegate, method = name)
93
+ entry(name) do |*arguments, &block|
94
+ __send__(delegate).__send__(method, *arguments, &block)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,13 @@
1
+ require 'dci/context'
2
+
3
+ module DCI
4
+ module RoleLookup
5
+ private
6
+
7
+ # Forward references to constants to the currently executing context
8
+ def const_missing(name)
9
+ context = DCI::Context.current
10
+ context.respond_to?(name) ? context.__send__(name) : super
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,77 @@
1
+ require 'dci/castable'
2
+
3
+ describe DCI::Castable do
4
+ let(:something) { Class.new { include DCI::Castable; def greet; 'hello'; end } }
5
+ subject(:actor) { something.new }
6
+
7
+ context 'when the actor is not participating in the current context' do
8
+ specify 'calling a nonexistent method raises NoMethodError' do
9
+ expect { actor.smile }.to raise_error(NoMethodError)
10
+ end
11
+
12
+ specify 'calling an implemented method works' do
13
+ expect(actor.greet).to eq 'hello'
14
+ end
15
+ end
16
+
17
+ context 'when the actor is participating in the current context' do
18
+ let(:context_participants) { { actor => [role] } }
19
+ let(:role) { Module.new }
20
+
21
+ before do
22
+ allow(DCI::Context).to receive(:current).and_return(context_participants)
23
+ end
24
+
25
+ specify 'calling a method implemented by none of its roles raises NoMethodError' do
26
+ expect(actor).to_not respond_to(:smile)
27
+ expect { actor.smile }.to raise_error(NoMethodError)
28
+ end
29
+
30
+ context 'when one of its roles implements a public method' do
31
+ let(:role) { Module.new { def eat; :yum; end } }
32
+
33
+ specify 'calling that method works' do
34
+ expect(actor).to respond_to(:eat)
35
+ expect(actor.eat).to be :yum
36
+ end
37
+ end
38
+
39
+ context 'when one of its roles implements a protected method' do
40
+ let(:role) { Module.new { protected; def eat; :yum; end } }
41
+
42
+ specify 'calling that method works' do
43
+ expect(actor).to respond_to(:eat)
44
+ expect(actor.eat).to be :yum
45
+ end
46
+ end
47
+
48
+ context 'when one of its roles implements a private method' do
49
+ let(:role) { Module.new { private; def eat; :yum; end } }
50
+
51
+ specify 'calling that method works' do
52
+ expect(actor).to respond_to(:eat)
53
+ expect(actor.eat).to be :yum
54
+ end
55
+ end
56
+
57
+ context 'when one of its role methods takes arguments' do
58
+ let(:role) { Module.new { def sing(song); song; end } }
59
+
60
+ specify 'calling that method without arguments raises ArgumentError' do
61
+ expect { actor.sing }.to raise_error(ArgumentError)
62
+ end
63
+
64
+ specify 'calling that method with arguments works' do
65
+ expect(actor.sing(:song)).to be :song
66
+ end
67
+ end
68
+
69
+ context 'when one of its role methods takes a block' do
70
+ let(:role) { Module.new { def sing(&block); block.call; end } }
71
+
72
+ specify 'calling that method with a block works' do
73
+ expect(actor.sing { :song }).to be :song
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,290 @@
1
+ require 'dci/context'
2
+
3
+ describe DCI::Context do
4
+ describe 'role implementations' do
5
+ let(:context) { Class.new { include DCI::Context } }
6
+ let(:context_instance) { context.new }
7
+ let(:participating_roles) { context_instance[role_player] }
8
+ let(:role_implementation) { Module.new }
9
+ let(:role_player) { double }
10
+
11
+ specify 'are applied using #cast' do
12
+ context_instance.cast(role_player, :as => role_implementation)
13
+
14
+ expect(role_player).to be_a(DCI::Castable)
15
+ expect(participating_roles).to eq [role_implementation]
16
+ end
17
+
18
+ specify 'are nil when not applied' do
19
+ expect(participating_roles).to be nil
20
+ end
21
+ end
22
+
23
+ describe 'role identifiers are defined using ::role' do
24
+ let(:context) do
25
+ Class.new do
26
+ include DCI::Context
27
+
28
+ role :One
29
+
30
+ def initialize(one)
31
+ self.One = one
32
+ end
33
+ end
34
+ end
35
+
36
+ context 'at runtime' do
37
+ subject(:context_instance) { context.new(one) }
38
+ let(:one) { double }
39
+
40
+ specify 'role players can be assigned' do
41
+ expect { context_instance }.to_not raise_error
42
+ end
43
+
44
+ specify 'role players can be accessed' do
45
+ expect(context_instance.One).to be one
46
+ end
47
+ end
48
+
49
+ describe 'when passed a block, that block becomes the implementation' do
50
+ let(:context) do
51
+ Class.new do
52
+ include DCI::Context
53
+
54
+ role :One do
55
+ def something
56
+ :expected
57
+ end
58
+ end
59
+
60
+ def initialize
61
+ self.One = Object.new
62
+ end
63
+ end
64
+ end
65
+
66
+ let(:context_instance) { context.new }
67
+ let(:participating_roles) { context_instance[role_player] }
68
+ let(:role_player) { context_instance.One }
69
+
70
+ specify 'it is applied when the role is assigned' do
71
+ expect(role_player).to be_a DCI::Castable
72
+ expect(participating_roles).to be_an Array
73
+ expect(participating_roles.first).to be_a Module
74
+ expect(participating_roles.first.public_instance_methods).to include :something
75
+ end
76
+ end
77
+ end
78
+
79
+ describe 'context entry points are identified using ::entry' do
80
+ subject(:context_instance) { context.new }
81
+
82
+ shared_examples 'correctly manages the currently executing context' do
83
+ specify 'it assigns the executing context' do
84
+ context_instance.something { expect(DCI::Context.current).to be context_instance }
85
+ end
86
+
87
+ specify 'it restores the previously executing context' do
88
+ expect { context_instance.something {} }.to_not change { DCI::Context.current }
89
+ end
90
+ end
91
+
92
+ context 'when the method exists' do
93
+ let(:context) do
94
+ Class.new do
95
+ include DCI::Context
96
+
97
+ def something(&block)
98
+ block.call
99
+ end
100
+
101
+ entry :something
102
+ end
103
+ end
104
+
105
+ specify 'it can be called' do
106
+ expect(context_instance).to respond_to :something
107
+ expect(context_instance.something { :expected }).to be :expected
108
+ end
109
+
110
+ include_examples 'correctly manages the currently executing context'
111
+ end
112
+
113
+ context 'when the method does not exist' do
114
+ let(:context) do
115
+ Class.new do
116
+ include DCI::Context
117
+
118
+ entry :something
119
+ end
120
+ end
121
+
122
+ specify 'it cannot be called' do
123
+ expect(context_instance).to_not respond_to :something
124
+ expect { context_instance.something }.to raise_error(NoMethodError, /something/)
125
+ end
126
+
127
+ context 'once the method is defined' do
128
+ before do
129
+ context.class_exec do
130
+ def something(&block)
131
+ block.call
132
+ end
133
+ end
134
+ end
135
+
136
+ specify 'it can be called' do
137
+ expect(context_instance).to respond_to :something
138
+ expect(context_instance.something { :expected }).to be :expected
139
+ end
140
+
141
+ include_examples 'correctly manages the currently executing context'
142
+ end
143
+ end
144
+
145
+ describe 'when passed a block, that block becomes the definition' do
146
+ let(:context) do
147
+ Class.new do
148
+ include DCI::Context
149
+
150
+ entry :point do
151
+ :expected
152
+ end
153
+ end
154
+ end
155
+
156
+ specify 'that can be called' do
157
+ expect(context_instance).to respond_to :point
158
+ expect(context_instance.point).to be :expected
159
+ end
160
+
161
+ context 'when the block has parameters' do
162
+ let(:context) do
163
+ Class.new do
164
+ include DCI::Context
165
+
166
+ entry :parameters do |argument|
167
+ argument
168
+ end
169
+ end
170
+ end
171
+
172
+ specify 'those parameters are required' do
173
+ expect { context_instance.parameters }.to raise_error ArgumentError
174
+ end
175
+
176
+ specify 'and can be called with arguments' do
177
+ expect(context_instance.parameters(:expected)).to be :expected
178
+ end
179
+ end
180
+
181
+ context 'when the block takes a block' do
182
+ let(:context) do
183
+ Class.new do
184
+ include DCI::Context
185
+
186
+ entry :block do |&block|
187
+ block.call
188
+ end
189
+ end
190
+ end
191
+
192
+ specify 'it works' do
193
+ expect(context_instance.block { :expected }).to be :expected
194
+ end
195
+ end
196
+ end
197
+
198
+ describe 'when passed a lambda, that lambda becomes the definition' do
199
+ let(:context) do
200
+ Class.new do
201
+ include DCI::Context
202
+
203
+ entry :parameters, ->(argument) do
204
+ argument
205
+ end
206
+ end
207
+ end
208
+
209
+ specify 'that can be called' do
210
+ expect(context_instance).to respond_to :parameters
211
+ expect { context_instance.parameters }.to raise_error ArgumentError
212
+ expect(context_instance.parameters(:expected)).to be :expected
213
+ end
214
+ end
215
+ end
216
+
217
+ describe 'use case triggers are identified using ::trigger' do
218
+ let(:context) do
219
+ Class.new do
220
+ include DCI::Context
221
+ trigger :trigger_name, :RoleName, :method_name
222
+ end
223
+ end
224
+ let(:context_instance) { context.new }
225
+
226
+ specify 'the trigger is callable' do
227
+ expect(context_instance).to respond_to :trigger_name
228
+ end
229
+
230
+ context 'when the role does not exist' do
231
+ specify 'calling the trigger raises a NoMethodError' do
232
+ expect { context_instance.trigger_name }.to raise_error(NoMethodError, /RoleName/)
233
+ end
234
+ end
235
+
236
+ context 'when the role method does not exist' do
237
+ before { context.class_eval { role :RoleName } }
238
+
239
+ specify 'calling the trigger raises a NoMethodError' do
240
+ expect { context_instance.trigger_name }.to raise_error(NoMethodError, /method_name/)
241
+ end
242
+ end
243
+
244
+ context 'when the role and method exist' do
245
+ before do
246
+ context.class_eval do
247
+ role :RoleName do
248
+ def method_name(&block)
249
+ block.call
250
+ end
251
+ end
252
+
253
+ def initialize
254
+ self.RoleName = Object.new
255
+ end
256
+ end
257
+ end
258
+
259
+ specify 'it can be called' do
260
+ expect(context_instance.trigger_name { :expected }).to be :expected
261
+ end
262
+
263
+ specify 'it assigns the executing context' do
264
+ context_instance.trigger_name { expect(DCI::Context.current).to be context_instance }
265
+ end
266
+
267
+ specify 'it restores the previously executing context' do
268
+ expect { context_instance.trigger_name {} }.to_not change { DCI::Context.current }
269
+ end
270
+ end
271
+
272
+ context 'with two arguments' do
273
+ let(:context) do
274
+ Class.new do
275
+ include DCI::Context
276
+
277
+ trigger :trigger_name, :RoleName
278
+ end
279
+ end
280
+ let(:role_player) { double }
281
+
282
+ before { allow(context_instance).to receive(:RoleName).and_return(role_player) }
283
+
284
+ specify 'the method defaults to trigger name' do
285
+ expect(role_player).to receive :trigger_name
286
+ context_instance.trigger_name
287
+ end
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,119 @@
1
+ require 'dci/role_lookup'
2
+
3
+ describe DCI::RoleLookup do
4
+ context 'when roles are lowercase' do
5
+ class LowercaseContext
6
+ include DCI::Context
7
+ extend DCI::RoleLookup
8
+
9
+ role :one
10
+ role :two do
11
+ def access_one
12
+ one
13
+ end
14
+ end
15
+
16
+ entry :access_one_from_two do
17
+ two.access_one
18
+ end
19
+
20
+ def initialize
21
+ self.one = Object.new
22
+ self.two = Object.new
23
+ end
24
+ end
25
+
26
+ let(:context_instance) { LowercaseContext.new }
27
+
28
+ specify 'role lookup does not work' do
29
+ expect { context_instance.access_one_from_two }.to raise_error(NameError, /undefined.*one/)
30
+ end
31
+ end
32
+
33
+ context 'when roles are capitalized' do
34
+ context 'in an anonymous module' do
35
+ let(:context) do
36
+ Class.new do
37
+ include DCI::Context
38
+ extend DCI::RoleLookup
39
+
40
+ role :One
41
+
42
+ entry :access_one do
43
+ One
44
+ end
45
+ end
46
+ end
47
+
48
+ let(:context_instance) { context.new }
49
+
50
+ specify 'role lookup does not work' do
51
+ expect { context_instance.access_one }.to raise_error(NameError, /One/)
52
+ end
53
+ end
54
+
55
+ context 'in a named module' do
56
+ class NamedContext
57
+ include DCI::Context
58
+ extend DCI::RoleLookup
59
+
60
+ role :One
61
+ role :Two
62
+ role :Three do
63
+ def access_one
64
+ One
65
+ end
66
+ end
67
+
68
+ module TwoMethods
69
+ extend DCI::RoleLookup
70
+
71
+ def access_one
72
+ One
73
+ end
74
+ end
75
+
76
+ entry :access_one_from_two do
77
+ Two.access_one
78
+ end
79
+
80
+ entry :access_one_from_three do
81
+ Three.access_one
82
+ end
83
+
84
+ def initialize(one)
85
+ self.One = one
86
+ self.Two = cast(Object.new, :as => TwoMethods)
87
+ self.Three = Object.new
88
+ end
89
+ end
90
+
91
+ let(:context_instance) { NamedContext.new(one) }
92
+ let(:one) { double }
93
+
94
+ specify 'role lookup works' do
95
+ expect(context_instance.access_one_from_two).to be == one
96
+ expect(context_instance.access_one_from_three).to be == one
97
+ end
98
+
99
+ specify 'those roles cannot be referenced by a later executing context' do
100
+ class FirstContext
101
+ include DCI::Context
102
+ role :One
103
+ entry :call do
104
+ SecondContext.new.call
105
+ end
106
+ end
107
+
108
+ class SecondContext
109
+ include DCI::Context
110
+ entry :call do
111
+ One
112
+ end
113
+ end
114
+
115
+ expect { FirstContext.new.call }.to raise_error(NameError, /One/)
116
+ end
117
+ end
118
+ end
119
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bandy-dci
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Chris Bandy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '2.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '2.14'
27
+ description: Facilitate DCI in Ruby
28
+ email:
29
+ - bandy.chris@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/dci.rb
35
+ - lib/dci/castable.rb
36
+ - lib/dci/context.rb
37
+ - lib/dci/role_lookup.rb
38
+ - spec/dci/castable_spec.rb
39
+ - spec/dci/context_spec.rb
40
+ - spec/dci/role_lookup_spec.rb
41
+ homepage: https://github.com/cbandy/ruby-dci
42
+ licenses:
43
+ - Apache License Version 2.0
44
+ metadata: {}
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '1.9'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project:
61
+ rubygems_version: 2.0.14
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: DCI
65
+ test_files:
66
+ - spec/dci/castable_spec.rb
67
+ - spec/dci/context_spec.rb
68
+ - spec/dci/role_lookup_spec.rb