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