flex_columns 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +38 -0
  4. data/Gemfile +17 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +124 -0
  7. data/Rakefile +6 -0
  8. data/flex_columns.gemspec +72 -0
  9. data/lib/flex_columns.rb +15 -0
  10. data/lib/flex_columns/active_record/base.rb +57 -0
  11. data/lib/flex_columns/contents/column_data.rb +376 -0
  12. data/lib/flex_columns/contents/flex_column_contents_base.rb +188 -0
  13. data/lib/flex_columns/definition/field_definition.rb +316 -0
  14. data/lib/flex_columns/definition/field_set.rb +89 -0
  15. data/lib/flex_columns/definition/flex_column_contents_class.rb +327 -0
  16. data/lib/flex_columns/errors.rb +236 -0
  17. data/lib/flex_columns/has_flex_columns.rb +187 -0
  18. data/lib/flex_columns/including/include_flex_columns.rb +179 -0
  19. data/lib/flex_columns/util/dynamic_methods_module.rb +86 -0
  20. data/lib/flex_columns/util/string_utils.rb +31 -0
  21. data/lib/flex_columns/version.rb +4 -0
  22. data/spec/flex_columns/helpers/database_helper.rb +174 -0
  23. data/spec/flex_columns/helpers/exception_helpers.rb +20 -0
  24. data/spec/flex_columns/helpers/system_helpers.rb +47 -0
  25. data/spec/flex_columns/system/basic_system_spec.rb +245 -0
  26. data/spec/flex_columns/system/bulk_system_spec.rb +153 -0
  27. data/spec/flex_columns/system/compression_system_spec.rb +218 -0
  28. data/spec/flex_columns/system/custom_methods_system_spec.rb +120 -0
  29. data/spec/flex_columns/system/delegation_system_spec.rb +175 -0
  30. data/spec/flex_columns/system/dynamism_system_spec.rb +158 -0
  31. data/spec/flex_columns/system/error_handling_system_spec.rb +117 -0
  32. data/spec/flex_columns/system/including_system_spec.rb +285 -0
  33. data/spec/flex_columns/system/json_alias_system_spec.rb +171 -0
  34. data/spec/flex_columns/system/performance_system_spec.rb +218 -0
  35. data/spec/flex_columns/system/postgres_json_column_type_system_spec.rb +85 -0
  36. data/spec/flex_columns/system/types_system_spec.rb +93 -0
  37. data/spec/flex_columns/system/unknown_fields_system_spec.rb +126 -0
  38. data/spec/flex_columns/system/validations_system_spec.rb +111 -0
  39. data/spec/flex_columns/unit/active_record/base_spec.rb +32 -0
  40. data/spec/flex_columns/unit/contents/column_data_spec.rb +520 -0
  41. data/spec/flex_columns/unit/contents/flex_column_contents_base_spec.rb +253 -0
  42. data/spec/flex_columns/unit/definition/field_definition_spec.rb +617 -0
  43. data/spec/flex_columns/unit/definition/field_set_spec.rb +142 -0
  44. data/spec/flex_columns/unit/definition/flex_column_contents_class_spec.rb +733 -0
  45. data/spec/flex_columns/unit/errors_spec.rb +297 -0
  46. data/spec/flex_columns/unit/has_flex_columns_spec.rb +365 -0
  47. data/spec/flex_columns/unit/including/include_flex_columns_spec.rb +144 -0
  48. data/spec/flex_columns/unit/util/dynamic_methods_module_spec.rb +105 -0
  49. data/spec/flex_columns/unit/util/string_utils_spec.rb +23 -0
  50. metadata +286 -0
