flex_columns 1.0.0

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