test_track_rails_client 4.0.0.alpha12 → 4.0.0.alpha13

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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/lib/test_track_rails_client/version.rb +1 -1
  3. data/vendor/gems/fakeable_her/lib/fakeable_her/model.rb +4 -21
  4. data/vendor/gems/her/her.gemspec +5 -6
  5. data/vendor/gems/her/lib/her/api.rb +30 -22
  6. data/vendor/gems/her/lib/her/collection.rb +2 -1
  7. data/vendor/gems/her/lib/her/errors.rb +11 -1
  8. data/vendor/gems/her/lib/her/json_api/model.rb +8 -12
  9. data/vendor/gems/her/lib/her/middleware/accept_json.rb +1 -0
  10. data/vendor/gems/her/lib/her/middleware/first_level_parse_json.rb +6 -5
  11. data/vendor/gems/her/lib/her/middleware/json_api_parser.rb +6 -5
  12. data/vendor/gems/her/lib/her/middleware/parse_json.rb +2 -1
  13. data/vendor/gems/her/lib/her/middleware/second_level_parse_json.rb +6 -5
  14. data/vendor/gems/her/lib/her/middleware.rb +1 -1
  15. data/vendor/gems/her/lib/her/model/associations/association.rb +38 -16
  16. data/vendor/gems/her/lib/her/model/associations/association_proxy.rb +2 -3
  17. data/vendor/gems/her/lib/her/model/associations/belongs_to_association.rb +1 -1
  18. data/vendor/gems/her/lib/her/model/associations/has_many_association.rb +3 -3
  19. data/vendor/gems/her/lib/her/model/associations.rb +6 -6
  20. data/vendor/gems/her/lib/her/model/attributes.rb +121 -92
  21. data/vendor/gems/her/lib/her/model/base.rb +2 -2
  22. data/vendor/gems/her/lib/her/model/http.rb +4 -4
  23. data/vendor/gems/her/lib/her/model/introspection.rb +6 -4
  24. data/vendor/gems/her/lib/her/model/nested_attributes.rb +1 -1
  25. data/vendor/gems/her/lib/her/model/orm.rb +101 -29
  26. data/vendor/gems/her/lib/her/model/parse.rb +35 -28
  27. data/vendor/gems/her/lib/her/model/paths.rb +3 -4
  28. data/vendor/gems/her/lib/her/model/relation.rb +51 -24
  29. data/vendor/gems/her/lib/her/version.rb +1 -1
  30. metadata +2 -2
@@ -18,8 +18,14 @@ module Her
18
18
  # include Her::Model
19
19
  # end
20
20
  #
21
- # User.new(name: "Tobias") # => #<User name="Tobias">
22
- def initialize(attributes={})
21
+ # User.new(name: "Tobias")
22
+ # # => #<User name="Tobias">
23
+ #
24
+ # User.new do |u|
25
+ # u.name = "Tobias"
26
+ # end
27
+ # # => #<User name="Tobias">
28
+ def initialize(attributes = {})
23
29
  attributes ||= {}
24
30
  @metadata = attributes.delete(:_metadata) || {}
25
31
  @response_errors = attributes.delete(:_errors) || {}
@@ -27,70 +33,16 @@ module Her
27
33
 
28
34
  attributes = self.class.default_scope.apply_to(attributes)
29
35
  assign_attributes(attributes)
36
+ yield self if block_given?
30
37
  run_callbacks :initialize
31
38
  end
32
39
 
