restorm 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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +31 -0
  5. data/.rubocop_todo.yml +232 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +55 -0
  8. data/.yardopts +2 -0
  9. data/CONTRIBUTING.md +26 -0
  10. data/Gemfile +10 -0
  11. data/HER_README.md +1065 -0
  12. data/LICENSE +7 -0
  13. data/README.md +7 -0
  14. data/Rakefile +11 -0
  15. data/UPGRADE.md +101 -0
  16. data/gemfiles/Gemfile.activemodel-4.2 +6 -0
  17. data/gemfiles/Gemfile.activemodel-5.0 +6 -0
  18. data/gemfiles/Gemfile.activemodel-5.1 +6 -0
  19. data/gemfiles/Gemfile.activemodel-5.2 +6 -0
  20. data/gemfiles/Gemfile.faraday-1.0 +6 -0
  21. data/lib/restorm/api.rb +121 -0
  22. data/lib/restorm/collection.rb +13 -0
  23. data/lib/restorm/errors.rb +29 -0
  24. data/lib/restorm/json_api/model.rb +42 -0
  25. data/lib/restorm/middleware/accept_json.rb +18 -0
  26. data/lib/restorm/middleware/first_level_parse_json.rb +37 -0
  27. data/lib/restorm/middleware/json_api_parser.rb +37 -0
  28. data/lib/restorm/middleware/parse_json.rb +22 -0
  29. data/lib/restorm/middleware/second_level_parse_json.rb +37 -0
  30. data/lib/restorm/middleware.rb +12 -0
  31. data/lib/restorm/model/associations/association.rb +128 -0
  32. data/lib/restorm/model/associations/association_proxy.rb +44 -0
  33. data/lib/restorm/model/associations/belongs_to_association.rb +95 -0
  34. data/lib/restorm/model/associations/has_many_association.rb +100 -0
  35. data/lib/restorm/model/associations/has_one_association.rb +79 -0
  36. data/lib/restorm/model/associations.rb +141 -0
  37. data/lib/restorm/model/attributes.rb +322 -0
  38. data/lib/restorm/model/base.rb +33 -0
  39. data/lib/restorm/model/deprecated_methods.rb +61 -0
  40. data/lib/restorm/model/http.rb +119 -0
  41. data/lib/restorm/model/introspection.rb +67 -0
  42. data/lib/restorm/model/nested_attributes.rb +45 -0
  43. data/lib/restorm/model/orm.rb +299 -0
  44. data/lib/restorm/model/parse.rb +223 -0
  45. data/lib/restorm/model/paths.rb +125 -0
  46. data/lib/restorm/model/relation.rb +209 -0
  47. data/lib/restorm/model.rb +75 -0
  48. data/lib/restorm/version.rb +3 -0
  49. data/lib/restorm.rb +19 -0
  50. data/restorm.gemspec +29 -0
  51. data/spec/api_spec.rb +120 -0
  52. data/spec/collection_spec.rb +41 -0
  53. data/spec/json_api/model_spec.rb +169 -0
  54. data/spec/middleware/accept_json_spec.rb +11 -0
  55. data/spec/middleware/first_level_parse_json_spec.rb +63 -0
  56. data/spec/middleware/json_api_parser_spec.rb +52 -0
  57. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  58. data/spec/model/associations/association_proxy_spec.rb +29 -0
  59. data/spec/model/associations_spec.rb +911 -0
  60. data/spec/model/attributes_spec.rb +354 -0
  61. data/spec/model/callbacks_spec.rb +176 -0
  62. data/spec/model/dirty_spec.rb +133 -0
  63. data/spec/model/http_spec.rb +201 -0
  64. data/spec/model/introspection_spec.rb +81 -0
  65. data/spec/model/nested_attributes_spec.rb +135 -0
  66. data/spec/model/orm_spec.rb +704 -0
  67. data/spec/model/parse_spec.rb +520 -0
  68. data/spec/model/paths_spec.rb +348 -0
  69. data/spec/model/relation_spec.rb +247 -0
  70. data/spec/model/validations_spec.rb +43 -0
  71. data/spec/model_spec.rb +45 -0
  72. data/spec/spec_helper.rb +25 -0
  73. data/spec/support/macros/her_macros.rb +17 -0
  74. data/spec/support/macros/model_macros.rb +36 -0
  75. data/spec/support/macros/request_macros.rb +27 -0
  76. metadata +203 -0
