seanohalpin / doodle

Doodle is a Ruby library and gem for simplifying the definition of Ruby classes by making attributes and their properties more declarative

This URL has Read+Write access

doodle / notes.org
100644 486 lines (364 sloc) 16.207 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
#+TAGS: test(t) benchmark(b) doc(d) cov(c) feature(f)
= Organize these notes and docs
== Categorize
=== public interface
==== features
===== existing features
====== bugs
===== planned features
===== experimental features
====== datatypes
=== implementation
=== rationale
=== other?
= Notes on doodle
See also [[file:~/doodle-bugs.mm]]
== Initialization
 
=== Notes
Something I've just realised (which should have been obvious of
course) is that definitions made in the singleton scope cannot use any
of the features enabled during initialization (for example, validation
& defaults).
 
I've put in a class_init method which does a basic initializaton after
calling the block - but don't want to repeat initialize_from_hash -
(but borks on arg_order if I call that)
 
Need to address this in a more systematic way
 
=== DONE Initial values :init => value/proc {} ?
 
   Do I need an initialize with default method? i.e. set an initial
value - a different concept to the default value? i.e. set only once
if no other value supplied.
 
This came up as I was using generated ids - I had to do this:
 
  symbol :identifier
 
  def initialize(*a, &b)
    self.identifier = "#{self.class}##{self.class.new_id}"
    super
  end
 
See [[file:~/interactive-fiction/world-model/world-model.rb::symbol%20identifier][file:~/interactive-fiction/world-model/world-model.rb::symbol identifier]]
 
where I think I'd rather do this:
 
  symbol :identifier, :init => proc { "#{self.class}##{self.class.new_id}" }
 
Note, this is not the same as the :default (which only gets 'realised'
when accessed). Perhaps I need to change the way :default works. Also
note, this has the same problem as :default in singletons - it won't
happen because there is no :initialize call (except that I can
interpret it on definition in singleton classes - but then I need to
know that I'm being executed in context of singleton class - that one
again... hmmm)
 
=== DONE Positional args :test:
 
Positional args should follow order in which attributes have been
defined. Doesn't seem to be doing that at the moment. So I need tests
 
Added =arg_order= to specify order of args in =initialize()=
 
Fixed [[file:lib/doodle.rb::def%20parents][parents]] & [[file:lib/doodle.rb::def%20collect_inherited%20message][collect_inherited]] to return inherited classes in
right order
 
=== TODO Fix parents and collect_inherited
Fix [[file:lib/doodle.rb::def%20parents][parents]] & [[file:lib/doodle.rb::def%20collect_inherited%20message][collect_inherited]] - they are a mess - need to tidy
up/refactor/rethink/clarify/etc.
=== TODO Optional args :test:
What to do about optional arguments?
 
==== reject
raise error if unrecognised arguments in init
 
==== accept
stash away somewhere so can be retrieved later
- this is what is currently happening - creates instance vars for unrecognised values
 
==== ignore
quietly discard - this is probably the least appealing option - don't
want to lose info without raising alert (e.g. could be misspelling)
 
=== TODO Inherited attributes :test:
 
Need better tests of inherited attributes - last two problems were to
do with this
 
=== TODO Benchmarking :benchmark:
 
Profiling and benchmarking
- how much does this cost?
- where can I improve it?
 
Added profile task to rake:
  $ rake profile
 
=== TODO Document how it works :doc:
 
In particular, the way inheritance works (mainly for my own benefit :).
 
=== TODO Get 100% coverage :test:cov:
 
- [ ] need to test inherited validations and conversions
 
=== TODO Validation of defaults
See [[file:lib/doodle.rb::bump%20off%20validate%20for%20Attributes%20maybe%20better%20way%20of%20doing][Attribute validation in doodle.rb]]
 