33
- # Initialize a collection of resources
34
- #
35
- # @private
36
- def self.initialize_collection(klass, parsed_data={})
37
- unless parsed_data[:errors].present?
38
- initialize_resource_collection(klass, parsed_data)
39
- else
40
- initialize_error_collection(parsed_data)
41
- end
42
- end
43
-
44
- def self.initialize_resource_collection(klass, parsed_data={})
45
- collection_data = klass.extract_array(parsed_data).map do |item_data|
46
- if item_data.kind_of?(klass)
47
- resource = item_data
48
- else
49
- resource = klass.new(klass.parse(item_data))
50
- resource.instance_variable_set(:@changed_attributes, {})
51
- resource.run_callbacks :find
52
- end
53
- resource
54
- end
55
- Her::Collection.new(collection_data, parsed_data[:metadata], parsed_data[:errors])
56
- end
57
-
58
- # Initialize an error collection
59
- #
60
- # @private
61
- def self.initialize_error_collection(parsed_data={})
62
- Her::ErrorCollection.new(parsed_data[:metadata], parsed_data[:errors])
63
- end
64
-
65
- # Use setter methods of model for each key / value pair in params
66
- # Return key / value pairs for which no setter method was defined on the model
67
- #
68
- # @private
69
- def self.use_setter_methods(model, params)
70
- params ||= {}
71
-
72
- reserved_keys = [:id, model.class.primary_key] + model.class.association_keys
73
- model.class.attributes *params.keys.reject { |k| reserved_keys.include?(k) || reserved_keys.map(&:to_s).include?(k) }
74
-
75
- setter_method_names = model.class.setter_method_names
76
- params.inject({}) do |memo, (key, value)|
77
- setter_method = key.to_s + '='
78
- if setter_method_names.include?(setter_method)
79
- model.send(setter_method, value)
80
- else
81
- key = key.to_sym if key.is_a?(String)
82
- memo[key] = value
83
- end
84
- memo
85
- end
86
- end
87
-
88
40
  # Handles missing methods
89
41
  #
90
42
  # @private
91
43
  def method_missing(method, *args, &blk)
92
44
  return super if ATTRIBUTE_BLACKLIST.include?(method)
93
- if method.to_s =~ /[?=]$/ || @attributes.include?(method)
45
+ if method.to_s =~ /[?=]$/ || attributes.include?(method)
94
46
  # Extract the attribute
95
47
  attribute = method.to_s.sub(/[?=]$/, '')
96
48
 
@@ -107,7 +59,7 @@ module Her
107
59
  # @private
108
60
  def respond_to_missing?(method, include_private = false)
109
61
  return super if ATTRIBUTE_BLACKLIST.include?(method)
110
- method.to_s.end_with?('=') || method.to_s.end_with?('?') || attributes.include?(method) || super
62
+ method.to_s =~ /[?=]$/ || attributes.include?(method) || super
111
63
  end
112
64
 
113
65
  # Assign new attributes to a resource
@@ -121,47 +73,55 @@ module Her
121
73
  # user.assign_attributes(name: "Lindsay")
122
74
  # user.changes # => { :name => ["Tobias", "Lindsay"] }
123
75
  def assign_attributes(new_attributes)
124
- @attributes ||= attributes
76
+ @_her_attributes ||= attributes
125
77
  # Use setter methods first
126
- unset_attributes = Her::Model::Attributes.use_setter_methods(self, new_attributes)
78
+ unset_attributes = self.class.use_setter_methods(self, new_attributes)
127
79
 
128
80
  # Then translate attributes of associations into association instances
129
- parsed_attributes = self.class.parse_associations(unset_attributes)
81
+ associations = self.class.parse_associations(unset_attributes)
130
82
 
131
- # Then merge the parsed_data into @attributes.
132
- @attributes.merge!(parsed_attributes)
83
+ # Then merge the associations into @_her_attributes.
84
+ @_her_attributes.merge!(associations)
133
85
  end
134
86
  alias attributes= assign_attributes
135
87
 
136
88
  def attributes
137
- @attributes ||= HashWithIndifferentAccess.new
89
+ # The natural choice of instance variable naming here would be
90
+ # `@attributes`. Unfortunately that causes a naming clash when
91
+ # used with `ActiveModel` version >= 5.2.0.
92
+ # As of v5.2.0 `ActiveModel` checks to see if `ActiveRecord`
93
+ # attributes exist, and assumes that if the instance variable
94
+ # `@attributes` exists on the instance, it is because they are
95
+ # `ActiveRecord` attributes.
96
+ @_her_attributes ||= HashWithIndifferentAccess.new
138
97
  end
139
98
 
140
99
  # Handles returning true for the accessible attributes
141
100
  #
142
101
  # @private
143
102
  def has_attribute?(attribute_name)
144
- @attributes.include?(attribute_name)
103
+ @_her_attributes.include?(attribute_name)
145
104
  end
146
105
 
147
106
  # Handles returning data for a specific attribute
148
107
  #
149
108
  # @private
150
109
  def get_attribute(attribute_name)
151
- @attributes[attribute_name]
110
+ @_her_attributes[attribute_name]
152
111
  end
153
112
  alias attribute get_attribute
154
113
 
155
114
  # Return the value of the model `primary_key` attribute
156
115
  def id
157
- @attributes[self.class.primary_key]
116
+ @_her_attributes[self.class.primary_key]
158
117
  end
