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