brassbound-dci 0.5.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/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in brassbound-dci.gemspec
4
+ gemspec
data/README.rdoc ADDED
@@ -0,0 +1,78 @@
1
+ = Brassbound
2
+
3
+ Brassbound is a simple but strict implementation of the Data, Context, and Interaction (DCI) paradigm for Ruby.
4
+
5
+ = Example
6
+
7
+ The canonical DCI example is the transfer of funds between a money source
8
+ and money sink. In this example, source and sink are both roles
9
+ that are attached to data objects, and the transfer of funds is orchestrated
10
+ by the context. Here's how the example looks using Brassbound.
11
+
12
+ require 'brassbound'
13
+
14
+ # Domain/data objects are plain old Ruby objects.
15
+ class Account
16
+ # Pretend to lookup accounts in a database.
17
+ def self.find(account_id)
18
+ case account_id
19
+ when 1
20
+ Account.new(account_id, 2000)
21
+ when 2
22
+ Account.new(account_id, 1000)
23
+ end
24
+ end
25
+
26
+ attr_reader :id
27
+ attr_accessor :balance
28
+
29
+ def initialize(id, balance)
30
+ @id = id
31
+ @balance = balance
32
+ end
33
+ end
34
+
35
+ # Roles are plain old Ruby modules, and automatically have access to
36
+ # the invoking context.
37
+ module MoneySource
38
+ def transfer_out(amount)
39
+ puts("Transferring #{amount} from account #{self.id} to account #{context.money_sink.id}")
40
+ self.balance -= amount
41
+ puts("Source account new balance: #{self.balance}")
42
+ context.money_sink.transfer_in(amount)
43
+ end
44
+ end
45
+
46
+ module MoneySink
47
+ def transfer_in(amount)
48
+ self.balance += amount
49
+ puts("Destination account new balance: #{self.balance}")
50
+ end
51
+ end
52
+
53
+ # Contexts are Ruby classes that include the Brassbound::Context module.
54
+ # The common idiom is for the initialize method to create all of the
55
+ # necessary objects and declare how they are bound to roles.
56
+ # Then, within the scope of the execute method, the objects will have been
57
+ # bound to the declared roles, and can be accessed by the role name
58
+ # (convert to lower case with underscores by default).
59
+ class TransferFunds
60
+ include Brassbound::Context
61
+
62
+ def initialize(source_account_id, dest_account_id, amount)
63
+ @amount = amount
64
+
65
+ role MoneySource, Account.find(source_account_id)
66
+ role MoneySink, Account.find(dest_account_id)
67
+ end
68
+
69
+ def execute
70
+ # Here, money_source refers to the object bound to the MoneySource role
71
+ # in the initialize method.
72
+ money_source.transfer_out(@amount)
73
+ end
74
+ end
75
+
76
+ # Now let's create and execute our context.
77
+ TransferFunds.new(1, 2, 100).call
78
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ $:.push File.expand_path("../lib", __FILE__)
4
+ require 'brassbound/version'
5
+ require 'rake'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "brassbound-dci"
9
+ s.version = Brassbound::VERSION
10
+ s.authors = ["Jason Voegele"]
11
+ s.email = ["jason@jvoegele.com"]
12
+ s.homepage = ""
13
+ s.summary = "Simple but strict DCI framework"
14
+ s.description = "Brassbound is a simple but strict implementation of the Data, Context, and Interaction (DCI) paradigm for Ruby."
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency 'rspec', '~> 2.10.0'
22
+ end
@@ -0,0 +1,67 @@
1
+ require 'brassbound'
2
+
3
+ # Domain/data objects are plain old Ruby objects.
4
+ class Account
5
+ # Pretend to lookup accounts in a database.
6
+ def self.find(account_id)
7
+ case account_id
8
+ when 1
9
+ Account.new(account_id, 2000)
10
+ when 2
11
+ Account.new(account_id, 1000)
12
+ end
13
+ end
14
+
15
+ attr_reader :id
16
+ attr_accessor :balance
17
+
18
+ def initialize(id, balance)
19
+ @id = id
20
+ @balance = balance
21
+ end
22
+ end
23
+
24
+ # Roles are plain old Ruby modules, and automatically have access to
25
+ # the invoking context.
26
+ module MoneySource
27
+ def transfer_out(amount)
28
+ puts("Transferring #{amount} from account #{self.id} to account #{context.money_sink.id}")
29
+ self.balance -= amount
30
+ puts("Source account new balance: #{self.balance}")
31
+ context.money_sink.transfer_in(amount)
32
+ end
33
+ end
34
+
35
+ module MoneySink
36
+ def transfer_in(amount)
37
+ self.balance += amount
38
+ puts("Destination account new balance: #{self.balance}")
39
+ end
40
+ end
41
+
42
+ # Contexts are Ruby classes that include the Brassbound::Context module.
43
+ # The common idiom is for the initialize method to create all of the
44
+ # necessary objects and declare how they are bound to roles.
45
+ # Then, within the scope of the execute method, the objects will have been
46
+ # bound to the declared roles, and can be accessed by the role name
47
+ # (convert to lower case with underscores by default).
48
+ class TransferFunds
49
+ include Brassbound::Context
50
+
51
+ def initialize(source_account_id, dest_account_id, amount)
52
+ @amount = amount
53
+
54
+ role MoneySource, Account.find(source_account_id)
55
+ role MoneySink, Account.find(dest_account_id)
56
+ end
57
+
58
+ def execute
59
+ # Here, money_source refers to the object bound to the MoneySource role
60
+ # in the initialize method.
61
+ money_source.transfer_out(@amount)
62
+ end
63
+ end
64
+
65
+ # Now let's create and execute our context.
66
+ TransferFunds.new(1, 2, 100).call
67
+
@@ -0,0 +1,52 @@
1
+ require 'brassbound'
2
+
3
+ class Person
4
+ attr_accessor :first_name, :last_name
5
+
6
+ def initialize(first_name, last_name)
7
+ @first_name = first_name
8
+ @last_name = last_name
9
+ end
10
+ end
11
+
12
+ module WifeToBe
13
+ def marry(husband)
14
+ self.last_name = husband.last_name
15
+ end
16
+ end
17
+
18
+ module HusbandToBe
19
+ def marry(wife)
20
+ end
21
+ end
22
+
23
+ module Minister
24
+ def administer_marraige
25
+ context.husband.marry(context.wife)
26
+ context.wife.marry(context.husband)
27
+ puts("I, #{first_name} #{last_name}, now pronounce you Mr. and Mrs. #{context.wife.last_name}")
28
+ end
29
+ end
30
+
31
+ # In a traditional wedding, a man and a woman are married by a minister.
32
+ class TraditionalWedding
33
+ include Brassbound::Context
34
+
35
+ def initialize(man_first_name, man_last_name,
36
+ woman_first_name, woman_last_name)
37
+ @man = Person.new(man_first_name, man_last_name)
38
+ @woman = Person.new(woman_first_name, woman_last_name)
39
+ @mark = Person.new('Mark', 'Schlafman')
40
+
41
+ role :husband, HusbandToBe, @man
42
+ role :wife, WifeToBe, @woman
43
+ role Minister, @mark
44
+ end
45
+
46
+ def execute
47
+ minister.administer_marraige
48
+ end
49
+ end
50
+
51
+ TraditionalWedding.new('Jason', 'Voegele', 'Jennifer', 'Bollinger').call
52
+
@@ -0,0 +1,163 @@
1
+ # This module represents the notion of the Context in the DCI paradigm.
2
+ #
3
+ # According to http://en.wikipedia.org/wiki/Data,_context_and_interaction
4
+ #
5
+ # The Context is the class (or its instance) whose code includes the roles
6
+ # for a given algorithm, scenario, or use case, as well as the code to map
7
+ # these roles into objects at run time and to enact the use case. Each role
8
+ # is bound to exactly one object during any given use case enactment; however,
9
+ # a single object may simultaneously play several roles. A context is
10
+ # instantiated at the beginning of the enactment of an algorithm, scenario,
11
+ # or use case. In summary, a Context comprises use cases and algorithms in
12
+ # which data objects are used through specific Roles.
13
+ #
14
+ # In Brassbound, context implementations are classes that include this module.
15
+ # Context implementations can use the #role method to bind roles to objects,
16
+ # and must provide an #execute method that enacts the use case behavior.
17
+ #
18
+ # Brassbound enforces the fact that each role is bound to exactly one object,
19
+ # since each role must have a unique name. However, any given object may
20
+ # simultaneously play several roles.
21
+ #
22
+ # Note that no special support is required for the role module or data object
23
+ # implementations; they are normal Ruby modules and objects, respectively.
24
+ # The context manages the necessary plumbing to bind roles to objects and to
25
+ # provide access to the context for the roles. In other words, role modules do
26
+ # not have to include any other modules or adhere to any particular conventions,
27
+ # and neither do the objects to which the roles are bound need to inherit from
28
+ # any other class or include any other modules.
29
+ module Brassbound::Context
30
+
31
+ require 'brassbound/util'
32
+
33
+ def self.current
34
+ Thread.current[:brassbound_context]
35
+ end
36
+
37
+ RoleMapping = Struct.new(:role_module, :data_object)
38
+
39
+ # A Hash containing all of the declared role mappings for this context.
40
+ # The keys of the Hash are the declared role names, while the values
41
+ # are the associated RoleMapping objects.
42
+ #
43
+ # Role mappings are declared using the #role method, which see for
44
+ # further details.
45
+ def roles
46
+ @declared_roles ||= Hash.new
47
+ end
48
+
49
+ # Declare a role mapping.
50
+ #
51
+ # This method accepts either two or three arguments. In the three argument
52
+ # form, the arguments are as follows:
53
+ #
54
+ # role_name:: The name of the role in this context.
55
+ # role_module:: The module that serves as the "methodful role".
56
+ # obj:: The data object to which the role_module will be attached.
57
+ #
58
+ # In the two argument form, the role_name is omitted and the role_name
59
+ # is derived from the name of the role_module by converting from
60
+ # CamelCaseName to underscore_name. For instance, if the role_name
61
+ # is not specified, then a role_module called MoneySource would be
62
+ # named money_source.
63
+ #
64
+ # This method adds the role mapping to the Hash returned by the #roles
65
+ # method. If the context is already executing when this method is
66
+ # invoked, it will call #apply_role to reify the role mapping
67
+ # immediately. Otherwise, all role mappings are applied when the
68
+ # context begins execution.
69
+ def role(*args)
70
+ case args.size
71
+ when 2
72
+ role_module = args[0]
73
+ obj = args[1]
74
+ role_name = Util.underscore(role_module.name).to_sym
75
+ when 3
76
+ role_name = args[0]
77
+ role_module = args[1]
78
+ obj = args[2]
79
+ else
80
+ raise ArgumentError
81
+ end
82
+
83
+ self.roles[role_name] = RoleMapping.new(role_module, obj)
84
+
85
+ if ::Brassbound::Context.current.equal?(self)
86
+ # If we are already in the execute method, apply the role mapping.
87
+ apply_role(role_name)
88
+ end
89
+ end
90
+
91
+ # Apply the role identified by role_name to the associated object.
92
+ # This has several effects. First, the object is extended with the
93
+ # role module (i.e. the "methodful role" in DCI terminology), thus
94
+ # adding all of the role module's methods to the object. Second,
95
+ # the object has a :context method added to it, which provides
96
+ # the role implementation access to this context. Finally, a new
97
+ # method is added to this context object, which is named after the
98
+ # role_name and which returns the object which is mapped to that role.
99
+ def apply_role(role_name)
100
+ role_mapping = self.roles[role_name]
101
+ role_module, obj = role_mapping.role_module, role_mapping.data_object
102
+ obj.extend(role_module)
103
+ obj.instance_variable_set(:@__brassbound_context, self)
104
+ class << obj
105
+ def context
106
+ @__brassbound_context
107
+ end
108
+ end
109
+ self.singleton_class.send(:define_method, role_name) do
110
+ obj
111
+ end
112
+ end
113
+
114
+ # Undo (most of) the effects of #apply_role. This method will remove all of
115
+ # the methods that were added to the mapped data object by #apply role, but
116
+ # the fact that the object has been extended with the role module cannot be
117
+ # undone. Therefore, <tt>obj.kind_of?(role_module)</tt> will still be true,
118
+ # even though the methods defined in role_module will have been removed from
119
+ # the object.
120
+ def unapply_role(role_name)
121
+ role_mapping = self.roles[role_name]
122
+ role_module, obj = role_mapping.role_module, role_mapping.data_object
123
+ obj.instance_variable_set(:@__brassbound_context, nil)
124
+ class << obj
125
+ remove_method(:context)
126
+ end
127
+ role_module.instance_methods.each do |m|
128
+ obj.singleton_class.send(:undef_method, m)
129
+ end
130
+ self.singleton_class.send(:undef_method, role_name)
131
+ end
132
+
133
+ def undef_role(role_module, obj)
134
+ obj.instance_variable_set(:@__brassbound_context, nil)
135
+ class << obj
136
+ remove_method(:context)
137
+ end
138
+ role_module.instance_methods.each do |m|
139
+ obj.singleton_class.send(:undef_method, m)
140
+ end
141
+ end
142
+
143
+ # Begin execution of this context. Note that context implementations must
144
+ # provide an #execute method, and should not override this #call method.
145
+ # This method will first set Context.current to self, apply all of the
146
+ # role mappings that have been declared with the #role method, and then
147
+ # call the #execute method.
148
+ #
149
+ # After the #execute method has returned, it will unapply all role mappings
150
+ # and set the current context back to its previous value.
151
+ def call(*args)
152
+ old_context = ::Brassbound::Context.current
153
+ Thread.current[:brassbound_context] = self
154
+
155
+ roles.each_key(&method(:apply_role))
156
+ begin
157
+ self.execute(*args)
158
+ ensure
159
+ roles.each_key(&method(:unapply_role))
160
+ Thread.current[:brassbound_context] = old_context
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,9 @@
1
+ module Util
2
+ def self.underscore(str)
3
+ str.gsub(/::/, '/').
4
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
5
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
6
+ tr("-", "_").
7
+ downcase
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Brassbound
2
+ VERSION = "0.5.0"
3
+ end
data/lib/brassbound.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'brassbound/version'
2
+
3
+ module Brassbound
4
+ end
5
+
6
+ require 'brassbound/context'
7
+
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+ include Brassbound
3
+
4
+ class Account
5
+ def self.find(account_id)
6
+ case account_id
7
+ when 1
8
+ Account.new(200)
9
+ when 2
10
+ Account.new(100)
11
+ end
12
+ end
13
+
14
+ attr_accessor :balance
15
+
16
+ def initialize(balance)
17
+ @balance = balance
18
+ end
19
+ end
20
+
21
+ module MoneySource
22
+ def transfer_out(amount)
23
+ self.balance -= amount
24
+ context.dest_account.transfer_in(amount)
25
+ end
26
+ end
27
+
28
+ module MoneySink
29
+ def transfer_in(amount)
30
+ self.balance += amount
31
+ end
32
+ end
33
+
34
+ class TransferFunds
35
+ include Context
36
+
37
+ def initialize(source_account_id, dest_account_id, amount)
38
+ @source_account_id = source_account_id
39
+ @dest_account_id = dest_account_id
40
+ @amount = amount
41
+
42
+ role :source_account, MoneySource, Account.find(@source_account_id)
43
+ role :dest_account, MoneySink, Account.find(@dest_account_id)
44
+ end
45
+
46
+ def execute
47
+ source_account.transfer_out(@amount)
48
+ # Allow spec to access the executing context for testing
49
+ yield self if block_given?
50
+ end
51
+ end
52
+
53
+ describe Context do
54
+ include Context
55
+
56
+ let(:source_account_id) { 1 }
57
+ let(:dest_account_id) { 2 }
58
+ let(:source_account) { Account.find(source_account_id) }
59
+ let(:dest_account) { Account.find(dest_account_id) }
60
+
61
+ let(:transfer_funds_context) {
62
+ TransferFunds.new(source_account_id, dest_account_id, 50)
63
+ }
64
+
65
+ context "#role" do
66
+ it "declares a role mapping" do
67
+ role MoneySource, source_account
68
+ mapping = roles[:money_source]
69
+ mapping.role_module.should == MoneySource
70
+ mapping.data_object.should == source_account
71
+
72
+ role :money_sink, MoneySink, dest_account
73
+ mapping = roles[:money_sink]
74
+ mapping.role_module.should == MoneySink
75
+ mapping.data_object.should == dest_account
76
+ end
77
+ end
78
+
79
+ context "#call" do
80
+ it "binds all roles to their mapped objects" do
81
+ transfer_funds_context.call do |ctx|
82
+ ctx.source_account.should == source_account
83
+ ctx.source_account.should be_kind_of(MoneySource)
84
+ ctx.dest_account.should == dest_account
85
+ ctx.dest_account.should be_kind_of(MoneySink)
86
+ end
87
+ end
88
+ end
89
+ end
90
+
@@ -0,0 +1,2 @@
1
+ require 'brassbound'
2
+
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: brassbound-dci
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jason Voegele
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70190658979080 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.10.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70190658979080
25
+ description: Brassbound is a simple but strict implementation of the Data, Context,
26
+ and Interaction (DCI) paradigm for Ruby.
27
+ email:
28
+ - jason@jvoegele.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - .gitignore
34
+ - Gemfile
35
+ - README.rdoc
36
+ - Rakefile
37
+ - brassbound-dci.gemspec
38
+ - examples/money_transfer.rb
39
+ - examples/wedding.rb
40
+ - lib/brassbound.rb
41
+ - lib/brassbound/context.rb
42
+ - lib/brassbound/util.rb
43
+ - lib/brassbound/version.rb
44
+ - spec/context_spec.rb
45
+ - spec/spec_helper.rb
46
+ homepage: ''
47
+ licenses: []
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 1.8.15
67
+ signing_key:
68
+ specification_version: 3
69
+ summary: Simple but strict DCI framework
70
+ test_files:
71
+ - spec/context_spec.rb
72
+ - spec/spec_helper.rb