parse_resource 1.7.3 → 1.8.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 (65) hide show
  1. data/.DS_Store +0 -0
  2. data/.travis.yml +1 -1
  3. data/Gemfile +4 -8
  4. data/Gemfile.lock +27 -23
  5. data/README.md +81 -2
  6. data/Rakefile +9 -8
  7. data/VERSION +1 -1
  8. data/fixtures/.DS_Store +0 -0
  9. data/fixtures/vcr_cassettes/.DS_Store +0 -0
  10. data/fixtures/vcr_cassettes/test_all.yml +319 -34
  11. data/fixtures/vcr_cassettes/test_attribute_getters.yml +256 -12
  12. data/fixtures/vcr_cassettes/test_attribute_setters.yml +256 -12
  13. data/fixtures/vcr_cassettes/test_authenticate.yml +260 -32
  14. data/fixtures/vcr_cassettes/test_chained_wheres.yml +320 -35
  15. data/fixtures/vcr_cassettes/test_chunk.yml +1359 -0
  16. data/fixtures/vcr_cassettes/test_count.yml +495 -84
  17. data/fixtures/vcr_cassettes/test_create.yml +154 -12
  18. data/fixtures/vcr_cassettes/test_created_at.yml +256 -12
  19. data/fixtures/vcr_cassettes/test_destroy.yml +364 -32
  20. data/fixtures/vcr_cassettes/test_destroy_all.yml +236 -48
  21. data/fixtures/vcr_cassettes/test_each.yml +488 -56
  22. data/fixtures/vcr_cassettes/test_fetching_closest_10.yml +1509 -0
  23. data/fixtures/vcr_cassettes/test_fetching_closest_by_kilometers.yml +1509 -0
  24. data/fixtures/vcr_cassettes/test_fetching_closest_by_miles.yml +1509 -0
  25. data/fixtures/vcr_cassettes/test_fetching_closest_by_radians.yml +1509 -0
  26. data/fixtures/vcr_cassettes/test_fetching_closest_within_box.yml +489 -0
  27. data/fixtures/vcr_cassettes/test_fetching_geopoint_field.yml +489 -0
  28. data/fixtures/vcr_cassettes/test_find.yml +312 -24
  29. data/fixtures/vcr_cassettes/test_find_all_by.yml +170 -34
  30. data/fixtures/vcr_cassettes/test_find_by.yml +174 -38
  31. data/fixtures/vcr_cassettes/test_first.yml +260 -23
  32. data/fixtures/vcr_cassettes/test_id.yml +256 -12
  33. data/fixtures/vcr_cassettes/test_installation_creation.yml +199 -0
  34. data/fixtures/vcr_cassettes/test_installation_creation_validation_check.yml +297 -0
  35. data/fixtures/vcr_cassettes/test_limit.yml +1138 -179
  36. data/fixtures/vcr_cassettes/test_map.yml +488 -56
  37. data/fixtures/vcr_cassettes/test_order_ascending.yml +395 -0
  38. data/fixtures/vcr_cassettes/test_order_descending.yml +446 -0
  39. data/fixtures/vcr_cassettes/test_save.yml +316 -24
  40. data/fixtures/vcr_cassettes/test_save_all_and_destroy_all.yml +869 -0
  41. data/fixtures/vcr_cassettes/test_saving_geo_point_with_quick_init.yml +395 -0
  42. data/fixtures/vcr_cassettes/test_saving_geopoint_with_coords.yml +395 -0
  43. data/fixtures/vcr_cassettes/test_skip.yml +120 -525
  44. data/fixtures/vcr_cassettes/test_update.yml +316 -24
  45. data/fixtures/vcr_cassettes/test_updated_at.yml +316 -24
  46. data/fixtures/vcr_cassettes/test_username_should_be_unique.yml +311 -21
  47. data/fixtures/vcr_cassettes/test_where.yml +117 -25
  48. data/lib/kaminari_extension.rb +60 -0
  49. data/lib/parse_resource.rb +4 -2
  50. data/lib/parse_resource/base.rb +262 -163
  51. data/lib/parse_resource/client.rb +8 -0
  52. data/lib/parse_resource/parse_error.rb +36 -22
  53. data/lib/parse_resource/query.rb +99 -7
  54. data/lib/parse_resource/query_methods.rb +64 -0
  55. data/lib/parse_resource/types/parse_geopoint.rb +19 -0
  56. data/parse_resource.gemspec +29 -9
  57. data/parse_resource.yml +2 -2
  58. data/test/active_model_lint_test.rb +0 -2
  59. data/test/helper.rb +13 -3
  60. data/test/test_parse_installation.rb +41 -0
  61. data/test/test_parse_resource.rb +108 -20
  62. data/test/test_parse_user.rb +4 -7
  63. data/test/test_query_options.rb +0 -38
  64. data/test/test_types.rb +186 -0
  65. metadata +38 -31
