remote_model 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+

|
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
|