lib/parse_resource/base.rb in parse_resource-1.7.3 vs lib/parse_resource/base.rb in parse_resource-1.8.0
- old
+ new
@@ -4,12 +4,14 @@
require "erb"
require "rest-client"
require "json"
require "active_support/hash_with_indifferent_access"
require "parse_resource/query"
+require "parse_resource/query_methods"
require "parse_resource/parse_error"
require "parse_resource/parse_exceptions"
+require "parse_resource/types/parse_geopoint"
module ParseResource
class Base
@@ -24,10 +26,12 @@
include ActiveModel::AttributeMethods
extend ActiveModel::Naming
extend ActiveModel::Callbacks
HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
+ attr_accessor :error_instances
+
define_model_callbacks :save, :create, :update, :destroy
# Instantiates a ParseResource::Base object
#
# @params [Hash], [Boolean] a `Hash` of attributes and a `Boolean` that should be false only if the object already exists
@@ -35,36 +39,40 @@
def initialize(attributes = {}, new=true)
#attributes = HashWithIndifferentAccess.new(attributes)
if new
@unsaved_attributes = attributes
+ @unsaved_attributes.stringify_keys!
else
@unsaved_attributes = {}
end
self.attributes = {}
+ self.error_instances = []
self.attributes.merge!(attributes)
self.attributes unless self.attributes.empty?
create_setters_and_getters!
end
# Explicitly adds a field to the model.
#
# @param [Symbol] name the name of the field, eg `:author`.
# @param [Boolean] val the return value of the field. Only use this within the class.
- def self.field(name, val=nil)
+ def self.field(fname, val=nil)
+ fname = fname.to_sym
class_eval do
- define_method(name) do
- @attributes[name] ? @attributes[name] : @unsaved_attributes[name]
+ define_method(fname) do
+ get_attribute("#{fname}")
end
- define_method("#{name}=") do |val|
- val = val.to_pointer if val.respond_to?(:to_pointer)
-
- @attributes[name] = val
- @unsaved_attributes[name] = val
-
- val
+ end
+ unless self.respond_to? "#{fname}="
+ class_eval do
+ define_method("#{fname}=") do |val|
+ set_attribute("#{fname}", val)
+
+ val
+ end
end
end
end
# Add multiple fields in one line. Same as `#field`, but accepts multiple args.
@@ -82,76 +90,63 @@
end
def to_pointer
klass_name = self.class.model_name
klass_name = "_User" if klass_name == "User"
- {"__type" => "Pointer", "className" => klass_name, "objectId" => self.id}
+ klass_name = "_Installation" if klass_name == "Installation"
+ {"__type" => "Pointer", "className" => klass_name.to_s, "objectId" => self.id}
end
+ def self.to_date_object(date)
+ date = date.to_time if date.respond_to?(:to_time)
+ {"__type" => "Date", "iso" => date.iso8601} if date && (date.is_a?(Date) || date.is_a?(DateTime) || date.is_a?(Time))
+ end
+
# Creates setter methods for model fields
def create_setters!(k,v)
- self.class.send(:define_method, "#{k}=") do |val|
- val = val.to_pointer if val.respond_to?(:to_pointer)
-
- @attributes[k.to_s] = val
- @unsaved_attributes[k.to_s] = val
-
- val
+ unless self.respond_to? "#{k}="
+ self.class.send(:define_method, "#{k}=") do |val|
+ set_attribute("#{k}", val)
+
+ val
+ end
end
end
- def self.method_missing(name, *args)
- name = name.to_s
- if name.start_with?("find_by_")
- attribute = name.gsub(/^find_by_/,"")
- finder_name = "find_all_by_#{attribute}"
+ def self.method_missing(method_name, *args)
+ method_name = method_name.to_s
+ if method_name.start_with?("find_by_")
+ attrib = method_name.gsub(/^find_by_/,"")
+ finder_name = "find_all_by_#{attrib}"
define_singleton_method(finder_name) do |target_value|
- where({attribute.to_sym => target_value}).first
+ where({attrib.to_sym => target_value}).first
end
send(finder_name, args[0])
- elsif name.start_with?("find_all_by_")
- attribute = name.gsub(/^find_all_by_/,"")
- finder_name = "find_all_by_#{attribute}"
+ elsif method_name.start_with?("find_all_by_")
+ attrib = method_name.gsub(/^find_all_by_/,"")
+ finder_name = "find_all_by_#{attrib}"
define_singleton_method(finder_name) do |target_value|
- where({attribute.to_sym => target_value}).all
+ where({attrib.to_sym => target_value}).all
end
send(finder_name, args[0])
else
- super(name.to_sym, *args)
+ super(method_name.to_sym, *args)
end
end
# Creates getter methods for model fields
def create_getters!(k,v)
- self.class.send(:define_method, "#{k}") do
-
- case @attributes[k]
- when Hash
-
- klass_name = @attributes[k]["className"]
- klass_name = "User" if klass_name == "_User"
-
- case @attributes[k]["__type"]
- when "Pointer"
- result = klass_name.constantize.find(@attributes[k]["objectId"])
- when "Object"
- result = klass_name.constantize.new(@attributes[k], false)
- when "File"
- result = @attributes[k]["url"]
- end #todo: support Dates and other types https://www.parse.com/docs/rest#objects-types
-
- else
- result = @attributes[k]
+ unless self.respond_to? "#{k}"
+ self.class.send(:define_method, "#{k}") do
+ get_attribute("#{k}")
end
-
- result
- end
+ end
end
def create_setters_and_getters!
@attributes.each_pair do |k,v|
create_setters!(k,v)
@@ -168,64 +163,145 @@
def self.load!(app_id, master_key)
@@settings = {"app_id" => app_id, "master_key" => master_key}
end
def self.settings
- if @@settings.nil?
- path = "config/parse_resource.yml"
- #environment = defined?(Rails) && Rails.respond_to?(:env) ? Rails.env : ENV["RACK_ENV"]
- environment = ENV["RACK_ENV"]
- @@settings = YAML.load(ERB.new(File.new(path).read).result)[environment]
+ load_settings
+ end
+
+ # Gets the current class's model name for the URI
+ def self.model_name_uri
+ if self.model_name == "User"
+ "users"
+ elsif self.model_name == "Installation"
+ "installations"
+ else
+ "classes/#{self.model_name}"
end
- @@settings
end
+
+ # Gets the current class's Parse.com base_uri
+ def self.model_base_uri
+ "https://api.parse.com/1/#{model_name_uri}"
+ end
+
+ # Gets the current instance's parent class's Parse.com base_uri
+ def model_base_uri
+ self.class.send(:model_base_uri)
+ end
+
# Creates a RESTful resource
# sends requests to [base_uri]/[classname]
#
def self.resource
- if @@settings.nil?
- path = "config/parse_resource.yml"
- environment = defined?(Rails) && Rails.respond_to?(:env) ? Rails.env : ENV["RACK_ENV"]
- @@settings = YAML.load(ERB.new(File.new(path).read).result)[environment]
- end
+ load_settings
- if model_name == "User" #https://parse.com/docs/rest#users-signup
- base_uri = "https://api.parse.com/1/users"
- else
- base_uri = "https://api.parse.com/1/classes/#{model_name}"
- end
-
#refactor to settings['app_id'] etc
app_id = @@settings['app_id']
master_key = @@settings['master_key']
- RestClient::Resource.new(base_uri, app_id, master_key)
+ RestClient::Resource.new(self.model_base_uri, app_id, master_key)
end
-
- # Creates a RESTful resource for file uploads
- # sends requests to [base_uri]/files
+
+ # Batch requests
+ # Sends multiple requests to /batch
+ # Set slice_size to send larger batches. Defaults to 20 to prevent timeouts.
+ # Parse doesn't support batches of over 20.
#
- def self.upload(file_instance, filename, options={})
- if @@settings.nil?
+ def self.batch_save(save_objects, slice_size = 20, method = nil)
+ return true if save_objects.blank?
+ load_settings
+
+ base_uri = "https://api.parse.com/1/batch"
+ app_id = @@settings['app_id']
+ master_key = @@settings['master_key']
+
+ res = RestClient::Resource.new(base_uri, app_id, master_key)
+
+ # Batch saves seem to fail if they're too big. We'll slice it up into multiple posts if they are.
+ save_objects.each_slice(slice_size) do |objects|
+ # attributes_for_saving
+ batch_json = { "requests" => [] }
+
+ objects.each do |item|
+ method ||= (item.new?) ? "POST" : "PUT"
+ object_path = "/1/#{item.class.model_name_uri}"
+ object_path = "#{object_path}/#{item.id}" if item.id
+ json = {
+ "method" => method,
+ "path" => object_path
+ }
+ json["body"] = item.attributes_for_saving unless method == "DELETE"
+ batch_json["requests"] << json
+ end
+ res.post(batch_json.to_json, :content_type => "application/json") do |resp, req, res, &block|
+ response = JSON.parse(resp) rescue nil
+ if resp.code == 400
+ puts resp
+ return false
+ end
+ if response && response.is_a?(Array) && response.length == objects.length
+ merge_all_attributes(objects, response) unless method == "DELETE"
+ end
+ end
+ end
+ true
+ end
+
+ def self.merge_all_attributes(objects, response)
+ i = 0
+ objects.each do |item|
+ item.merge_attributes(response[i]["success"]) if response[i] && response[i]["success"]
+ i += 1
+ end
+ nil
+ end
+
+ def self.save_all(objects)
+ batch_save(objects)
+ end
+
+ def self.destroy_all(objects=nil)
+ objects ||= self.all
+ batch_save(objects, 20, "DELETE")
+ end
+
+ def self.delete_all(o)
+ raise StandardError.new("Parse Resource: delete_all doesn't exist. Did you mean destroy_all?")
+ end
+
+ def self.load_settings
+ @@settings ||= begin
path = "config/parse_resource.yml"
environment = defined?(Rails) && Rails.respond_to?(:env) ? Rails.env : ENV["RACK_ENV"]
- @@settings = YAML.load(ERB.new(File.new(path).read).result)[environment]
+ YAML.load(ERB.new(File.new(path).read).result)[environment]
end
+ @@settings
+ end
+
+ # Creates a RESTful resource for file uploads
+ # sends requests to [base_uri]/files
+ #
+ def self.upload(file_instance, filename, options={})
+ load_settings
+
base_uri = "https://api.parse.com/1/files"
#refactor to settings['app_id'] etc
app_id = @@settings['app_id']
master_key = @@settings['master_key']
options[:content_type] ||= 'image/jpg' # TODO: Guess mime type here.
file_instance = File.new(file_instance, 'rb') if file_instance.is_a? String
+ filename = filename.parameterize
+
private_resource = RestClient::Resource.new "#{base_uri}/#{filename}", app_id, master_key
private_resource.post(file_instance, options) do |resp, req, res, &block|
return false if resp.code == 400
- return JSON.parse(resp)
+ return JSON.parse(resp) rescue {"code" => 0, "error" => "unknown error"}
end
false
end
# Find a ParseResource::Base object by ID
@@ -241,52 +317,18 @@
#
def self.where(*args)
Query.new(self).where(*args)
end
- # Include the attributes of a parent ojbect in the results
- # Similar to ActiveRecord eager loading
- #
- def self.include_object(parent)
- Query.new(self).include_object(parent)
- end
- # Add this at the end of a method chain to get the count of objects, instead of an Array of objects
- def self.count
- #https://www.parse.com/docs/rest#queries-counting
- Query.new(self).count(1)
- end
+ include ParseResource::QueryMethods
- # Find all ParseResource::Base objects for that model.
- #
- # @return [Array] an `Array` of objects that subclass `ParseResource`.
- def self.all
- Query.new(self).all
- end
- # Find the first object. Fairly random, not based on any specific condition.
- #
- def self.first
- Query.new(self).limit(1).first
+ def self.chunk(attribute)
+ Query.new(self).chunk(attribute)
end
- # Limits the number of objects returned
- #
- def self.limit(n)
- Query.new(self).limit(n)
- end
-
- # Skip the number of objects
- #
- def self.skip(n)
- Query.new(self).skip(n)
- end
-
- #def self.order(attribute)
- # Query.new(self).order(attribute)
- #end
-
# Create a ParseResource::Base object.
#
# @param [Hash] attributes a `Hash` of attributes
# @return [ParseResource] an object that subclasses `ParseResource`. Or returns `false` if object fails to save.
def self.create(attributes = {})
@@ -294,15 +336,16 @@
obj = new(attributes)
obj.save
obj
end
- def self.destroy_all
- all.each do |object|
- object.destroy
- end
- end
+ # Replaced with a batch destroy_all method.
+ # def self.destroy_all(all)
+ # all.each do |object|
+ # object.destroy
+ # end
+ # end
def self.class_attributes
@class_attributes ||= {}
end
@@ -327,32 +370,22 @@
# sends requests to [base_uri]/[classname]/[objectId]
def instance_resource
self.class.resource["#{self.id}"]
end
- def create
- opts = {:content_type => "application/json"}
- attrs = @unsaved_attributes.to_json
- result = self.resource.post(attrs, opts) do |resp, req, res, &block|
-
- case resp.code
- when 400
-
- # https://www.parse.com/docs/ios/api/Classes/PFConstants.html
- error_response = JSON.parse(resp)
- pe = ParseError.new(error_response["code"]).to_array
- self.errors.add(pe[0], pe[1])
- return false
+ def pointerize(hash)
+ new_hash = {}
+ hash.each do |k, v|
+ if v.respond_to?(:to_pointer)
+ new_hash[k] = v.to_pointer
+ elsif v.is_a?(Date) || v.is_a?(Time) || v.is_a?(DateTime)
+ new_hash[k] = self.class.to_date_object(v)
else
- @attributes.merge!(JSON.parse(resp))
- @attributes.merge!(@unsaved_attributes)
- attributes = HashWithIndifferentAccess.new(attributes)
- @unsaved_attributes = {}
- create_setters_and_getters!
- return true
+ new_hash[k] = v
end
end
+ new_hash
end
def save
if valid?
run_callbacks :save do
@@ -366,44 +399,65 @@
false
end
rescue false
end
+ def create
+ attrs = attributes_for_saving.to_json
+ opts = {:content_type => "application/json"}
+ result = self.resource.post(attrs, opts) do |resp, req, res, &block|
+ return post_result(resp, req, res, &block)
+ end
+ end
+
def update(attributes = {})
attributes = HashWithIndifferentAccess.new(attributes)
@unsaved_attributes.merge!(attributes)
-
- put_attrs = @unsaved_attributes
- put_attrs.delete('objectId')
- put_attrs.delete('createdAt')
- put_attrs.delete('updatedAt')
- put_attrs = put_attrs.to_json
+ put_attrs = attributes_for_saving.to_json
opts = {:content_type => "application/json"}
result = self.instance_resource.put(put_attrs, opts) do |resp, req, res, &block|
- case resp.code
- when 400
-
- # https://www.parse.com/docs/ios/api/Classes/PFConstants.html
- error_response = JSON.parse(resp)
- pe = ParseError.new(error_response["code"], error_response["error"]).to_array
- self.errors.add(pe[0], pe[1])
-
- return false
+ return post_result(resp, req, res, &block)
+ end
+ end
+
+ # Merges in the return value of a save and resets the unsaved_attributes
+ def merge_attributes(results)
+ @attributes.merge!(results)
+ @attributes.merge!(@unsaved_attributes)
+ @unsaved_attributes = {}
+ create_setters_and_getters!
+ @attributes
+ end
+
+ def post_result(resp, req, res, &block)
+ if resp.code.to_s == "200" || resp.code.to_s == "201"
+ merge_attributes(JSON.parse(resp))
+ return true
+ else
+ error_response = JSON.parse(resp)
+ if error_response["error"]
+ pe = ParseError.new(error_response["code"], error_response["error"])
else
-
- @attributes.merge!(JSON.parse(resp))
- @attributes.merge!(@unsaved_attributes)
- @unsaved_attributes = {}
- create_setters_and_getters!
-
- return true
+ pe = ParseError.new(resp.code.to_s)
end
- end
+ self.errors.add(pe.code.to_s.to_sym, pe.msg)
+ self.error_instances << pe
+ return false
+ end
end
+
+ def attributes_for_saving
+ @unsaved_attributes = pointerize(@unsaved_attributes)
+ put_attrs = @unsaved_attributes
+ put_attrs.delete('objectId')
+ put_attrs.delete('createdAt')
+ put_attrs.delete('updatedAt')
+ put_attrs
+ end
def update_attributes(attributes = {})
self.update(attributes)
end
@@ -423,10 +477,18 @@
@attributes.update(fresh_object.instance_variable_get('@attributes'))
@unsaved_attributes = {}
self
end
+
+ def dirty?
+ @unsaved_attributes.length > 0
+ end
+
+ def clean?
+ !dirty?
+ end
# provides access to @attributes for getting and setting
def attributes
@attributes ||= self.class.class_attributes
@attributes
@@ -436,15 +498,52 @@
def attributes=(n)
@attributes = n
@attributes
end
+ def get_attribute(k)
+ attrs = @unsaved_attributes[k.to_s] ? @unsaved_attributes : @attributes
+ case attrs[k]
+ when Hash
+ klass_name = attrs[k]["className"]
+ klass_name = "User" if klass_name == "_User"
+ case attrs[k]["__type"]
+ when "Pointer"
+ result = klass_name.constantize.find(attrs[k]["objectId"])
+ when "Object"
+ result = klass_name.constantize.new(attrs[k], false)
+ when "Date"
+ result = DateTime.parse(attrs[k]["iso"]).to_time_in_current_zone
+ when "File"
+ result = attrs[k]["url"]
+ when "GeoPoint"
+ result = ParseGeoPoint.new(attrs[k])
+ end #todo: support other types https://www.parse.com/docs/rest#objects-types
+ else
+ result = attrs["#{k}"]
+ end
+ result
+ end
+
+ def set_attribute(k, v)
+ if v.is_a?(Date) || v.is_a?(Time) || v.is_a?(DateTime)
+ v = self.class.to_date_object(v)
+ elsif v.respond_to?(:to_pointer)
+ v = v.to_pointer
+ end
+ @unsaved_attributes[k.to_s] = v unless v == @attributes[k.to_s] # || @unsaved_attributes[k.to_s]
+ @attributes[k.to_s] = v
+ v
+ end
+
+
# aliasing for idiomatic Ruby
- def id; self.objectId rescue nil; end
+ def id; get_attribute("objectId") rescue nil; end
+ def objectId; get_attribute("objectId") rescue nil; end
- def created_at; self.createdAt; end
+ def created_at; get_attribute("createdAt"); end
- def updated_at; self.updatedAt rescue nil; end
+ def updated_at; get_attribute("updatedAt"); rescue nil; end
def self.included(base)
base.extend(ClassMethods)
end