@@ -0,0 +1,60 @@
1
+ if defined?(Kaminari)
2
+ module KaminariExtension
3
+ module ParseBaseExt
4
+ extend ActiveSupport::Concern
5
+ include Kaminari::ConfigurationMethods
6
+
7
+ module ClassMethods
8
+ def page(num)
9
+ Query.new(self).page(num)
10
+ end
11
+ end
12
+ end
13
+
14
+ module QueryExt
15
+ extend ActiveSupport::Concern
16
+ include Kaminari::PageScopeMethods
17
+
18
+ included do
19
+ alias :offset :skip
20
+ end
21
+
22
+ def limit_value
23
+ criteria[:limit]
24
+ end
25
+
26
+ def offset_value
27
+ criteria[:skip]
28
+ end
29
+
30
+ def total_count
31
+ count
32
+ end
33
+
34
+ def max_per_page
35
+ @klass.max_per_page
36
+ end
37
+
38
+ def page(num)
39
+ limit(@klass.default_per_page).skip(@klass.default_per_page * ([num.to_i, 1].max - 1))
40
+ self
41
+ end
42
+
43
+ def per(num)
44
+ if (n = num.to_i) <= 0
45
+ self
46
+ elsif max_per_page && max_per_page < n
47
+ new_offset_value = offset_value / limit_value * max_per_page
48
+ limit(max_per_page).offset(new_offset_value)
49
+ else
50
+ new_offset_value = offset_value / limit_value * n
51
+ limit(n).offset(new_offset_value)
52
+ end
53
+ self
54
+ end
55
+ end
56
+ end
57
+
58
+ ParseResource::Base.send :include, KaminariExtension::ParseBaseExt
59
+ Query.send :include, KaminariExtension::QueryExt
60
+ end
@@ -1,9 +1,11 @@
1
1
  require 'parse_resource/base'
2
+ require 'parse_resource/query_methods'
3
+ require 'parse_resource/client'
2
4
  require 'parse_resource/query'
3
5
  require 'parse_resource/parse_user'
4
6
  require 'parse_resource/parse_user_validator'
5
7
  require 'parse_resource/parse_error'
6
-
8
+ require 'kaminari_extension'
7
9
 
8
10
  module ParseResource
9
- end
11
+ end
@@ -6,8 +6,10 @@ require "rest-client"
6
6
  require "json"
7
7
  require "active_support/hash_with_indifferent_access"
8
8
  require "parse_resource/query"
9
+ require "parse_resource/query_methods"
9
10
  require "parse_resource/parse_error"
10
11
  require "parse_resource/parse_exceptions"
12
+ require "parse_resource/types/parse_geopoint"
11
13
 
12
14
  module ParseResource
13
15
 
@@ -26,6 +28,8 @@ module ParseResource
26
28
  extend ActiveModel::Callbacks
27
29
  HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
28
30
 
31
+ attr_accessor :error_instances
32
+
29
33
  define_model_callbacks :save, :create, :update, :destroy
30
34
 
31
35
  # Instantiates a ParseResource::Base object
@@ -37,10 +41,12 @@ module ParseResource
37
41
 
38
42
  if new
39
43
  @unsaved_attributes = attributes
44
+ @unsaved_attributes.stringify_keys!
40
45
  else
41
46
  @unsaved_attributes = {}
42
47
  end
43
48
  self.attributes = {}
49
+ self.error_instances = []
44
50
 
45
51
  self.attributes.merge!(attributes)
46
52
  self.attributes unless self.attributes.empty?
