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.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/History.txt +1 -0
- data/Manifest.txt +40 -0
- data/README.md +318 -0
- data/Rakefile +39 -0
- data/lib/metaruby/class.rb +120 -0
- data/lib/metaruby/dsls/doc.rb +78 -0
- data/lib/metaruby/dsls/find_through_method_missing.rb +76 -0
- data/lib/metaruby/dsls.rb +2 -0
- data/lib/metaruby/gui/exception_view.rb +124 -0
- data/lib/metaruby/gui/html/button.rb +65 -0
- data/lib/metaruby/gui/html/collection.rb +103 -0
- data/lib/metaruby/gui/html/exception_view.css +8 -0
- data/lib/metaruby/gui/html/jquery.min.js +154 -0
- data/lib/metaruby/gui/html/jquery.selectfilter.js +65 -0
- data/lib/metaruby/gui/html/jquery.tagcloud.min.js +8 -0
- data/lib/metaruby/gui/html/jquery.tinysort.min.js +8 -0
- data/lib/metaruby/gui/html/list.rhtml +24 -0
- data/lib/metaruby/gui/html/page.css +55 -0
- data/lib/metaruby/gui/html/page.rb +283 -0
- data/lib/metaruby/gui/html/page.rhtml +13 -0
- data/lib/metaruby/gui/html/page_body.rhtml +17 -0
- data/lib/metaruby/gui/html/rock-website.css +694 -0
- data/lib/metaruby/gui/html.rb +16 -0
- data/lib/metaruby/gui/model_browser.rb +262 -0
- data/lib/metaruby/gui/model_selector.rb +266 -0
- data/lib/metaruby/gui/rendering_manager.rb +112 -0
- data/lib/metaruby/gui/ruby_constants_item_model.rb +253 -0
- data/lib/metaruby/gui.rb +9 -0
- data/lib/metaruby/inherited_attribute.rb +482 -0
- data/lib/metaruby/module.rb +158 -0
- data/lib/metaruby/registration.rb +157 -0
- data/lib/metaruby/test.rb +79 -0
- data/lib/metaruby.rb +17 -0
- data/manifest.xml +12 -0
- data/test/suite.rb +15 -0
- data/test/test_attributes.rb +323 -0
- data/test/test_class.rb +68 -0
- data/test/test_dsls.rb +49 -0
- data/test/test_module.rb +105 -0
- data/test/test_registration.rb +182 -0
- 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
|