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,187 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'active_support/core_ext'
|
4
|
+
require 'flex_columns/contents/flex_column_contents_base'
|
5
|
+
|
6
|
+
module FlexColumns
|
7
|
+
# HasFlexColumns is the module that gets included in an ActiveRecord model class as soon as it declares a flex
|
8
|
+
# column (using FlexColumns::ActiveRecord::Base#flex_column). While most of the actual work of maintaining and working
|
9
|
+
# with a flex column is accomplished by the FlexColumns::Definition::FlexColumnContentsClass module and the
|
10
|
+
# FlexColumns::Contents::FlexColumnContentsBase class, there remains, nevertheless, some important work to do here.
|
11
|
+
module HasFlexColumns
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
# Register our hooks: we need to run before validation to make sure any validations defined directly on a flex
|
15
|
+
# column class are run (and transfer their errors over to the model object itself), and to run before save to make
|
16
|
+
# sure we serialize up any changes from a flex-column object.
|
17
|
+
included do
|
18
|
+
before_validation :_flex_columns_before_validation!
|
19
|
+
before_save :_flex_columns_before_save!
|
20
|
+
end
|
21
|
+
|
22
|
+
# Before we save this model, make sure each flex column has a chance to serialize itself up and assign itself
|
23
|
+
# properly to this model object. Note that we only need to call through to flex-column objects that have actually
|
24
|
+
# been instantiated, since, by definition, there's no way the contents of any other flex columns could possibly
|
25
|
+
# have been changed.
|
26
|
+
def _flex_columns_before_save!
|
27
|
+
self.class._all_flex_column_names.each do |flex_column_name|
|
28
|
+
klass = self.class._flex_column_class_for(flex_column_name)
|
29
|
+
if klass.requires_serialization_on_save?(self)
|
30
|
+
_flex_column_object_for(flex_column_name).before_save!
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Before we validate this model, make sure each flex column has a chance to run its validations and propagate any
|
36
|
+
# errors back to this model. Note that we need to call through to any flex-column object that has a validation
|
37
|
+
# defined, since we want to comply with Rails' validation strategy: validations run whenever you save an object,
|
38
|
+
# whether you've changed that particular attribute or not.
|
39
|
+
def _flex_columns_before_validation!
|
40
|
+
_all_present_flex_column_objects.each do |flex_column_object|
|
41
|
+
flex_column_object.before_validation! if flex_column_object.touched?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the correct flex-column object for the given column name. This simply creates an instance of the
|
46
|
+
# appropriate flex-column class, and saves it away so it will be returned again if someone requests the object for
|
47
|
+
# the same column later.
|
48
|
+
def _flex_column_object_for(column_name, create_if_needed = true)
|
49
|
+
# It's possible to end up with two copies of this method on a class, if that class both has a flex column of its
|
50
|
+
# own _and_ includes one via FlexColumns::Including::IncludeFlexColumns#include_flex_columns_from. If so, we want
|
51
|
+
# each method to defer to the other one, so that both will work.
|
52
|
+
begin
|
53
|
+
return super(column_name)
|
54
|
+
rescue NoMethodError
|
55
|
+
# ok
|
56
|
+
rescue FlexColumns::Errors::NoSuchColumnError
|
57
|
+
# ok
|
58
|
+
end
|
59
|
+
|
60
|
+
column_name = self.class._flex_column_normalize_name(column_name)
|
61
|
+
|
62
|
+
out = _flex_column_objects[column_name]
|
63
|
+
if (! out) && create_if_needed
|
64
|
+
out = _flex_column_objects[column_name] = self.class._flex_column_class_for(column_name).new(self)
|
65
|
+
end
|
66
|
+
out
|
67
|
+
end
|
68
|
+
|
69
|
+
# When you reload a model object, we should reload its flex-column objects, too.
|
70
|
+
def reload(*args)
|
71
|
+
super(*args)
|
72
|
+
@_flex_column_objects = { }
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
# Returns the Hash that we keep flex-column objects in, indexed by column name.
|
77
|
+
def _flex_column_objects
|
78
|
+
@_flex_column_objects ||= { }
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns all flex-column objects that have been instantiated -- that is, any flex-column object that anybody has
|
82
|
+
# asked for yet.
|
83
|
+
def _all_present_flex_column_objects
|
84
|
+
_flex_column_objects.values
|
85
|
+
end
|
86
|
+
|
87
|
+
module ClassMethods
|
88
|
+
# Does this class have any flex columns? If this module has been included into a class, then the answer is true.
|
89
|
+
def has_any_flex_columns?
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
# What are the names of all flex columns defined on this model?
|
94
|
+
def _all_flex_column_names
|
95
|
+
_flex_column_classes.map(&:column_name)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Normalizes the name of a flex column, so we're consistent when using it for things like hash keys, no matter
|
99
|
+
# how the client specifies it to us.
|
100
|
+
def _flex_column_normalize_name(flex_column_name)
|
101
|
+
flex_column_name.to_s.strip.downcase.to_sym
|
102
|
+
end
|
103
|
+
|
104
|
+
# Given the name of a flex column, returns the flex-column class for that column. Raises
|
105
|
+
# FlexColumns::Errors::NoSuchColumnError if there is no column with the given name.
|
106
|
+
def _flex_column_class_for(flex_column_name)
|
107
|
+
flex_column_name = _flex_column_normalize_name(flex_column_name)
|
108
|
+
out = _flex_column_classes.detect { |fcc| fcc.column_name == flex_column_name }
|
109
|
+
|
110
|
+
unless out
|
111
|
+
raise FlexColumns::Errors::NoSuchColumnError, %{Model class #{self.name} has no flex column named #{flex_column_name.inspect};
|
112
|
+
it has flex columns named: #{_all_flex_column_names.sort_by(&:to_s).inspect}.}
|
113
|
+
end
|
114
|
+
|
115
|
+
out
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns the DynamicMethodsModule that we add methods to that should be present on this model class.
|
119
|
+
def _flex_column_dynamic_methods_module
|
120
|
+
@_flex_column_dynamic_methods_module ||= FlexColumns::Util::DynamicMethodsModule.new(self, :FlexColumnsDynamicMethods)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Declares a new flex column. +flex_column_name+ is its name; +options+ is passed through to
|
124
|
+
# FlexColumns::Definition::FlexColumnContentsClass#setup!, and so can contain any of the options that that method
|
125
|
+
# accepts. The block, if passed, will be evaluated in the context of the generated class.
|
126
|
+
def flex_column(flex_column_name, options = { }, &block)
|
127
|
+
flex_column_name = _flex_column_normalize_name(flex_column_name)
|
128
|
+
|
129
|
+
new_class = Class.new(FlexColumns::Contents::FlexColumnContentsBase)
|
130
|
+
new_class.setup!(self, flex_column_name, options, &block)
|
131
|
+
|
132
|
+
_flex_column_classes.delete_if { |fcc| fcc.column_name == flex_column_name }
|
133
|
+
_flex_column_classes << new_class
|
134
|
+
|
135
|
+
define_method(flex_column_name) do
|
136
|
+
_flex_column_object_for(flex_column_name)
|
137
|
+
end
|
138
|
+
|
139
|
+
_flex_column_dynamic_methods_module.remove_all_methods!
|
140
|
+
_flex_column_classes.each(&:sync_methods!)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Exactly like #create_flex_objects_from, except that instead of taking an Array of raw strings and returning
|
144
|
+
# an Array of flex-column objects, takes a single raw string and returns a single flex-column object.
|
145
|
+
#
|
146
|
+
# #create_flex_objects_from is currently very slightly faster than simply calling this method in a loop; however,
|
147
|
+
# in the future, the difference in performance may increase. If you have more than one string to create a
|
148
|
+
# flex-column object from, you should definitely use #create_flex_objects_from instead of this method.
|
149
|
+
def create_flex_object_from(column_name, raw_string)
|
150
|
+
_flex_column_class_for(column_name).new(raw_string)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Given the name of a column in +column_name+ and an Array of (possibly nil) JSON-formatted strings (which can
|
154
|
+
# also be compressed using the +flex_columns+ compression mechanism), returns an Array of new flex-column objects
|
155
|
+
# for that column that are not attached to any particular model instance. These objects will obey all
|
156
|
+
# field-definition rules for that column, be able to validate themselves (if you call #valid? on them),
|
157
|
+
# retrieve data, have any custom methods defined on them that you defined on that flex column, and so on.
|
158
|
+
#
|
159
|
+
# However, because they're detached from any model instance, they also won't save themselves to the database under
|
160
|
+
# any circumstances; you are responsible for calling #to_stored_data on them, and getting those strings into the
|
161
|
+
# database in the right places yourself, if you want to save them.
|
162
|
+
#
|
163
|
+
# The purpose of this method is to allow you to use +flex_columns+ in bulk-access situations, such as when you've
|
164
|
+
# selected many records from the database without using ActiveRecord, for performance reasons (_e.g._,
|
165
|
+
# <tt>User.connection.select_all("..."))</tt>.
|
166
|
+
def create_flex_objects_from(column_name, raw_strings)
|
167
|
+
column_class = _flex_column_class_for(column_name)
|
168
|
+
raw_strings.map do |rs|
|
169
|
+
column_class.new(rs)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
# Returns the set of currently-active flex-column classes -- that is, classes that inherit from
|
175
|
+
# FlexColumns::Contents::FlexColumnContentsBase and represent our declared flex columns. We say "currently active"
|
176
|
+
# because declaring a new flex column with the same name as a previous one will replace its class in this list.
|
177
|
+
#
|
178
|
+
# This is an Array instead of a Hash because the order in which we sync methods to the dynamic-methods module
|
179
|
+
# matters: flex columns declared later should have any delegate methods they declare supersede any methods from
|
180
|
+
# flex columns declared previously. While Ruby >= 1.9 has ordered Hashes, which means we could use it here, we
|
181
|
+
# still support Ruby 1.8, and so need the ordering that an Array gives us.
|
182
|
+
def _flex_column_classes
|
183
|
+
@_flex_column_classes ||= [ ]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module FlexColumns
|
4
|
+
module Including
|
5
|
+
# IncludeFlexColumns defines the methods on ActiveRecord::Base that get triggered when you say
|
6
|
+
# 'include_flex_columns_from' on an ActiveRecord model. Note, however, that it is not simply directly included
|
7
|
+
# into ActiveRecord::Base; rather, it's included only when you actually make that declaration, and included
|
8
|
+
# into the specific model class itself. (This helps avoid pollution or conflict on any ActiveRecord models that
|
9
|
+
# have not used this functionality.)
|
10
|
+
#
|
11
|
+
# This module works in a pretty different way from FlexColumns::HasFlexColumns, which is the corresponding module
|
12
|
+
# that gets included when you declare a flex column with <tt>flex_column :foo do ... end</tt>. That module builds
|
13
|
+
# up an object representation of the flex column itself and of all its fields, and then holds onto these objects
|
14
|
+
# and uses them to do its work. This module, on the other hand, actively and aggressively defines the appropriate
|
15
|
+
# methods when you call #include_flex_columns_from, but does not create or hold onto any object representation of
|
16
|
+
# the included columns. This is for two reasons: first off, there's a lot more complexity in defining a flex
|
17
|
+
# column itself than in simply including one. Secondly, and more subtly, defining a flex column is a process with
|
18
|
+
# a decided start and end -- the contents of the block passed to +flex_column+. Including fields, however, is a
|
19
|
+
# component part of a class that's defined using the Ruby +class+ keyword, and which can get reopened and redefined
|
20
|
+
# at any given time. Thus, we really have no choice but to aggressively define methods when
|
21
|
+
# +include_flex_columns_from+ is called; holding onto an object representation would largely just ensure that that
|
22
|
+
# object representation grew incorrect over time in development mode, as columns get defined and redefined over
|
23
|
+
# time.
|
24
|
+
#
|
25
|
+
# (A corollary of this is that, in Rails development mode, depending on how classes get reloaded, it's possible that
|
26
|
+
# if you remove an +include_flex_columns_from+ declaration from a model, the defined methods won't actually
|
27
|
+
# disappear until you restart your server. There's really not much we can do about this, since there's no Ruby hook
|
28
|
+
# that says "someone is defining methods on class X" -- nor would one make any sense, since you can re-open classes
|
29
|
+
# at any time and as many times as you want in Ruby.)
|
30
|
+
#
|
31
|
+
# In comments below, we're working with the following example:
|
32
|
+
#
|
33
|
+
# class UserDetail < ActiveRecord::Base
|
34
|
+
# self.primary_key = :user_id
|
35
|
+
# belongs_to :user
|
36
|
+
#
|
37
|
+
# flex_column :details do
|
38
|
+
# field :background_color
|
39
|
+
# field :likes_peaches
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# class User < ActiveRecord::Base
|
44
|
+
# has_one :detail
|
45
|
+
#
|
46
|
+
# include_flex_columns_from :detail
|
47
|
+
# end
|
48
|
+
module IncludeFlexColumns
|
49
|
+
# Make sure our ClassMethods module gets +extend+ed into any class that +include+s us.
|
50
|
+
extend ActiveSupport::Concern
|
51
|
+
|
52
|
+
# This is the method that gets called by generated delegated methods, and called in order to retrieve the
|
53
|
+
# correct flex-column object for a column. In other words, the generated method User#background_color looks
|
54
|
+
# something like:
|
55
|
+
#
|
56
|
+
# def background_color
|
57
|
+
# flex_column_object = _flex_column_object_for(:details)
|
58
|
+
# flex_column_object.background_color
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# (We do this partially so that the exact same method definition works for UserDetail and for User; _i.e._,
|
62
|
+
# whether you're running on a class that itself has a flex column, or on a class that simply is including another
|
63
|
+
# class's flex columns, #_flex_column_object_for will get you the right object.)
|
64
|
+
#
|
65
|
+
# There's only one nasty case to deal with here: what if User has its own flex column +detail+? In such a case, we
|
66
|
+
# want to return the flex-column object that's defined for the column the class has itself, not for the one it's
|
67
|
+
# including.
|
68
|
+
def _flex_column_object_for(column_name)
|
69
|
+
# This is the "nasty case", above.
|
70
|
+
begin
|
71
|
+
return super(column_name)
|
72
|
+
rescue NoMethodError
|
73
|
+
# ok
|
74
|
+
rescue FlexColumns::Errors::NoSuchColumnError
|
75
|
+
# ok
|
76
|
+
end
|
77
|
+
|
78
|
+
# Fetch the association that this column is included from.
|
79
|
+
association = self.class._flex_column_is_included_from(column_name)
|
80
|
+
|
81
|
+
if association
|
82
|
+
# Get the associated object. We automatically will build the associated object, if necessary; this is so that
|
83
|
+
# you don't have to either actively create associated objects ahead of time, just in case you need them later,
|
84
|
+
# or litter your code with checks to see if those objects exist already or not.
|
85
|
+
associated_object = send(association) || send("build_#{association}")
|
86
|
+
return associated_object.send(column_name)
|
87
|
+
else
|
88
|
+
raise FlexColumns::Errors::NoSuchColumnError.new(%{Class #{self.class.name} knows nothing of a flex column named #{column_name.inspect}.})
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
module ClassMethods
|
93
|
+
# The DynamicMethodsModule on which we define all methods generated by included flex columns.
|
94
|
+
def _flex_columns_include_flex_columns_dynamic_methods_module
|
95
|
+
@_flex_columns_include_flex_columns_dynamic_methods_module ||= FlexColumns::Util::DynamicMethodsModule.new(self, :FlexColumnsIncludedColumnsMethods)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns the name of the association from which a flex column of the given name was included.
|
99
|
+
def _flex_column_is_included_from(flex_column_name)
|
100
|
+
@_included_flex_columns_map[flex_column_name]
|
101
|
+
end
|
102
|
+
|
103
|
+
# Includes methods from the given flex column or flex columns into this class.
|
104
|
+
#
|
105
|
+
# +args+ should be a list of association names from which you want to include columns. It can also end in an
|
106
|
+
# options Hash, which can contain:
|
107
|
+
#
|
108
|
+
# [:prefix] If set, included method names will be prefixed with the given string (followed by an underscore).
|
109
|
+
# If not set, the prefix defined on each flex column, if any, will be used; you can override this by
|
110
|
+
# explicitly passing +nil+ here.
|
111
|
+
# [:visibility] If set to +:private+, included methods will be marked +private+, meaning they can only be
|
112
|
+
# accessed from inside this model. This can be used to ensure random code across your system
|
113
|
+
# can't directly manipulate flex-column fields.
|
114
|
+
# [:delegate] If set to +false+ or +nil+, then only the method that accesses the flex column itself (above,
|
115
|
+
# User#details) will be created; other methods (User#background_color, User#likes_peaches) will
|
116
|
+
# not be automatically delegated.
|
117
|
+
def include_flex_columns_from(*args, &block)
|
118
|
+
# Grab our options, and validate them as necessary...
|
119
|
+
options = args.pop if args[-1] && args[-1].kind_of?(Hash)
|
120
|
+
options ||= { }
|
121
|
+
|
122
|
+
options.assert_valid_keys(:prefix, :visibility, :delegate)
|
123
|
+
|
124
|
+
case options[:prefix]
|
125
|
+
when nil, String, Symbol then nil
|
126
|
+
else raise ArgumentError, "Invalid value for :prefix: #{options[:prefix].inspect}"
|
127
|
+
end
|
128
|
+
|
129
|
+
unless [ :public, :private, nil ].include?(options[:visibility])
|
130
|
+
raise ArgumentError, "Invalid value for :visibility: #{options[:visibility].inspect}"
|
131
|
+
end
|
132
|
+
|
133
|
+
unless [ true, false, nil ].include?(options[:delegate])
|
134
|
+
raise ArgumentError, "Invalid value for :delegate: #{options[:delegate].inspect}"
|
135
|
+
end
|
136
|
+
|
137
|
+
association_names = args
|
138
|
+
|
139
|
+
@_included_flex_columns_map ||= { }
|
140
|
+
|
141
|
+
# Iterate through each association...
|
142
|
+
association_names.each do |association_name|
|
143
|
+
# Get the association and make sure it's of the right type...
|
144
|
+
association = reflect_on_association(association_name.to_sym)
|
145
|
+
unless association
|
146
|
+
raise ArgumentError, %{You asked #{self.name} to include flex columns from association #{association_name.inspect},
|
147
|
+
but this class doesn't seem to have such an association. Associations it has are:
|
148
|
+
|
149
|
+
#{reflect_on_all_associations.map(&:name).sort_by(&:to_s).join(", ")}}
|
150
|
+
end
|
151
|
+
|
152
|
+
unless [ :has_one, :belongs_to ].include?(association.macro)
|
153
|
+
raise ArgumentError, %{You asked #{self.name} to include flex columns from association #{association_name.inspect},
|
154
|
+
but that association is of type #{association.macro.inspect}, not :has_one or :belongs_to.
|
155
|
+
|
156
|
+
We can only include flex columns from an association of these types, because otherwise
|
157
|
+
there is no way to know which target object to include the data from.}
|
158
|
+
end
|
159
|
+
|
160
|
+
# Grab the target model class, and make sure it has one or more flex columns...
|
161
|
+
target_class = association.klass
|
162
|
+
if (! target_class.respond_to?(:has_any_flex_columns?)) || (! target_class.has_any_flex_columns?)
|
163
|
+
raise ArgumentError, %{You asked #{self.name} to include flex columns from association #{association_name.inspect},
|
164
|
+
but the target class of that association, #{association.klass.name}, has no flex columns defined.}
|
165
|
+
end
|
166
|
+
|
167
|
+
# Call through and tell those flex columns to create the appropriate methods.
|
168
|
+
target_class._all_flex_column_names.each do |flex_column_name|
|
169
|
+
@_included_flex_columns_map[flex_column_name] = association_name
|
170
|
+
|
171
|
+
flex_column_class = target_class._flex_column_class_for(flex_column_name)
|
172
|
+
flex_column_class.include_fields_into(_flex_columns_include_flex_columns_dynamic_methods_module, association_name, self, options)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module FlexColumns
|
2
|
+
module Util
|
3
|
+
# A DynamicMethodsModule is used to add dynamically-generated methods to an existing class.
|
4
|
+
#
|
5
|
+
# Why do we need a module to do that? Why can't we simply call #define_method on the class itself?
|
6
|
+
#
|
7
|
+
# We could. However, if you do that, a few problems crop up:
|
8
|
+
#
|
9
|
+
# * There is no precendence that you can control. If you define a method +:foo+ on class Bar, then that method is
|
10
|
+
# always run when an instance of that class is sent the message +:foo+. The only way to change the behavior of
|
11
|
+
# that class is to completely redefine that method, which brings us to the second problem...
|
12
|
+
# * Overriding and +super+ doesn't work. That is, you can't override such a method and call the original method
|
13
|
+
# using +super+. You're reduced to using +alias_method_chain+, which is a mess.
|
14
|
+
# * There's no namespacing at all -- at runtime, it's not even remotely clear where these methods are coming from.
|
15
|
+
# * Finally, if you're living in a dynamic environment -- like Rails' development mode, where classes get reloaded
|
16
|
+
# very frequently -- once you define a method, it is likely to be forever defined. You have to write code to keep
|
17
|
+
# track of what you've defined, and remove it when it's no longer present.
|
18
|
+
#
|
19
|
+
# A DynamicMethodsModule fixes these problems. It's little more than a Module that lets you define methods (and
|
20
|
+
# helpfully makes #define_method +public+ to help), but it also will include itself into a target class and bind
|
21
|
+
# itself to a constant in that class (which magically gives the module a name, too). Further, it also keeps track
|
22
|
+
# of which methods you've defined, and can remove them all with #remove_all_methods!. This allows you to construct
|
23
|
+
# a much more reliable paradigm: instead of trying to figure out what methods you should remove and add when things
|
24
|
+
# change, you can just call #remove_all_methods! and then redefine whatever methods _currently_ should exist.
|
25
|
+
class DynamicMethodsModule < ::Module
|
26
|
+
# Creates a new instance. +target_class+ is the Class into which this module should include itself; +name+ is the
|
27
|
+
# name to which it should bind itself. (This will be bound as a constant inside that class, not at top-level on
|
28
|
+
# Object; so, for example, if +target_class+ is +User+ and +name+ is +Foo+, then this module will end up named
|
29
|
+
# +User::Foo+, not simply +Foo+.)
|
30
|
+
#
|
31
|
+
# If passed a block, the block will be evaluated in the context of this module, just like Module#new. Note that
|
32
|
+
# you <em>should not</em> use this to define methods that you want #remove_all_methods!, below, to remove; it
|
33
|
+
# won't work. Any methods you add in this block using normal +def+ will persist, even through #remove_all_methods!.
|
34
|
+
def initialize(target_class, name, &block)
|
35
|
+
raise ArgumentError, "Target class must be a Class, not: #{target_class.inspect}" unless target_class.kind_of?(Class)
|
36
|
+
raise ArgumentError, "Name must be a Symbol or String, not: #{name.inspect}" unless name.kind_of?(Symbol) || name.kind_of?(String)
|
37
|
+
|
38
|
+
@target_class = target_class
|
39
|
+
@name = name.to_sym
|
40
|
+
|
41
|
+
# Unfortunately, there appears to be no way to "un-include" a Module in Ruby -- so we have no way of replacing
|
42
|
+
# an existing DynamicMethodsModule on the target class, which is what we'd really like to do in this situation.
|
43
|
+
if @target_class.const_defined?(@name)
|
44
|
+
existing = @target_class.const_get(@name)
|
45
|
+
|
46
|
+
if existing && existing != self
|
47
|
+
raise NameError, %{You tried to define a #{self.class.name} named #{name.inspect} on class #{target_class.name},
|
48
|
+
but that class already has a constant named #{name.inspect}: #{existing.inspect}}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
@target_class.const_set(@name, self)
|
53
|
+
@target_class.send(:include, self)
|
54
|
+
|
55
|
+
@methods_defined = { }
|
56
|
+
|
57
|
+
super(&block)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Removes all methods that have been defined on this module using #define_method, below. (If you use some other
|
61
|
+
# mechanism to define a method on this DynamicMethodsModule, then it will not be removed when this method is
|
62
|
+
# called.)
|
63
|
+
def remove_all_methods!
|
64
|
+
instance_methods.each do |method_name|
|
65
|
+
# Important -- we use Class#remove_method, not Class#undef_method, which does something that's different in
|
66
|
+
# some important ways.
|
67
|
+
remove_method(method_name) if @methods_defined[method_name.to_sym]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Defines a method. Works identically to Module#define_method, except that it's +public+ and #remove_all_methods!
|
72
|
+
# will remove the method.
|
73
|
+
def define_method(name, &block)
|
74
|
+
name = name.to_sym
|
75
|
+
super(name, &block)
|
76
|
+
@methods_defined[name] = true
|
77
|
+
end
|
78
|
+
|
79
|
+
# Makes it so you can say, for example:
|
80
|
+
#
|
81
|
+
# my_dynamic_methods_module.define_method(:foo) { ... }
|
82
|
+
# my_dynamic_methods_module.private(:foo)
|
83
|
+
public :private # teehee
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|