require 'rubygems' require 'rack' require 'json' require 'logger' require 'capcode/version' require 'capcode/core_ext' module Capcode @@__ROUTES = {} class ParameterError < ArgumentError end class RouteError < ArgumentError end # Helpers contains methods available in your controllers module Helpers # Help you to return a JSON response # # module Capcode # class JsonResponse < Route '/json/([^\/]*)/(.*)' # def get( arg1, arg2 ) # json( { :1 => arg1, :2 => arg2 }) # end # end # end def json( d ) @response['Content-Type'] = 'application/json' d.to_json end # Send a redirect response # # module Capcode # class Hello < Route '/hello/(.*)' # def get( you ) # if you.nil? # redirect( WhoAreYou ) # else # ... # end # end # end # end def redirect( klass, *a ) [302, {'Location' => URL(klass, *a)}, ''] end # Builds an URL route to a controller or a path # # if you declare the controller Hello : # # module Capcode # class Hello < Route '/hello/(.*)' # ... # end # end # # then # # URL( Hello, "you" ) # => /hello/you def URL( klass, *a ) path = nil a = a.delete_if{ |x| x.nil? } if klass.class == Class Capcode.routes.each do |p, k| path = p if k.class == klass end else path = klass end path+((a.size>0)?("/"+a.join("/")):("")) end end include Rack # HTTPError help you to create your own 404, 500 and/or 501 response # # To create a custom 404 reponse, create a fonction HTTPError.r404 in # your application : # # module Capcode # class HTTPError # def r404(f) # "#{f} not found :(" # end # end # end # # Do the same (r500 and r501) to customize 500 and 501 errors class HTTPError def initialize(app)#:nodoc: @app = app end def call(env) #:nodoc: status, headers, body = @app.call(env) if self.methods.include? "r#{status}" body = self.send( "r#{status}", env['REQUEST_PATH'] ) headers['Content-Length'] = body.length.to_s end [status, headers, body] end end class << self # Add routes to a controller class # # module Capcode # class Hello < Route '/hello/(.*)', '/hello/([^#]*)#(.*)' # def get( arg1, arg2 ) # ... # end # end # end # # In the get method, you will receive the maximum of parameters declared # by the routes. In this example, you will receive 2 parameters. So if you # go to /hello/world#friend then arg1 will be set to world and arg2 # will be set to friend. Now if you go to /hello/you, then arg1 will # be set to you and arg2 will be set to nil # # If the regexp in the route does not match, all arguments will be nil def Route *u Class.new { meta_def(:__urls__){ # < Route '/hello/world/([^\/]*)/id(\d*)', '/hello/(.*)' # # => [ {'/hello/world' => '([^\/]*)/id(\d*)', '/hello' => '(.*)'}, 2, ] h = {} max = 0 u.each do |_u| m = /\/([^\/]*\(.*)/.match( _u ) if m.nil? raise Capcode::RouteError, "Route `#{_u}' already defined with regexp `#{h[_u]}' !", caller if h.keys.include?(_u) h[_u] = '' else _pre = m.pre_match _pre = "/" if _pre.size == 0 raise Capcode::RouteError, "Route `#{_pre}' already defined with regexp `#{h[_pre]}' !", caller if h.keys.include?(_pre) h[_pre] = m.captures[0] max = Regexp.new(m.captures[0]).number_of_captures if max < Regexp.new(m.captures[0]).number_of_captures end end [h, max, self] } # Hash containing all the request parameters (GET or POST) def params @request.params end # Hash containing all the environment variables def env @env end # Hash session def session @env['rack.session'] end # Return the Rack::Request object def request @request end # Return the Rack::Response object def response @response end def call( e ) #:nodoc: @env = e @response = Rack::Response.new @request = Rack::Request.new(@env) r = case @env["REQUEST_METHOD"] when "GET" finalPath = nil finalArgs = nil finalNArgs = nil aPath = @request.path.gsub( /^\//, "" ).split( "/" ) self.class.__urls__[0].each do |p, r| xPath = p.gsub( /^\//, "" ).split( "/" ) if (xPath - aPath).size == 0 diffArgs = aPath - xPath diffNArgs = diffArgs.size if finalNArgs.nil? or finalNArgs > diffNArgs finalPath = p finalNArgs = diffNArgs finalArgs = diffArgs end end end # TODO : correct above to use this : #puts "finalPath = #{finalPath}" #puts "finalNArgs = #{finalNArgs}" #print "finalArgs = "; p finalArgs nargs = self.class.__urls__[1] regexp = Regexp.new( self.class.__urls__[0][finalPath] ) args = regexp.match( Rack::Utils.unescape(@request.path_info).gsub( /^\//, "" ) ) if args.nil? raise Capcode::ParameterError, "Path info `#{@request.path_info}' does not match route regexp `#{regexp.source}'" else args = args.captures end while args.size < nargs args << nil end get( *args ) when "POST" post end if r.respond_to?(:to_ary) @response.status = r[0] r[1].each do |k,v| @response[k] = v end @response.body = r[2] else @response.write r end @response.finish end include Capcode::Helpers } end # This method help you to map and URL to a Rack or What you want Helper # # Capcode.map( "/file" ) do # Rack::File.new( "." ) # end def map( r, &b ) @@__ROUTES[r] = yield end # Start your application. # # Options : # * :port = Listen port # * :host = Listen host # * :server = Server type (webrick or mongrel) # * :log = Output logfile (default: STDOUT) # * :session = Session parameters. See Rack::Session for more informations # * :pid = PID file (default: $0.pid) # * :daemonize = Daemonize application (default: false) def run( args = {} ) conf = { :port => args[:port]||3000, :host => args[:host]||"localhost", :server => args[:server]||nil, :log => args[:log]||$stdout, :session => args[:session]||{}, :pid => args[:pid]||"#{$0}.pid", :daemonize => args[:daemonize]||false, :db_config => args[:db_config]||"database.yml" } # Check that mongrel exists if conf[:server].nil? || conf[:server] == "mongrel" begin require 'mongrel' conf[:server] = "mongrel" rescue LoadError puts "!! could not load mongrel. Falling back to webrick." conf[:server] = "webrick" end end Capcode.constants.each do |k| begin if eval "Capcode::#{k}.public_methods(true).include?( '__urls__' )" u, m, c = eval "Capcode::#{k}.__urls__" u.keys.each do |_u| raise Capcode::RouteError, "Route `#{_u}' already define !", caller if @@__ROUTES.keys.include?(_u) @@__ROUTES[_u] = c.new end end rescue => e raise e.message end end app = Rack::URLMap.new(@@__ROUTES) app = Rack::Session::Cookie.new( app, conf[:session] ) app = Capcode::HTTPError.new(app) app = Rack::ContentLength.new(app) app = Rack::Lint.new(app) app = Rack::ShowExceptions.new(app) # app = Rack::Reloader.new(app) ## -- NE RELOAD QUE capcode.rb -- So !!! app = Rack::CommonLogger.new( app, Logger.new(conf[:log]) ) # From rackup !!! if conf[:daemonize] if RUBY_VERSION < "1.9" exit if fork Process.setsid exit if fork # Dir.chdir "/" File.umask 0000 STDIN.reopen "/dev/null" STDOUT.reopen "/dev/null", "a" STDERR.reopen "/dev/null", "a" else Process.daemon end File.open(conf[:pid], 'w'){ |f| f.write("#{Process.pid}") } at_exit { File.delete(conf[:pid]) if File.exist?(conf[:pid]) } end # Start database if self.methods.include? "db_connect" db_connect( conf[:db_config], conf[:log] ) end # Start server case conf[:server] when "mongrel" puts "** Starting Mongrel on #{conf[:host]}:#{conf[:port]}" Rack::Handler::Mongrel.run( app, {:Port => conf[:port], :Host => conf[:host]} ) { |server| trap "SIGINT", proc { server.stop } } when "webrick" puts "** Starting WEBrick on #{conf[:host]}:#{conf[:port]}" Rack::Handler::WEBrick.run( app, {:Port => conf[:port], :BindAddress => conf[:host]} ) { |server| trap "SIGINT", proc { server.shutdown } } end end def routes #:nodoc: @@__ROUTES end end end