maroon 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Examples/Dijkstra/CalculateShortestDistance.rb +65 -0
- data/Examples/Dijkstra/calculate_shortest_path.rb +352 -0
- data/Examples/Dijkstra/data.rb +210 -0
- data/Examples/Dijkstra/dijkstra.rb +84 -0
- data/Examples/MoneyTransfer.rb +61 -0
- data/Examples/greeter.rb +45 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +41 -0
- data/Rakefile +1 -0
- data/lib/maroon.rb +363 -0
- data/lib/maroon/kernel.rb +5 -0
- data/lib/maroon/version.rb +3 -0
- data/maroon.gemspec +28 -0
- metadata +158 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require '../../Lib/maroon.rb'
|
3
|
+
require './data.rb'
|
4
|
+
require './CalculateShortestDistance.rb'
|
5
|
+
require './Calculate_Shortest_Path.rb'
|
6
|
+
#!/usr/bin/env ruby
|
7
|
+
# Example in Ruby -- Dijkstra's algorithm in DCI
|
8
|
+
# Modified and simplified for a Manhattan geometry with 8 roles
|
9
|
+
#
|
10
|
+
#
|
11
|
+
#
|
12
|
+
# Demonstrates an example where:
|
13
|
+
#
|
14
|
+
# - objects of class Node play several roles simultaneously
|
15
|
+
# (albeit spread across Contexts: a Node can
|
16
|
+
# play the CurrentIntersection in one Context and an Eastern or
|
17
|
+
# Southern Neighbor in another)
|
18
|
+
# - stacked Contexts (to implement recursion)
|
19
|
+
# - mixed access of objects of Node through different
|
20
|
+
# paths of role elaboration (the root is just a node,
|
21
|
+
# whereas others play roles)
|
22
|
+
# - there is a significant pre-existing data structure called
|
23
|
+
# a Geometry (plays the Map role) which contains the objects
|
24
|
+
# of instance. Where DCI comes in is to ascribe roles to those
|
25
|
+
# objects and let them interact with each other to evaluate the
|
26
|
+
# minimal path through the network
|
27
|
+
# - true to core DCI we are almost always concerned about
|
28
|
+
# what happens between the objects (paths and distance)
|
29
|
+
# rather than in the objects themselves (which have
|
30
|
+
# relatively uninteresting properties like "name")
|
31
|
+
# - equality of nodes is not identity, and several
|
32
|
+
# nodes compare equal with each other by standard
|
33
|
+
# equality (eql?)
|
34
|
+
# - returns references to the original data objects
|
35
|
+
# in a vector, to describe the resulting path
|
36
|
+
#
|
37
|
+
# There are some curiosities
|
38
|
+
#
|
39
|
+
# - east_neighbor and south_neighbor were typographically equivalent,
|
40
|
+
# so I folded them into a single role: Neighbor. That type still
|
41
|
+
# serves the two original roles
|
42
|
+
# - Roles are truly scoped to the use case context
|
43
|
+
# - The Map and Distance_labeled_graph_node roles have to be
|
44
|
+
# duplicated in two Contexts. blah blah blah
|
45
|
+
# - Node inheritance is replaced by injecting two roles
|
46
|
+
# into the object
|
47
|
+
# - Injecting roles no longer adds new fields to existing
|
48
|
+
# data objects.
|
49
|
+
# - There is an intentional call to distance_between while the
|
50
|
+
# Context is still extant, but outside the scope of the
|
51
|
+
# Context itself. Should that be legal?
|
52
|
+
# - I have added a tentative_distance_values array to the Context
|
53
|
+
# to support the algorithm. Its data are shared across the
|
54
|
+
# roles of the CalculateShortestPath Context
|
55
|
+
# - nearest_unvisited_node_to_target is now a feature of Map,
|
56
|
+
# which seems to reflect better coupling than in the old
|
57
|
+
# design
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
# --- Main Program: test driver
|
62
|
+
#
|
63
|
+
geometries = Geometry_1.new
|
64
|
+
path = CalculateShortestPath.new(geometries.root, geometries.destination, geometries)
|
65
|
+
print 'Path is: '
|
66
|
+
path.each { |node| print "#{node.name} " }
|
67
|
+
print "\n"
|
68
|
+
puts "distance is #{CalculateShortestDistance.new(geometries.root, geometries).distance}"
|
69
|
+
|
70
|
+
puts ''
|
71
|
+
|
72
|
+
geometries = ManhattanGeometry2.new
|
73
|
+
path = CalculateShortestPath.new(geometries.root, geometries.destination, geometries)
|
74
|
+
print 'Path is: '
|
75
|
+
last_node = nil
|
76
|
+
path.each do |node|
|
77
|
+
if last_node != nil; print " - #{geometries.distances[Edge.new(node, last_node)]} - " end
|
78
|
+
print "#{node.name}"
|
79
|
+
last_node = node
|
80
|
+
end
|
81
|
+
print "\n"
|
82
|
+
|
83
|
+
geometries = ManhattanGeometry2.new
|
84
|
+
puts "distance is #{CalculateShortestDistance.new(geometries.root, geometries).distance }"
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require '../lib/maroon.rb'
|
2
|
+
|
3
|
+
Context::define :MoneyTransfer do
|
4
|
+
role :source do
|
5
|
+
withdraw do |amount|
|
6
|
+
source.movement(amount)
|
7
|
+
source.log "withdrawal #{amount}"
|
8
|
+
end
|
9
|
+
log do |message|
|
10
|
+
p "#{@source} source #{message}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
role :destination do
|
15
|
+
deposit do |amount|
|
16
|
+
@destination.movement(amount)
|
17
|
+
@destination.log "deposit #{amount}"
|
18
|
+
end
|
19
|
+
logger do |message|
|
20
|
+
p "#{@source} destination #{message}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
role :amount do end
|
25
|
+
|
26
|
+
transfer do
|
27
|
+
source.withdraw -amount
|
28
|
+
destination.deposit amount
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class MoneyTransfer
|
33
|
+
def initialize(source, destination, amount)
|
34
|
+
@source = source
|
35
|
+
@destination = destination
|
36
|
+
@amount = amount
|
37
|
+
end
|
38
|
+
end
|
39
|
+
class Account
|
40
|
+
def initialize (amount, id)
|
41
|
+
@balance = amount
|
42
|
+
@account_id = id
|
43
|
+
end
|
44
|
+
|
45
|
+
def movement(amount)
|
46
|
+
log "Amount #{amount}"
|
47
|
+
@balance+=amount
|
48
|
+
end
|
49
|
+
|
50
|
+
def log(message)
|
51
|
+
(p s = "instance #{message}")
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
"balance of #{@account_id}: #{@balance}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
account = Account.new 1000, "source"
|
60
|
+
ctx = MoneyTransfer.new account, account, 100
|
61
|
+
ctx.transfer
|
data/Examples/greeter.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
#Thanks to Ted Milken for updating the original example
|
2
|
+
|
3
|
+
require '../lib/maroon.rb'
|
4
|
+
require '../lib/Moby/kernel.rb'
|
5
|
+
|
6
|
+
class Person
|
7
|
+
attr_accessor :name
|
8
|
+
attr_accessor :greeting
|
9
|
+
end
|
10
|
+
|
11
|
+
context :Greet_Someone, :greet do
|
12
|
+
role :greeter do
|
13
|
+
welcome do
|
14
|
+
self.greeting
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
role :greeted do
|
19
|
+
end
|
20
|
+
|
21
|
+
greet do
|
22
|
+
puts "#{greeter.name}: \"#{greeter.welcome}, #{greeted.name}!\""
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Greet_Someone
|
27
|
+
def initialize(greeter, greeted)
|
28
|
+
@greeter = greeter
|
29
|
+
@greeted = greeted
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
p1 = Person.new
|
34
|
+
p1.name = 'Bob'
|
35
|
+
p1.greeting = 'Hello'
|
36
|
+
|
37
|
+
p2 = Person.new
|
38
|
+
p2.name = 'World!'
|
39
|
+
p2.greeting = 'Greetings'
|
40
|
+
|
41
|
+
#Execute is automagically created for the default interaction (specified by the second argument in context :Greet_Someone, :greet do)
|
42
|
+
#Executes construc a context object and calls the default interaction on this object
|
43
|
+
Greet_Someone.execute p1, p2
|
44
|
+
#constructs a Greet_Someone context object and executes greet.
|
45
|
+
Greet_Someone.new(p2, p1).greet
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 TODO: Write your name
|
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,41 @@
|
|
1
|
+
# Maroon
|
2
|
+
|
3
|
+
A module to make pure DCI available in Ruby
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'maroon'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install maroon
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
See the examples for detailed information on how to use maroon.
|
22
|
+
|
23
|
+
Essentially you can define a context by using
|
24
|
+
|
25
|
+
Context::define :context_name do
|
26
|
+
role :role_name do
|
27
|
+
print_self do |x| #notice no symbol
|
28
|
+
p "#{role_name} #use role_name to refer to the role of said name
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
## Contributing
|
35
|
+
|
36
|
+
1. Fork it
|
37
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
38
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
39
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
40
|
+
5. Create new Pull Request
|
41
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/maroon.rb
ADDED
@@ -0,0 +1,363 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require 'live_ast'
|
3
|
+
require 'live_ast/to_ruby'
|
4
|
+
|
5
|
+
##
|
6
|
+
# The Context class is used to define a DCI context with roles and their role methods
|
7
|
+
# to define a context call define with the name of the context (this name will become the name of the class that defines the context)
|
8
|
+
# the name should be a symbol and since it's going to be used as a class name, use class naming conventions
|
9
|
+
# follow the name with a block. With in this block you can define roles and interactions
|
10
|
+
# and interaction is defined by write the name of the interaction (hello in the below example) followed by a block
|
11
|
+
# the block will become the method body
|
12
|
+
# a role can be defined much like a context. instead of calling the define method call the role method followed by the role name (as a symbol)
|
13
|
+
# the role will be used for a private instance variable and the naming convention should match this
|
14
|
+
# With in the block supplied to the role method you can define role methods the same way as you define interactions. See the method who
|
15
|
+
# in the below example
|
16
|
+
# = Example
|
17
|
+
# Context::define :Greeter do
|
18
|
+
# role :who do
|
19
|
+
# say do
|
20
|
+
# @who #could be self as well to refer to the current role player of the 'who' role
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
# greeting do
|
24
|
+
# p "Hello #{who.say}!"
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# class Greeter
|
29
|
+
# def initialize(player)
|
30
|
+
# #@who = player
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# Greeter.new('world').greeting #Will print "Hello world!"
|
35
|
+
#maroon is base on Marvin which was the first injectionless language for DCI
|
36
|
+
#being injectionless there's no runtime extend or anything else impacting the performance. There' only regular method invocation even when using role methods
|
37
|
+
#Author:: Rune Funch Søltoft (funchsoltoft@gmail.com)
|
38
|
+
#License:: Same as for Ruby
|
39
|
+
##
|
40
|
+
class Context
|
41
|
+
@roles
|
42
|
+
@interactions
|
43
|
+
@defining_role
|
44
|
+
@role_alias
|
45
|
+
@alias_list
|
46
|
+
@cached_roles_and_alias_list
|
47
|
+
|
48
|
+
#define is the only exposed method and can be used to define a context (class)
|
49
|
+
#if maroon/kernel is required calling context of Context::define are equivalent
|
50
|
+
#params
|
51
|
+
#name:: the name of the context. Since this is used as the name of a class, class naming convetions should be used
|
52
|
+
#block:: the body of the context. Can include definitions of roles (through the role method) or definitions of interactions
|
53
|
+
#by simply calling a method with the name of the interaction and passing a block as the body of the interaction
|
54
|
+
def self.define(*args, &block)
|
55
|
+
name,base_class,default_interaction = *args
|
56
|
+
#if there's two arguments and the second is not a class it must be an interaction
|
57
|
+
base_class,default_interaction = default_interaction, base_class if base_class and !default_interaction and base_class.instance_of? Symbol
|
58
|
+
ctx = Context.new
|
59
|
+
ctx.instance_eval &block
|
60
|
+
return ctx.send(:finalize, name,base_class,default_interaction)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
##
|
65
|
+
#Defines a role with the given name
|
66
|
+
#role methods can be defined inside a block passed to this method
|
67
|
+
# = Example
|
68
|
+
# role :who do
|
69
|
+
# say do
|
70
|
+
# p @who
|
71
|
+
# end
|
72
|
+
# end
|
73
|
+
#The above code defines a role called 'who' with a role method called say
|
74
|
+
##
|
75
|
+
def role(role_name)
|
76
|
+
raise 'Argument role_name must be a symbol' unless role_name.instance_of? Symbol
|
77
|
+
|
78
|
+
@defining_role = role_name
|
79
|
+
@roles[role_name] = Hash.new
|
80
|
+
yield if block_given?
|
81
|
+
@defining_role = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def initialize
|
85
|
+
@roles = Hash.new
|
86
|
+
@interactions = Hash.new
|
87
|
+
@role_alias = Array.new
|
88
|
+
end
|
89
|
+
|
90
|
+
def role_aliases
|
91
|
+
@alias_list if @alias_list
|
92
|
+
@alias_list = Hash.new
|
93
|
+
@role_alias.each {|aliases|
|
94
|
+
aliases.each {|k,v|
|
95
|
+
@alias_list[k] = v
|
96
|
+
}
|
97
|
+
}
|
98
|
+
@alias_list
|
99
|
+
end
|
100
|
+
|
101
|
+
def roles
|
102
|
+
@cached_roles_and_alias_list if @cached_roles_and_alias_list
|
103
|
+
@roles unless @role_alias and @role_alias.length
|
104
|
+
@cached_roles_and_alias_list = Hash.new
|
105
|
+
@roles.each {|k,v|
|
106
|
+
@cached_roles_and_alias_list[k] = v
|
107
|
+
}
|
108
|
+
role_aliases.each {|k,v|
|
109
|
+
@cached_roles_and_alias_list[k] = @roles[v]
|
110
|
+
}
|
111
|
+
@cached_roles_and_alias_list
|
112
|
+
end
|
113
|
+
|
114
|
+
def methods
|
115
|
+
(@defining_role ? @roles[@defining_role] : @interactions)
|
116
|
+
end
|
117
|
+
|
118
|
+
def add_alias (a,role_name)
|
119
|
+
@cached_roles_and_alias_list,@alias_list = nil
|
120
|
+
@role_alias.last()[a] = role_name
|
121
|
+
end
|
122
|
+
|
123
|
+
def finalize(name, base_class, default)
|
124
|
+
c = base_class ? (Class.new base_class) : Class.new
|
125
|
+
Kernel.const_set name, c
|
126
|
+
code = ''
|
127
|
+
fields = ''
|
128
|
+
getters = ''
|
129
|
+
impl = ''
|
130
|
+
interactions = ''
|
131
|
+
@interactions.each do |method_name, method_source|
|
132
|
+
@defining_role = nil
|
133
|
+
interactions << " #{lambda2method(method_name, method_source)}"
|
134
|
+
end
|
135
|
+
if default
|
136
|
+
interactions <<"\ndef self.execute(*args);#{name}.new(*args).#{default}; end\n"
|
137
|
+
end
|
138
|
+
|
139
|
+
@roles.each do |role, methods|
|
140
|
+
fields << "@#{role}\n"
|
141
|
+
getters << "def #{role};@#{role} end\n"
|
142
|
+
|
143
|
+
methods.each do |method_name, method_source|
|
144
|
+
@defining_role = role
|
145
|
+
rewritten_method_name = "self_#{role}_#{method_name}"
|
146
|
+
definition = lambda2method rewritten_method_name, method_source
|
147
|
+
impl << " #{definition}" if definition
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
code << "#{interactions}\n#{fields}\n private\n#{getters}\n#{impl}\n"
|
152
|
+
|
153
|
+
complete = "class #{name}\r\n#{code}\r\nend"
|
154
|
+
return c.class_eval(code),complete
|
155
|
+
end
|
156
|
+
|
157
|
+
def role_or_interaction_method(method_name,*args, &b)
|
158
|
+
raise "method with out block #{method_name}" unless b
|
159
|
+
|
160
|
+
args, block = block2source b.to_ruby, method_name
|
161
|
+
args = "|#{args}|" if args
|
162
|
+
source = "(proc do #{args}\n #{block}\nend)"
|
163
|
+
methods[method_name] = source
|
164
|
+
end
|
165
|
+
|
166
|
+
alias method_missing role_or_interaction_method
|
167
|
+
|
168
|
+
def role_method_call(ast, method)
|
169
|
+
is_call_expression = ast && ast[0] == :call
|
170
|
+
self_is_instance_expression = is_call_expression && (!ast[1]) #implicit self
|
171
|
+
is_in_block = ast && ast[0] == :lvar
|
172
|
+
role_name_index = self_is_instance_expression ? 2 : 1
|
173
|
+
role = (self_is_instance_expression || is_in_block) ? roles[ast[role_name_index]] : nil #is it a call to a role getter
|
174
|
+
is_role_method = role && role.has_key?(method)
|
175
|
+
role_name = is_in_block ? role_aliases[ast[1]] : (ast[2] if self_is_instance_expression)
|
176
|
+
role_name if is_role_method #return role name
|
177
|
+
end
|
178
|
+
|
179
|
+
def lambda2method (method_name, method_source)
|
180
|
+
evaluated = ast_eval method_source, binding
|
181
|
+
ast = evaluated.to_ast
|
182
|
+
transform_ast ast
|
183
|
+
args, block = block2source LiveAST.parser::Unparser.unparse(ast), method_name
|
184
|
+
args = "(#{args})" if args
|
185
|
+
"\ndef #{method_name} #{args}\n#{block} end\n"
|
186
|
+
end
|
187
|
+
|
188
|
+
##
|
189
|
+
#Test if there's a block that needs to potentially be transformed
|
190
|
+
##
|
191
|
+
def transform_block(exp)
|
192
|
+
if exp && exp[0] == :iter
|
193
|
+
(exp.length-1).times do |i|
|
194
|
+
expr = exp[i+1]
|
195
|
+
#find the block
|
196
|
+
if expr && expr.length && expr[0] == :block
|
197
|
+
transform_ast exp if rewrite_bind? expr,expr[1]
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
##
|
204
|
+
#Calls rewrite_block if needed and will return true if the AST was changed otherwise false
|
205
|
+
##
|
206
|
+
def rewrite_bind?(block, expr)
|
207
|
+
#check if the first call is a bind call
|
208
|
+
if expr && expr.length && (expr[0] == :call && expr[1] == nil && expr[2] == :bind)
|
209
|
+
arglist = expr[3]
|
210
|
+
if arglist && arglist[0] == :arglist
|
211
|
+
arguments = arglist[1]
|
212
|
+
if arguments && arguments[0] == :hash
|
213
|
+
block.delete_at 1
|
214
|
+
count = (arguments.length-1) / 2
|
215
|
+
(1..count).each do |j|
|
216
|
+
temp = j * 2
|
217
|
+
local = arguments[temp-1][1]
|
218
|
+
if local.instance_of? Sexp
|
219
|
+
local = local[1]
|
220
|
+
end
|
221
|
+
raise 'invalid value for role alias' unless local.instance_of? Symbol
|
222
|
+
#find the name of the role being bound to
|
223
|
+
aliased_role = arguments[temp][1]
|
224
|
+
if aliased_role.instance_of? Sexp
|
225
|
+
aliased_role = aliased_role[1]
|
226
|
+
end
|
227
|
+
raise "#{aliased_role} used in binding is an unknown role #{roles}" unless aliased_role.instance_of? Symbol and @roles.has_key? aliased_role
|
228
|
+
add_alias local, aliased_role
|
229
|
+
#replace bind call with assignment of iteration variable to role field
|
230
|
+
rewrite_bind(aliased_role, local, block)
|
231
|
+
return true
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
false
|
237
|
+
end
|
238
|
+
|
239
|
+
##
|
240
|
+
#removes call to bind in a block
|
241
|
+
#and replaces it with assignment to the proper role player local variables
|
242
|
+
#in the end of the block the local variables have their original values reassigned
|
243
|
+
def rewrite_bind(aliased_role, local, block)
|
244
|
+
raise 'aliased_role must be a Symbol' unless aliased_role.instance_of? Symbol
|
245
|
+
raise 'local must be a Symbol' unless local.instance_of? Symbol
|
246
|
+
assignment = Sexp.new
|
247
|
+
assignment[0] = :iasgn
|
248
|
+
assignment[1] = aliased_role
|
249
|
+
load_arg = Sexp.new
|
250
|
+
load_arg[0] = :lvar
|
251
|
+
load_arg[1] = local
|
252
|
+
assignment[2] = load_arg
|
253
|
+
block.insert 1, assignment
|
254
|
+
|
255
|
+
# assign role player to temp
|
256
|
+
temp_symbol = "temp____#{aliased_role}".to_sym
|
257
|
+
assignment = Sexp.new
|
258
|
+
assignment[0] = :lasgn
|
259
|
+
assignment[1] = temp_symbol
|
260
|
+
load_field = Sexp.new
|
261
|
+
load_field[0] = :ivar
|
262
|
+
load_field[1] = aliased_role
|
263
|
+
assignment[2] = load_field
|
264
|
+
block.insert 1, assignment
|
265
|
+
|
266
|
+
# reassign original player
|
267
|
+
assignment = Sexp.new
|
268
|
+
assignment[0] = :iasgn
|
269
|
+
assignment[1] = aliased_role
|
270
|
+
load_temp = Sexp.new
|
271
|
+
load_temp[0] = :lvar
|
272
|
+
load_temp[1] = temp_symbol
|
273
|
+
assignment[2] = load_temp
|
274
|
+
block[block.length] = assignment
|
275
|
+
end
|
276
|
+
|
277
|
+
# rewrites a call to self in a role method to a call to the role player accessor
|
278
|
+
# which is subsequently rewritten to a call to the instance variable itself
|
279
|
+
# in the case where no role method is called on the role player
|
280
|
+
# It's rewritten to an instance call on the context object if a role method is called
|
281
|
+
def rewrite_self (ast)
|
282
|
+
ast.length.times do |i|
|
283
|
+
raise 'Invalid argument. must be an expression' unless ast.instance_of? Sexp
|
284
|
+
exp = ast[i]
|
285
|
+
if exp == :self
|
286
|
+
ast[0] = :call
|
287
|
+
ast[1] = nil
|
288
|
+
ast[2] = @defining_role
|
289
|
+
arglist = Sexp.new
|
290
|
+
ast[3] = arglist
|
291
|
+
arglist[0] = :arglist
|
292
|
+
elsif exp.instance_of? Sexp
|
293
|
+
rewrite_self exp
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
#rewrites the ast so that role method calls are rewritten to a method invocation on the context object rather than the role player
|
299
|
+
#also does rewriting of binds in blocks
|
300
|
+
def transform_ast(ast)
|
301
|
+
if ast
|
302
|
+
if @defining_role
|
303
|
+
rewrite_self ast
|
304
|
+
end
|
305
|
+
ast.length.times do |k|
|
306
|
+
exp = ast[k]
|
307
|
+
if exp
|
308
|
+
method_name = exp[2]
|
309
|
+
role = role_method_call exp[1], exp[2]
|
310
|
+
if exp[0] == :iter
|
311
|
+
@role_alias.push Hash.new
|
312
|
+
transform_block exp
|
313
|
+
@role_alias.pop()
|
314
|
+
end
|
315
|
+
if exp[0] == :call && role
|
316
|
+
exp[1] = nil #remove call to attribute
|
317
|
+
exp[2] = "self_#{role}_#{method_name}".to_sym
|
318
|
+
end
|
319
|
+
if exp.instance_of? Sexp
|
320
|
+
transform_ast exp
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
#cleans up the string for further processing and separates arguments from body
|
328
|
+
def block2source(b, method_name)
|
329
|
+
args = nil
|
330
|
+
block = b.strip
|
331
|
+
block = block[method_name.length..-1].strip if block.start_with? method_name.to_s
|
332
|
+
block = cleanup_head_and_tail(block)
|
333
|
+
if block.start_with? '|'
|
334
|
+
args = block.scan(/\|([\w\d,\s]*)\|/)
|
335
|
+
if args.length && args[0]
|
336
|
+
args = args[0][0]
|
337
|
+
else
|
338
|
+
args = nil
|
339
|
+
end
|
340
|
+
block = block[(2 + (block[1..-1].index '|'))..-1].strip
|
341
|
+
end
|
342
|
+
return args, block
|
343
|
+
end
|
344
|
+
|
345
|
+
# removes proc do/{ at start and } or end at the end of the string
|
346
|
+
def cleanup_head_and_tail(block)
|
347
|
+
if /^proc\s/.match(block)
|
348
|
+
block = block['proc'.length..-1].strip
|
349
|
+
end
|
350
|
+
if /^do\s/.match(block)
|
351
|
+
block = block[2..-1].strip
|
352
|
+
elsif block.start_with? '{'
|
353
|
+
block = block[1..-1].strip
|
354
|
+
end
|
355
|
+
|
356
|
+
if /end$/.match(block)
|
357
|
+
block = block[0..-4]
|
358
|
+
elsif /\}$/.match(block)
|
359
|
+
block = block[0..-2]
|
360
|
+
end
|
361
|
+
block
|
362
|
+
end
|
363
|
+
end
|