@@ -51,18 +57,20 @@ module ParseResource
51
57
  #
52
58
  # @param [Symbol] name the name of the field, eg `:author`.
53
59
  # @param [Boolean] val the return value of the field. Only use this within the class.
54
- def self.field(name, val=nil)
60
+ def self.field(fname, val=nil)
61
+ fname = fname.to_sym
55
62
  class_eval do
56
- define_method(name) do
57
- @attributes[name] ? @attributes[name] : @unsaved_attributes[name]
63
+ define_method(fname) do
64
+ get_attribute("#{fname}")
58
65
  end
59
- define_method("#{name}=") do |val|
60
- val = val.to_pointer if val.respond_to?(:to_pointer)
61
-
62
- @attributes[name] = val
63
- @unsaved_attributes[name] = val
64
-
65
- val
66
+ end
67
+ unless self.respond_to? "#{fname}="
68
+ class_eval do
69
+ define_method("#{fname}=") do |val|
70
+ set_attribute("#{fname}", val)
71
+
72
+ val
73
+ end
66
74
  end
67
75
  end
68
76
  end
@@ -84,72 +92,59 @@ module ParseResource
84
92
  def to_pointer
85
93
  klass_name = self.class.model_name
86
94
  klass_name = "_User" if klass_name == "User"
87
- {"__type" => "Pointer", "className" => klass_name, "objectId" => self.id}
95
+ klass_name = "_Installation" if klass_name == "Installation"
96
+ {"__type" => "Pointer", "className" => klass_name.to_s, "objectId" => self.id}
97
+ end
98
+
99
+ def self.to_date_object(date)
100
+ date = date.to_time if date.respond_to?(:to_time)
101
+ {"__type" => "Date", "iso" => date.iso8601} if date && (date.is_a?(Date) || date.is_a?(DateTime) || date.is_a?(Time))
88
102
  end
89
103
 
90
104
  # Creates setter methods for model fields
91
105
  def create_setters!(k,v)
92
- self.class.send(:define_method, "#{k}=") do |val|
93
- val = val.to_pointer if val.respond_to?(:to_pointer)
94
-
95
- @attributes[k.to_s] = val
96
- @unsaved_attributes[k.to_s] = val
97
-
98
- val
106
+ unless self.respond_to? "#{k}="
107
+ self.class.send(:define_method, "#{k}=") do |val|
108
+ set_attribute("#{k}", val)
109
+
110
+ val
111
+ end
99
112
  end
100
113
  end
101
114
 
102
- def self.method_missing(name, *args)
103
- name = name.to_s
104
- if name.start_with?("find_by_")
105
- attribute = name.gsub(/^find_by_/,"")
106
- finder_name = "find_all_by_#{attribute}"
115
+ def self.method_missing(method_name, *args)
116
+ method_name = method_name.to_s
117
+ if method_name.start_with?("find_by_")
118
+ attrib = method_name.gsub(/^find_by_/,"")
119
+ finder_name = "find_all_by_#{attrib}"
107
120
 
108
121
  define_singleton_method(finder_name) do |target_value|
109
- where({attribute.to_sym => target_value}).first
122
+ where({attrib.to_sym => target_value}).first
110
123
  end
111
124
 
112
125
  send(finder_name, args[0])
113
126
 
114
- elsif name.start_with?("find_all_by_")
115
- attribute = name.gsub(/^find_all_by_/,"")
116
- finder_name = "find_all_by_#{attribute}"
127
+ elsif method_name.start_with?("find_all_by_")
128
+ attrib = method_name.gsub(/^find_all_by_/,"")
129
+ finder_name = "find_all_by_#{attrib}"
117
130
 
118
131
  define_singleton_method(finder_name) do |target_value|
119
- where({attribute.to_sym => target_value}).all
132
+ where({attrib.to_sym => target_value}).all
120
133
  end
121
134
 
122
135
  send(finder_name, args[0])
123
136
  else
124
- super(name.to_sym, *args)
137
+ super(method_name.to_sym, *args)
125
138
  end
126
139
  end
127
140
 
128
141
  # Creates getter methods for model fields
129
142
  def create_getters!(k,v)
