dci-ruby 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +10 -7
- data/examples/money_transfer.rb +11 -4
- data/lib/dci-ruby/dci/context.rb +70 -31
- data/lib/dci-ruby/dci/role.rb +7 -16
- data/lib/dci-ruby/version.rb +1 -1
- data/spec/interaction_spec.rb +5 -5
- data/spec/roleplayers_spec.rb +4 -4
- metadata +3 -3
data/README.rdoc
CHANGED
@@ -34,34 +34,37 @@ dci-ruby gives you the class DCI::Context to inherit from to create your own con
|
|
34
34
|
end
|
35
35
|
|
36
36
|
# Interactions
|
37
|
-
def run(amount=
|
37
|
+
def run(amount=settings(:amount))
|
38
38
|
source_account.transfer(amount)
|
39
39
|
target_account.get_transfer(amount)
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
43
|
Every context defines some roles to be played by external objects (players) and their interactions. This way
|
44
|
-
you have all the agents and operations in a user case wrapped in just one entity instead of
|
44
|
+
you have all the agents and operations in a user case wrapped in just one entity instead of spread out throughout the
|
45
45
|
application code.
|
46
46
|
|
47
|
-
Use the defined contexts, instantiating them wherever you need in your code:
|
47
|
+
Use the defined contexts, instantiating them, wherever you need in your code:
|
48
48
|
|
49
49
|
MoneyTransfer.new(:source_account => Account.new(1),
|
50
50
|
:target_account => Account.new(2)).run(100)
|
51
51
|
|
52
|
-
In a context instance, every role
|
53
|
-
and
|
52
|
+
In a context instance, every role instantiates an object (roleplayer) that gathers the behaviour defined inside its role,
|
53
|
+
and has private access to the original object adopting the role (player): the Account instances above are players.
|
54
54
|
Instances of MoneyTransfer::SourceAccount and MoneyTransfer::TargetAccount accessible inside MoneyTransfer.new via
|
55
55
|
the private instance_methods #source_account and #target_account are roleplayers.
|
56
56
|
Also, every roleplayer has private access to the rest of roleplayers in its context.
|
57
57
|
|
58
|
+
Unlike extending players with role modules this Presenter approach gets on well with ruby method call caching mechanism.
|
59
|
+
(see Tony Arcieri's article {DCI in Ruby is completely broken}[http://tonyarcieri.com/dci-in-ruby-is-completely-broken])
|
60
|
+
|
58
61
|
When instanciating a Context, the extra no-role pairs given as arguments are read-only attributes accessible inside the instance:
|
59
62
|
|
60
63
|
MoneyTransfer.new(:source_account => Account.new(1),
|
61
64
|
:target_account => Account.new(2),
|
62
|
-
:
|
65
|
+
:amount => 500).run
|
63
66
|
|
64
|
-
here,
|
67
|
+
here, :amount is not a player (has no associated role) but is still privately accessible in the interactions via settings(:amount).
|
65
68
|
|
66
69
|
See the examples[https://github.com/ltello/dci-ruby/tree/master/examples] folder for examples of use and the DCI-Sample[https://github.com/ltello/DCI-Sample] repository for a sample application using DCI through this gem.
|
67
70
|
|
data/examples/money_transfer.rb
CHANGED
@@ -42,7 +42,7 @@ class MoneyTransferContext < DCI::Context
|
|
42
42
|
|
43
43
|
# Interactions
|
44
44
|
|
45
|
-
def run
|
45
|
+
def run(amount=settings(:amount))
|
46
46
|
puts "Balances Before: #{balances}"
|
47
47
|
source_account.run_transfer_of(amount)
|
48
48
|
target_account.run_transfer_of(amount)
|
@@ -61,6 +61,13 @@ class MoneyTransferContext < DCI::Context
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
-
|
65
|
-
|
66
|
-
|
64
|
+
acc1 = CheckingAccount.new(1, 1000)
|
65
|
+
acc2 = CheckingAccount.new(2)
|
66
|
+
|
67
|
+
MoneyTransferContext.new(:source_account => acc1,
|
68
|
+
:target_account => acc2,
|
69
|
+
:amount => 200).run
|
70
|
+
4.times do
|
71
|
+
MoneyTransferContext.new(:source_account => acc1,
|
72
|
+
:target_account => acc2).run(200)
|
73
|
+
end
|
data/lib/dci-ruby/dci/context.rb
CHANGED
@@ -19,16 +19,51 @@ module DCI
|
|
19
19
|
|
20
20
|
private
|
21
21
|
|
22
|
-
# The macro role is defined to allow a subclass of Context to define roles in its definition.
|
23
|
-
# Every new role
|
24
|
-
#
|
25
|
-
|
26
|
-
|
27
|
-
|
22
|
+
# The macro role is defined to allow a subclass of Context to define roles in its definition body.
|
23
|
+
# Every new role is added to the hash of roles in that Context subclass.
|
24
|
+
# A reader to access the object playing the new role is also defined and available in every instance of the context subclass.
|
25
|
+
# Also, readers to allow each other role access are defined.
|
26
|
+
def role(rolekey, &block)
|
27
|
+
raise "role name must be a symbol" unless rolekey.is_a?(Symbol)
|
28
|
+
create_role_from(rolekey, &block)
|
29
|
+
define_reader_for_role(rolekey)
|
30
|
+
define_mate_roleplayers_readers_after_newrole(rolekey)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Adds a new entry to the roles accumulator hash.
|
34
|
+
def create_role_from(key, &block)
|
35
|
+
roles.merge!(key => create_role_subclass_from(key, &block))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Defines and return a new subclass of DCI::Role named after the given rolekey and with body the given block.
|
39
|
+
def create_role_subclass_from(rolekey, &block)
|
40
|
+
new_klass_name = rolekey.to_s.split(/\_+/).map(&:capitalize).join('')
|
28
41
|
const_set(new_klass_name, Class.new(::DCI::Role, &block))
|
29
|
-
|
30
|
-
|
31
|
-
|
42
|
+
end
|
43
|
+
|
44
|
+
# Defines a private reader to allow a context instance access to the roleplayer object associated to the given rolekey.
|
45
|
+
def define_reader_for_role(rolekey)
|
46
|
+
attr_reader rolekey
|
47
|
+
private rolekey
|
48
|
+
end
|
49
|
+
|
50
|
+
# After a new role is defined, you've got to create a reader method for this new role in the rest of context
|
51
|
+
# roles, and viceverse: create a reader method in the new role klass for each of the other roles in the context.
|
52
|
+
# This method does exactly this.
|
53
|
+
def define_mate_roleplayers_readers_after_newrole(new_rolekey)
|
54
|
+
new_roleklass = roles[new_rolekey]
|
55
|
+
mate_roles = mate_roles_of(new_rolekey)
|
56
|
+
mate_roles.each do |mate_rolekey, mate_roleklass|
|
57
|
+
mate_roleklass.add_role_reader_for!(new_rolekey)
|
58
|
+
new_roleklass.add_role_reader_for!(mate_rolekey)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# For a give role key, returns a hash with the rest of the roles (pair :rolekey => roleklass) in the context it belongs to.
|
63
|
+
def mate_roles_of(rolekey)
|
64
|
+
roles.dup.tap do |roles|
|
65
|
+
roles.delete(rolekey)
|
66
|
+
end
|
32
67
|
end
|
33
68
|
|
34
69
|
end
|
@@ -40,19 +75,24 @@ module DCI
|
|
40
75
|
# Non players args are associated to instance_variables and readers defined.
|
41
76
|
def initialize(args={})
|
42
77
|
check_all_roles_provided_in!(args)
|
43
|
-
players, noplayers = args.partition {|key,
|
78
|
+
players, noplayers = args.partition {|key, *| roles.has_key?(key)}.map {|group| Hash[*group.flatten]}
|
44
79
|
assign_roles_to_players(players)
|
45
|
-
|
80
|
+
@settings = noplayers
|
46
81
|
end
|
47
82
|
|
48
83
|
|
49
84
|
private
|
50
85
|
|
86
|
+
# Private access to the extra args received in the instantiation.
|
87
|
+
def settings(key)
|
88
|
+
@settings[key]
|
89
|
+
end
|
90
|
+
|
51
91
|
# Checks there is a player for each role.
|
52
92
|
# Raises and error message in case of missing roles.
|
53
93
|
def check_all_roles_provided_in!(players={})
|
54
|
-
|
55
|
-
raise "missing roles #{
|
94
|
+
missing_rolekeys = missing_roles(players)
|
95
|
+
raise "missing roles #{missing_rolekeys}" unless missing_rolekeys.empty?
|
56
96
|
end
|
57
97
|
|
58
98
|
# The list of roles with no player provided
|
@@ -62,33 +102,32 @@ module DCI
|
|
62
102
|
|
63
103
|
# Associates every role to the intended player.
|
64
104
|
def assign_roles_to_players(players={})
|
65
|
-
roles.keys.each do |
|
66
|
-
assign_role_to_player(
|
105
|
+
roles.keys.each do |rolekey|
|
106
|
+
assign_role_to_player(rolekey, players[rolekey])
|
67
107
|
end
|
68
108
|
end
|
69
109
|
|
70
110
|
# Associates a role to an intended player:
|
71
|
-
# - A new role instance is created from the associated
|
111
|
+
# - A new role instance is created from the associated rolekey class and the player to get that role.
|
72
112
|
# - The new role instance has access to the context it is playing.
|
73
113
|
# - The new role instance has access to the rest of players in its context through instance methods named after their role keys.
|
74
114
|
# - The context instance has access to this new role instance through an instance method named after the role key.
|
75
|
-
def assign_role_to_player(
|
76
|
-
role_klass
|
77
|
-
|
78
|
-
|
79
|
-
instance_variable_set(:"@#{role_key}", role_instance)
|
115
|
+
def assign_role_to_player(rolekey, player)
|
116
|
+
role_klass = roles[rolekey]
|
117
|
+
role_instance = role_klass.new(:player => player, :context => self)
|
118
|
+
instance_variable_set(:"@#{rolekey}", role_instance)
|
80
119
|
end
|
81
120
|
|
82
|
-
# For each given pair in vars, define a private method named the key that returns the entry associated value.
|
83
|
-
def define_readers_for_no_players(vars={})
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
121
|
+
# # For each given pair in vars, define a private method named the key that returns the entry associated value.
|
122
|
+
# def define_readers_for_no_players(vars={})
|
123
|
+
# vars.each do |name, value|
|
124
|
+
# instance_variable_set(:"@#{name}", value)
|
125
|
+
# singleton_class.class_exec(name.to_sym) do |varkey|
|
126
|
+
# private
|
127
|
+
# attr_reader varkey
|
128
|
+
# end
|
129
|
+
# end
|
130
|
+
# end
|
92
131
|
|
93
132
|
end
|
94
133
|
|
data/lib/dci-ruby/dci/role.rb
CHANGED
@@ -10,6 +10,13 @@ module DCI
|
|
10
10
|
super
|
11
11
|
end
|
12
12
|
|
13
|
+
# Defines a new reader instance method for a context mate role, delegating it to the context object.
|
14
|
+
def add_role_reader_for!(rolekey)
|
15
|
+
return if private_method_defined?(rolekey)
|
16
|
+
define_method(rolekey) {@context.send(rolekey)}
|
17
|
+
private rolekey
|
18
|
+
end
|
19
|
+
|
13
20
|
end
|
14
21
|
|
15
22
|
# Opts:
|
@@ -19,7 +26,6 @@ module DCI
|
|
19
26
|
def initialize(opts={})
|
20
27
|
@player = opts[:player]
|
21
28
|
@context = opts[:context]
|
22
|
-
define_role_mate_methods!(opts[:role_mate_keys])
|
23
29
|
end
|
24
30
|
|
25
31
|
|
@@ -30,20 +36,5 @@ module DCI
|
|
30
36
|
@player
|
31
37
|
end
|
32
38
|
|
33
|
-
# For each role in the context, define a method so inside a role you can access all the others.
|
34
|
-
def define_role_mate_methods!(role_mate_keys)
|
35
|
-
self.class.class_exec(role_mate_keys) do |other_role_keys|
|
36
|
-
other_role_keys.each do |role_key|
|
37
|
-
if not private_method_defined?(role_key)
|
38
|
-
define_method(role_key) do
|
39
|
-
@context.send(role_key)
|
40
|
-
end
|
41
|
-
private role_key
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
39
|
end
|
48
|
-
|
49
40
|
end
|
data/lib/dci-ruby/version.rb
CHANGED
data/spec/interaction_spec.rb
CHANGED
@@ -15,7 +15,7 @@ describe 'Interaction:' do
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
@player1, @player2 = Object.new, Object.new
|
18
|
-
@test_interactions_context = TestingInteractionsContext.new(:role1 => @player1, :role2 => @player2, :
|
18
|
+
@test_interactions_context = TestingInteractionsContext.new(:role1 => @player1, :role2 => @player2, :setting1 => :one)
|
19
19
|
end
|
20
20
|
|
21
21
|
it("...the developer has access to all the roleplayers...") do
|
@@ -26,11 +26,11 @@ describe 'Interaction:' do
|
|
26
26
|
@test_interactions_context.private_methods(false).should include('role1', 'role2')
|
27
27
|
end
|
28
28
|
|
29
|
-
it("He also have access to extra args received in the instantiation of its context...") do
|
30
|
-
@test_interactions_context.
|
29
|
+
it("He also have private access to extra args received in the instantiation of its context...") do
|
30
|
+
@test_interactions_context.private_methods.should include('settings')
|
31
31
|
end
|
32
|
-
it("...
|
33
|
-
@test_interactions_context.
|
32
|
+
it("...calling #settings(key)") do
|
33
|
+
@test_interactions_context.send(:settings, :setting1).should be(:one)
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
data/spec/roleplayers_spec.rb
CHANGED
@@ -63,11 +63,11 @@ describe 'RolePlayers' do
|
|
63
63
|
@testing_roleplayers_context.send(:role2).send(:player).name.should eq('player2')
|
64
64
|
end
|
65
65
|
|
66
|
-
it("Roleplayers have private access to other roleplayers in their context
|
67
|
-
@testing_roleplayers_context.send(:role2).private_methods
|
68
|
-
@testing_roleplayers_context.send(:role2).rolemethod2.should
|
66
|
+
it("Roleplayers have private access to other roleplayers in their context through methods named after their keys.") do
|
67
|
+
@testing_roleplayers_context.send(:role2).private_methods.should include('role1')
|
68
|
+
@testing_roleplayers_context.send(:role2).rolemethod2.should be(@testing_roleplayers_context.send(:role1))
|
69
69
|
end
|
70
|
-
it("
|
70
|
+
it("However, they dont have a method to access the context.") do
|
71
71
|
expect {@testing_roleplayers_context.send(:role2).send(:context)}.to raise_error(NoMethodError)
|
72
72
|
@testing_roleplayers_context.send(:role2).instance_variables.should include('@context')
|
73
73
|
@testing_roleplayers_context.send(:role2).instance_variable_get(:@context).should be(@testing_roleplayers_context)
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dci-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 11
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 2
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 2.0.0
|
10
|
+
version: 2.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Lorenzo Tello
|