module Pakyow class Application class << self attr_accessor :routes_proc, :middleware_proc, :configurations, :error_handlers # Sets the path to the application file so it can be reloaded later. # def inherited(subclass) Pakyow::Configuration::App.application_path = parse_path_from_caller(caller[0]) end def parse_path_from_caller(caller) caller.match(/^(.+)(:?:\d+(:?:in `.+')?$)/)[1] end # Runs the application. Accepts the environment(s) to run, for example: # run(:development) # run([:development, :staging]) # def run(*args) return if running? @running = true self.builder.run(self.prepare(*args)) detect_handler.run(builder, :Host => Pakyow::Configuration::Base.server.host, :Port => Pakyow::Configuration::Base.server.port) end # Stages the application. Everything is loaded but the application is # not started. Accepts the same arguments as #run. # def stage(*args) return if staged? @staged = true prepare(*args) end def builder @builder ||= Rack::Builder.new end def prepared? @prepared end # Returns true if the application is running. # def running? @running end # Returns true if the application is staged. # def staged? @staged end # Convenience method for base configuration class. # def config Pakyow::Configuration::Base end # Creates configuration for a particular environment. Example: # configure(:development) { app.auto_reload = true } # def configure(environment, &block) self.configurations ||= {} self.configurations[environment] = block end # Creates routes. Example: # routes { get '/' { # do something } } # def routes(&block) self.routes_proc = block end # Creates an error handler (currently 404 and 500 errors are handled). # The handler can be created one of two ways: # # Define a controller/action for a particular error: # error(404, :ApplicationController, :handle_404) # # Specify a block for a particular error: # error(404) { # handle error } # def error(*args, &block) self.error_handlers ||= {} code, controller, action = args if block self.error_handlers[code] = block else self.error_handlers[code] = { :controller => controller, :action => action } end end def middleware(&block) self.middleware_proc = block end protected # Prepares the application for running or staging and returns an instance # of the application. def prepare(*args) self.load_config args.empty? || args.first.nil? ? [Configuration::Base.app.default_environment] : args return if prepared? self.builder.use(Rack::MethodOverride) self.builder.instance_eval(&self.middleware_proc) if self.middleware_proc @prepared = true $:.unshift(Dir.pwd) unless $:.include? Dir.pwd return self.new end def load_config(args) if self.configurations args << Configuration::Base.app.default_environment if args.empty? args.each do |env| next unless config = self.configurations[env] Configuration::Base.instance_eval(&config) end end end def detect_handler ['thin', 'mongrel', 'webrick'].each do |server| begin return Rack::Handler.get(server) rescue LoadError rescue NameError end end end end include Helpers attr_accessor :request, :response, :presenter, :route_store, :restful_routes def initialize Pakyow.app = self # Create static handler @static_handler = Rack::File.new(Configuration::Base.app.public_dir) # This configuration option will be set if a presenter is to be used if Configuration::Base.app.presenter # Create a new instance of the presenter self.presenter = Configuration::Base.app.presenter.new end # Load application files load_app end # Interrupts the application and returns response immediately. # def interrupt! @interrupted = true throw :halt, self.response end # Called on every request. # def call(env) start_time = Time.now.to_f # Handle static files if env['PATH_INFO'] =~ /\.(.*)$/ && File.exists?(File.join(Configuration::Base.app.public_dir, env['PATH_INFO'])) @static = true @static_handler.call(env) else # The request object self.request = Request.new(env) # Reload application files load_app if Configuration::Base.app.presenter # Handle presentation for this request self.presenter.present_for_request(request) end Log.enter "Processing #{env['PATH_INFO']} (#{env['REMOTE_ADDR']} at #{Time.now}) [#{env['REQUEST_METHOD']}]" # The response object self.response = Rack::Response.new rhs = nil just_the_path, format = StringUtils.split_at_last_dot(self.request.path) self.request.format = ((format && (format[format.length - 1, 1] == '/')) ? format[0, format.length - 1] : format) catch(:halt) do rhs, packet = @route_store.get_block(just_the_path, self.request.method) request.params.merge!(HashUtils.strhash(packet[:vars])) self.request.route_spec = packet[:data][:route_spec] if packet[:data] restful_info = packet[:data][:restful] if packet[:data] self.request.restful = restful_info rhs.call() if rhs && !Pakyow::Configuration::App.ignore_routes end if !self.interrupted? if Configuration::Base.app.presenter self.response.body = [self.presenter.content] end # 404 if no facts matched and no views were found if !rhs && (!self.presenter || !self.presenter.presented?) self.handle_error(404) Log.enter "[404] Not Found" self.response.status = 404 end end return finish! end rescue StandardError => error self.request.error = error self.handle_error(500) Log.enter "[500] #{error}\n" Log.enter error.backtrace.join("\n") + "\n\n" # self.response = Rack::Response.new if Configuration::Base.app.errors_in_browser # Show errors in browser self.response.body = [] self.response.body << "

#{CGI.escapeHTML(error.to_s)}