130
- self.class.send(:define_method, "#{k}") do
131
-
132
- case @attributes[k]
133
- when Hash
134
-
135
- klass_name = @attributes[k]["className"]
136
- klass_name = "User" if klass_name == "_User"
137
-
138
- case @attributes[k]["__type"]
139
- when "Pointer"
140
- result = klass_name.constantize.find(@attributes[k]["objectId"])
141
- when "Object"
142
- result = klass_name.constantize.new(@attributes[k], false)
143
- when "File"
144
- result = @attributes[k]["url"]
145
- end #todo: support Dates and other types https://www.parse.com/docs/rest#objects-types
146
-
147
- else
148
- result = @attributes[k]
143
+ unless self.respond_to? "#{k}"
144
+ self.class.send(:define_method, "#{k}") do
145
+ get_attribute("#{k}")
149
146
  end
150
-
151
- result
152
- end
147
+ end
153
148
  end
154
149
 
155
150
  def create_setters_and_getters!
@@ -170,47 +165,126 @@ module ParseResource
170
165
  end
171
166
 
172
167
  def self.settings
173
- if @@settings.nil?
174
- path = "config/parse_resource.yml"
175
- #environment = defined?(Rails) && Rails.respond_to?(:env) ? Rails.env : ENV["RACK_ENV"]
176
- environment = ENV["RACK_ENV"]
177
- @@settings = YAML.load(ERB.new(File.new(path).read).result)[environment]
168
+ load_settings
169
+ end
170
+
171
+ # Gets the current class's model name for the URI
172
+ def self.model_name_uri
173
+ if self.model_name == "User"
174
+ "users"
175
+ elsif self.model_name == "Installation"
176
+ "installations"
177
+ else
178
+ "classes/#{self.model_name}"
178
179
  end
179
- @@settings
180
180
  end
181
+
182
+ # Gets the current class's Parse.com base_uri
183
+ def self.model_base_uri
184
+ "https://api.parse.com/1/#{model_name_uri}"
185
+ end
186
+
187
+ # Gets the current instance's parent class's Parse.com base_uri
188
+ def model_base_uri
189
+ self.class.send(:model_base_uri)
190
+ end
191
+
181
192
 
182
193
  # Creates a RESTful resource
183
194
  # sends requests to [base_uri]/[classname]
184
195
  #
185
196
  def self.resource
186
- if @@settings.nil?
187
- path = "config/parse_resource.yml"
188
- environment = defined?(Rails) && Rails.respond_to?(:env) ? Rails.env : ENV["RACK_ENV"]
189
- @@settings = YAML.load(ERB.new(File.new(path).read).result)[environment]
190
- end
191
-
192
- if model_name == "User" #https://parse.com/docs/rest#users-signup
193
- base_uri = "https://api.parse.com/1/users"
194
- else
195
- base_uri = "https://api.parse.com/1/classes/#{model_name}"
196
- end
197
+ load_settings
197
198
 
198
199
  #refactor to settings['app_id'] etc
199
200
  app_id = @@settings['app_id']
200
201
  master_key = @@settings['master_key']
201
- RestClient::Resource.new(base_uri, app_id, master_key)
202
+ RestClient::Resource.new(self.model_base_uri, app_id, master_key)
202
203
  end
203
-
204
- # Creates a RESTful resource for file uploads
205
- # sends requests to [base_uri]/files
204
+
205
+ # Batch requests
206
+ # Sends multiple requests to /batch
207
+ # Set slice_size to send larger batches. Defaults to 20 to prevent timeouts.
208
+ # Parse doesn't support batches of over 20.
206
209
  #
