lafcadio 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/lafcadio.rb CHANGED
@@ -16,7 +16,7 @@
16
16
  # http://lafcadio.rubyforge.org/tutorial.html.
17
17
 
18
18
  module Lafcadio
19
- Version = "0.7.0"
19
+ Version = "0.7.1"
20
20
 
21
21
  require 'lafcadio/dateTime'
22
22
  require 'lafcadio/depend'
data/lib/lafcadio.rb~ CHANGED
@@ -16,7 +16,7 @@
16
16
  # http://lafcadio.rubyforge.org/tutorial.html.
17
17
 
18
18
  module Lafcadio
19
- Version = "0.6.0"
19
+ Version = "0.7.0"
20
20
 
21
21
  require 'lafcadio/dateTime'
22
22
  require 'lafcadio/depend'
@@ -1,3 +1,4 @@
1
+ require 'extensions/module'
1
2
  require 'lafcadio/objectField'
2
3
  require 'lafcadio/util'
3
4
  require 'rexml/document'
@@ -359,7 +360,7 @@ module Lafcadio
359
360
  if field
360
361
  field
361
362
  else
362
- errStr = "Couldn't find field \"#{ field }\" in " +
363
+ errStr = "Couldn't find field \"#{ fieldName }\" in " +
363
364
  "#{ self } domain class"
364
365
  raise( MissingError, errStr, caller )
365
366
  end
@@ -393,8 +394,7 @@ module Lafcadio
393
394
 
394
395
  def self.method_missing( methodId, *args ) #:nodoc:
395
396
  method_name = methodId.id2name
396
- maybe_field_class_name = ( method_name.gsub( /^(.)/ ) { $&.upcase } ) +
397
- 'Field'
397
+ maybe_field_class_name = method_name.underscore_to_camel_case + 'Field'
398
398
  field_class = Lafcadio.const_get( maybe_field_class_name )
399
399
  create_field( field_class, args[0], args[1] || {} )
400
400
  end
@@ -1,3 +1,4 @@
1
+ require 'extensions/module'
1
2
  require 'lafcadio/objectField'
2
3
  require 'lafcadio/util'
3
4
  require 'rexml/document'
@@ -342,13 +343,10 @@ module Lafcadio
342
343
  end
343
344
 
344
345
  def self.get_domain_dirs #:nodoc:
345
- config = LafcadioConfig.new
346
- classPath = config['classpath']
347
- domainDirStr = config['domainDirs']
348
- if domainDirStr
349
- domainDirs = domainDirStr.split(',')
346
+ if ( domainDirStr = LafcadioConfig.new['domainDirs'] )
347
+ domainDirStr.split(',')
350
348
  else
351
- domainDirs = [ classPath + 'domain/' ]
349
+ []
352
350
  end
353
351
  end
354
352
 
@@ -362,7 +360,7 @@ module Lafcadio
362
360
  if field
363
361
  field
364
362
  else
365
- errStr = "Couldn't find field \"#{ field }\" in " +
363
+ errStr = "Couldn't find field \"#{ fieldName }\" in " +
366
364
  "#{ self } domain class"
367
365
  raise( MissingError, errStr, caller )
368
366
  end
@@ -424,7 +424,7 @@ module Lafcadio
424
424
  unless name
425
425
  linked_type.name =~ /::/
426
426
  name = $' || linked_type.name
427
- name = name.decapitalize
427
+ name = name.camel_case_to_underscore
428
428
  end
429
429
  super( domain_class, name )
430
430
  ( @linked_type, @delete_cascade ) = linked_type, delete_cascade