" self.response.body << error.backtrace.join("
") end self.response.status = 500 return finish! ensure unless @static end_time = Time.now.to_f difference = ((end_time - start_time) * 1000).to_f Log.enter "Completed in #{difference}ms | #{self.response.status} | [#{self.request.url}]" Log.enter end end # Sends a file in the response (immediately). Accepts a File object. Mime # type is automatically detected. # def send_file(source_file, send_as = nil, type = nil) path = source_file.is_a?(File) ? source_file.path : source_file send_as ||= path type ||= Rack::Mime.mime_type(".#{send_as.split('.')[-1]}") data = "" File.open(path, "r").each_line { |line| data << line } self.response = Rack::Response.new(data, self.response.status, self.response.header.merge({ "Content-Type" => type })) interrupt! end # Sends data in the response (immediately). Accepts the data, mime type, # and optional file name. # def send_data(data, type, file_name = nil) status = self.response ? self.response.status : 200 headers = self.response ? self.response.header : {} headers = headers.merge({ "Content-Type" => type }) headers = headers.merge({ "Content-disposition" => "attachment; filename=#{file_name}"}) if file_name self.response = Rack::Response.new(data, status, headers) interrupt! end # Redirects to location (immediately). # def redirect_to(location, status_code = 302) headers = self.response ? self.response.header : {} headers = headers.merge({'Location' => location}) self.response = Rack::Response.new('', status_code, headers) interrupt! end # Registers a route for GET requests. Route can be defined one of two ways: # get('/', :ControllerClass, :action_method) # get('/') { # do something } # # Routes for namespaced controllers (e.g. Admin::ControllerClass) can be defined like this: # get('/', :Admin_ControllerClass, :action_method) # def get(route, controller = nil, action = nil, &block) register_route(route, block, :get, controller, action) end # Registers a route for POST requests (see #get). # def post(route, controller = nil, action = nil, &block) register_route(route, block, :post, controller, action) end # Registers a route for PUT requests (see #get). # def put(route, controller = nil, action = nil, &block) register_route(route, block, :put, controller, action) end # Registers a route for DELETE requests (see #get). # def delete(route, controller = nil, action = nil, &block) register_route(route, block, :delete, controller, action) end # Registers the default route (see #get). # def default(controller = nil, action = nil, &block) register_route('/', block, :get, controller, action) end # Creates REST routes for a resource. Arguments: url, controller, model # def restful(*args, &block) url, controller, model = args with_scope(:url => url.gsub(/^[\/]+|[\/]+$/,""), :model => model) do nest_scope(&block) if block_given? @restful_routes ||= {} @restful_routes[model] ||= {} if model @@restful_actions.each do |opts| action_url = current_path if suffix = opts[:url_suffix] action_url = File.join(action_url, suffix) end # Create the route register_route(action_url, nil, opts[:method], controller, opts[:action], :restful) # Store url for later use (currently used by Binder#action) @restful_routes[model][opts[:action]] = action_url if model end remove_scope end end @@restful_actions = [ { :action => :edit, :method => :get, :url_suffix => 'edit/:id' }, { :action => :show, :method => :get, :url_suffix => ':id' }, { :action => :new, :method => :get, :url_suffix => 'new' }, { :action => :update, :method => :put, :url_suffix => ':id' }, { :action => :delete, :method => :delete, :url_suffix => ':id' }, { :action => :index, :method => :get }, { :action => :create, :method => :post } ] protected def interrupted? @interrupted end # Handles route registration. # def register_route(route, block, method, controller = nil, action = nil, type = :user) if controller controller = eval(controller.to_s) action ||= Configuration::Base.app.default_action block = lambda { instance = controller.new request.controller = instance request.action = action instance.send(action) } end data = {:route_type=>type, :route_spec=>route} if type == :restful data[:restful] = {:restful_action=>action} end @route_store.add_route(route, block, method, data) end def with_scope(opts) @scope ||= {} @scope[:path] ||= [] @scope[:model] = opts[:model] @scope[:path] << opts[:url] yield end def remove_scope @scope[:path].pop end def nest_scope(&block) @scope[:path].insert(-1, ":#{StringUtils.underscore(@scope[:model].to_s)}_id") yield @scope[:path].pop end def current_path @scope[:path].join('/') end def handle_error(code) return unless self.class.error_handlers return unless handler = self.class.error_handlers[code] if handler.is_a? Proc Pakyow.app.instance_eval(&handler) else c = eval(handler[:controller].to_s).new c.send(handler[:action]) end self.response.body = [self.presenter.content] end def set_cookies if self.request.cookies && self.request.cookies != {} self.request.cookies.each do |key, value| if value.is_a?(Hash) self.response.set_cookie(key, {:path => '/', :expires => Time.now + 604800}.merge(value)) elsif value.is_a?(String) self.response.set_cookie(key, {:path => '/', :expires => Time.now + 604800}.merge({:value => value})) else self.response.set_cookie(key, {:path => '/', :expires => Time.now + 604800 * -1 }.merge({:value => value})) end end end end # Reloads all application files in application_path and presenter (if specified). # def load_app return if @loaded && !Configuration::Base.app.auto_reload @loaded = true # Reload Application load(Configuration::App.application_path) @loader = Loader.new unless @loader @loader.load!(Configuration::Base.app.src_dir) load_routes # Reload views if Configuration::Base.app.presenter self.presenter.reload! end end def load_routes @route_store = RouteStore.new self.instance_eval(&self.class.routes_proc) if self.class.routes_proc end # Send the response and cleanup. # def finish! @interrupted = false @static = false # Set cookies set_cookies # Finished self.response.finish end end end