207
- def self.upload(file_instance, filename, options={})
208
- if @@settings.nil?
210
+ def self.batch_save(save_objects, slice_size = 20, method = nil)
211
+ return true if save_objects.blank?
212
+ load_settings
213
+
214
+ base_uri = "https://api.parse.com/1/batch"
215
+ app_id = @@settings['app_id']
216
+ master_key = @@settings['master_key']
217
+
218
+ res = RestClient::Resource.new(base_uri, app_id, master_key)
219
+
220
+ # Batch saves seem to fail if they're too big. We'll slice it up into multiple posts if they are.
221
+ save_objects.each_slice(slice_size) do |objects|
222
+ # attributes_for_saving
223
+ batch_json = { "requests" => [] }
224
+
225
+ objects.each do |item|
226
+ method ||= (item.new?) ? "POST" : "PUT"
227
+ object_path = "/1/#{item.class.model_name_uri}"
228
+ object_path = "#{object_path}/#{item.id}" if item.id
229
+ json = {
230
+ "method" => method,
231
+ "path" => object_path
232
+ }
233
+ json["body"] = item.attributes_for_saving unless method == "DELETE"
234
+ batch_json["requests"] << json
235
+ end
236
+ res.post(batch_json.to_json, :content_type => "application/json") do |resp, req, res, &block|
237
+ response = JSON.parse(resp) rescue nil
238
+ if resp.code == 400
239
+ puts resp
240
+ return false
241
+ end
242
+ if response && response.is_a?(Array) && response.length == objects.length
243
+ merge_all_attributes(objects, response) unless method == "DELETE"
244
+ end
245
+ end
246
+ end
247
+ true
248
+ end
249
+
250
+ def self.merge_all_attributes(objects, response)
251
+ i = 0
252
+ objects.each do |item|
253
+ item.merge_attributes(response[i]["success"]) if response[i] && response[i]["success"]
254
+ i += 1
255
+ end
256
+ nil
257
+ end
258
+
259
+ def self.save_all(objects)
260
+ batch_save(objects)
261
+ end
262
+
263
+ def self.destroy_all(objects=nil)
264
+ objects ||= self.all
265
+ batch_save(objects, 20, "DELETE")
266
+ end
267
+
268
+ def self.delete_all(o)
269
+ raise StandardError.new("Parse Resource: delete_all doesn't exist. Did you mean destroy_all?")
270
+ end
271
+
272
+ def self.load_settings
273
+ @@settings ||= begin
209
274
  path = "config/parse_resource.yml"
210
275
  environment = defined?(Rails) && Rails.respond_to?(:env) ? Rails.env : ENV["RACK_ENV"]
211
- @@settings = YAML.load(ERB.new(File.new(path).read).result)[environment]
276
+ YAML.load(ERB.new(File.new(path).read).result)[environment]
212
277
  end
278
+ @@settings
279
+ end
280
+
213
281
 
282
+ # Creates a RESTful resource for file uploads
283
+ # sends requests to [base_uri]/files
284
+ #
285
+ def self.upload(file_instance, filename, options={})
286
+ load_settings
287
+
214
288
  base_uri = "https://api.parse.com/1/files"
215
289
 
216
290
  #refactor to settings['app_id'] etc
@@ -220,10 +294,12 @@ module ParseResource
220
294
  options[:content_type] ||= 'image/jpg' # TODO: Guess mime type here.
221
295
  file_instance = File.new(file_instance, 'rb') if file_instance.is_a? String
222
296
 
297
+ filename = filename.parameterize
298
+
223
299
  private_resource = RestClient::Resource.new "#{base_uri}/#{filename}", app_id, master_key
224
300
  private_resource.post(file_instance, options) do |resp, req, res, &block|
225
301
  return false if resp.code == 400
226
- return JSON.parse(resp)
302
+ return JSON.parse(resp) rescue {"code" => 0, "error" => "unknown error"}
227
303
  end
228
304
  false
229
305
  end
@@ -243,48 +319,14 @@ module ParseResource
243
319
  Query.new(self).where(*args)
244
320
  end
245
321
 
246
- # Include the attributes of a parent ojbect in the results
247
- # Similar to ActiveRecord eager loading
248
- #
249
- def self.include_object(parent)
250
- Query.new(self).include_object(parent)
251
- end
252
322
 
253
- # Add this at the end of a method chain to get the count of objects, instead of an Array of objects
254
- def self.count
255
- #https://www.parse.com/docs/rest#queries-counting
256
- Query.new(self).count(1)
257
- end
323
+ include ParseResource::QueryMethods
258
324
 
259
- # Find all ParseResource::Base objects for that model.
260
- #
261
- # @return [Array] an `Array` of objects that subclass `ParseResource`.
262
- def self.all
263
- Query.new(self).all
264
- end
265
325
 
