him 0.1.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 (75) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +40 -0
  3. data/.gitignore +6 -0
  4. data/.qlty/qlty.toml +57 -0
  5. data/.rspec +1 -0
  6. data/.ruby-version +1 -0
  7. data/.yardopts +2 -0
  8. data/CONTRIBUTING.md +26 -0
  9. data/Gemfile +2 -0
  10. data/LICENSE +8 -0
  11. data/README.md +1007 -0
  12. data/Rakefile +11 -0
  13. data/UPGRADE.md +101 -0
  14. data/gemfiles/Gemfile.activemodel-6.1 +6 -0
  15. data/gemfiles/Gemfile.activemodel-7.0 +6 -0
  16. data/gemfiles/Gemfile.activemodel-7.1 +6 -0
  17. data/gemfiles/Gemfile.activemodel-7.2 +6 -0
  18. data/gemfiles/Gemfile.activemodel-8.0 +6 -0
  19. data/him.gemspec +28 -0
  20. data/lib/him/api.rb +121 -0
  21. data/lib/him/collection.rb +21 -0
  22. data/lib/him/errors.rb +29 -0
  23. data/lib/him/json_api/model.rb +42 -0
  24. data/lib/him/middleware/accept_json.rb +18 -0
  25. data/lib/him/middleware/first_level_parse_json.rb +37 -0
  26. data/lib/him/middleware/json_api_parser.rb +65 -0
  27. data/lib/him/middleware/parse_json.rb +22 -0
  28. data/lib/him/middleware/second_level_parse_json.rb +37 -0
  29. data/lib/him/middleware.rb +12 -0
  30. data/lib/him/model/associations/association.rb +147 -0
  31. data/lib/him/model/associations/association_proxy.rb +47 -0
  32. data/lib/him/model/associations/belongs_to_association.rb +95 -0
  33. data/lib/him/model/associations/has_many_association.rb +113 -0
  34. data/lib/him/model/associations/has_one_association.rb +79 -0
  35. data/lib/him/model/associations.rb +141 -0
  36. data/lib/him/model/attributes.rb +337 -0
  37. data/lib/him/model/base.rb +33 -0
  38. data/lib/him/model/http.rb +113 -0
  39. data/lib/him/model/introspection.rb +77 -0
  40. data/lib/him/model/nested_attributes.rb +45 -0
  41. data/lib/him/model/orm.rb +306 -0
  42. data/lib/him/model/parse.rb +224 -0
  43. data/lib/him/model/paths.rb +125 -0
  44. data/lib/him/model/relation.rb +212 -0
  45. data/lib/him/model.rb +79 -0
  46. data/lib/him/version.rb +3 -0
  47. data/lib/him.rb +22 -0
  48. data/spec/api_spec.rb +120 -0
  49. data/spec/collection_spec.rb +70 -0
  50. data/spec/json_api/model_spec.rb +260 -0
  51. data/spec/middleware/accept_json_spec.rb +11 -0
  52. data/spec/middleware/first_level_parse_json_spec.rb +63 -0
  53. data/spec/middleware/json_api_parser_spec.rb +52 -0
  54. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  55. data/spec/model/associations/association_proxy_spec.rb +29 -0
  56. data/spec/model/associations_spec.rb +1010 -0
  57. data/spec/model/attributes_spec.rb +384 -0
  58. data/spec/model/callbacks_spec.rb +194 -0
  59. data/spec/model/dirty_spec.rb +133 -0
  60. data/spec/model/http_spec.rb +187 -0
  61. data/spec/model/introspection_spec.rb +110 -0
  62. data/spec/model/nested_attributes_spec.rb +135 -0
  63. data/spec/model/orm_spec.rb +717 -0
  64. data/spec/model/parse_spec.rb +619 -0
  65. data/spec/model/paths_spec.rb +348 -0
  66. data/spec/model/relation_spec.rb +255 -0
  67. data/spec/model/validations_spec.rb +45 -0
  68. data/spec/model_spec.rb +55 -0
  69. data/spec/spec_helper.rb +25 -0
  70. data/spec/support/extensions/array.rb +6 -0
  71. data/spec/support/extensions/hash.rb +6 -0
  72. data/spec/support/macros/her_macros.rb +17 -0
  73. data/spec/support/macros/model_macros.rb +36 -0
  74. data/spec/support/macros/request_macros.rb +27 -0
  75. metadata +201 -0
