metaruby 1.0.0.rc1

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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/History.txt +1 -0
  4. data/Manifest.txt +40 -0
  5. data/README.md +318 -0
  6. data/Rakefile +39 -0
  7. data/lib/metaruby/class.rb +120 -0
  8. data/lib/metaruby/dsls/doc.rb +78 -0
  9. data/lib/metaruby/dsls/find_through_method_missing.rb +76 -0
  10. data/lib/metaruby/dsls.rb +2 -0
  11. data/lib/metaruby/gui/exception_view.rb +124 -0
  12. data/lib/metaruby/gui/html/button.rb +65 -0
  13. data/lib/metaruby/gui/html/collection.rb +103 -0
  14. data/lib/metaruby/gui/html/exception_view.css +8 -0
  15. data/lib/metaruby/gui/html/jquery.min.js +154 -0
  16. data/lib/metaruby/gui/html/jquery.selectfilter.js +65 -0
  17. data/lib/metaruby/gui/html/jquery.tagcloud.min.js +8 -0
  18. data/lib/metaruby/gui/html/jquery.tinysort.min.js +8 -0
  19. data/lib/metaruby/gui/html/list.rhtml +24 -0
  20. data/lib/metaruby/gui/html/page.css +55 -0
  21. data/lib/metaruby/gui/html/page.rb +283 -0
  22. data/lib/metaruby/gui/html/page.rhtml +13 -0
  23. data/lib/metaruby/gui/html/page_body.rhtml +17 -0
  24. data/lib/metaruby/gui/html/rock-website.css +694 -0
  25. data/lib/metaruby/gui/html.rb +16 -0
  26. data/lib/metaruby/gui/model_browser.rb +262 -0
  27. data/lib/metaruby/gui/model_selector.rb +266 -0
  28. data/lib/metaruby/gui/rendering_manager.rb +112 -0
  29. data/lib/metaruby/gui/ruby_constants_item_model.rb +253 -0
  30. data/lib/metaruby/gui.rb +9 -0
  31. data/lib/metaruby/inherited_attribute.rb +482 -0
  32. data/lib/metaruby/module.rb +158 -0
  33. data/lib/metaruby/registration.rb +157 -0
  34. data/lib/metaruby/test.rb +79 -0
  35. data/lib/metaruby.rb +17 -0
  36. data/manifest.xml +12 -0
  37. data/test/suite.rb +15 -0
  38. data/test/test_attributes.rb +323 -0
  39. data/test/test_class.rb +68 -0
  40. data/test/test_dsls.rb +49 -0
  41. data/test/test_module.rb +105 -0
  42. data/test/test_registration.rb +182 -0
  43. metadata +160 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 58ab70780a173a248a3f2a14a86d9b210e740f14
