iotaz 0.1.0

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.
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ #--
4
+ # Copyright � Peter Wood, 2005
5
+ #
6
+ # The contents of this file are subject to the Mozilla Public License Version
7
+ # 1.1 (the "License"); you may not use this file except in compliance with the
8
+ # License. You may obtain a copy of the License at
9
+ #
10
+ # http://www.mozilla.org/MPL/
11
+ #
12
+ # Software distributed under the License is distributed on an "AS IS" basis,
13
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
14
+ # the specificlanguage governing rights and limitations under the License.
15
+ #
16
+ # The Original Code is the FireRuby extension for the Ruby language.
17
+ #
18
+ # The Initial Developer of the Original Code is Peter Wood. All Rights
19
+ # Reserved.
20
+ #++
21
+ #
22
+
23
+ module Iotaz
24
+ #
25
+ # This class represents the base exception class used throughout the Iotaz
26
+ # library code.
27
+ #
28
+ class IotazError < StandardError
29
+ #
30
+ # This is the constructor for the IotazError class.
31
+ #
32
+ # ==== Parameters
33
+ # message:: The base error message associated with the exception.
34
+ # context:: An array of any remaining parameters passed to the class
35
+ # initializer. These will be treated as context details that
36
+ # will be used to populate the message generated.
37
+ #
38
+ def initialize(message, *context)
39
+ @message = message
40
+ @context = context
41
+ end
42
+
43
+
44
+ #
45
+ # This method creates a copy of the context details for an IotazError
46
+ # object.
47
+ #
48
+ def context
49
+ [].concat(@context)
50
+ end
51
+
52
+
53
+ #
54
+ # This method generates the error message associated with an IotazError
55
+ # object.
56
+ #
57
+ def message
58
+ populate(@message)
59
+ end
60
+
61
+
62
+ #
63
+ # This method populates an input string with the context details for a
64
+ # IotazError object. This population is achieved by the substitution of
65
+ # tokens within the input string. Tokens take the form of {n}, where
66
+ # n is the offset from the first context detail of the actual context
67
+ # detail to be substituted (i.e. 0 is the first context detail).
68
+ #
69
+ # ==== Parameters
70
+ # text:: The input string that will be populated.
71
+ #
72
+ def populate(text)
73
+ message = text
74
+ @context.each_index do |index|
75
+ expr = Regexp.new("\\{#{index}\\}")
76
+ message.gsub!(expr, @context[index].to_s)
77
+ end
78
+ message
79
+ end
80
+ end # End of the IotazError class.
81
+ end # End of the Iotaz module.
@@ -0,0 +1,646 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ #--
4
+ # Copyright � Peter Wood, 2005
5
+ #
6
+ # The contents of this file are subject to the Mozilla Public License Version
7
+ # 1.1 (the "License"); you may not use this file except in compliance with the
8
+ # License. You may obtain a copy of the License at
9
+ #
10
+ # http://www.mozilla.org/MPL/
11
+ #
12
+ # Software distributed under the License is distributed on an "AS IS" basis,
13
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
14
+ # the specificlanguage governing rights and limitations under the License.
15
+ #
16
+ # The Original Code is the FireRuby extension for the Ruby language.
17
+ #
18
+ # The Initial Developer of the Original Code is Peter Wood. All Rights
19
+ # Reserved.
20
+ #++
21
+ #
22
+
23
+ require 'iotaz/IotazError'
24
+ require 'stringio'
25
+
26
+ module Iotaz
27
+ #
28
+ # This class represents the definition of the meta-data for a class attribute
29
+ # and is used by the IotazMetaData class.
30
+ #
31
+ class Attribute
32
+ #
33
+ # This is the construct for thte Attribute class.
34
+ #
35
+ # ==== Parameters
36
+ # name:: The name of the class attribute.
37
+ # column:: The name of the database column that the attribute will be
38
+ # stored in. This defaults to nil to indicate that the database
39
+ # column name mirrors the attribute name.
40
+ # type:: An indicator of the type that is expected for the column
41
+ # value. This defaults to nil to indicate that the type will
42
+ # be deduced automatically.
43
+ #
44
+ def initialize(name, column=nil, type=nil)
45
+ @name = name
46
+ @column = column == nil ? name : column
47
+ @type = type
48
+ @accessor = name
49
+ @mutator = "#{name}="
50
+ end
51
+
52
+
53
+ #
54
+ # Attribute accessor to indicate whether the attribute is a generated
55
+ # value. Always returns false.
56
+ #
57
+ def is_generated?
58
+ false
59
+ end
60
+
61
+
62
+ #
63
+ # This method invokes the accessor for an attribute against a specified
64
+ # object, returning the result.
65
+ #
66
+ # ==== Parameters
67
+ # object:: The object to invoked the accessor for.
68
+ #
69
+ def get(object)
70
+ object.__send__("#{@accessor}".intern)
71
+ end
72
+
73
+
74
+ #
75
+ # This method invokes the mutator for an attribute against a specified
76
+ # object.
77
+ #
78
+ # ==== Parameters
79
+ # object:: The object to invoked the muator for.
80
+ # value:: The value to be passed as a parameter to the mutator.
81
+ #
82
+ def set(object, value)
83
+ object.__send__("#{@mutator}".intern, value)
84
+ end
85
+
86
+
87
+ #
88
+ # This method provides a textual description for an Attribute object.
89
+ #
90
+ # ==== Parameters
91
+ # indent:: The number of spaces to prefix to the lines of the string
92
+ # generated by the method. Defaults to zero.
93
+ #
94
+ def to_s(indent=0)
95
+ prefix = indent > 0 ? ' ' * indent : ''
96
+ text = StringIO.new
97
+ text << prefix << "Attribute:\n" << prefix << " Name: #{@name}, "
98
+ text << "Column: #{@column}, Type: #{@type}\n" << prefix << ' '
99
+ text << "Accessor: #{@accessor}, Mutator: #{@mutator}"
100
+ text.string
101
+ end
102
+
103
+
104
+ #
105
+ # This method overloads the equivalence test operator for the Attribute
106
+ # class.
107
+ #
108
+ # ==== Parameters
109
+ # object:: A reference to the object to be compared with.
110
+ #
111
+ def ==(object)
112
+ result = false
113
+ if object.kind_of?(Attribute)
114
+ result = (@name == object.name and
115
+ @column == object.column and
116
+ @type == object.type and
117
+ @accessor == object.accessor and
118
+ @mutator == object.mutator)
119
+ end
120
+ result
121
+ end
122
+
123
+
124
+ # Class attribute accessor.
125
+ attr_reader :name, :column, :type, :accessor, :mutator
126
+
127
+ # Class attribute mutator.
128
+ attr_writer :name, :column, :type, :accessor, :mutator
129
+ end # End of the Attribute class.
130
+
131
+
132
+ #
133
+ # This class represents a value that is automatically generated by the
134
+ # library as opposed to actually being set within the object.
135
+ #
136
+ class GeneratedAttribute < Attribute
137
+ #
138
+ # This is the constructor for the the GeneratedAttribute class.
139
+ #
140
+ # ==== Parameters
141
+ # name:: The name of the class attribute.
142
+ # type:: An indicator of the type that is expected for the column
143
+ # value. This should be one of 'SEQUENCE', 'DATE', 'TIME' or
144
+ # 'TIMESTAMP'.
145
+ # events:: This parameter indicates what events will cause the value to
146
+ # be generated. This should be one of 'INSERT', 'UPDATE' or
147
+ # 'INSERT,UPDATE'.
148
+ # source:: A type related value that provides additional information
149
+ # for use in generating the value. For sequences this should be
150
+ # the name of the sequence. For dates this should be one of
151
+ # 'YESTERDAY', 'TODAY' or 'TOMORROW'. For time and timestamp
152
+ # values 'NOW' is the only supported value.
153
+ # column:: The name of the database column that the attribute will be
154
+ # stored in. This defaults to nil to indicate that the database
155
+ # column name mirrors the attribute name.
156
+ #
157
+ # ==== Exceptions
158
+ # IotazError:: Generated whenever an invalid type, events or source value
159
+ # is specified.
160
+ #
161
+ def initialize(name, type, events, source, column=nil)
162
+ super(name, column, type.strip.upcase)
163
+ @events = events.gsub(/\s+/, '').upcase
164
+ @source = source
165
+
166
+ # Validate the parameters.
167
+ types = ['SEQUENCE', 'TIME', 'DATE', 'TIMESTAMP']
168
+ if types.include?(type.strip.upcase) == false
169
+ raise IotazError.new("'{0}' is not a valid type for a generated "\
170
+ "attribute. Acceptable values are {1}.",
171
+ type, types.join(",'"))
172
+ end
173
+
174
+ occurs = ['INSERT', 'UPDATE', 'INSERT,UPDATE', 'UPDATE,INSERT']
175
+ if occurs.include?(@events) == false
176
+ raise IotazError.new("'{0}' is not a valid event set for the "\
177
+ "{1} generated attribute. Valid settings "\
178
+ "are {1}.", events, name,
179
+ occurs[1,3].join(', '))
180
+ end
181
+
182
+ if type == 'SEQUENCE'
183
+ if source == nil or source.length == 0
184
+ raise IotazError.new("Sequence name not specified for the {0} "\
185
+ "generated attribute.", name)
186
+ end
187
+ elsif type == 'DATE'
188
+ values = ['YESTERDAY', 'TODAY', 'TOMORROW']
189
+ if values.include?(source.upcase) == false
190
+ raise IotazError.new("Invalid generation source specified for "\
191
+ "the {0} generated attribute. Valid "\
192
+ "settings are {1}.", name, values.join(', '))
193
+ end
194
+ @source = @source.upcase
195
+ else
196
+ if source.upcase != "NOW"
197
+ raise IotazError.new("Invalid generation source specified for "\
198
+ "the {0} generated attribute. Valid setting "\
199
+ "is {1}.", name, 'NOW')
200
+ end
201
+ end
202
+ end
203
+
204
+
205
+ #
206
+ # This method is the mutator for the events attribute, policing the values
207
+ # specified to the object.
208
+ #
209
+ # ==== Parameters
210
+ # setting:: A string containing the new events setting for the object.
211
+ #
212
+ # ==== Exceptions
213
+ # IotazError:: Generated whenever an invalid events setting is specified.
214
+ #
215
+ def events=(setting)
216
+ value = setting.gsub(/\s+/, '').upcase
217
+ occurs = ['INSERT', 'UPDATE', 'INSERT,UPDATE', 'UPDATE,INSERT']
218
+ if occurs.include?(value) == false
219
+ raise IotazError.new("'{0}' is not a valid event set for the "\
220
+ "{1} generated attribute. Valid settings "\
221
+ "are {1}.", setting, name,
222
+ occurs[1,3].join(', '))
223
+ end
224
+ @events = value
225
+ end
226
+
227
+
228
+ #
229
+ # This method is the mutator for the source attribute, policing the values
230
+ # specified to the object.
231
+ #
232
+ # ==== Parameters
233
+ # source:: A string containing the objects new source setting.
234
+ #
235
+ # ==== Exceptions
236
+ # IotazError:: Generated whenever the source specified is not valid for
237
+ # the type.
238
+ #
239
+ def source=(source)
240
+ if type == 'SEQUENCE'
241
+ if source == nil or source.length == 0
242
+ raise IotazError.new("Sequence name not specified for the {0} "\
243
+ "generated attribute.", name)
244
+ end
245
+ elsif type == 'DATE'
246
+ values = ['YESTERDAY', 'TODAY', 'TOMORROW']
247
+ if values.include?(source.upcase) == false
248
+ raise IotazError.new("Invalid generation source specified for "\
249
+ "the {0} generated attribute. Valid "\
250
+ "settings are {1}.", name, values.join(', '))
251
+ end
252
+ else
253
+ if source.upcase != "NOW"
254
+ raise IotazError.new("Invalid generation source specified for "\
255
+ "the {0} generated attribute. Valid setting "\
256
+ "is {1}.", name, 'NOW')
257
+ end
258
+ end
259
+ @source = source
260
+ end
261
+
262
+
263
+ #
264
+ # This method is the mutator for the type attribute, policing the values
265
+ # specified to the object.
266
+ #
267
+ # ==== Parameters
268
+ # setting:: A string containing the new type setting for the object.
269
+ #
270
+ # ==== Exceptions
271
+ # IotazError:: Generated whenever an invalid type setting is specified.
272
+ #
273
+ def type=(setting)
274
+ value = setting.strip.upcase
275
+ types = ['SEQUENCE', 'TIME', 'DATE', 'TIMESTAMP']
276
+ if types.include?(type.upcase) == false
277
+ raise IotazError.new("'{0}' is not a valid type for a generated "\
278
+ "attribute. Acceptable values are {1}.",
279
+ type, types.join(",'"))
280
+ end
281
+ super(value)
282
+ end
283
+
284
+
285
+ #
286
+ # Attribute accessor to indicate whether the attribute is a generated
287
+ # value. Always returns true.
288
+ #
289
+ def is_generated?
290
+ true
291
+ end
292
+
293
+
294
+ #
295
+ # This method overloads the equivalence test operator for the
296
+ # GeneratedAttribute class.
297
+ #
298
+ # ==== Parameters
299
+ # object:: A reference to the object to be compared with.
300
+ #
301
+ def ==(object)
302
+ result = false
303
+ if object.instance_of?(GeneratedAttribute)
304
+ result = super(object)
305
+ if result
306
+ result = (@events == object.events and @source == object.source)
307
+ end
308
+ end
309
+ result
310
+ end
311
+
312
+
313
+ #
314
+ # This method generates a textual description for a GeneratedAttribute
315
+ # object.
316
+ #
317
+ # ==== Parameters
318
+ # indent:: The number of spaces to prefix to the lines of the string
319
+ # generated by the method. Defaults to zero.
320
+ #
321
+ def to_s(indent=0)
322
+ prefix = indent > 0 ? ' ' * indent : ''
323
+ text = StringIO.new
324
+ text << super(indent) << "\n" << prefix << " Events: #{@events}, "
325
+ text << "Source: #{@source}"
326
+ text.string.gsub(/Attribute:/, 'Generated Attribute:')
327
+ end
328
+
329
+
330
+ # Class attribute accessor.
331
+ attr_reader :events, :source
332
+ end # End of the GeneratedAttribute class.
333
+
334
+
335
+ #
336
+ # This class represents the meta-data for a class that can be used in
337
+ # persisting instances of the class. Basically this class hold the details
338
+ # of what is to be stored, where it is to be stored, how the specified
339
+ # values can be accessed or updated and whether the value is automatically
340
+ # generated.
341
+ #
342
+ class IotazMetaData
343
+ # Includes.
344
+ include Enumerable
345
+
346
+
347
+ #
348
+ # This is the constructor for the IotazMetaData class.
349
+ #
350
+ # ==== Parameters
351
+ # klass:: Eiher class that the meta-data will relate to or a string
352
+ # containing the class name.
353
+ # table:: The name of the database table that the class data will be
354
+ # fed into. This defaults to nil to indicate that the table
355
+ # name is the same as the class name.
356
+ #
357
+ def initialize(klass, table=nil)
358
+ terse = IotazMetaData.get_class_name(klass)
359
+ @klass = klass.class == Class ? klass : Kernel.const_get(klass)
360
+ @name = klass.instance_of?(String) ? klass : klass.name
361
+ @table = table == nil ? terse : table
362
+ @attributes = Hash.new
363
+ @keys = Array.new
364
+ end
365
+
366
+
367
+ #
368
+ # This method adds an attribute to the list maintained by an instance of
369
+ # the IotazMetaData class.
370
+ #
371
+ # ==== Parameters
372
+ # attribute:: A reference to an Attribute object containing the details
373
+ # of the new attribute.
374
+ # key:: A boolean flag to indicate whether the field is part of
375
+ # the key for the record. This defaults to false.
376
+ #
377
+ # ==== Exceptions
378
+ # IotazError:: Generated if the attribute name clashes with an existing
379
+ # meta-data attribute or the Attribute object specifies a
380
+ # column that has already been specified by another attribute.
381
+ #
382
+ def add_attribute(attribute, key=false)
383
+ if @attributes.key?(attribute.name)
384
+ raise IotazError.new("The meta-data for the {0} class already "\
385
+ "possesses an attribute called {1}.",
386
+ @name, attribute.name)
387
+ end
388
+
389
+ match = @attributes.find {|entry| entry[1].column == attribute.column}
390
+ if match != nil
391
+ raise IotazError.new("The {0} meta-data attribute of the {1} class "\
392
+ "is specified for the {2} database column. The "\
393
+ "{3} attribute also specifies this column.",
394
+ attribute.name, @name, attribute.column,
395
+ match.name)
396
+ end
397
+ @attributes[attribute.name] = attribute
398
+ @keys.push(attribute.name) if key
399
+ end
400
+
401
+
402
+ #
403
+ # This method fetches an attribute definition from a IotazMetaData class
404
+ # instance. If the requested attribute does not exist then the method
405
+ # returns nil.
406
+ #
407
+ # ==== Parameters
408
+ # name:: The name of the attribute to be fetched.
409
+ #
410
+ def get_attribute(name)
411
+ @attributes[name]
412
+ end
413
+
414
+
415
+ #
416
+ # This method fetches an attribute from the MetaData object based on the
417
+ # attribute column name. The column name comparison is case insensitive.
418
+ #
419
+ # ==== Parameters
420
+ # column:: The name of the column to fetch the attribute for.
421
+ #
422
+ def get_attribute_for_column(column)
423
+ @attributes.values.find do |attribute|
424
+ attribute.column.upcase == column.upcase
425
+ end
426
+ end
427
+
428
+
429
+ #
430
+ # This method removes an attribute definition from a IotazMetaData class
431
+ # instance. If the specified attribute does not exist then the method
432
+ # does nothing.
433
+ #
434
+ # ==== Parameters
435
+ # name:: The name of the attribute to be removed.
436
+ #
437
+ def delete_attribute(name)
438
+ @attributes.delete(name) if @attributes.key?(name)
439
+ @keys.delete_if {|entry| entry == name}
440
+ end
441
+
442
+
443
+ #
444
+ # This method fetches an array containing the value for the key attributes
445
+ # for a given object.
446
+ #
447
+ # ==== Parameters
448
+ # object:: A reference to the object to fetch the key values for.
449
+ #
450
+ def get_key_values(object)
451
+ values = []
452
+ @keys.each do |key|
453
+ values.push(@attributes[key].get(object))
454
+ end
455
+ values
456
+ end
457
+
458
+
459
+ #
460
+ # This method provides for iteration over the attribute contents of a
461
+ # IotazMetaData object.
462
+ #
463
+ def each
464
+ result = nil
465
+ if block_given?
466
+ @attributes.each do |name, attribute|
467
+ result = yield attribute
468
+ end
469
+ end
470
+ result
471
+ end
472
+
473
+
474
+ #
475
+ # This method is used to determine whether a specified attribute exists
476
+ # within a IotazMetaData object.
477
+ #
478
+ # ==== Parameters
479
+ # name:: The name of the attribute to check for.
480
+ #
481
+ def attribute_exists?(name)
482
+ @attributes.key?(name)
483
+ end
484
+
485
+
486
+ #
487
+ # This method takes a QueryRow record and transforms it into an instance
488
+ # of the class associated with the IotazMetaData instance.
489
+ #
490
+ # ==== Parameters
491
+ # row:: A reference to a QueryRow object containing the data that will
492
+ # be used in creating the object.
493
+ #
494
+ # ==== Exceptions
495
+ # IotazMetaData:: Generated whenever the row data doesn't contain all
496
+ # the required object fields.
497
+ #
498
+ def to_object(row)
499
+ populate(@klass.allocate, row)
500
+ end
501
+
502
+
503
+ #
504
+ # This method populates an object with a specified set of values.
505
+ #
506
+ # ==== Parameters
507
+ # object:: A reference to the object to be populated.
508
+ # data:: A hash containing a mapping between attribute column names and
509
+ # the values for the attributes.
510
+ #
511
+ def populate(object, data)
512
+ data.each do |name, value|
513
+ get_attribute_for_column(name).set(object, value)
514
+ end
515
+ object
516
+ end
517
+
518
+
519
+ #
520
+ # This method retrieves a textual description for a IotazMetaData object.
521
+ #
522
+ # ==== Parameters
523
+ # indent:: The number of spaces to prefix to the lines generated by the
524
+ # method. Defaults to zero.
525
+ #
526
+ def to_s(indent=0)
527
+ prefix = indent > 0 ? ' ' * indent : ''
528
+ text = StringIO.new
529
+ text << prefix << "Iotaz Meta Data:\n" << prefix << " Class: "
530
+ text << "#{@name}, Table: #{@table}, Keys: #{@keys.join(', ')}"
531
+ @attributes.each do |name, attribute|
532
+ text << "\n" << attribute.to_s(indent + 3)
533
+ end
534
+ text.string
535
+ end
536
+
537
+
538
+ #
539
+ # This method is used to determine the table name for the meta-data.
540
+ #
541
+ # ==== Parameters
542
+ # klass:: A reference to the class that the meta-data will represent.
543
+ #
544
+ def IotazMetaData.get_class_name(klass)
545
+ name = klass.name
546
+ index = klass.name.rindex('::')
547
+ if index != nil
548
+ index = index + 2
549
+ name = name[index, name.length - index]
550
+ end
551
+ name
552
+ end
553
+
554
+
555
+ #
556
+ # This method takes a class and scans it to automatically generate a
557
+ # meta-data profile for the class. The scanning follows some simple
558
+ # rules
559
+ # - An attribute that can be persisted is considered to exist within
560
+ # the class if methods called {name}/is_{name}/is_{name}? and {name}=
561
+ # exist within the class (i.e. an accessor and mutator can be located
562
+ # for the attribute).
563
+ # - If the class possesses an attribute called id or {lower case class
564
+ # name}_id then this will be considered a generated attribute based on
565
+ # a sequence. The name of the sequence to be used will be the attribute
566
+ # name, converted to upper case, suffixed with '_ID_SQ'. The attribute
567
+ # will also be set as the key for the meta-data object.
568
+ # - If the class possesses attributes called created or updated then
569
+ # these will be considered generated values. The created attribute
570
+ # will be assigned a value for insertion and never altered. The
571
+ # update attribute will be assigned values for updates.
572
+ #
573
+ # ==== Parameters
574
+ # klass:: A reference to the class to be scanned.
575
+ #
576
+ def IotazMetaData.scan(klass)
577
+ terse = IotazMetaData.get_class_name(klass)
578
+
579
+ # Create a list of all class methods.
580
+ all = klass.public_instance_methods
581
+ all.concat(klass.protected_instance_methods)
582
+ all.concat(klass.private_instance_methods)
583
+
584
+ # Locate mutators.
585
+ mutators = all.collect do |entry|
586
+ if ['==', '==='].include?(entry) == false and entry[-1,1] == "="
587
+ entry
588
+ else
589
+ nil
590
+ end
591
+ end
592
+ mutators.compact!
593
+
594
+ # Compile a list of attribute names, accessors and mutators.
595
+ methods = Array.new
596
+ mutators.each do |entry|
597
+ name = entry.chop
598
+ if all.include?(name)
599
+ methods.push([name, name, entry])
600
+ elsif all.include("is_#{name}")
601
+ methods.push([name, "is_#{name}", entry])
602
+ elsif all.include("is_#{name}?")
603
+ methods.push([name, "is_#{name}?", entry])
604
+ end
605
+ end
606
+
607
+ result = IotazMetaData.new(klass)
608
+ methods.each do |entry|
609
+ attribute = nil
610
+ key = false
611
+ if entry[0] == 'id' or entry == "#{klass.name.downcase}_id"
612
+ attribute = GeneratedAttribute.new(entry[0], 'SEQUENCE',
613
+ 'INSERT',
614
+ "#{terse}_ID_SQ")
615
+
616
+ attribute.accessor = entry[1]
617
+ attribute.mutator = entry[2]
618
+ key = true
619
+ elsif entry[0] == 'created'
620
+ attribute = GeneratedAttribute.new(entry[0], 'TIMESTAMP',
621
+ 'INSERT', 'NOW')
622
+ attribute.accessor = entry[1]
623
+ attribute.mutator = entry[2]
624
+ elsif entry[0] == 'updated'
625
+ attribute = GeneratedAttribute.new(entry[0], 'TIMESTAMP',
626
+ 'UPDATE', 'NOW')
627
+ attribute.accessor = entry[1]
628
+ attribute.mutator = entry[2]
629
+ else
630
+ attribute = Attribute.new(entry[0])
631
+ attribute.accessor = entry[1]
632
+ attribute.mutator = entry[2]
633
+ end
634
+ result.add_attribute(attribute, key)
635
+ end
636
+ result
637
+ end
638
+
639
+
640
+ # Class attribute accessor.
641
+ attr_reader :klass, :name, :table, :keys
642
+
643
+ # Class attribute mutator.
644
+ attr_writer :table
645
+ end # End of the IotazMetaData class.
646
+ end # End of the Iotaz module.