@@ -0,0 +1,337 @@
1
+ module Him
2
+ module Model
3
+ # This module handles all methods related to model attributes
4
+ module Attributes
5
+ extend ActiveSupport::Concern
6
+
7
+ # Initialize a new object with data
8
+ #
9
+ # @param [Hash] attributes The attributes to initialize the object with
10
+ # @option attributes [Hash,Array] :_metadata
11
+ # @option attributes [Hash,Array] :_errors
12
+ # @option attributes [Boolean] :_destroyed
13
+ #
14
+ # @example
15
+ # class User
16
+ # include Him::Model
17
+ # end
18
+ #
19
+ # User.new(name: "Tobias")
20
+ # # => #<User name="Tobias">
21
+ #
22
+ # User.new do |u|
23
+ # u.name = "Tobias"
24
+ # end
25
+ # # => #<User name="Tobias">
26
+ def initialize(attributes = {})
27
+ attributes ||= {}
28
+ @metadata = attributes.delete(:_metadata) || {}
29
+ @response_errors = attributes.delete(:_errors) || {}
30
+ @destroyed = attributes.delete(:_destroyed) || false
31
+ @_her_new_record = attributes.delete(:_new_record) { true }
32
+
33
+ attributes = self.class.default_scope.apply_to(attributes)
34
+ assign_attributes(attributes)
35
+ yield self if block_given?
36
+ run_callbacks :initialize
37
+ end
38
+
39
+ # Ensure dup/clone creates a deep copy of mutable state
40
+ #
41
+ # @private
42
+ def initialize_copy(other)
43
+ super
44
+ @_her_attributes = other.attributes.dup
45
+ @metadata = other.metadata.dup if other.metadata
46
+ @response_errors = other.response_errors.dup if other.response_errors
47
+ end
48
+
49
+ # Handles missing methods
50
+ #
51
+ # @private
52
+ def method_missing(method, *args, &blk)
53
+ if method.to_s =~ /[?=]$/ || @_her_attributes.include?(method)
54
+ # Extract the attribute
55
+ attribute = method.to_s.sub(/[?=]$/, '')
56
+
57
+ # Create a new `attribute` methods set
58
+ self.class.attributes(*attribute)
59
+
60
+ # Resend the method!
61
+ send(method, *args, &blk)
62
+ else
63
+ super
64
+ end
65
+ end
66
+
67
+ # @private
68
+ def respond_to_missing?(method, include_private = false)
69
+ return false if Thread.current[:her_respond_to_missing]
70
+ method.to_s =~ /[?=]$/ || @_her_attributes.include?(method) || super
71
+ end
72
+
73
+ # Check if the model responds to a method without considering
74
+ # method_missing-based dynamic attributes.
75
+ def respond_to_without_missing?(method, include_private = false)
76
+ Thread.current[:her_respond_to_missing] = true
77
+ respond_to?(method, include_private)
78
+ ensure
79
+ Thread.current[:her_respond_to_missing] = false
80
+ end
81
+
82
+ # Assign new attributes to a resource
83
+ #
84
+ # @example
85
+ # class User
86
+ # include Him::Model
87
+ # end
88
+ #
89
+ # user = User.find(1) # => #<User id=1 name="Tobias">
90
+ # user.assign_attributes(name: "Lindsay")
91
+ # user.changes # => { :name => ["Tobias", "Lindsay"] }
92
+ def assign_attributes(new_attributes)
93
+ if !new_attributes.respond_to?(:to_hash)
94
+ raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
95
+ end
96
+
97
+ # Coerce new_attributes to hash in case of strong parameters
98
+ new_attributes = new_attributes.to_hash
99
+
100
+ @_her_attributes ||= attributes
101
+
102
+ # Use setter methods first
103
+ unset_attributes = self.class.use_setter_methods(self, new_attributes)
104
+
105
+ # Then translate attributes of associations into association instances
106
+ associations = self.class.parse_associations(unset_attributes)
107
+
108
+ # Then merge the associations into @_her_attributes.
109
+ @_her_attributes.merge!(associations)
110
+ end
111
+ alias attributes= assign_attributes
112
+
113
+ def attributes
114
+ # The natural choice of instance variable naming here would be
115
+ # `@attributes`. Unfortunately that causes a naming clash when
116
+ # used with `ActiveModel` version >= 5.2.0.
117
+ # As of v5.2.0 `ActiveModel` checks to see if `ActiveRecord`
118
+ # attributes exist, and assumes that if the instance variable
119
+ # `@attributes` exists on the instance, it is because they are
120
+ # `ActiveRecord` attributes.
121
+ @_her_attributes ||= HashWithIndifferentAccess.new
122
+ end
123
+
124
+ # Handles returning true for the accessible attributes
125
+ #
126
+ # @private
127
+ def has_attribute?(attribute_name)
128
+ @_her_attributes.include?(attribute_name)
129
+ end
130
+
131
+ # Handles returning data for a specific attribute
132
+ #
133
+ # @private
134
+ def get_attribute(attribute_name)
135
+ @_her_attributes[attribute_name]
136
+ end
137
+ alias attribute get_attribute
138
+
139
+ # Return the value of the model `primary_key` attribute
140
+ def id
141
+ @_her_attributes[self.class.primary_key]
142
+ end
143
+
144
+ # Return `true` if the other object is also a Him::Model and has matching
145
+ # data
146
+ #
147
+ # @private
148
+ def ==(other)
149
+ other.is_a?(Him::Model) && @_her_attributes == other.attributes
150
+ end
151
+
152
+ # Delegate to the == method
153
+ #
154
+ # @private
155
+ def eql?(other)
156
+ self == other
157
+ end
158
+
159
+ # Delegate to @_her_attributes, allowing models to act correctly in code like:
160
+ # [ Model.find(1), Model.find(1) ].uniq # => [ Model.find(1) ]
161
+ # @private
162
+ def hash
163
+ @_her_attributes.hash
164
+ end
165
+
166
+ # Assign attribute value (ActiveModel convention method).
167
+ #
168
+ # @private
169
+ def attribute=(attribute, value)
170
+ @_her_attributes[attribute] = nil unless @_her_attributes.include?(attribute)
171
+ send("#{attribute}_will_change!") unless value == @_her_attributes[attribute]
172
+ @_her_attributes[attribute] = value
173
+ end
174
+
175
+ # Check attribute value to be present (ActiveModel convention method).
176
+ #
177
+ # @private
178
+ def attribute?(attribute)
179
+ @_her_attributes.include?(attribute) && @_her_attributes[attribute].present?
180
+ end
181
+
182
+ module ClassMethods
183
+ # Initialize a single resource
184
+ #
185
+ # @private
186
+ def instantiate_record(klass, parsed_data)
187
+ if (record = parsed_data[:data]) && record.is_a?(klass)
188
+ record
189
+ else
190
+ attributes = klass.parse(record).merge(_metadata: parsed_data[:metadata],
191
+ _errors: parsed_data[:errors],
192
+ _new_record: false)
193
+ klass.new(attributes).tap do |record_instance|
194
+ record_instance.send :clear_changes_information
195
+ record_instance.run_callbacks :find
196
+ end
197
+ end
198
+ end
199
+
200
+ # Initialize a collection of resources
201
+ #
202
+ # @private
203
+ def instantiate_collection(klass, parsed_data = {})
204
+ raw_data = klass.extract_array(parsed_data)
205
+ raw_data = [] if raw_data.blank?
206
+ records = raw_data.map do |record|
207
+ instantiate_record(klass, data: record)
208
+ end
209
+ Him::Collection.new(records, parsed_data[:metadata], parsed_data[:errors])
210
+ end
211
+
212
+ # Initialize a collection of resources with raw data from an HTTP request
213
+ #
214
+ # @param [Array] parsed_data
215
+ # @private
216
+ def new_collection(parsed_data)
217
+ instantiate_collection(self, parsed_data)
218
+ end
219
+
220
+ # Initialize a new object with the "raw" parsed_data from the parsing middleware
221
+ #
222
+ # @private
223
+ def new_from_parsed_data(parsed_data)
224
+ instantiate_record(self, parsed_data)
225
+ end
226
+
227
+ # Use setter methods of model for each key / value pair in params
228
+ # Return key / value pairs for which no setter method was defined on the
229
+ # model
230
+ #
231
+ # @private
232
+ def use_setter_methods(model, params = {})
233
+ reserved = [:id, :class, :attributes, model.class.primary_key, *model.class.association_keys]
234
+ settable_params = params.reject { |k, _| reserved.include?(k.to_sym) }
235
+ model.class.attributes *settable_params.keys
236
+
237
+ settable_params.each_with_object(params.slice(*reserved)) do |(key, value), memo|
238
+ setter_method = "#{key}="
239
+ if model.respond_to_without_missing?(setter_method)
240
+ model.send setter_method, value
241
+ else
242
+ memo[key.to_sym] = value
243
+ end
244
+ end
245
+ end
246
+
247
+ # Define attribute method matchers to automatically define them using
248
+ # ActiveModel's define_attribute_methods.
249
+ #
250
+ # @private
251
+ def define_attribute_method_matchers
252
+ attribute_method_suffix '='
253
+ attribute_method_suffix '?'
254
+ end
255
+
256
+ # Create a mutex for dynamically generated attribute methods or use one
257
+ # defined by ActiveModel.
258
+ #
259
+ # @private
260
+ def attribute_methods_mutex
261
+ @attribute_methods_mutex ||= begin
262
+ if generated_attribute_methods.respond_to? :mu_synchronize
263
+ generated_attribute_methods
264
+ else
265
+ Mutex.new
266
+ end
267
+ end
268
+ end
269
+
270
+ # Define the attributes that will be used to track dirty attributes and
271
+ # validations
272
+ #
273
+ # @param [Array] attributes
274
+ # @example
275
+ # class User
276
+ # include Him::Model
277
+ # attributes :name, :email
278
+ # end
279
+ def attributes(*attributes)
280
+ attribute_methods_mutex.synchronize do
281
+ define_attribute_methods attributes
282
+ end
283
+ end
284
+
285
+ # Define the accessor in which the API response errors (obtained from
286
+ # the parsing middleware) will be stored
287
+ #
288
+ # @param [Symbol] store_response_errors
289
+ #
290
+ # @example
291
+ # class User
292
+ # include Him::Model
293
+ # store_response_errors :server_errors
294
+ # end
295
+ def store_response_errors(value = nil)
296
+ store_her_data(:response_errors, value)
297
+ end
298
+
299
+ # Define the accessor in which the API response metadata (obtained from
300
+ # the parsing middleware) will be stored
301
+ #
302
+ # @param [Symbol] store_metadata
303
+ #
304
+ # @example
305
+ # class User
306
+ # include Him::Model
307
+ # store_metadata :server_data
308
+ # end
309
+ def store_metadata(value = nil)
310
+ store_her_data(:metadata, value)
311
+ end
312
+
313
+ private
314
+
315
+ # @private
316
+ def store_her_data(name, value)
317
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
318
+ if @_her_store_#{name} && value.present?
319
+ remove_method @_her_store_#{name}.to_sym
320
+ remove_method @_her_store_#{name}.to_s + '='
321
+ end
322
+
323
+ @_her_store_#{name} ||= begin
324
+ superclass.store_#{name} if superclass.respond_to?(:store_#{name})
325
+ end
326
+
327
+ return @_her_store_#{name} unless value
328
+ @_her_store_#{name} = value
329
+
330
+ define_method(value) { @#{name} }
331
+ define_method(value.to_s+'=') { |value| @#{name} = value }
332
+ RUBY
333
+ end
334
+ end
335
+ end
336
+ end
337
+ end
@@ -0,0 +1,33 @@
1
+ module Him
2
+ module Model
3
+ # This module includes basic functionality to Him::Model
4
+ module Base
5
+ extend ActiveSupport::Concern
6
+
7
+ # Returns true if attribute_name is
8
+ # * in resource attributes
9
+ # * an association
10
+ #
11
+ # @private
12
+ def has_key?(attribute_name)
13
+ has_attribute?(attribute_name) ||
14
+ has_association?(attribute_name)
15
+ end
16
+
17
+ # Returns
18
+ # * the value of the attribute_name attribute if it's in orm data
19
+ # * the resource/collection corresponding to attribute_name if it's an association
20
+ #
21
+ # @private
22
+ def [](attribute_name)
23
+ get_attribute(attribute_name) ||
24
+ get_association(attribute_name)
25
+ end
26
+
27
+ # @private
28
+ def singularized_resource_name
29
+ self.class.name.split('::').last.tableize.singularize
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,113 @@
1
+ module Him
2
+ module Model
3
+ # This module interacts with Him::API to fetch HTTP data
4
+ module HTTP
5
+ extend ActiveSupport::Concern
6
+ METHODS = [:get, :post, :put, :patch, :delete, :options]
7
+
8
+ # For each HTTP method, define these class methods:
9
+ #
10
+ # - <method>(path, params)
11
+ # - <method>_raw(path, params, &block)
12
+ # - <method>_collection(path, params, &block)
13
+ # - <method>_resource(path, params, &block)
14
+ # - custom_<method>(*paths)
15
+ #
16
+ # @example
17
+ # class User
18
+ # include Him::Model
19
+ # custom_get :active
20
+ # end
21
+ #
22
+ # User.get(:popular) # GET "/users/popular"
23
+ # User.active # GET "/users/active"
24
+ module ClassMethods
25
+ # Change which API the model will use to make its HTTP requests
26
+ #
27
+ # @example
28
+ # secondary_api = Him::API.new :url => "https://api.example" do |connection|
29
+ # connection.use Faraday::Request::UrlEncoded
30
+ # connection.use Him::Middleware::DefaultParseJSON
31
+ # end
32
+ #
33
+ # class User
34
+ # include Him::Model
35
+ # use_api secondary_api
36
+ # end
37
+ def use_api(value = nil)
38
+ @_her_use_api ||= begin
39
+ superclass.use_api if superclass.respond_to?(:use_api)
40
+ end
41
+
42
+ unless value
43
+ return (@_her_use_api.respond_to? :call) ? @_her_use_api.call : @_her_use_api
44
+ end
45
+
46
+ @_her_use_api = value
47
+ end
48
+
49
+ alias her_api use_api
50
+ alias uses_api use_api
51
+
52
+ # Main request wrapper around Him::API. Used to make custom request to the API.
53
+ #
54
+ # @private
55
+ def request(params = {})
56
+ request = her_api.request(params)
57
+ parsed_data = request[:parsed_data]
58
+ parsed_data = parsed_data.symbolize_keys if parsed_data.respond_to?(:symbolize_keys)
59
+
60
+ if block_given?
61
+ yield parsed_data, request[:response]
62
+ else
63
+ { parsed_data: parsed_data, response: request[:response] }
64
+ end
65
+ end
66
+
67
+ METHODS.each do |method|
68
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
69
+ def #{method}(path, params={})
70
+ path = build_request_path_from_string_or_symbol(path, params)
71
+ params = to_params(params) unless #{method.to_sym.inspect} == :get
72
+ send(:'#{method}_raw', path, params) do |parsed_data, response|
73
+ if parsed_data[:data].is_a?(Array) || json_api_format? || (active_model_serializers_format? && parsed_data[:data].is_a?(Hash) && parsed_data[:data].key?(pluralized_parsed_root_element))
74
+ new_collection(parsed_data)
75
+ else
76
+ new_from_parsed_data(parsed_data)
77
+ end
78
+ end
79
+ end
80
+
81
+ def #{method}_raw(path, params={}, &block)
82
+ path = build_request_path_from_string_or_symbol(path, params)
83
+ request(params.merge(:_method => #{method.to_sym.inspect}, :_path => path), &block)
84
+ end
85
+
86
+ def #{method}_collection(path, params={})
87
+ path = build_request_path_from_string_or_symbol(path, params)
88
+ send(:'#{method}_raw', build_request_path_from_string_or_symbol(path, params), params) do |parsed_data, response|
89
+ new_collection(parsed_data)
90
+ end
91
+ end
92
+
93
+ def #{method}_resource(path, params={})
94
+ path = build_request_path_from_string_or_symbol(path, params)
95
+ send(:"#{method}_raw", path, params) do |parsed_data, response|
96
+ new_from_parsed_data(parsed_data)
97
+ end
98
+ end
99
+
100
+ def custom_#{method}(*paths)
101
+ paths.each do |path|
102
+ singleton_class.send(:define_method, path) do |*params|
103
+ params = params.first || {}
104
+ send(#{method.to_sym.inspect}, path, params)
105
+ end
106
+ end
107
+ end
108
+ RUBY
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,77 @@
1
+ module Him
2
+ module Model
3
+ module Introspection
4
+ extend ActiveSupport::Concern
5
+ # Inspect an element, returns it for introspection.
6
+ #
7
+ # @example
8
+ # class User
9
+ # include Him::Model
10
+ # end
11
+ #
12
+ # @user = User.find(1)
13
+ # p @user # => #<User(/users/1) id=1 name="Tobias Fünke">
14
+ def inspect
15
+ first = Thread.current[:her_inspect_objects].nil?
16
+ Thread.current[:her_inspect_objects] = [] if first
17
+
18
+ resource_path = begin
19
+ request_path
20
+ rescue Him::Errors::PathError => e
21
+ "<unknown path, missing `#{e.missing_parameter}`>"
22
+ end
23
+
24
+ if Thread.current[:her_inspect_objects].include?(object_id)
25
+ "#<#{self.class}(#{resource_path}) ...>"
26
+ else
27
+ Thread.current[:her_inspect_objects] << object_id
28
+ "#<#{self.class}(#{resource_path}) #{attributes.keys.map { |k| "#{k}=#{attribute_for_inspect(send(k))}" }.join(" ")}>"
29
+ end
30
+ ensure
31
+ Thread.current[:her_inspect_objects] = nil if first
32
+ end
33
+
34
+ private
35
+
36
+ def attribute_for_inspect(value)
37
+ if value.is_a?(String) && value.length > 50
38
+ "#{value[0..50]}...".inspect
39
+ elsif value.is_a?(Date) || value.is_a?(Time)
40
+ %("#{value}")
41
+ else
42
+ value.inspect
43
+ end
44
+ end
45
+
46
+ # @private
47
+ module ClassMethods
48
+ # Finds a class at the same level as this one or at the global level.
49
+ #
50
+ # @private
51
+ def her_nearby_class(name)
52
+ her_sibling_class(name) || name.constantize
53
+ end
54
+
55
+ protected
56
+
57
+ # Looks for a class at the same level as this one with the given name.
58
+ #
59
+ # @private
60
+ def her_sibling_class(name)
61
+ if mod = her_containing_module
62
+ @_her_sibling_class ||= Hash.new { Hash.new }
63
+ @_her_sibling_class[mod][name] ||= "#{mod.name}::#{name}".constantize rescue nil
64
+ end
65
+ end
66
+
67
+ # If available, returns the containing Module for this class.
68
+ #
69
+ # @private
70
+ def her_containing_module
71
+ return unless name =~ /::/
72
+ name.split("::")[0..-2].join("::").constantize
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,45 @@
1
+ module Him
2
+ module Model
3
+ module NestedAttributes
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ # Allow nested attributes for an association
8
+ #
9
+ # @example
10
+ # class User
11
+ # include Him::Model
12
+ #
13
+ # has_one :role
14
+ # accepts_nested_attributes_for :role
15
+ # end
16
+ #
17
+ # class Role
18
+ # include Him::Model
19
+ # end
20
+ #
21
+ # user = User.new(name: "Tobias", role_attributes: { title: "moderator" })
22
+ # user.role # => #<Role title="moderator">
23
+ def accepts_nested_attributes_for(*associations)
24
+ allowed_association_names = association_names
25
+
26
+ associations.each do |association_name|
27
+ unless allowed_association_names.include?(association_name)
28
+ raise Him::Errors::AssociationUnknownError, "Unknown association name :#{association_name}"
29
+ end
30
+
31
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
32
+ if method_defined?(:#{association_name}_attributes=)
33
+ remove_method(:#{association_name}_attributes=)
34
+ end
35
+
36
+ def #{association_name}_attributes=(attributes)
37
+ self.#{association_name}.assign_nested_attributes(attributes)
38
+ end
39
+ RUBY
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end