plezi 0.14.4 → 0.14.5

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