flex_columns 1.0.0

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