flex_columns 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|