parse_resource 1.7.1 → 1.7.2

Sign up to get free protection for your applications and to get access to all the features.
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