plezi 0.14.4 → 0.14.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,257 +4,277 @@ require 'plezi/controller/controller_class'
4
4
  require 'plezi/websockets/message_dispatch'
5
5
 
6
6
  module Plezi
7
- # This module contains the functionality provided to any Controller class.
8
- #
9
- # This module will be included within every Class that is asigned to a route, providing the functionality without forcing an inheritance model.
10
- module Controller
11
- def self.included(base)
12
- base.extend ::Plezi::Controller::ClassMethods
13
- end
14
-
15
- # A Rack::Request object for the current request.
16
- attr_reader :request
17
- # A Rack::Response object used for the current request.
18
- attr_reader :response
19
- # A union between the `request.params` and the route's inline parameters. This is different then `request.params`
20
- attr_reader :params
21
- # A cookie jar for both accessing and setting cookies. Unifies `request.set_cookie`, `request.delete_cookie` and `request.cookies` with a single Hash like inteface.
22
- #
23
- # Read a cookie:
24
- #
25
- # cookies["name"]
26
- #
27
- # Set a cookie:
28
- #
29
- # cookies["name"] = "value"
30
- # cookies["name"] = {value: "value", secure: true}
31
- #
32
- # Delete a cookie:
33
- #
34
- # cookies["name"] = nil
35
- #
36
- attr_reader :cookies
7
+ # This module contains the functionality provided to any Controller class.
8
+ #
9
+ # This module will be included within every Class that is asigned to a route, providing the functionality without forcing an inheritance model.
10
+ module Controller
11
+ def self.included(base)
12
+ base.extend ::Plezi::Controller::ClassMethods
13
+ end
37
14
 
38
- # @private
39
- # This function is used internally by Plezi, do not call.
40
- def _pl_respond(request, response, params)
41
- @request = request
42
- @response = response
43
- @params = params
44
- @cookies = Cookies.new(request, response)
45
- m = requested_method
46
- # puts "m == #{m.nil? ? 'nil' : m.to_s}"
47
- return _pl_ad_httpreview(__send__(m)) if m
48
- false
49
- end
15
+ # A Rack::Request object for the current request.
16
+ attr_reader :request
17
+ # A Rack::Response object used for the current request.
18
+ attr_reader :response
19
+ # A union between the `request.params` and the route's inline parameters. This is different then `request.params`
20
+ attr_reader :params
21
+ # A cookie jar for both accessing and setting cookies. Unifies `request.set_cookie`, `request.delete_cookie` and `request.cookies` with a single Hash like inteface.
22
+ #
23
+ # Read a cookie:
24
+ #
25
+ # cookies["name"]
26
+ #
27
+ # Set a cookie:
28
+ #
29
+ # cookies["name"] = "value"
30
+ # cookies["name"] = {value: "value", secure: true}
31
+ #
32
+ # Delete a cookie:
33
+ #
34
+ # cookies["name"] = nil
35
+ #
36
+ attr_reader :cookies
50
37
 
51
- # Returns the method that was called by the HTTP request.
52
- #
53
- # It's possible to override this method to change the default Controller behavior.
54
- #
55
- # For Websocket connections this method is most likely to return :preform_upgrade
56
- def requested_method
57
- params['_method'.freeze] = (params['_method'.freeze] || request.request_method.downcase).to_sym
58
- self.class._pl_params2method(params, request.env)
59
- end
38
+ # @private
39
+ # This function is used internally by Plezi, do not call.
40
+ def _pl_respond(request, response, params)
41
+ @request = request
42
+ @response = response
43
+ @params = params
44
+ @cookies = Cookies.new(request, response)
45
+ mthd = requested_method
46
+ # puts "m == #{m.nil? ? 'nil' : m.to_s}"
47
+ return _pl_ad_httpreview(__send__(mthd)) if mthd
48
+ false
49
+ end
60
50
 
61
- # Renders the requested template (should be a string, subfolders are fine).
62
- #
63
- # Template name shouldn't include the template's extension or format - this allows for dynamic format template resolution, so that `json` and `html` requests can share the same code. i.e.
64
- #
65
- # Plezi.templates = "views/"
66
- # render "users/index"
67
- #
68
- # Using layouts (nested templates) is easy by using a block (a little different then other frameworks):
69
- #
70
- # render("users/layout") { render "users/index" }
71
- #
72
- def render(template, &block)
73
- frmt = params['format'.freeze] || 'html'.freeze
74
- mime = nil
75
- ret = ::Plezi::Renderer.render "#{File.join(::Plezi.templates, template.to_s)}.#{frmt}", binding, &block
76
- response[Rack::CONTENT_TYPE] = mime if ret && !response.content_type && (mime = Rack::Mime.mime_type(".#{frmt}".freeze, nil))
77
- ret
78
- end
51
+ # Returns the method that was called by the HTTP request.
52
+ #
53
+ # It's possible to override this method to change the default Controller behavior.
54
+ #
55
+ # For Websocket connections this method is most likely to return :preform_upgrade
56
+ def requested_method
57
+ params['_method'.freeze] = (params['_method'.freeze] || request.request_method.downcase).to_sym
58
+ self.class._pl_params2method(params, request.env)
59
+ end
79
60
 
