meteor-motion 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.repl_history +0 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +166 -0
  7. data/Rakefile +17 -0
  8. data/app/app_delegate.rb +11 -0
  9. data/app/controllers/book_controller.rb +92 -0
  10. data/app/controllers/book_list_controller.rb +105 -0
  11. data/app/controllers/connection_controller.rb +83 -0
  12. data/app/controllers/login_controller.rb +35 -0
  13. data/lib/meteor-motion.rb +12 -0
  14. data/lib/meteor-motion/version.rb +3 -0
  15. data/meteor-motion.gemspec +27 -0
  16. data/motion/adapters/motion_model.rb +61 -0
  17. data/motion/client.rb +179 -0
  18. data/motion/collection.rb +50 -0
  19. data/motion/collections/default.rb +56 -0
  20. data/motion/collections/motion_model.rb +52 -0
  21. data/motion/ddp.rb +161 -0
  22. data/motion/srp/securerandom.rb +248 -0
  23. data/motion/srp/srp.rb +250 -0
  24. data/spec/adapters/motion_model_spec.rb +38 -0
  25. data/spec/client_spec.rb +104 -0
  26. data/spec/collection_spec.rb +63 -0
  27. data/spec/collections/default_spec.rb +46 -0
  28. data/spec/collections/motion_model_spec.rb +69 -0
  29. data/spec/ddp_spec.rb +123 -0
  30. data/spec/server/.meteor/.gitignore +1 -0
  31. data/spec/server/.meteor/packages +9 -0
  32. data/spec/server/.meteor/release +1 -0
  33. data/spec/server/collections/books.js +11 -0
  34. data/spec/server/server/fixtures.js +28 -0
  35. data/spec/server/server/publications.js +3 -0
  36. data/spec/server/smart.json +3 -0
  37. data/vendor/SocketRocket/NSData+SRB64Additions.h +24 -0
  38. data/vendor/SocketRocket/NSData+SRB64Additions.m +39 -0
  39. data/vendor/SocketRocket/SRWebSocket.h +114 -0
  40. data/vendor/SocketRocket/SRWebSocket.m +1757 -0
  41. data/vendor/SocketRocket/SocketRocket-Prefix.pch +27 -0
  42. data/vendor/SocketRocket/SocketRocket.bridgesupport +160 -0
  43. data/vendor/SocketRocket/base64.c +314 -0
  44. data/vendor/SocketRocket/base64.h +34 -0
  45. metadata +190 -0
