parse_resource 1.7.1 → 1.7.2

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.
data/Gemfile CHANGED
@@ -18,5 +18,3 @@ end
18
18
  gem "rest-client"
19
19
  gem "activesupport"
20
20
  gem "activemodel"
21
-
22
-
data/README.md CHANGED
@@ -26,7 +26,7 @@ Installation
26
26
  Include in your `Gemfile`:
27
27
 
28
28
  ```ruby
29
- gem "parse_resource", "~> 1.7.0"
29
+ gem "parse_resource", "~> 1.7.1"
30
30
  ```
31
31
 
32
32
  Or just gem install:
@@ -124,6 +124,10 @@ posts.map {|p| p.title} #=> ["Unpaid blogger", "Uncrunched"]
124
124
  id = "DjiH4Qffke"
125
125
  p = Post.find(id) #simple find by id
126
126
 
127
+ # ActiveRecord style find commands
128
+ Post.find_by_title("Uncrunched") #=> A Post object
129
+ Post.find_all_by_author("Arrington") #=> An Array of Posts
130
+
127
131
  # you can chain method calls, just like in ActiveRecord
128
132
  Post.where(:param1 => "foo").where(:param2 => "bar").all
129
133
 
@@ -138,16 +142,76 @@ posts.length #=> 5
138
142
  Post.where(:bar => "foo").count #=> 1337
139
143
  ```
140
144
 
145
+ Users
146
+
147
+ Note: Because users are special in the Parse API, you must name your class User if you want to subclass ParseUser.
148
+
149
+ ```ruby
150
+ # app/models/user.rb
151
+ class User < ParseUser
152
+ # no validations included, but feel free to add your own
153
+ validates_presence_of :username
154
+
155
+ # you can add fields, like any other kind of Object...
156
+ fields :name, :bio
157
+
158
+ # but note that email is a special field in the Parse API.
159
+ fields :email
160
+ end
161
+
162
+ # create a user
163
+ user = User.new(:username => "adelevie")
164
+ user.password = "asecretpassword"
165
+ user.save
166
+ # after saving, the password is automatically hashed by Parse's server
167
+ # user.password will return the unhashed password when the original object is in memory
168
+ # from a new session, User.where(:username => "adelevie").first.password will return nil
169
+
170
+ # check if a user is logged in
171
+ User.authenticate("adelevie", "foooo") #=> false
172
+ User.authenticate("adelevie", "asecretpassword") #=> #<User...>
173
+
174
+
175
+ # A simple controller to authenticate users
176
+ class SessionsController < ApplicationController
177
+ def new
178
+ end
179
+
180
+ def create
181
+ user = User.authenticate(params[:username], params[:password])
182
+ if user
183
+ session[:user_id] = user.id
184
+ redirect_to root_url, :notice => "logged in !"
185
+ else
186
+ flash.now.alert = "Invalid username or password"
187
+ render "new"
188
+ end
189
+ end
190
+
191
+ def destroy
192
+ session[:user_id] = nil
193
+ redirect_to root_url, :notice => "Logged out!"
194
+ end
195
+
196
+ end
197
+
198
+ ```
199
+
200
+ If you want to use parse_resource to back a simple authentication system for a Rails app, follow this [tutorial](http://asciicasts.com/episodes/250-authentication-from-scratch), and make some simple modifications.
201
+
202
+
141
203
  Associations
142
204
 
143
205
  ```ruby
144
206
  class Post < ParseResource::Base
145
- belongs_to :author
207
+ # As with ActiveRecord, associations names can differ from class names...
208
+ belongs_to :author, :class_name => 'User'
146
209
  fields :title, :body
147
210
  end
148
211
 
149
- class Author < ParseResource::Base
150
- has_many :posts
212
+ class User < ParseUser
213
+ # ... but on the other end, use :inverse_of to complete the link.
214
+ has_many :posts, :inverse_of => :author
151
215
  field :name
152
216
  end
153
217
 
@@ -163,8 +227,15 @@ author.posts << post2
163
227
  post3 = Post.create(:title => "Goosebumps 3")
164
228
  post3.author = author
165
229
  post3.save
166
- ```
167
230
 
231
+ # relational queries
232
+ posts = Post.include_object(:author).all
233
+ posts.each do |post|
234
+ puts post.author.name
235
+ # because you used Post#include_object, calling post.title won't execute a new query
236
+ # this is similar to ActiveRecord's eager loading
237
+ end
238
+ ```
168
239
 
169
240
  Documentation
170
241
  -------------
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.7.1
1
+ 1.7.2
Binary file
@@ -1,8 +1,9 @@
1
- require 'base'
2
- require 'query'
3
- require 'parse_user'
4
- require 'parse_user_validator'
5
- require 'parse_error'
1
+ require 'parse_resource/base'
2
+ require 'parse_resource/query'
3
+ require 'parse_resource/parse_user'
4
+ require 'parse_resource/parse_user_validator'
5
+ require 'parse_resource/parse_error'
6
+
6
7
 