266
- # Find the first object. Fairly random, not based on any specific condition.
267
- #
268
- def self.first
269
- Query.new(self).limit(1).first
270
- end
271
-
272
- # Limits the number of objects returned
273
- #
274
- def self.limit(n)
275
- Query.new(self).limit(n)
326
+ def self.chunk(attribute)
327
+ Query.new(self).chunk(attribute)
276
328
  end
277
329
 
278
- # Skip the number of objects
279
- #
280
- def self.skip(n)
281
- Query.new(self).skip(n)
282
- end
283
-
284
- #def self.order(attribute)
285
- # Query.new(self).order(attribute)
286
- #end
287
-
288
330
  # Create a ParseResource::Base object.
289
331
  #
290
332
  # @param [Hash] attributes a `Hash` of attributes
@@ -296,11 +338,12 @@ module ParseResource
296
338
  obj
297
339
  end
298
340
 
299
- def self.destroy_all
300
- all.each do |object|
301
- object.destroy
302
- end
303
- end
341
+ # Replaced with a batch destroy_all method.
342
+ # def self.destroy_all(all)
343
+ # all.each do |object|
344
+ # object.destroy
345
+ # end
346
+ # end
304
347
 
305
348
  def self.class_attributes
306
349
  @class_attributes ||= {}
@@ -329,28 +372,18 @@ module ParseResource
329
372
  self.class.resource["#{self.id}"]
330
373
  end
331
374
 
332
- def create
333
- opts = {:content_type => "application/json"}
334
- attrs = @unsaved_attributes.to_json
335
- result = self.resource.post(attrs, opts) do |resp, req, res, &block|
336
-
337
- case resp.code
338
- when 400
339
-
340
- # https://www.parse.com/docs/ios/api/Classes/PFConstants.html
341
- error_response = JSON.parse(resp)
342
- pe = ParseError.new(error_response["code"]).to_array
343
- self.errors.add(pe[0], pe[1])
344
- return false
375
+ def pointerize(hash)
376
+ new_hash = {}
377
+ hash.each do |k, v|
378
+ if v.respond_to?(:to_pointer)
379
+ new_hash[k] = v.to_pointer
380
+ elsif v.is_a?(Date) || v.is_a?(Time) || v.is_a?(DateTime)
381
+ new_hash[k] = self.class.to_date_object(v)
345
382
  else
346
- @attributes.merge!(JSON.parse(resp))
347
- @attributes.merge!(@unsaved_attributes)
348
- attributes = HashWithIndifferentAccess.new(attributes)
349
- @unsaved_attributes = {}
350
- create_setters_and_getters!
351
- return true
383
+ new_hash[k] = v
352
384
  end
353
385
  end
386
+ new_hash
354
387
  end
355
388
 
356
389
  def save
@@ -368,39 +401,60 @@ module ParseResource
368
401
  rescue false
369
402
  end
370
403
 
404
+ def create
405
+ attrs = attributes_for_saving.to_json
406
+ opts = {:content_type => "application/json"}
407
+ result = self.resource.post(attrs, opts) do |resp, req, res, &block|
408
+ return post_result(resp, req, res, &block)
409
+ end
410
+ end
411
+
371
412
  def update(attributes = {})
372
413
 
373
414
  attributes = HashWithIndifferentAccess.new(attributes)
374
415
 
375
416
  @unsaved_attributes.merge!(attributes)
376
-
377
- put_attrs = @unsaved_attributes
378
- put_attrs.delete('objectId')
379
- put_attrs.delete('createdAt')
380
- put_attrs.delete('updatedAt')
381
- put_attrs = put_attrs.to_json
417
+ put_attrs = attributes_for_saving.to_json
382
418
 
383
419
  opts = {:content_type => "application/json"}
384
420
  result = self.instance_resource.put(put_attrs, opts) do |resp, req, res, &block|
