lib/plezi/controller/controller.rb in plezi-0.14.4 vs lib/plezi/controller/controller.rb in plezi-0.14.5
- old
+ new
@@ -2,259 +2,279 @@
require 'plezi/controller/cookies'
require 'plezi/controller/controller_class'
require 'plezi/websockets/message_dispatch'
module Plezi
- # This module contains the functionality provided to any Controller class.
- #
- # This module will be included within every Class that is asigned to a route, providing the functionality without forcing an inheritance model.
- module Controller
- def self.included(base)
- base.extend ::Plezi::Controller::ClassMethods
- end
+ # This module contains the functionality provided to any Controller class.
+ #
+ # This module will be included within every Class that is asigned to a route, providing the functionality without forcing an inheritance model.
+ module Controller
+ def self.included(base)
+ base.extend ::Plezi::Controller::ClassMethods
+ end
- # A Rack::Request object for the current request.
- attr_reader :request
- # A Rack::Response object used for the current request.
- attr_reader :response
- # A union between the `request.params` and the route's inline parameters. This is different then `request.params`
- attr_reader :params
- # 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.
- #
- # Read a cookie:
- #
- # cookies["name"]
- #
- # Set a cookie:
- #
- # cookies["name"] = "value"
- # cookies["name"] = {value: "value", secure: true}
- #
- # Delete a cookie:
- #
- # cookies["name"] = nil
- #
- attr_reader :cookies
+ # A Rack::Request object for the current request.
+ attr_reader :request
+ # A Rack::Response object used for the current request.
+ attr_reader :response
+ # A union between the `request.params` and the route's inline parameters. This is different then `request.params`
+ attr_reader :params
+ # 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.
+ #
+ # Read a cookie:
+ #
+ # cookies["name"]
+ #
+ # Set a cookie:
+ #
+ # cookies["name"] = "value"
+ # cookies["name"] = {value: "value", secure: true}
+ #
+ # Delete a cookie:
+ #
+ # cookies["name"] = nil
+ #
+ attr_reader :cookies
- # @private
- # This function is used internally by Plezi, do not call.
- def _pl_respond(request, response, params)
- @request = request
- @response = response
- @params = params
- @cookies = Cookies.new(request, response)
- m = requested_method
- # puts "m == #{m.nil? ? 'nil' : m.to_s}"
- return _pl_ad_httpreview(__send__(m)) if m
- false
- end
+ # @private
+ # This function is used internally by Plezi, do not call.
+ def _pl_respond(request, response, params)
+ @request = request
+ @response = response
+ @params = params
+ @cookies = Cookies.new(request, response)
+ mthd = requested_method
+ # puts "m == #{m.nil? ? 'nil' : m.to_s}"
+ return _pl_ad_httpreview(__send__(mthd)) if mthd
+ false
+ end
- # Returns the method that was called by the HTTP request.
- #
- # It's possible to override this method to change the default Controller behavior.
- #
- # For Websocket connections this method is most likely to return :preform_upgrade
- def requested_method
- params['_method'.freeze] = (params['_method'.freeze] || request.request_method.downcase).to_sym
- self.class._pl_params2method(params, request.env)
- end
+ # Returns the method that was called by the HTTP request.
+ #
+ # It's possible to override this method to change the default Controller behavior.
+ #
+ # For Websocket connections this method is most likely to return :preform_upgrade
+ def requested_method
+ params['_method'.freeze] = (params['_method'.freeze] || request.request_method.downcase).to_sym
+ self.class._pl_params2method(params, request.env)
+ end
- # Renders the requested template (should be a string, subfolders are fine).
- #
- # 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.
- #
- # Plezi.templates = "views/"
- # render "users/index"
- #
- # Using layouts (nested templates) is easy by using a block (a little different then other frameworks):
- #
- # render("users/layout") { render "users/index" }
- #
- def render(template, &block)
- frmt = params['format'.freeze] || 'html'.freeze
- mime = nil
- ret = ::Plezi::Renderer.render "#{File.join(::Plezi.templates, template.to_s)}.#{frmt}", binding, &block
- response[Rack::CONTENT_TYPE] = mime if ret && !response.content_type && (mime = Rack::Mime.mime_type(".#{frmt}".freeze, nil))
- ret
- end
+ # Renders the requested template (should be a string, subfolders are fine).
+ #
+ # 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.
+ #
+ # Plezi.templates = "views/"
+ # render "users/index"
+ #
+ # Using layouts (nested templates) is easy by using a block (a little different then other frameworks):
+ #
+ # render("users/layout") { render "users/index" }
+ #
+ def render(template, &block)
+ frmt = params['format'.freeze] || 'html'.freeze
+ mime = nil
+ ret = ::Plezi::Renderer.render "#{File.join(::Plezi.templates, template.to_s)}.#{frmt}", binding, &block
+ response[Rack::CONTENT_TYPE] = mime if ret && !response.content_type && (mime = Rack::Mime.mime_type(".#{frmt}".freeze, nil))
+ ret
+ end
- # 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.
- #
- # By default, `send_data` sends the data as an attachment, unless `inline: true` was set.
- #
- # If a mime type is provided, it will be used to set the Content-Type header. i.e. `mime: "text/plain"`
- #
- # 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`
- #
- # Available options: `:inline` (`true` / `false`), `:filename`, `:mime`.
- def send_data(data, options = {})
- response.write data if data
- # set headers
- content_disposition = options[:inline] ? 'inline'.dup : 'attachment'.dup
- content_disposition << "; filename=#{::File.basename(options[:filename])}" if options[:filename]
+ # 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.
+ #
+ # By default, `send_data` sends the data as an attachment, unless `inline: true` was set.
+ #
+ # If a mime type is provided, it will be used to set the Content-Type header. i.e. `mime: "text/plain"`
+ #
+ # 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`
+ #
+ # Available options: `:inline` (`true` / `false`), `:filename`, `:mime`.
+ def send_data(data, options = {})
+ response.write data if data
+ filename = options[:filename]
+ # set headers
+ content_disposition = options[:inline] ? 'inline'.dup : 'attachment'.dup
+ content_disposition << "; filename=#{::File.basename(options[:filename])}" if filename
+ cont_type = (options[:mime] ||= filename && Rack::Mime.mime_type(::File.extname(filename)))
+ response['content-type'.freeze] = cont_type if cont_type
+ response['content-disposition'.freeze] = content_disposition
+ true
+ end
- response['content-type'.freeze] = (options[:mime] ||= options[:filename] && Rack::Mime.mime_type(::File.extname(options[:filename])))
- response.delete('content-type'.freeze) unless response['content-type'.freeze]
- response['content-disposition'.freeze] = content_disposition
- true
- end
+ # Same as {#send_data}, but accepts a file name (to be opened and sent) rather then a String.
+ #
+ # See {#send_data} for available options.
+ def send_file(filename, options = {})
+ response['X-Sendfile'.freeze] = filename
+ options[:filename] ||= File.basename(filename)
+ filename = File.open(filename, 'rb'.freeze) # unless Iodine::Rack.public
+ send_data filename, options
+ end
- # Same as {#send_data}, but accepts a file name (to be opened and sent) rather then a String.
- #
- # See {#send_data} for available options.
- def send_file(filename, options = {})
- response['X-Sendfile'.freeze] = filename
- options[:filename] ||= File.basename(filename)
- filename = File.open(filename, 'rb'.freeze) # unless Iodine::Rack.public
- send_data filename, options
- end
+ # A shortcut for Rack's `response.redirect`.
+ def redirect_to(target, status = 302)
+ response.redirect target, status
+ true
+ end
- # A shortcut for Rack's `response.redirect`.
- def redirect_to(target, status = 302)
- response.redirect target, status
- true
- end
+ # Returns a relative URL for the controller, placing the requested parameters in the URL (inline, where possible and as query data when not possible).
+ def url_for(func, params = {})
+ ::Plezi::Base::Router.url_for self.class, func, params
+ end
- # Returns a relative URL for the controller, placing the requested parameters in the URL (inline, where possible and as query data when not possible).
- def url_for(func, params = {})
- ::Plezi::Base::Router.url_for self.class, func, params
- end
+ # A connection's Plezi ID uniquely identifies the connection across application instances, allowing it to receive and send messages using {#unicast}.
+ def id
+ @_pl_id ||= (conn_id && "#{::Plezi::Base::MessageDispatch.pid}-#{conn_id.to_s(16)}")
+ end
- # A connection's Plezi ID uniquely identifies the connection across application instances, allowing it to receive and send messages using {#unicast}.
- def id
- @_pl_id ||= (conn_id && "#{::Plezi::Base::MessageDispatch.pid}-#{conn_id.to_s(16)}")
- end
+ # @private
+ # This is the process specific Websocket's UUID. This function is here to protect you from yourself. Don't call it.
+ def conn_id
+ defined?(super) && super
+ end
- # @private
- # This is the process specific Websocket's UUID. This function is here to protect you from yourself. Don't call it.
- def conn_id
- defined?(super) && super
- end
+ # Override this method to read / write cookies, perform authentication or perform validation before establishing a Websocket connecion.
+ #
+ # Return `false` or `nil` to refuse the websocket connection.
+ def pre_connect
+ true
+ end
- # Override this method to read / write cookies, perform authentication or perform validation before establishing a Websocket connecion.
- #
- # Return `false` or `nil` to refuse the websocket connection.
- def pre_connect
- true
- end
+ # Experimental: takes a module to be used for Websocket callbacks events.
+ #
+ # This function can only be called **after** a websocket connection was established (i.e., within the `on_open` callback).
+ #
+ # 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.
+ #
+ # By dynamically extending a Controller instance using a module, Websocket broadcasts will be allowed to invoke the module's functions.
+ #
+ # Notice: It is impossible to `unextend` an extended module at this time.
+ def extend(mod)
+ raise TypeError, '`mod` should be a module' unless mod.class == Module
+ raise "#{self} already extended by #{mod.name}" if is_a?(mod)
+ mod.extend ::Plezi::Controller::ClassMethods
+ super(mod)
+ _pl_ws_map.update mod._pl_ws_map
+ _pl_ad_map.update mod._pl_ad_map
+ end
- # Experimental: takes a module to be used for Websocket callbacks events.
- #
- # This function can only be called **after** a websocket connection was established (i.e., within the `on_open` callback).
- #
- # 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.
- #
- # By dynamically extending a Controller instance using a module, Websocket broadcasts will be allowed to invoke the module's functions.
- #
- # Notice: It is impossible to `unextend` an extended module at this time.
- def extend(mod)
- raise TypeError, '`mod` should be a module' unless mod.class == Module
- raise "#{self} already extended by #{mod.name}" if is_a?(mod)
- mod.extend ::Plezi::Controller::ClassMethods
- super(mod)
- _pl_ws_map.update mod._pl_ws_map
- _pl_ad_map.update mod._pl_ad_map
- end
+ # Invokes a method on the `target` websocket connection. When using Iodine, the method is invoked asynchronously.
+ #
+ # def perform_poke(target)
+ # unicast target, :poke, self.id
+ # end
+ # def poke(from)
+ # unicast from, :poke_back, self.id
+ # end
+ # def poke_back(from)
+ # puts "#{from} is available"
+ # end
+ #
+ # 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.
+ def unicast(target, event_method, *args)
+ ::Plezi::Base::MessageDispatch.unicast(id ? self : self.class, target, event_method, args)
+ end
- # Invokes a method on the `target` websocket connection. When using Iodine, the method is invoked asynchronously.
- #
- # def perform_poke(target)
- # unicast target, :poke, self.id
- # end
- # def poke(from)
- # unicast from, :poke_back, self.id
- # end
- # def poke_back(from)
- # puts "#{from} is available"
- # end
- #
- # 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.
- def unicast(target, event_method, *args)
- ::Plezi::Base::MessageDispatch.unicast(id ? self : self.class, target, event_method, args)
- end
+ # Invokes a method on every websocket connection (except `self`) that belongs to this Controller / Type. When using Iodine, the method is invoked asynchronously.
+ #
+ # self.broadcast :my_method, "argument 1", "argument 2", 3
+ #
+ # 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.
+ def broadcast(event_method, *args)
+ ::Plezi::Base::MessageDispatch.broadcast(id ? self : self.class, event_method, args)
+ end
- # Invokes a method on every websocket connection (except `self`) that belongs to this Controller / Type. When using Iodine, the method is invoked asynchronously.
- #
- # self.broadcast :my_method, "argument 1", "argument 2", 3
- #
- # 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.
- def broadcast(event_method, *args)
- ::Plezi::Base::MessageDispatch.broadcast(id ? self : self.class, event_method, args)
- end
+ # Invokes a method on every websocket connection in the application (except `self`).
+ #
+ # self.multicast :my_method, "argument 1", "argument 2", 3
+ #
+ # 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.
+ def multicast(event_method, *args)
+ ::Plezi::Base::MessageDispatch.multicast(id ? self : self.class, event_method, args)
+ end
- # Invokes a method on every websocket connection in the application (except `self`).
- #
- # self.multicast :my_method, "argument 1", "argument 2", 3
- #
- # 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.
- def multicast(event_method, *args)
- ::Plezi::Base::MessageDispatch.multicast(id ? self : self.class, event_method, args)
- end
+ # 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`.
+ #
+ # self.write2everyone {event: "global", message: "This will be sent to everyone"}.to_json
+ # # or, we can define a filter method somewhere in our code
+ # module Filter
+ # def self.should_send? ws
+ # true
+ # end
+ # end
+ # # and we can use this filter method.
+ # data = {event: "global", message: "This will be sent to everyone"}.to_json
+ # self.write2everyone data, ::Filter, :should_send?
+ #
+ # 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.
+ def write2everyone(data, filter_owner = nil, filter_name = nil)
+ ::Plezi::Base::MessageDispatch.write2everyone(id ? self : self.class, data, filter_owner, filter_name)
+ end
- # @private
- # This function is used internally by Plezi, do not call.
- def _pl_ws_map
- @_pl_ws_map ||= self.class._pl_ws_map.dup
- end
+ # @private
+ # This function is used internally by Plezi, do not call.
+ def _pl_ws_map
+ @_pl_ws_map ||= self.class._pl_ws_map.dup
+ end
- # @private
- # This function is used internally by Plezi, do not call.
- def _pl_ad_map
- @_pl_ad_map ||= self.class._pl_ad_map.dup
- end
+ # @private
+ # This function is used internally by Plezi, do not call.
+ def _pl_ad_map
+ @_pl_ad_map ||= self.class._pl_ad_map.dup
+ end
- # @private
- # This function is used internally by Plezi, for Auto-Dispatch support do not call.
- def on_message(data)
- json = nil
- begin
- json = JSON.parse(data, symbolize_names: true)
- rescue
- puts 'AutoDispatch Warnnig: Received non-JSON message. Closing Connection.'
- close
- return
+ # @private
+ # This function is used internally by Plezi, for Auto-Dispatch support do not call.
+ def on_message(data)
+ json = nil
+ begin
+ json = JSON.parse(data, symbolize_names: true)
+ rescue
+ puts 'AutoDispatch Warnnig: Received non-JSON message. Closing Connection.'
+ close
+ return
+ end
+ envt = _pl_ad_map[json[:event]] || _pl_ad_map[:unknown]
+ if json[:event].nil? || envt.nil?
+ puts _pl_ad_map
+ puts "AutoDispatch Warnnig: JSON missing/invalid `event` name '#{json[:event]}' for class #{self.class.name}. Closing Connection."
+ close
+ end
+ write("{\"event\":\"_ack_\",\"_EID_\":#{json[:_EID_].to_json}}") if json[:_EID_]
+ _pl_ad_review __send__(envt, json)
end
- envt = _pl_ad_map[json[:event]] || _pl_ad_map[:unknown]
- if json[:event].nil? || envt.nil?
- puts _pl_ad_map
- puts "AutoDispatch Warnnig: JSON missing/invalid `event` name '#{json[:event]}' for class #{self.class.name}. Closing Connection."
- close
+
+ # @private
+ # This function is used internally by Plezi, do not call.
+ def _pl_ad_review(data)
+ if self.class._pl_is_ad?
+ case data
+ when Hash
+ write data.to_json
+ when String
+ write data
+ # when Array
+ # write ret
+ end
+ end
+ data
end
- write("{\"event\":\"_ack_\",\"_EID_\":#{json[:_EID_].to_json}}") if json[:_EID_]
- _pl_ad_review __send__(envt, json)
- end
- # @private
- # This function is used internally by Plezi, do not call.
- def _pl_ad_review(data)
- case data
- when Hash
- write data.to_json
- when String
- write data
- # when Array
- # write ret
- end if self.class._pl_is_ad?
- data
- end
+ # @private
+ # This function is used internally by Plezi, do not call.
+ def _pl_ad_httpreview(data)
+ return data.to_json if self.class._pl_is_ad? && data.is_a?(Hash)
+ data
+ end
- # @private
- # This function is used internally by Plezi, do not call.
- def _pl_ad_httpreview(data)
- return data.to_json if self.class._pl_is_ad? && data.is_a?(Hash)
- data
- end
+ private
- private
-
- # @private
- # This function is used internally by Plezi, do not call.
- def preform_upgrade
- return false unless pre_connect
- request.env['upgrade.websocket'.freeze] = self
- @params = @params.dup # disable memory saving (used a single object per thread)
- @_pl_ws_map = self.class._pl_ws_map.dup
- @_pl_ad_map = self.class._pl_ad_map.dup
- true
- end
- end
+ # @private
+ # This function is used internally by Plezi, do not call.
+ def preform_upgrade
+ return false unless pre_connect
+ request.env['upgrade.websocket'.freeze] = self
+ @params = @params.dup # disable memory saving (used a single object per thread)
+ @_pl_ws_map = self.class._pl_ws_map.dup
+ @_pl_ad_map = self.class._pl_ad_map.dup
+ true
+ end
+ end
end