iotaz 0.1.0

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