385
- case resp.code
386
- when 400
387
-
388
- # https://www.parse.com/docs/ios/api/Classes/PFConstants.html
389
- error_response = JSON.parse(resp)
390
- pe = ParseError.new(error_response["code"], error_response["error"]).to_array
391
- self.errors.add(pe[0], pe[1])
392
-
393
- return false
421
+ return post_result(resp, req, res, &block)
422
+ end
423
+ end
424
+
425
+ # Merges in the return value of a save and resets the unsaved_attributes
426
+ def merge_attributes(results)
427
+ @attributes.merge!(results)
428
+ @attributes.merge!(@unsaved_attributes)
429
+ @unsaved_attributes = {}
430
+ create_setters_and_getters!
431
+ @attributes
432
+ end
433
+
434
+ def post_result(resp, req, res, &block)
435
+ if resp.code.to_s == "200" || resp.code.to_s == "201"
436
+ merge_attributes(JSON.parse(resp))
437
+ return true
438
+ else
439
+ error_response = JSON.parse(resp)
440
+ if error_response["error"]
441
+ pe = ParseError.new(error_response["code"], error_response["error"])
394
442
  else
395
-
396
- @attributes.merge!(JSON.parse(resp))
397
- @attributes.merge!(@unsaved_attributes)
398
- @unsaved_attributes = {}
399
- create_setters_and_getters!
400
-
401
- return true
443
+ pe = ParseError.new(resp.code.to_s)
402
444
  end
403
- end
445
+ self.errors.add(pe.code.to_s.to_sym, pe.msg)
446
+ self.error_instances << pe
447
+ return false
448
+ end
449
+ end
450
+
451
+ def attributes_for_saving
452
+ @unsaved_attributes = pointerize(@unsaved_attributes)
453
+ put_attrs = @unsaved_attributes
454
+ put_attrs.delete('objectId')
455
+ put_attrs.delete('createdAt')
456
+ put_attrs.delete('updatedAt')
457
+ put_attrs
404
458
  end
405
459
 
406
460
  def update_attributes(attributes = {})
@@ -425,6 +479,14 @@ module ParseResource
425
479
 
426
480
  self
427
481
  end
482
+
483
+ def dirty?
484
+ @unsaved_attributes.length > 0
485
+ end
486
+
487
+ def clean?
488
+ !dirty?
489
+ end
428
490
 
429
491
  # provides access to @attributes for getting and setting
430
492
  def attributes
@@ -438,12 +500,49 @@ module ParseResource
438
500
  @attributes
439
501
  end
440
502
 
503
+ def get_attribute(k)
504
+ attrs = @unsaved_attributes[k.to_s] ? @unsaved_attributes : @attributes
505
+ case attrs[k]
506
+ when Hash
507
+ klass_name = attrs[k]["className"]
508
+ klass_name = "User" if klass_name == "_User"
509
+ case attrs[k]["__type"]
510
+ when "Pointer"
511
+ result = klass_name.constantize.find(attrs[k]["objectId"])
512
+ when "Object"
513
+ result = klass_name.constantize.new(attrs[k], false)
514
+ when "Date"
515
+ result = DateTime.parse(attrs[k]["iso"]).to_time_in_current_zone
516
+ when "File"
517
+ result = attrs[k]["url"]
518
+ when "GeoPoint"
519
+ result = ParseGeoPoint.new(attrs[k])
520
+ end #todo: support other types https://www.parse.com/docs/rest#objects-types
521
+ else
522
+ result = attrs["#{k}"]
523
+ end
524
+ result
525
+ end
526
+
527
+ def set_attribute(k, v)
528
+ if v.is_a?(Date) || v.is_a?(Time) || v.is_a?(DateTime)
529
+ v = self.class.to_date_object(v)
530
+ elsif v.respond_to?(:to_pointer)
531
+ v = v.to_pointer
532
+ end
533
+ @unsaved_attributes[k.to_s] = v unless v == @attributes[k.to_s] # || @unsaved_attributes[k.to_s]
534
+ @attributes[k.to_s] = v
535
+ v
536
+ end
537
+
538
+
441
539
  # aliasing for idiomatic Ruby
442
- def id; self.objectId rescue nil; end
540
+ def id; get_attribute("objectId") rescue nil; end
541
+ def objectId; get_attribute("objectId") rescue nil; end
443
542
 
444
- def created_at; self.createdAt; end
543
+ def created_at; get_attribute("createdAt"); end
445
544
 
446
- def updated_at; self.updatedAt rescue nil; end
545
+ def updated_at; get_attribute("updatedAt"); rescue nil; end
447
546
 
448
547
  def self.included(base)
449
548
  base.extend(ClassMethods)