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,306 @@
1
+ # coding: utf-8
2
+
3
+ module Him
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
+ @_her_new_record
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
+ path = if new?
43
+ self.class.build_request_path(self.class.collection_path, attributes.except(self.class.primary_key))
44
+ else
45
+ request_path
46
+ end
47
+
48
+ run_callbacks :save do
49
+ run_callbacks callback do
50
+ self.class.request(to_params.merge(_method: method, _path: path)) do |parsed_data, response|
51
+ load_from_parsed_data(parsed_data)
52
+ return false if !response.success? || @response_errors.any?
53
+ @_her_new_record = false
54
+ changes_applied
55
+ end
56
+ end
57
+ end
58
+
59
+ self
60
+ end
61
+
62
+ # Similar to save(), except that ResourceInvalid is raised if the save fails
63
+ def save!
64
+ unless save
65
+ raise Him::Errors::ResourceInvalid, self
66
+ end
67
+ self
68
+ end
69
+
70
+ # Update a resource and return it
71
+ #
72
+ # @example
73
+ # @user = User.find(1)
74
+ # @user.update_attributes(:name => "Tobias Fünke")
75
+ # # Called via PUT "/users/1"
76
+ def update_attributes(attributes)
77
+ assign_attributes(attributes) && save
78
+ end
79
+
80
+ # Destroy a resource
81
+ #
82
+ # @example
83
+ # @user = User.find(1)
84
+ # @user.destroy
85
+ # # Called via DELETE "/users/1"
86
+ def destroy(params = {})
87
+ method = self.class.method_for(:destroy)
88
+ run_callbacks :destroy do
89
+ self.class.request(params.merge(:_method => method, :_path => request_path)) do |parsed_data, response|
90
+ load_from_parsed_data(parsed_data)
91
+ @destroyed = response.success?
92
+ end
93
+ end
94
+ self
95
+ end
96
+
97
+ # Initializes +attribute+ to zero if +nil+ and adds the value passed as
98
+ # +by+ (default is 1). The increment is performed directly on the
99
+ # underlying attribute, no setter is invoked. Only makes sense for
100
+ # number-based attributes. Returns +self+.
101
+ def increment(attribute, by = 1)
102
+ attributes[attribute] ||= 0
103
+ attributes[attribute] += by
104
+ self
105
+ end
106
+
107
+ # Wrapper around #increment that saves the resource. Saving is subjected
108
+ # to validation checks. Returns +self+.
109
+ def increment!(attribute, by = 1)
110
+ increment(attribute, by) && save
111
+ self
112
+ end
113
+
114
+ # Initializes +attribute+ to zero if +nil+ and subtracts the value passed as
115
+ # +by+ (default is 1). The decrement is performed directly on the
116
+ # underlying attribute, no setter is invoked. Only makes sense for
117
+ # number-based attributes. Returns +self+.
118
+ def decrement(attribute, by = 1)
119
+ increment(attribute, -by)
120
+ end
121
+
122
+ # Wrapper around #decrement that saves the resource. Saving is subjected
123
+ # to validation checks. Returns +self+.
124
+ def decrement!(attribute, by = 1)
125
+ increment!(attribute, -by)
126
+ end
127
+
128
+ # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
129
+ # if the predicate returns +true+ the attribute will become +false+. This
130
+ # method toggles directly the underlying value without calling any setter.
131
+ # Returns +self+.
132
+ #
133
+ # @example
134
+ # user = User.first
135
+ # user.admin? # => false
136
+ # user.toggle(:admin)
137
+ # user.admin? # => true
138
+ def toggle(attribute)
139
+ attributes[attribute] = !public_send("#{attribute}?")
140
+ self
141
+ end
142
+
143
+ # Wrapper around #toggle that saves the resource. Saving is subjected to
144
+ # validation checks. Returns +true+ if the record could be saved.
145
+ def toggle!(attribute)
146
+ toggle(attribute) && save
147
+ end
148
+
149
+ # Refetches the resource
150
+ #
151
+ # This method finds the resource by its primary key (which could be
152
+ # assigned manually) and modifies the object in-place.
153
+ #
154
+ # @example
155
+ # user = User.find(1)
156
+ # # => #<User(users/1) id=1 name="Tobias Fünke">
157
+ # user.name = "Oops"
158
+ # user.reload # Fetched again via GET "/users/1"
159
+ # # => #<User(users/1) id=1 name="Tobias Fünke">
160
+ def reload(options = nil)
161
+ fresh_object = self.class.find(id)
162
+ assign_attributes(fresh_object.attributes)
163
+ self
164
+ end
165
+
166
+ # Uses parsed response to assign attributes and metadata
167
+ #
168
+ # @private
169
+ def load_from_parsed_data(parsed_data)
170
+ data = parsed_data[:data]
171
+ assign_attributes(self.class.parse(data)) if data.any?
172
+ @metadata = parsed_data[:metadata]
173
+ @response_errors = parsed_data[:errors]
174
+ end
175
+
176
+ module ClassMethods
177
+ # Create a new chainable scope
178
+ #
179
+ # @example
180
+ # class User
181
+ # include Him::Model
182
+ #
183
+ # scope :admins, lambda { where(:admin => 1) }
184
+ # scope :page, lambda { |page| where(:page => page) }
185
+ # enc
186
+ #
187
+ # User.admins # Called via GET "/users?admin=1"
188
+ # User.page(2).all # Called via GET "/users?page=2"
189
+ def scope(name, code)
190
+ # Add the scope method to the class
191
+ singleton_class.send(:define_method, name) do |*args|
192
+ instance_exec(*args, &code)
193
+ end
194
+
195
+ # Add the scope method to the default/blank relation
196
+ scoped.define_singleton_method(name) { |*args| instance_exec(*args, &code) }
197
+ end
198
+
199
+ # @private
200
+ def scoped
201
+ @_her_default_scope || blank_relation
202
+ end
203
+
204
+ # Define the default scope for the model
205
+ #
206
+ # @example
207
+ # class User
208
+ # include Him::Model
209
+ #
210
+ # default_scope lambda { where(:admin => 1) }
211
+ # enc
212
+ #
213
+ # User.all # Called via GET "/users?admin=1"
214
+ # User.new.admin # => 1
215
+ def default_scope(block = nil)
216
+ @_her_default_scope ||= (!respond_to?(:default_scope) && superclass.respond_to?(:default_scope)) ? superclass.default_scope : scoped
217
+ @_her_default_scope = @_her_default_scope.instance_exec(&block) unless block.nil?
218
+ @_her_default_scope
219
+ end
220
+
221
+ # Delegate the following methods to `scoped`
222
+ [:all, :where, :create, :build, :find, :find_by, :find_or_create_by,
223
+ :find_or_initialize_by, :first_or_create, :first_or_initialize].each do |method|
224
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
225
+ def #{method}(*params)
226
+ scoped.send(#{method.to_sym.inspect}, *params)
227
+ end
228
+ RUBY
229
+ end
230
+
231
+ # Save an existing resource and return it
232
+ #
233
+ # @example
234
+ # @user = User.save_existing(1, { :fullname => "Tobias Fünke" })
235
+ # # Called via PUT "/users/1"
236
+ def save_existing(id, params)
237
+ save_existing!(id, params)
238
+ rescue Him::Errors::ResourceInvalid => e
239
+ e.resource
240
+ end
241
+
242
+ # Similar to .save_existing but raises ResourceInvalid if save fails
243
+ def save_existing!(id, params)
244
+ resource = new(params.merge(primary_key => id, _new_record: false))
245
+ resource.save!
246
+ resource
247
+ end
248
+
249
+ # Destroy an existing resource
250
+ #
251
+ # @example
252
+ # User.destroy_existing(1)
253
+ # # Called via DELETE "/users/1"
254
+ def destroy_existing(id, params = {})
255
+ request(params.merge(:_method => method_for(:destroy), :_path => build_request_path(params.merge(primary_key => id)))) do |parsed_data, response|
256
+ data = parse(parsed_data[:data])
257
+ metadata = parsed_data[:metadata]
258
+ response_errors = parsed_data[:errors]
259
+ record = new(data.merge(:_destroyed => response.success?, :metadata => metadata))
260
+ record.response_errors = response_errors
261
+ record
262
+ end
263
+ end
264
+
265
+ # Return or change the HTTP method used to create or update records
266
+ #
267
+ # @param [Symbol, String] action The behavior in question (`:create` or `:update`)
268
+ # @param [Symbol, String] method The HTTP method to use (`'PUT'`, `:post`, etc.)
269
+ def method_for(action = nil, method = nil)
270
+ @method_for ||= (superclass.respond_to?(:method_for) ? superclass.method_for : {})
271
+ return @method_for if action.nil?
272
+
273
+ action = action.to_s.downcase.to_sym
274
+
275
+ return @method_for[action] if method.nil?
276
+ @method_for[action] = method.to_s.downcase.to_sym
277
+ end
278
+
279
+ # Build a new resource with the given attributes.
280
+ # If the request_new_object_on_build flag is set, the new object is requested via API.
281
+ def build(attributes = {})
282
+ params = attributes
283
+ return new(params) unless request_new_object_on_build?
284
+
285
+ path = build_request_path(params.merge(primary_key => 'new'))
286
+ method = method_for(:new)
287
+
288
+ resource = nil
289
+ request(params.merge(_method: method, _path: path)) do |parsed_data, response|
290
+ if response.success?
291
+ resource = new_from_parsed_data(parsed_data)
292
+ resource.instance_variable_set(:@_her_new_record, true)
293
+ end
294
+ end
295
+ resource
296
+ end
297
+
298
+ # @private
299
+ def blank_relation
300
+ @blank_relation ||= superclass.blank_relation.clone.tap { |r| r.parent = self } if superclass.respond_to?(:blank_relation)
301
+ @blank_relation ||= Relation.new(self)
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,224 @@
1
+ module Him
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 Him::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!(embedded_params(attributes))
46
+
47
+ if her_api.options[:send_only_modified_attributes] && changes.any?
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 embedded_params(attributes)
64
+ associations.values.flatten.each_with_object({}) do |definition, hash|
65
+ value = case association = attributes[definition[:name]]
66
+ when Him::Collection, Array
67
+ association.map { |a| a.to_params }.reject(&:empty?)
68
+ when Him::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 Him::Model
80
+ # include_root_in_json true
81
+ # end
82
+ def include_root_in_json(value, options = {})
83
+ @_her_include_root_in_json = value
84
+ @_her_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 Him::Model
92
+ # parse_root_in_json true
93
+ # end
94
+ #
95
+ # class User
96
+ # include Him::Model
97
+ # parse_root_in_json true, format: :active_model_serializers
98
+ # end
99
+ #
100
+ # class User
101
+ # include Him::Model
102
+ # parse_root_in_json true, format: :json_api
103
+ # end
104
+ def parse_root_in_json(value, options = {})
105
+ @_her_parse_root_in_json = value
106
+ @_her_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 Him::Model
114
+ # request_new_object_on_build true
115
+ # end
116
+ def request_new_object_on_build(value = nil)
117
+ @_her_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 Him::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
+ @_her_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
+ @_her_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 Him::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 Him::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
+ data = request_data[:data]
177
+ if data.is_a?(Hash) && (parse_root_in_json? || active_model_serializers_format? || json_api_format?)
178
+ data[pluralized_parsed_root_element]
179
+ else
180
+ data
181
+ end
182
+ end
183
+
184
+ # @private
185
+ def pluralized_parsed_root_element
186
+ parsed_root_element.to_s.pluralize.to_sym
187
+ end
188
+
189
+ # @private
190
+ def parsed_root_element
191
+ parse_root_in_json? == true ? root_element : parse_root_in_json?
192
+ end
193
+
194
+ # @private
195
+ def active_model_serializers_format?
196
+ @_her_parse_root_in_json_format == :active_model_serializers || (superclass.respond_to?(:active_model_serializers_format?) && superclass.active_model_serializers_format?)
197
+ end
198
+
199
+ # @private
200
+ def json_api_format?
201
+ @_her_parse_root_in_json_format == :json_api || (superclass.respond_to?(:json_api_format?) && superclass.json_api_format?)
202
+ end
203
+
204
+ # @private
205
+ def request_new_object_on_build?
206
+ return @_her_request_new_object_on_build unless @_her_request_new_object_on_build.nil?
207
+ superclass.respond_to?(:request_new_object_on_build?) && superclass.request_new_object_on_build?
208
+ end
209
+
210
+ # @private
211
+ def include_root_in_json?
212
+ return @_her_include_root_in_json unless @_her_include_root_in_json.nil?
213
+ superclass.respond_to?(:include_root_in_json?) && superclass.include_root_in_json?
214
+ end
215
+
216
+ # @private
217
+ def parse_root_in_json?
218
+ return @_her_parse_root_in_json unless @_her_parse_root_in_json.nil?
219
+ superclass.respond_to?(:parse_root_in_json?) && superclass.parse_root_in_json?
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,125 @@
1
+ module Him
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 Him::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 Him::Model
27
+ # primary_key 'UserId'
28
+ # end
29
+ #
30
+ # @param [Symbol] value
31
+ def primary_key(value = nil)
32
+ @_her_primary_key ||= begin
33
+ superclass.primary_key if superclass.respond_to?(:primary_key)
34
+ end
35
+
36
+ return @_her_primary_key unless value
37
+ @_her_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 Him::Model
45
+ # collection_path "/users"
46
+ # end
47
+ def collection_path(path = nil)
48
+ if path.nil?
49
+ @_her_collection_path ||= root_element.to_s.pluralize
50
+ else
51
+ @_her_collection_path = path
52
+ @_her_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 Him::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 Him::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
+ @_her_resource_path ||= "#{root_element.to_s.pluralize}/:id"
82
+ else
83
+ @_her_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(Him::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