lafcadio 0.7.0 → 0.7.1

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.
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