module RocketIO class Controller extend Forwardable def_delegators 'self.class', :url, :dirname, :parameters_policy def_delegators :request, :session, :request_method def_delegators RocketIO, :indifferent_params, :indifferent_hash, :mime_type, :engine_const, :engine_class def_delegators CGI, :escape_html def_delegators RocketIO, :environment RocketIO::ENVIRONMENTS.each_key do |env| def_delegators RocketIO, :"#{env}?" end # defining get, post etc. methods that will be called # when a request matches current controller and appropriate request method used. # # by default all requests, except HEAD, will return a NotImplementedError. # override the methods you need to be handled by controller. # RocketIO::REQUEST_METHODS.each_value do |verb| define_method(verb) {|*| error(501)} end def head(*); end # call requested method. # also call #before, #around and #after filters. # # @param [Hash] env # @return [Rack::Response] # def call env catch :__response__ do if error_handlers[500] begin __call__(env) rescue Exception => e error(500, e) end else __call__(env) end end end private def __call__ env self.env = env validate_or_request_authentication_if_needed validate_or_request_authorization_if_needed validate_parameters __run__ proc { invoke_before_filter invoke_around_filter proc { response.body = public_send(requested_method, *path_params_array) } invoke_after_filter } response.body ||= RocketIO::EMPTY_ARRAY response.body = [] if head? # dropping body on HEAD requests response.finish end private def __run__ app app.call end def env= env @__env__ = env end def env @__env__ end def validate_parameters # enforce policy only if a method defined for current request method # cause stock REST methods accepting any number of arguments return unless policy = parameters_policy[requested_method] if path_params_array.size >= policy[:min] return if policy[:max] == :* || path_params_array.size <= policy[:max] end error(409) end def path_params @__path_params__ ||= begin rangemap = self.class.path_params[requested_method] || raise(StandardError, 'No path_params map found for %s method' % requested_method) indifferent_params(rangemap.each_with_object({}) {|(m,r),o| o[m] = if r.min && r.max r.min == r.max ? path_params_array[r.min] : path_params_array[r] else path_params_array[r] end }).freeze end end def path_params_array @__path_params_array__ end def params @__params__ ||= indifferent_params(request.params) end def request @__request__ ||= RocketIO::Request.new(env) end def response @__response__ ||= RocketIO::Response.new end def requested_method @__requested_method__ ||= RocketIO::REQUEST_METHODS[request_method] end end class << Controller # only public non-inherited methods are included in public API directly. def api return [] if self == RocketIO::Controller public_instance_methods(false).concat(inherited_api) - private_api end # inherited methods are excluded from public API # but we still need a way to know what API methods was inherited def inherited_api @__inherited_api__ end # used internally to keep a list of public methods # that should be excluded from public API def private_api @__private_api__ end # import some config from some controller def import setup, from: __send__(:"define_#{setup}_methods", from) end def inherited base # registering new controller RocketIO.controllers.push(base) base.instance_variable_set(:@__private_api__, self.private_api.uniq) base.instance_variable_set(:@__inherited_api__, self.api.freeze) # new controller inherits all setups from superclass base.import :before, from: self base.import :around, from: self base.import :after, from: self base.import :basic_auth, from: self base.import :digest_auth, from: self base.import :token_auth, from: self base.import :error_handlers, from: self base.import :middleware, from: self base.import :sessions, from: self base.import :engine, from: self base.import :layout, from: self base.import :layouts, from: self base.import :templates, from: self # removing superclass name from new controller name path = RocketIO.underscore(base.name.to_s.sub(self.name.to_s + '::', '').gsub('::', '/')) # new controller uses for URL its underscored name prefixed by superclass URL base.map RocketIO.rootify_path(url, path) # setting dirname for new controller base.instance_variable_set(:@__dirname__, RocketIO.caller_to_dirname(caller).freeze) end # by default controllers will use underscored name for base URL. # this method allow to set a custom base URL. # # @example Users::Register will listen on /users/register by default # class Users # class Register < RocketIO # # end # end # # @note # # @example make Users::Register to listen on /users/join rather than /users/register # # class Users # class Register < RocketIO # map :join # # end # end # # @note if given URL starts with a slash it will ignore class name and set URL as is # # @example make Users::Register to listen on /members/join # # class Users < RocketIO # class Register < self # map '/members/join' # # end # end # # @param path [String || Symbol] # def map path path = path.to_s @__url__ = if path =~ /\A\// path else if superclass == Object RocketIO.rootify_path(path) else RocketIO.rootify_path(superclass.url, path) end end.freeze end # allow controllers to serve multiple URLs # # @param path [String || Symbol] # def alias_url path path = path.to_s path = if path =~ /\A\// path else if superclass == Object RocketIO.rootify_path(path) else RocketIO.rootify_path(superclass.url, path) end end.freeze aliases.push(path) end def aliases @__aliases__ ||= [] end # build a URL from given chunks prefixing them with actual path # # @param *args [Array] # @return [String] def url *args return @__url__ if args.empty? query = if args.last.is_a?(Hash) RocketIO::QUERY_PREFIX + ::Rack::Utils.build_nested_query(args.pop) else RocketIO::EMPTY_STRING end ::File.join(@__url__, args.map!(&:to_s)) + query end def method_added meth parameters = instance_method(meth).parameters path_params[meth] = RocketIO.path_params(parameters).freeze if requested_method = RocketIO::REQUEST_METHODS.values.find {|verb| verb == meth} # REST methods should be called with a predetermined set of parameters. # setting an appropriate policy for just defined method based on its parameters. parameters_policy[requested_method] = RocketIO.parameters_policy(parameters).freeze end end # initializing the controller to process a HTTP request # # @param path_params [Array] # @return a RocketIO::Route instance def initialize_controller requested_method = nil, path_params_array = nil controller = allocate controller.instance_variable_set(:@__requested_method__, requested_method.to_sym) if requested_method controller.instance_variable_set(:@__path_params_array__, (path_params_array || []).freeze) controller end def parameters_policy @__parameters_policy__ ||= {} end def path_params @__path_params__ ||= {} end def dirname *args ::File.join(@__dirname__, args.map!(&:to_s)) end # making controller to act as a Rack application def call env initialize_controller.call(env) end end Controller.instance_variable_set(:@__private_api__, []) end require 'rocketio/controller/authentication' require 'rocketio/controller/authorization' require 'rocketio/controller/cookies' require 'rocketio/controller/error_handlers' require 'rocketio/controller/filters' require 'rocketio/controller/flash' require 'rocketio/controller/helpers' require 'rocketio/controller/middleware' require 'rocketio/controller/request' require 'rocketio/controller/response' require 'rocketio/controller/sessions' require 'rocketio/controller/websocket' require 'rocketio/controller/render'