7
8
  module ParseResource
8
- end
9
+ end
@@ -0,0 +1,459 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+ require "active_model"
4
+ require "erb"
5
+ require "rest-client"
6
+ require "json"
7
+ require "active_support/hash_with_indifferent_access"
8
+ require "parse_resource/query"
9
+ require "parse_resource/parse_error"
10
+ require "parse_resource/parse_exceptions"
11
+
12
+ module ParseResource
13
+
14
+
15
+ class Base
16
+ # ParseResource::Base provides an easy way to use Ruby to interace with a Parse.com backend
17
+ # Usage:
18
+ # class Post < ParseResource::Base
19
+ # fields :title, :author, :body
20
+ # end
21
+
22
+ include ActiveModel::Validations
23
+ include ActiveModel::Conversion
24
+ include ActiveModel::AttributeMethods
25
+ extend ActiveModel::Naming
26
+ extend ActiveModel::Callbacks
27
+ HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
28
+
29
+ define_model_callbacks :save, :create, :update, :destroy
30
+
31
+ # Instantiates a ParseResource::Base object
32
+ #
33
+ # @params [Hash], [Boolean] a `Hash` of attributes and a `Boolean` that should be false only if the object already exists
34
+ # @return [ParseResource::Base] an object that subclasses `Parseresource::Base`
35
+ def initialize(attributes = {}, new=true)
36
+ #attributes = HashWithIndifferentAccess.new(attributes)
37
+
38
+ if new
39
+ @unsaved_attributes = attributes
40
+ else
41
+ @unsaved_attributes = {}
42
+ end
43
+ self.attributes = {}
44
+
45
+ self.attributes.merge!(attributes)
46
+ self.attributes unless self.attributes.empty?
47
+ create_setters_and_getters!
48
+ end
49
+
50
+ # Explicitly adds a field to the model.
51
+ #
52
+ # @param [Symbol] name the name of the field, eg `:author`.
53
+ # @param [Boolean] val the return value of the field. Only use this within the class.
54
+ def self.field(name, val=nil)
55
+ class_eval do
56
+ define_method(name) do
57
+ @attributes[name] ? @attributes[name] : @unsaved_attributes[name]
58
+ 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
+ end
68
+ end
69
+
70
+ # Add multiple fields in one line. Same as `#field`, but accepts multiple args.
71
+ #
72
+ # @param [Array] *args an array of `Symbol`s, `eg :author, :body, :title`.
73
+ def self.fields(*args)
74
+ args.each {|f| field(f)}
75
+ end
76
+
77
+ # Similar to its ActiveRecord counterpart.
78
+ #
79
+ # @param [Hash] options Added so that you can specify :class_name => '...'. It does nothing at all, but helps you write self-documenting code.
80
+ def self.belongs_to(parent, options = {})
81
+ field(parent)
82
+ end
83
+
84
+ def to_pointer
85
+ klass_name = self.class.model_name
86
+ klass_name = "_User" if klass_name == "User"
87
+ {"__type" => "Pointer", "className" => klass_name, "objectId" => self.id}
88
+ end
89
+
90
+ # Creates setter methods for model fields
91
+ 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
99
+ end
100
+ end
101
+
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}"
107
+
108
+ define_singleton_method(finder_name) do |target_value|
109
+ where({attribute.to_sym => target_value}).first
110
+ end
111
+
112
+ send(finder_name, args[0])
113
+
114
+ elsif name.start_with?("find_all_by_")
115
+ attribute = name.gsub(/^find_all_by_/,"")
116
+ finder_name = "find_all_by_#{attribute}"
117
+
118
+ define_singleton_method(finder_name) do |target_value|
119
+ where({attribute.to_sym => target_value}).all
120
+ end
121
+
122
+ send(finder_name, args[0])
123
+ else
124
+ super(name.to_sym, *args)
125
+ end
126
+ end
127
+
128
+ # Creates getter methods for model fields
129
+ 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
+ end #todo: support Dates and other types https://www.parse.com/docs/rest#objects-types
144
+
145
+ else
146
+ result = @attributes[k]
147
+ end
148
+
149
+ result
150
+ end
151
+ end
152
+
153
+ def create_setters_and_getters!
154
+ @attributes.each_pair do |k,v|
155
+ create_setters!(k,v)
156
+ create_getters!(k,v)
157
+ end
158
+ end
159
+
160
+ def self.has_many(children, options = {})
161
+ options.stringify_keys!
162
+
163
+ parent_klass_name = model_name
164
+ lowercase_parent_klass_name = parent_klass_name.downcase
165
+ parent_klass = model_name.constantize
166
+ child_klass_name = options['class_name'] || children.to_s.singularize.camelize
167
+ child_klass = child_klass_name.constantize
168
+
169
+ if parent_klass_name == "User"
170
+ parent_klass_name = "_User"
171
+ end
172
+
173
+ @@parent_klass_name = parent_klass_name
174
+ @@options ||= {}
175
+ @@options[children] ||= {}
176
+ @@options[children].merge!(options)
177
+
178
+ send(:define_method, children) do
179
+ @@parent_id = self.id
180
+ @@parent_instance = self
181
+
182
+ parent_klass_name = case
183
+ when @@options[children]['inverse_of'] then @@options[children]['inverse_of'].downcase
184
+ when @@parent_klass_name == "User" then "_User"
185
+ else @@parent_klass_name.downcase
186
+ end
187
+
188
+ query = child_klass.where(parent_klass_name.to_sym => @@parent_instance.to_pointer)
189
+ singleton = query.all
190
+
191
+ class << singleton
192
+ def <<(child)
193
+ parent_klass_name = case
194
+ when @@options[children]['inverse_of'] then @@options[children]['inverse_of'].downcase
195
+ when @@parent_klass_name == "User" then @@parent_klass_name
196
+ else @@parent_klass_name.downcase
197
+ end
198
+ if @@parent_instance.respond_to?(:to_pointer)
199
+ child.send("#{parent_klass_name}=", @@parent_instance.to_pointer)
200
+ child.save
201
+ end
202
+ super(child)
203
+ end
204
+ end
205
+
206
+ singleton
207
+ end
208
+
209
+ end
210
+
211
+ @@settings ||= nil
212
+
213
+ # Explicitly set Parse.com API keys.
214
+ #
215
+ # @param [String] app_id the Application ID of your Parse database
216
+ # @param [String] master_key the Master Key of your Parse database
217
+ def self.load!(app_id, master_key)
218
+ @@settings = {"app_id" => app_id, "master_key" => master_key}
219
+ end
220
+
221
+ def self.settings
222
+ if @@settings.nil?
223
+ path = "config/parse_resource.yml"
224
+ #environment = defined?(Rails) && Rails.respond_to?(:env) ? Rails.env : ENV["RACK_ENV"]
225
+ environment = ENV["RACK_ENV"]
226
+ @@settings = YAML.load(ERB.new(File.new(path).read).result)[environment]
227
+ end
228
+ @@settings
229
+ end
230
+
231
+ # Creates a RESTful resource
232
+ # sends requests to [base_uri]/[classname]
233
+ #
234
+ def self.resource
235
+ if @@settings.nil?
236
+ path = "config/parse_resource.yml"
237
+ environment = defined?(Rails) && Rails.respond_to?(:env) ? Rails.env : ENV["RACK_ENV"]
238
+ @@settings = YAML.load(ERB.new(File.new(path).read).result)[environment]
239
+ end
240
+
241
+ if model_name == "User" #https://parse.com/docs/rest#users-signup
242
+ base_uri = "https://api.parse.com/1/users"
243
+ else
244
+ base_uri = "https://api.parse.com/1/classes/#{model_name}"
245
+ end
246
+
247
+ #refactor to settings['app_id'] etc
248
+ app_id = @@settings['app_id']
249
+ master_key = @@settings['master_key']
250
+ RestClient::Resource.new(base_uri, app_id, master_key)
251
+ end
252
+
253
+ # Find a ParseResource::Base object by ID
254
+ #
255
+ # @param [String] id the ID of the Parse object you want to find.
256
+ # @return [ParseResource] an object that subclasses ParseResource.
257
+ def self.find(id)
258
+ raise RecordNotFound if id.blank?
259
+ where(:objectId => id).first
260
+ end
261
+
262
+ # Find a ParseResource::Base object by chaining #where method calls.
263
+ #
264
+ def self.where(*args)
265
+ Query.new(self).where(*args)
266
+ end
267
+
268
+ # Include the attributes of a parent ojbect in the results
269
+ # Similar to ActiveRecord eager loading
270
+ #
271
+ def self.include_object(parent)
272
+ Query.new(self).include_object(parent)
273
+ end
274
+
275
+ # Add this at the end of a method chain to get the count of objects, instead of an Array of objects
276
+ def self.count
277
+ #https://www.parse.com/docs/rest#queries-counting
278
+ Query.new(self).count(1)
279
+ end
280
+
281
+ # Find all ParseResource::Base objects for that model.
282
+ #
283
+ # @return [Array] an `Array` of objects that subclass `ParseResource`.
284
+ def self.all
285
+ Query.new(self).all
286
+ end
287
+
288
+ # Find the first object. Fairly random, not based on any specific condition.
289
+ #
290
+ def self.first
291
+ Query.new(self).limit(1).first
292
+ end
293
+
294
+ # Limits the number of objects returned
295
+ #
296
+ def self.limit(n)
297
+ Query.new(self).limit(n)
298
+ end
299
+
300
+ def self.order(attribute)
301
+ Query.new(self).order(attribute)
302
+ end
303
+
304
+ # Create a ParseResource::Base object.
305
+ #
306
+ # @param [Hash] attributes a `Hash` of attributes
307
+ # @return [ParseResource] an object that subclasses `ParseResource`. Or returns `false` if object fails to save.
308
+ def self.create(attributes = {})
309
+ attributes = HashWithIndifferentAccess.new(attributes)
310
+ new(attributes).save
311
+ end
312
+
313
+ def self.destroy_all
314
+ all.each do |object|
315
+ object.destroy
316
+ end
317
+ end
318
+
319
+ def self.class_attributes
320
+ @class_attributes ||= {}
321
+ end
322
+
323
+ def persisted?
324
+ if id
325
+ true
326
+ else
327
+ false
328
+ end
329
+ end
330
+
331
+ def new?
332
+ !persisted?
333
+ end
334
+
335
+ # delegate from Class method
336
+ def resource
337
+ self.class.resource
338
+ end
339
+
340
+ # create RESTful resource for the specific Parse object
341
+ # sends requests to [base_uri]/[classname]/[objectId]
342
+ def instance_resource
343
+ self.class.resource["#{self.id}"]
344
+ end
345
+
346
+ def create
347
+ opts = {:content_type => "application/json"}
348
+ attrs = @unsaved_attributes.to_json
349
+ result = self.resource.post(attrs, opts) do |resp, req, res, &block|
350
+
351
+ case resp.code
352
+ when 400
353
+
354
+ # https://www.parse.com/docs/ios/api/Classes/PFConstants.html
355
+ error_response = JSON.parse(resp)
356
+ pe = ParseError.new(error_response["code"]).to_array
357
+ self.errors.add(pe[0], pe[1])
358
+
359
+ else
360
+ @attributes.merge!(JSON.parse(resp))
361
+ @attributes.merge!(@unsaved_attributes)
362
+ attributes = HashWithIndifferentAccess.new(attributes)
363
+ @unsaved_attributes = {}
364
+ create_setters_and_getters!
365
+ end
366
+
367
+ self
368
+ end
369
+
370
+ result
371
+ end
372
+
373
+ def save
374
+ if valid?
375
+ run_callbacks :save do
376
+ new? ? create : update
377
+ end
378
+ else
379
+ false
380
+ end
381
+ rescue false
382
+ end
383
+
384
+ def update(attributes = {})
385
+
386
+ attributes = HashWithIndifferentAccess.new(attributes)
387
+
388
+ @unsaved_attributes.merge!(attributes)
389
+
390
+ put_attrs = @unsaved_attributes
391
+ put_attrs.delete('objectId')
392
+ put_attrs.delete('createdAt')
393
+ put_attrs.delete('updatedAt')
394
+ put_attrs = put_attrs.to_json
395
+
396
+ opts = {:content_type => "application/json"}
397
+ result = self.instance_resource.put(put_attrs, opts) do |resp, req, res, &block|
398
+
399
+ case resp.code
400
+ when 400
401
+
402
+ # https://www.parse.com/docs/ios/api/Classes/PFConstants.html
403
+ error_response = JSON.parse(resp)
404
+ pe = ParseError.new(error_response["code"], error_response["error"]).to_array
405
+ self.errors.add(pe[0], pe[1])
406
+
407
+ else
408
+
409
+ @attributes.merge!(JSON.parse(resp))
410
+ @attributes.merge!(@unsaved_attributes)
411
+ @unsaved_attributes = {}
412
+ create_setters_and_getters!
413
+
414
+ self
415
+ end
416
+
417
+ result
418
+ end
419
+
420
+ end
421
+
422
+ def update_attributes(attributes = {})
423
+ self.update(attributes)
424
+ end
425
+
426
+ def destroy
427
+ self.instance_resource.delete
428
+ @attributes = {}
429
+ @unsaved_attributes = {}
430
+ nil
431
+ end
432
+
433
+ # provides access to @attributes for getting and setting
434
+ def attributes
435
+ @attributes ||= self.class.class_attributes
436
+ @attributes
437
+ end
438
+
439
+ def attributes=(n)
440
+ @attributes = n
441
+ @attributes
442
+ end
443
+
444
+ # aliasing for idiomatic Ruby
445
+ def id; self.objectId rescue nil; end
446
+
447
+ def created_at; self.createdAt; end
448
+
449
+ def updated_at; self.updatedAt rescue nil; end
450
+
451
+ def self.included(base)
452
+ base.extend(ClassMethods)
453
+ end
454
+
455
+ module ClassMethods
456
+ end
457
+
458
+ end
459
+ end