159
118
 
160
- # Return `true` if the other object is also a Her::Model and has matching data
119
+ # Return `true` if the other object is also a Her::Model and has matching
120
+ # data
161
121
  #
162
122
  # @private
163
123
  def ==(other)
164
- other.is_a?(Her::Model) && @attributes == other.attributes
124
+ other.is_a?(Her::Model) && @_her_attributes == other.attributes
165
125
  end
166
126
 
167
127
  # Delegate to the == method
@@ -171,47 +131,108 @@ module Her
171
131
  self == other
172
132
  end
173
133
 
174
- # Delegate to @attributes, allowing models to act correctly in code like:
134
+ # Delegate to @_her_attributes, allowing models to act correctly in code like:
175
135
  # [ Model.find(1), Model.find(1) ].uniq # => [ Model.find(1) ]
176
136
  # @private
177
137
  def hash
178
- @attributes.hash
138
+ @_her_attributes.hash
179
139
  end
180
140
 
181
141
  # Assign attribute value (ActiveModel convention method).
182
142
  #
183
143
  # @private
184
144
  def attribute=(attribute, value)
185
- @attributes[attribute] = nil unless @attributes.include?(attribute)
186
- self.send(:"#{attribute}_will_change!") if @attributes[attribute] != value
187
- @attributes[attribute] = value
145
+ @_her_attributes[attribute] = nil unless @_her_attributes.include?(attribute)
146
+ send("#{attribute}_will_change!") unless value == @_her_attributes[attribute]
147
+ @_her_attributes[attribute] = value
188
148
  end
189
149
 
190
150
  # Check attribute value to be present (ActiveModel convention method).
191
151
  #
192
152
  # @private
193
153
  def attribute?(attribute)
194
- @attributes.include?(attribute) && @attributes[attribute].present?
154
+ @_her_attributes.include?(attribute) && @_her_attributes[attribute].present?
195
155
  end
196
156
 
197
157
  module ClassMethods
158
+ # Initialize a single resource
159
+ #
160
+ # @private
161
+ def instantiate_record(klass, parsed_data)
162
+ if (record = parsed_data[:data]) && record.is_a?(klass)
163
+ record
164
+ else
165
+ attributes = klass.parse(record).merge(_metadata: parsed_data[:metadata],
166
+ _errors: parsed_data[:errors])
167
+ klass.new(attributes).tap do |record_instance|
168
+ record_instance.send :clear_changes_information
169
+ record_instance.run_callbacks :find
170
+ end
171
+ end
172
+ end
173
+
174
+ # Initialize a collection of resources
175
+ #
176
+ # @private
177
+ def instantiate_collection(klass, parsed_data = {})
178
+ unless parsed_data[:errors].present?
179
+ instantiate_resource_collection(klass, parsed_data)
180
+ else
181
+ instantiate_error_collection(parsed_data)
182
+ end
183
+ end
184
+
185
+ def instantiate_resource_collection(klass, parsed_data = {})
186
+ records = klass.extract_array(parsed_data).map do |record|
187
+ instantiate_record(klass, data: record)
188
+ end
189
+ Her::Collection.new(records, parsed_data[:metadata], parsed_data[:errors])
190
+ end
191
+
192
+ # Initialize an error collection
193
+ #
194
+ # @private
195
+ def instantiate_error_collection(parsed_data={})
196
+ Her::ErrorCollection.new(parsed_data[:metadata], parsed_data[:errors])
197
+ end
198
+
198
199
  # Initialize a collection of resources with raw data from an HTTP request
199
200
  #
200
201
  # @param [Array] parsed_data
201
202
  # @private
202
203
  def new_collection(parsed_data)
203
- Her::Model::Attributes.initialize_collection(self, parsed_data)
204
+ instantiate_collection(self, parsed_data)
204
205
  end
205
206
 
206
207
  # Initialize a new object with the "raw" parsed_data from the parsing middleware
207
208
  #
208
209
  # @private
209
210
  def new_from_parsed_data(parsed_data)