80
- # Sends a block of data, setting a file name, mime type and content disposition headers when possible. This should also be a good choice when sending large amounts of data.
81
- #
82
- # By default, `send_data` sends the data as an attachment, unless `inline: true` was set.
83
- #
84
- # If a mime type is provided, it will be used to set the Content-Type header. i.e. `mime: "text/plain"`
85
- #
86
- # If a file name was provided, Rack will be used to find the correct mime type (unless provided). i.e. `filename: "sample.pdf"` will set the mime type to `application/pdf`
87
- #
88
- # Available options: `:inline` (`true` / `false`), `:filename`, `:mime`.
89
- def send_data(data, options = {})
90
- response.write data if data
91
- # set headers
92
- content_disposition = options[:inline] ? 'inline'.dup : 'attachment'.dup
93
- content_disposition << "; filename=#{::File.basename(options[:filename])}" if options[:filename]
61
+ # Renders the requested template (should be a string, subfolders are fine).
62
+ #
63
+ # Template name shouldn't include the template's extension or format - this allows for dynamic format template resolution, so that `json` and `html` requests can share the same code. i.e.
64
+ #
65
+ # Plezi.templates = "views/"
66
+ # render "users/index"
67
+ #
68
+ # Using layouts (nested templates) is easy by using a block (a little different then other frameworks):
69
+ #
70
+ # render("users/layout") { render "users/index" }
71
+ #
72
+ def render(template, &block)
73
+ frmt = params['format'.freeze] || 'html'.freeze
74
+ mime = nil
75
+ ret = ::Plezi::Renderer.render "#{File.join(::Plezi.templates, template.to_s)}.#{frmt}", binding, &block
76
+ response[Rack::CONTENT_TYPE] = mime if ret && !response.content_type && (mime = Rack::Mime.mime_type(".#{frmt}".freeze, nil))
77
+ ret
78
+ end
94
79
 
95
- response['content-type'.freeze] = (options[:mime] ||= options[:filename] && Rack::Mime.mime_type(::File.extname(options[:filename])))
96
- response.delete('content-type'.freeze) unless response['content-type'.freeze]
97
- response['content-disposition'.freeze] = content_disposition
98
- true
99
- end
80
+ # Sends a block of data, setting a file name, mime type and content disposition headers when possible. This should also be a good choice when sending large amounts of data.
81
+ #
82
+ # By default, `send_data` sends the data as an attachment, unless `inline: true` was set.
83
+ #
84
+ # If a mime type is provided, it will be used to set the Content-Type header. i.e. `mime: "text/plain"`
85
+ #
86
+ # If a file name was provided, Rack will be used to find the correct mime type (unless provided). i.e. `filename: "sample.pdf"` will set the mime type to `application/pdf`
87
+ #
88
+ # Available options: `:inline` (`true` / `false`), `:filename`, `:mime`.
89
+ def send_data(data, options = {})
90
+ response.write data if data
91
+ filename = options[:filename]
92
+ # set headers
93
+ content_disposition = options[:inline] ? 'inline'.dup : 'attachment'.dup
94
+ content_disposition << "; filename=#{::File.basename(options[:filename])}" if filename
95
+ cont_type = (options[:mime] ||= filename && Rack::Mime.mime_type(::File.extname(filename)))
96
+ response['content-type'.freeze] = cont_type if cont_type
97
+ response['content-disposition'.freeze] = content_disposition
98
+ true
99
+ end
100
100
 
101
- # Same as {#send_data}, but accepts a file name (to be opened and sent) rather then a String.
102
- #
103
- # See {#send_data} for available options.
104
- def send_file(filename, options = {})
105
- response['X-Sendfile'.freeze] = filename
106
- options[:filename] ||= File.basename(filename)
107
- filename = File.open(filename, 'rb'.freeze) # unless Iodine::Rack.public
108
- send_data filename, options
109
- end
101
+ # Same as {#send_data}, but accepts a file name (to be opened and sent) rather then a String.
102
+ #
103
+ # See {#send_data} for available options.
104
+ def send_file(filename, options = {})
105
+ response['X-Sendfile'.freeze] = filename
106
+ options[:filename] ||= File.basename(filename)
107
+ filename = File.open(filename, 'rb'.freeze) # unless Iodine::Rack.public
108
+ send_data filename, options
109
+ end
110
110
 