4
+ data.tar.gz: 178e85ee93eb3727f53afc7d8b0c1505cd01fa0d
5
+ SHA512:
6
+ metadata.gz: e320d1c8f9b52c88d7f3d24939210ebce53777711cc3909a48a7d7512e7e1724adf8bfde091e80695137d598aa23bd82c0232bb7c7e2538918bda3a721c23487
7
+ data.tar.gz: 28eb0b645abbaa873e93f5fb138e2df333d913e9adb791f8229a4edc0edce87630f54db2f36dc8a4a1454b178b1bab00559a42acbd68dceb374ba205dcb32884
data/.gemtest ADDED
File without changes
data/History.txt ADDED
@@ -0,0 +1 @@
1
+ ===
data/Manifest.txt ADDED
@@ -0,0 +1,40 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.md
4
+ Rakefile
5
+ lib/metaruby.rb
6
+ lib/metaruby/class.rb
7
+ lib/metaruby/dsls.rb
8
+ lib/metaruby/dsls/doc.rb
9
+ lib/metaruby/dsls/find_through_method_missing.rb
10
+ lib/metaruby/gui.rb
11
+ lib/metaruby/gui/exception_view.rb
12
+ lib/metaruby/gui/html.rb
13
+ lib/metaruby/gui/html/button.rb
14
+ lib/metaruby/gui/html/collection.rb
15
+ lib/metaruby/gui/html/exception_view.css
16
+ lib/metaruby/gui/html/jquery.min.js
17
+ lib/metaruby/gui/html/jquery.selectfilter.js
18
+ lib/metaruby/gui/html/jquery.tagcloud.min.js
19
+ lib/metaruby/gui/html/jquery.tinysort.min.js
20
+ lib/metaruby/gui/html/list.rhtml
21
+ lib/metaruby/gui/html/page.css
22
+ lib/metaruby/gui/html/page.rb
23
+ lib/metaruby/gui/html/page.rhtml
24
+ lib/metaruby/gui/html/page_body.rhtml
25
+ lib/metaruby/gui/html/rock-website.css
26
+ lib/metaruby/gui/model_browser.rb
27
+ lib/metaruby/gui/model_selector.rb
28
+ lib/metaruby/gui/rendering_manager.rb
29
+ lib/metaruby/gui/ruby_constants_item_model.rb
30
+ lib/metaruby/inherited_attribute.rb
31
+ lib/metaruby/module.rb
32
+ lib/metaruby/registration.rb
33
+ lib/metaruby/test.rb
34
+ manifest.xml
35
+ test/suite.rb
36
+ test/test_attributes.rb
37
+ test/test_class.rb
38
+ test/test_dsls.rb
39
+ test/test_module.rb
40
+ test/test_registration.rb
data/README.md ADDED
@@ -0,0 +1,318 @@
1
+ # Metamodelling in the Ruby type system
2
+
3
+ * https://gitorious.org/rock-toolchain/metaruby
4
+
5
+ MetaRuby is a library that allows to (ab)use the Ruby type system to create
6
+ reflexive programs: create a specialized modelling API (a.k.a. "a DSL") at the
7
+ class/module level and then get access to this model information from the
8
+ objects.
9
+
10
+ This page will describe the various functionality that metaruby provides to help
11
+ modelling in Ruby.
12
+
13
+ This page will reuse one of the most overused example of modelling: a car and
14
+ colors.
15
+
16
+ ## Models
17
+
18
+ Using MetaRuby, models can either be represented by Ruby classes or by Ruby
19
+ modules. You use the first one when you want to model something from which an
20
+ object can be created, in our example: a car. You use the second for things that
21
+ cannot be instanciated, but can be used as attributes of another object, in our
22
+ example: a color.
23
+
24
+ Another point of terminology: _metamodel_. The metamodel is the
25
+ model-of-the-model, i.e. it is the bits and pieces that allow to describe a
26
+ model (the model itself describing an object). As you will see, metamodels in
27
+ MetaRuby are all described in modules.
28
+
29
+ ## Models as classes
30
+
31
+ The metamodel of models that are represented by classes must include
32
+ {MetaRuby::ModelAsClass} and are then used to extend said class
33
+
34
+ ~~~
35
+ module Models
36
+ module Car
37
+ include MetaRuby::ModelAsClass
38
+ end
39
+ end
40
+ class Car
41
+ extend Models::Car
42
+ end
43
+ ~~~
44
+
45
+ Then, creating a new Car model is done by subclassing the Car class:
46
+
47
+ ~~~
48
+ class Peugeot < Car
49
+ # Call methods from the modelling DSL defined by Models::Car
50
+ end
51
+ ~~~
52
+
53
+ This creates a _named model_, i.e. a model that can be accessed by name. Another
54
+ way is to create an anonymous model by calling {MetaRuby::ModelAsClass#new_submodel new_submodel}:
55
+
56
+ ~~~
57
+ model = Car.new_submodel do
58
+ # Call methods from the modelling DSL defined by Models::Car
59
+ end
60
+ ~~~
61
+
62
+ Note that this mechanism naturally extends to submodels-of-submodels, e.g.
63
+
64
+ ~~~
65
+ class P806 < Peugeot
66
+ # Call methods from the modelling DSL defined by Models::Car
67
+ end
68
+ ~~~
69
+
70
+ ## Models as modules
71
+
72
+ The metamodel of models that are represented by modules must include
73
+ {MetaRuby::ModelAsModule} and are then used to extend said module
74
+
75
+ ~~~
76
+ module Models
77
+ module Color
78
+ include MetaRuby::ModelAsModule
79
+ end
80
+ end
81
+ module Color
82
+ extend Models::Color
83
+ end
84
+ ~~~
85
+
86
+ Then, creating a new Color model is done by calling {MetaRuby::ModelAsModule#new_submodel new_submodel} on Color
87
+
88
+ ~~~
89
+ red = Color.new_submodel
90
+ ~~~
91
+
92
+ A common pattern is to define a method on the Module class, that creates new
93
+ models and assigns them to constants. MetaRuby provides a helper method for this
94
+ purpose, that we strongly recommend you use:
95
+
96
+ ~~~
97
+ class Module
98
+ def color(name, &block)
99
+ MetaRuby::ModelAsModule.create_ang_register_submodel(self, name, Color, &block)
100
+ end
101
+ end
102
+ ~~~
103
+
104
+ Which can then be used with:
105
+
106
+ ~~~
107
+ module MyNamespace
108
+ color 'Red' do
109
+ # Call methods from the color modelling DSL defined by Models::Color
110
+ end
111
+ end
112
+ ~~~
113
+
114
+ The new Red color model can then be accessed with MyNamespace::Red
115
+
116
+ A model hierarchy can be built by telling MetaRuby that a given model _provides_
117
+ another one, for instance:
118
+
119
+ ~~~
120
+ color 'Yellow' do
121
+ provides Red
122
+ provides Green
123
+ end
124
+ ~~~
125
+
126
+ And, finally, a class-based model can provide a module-based one:
127
+
128
+ ~~~
129
+ class Peugeot < Car
130
+ # All peugeots are yellow
131
+ provides Yellow
132
+ end
133
+ ~~~
134
+
135
+ # Attributes
136
+
137
+ One important part of the whole modelling is to list _attributes_ of the things
138
+ that are getting modelled. The important bit being the definition of what should
139
+ happen when creating a new submodel for an existing model. Even though we will
140
+ use the class-as-model representation in all the following examples, the exact
141
+ same mechanisms are available in the model-as-module. The only difference is
142
+ that a class-as-model is a submodel of all its parent classes while a
143
+ class-as-module is a submodel of all the other modules it provides.
144
+
145
+ # Zero-or-one attributes
146
+
147
+ These are attributes that hold at most one value (and possibly none). The
148
+ typical example is the predicate (boolean attribute)
149
+
150
+ ~~~
151
+ module Models::Car
152
+ include MetaRuby::ModelAsClass
153
+
154
+ # Attribute inherited along the hierarchy of models
155
+ inherited_single_value_attribute("number_of_doors")
156
+ end
157
+ ~~~
158
+ ~~~
159
+ class SportsCar < Car
160
+ # Make the default number of doors of all sports car 2
161
+ number_of_doors 2
162
+ end
163
+ class ASportsCar < SportsCar
164
+ # Actually, this one has a trunk
165
+ number_of_doors 3
166
+ end
167
+ class AnotherSportsCar < SportsCar
168
+ end
169
+ ~~~
170
+ ~~~
171
+ Car.number_of_doors => nil
172
+ SportsCar.number_of_doors => 2
173
+ ASportsCar.number_of_doors => 3
174
+ AnotherSportsCar.number_of_doors => 2 # Inherited from SportsCar
175
+ ~~~
176
+
177
+ ## Set attributes
178
+ These are attributes that hold a set of values. When taking into account the
179
+ hierarchy of models, the set for a model X is the union of all the sets of X and
180
+ all its parents:
181
+
182
+ ~~~
183
+ module Models::Car
184
+ include MetaRuby::ModelAsClass
185
+ # Attribute inherited along the hierarchy of models
186
+ inherited_attribute("material", "materials")
187
+ end
188
+ ~~~
189
+ ~~~
190
+ class Car
191
+ extend Models::Car
192
+ materials << 'iron' # all cars contain iron
193
+ end
194
+ class Peugeot < Car
195
+ materials << 'plastic' # additionally, all peugeot cars contain plastic
196
+ end
197
+ ~~~
198
+ ~~~
199
+ Car.each_material.to_a => ['iron']
200
+ Peugeot.each_material.to_a => ['iron', 'plastic']
201
+ Car.all_materials => ['iron']
202
+ Peugeot.all_materials => ['iron', 'plastic']
203
+ Car.self_materials => ['iron']
204
+ Peugeot.self_materials => ['plastic']
205
+ ~~~
206
+
207
+ ## Named attributes
208
+ In certain situations, elements of the sets that we represented in the previous
209
+ section can actually have names (where the names are actually part of the
210
+ modelling).
211
+
212
+ ~~~
213
+ module Models::Car
214
+ include MetaRuby::ModelAsClass
215
+ # Attribute inherited along the hierarchy of models
216
+ inherited_attribute("door_color", "door_colors")
217
+ def number_of_doors
218
+ all_door_colors.to_a.size
219
+ end
220
+ end
221
+ ~~~
222
+
223
+ ~~~
224
+ class Car
225
+ extend Models::Car
226
+ door_colors['driver'] = Color # There is a driver door, but we don't know
227
+ # the color
228
+ door_colors['other'] = Color # There is another door, but we don't know
229
+ # the color
230
+ end
231
+ class Peugeot < Car
232
+ # All peugeot have a red driver door and a green trunk door
233
+ door_colors['driver'] = Red
234
+ door_colors['trunk'] = Green
235
+ end
236
+ ~~~
237
+
238
+ ~~~
239
+ Car.self_door_colors => {'driver' => Color, 'other' => Color }
240
+ Car.all_door_colors => {'driver' => Color, 'other' => Color }
241
+ Peugeot.self_door_colors => {'driver' => Red, 'trunk' => Green }
242
+ Peugeot.self_door_colors => {'driver' => Red, 'other' => Color, 'trunk' => Green }
243
+ ~~~
244
+
245
+ ## Value promotion
246
+ In some cases, one need to modify the values inherited from the parent models
247
+ before they can become proper attributes of the child model, commonly because
248
+ the objects stored in the attributes refer to the model they are part of. For
249
+ instance, let's assume we have a Door object defined thus:
250
+
251
+ ~~~
252
+ Door = Struct :car_model, :color
253
+ ~~~
254
+
255
+ and
256
+
257
+ ~~~
258
+ Car.doors['driver'] = Door.new(Car, Color)
259
+ Car.doors['other'] = Door.new(Car, Color)
260
+ ~~~
261
+
262
+ Now,
263
+
264
+ ~~~
265
+ Peugeot.find_door('driver').car_model => Car
266
+ ~~~
267
+
268
+ In most cases, we would like to have this last value be Peugeot. This can be
269
+ done by defining a promotion method on the metamodel _before_ the inherited
270
+ attribute is defined:
271
+
272
+ ~~~
273
+ module Models::Car
274
+ # Called to promote a door model from its immediate supermodel to this
275
+ # model
276
+ def promote_door(door_name, door)
277
+ # You have to create a new door object !
278
+ door = door.dup
279
+ door.car_model = self
280
+ door
281
+ end
282
+
283
+ # Define the attribute *after* the promotion method
284
+ inherited_attribute("door", "doors")
285
+ end
286
+ ~~~
287
+
288
+ # Model Registration
289
+ The last bit that MetaRuby takes care of is to register all models that have
290
+ been defined, allowing to browse them by type. For instance, all models based on
291
+ the Car model can be enumerated with:
292
+
293
+ ~~~
294
+ Car.each_submodel
295
+ ~~~
296
+
297
+ Because this mechanism keeps a reference on all model objects, it is necessary
298
+ to clear the registered submodels dealing with e.g. tests that create submodels
299
+ on the fly. This is done by calling {MetaRuby::Registration#clear_submodels
300
+ clear_submodels} in the tests teardown:
301
+
302
+ ~~~
303
+ Car.clear_submodels
304
+ ~~~
305
+
306
+ This will only clear anonymous models. Models that are created either by
307
+ subclassing a model class or by using
308
+ {MetaRuby::ModelAsModule#create_ang_register_submodel
309
+ create_ang_register_submodel} are marked as
310
+ {MetaRuby::Registration#permanent_model? permanent models} and therefore
311
+ protected from removal by #clear_submodel
312
+
313
+ # Adding options to the submodel creation process
314
+
315
+ If you need to customize the submodel creation process, for instance by
316
+ providing options to the subprocess, do so by overloading #setup_submodel. Do
317
+ NOT overload #new_submodel unless you really know what you are doing, and pass
318
+ the options as an option hash
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ task 'default'
2
+ require 'metaruby/version'
3
+
4
+ require 'utilrb/doc/rake'
5
+ Utilrb.doc :include => ['lib/**/*.rb']
6
+
7
+ begin
8
+ require 'hoe'
9
+ Hoe::plugin :yard
10
+
11
+ config = Hoe.spec 'metaruby' do
12
+ self.version = MetaRuby::VERSION
13
+ self.developer "Sylvain Joyeux", "sylvain.joyeux@m4x.org"
14
+ self.summary = 'Modelling using the Ruby language as a metamodel'
15
+ self.description = paragraphs_of('README.md', 3..6).join("\n\n")
16
+ self.changes = paragraphs_of('History.txt', 0..1).join("\n\n")
17
+ self.readme_file = 'README.md'
18
+ self.history_file = 'History.txt'
19
+ self.license 'LGPLv3+'
20
+
21
+ extra_deps <<
22
+ ['utilrb', '>= 1.3.4']
23
+ extra_dev_deps <<
24
+ ['rake', '>= 0.8'] <<
25
+ ['hoe-yard', '>= 0.1.2']
26
+ end
27
+
28
+ Rake.clear_tasks(/^default$/)
29
+ task :default => []
30
+ task :doc => :yard
31
+ rescue LoadError
32
+ STDERR.puts "cannot load the Hoe gem. Distribution is disabled"
33
+ rescue Exception => e
34
+ puts e.backtrace
35
+ if e.message !~ /\.rubyforge/
36
+ STDERR.puts "WARN: cannot load the Hoe gem, or Hoe fails. Publishing tasks are disabled"
37
+ STDERR.puts "WARN: error message is: #{e.message}"
38
+ end
39
+ end
@@ -0,0 +1,120 @@
1
+ module MetaRuby
2
+ # Extend in classes that are used to represent models
3
+ #
4
+ # @example
5
+ # class MyBaseClass
6
+ # extend MetaRuby::ModelAsClass
7
+ # end
8
+ #
9
+ # Alternatively, you can create a module that describes the metamodel and
10
+ # extend the base model class with it
11
+ #
12
+ # @example
13
+ # module MyBaseMetamodel
14
+ # include MetaRuby::ModelAsModule
15
+ # end
16
+ # class MyBaseModel
17
+ # extend MyBaseMetamodel
18
+ # end
19
+ #
20
+ module ModelAsClass
21
+ include Attributes
22
+ include Registration
23
+ extend Attributes
24
+
25
+ # The call stack at the point of definition of this model
26
+ attr_accessor :definition_location
27
+
28
+ # @return [String] set or get the documentation text for this model
29
+ inherited_single_value_attribute :doc
30
+
31
+ # Sets a name on this model
32
+ #
33
+ # Only use this on 'anonymous models', i.e. on models that are not
34
+ # meant to be assigned on a Ruby constant
35
+ #
36
+ # @return [String] the assigned name
37
+ def name=(name)
38
+ def self.name
39
+ if @name then @name
40
+ else super
41
+ end
42
+ end
43
+ @name = name
44
+ end
45
+
46
+ # The model next in the ancestry chain, or nil if +self+ is root
47
+ #
48
+ # @return [Class]
49
+ def supermodel
50
+ if superclass.respond_to?(:supermodel)
51
+ return superclass
52
+ end
53
+ end
54
+
55
+ # This flag is used to notify {#inherited} that it is being called from
56
+ # new_submodel, in which case it should not
57
+ #
58
+ # This mechanism works as:
59
+ # - inherited(subclass) is called right away after class.new is called
60
+ # (so, we don't have to take recursive calls into account)
61
+ # - it is a TLS, so thread safe
62
+ #
63
+ FROM_NEW_SUBMODEL_TLS = :metaruby_class_new_called_from_new_submodel
64
+
65
+ # Creates a new submodel of +self+
66
+ #
67
+ # @option options [String] name forcefully set a name on the new
68
+ # model. Use this only for models that are not meant to be
69
+ # assigned on a Ruby constant
70
+ #
71
+ # @return [Module] a subclass of self
72
+ def new_submodel(options = Hash.new, &block)
73
+ options, submodel_options = Kernel.filter_options options,
74
+ :name => nil
75
+
76
+ Thread.current[FROM_NEW_SUBMODEL_TLS] = true
77
+ model = self.class.new(self)
78
+ model.permanent_model = false
79
+ if options[:name]
80
+ model.name = options[:name]
81
+ end
82
+ setup_submodel(model, submodel_options, &block)
83
+ model
84
+ end
85
+
86
+ # Called to apply a DSL block on this model
87
+ def apply_block(&block)
88
+ class_eval(&block)
89
+ end
90
+
91
+ # Called at the end of the definition of a new submodel
92
+ def setup_submodel(submodel, options = Hash.new, &block)
93
+ register_submodel(submodel)
94
+
95
+ if block_given?
96
+ submodel.apply_block(&block)
97
+ end
98
+ end
99
+
100
+ # Registers submodels when a subclass is created
101
+ def inherited(subclass)
102
+ from_new_submodel = Thread.current[FROM_NEW_SUBMODEL_TLS]
103
+ Thread.current[FROM_NEW_SUBMODEL_TLS] = false
104
+
105
+ subclass.definition_location = call_stack
106
+ super
107
+ subclass.permanent_model = subclass.accessible_by_name? &&
108
+ subclass.permanent_definition_context?
109
+ if !from_new_submodel
110
+ setup_submodel(subclass)
111
+ end
112
+ end
113
+
114
+ # Call to declare that this model provides the given model-as-module
115
+ def provides(model_as_module)
116
+ include model_as_module
117
+ end
118
+ end
119
+ end
120
+
@@ -0,0 +1,78 @@
1
+ require 'facets/kernel/call_stack'
2
+ module MetaRuby
3
+ module DSLs
4
+ # Looks for the documentation block for the element that is being built.
5
+ #
6
+ # @param [#===] file_match an object (typically a regular expression)
7
+ # that matches the file name in which the DSL is being used
8
+ # @param [#===] trigger_method an object (typically a regular expression)
9
+ # that matches the name of the method that initiates the creation of
10
+ # the element whose documentation we are looking for.
11
+ # @return [String,nil] the parsed documentation, or nil if there is no
12
+ # documentation
13
+ def self.parse_documentation_block(file_match, trigger_method = /.*/)
14
+ last_method_matched = false
15
+ call_stack.each do |call|
16
+ this_method_matched =
17
+ if trigger_method === call[2].to_s
18
+ true
19
+ elsif call[2] == :method_missing
20
+ last_method_matched
21
+ else
22
+ false
23
+ end
24
+
25
+ if !this_method_matched && last_method_matched && (file_match === call[0])
26
+ if File.file?(call[0])
27
+ return parse_documentation_block_at(call[0], call[1])
28
+ else return
29
+ end
30
+ end
31
+ last_method_matched = this_method_matched
32
+ end
33
+ nil
34
+ end
35
+
36
+ # Parses upwards a Ruby documentation block whose last line starts at or
37
+ # just before the given line in the given file
38
+ #
39
+ # @param [String] file
40
+ # @param [Integer] line
41
+ # @return [String,nil] the parsed documentation, or nil if there is no
42
+ # documentation
43
+ def self.parse_documentation_block_at(file, line)
44
+ lines = File.readlines(file)
45
+
46
+ block = []
47
+ # Lines are given 1-based (as all editors work that way), and we
48
+ # want the line before the definition. Remove two
49
+ line = line - 2
50
+ while true
51
+ case l = lines[line]
52
+ when /^\s*$/
53
+ break
54
+ when /^\s*#/
55
+ block << l
56
+ else break
57
+ end
58
+ line = line - 1
59
+ end
60
+ block = block.map do |l|
61
+ l.strip.gsub(/^\s*#/, '')
62
+ end
63
+ # Now remove the same amount of spaces in front of each lines
64
+ space_count = block.map do |l|
65
+ l =~ /^(\s*)/
66
+ if $1.size != l.size
67
+ $1.size
68
+ end
69
+ end.compact.min
70
+ block = block.map do |l|
71
+ l[space_count..-1]
72
+ end
73
+ if !block.empty?
74
+ block.reverse.join("\n")
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,76 @@
1
+ module MetaRuby
2
+ module DSLs
3
+ # Generic implementation to create suffixed accessors for child objects
4
+ # on a class
5
+ #
6
+ # Given an object category (let's say 'state'), this allows to properly
7
+ # implement a method-missing based accessor of the style
8
+ #
9
+ # blabla_state
10
+ #
11
+ # using a find_state method that the object should respond to
12
+ #
13
+ # @param [Object] object the object on which the find method is going to
14
+ # be called
15
+ # @param [Symbol] m the method name
16
+ # @param [Array] args the method arguments
17
+ # @param [Array<String>] suffixes the accessor suffixes that should be
18
+ # resolved. The last argument can be a hash, in which case the keys
19
+ # are used as suffixes and the values are the name of the find methods
20
+ # that should be used.
21
+ # @return [Object,nil] an object if one of the listed suffixes matches
22
+ # the method name, or nil if the method name does not match the
23
+ # requested pattern.
24
+ #
25
+ # @raise [NoMethodError] if the requested object does not exist (i.e. if
26
+ # the find method returns nil)
27
+ # @raise [ArgumentError] if the method name matches one of the suffixes,
28
+ # but arguments were given. It is raised regardless of the existence
29
+ # of the requested object
30
+ #
31
+ # @example
32
+ # class MyClass
33
+ # def find_state(name)
34
+ # states[name]
35
+ # end
36
+ # def find_transition(name)
37
+ # transitions[name]
38
+ # end
39
+ # def method_missing(m, *args, &block)
40
+ # MetaRuby::DSLs.find_through_method_missing(self, m, args,
41
+ # 'state', 'transition') || super
42
+ # end
43
+ # end
44
+ # object = MyClass.new
45
+ # object.add_state 'my'
46
+ # object.my_state # will resolve the 'my' state
47
+ #
48
+ def self.find_through_method_missing(object, m, args, *suffixes)
49
+ suffix_match = Hash.new
50
+ if suffixes.last.kind_of?(Hash)
51
+ suffix_match.merge!(suffixes.pop)
52
+ end
53
+ suffixes.each do |name|
54
+ suffix_match[name] = "find_#{name}"
55
+ end
56
+
57
+ m = m.to_s
58
+ suffix_match.each do |s, find_method_name|
59
+ if m == find_method_name
60
+ raise NoMethodError.new("#{object} has no method called #{find_method_name}", m)
61
+ elsif m =~ /(.*)_#{s}$/
62
+ name = $1
63
+ if !args.empty?
64
+ raise ArgumentError, "expected zero arguments to #{m}, got #{args.size}", caller(4)
65
+ elsif found = object.send(find_method_name, name)
66
+ return found
67
+ else
68
+ msg = "#{object} has no #{s} named #{name}"
69
+ raise NoMethodError.new(msg, m), msg, caller(4)
70
+ end
71
+ end
72
+ end
73
+ nil
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,2 @@
1
+ require 'metaruby/dsls/doc'
2
+ require 'metaruby/dsls/find_through_method_missing'