lafcadio 0.8.3 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -352,6 +352,23 @@ module Lafcadio
352
352
  end
353
353
 
354
354
  def self.first; all.first; end
355
+
356
+ def self.get( *args )
357
+ if block_given?
358
+ query = Query.infer( self ) { |dobj| yield( dobj ) }
359
+ ObjectStore.get_object_store.get_subset( query )
360
+ elsif args.size == 1
361
+ arg = args.first
362
+ if arg.is_a? Fixnum
363
+ ObjectStore.get_object_store.get( self, *args )
364
+ else
365
+ qry = Query.new( self, nil, { :group_functions => [ :count ] } )
366
+ ObjectStore.get_object_store.query qry
367
+ end
368
+ else
369
+ ObjectStore.get_object_store.get_filtered( self.name, *args )
370
+ end
371
+ end
355
372
 
356
373
  def self.get_class_field(fieldName) #:nodoc:
357
374
  field = nil
@@ -444,44 +461,33 @@ module Lafcadio
444
461
 
445
462
  def self.method_missing( methodId, *args ) #:nodoc:
446
463
  method_name = methodId.id2name
447
- if method_name == 'get'
448
- if block_given?
449
- query = Query.infer( self ) { |dobj| yield( dobj ) }
450
- ObjectStore.get_object_store.get_subset( query )
451
- elsif args.size == 1
452
- ObjectStore.get_object_store.get( self, *args )
453
- else
454
- ObjectStore.get_object_store.get_filtered( self.name, *args )
455
- end
456
- else
457
- maybe_field_class_name = method_name.underscore_to_camel_case + 'Field'
458
- begin
459
- field_class = Lafcadio.const_get( maybe_field_class_name )
460
- create_field( field_class, *args )
461
- rescue NameError
462
- singular = method_name.singular
463
- if singular
464
- maybe_field_class_name = singular.underscore_to_camel_case + 'Field'
465
- begin
466
- field_class = Lafcadio.const_get( maybe_field_class_name )
467
- arg = args.shift
468
- until args.empty?
469
- next_arg = args.shift
470
- if next_arg.is_a? String or next_arg.is_a? Symbol
471
- create_field( field_class, arg )
472
- arg = next_arg
473
- else
474
- create_field( field_class, arg, next_arg )
475
- arg = args.shift
476
- end
464
+ maybe_field_class_name = method_name.underscore_to_camel_case + 'Field'
465
+ begin
466
+ field_class = Lafcadio.const_get( maybe_field_class_name )
467
+ create_field( field_class, *args )
468
+ rescue NameError
469
+ singular = method_name.singular
470
+ if singular
471
+ maybe_field_class_name = singular.underscore_to_camel_case + 'Field'
472
+ begin
473
+ field_class = Lafcadio.const_get( maybe_field_class_name )
474
+ arg = args.shift
475
+ until args.empty?
476
+ next_arg = args.shift
477
+ if next_arg.is_a? String or next_arg.is_a? Symbol
478
+ create_field( field_class, arg )
479
+ arg = next_arg
480
+ else
481
+ create_field( field_class, arg, next_arg )
482
+ arg = args.shift
477
483
  end
478
- create_field( field_class, arg ) unless arg.nil?
479
- rescue NameError
480
- super
481
484
  end
482
- else
485
+ create_field( field_class, arg ) unless arg.nil?
486
+ rescue NameError
483
487
  super
484
488
  end
489
+ else
490
+ super
485
491
  end
486
492
  end
487
493
  end
@@ -594,6 +600,7 @@ module Lafcadio
594
600
  def initialize(fieldHash)
595
601
  if fieldHash.respond_to? :keys
596
602
  fieldHash.keys.each { |key|
603
+ key = key.to_s
597
604
  begin
598
605
  self.class.get_field( key )
599
606
  rescue MissingError
@@ -601,11 +608,16 @@ module Lafcadio
601
608
  end
602
609
  }
603
610
  end
604
- @fieldHash = fieldHash
611
+ if fieldHash.is_a? Hash
612
+ @fieldHash = {}
613
+ fieldHash.each do |k, v| @fieldHash[k.to_s] = v; end
614
+ else
615
+ @fieldHash = fieldHash
616
+ end
605
617
  @error_messages = []