@@ -0,0 +1,188 @@
1
+ require 'active_model'
2
+ require 'flex_columns/errors'
3
+ require 'flex_columns/util/dynamic_methods_module'
4
+ require 'flex_columns/contents/column_data'
5
+ require 'flex_columns/definition/field_set'
6
+ require 'flex_columns/definition/flex_column_contents_class'
7
+
8
+ module FlexColumns
9
+ module Contents
10
+ # When you declare a flex column, we actually generate a brand-new Class for that column; instances of that flex
11
+ # column are instances of this new Class. This class acquires functionality from two places: FlexColumnContentsBase,
12
+ # which defines its instance methods, and FlexColumnContentsClass, which defines its class methods. (While
13
+ # FlexColumnContentsBase is an actual Class, FlexColumnContentsClass is a Module that FlexColumnContentsBase
14
+ # +extend+s. Both could be combined, but, simply for readability and maintainability, it was better to make them
15
+ # separate.)
16
+ #
17
+ # This Class therefore defines the methods that are available on an instance of a flex-column class -- on the
18
+ # object returned by <tt>my_user.user_attributes</tt>, for example.
19
+ class FlexColumnContentsBase
20
+ # Because of the awesome work done in Rails 3 to modularize ActiveRecord and friends, including this gives us
21
+ # validation support basically for free.
22
+ include ActiveModel::Validations
23
+
24
+ # Grab all the class methods. :)
25
+ extend FlexColumns::Definition::FlexColumnContentsClass
26
+
27
+ # Creates a new instance. +input+ is the source of data we should use: normally this is an instance of the
28
+ # enclosing model class (_e.g._, +User+), but it can also be a simple String (if you're creating an instance
29
+ # using the bulk API -- +HasFlexColumns#create_flex_objects_from+, for example) containing the stored JSON for
30
+ # this object, or +nil+ (if you're doing the same, but have no source data).
31
+ #
32
+ # The reason this class hangs onto the whole model instance, instead of just the string, is twofold:
33
+ #
34
+ # * It needs to be able to add validation errors back onto the model instance;
35
+ # * It wants to be able to pass a description of the model instance into generated exceptions and the
36
+ # ActiveSupport::Notifications calls made, so that when things go wrong or you're doing performance work, you
37
+ # can understand what row in what table contains incorrect data or data that is making things slow.
38
+ def initialize(input)
39
+ storage_string = nil
40
+
41
+ if input.kind_of?(String)
42
+ @model_instance = nil
43
+ storage_string = input
44
+ @source_string = input
45
+ elsif (! input)
46
+ @model_instance = nil
47
+ storage_string = nil
48
+ elsif input.class.equal?(self.class.model_class)
49
+ @model_instance = input
50
+ storage_string = @model_instance[self.class.column_name]
51
+ else
52
+ raise ArgumentError, %{You can create a #{self.class.name} from a String, nil, or #{self.class.model_class.name} (#{self.class.model_class.object_id}),
53
+ not #{input.inspect} (#{input.object_id}).}
54
+ end
55
+
56
+ # Creates an instance of FlexColumns::Contents::ColumnData, which is the thing that does most of the actual
57
+ # work with the underlying data for us.
58
+ @column_data = self.class._flex_columns_create_column_data(storage_string, self)
59
+ end
60
+
61
+ # Returns a String, appropriate for human consumption, that describes the model instance we're created from (or
62
+ # raw String, if that's the case). This is used solely by the errors in FlexColumns::Errors, and is used to give
63
+ # good, actionable diagnostic messages when something goes wrong.
64
+ def describe_flex_column_data_source
65
+ if model_instance
66
+ out = self.class.model_class.name.dup
67
+ out << " ID #{model_instance.id}" if model_instance.id
68
+ out << ", column #{self.class.column_name.inspect}"
69
+ else
70
+ "(data passed in by client, for #{self.class.model_class.name}, column #{self.class.column_name.inspect})"
71
+ end
72
+ end
73
+
74
+ # Returns a Hash, appropriate for integration into the payload of an ActiveSupport::Notification call, that
75
+ # describes the model instance we're created from (or raw String, if that's the case). This is used by the
76
+ # calls made to ActiveSupport::Notifications when a flex-column object is serialized or deserialized, and is used
77
+ # to give good, actionable content when monitoring system performance.
78
+ def notification_hash_for_flex_column_data_source
79
+ out = {
80
+ :model_class => self.class.model_class,
81
+ :column_name => self.class.column_name
82
+ }
83
+
84
+ if model_instance
85
+ out[:model] = model_instance
86
+ else
87
+ out[:source] = @source_string
88
+ end
89
+
90
+ out
91
+ end
92
+
93
+ # This is required by ActiveModel::Validations; it's asking, "what's the ActiveModel object I should use for
94
+ # validation purposes?". And, here, it's this same object.
95
+ def to_model
96
+ self
97
+ end
98
+
99
+ # Provides Hash-style read access to fields in the flex column. This delegates to the ColumnData object, which
100
+ # does most of the actual work.
101
+ def [](field_name)
102
+ column_data[field_name]
103
+ end
104
+
105
+ # Provides Hash-style write access to fields in the flex column. This delegates to the ColumnData object, which
106
+ # does most of the actual work.
107
+ def []=(field_name, new_value)
108
+ column_data[field_name] = new_value
109
+ end
110
+
111
+ # A flex column has been "touched" if it has had at least one field changed to a different value than it had
112
+ # before, or if someone has called #touch! on it. If a column has not been touched, validations are not run on it,
113
+ # nor is it re-serialized back out to the database on save!. Generally, this is a good thing: it increases
114
+ # performance substantially for times when you haven't actually changed the flex column's contents at all. It does
115
+ # mean that invalid data won't be detected and unknown fields won't be removed (if you've specified
116
+ # <tt>:unknown_fields => delete</tt>), however.
117
+ #
118
+ # There may be times, however, when you want to make sure the column is stored back out (including removing any
119
+ # unknown fields, if you selected that option), or to make sure that validations get run, no matter what.
120
+ # In this case, you can call #touch!.
121
+ def touch!
122
+ column_data.touch!
123
+ end
124
+
125
+ # Has at least one field in the column been changed, or has someone called #touch! ?
126
+ def touched?
127
+ column_data.touched?
128
+ end
129
+
130
+ # Called via the ActiveRecord::Base#before_validation hook that gets installed on the enclosing model instance.
131
+ # This runs any validations that are present on this flex-column object, and then propagates any errors back to
132
+ # the enclosing model instance, so that errors show up there, as well.
133
+ def before_validation!
134
+ return unless model_instance
135
+ unless valid?
136
+ errors.each do |name, message|
137
+ model_instance.errors.add("#{column_name}.#{name}", message)
138
+ end
139
+ end
140
+ end
141
+
142
+ # Returns a JSON string representing the current contents of this flex column. Note that this is _not_ always
143
+ # exactly what gets stored in the database, because of binary columns and compression; for that, use
144
+ # #to_stored_data, below.
145
+ def to_json
146
+ column_data.to_json
147
+ end
148
+
149
+ # Returns a String representing exactly the data that will get stored in the database, for this flex column.
150
+ # This will be a UTF-8-encoded String containing pure JSON if this is a textual column, or, if it's a binary
151
+ # column, either a UTF-8-encoded JSON String prefixed by a small header, or a BINARY-encoded String containing
152
+ # GZip'ed JSON, prefixed by a small header.
153
+ def to_stored_data
154
+ column_data.to_stored_data
155
+ end
156
+
157
+ # Called via the ActiveRecord::Base#before_save hook that gets installed on the enclosing model instance. This is
158
+ # what actually serializes the column data and sets it on the ActiveRecord model when it's being saved.
159
+ def before_save!
160
+ return unless model_instance
161
+
162
+ # Make sure we only save if we need to -- otherwise, save the CPU cycles.
163
+ if self.class.requires_serialization_on_save?(model_instance)
164
+ model_instance[column_name] = column_data.to_stored_data
165
+ end
166
+ end
167
+
168
+ # Returns an Array containing the names (as Symbols) of all fields on this flex-column object <em>that currently
169
+ # have any data set for them</em> &mdash; _i.e._, that are not +nil+.
170
+ def keys
171
+ column_data.keys
172
+ end
173
+
174
+ private
175
+ attr_reader :model_instance, :column_data
176
+
177
+ # What's the name of the flex column itself?
178
+ def column_name
179
+ self.class.column_name
180
+ end
181
+
182
+ # What's the ActiveRecord ColumnDefinition object for this flex column?
183
+ def column
184
+ self.class.column
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,316 @@
1
+ module FlexColumns
2
+ module Definition
3
+ # A FieldDefinition represents, well, the definition of a field. One of these objects is created for each field
4
+ # you declare in a flex column. It keeps track of (at minimum) the name of the field; it also is responsible for
5
+ # implementing our "shorthand types" system (where declaring your field as +:integer+ adds a validation that
6
+ # requires it to be an integer, for example).
7
+ #
8
+ # Perhaps most significantly, a FieldDefinition object is responsible for creating the appropriate methods on
9
+ # the flex-column class and on the model class, and also for adding methods to classes that have invoked
10
+ # IncludeFlexColumns#include_flex_columns_from.
11
+ class FieldDefinition
12
+ class << self
13
+ # Given the name of a field, returns a normalized version of that name -- so we can compare using +==+ without
14
+ # worrying about String vs. Symbol and so on.
15
+ def normalize_name(name)
16
+ case name
17
+ when Symbol then name
18
+ when String then
19
+ raise "You must supply a non-empty String, not: #{name.inspect}" if name.strip.length == 0
20
+ name.strip.downcase.to_sym
21
+ else raise ArgumentError, "You must supply a name, not: #{name.inspect}"
22
+ end
23
+ end
24
+ end
25
+
26
+ attr_reader :field_name
27
+
28
+ # Creates a new instance. +flex_column_class+ is the Class we created for this flex column -- _i.e._, a class
29
+ # that inherits from FlexColumns::Contents::FlexColumnContentsBase. +field_name+ is the name of the
30
+ # field. +additional_arguments+ is an Array containing any additional arguments that were passed -- right now,
31
+ # that can only be the type of the field (_e.g._, +:integer+, etc.). +options+ is any options that were passed;
32
+ # this can contain:
33
+ #
34
+ # :visibility, :null, :enum, :limit, :json
35
+ # [:visibility] Can be set to +:public+ or +:private+; will override the default visibility for fields specified
36
+ # on the flex-column class itself.
37
+ # [:null] If present and set to +false+, a validation requiring data in this field will be added.
38
+ # [:enum] If present, must be mapped to an Array; a validation requiring the data to be one of the elements of
39
+ # the array will be added.
40
+ # [:limit] If present, must be mapped to an integer; a validation requiring the length of the data to be at most
41
+ # this value will be added.
42
+ # [:json] If present, must be mapped to a String or Symbol; this specifies that the field should be stored under
43
+ # the given key in the JSON, rather than its field name.
44
+ def initialize(flex_column_class, field_name, additional_arguments, options)
45
+ unless flex_column_class.respond_to?(:is_flex_column_class?) && flex_column_class.is_flex_column_class?
46
+ raise ArgumentError, "You can't define a flex-column field against #{flex_column_class.inspect}; that isn't a flex-column class."
47
+ end
48
+
49
+ validate_options(options)
50
+ @flex_column_class = flex_column_class
51
+ @field_name = self.class.normalize_name(field_name)
52
+ @options = options
53
+ @field_type = nil
54
+
55
+ apply_additional_arguments(additional_arguments)
56
+ apply_validations!
57
+ end
58
+
59
+ # Returns the key under which the field's value should be stored in the JSON.
60
+ def json_storage_name
61
+ (options[:json] || field_name).to_s.strip.downcase.to_sym
62
+ end
63
+
64
+ # Defines appropriate accessor methods for this field on the given DynamicMethodsModule, which should be included
65
+ # in the flex-column class (not the model class). These are quite simple; they always exist (and should overwrite
66
+ # any existing methods, since we're last-definition-wins). We just need to make them work, and make them private,
67
+ # if needed.
68
+ def add_methods_to_flex_column_class!(dynamic_methods_module)
69
+ fn = field_name
70
+
71
+ dynamic_methods_module.define_method(fn) do
72
+ self[fn]
73
+ end
74
+
75
+ dynamic_methods_module.define_method("#{fn}=") do |x|
76
+ self[fn] = x
77
+ end
78
+
79
+ if private?
80
+ dynamic_methods_module.private(fn)
81
+ dynamic_methods_module.private("#{fn}=")
82
+ end
83
+ end
84
+
85
+ # Defines appropriate accessor methods for this field on the given DynamicMethodsModule, which should be included
86
+ # in the model class. We also pass +model_class+ so that we can check to see if we're going to conflict with one
87
+ # of its columns first, or other methods we shouldn't clobber.
88
+ #
89
+ # We need to respect visibility (public or private) of methods, and the delegation prefix assigned at the
90
+ # flex-column level.
91
+ def add_methods_to_model_class!(dynamic_methods_module, model_class)
92
+ return if (! flex_column_class.delegation_type) # :delegate => false on the flex column means don't delegate from the model at all
93
+
94
+ mn = field_name
95
+ mn = "#{flex_column_class.delegation_prefix}_#{mn}".to_sym if flex_column_class.delegation_prefix
96
+
97
+ if model_class._flex_columns_safe_to_define_method?(mn)
98
+ fcc = flex_column_class
99
+ fn = field_name
100
+
101
+ should_be_private = (private? || flex_column_class.delegation_type == :private)
102
+
103
+ dynamic_methods_module.define_method(mn) do
104
+ flex_instance = fcc.object_for(self)
105
+ flex_instance[fn]
106
+ end
107
+ dynamic_methods_module.private(mn) if should_be_private
108
+
109
+ dynamic_methods_module.define_method("#{mn}=") do |x|
110
+ flex_instance = fcc.object_for(self)
111
+ flex_instance[fn] = x
112
+ end
113
+ dynamic_methods_module.private("#{mn}=") if should_be_private
114
+ end
115
+ end
116
+
117
+ # Defines appropriate accessor methods for this field on the given DynamicMethodsModule, which should be included
118
+ # in some target model class that has said +include_flex_columns_from+ on the clsas containing this field.
119
+ # +association_name+ is the name of the association method name that, when called on the class that includes the
120
+ # DynamicMethodsModule, will return an instance of the model class in which this field lives. +target_class+ is
121
+ # the target class we're defining methods on, so that we can check if we're going to conflict with some method
122
+ # there that we should not clobber.
123
+ #
124
+ # +options+ can contain:
125
+ #
126
+ # [:visibility] If +:private+, then methods will be defined as private.
127
+ # [:prefix] If specified, then methods will be prefixed with the given prefix. This will override the prefix
128
+ # specified on the flex-column class, if any.
129
+ def add_methods_to_included_class!(dynamic_methods_module, association_name, target_class, options)
130
+ return if (! flex_column_class.delegation_type)
131
+
132
+ prefix = if options.has_key?(:prefix) then options[:prefix] else flex_column_class.delegation_prefix end
133
+ is_private = private? || (flex_column_class.delegation_type == :private) || (options[:visibility] == :private)
134
+
135
+ if is_private && options[:visibility] == :public
136
+ raise ArgumentError, %{You asked for public visibility for methods included from association #{association_name.inspect},
137
+ but the flex column #{flex_column_class.model_class.name}.#{flex_column_class.column_name} has its methods
138
+ defined with private visibility (either in the flex column itself, or at the model level).
139
+
140
+ You can't have methods be 'more public' in the included class than they are in the class
141
+ they're being included from.}
142
+ end
143
+
144
+ mn = field_name
145
+ mn = "#{prefix}_#{mn}".to_sym if prefix
146
+
147
+ fcc = flex_column_class
148
+ fn = field_name
149
+
150
+ if target_class._flex_columns_safe_to_define_method?(mn)
151
+ dynamic_methods_module.define_method(mn) do
152
+ associated_object = send(association_name) || send("build_#{association_name}")
153
+ flex_column_object = associated_object.send(fcc.column_name)
154
+ flex_column_object.send(fn)
155
+ end
156
+
157
+ dynamic_methods_module.define_method("#{mn}=") do |x|
158
+ associated_object = send(association_name) || send("build_#{association_name}")
159
+ flex_column_object = associated_object.send(fcc.column_name)
160
+ flex_column_object.send("#{fn}=", x)
161
+ end
162
+
163
+ if is_private
164
+ dynamic_methods_module.private(mn)
165
+ dynamic_methods_module.private("#{mn}=")
166
+ end
167
+ end
168
+ end
169
+
170
+ private
171
+ attr_reader :flex_column_class, :options
172
+
173
+ # Checks that the options passed into this class are correct. This is both so that we have good exceptions, and so
174
+ # that we have them early -- it's much nicer if errors happen when you try to define your flex column, rather than
175
+ # much later on, when it really matters, possibly in production.
176
+ def validate_options(options)
177
+ options.assert_valid_keys(:visibility, :null, :enum, :limit, :json)
178
+
179
+ case options[:visibility]
180
+ when nil then nil
181
+ when :public then nil
182
+ when :private then nil
183
+ else raise ArgumentError, "Invalid value for :visibility: #{options[:visibility].inspect}"
184
+ end
185
+
186
+ case options[:json]
187
+ when nil, String, Symbol then nil
188
+ else raise ArgumentError, "Invalid value for :json: #{options[:json].inspect}"
189
+ end
190
+
191
+ unless [ nil, true, false ].include?(options[:null])
192
+ raise ArgumentError, "Invalid value for :null: #{options[:null].inspect}"
193
+ end
194
+ end
195
+
196
+ # Should we define private methods?
197
+ def private?
198
+ case options[:visibility]
199
+ when :public then false
200
+ when :private then true
201
+ when nil then flex_column_class.fields_are_private_by_default?
202
+ else raise "This should never happen: #{options[:visibility].inspect}"
203
+ end
204
+ end
205
+
206
+ # Given any additional arguments after the name of the field (e.g., <tt>field :foo, :integer</tt>), apply them
207
+ # as appropriate. Currently, the only kind of accepted additional argument is a type.
208
+ def apply_additional_arguments(additional_arguments)
209
+ type = additional_arguments.shift
210
+ if type
211
+ begin
212
+ send("apply_validations_for_#{type}")
213
+ rescue NoMethodError => nme
214
+ raise ArgumentError, "Unknown type: #{type.inspect}"
215
+ end
216
+ end
217
+
218
+ if additional_arguments.length > 0
219
+ raise ArgumentError, "Invalid additional arguments: #{additional_arguments.inspect}"
220
+ end
221
+ end
222
+
223
+ # Apply the correct validations for a field of type :integer. (Called from #apply_additional_arguments via
224
+ # metaprogramming.)
225
+ def apply_validations_for_integer
226
+ flex_column_class.validates field_name, :numericality => { :only_integer => true }
227
+ end
228
+
229
+ # Apply the correct validations for a field of type :string. (Called from #apply_additional_arguments via
230
+ # metaprogramming.)
231
+ def apply_validations_for_string
232
+ flex_column_class.validates_each field_name do |record, attr, value|
233
+ record.errors.add(attr, "must be a String") if value && (! value.kind_of?(String)) && (! value.kind_of?(Symbol))
234
+ end
235
+ end
236
+
237
+ # Apply the correct validations for a field of type :text. (Called from #apply_additional_arguments via
238
+ # metaprogramming.)
239
+ def apply_validations_for_text
240
+ apply_validations_for_string
241
+ end
242
+
243
+ # Apply the correct validations for a field of type :float. (Called from #apply_additional_arguments via
244
+ # metaprogramming.)
245
+ def apply_validations_for_float
246
+ flex_column_class.validates field_name, :numericality => true, :allow_nil => true
247
+ end
248
+
249
+ # Apply the correct validations for a field of type :decimal. (Called from #apply_additional_arguments via
250
+ # metaprogramming.)
251
+ def apply_validations_for_decimal
252
+ apply_validations_for_float
253
+ end
254
+
255
+ # Apply the correct validations for a field of type :date. (Called from #apply_additional_arguments via
256
+ # metaprogramming.)
257
+ def apply_validations_for_date
258
+ flex_column_class.validates_each field_name do |record, attr, value|
259
+ record.errors.add(attr, "must be a Date") if value && (! value.kind_of?(Date))
260
+ end
261
+ end
262
+
263
+ # Apply the correct validations for a field of type :time. (Called from #apply_additional_arguments via
264
+ # metaprogramming.)
265
+ def apply_validations_for_time
266
+ flex_column_class.validates_each field_name do |record, attr, value|
267
+ record.errors.add(attr, "must be a Time") if value && (! value.kind_of?(Time))
268
+ end
269
+ end
270
+
271
+ # Apply the correct validations for a field of type :timestamp. (Called from #apply_additional_arguments via
272
+ # metaprogramming.)
273
+ def apply_validations_for_timestamp
274
+ apply_validations_for_datetime
275
+ end
276
+
277
+ # Apply the correct validations for a field of type :datetime. (Called from #apply_additional_arguments via
278
+ # metaprogramming.)
279
+ def apply_validations_for_datetime
280
+ flex_column_class.validates_each field_name do |record, attr, value|
281
+ record.errors.add(attr, "must be a Time or DateTime") if value && (! value.kind_of?(Time)) && (value.class.name != 'DateTime')
282
+ end
283
+ end
284
+
285
+ # Apply the correct validations for a field of type :boolean. (Called from #apply_additional_arguments via
286
+ # metaprogramming.)
287
+ def apply_validations_for_boolean
288
+ flex_column_class.validates field_name, :inclusion => { :in => [ true, false, nil ] }
289
+ end
290
+
291
+ # Applies any validations resulting from options to this class (but not types; they're handled by
292
+ # #apply_additional_arguments, above). Currently, this applies validations for +:null+, +:enum+, and +:limit+.
293
+ def apply_validations!
294
+ if options.has_key?(:null) && (! options[:null])
295
+ flex_column_class.validates field_name, :presence => true
296
+ end
297
+
298
+ if options.has_key?(:enum)
299
+ values = options[:enum]
300
+ unless values.kind_of?(Array)
301
+ raise ArgumentError, "Must specify an Array of possible values, not: #{options[:enum].inspect}"
302
+ end
303
+
304
+ flex_column_class.validates field_name, :inclusion => { :in => values }
305
+ end
306
+
307
+ if options.has_key?(:limit)
308
+ limit = options[:limit]
309
+ raise ArgumentError, "Limit must be > 0, not: #{limit.inspect}" unless limit.kind_of?(Integer) && limit > 0
310
+
311
+ flex_column_class.validates field_name, :length => { :maximum => limit }
312
+ end
313
+ end
314
+ end
315
+ end
316
+ end