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,299 @@
1
+ # coding: utf-8
2
+
3
+ module Restorm
4
+ module Model
5
+ # This module adds ORM-like capabilities to the model
6
+ module ORM
7
+ extend ActiveSupport::Concern
8
+
9
+ # Return `true` if a resource was not saved yet
10
+ def new?
11
+ id.nil?
12
+ end
13
+ alias new_record? new?
14
+
15
+ # Return `true` if a resource is not `#new?`
16
+ def persisted?
17
+ !new?
18
+ end
19
+
20
+ # Return whether the object has been destroyed
21
+ def destroyed?
22
+ @destroyed == true
23
+ end
24
+
25
+ # Save a resource and return `false` if the response is not a successful one or
26
+ # if there are errors in the resource. Otherwise, return the newly updated resource
27
+ #
28
+ # @example Save a resource after fetching it
29
+ # @user = User.find(1)
30
+ # # Fetched via GET "/users/1"
31
+ # @user.fullname = "Tobias Fünke"
32
+ # @user.save
33
+ # # Called via PUT "/users/1"
34
+ #
35
+ # @example Save a new resource by creating it
36
+ # @user = User.new({ :fullname => "Tobias Fünke" })
37
+ # @user.save
38
+ # # Called via POST "/users"
39
+ def save
40
+ callback = new? ? :create : :update
41
+ method = self.class.method_for(callback)
42
+
43
+ run_callbacks :save do
44
+ run_callbacks callback do
45
+ self.class.request(to_params.merge(:_method => method, :_path => request_path)) do |parsed_data, response|
46
+ load_from_parsed_data(parsed_data)
47
+ return false if !response.success? || @response_errors.any?
48
+ changes_applied
49
+ end
50
+ end
51
+ end
52
+
53
+ self
54
+ end
55
+
56
+ # Similar to save(), except that ResourceInvalid is raised if the save fails
57
+ def save!
58
+ unless save
59
+ raise Restorm::Errors::ResourceInvalid, self
60
+ end
61
+ self
62
+ end
63
+
64
+ # Update a resource and return it
65
+ #
66
+ # @example
67
+ # @user = User.find(1)
68
+ # @user.update_attributes(:name => "Tobias Fünke")
69
+ # # Called via PUT "/users/1"
70
+ def update_attributes(attributes)
71
+ assign_attributes(attributes) && save
72
+ end
73
+
74
+ # Destroy a resource
75
+ #
76
+ # @example
77
+ # @user = User.find(1)
78
+ # @user.destroy
79
+ # # Called via DELETE "/users/1"
80
+ def destroy(params = {})
81
+ method = self.class.method_for(:destroy)
82
+ run_callbacks :destroy do
83
+ self.class.request(params.merge(:_method => method, :_path => request_path)) do |parsed_data, response|
84
+ load_from_parsed_data(parsed_data)
85
+ @destroyed = response.success?
86
+ end
87
+ end
88
+ self
89
+ end
90
+
91
+ # Initializes +attribute+ to zero if +nil+ and adds the value passed as
92
+ # +by+ (default is 1). The increment is performed directly on the
93
+ # underlying attribute, no setter is invoked. Only makes sense for
94
+ # number-based attributes. Returns +self+.
95
+ def increment(attribute, by = 1)
96
+ attributes[attribute] ||= 0
97
+ attributes[attribute] += by
98
+ self
99
+ end
100
+
101
+ # Wrapper around #increment that saves the resource. Saving is subjected
102
+ # to validation checks. Returns +self+.
103
+ def increment!(attribute, by = 1)
104
+ increment(attribute, by) && save
105
+ self
106
+ end
107
+
108
+ # Initializes +attribute+ to zero if +nil+ and substracts the value passed as
109
+ # +by+ (default is 1). The decrement is performed directly on the
110
+ # underlying attribute, no setter is invoked. Only makes sense for
111
+ # number-based attributes. Returns +self+.
112
+ def decrement(attribute, by = 1)
113
+ increment(attribute, -by)
114
+ end
115
+
116
+ # Wrapper around #decrement that saves the resource. Saving is subjected
117
+ # to validation checks. Returns +self+.
118
+ def decrement!(attribute, by = 1)
119
+ increment!(attribute, -by)
120
+ end
121
+
122
+ # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
123
+ # if the predicate returns +true+ the attribute will become +false+. This
124
+ # method toggles directly the underlying value without calling any setter.
125
+ # Returns +self+.
126
+ #
127
+ # @example
128
+ # user = User.first
129
+ # user.admin? # => false
130
+ # user.toggle(:admin)
131
+ # user.admin? # => true
132
+ def toggle(attribute)
133
+ attributes[attribute] = !public_send("#{attribute}?")
134
+ self
135
+ end
136
+
137
+ # Wrapper around #toggle that saves the resource. Saving is subjected to
138
+ # validation checks. Returns +true+ if the record could be saved.
139
+ def toggle!(attribute)
140
+ toggle(attribute) && save
141
+ end
142
+
143
+ # Refetches the resource
144
+ #
145
+ # This method finds the resource by its primary key (which could be
146
+ # assigned manually) and modifies the object in-place.
147
+ #
148
+ # @example
149
+ # user = User.find(1)
150
+ # # => #<User(users/1) id=1 name="Tobias Fünke">
151
+ # user.name = "Oops"
152
+ # user.reload # Fetched again via GET "/users/1"
153
+ # # => #<User(users/1) id=1 name="Tobias Fünke">
154
+ def reload(options = nil)
155
+ fresh_object = self.class.find(id)
156
+ assign_attributes(fresh_object.attributes)
157
+ self
158
+ end
159
+
160
+ # Uses parsed response to assign attributes and metadata
161
+ #
162
+ # @private
163
+ def load_from_parsed_data(parsed_data)
164
+ data = parsed_data[:data]
165
+ assign_attributes(self.class.parse(data)) if data.any?
166
+ @metadata = parsed_data[:metadata]
167
+ @response_errors = parsed_data[:errors]
168
+ end
169
+
170
+ module ClassMethods
171
+ # Create a new chainable scope
172
+ #
173
+ # @example
174
+ # class User
175
+ # include Restorm::Model
176
+ #
177
+ # scope :admins, lambda { where(:admin => 1) }
178
+ # scope :page, lambda { |page| where(:page => page) }
179
+ # enc
180
+ #
181
+ # User.admins # Called via GET "/users?admin=1"
182
+ # User.page(2).all # Called via GET "/users?page=2"
183
+ def scope(name, code)
184
+ # Add the scope method to the class
185
+ (class << self; self end).send(:define_method, name) do |*args|
186
+ instance_exec(*args, &code)
187
+ end
188
+
189
+ # Add the scope method to the default/blank relation
190
+ scoped.define_singleton_method(name) { |*args| instance_exec(*args, &code) }
191
+ end
192
+
193
+ # @private
194
+ def scoped
195
+ @_restorm_default_scope || blank_relation
196
+ end
197
+
198
+ # Define the default scope for the model
199
+ #
200
+ # @example
201
+ # class User
202
+ # include Restorm::Model
203
+ #
204
+ # default_scope lambda { where(:admin => 1) }
205
+ # enc
206
+ #
207
+ # User.all # Called via GET "/users?admin=1"
208
+ # User.new.admin # => 1
209
+ def default_scope(block = nil)
210
+ @_restorm_default_scope ||= (!respond_to?(:default_scope) && superclass.respond_to?(:default_scope)) ? superclass.default_scope : scoped
211
+ @_restorm_default_scope = @_restorm_default_scope.instance_exec(&block) unless block.nil?
212
+ @_restorm_default_scope
213
+ end
214
+
215
+ # Delegate the following methods to `scoped`
216
+ [:all, :where, :create, :build, :find, :find_by, :find_or_create_by,
217
+ :find_or_initialize_by, :first_or_create, :first_or_initialize].each do |method|
218
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
219
+ def #{method}(*params)
220
+ scoped.send(#{method.to_sym.inspect}, *params)
221
+ end
222
+ RUBY
223
+ end
224
+
225
+ # Save an existing resource and return it
226
+ #
227
+ # @example
228
+ # @user = User.save_existing(1, { :fullname => "Tobias Fünke" })
229
+ # # Called via PUT "/users/1"
230
+ def save_existing(id, params)
231
+ save_existing!(id, params)
232
+ rescue Restorm::Errors::ResourceInvalid => e
233
+ e.resource
234
+ end
235
+
236
+ # Similar to .save_existing but raises ResourceInvalid if save fails
237
+ def save_existing!(id, params)
238
+ resource = new(params.merge(primary_key => id))
239
+ resource.save!
240
+ resource
241
+ end
242
+
243
+ # Destroy an existing resource
244
+ #
245
+ # @example
246
+ # User.destroy_existing(1)
247
+ # # Called via DELETE "/users/1"
248
+ def destroy_existing(id, params = {})
249
+ request(params.merge(:_method => method_for(:destroy), :_path => build_request_path(params.merge(primary_key => id)))) do |parsed_data, response|
250
+ data = parse(parsed_data[:data])
251
+ metadata = parsed_data[:metadata]
252
+ response_errors = parsed_data[:errors]
253
+ record = new(data.merge(:_destroyed => response.success?, :metadata => metadata))
254
+ record.response_errors = response_errors
255
+ record
256
+ end
257
+ end
258
+
259
+ # Return or change the HTTP method used to create or update records
260
+ #
261
+ # @param [Symbol, String] action The behavior in question (`:create` or `:update`)
262
+ # @param [Symbol, String] method The HTTP method to use (`'PUT'`, `:post`, etc.)
263
+ def method_for(action = nil, method = nil)
264
+ @method_for ||= (superclass.respond_to?(:method_for) ? superclass.method_for : {})
265
+ return @method_for if action.nil?
266
+
267
+ action = action.to_s.downcase.to_sym
268
+
269
+ return @method_for[action] if method.nil?
270
+ @method_for[action] = method.to_s.downcase.to_sym
271
+ end
272
+
273
+ # Build a new resource with the given attributes.
274
+ # If the request_new_object_on_build flag is set, the new object is requested via API.
275
+ def build(attributes = {})
276
+ params = attributes
277
+ return new(params) unless request_new_object_on_build?
278
+
279
+ path = build_request_path(params.merge(primary_key => 'new'))
280
+ method = method_for(:new)
281
+
282
+ resource = nil
283
+ request(params.merge(:_method => method, :_path => path)) do |parsed_data, response|
284
+ if response.success?
285
+ resource = new_from_parsed_data(parsed_data)
286
+ end
287
+ end
288
+ resource
289
+ end
290
+
291
+ # @private
292
+ def blank_relation
293
+ @blank_relation ||= superclass.blank_relation.clone.tap { |r| r.parent = self } if superclass.respond_to?(:blank_relation)
294
+ @blank_relation ||= Relation.new(self)
295
+ end
296
+ end
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,223 @@
1
+ module Restorm
2
+ module Model
3
+ # This module handles resource data parsing at the model level (after the parsing middleware)
4
+ module Parse
5
+ extend ActiveSupport::Concern
6
+
7
+ # Convert into a hash of request parameters, based on `include_root_in_json`.
8
+ #
9
+ # @example
10
+ # @user.to_params
11
+ # # => { :id => 1, :name => 'John Smith' }
12
+ def to_params
13
+ self.class.to_params(attributes, changes)
14
+ end
15
+
16
+ module ClassMethods
17
+ # Parse data before assigning it to a resource, based on `parse_root_in_json`.
18
+ #
19
+ # @param [Hash] data
20
+ # @private
21
+ def parse(data)
22
+ if parse_root_in_json? && root_element_included?(data)
23
+ if json_api_format?
24
+ data.fetch(parsed_root_element).first
25
+ else
26
+ data.fetch(parsed_root_element) { data }
27
+ end
28
+ else
29
+ data
30
+ end
31
+ end
32
+
33
+ # @private
34
+ def to_params(attributes, changes = {})
35
+ filtered_attributes = attributes.each_with_object({}) do |(key, value), memo|
36
+ case value
37
+ when Restorm::Model
38
+ when ActiveModel::Serialization
39
+ value = value.serializable_hash.symbolize_keys
40
+ end
41
+
42
+ memo[key.to_sym] = value
43
+ end
44
+
45
+ filtered_attributes.merge!(embeded_params(attributes))
46
+
47
+ if restorm_api.options[:send_only_modified_attributes]
48
+ filtered_attributes.slice! *changes.keys.map(&:to_sym)
49
+ end
50
+
51
+ if include_root_in_json?
52
+ if json_api_format?
53
+ { included_root_element => [filtered_attributes] }
54
+ else
55
+ { included_root_element => filtered_attributes }
56
+ end
57
+ else
58
+ filtered_attributes
59
+ end
60
+ end
61
+
62
+ # @private
63
+ def embeded_params(attributes)
64
+ associations.values.flatten.each_with_object({}) do |definition, hash|
65
+ value = case association = attributes[definition[:name]]
66
+ when Restorm::Collection, Array
67
+ association.map { |a| a.to_params }.reject(&:empty?)
68
+ when Restorm::Model
69
+ association.to_params
70
+ end
71
+ hash[definition[:data_key]] = value if value.present?
72
+ end
73
+ end
74
+
75
+ # Return or change the value of `include_root_in_json`
76
+ #
77
+ # @example
78
+ # class User
79
+ # include Restorm::Model
80
+ # include_root_in_json true
81
+ # end
82
+ def include_root_in_json(value, options = {})
83
+ @_restorm_include_root_in_json = value
84
+ @_restorm_include_root_in_json_format = options[:format]
85
+ end
86
+
87
+ # Return or change the value of `parse_root_in_json`
88
+ #
89
+ # @example
90
+ # class User
91
+ # include Restorm::Model
92
+ # parse_root_in_json true
93
+ # end
94
+ #
95
+ # class User
96
+ # include Restorm::Model
97
+ # parse_root_in_json true, format: :active_model_serializers
98
+ # end
99
+ #
100
+ # class User
101
+ # include Restorm::Model
102
+ # parse_root_in_json true, format: :json_api
103
+ # end
104
+ def parse_root_in_json(value, options = {})
105
+ @_restorm_parse_root_in_json = value
106
+ @_restorm_parse_root_in_json_format = options[:format]
107
+ end
108
+
109
+ # Return or change the value of `request_new_object_on_build`
110
+ #
111
+ # @example
112
+ # class User
113
+ # include Restorm::Model
114
+ # request_new_object_on_build true
115
+ # end
116
+ def request_new_object_on_build(value = nil)
117
+ @_restorm_request_new_object_on_build = value
118
+ end
119
+
120
+ # Return or change the value of `root_element`. Always defaults to the base name of the class.
121
+ #
122
+ # @example
123
+ # class User
124
+ # include Restorm::Model
125
+ # parse_root_in_json true
126
+ # root_element :huh
127
+ # end
128
+ #
129
+ # user = User.find(1) # { :huh => { :id => 1, :name => "Tobias" } }
130
+ # user.name # => "Tobias"
131
+ def root_element(value = nil)
132
+ if value.nil?
133
+ @_retorm_root_element ||= if json_api_format?
134
+ name.split("::").last.pluralize.underscore.to_sym
135
+ else
136
+ name.split("::").last.underscore.to_sym
137
+ end
138
+ else
139
+ @_restorm_root_element = value.to_sym
140
+ end
141
+ end
142
+
143
+ # @private
144
+ def root_element_included?(data)
145
+ element = data[parsed_root_element]
146
+ element.is_a?(Hash) || element.is_a?(Array)
147
+ end
148
+
149
+ # @private
150
+ def included_root_element
151
+ include_root_in_json? == true ? root_element : include_root_in_json?
152
+ end
153
+
154
+ # Extract an array from the request data
155
+ #
156
+ # @example
157
+ # # with parse_root_in_json true, :format => :active_model_serializers
158
+ # class User
159
+ # include Restorm::Model
160
+ # parse_root_in_json true, :format => :active_model_serializers
161
+ # end
162
+ #
163
+ # users = User.all # { :users => [ { :id => 1, :name => "Tobias" } ] }
164
+ # users.first.name # => "Tobias"
165
+ #
166
+ # # without parse_root_in_json
167
+ # class User
168
+ # include Restorm::Model
169
+ # end
170
+ #
171
+ # users = User.all # [ { :id => 1, :name => "Tobias" } ]
172
+ # users.first.name # => "Tobias"
173
+ #
174
+ # @private
175
+ def extract_array(request_data)
176
+ if request_data[:data].is_a?(Hash) && (active_model_serializers_format? || json_api_format?)
177
+ request_data[:data][pluralized_parsed_root_element]
178
+ else
179
+ request_data[:data]
180
+ end
181
+ end
182
+
183
+ # @private
184
+ def pluralized_parsed_root_element
185
+ parsed_root_element.to_s.pluralize.to_sym
186
+ end
187
+
188
+ # @private
189
+ def parsed_root_element
190
+ parse_root_in_json? == true ? root_element : parse_root_in_json?
191
+ end
192
+
193
+ # @private
194
+ def active_model_serializers_format?
195
+ @_restorm_parse_root_in_json_format == :active_model_serializers || (superclass.respond_to?(:active_model_serializers_format?) && superclass.active_model_serializers_format?)
196
+ end
197
+
198
+ # @private
199
+ def json_api_format?
200
+ @_restorm_parse_root_in_json_format == :json_api || (superclass.respond_to?(:json_api_format?) && superclass.json_api_format?)
201
+ end
202
+
203
+ # @private
204
+ def request_new_object_on_build?
205
+ return @_restorm_request_new_object_on_build unless @_restorm_request_new_object_on_build.nil?
206
+ superclass.respond_to?(:request_new_object_on_build?) && superclass.request_new_object_on_build?
207
+ end
208
+
209
+ # @private
210
+ def include_root_in_json?
211
+ return @_restorm_include_root_in_json unless @_restorm_include_root_in_json.nil?
212
+ superclass.respond_to?(:include_root_in_json?) && superclass.include_root_in_json?
213
+ end
214
+
215
+ # @private
216
+ def parse_root_in_json?
217
+ return @_restorm_parse_root_in_json unless @_restorm_parse_root_in_json.nil?
218
+ superclass.respond_to?(:parse_root_in_json?) && superclass.parse_root_in_json?
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,125 @@
1
+ module Restorm
2
+ module Model
3
+ module Paths
4
+ extend ActiveSupport::Concern
5
+ # Return a path based on the collection path and a resource data
6
+ #
7
+ # @example
8
+ # class User
9
+ # include Restorm::Model
10
+ # collection_path "/utilisateurs"
11
+ # end
12
+ #
13
+ # User.find(1) # Fetched via GET /utilisateurs/1
14
+ #
15
+ # @param [Hash] params An optional set of additional parameters for
16
+ # path construction. These will not override attributes of the resource.
17
+ def request_path(params = {})
18
+ self.class.build_request_path(params.merge(attributes.dup))
19
+ end
20
+
21
+ module ClassMethods
22
+ # Define the primary key field that will be used to find and save records
23
+ #
24
+ # @example
25
+ # class User
26
+ # include Restorm::Model
27
+ # primary_key 'UserId'
28
+ # end
29
+ #
30
+ # @param [Symbol] value
31
+ def primary_key(value = nil)
32
+ @_restorm_primary_key ||= begin
33
+ superclass.primary_key if superclass.respond_to?(:primary_key)
34
+ end
35
+
36
+ return @_restorm_primary_key unless value
37
+ @_restorm_primary_key = value.to_sym
38
+ end
39
+
40
+ # Defines a custom collection path for the resource
41
+ #
42
+ # @example
43
+ # class User
44
+ # include Restorm::Model
45
+ # collection_path "/users"
46
+ # end
47
+ def collection_path(path = nil)
48
+ if path.nil?
49
+ @_restorm_collection_path ||= root_element.to_s.pluralize
50
+ else
51
+ @_restorm_collection_path = path
52
+ @_restorm_resource_path = "#{path}/:id"
53
+ end
54
+ end
55
+
56
+ # Defines a custom resource path for the resource
57
+ #
58
+ # @example
59
+ # class User
60
+ # include Restorm::Model
61
+ # resource_path "/users/:id"
62
+ # end
63
+ #
64
+ # Note that, if used in combination with resource_path, you may specify
65
+ # either the real primary key or the string ':id'. For example:
66
+ #
67
+ # @example
68
+ # class User
69
+ # include Restorm::Model
70
+ # primary_key 'user_id'
71
+ #
72
+ # # This works because we'll have a user_id attribute
73
+ # resource_path '/users/:user_id'
74
+ #
75
+ # # This works because we replace :id with :user_id
76
+ # resource_path '/users/:id'
77
+ # end
78
+ #
79
+ def resource_path(path = nil)
80
+ if path.nil?
81
+ @_restorm_resource_path ||= "#{root_element.to_s.pluralize}/:id"
82
+ else
83
+ @_restorm_resource_path = path
84
+ end
85
+ end
86
+
87
+ # Return a custom path based on the collection path and variable parameters
88
+ #
89
+ # @private
90
+ def build_request_path(path = nil, parameters = {})
91
+ parameters = parameters.try(:with_indifferent_access)
92
+
93
+ unless path.is_a?(String)
94
+ parameters = path.try(:with_indifferent_access) || parameters
95
+ path =
96
+ if parameters.include?(primary_key) && parameters[primary_key] && !parameters[primary_key].is_a?(Array)
97
+ resource_path.dup
98
+ else
99
+ collection_path.dup
100
+ end
101
+
102
+ # Replace :id with our actual primary key
103
+ path.gsub!(/(\A|\/):id(\Z|\/)/, "\\1:#{primary_key}\\2")
104
+ end
105
+
106
+ path.gsub(/:([\w_]+)/) do
107
+ # Look for :key or :_key, otherwise raise an exception
108
+ key = $1.to_sym
109
+ value = parameters.delete(key) || parameters.delete(:"_#{key}")
110
+ if value
111
+ Faraday::Utils.escape value
112
+ else
113
+ raise(Restorm::Errors::PathError.new("Missing :_#{$1} parameter to build the request path. Path is `#{path}`. Parameters are `#{parameters.symbolize_keys.inspect}`.", $1))
114
+ end
115
+ end
116
+ end
117
+
118
+ # @private
119
+ def build_request_path_from_string_or_symbol(path, params = {})
120
+ path.is_a?(Symbol) ? "#{build_request_path(params)}/#{path}" : path
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end