drsi 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2e2a89f6f00a413fd3ae85e494e75e0f4bcc4aac
4
+ data.tar.gz: b44a051e6678c13077ae65a55e8c11ba36530128
5
+ SHA512:
6
+ metadata.gz: abd2d518e9b8952cfff3d150e323e0c440529aadf51c5dc2d2df388c8e93353d86bdd542faeaaeb276635ca503ce1cfa48b7bc8a43f1a8c94fa3facf18a7fb40
7
+ data.tar.gz: eae8418a3b0e7949b569cc73a510998d6a7a358ce4836d1548eb5c713b31ee0b79fca8c69cf51b7641c93bfa72c41e547b9edf2bc5393108e7faf01b487ec7aa
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format nested
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ drsi
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.1.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in drsi.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Lorenzo Tello
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # drsi
2
+
3
+ **_Trygve Reenskaug_**, the parent of MVC, proposes an evolution to traditional OO paradigm: [The DCI Architecture: A New Vision of Object-Oriented Programming](http://www.artima.com/articles/dci_vision.html "The DCI Architecture: A New Vision of Object-Oriented Programming").
4
+
5
+ This gem makes Data-Context-Interaction paradigm ready to be used in your Ruby application. See also [Data Context Interaction: The Evolution of the Object Oriented Paradigm](http://rubysource.com/dci-the-evolution-of-the-object-oriented-paradigm/ "Data Context Interaction: The Evolution of the Object Oriented Paradigm").
6
+
7
+ ## Installation
8
+
9
+ Install as usual, either with rubygems
10
+
11
+ gem install drsi
12
+
13
+ or including it in your Gemfile and running bundle install:
14
+
15
+ # Gemfile
16
+ gem "drsi"
17
+
18
+ $ bundle install
19
+
20
+ _**Note**: only **ruby 2.1+** compatible._
21
+
22
+
23
+ ## Usage
24
+
25
+ dci-ruby gives you the class DCI::Context to inherit from to create your own contexts:
26
+
27
+ class MoneyTransfer < DCI::Context
28
+
29
+ # Roles
30
+
31
+ role :source_account do
32
+ def transfer(amount)
33
+ self.balance -= amount
34
+ target_account.get_transfer(amount)
35
+ end
36
+ end
37
+
38
+ role :target_account do
39
+ def get_transfer(amount)
40
+ self.balance += amount
41
+ end
42
+ end
43
+
44
+
45
+ # Interactions
46
+
47
+ def run(amount = settings(:amount))
48
+ source_account.transfer(amount)
49
+ end
50
+ end
51
+
52
+ Every context defines some roles to be played by external objects (players) and their interactions. This way
53
+ you have all the agents and operations in a use case wrapped in just one entity instead of spread out throughout the
54
+ application code.
55
+
56
+ Use the defined contexts, instantiating them, wherever you need in your code:
57
+
58
+ MoneyTransfer.new(:source_account => Account.new(1),
59
+ :target_account => Account.new(2)).run(100)
60
+
61
+ or the short preferred way:
62
+
63
+ MoneyTransfer[:source_account => Account.new(1),
64
+ :target_account => Account.new(2),
65
+ :amount => 100]
66
+
67
+ Inside a context instance, every player object incorporates the behaviour (methods) defined by its role while keeping its own.
68
+
69
+ The Account instances above are players. They are accesible inside #run through #source_account and #target_account private methods.
70
+ Also, every role player has private access to the rest of role players in the context.
71
+
72
+ Unlike the Presenter approach in dci-ruby (where the object to play a role and the one inside the context playing it are associated but different), this extending/unextending approach preserves unique identity of objects playing roles.
73
+
74
+ When instanciating a Context, the extra no-role pairs given as arguments are read-only attributes accessible via #settings:
75
+
76
+ MoneyTransfer[:source_account => Account.new(1),
77
+ :target_account => Account.new(2),
78
+ :amount => 500]
79
+
80
+ here, :amount is not a player (has no associated role) but is still privately accessible both in the interactions and the roles
81
+ via #settings(:amount).
82
+
83
+
84
+ See the [examples](https://github.com/ltello/drsi/tree/master/examples) folder for examples of use and the [drsi-DCI-Sample](https://github.com/ltello/drsi-DCI-Sample) repository for a sample Rails application using DCI through this gem.
85
+
86
+ Notice how your models and controllers are not overloaded anymore. They are thinner and simpler.
87
+ Also note how now most of the functionality of the system is isolated, totally dry-ied and easily maintainable in the different context classes.
88
+
89
+ ## Contributing
90
+
91
+ 1. Fork it
92
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
93
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
94
+ 4. Push to the branch (`git push origin my-new-feature`)
95
+ 5. Create new Pull Request
96
+
97
+
98
+ ## Copyright
99
+
100
+ Copyright (c) 2012, 2013 Lorenzo Tello. See LICENSE.txt for further details.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/drsi.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/drsi/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "drsi"
6
+ gem.version = Drsi::VERSION
7
+ gem.authors = ["Lorenzo Tello"]
8
+ gem.email = ["ltello8a@gmail.com"]
9
+ gem.homepage = "http://github.com/ltello/drsi"
10
+ gem.description = "Make DCI paradigm available to Ruby applications"
11
+ gem.summary = "Make DCI paradigm available to Ruby applications by enabling developers defining contexts subclassing the class DCI::Context. You define roles inside the definition. Match roles and player objects in context instantiation. Single Identity approach."
12
+ gem.licenses = ["MIT"]
13
+
14
+ gem.rubyforge_project = "drsi"
15
+
16
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ gem.files = `git ls-files`.split("\n")
18
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ gem.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ gem.add_development_dependency "rspec", "~> 2.0"
23
+ # s.add_runtime_dependency "rest-client"
24
+
25
+ end
@@ -0,0 +1,80 @@
1
+ require 'drsi'
2
+
3
+
4
+ class CheckingAccount
5
+ attr_reader :account_id, :currency
6
+ attr_accessor :balance
7
+
8
+ def initialize(account_id, initial_balance)
9
+ @account_id = account_id
10
+ b, @currency = initial_balance.split(' ')
11
+ @balance = b.to_i
12
+ end
13
+ end
14
+
15
+ class Amount
16
+ attr_reader :quantity, :currency
17
+
18
+ def initialize(data)
19
+ q, @currency = data.split(' ')
20
+ @quantity = q.to_i
21
+ end
22
+
23
+ def to_s
24
+ "#{quantity}#{currency}"
25
+ end
26
+ end
27
+
28
+
29
+ class MoneyTransferContext < DCI::Context
30
+
31
+ # Roles Definitions
32
+
33
+ role :source_account do
34
+ def run_transfer
35
+ self.balance -= amount.quantity
36
+ puts "\t\tAccount(\##{account_id}) sent #{amount} to Account(\##{target_account.account_id})."
37
+ end
38
+ end
39
+
40
+ role :target_account do
41
+ def run_transfer
42
+ self.balance += amount.quantity
43
+ puts "\t\tAccount(\##{account_id}) received #{amount} from Account(\##{source_account.account_id})."
44
+ end
45
+ end
46
+
47
+ role :amount
48
+
49
+
50
+ # Interactions
51
+
52
+ def run
53
+ puts "\nMoney Transfer of #{amount} between Account(\##{source_account.account_id}) and Account(\##{target_account.account_id})"
54
+ puts "\tBalances Before: #{balances}"
55
+ source_account.run_transfer
56
+ target_account.run_transfer
57
+ puts "\tBalances After: #{balances}"
58
+ end
59
+
60
+
61
+ private
62
+
63
+ def accounts
64
+ [source_account, target_account]
65
+ end
66
+
67
+ def balances
68
+ accounts.map {|account| "#{account.balance}#{account.currency}"}.join(' - ')
69
+ end
70
+ end
71
+
72
+ acc1 = CheckingAccount.new(1, '1000 €')
73
+ acc2 = CheckingAccount.new(2, '0 €')
74
+ amount = Amount.new('200 €')
75
+
76
+ 5.times do
77
+ MoneyTransferContext.new(:source_account => acc1,
78
+ :target_account => acc2,
79
+ :amount => amount).run
80
+ end
@@ -0,0 +1,170 @@
1
+ require 'drsi/dci/role'
2
+
3
+ module DCI
4
+ class Context
5
+
6
+ class << self
7
+
8
+ # Every subclass of Context has is own class and instance method roles defined.
9
+ # The instance method delegates value to the class.
10
+ def inherited(subklass)
11
+ subklass.class_eval do
12
+ @roles ||= {}
13
+ def self.roles; @roles end
14
+ def roles; self.class.roles end
15
+ private :roles
16
+ assign_unplay_roles_within_klass_instance_methods!(subklass)
17
+ end
18
+ end
19
+
20
+ # A short way for ContextSubclass.new(players_and_extra_args).run(extra_args)
21
+ def [](*args)
22
+ new(*args).run
23
+ end
24
+
25
+
26
+ private
27
+
28
+ # The macro role is defined to allow a subclass of Context to define roles in its definition body.
29
+ # Every new role is added to the hash of roles in that Context subclass.
30
+ # A reader to access the object playing the new role is also defined and available in every instance of the context subclass.
31
+ # Also, readers to allow each other role access are defined.
32
+ def role(rolekey, &block)
33
+ raise "role name must be a symbol" unless rolekey.is_a?(Symbol)
34
+ create_role_from(rolekey, &block)
35
+ define_reader_for_role(rolekey)
36
+ define_mate_roleplayers_readers_after_newrole(rolekey)
37
+ end
38
+
39
+ # Adds a new entry to the roles accumulator hash.
40
+ def create_role_from(key, &block)
41
+ roles.merge!(key => create_role_module_from(key, &block))
42
+ end
43
+
44
+ # Defines and return a new subclass of DCI::Role named after the given rolekey and with body the given block.
45
+ def create_role_module_from(rolekey, &block)
46
+ new_mod_name = rolekey.to_s.split(/\_+/).map(&:capitalize).join('')
47
+ const_set(new_mod_name, Module.new(&block))
48
+ const_get(new_mod_name).tap {|mod| mod.send(:extend, ::DCI::Role)}
49
+ end
50
+
51
+ # Defines a private reader to allow a context instance access to the roleplayer object associated to the given rolekey.
52
+ def define_reader_for_role(rolekey)
53
+ private
54
+ attr_reader rolekey
55
+ end
56
+
57
+ # After a new role is defined, you've got to create a reader method for this new role in the rest of context
58
+ # roles, and viceverse: create a reader method in the new role mod for each of the other roles in the context.
59
+ # This method does exactly this.
60
+ def define_mate_roleplayers_readers_after_newrole(new_rolekey)
61
+ new_role_mod = roles[new_rolekey]
62
+ mate_roles = mate_roles_of(new_rolekey)
63
+ mate_roles.each do |mate_rolekey, mate_role_mod|
64
+ mate_role_mod.send(:add_role_reader_for!, new_rolekey)
65
+ new_role_mod.send(:add_role_reader_for!, mate_rolekey)
66
+ end
67
+ end
68
+
69
+ # For a give role key, returns a hash with the rest of the roles (pair :rolekey => role_mod) in the context it belongs to.
70
+ def mate_roles_of(rolekey)
71
+ roles.dup.tap do |roles|
72
+ roles.delete(rolekey)
73
+ end
74
+ end
75
+
76
+ # Wraps every existing public/protected instance_method of klass (not superclasses methods) to assign roles to
77
+ # player objects before the execution and to un-assign roles from player objects at the end of execution just
78
+ # before returning control.
79
+ # Also inject code to magically do the same to every new method defined in klass.
80
+ def assign_unplay_roles_within_klass_instance_methods!(klass)
81
+ klass.instance_methods(false).each do |existing_methodname|
82
+ assign_unplay_roles_within_klass_instance_method!(klass, existing_methodname)
83
+ end
84
+ def klass.method_added(methodname)
85
+ if not @context_internals and public_method_defined?(methodname)
86
+ @context_internals = true
87
+ assign_unplay_roles_within_klass_instance_method!(self, methodname)
88
+ @context_internals = false
89
+ end
90
+ end
91
+ end
92
+
93
+ # Wraps the given klass's methodname to assign/un-assign roles to player objects before and after actual method
94
+ # execution.
95
+ def assign_unplay_roles_within_klass_instance_method!(klass, methodname)
96
+ klass.class_eval do
97
+ method_object = instance_method(methodname)
98
+ define_method(methodname) do |*args, &block|
99
+ players_play_role!
100
+ method_object.bind(self).call(*args, &block).tap {players_unplay_role!}
101
+ end
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+
108
+ # Instances of a defined subclass of Context are initialized checking first that all subclass defined roles
109
+ # are provided in the creation invocation raising an error if any of them is missing.
110
+ # Once the previous check is met, every object playing in the context instance is associated to the stated role.
111
+ # Non players args are associated to instance_variables and readers defined.
112
+ def initialize(args={})
113
+ check_all_roles_provided_in!(args)
114
+ players, noplayers = args.partition {|key, *| roles.has_key?(key)}.map {|group| Hash[*group.flatten]}
115
+ @_players = players
116
+ @settings = noplayers
117
+ end
118
+
119
+
120
+ private
121
+
122
+ # Private access to the extra args received in the instantiation.
123
+ # Returns a hash (copy of the instantiation extra args) with only the args included in 'keys' or all of them
124
+ # when called with no args.
125
+ def settings(*keys)
126
+ return @settings.dup if keys.empty?
127
+ entries = @settings.reject {|k, v| !keys.include?(k)}
128
+ keys.size == 1 ? entries.values.first : entries
129
+ end
130
+
131
+ # Checks there is a player for each role.
132
+ # Raises and error message in case of missing roles.
133
+ def check_all_roles_provided_in!(players={})
134
+ missing_rolekeys = missing_roles(players)
135
+ raise "missing roles #{missing_rolekeys}" unless missing_rolekeys.empty?
136
+ end
137
+
138
+ # The list of roles with no player provided
139
+ def missing_roles(players={})
140
+ (roles.keys - players.keys)
141
+ end
142
+
143
+ # Associates every role to the intended player.
144
+ def players_play_role!
145
+ roles.keys.each do |rolekey|
146
+ assign_role_to_player!(rolekey, @_players[rolekey])
147
+ end
148
+ end
149
+
150
+ # Associates a role to an intended player:
151
+ # - The player object is 'extended' with the methods of the role to play.
152
+ # - The player get access to the context it is playing.
153
+ # - The player get access to the rest of players in its context through instance methods named after their role keys.
154
+ # - This context instance get access to this new role player through an instance method named after the role key.
155
+ def assign_role_to_player!(rolekey, player)
156
+ role_mod = roles[rolekey]
157
+ player.__play_role!(role_mod, self)
158
+ instance_variable_set(:"@#{rolekey}", player)
159
+ end
160
+
161
+ # Disassociates every role from the playing object.
162
+ def players_unplay_role!
163
+ roles.keys.each do |rolekey|
164
+ @_players[rolekey].__unplay_last_role!
165
+ # 'instance_variable_set(:"@#{rolekey}", nil)
166
+ end
167
+ end
168
+
169
+ end
170
+ end
@@ -0,0 +1,18 @@
1
+ module DCI
2
+ module Role
3
+
4
+ private
5
+
6
+ def context
7
+ raise 'This method must be redefined in every module including DCI::Role'
8
+ end
9
+
10
+ # Defines a new private reader instance method for a context mate role, delegating it to the context object.
11
+ def add_role_reader_for!(rolekey)
12
+ return if private_method_defined?(rolekey)
13
+ private
14
+ define_method(rolekey) {context.send(rolekey)}
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ class Module
2
+
3
+ # Define instance methods delegating execution to the corresponding ones in 'mod'.
4
+ def __copy_instance_methods_from(mod)
5
+ [:public_instance_methods, :protected_instance_methods, :private_instance_methods].each do |methods_type|
6
+ methods = mod.send(methods_type, false).map {|methodname| mod.instance_method(methodname)}
7
+ type = methods_type.to_s.split('_').first.to_sym
8
+ __add_instance_methods(methods, type)
9
+ end
10
+ end
11
+
12
+
13
+ private
14
+
15
+ # Define instance methods binding to self the unbound ones received in 'methods'.
16
+ # Also, set their visibility from 'type' (:public, :protected, :private).
17
+ def __add_instance_methods(methods, type)
18
+ module_exec(methods, type) do |methods, type|
19
+ methods.each do |method|
20
+ define_method(method.name) do |*args, &block|
21
+ method.bind(self).call(*args, &block)
22
+ end
23
+ end
24
+ send(type, *methods.map(&:name))
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,6 @@
1
+ require 'drsi/rolable'
2
+
3
+ class Object
4
+ include Rolable
5
+ end
6
+
@@ -0,0 +1,66 @@
1
+ require 'drsi/module'
2
+
3
+ # This module defines a mechanism to extend and 'unextend' modules in an object.
4
+ #
5
+ # The idea is to provide an object with a heap of extended modules:
6
+ # the highest ones filled with the methods associated to the roles the object currently plays,
7
+ # and the lowest ones, clean (no methods) when the object finishes playing roles.
8
+ # reusing the empty ones or adding and extending new ones when it is needed.
9
+ module Rolable
10
+
11
+ # Make an object play the role defined as a module in 'mod'
12
+ def __play_role!(role_klass, context)
13
+ new_role = __next_empty_role
14
+ new_role.__copy_instance_methods_from(role_klass)
15
+ new_role.send(:define_method, :context) {context}
16
+ end
17
+
18
+ # Make an object stop playing the last role it plays, if any.
19
+ def __unplay_last_role!
20
+ if role = __last_role
21
+ methods = role.public_instance_methods(false) + role.protected_instance_methods(false) + role.private_instance_methods(false)
22
+ methods.each {|name| role.send(:remove_method, name)}
23
+ @__last_role_index = __last_role_index - 1
24
+ end
25
+ end
26
+
27
+
28
+ private
29
+
30
+ def __roles
31
+ @__roles ||= Array.new
32
+ end
33
+
34
+ def __last_role_index
35
+ @__last_role_index ||= -1
36
+ end
37
+
38
+ def __last_role
39
+ __roles[__last_role_index]
40
+ end
41
+
42
+ # Returns the highest role module free of methods. If none, creates a new empty module ready to be filled with
43
+ # role instance methods.
44
+ def __next_empty_role
45
+ @__last_role_index = __last_role_index + 1
46
+ __add_empty_role! unless __last_role
47
+ __last_role
48
+ end
49
+
50
+ # Creates and extends a new module ready to be filled with role instance methods.
51
+ def __add_empty_role!
52
+ role = Module.new
53
+ extend(role)
54
+ __roles << role
55
+ end
56
+
57
+ # The context a role is played within. This method must be overidden in every __role definition module.
58
+ def context
59
+ nil
60
+ end
61
+
62
+ # The role definition code also have private access to the extra args given in the context instantiation.
63
+ def settings(*keys)
64
+ context.send(:settings, *keys) if context
65
+ end
66
+ end
@@ -0,0 +1,3 @@
1
+ module Drsi
2
+ VERSION = "0.0.1"
3
+ end
data/lib/drsi.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'drsi/version'
2
+ require 'drsi/object'
3
+ require 'drsi/dci/context'
4
+
5
+ module Drsi
6
+ end
@@ -0,0 +1,117 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe DCI::Context do
4
+
5
+ context "Definition:" do
6
+ context "When Inheriting from DCI::Context..." do
7
+ before(:all) do
8
+ class TestingDefinitionContext < DCI::Context
9
+ role :role_name do
10
+ def role_name_method
11
+ end
12
+ end
13
+
14
+ def interaction1
15
+ end
16
+ end
17
+ end
18
+
19
+ it("...a new dci context is ready to be used...") {TestingDefinitionContext.superclass.should be(DCI::Context)}
20
+ it("...in which the developer can define roles...") do
21
+ TestingDefinitionContext.private_methods.map(&:to_s).should include("role")
22
+ end
23
+ it("...but privately inside the subclass.") {TestingDefinitionContext.should_not respond_to(:role)}
24
+
25
+ it("A role is defined calling the private macro #role with a role_key and a block defining the specific methods of the role.") do
26
+ TestingDefinitionContext.roles.size.should be(1)
27
+ TestingDefinitionContext.roles.keys.should include(:role_name)
28
+ end
29
+
30
+ it("The #roles public class_and_instance_method will return a hash with pairs (role_key => ContextSubclass::Rolekey)...") do
31
+ TestingDefinitionContext.roles.should eq({:role_name => TestingDefinitionContext::RoleName})
32
+ TestingDefinitionContext.new(:role_name => Object.new).send(:roles).should eq(:role_name => TestingDefinitionContext::RoleName)
33
+ end
34
+ it("... where every ContextSubclass::Rolekey is a new module created at load time,...") do
35
+ TestingDefinitionContext.roles[:role_name].should be_a(Module)
36
+ TestingDefinitionContext.roles[:role_name].should be(TestingDefinitionContext::RoleName)
37
+ end
38
+ it("... named after the associated role_key...") do
39
+ TestingDefinitionContext.const_defined?(:RoleName).should be(true)
40
+ end
41
+ it("... and defined after the block given to the associated role in its definition.") do
42
+ TestingDefinitionContext::RoleName.public_instance_methods.map(&:to_s).should include("role_name_method")
43
+ end
44
+
45
+ it("Inside the context subclass, the developer defines context methods (instance methods) that act as interactions.") do
46
+ TestingDefinitionContext.public_instance_methods(false).map(&:to_s).should include('interaction1')
47
+ end
48
+ end
49
+ end
50
+
51
+ context "Use:" do
52
+ context "To use a Context..." do
53
+ before(:all) do
54
+ class TestingUseContext < DCI::Context
55
+ role :role1 do
56
+ def role1_method
57
+ :role1_method
58
+ end
59
+ end
60
+ role :role2 do
61
+ end
62
+
63
+ def run
64
+ role1.role1_method
65
+ end
66
+
67
+ def interaction2
68
+ role1
69
+ end
70
+
71
+ def interaction3
72
+ role1.object_id - role2.object_id
73
+ end
74
+ end
75
+ @player1, @player2 = Object.new, Object.new
76
+ @context_instance_1 = TestingUseContext.new(:role1 => @player1, :role2 => @player2)
77
+ @context_instance_2 = TestingUseContext.new(:role1 => @player1, :role2 => @player2, :extra_arg => :extra)
78
+ end
79
+
80
+ it("...instanciate it from its correspondig DCI::Context subclass as usual...") do
81
+ @context_instance_1.should be_a(TestingUseContext)
82
+ end
83
+ it("...providing pairs of type :rolekey1 => player1 as arguments...") do
84
+ expect {TestingUseContext.new(@player1, @player2)}.to raise_error
85
+ end
86
+ it("...with ALL the role_keys in the subclass as keys...") do
87
+ expect {TestingUseContext.new(:role1 => @player1)}.to raise_error(/missing roles(.+)role2/)
88
+ end
89
+ it("...and the objects to play those roles as values.") do
90
+ @context_instance_1.interaction2
91
+ [@player1, @player2].should include(@context_instance_1.send(:role1), @context_instance_1.send(:role2))
92
+ end
93
+
94
+ it("...You can also include other extra pairs as arguments...") do
95
+ expect {TestingUseContext.new(:role1 => @player1, :role2 => @player2, :extra_arg => :extra)}.not_to raise_error
96
+ end
97
+
98
+ it("A shorter way to instantiate a context is through the class method []...") do
99
+ expect {TestingUseContext[:role1 => @player1, :role2 => @player2, :extra_arg => :extra]}.not_to raise_error
100
+ end
101
+ it("...which is equivalent to create a new instance and call #run on it.") do
102
+ TestingUseContext[:role1 => @player1, :role2 => @player2].should be(:role1_method)
103
+ end
104
+
105
+
106
+ it("Once instantiated...") {@context_instance_1.should be_a(TestingUseContext)}
107
+ it("...you call an interaction (instance method) on it") do
108
+ @context_instance_1.should respond_to(:interaction2)
109
+ end
110
+ it("...to start interaction among roleplayers inside the context.") do
111
+ @context_instance_1.interaction3.should be_instance_of(Fixnum)
112
+ end
113
+
114
+ end
115
+ end
116
+
117
+ end
@@ -0,0 +1,48 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe 'Interaction:' do
4
+
5
+ context "Inside a Context instance method(interaction)..." do
6
+ before(:all) do
7
+ class TestingInteractionsContext < DCI::Context
8
+ role :role1 do
9
+ end
10
+ role :role2 do
11
+ end
12
+
13
+ def interaction1
14
+ role1
15
+ end
16
+ end
17
+ @player1, @player2 = Object.new, Object.new
18
+ @test_interactions_context = TestingInteractionsContext.new(:role1 => @player1,
19
+ :role2 => @player2,
20
+ :setting1 => :one,
21
+ :setting2 => :two,
22
+ :setting3 => :three)
23
+ end
24
+
25
+ it("...the developer has access to all the roleplayers...") do
26
+ @test_interactions_context.interaction1.should be(@player1)
27
+ @test_interactions_context.send(:role2).should be(@player2)
28
+ end
29
+ it("...via private instance methods named after their role keys.") do
30
+ @test_interactions_context.private_methods(false).map(&:to_s).should include('role1', 'role2')
31
+ end
32
+
33
+ it("He also have private access to extra args received in the instantiation of its context...") do
34
+ @test_interactions_context.private_methods.map(&:to_s).should include('settings')
35
+ @test_interactions_context.public_methods.map(&:to_s).should_not include('settings')
36
+ end
37
+ it("...calling #settings that returns a hash with all the extra args...") do
38
+ @test_interactions_context.send(:settings).should eq({:setting1 => :one, :setting2 => :two, :setting3 => :three})
39
+ end
40
+ it("...or #settings(key) that returns the value of the given extra arg...") do
41
+ @test_interactions_context.send(:settings, :setting2).should be(:two)
42
+ end
43
+ it("...or #settings(key1, key2, ...) that returns a hash with the given extra args.") do
44
+ @test_interactions_context.send(:settings, :setting1, :setting3).should eq({:setting1 => :one, :setting3 => :three})
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,83 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ # require 'ostruct'
3
+
4
+
5
+ describe 'Players:' do
6
+
7
+ class CheckingAccount
8
+ attr_reader :account_id
9
+ attr_accessor :balance
10
+
11
+ def initialize(account_id, initial_balance=0)
12
+ @account_id, @balance = account_id, initial_balance
13
+ end
14
+ end
15
+
16
+
17
+ class MoneyTransferContext < DCI::Context
18
+
19
+ # Roles Definitions
20
+ role :source_account do
21
+ def run_transfer_of(amount)
22
+ self.balance -= amount
23
+ end
24
+ end
25
+
26
+ role :target_account do
27
+ def run_transfer_of(amount)
28
+ self.balance += amount
29
+ end
30
+ end
31
+
32
+ # Interactions
33
+ def run(amount=settings(:amount))
34
+ source_account.run_transfer_of(amount)
35
+ target_account.run_transfer_of(amount)
36
+ balances
37
+ end
38
+
39
+ private
40
+ def accounts
41
+ [source_account, target_account]
42
+ end
43
+
44
+ def balances
45
+ accounts.map(&:balance)
46
+ end
47
+ end
48
+
49
+ context "Are common Ruby objects that play roles inside contexts:" do
50
+ before(:all) do
51
+ @account1 = CheckingAccount.new(1, 1000)
52
+ @account2 = CheckingAccount.new(2)
53
+ @account1_public_interface = @account1.public_methods
54
+ end
55
+
56
+ context "Before becoming roleplayers inside a context..." do
57
+ it("...they are in an initial state...") do
58
+ @account1.balance.should be(1000)
59
+ @account2.balance.should be(0)
60
+ end
61
+ it("...and have got a given public interface.") do
62
+ @account1_public_interface.should be_true
63
+ end
64
+ end
65
+
66
+ context "After playing a role inside a context..." do
67
+ before(:all) do
68
+ MoneyTransferContext.new(:source_account => @account1,
69
+ :target_account => @account2).run(200)
70
+ end
71
+ it("...they still preserve their public interface...") do
72
+ @account1.public_methods.should eql(@account1_public_interface)
73
+ @account1.should_not respond_to(:run_transfer_of)
74
+ @account1.private_methods.should_not include(:run_transfer_of)
75
+ end
76
+ it("...although their state might have been changed!") do
77
+ @account1.balance.should_not be(1000)
78
+ @account2.balance.should_not be(0)
79
+ end
80
+ end
81
+ end
82
+
83
+ end
data/spec/role_spec.rb ADDED
@@ -0,0 +1,33 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe 'Role' do
4
+
5
+ context "When defining roles inside a DCI::Context subclass..." do
6
+ before(:all) do
7
+ class TestingRoleContext < DCI::Context
8
+ role :rolename do
9
+ end
10
+ role :anotherrolename do
11
+ end
12
+ end
13
+ end
14
+ it("...you can define as many as you want.") do
15
+ TestingRoleContext.roles.keys.size.should eql(2)
16
+ end
17
+ it("Each rolename must be provided as a symbol...") do
18
+ TestingRoleContext.roles.keys.should include(:rolename, :anotherrolename)
19
+ end
20
+ it("...and not as a string.") do
21
+ expect do
22
+ class TestingRoleContext < DCI::Context
23
+ role "rolename" do
24
+ end
25
+ end
26
+ end.to raise_error
27
+ end
28
+ it("A block defining rolemethods must be provided as well.") do
29
+ TestingRoleContext.roles[:rolename].should be_a(Module)
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,107 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'ostruct'
3
+
4
+ describe 'RolePlayers' do
5
+
6
+ context "Are Ruby objects inside a context instance..." do
7
+ before(:all) do
8
+ class TestingRoleplayersContext < DCI::Context
9
+ role :role1 do
10
+ def role1method1; :role1method1_executed end
11
+ def role1self; self end
12
+ end
13
+
14
+ role :role2 do
15
+ def role2method1; role1 end
16
+ def role2self; self end
17
+
18
+ private
19
+ def private_role2method2; :private_rolemethod_return_value end
20
+ end
21
+
22
+ def check_role_interaccess; role2.role2method1 == role1 end
23
+
24
+ def check_role1_identity(obj)
25
+ [role1 == obj, role1.role1self == obj, role1.respond_to?(:role1method1), obj.respond_to?(:role1method1),
26
+ role1.role1method1 == :role1method1_executed, obj.role1method1 == :role1method1_executed].uniq == [true]
27
+ end
28
+
29
+ def check_role2_identity(obj)
30
+ [role2 == obj, role2.role2self == obj, role2.respond_to?(:role2method1), obj.respond_to?(:role2method1),
31
+ role2.send(:private_role2method2) == :private_rolemethod_return_value,
32
+ obj.send(:private_role2method2) == :private_rolemethod_return_value,
33
+ role2.role2method1 == role1, obj.role2method1 == role1].uniq == [true]
34
+ end
35
+
36
+ def access_role1_external_interface
37
+ role1.name
38
+ end
39
+
40
+ def check_role1_context_access
41
+ role1.context == self
42
+ end
43
+
44
+ def check_role1_settings_access
45
+ [!role1.respond_to?(:settings), role1.private_methods.map(&:to_s).include?('settings'),
46
+ role1.send(:settings) == settings].uniq == [true]
47
+ end
48
+ end
49
+
50
+ @player1, @player2 = OpenStruct.new(:name => 'player1'), OpenStruct.new(:name => 'player2')
51
+ @testing_roleplayers_context = TestingRoleplayersContext.new(:role1 => @player1,
52
+ :role2 => @player2,
53
+ :setting1 => :one,
54
+ :setting2 => :two,
55
+ :setting3 => :three)
56
+ end
57
+
58
+ it("...that adquire the public instance methods defined in their role...") do
59
+ @testing_roleplayers_context.check_role1_identity(@player1).should be_true
60
+ end
61
+ it("...as well as the private ones.") do
62
+ @testing_roleplayers_context.check_role2_identity(@player2).should be_true
63
+ end
64
+
65
+ it("They still preserve their identity") do
66
+ @testing_roleplayers_context.check_role1_identity(@player1).should be_true
67
+ end
68
+ it("...and therefore, their state and behaviour are accessible inside the context.") do
69
+ @testing_roleplayers_context.access_role1_external_interface.should eq('player1')
70
+ end
71
+
72
+ it("Inside the context, roleplayers have private access to other roleplayers through methods named after their keys.") do
73
+ @testing_roleplayers_context.check_role_interaccess.should be_true
74
+ end
75
+ it("...and offer a public_method to access the context.") do
76
+ @testing_roleplayers_context.check_role1_context_access.should be_true
77
+ end
78
+
79
+ it("They also have private access to extra args received in the instantiation of its context...") do
80
+ @testing_roleplayers_context.check_role1_settings_access.should be_true
81
+ end
82
+ it("...calling #settings that returns a hash with all the extra args...") do
83
+ @testing_roleplayers_context.send(:settings).should eq({:setting1 => :one, :setting2 => :two, :setting3 => :three})
84
+ end
85
+ it("...or #settings(key) that returns the value of the given extra arg...") do
86
+ @testing_roleplayers_context.send(:settings, :setting2).should be(:two)
87
+ end
88
+ it("...or #settings(key1, key2, ...) that returns a hash with the given extra args.") do
89
+ @testing_roleplayers_context.send(:settings, :setting1, :setting3).should eq({:setting1 => :one, :setting3 => :three})
90
+ end
91
+
92
+ it("But all these features, are only inside a context. Never out of it!") do
93
+ @player1.should_not respond_to(:role1method1)
94
+ @player2.private_methods.map(&:to_s).should_not include(:private_role2method2)
95
+ @player1.name.should eq('player1')
96
+ @player2.should_not respond_to(:role1)
97
+ @player2.should_not respond_to(:context)
98
+ @player1.should_not respond_to(:settings)
99
+ @player1.private_methods.map(&:to_s).should include('settings')
100
+ @player1.send(:settings).should be_nil
101
+ end
102
+
103
+
104
+
105
+ end
106
+
107
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'drsi'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: drsi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Lorenzo Tello
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-12 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.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ description: Make DCI paradigm available to Ruby applications
28
+ email:
29
+ - ltello8a@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".gitignore"
35
+ - ".rspec"
36
+ - ".ruby-gemset"
37
+ - ".ruby-version"
38
+ - Gemfile
39
+ - LICENSE
40
+ - README.md
41
+ - Rakefile
42
+ - drsi.gemspec
43
+ - examples/money_transfer.rb
44
+ - lib/drsi.rb
45
+ - lib/drsi/dci/context.rb
46
+ - lib/drsi/dci/role.rb
47
+ - lib/drsi/module.rb
48
+ - lib/drsi/object.rb
49
+ - lib/drsi/rolable.rb
50
+ - lib/drsi/version.rb
51
+ - spec/context_spec.rb
52
+ - spec/interaction_spec.rb
53
+ - spec/players_spec.rb
54
+ - spec/role_spec.rb
55
+ - spec/roleplayers_spec.rb
56
+ - spec/spec_helper.rb
57
+ homepage: http://github.com/ltello/drsi
58
+ licenses:
59
+ - MIT
60
+ metadata: {}
61
+ post_install_message:
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubyforge_project: drsi
77
+ rubygems_version: 2.2.1
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: Make DCI paradigm available to Ruby applications by enabling developers defining
81
+ contexts subclassing the class DCI::Context. You define roles inside the definition.
82
+ Match roles and player objects in context instantiation. Single Identity approach.
83
+ test_files:
84
+ - spec/context_spec.rb
85
+ - spec/interaction_spec.rb
86
+ - spec/players_spec.rb
87
+ - spec/role_spec.rb
88
+ - spec/roleplayers_spec.rb
89
+ - spec/spec_helper.rb