rgen 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/CHANGELOG +7 -0
  2. data/lib/rgen/metamodel_builder.rb +5 -4
  3. data/lib/rgen/metamodel_builder.rb.bak +196 -0
  4. data/lib/rgen/metamodel_builder/builder_extensions.rb +51 -38
  5. data/lib/rgen/metamodel_builder/builder_extensions.rb.bak +437 -0
  6. data/lib/rgen/metamodel_builder/builder_runtime.rb +2 -20
  7. data/lib/rgen/metamodel_builder/builder_runtime.rb.bak +73 -0
  8. data/lib/rgen/name_helper.rb.bak +37 -0
  9. data/lib/rgen/template_language.rb +8 -0
  10. data/lib/rgen/template_language.rb.bak +289 -0
  11. data/lib/rgen/template_language/directory_template_container.rb +11 -0
  12. data/lib/rgen/template_language/directory_template_container.rb.bak +69 -0
  13. data/lib/rgen/template_language/output_handler.rb +3 -2
  14. data/lib/rgen/template_language/output_handler.rb.bak +88 -0
  15. data/lib/rgen/template_language/template_container.rb +5 -4
  16. data/lib/rgen/template_language/template_container.rb.bak +196 -0
  17. data/lib/rgen/transformer.rb.bak +381 -0
  18. data/test/environment_test.rb.bak +52 -0
  19. data/test/metamodel_builder_test.rb +6 -0
  20. data/test/metamodel_builder_test.rb.bak +443 -0
  21. data/test/metamodel_roundtrip_test/TestModel_Regenerated.rb +34 -32
  22. data/test/metamodel_roundtrip_test/houseMetamodel_Regenerated.ecore +58 -58
  23. data/test/output_handler_test.rb +8 -0
  24. data/test/output_handler_test.rb.bak +50 -0
  25. data/test/template_language_test.rb +23 -0
  26. data/test/template_language_test.rb.bak +72 -0
  27. data/test/template_language_test/indentStringTestDefaultIndent.out +1 -0
  28. data/test/template_language_test/indentStringTestTabIndent.out +1 -0
  29. data/test/template_language_test/templates/indent_string_test.tpl +12 -0
  30. data/test/template_language_test/templates/null_context_test.tpl +12 -0
  31. data/test/transformer_test.rb.bak +223 -0
  32. metadata +65 -48
  33. 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(" "*@indent)
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
- throw "Template not found: #{$1}" unless @templates[tplname]
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