111
- # A shortcut for Rack's `response.redirect`.
112
- def redirect_to(target, status = 302)
113
- response.redirect target, status
114
- true
115
- end
111
+ # A shortcut for Rack's `response.redirect`.
112
+ def redirect_to(target, status = 302)
113
+ response.redirect target, status
114
+ true
115
+ end
116
116
 
117
- # Returns a relative URL for the controller, placing the requested parameters in the URL (inline, where possible and as query data when not possible).
118
- def url_for(func, params = {})
119
- ::Plezi::Base::Router.url_for self.class, func, params
120
- end
117
+ # Returns a relative URL for the controller, placing the requested parameters in the URL (inline, where possible and as query data when not possible).
118
+ def url_for(func, params = {})
119
+ ::Plezi::Base::Router.url_for self.class, func, params
120
+ end
121
121
 
122
- # A connection's Plezi ID uniquely identifies the connection across application instances, allowing it to receive and send messages using {#unicast}.
123
- def id
124
- @_pl_id ||= (conn_id && "#{::Plezi::Base::MessageDispatch.pid}-#{conn_id.to_s(16)}")
125
- end
122
+ # A connection's Plezi ID uniquely identifies the connection across application instances, allowing it to receive and send messages using {#unicast}.
123
+ def id
124
+ @_pl_id ||= (conn_id && "#{::Plezi::Base::MessageDispatch.pid}-#{conn_id.to_s(16)}")
125
+ end
126
126
 
127
- # @private
128
- # This is the process specific Websocket's UUID. This function is here to protect you from yourself. Don't call it.
129
- def conn_id
130
- defined?(super) && super
131
- end
127
+ # @private
128
+ # This is the process specific Websocket's UUID. This function is here to protect you from yourself. Don't call it.
129
+ def conn_id
130
+ defined?(super) && super
131
+ end
132
132
 
133
- # Override this method to read / write cookies, perform authentication or perform validation before establishing a Websocket connecion.
134
- #
135
- # Return `false` or `nil` to refuse the websocket connection.
136
- def pre_connect
137
- true
138
- end
133
+ # Override this method to read / write cookies, perform authentication or perform validation before establishing a Websocket connecion.
134
+ #
135
+ # Return `false` or `nil` to refuse the websocket connection.
136
+ def pre_connect
137
+ true
138
+ end
139
139
 
140
- # Experimental: takes a module to be used for Websocket callbacks events.
141
- #
142
- # This function can only be called **after** a websocket connection was established (i.e., within the `on_open` callback).
143
- #
144
- # This allows a module "library" to be used similar to the way "rooms" are used in node.js, so that a number of different Controllers can listen to shared events.
145
- #
146
- # By dynamically extending a Controller instance using a module, Websocket broadcasts will be allowed to invoke the module's functions.
147
- #
148
- # Notice: It is impossible to `unextend` an extended module at this time.
149
- def extend(mod)
150
- raise TypeError, '`mod` should be a module' unless mod.class == Module
151
- raise "#{self} already extended by #{mod.name}" if is_a?(mod)
152
- mod.extend ::Plezi::Controller::ClassMethods
153
- super(mod)
154
- _pl_ws_map.update mod._pl_ws_map
155
- _pl_ad_map.update mod._pl_ad_map
156
- end
140
+ # Experimental: takes a module to be used for Websocket callbacks events.
141
+ #
142
+ # This function can only be called **after** a websocket connection was established (i.e., within the `on_open` callback).
143
+ #
144
+ # This allows a module "library" to be used similar to the way "rooms" are used in node.js, so that a number of different Controllers can listen to shared events.
145
+ #
146
+ # By dynamically extending a Controller instance using a module, Websocket broadcasts will be allowed to invoke the module's functions.
147
+ #
148
+ # Notice: It is impossible to `unextend` an extended module at this time.
149
+ def extend(mod)
150
+ raise TypeError, '`mod` should be a module' unless mod.class == Module
151
+ raise "#{self} already extended by #{mod.name}" if is_a?(mod)
152
+ mod.extend ::Plezi::Controller::ClassMethods
153
+ super(mod)
154
+ _pl_ws_map.update mod._pl_ws_map
155
+ _pl_ad_map.update mod._pl_ad_map
156
+ end
157
157
 
