class EBuilder
Attributes
Public Class Methods
# File lib/e-builder/base.rb, line 5 def self.call env new(:automount).call(env) end
creates new Espresso app.
@param automount if set to any positive value(except Class, Module
or Regexp),
all found controllers will be mounted, if set to a Class, Module or Regexp, only controllers under given namespace will be mounted.
@param [Proc] proc if block given, it will be executed inside newly created app
# File lib/e-builder/base.rb, line 19 def initialize automount = false, &proc @controllers, @subcontrollers = {}, [] @routes, @hosts, @controllers_hosts = {}, {}, {} @automount = automount proc && self.instance_exec(&proc) use ExtendedRack compiler_pool(Hash.new) end
Public Instance Methods
# File lib/e-builder/base.rb, line 157 def app @app ||= begin on_boot! mount_controllers! @sorted_routes = sorted_routes.freeze @routes.freeze middleware.reverse.inject(lambda {|env| call!(env)}) {|a,e| e[a]} end end
auto-mount auto-discovered controllers. call this only after all controllers defined and app ready to start! leaving it in public zone for better control over mounting.
# File lib/e-builder/base.rb, line 67 def automount! controllers = [Class, Module, Regexp].include?(@automount.class) ? extract_controllers(@automount) : discover_controllers mount controllers.select {|c| c.accept_automount?} end
# File lib/e-builder/setup.rb, line 10 def base_url @base_url || '' end
set authorization at app level. any controller/action will be protected.
# File lib/e-builder/setup.rb, line 86 def basic_auth opts = {}, &proc use Rack::Auth::Basic, opts[:realm] || 'AccessRestricted', &proc end
# File lib/e-builder/base.rb, line 153 def call env app.call env end
same as clear_compiler
! except it work only on current process
# File lib/e-more/view/e-builder.rb, line 33 def clear_compiler *keys compiler_pool && compiler_pool.clear end
clearing compiler cache
# File lib/e-more/view/e-builder.rb, line 27 def clear_compiler! clear_compiler ipcm_trigger :clear_compiler end
for most apps, most expensive operations are fs operations and template compilation. to avoid these operations, templates are compiled and stored into memory. on consequent requests they are just rendered.
by default, compiler are disabled. to enable it, set compiler pool at app level.
@example
class App < E # ... end app = E.new app.compiler_pool Hash.new # will store compiler cache into a hash app.run
if you want to use a custom pool, make sure your pool behaves just like a Hash, meant it responds to ‘[]=`, `[]`, and `clear` methods.
# File lib/e-more/view/e-builder.rb, line 20 def compiler_pool pool = nil @compiler_pool = pool if pool @compiler_pool end
(see basic_auth
)
# File lib/e-builder/setup.rb, line 92 def digest_auth opts = {}, &proc opts[:realm] ||= 'AccessRestricted' opts[:opaque] ||= opts[:realm] use Rack::Auth::Digest::MD5, opts, &proc end
# File lib/e-builder/base.rb, line 109 def environment ENV[ENV__RACK_ENV] || :development end
proc given here will be executed inside all controllers. used to setup multiple controllers at once.
# File lib/e-builder/base.rb, line 76 def global_setup &proc @global_setup = proc self end
set base URL to be prepended to all controllers
# File lib/e-builder/setup.rb, line 4 def map url, opts = {} url.is_a?(Hash) && (opts = url) && (url = '') @base_url = rootify_url(url).freeze @hosts = extract_hosts(opts) end
# File lib/e-builder/setup.rb, line 144 def middleware @middleware ||= [] end
mount given/discovered controllers into current app. any number of arguments accepted. String arguments are treated as roots/canonicals. any other arguments are used to discover controllers. controllers can be passed directly or as a Module
that contain controllers or as a Regexp matching controller’s name.
proc given here will be executed inside given/discovered controllers
# File lib/e-builder/base.rb, line 37 def mount *args, &setup root, controllers, applications = nil, [], [] opts = args.last.is_a?(Hash) ? args.pop : {} args.flatten.each do |a| if a.is_a?(String) root = rootify_url(a) elsif is_app?(a) controllers << a elsif a.respond_to?(:call) applications << a else controllers.concat extract_controllers(a) end end controllers.each do |c| @controllers[c] = [root, opts, setup] c.subcontrollers.each do |sc| mount(sc, root.to_s + c.base_url, opts) @subcontrollers << sc end end mount_applications applications, root, opts self end
block(s) to run just before application starts
# File lib/e-builder/setup.rb, line 234 def on_boot &proc (@on_boot ||= []).push(proc) end
declaring rewrite rules.
first argument should be a regex and a proc should be provided.
the regex(actual rule) will be compared against Request-URI, i.e. current URL without query string. if some rule depend on query string, use ‘params` inside proc to determine either some param was or not set.
the proc will decide how to operate when rule matched. you can do: ‘redirect(’location’)‘
redirect to new location using 302 status code
‘permanent_redirect(’location’)‘
redirect to new location using 301 status code
‘pass(controller, action, any, params, with => opts)`
pass control to given controller and action without redirect. consequent params are used to build URL to be sent to given controller.
‘halt(status|body|headers|response)`
send response to browser without redirect. accepts an arbitrary number of arguments. if arg is an Integer, it will be used as status code. if arg is a Hash, it is treated as headers. if it is an array, it is treated as Rack response and are sent immediately, ignoring other args. any other args are treated as body.
@note any method available to controller instance are also available inside rule proc.
so you can fine tune the behavior of any rule. ex. redirect on GET requests and pass control on POST requests. or do permanent redirect for robots and simple redirect for browsers etc.
@example
app = E.new # redirect to new address app.rewrite /\A\/(.*)\.php$/ do |title| redirect Controller.route(:index, title) end # permanent redirect app.rewrite /\A\/news\/([\w|\d]+)\-(\d+)\.html/ do |title, id| permanent_redirect Forum, :posts, :title => title, :id => id end # no redirect, just pass control to News controller app.rewrite /\A\/latest\/(.*)\.html/ do |title| pass News, :index, :scope => :latest, :title => title end # Return arbitrary body, status-code, headers, without redirect: # If argument is a hash, it is added to headers. # If argument is a Integer, it is treated as Status-Code. # Any other arguments are treated as body. app.rewrite /\A\/archived\/(.*)\.html/ do |title| if page = Model::Page.first(:url => title) halt page.content, 'Last-Modified' => page.last_modified.to_rfc2822 else halt 404, 'page not found' end end app.run
# File lib/e-builder/setup.rb, line 211 def rewrite rule, &proc proc || raise(ArgumentError, "Rewrite rules requires a block to run") @routes[rule] = {'GET' => {:rewriter => proc}} end
set/get app root
# File lib/e-builder/setup.rb, line 15 def root path = nil @root = ('%s/' % path).sub(/\/+\Z/, '/').freeze if path @root ||= (::Dir.pwd << '/').freeze end
by default, Espresso will use WEBrick server. pass :server option and any option accepted by selected(or default) server:
@example use Thin server on its default port
app.run :server => :Thin
@example use EventedMongrel server with custom options
app.run :server => :EventedMongrel, :port => 9090, :num_processors => 1000
@param [Hash] opts @option opts [Symbol] :server (:WEBrick) web server @option opts [Integer] :port (5252) @option opts [String] :host (0.0.0.0)
# File lib/e-builder/base.rb, line 126 def run opts = {} boot! handler = opts.delete(:server) (handler && Rack::Handler.const_defined?(handler)) || (handler = HTTP__DEFAULT_SERVER) port = opts.delete(:port) opts[:Port] ||= port || HTTP__DEFAULT_PORT host = opts.delete(:host) || opts.delete(:bind) opts[:Host] = host if host $stderr.puts "\n--- Starting Espresso for %s on %s port backed by %s server ---\n\n" % [ environment, opts[:Port], handler ] Rack::Handler.const_get(handler).run app, opts do |server| %w[INT TERM].each do |sig| Signal.trap(sig) do $stderr.puts "\n--- Stopping Espresso... ---\n\n" server.respond_to?(:stop!) ? server.stop! : server.stop end end server.threaded = opts[:threaded] if server.respond_to? :threaded= yield server if block_given? end end
allow app to use sessions.
@example keep sessions in memory
class App < E # ... end app = E.new app.session :memory app.run
@example keep sessions in memory using custom options
class App < E # ... end app = E.new app.session :memory, :domain => 'foo.com', :expire_after => 2592000 app.run
@example keep sessions in cookies
class App < E # ... end app = E.new app.session :cookies app.run
@example keep sessions in memcache
class App < E # ... end app = E.new app.session :memcache app.run
@example use a custom pool, i.e. github.com/migrs/rack-session-mongo
#> gem install rack-session-mongo class App < E # ... end require 'rack/session/mongo' app = E.new app.session Rack::Session::Mongo app.run
@param [Symbol, Class] use @param [Array] args
# File lib/e-builder/setup.rb, line 70 def session use, *args args.unshift case use when :memory Rack::Session::Pool when :cookies Rack::Session::Cookie when :memcache Rack::Session::Memcache else use end use(*args) end
by default Espresso will use EventMachine for streaming, but it also supports Celluloid, when Reel web-server used. use this method to set Celluloid as streaming backend.
@example
app = E.new do streaming_backend :Celluloid end
# File lib/e-builder/setup.rb, line 226 def streaming_backend backend = nil @streaming_backend = backend def streaming_backend @streaming_backend end end
# File lib/e-builder/base.rb, line 167 def to_app app self end
displays URLs the app will respond to, with controller and action that serving each URL.
# File lib/e-builder/base.rb, line 86 def url_map opts = {} mount_controllers! map = sorted_routes.inject({}) do |m,r| @routes[r].each_pair {|rm,rs| (m[r] ||= {})[rm] = rs.dup}; m end def map.to_s out = '' self.each_pair do |route, request_methods| next if route.source.size == 0 out << "%s\n" % route.source request_methods.each_pair do |rm,rs| out << " %s%s" % [rm, ' ' * (10 - rm.to_s.size)] out << "%s\n" % (rs[:app] || [rs[:controller], rs[:action]]*'#') end out << "\n" end out end map end
middleware declared here will be used on all controllers.
especially, here should go middleware that changes app state, which wont work if defined inside controller.
you can of course define any type of middleware at app level, it is even recommended to do so to avoid redundant middleware declaration at controllers level.
@example
class App < E # ... end app = E.new app.use SomeMiddleware, :with, :some => :opts app.run
Any middleware that does not change app state, i.e. non-upfront middleware, can be defined inside controllers.
@note middleware defined inside some controller will run only for that controller.
to have global middleware, define it at app level.
@example defining middleware at app level
module App class Forum < E map '/forum' # ... end class Blog < E map '/blog' # ... end end app = E.new app.use Rack::CommonLogger app.use Rack::ShowExceptions app.run
# File lib/e-builder/setup.rb, line 140 def use ware, *args, &block middleware << proc { |app| ware.new(app, *args, &block) } end
Private Instance Methods
# File lib/e-builder/base.rb, line 175 def call! env path = env[ENV__PATH_INFO] script_name = env[ENV__SCRIPT_NAME] @sorted_routes.each do |route| if matches = route.match(path) if route_setup = @routes[route][env[ENV__REQUEST_METHOD]] || @routes[route][:*] if route_setup[:rewriter] break unless valid_host?(@hosts.merge(@controllers_hosts), env) app = ERewriter.new(*matches.captures, &route_setup[:rewriter]) return app.call(env) elsif route_setup[:app] break unless valid_host?(@hosts.merge(@controllers_hosts), env) env[ENV__PATH_INFO] = normalize_path(matches[1].to_s) return route_setup[:app].call(env) else break unless valid_host?(@hosts.merge(route_setup[:controller].hosts), env) env[ENV__SCRIPT_NAME] = route_setup[:path].freeze env[ENV__PATH_INFO] = normalize_path(matches[1].to_s) if format_regexp = route_setup[:format_regexp] env[ENV__ESPRESSO_PATH_INFO], env[ENV__ESPRESSO_FORMAT] = \ env[ENV__PATH_INFO].split(format_regexp) end controller_instance = route_setup[:controller].new controller_instance.action_setup = route_setup app = Rack::Builder.new app.run controller_instance route_setup[:controller].middleware.each {|w,a,p| app.use w, *a, &p} return app.call(env) end else return [ STATUS__NOT_IMPLEMENTED, {"Content-Type" => "text/plain"}, ["Resource found but it can be accessed only through %s" % @routes[route].keys.join(", ")] ] end end end [ STATUS__NOT_FOUND, {'Content-Type' => "text/plain", "X-Cascade" => "pass"}, ['Not Found: %s' % env[ENV__PATH_INFO]] ] ensure env[ENV__PATH_INFO] = path env[ENV__SCRIPT_NAME] = script_name end
# File lib/e-builder/base.rb, line 280 def discover_controllers namespace = nil controllers = ObjectSpace.each_object(Class). select { |c| is_app?(c) }.reject { |c| [E].include? c } namespace.is_a?(Regexp) ? controllers.select { |c| c.name =~ namespace } : controllers end
# File lib/e-builder/base.rb, line 289 def extract_controllers namespace if [Class, Module].include?(namespace.class) return discover_controllers.select {|c| c.name =~ /\A#{namespace}/} end discover_controllers namespace end
# File lib/e-builder/base.rb, line 296 def mount_applications applications, root = nil, opts = {} applications = [applications] unless applications.is_a?(Array) applications.compact! return if applications.empty? root.is_a?(Hash) && (opts = root) && (root = nil) request_methods = (opts[:on] || opts[:request_method] || opts[:request_methods]) request_methods = [request_methods] unless request_methods.is_a?(Array) request_methods.compact! request_methods.map! {|m| m.to_s.upcase}.reject! do |m| HTTP__REQUEST_METHODS.none? {|lm| lm == m} end request_methods = HTTP__REQUEST_METHODS if request_methods.empty? route = route_to_regexp(rootify_url(root || '/')) applications.each do |a| @routes[route] = request_methods.inject({}) {|map,m| map.merge(m => {app: a})} end end
# File lib/e-builder/base.rb, line 258 def mount_controller controller, root = nil, opts = {}, &setup return if @mounted_controllers.include?(controller) root.is_a?(Hash) && (opts = root) && (root = nil) if root || base_url.size > 0 controller.remap!(base_url + root.to_s, opts) end unless @subcontrollers.include?(controller) @global_setup && controller.global_setup!(&@global_setup) setup && controller.external_setup!(&setup) end controller.mount! self @routes.update controller.routes @controllers_hosts.update controller.hosts controller.rewrite_rules.each {|(rule,proc)| rewrite_rule(rule, &proc)} @mounted_controllers << controller end
# File lib/e-builder/base.rb, line 252 def mount_controllers! automount! if @automount @mounted_controllers = [] @controllers.each_pair {|c,(root,opts,setup)| mount_controller(c, root, opts, &setup)} end
# File lib/e-builder/base.rb, line 241 def normalize_path path (path_ok?(path) ? path : '/' << path).freeze end
execute blocks defined via ‘on_boot`
# File lib/e-builder/base.rb, line 318 def on_boot! (@on_boot || []).each {|b| b.call} end
checking whether path is empty or starts with a slash
# File lib/e-builder/base.rb, line 246 def path_ok? path # comparing fixnums are much faster than comparing strings path.hash == (@empty_string_hash ||= '' .hash) || # faster than path.empty? path[0].hash == (@slash_hash ||= '/'.hash) # faster than path =~ /^\// end
# File lib/e-builder/base.rb, line 228 def sorted_routes @routes.keys.sort {|a,b| b.source.size <=> a.source.size} end
# File lib/e-builder/base.rb, line 232 def valid_host? accepted_hosts, env http_host, server_name, server_port = env.values_at(ENV__HTTP_HOST, ENV__SERVER_NAME, ENV__SERVER_PORT) accepted_hosts[http_host] || accepted_hosts[server_name] || http_host == server_name || http_host == server_name+':'+server_port end