meteor-motion 0.1.0

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