dci-ruby 2.0.0 → 2.1.0
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/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
|