# Please read the README.rdoc file !
require 'rubygems'
require 'rack'
require 'logger'
Logger.class_eval { alias :write :<< } unless Logger.instance_methods.include? "write"
require 'optparse'
require 'irb'
require 'active_support'
require 'capcode/version'
require 'capcode/core_ext'
require 'capcode/helpers/auth'
require 'capcode/render/text'
require 'capcode/configuration'
require 'capcode/filters'
require 'capcode/ext/rack/urlmap'
module Capcode
class ParameterError < ArgumentError #:nodoc: all
end
class RouteError < ArgumentError #:nodoc: all
end
class RenderError < ArgumentError #:nodoc: all
end
class MissingLibrary < Exception #:nodoc: all
end
# Views is an empty module in which you can store your markaby or xml views.
module Views; end
# Helpers contains methods available in your controllers
module Helpers
def self.args
@args ||= nil
end
def self.args=(x)
@args = x
end
# 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.haml exist in haml_path
# * :sass => :my_sass_file : this suppose that's my_sass_file.sass exist in sass_path
# * :text => "my text"
# * :json => MyObject : this suppose that's MyObject respond to .to_json
# * :static => "my_file.xxx" : this suppose that's my_file.xxx exist in the static directory
# * :xml => :my_func : :my_func must be defined in Capcode::Views
# * :webdav => /path/to/root
#
# Or you can use a "HTTP code" renderer :
#
# render 200 => "Ok", :server => "Capcode #{Capcode::CAPCOD_VERION}", ...
#
# If you want to use a specific layout, you can specify it with option
# :layout
#
# If you want to change the Content-Type, you can specify it with option
# :content_type
# Note that this will not work with the JSON renderer
#
# If you use the WebDav renderer, you can use the option
# :resource_class (see http://github.com/georgi/rack_dav for more informations)
def render( hash )
if hash.class == Hash
render_type = nil
possible_code_renderer = nil
hash.keys.each do |key|
begin
gem "capcode-render-#{key.to_s}"
require "capcode/render/#{key.to_s}"
rescue Gem::LoadError
nil
rescue LoadError
raise Capcode::RenderError, "Hum... The #{key} renderer is malformated! Please try to install a new version or use an other renderer!", caller
end
if self.respond_to?("render_#{key.to_s}")
unless render_type.nil?
raise Capcode::RenderError, "Can't use multiple renderer (`#{render_type}' and `#{key}') !", caller
end
render_type = key
end
if key.class == Fixnum
possible_code_renderer = key
end
end
if render_type.nil? and possible_code_renderer.nil?
raise Capcode::RenderError, "Renderer type not specified!", caller
end
unless self.respond_to?("render_#{render_type.to_s}")
if possible_code_renderer.nil?
raise Capcode::RenderError, "#{render_type} renderer not present ! please require 'capcode/render/#{render_type}'", caller
else
code = possible_code_renderer
body = hash.delete(possible_code_renderer)
header = {}
hash.each do |k, v|
k = k.to_s.split(/_/).map{|e| e.capitalize}.join("-")
header[k] = v
end
[code, header, body]
end
else
render_name = hash.delete(render_type)
content_type = hash.delete(:content_type)
unless content_type.nil?
@response['Content-Type'] = content_type
end
begin
self.send( "render_#{render_type.to_s}", render_name, hash )
rescue => e
raise Capcode::RenderError, "Error rendering `#{render_type.to_s}' : #{e.message}", caller
end
end
else
render( :text => hash )
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
#
# DEPRECATED, please use render( :json => o )
def json( d ) ## DELETE THIS IN 1.0.0
warn( "json is deprecated and will be removed in version 1.0, please use `render( :json => ... )'" )
render :json => d
end
# Send a redirect response
#
# module Capcode
# class Hello < Route '/hello/(.*)'
# def get( you )
# if you.nil?
# redirect( WhoAreYou )
# else
# ...
# end
# end
# end
# end
#
# The first parameter can be a controller class name
#
# redirect( MyController )
#
# it can be a string path
#
# redirect( "/path/to/my/resource" )
#
# it can be an http status code (by default redirect use the http status code 302)
#
# redirect( 304, MyController )
#
# For more informations about HTTP status, see http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection
def redirect( klass, *a )
httpCode = 302
if( klass.class == Fixnum )
httpCode = klass
klass = a.shift
end
[httpCode, {'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( Capcode::Hello, "you" ) # => /hello/you
def URL( klass, *a )
path = nil
result = {}
a = a.delete_if{ |x| x.nil? }
if klass.class == Class
last_size = 0
klass.__urls__[0].each do |cpath, regexp|
data = a.clone
n = Regexp.new( regexp ).number_of_captures
equart = (a.size - n).abs
rtable = regexp.dup.gsub( /\\\(/, "" ).gsub( /\\\)/, "" ).split( /\([^\)]*\)/ )
rtable.each do |r|
if r == ""
cpath = cpath + "/#{data.shift}"
else
cpath = cpath + "/#{r}"
end
end
cpath = (cpath + "/" + data.join( "/" )).gsub( /\/\//, "/" ).gsub( /\/$/, "" )
result[equart] = cpath
end
path = result[result.keys.min]
else
path = klass
end
(ENV['RACK_BASE_URI']||'')+path
end
# Calling content_for stores a block of markup in an identifier.
#
# module Capcode
# class ContentFor < Route '/'
# def get
# render( :markaby => :page, :layout => :layout )
# end
# end
# end
#
# module Capcode::Views
# def layout
# html do
# head do
# yield :header
# end
# body do
# yield :content
# end
# end
# end
#
# def page
# content_for :header do
# title "This is the title!"
# end
#
# content_for :content do
# p "this is the content!"
# end
# end
# end
def content_for( x )
if Capcode::Helpers.args.map{|_| _.to_s }.include?(x.to_s)
yield
end
end
# Return information about the static directory
#
# * static[:uri] give the static URI
# * static[:path] give the path to the static directory on the server
def static
{
:uri => Capcode.static,
:path => File.expand_path( File.join(Capcode::Configuration.get(:root), Capcode::Configuration.get(:static) ) )
}
end
include Authorization
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
#
# the rXXX method can also receive a second optional parameter corresponding
# of the header's Hash :
#
# module Capcode
# class HTTPError
# def r404(f, h)
# h['Content-Type'] = 'text/plain'
# "You are here ---> X (#{f} point)"
# 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}"
headers.delete('Content-Type') if headers.keys.include?('Content-Type')
body = begin
self.send( "r#{status}", env['REQUEST_PATH'], headers )
rescue
self.send( "r#{status}", env['REQUEST_PATH'] )
end
headers['Content-Length'] = body.length.to_s
headers['Content-Type'] = "text/html" unless headers.keys.include?('Content-Type')
end
[status, headers, body]
end
end
class << self
attr :__auth__, true #:nodoc:
# 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 *routes_paths
create_path = routes_paths[0].nil?
Class.new {
meta_def(:__urls__) {
routes_paths = ['/'+self.to_s.gsub( /^Capcode::/, "" ).underscore] if create_path == true
# < Route '/hello/world/([^\/]*)/id(\d*)', '/hello/(.*)', :agent => /Songbird (\d\.\d)[\d\/]*?/
# # => [ {'/hello/world' => '([^\/]*)/id(\d*)', '/hello' => '(.*)'},
# 2,
# ,
# {:agent => /Songbird (\d\.\d)[\d\/]*?/} ]
hash_of_routes = {}
max_captures_for_routes = 0
routes_paths.each do |current_route_path|
if current_route_path.class == String
m = /\/([^\/]*\(.*)/.match( current_route_path )
if m.nil?
raise Capcode::RouteError, "Route `#{current_route_path}' already defined with regexp `#{hash_of_routes[current_route_path]}' !", caller if hash_of_routes.keys.include?(current_route_path)
hash_of_routes[current_route_path] = ''
else
_pre = m.pre_match
_pre = "/" if _pre.size == 0
raise Capcode::RouteError, "Route `#{_pre}' already defined with regexp `#{hash_of_routes[_pre]}' !", caller if hash_of_routes.keys.include?(_pre)
hash_of_routes[_pre] = m.captures[0]
max_captures_for_routes = Regexp.new(m.captures[0]).number_of_captures if max_captures_for_routes < Regexp.new(m.captures[0]).number_of_captures
end
else
raise Capcode::ParameterError, "Bad route declaration !", caller
end
end
[hash_of_routes, max_captures_for_routes, 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
# Session hash
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)
# Check authz
authz_options = nil
if Capcode.__auth__ and Capcode.__auth__.size > 0
authz_options = Capcode.__auth__[@request.path]||nil
if authz_options.nil?
route = nil
Capcode.__auth__.each do |r, o|
regexp = "^#{r.gsub(/\/$/, "")}([/]{1}.*)?$"
if Regexp.new(regexp).match( @request.path )
if route.nil? or r.size > route.size
route = r
authz_options = o
end
end
end
end
end
r = catch(:halt) {
unless authz_options.nil?
http_authentication( :type => authz_options[:type], :realm => authz_options[:realm], :opaque => authz_options[:realm] ) {
authz_options[:autz]
}
end
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 - 1
if finalNArgs.nil? or finalNArgs > diffNArgs
finalPath = p
finalNArgs = diffNArgs
finalArgs = diffArgs
end
end
end
if finalNArgs > self.class.__urls__[1]
return [404, {'Content-Type' => 'text/plain'}, "Not Found: #{@request.path}"]
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
filter_output = Capcode::Filter.execute( self )
if( filter_output.nil? )
case @env["REQUEST_METHOD"]
when "GET"
get( *args )
when "POST"
_method = params.delete( "_method" ) { |_| "post" }
send( _method.downcase.to_sym, *args )
else
_method = @env["REQUEST_METHOD"]
send( _method.downcase.to_sym, *args )
end
else
filter_output
end
}
if r.respond_to?(:to_ary)
@response.status = r.shift #r[0]
#r[1].each do |k,v|
r.shift.each do |k,v|
@response[k] = v
end
@response.write r.shift #r[2]
else
@response.write r
end
@response.finish
end
include Capcode::Helpers
include Capcode::Views
}
end
Capcode::Route = Capcode::Route(nil)
# 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( route, &b )
Capcode.routes[route] = yield
end
# This method allow you to use a Rack middleware
#
# Example :
#
# module Capcode
# ...
# use Rack::Codehighlighter, :coderay, :element => "pre",
# :pattern => /\A:::(\w+)\s*\n/, :logging => false
# ...
# end
def use(middleware, *args, &block)
middlewares << [middleware, args, block]
end
def middlewares #:nodoc:
@middlewares ||= []
end
# Allow you to add and HTTP Authentication (Basic or Digest) to controllers for or specific route
#
# Options :
# * :type : Authentication type (:basic or :digest) - default : :basic
# * :realm : realm ;) - default : "Capcode.app"
# * :opaque : Your secret passphrase. You MUST set it if you use Digest Auth - default : "opaque"
# * :routes : Routes - default : "/"
#
# The block must return a Hash of username => password like that :
# {
# "user1" => "pass1",
# "user2" => "pass2",
# # ...
# }
def http_authentication( opts = {}, &b )
options = {
:type => :basic,
:realm => "Capcode.app",
:opaque => "opaque",
:routes => "/"
}.merge( opts )
options[:autz] = b.call()
@__auth__ ||= {}
if options[:routes].class == Array
options[:routes].each do |r|
@__auth__[r] = options
end
else
@__auth__[options[:routes]] = options
end
end
# Return the Rack App.
#
# Options : see Capcode::Configuration.set
#
# Options set here replace the ones set globally
def application( args = {} )
Capcode::Configuration.configuration(args)
Capcode::Configuration.print_debug if Capcode::Configuration.get(:verbose)
Capcode.constants.clone.delete_if {|k|
not( Capcode.const_get(k).to_s =~ /Capcode/ ) or [
"Filter",
"Helpers",
"RouteError",
"Views",
"ParameterError",
"HTTPError",
"Configuration",
"MissingLibrary",
"Route",
"RenderError"
].include?(k)
}.each do |k|
begin
if eval "Capcode::#{k}.public_methods(true).include?( '__urls__' )"
hash_of_routes, max_captures_for_routes, klass = eval "Capcode::#{k}.__urls__"
hash_of_routes.keys.each do |current_route_path|
#raise Capcode::RouteError, "Route `#{current_route_path}' already define !", caller if @@__ROUTES.keys.include?(current_route_path)
raise Capcode::RouteError, "Route `#{current_route_path}' already define !", caller if Capcode.routes.keys.include?(current_route_path)
# Capcode.routes[current_route_path] = klass.new
Capcode.routes[current_route_path] = klass
end
end
rescue => e
raise e.message
end
end
# Set Static directory
Capcode.static = (Capcode::Configuration.get(:static)[0].chr == "/")?Capcode::Configuration.get(:static):"/"+Capcode::Configuration.get(:static) unless Capcode::Configuration.get(:static).nil?
# Initialize Rack App
puts "** Map routes." if Capcode::Configuration.get(:verbose)
# app = Rack::URLMap.new(Capcode.routes)
app = Capcode::Ext::Rack::URLMap.new(Capcode.routes)
puts "** Initialize static directory (#{Capcode.static}) in #{File.expand_path(Capcode::Configuration.get(:root))}" if Capcode::Configuration.get(:verbose)
app = Rack::Static.new(
app,
#:urls => [@@__STATIC_DIR],
:urls => [Capcode.static],
:root => File.expand_path(Capcode::Configuration.get(:root))
) unless Capcode::Configuration.get(:static).nil?
puts "** Initialize session" if Capcode::Configuration.get(:verbose)
app = Rack::Session::Cookie.new( app, Capcode::Configuration.get(: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 !!!
middlewares.each do |mw|
middleware, args, block = mw
puts "** Load middleware #{middleware}" if Capcode::Configuration.get(:verbose)
if block
app = middleware.new( app, *args, &block )
else
app = middleware.new( app, *args )
end
end
# Start database
if self.methods.include? "db_connect"
db_connect( Capcode::Configuration.get(:db_config), Capcode::Configuration.get(:log) )
end
if block_given?
puts "** Execute block" if Capcode::Configuration.get(:verbose)
yield( self )
end
return app
end
# Start your application.
#
# Options : see Capcode::Configuration.set
#
# Options set here replace the ones set globally
def run( args = {} )
Capcode::Configuration.configuration(args)
# 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)" ) {
Capcode::Configuration.set :console, true
}
opts.on( "-h", "--host HOSTNAME", "Host for web server to bind to (default: #{Capcode::Configuration.get(:host)})" ) { |h|
Capcode::Configuration.set :host, h
}
opts.on( "-p", "--port NUM", "Port for web server (default: #{Capcode::Configuration.get(:port)})" ) { |p|
Capcode::Configuration.set :port, p
}
opts.on( "-d", "--daemonize [true|false]", "Daemonize (default: #{Capcode::Configuration.get(:daemonize)})" ) { |d|
Capcode::Configuration.set :daemonize, d
}
opts.on( "-r", "--root PATH", "Working directory (default: #{Capcode::Configuration.get(:root)})" ) { |w|
Capcode::Configuration.set :root, w
}
opts.on( "-s", "--static PATH", "Static directory -- relative to the root directory (default: #{Capcode::Configuration.get(:static)})" ) { |r|
Capcode::Configuration.set :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
Capcode::Configuration.set :verbose, true
end
end
begin
opts.parse! ARGV
rescue OptionParser::ParseError => ex
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(Capcode::Configuration.get(:root))})" if Capcode::Configuration.get(:verbose)
Dir.chdir( Capcode::Configuration.get(:root) ) do
# Check that mongrel exists
if Capcode::Configuration.get(:server).nil? || Capcode::Configuration.get(:server) == "mongrel"
begin
require 'mongrel'
Capcode::Configuration.set :server, :mongrel
rescue LoadError
puts "!! could not load mongrel. Falling back to webrick."
Capcode::Configuration.set :server, :webrick
end
end
# From rackup !!!
if Capcode::Configuration.get(: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(Capcode::Configuration.get(:pid), 'w'){ |f| f.write("#{Process.pid}") }
at_exit { File.delete(Capcode::Configuration.get(:pid)) if File.exist?(Capcode::Configuration.get(:pid)) }
end
app = nil
if block_given?
app = application(Capcode::Configuration.get) { yield( self ) }
else
app = application(Capcode::Configuration.get)
end
app = Rack::CommonLogger.new( app, Logger.new(Capcode::Configuration.get(:log)) )
if Capcode::Configuration.get(:console)
puts "Run console..."
IRB.start
exit
end
# Start server
case Capcode::Configuration.get(:server).to_s
when "mongrel"
puts "** Starting Mongrel on #{Capcode::Configuration.get(:host)}:#{Capcode::Configuration.get(:port)}"
Rack::Handler::Mongrel.run( app, {:Port => Capcode::Configuration.get(:port), :Host => Capcode::Configuration.get(:host)} ) { |server|
trap "SIGINT", proc { server.stop }
}
when "webrick"
puts "** Starting WEBrick on #{Capcode::Configuration.get(:host)}:#{Capcode::Configuration.get(:port)}"
Rack::Handler::WEBrick.run( app, {:Port => Capcode::Configuration.get(:port), :BindAddress => Capcode::Configuration.get(:host)} ) { |server|
trap "SIGINT", proc { server.shutdown }
}
when "thin"
puts "** Starting Thin on #{Capcode::Configuration.get(:host)}:#{Capcode::Configuration.get(:port)}"
Rack::Handler::Thin.run( app, {:Port => Capcode::Configuration.get(:port), :Host => Capcode::Configuration.get(:host)} ) { |server|
trap "SIGINT", proc { server.stop }
}
end
end
end
def routes #:nodoc:
@routes ||= {}
end
def static #:nodoc:
@static_dir ||= nil
end
def static=(x) #:nodoc:
@static_dir = x
end
end
end