210
- parsed_data = parsed_data.with_indifferent_access
211
- new(parse(parsed_data[:data]).merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
211
+ instantiate_record(self, parsed_data)
212
+ end
213
+
214
+ # Use setter methods of model for each key / value pair in params
215
+ # Return key / value pairs for which no setter method was defined on the
216
+ # model
217
+ #
218
+ # @private
219
+ def use_setter_methods(model, params = {})
220
+ reserved = [:id, model.class.primary_key, *model.class.association_keys]
221
+ model.class.attributes *params.keys.reject { |k| reserved.include?(k) }
222
+
223
+ setter_method_names = model.class.setter_method_names
224
+ params.each_with_object({}) do |(key, value), memo|
225
+ setter_method = "#{key}="
226
+ if setter_method_names.include?(setter_method)
227
+ model.send setter_method, value
228
+ else
229
+ memo[key.to_sym] = value
230
+ end
231
+ end
212
232
  end
213
233
 
214
- # Define attribute method matchers to automatically define them using ActiveModel's define_attribute_methods.
234
+ # Define attribute method matchers to automatically define them using
235
+ # ActiveModel's define_attribute_methods.
215
236
  #
216
237
  # @private
217
238
  def define_attribute_method_matchers
@@ -219,18 +240,22 @@ module Her
219
240
  attribute_method_suffix '?'
220
241
  end
221
242
 
222
- # Create a mutex for dynamically generated attribute methods or use one defined by ActiveModel.
243
+ # Create a mutex for dynamically generated attribute methods or use one
244
+ # defined by ActiveModel.
223
245
  #
224
246
  # @private
225
247
  def attribute_methods_mutex
226
- @attribute_methods_mutex ||= if generated_attribute_methods.respond_to? :mu_synchronize
227
- generated_attribute_methods
228
- else
229
- Mutex.new
230
- end
248
+ @attribute_methods_mutex ||= begin
249
+ if generated_attribute_methods.respond_to? :mu_synchronize
250
+ generated_attribute_methods
251
+ else
252
+ Mutex.new
253
+ end
254
+ end
231
255
  end
232
256
 
233
- # Define the attributes that will be used to track dirty attributes and validations
257
+ # Define the attributes that will be used to track dirty attributes and
258
+ # validations
234
259
  #
235
260
  # @param [Array] attributes
236
261
  # @example
@@ -244,7 +269,8 @@ module Her
244
269
  end
245
270
  end
246
271
 
247
- # Define the accessor in which the API response errors (obtained from the parsing middleware) will be stored
272
+ # Define the accessor in which the API response errors (obtained from
273
+ # the parsing middleware) will be stored
248
274
  #
249
275
  # @param [Symbol] store_response_errors
250
276
  #
@@ -257,7 +283,8 @@ module Her
257
283
  store_her_data(:response_errors, value)
258
284
  end
259
285
 
260
- # Define the accessor in which the API response metadata (obtained from the parsing middleware) will be stored
286
+ # Define the accessor in which the API response metadata (obtained from
287
+ # the parsing middleware) will be stored
261
288
  #
262
289
  # @param [Symbol] store_metadata
263
290
  #
@@ -272,13 +299,15 @@ module Her
272
299
 
273
300
  # @private
274
301
  def setter_method_names
275
- @_her_setter_method_names ||= instance_methods.inject(Set.new) do |memo, method_name|
276
- memo << method_name.to_s if method_name.to_s.end_with?('=')
277
- memo
302
+ @_her_setter_method_names ||= begin
303
+ instance_methods.each_with_object(Set.new) do |method, memo|
304
+ memo << method.to_s if method.to_s.end_with?('=')
305
+ end
278
306
  end
279
307
  end
280
308
 
281
309
  private
310
+
282
311
  # @private
283
312
  def store_her_data(name, value)
284
313
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -11,7 +11,7 @@ module Her
11
11
  # @private
12
12
  def has_key?(attribute_name)
13
13
  has_attribute?(attribute_name) ||
14
- has_association?(attribute_name)
14
+ has_association?(attribute_name)
15
15
  end
16
16
 
17
17
  # Returns
@@ -21,7 +21,7 @@ module Her
21
21
  # @private
22
22
  def [](attribute_name)
23
23
  get_attribute(attribute_name) ||
24
- get_association(attribute_name)
24
+ get_association(attribute_name)
25
25
  end
26
26
 
27
27
  # @private
@@ -3,7 +3,7 @@ module Her
3
3
  # This module interacts with Her::API to fetch HTTP data
4
4
  module HTTP
5
5
  extend ActiveSupport::Concern
6
- METHODS = [:get, :post, :put, :patch, :delete]
6
+ METHODS = [:get, :post, :put, :patch, :delete, :options]
7
7
 
8
8
  # For each HTTP method, define these class methods:
9
9
  #
@@ -52,7 +52,7 @@ module Her
52
52
  # Main request wrapper around Her::API. Used to make custom request to the API.
53
53
  #
54
54
  # @private
55
- def request(params={})
55
+ def request(params = {})
56
56
  request = her_api.request(params)
57
57
  status = request[:response].status
58
58
 
@@ -74,7 +74,7 @@ module Her
74
74
  if parsed_data[:data].is_a?(Array) || active_model_serializers_format? || json_api_format?
75
75
  new_collection(parsed_data)
76
76
  else
77
- new(parse(parsed_data[:data]).merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
77
+ new_from_parsed_data(parsed_data)
78
78
  end
79
79
  end
80
80
  end
@@ -94,7 +94,7 @@ module Her
94
94
  def #{method}_resource(path, params={})
95
95
  path = build_request_path_from_string_or_symbol(path, params)
96
96
  send(:"#{method}_raw", path, params) do |parsed_data, response|
97
- new(parse(parsed_data[:data]).merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
97
+ new_from_parsed_data(parsed_data)
98
98
  end
99
99
  end
100
100
 
@@ -22,6 +22,7 @@ module Her
22
22
  end
23
23
 
24
24
  private
25
+
25
26
  def attribute_for_inspect(value)
26
27
  if value.is_a?(String) && value.length > 50
27
28
  "#{value[0..50]}...".inspect
@@ -38,15 +39,16 @@ module Her
38
39
  #
39
40
  # @private
40
41
  def her_nearby_class(name)
41
- her_sibling_class(name) || name.constantize rescue nil
42
+ her_sibling_class(name) || name.constantize
42
43
  end
43
44
 
44
45
  protected
46
+
45
47
  # Looks for a class at the same level as this one with the given name.
46
48
  #
47
49
  # @private
48
50
  def her_sibling_class(name)
49
- if mod = self.her_containing_module
51
+ if mod = her_containing_module
50
52
  @_her_sibling_class ||= Hash.new { Hash.new }
51
53
  @_her_sibling_class[mod][name] ||= "#{mod.name}::#{name}".constantize rescue nil
52
54
  end
@@ -56,8 +58,8 @@ module Her
56
58
  #
57
59
  # @private
58
60
  def her_containing_module
59
- return unless self.name =~ /::/
60
- self.name.split("::")[0..-2].join("::").constantize
61
+ return unless name =~ /::/
62
+ name.split("::")[0..-2].join("::").constantize
61
63
  end
62
64
  end
63
65
  end
@@ -25,7 +25,7 @@ module Her
25
25
 
26
26
  associations.each do |association_name|
27
27
  unless allowed_association_names.include?(association_name)
28
- raise Her::Errors::AssociationUnknownError.new("Unknown association name :#{association_name}")
28
+ raise Her::Errors::AssociationUnknownError, "Unknown association name :#{association_name}"
29
29
  end
30
30
 
31
31
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+
2
3
  module Her
3
4
  module Model
4
5
  # This module adds ORM-like capabilities to the model
@@ -40,20 +41,13 @@ module Her
40
41
  callback = new? ? :create : :update
41
42
  method = self.class.method_for(callback)
42
43
 
43
- run_callbacks callback do
44
- run_callbacks :save do
45
- params = to_params
44
+ run_callbacks :save do
45
+ run_callbacks callback do
46
46
  self.class.request(to_params.merge(:_method => method, :_path => request_path, :_headers => request_headers, :_options => request_options)) do |parsed_data, response|
47
- assign_attributes(self.class.parse(parsed_data[:data])) if parsed_data[:data].any?
48
- @metadata = parsed_data[:metadata]
49
- @response_errors = parsed_data[:errors]
50
- self.assign_attributes(status_code: response.status)
51
-
47
+ load_from_parsed_data(parsed_data)
48
+ assign_attributes(status_code: response.status)
52
49
  return false if !response.success? || @response_errors.any?
53
- if self.changed_attributes.present?
54
- @previously_changed = self.changed_attributes.clone
55
- self.changed_attributes.clear
56
- end
50
+ changes_applied
57
51
  end
58
52
  end
59
53
  end
@@ -87,9 +81,7 @@ module Her
87
81
  run_callbacks :destroy do
88
82
  self.class.request(params.merge(:_method => method, :_path => request_path, :_headers => request_headers, :_options => request_options)) do |parsed_data, response|
89
83
  raise Her::Errors::ResponseError.for(response.status).new("Request against #{self.class} returned a #{response.status}") if response.status >= 400
90
- assign_attributes(self.class.parse(parsed_data[:data])) if parsed_data[:data].any?
91
- @metadata = parsed_data[:metadata]
92
- @response_errors = parsed_data[:errors]
84
+ load_from_parsed_data(parsed_data)
93
85
  @destroyed = response.success?
94
86
  end
95
87
  end
@@ -97,11 +89,90 @@ module Her
97
89
  end
98
90
 
99
91
  def full_error_messages
100
- self.response_errors.map do |field, messages|
92
+ response_errors.map do |field, messages|
101
93
  "#{field} #{messages.join(', ')}"
102
94
  end.join('. ')
103
95
  end
104
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 substracts 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
+
105
176
  module ClassMethods
106
177
  # Create a new chainable scope
107
178
  #
@@ -121,10 +192,8 @@ module Her
121
192
  instance_exec(*args, &code)
122
193
  end
123
194
 
124
- # Add the scope method to the Relation class
125
- Relation.instance_eval do
126
- define_method(name) { |*args| instance_exec(*args, &code) }
127
- end
195
+ # Add the scope method to the default/blank relation
196
+ scoped.define_singleton_method(name) { |*args| instance_exec(*args, &code) }
128
197
  end
129
198
 
130
199
  # @private
@@ -143,14 +212,15 @@ module Her
143
212
  #
144
213
  # User.all # Called via GET "/users?admin=1"
145
214
  # User.new.admin # => 1
146
- def default_scope(block=nil)
215
+ def default_scope(block = nil)
147
216
  @_her_default_scope ||= (!respond_to?(:default_scope) && superclass.respond_to?(:default_scope)) ? superclass.default_scope : scoped
148
217
  @_her_default_scope = @_her_default_scope.instance_exec(&block) unless block.nil?
149
218
  @_her_default_scope
150
219
  end
151
220
 
152
221
  # Delegate the following methods to `scoped`
153
- [:all, :where, :create, :create!, :build, :find, :find_by, :first, :first_or_create, :first_or_initialize].each do |method|
222
+ [:all, :where, :create, :create!, :build, :find, :find_by, :first, :find_or_create_by,
223
+ :find_or_initialize_by, :first_or_create, :first_or_initialize].each do |method|
154
224
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
155
225
  def #{method}(*params)
156
226
  scoped.send(#{method.to_sym.inspect}, *params)
@@ -184,7 +254,9 @@ module Her
184
254
  data = parse(parsed_data[:data])
185
255
  metadata = parsed_data[:metadata]
186
256
  response_errors = parsed_data[:errors]
187
- new(data.merge(:_destroyed => response.success?, :metadata => metadata, :response_errors => response_errors))
257
+ record = new(data.merge(:_destroyed => response.success?, :metadata => metadata))
258
+ record.response_errors = response_errors
259
+ record
188
260
  end
189
261
  end
190
262
 
@@ -206,23 +278,23 @@ module Her
206
278
  # If the request_new_object_on_build flag is set, the new object is requested via API.
207
279
  def build(attributes = {})
208
280
  params = attributes
209
- return self.new(params) unless self.request_new_object_on_build?
281
+ return new(params) unless request_new_object_on_build?
210
282
 
211
- path = self.build_request_path(params.merge(self.primary_key => 'new'))
212
- method = self.method_for(:new)
283
+ path = build_request_path(params.merge(primary_key => 'new'))
284
+ method = method_for(:new)
213
285
 
214
286
  resource = nil
215
- self.request(params.merge(:_method => method, :_path => path, :_headers => request_headers)) do |parsed_data, response|
287
+ request(params.merge(:_method => method, :_path => path, :_headers => request_headers)) do |parsed_data, response|
216
288
  if response.success?
217
- resource = self.new_from_parsed_data(parsed_data)
289
+ resource = new_from_parsed_data(parsed_data)
218
290
  end
219
291
  end
220
292
  resource
221
293
  end
222
294
 
223
- private
224
295
  # @private
225
296
  def blank_relation
297
+ @blank_relation ||= superclass.blank_relation.clone.tap { |r| r.parent = self } if superclass.respond_to?(:blank_relation)
226
298
  @blank_relation ||= Relation.new(self)
227
299
  end
228
300
  end