rgen 0.4.2 → 0.4.3
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +7 -0
- data/lib/rgen/metamodel_builder.rb +5 -4
- data/lib/rgen/metamodel_builder.rb.bak +196 -0
- data/lib/rgen/metamodel_builder/builder_extensions.rb +51 -38
- data/lib/rgen/metamodel_builder/builder_extensions.rb.bak +437 -0
- data/lib/rgen/metamodel_builder/builder_runtime.rb +2 -20
- data/lib/rgen/metamodel_builder/builder_runtime.rb.bak +73 -0
- data/lib/rgen/name_helper.rb.bak +37 -0
- data/lib/rgen/template_language.rb +8 -0
- data/lib/rgen/template_language.rb.bak +289 -0
- data/lib/rgen/template_language/directory_template_container.rb +11 -0
- data/lib/rgen/template_language/directory_template_container.rb.bak +69 -0
- data/lib/rgen/template_language/output_handler.rb +3 -2
- data/lib/rgen/template_language/output_handler.rb.bak +88 -0
- data/lib/rgen/template_language/template_container.rb +5 -4
- data/lib/rgen/template_language/template_container.rb.bak +196 -0
- data/lib/rgen/transformer.rb.bak +381 -0
- data/test/environment_test.rb.bak +52 -0
- data/test/metamodel_builder_test.rb +6 -0
- data/test/metamodel_builder_test.rb.bak +443 -0
- data/test/metamodel_roundtrip_test/TestModel_Regenerated.rb +34 -32
- data/test/metamodel_roundtrip_test/houseMetamodel_Regenerated.ecore +58 -58
- data/test/output_handler_test.rb +8 -0
- data/test/output_handler_test.rb.bak +50 -0
- data/test/template_language_test.rb +23 -0
- data/test/template_language_test.rb.bak +72 -0
- data/test/template_language_test/indentStringTestDefaultIndent.out +1 -0
- data/test/template_language_test/indentStringTestTabIndent.out +1 -0
- data/test/template_language_test/templates/indent_string_test.tpl +12 -0
- data/test/template_language_test/templates/null_context_test.tpl +12 -0
- data/test/transformer_test.rb.bak +223 -0
- metadata +65 -48
- data/lib/rgen/environment.rb.bak +0 -42
@@ -51,6 +51,17 @@ class DirectoryTemplateContainer
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
+
# Set indentation string.
|
55
|
+
# Every generated line will be prependend with n times this string at an indentation level of n.
|
56
|
+
# Defaults to " " (3 spaces)
|
57
|
+
def indentString=(str)
|
58
|
+
@indentString = str
|
59
|
+
end
|
60
|
+
|
61
|
+
def indentString
|
62
|
+
@indentString || (@parent && @parent.indentString) || " "
|
63
|
+
end
|
64
|
+
|
54
65
|
private
|
55
66
|
|
56
67
|
def _expand(template, *all_args)
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# RGen Framework
|
2
|
+
# (c) Martin Thiede, 2006
|
3
|
+
|
4
|
+
require 'rgen/template_language/template_container'
|
5
|
+
require 'rgen/template_language/template_helper'
|
6
|
+
|
7
|
+
module RGen
|
8
|
+
|
9
|
+
module TemplateLanguage
|
10
|
+
|
11
|
+
class DirectoryTemplateContainer
|
12
|
+
include TemplateHelper
|
13
|
+
|
14
|
+
def initialize(metamodel=nil, output_path=nil, parent=nil)
|
15
|
+
@containers = {}
|
16
|
+
@parent = parent
|
17
|
+
@metamodel = metamodel
|
18
|
+
@output_path = output_path
|
19
|
+
end
|
20
|
+
|
21
|
+
def load(dir)
|
22
|
+
#print "Loading templates in #{dir} ...\n"
|
23
|
+
Dir.foreach(dir) { |f|
|
24
|
+
qf = dir+"/"+f
|
25
|
+
if !File.directory?(qf) && f =~ /^(.*)\.tpl$/
|
26
|
+
(@containers[$1] = TemplateContainer.dup.new(@metamodel, @output_path, self,qf)).load
|
27
|
+
elsif File.directory?(qf) && f != "." && f != ".."
|
28
|
+
(@containers[f] = DirectoryTemplateContainer.new(@metamodel, @output_path, self)).load(qf)
|
29
|
+
end
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def expand(template, *all_args)
|
34
|
+
if template =~ /^\//
|
35
|
+
if @parent
|
36
|
+
# pass to parent
|
37
|
+
@parent.expand(template, *all_args)
|
38
|
+
else
|
39
|
+
# this is root
|
40
|
+
_expand(template, *all_args)
|
41
|
+
end
|
42
|
+
elsif template =~ /^\.\.\/(.*)/
|
43
|
+
if @parent
|
44
|
+
# pass to parent
|
45
|
+
@parent.expand($1, *all_args)
|
46
|
+
else
|
47
|
+
raise "No parent directory for root"
|
48
|
+
end
|
49
|
+
else
|
50
|
+
_expand(template, *all_args)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def _expand(template, *all_args)
|
57
|
+
if template =~ /^\/?([^:\/]+)(?:::|\/)([^:\/].*)/
|
58
|
+
raise "Template not found: #{$1}" unless @containers[$1]
|
59
|
+
@containers[$1].expand($2, *all_args)
|
60
|
+
else
|
61
|
+
raise "Invalid template name: #{template}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -8,9 +8,10 @@ module TemplateLanguage
|
|
8
8
|
class OutputHandler
|
9
9
|
attr_writer :indent
|
10
10
|
|
11
|
-
def initialize(indent=0, mode=:explicit)
|
11
|
+
def initialize(indent=0, indentString=" ", mode=:explicit)
|
12
12
|
self.mode = mode
|
13
13
|
@indent = indent
|
14
|
+
@indentString = indentString
|
14
15
|
@state = :wait_for_nonws
|
15
16
|
@output = ""
|
16
17
|
end
|
@@ -42,7 +43,7 @@ module TemplateLanguage
|
|
42
43
|
if s =~ /\A\s*(\S+.*)/m
|
43
44
|
s = $1 || ""
|
44
45
|
if !@noIndentNextLine && !(@output.to_s.size > 0 && @output.to_s[-1] != "\n"[0])
|
45
|
-
@output.concat(
|
46
|
+
@output.concat(@indentString * @indent)
|
46
47
|
else
|
47
48
|
@noIndentNextLine = false
|
48
49
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# RGen Framework
|
2
|
+
# (c) Martin Thiede, 2006
|
3
|
+
|
4
|
+
module RGen
|
5
|
+
|
6
|
+
module TemplateLanguage
|
7
|
+
|
8
|
+
class OutputHandler
|
9
|
+
attr_writer :indent
|
10
|
+
|
11
|
+
def initialize(indent=0, mode=:explicit)
|
12
|
+
self.mode = mode
|
13
|
+
@indent = indent
|
14
|
+
@state = :wait_for_nonws
|
15
|
+
@output = ""
|
16
|
+
end
|
17
|
+
|
18
|
+
# ERB will call this method for every string s which is part of the
|
19
|
+
# template file in between %> and <%. If s contains a newline, it will
|
20
|
+
# call this method for every part of s which is terminated by a \n
|
21
|
+
#
|
22
|
+
def concat(s)
|
23
|
+
return @output.concat(s) if s.is_a? OutputHandler
|
24
|
+
s = s.to_str.gsub(/^[\t ]*\r?\n/,'') if @ignoreNextNL
|
25
|
+
s = s.to_str.gsub(/^\s+/,'') if @ignoreNextWS
|
26
|
+
@ignoreNextNL = @ignoreNextWS = false if s =~ /\S/
|
27
|
+
if @mode == :direct
|
28
|
+
@output.concat(s)
|
29
|
+
elsif @mode == :explicit
|
30
|
+
while s.size > 0
|
31
|
+
if @state == :wait_for_nl
|
32
|
+
if s =~ /\A([^\r\n]*\r?\n)(.*)/m
|
33
|
+
rest = $2
|
34
|
+
@output.concat($1.gsub(/[\t ]+(?=\r|\n)/,''))
|
35
|
+
s = rest || ""
|
36
|
+
@state = :wait_for_nonws
|
37
|
+
else
|
38
|
+
@output.concat(s)
|
39
|
+
s = ""
|
40
|
+
end
|
41
|
+
elsif @state == :wait_for_nonws
|
42
|
+
if s =~ /\A\s*(\S+.*)/m
|
43
|
+
s = $1 || ""
|
44
|
+
if !@noIndentNextLine && !(@output.to_s.size > 0 && @output.to_s[-1] != "\n"[0])
|
45
|
+
@output.concat(" "*@indent)
|
46
|
+
else
|
47
|
+
@noIndentNextLine = false
|
48
|
+
end
|
49
|
+
@state = :wait_for_nl
|
50
|
+
else
|
51
|
+
s = ""
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
alias << concat
|
58
|
+
|
59
|
+
def to_str
|
60
|
+
@output
|
61
|
+
end
|
62
|
+
alias to_s to_str
|
63
|
+
|
64
|
+
def direct_concat(s)
|
65
|
+
@output.concat(s)
|
66
|
+
end
|
67
|
+
|
68
|
+
def ignoreNextNL
|
69
|
+
@ignoreNextNL = true
|
70
|
+
end
|
71
|
+
|
72
|
+
def ignoreNextWS
|
73
|
+
@ignoreNextWS = true
|
74
|
+
end
|
75
|
+
|
76
|
+
def noIndentNextLine
|
77
|
+
@noIndentNextLine = true
|
78
|
+
end
|
79
|
+
|
80
|
+
def mode=(m)
|
81
|
+
raise StandardError.new("Unknown mode: #{m}") unless [:direct, :explicit].include?(m)
|
82
|
+
@mode = m
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -96,8 +96,8 @@ module RGen
|
|
96
96
|
@templates[template][cls] = block
|
97
97
|
end
|
98
98
|
|
99
|
-
def file(name)
|
100
|
-
old_output, @output = @output, OutputHandler.new(@indent)
|
99
|
+
def file(name, indentString=nil)
|
100
|
+
old_output, @output = @output, OutputHandler.new(@indent, indentString || @parent.indentString)
|
101
101
|
begin
|
102
102
|
yield
|
103
103
|
rescue Exception => e
|
@@ -126,6 +126,7 @@ module RGen
|
|
126
126
|
LOCAL_TEMPLATE_REGEX = /^:*(\w+)$/
|
127
127
|
|
128
128
|
def _expand(template, args, params)
|
129
|
+
raise StandardError.new("expand :for argument evaluates to nil") if params.has_key?(:for) && params[:for].nil?
|
129
130
|
context = params[:for]
|
130
131
|
@indent = params[:indent] || @indent
|
131
132
|
# if this is the first call to expand within this container, @output is nil
|
@@ -135,8 +136,8 @@ module RGen
|
|
135
136
|
local_output = nil
|
136
137
|
if template =~ LOCAL_TEMPLATE_REGEX
|
137
138
|
tplname = $1
|
138
|
-
|
139
|
-
old_output, @output = @output, OutputHandler.new(@indent)
|
139
|
+
raise StandardError.new("Template not found: #{$1}") unless @templates[tplname]
|
140
|
+
old_output, @output = @output, OutputHandler.new(@indent, @parent.indentString)
|
140
141
|
@output.noIndentNextLine if noIndentNextLine
|
141
142
|
_call_template(tplname, @context, args)
|
142
143
|
local_output, @output = @output, old_output
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# RGen Framework
|
2
|
+
# (c) Martin Thiede, 2006
|
3
|
+
|
4
|
+
require 'erb'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'rgen/template_language/output_handler'
|
7
|
+
require 'rgen/template_language/template_helper'
|
8
|
+
|
9
|
+
module RGen
|
10
|
+
|
11
|
+
module TemplateLanguage
|
12
|
+
|
13
|
+
class TemplateContainer
|
14
|
+
include TemplateHelper
|
15
|
+
|
16
|
+
def initialize(metamodels, output_path, parent, filename)
|
17
|
+
@templates = {}
|
18
|
+
@parent = parent
|
19
|
+
@filename = filename
|
20
|
+
@indent = 0
|
21
|
+
@output_path = output_path
|
22
|
+
raise StandardError.new("Can not set metamodel, dup class first") if self.class == TemplateContainer
|
23
|
+
@@metamodels = metamodels
|
24
|
+
@@metamodels = [ @@metamodels ] unless @@metamodels.is_a?(Array)
|
25
|
+
end
|
26
|
+
|
27
|
+
def load
|
28
|
+
#print "Loading templates in #{@filename} ...\n"
|
29
|
+
File.open(@filename) { |f|
|
30
|
+
begin
|
31
|
+
ERB.new(f.read,nil,nil,'@output').result(binding)
|
32
|
+
rescue Exception => e
|
33
|
+
processAndRaise(e)
|
34
|
+
end
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
# if this container can handle the call, the expansion result is returned
|
39
|
+
# otherwise expand is called on the appropriate container and the result is added to @output
|
40
|
+
def expand(template, *all_args)
|
41
|
+
args, params = _splitArgsAndOptions(all_args)
|
42
|
+
if params[:foreach].is_a? Enumerable
|
43
|
+
_expand_foreach(template, args, params)
|
44
|
+
else
|
45
|
+
_expand(template, args, params)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def this
|
50
|
+
@context
|
51
|
+
end
|
52
|
+
|
53
|
+
def method_missing(name, *args)
|
54
|
+
@context.send(name, *args)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.const_missing(name)
|
58
|
+
super unless @@metamodels
|
59
|
+
@@metamodels.each do |mm|
|
60
|
+
return mm.const_get(name) rescue NameError
|
61
|
+
end
|
62
|
+
super
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def nonl
|
68
|
+
@output.ignoreNextNL
|
69
|
+
end
|
70
|
+
|
71
|
+
def nows
|
72
|
+
@output.ignoreNextWS
|
73
|
+
end
|
74
|
+
|
75
|
+
def nl
|
76
|
+
_direct_concat("\n")
|
77
|
+
end
|
78
|
+
|
79
|
+
def ws
|
80
|
+
_direct_concat(" ")
|
81
|
+
end
|
82
|
+
|
83
|
+
def iinc
|
84
|
+
@indent += 1
|
85
|
+
@output.indent = @indent
|
86
|
+
end
|
87
|
+
|
88
|
+
def idec
|
89
|
+
@indent -= 1 if @indent > 0
|
90
|
+
@output.indent = @indent
|
91
|
+
end
|
92
|
+
|
93
|
+
def define(template, params={}, &block)
|
94
|
+
@templates[template] ||= {}
|
95
|
+
cls = params[:for] || Object
|
96
|
+
@templates[template][cls] = block
|
97
|
+
end
|
98
|
+
|
99
|
+
def file(name)
|
100
|
+
old_output, @output = @output, OutputHandler.new(@indent)
|
101
|
+
begin
|
102
|
+
yield
|
103
|
+
rescue Exception => e
|
104
|
+
processAndRaise(e)
|
105
|
+
end
|
106
|
+
path = ""
|
107
|
+
path += @output_path+"/" if @output_path
|
108
|
+
dirname = File.dirname(path+name)
|
109
|
+
FileUtils.makedirs(dirname) unless File.exist?(dirname)
|
110
|
+
File.open(path+name,"w") { |f| f.write(@output) }
|
111
|
+
@output = old_output
|
112
|
+
end
|
113
|
+
|
114
|
+
# private private
|
115
|
+
|
116
|
+
def _expand_foreach(template, args, params)
|
117
|
+
sep = params[:separator]
|
118
|
+
params[:foreach].each_with_index {|e,i|
|
119
|
+
single_params = params.dup
|
120
|
+
single_params[:for] = e
|
121
|
+
_direct_concat(sep.to_s) if sep && i > 0
|
122
|
+
_expand(template, args, single_params)
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
LOCAL_TEMPLATE_REGEX = /^:*(\w+)$/
|
127
|
+
|
128
|
+
def _expand(template, args, params)
|
129
|
+
context = params[:for]
|
130
|
+
@indent = params[:indent] || @indent
|
131
|
+
# if this is the first call to expand within this container, @output is nil
|
132
|
+
noIndentNextLine = params[:noIndentNextLine]
|
133
|
+
noIndentNextLine = (@output.to_s.size > 0 && @output.to_s[-1] != "\n"[0]) if noIndentNextLine.nil?
|
134
|
+
old_context, @context = @context, context if context
|
135
|
+
local_output = nil
|
136
|
+
if template =~ LOCAL_TEMPLATE_REGEX
|
137
|
+
tplname = $1
|
138
|
+
throw "Template not found: #{$1}" unless @templates[tplname]
|
139
|
+
old_output, @output = @output, OutputHandler.new(@indent)
|
140
|
+
@output.noIndentNextLine if noIndentNextLine
|
141
|
+
_call_template(tplname, @context, args)
|
142
|
+
local_output, @output = @output, old_output
|
143
|
+
else
|
144
|
+
local_output = @parent.expand(template, *(args.dup << {:for => @context, :indent => @indent, :noIndentNextLine => noIndentNextLine}))
|
145
|
+
end
|
146
|
+
_direct_concat(local_output)
|
147
|
+
@context = old_context if old_context
|
148
|
+
local_output
|
149
|
+
end
|
150
|
+
|
151
|
+
def processAndRaise(e, tpl=nil)
|
152
|
+
bt = e.backtrace.dup
|
153
|
+
e.backtrace.each_with_index do |t,i|
|
154
|
+
if t =~ /\(erb\):(\d+):/
|
155
|
+
bt[i] = "#{@filename}:#{$1}"
|
156
|
+
bt[i] += ":in '#{tpl}'" if tpl
|
157
|
+
break
|
158
|
+
end
|
159
|
+
end
|
160
|
+
raise e, e.to_s, bt
|
161
|
+
end
|
162
|
+
|
163
|
+
def _call_template(tpl, context, args)
|
164
|
+
found = false
|
165
|
+
@templates[tpl].each_pair { |key, value|
|
166
|
+
if context.is_a?(key)
|
167
|
+
proc = @templates[tpl][key]
|
168
|
+
arity = proc.arity
|
169
|
+
arity = 0 if arity == -1 # if no args are given
|
170
|
+
raise StandardError.new("Wrong number of arguments calling template #{tpl}: #{args.size} for #{arity} "+
|
171
|
+
"(Beware: Hashes as last arguments are taken as options and are ignored)") \
|
172
|
+
if arity != args.size
|
173
|
+
begin
|
174
|
+
proc.call(*args)
|
175
|
+
rescue Exception => e
|
176
|
+
processAndRaise(e, tpl)
|
177
|
+
end
|
178
|
+
found = true
|
179
|
+
end
|
180
|
+
}
|
181
|
+
raise StandardError.new("Template class not matching: #{tpl} for #{context.class.name}") unless found
|
182
|
+
end
|
183
|
+
|
184
|
+
def _direct_concat(s)
|
185
|
+
if @output.is_a? OutputHandler
|
186
|
+
@output.direct_concat(s)
|
187
|
+
else
|
188
|
+
@output << s
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
@@ -0,0 +1,381 @@
|
|
1
|
+
module RGen
|
2
|
+
|
3
|
+
# The Transformer class can be used to specify model transformations.
|
4
|
+
#
|
5
|
+
# Model transformations take place between a <i>source model</i> (located in the <i>source
|
6
|
+
# environment</i> being an instance of the <i>source metamodel</i>) and a <i>target model</i> (located
|
7
|
+
# in the <i>target environment</i> being an instance of the <i>target metamodel</i>).
|
8
|
+
# Normally a "model" consists of several model elements associated with each other.
|
9
|
+
#
|
10
|
+
# =Transformation Rules
|
11
|
+
#
|
12
|
+
# The transformation is specified within a subclass of Transformer.
|
13
|
+
# Within the subclass, the Transformer.transform class method can be used to specify transformation
|
14
|
+
# blocks for specific metamodel classes of the source metamodel.
|
15
|
+
#
|
16
|
+
# If there is no transformation rule for the current object's class, a rule for the superclass
|
17
|
+
# is used instead. If there's no rule for the superclass, the class hierarchy is searched
|
18
|
+
# this way until Object.
|
19
|
+
#
|
20
|
+
# Here is an example:
|
21
|
+
#
|
22
|
+
# class MyTransformer < RGen::Transformer
|
23
|
+
#
|
24
|
+
# transform InputClass, :to => OutputClass do
|
25
|
+
# { :name => name, :otherClass => trans(otherClass) }
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# transform OtherInputClass, :to => OtherOutputClass do
|
29
|
+
# { :name => name }
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# In this example a transformation rule is specified for model elements of class InputClass
|
34
|
+
# as well as for elements of class OtherInputClass. The former is to be transformed into
|
35
|
+
# an instance of OutputClass, the latter into an instance of OtherOutputClass.
|
36
|
+
# Note that the Ruby class objects are used to specifiy the classes.
|
37
|
+
#
|
38
|
+
# =Transforming Attributes
|
39
|
+
#
|
40
|
+
# Besides the target class of a transformation, the attributes of the result object are
|
41
|
+
# specified in the above example. This is done by providing a Ruby block with the call of
|
42
|
+
# +transform+. Within this block arbitrary Ruby code may be placed, however the block
|
43
|
+
# must return a hash. This hash object specifies the attribute assignment of the
|
44
|
+
# result object using key/value pairs: The key must be a Symbol specifying the attribute
|
45
|
+
# which is to be assigned by name, the value is the value that will be assigned.
|
46
|
+
#
|
47
|
+
# For convenience, the transformation block will be evaluated in the context of the
|
48
|
+
# source model element which is currently being converted. This way it is possible to just
|
49
|
+
# write <code>:name => name</code> in the example in order to assign the name of the source
|
50
|
+
# object to the name attribute of the target object.
|
51
|
+
#
|
52
|
+
# =Transforming References
|
53
|
+
#
|
54
|
+
# When attributes of elements are references to other elements, those referenced
|
55
|
+
# elements have to be transformed as well. As shown above, this can be done by calling
|
56
|
+
# the Transformer#trans method. This method initiates a transformation of the element
|
57
|
+
# or array of elements passed as parameter according to transformation rules specified
|
58
|
+
# using +transform+. In fact the +trans+ method is the only way to start the transformation
|
59
|
+
# at all.
|
60
|
+
#
|
61
|
+
# For convenience and performance reasons, the result of +trans+ is cached with respect
|
62
|
+
# to the parameter object. I.e. calling trans on the same source object a second time will
|
63
|
+
# return the same result object _without_ a second evaluation of the corresponding
|
64
|
+
# transformation rules.
|
65
|
+
#
|
66
|
+
# This way the +trans+ method can be used to lookup the target element for some source
|
67
|
+
# element without the need to locally store a reference to the target element. In addition
|
68
|
+
# this can be useful if it is not clear if certain element has already been transformed
|
69
|
+
# when it is required within some other transformation block. See example below.
|
70
|
+
#
|
71
|
+
# Special care has been taken to allow the transformation of elements which reference
|
72
|
+
# each other cyclically. The key issue here is that the target element of some transformation
|
73
|
+
# is created _before_ the transformation's block is evaluated, i.e before the elements
|
74
|
+
# attributes are set. Otherwise a call to +trans+ within the transformation's block
|
75
|
+
# could lead to a +trans+ of the element itself.
|
76
|
+
#
|
77
|
+
# Here is an example:
|
78
|
+
#
|
79
|
+
# transform ModelAIn, :to => ModelAOut do
|
80
|
+
# { :name => name, :modelB => trans(modelB) }
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# transform ModelBIn, :to => ModelBOut do
|
84
|
+
# { :name => name, :modelA => trans(modelA) }
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# Note that in this case it does not matter if the transformation is initiated by calling
|
88
|
+
# +trans+ with a ModelAIn element or ModelBIn element due to the caching feature described
|
89
|
+
# above.
|
90
|
+
#
|
91
|
+
# =Transformer Methods
|
92
|
+
#
|
93
|
+
# When code in transformer blocks becomes more complex it might be useful to refactor
|
94
|
+
# it into smaller methods. If regular Ruby methods within the Transformer subclass are
|
95
|
+
# used for this purpose, it is necessary to know the source element being transformed.
|
96
|
+
# This could be achieved by explicitly passing the +@current_object+ as parameter of the
|
97
|
+
# method (see Transformer#trans).
|
98
|
+
#
|
99
|
+
# A more convenient way however is to define a special kind of method using the
|
100
|
+
# Transformer.method class method. Those methods are evaluated within the context of the
|
101
|
+
# current source element being transformed just the same as transformer blocks are.
|
102
|
+
#
|
103
|
+
# Here is an example:
|
104
|
+
#
|
105
|
+
# transform ModelIn, :to => ModelOut do
|
106
|
+
# { :number => doubleNumber }
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# method :doubleNumber do
|
110
|
+
# number * 2;
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# In this example the transformation assigns the 'number' attribute of the source element
|
114
|
+
# multiplied by 2 to the target element. The multiplication is done in a dedicated method
|
115
|
+
# called 'doubleNumber'. Note that the 'number' attribute of the source element is
|
116
|
+
# accessed without an explicit reference to the source element as the method's body
|
117
|
+
# evaluates in the source element's context.
|
118
|
+
#
|
119
|
+
# =Conditional Transformations
|
120
|
+
#
|
121
|
+
# Using the transformations as described above, all elements of the same class are
|
122
|
+
# transformed the same way. Conditional transformations allow to transform elements of
|
123
|
+
# the same class into elements of different target classes as well as applying different
|
124
|
+
# transformations on the attributes.
|
125
|
+
#
|
126
|
+
# Conditional transformations are defined by specifying multiple transformer blocks for
|
127
|
+
# the same source class and providing a condition with each block. Since it is important
|
128
|
+
# to create the target object before evaluation of the transformation block (see above),
|
129
|
+
# the conditions must also be evaluated separately _before_ the transformer block.
|
130
|
+
#
|
131
|
+
# Conditions are specified using transformer methods as described above. If the return
|
132
|
+
# value is true, the corresponding block is used for transformation. If more than one
|
133
|
+
# conditions are true, only the first transformer block will be evaluated.
|
134
|
+
#
|
135
|
+
# If there is no rule with a condition evaluating to true, rules for superclasses will
|
136
|
+
# be checked as described above.
|
137
|
+
#
|
138
|
+
# Here is an example:
|
139
|
+
#
|
140
|
+
# transform ModelIn, :to => ModelOut, :if => :largeNumber do
|
141
|
+
# { :number => number * 2}
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
# transform ModelIn, :to => ModelOut, :if => :smallNumber do
|
145
|
+
# { :number => number / 2 }
|
146
|
+
# end
|
147
|
+
#
|
148
|
+
# method :largeNumber do
|
149
|
+
# number > 1000
|
150
|
+
# end
|
151
|
+
#
|
152
|
+
# method :smallNumber do
|
153
|
+
# number < 500
|
154
|
+
# end
|
155
|
+
#
|
156
|
+
# In this case the transformation of an element of class ModelIn depends on the value
|
157
|
+
# of the element's 'number' attribute. If the value is greater than 1000, the first rule
|
158
|
+
# as taken and the number is doubled. If the value is smaller than 500, the second rule
|
159
|
+
# is taken and the number is divided by two.
|
160
|
+
#
|
161
|
+
# Note that it is up to the user to avoid cycles within the conditions. A cycle could
|
162
|
+
# occure if the condition are based on transformation target elements, i.e. if +trans+
|
163
|
+
# is used within the condition to lookup or transform other elements.
|
164
|
+
#
|
165
|
+
class Transformer
|
166
|
+
|
167
|
+
TransformationDescription = Struct.new(:block, :target) # :nodoc:
|
168
|
+
|
169
|
+
@@methods = {}
|
170
|
+
@@transformer_blocks = {}
|
171
|
+
|
172
|
+
def self._transformer_blocks # :nodoc:
|
173
|
+
@@transformer_blocks[self] ||= {}
|
174
|
+
end
|
175
|
+
|
176
|
+
def self._methods # :nodoc:
|
177
|
+
@@methods[self] ||= {}
|
178
|
+
end
|
179
|
+
|
180
|
+
# This class method is used to specify a transformation rule.
|
181
|
+
#
|
182
|
+
# The first argument specifies the class of elements for which this rule applies.
|
183
|
+
# The second argument must be a hash including the target class
|
184
|
+
# (as value of key ':to') and an optional condition (as value of key ':if').
|
185
|
+
#
|
186
|
+
# The target class is specified by passing the actual Ruby class object.
|
187
|
+
# The condition is either the name of a transformer method (see Transfomer.method) as
|
188
|
+
# a symbol or a proc object. In either case the block is evaluated at transformation
|
189
|
+
# time and its result value determines if the rule applies.
|
190
|
+
#
|
191
|
+
def self.transform(from, desc=nil, &block)
|
192
|
+
to = (desc && desc.is_a?(Hash) && desc[:to])
|
193
|
+
condition = (desc && desc.is_a?(Hash) && desc[:if])
|
194
|
+
raise StandardError.new("No transformation target specified.") unless to
|
195
|
+
block_desc = TransformationDescription.new(block, to)
|
196
|
+
if condition
|
197
|
+
_transformer_blocks[from] ||= {}
|
198
|
+
raise StandardError.new("Multiple (non-conditional) transformations for class #{from.name}.") unless _transformer_blocks[from].is_a?(Hash)
|
199
|
+
_transformer_blocks[from][condition] = block_desc
|
200
|
+
else
|
201
|
+
raise StandardError.new("Multiple (non-conditional) transformations for class #{from.name}.") unless _transformer_blocks[from].nil?
|
202
|
+
_transformer_blocks[from] = block_desc
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# This class method specifies that all objects of class +from+ are to be copied
|
207
|
+
# into an object of class +to+. If +to+ is omitted, +from+ is used as target class.
|
208
|
+
# During copy, all attributes and references of the target object
|
209
|
+
# are set to their transformed counterparts of the source object.
|
210
|
+
#
|
211
|
+
def self.copy(from, to=nil)
|
212
|
+
transform(from, :to => to || from) do
|
213
|
+
Hash[*@current_object.class.ecore.eAllStructuralFeatures.inject([]) {|l,a|
|
214
|
+
l + [a.name.to_sym, trans(@current_object.send(a.name))]
|
215
|
+
}]
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Define a transformer method for the current transformer class.
|
220
|
+
# In contrast to regular Ruby methods, a method defined this way executes in the
|
221
|
+
# context of the object currently being transformed.
|
222
|
+
#
|
223
|
+
def self.method(name, &block)
|
224
|
+
_methods[name.to_s] = block
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
# Creates a new transformer
|
229
|
+
# Optionally an input and output Environment can be specified.
|
230
|
+
#
|
231
|
+
def initialize(env_in=nil, env_out=nil)
|
232
|
+
@env_in = env_in
|
233
|
+
@env_out = env_out
|
234
|
+
@transformer_results = {}
|
235
|
+
@transformer_jobs = []
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
# Transforms a given model element according to the rules specified by means of
|
240
|
+
# the Transformer.transform class method.
|
241
|
+
#
|
242
|
+
# The transformation result element is created in the output environment and returned.
|
243
|
+
# In addition, the result is cached, i.e. a second invocation with the same parameter
|
244
|
+
# object will return the same result object without any further evaluation of the
|
245
|
+
# transformation rules. Nil will be transformed into nil. Ruby "singleton" objects
|
246
|
+
# +true+, +false+, numerics and symbols will be returned without any change. Ruby strings
|
247
|
+
# will be duplicated with the result being cached.
|
248
|
+
#
|
249
|
+
# The transformation input can be given as:
|
250
|
+
# * a single object
|
251
|
+
# * an array each element of which is transformed in turn
|
252
|
+
# * a hash used as input to Environment#find with the result being transformed
|
253
|
+
#
|
254
|
+
def trans(obj)
|
255
|
+
if obj.is_a?(Hash)
|
256
|
+
raise StandardError.new("No input environment available to find model element.") unless @env_in
|
257
|
+
obj = @env_in.find(obj)
|
258
|
+
end
|
259
|
+
return nil if obj.nil?
|
260
|
+
return obj if obj.is_a?(TrueClass) or obj.is_a?(FalseClass) or obj.is_a?(Numeric) or obj.is_a?(Symbol)
|
261
|
+
return @transformer_results[obj] if @transformer_results[obj]
|
262
|
+
return @transformer_results[obj] = obj.dup if obj.is_a?(String)
|
263
|
+
return obj.collect{|o| trans(o)}.compact if obj.is_a? Array
|
264
|
+
raise StandardError.new("No transformer for class #{obj.class.name}") unless _transformerBlock(obj.class)
|
265
|
+
block_desc = _evaluateCondition(obj)
|
266
|
+
return nil unless block_desc
|
267
|
+
@transformer_results[obj] = _instantiateTargetClass(obj, block_desc.target)
|
268
|
+
# we will transform the properties later
|
269
|
+
@transformer_jobs << TransformerJob.new(self, obj, block_desc)
|
270
|
+
# if there have been jobs in the queue before, don't process them in this call
|
271
|
+
# this way calls to trans are not nested; a recursive implementation
|
272
|
+
# may cause a "Stack level too deep" exception for large models
|
273
|
+
return @transformer_results[obj] if @transformer_jobs.size > 1
|
274
|
+
# otherwise this is the first call of trans, process all jobs here
|
275
|
+
# more jobs will be added during job execution
|
276
|
+
while @transformer_jobs.size > 0
|
277
|
+
@transformer_jobs.first.execute
|
278
|
+
@transformer_jobs.shift
|
279
|
+
end
|
280
|
+
@transformer_results[obj]
|
281
|
+
end
|
282
|
+
|
283
|
+
def _transformProperties(obj, block_desc) #:nodoc:
|
284
|
+
old_object, @current_object = @current_object, obj
|
285
|
+
block_result = instance_eval(&block_desc.block)
|
286
|
+
raise StandardError.new("Transformer must return a hash") unless block_result.is_a? Hash
|
287
|
+
@current_object = old_object
|
288
|
+
_attributesFromHash(@transformer_results[obj], block_result)
|
289
|
+
end
|
290
|
+
|
291
|
+
class TransformerJob #:nodoc:
|
292
|
+
def initialize(transformer, obj, block_desc)
|
293
|
+
@transformer, @obj, @block_desc = transformer, obj, block_desc
|
294
|
+
end
|
295
|
+
def execute
|
296
|
+
@transformer._transformProperties(@obj, @block_desc)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Each call which is not handled by the transformer object is passed to the object
|
301
|
+
# currently being transformed.
|
302
|
+
# If that object also does not respond to the call, it is treated as a transformer
|
303
|
+
# method call (see Transformer.method).
|
304
|
+
#
|
305
|
+
def method_missing(m, *args) #:nodoc:
|
306
|
+
if @current_object.respond_to?(m)
|
307
|
+
@current_object.send(m, *args)
|
308
|
+
else
|
309
|
+
_invokeMethod(m, *args)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
private
|
314
|
+
|
315
|
+
# returns _transformer_blocks content for clazz or one of its superclasses
|
316
|
+
def _transformerBlock(clazz) # :nodoc:
|
317
|
+
block = self.class._transformer_blocks[clazz]
|
318
|
+
block = _transformerBlock(clazz.superclass) if block.nil? && clazz != Object
|
319
|
+
block
|
320
|
+
end
|
321
|
+
|
322
|
+
# returns the first TransformationDescription for clazz or one of its superclasses
|
323
|
+
# for which condition is true
|
324
|
+
def _evaluateCondition(obj, clazz=obj.class) # :nodoc:
|
325
|
+
tb = self.class._transformer_blocks[clazz]
|
326
|
+
block_description = nil
|
327
|
+
if tb.is_a?(TransformationDescription)
|
328
|
+
# non-conditional
|
329
|
+
block_description = tb
|
330
|
+
elsif tb
|
331
|
+
old_object, @current_object = @current_object, obj
|
332
|
+
tb.each_pair {|condition, block|
|
333
|
+
if condition.is_a?(Proc)
|
334
|
+
result = instance_eval(&condition)
|
335
|
+
elsif condition.is_a?(Symbol)
|
336
|
+
result = _invokeMethod(condition)
|
337
|
+
else
|
338
|
+
result = condition
|
339
|
+
end
|
340
|
+
if result
|
341
|
+
block_description = block
|
342
|
+
break
|
343
|
+
end
|
344
|
+
}
|
345
|
+
@current_object = old_object
|
346
|
+
end
|
347
|
+
block_description = _evaluateCondition(obj, clazz.superclass) if block_description.nil? && clazz != Object
|
348
|
+
block_description
|
349
|
+
end
|
350
|
+
|
351
|
+
def _instantiateTargetClass(obj, target_desc) # :nodoc:
|
352
|
+
old_object, @current_object = @current_object, obj
|
353
|
+
if target_desc.is_a?(Proc)
|
354
|
+
target_class = instance_eval(&target_desc)
|
355
|
+
elsif target_desc.is_a?(Symbol)
|
356
|
+
target_class = _invokeMethod(target_desc)
|
357
|
+
else
|
358
|
+
target_class = target_desc
|
359
|
+
end
|
360
|
+
@current_object = old_object
|
361
|
+
result = target_class.new
|
362
|
+
@env_out << result if @env_out
|
363
|
+
result
|
364
|
+
end
|
365
|
+
|
366
|
+
def _invokeMethod(m) # :nodoc:
|
367
|
+
raise StandardError.new("Method not found: #{m}") unless self.class._methods[m.to_s]
|
368
|
+
instance_eval(&self.class._methods[m.to_s])
|
369
|
+
end
|
370
|
+
|
371
|
+
def _attributesFromHash(obj, hash) # :nodoc:
|
372
|
+
hash.delete(:class)
|
373
|
+
hash.each_pair{|k,v|
|
374
|
+
obj.send("#{k}=", v)
|
375
|
+
}
|
376
|
+
obj
|
377
|
+
end
|
378
|
+
|
379
|
+
end
|
380
|
+
|
381
|
+
end
|