606
618
  @fields = {}
607
619
  @fields_set = []
608
- reset_original_values_hash @fieldHash
620
+ @original_values = OriginalValuesHash.new( @fieldHash )
609
621
  check_fields = LafcadioConfig.new()['checkFields']
610
622
  verify if %w( onInstantiate onAllStates ).include?( check_fields )
611
623
  end
@@ -699,10 +711,6 @@ module Lafcadio
699
711
  def post_commit_trigger
700
712
  nil
701
713
  end
702
-
703
- def reset_original_values_hash( f = @fields ) #:nodoc:
704
- @original_values = OriginalValuesHash.new( f.clone )
705
- end
706
714
 
707
715
  def set_field( field, value ) #:nodoc:
708
716
  if field.class <= DomainObjectField
@@ -1,4 +1,5 @@
1
1
  require 'lafcadio/objectStore'
2
+ require 'lafcadio/util'
2
3
 
3
4
  module Lafcadio
4
5
  class MockDbBridge #:nodoc:
@@ -42,7 +43,13 @@ module Lafcadio
42
43
  end
43
44
  }
44
45
  if ( order_by = query.order_by )
45
- objects = objects.sort_by { |dobj| dobj.send( order_by ) }
46
+ objects = objects.sort_by { |dobj|
47
+ if order_by.is_a?( Array )
48
+ order_by.map { |field_name| dobj.send( field_name ) }
49
+ else
50
+ dobj.send( order_by )
51
+ end
52
+ }
46
53
  objects.reverse! if query.order_by_order == Query::DESC
47
54
  else
48
55
  objects = objects.sort_by { |dobj| dobj.pk_id }
@@ -83,17 +90,7 @@ module Lafcadio
83
90
  end
84
91
 
85
92
  def group_query( query )
86
- if query.class == Query::Max
87
- dobjs_by_pk_id = @objects[query.domain_class]
88
- if dobjs_by_pk_id
89
- dobjs = dobjs_by_pk_id.values.sort_by { |dobj|
90
- dobj.send( query.field_name )
91
- }
92
- [ dobjs.last.send( query.field_name ) ]
93
- else
94
- [ nil ]
95
- end
96
- end
93
+ query.collect( get_objects_by_domain_class( query.domain_class ).values )
97
94
  end
98
95
 
99
96
  def set_next_pk_id( domain_class, npi )
