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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +38 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +22 -0
- data/README.md +124 -0
- data/Rakefile +6 -0
- data/flex_columns.gemspec +72 -0
- data/lib/flex_columns.rb +15 -0
- data/lib/flex_columns/active_record/base.rb +57 -0
- data/lib/flex_columns/contents/column_data.rb +376 -0
- data/lib/flex_columns/contents/flex_column_contents_base.rb +188 -0
- data/lib/flex_columns/definition/field_definition.rb +316 -0
- data/lib/flex_columns/definition/field_set.rb +89 -0
- data/lib/flex_columns/definition/flex_column_contents_class.rb +327 -0
- data/lib/flex_columns/errors.rb +236 -0
- data/lib/flex_columns/has_flex_columns.rb +187 -0
- data/lib/flex_columns/including/include_flex_columns.rb +179 -0
- data/lib/flex_columns/util/dynamic_methods_module.rb +86 -0
- data/lib/flex_columns/util/string_utils.rb +31 -0
- data/lib/flex_columns/version.rb +4 -0
- data/spec/flex_columns/helpers/database_helper.rb +174 -0
- data/spec/flex_columns/helpers/exception_helpers.rb +20 -0
- data/spec/flex_columns/helpers/system_helpers.rb +47 -0
- data/spec/flex_columns/system/basic_system_spec.rb +245 -0
- data/spec/flex_columns/system/bulk_system_spec.rb +153 -0
- data/spec/flex_columns/system/compression_system_spec.rb +218 -0
- data/spec/flex_columns/system/custom_methods_system_spec.rb +120 -0
- data/spec/flex_columns/system/delegation_system_spec.rb +175 -0
- data/spec/flex_columns/system/dynamism_system_spec.rb +158 -0
- data/spec/flex_columns/system/error_handling_system_spec.rb +117 -0
- data/spec/flex_columns/system/including_system_spec.rb +285 -0
- data/spec/flex_columns/system/json_alias_system_spec.rb +171 -0
- data/spec/flex_columns/system/performance_system_spec.rb +218 -0
- data/spec/flex_columns/system/postgres_json_column_type_system_spec.rb +85 -0
- data/spec/flex_columns/system/types_system_spec.rb +93 -0
- data/spec/flex_columns/system/unknown_fields_system_spec.rb +126 -0
- data/spec/flex_columns/system/validations_system_spec.rb +111 -0
- data/spec/flex_columns/unit/active_record/base_spec.rb +32 -0
- data/spec/flex_columns/unit/contents/column_data_spec.rb +520 -0
- data/spec/flex_columns/unit/contents/flex_column_contents_base_spec.rb +253 -0
- data/spec/flex_columns/unit/definition/field_definition_spec.rb +617 -0
- data/spec/flex_columns/unit/definition/field_set_spec.rb +142 -0
- data/spec/flex_columns/unit/definition/flex_column_contents_class_spec.rb +733 -0
- data/spec/flex_columns/unit/errors_spec.rb +297 -0
- data/spec/flex_columns/unit/has_flex_columns_spec.rb +365 -0
- data/spec/flex_columns/unit/including/include_flex_columns_spec.rb +144 -0
- data/spec/flex_columns/unit/util/dynamic_methods_module_spec.rb +105 -0
- data/spec/flex_columns/unit/util/string_utils_spec.rb +23 -0
- 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> — _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
|