├── .gitignore ├── .repl_history ├── Gemfile ├── LICENSE ├── ParseModel.gemspec ├── README.md ├── Rakefile └── lib ├── ParseModel.rb └── ParseModel ├── Model.rb ├── User.rb ├── cloud.rb ├── query.rb └── version.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | build 19 | -------------------------------------------------------------------------------- /.repl_history: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelevie/ParseModel/1d5892ccaaaea642d8afd702c0fa8e7838a10482/.repl_history -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in ParseModel.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Alan deLevie 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /ParseModel.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/ParseModel/version', __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "ParseModel" 6 | s.version = ParseModel::VERSION 7 | s.authors = ["Alan deLevie"] 8 | s.email = ["adelevie@gmail.com"] 9 | s.homepage = "" 10 | s.summary = %q{An Active Record pattern for your Parse models on RubyMotion.} 11 | s.description = %q{An Active Record pattern for your Parse models on RubyMotion.} 12 | 13 | s.rubyforge_project = "ParseModel" 14 | 15 | s.files = `git ls-files`.split("\n") 16 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 17 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 18 | s.require_paths = ["lib"] 19 | 20 | # specify any dependencies here; for example: 21 | # s.add_development_dependency "rspec" 22 | # s.add_runtime_dependency "rest-client" 23 | end 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | ParseModel provides an Active Record pattern to your Parse models on RubyMotion. 4 | 5 | I'm using ParseModel internally for a project, slowly but surely making it much, much better. When the project is near completion, I'm going to extract additional functionality into the gem. 6 | 7 | Expect a much more Ruby-esque API that still leaves full access to all of the features in the Parse iOS SDK. I'm not trying to re-implement features from the Parse iOS SDK, I just want to make them easier to use. Moreover, you should be able to rely on [Parse's iOS docs](https://parse.com/docs/ios/api/) when using ParseModel. 8 | 9 | If you have any questions or suggestions, email me. 10 | 11 | ## Usage 12 | 13 | Create a model: 14 | 15 | ```ruby 16 | class Post 17 | include ParseModel::Model 18 | 19 | fields :title, :body, :author 20 | end 21 | ``` 22 | 23 | Create an instance: 24 | 25 | ```ruby 26 | p = Post.new 27 | p.title = "Why RubyMotion Is Better Than Objective-C" 28 | p.author = "Josh Symonds" 29 | p.body = "trololol" 30 | ``` 31 | 32 | `ParseModel::Model` objects will `respond_to?` to all methods available to [`PFObject`](https://parse.com/docs/ios/api/Classes/PFObject.html) in the Parse iOS SDK. You can also access the `PFObject` instance directly with, you guessed it, `ParseModel::Model#PFObject`. 33 | 34 | ### Saving objects 35 | 36 | ```ruby 37 | p = Post.new 38 | p.author = "Alan" 39 | 40 | # save using main thread (blocking) 41 | p.save 42 | 43 | # save eventually (non-blocking) 44 | p.saveEventually 45 | 46 | # save in background with a block 47 | p.saveInBackgroundWithBlock(lambda do |success, error| 48 | # do something... 49 | end) 50 | 51 | ``` 52 | 53 | ### New: Cloud Code functions 54 | 55 | ```ruby 56 | # with block: 57 | ParseModel::Cloud.callFunction("myFunction", {"myParam" => "myValue"}) do |result, error| 58 | # do something... 59 | end 60 | 61 | # without block: 62 | ParseModel::Cloud.callFunction("myFunction", {"myParam" => "myValue"}) 63 | ``` 64 | 65 | #### Cloud Code Examples 66 | ```javascript 67 | Parse.Cloud.define('trivial', function(request, response) { 68 | response.success(request.params); 69 | }); 70 | ``` 71 | 72 | ```ruby 73 | ParseModel::Cloud.callFunction("trivial", {"foo" => "bar"}) 74 | #=> {"foo"=>"bar"} 75 | ``` 76 | 77 | ```javascript 78 | // implementation of random queries using Parse Cloud Code 79 | // see https://parse.com/questions/random-search-its-possibile for setup details 80 | 81 | Parse.Cloud.define('randomNouns', function(request, response) { 82 | var NounMaster = Parse.Object.extend("NounMaster"); 83 | var maxQuery = new Parse.Query(NounMaster); 84 | maxQuery.first({ 85 | success: function(object) { 86 | var max = object.get("nextWordIndex"); 87 | var n = 10; 88 | if(request.params.count) { n = request.params.count; } 89 | var arr = []; 90 | while (arr.length < n) { 91 | arr.push(Math.ceil(Math.random() * max)); 92 | } 93 | var indexes = arr; 94 | var Noun = Parse.Object.extend("Noun"); 95 | var nounQuery = new Parse.Query(Noun); 96 | nounQuery.containedIn("index", indexes); 97 | nounQuery.find({ 98 | success: function(results) { 99 | response.success(results); 100 | } 101 | }); 102 | } 103 | }); 104 | }); 105 | ``` 106 | 107 | ```ruby 108 | ParseModel::Cloud.callFunction("randomNouns", {"count" => 3}) 109 | #=> [#, #, #] 110 | ``` 111 | 112 | ### Users 113 | 114 | ```ruby 115 | class User 116 | include ParseModel::User 117 | end 118 | 119 | user = User.new 120 | user.username = "adelevie" 121 | user.email = "adelevie@gmail.com" 122 | user.password = "foobar" 123 | user.signUp 124 | ``` 125 | Querying the User class requires a special PFQuery object. 126 | ```ruby 127 | userQuery = User.query 128 | #=> # 129 | userQuery.whereKey("email", equalTo:"bill@ms.com") 130 | userQuery.findObjects 131 | #=> [#] 132 | ``` 133 | The User.all method simply runs a query with no conditions, and returns an array of User objects. 134 | ```ruby 135 | users = User.all 136 | #=> [#>] 137 | ``` 138 | For additional details on User query methods, see: https://parse.com/docs/ios_guide#users-querying/iOS 139 | 140 | `ParseModel::User` delegates to `PFUser` in a very similar fashion as `ParseModel::Model` delegates to `PFOBject`. 141 | 142 | #### Current User 143 | 144 | ```ruby 145 | if User.current_user 146 | @user = User.current_user 147 | end 148 | ``` 149 | 150 | ### Queries 151 | 152 | Parse provides some great ways to query for objects: in the current blocking thread (`PFQuery#findObjects`, or in the background with a block (`PFQuery#findObjectsInBackGroundWithBlock()`). 153 | 154 | These method names are a little long and verbose for my taste, so I added a little but of syntactic sugar: 155 | 156 | ```ruby 157 | query = Post.query #=> ... this is a subclass of PFQuery 158 | query.whereKey("author", equalTo:"Alan") 159 | query.find # finds objects in the main thread, like PFQuery#findObjects 160 | 161 | # Or run the query in a background thread 162 | 163 | query.find do |objects, error| 164 | puts "You have #{objects.length} objects of class #{objects.first.class}." 165 | end 166 | ``` 167 | 168 | By passing a two-argument block to `ParseModel::Query#find(&block)`, the query will automatically run in the background, with the code from the given block executing on completion. 169 | 170 | Also note that `ParseModel::Query#find` and `ParseModel::Query#find(&block)` return `ParseModel::Model` objects, and not `PFObject`s. 171 | 172 | Because I want Parse's documentation to be as relevant as possible, here's how I'm matching up `ParseModel::Query`'s convenience methods to `PFQuery`: 173 | 174 | `ParseModel::Query` method | Equivalent `PFQuery` method 175 | ---------------------------|----------------------------| 176 | `ParseModel::Query#find`| [`PFQuery#findObjects`][findObjects] 177 | `ParseModel::Query#find(&block)`| [`PFQuery#findObjectsInBackgroundWithBlock`][findObjectsInBackgroundWithBlock] 178 | `ParseModel::Query#getFirst`| [`PFQuery#getFirstObject`][getFirstObject] 179 | `ParseModel::Query#get(id)`| [`PFQuery#getObjectWithId`][getObjectWithId] 180 | `ParseModel::Query#get(id, &block)`| [`PFQuery#getObjectInBackgroundWithId:block:`][getObjectInBackgroundWithId] 181 | `ParseModel::Query#count`| [`PFQuery#countObjects`][countObjects] 182 | `ParseModel::Query#count(&block)`| [`PFQuery#countObjectsInBackgroundWithBlock`][countObjectsInBackgroundWithBlock] 183 | 184 | [findObjects]: https://parse.com/docs/ios/api/Classes/PFQuery.html#//api/name/findObjects 185 | [findObjectsInBackGroundWithBlock]: https://parse.com/docs/ios/api/Classes/PFQuery.html#//api/name/countObjectsInBackgroundWithBlock: 186 | [getFirstObject]: https://parse.com/docs/ios/api/Classes/PFQuery.html#//api/name/getFirstObject 187 | [getFirstObjectInBackgroundWithBlock]: https://parse.com/docs/ios/api/Classes/PFQuery.html#//api/name/getFirstObjectInBackgroundWithBlock: 188 | [getObjectWithId]: https://parse.com/docs/ios/api/Classes/PFQuery.html#//api/name/getObjectWithId 189 | [getObjectInBackgroundWithId]: https://parse.com/docs/ios/api/Classes/PFQuery.html#//api/name/getFirstObjectInBackgroundWithBlock: 190 | [countObjects]: https://parse.com/docs/ios/api/Classes/PFQuery.html#//api/name/countObjects 191 | [countObjectsInBackgroundWithBlock]: https://parse.com/docs/ios/api/Classes/PFQuery.html#//api/name/countObjectsInBackgroundWithBlock: 192 | 193 | 194 | Essentially, I'm omitting the words "object" and "InBackgroundWithBlock" from `ParseModel`'s method signatures. I think it's a reasonable assumption that it can simply be implied that we're dealing with "objects." If I'm passing a block, it's repetitive to declare that I'm passing a block. 195 | 196 | ## Installation 197 | 198 | Either `gem install ParseModel` then `require 'ParseModel'` in your `Rakefile`, OR 199 | `gem "ParseModel"` in your Gemfile. ([Instructions for Bundler setup with Rubymotion)](http://thunderboltlabs.com/posts/using-bundler-with-rubymotion) 200 | 201 | Somewhere in your code, such as `app/app_delegate.rb` set your API keys: 202 | 203 | ```ruby 204 | Parse.setApplicationId("1234567890", clientKey:"abcdefghijk") 205 | ``` 206 | 207 | To install the Parse iOS SDK in your RubyMotion project, add the Parse iOS SDK to your `vendor` folder, then add the following to your `Rakefile`: 208 | 209 | ```ruby 210 | app.libs << '/usr/lib/libz.1.1.3.dylib' 211 | app.libs << '/usr/lib/libsqlite3.dylib' 212 | app.frameworks += [ 213 | 'AudioToolbox', 214 | 'CFNetwork', 215 | 'CoreGraphics', 216 | 'CoreLocation', 217 | 'MobileCoreServices', 218 | 'QuartzCore', 219 | 'Security', 220 | 'StoreKit', 221 | 'SystemConfiguration'] 222 | 223 | # in case app.deployment_target < '6.0' 224 | app.weak_frameworks += [ 225 | 'Accounts', 226 | 'AdSupport', 227 | 'Social'] 228 | 229 | app.vendor_project('vendor/Parse.framework', :static, 230 | :products => ['Parse'], 231 | :headers_dir => 'Headers') 232 | ``` 233 | 234 | More info on installation: [this](http://www.rubymotion.com/developer-center/guides/project-management/#_using_3rd_party_libraries) and [this](http://stackoverflow.com/a/10453895/94154). 235 | 236 | ## License 237 | 238 | See LICENSE.txt 239 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | #$:.unshift("/Library/RubyMotion/lib") 5 | #require 'motion/project' 6 | 7 | #require 'bundler' 8 | #Bundler.setup -------------------------------------------------------------------------------- /lib/ParseModel.rb: -------------------------------------------------------------------------------- 1 | require "ParseModel/version" 2 | 3 | Motion::Project::App.setup do |app| 4 | Dir.glob(File.join(File.dirname(__FILE__), "ParseModel/*.rb")).each do |file| 5 | app.files.unshift(file) 6 | end 7 | end -------------------------------------------------------------------------------- /lib/ParseModel/Model.rb: -------------------------------------------------------------------------------- 1 | module ParseModel 2 | 3 | module Model 4 | attr_accessor :PFObject 5 | 6 | def initialize(arg=nil) 7 | if arg.is_a?(PFObject) 8 | @PFObject = arg 9 | else 10 | @PFObject = PFObject.objectWithClassName(self.class.to_s) 11 | if arg.is_a?(Hash) 12 | arg.each do |k,v| 13 | @PFObject.setObject(v, forKey:k) if fields.include?(k) 14 | end 15 | end 16 | end 17 | end 18 | 19 | def method_missing(method, *args, &block) 20 | if fields.include?(method) 21 | @PFObject.objectForKey(method) 22 | elsif fields.map {|f| "#{f}="}.include?("#{method}") 23 | method = method.split("=")[0] 24 | @PFObject.setObject(args.first, forKey:method) 25 | elsif @PFObject.respond_to?(method) 26 | @PFObject.send(method, *args, &block) 27 | else 28 | super 29 | end 30 | end 31 | 32 | def fields 33 | self.class.send(:get_fields) 34 | end 35 | 36 | module ClassMethods 37 | def fields(*args) 38 | args.each {|arg| field(arg)} 39 | end 40 | 41 | def field(name) 42 | @fields ||= [] 43 | @fields << name 44 | end 45 | 46 | def query 47 | ParseModel::ParseQuery.alloc.initWithClassNameAndClassObject(self.name, classObject:self) 48 | end 49 | 50 | def get_fields 51 | @fields 52 | end 53 | end 54 | 55 | def self.included(base) 56 | base.extend(ClassMethods) 57 | end 58 | 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/ParseModel/User.rb: -------------------------------------------------------------------------------- 1 | module ParseModel 2 | module User 3 | attr_accessor :PFUser 4 | 5 | RESERVED_KEYS = ['username', 'password', 'email'] 6 | 7 | def initialize(pf_user_object=nil) 8 | if pf_user_object 9 | @PFUser = pf_user_object 10 | else 11 | @PFUser = PFUser.user 12 | end 13 | end 14 | 15 | def method_missing(method, *args, &block) 16 | if RESERVED_KEYS.include?("#{method}") 17 | @PFUser.send(method) 18 | elsif RESERVED_KEYS.map {|f| "#{f}="}.include?("#{method}") 19 | @PFUser.send(method, args.first) 20 | elsif fields.include?(method) 21 | @PFUser.objectForKey(method) 22 | elsif fields.map {|f| "#{f}="}.include?("#{method}") 23 | method = method.split("=")[0] 24 | @PFUser.setObject(args.first, forKey:method) 25 | elsif @PFUser.respond_to?(method) 26 | @PFUser.send(method, *args, &block) 27 | else 28 | super 29 | end 30 | end 31 | 32 | def fields 33 | self.class.send(:get_fields) 34 | end 35 | 36 | module ClassMethods 37 | def fields(*args) 38 | args.each {|arg| field(arg)} 39 | end 40 | 41 | def field(name) 42 | @fields ||= [] 43 | @fields << name 44 | end 45 | 46 | def current_user 47 | if PFUser.currentUser 48 | u = new 49 | u.PFUser = PFUser.currentUser 50 | return u 51 | else 52 | return nil 53 | end 54 | end 55 | 56 | def query 57 | PFUser.query 58 | end 59 | 60 | def all(&block) 61 | return PFUser.query.findObjects.map {|obj| self.new(obj)} unless block_given? 62 | 63 | PFUser.query.findObjectsInBackgroundWithBlock(lambda do |objects, error| 64 | objects = objects.map {|obj| self.new(obj)} if objects 65 | block.call(objects, error) 66 | end) 67 | end 68 | 69 | def get_fields 70 | @fields 71 | end 72 | 73 | end 74 | 75 | def self.included(base) 76 | base.extend(ClassMethods) 77 | end 78 | 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/ParseModel/cloud.rb: -------------------------------------------------------------------------------- 1 | 2 | # with block: 3 | # ParseModel::Cloud.callFunction("myFunction", {"myParam" => "myValue"}) do |result, error| 4 | # # do something... 5 | # end 6 | 7 | # without block: 8 | # ParseModel::Cloud.callFunction("myFunction", {"myParam" => "myValue"}) 9 | module ParseModel 10 | class Cloud 11 | def self.callFunction(function, params={}, &block) 12 | return PFCloud.callFunction(function, withParameters:params) unless block_given? 13 | 14 | PFCloud.callFunctionInBackground(function, withParameters:params, block:lambda do |result, error| 15 | block.call(result, error) 16 | end) 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /lib/ParseModel/query.rb: -------------------------------------------------------------------------------- 1 | module ParseModel 2 | class ParseQuery < PFQuery 3 | 4 | def setClassObject(classObject) 5 | @classObject = classObject 6 | self 7 | end 8 | 9 | def initWithClassNameAndClassObject(className, classObject:myClassObject) 10 | @className = className 11 | self.initWithClassName(className) 12 | self.setClassObject(myClassObject) 13 | self 14 | end 15 | 16 | def find(&block) 17 | return self.findObjects.map {|obj| @classObject.new(obj)} unless block_given? 18 | 19 | self.findObjectsInBackgroundWithBlock(lambda do |objects, error| 20 | objects = objects.map {|obj| @classObject.new(obj)} if objects 21 | block.call(objects, error) 22 | end) 23 | end 24 | 25 | def getFirst(&block) 26 | return @classObject.new(self.getFirstObject) unless block_given? 27 | 28 | self.getFirstObjectInBackgroundWithBlock(lambda do |object, error| 29 | obj = @classObject.new(object) if object 30 | block.call(obj, error) 31 | end) 32 | end 33 | 34 | def get(id, &block) 35 | return @classObject.new(self.getObjectWithId(id)) unless block_given? 36 | 37 | self.getObjectInBackgroundWithId(id, block:lambda do |object, error| 38 | obj = @classObject.new(object) if object 39 | block.call(obj, error) 40 | end) 41 | end 42 | 43 | def count(&block) 44 | return self.countObjects unless block_given? 45 | 46 | self.countObjectsInBackgroundWithBlock(lambda do |count, error| 47 | block.call(count, error) 48 | end) 49 | end 50 | 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /lib/ParseModel/version.rb: -------------------------------------------------------------------------------- 1 | module ParseModel 2 | VERSION = "0.0.6" 3 | end 4 | --------------------------------------------------------------------------------