@@ -0,0 +1,110 @@
1
+ require 'lafcadio/objectStore'
2
+ require 'lafcadio/util'
3
+
4
+ module Lafcadio
5
+ class MockDbBridge #:nodoc:
6
+ attr_reader :last_pk_id_inserted, :retrievals_by_type, :query_count
7
+
8
+ def initialize
9
+ @objects = {}
10
+ @retrievals_by_type = Hash.new 0
11
+ @query_count = Hash.new( 0 )
12
+ @next_pk_ids = {}
13
+ end
14
+
15
+ def commit(db_object)
16
+ if db_object.pk_id and !db_object.pk_id.is_a?( Integer )
17
+ raise ArgumentError
18
+ end
19
+ objects_by_domain_class = get_objects_by_domain_class(
20
+ db_object.domain_class
21
+ )
22
+ if db_object.delete
23
+ objects_by_domain_class.delete( db_object.pk_id )
24
+ else
25
+ object_pk_id = get_pk_id_before_committing( db_object )
26
+ objects_by_domain_class[object_pk_id] = db_object
27
+ end
28
+ end
29
+
30
+ def _get_all( domain_class )
31
+ @retrievals_by_type[domain_class] = @retrievals_by_type[domain_class] + 1
32
+ @objects[domain_class] ? @objects[domain_class].values : []
33
+ end
34
+
35
+ def get_collection_by_query(query)
36
+ @query_count[query.to_sql] += 1
37
+ objects = []
38
+ _get_all( query.domain_class ).each { |dbObj|
39
+ if query.condition
40
+ objects << dbObj if query.condition.object_meets(dbObj)
41
+ else
42
+ objects << dbObj
43
+ end
44
+ }
45
+ if ( order_by = query.order_by )
46
+ objects = objects.sort_by { |dobj| dobj.send( order_by ) }
47
+ objects.reverse! if query.order_by_order == Query::DESC
48
+ else
49
+ objects = objects.sort_by { |dobj| dobj.pk_id }
50
+ end
51
+ if (range = query.limit)
52
+ objects = objects[range]
53
+ end
54
+ objects
55
+ end
56
+
57
+ def get_pk_id_before_committing( db_object )
58
+ if db_object.pk_id
59
+ db_object.pk_id
60
+ else
61
+ if ( next_pk_id = @next_pk_ids[db_object.domain_class] )
62
+ @last_pk_id_inserted = next_pk_id
63
+ @next_pk_ids[db_object.domain_class] = nil
64
+ next_pk_id
65
+ else
66
+ maxpk_id = 0
67
+ pk_ids = get_objects_by_domain_class( db_object.domain_class ).keys
68
+ pk_ids.each { |pk_id|
69
+ maxpk_id = pk_id if pk_id > maxpk_id
70
+ }
71
+ @last_pk_id_inserted = maxpk_id + 1
72
+ @last_pk_id_inserted
73
+ end
74
+ end
75
+ end
76
+
77
+ def get_objects_by_domain_class( domain_class )
78
+ objects_by_domain_class = @objects[domain_class]
79
+ unless objects_by_domain_class
80
+ objects_by_domain_class = {}
81
+ @objects[domain_class] = objects_by_domain_class
82
+ end
83
+ objects_by_domain_class
84
+ end
85
+
86
+ def group_query( query )
87
+ query.collect( get_objects_by_domain_class( query.domain_class ).values )
88
+ end
89
+
90
+ def set_next_pk_id( domain_class, npi )
91
+ @next_pk_ids[ domain_class ] = npi
92
+ end
93
+ end
94
+
95
+ # Externally, the MockObjectStore looks and acts exactly like the ObjectStore,
96
+ # but stores all its data in memory. This makes it very useful for unit
97
+ # testing, and in fact LafcadioTestCase#setup creates a new instance of
98
+ # MockObjectStore for each test case.
99
+ class MockObjectStore < ObjectStore
100
+ public_class_method :new
101
+
102
+ def initialize # :nodoc:
103
+ super( MockDbBridge.new )
104
+ end
105
+
106
+ def mock? # :nodoc:
107
+ true
108
+ end
109
+ end
110
+ end
@@ -12,10 +12,10 @@ module Lafcadio
12
12
 
13
13
  def self.instantiate_from_xml( domain_class, fieldElt ) #:nodoc:
14
14
  parameters = instantiation_parameters( fieldElt )
15
- instantiate_with_parameters( domain_class, parameters )
15
+ create_with_args( domain_class, parameters )
16
16
  end
17
17
 
18
- def self.instantiate_with_parameters( domain_class, parameters ) #:nodoc:
18
+ def self.create_with_args( domain_class, parameters ) #:nodoc:
19
19
  instance = self.new( domain_class, parameters['name'] )
20
20
  if ( db_field_name = parameters['db_field_name'] )
21
21
  instance.db_field_name = db_field_name