@@ -0,0 +1,83 @@
1
+ class ConnectionController < UIViewController
2
+
3
+ def viewDidLoad
4
+ super
5
+
6
+ self.view.backgroundColor = UIColor.whiteColor
7
+ @label = UILabel.alloc.initWithFrame(CGRectZero)
8
+ @label.text = "Connecting..."
9
+ @label.sizeToFit
10
+ @label.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2)
11
+ self.view.addSubview @label
12
+
13
+ @meteorClient = MeteorMotion::Client.new
14
+ @meteorClient.on_error( self.method(:meteor_error) )
15
+ @meteorClient.connect 'localhost', 3000, self.method(:connected)
16
+
17
+ end
18
+
19
+ def connected result, details
20
+ if result
21
+ @meteorClient.add_collection('books')
22
+ @meteorClient.subscribe('books')
23
+
24
+ form = buildLoginForm
25
+
26
+ @controller = LoginController.alloc.initWithForm(form)
27
+ @controller.meteor = @meteorClient
28
+
29
+ self.presentViewController( @controller, animated: true, completion: nil )
30
+ else
31
+ exit(0)
32
+ end
33
+ end
34
+
35
+ # Meteor basic error handler - alert error to screen
36
+ #
37
+ def meteor_error code, reason, details
38
+ if reason != :unknown
39
+ alert = UIAlertView.new
40
+ alert.message = reason.to_s
41
+ alert.addButtonWithTitle "OK"
42
+ alert.show
43
+ else
44
+ puts "Meteor Error: #{reason}. Details: #{details}"
45
+ end
46
+ end
47
+
48
+
49
+ def buildLoginForm
50
+ form = Formotion::Form.new
51
+
52
+ form.build_section do |section|
53
+ section.title = "Credentials"
54
+
55
+ section.build_row do |row|
56
+ row.title = "Username"
57
+ row.key = :user
58
+ row.type = :string
59
+ row.auto_correction = :no
60
+ row.auto_capitalization = :none
61
+ row.placeholder = 'Your username'
62
+ end
63
+
64
+ section.build_row do |row|
65
+ row.title = "Password"
66
+ row.key = :pass
67
+ row.type = :string
68
+ row.secure = true
69
+ row.auto_correction = :no
70
+ row.auto_capitalization = :none
71
+ row.placeholder = 'Your password'
72
+ end
73
+
74
+ section.build_row do |row|
75
+ row.title = 'Submit'
76
+ row.type = :submit
77
+ end
78
+ end
79
+
80
+ return form
81
+ end
82
+
83
+ end
@@ -0,0 +1,35 @@
1
+ class LoginController < Formotion::FormController
2
+ attr_accessor :meteor
3
+
4
+ def viewDidLoad
5
+ super
6
+
7
+ self.view.backgroundColor = UIColor.whiteColor
8
+ self.title = "Login"
9
+
10
+ self.form.on_submit do |form|
11
+ data = form.render
12
+ self.view.endEditing(true)
13
+
14
+ @meteor.login_with_username( data[:user], data[:pass], self.method(:login_handler) )
15
+ end
16
+
17
+ end
18
+
19
+ def login_handler action, details
20
+ if action == :success
21
+ controller = BookListController.alloc.initWithNibName(nil, bundle: nil)
22
+ controller.meteor = @meteor
23
+ navigationController = UINavigationController.alloc.initWithRootViewController(controller)
24
+
25
+ self.presentViewController(navigationController , animated: true, completion: nil)
26
+
27
+ elsif action == :error
28
+ alert = UIAlertView.new
29
+ alert.message = "Error: #{details['reason'].to_s}"
30
+ alert.addButtonWithTitle "OK"
31
+ alert.show
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,12 @@
1
+ require "meteor-motion/version"
2
+ BW.require 'motion/**/*.rb'
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
+ app.vendor_project(File.expand_path(File.join(File.dirname(__FILE__), '../vendor/SocketRocket')), :static, cflags: "-fobjc-arc")
10
+ app.libs += ['/usr/lib/libicucore.dylib']
11
+ app.frameworks += ['CFNetwork', 'Security', 'Foundation']
12
+ end
@@ -0,0 +1,3 @@
1
+ module MeteorMotion
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'meteor-motion/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "meteor-motion"
8
+ spec.version = MeteorMotion::VERSION
9
+ spec.authors = ["Miguel Tavares"]
10
+ spec.email = ["mtavares.azrael@gmail.com"]
11
+ spec.description = %q{Write a gem description}
12
+ spec.summary = %q{Write a gem summary}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'bubble-wrap'
22
+ spec.add_dependency 'rm-digest'
23
+ spec.add_dependency 'motion_model'
24
+ spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "motion-redgreen"
27
+ end
@@ -0,0 +1,61 @@
1
+ module MeteorMotion
2
+ module Adapters
3
+ module MotionModel
4
+ include ::MotionModel::ArrayModelAdapter
5
+
6
+ def self.set_client client
7
+ @@client = client
8
+ end
9
+
10
+
11
+
12
+ # Sends the data to the server and supers for the record to be saved. If saving on the server fails
13
+ # it has to be dealt with by the user.
14
+ def do_insert options = {}
15
+ if !options[:local]
16
+ attr_hash = build_attr_hash
17
+ id = SecureRandom.hex
18
+ @@client.call("/#{self.class.to_s.downcase}/insert", self.method(:handle_update), [{_id: id}.merge(attr_hash)])
19
+ end
20
+
21
+ super
22
+ end
23
+
24
+ def do_update options = {}
25
+ if !options[:local]
26
+ id = self.get_attr(:id)
27
+ @@client.call("/#{self.class.to_s.downcase}/update", self.method(:handle_update), [{_id: id}, attr_hash])
28
+ end
29
+
30
+ super
31
+ end
32
+
33
+ def do_delete
34
+ send_remove
35
+
36
+ super
37
+ end
38
+
39
+ def send_remove
40
+ id = self.get_attr(:id)
41
+ @@client.call("/#{self.class.to_s.downcase}/remove", self.method(:handle_remove), [{_id: id}])
42
+ end
43
+
44
+ def build_attr_hash
45
+ attributes.reject{|k,v| k == :id}
46
+ end
47
+
48
+ # Stubs to handle inserts, updates and deletes. Can be overrriden by superclass
49
+ #
50
+ def handle_insert action, result; end
51
+ def handle_update action, result; end
52
+ def handle_remove action, result; end
53
+
54
+ # Nasty, nasty hack to work around inheritance issues. TODO: refactor as adapter.
55
+ def superclass
56
+ return Object
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,179 @@
1
+ module MeteorMotion
2
+ class Client
3
+ attr_accessor :collections, :subscriptions, :method_callbacks
4
+
5
+ def initialize
6
+ @collections = {}
7
+ @subscriptions = {}
8
+ @method_callbacks = {}
9
+
10
+ @auth_client = nil
11
+ @auth_key = nil
12
+ @username = nil
13
+ @password = nil
14
+
15
+ @ddp = MeteorMotion::DDP.new self
16
+ @error_handler = nil
17
+
18
+ MeteorMotion::Adapters::MotionModel.set_client(self)
19
+ end
20
+
21
+
22
+ def connect hostname='localhost', port=3000, callback=nil
23
+ @ddp.connect hostname, port
24
+ if callback
25
+ @method_callbacks['connect'] = callback
26
+ end
27
+ end
28
+
29
+ def handle_connect status
30
+ if @method_callbacks['connect']
31
+ obj = { method: @method_callbacks['connect'], action: status == :success, result: nil}
32
+ self.performSelectorInBackground('background_method_handler:', withObject: obj)
33
+ @method_callbacks.delete('connect')
34
+ end
35
+ end
36
+
37
+ def add_collection klass, name = ""
38
+ if klass.is_a? String
39
+ if @collections[klass]
40
+ raise "A MeteorMotion::Collection named '#{klass}' already exists."
41
+ end
42
+
43
+ @collections[klass] = MeteorMotion::Collections::Default.new klass
44
+ else
45
+ coll = MeteorMotion::Collections::MotionModel.new klass, name
46
+
47
+ @collections[coll.name] = coll
48
+ end
49
+ end
50
+
51
+
52
+ def remove_collection name
53
+ @collections.delete(name)
54
+ end
55
+
56
+
57
+ def subscribe pub_name, params=[]
58
+ sub_id = @ddp.sub(pub_name, params)
59
+ @subscriptions[sub_id] = {name: pub_name, ready: false}
60
+
61
+ return sub_id
62
+ end
63
+
64
+ def unsubscribe sub_id
65
+ @ddp.unsub(sub_id)
66
+ @subscriptions.delete(sub_id)
67
+ end
68
+
69
+
70
+ def call method_name, callback, params=[]
71
+ method_id = @ddp.call(method_name, params)
72
+
73
+ @method_callbacks[method_id] = { method: callback, result: false, updated: false}
74
+ end
75
+
76
+
77
+ # Methods for SRP authentication
78
+ #
79
+ def login_with_username username, password, callback
80
+ @method_callbacks['login'] = callback
81
+ @auth_client = SRP::Client.new
82
+ @username = username
83
+ @password = password
84
+
85
+
86
+ a = @auth_client.start_authentication()
87
+ id = @ddp.call( 'beginPasswordExchange', { 'A' => a, user: { username: username } } )
88
+
89
+ @method_callbacks[id] = { method: self.method(:handle_login_challenge), result: false, updated: false }
90
+ end
91
+
92
+ def handle_login_challenge action, result
93
+ if action == :updated
94
+ return
95
+ elsif action == :error
96
+ obj = { method: @method_callbacks['login'], action: action, result: result }
97
+ self.performSelectorInBackground('background_method_handler:', withObject: obj)
98
+
99
+ @method_callbacks.delete('login')
100
+ return
101
+ end
102
+
103
+ m = @auth_client.process_challenge(result['identity'], @password, result['salt'], result['B'])
104
+ id = @ddp.call( 'login', {srp: {'M' => m} } )
105
+
106
+ @method_callbacks[id] = { method: self.method(:verify_login), result: false, updated: false }
107
+ end
108
+
109
+ def verify_login action, result
110
+ if action == :updated
111
+ return
112
+ elsif action == :error
113
+ obj = { method: @method_callbacks['login'], action: action, result: result }
114
+ self.performSelectorInBackground('background_method_handler:', withObject: obj)
115
+
116
+ @method_callbacks.delete('login')
117
+ return
118
+ end
119
+
120
+ if @auth_client.verify result['HAMK']
121
+ obj = { method: @method_callbacks['login'], action: :success, result: nil }
122
+ else
123
+ obj = { method: @method_callbacks['login'], action: :error, result: {reason: 'Failed HAMK validation.'} }
124
+ end
125
+
126
+ self.performSelectorInBackground('background_method_handler:', withObject: obj)
127
+ @method_callbacks.delete('login')
128
+ end
129
+
130
+
131
+ def on_error method
132
+ @error_handler = method
133
+ end
134
+
135
+ # Methods required for MeteorMotion::DDP delegation
136
+ #
137
+ def handle_method id, action, result
138
+ callback = @method_callbacks[id]
139
+
140
+ if !callback
141
+ return
142
+ end
143
+
144
+ obj = { method: callback[:method], action: action, result: result}
145
+
146
+ self.performSelectorInBackground('background_method_handler:', withObject: obj)
147
+
148
+ if callback[action] == :error
149
+ callback[action] = :result
150
+ end
151
+
152
+ callback[action] = true
153
+ if callback[:result] && callback[:updated]
154
+ @method_callbacks.delete(id)
155
+ else
156
+ @method_callbacks[id] = callback
157
+ end
158
+ end
159
+
160
+ def error code, reason, details
161
+ if @error_handler
162
+ obj = { method: @error_handler, code: code, reason: reason, details: details}
163
+ self.performSelectorInBackground('background_error_handler:', withObject: obj)
164
+ else
165
+ #TODO: silent error handling
166
+ end
167
+ end
168
+
169
+ private
170
+ def background_method_handler obj
171
+ obj[:method].call( obj[:action], obj[:result])
172
+ end
173
+
174
+ def background_error_handler obj
175
+ obj[:method].call( obj[:code], obj[:reason], obj[:details])
176
+ end
177
+
178
+ end
179
+ end
@@ -0,0 +1,50 @@
1
+ module MeteorMotion
2
+ class Collection
3
+ attr_accessor :name, :observers
4
+
5
+ def initialize name
6
+ @name = name
7
+ @observers = []
8
+ end
9
+
10
+ # Method hooks - be sure to call super after reimplementing these
11
+ # Right now these are here more for testing purposes, should be reimplemented as a hook with some testing compromise
12
+ #
13
+ def add id, fields
14
+ notify_observers :added, id
15
+ end
16
+
17
+ def update id, fields, cleared
18
+ notify_observers :changed, id
19
+ end
20
+
21
+ def remove id
22
+ notify_observers :removed, id
23
+ end
24
+
25
+ # Observer methods
26
+ #
27
+ def add_observer method
28
+ @observers << method
29
+ end
30
+
31
+ def remove_observer method
32
+ @observers.delete( method )
33
+ end
34
+
35
+ private
36
+ def notify_observers action, id
37
+ obj = {action: action, id: id}
38
+ self.performSelectorInBackground('background_notify:', withObject: obj)
39
+ end
40
+
41
+ # These callbacks should be done as background tasks to prevent the connection thread from holding in intensive tasks
42
+ #
43
+ def background_notify object
44
+ observers.each do |method_name|
45
+ #puts "Calling method with action #{object[:action]}"
46
+ method_name.call( object[:action], object[:id] )
47
+ end
48
+ end
49
+ end
50
+ end