flex_columns 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +38 -0
  4. data/Gemfile +17 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +124 -0
  7. data/Rakefile +6 -0
  8. data/flex_columns.gemspec +72 -0
  9. data/lib/flex_columns.rb +15 -0
  10. data/lib/flex_columns/active_record/base.rb +57 -0
  11. data/lib/flex_columns/contents/column_data.rb +376 -0
  12. data/lib/flex_columns/contents/flex_column_contents_base.rb +188 -0
  13. data/lib/flex_columns/definition/field_definition.rb +316 -0
  14. data/lib/flex_columns/definition/field_set.rb +89 -0
  15. data/lib/flex_columns/definition/flex_column_contents_class.rb +327 -0
  16. data/lib/flex_columns/errors.rb +236 -0
  17. data/lib/flex_columns/has_flex_columns.rb +187 -0
  18. data/lib/flex_columns/including/include_flex_columns.rb +179 -0
  19. data/lib/flex_columns/util/dynamic_methods_module.rb +86 -0
  20. data/lib/flex_columns/util/string_utils.rb +31 -0
  21. data/lib/flex_columns/version.rb +4 -0
  22. data/spec/flex_columns/helpers/database_helper.rb +174 -0
  23. data/spec/flex_columns/helpers/exception_helpers.rb +20 -0
  24. data/spec/flex_columns/helpers/system_helpers.rb +47 -0
  25. data/spec/flex_columns/system/basic_system_spec.rb +245 -0
  26. data/spec/flex_columns/system/bulk_system_spec.rb +153 -0
  27. data/spec/flex_columns/system/compression_system_spec.rb +218 -0
  28. data/spec/flex_columns/system/custom_methods_system_spec.rb +120 -0
  29. data/spec/flex_columns/system/delegation_system_spec.rb +175 -0
  30. data/spec/flex_columns/system/dynamism_system_spec.rb +158 -0
  31. data/spec/flex_columns/system/error_handling_system_spec.rb +117 -0
  32. data/spec/flex_columns/system/including_system_spec.rb +285 -0
  33. data/spec/flex_columns/system/json_alias_system_spec.rb +171 -0
  34. data/spec/flex_columns/system/performance_system_spec.rb +218 -0
  35. data/spec/flex_columns/system/postgres_json_column_type_system_spec.rb +85 -0
  36. data/spec/flex_columns/system/types_system_spec.rb +93 -0
  37. data/spec/flex_columns/system/unknown_fields_system_spec.rb +126 -0
  38. data/spec/flex_columns/system/validations_system_spec.rb +111 -0
  39. data/spec/flex_columns/unit/active_record/base_spec.rb +32 -0
  40. data/spec/flex_columns/unit/contents/column_data_spec.rb +520 -0
  41. data/spec/flex_columns/unit/contents/flex_column_contents_base_spec.rb +253 -0
  42. data/spec/flex_columns/unit/definition/field_definition_spec.rb +617 -0
  43. data/spec/flex_columns/unit/definition/field_set_spec.rb +142 -0
  44. data/spec/flex_columns/unit/definition/flex_column_contents_class_spec.rb +733 -0
  45. data/spec/flex_columns/unit/errors_spec.rb +297 -0
  46. data/spec/flex_columns/unit/has_flex_columns_spec.rb +365 -0
  47. data/spec/flex_columns/unit/including/include_flex_columns_spec.rb +144 -0
  48. data/spec/flex_columns/unit/util/dynamic_methods_module_spec.rb +105 -0
  49. data/spec/flex_columns/unit/util/string_utils_spec.rb +23 -0
  50. metadata +286 -0
@@ -0,0 +1,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