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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.rubocop.yml +31 -0
- data/.rubocop_todo.yml +232 -0
- data/.ruby-version +1 -0
- data/.travis.yml +55 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +10 -0
- data/HER_README.md +1065 -0
- data/LICENSE +7 -0
- data/README.md +7 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +101 -0
- data/gemfiles/Gemfile.activemodel-4.2 +6 -0
- data/gemfiles/Gemfile.activemodel-5.0 +6 -0
- data/gemfiles/Gemfile.activemodel-5.1 +6 -0
- data/gemfiles/Gemfile.activemodel-5.2 +6 -0
- data/gemfiles/Gemfile.faraday-1.0 +6 -0
- data/lib/restorm/api.rb +121 -0
- data/lib/restorm/collection.rb +13 -0
- data/lib/restorm/errors.rb +29 -0
- data/lib/restorm/json_api/model.rb +42 -0
- data/lib/restorm/middleware/accept_json.rb +18 -0
- data/lib/restorm/middleware/first_level_parse_json.rb +37 -0
- data/lib/restorm/middleware/json_api_parser.rb +37 -0
- data/lib/restorm/middleware/parse_json.rb +22 -0
- data/lib/restorm/middleware/second_level_parse_json.rb +37 -0
- data/lib/restorm/middleware.rb +12 -0
- data/lib/restorm/model/associations/association.rb +128 -0
- data/lib/restorm/model/associations/association_proxy.rb +44 -0
- data/lib/restorm/model/associations/belongs_to_association.rb +95 -0
- data/lib/restorm/model/associations/has_many_association.rb +100 -0
- data/lib/restorm/model/associations/has_one_association.rb +79 -0
- data/lib/restorm/model/associations.rb +141 -0
- data/lib/restorm/model/attributes.rb +322 -0
- data/lib/restorm/model/base.rb +33 -0
- data/lib/restorm/model/deprecated_methods.rb +61 -0
- data/lib/restorm/model/http.rb +119 -0
- data/lib/restorm/model/introspection.rb +67 -0
- data/lib/restorm/model/nested_attributes.rb +45 -0
- data/lib/restorm/model/orm.rb +299 -0
- data/lib/restorm/model/parse.rb +223 -0
- data/lib/restorm/model/paths.rb +125 -0
- data/lib/restorm/model/relation.rb +209 -0
- data/lib/restorm/model.rb +75 -0
- data/lib/restorm/version.rb +3 -0
- data/lib/restorm.rb +19 -0
- data/restorm.gemspec +29 -0
- data/spec/api_spec.rb +120 -0
- data/spec/collection_spec.rb +41 -0
- data/spec/json_api/model_spec.rb +169 -0
- data/spec/middleware/accept_json_spec.rb +11 -0
- data/spec/middleware/first_level_parse_json_spec.rb +63 -0
- data/spec/middleware/json_api_parser_spec.rb +52 -0
- data/spec/middleware/second_level_parse_json_spec.rb +35 -0
- data/spec/model/associations/association_proxy_spec.rb +29 -0
- data/spec/model/associations_spec.rb +911 -0
- data/spec/model/attributes_spec.rb +354 -0
- data/spec/model/callbacks_spec.rb +176 -0
- data/spec/model/dirty_spec.rb +133 -0
- data/spec/model/http_spec.rb +201 -0
- data/spec/model/introspection_spec.rb +81 -0
- data/spec/model/nested_attributes_spec.rb +135 -0
- data/spec/model/orm_spec.rb +704 -0
- data/spec/model/parse_spec.rb +520 -0
- data/spec/model/paths_spec.rb +348 -0
- data/spec/model/relation_spec.rb +247 -0
- data/spec/model/validations_spec.rb +43 -0
- data/spec/model_spec.rb +45 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/macros/her_macros.rb +17 -0
- data/spec/support/macros/model_macros.rb +36 -0
- data/spec/support/macros/request_macros.rb +27 -0
- 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
|