158
- # Invokes a method on the `target` websocket connection. When using Iodine, the method is invoked asynchronously.
159
- #
160
- # def perform_poke(target)
161
- # unicast target, :poke, self.id
162
- # end
163
- # def poke(from)
164
- # unicast from, :poke_back, self.id
165
- # end
166
- # def poke_back(from)
167
- # puts "#{from} is available"
168
- # end
169
- #
170
- # Methods invoked using {unicast}, {broadcast} or {multicast} will quietly fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
171
- def unicast(target, event_method, *args)
172
- ::Plezi::Base::MessageDispatch.unicast(id ? self : self.class, target, event_method, args)
173
- end
158
+ # Invokes a method on the `target` websocket connection. When using Iodine, the method is invoked asynchronously.
159
+ #
160
+ # def perform_poke(target)
161
+ # unicast target, :poke, self.id
162
+ # end
163
+ # def poke(from)
164
+ # unicast from, :poke_back, self.id
165
+ # end
166
+ # def poke_back(from)
167
+ # puts "#{from} is available"
168
+ # end
169
+ #
170
+ # Methods invoked using {unicast}, {broadcast} or {multicast} will quietly fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
171
+ def unicast(target, event_method, *args)
172
+ ::Plezi::Base::MessageDispatch.unicast(id ? self : self.class, target, event_method, args)
173
+ end
174
174
 
175
- # Invokes a method on every websocket connection (except `self`) that belongs to this Controller / Type. When using Iodine, the method is invoked asynchronously.
176
- #
177
- # self.broadcast :my_method, "argument 1", "argument 2", 3
178
- #
179
- # Methods invoked using {unicast}, {broadcast} or {multicast} will quietly fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
180
- def broadcast(event_method, *args)
181
- ::Plezi::Base::MessageDispatch.broadcast(id ? self : self.class, event_method, args)
182
- end
175
+ # Invokes a method on every websocket connection (except `self`) that belongs to this Controller / Type. When using Iodine, the method is invoked asynchronously.
176
+ #
177
+ # self.broadcast :my_method, "argument 1", "argument 2", 3
178
+ #
179
+ # Methods invoked using {unicast}, {broadcast} or {multicast} will quietly fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
180
+ def broadcast(event_method, *args)
181
+ ::Plezi::Base::MessageDispatch.broadcast(id ? self : self.class, event_method, args)
182
+ end
183
183
 
184
- # Invokes a method on every websocket connection in the application (except `self`).
185
- #
186
- # self.multicast :my_method, "argument 1", "argument 2", 3
187
- #
188
- # Methods invoked using {unicast}, {broadcast} or {multicast} will quietly fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
189
- def multicast(event_method, *args)
190
- ::Plezi::Base::MessageDispatch.multicast(id ? self : self.class, event_method, args)
191
- end
184
+ # Invokes a method on every websocket connection in the application (except `self`).
185
+ #
186
+ # self.multicast :my_method, "argument 1", "argument 2", 3
187
+ #
188
+ # Methods invoked using {unicast}, {broadcast} or {multicast} will quietly fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
189
+ def multicast(event_method, *args)
190
+ ::Plezi::Base::MessageDispatch.multicast(id ? self : self.class, event_method, args)
191
+ end
192
192
 
193
- # @private
194
- # This function is used internally by Plezi, do not call.
195
- def _pl_ws_map
196
- @_pl_ws_map ||= self.class._pl_ws_map.dup
197
- end
193
+ # Writes a message to every client websocket connection, for all controllers(!), EXCEPT self. Accepts an optional filter method using a location reference for a *static* (Class/Module/global) method. The filter method will be passerd the websocket object and it should return `true` / `false`.
194
+ #
195
+ # self.write2everyone {event: "global", message: "This will be sent to everyone"}.to_json
196
+ # # or, we can define a filter method somewhere in our code
197
+ # module Filter
198
+ # def self.should_send? ws
199
+ # true
200
+ # end
201
+ # end
202
+ # # and we can use this filter method.
203
+ # data = {event: "global", message: "This will be sent to everyone"}.to_json
204
+ # self.write2everyone data, ::Filter, :should_send?
205
+ #
206
+ # It's important that the filter method is defined statically in our code and isn't dynamically allocated. Otherwise, scaling the application would be impossible.
207
+ def write2everyone(data, filter_owner = nil, filter_name = nil)
208
+ ::Plezi::Base::MessageDispatch.write2everyone(id ? self : self.class, data, filter_owner, filter_name)
209
+ end
198
210
 
