rgen 0.4.2 → 0.4.3

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.
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