@@ -0,0 +1,322 @@
1
+ module Restorm
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 Restorm::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
+
32
+ attributes = self.class.default_scope.apply_to(attributes)
33
+ assign_attributes(attributes)
34
+ yield self if block_given?
35
+ run_callbacks :initialize
36
+ end
37
+
38
+ # Handles missing methods
39
+ #
40
+ # @private
41
+ def method_missing(method, *args, &blk)
42
+ if method.to_s =~ /[?=]$/ || @_restorm_attributes.include?(method)
43
+ # Extract the attribute
44
+ attribute = method.to_s.sub(/[?=]$/, '')
45
+
46
+ # Create a new `attribute` methods set
47
+ self.class.attributes(*attribute)
48
+
49
+ # Resend the method!
50
+ send(method, *args, &blk)
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ # @private
57
+ def respond_to_missing?(method, include_private = false)
58
+ method.to_s =~ /[?=]$/ || @_restorm_attributes.include?(method) || super
59
+ end
60
+
61
+ # Assign new attributes to a resource
62
+ #
63
+ # @example
64
+ # class User
65
+ # include Restorm::Model
66
+ # end
67
+ #
68
+ # user = User.find(1) # => #<User id=1 name="Tobias">
69
+ # user.assign_attributes(name: "Lindsay")
70
+ # user.changes # => { :name => ["Tobias", "Lindsay"] }
71
+ def assign_attributes(new_attributes)
72
+ if !new_attributes.respond_to?(:to_hash)
73
+ raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
74
+ end
75
+
76
+ # Coerce new_attributes to hash in case of strong parameters
77
+ new_attributes = new_attributes.to_hash
78
+
79
+ @_restorm_attributes ||= attributes
80
+
81
+ # Use setter methods first
82
+ unset_attributes = self.class.use_setter_methods(self, new_attributes)
83
+
84
+ # Then translate attributes of associations into association instances
85
+ associations = self.class.parse_associations(unset_attributes)
86
+
87
+ # Then merge the associations into @_restorm_attributes.
88
+ @_restorm_attributes.merge!(associations)
89
+ end
90
+ alias attributes= assign_attributes
91
+
92
+ def attributes
93
+ # The natural choice of instance variable naming here would be
94
+ # `@attributes`. Unfortunately that causes a naming clash when
95
+ # used with `ActiveModel` version >= 5.2.0.
96
+ # As of v5.2.0 `ActiveModel` checks to see if `ActiveRecord`
97
+ # attributes exist, and assumes that if the instance variable
98
+ # `@attributes` exists on the instance, it is because they are
99
+ # `ActiveRecord` attributes.
100
+ @_restorm_attributes ||= HashWithIndifferentAccess.new
101
+ end
102
+
103
+ # Handles returning true for the accessible attributes
104
+ #
105
+ # @private
106
+ def has_attribute?(attribute_name)
107
+ @_restorm_attributes.include?(attribute_name)
108
+ end
109
+
110
+ # Handles returning data for a specific attribute
111
+ #
112
+ # @private
113
+ def get_attribute(attribute_name)
114
+ @_restorm_attributes[attribute_name]
115
+ end
116
+ alias attribute get_attribute
117
+
118
+ # Return the value of the model `primary_key` attribute
119
+ def id
120
+ @_restorm_attributes[self.class.primary_key]
121
+ end
122
+
123
+ # Return `true` if the other object is also a Restorm::Model and has matching
124
+ # data
125
+ #
126
+ # @private
127
+ def ==(other)
128
+ other.is_a?(Restorm::Model) && @_restorm_attributes == other.attributes
129
+ end
130
+
131
+ # Delegate to the == method
132
+ #
133
+ # @private
134
+ def eql?(other)
135
+ self == other
136
+ end
137
+
138
+ # Delegate to @_restorm_attributes, allowing models to act correctly in code like:
139
+ # [ Model.find(1), Model.find(1) ].uniq # => [ Model.find(1) ]
140
+ # @private
141
+ def hash
142
+ @_restorm_attributes.hash
143
+ end
144
+
145
+ # Assign attribute value (ActiveModel convention method).
146
+ #
147
+ # @private
148
+ def attribute=(attribute, value)
149
+ @_restorm_attributes[attribute] = nil unless @_restorm_attributes.include?(attribute)
150
+ send("#{attribute}_will_change!") unless value == @_restorm_attributes[attribute]
151
+ @_restorm_attributes[attribute] = value
152
+ end
153
+
154
+ # Check attribute value to be present (ActiveModel convention method).
155
+ #
156
+ # @private
157
+ def attribute?(attribute)
158
+ @_restorm_attributes.include?(attribute) && @_restorm_attributes[attribute].present?
159
+ end
160
+
161
+ module ClassMethods
162
+ # Initialize a single resource
163
+ #
164
+ # @private
165
+ def instantiate_record(klass, parsed_data)
166
+ if (record = parsed_data[:data]) && record.is_a?(klass)
167
+ record
168
+ else
169
+ attributes = klass.parse(record).merge(_metadata: parsed_data[:metadata],
170
+ _errors: parsed_data[:errors])
171
+ klass.new(attributes).tap do |record_instance|
172
+ record_instance.send :clear_changes_information
173
+ record_instance.run_callbacks :find
174
+ end
175
+ end
176
+ end
177
+
178
+ # Initialize a collection of resources
179
+ #
180
+ # @private
181
+ def instantiate_collection(klass, parsed_data = {})
182
+ records = klass.extract_array(parsed_data).map do |record|
183
+ instantiate_record(klass, data: record)
184
+ end
185
+ Restorm::Collection.new(records, parsed_data[:metadata], parsed_data[:errors])
186
+ end
187
+
188
+ # Initialize a collection of resources with raw data from an HTTP request
189
+ #
190
+ # @param [Array] parsed_data
191
+ # @private
192
+ def new_collection(parsed_data)
193
+ instantiate_collection(self, parsed_data)
194
+ end
195
+
196
+ # Initialize a new object with the "raw" parsed_data from the parsing middleware
197
+ #
198
+ # @private
199
+ def new_from_parsed_data(parsed_data)
200
+ instantiate_record(self, parsed_data)
201
+ end
202
+
203
+ # Use setter methods of model for each key / value pair in params
204
+ # Return key / value pairs for which no setter method was defined on the
205
+ # model
206
+ #
207
+ # @private
208
+ def use_setter_methods(model, params = {})
209
+ reserved = [:id, model.class.primary_key, *model.class.association_keys]
210
+ model.class.attributes *params.keys.reject { |k| reserved.include?(k) }
211
+
212
+ setter_method_names = model.class.setter_method_names
213
+ params.each_with_object({}) do |(key, value), memo|
214
+ setter_method = "#{key}="
215
+ if setter_method_names.include?(setter_method)
216
+ model.send setter_method, value
217
+ else
218
+ memo[key.to_sym] = value
219
+ end
220
+ end
221
+ end
222
+
223
+ # Define attribute method matchers to automatically define them using
224
+ # ActiveModel's define_attribute_methods.
225
+ #
226
+ # @private
227
+ def define_attribute_method_matchers
228
+ attribute_method_suffix '='
229
+ attribute_method_suffix '?'
230
+ end
231
+
232
+ # Create a mutex for dynamically generated attribute methods or use one
233
+ # defined by ActiveModel.
234
+ #
235
+ # @private
236
+ def attribute_methods_mutex
237
+ @attribute_methods_mutex ||= begin
238
+ if generated_attribute_methods.respond_to? :mu_synchronize
239
+ generated_attribute_methods
240
+ else
241
+ Mutex.new
242
+ end
243
+ end
244
+ end
245
+
246
+ # Define the attributes that will be used to track dirty attributes and
247
+ # validations
248
+ #
249
+ # @param [Array] attributes
250
+ # @example
251
+ # class User
252
+ # include Restorm::Model
253
+ # attributes :name, :email
254
+ # end
255
+ def attributes(*attributes)
256
+ attribute_methods_mutex.synchronize do
257
+ define_attribute_methods attributes
258
+ end
259
+ end
260
+
261
+ # Define the accessor in which the API response errors (obtained from
262
+ # the parsing middleware) will be stored
263
+ #
264
+ # @param [Symbol] store_response_errors
265
+ #
266
+ # @example
267
+ # class User
268
+ # include Restorm::Model
269
+ # store_response_errors :server_errors
270
+ # end
271
+ def store_response_errors(value = nil)
272
+ store_restorm_data(:response_errors, value)
273
+ end
274
+
275
+ # Define the accessor in which the API response metadata (obtained from
276
+ # the parsing middleware) will be stored
277
+ #
278
+ # @param [Symbol] store_metadata
279
+ #
280
+ # @example
281
+ # class User
282
+ # include Restorm::Model
283
+ # store_metadata :server_data
284
+ # end
285
+ def store_metadata(value = nil)
286
+ store_restorm_data(:metadata, value)
287
+ end
288
+
289
+ # @private
290
+ def setter_method_names
291
+ @_restorm_setter_method_names ||= begin
292
+ instance_methods.each_with_object(Set.new) do |method, memo|
293
+ memo << method.to_s if method.to_s.end_with?('=')
294
+ end
295
+ end
296
+ end
297
+
298
+ private
299
+
300
+ # @private
301
+ def store_restorm_data(name, value)
302
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
303
+ if @_restorm_store_#{name} && value.present?
304
+ remove_method @_restorm_store_#{name}.to_sym
305
+ remove_method @_restorm_store_#{name}.to_s + '='
306
+ end
307
+
308
+ @_restorm_store_#{name} ||= begin
309
+ superclass.store_#{name} if superclass.respond_to?(:store_#{name})
310
+ end
311
+
312
+ return @_restorm_store_#{name} unless value
313
+ @_restorm_store_#{name} = value
314
+
315
+ define_method(value) { @#{name} }
316
+ define_method(value.to_s+'=') { |value| @#{name} = value }
317
+ RUBY
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,33 @@
1
+ module Restorm
2
+ module Model
3
+ # This module includes basic functionnality to Restorm::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 corrsponding 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,61 @@
1
+ module Restorm
2
+ module Model
3
+ # @private
4
+ module DeprecatedMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ def self.deprecate!(old, new, object, *args)
8
+ line = begin
9
+ raise StandardError
10
+ rescue StandardError => e
11
+ e.backtrace[2]
12
+ end
13
+
14
+ warn "#{line} - The `#{old}` method is deprecated and may be removed soon. Please update your code with `#{new}` instead."
15
+ object.send(new, *args)
16
+ end
17
+
18
+ def data(*args)
19
+ Restorm::Model::DeprecatedMethods.deprecate! :data, :attributes, self, *args
20
+ end
21
+
22
+ def data=(*args)
23
+ Restorm::Model::DeprecatedMethods.deprecate! :data=, :attributes=, self, *args
24
+ end
25
+
26
+ def update_attributes(*args)
27
+ Restorm::Model::DeprecatedMethods.deprecate! :update_attributes, :assign_attributes, self, *args
28
+ end
29
+
30
+ def assign_data(*args)
31
+ Restorm::Model::DeprecatedMethods.deprecate! :assign_data, :assign_attributes, self, *args
32
+ end
33
+
34
+ def has_data?(*args)
35
+ Restorm::Model::DeprecatedMethods.deprecate! :has_data?, :has_attribute?, self, *args
36
+ end
37
+
38
+ def get_data(*args)
39
+ Restorm::Model::DeprecatedMethods.deprecate! :get_data, :get_attribute, self, *args
40
+ end
41
+
42
+ module ClassMethods
43
+ def has_relationship?(*args)
44
+ Restorm::Model::DeprecatedMethods.deprecate! :has_relationship?, :has_association?, self, *args
45
+ end
46
+
47
+ def get_relationship(*args)
48
+ Restorm::Model::DeprecatedMethods.deprecate! :get_relationship, :get_association, self, *args
49
+ end
50
+
51
+ def relationships(*args)
52
+ Restorm::Model::DeprecatedMethods.deprecate! :relationships, :associations, self, *args
53
+ end
54
+
55
+ def her_api(*args)
56
+ Restorm::Model::DeprecatedMethods.deprecate! :restorm_api, :use_api, self, *args
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,119 @@
1
+ module Restorm
2
+ module Model
3
+ # This module interacts with Restorm::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 Restorm::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 = Restorm::API.new :url => "https://api.example" do |connection|
29
+ # connection.use Faraday::Request::UrlEncoded
30
+ # connection.use Restorm::Middleware::DefaultParseJSON
31
+ # end
32
+ #
33
+ # class User
34
+ # include Restorm::Model
35
+ # use_api secondary_api
36
+ # end
37
+ def use_api(value = nil)
38
+ @_restorm_use_api ||= begin
39
+ superclass.use_api if superclass.respond_to?(:use_api)
40
+ end
41
+
42
+ unless value
43
+ return (@_restorm_use_api.respond_to? :call) ? @_restorm_use_api.call : @_restorm_use_api
44
+ end
45
+
46
+ @_restorm_use_api = value
47
+ end
48
+
49
+ alias restorm_api use_api
50
+ alias uses_api use_api
51
+
52
+ # Main request wrapper around Restorm::API. Used to make custom request to the API.
53
+ #
54
+ # @private
55
+ def request(params = {})
56
+ request = restorm_api.request(params)
57
+
58
+ if block_given?
59
+ yield request[:parsed_data], request[:response]
60
+ else
61
+ { :parsed_data => request[:parsed_data], :response => request[:response] }
62
+ end
63
+ end
64
+
65
+ METHODS.each do |method|
66
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
67
+ def #{method}(path, params={})
68
+ path = build_request_path_from_string_or_symbol(path, params)
69
+ params = to_params(params) unless #{method.to_sym.inspect} == :get
70
+ send(:'#{method}_raw', path, params) do |parsed_data, response|
71
+ if parsed_data[:data].is_a?(Array) || active_model_serializers_format? || json_api_format?
72
+ new_collection(parsed_data)
73
+ else
74
+ new_from_parsed_data(parsed_data)
75
+ end
76
+ end
77
+ end
78
+
79
+ def #{method}_raw(path, params={}, &block)
80
+ path = build_request_path_from_string_or_symbol(path, params)
81
+ request(params.merge(:_method => #{method.to_sym.inspect}, :_path => path), &block)
82
+ end
83
+
84
+ def #{method}_collection(path, params={})
85
+ path = build_request_path_from_string_or_symbol(path, params)
86
+ send(:'#{method}_raw', build_request_path_from_string_or_symbol(path, params), params) do |parsed_data, response|
87
+ new_collection(parsed_data)
88
+ end
89
+ end
90
+
91
+ def #{method}_resource(path, params={})
92
+ path = build_request_path_from_string_or_symbol(path, params)
93
+ send(:"#{method}_raw", path, params) do |parsed_data, response|
94
+ new_from_parsed_data(parsed_data)
95
+ end
96
+ end
97
+
98
+ def custom_#{method}(*paths)
99
+ metaclass = (class << self; self; end)
100
+
101
+ # TODO: Remove this check after January 2020
102
+ if paths.last.is_a?(Hash)
103
+ warn("[DEPRECATION] options for custom request methods are deprecated and will be removed on or after January 2020.")
104
+ paths.pop
105
+ end
106
+
107
+ paths.each do |path|
108
+ metaclass.send(:define_method, path) do |*params|
109
+ params = params.first || Hash.new
110
+ send(#{method.to_sym.inspect}, path, params)
111
+ end
112
+ end
113
+ end
114
+ RUBY
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,67 @@
1
+ module Restorm
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 Restorm::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
+ resource_path = begin
16
+ request_path
17
+ rescue Restorm::Errors::PathError => e
18
+ "<unknown path, missing `#{e.missing_parameter}`>"
19
+ end
20
+
21
+ "#<#{self.class}(#{resource_path}) #{attributes.keys.map { |k| "#{k}=#{attribute_for_inspect(send(k))}" }.join(" ")}>"
22
+ end
23
+
24
+ private
25
+
26
+ def attribute_for_inspect(value)
27
+ if value.is_a?(String) && value.length > 50
28
+ "#{value[0..50]}...".inspect
29
+ elsif value.is_a?(Date) || value.is_a?(Time)
30
+ %("#{value}")
31
+ else
32
+ value.inspect
33
+ end
34
+ end
35
+
36
+ # @private
37
+ module ClassMethods
38
+ # Finds a class at the same level as this one or at the global level.
39
+ #
40
+ # @private
41
+ def restorm_nearby_class(name)
42
+ restorm_sibling_class(name) || name.constantize
43
+ end
44
+
45
+ protected
46
+
47
+ # Looks for a class at the same level as this one with the given name.
48
+ #
49
+ # @private
50
+ def restorm_sibling_class(name)
51
+ if mod = restorm_containing_module
52
+ @_restorm_sibling_class ||= Hash.new { Hash.new }
53
+ @_restorm_sibling_class[mod][name] ||= "#{mod.name}::#{name}".constantize rescue nil
54
+ end
55
+ end
56
+
57
+ # If available, returns the containing Module for this class.
58
+ #
59
+ # @private
60
+ def restorm_containing_module
61
+ return unless name =~ /::/
62
+ name.split("::")[0..-2].join("::").constantize
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,45 @@
1
+ module Restorm
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 Restorm::Model
12
+ #
13
+ # has_one :role
14
+ # accepts_nested_attributes_for :role
15
+ # end
16
+ #
17
+ # class Role
18
+ # include Restorm::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 Restorm::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