bandy-dci 0.0.4

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,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