require 'rubygems'
require 'rack'
require 'json' ## DELETE THIS IN 1.0.0
require 'logger'
require 'optparse'
require 'irb'
require 'mime/types'
require 'capcode/version'
require 'capcode/core_ext'
require 'capcode/render/text'
module Capcode
@@__ROUTES = {}
@@__STATIC_DIR = nil
# @@__FILTERS = []
# def self.before_filter( opts, &b )
# opts[:action] = b
# @@__FILTERS << opts
# end
class ParameterError < ArgumentError #:nodoc:
end
class RouteError < ArgumentError #:nodoc:
end
class RenderError < ArgumentError
end
module Views; end
# Helpers contains methods available in your controllers
module Helpers
@@__ARGS__ = nil
# Render a view
#
# render's parameter can be a Hash or a string. Passing a string is equivalent to do
# render( :text => string )
#
# If you want to use a specific renderer, use one of this options :
#
# * :markaby => :my_func : :my_func must be defined in Capcode::Views
# * :erb => :my_erb_file : this suppose that's my_erb_file.rhtml exist in erb_path
# * :haml => :my_haml_file : this suppose that's my_haml_file.rhtml exist in haml_path
# * :text => "my text"
# * :json => MyObject : this suppose that's MyObject respond to .to_json
#
# If you want to use a specific layout, you can specify it with option
# :layout
def render( h )
if h.class == Hash
render_type = nil
h.keys.each do |k|
if self.respond_to?("render_#{k.to_s}")
unless render_type.nil?
raise Capcode::RenderError, "Can't use multiple renderer (`#{render_type}' and `#{k}') !", caller
end
render_type = k
end
end
if render_type.nil?
raise Capcode::RenderError, "Renderer type not specified!", caller
end
render_name = h.delete(render_type)
begin
self.send( "render_#{render_type.to_s}", render_name, h )
rescue => e
raise Capcode::RenderError, "Error rendering `#{render_type.to_s}' : #{e.message}", caller
end
else
render( :text => h )
end
end
# 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 ) ## DELETE THIS IN 1.0.0
warn( "json is deprecated, please use `render( :json => ... )'" )
@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
# Calling content_for stores a block of markup in an identifier.
def content_for( x )
if @@__ARGS__.map{|_| _.to_s }.include?(x.to_s)
yield
end
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, r501, r403) to customize 500, 501, 403 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
attr :__args__, true
# 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)
# __k = self.class.to_s.split( /::/ )[-1].downcase.to_sym
# @@__FILTERS.each do |f|
# proc = f.delete(:action)
# __run = true
# if f[:only]
# __run = f[:only].include?(__k)
# end
# if f[:except]
# __run = !f[:except].include?(__k)
# end
#
# # proc.call(self) if __run
# puts "call #{proc} for #{__k}"
# end
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
nargs = self.class.__urls__[1]
regexp = Regexp.new( self.class.__urls__[0][finalPath] )
args = regexp.match( Rack::Utils.unescape(@request.path).gsub( Regexp.new( "^#{finalPath}" ), "" ).gsub( /^\//, "" ) )
if args.nil?
raise Capcode::ParameterError, "Path info `#{@request.path_info}' does not match route regexp `#{regexp.source}'"
else
args = args.captures.map { |x| (x.size == 0)?nil:x }
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
include Capcode::Views
}
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)
# * :db_config = database configuration file (default: database.yml)
# * :static = Static directory (default: none -- relative to the working directory)
# * :root = Root directory (default: directory of the main.rb) -- This is also the working directory !
def run( args = {} )
__VERBOSE = false
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",
:static => args[:static]||nil,
:root => args[:root]||File.expand_path(File.dirname($0)),
:console => false
}
# Parse options
opts = OptionParser.new do |opts|
opts.banner = "Usage: #{File.basename($0)} [options]"
opts.separator ""
opts.separator "Specific options:"
opts.on( "-C", "--console", "Run in console mode with IRB (default: false)" ) {
conf[:console] = true
}
opts.on( "-h", "--host HOSTNAME", "Host for web server to bind to (default: #{conf[:host]})" ) { |h|
conf[:host] = h
}
opts.on( "-p", "--port NUM", "Port for web server (default: #{conf[:port]})" ) { |p|
conf[:port] = p
}
opts.on( "-d", "--daemonize [true|false]", "Daemonize (default: #{conf[:daemonize]})" ) { |d|
conf[:daemonize] = d
}
opts.on( "-r", "--root PATH", "Working directory (default: #{conf[:root]})" ) { |w|
conf[:root] = w
}
opts.on( "-s", "--static PATH", "Static directory -- relative to the root directory (default: #{conf[:static]})" ) { |r|
conf[:static] = r
}
opts.separator ""
opts.separator "Common options:"
opts.on("-?", "--help", "Show this message") do
puts opts
exit
end
opts.on("-v", "--version", "Show versions") do
puts "Capcode version #{Capcode::CAPCOD_VERION} (ruby v#{RUBY_VERSION})"
exit
end
opts.on_tail( "-V", "--verbose", "Run in verbose mode" ) do
__VERBOSE = true
end
end
begin
opts.parse! ARGV
rescue OptionParser::ParseError => ex
STDERR.puts "!! #{ex.message}"
puts "** use `#{File.basename($0)} --help` for more details..."
exit 1
end
# Run in the Working directory
puts "** Go on root directory (#{File.expand_path(conf[:root])})" if __VERBOSE
Dir.chdir( conf[:root] ) do
# 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
# Set Static directory
@@__STATIC_DIR = File.expand_path(File.join("/", conf[:static]))
# Initialize Rack App
puts "** Map routes." if __VERBOSE
app = Rack::URLMap.new(@@__ROUTES)
puts "** Initialize static directory (#{conf[:static]})" if __VERBOSE
app = Rack::Static.new(
app,
:urls => [@@__STATIC_DIR],
:root => File.expand_path(conf[:root])
) unless conf[:static].nil?
puts "** Initialize session" if __VERBOSE
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 /java/.match(RUBY_PLATFORM).nil?
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
else
puts "!! daemonize option unavailable on #{RUBY_PLATFORM} platform."
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
if block_given?
yield( self )
end
if conf[:console]
puts "Run console..."
IRB.start
exit
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
end
def routes #:nodoc:
@@__ROUTES
end
def static #:nodoc:
puts "-------------------- #{@@__STATIC_DIR}"
@@__STATIC_DIR
end
end
end