remote_model 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ .repl_history
2
+ build
3
+ examples/FacebookGraph/build
4
+ examples/FacebookGraph/vendor
5
+ resources/*.nib
6
+ resources/*.momd
7
+ resources/*.storyboardc
8
+ *.gem
9
+ .bundle
10
+ Gemfile.lock
11
+ pkg/*
data/.gitmodules ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in remote_model.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # RemoteModel
2
+
3
+
4
+ JSON API <-> NSObject in one line. Powered by RubyMotion and [BubbleWrap](https://github.com/mattetti/BubbleWrap/).
5
+
6
+ ## Example
7
+
8
+ Let's say we have some User and Question objects retrievable via our API. We can do fun stuff like:
9
+
10
+ ```ruby
11
+ # GET http://ourapi.com/users/1.json -> {:user => {id: 1}}
12
+ user = User.find(1) do |user|
13
+ # async
14
+ # GET http://ourapi.com/users/1/questions.json -> {:questions => [...]}
15
+ Question.find_all(user_id: user.id) do |questions|
16
+ # async
17
+ puts questions
18
+ end
19
+ end
20
+
21
+ # Later...
22
+ => [#<Question @user=#<User>,
23
+ #<Question @user=#<User>]
24
+ ```
25
+
26
+ Here's what our files look like:
27
+
28
+ #### ./app/models/user
29
+ ```ruby
30
+ class User < RemoteModule::RemoteModel
31
+ attr_accessor :id
32
+
33
+ has_many :questions
34
+
35
+ collection_url "users"
36
+ member_url "users/:id"
37
+ end
38
+ ```
39
+
40
+ #### ./app/models/question.rb
41
+ ```ruby
42
+ class Question < RemoteModule::RemoteModel
43
+ attr_accessor :id, :question, :is_active
44
+
45
+ belongs_to :user
46
+
47
+ collection_url "users/:user_id/questions"
48
+ member_url "users/:user_id/questions/:id"
49
+
50
+ custom_urls :active_url => member_url + "/make_active"
51
+
52
+ # The urls substitute params based on a passed hash and/or object's methods,
53
+ # so we define user_id to use for the collection/member urls
54
+ def user_id
55
+ user && user.id
56
+ end
57
+
58
+ # An example of how we can use custom URLs to make custom nice(r) methods
59
+ # EX
60
+ # a_question.make_active(false) do |question|
61
+ # p question.is_active
62
+ # end
63
+ def make_active(active)
64
+ post(self.active_url, payload: {active: active}) do |response, json|
65
+ self.is_active = json[:question][:is_active]
66
+ if block_given?
67
+ yield self
68
+ end
69
+ end
70
+ end
71
+ end
72
+ ```
73
+
74
+ ## Installation
75
+
76
+ ```ruby
77
+ gem install remote_model
78
+ ```
79
+
80
+ And now in your Rakefile, require `remote_model`:
81
+
82
+ ```ruby
83
+ $:.unshift("/Library/RubyMotion/lib")
84
+ require 'motion/project'
85
+ require 'remote_model'
86
+
87
+ Motion::Project::App.setup do |app|
88
+ ...
89
+ end
90
+ ```
91
+
92
+ ## Setup
93
+
94
+ Add an initialization file somewhere, like ./app/initializers/remote_model.rb. This is where we put the API specifications:
95
+
96
+ ```ruby
97
+ module RemoteModule
98
+ class RemoteModel
99
+ # The default URL for our requests.
100
+ # Overrideable per model subclass
101
+ self.root_url = "http://localhost:5000/"
102
+
103
+ # Options attached to every request
104
+ # Appendable per model subclass
105
+ # See BubbleWrap docs on what can be passed in BubbleWrap::HTTP.<method>(url, options)
106
+ self.default_url_options = {
107
+ :headers => {
108
+ "x-api-token" => "some_token",
109
+ "Accept" => "application/json"
110
+ }
111
+ }
112
+ end
113
+ end
114
+ ```
115
+
116
+ ## How?
117
+
118
+ RemoteModel is designed for JSON APIs which return structures with "nice" properties.
119
+
120
+ When you make a request with a RemoteModel (self.get/put/post/delete), the result is always parsed as JSON. The ActiveRecord-esque methods take this JSON and create objects out of it. It's clever and creates the proper associations (belongs_to/has_one/has_many) within the objects, as defined in the models.
121
+
122
+ #### FormatableString
123
+
124
+ The AR methods also use the member/collection defined URLs to make requests. These URLs are a string which you can use :symbols to input dynamic values. These strings can be formatted using a hash and/or using an object (it will look to see if the object responds to these symbols and call the method if applicable):
125
+
126
+ ```ruby
127
+ >> s = RemoteModule::FormatableString.new("url/:param")
128
+ => "url/:param"
129
+ >> s.format({param: 6})
130
+ => "url/6"
131
+ >> obj = Struct.new("Paramer", :param).new(param: 100)
132
+ => ...
133
+ >> s.format({}, obj)
134
+ => "url/100"
135
+ ```
136
+
137
+ RemoteModels can define custom urls and call those as methods (see question.rb above).
138
+
139
+ ## Todo
140
+
141
+ - More tests
142
+ - CoreData integration
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,7 @@
1
+ class AppDelegate
2
+ def application(application, didFinishLaunchingWithOptions:launchOptions)
3
+ @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
4
+ @window.rootViewController = UIViewController.alloc.init
5
+ true
6
+ end
7
+ end
@@ -0,0 +1,2 @@
1
+ .repl_history
2
+ build
@@ -0,0 +1,34 @@
1
+ # Facebook Graph Example
2
+
3
+ The Facebook Graph API is a great example of how powerful RemoteModel is. Facebook auth code adapted from [facebook-auth-ruby-motion-example](https://github.com/aaronfeng/facebook-auth-ruby-motion-example)
4
+
5
+ ![Facebook Example Pic](http://i.imgur.com/BAwTK.png)
6
+
7
+ ## Running
8
+
9
+ You need [motion-cocoapods](https://github.com/HipByte/motion-cocoapods) installed to load the Facebook iOS SDK.
10
+
11
+ It also appears that (as of May 9 2011), motion-cocoapods doesn't play nice with the FB SDK and you need to use `rake --trace` to get it to load correctly.
12
+
13
+ You need to specify an FB app ID, which you can create [in FB's Developer app](https://www.facebook.com/developers):
14
+
15
+ ###### app_delegate.rb
16
+
17
+ ```ruby
18
+ def application(application, didFinishLaunchingWithOptions:launchOptions)
19
+ ...
20
+ @facebook = Facebook.alloc.initWithAppId("YOUR-APP-ID", andDelegate:self)
21
+ ...
22
+ end
23
+ ```
24
+
25
+ ###### Rakefile
26
+
27
+ ```ruby
28
+ Motion::Project::App.setup do |app|
29
+ ...
30
+ fb_app_id = "YOUR-APP-ID"
31
+ app.info_plist['CFBundleURLTypes'] = [{'CFBundleURLSchemes' => ["fb#{fb_app_id}"]}]
32
+ ...
33
+ end
34
+ ```
@@ -0,0 +1,19 @@
1
+ $:.unshift("/Library/RubyMotion/lib")
2
+ require 'motion/project'
3
+ require 'motion-cocoapods'
4
+ require 'remote_model'
5
+
6
+ Motion::Project::App.setup do |app|
7
+ # Use `rake config' to see complete project settings.
8
+ app.name = 'FacebookGraph'
9
+ app.files_dependencies 'app/controllers/facebook_login_controller.rb' => 'app/initializers/remote_model.rb'
10
+ fb_app_id = "YOUR-APP-ID"
11
+ if fb_app_id == "YOUR-APP-ID"
12
+ raise "You need to specify a Facebook App ID in ./Rakefile"
13
+ end
14
+ app.info_plist['CFBundleURLTypes'] = [{'CFBundleURLSchemes' => ["fb#{fb_app_id}"]}]
15
+
16
+ app.pods do
17
+ dependency 'Facebook-iOS-SDK'
18
+ end
19
+ end
@@ -0,0 +1,52 @@
1
+ class AppDelegate
2
+ attr_accessor :facebook
3
+ attr_accessor :navigationController
4
+
5
+ def application(application, didFinishLaunchingWithOptions:launchOptions)
6
+ @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
7
+ @navigationController = UINavigationController.alloc.init
8
+ @window.rootViewController = @navigationController
9
+
10
+ fb_app_id = "YOUR-APP-ID"
11
+ if fb_app_id == "YOUR-APP-ID"
12
+ raise "You need to specify a Facebook App ID in ./app/app_delegate.rb"
13
+ end
14
+ @facebook = Facebook.alloc.initWithAppId(fb_app_id, andDelegate:self)
15
+
16
+ defaults = NSUserDefaults.standardUserDefaults
17
+
18
+ if defaults["FBAccessTokenKey"] && defaults["FBExpirationDateKey"]
19
+ @facebook.accessToken = defaults["FBAccessTokenKey"]
20
+ @facebook.expirationDate = defaults["FBExpirationDateKey"]
21
+ end
22
+
23
+ if facebook.isSessionValid
24
+ openFriendsContorller
25
+ else
26
+ @navigationController.pushViewController(FacebookLoginController.alloc.init, animated: false)
27
+ end
28
+
29
+ @window.rootViewController.wantsFullScreenLayout = true
30
+ @window.makeKeyAndVisible
31
+ true
32
+ end
33
+
34
+ def openFriendsContorller
35
+ @navigationController.setViewControllers([FriendsController.alloc.initWithUserId], animated: false)
36
+ end
37
+
38
+ def fbDidLogin
39
+ defaults = NSUserDefaults.standardUserDefaults
40
+ defaults["FBAccessTokenKey"] = @facebook.accessToken
41
+ defaults["FBExpirationDateKey"] = @facebook.expirationDate
42
+ defaults.synchronize
43
+ openFriendsContorller
44
+ end
45
+
46
+ def application(application,
47
+ openURL:url,
48
+ sourceApplication:sourceApplication,
49
+ annotation:annotation)
50
+ @facebook.handleOpenURL(url)
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ class FacebookLoginController < UIViewController
2
+ def viewDidLoad
3
+ self.title = "Login"
4
+ self.view.backgroundColor = UIColor.whiteColor
5
+
6
+ button = UIButton.buttonWithType UIButtonTypeRoundedRect
7
+ button.when(UIControlEventTouchUpInside) do
8
+ UIApplication.sharedApplication.delegate.facebook.authorize nil
9
+ end
10
+ button.setTitle("FB Login", forState: UIControlStateNormal)
11
+ button.sizeToFit
12
+
13
+ # ugly, dont really do this.
14
+ width, height = button.frame.size.width, button.frame.size.height
15
+ button.frame = CGRectMake(((self.view.frame.size.width - width) / 2).round,
16
+ ((self.view.frame.size.height - height) / 2).round,
17
+ width,
18
+ height)
19
+ self.view.addSubview button
20
+ end
21
+ end
@@ -0,0 +1,82 @@
1
+ class FriendsController < UITableViewController
2
+ attr_reader :user
3
+
4
+ def initWithUserId(id = "me")
5
+ @user = User.new(id: id)
6
+ self
7
+ end
8
+
9
+ def initWithUser(user)
10
+ raise "User cannot be nil" if user.nil?
11
+ @user = user
12
+ self
13
+ end
14
+
15
+ def viewDidLoad
16
+ super
17
+ self.title = "About #{@user.name || @user.id}"
18
+
19
+ defaults = NSUserDefaults.standardUserDefaults
20
+ RemoteModule::RemoteModel.set_access_token(defaults["FBAccessTokenKey"])
21
+
22
+ @activity = UIActivityIndicatorView.alloc.initWithActivityIndicatorStyle(UIActivityIndicatorViewStyleGray)
23
+ self.view.addSubview @activity
24
+ @activity.center = CGPointMake(self.view.frame.size.width/2, self.view.frame.size.height/2)
25
+ @activity.startAnimating
26
+
27
+ @user.find_friends do |user|
28
+ @activity.stopAnimating
29
+ @activity.removeFromSuperview
30
+ self.tableView.reloadData
31
+ end
32
+ end
33
+
34
+ def numberOfSectionsInTableView(tableView)
35
+ return 2
36
+ end
37
+
38
+ def tableView(tableView, titleForHeaderInSection:section)
39
+ return ["Wall Posts", "Friends"][section]
40
+ end
41
+
42
+ def tableView(tableView, numberOfRowsInSection:section)
43
+ return [1, @user.friends.count][section]
44
+ end
45
+
46
+ def layout_friend_in_cell(friend, cell)
47
+ cell.textLabel.text = friend.name
48
+ cell.detailTextLabel.text = friend.id
49
+ end
50
+
51
+ def tableView(tableView, cellForRowAtIndexPath:indexPath)
52
+ reuseIdentifier = ["WallPostsCell","FriendCell"][indexPath.section]
53
+
54
+ cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier) || begin
55
+ cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier:reuseIdentifier)
56
+ cell
57
+ end
58
+
59
+ cell.accessoryType = [UITableViewCellAccessoryDisclosureIndicator, UITableViewCellAccessoryNone][indexPath.section]
60
+
61
+ if indexPath.section == 0
62
+ cell.textLabel.text = "Wall Posts"
63
+ cell.detailTextLabel.text = ""
64
+ else
65
+ friend = @user.friends[indexPath.row]
66
+ layout_friend_in_cell(friend, cell)
67
+ end
68
+
69
+ cell
70
+ end
71
+
72
+ def tableView(tableView, didSelectRowAtIndexPath:indexPath)
73
+ tableView.deselectRowAtIndexPath(indexPath, animated:true)
74
+
75
+ if indexPath.section == 0
76
+ UIApplication.sharedApplication.delegate.navigationController.pushViewController(WallPostsController.alloc.initWithUser(user), animated: true)
77
+ else
78
+ friend = @user.friends[indexPath.row]
79
+ UIApplication.sharedApplication.delegate.navigationController.pushViewController(FriendsController.alloc.initWithUser(friend), animated: true)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,51 @@
1
+ class WallPostsController < UITableViewController
2
+ attr_reader :user
3
+
4
+ def initWithUser(user)
5
+ raise "User cannot be nil" if user.nil?
6
+ @user = user
7
+ self
8
+ end
9
+
10
+ def viewDidLoad
11
+ super
12
+ self.title = "Wall Posts for #{@user.name || @user.id}"
13
+
14
+ defaults = NSUserDefaults.standardUserDefaults
15
+ RemoteModule::RemoteModel.set_access_token(defaults["FBAccessTokenKey"])
16
+
17
+ @activity = UIActivityIndicatorView.alloc.initWithActivityIndicatorStyle(UIActivityIndicatorViewStyleGray)
18
+ self.view.addSubview @activity
19
+ @activity.center = CGPointMake(self.view.frame.size.width/2, self.view.frame.size.height/2)
20
+ @activity.startAnimating
21
+
22
+ @user.find_wall_posts do |user|
23
+ @activity.stopAnimating
24
+ @activity.removeFromSuperview
25
+ self.tableView.reloadData
26
+ end
27
+ end
28
+
29
+ def tableView(tableView, numberOfRowsInSection:section)
30
+ return @user.wall_posts.count
31
+ end
32
+
33
+ def tableView(tableView, cellForRowAtIndexPath:indexPath)
34
+ reuseIdentifier = "WallPostCell"
35
+
36
+ cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier) || begin
37
+ cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier:reuseIdentifier)
38
+ cell
39
+ end
40
+
41
+ wall_post = @user.wall_posts[indexPath.row]
42
+ cell.textLabel.text = wall_post.message
43
+ cell.detailTextLabel.text = wall_post.created_time_string
44
+
45
+ cell
46
+ end
47
+
48
+ def tableView(tableView, didSelectRowAtIndexPath:indexPath)
49
+ tableView.deselectRowAtIndexPath(indexPath, animated:true)
50
+ end
51
+ end
@@ -0,0 +1,14 @@
1
+ module RemoteModule
2
+ class RemoteModel
3
+ self.root_url = "https://graph.facebook.com/"
4
+ self.extension = ""
5
+
6
+ def self.set_access_token(token)
7
+ self.default_url_options = {
8
+ :query => {
9
+ "access_token" => token
10
+ }
11
+ }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,49 @@
1
+ class User < RemoteModule::RemoteModel
2
+ attr_accessor :id, :name, :bio
3
+
4
+ has_many :wall_posts
5
+ has_many :friends => :user
6
+
7
+ collection_url ""
8
+ member_url ":id"
9
+
10
+ custom_urls :friends_url => member_url + "/friends",
11
+ :wall_posts_url => member_url + "/feed"
12
+
13
+ # EX
14
+ # user.find_friends do |user|
15
+ # p user.friends[0]
16
+ # end
17
+ def find_friends(&block)
18
+ get(self.friends_url) do |response, json|
19
+ self.friends = (json && json[:data]) || []
20
+ if json.nil?
21
+ show_privacy_alert("Friends")
22
+ end
23
+ if block
24
+ block.call self
25
+ end
26
+ end
27
+ end
28
+
29
+ def find_wall_posts(&block)
30
+ get(self.wall_posts_url) do |response, json|
31
+ self.wall_posts = (json && json[:data]) || []
32
+ if json.nil?
33
+ show_privacy_alert("Wall Posts")
34
+ end
35
+ if block
36
+ block.call self
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+ def show_privacy_alert(entity)
43
+ alert = UIAlertView.new
44
+ alert.title = "#{entity} not given"
45
+ alert.message = "Denied privacy permissions."
46
+ alert.addButtonWithTitle "OK"
47
+ alert.show
48
+ end
49
+ end
@@ -0,0 +1,43 @@
1
+ class WallPost < RemoteModule::RemoteModel
2
+ attr_accessor :id, :message
3
+ attr_accessor :created_time
4
+
5
+ # if we encounter "from" in the JSON return,
6
+ # use the User class.
7
+ has_one :from => :user
8
+
9
+ collection_url ""
10
+ member_url ":id"
11
+
12
+ def self.from_string_date_formatter
13
+ @from_string_date_formatter ||= begin
14
+ dateFormat = NSDateFormatter.alloc.init
15
+ dateFormat.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
16
+ dateFormat
17
+ end
18
+ end
19
+
20
+ def self.to_string_date_formatter
21
+ @to_string_date_formatter ||= begin
22
+ dateFormat = NSDateFormatter.alloc.init
23
+ dateFormat.dateFormat = "yyyy'-'MM'-'dd"
24
+ dateFormat
25
+ end
26
+ end
27
+
28
+ # EX 2012-05-09T21:57:42+0000
29
+ def created_time=(created_time)
30
+ if created_time.class == String
31
+ @created_time = WallPost.from_string_date_formatter.dateFromString(created_time)
32
+ elsif created_time.class == NSDate
33
+ @created_time = created_time
34
+ else
35
+ raise "Incorrect class for created_time: #{created_time.class.to_s}"
36
+ end
37
+ @created_time
38
+ end
39
+
40
+ def created_time_string
41
+ @created_time.nil? ? "" : WallPost.to_string_date_formatter.stringFromDate(@created_time)
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ describe "Application 'FacebookGraph'" do
2
+ before do
3
+ @app = UIApplication.sharedApplication
4
+ end
5
+
6
+ it "has one window" do
7
+ @app.windows.size.should == 1
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ require "remote_model/version"
2
+ require "bubble-wrap"
3
+
4
+ unless defined?(Motion::Project::Config)
5
+ raise "This file must be required within a RubyMotion project Rakefile."
6
+ end
7
+
8
+ Motion::Project::App.setup do |app|
9
+ Dir.glob(File.join(File.dirname(__FILE__), 'remote_model/*.rb')).each do |file|
10
+ app.files.unshift(file)
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ module RemoteModule
2
+ class FormatableString < String
3
+ # Takes in a hash and spits out the formatted string
4
+ # Checks the delegate first
5
+ def format(params = {}, delegate = nil)
6
+ params ||= {}
7
+ split = self.split '/'
8
+ split.collect { |path|
9
+ ret = path
10
+ if path[0] == ':'
11
+ path_sym = path[1..-1].to_sym
12
+
13
+ curr = nil
14
+ if delegate && delegate.respond_to?(path_sym)
15
+ curr = delegate.send(path_sym)
16
+ end
17
+
18
+ ret = (curr || params[path_sym] || path).to_s
19
+ end
20
+
21
+ ret
22
+ }.join '/'
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,63 @@
1
+ module RemoteModule
2
+ #################################
3
+ # ActiveRecord-esque methods
4
+ class RemoteModel
5
+ class << self
6
+ def find(id, params = {}, &block)
7
+ get(member_url.format(params.merge(id: id))) do |response, json|
8
+ obj = self.new(json)
9
+ request_block_call(block, obj, response)
10
+ end
11
+ end
12
+
13
+ def find_all(params = {}, &block)
14
+ get(collection_url.format(params)) do |response, json|
15
+ objs = []
16
+ arr_rep = nil
17
+ if json.class == Array
18
+ arr_rep = json
19
+ elsif json.class == Hash
20
+ plural_sym = self.pluralize.to_sym
21
+ if json.has_key? plural_sym
22
+ arr_rep = json[plural_sym]
23
+ end
24
+ end
25
+ arr_rep.each { |one_obj_hash|
26
+ objs << self.new(one_obj_hash)
27
+ }
28
+ request_block_call(block, objs, response)
29
+ end
30
+ end
31
+
32
+ # Enables the find
33
+ private
34
+ def request_block_call(block, default_arg, extra_arg)
35
+ if block
36
+ if block.arity == 1
37
+ block.call default_arg
38
+ elsif block.arity == 2
39
+ block.call default_arg, extra_arg
40
+ else
41
+ raise "Not enough arguments to block"
42
+ end
43
+ else
44
+ raise "No block given"
45
+ end
46
+ end
47
+ end
48
+
49
+ # EX
50
+ # a_model.destroy do |response, json|
51
+ # if json[:success]
52
+ # p "success!"
53
+ # end
54
+ # end
55
+ def destroy(&block)
56
+ delete(member_url) do |response, json|
57
+ if block
58
+ block.call response, json
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,218 @@
1
+ module RemoteModule
2
+ class RemoteModel
3
+ HTTP_METHODS = [:get, :post, :put, :delete]
4
+
5
+ class << self
6
+ # These three methods (has_one/many/ + belongs_to)
7
+ # map a symbol to a class for method_missing lookup
8
+ # for each :symbol in params.
9
+ # Can also be used to view the current mappings:
10
+ # EX
11
+ # Question.has_one
12
+ # => {:user => User}
13
+
14
+ # EX
15
+ # self.has_one :question, :answer, :camel_case
16
+ # => {:question => Question, :answer => Answer, :camel_case => CamelCase}
17
+ def has_one(params = [])
18
+ make_fn_lookup "has_one", params, singular_klass_str_lambda
19
+ end
20
+
21
+ # EX
22
+ # self.has_many :questions, :answers, :camel_cases
23
+ # => {:questions => Question, :answers => Answer, :camel_cases => CamelCase}
24
+ def has_many(params = [])
25
+ make_fn_lookup "has_many", params, lambda { |sym| sym.to_s.singularize.split("_").collect {|s| s.capitalize}.join }
26
+ end
27
+
28
+ # EX
29
+ # self.belongs_to :question, :answer, :camel_case
30
+ # => {:question => Question, :answer => Answer, :camel_case => CamelCase}
31
+ def belongs_to(params = [])
32
+ make_fn_lookup "belongs_to", params, singular_klass_str_lambda
33
+ end
34
+
35
+ def pluralize
36
+ self.to_s.downcase + "s"
37
+ end
38
+
39
+ def method_missing(method, *args, &block)
40
+ if self.custom_urls.has_key? method
41
+ return self.custom_urls[method].format(args && args[0], self)
42
+ end
43
+
44
+ super
45
+ end
46
+
47
+ private
48
+ # This is kind of neat.
49
+ # Because models can be mutually dependent (User has a Question, Question has a User),
50
+ # sometimes RubyMotion hasn't loaded the classes when this is run.
51
+ # SO we check to see if the class is loaded; if not, then we just add it to the
52
+ # namespace to make everything run smoothly and assume that by the time the app is running,
53
+ # all the classes have been loaded.
54
+ def make_klass(klass_str)
55
+ begin
56
+ klass = Object.const_get(klass_str)
57
+ rescue NameError => e
58
+ klass = Object.const_set(klass_str, Class.new(RemoteModule::RemoteModel))
59
+ end
60
+ end
61
+
62
+ def singular_klass_str_lambda
63
+ lambda { |sym| sym.to_s.split("_").collect {|s| s.capitalize}.join }
64
+ end
65
+
66
+ # How we fake define_method, essentially.
67
+ # ivar_suffix -> what is the new @ivar called
68
+ # params -> the :symbols to map to classes
69
+ # transform -> how we transform the :symbol into a class name
70
+ def make_fn_lookup(ivar_suffix, params, transform)
71
+ ivar = "@" + ivar_suffix
72
+ if !instance_variable_defined? ivar
73
+ instance_variable_set(ivar, {})
74
+ end
75
+
76
+ sym_to_klass_sym = {}
77
+ if params.class == Symbol
78
+ sym_to_klass_sym[params] = transform.call(params)
79
+ elsif params.class == Array
80
+ params.each {|klass_sym|
81
+ sym_to_klass_sym[klass_sym] = transform.call(klass_sym)
82
+ }
83
+ else
84
+ params.each { |fn_sym, klass_sym| params[fn_sym] = singular_klass_str_lambda.call(klass_sym) }
85
+ sym_to_klass_sym = params
86
+ end
87
+
88
+ sym_to_klass_sym.each do |relation_sym, klass_sym|
89
+ klass_str = klass_sym.to_s
90
+ instance_variable_get(ivar)[relation_sym] = make_klass(klass_str)
91
+ end
92
+
93
+ instance_variable_get(ivar)
94
+ end
95
+ end
96
+
97
+ def initialize(params = {})
98
+ update_attributes(params)
99
+ end
100
+
101
+ def update_attributes(params = {})
102
+ attributes = self.methods - Object.methods
103
+ params.each do |key, value|
104
+ if attributes.member?((key.to_s + "=:").to_sym)
105
+ self.send((key.to_s + "=:").to_sym, value)
106
+ end
107
+ end
108
+ end
109
+
110
+ def remote_model_methods
111
+ methods = []
112
+ [self.class.has_one, self.class.has_many, self.class.belongs_to].each {|fn_hash|
113
+ methods += fn_hash.collect {|sym, klass|
114
+ [sym, (sym.to_s + "=:").to_sym, ("set" + sym.to_s.capitalize).to_sym]
115
+ }.flatten
116
+ }
117
+ methods + RemoteModule::RemoteModel::HTTP_METHODS
118
+ end
119
+
120
+ def methods
121
+ super + remote_model_methods
122
+ end
123
+
124
+ def respond_to?(symbol, include_private = false)
125
+ if remote_model_methods.include? symbol
126
+ return true
127
+ end
128
+
129
+ super
130
+ end
131
+
132
+ def method_missing(method, *args, &block)
133
+ # Check for custom URLs
134
+ if self.class.custom_urls.has_key? method
135
+ return self.class.custom_urls[method].format(args && args[0], self)
136
+ end
137
+
138
+ # has_one relationships
139
+ if self.class.has_one.has_key?(method) || self.class.belongs_to.has_key?(method)
140
+ return instance_variable_get("@" + method.to_s)
141
+ elsif (setter_vals = setter_klass(self.class.has_one, method) || setter_vals = setter_klass(self.class.belongs_to, method))
142
+ klass, hash_symbol = setter_vals
143
+ obj = args[0]
144
+ if obj.class != klass
145
+ obj = klass.new(obj)
146
+ end
147
+ return instance_variable_set("@" + hash_symbol.to_s, obj)
148
+ end
149
+
150
+ # has_many relationships
151
+ if self.class.has_many.has_key?(method)
152
+ ivar = "@" + method.to_s
153
+ if !instance_variable_defined? ivar
154
+ instance_variable_set(ivar, [])
155
+ end
156
+ return instance_variable_get ivar
157
+ elsif (setter_vals = setter_klass(self.class.has_many, method))
158
+ klass, hash_symbol = setter_vals
159
+ ivar = "@" + hash_symbol.to_s
160
+
161
+ tmp = []
162
+ args[0].each do |arg|
163
+ rep = nil
164
+ if arg.class == Hash
165
+ rep = klass.new(arg)
166
+ elsif arg.class == klass
167
+ rep = arg
168
+ end
169
+
170
+ if rep.class.belongs_to.values.member? self.class
171
+ rep.send((rep.class.belongs_to.invert[self.class].to_s + "=").to_sym, self)
172
+ end
173
+
174
+ tmp << rep
175
+ end
176
+
177
+ instance_variable_set(ivar, tmp)
178
+ return instance_variable_get(ivar)
179
+ end
180
+
181
+ # HTTP methods
182
+ if RemoteModule::RemoteModel::HTTP_METHODS.member? method
183
+ return self.class.send(method, *args, &block)
184
+ end
185
+
186
+ super
187
+ end
188
+
189
+ private
190
+ # PARAMS For a given method symbol, look through the hash
191
+ # (which is a map of :symbol => Class)
192
+ # and see if that symbol applies to any keys.
193
+ # RETURNS an array [Klass, symbol] for which the original
194
+ # method symbol applies.
195
+ # EX
196
+ # setter_klass({:answers => Answer}, :answers=)
197
+ # => [Answer, :answers]
198
+ # setter_klass({:answers => Answer}, :setAnswers)
199
+ # => [Answer, :answers]
200
+ def setter_klass(hash, symbol)
201
+
202
+ # go ahead and guess it's of the form :symbol=:
203
+ hash_symbol = symbol.to_s[0..-2].to_sym
204
+
205
+ # if it's the ObjC style setSymbol, change it to that.
206
+ if symbol[0..2] == "set"
207
+ # handles camel case arguments. ex setSomeVariableLikeThis => some_variable_like_this
208
+ hash_symbol = symbol.to_s[3..-1].split(/([[:upper:]][[:lower:]]*)/).delete_if(&:empty?).map(&:downcase).join("_").to_sym
209
+ end
210
+
211
+ klass = hash[hash_symbol]
212
+ if klass.nil?
213
+ return nil
214
+ end
215
+ [klass, hash_symbol]
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,95 @@
1
+ module RemoteModule
2
+ class RemoteModel
3
+ class << self
4
+ attr_accessor :root_url, :default_url_options
5
+ attr_writer :extension
6
+
7
+ def extension
8
+ @extension || (self == RemoteModel ? false : RemoteModel.extension) || ".json"
9
+ end
10
+
11
+ #################################
12
+ # URLs for the resource
13
+ # Can be called by <class>.<url>
14
+ def collection_url(url_format = -1)
15
+ return @collection_url || nil if url_format == -1
16
+
17
+ @collection_url = RemoteModule::FormatableString.new(url_format)
18
+ end
19
+
20
+ def member_url(url_format = -1)
21
+ return @member_url if url_format == -1
22
+
23
+ @member_url = RemoteModule::FormatableString.new(url_format)
24
+ end
25
+
26
+ def custom_urls(params = {})
27
+ @custom_urls ||= {}
28
+ params.each do |fn, url_format|
29
+ @custom_urls[fn] = RemoteModule::FormatableString.new(url_format)
30
+ end
31
+ @custom_urls
32
+ end
33
+
34
+ #################################
35
+ # URL helpers (via BubbleWrap)
36
+ # EX
37
+ # Question.get(a_question.custom_url) do |response, json|
38
+ # p json
39
+ # end
40
+ def get(url, params = {}, &block)
41
+ http_call(:get, url, params, &block)
42
+ end
43
+
44
+ def post(url, params = {}, &block)
45
+ http_call(:post, url, params, &block)
46
+ end
47
+
48
+ def put(url, params = {}, &block)
49
+ http_call(:put, url, params, &block)
50
+ end
51
+
52
+ def delete(url, params = {}, &block)
53
+ http_call(:delete, url, params, &block)
54
+ end
55
+
56
+ private
57
+ def complete_url(fragment)
58
+ if fragment[0..3] == "http"
59
+ return fragment
60
+ end
61
+ (self.root_url || RemoteModule::RemoteModel.root_url) + fragment + self.extension
62
+ end
63
+
64
+ def http_call(method, url, call_options = {}, &block)
65
+ options = call_options
66
+ options.merge!(RemoteModule::RemoteModel.default_url_options || {})
67
+ if query = options.delete(:query)
68
+ if url.index("?").nil?
69
+ url += "?"
70
+ end
71
+ url += query.map{|k,v| "#{k}=#{v}"}.join('&')
72
+ end
73
+ if self.default_url_options
74
+ options.merge!(self.default_url_options)
75
+ end
76
+ BubbleWrap::HTTP.send(method, complete_url(url), options) do |response|
77
+ if response.ok?
78
+ json = BubbleWrap::JSON.parse(response.body.to_str)
79
+ block.call response, json
80
+ else
81
+ block.call response, nil
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ def collection_url(params = {})
88
+ self.class.collection_url.format(params, self)
89
+ end
90
+
91
+ def member_url(params = {})
92
+ self.class.member_url.format(params, self)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,9 @@
1
+ class String
2
+ def pluralize
3
+ self + "s"
4
+ end
5
+
6
+ def singularize
7
+ self[0..-2]
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module RemoteModel
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/remote_model/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "remote_model"
6
+ s.version = RemoteModel::VERSION
7
+ s.authors = ["Clay Allsopp"]
8
+ s.email = ["clay.allsopp@gmail.com"]
9
+ s.homepage = "https://github.com/clayallsopp/remote_model"
10
+ s.summary = "JSON API <-> NSObject via RubyMotion"
11
+ s.description = "JSON API <-> NSObject via RubyMotion. Create REST-aware models."
12
+
13
+ s.files = `git ls-files`.split($\)
14
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
15
+ s.require_paths = ["lib"]
16
+
17
+ s.add_dependency "bubble-wrap"
18
+ end
@@ -0,0 +1,113 @@
1
+ class User < RemoteModule::RemoteModel
2
+ attr_accessor :id
3
+
4
+ has_many :questions
5
+ end
6
+
7
+ class Answer < RemoteModule::RemoteModel
8
+ attr_accessor :id
9
+
10
+ belongs_to :question
11
+ end
12
+
13
+ class Question < RemoteModule::RemoteModel
14
+ attr_accessor :id, :question
15
+
16
+ belongs_to :user
17
+ has_many :answers
18
+
19
+ def user_id
20
+ user && user.id
21
+ end
22
+ end
23
+
24
+ class CamelCaseModel < RemoteModule::RemoteModel
25
+ has_one :another_camel_case_model
26
+ has_many :bunch_of_camel_case_models
27
+ end
28
+
29
+ class AnotherCamelCaseModel < RemoteModule::RemoteModel
30
+ attr_accessor :id
31
+
32
+ belongs_to :camel_case_model
33
+ end
34
+
35
+ class BunchOfCamelCaseModel < RemoteModule::RemoteModel
36
+ belongs_to :camel_case_model
37
+ end
38
+
39
+ describe "The active record-esque stuff" do
40
+ it "creates object from hash" do
41
+ hash = {id: 5, question: "Hello my name is clay"}
42
+ q = Question.new(hash)
43
+ hash.each {|key, value|
44
+ q.send(key).should == value
45
+ }
46
+
47
+ # test other classes
48
+ [User, Answer].each {|klass|
49
+ hash = {id: 1337}
50
+ obj = klass.new(hash)
51
+ obj.id.should == hash[:id]
52
+ }
53
+ end
54
+
55
+ it "creates nested objects" do
56
+ hash = {id: 5, question: "question this", user: {id: 6}}
57
+ q = Question.new(hash)
58
+ q.user.class.should == User
59
+ q.user.id.should == hash[:user][:id]
60
+ end
61
+
62
+ def check_question_and_answers(q, answers)
63
+ q.answers.count.should == answers.count
64
+ q.answers.each_with_index { |answer, index|
65
+ answer.class.should == Answer
66
+ answer.id.should == answers[index][:id]
67
+ answer.question.should == q
68
+ }
69
+ end
70
+
71
+ it "creates nested relationships" do
72
+ answers = [{id: 3, id: 100}]
73
+ hash = {id: 5, question: "my question", answers: answers}
74
+ q = Question.new(hash)
75
+ check_question_and_answers(q, answers)
76
+ end
77
+
78
+ it "creates inception relationships" do
79
+ answers = [[], [{id: 3, id: 100}]]
80
+ questions = [{id: 8, question: "question 8"}, {id: 10, question: "question 10", answers: answers[1]}]
81
+ hash = {id: 1, questions: questions}
82
+ u = User.new(hash)
83
+ u.questions.count.should == questions.count
84
+ u.questions.each_with_index {|q, index|
85
+ q.class.should == Question
86
+ q.user.should == u
87
+ q.id.should == questions[index][:id]
88
+ q.question.should == questions[index][:question]
89
+ if q.answers.count > 0
90
+ check_question_and_answers(q, answers[index])
91
+ end
92
+ }
93
+ end
94
+
95
+ it "works with camel cased models" do
96
+ c = CamelCaseModel.new({another_camel_case_model: {id: 7}, bunch_of_camel_case_models: [{}, {}]})
97
+ c.another_camel_case_model.class.should == AnotherCamelCaseModel
98
+ c.another_camel_case_model.id.should == 7
99
+ c.bunch_of_camel_case_models.count.should == 2
100
+ c.bunch_of_camel_case_models.each {|model|
101
+ model.class.should == BunchOfCamelCaseModel
102
+ }
103
+
104
+ c.setAnotherCamelCaseModel({id: 8})
105
+ c.another_camel_case_model.id.should == 8
106
+
107
+ c.setBunchOfCamelCaseModels([{}, {}, {}])
108
+ c.bunch_of_camel_case_models.count.should == 3
109
+ c.bunch_of_camel_case_models.each {|model|
110
+ model.class.should == BunchOfCamelCaseModel
111
+ }
112
+ end
113
+ end
@@ -0,0 +1,35 @@
1
+ class CustomUrlModel < RemoteModule::RemoteModel
2
+ collection_url "collection"
3
+ member_url "collection/:id"
4
+
5
+ custom_urls :a_url => "custom", :format_url => "custom/:var",
6
+ :method_url => "custom/:a_method"
7
+
8
+ def id
9
+ 8
10
+ end
11
+
12
+ def a_method
13
+ 10
14
+ end
15
+ end
16
+
17
+ describe "URLs" do
18
+ it "should make visible urls at class and instance level" do
19
+ CustomUrlModel.a_url.should == "custom"
20
+ CustomUrlModel.collection_url.should == "collection"
21
+ CustomUrlModel.member_url.should == "collection/:id"
22
+
23
+ # NOTE that Class.member_url(params) won't work (it's the setter).
24
+ CustomUrlModel.member_url.format(:id => 9).should == "collection/9"
25
+
26
+ c = CustomUrlModel.new
27
+ c.collection_url.should == "collection"
28
+ c.member_url.should == "collection/8"
29
+ c.a_url.should == "custom"
30
+
31
+ CustomUrlModel.format_url.should == "custom/:var"
32
+ c.format_url(:var => 3).should == "custom/3"
33
+ c.method_url.should == "custom/10"
34
+ end
35
+ end
@@ -0,0 +1,14 @@
1
+ describe "The requests stuff" do
2
+ it "should parse json" do
3
+ ran = false
4
+ RemoteModule::RemoteModel.get("http://graph.facebook.com/btaylor") do |response, json|
5
+ json.class.should == Hash
6
+ response.ok?.should == true
7
+ ran = true
8
+ end
9
+ # really stupid, haven't made an async request example...
10
+ wait 5.0 do
11
+ ran.should == true
12
+ end
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remote_model
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Clay Allsopp
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-05-23 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bubble-wrap
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ description: JSON API <-> NSObject via RubyMotion. Create REST-aware models.
27
+ email:
28
+ - clay.allsopp@gmail.com
29
+ executables: []
30
+
31
+ extensions: []
32
+
33
+ extra_rdoc_files: []
34
+
35
+ files:
36
+ - .gitignore
37
+ - .gitmodules
38
+ - Gemfile
39
+ - README.md
40
+ - Rakefile
41
+ - app/app_delegate.rb
42
+ - examples/FacebookGraph/.gitignore
43
+ - examples/FacebookGraph/README.md
44
+ - examples/FacebookGraph/Rakefile
45
+ - examples/FacebookGraph/app/app_delegate.rb
46
+ - examples/FacebookGraph/app/controllers/facebook_login_controller.rb
47
+ - examples/FacebookGraph/app/controllers/friends_controller.rb
48
+ - examples/FacebookGraph/app/controllers/wall_posts_controller.rb
49
+ - examples/FacebookGraph/app/initializers/remote_model.rb
50
+ - examples/FacebookGraph/app/models/User.rb
51
+ - examples/FacebookGraph/app/models/wall_post.rb
52
+ - examples/FacebookGraph/spec/main_spec.rb
53
+ - lib/remote_model.rb
54
+ - lib/remote_model/formatable_string.rb
55
+ - lib/remote_model/record.rb
56
+ - lib/remote_model/remote_model.rb
57
+ - lib/remote_model/requests.rb
58
+ - lib/remote_model/string.rb
59
+ - lib/remote_model/version.rb
60
+ - remote_model.gemspec
61
+ - spec/record_spec.rb
62
+ - spec/remote_model_spec.rb
63
+ - spec/requests_spec.rb
64
+ homepage: https://github.com/clayallsopp/remote_model
65
+ licenses: []
66
+
67
+ post_install_message:
68
+ rdoc_options: []
69
+
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: "0"
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: "0"
84
+ requirements: []
85
+
86
+ rubyforge_project:
87
+ rubygems_version: 1.8.21
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: JSON API <-> NSObject via RubyMotion
91
+ test_files:
92
+ - spec/record_spec.rb
93
+ - spec/remote_model_spec.rb
94
+ - spec/requests_spec.rb