- Defaults are not validated - should they be?
- Also, because defaults are set on access, validation sets them - do
  I want this? means that e.g. setting a variable after initialization
  can cause validation error (see "should allow changing start_date without
  changing end_date" in spec/validation_2.rb) because now start_date >
  end_date (rather than just updating start_date and leaving end_date
  as default derived value) - this might be quite tricky to fix
- if I don't set default on access, then array defaults can be updated
  without setting instance_variable (which means the update is lost)
- 2008-03-16: this has been addressed with :init => value option -
  defaults no longer create an instance variable
 
=== TODO Validate array members
 
- do I want to enforce kind of array members? 'typesafe' containers?
  - e.g. ensure that all members of array are Things
  - 2008-03-16: this is partly addressed with :collect interface
 
=== DONE Singleton attribute defaults
 
- because singleton objects do not have an initialize which is called,
  defaults don't work (I think this is the reason) - fix this
 
- now use class_init which will initialise from :init => values
 
=== TODO Conversions in initialization
 
I have Thing.from(other) - perhaps I could apply conversions in
initialization so I can do Thing.new(other)
- would need to be careful about infinite regression
- need to test conversions on input
 
=== TODO Factory functions
 
Some wrinkles with factory functions - not always defined properly?
Anonymous classes, etc. Need to test in modules, etc.
 
==== What is a factory function?
 
A factory function is a function that has the same name as
a class which acts just like class.new. For example:
 
  Cat(:name => 'Ren')
 
is the same as:
 
  Cat.new(:name => 'Ren')
 
The notion of a factory function is somewhat contentious [xref
ruby-talk], so while you get them automatically when deriving from
Doodle::Base, you need to explicitly ask for them by including Factory
in your base class if you use Doodle by including Doodle::Helper:
 
  class Base < Doodle::Base
  end
 
  class Dog < Base
    has :name
  end
 
  stimpy = Dog(:name => 'Stimpy')
 
but
 
  class Cat
    include Doodle::Helper
    include Doodle::Factory
    has :name
  end
 
  ren = Cat('Ren')
etc.
 
 
==== More ideas about factory functions
 
Factory functions are not imported into current namespace when you include module.
 
==== Perhaps should change name to Constructor functions
 
=== DONE Collector
 
  has :locations, :init => [], :collect => Location
 
should define a method :location like:
 
  def location(*args, &block)
    locations << Location.new(*args, &block)
  end
 
  has :locations, :init => [], :collect => {:location => Location}
 
=== TODO Nested hash initialization?
=== TODO Call initialization blocks on instantiation?
i.e. use the fact that we have deferred processing to call the typedef
blocks on object initialization rather than on definition? Would this
work? Would get around the default vs init treatment of procs maybe?
 
== what else?
 
=== purpose of Attribute class
 
- defines constraints on an object when a member of class
- not standalone - rules it has to follow as member of set
- maybe Constraint would be a better name
- whereas object_validation is about whole object validity
- attribute validity is as part of another object
 
=== initialization [3/3]
 
- [X] block init
- [X] init from hash - error handling
- [X] positional args
 
=== attribute accessors [2/2]
 
- [X] getter_setter
- [X] default does not create an instance_variable
- [X] init does create an instance_variable
 
=== defaults [4/4]
 
Unless a default is specified, an attribute is assumed to be required.
You can specify :default => nil :init => nil if that is what you want.
 
- [X] default value
-- :default => value
-- :default => proc {}
-- do default value end
-- do default do ... end end
- [X] optional if has :default or :init, required otherwise
- [X] enforce required
- [X] When handling defaults, want to be able to execute default block
      in context of instance, e.g.
 
        class DateRange < Doodle::Root
          has :start_date, :kind => Date do
            default { Date.today }
          end
          has :end_date, :kind => Date do
            default { start_date }
          end
        end
 
        dr = DateRange.new
        dr.end_date # => Date.today
 
=== validation and conversion [7/7]
 
- [X] kind
- [X] validations
- [X] conversions
- [X] from
- [X] must
- [X] validate (Attribute validation)
- [X] validate! (Object validation)
- notes
  - (would rather have 'should' instead of 'must' but that's taken by rspec :/)
  - use ancestor conversion if direct not available
=== user defined datatypes
 
These are not part of Doodle (yet) as there are issues involved with
defining your own datatype declarations that I have not yet fully
worked out. (I will probably end up using a separate block to enable
datatype directives.)
 
However, to see what can be done, see
[[file:~/scratch/doodle/trunk/examples/datatype-defs.rb]],
[[file:examples/datatypes.rb][file:~/scratch/doodle/trunk/examples/datatypes.rb]] and
[[file:examples/application-model.rb][file:~/scratch/doodle/trunk/examples/application-model.rb]]
 
== DONE Hide behind extension __doodle__
 
Rather than pollute the instance variable namespace with my variables,
I've chosen to hang all Doodle related info off another structure,
i.e. the method :__doodle__ and access everything through that
 
This gives access to a DoodleInfo object which is stored centrally
inside the DoodleInfo::DOODLES hash (keyed by object_id).
 
- 2009-07-20 23:35:57 - not any more - this was preventing garbage collection
 
== DONE Attribute has :init => value as well as default
 
this is different to default - this sets the value in initialization
and so creates an instance variable whereas default never creates an
instance variable
 
=== DONE Specs
 
= What to show
== normal instance attributes
   has :name
== class attributes
   class_init do
     has :name
   end
== singleton instance attributes
   o.singleton_class.has :name
= Address community questions
== Serialization
== Compatibility with other libraries
=== ActiveRecord
=== Sequel
=== JRuby
=== Ruby 1.9
=== Rubinius
== How to do something comparable to has_many/belongs_to
== Performance
== Use cases
=== TODO Example real-world use cases
= Thoughts about implementation
== 2009-07-19 19:29:06
- Union type - need some way to resolve when parsing, e.g. XML, to select which constructor to use
- cf. parsing
== 2008-04-07 04:19:14
- The more I have to hack up dirty workarounds, the more I feel
  frustrated with the limitations of Ruby 1.8
- I'm tempted to move to 1.9 or Rubinius, though there's no guarantee
  that what I'm trying to do would work there either
 
== 2008-04-07 Datatypes
- Right now I'm trying to find a way to apply a mixin
  temporarily. This is so I can 'hygenically' create datatype
  definitions (e.g. use string :name instead of has :name, :kind =>
  String). The best solution I've come up with so far is to create an
  anonymous class which includes the extensions I want, then copy over
  the resulting Attributes. I also need to copy the accessors and
  collectors too (not done yet).
- I want to apply methods like #string inside a block, the result of
  which applies to the enclosing class definition, without extending
  the class to have a #string method.
- One way would be to define the datatypes to generate data structures
  that represent the datatype definitions without applying them,
  i.e. just collect the parameters. This is made a little awkward by
  the fact you cannot pass blocks to blocks in 1.8 (because I wanted
  datatype definition to look something like this:
 
    define_datatype :date, :kind => Date do
      from String do |s|
        Date.parse(s)
      end
      from Array do |y,m,d|
        Date.new(y, m, d)
      end
      from Integer do |jd|
        Date.new(*Date.jd_to_civil(jd))
      end
    end
 
  i.e. so you're able to use #from and #must like in an ordinary
  Attribute definition.
 
- I would then like to apply these definitions within an encapsulated
  namespace, like this:
 
  class DateRange < Base
    doodle do
      date :start
      date :end do
        default { start }
      end
    end
  end
 
  so that the definition of #date is valid only within the doodle
  block, but applies to the class. binding_of_caller would do the
  trick, but that's not available any more.
 
  2008-04-07 07:51:53 - see examples/datatypes.rb for current approach
  using a proxy object
 
== 2008-04-12 23:46:19
- trying to discover context, e.g. class << self or singleton class,
  etc.
== 2008-04-13 01:34:28
- seems like the whole way I'm doing class and singleton attributes is
  wrong - keep hitting problems - maybe time for a complete rethink
 
 
== 2008-04-18 03:31:00
- well, using a simpler superclass-based version of parents has
  resulted in 9 specs failing, all to do with singletons in one way or
  another. They are mostly somewhat suspect anyway I think. I'll have
  to review them in detail.
 
  Perhaps now would be a good time to get my head straightened out on
  exactly what inheritance patterns make sense and were the metadata
  relating to them should be stored.
 
  Even though an attribute can be added to an instance's singleton
  class, it should be visible through its attributes collection
  (e.g. instance.attributes) in the same way that a singleton method
  appears in the methods collection. So should probably rethink the
  interfaces too, e.g.
 
  | attributes | all, including inherited |
  | attributes(false) | only attributes defined in class |
  | singleton_attributes | only singleton attributes |
  | instance_attributes | ? |
 
  There are also the public_methods, protected_methods and
  private_methods methods. Doodle does not interact well with #private
  and #protected at the moment, so I'll park those.
 
  (While I'm at it, I could probably get rid of all private methods
  and make them functions of __doodle__.)
 
  Also, #public_methods and #protected_methods could probably do with
  a clean out.
 
  Move to a more functional way of doing things - might be a bit
  tedious but would certainly remove some noise pollution.
 
  But certainly, I need to decide what I want for the singleton
  inheritance chain. Makes sense for classes to inherit methods
  defined in their superclass singleton classes (~ class methods). But
  does it make sense for there to be any inheritance chain at all for
  instance singletons? Shouldn't they simply be like instances with
  their own special behaviour? Objects of a class of one.
 
 
 
= Inheritance
 
Bar.class_attributes = Bar.singleton_attributes + class attributes (inherited along superclass singletons)
bar.class_attributes = Bar.class_attributes
bar.attributes = bar.singleton_attributes + Bar.attributes
Bar.attributes = attribute definitions
 
Bar.instance_attributes = ones that define instances
Bar.singleton_attributes = ones defined only in Bar
Bar.class_attributes = full list of class attributes
 
bar.attributes = bar.singleton_attributes + Bar.instance_attributes
bar.instance_attributes = ones held at class level
bar.singleton_attributes = singleton only
bar.class_attributes = ones defined in class
 
when I ask for self.attributes, I want the ones that apply to self
 
so for Bar, I want its class_attributes
for bar, I want the singleton_attributes + instance_attributes
 
then what happens inside singleton?
 
 
= 2008-04-18 16:25:57
Now have only a few tests failing, the ones relating to class
attribute inheritance
 
= 2008-04-22 01:15:59
Got all specs working! Changed interpretation of
klass.singleton_class.attributes to be consistent with
instance.singleton_class.attributes to return only those attributes
defined on singleton_class (so works similarly to
local_attributes). To access full list of inherited class attributes,
use klass.class_attributes
 
now make consistent throughout:
- instance_attributes
- class_attributes
- singleton_attributes
 
and same with conversions and validations
 
= 2008-05-06 03:24:41
- 0.1.4 release
  - keyed collections
  - specialized attributes
 
Of course, as soon as I release a version, I spot a bug - YAML loading
data not working where attribute has a default - fixed now, but what a
pain