199
- # @private
200
- # This function is used internally by Plezi, do not call.
201
- def _pl_ad_map
202
- @_pl_ad_map ||= self.class._pl_ad_map.dup
203
- end
211
+ # @private
212
+ # This function is used internally by Plezi, do not call.
213
+ def _pl_ws_map
214
+ @_pl_ws_map ||= self.class._pl_ws_map.dup
215
+ end
204
216
 
205
- # @private
206
- # This function is used internally by Plezi, for Auto-Dispatch support do not call.
207
- def on_message(data)
208
- json = nil
209
- begin
210
- json = JSON.parse(data, symbolize_names: true)
211
- rescue
212
- puts 'AutoDispatch Warnnig: Received non-JSON message. Closing Connection.'
213
- close
214
- return
217
+ # @private
218
+ # This function is used internally by Plezi, do not call.
219
+ def _pl_ad_map
220
+ @_pl_ad_map ||= self.class._pl_ad_map.dup
215
221
  end
216
- envt = _pl_ad_map[json[:event]] || _pl_ad_map[:unknown]
217
- if json[:event].nil? || envt.nil?
218
- puts _pl_ad_map
219
- puts "AutoDispatch Warnnig: JSON missing/invalid `event` name '#{json[:event]}' for class #{self.class.name}. Closing Connection."
220
- close
222
+
223
+ # @private
224
+ # This function is used internally by Plezi, for Auto-Dispatch support do not call.
225
+ def on_message(data)
226
+ json = nil
227
+ begin
228
+ json = JSON.parse(data, symbolize_names: true)
229
+ rescue
230
+ puts 'AutoDispatch Warnnig: Received non-JSON message. Closing Connection.'
231
+ close
232
+ return
233
+ end
234
+ envt = _pl_ad_map[json[:event]] || _pl_ad_map[:unknown]
235
+ if json[:event].nil? || envt.nil?
236
+ puts _pl_ad_map
237
+ puts "AutoDispatch Warnnig: JSON missing/invalid `event` name '#{json[:event]}' for class #{self.class.name}. Closing Connection."
238
+ close
239
+ end
240
+ write("{\"event\":\"_ack_\",\"_EID_\":#{json[:_EID_].to_json}}") if json[:_EID_]
241
+ _pl_ad_review __send__(envt, json)
221
242
  end
222
- write("{\"event\":\"_ack_\",\"_EID_\":#{json[:_EID_].to_json}}") if json[:_EID_]
223
- _pl_ad_review __send__(envt, json)
224
- end
225
243
 
226
- # @private
227
- # This function is used internally by Plezi, do not call.
228
- def _pl_ad_review(data)
229
- case data
230
- when Hash
231
- write data.to_json
232
- when String
233
- write data
234
- # when Array
235
- # write ret
236
- end if self.class._pl_is_ad?
237
- data
238
- end
244
+ # @private
245
+ # This function is used internally by Plezi, do not call.
246
+ def _pl_ad_review(data)
247
+ if self.class._pl_is_ad?
248
+ case data
249
+ when Hash
250
+ write data.to_json
251
+ when String
252
+ write data
253
+ # when Array
254
+ # write ret
255
+ end
256
+ end
257
+ data
258
+ end
239
259
 
240
- # @private
241
- # This function is used internally by Plezi, do not call.
242
- def _pl_ad_httpreview(data)
243
- return data.to_json if self.class._pl_is_ad? && data.is_a?(Hash)
244
- data
245
- end
260
+ # @private
261
+ # This function is used internally by Plezi, do not call.
262
+ def _pl_ad_httpreview(data)
263
+ return data.to_json if self.class._pl_is_ad? && data.is_a?(Hash)
264
+ data
265
+ end
246
266
 
247
- private
267
+ private
248
268
 
249
- # @private
250
- # This function is used internally by Plezi, do not call.
251
- def preform_upgrade
252
- return false unless pre_connect
253
- request.env['upgrade.websocket'.freeze] = self
254
- @params = @params.dup # disable memory saving (used a single object per thread)
255
- @_pl_ws_map = self.class._pl_ws_map.dup
256
- @_pl_ad_map = self.class._pl_ad_map.dup
257
- true
258
- end
259
- end
269
+ # @private
270
+ # This function is used internally by Plezi, do not call.
271
+ def preform_upgrade
272
+ return false unless pre_connect
273
+ request.env['upgrade.websocket'.freeze] = self
274
+ @params = @params.dup # disable memory saving (used a single object per thread)
275
+ @_pl_ws_map = self.class._pl_ws_map.dup
276
+ @_pl_ad_map = self.class._pl_ad_map.dup
277
+ true
278
+ end
279
+ end
260
280
  end