remote_model 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +11 -0
- data/.gitmodules +0 -0
- data/Gemfile +4 -0
- data/README.md +142 -0
- data/Rakefile +1 -0
- data/app/app_delegate.rb +7 -0
- data/examples/FacebookGraph/.gitignore +2 -0
- data/examples/FacebookGraph/README.md +34 -0
- data/examples/FacebookGraph/Rakefile +19 -0
- data/examples/FacebookGraph/app/app_delegate.rb +52 -0
- data/examples/FacebookGraph/app/controllers/facebook_login_controller.rb +21 -0
- data/examples/FacebookGraph/app/controllers/friends_controller.rb +82 -0
- data/examples/FacebookGraph/app/controllers/wall_posts_controller.rb +51 -0
- data/examples/FacebookGraph/app/initializers/remote_model.rb +14 -0
- data/examples/FacebookGraph/app/models/User.rb +49 -0
- data/examples/FacebookGraph/app/models/wall_post.rb +43 -0
- data/examples/FacebookGraph/spec/main_spec.rb +9 -0
- data/lib/remote_model.rb +12 -0
- data/lib/remote_model/formatable_string.rb +25 -0
- data/lib/remote_model/record.rb +63 -0
- data/lib/remote_model/remote_model.rb +218 -0
- data/lib/remote_model/requests.rb +95 -0
- data/lib/remote_model/string.rb +9 -0
- data/lib/remote_model/version.rb +3 -0
- data/remote_model.gemspec +18 -0
- data/spec/record_spec.rb +113 -0
- data/spec/remote_model_spec.rb +35 -0
- data/spec/requests_spec.rb +14 -0
- metadata +94 -0
data/.gitignore
ADDED
data/.gitmodules
ADDED
File without changes
|
data/Gemfile
ADDED
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"
|
data/app/app_delegate.rb
ADDED
@@ -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,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
|
data/lib/remote_model.rb
ADDED
@@ -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,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
|
data/spec/record_spec.rb
ADDED
@@ -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
|