@@ -277,7 +277,7 @@ module Lafcadio
277
277
  ( $' || linked_type.name ).camel_case_to_underscore
278
278
  end
279
279
 
280
- def self.instantiate_with_parameters( domain_class, parameters ) #:nodoc:
280
+ def self.create_with_args( domain_class, parameters ) #:nodoc:
281
281
  linked_type = parameters['linked_type']
282
282
  instance = self.new(
283
283
  domain_class, linked_type,
@@ -395,7 +395,7 @@ module Lafcadio
395
395
  # enums as the +enums+ argument.
396
396
  #
397
397
  class EnumField < StringField
398
- def self.instantiate_with_parameters( domain_class, parameters ) #:nodoc:
398
+ def self.create_with_args( domain_class, parameters ) #:nodoc:
399
399
  self.new( domain_class, parameters['name'], parameters['enums'] )
400
400
  end
401
401
 
@@ -457,7 +457,7 @@ module Lafcadio
457
457
 
458
458
  # FloatField represents a decimal value.
459
459
  class FloatField < ObjectField
460
- def self.instantiate_with_parameters( domain_class, parameters ) #:nodoc:
460
+ def self.create_with_args( domain_class, parameters ) #:nodoc:
461
461
  self.new( domain_class, parameters['name'] )
462
462
  end
463
463
 
@@ -504,7 +504,7 @@ module Lafcadio
504
504
  end
505
505
 
506
506
  class SubsetDomainObjectField < DomainObjectField #:nodoc:
507
- def self.instantiate_with_parameters( domain_class, parameters )
507
+ def self.create_with_args( domain_class, parameters )
508
508
  self.new( domain_class, parameters['linked_type'],
509
509
  parameters['subset_field'], parameters['name'] )
510
510
  end
@@ -0,0 +1,564 @@
1
+ require 'date'
2
+ require 'lafcadio/depend'
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
+ # A StringField is expected to contain a string value.
117
+ class StringField < ObjectField
118
+ def value_for_sql(value) #:nodoc:
119
+ if value
120
+ value = value.gsub(/(\\?')/) { |m| m.length == 1 ? "''" : m }
121
+ value = value.gsub(/\\/) { '\\\\' }
122
+ "'#{value}'"
123
+ else
124
+ "null"
125
+ end
126
+ end
127
+ end
128
+
129
+ # IntegerField represents an integer.
130
+ class IntegerField < ObjectField
131
+ def value_from_sql(string) #:nodoc:
132
+ value = super
133
+ value ? value.to_i : nil
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
+ # A DomainObjectField is used to link from one domain class to another.
274
+ class DomainObjectField < ObjectField
275
+ def self.auto_name( linked_type )
276
+ linked_type.name =~ /::/
277
+ ( $' || linked_type.name ).camel_case_to_underscore
278
+ end
279
+
280
+ def self.instantiate_with_parameters( domain_class, parameters ) #:nodoc:
281
+ linked_type = parameters['linked_type']
282
+ instance = self.new(
283
+ domain_class, linked_type,
284
+ parameters['name'] || auto_name( linked_type ),
285
+ parameters['delete_cascade']
286
+ )
287
+ if parameters['db_field_name']
288
+ instance.db_field_name = parameters['db_field_name']
289
+ end
290
+ instance
291
+ end
292
+
293
+ def self.instantiation_parameters( fieldElt ) #:nodoc:
294
+ parameters = super( fieldElt )
295
+ linked_typeStr = fieldElt.attributes['linked_type']
296
+ linked_type = DomainObject.get_domain_class_from_string( linked_typeStr )
297
+ parameters['linked_type'] = linked_type
298
+ parameters['delete_cascade'] = fieldElt.attributes['delete_cascade'] == 'y'
299
+ parameters
300
+ end
301
+
302
+ attr_reader :linked_type
303
+ attr_accessor :delete_cascade
304
+
305
+ # [domain_class] The domain class that this field belongs to.
306
+ # [linked_type] The domain class that this field points to.
307
+ # [name] The name of this field.
308
+ # [delete_cascade] If this is true, deleting the domain object that is
309
+ # linked to will cause this domain object to be deleted
310
+ # as well.
311
+ def initialize( domain_class, linked_type, name = nil,
312
+ delete_cascade = false )
313
+ name = self.class.auto_name( linked_type ) unless name
314
+ super( domain_class, name )
315
+ ( @linked_type, @delete_cascade ) = linked_type, delete_cascade
316
+ end
317
+
318
+ def value_from_sql(string) #:nodoc:
319
+ string != nil ? DomainObjectProxy.new(@linked_type, string.to_i) : nil
320
+ end
321
+
322
+ def value_for_sql(value) #:nodoc:
323
+ if !value
324
+ "null"
325
+ elsif value.pk_id
326
+ value.pk_id
327
+ else
328
+ raise( DomainObjectInitError, "Can't commit #{name} without pk_id",
329
+ caller )
330
+ end
331
+ end
332
+
333
+ def verify_non_nil(value, pk_id) #:nodoc:
334
+ super
335
+ if @linked_type != @domain_class && pk_id
336
+ subsetDomainObjectField = @linked_type.class_fields.find { |field|
337
+ field.class == SubsetDomainObjectField && field.subset_field == @name
338
+ }
339
+ if subsetDomainObjectField
340
+ verify_subset_link_field( subsetDomainObjectField, pk_id )
341
+ end
342
+ end
343
+ end
344
+
345
+ def verify_subset_link_field( subsetDomainObjectField, pk_id )
346
+ begin
347
+ prevObj = ObjectStore.get_object_store.get( domain_class, pk_id )
348
+ prevObjLinkedTo = prevObj.send(name)
349
+ possiblyMyObj = prevObjLinkedTo.send(subsetDomainObjectField.name)
350
+ if possiblyMyObj && possiblyMyObj.pk_id == pk_id
351
+ cantChangeMsg = "You can't change that."
352
+ raise FieldValueError, cantChangeMsg, caller
353
+ end
354
+ rescue DomainObjectNotFoundError
355
+ # no previous value, so nothing to check for
356
+ end
357
+ end
358
+ end
359
+
360
+ # EmailField takes a text value that is expected to be formatted as a single
361
+ # valid email address.
362
+ class EmailField < StringField
363
+ # Is +address+ a valid email address?
364
+ def self.valid_address(address)
365
+ address =~ /^[^ @]+@[^ \.]+\.[^ ,]+$/
366
+ end
367
+
368
+ def initialize( domain_class, name = "email" )
369
+ super( domain_class, name )
370
+ end
371
+
372
+ def verify_non_nil(value, pk_id) #:nodoc:
373
+ super(value, pk_id)
374
+ if !EmailField.valid_address(value)
375
+ raise(
376
+ FieldValueError,
377
+ "#{ domain_class.name }##{ name } needs a valid email address.",
378
+ caller
379
+ )
380
+ end
381
+ end
382
+ end
383
+
384
+ # EnumField represents an enumerated field that can only be set to one of a
385
+ # set range of string values. To set the enumeration in the class definition
386
+ # XML, use the following format:
387
+ # <field name="flavor" class="EnumField">
388
+ # <enums>
389
+ # <enum>Vanilla</enum>
390
+ # <enum>Chocolate</enum>
391
+ # <enum>Lychee</enum>
392
+ # </enums>
393
+ # </field>
394
+ # If you're defining the field in Ruby, you can simply pass in an array of
395
+ # enums as the +enums+ argument.
396
+ #
397
+ class EnumField < StringField
398
+ def self.instantiate_with_parameters( domain_class, parameters ) #:nodoc:
399
+ self.new( domain_class, parameters['name'], parameters['enums'] )
400
+ end
401
+
402
+ def self.enum_queue_hash( fieldElt )
403
+ enumValues = []
404
+ fieldElt.elements.each( 'enums/enum' ) { |enumElt|
405
+ enumValues << enumElt.attributes['key']
406
+ enumValues << enumElt.text.to_s
407
+ }
408
+ QueueHash.new( *enumValues )
409
+ end
410
+
411
+ def self.instantiation_parameters( fieldElt ) #:nodoc:
412
+ parameters = super( fieldElt )
413
+ if fieldElt.elements['enums'][1].attributes['key']
414
+ parameters['enums'] = enum_queue_hash( fieldElt )
415
+ else
416
+ parameters['enums'] = []
417
+ fieldElt.elements.each( 'enums/enum' ) { |enumElt|
418
+ parameters['enums'] << enumElt.text.to_s
419
+ }
420
+ end
421
+ parameters
422
+ end
423
+
424
+ attr_reader :enums
425
+
426
+ # [domain_class] The domain class that this field belongs to.
427
+ # [name] The name of this domain class.
428
+ # [enums] An array of Strings representing the possible choices for
429
+ # this field.
430
+ def initialize( domain_class, name, enums )
431
+ super( domain_class, name )
432
+ if enums.class == Array
433
+ @enums = QueueHash.new_from_array enums
434
+ else
435
+ @enums = enums
436
+ end
437
+ end
438
+
439
+ def value_for_sql(value) #:nodoc:
440
+ value != '' ?(super(value)) : 'null'
441
+ end
442
+
443
+ def verify_non_nil( value, pk_id ) #:nodoc:
444
+ super
445
+ if @enums[value].nil?
446
+ key_str = '[ ' +
447
+ ( @enums.keys.map { |key| "\"#{ key }\"" } ).join(', ') + ' ]'
448
+ err_str = "#{ @domain_class.name }##{ name } needs a value that is " +
449
+ "one of #{ key_str }"
450
+ raise( FieldValueError, err_str, caller )
451
+ end
452
+ end
453
+ end
454
+
455
+ class FieldValueError < RuntimeError #:nodoc:
456
+ end
457
+
458
+ # FloatField represents a decimal value.
459
+ class FloatField < ObjectField
460
+ def self.instantiate_with_parameters( domain_class, parameters ) #:nodoc:
461
+ self.new( domain_class, parameters['name'] )
462
+ end
463
+
464
+ def self.value_type #:nodoc:
465
+ Numeric
466
+ end
467
+
468
+ def process_before_verify(value) #:nodoc:
469
+ value = super value
470
+ value != nil && value != '' ? value.to_f : nil
471
+ end
472
+
473
+ def value_from_sql(string, lookupLink = true) #:nodoc:
474
+ string != nil ? string.to_f : nil
475
+ end
476
+ end
477
+
478
+ # Accepts a Month as a value. This field automatically saves in MySQL as a
479
+ # date corresponding to the first day of the month.
480
+ class MonthField < DateField
481
+ def self.value_type #:nodoc:
482
+ Month
483
+ end
484
+
485
+ def value_for_sql(value) #:nodoc:
486
+ "'#{value.year}-#{value.month}-01'"
487
+ end
488
+ end
489
+
490
+ class PrimaryKeyField < IntegerField
491
+ def initialize( domain_class )
492
+ super( domain_class, 'pk_id' )
493
+ @not_null = false
494
+ end
495
+ end
496
+
497
+ # A StateField is a specialized subclass of EnumField; its possible values are
498
+ # any of the 50 states of the United States, stored as each state's two-letter
499
+ # postal code.
500
+ class StateField < EnumField
501
+ def initialize( domain_class, name = "state" )
502
+ super( domain_class, name, USCommerce::UsStates.states )
503
+ end
504
+ end
505
+
506
+ class SubsetDomainObjectField < DomainObjectField #:nodoc:
507
+ def self.instantiate_with_parameters( domain_class, parameters )
508
+ self.new( domain_class, parameters['linked_type'],
509
+ parameters['subset_field'], parameters['name'] )
510
+ end
511
+
512
+ def self.instantiation_parameters( fieldElt )
513
+ parameters = super( fieldElt )
514
+ parameters['subset_field'] = fieldElt.attributes['subset_field']
515
+ parameters
516
+ end
517
+
518
+ attr_accessor :subset_field
519
+
520
+ def initialize( domain_class, linked_type, subset_field,
521
+ name = linked_type.name.downcase )
522
+ super( domain_class, linked_type, name )
523
+ @subset_field = subset_field
524
+ end
525
+ end
526
+
527
+ # TextListField maps to any String SQL field that tries to represent a
528
+ # quick-and-dirty list with a comma-separated string. It returns an Array.
529
+ # For example, a SQL field with the value "john,bill,dave", then the Ruby
530
+ # field will have the value <tt>[ "john", "bill", "dave" ]</tt>.
531
+ class TextListField < ObjectField
532
+ def self.value_type #:nodoc:
533
+ Array
534
+ end
535
+
536
+ def value_for_sql(objectValue) #:nodoc:
537
+ if objectValue.is_a?( Array )
538
+ str = objectValue.join(',')
539
+ else
540
+ str = objectValue
541
+ end
542
+ "'" + str + "'"
543
+ end
544
+
545
+ def value_from_sql(sqlString, lookupLink = true) #:nodoc:
546
+ if sqlString
547
+ sqlString.split ','
548
+ else
549
+ []
550
+ end
551
+ end
552
+ end
553
+
554
+ class TimeStampField < DateTimeField #:nodoc:
555
+ def initialize( domain_class, name = 'timeStamp' )
556
+ super( domain_class, name )
557
+ @not_null = false
558
+ end
559
+
560
+ def db_will_automatically_write
561
+ true
562
+ end
563
+ end
564
+ end