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