metaruby 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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'