@@ -0,0 +1,561 @@
1
+ require 'date'
2
+ require 'lafcadio/dateTime'
3
+ require 'lafcadio/util'
4
+
5
+ module Lafcadio
6
+ # ObjectField is the abstract base class of any field for domain objects.
7
+ class ObjectField
8
+ include Comparable
9
+
10
+ attr_reader :name, :domain_class
11
+ attr_accessor :not_null, :db_field_name
12
+
13
+ def self.instantiate_from_xml( domain_class, fieldElt ) #:nodoc:
14
+ parameters = instantiation_parameters( fieldElt )
15
+ instantiate_with_parameters( domain_class, parameters )
16
+ end
17
+
18
+ def self.instantiate_with_parameters( domain_class, parameters ) #:nodoc:
19
+ instance = self.new( domain_class, parameters['name'] )
20
+ if ( db_field_name = parameters['db_field_name'] )
21
+ instance.db_field_name = db_field_name
22
+ end
23
+ instance
24
+ end
25
+
26
+ def self.instantiation_parameters( fieldElt ) #:nodoc:
27
+ parameters = {}
28
+ parameters['name'] = fieldElt.attributes['name']
29
+ parameters['db_field_name'] = fieldElt.attributes['db_field_name']
30
+ parameters
31
+ end
32
+
33
+ def self.value_type #:nodoc:
34
+ Object
35
+ end
36
+
37
+ # [domain_class] The domain class that this object field belongs to.
38
+ # [name] The name of this field.
39
+ def initialize( domain_class, name )
40
+ @domain_class = domain_class
41
+ @name = name
42
+ @db_field_name = name
43
+ @not_null = true
44
+ end
45
+
46
+ def <=>(other)
47
+ if @domain_class == other.domain_class && name == other.name
48
+ 0
49
+ else
50
+ object_id <=> other.object_id
51
+ end
52
+ end
53
+
54
+ def bind_write?; false; end #:nodoc:
55
+
56
+ def db_table_and_field_name
57
+ "#{ domain_class.table_name }.#{ db_field_name }"
58
+ end
59
+
60
+ def db_will_automatically_write #:nodoc:
61
+ false
62
+ end
63
+
64
+ # Returns the name that this field is referenced by in the MySQL table. By
65
+ # default this is the same as the name; to override it, set
66
+ # ObjectField#db_field_name.
67
+ def name_for_sql
68
+ db_field_name
69
+ end
70
+
71
+ def prev_value(pk_id) #:nodoc:
72
+ prevObject = ObjectStore.get_object_store.get( @domain_class, pk_id )
73
+ prevObject.send(name)
74
+ end
75
+
76
+ def process_before_verify(value) #:nodoc:
77
+ value = @default if value == nil
78
+ value
79
+ end
80
+
81
+ # Returns a string value suitable for committing this field's value to
82
+ # MySQL.
83
+ def value_for_sql(value)
84
+ value || 'null'
85
+ end
86
+
87
+ def verify(value, pk_id) #:nodoc:
88
+ if value.nil? && not_null
89
+ raise(
90
+ FieldValueError,
91
+ "#{ self.domain_class.name }##{ name } can not be nil.",
92
+ caller
93
+ )
94
+ end
95
+ verify_non_nil( value, pk_id ) if value
96
+ end
97
+
98
+ def verify_non_nil( value, pk_id )
99
+ value_type = self.class.value_type
100
+ unless value.class <= value_type
101
+ raise(
102
+ FieldValueError,
103
+ "#{ domain_class.name }##{ name } needs a " + value_type.name +
104
+ " value.",
105
+ caller
106
+ )
107
+ end
108
+ end
109
+
110
+ # Given the SQL value string, returns a Ruby-native value.
111
+ def value_from_sql(string)
112
+ string
113
+ end
114
+ end
115
+
116
+ # IntegerField represents an integer.
117
+ class IntegerField < ObjectField
118
+ def value_from_sql(string) #:nodoc:
119
+ value = super
120
+ value ? value.to_i : nil
121
+ end
122
+ end
123
+
124
+ # A TextField is expected to contain a string value.
125
+ class TextField < ObjectField
126
+ def value_for_sql(value) #:nodoc:
127
+ if value
128
+ value = value.gsub(/(\\?')/) { |m| m.length == 1 ? "''" : m }
129
+ value = value.gsub(/\\/) { '\\\\' }
130
+ "'#{value}'"
131
+ else
132
+ "null"
133
+ end
134
+ end
135
+ end
136
+
137
+ # BlobField stores a string value and expects to store its value in a BLOB
138
+ # field in the database.
139
+ class BlobField < ObjectField
140
+ attr_accessor :size
141
+
142
+ def self.value_type; String; end
143
+
144
+ def bind_write?; true; end #:nodoc:
145
+
146
+ def value_for_sql(value); "?"; end #:nodoc:
147
+ end
148
+
149
+ # BooleanField represents a boolean value. By default, it assumes that the
150
+ # table field represents True and False with the integers 1 and 0. There are
151
+ # two different ways to change this default.
152
+ #
153
+ # First, BooleanField includes a few enumerated defaults. Currently there are
154
+ # only
155
+ # * BooleanField::ENUMS_ONE_ZERO (the default, uses integers 1 and 0)
156
+ # * BooleanField::ENUMS_CAPITAL_YES_NO (uses characters 'Y' and 'N')
157
+ # In the XML class definition, this field would look like
158
+ # <field name="field_name" class="BooleanField"
159
+ # enum_type="ENUMS_CAPITAL_YES_NO"/>
160
+ # If you're defining a field in Ruby, simply set BooleanField#enum_type to one
161
+ # of the values.
162
+ #
163
+ # For more fine-grained specification you can pass specific values in. Use
164
+ # this format for the XML class definition:
165
+ # <field name="field_name" class="BooleanField">
166
+ # <enums>
167
+ # <enum key="true">yin</enum>
168
+ # <enum key="false">tang</enum>
169
+ # </enums>
170
+ # </field>
171
+ # If you're defining the field in Ruby, set BooleanField#enums to a hash.
172
+ # myBooleanField.enums = { true => 'yin', false => 'yang' }
173
+ #
174
+ # +enums+ takes precedence over +enum_type+.
175
+ class BooleanField < ObjectField
176
+ ENUMS_ONE_ZERO = 0
177
+ ENUMS_CAPITAL_YES_NO = 1
178
+
179
+ attr_accessor :enum_type, :enums
180
+
181
+ def initialize( domain_class, name )
182
+ super( domain_class, name )
183
+ @enum_type = ENUMS_ONE_ZERO
184
+ @enums = nil
185
+ end
186
+
187
+ def false_enum # :nodoc:
188
+ get_enums[false]
189
+ end
190
+
191
+ def get_enums( value = nil ) # :nodoc:
192
+ if @enums
193
+ @enums
194
+ elsif @enum_type == ENUMS_ONE_ZERO
195
+ if value.class == String
196
+ { true => '1', false => '0' }
197
+ else
198
+ { true => 1, false => 0 }
199
+ end
200
+ elsif @enum_type == ENUMS_CAPITAL_YES_NO
201
+ { true => 'Y', false => 'N' }
202
+ else
203
+ raise MissingError
204
+ end
205
+ end
206
+
207
+ def text_enum_type # :nodoc:
208
+ @enums ? @enums[true].class == String : @enum_type == ENUMS_CAPITAL_YES_NO
209
+ end
210
+
211
+ def true_enum( value = nil ) # :nodoc:
212
+ get_enums( value )[true]
213
+ end
214
+
215
+ def value_for_sql(value) # :nodoc:
216
+ if value
217
+ vfs = true_enum
218
+ else
219
+ vfs = false_enum
220
+ end
221
+ text_enum_type ? "'#{vfs}'" : vfs
222
+ end
223
+
224
+ def value_from_sql(value, lookupLink = true) # :nodoc:
225
+ value == true_enum( value )
226
+ end
227
+ end
228
+
229
+ # DateField represents a Date.
230
+ class DateField < ObjectField
231
+ def self.value_type # :nodoc:
232
+ Date
233
+ end
234
+
235
+ def initialize( domain_class, name = "date" )
236
+ super( domain_class, name )
237
+ end
238
+
239
+ def value_for_sql(value) # :nodoc:
240
+ value ? "'#{value.to_s}'" : 'null'
241
+ end
242
+
243
+ def value_from_sql(dbiDate, lookupLink = true) # :nodoc:
244
+ begin
245
+ dbiDate ? dbiDate.to_date : nil
246
+ rescue ArgumentError
247
+ nil
248
+ end
249
+ end
250
+ end
251
+
252
+ # DateTimeField represents a DateTime.
253
+ class DateTimeField < ObjectField
254
+ def value_for_sql(value) # :nodoc:
255
+ if value
256
+ year = value.year
257
+ month = value.mon.to_s.pad( 2, "0" )
258
+ day = value.day.to_s.pad( 2, "0" )
259
+ hour = value.hour.to_s.pad( 2, "0" )
260
+ minute = value.min.to_s.pad( 2, "0" )
261
+ second = value.sec.to_s.pad( 2, "0" )
262
+ "'#{year}-#{month}-#{day} #{hour}:#{minute}:#{second}'"
263
+ else
264
+ "null"
265
+ end
266
+ end
267
+
268
+ def value_from_sql(dbi_value, lookupLink = true) # :nodoc:
269
+ dbi_value ? dbi_value.to_time : nil
270
+ end
271
+ end
272
+
273
+ # DecimalField represents a decimal value.
274
+ class DecimalField < ObjectField
275
+ def self.instantiate_with_parameters( domain_class, parameters ) #:nodoc:
276
+ self.new( domain_class, parameters['name'] )
277
+ end
278
+
279
+ def self.value_type #:nodoc:
280
+ Numeric
281
+ end
282
+
283
+ def process_before_verify(value) #:nodoc:
284
+ value = super value
285
+ value != nil && value != '' ? value.to_f : nil
286
+ end
287
+
288
+ def value_from_sql(string, lookupLink = true) #:nodoc:
289
+ string != nil ? string.to_f : nil
290
+ end
291
+ end
292
+
293
+ # EmailField takes a text value that is expected to be formatted as a single
294
+ # valid email address.
295
+ class EmailField < TextField
296
+ # Is +address+ a valid email address?
297
+ def self.valid_address(address)
298
+ address =~ /^[^ @]+@[^ \.]+\.[^ ,]+$/
299
+ end
300
+
301
+ def initialize( domain_class, name = "email" )
302
+ super( domain_class, name )
303
+ end
304
+
305
+ def verify_non_nil(value, pk_id) #:nodoc:
306
+ super(value, pk_id)
307
+ if !EmailField.valid_address(value)
308
+ raise(
309
+ FieldValueError,
310
+ "#{ domain_class.name }##{ name } needs a valid email address.",
311
+ caller
312
+ )
313
+ end
314
+ end
315
+ end
316
+
317
+ # EnumField represents an enumerated field that can only be set to one of a
318
+ # set range of string values. To set the enumeration in the class definition
319
+ # XML, use the following format:
320
+ # <field name="flavor" class="EnumField">
321
+ # <enums>
322
+ # <enum>Vanilla</enum>
323
+ # <enum>Chocolate</enum>
324
+ # <enum>Lychee</enum>
325
+ # </enums>
326
+ # </field>
327
+ # If you're defining the field in Ruby, you can simply pass in an array of
328
+ # enums as the +enums+ argument.
329
+ #
330
+ class EnumField < TextField
331
+ def self.instantiate_with_parameters( domain_class, parameters ) #:nodoc:
332
+ self.new( domain_class, parameters['name'], parameters['enums'] )
333
+ end
334
+
335
+ def self.enum_queue_hash( fieldElt )
336
+ enumValues = []
337
+ fieldElt.elements.each( 'enums/enum' ) { |enumElt|
338
+ enumValues << enumElt.attributes['key']
339
+ enumValues << enumElt.text.to_s
340
+ }
341
+ QueueHash.new( *enumValues )
342
+ end
343
+
344
+ def self.instantiation_parameters( fieldElt ) #:nodoc:
345
+ parameters = super( fieldElt )
346
+ if fieldElt.elements['enums'][1].attributes['key']
347
+ parameters['enums'] = enum_queue_hash( fieldElt )
348
+ else
349
+ parameters['enums'] = []
350
+ fieldElt.elements.each( 'enums/enum' ) { |enumElt|
351
+ parameters['enums'] << enumElt.text.to_s
352
+ }
353
+ end
354
+ parameters
355
+ end
356
+
357
+ attr_reader :enums
358
+
359
+ # [domain_class] The domain class that this field belongs to.
360
+ # [name] The name of this domain class.
361
+ # [enums] An array of Strings representing the possible choices for
362
+ # this field.
363
+ def initialize( domain_class, name, enums )
364
+ super( domain_class, name )
365
+ if enums.class == Array
366
+ @enums = QueueHash.new_from_array enums
367
+ else
368
+ @enums = enums
369
+ end
370
+ end
371
+
372
+ def value_for_sql(value) #:nodoc:
373
+ value != '' ?(super(value)) : 'null'
374
+ end
375
+
376
+ def verify_non_nil( value, pk_id ) #:nodoc:
377
+ super
378
+ if @enums[value].nil?
379
+ key_str = '[ ' +
380
+ ( @enums.keys.map { |key| "\"#{ key }\"" } ).join(', ') + ' ]'
381
+ err_str = "#{ @domain_class.name }##{ name } needs a value that is " +
382
+ "one of #{ key_str }"
383
+ raise( FieldValueError, err_str, caller )
384
+ end
385
+ end
386
+ end
387
+
388
+ class FieldValueError < RuntimeError #:nodoc:
389
+ end
390
+
391
+ # A LinkField is used to link from one domain class to another.
392
+ class LinkField < ObjectField
393
+ def self.instantiate_with_parameters( domain_class, parameters ) #:nodoc:
394
+ instance = self.new(
395
+ domain_class, parameters['linked_type'], parameters['name'],
396
+ parameters['delete_cascade']
397
+ )
398
+ if parameters['db_field_name']
399
+ instance.db_field_name = parameters['db_field_name']
400
+ end
401
+ instance
402
+ end
403
+
404
+ def self.instantiation_parameters( fieldElt ) #:nodoc:
405
+ parameters = super( fieldElt )
406
+ linked_typeStr = fieldElt.attributes['linked_type']
407
+ linked_type = DomainObject.get_domain_class_from_string( linked_typeStr )
408
+ parameters['linked_type'] = linked_type
409
+ parameters['delete_cascade'] = fieldElt.attributes['delete_cascade'] == 'y'
410
+ parameters
411
+ end
412
+
413
+ attr_reader :linked_type
414
+ attr_accessor :delete_cascade
415
+
416
+ # [domain_class] The domain class that this field belongs to.
417
+ # [linked_type] The domain class that this field points to.
418
+ # [name] The name of this field.
419
+ # [delete_cascade] If this is true, deleting the domain object that is
420
+ # linked to will cause this domain object to be deleted
421
+ # as well.
422
+ def initialize( domain_class, linked_type, name = nil,
423
+ delete_cascade = false )
424
+ unless name
425
+ linked_type.name =~ /::/
426
+ name = $' || linked_type.name
427
+ name = name.decapitalize
428
+ end
429
+ super( domain_class, name )
430
+ ( @linked_type, @delete_cascade ) = linked_type, delete_cascade
431
+ end
432
+
433
+ def value_from_sql(string) #:nodoc:
434
+ string != nil ? DomainObjectProxy.new(@linked_type, string.to_i) : nil
435
+ end
436
+
437
+ def value_for_sql(value) #:nodoc:
438
+ if !value
439
+ "null"
440
+ elsif value.pk_id
441
+ value.pk_id
442
+ else
443
+ raise( DomainObjectInitError, "Can't commit #{name} without pk_id",
444
+ caller )
445
+ end
446
+ end
447
+
448
+ def verify_non_nil(value, pk_id) #:nodoc:
449
+ super
450
+ if @linked_type != @domain_class && pk_id
451
+ subsetLinkField = @linked_type.class_fields.find { |field|
452
+ field.class == SubsetLinkField && field.subset_field == @name
453
+ }
454
+ if subsetLinkField
455
+ verify_subset_link_field( subsetLinkField, pk_id )
456
+ end
457
+ end
458
+ end
459
+
460
+ def verify_subset_link_field( subsetLinkField, pk_id )
461
+ begin
462
+ prevObj = ObjectStore.get_object_store.get( domain_class, pk_id )
463
+ prevObjLinkedTo = prevObj.send(name)
464
+ possiblyMyObj = prevObjLinkedTo.send(subsetLinkField.name)
465
+ if possiblyMyObj && possiblyMyObj.pk_id == pk_id
466
+ cantChangeMsg = "You can't change that."
467
+ raise FieldValueError, cantChangeMsg, caller
468
+ end
469
+ rescue DomainObjectNotFoundError
470
+ # no previous value, so nothing to check for
471
+ end
472
+ end
473
+ end
474
+
475
+ # Accepts a Month as a value. This field automatically saves in MySQL as a
476
+ # date corresponding to the first day of the month.
477
+ class MonthField < DateField
478
+ def self.value_type #:nodoc:
479
+ Month
480
+ end
481
+
482
+ def value_for_sql(value) #:nodoc:
483
+ "'#{value.year}-#{value.month}-01'"
484
+ end
485
+ end
486
+
487
+ class PrimaryKeyField < IntegerField
488
+ def initialize( domain_class )
489
+ super( domain_class, 'pk_id' )
490
+ @not_null = false
491
+ end
492
+ end
493
+
494
+ # A StateField is a specialized subclass of EnumField; its possible values are
495
+ # any of the 50 states of the United States, stored as each state's two-letter
496
+ # postal code.
497
+ class StateField < EnumField
498
+ def initialize( domain_class, name = "state" )
499
+ super( domain_class, name, UsStates.states )
500
+ end
501
+ end
502
+
503
+ class SubsetLinkField < LinkField #:nodoc:
504
+ def self.instantiate_with_parameters( domain_class, parameters )
505
+ self.new( domain_class, parameters['linked_type'],
506
+ parameters['subset_field'], parameters['name'] )
507
+ end
508
+
509
+ def self.instantiation_parameters( fieldElt )
510
+ parameters = super( fieldElt )
511
+ parameters['subset_field'] = fieldElt.attributes['subset_field']
512
+ parameters
513
+ end
514
+
515
+ attr_accessor :subset_field
516
+
517
+ def initialize( domain_class, linked_type, subset_field,
518
+ name = linked_type.name.downcase )
519
+ super( domain_class, linked_type, name )
520
+ @subset_field = subset_field
521
+ end
522
+ end
523
+
524
+ # TextListField maps to any String SQL field that tries to represent a
525
+ # quick-and-dirty list with a comma-separated string. It returns an Array.
526
+ # For example, a SQL field with the value "john,bill,dave", then the Ruby
527
+ # field will have the value <tt>[ "john", "bill", "dave" ]</tt>.
528
+ class TextListField < ObjectField
529
+ def self.value_type #:nodoc:
530
+ Array
531
+ end
532
+
533
+ def value_for_sql(objectValue) #:nodoc:
534
+ if objectValue.is_a?( Array )
535
+ str = objectValue.join(',')
536
+ else
537
+ str = objectValue
538
+ end
539
+ "'" + str + "'"
540
+ end
541
+
542
+ def value_from_sql(sqlString, lookupLink = true) #:nodoc:
543
+ if sqlString
544
+ sqlString.split ','
545
+ else
546
+ []
547
+ end
548
+ end
549
+ end
550
+
551
+ class TimeStampField < DateTimeField #:nodoc:
552
+ def initialize( domain_class, name = 'timeStamp' )
553
+ super( domain_class, name )
554
+ @not_null = false
555
+ end
556
+
557
+ def db_will_automatically_write
558
+ true
559
+ end
560
+ end
561
+ end