1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
|
# frozen_string_literal: true
module Rack
# Rack::Builder implements a small DSL to iteratively construct Rack
# applications.
#
# Example:
#
# require 'rack/lobster'
# app = Rack::Builder.new do
# use Rack::CommonLogger
# use Rack::ShowExceptions
# map "/lobster" do
# use Rack::Lint
# run Rack::Lobster.new
# end
# end
#
# run app
#
# Or
#
# app = Rack::Builder.app do
# use Rack::CommonLogger
# run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
# end
#
# run app
#
# +use+ adds middleware to the stack, +run+ dispatches to an application.
# You can use +map+ to construct a Rack::URLMap in a convenient way.
class Builder
# https://stackoverflow.com/questions/2223882/whats-the-difference-between-utf-8-and-utf-8-without-bom
UTF_8_BOM = '\xef\xbb\xbf'
def self.parse_file(config, opts = Server::Options.new)
if config.end_with?('.ru')
return self.load_file(config, opts)
else
require config
app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join(''))
return app, {}
end
end
def self.load_file(path, opts = Server::Options.new)
options = {}
cfgfile = ::File.read(path)
cfgfile.slice!(/\A#{UTF_8_BOM}/) if cfgfile.encoding == Encoding::UTF_8
if cfgfile[/^#\\(.*)/] && opts
options = opts.parse! $1.split(/\s+/)
end
cfgfile.sub!(/^__END__\n.*\Z/m, '')
app = new_from_string cfgfile, path
return app, options
end
def self.new_from_string(builder_script, file = "(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
end
def initialize(default_app = nil, &block)
@use, @map, @run, @warmup, @freeze_app = [], nil, default_app, nil, false
instance_eval(&block) if block_given?
end
def self.app(default_app = nil, &block)
self.new(default_app, &block).to_app
end
# Specifies middleware to use in a stack.
#
# class Middleware
# def initialize(app)
# @app = app
# end
#
# def call(env)
# env["rack.some_header"] = "setting an example"
# @app.call(env)
# end
# end
#
# use Middleware
# run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
#
# All requests through to this application will first be processed by the middleware class.
# The +call+ method in this example sets an additional environment key which then can be
# referenced in the application if required.
def use(middleware, *args, &block)
if @map
mapping, @map = @map, nil
@use << proc { |app| generate_map(app, mapping) }
end
@use << proc { |app| middleware.new(app, *args, &block) }
end
ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
# Takes an argument that is an object that responds to #call and returns a Rack response.
# The simplest form of this is a lambda object:
#
# run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
#
# However this could also be a class:
#
# class Heartbeat
# def self.call(env)
# [200, { "Content-Type" => "text/plain" }, ["OK"]]
# end
# end
#
# run Heartbeat
def run(app)
@run = app
end
# Takes a lambda or block that is used to warm-up the application.
#
# warmup do |app|
# client = Rack::MockRequest.new(app)
# client.get('/')
# end
#
# use SomeMiddleware
# run MyApp
def warmup(prc = nil, &block)
@warmup = prc || block
end
# Creates a route within the application.
#
# Rack::Builder.app do
# map '/' do
# run Heartbeat
# end
# end
#
# The +use+ method can also be used here to specify middleware to run under a specific path:
#
# Rack::Builder.app do
# map '/' do
# use Middleware
# run Heartbeat
# end
# end
#
# This example includes a piece of middleware which will run before requests hit +Heartbeat+.
#
def map(path, &block)
@map ||= {}
@map[path] = block
end
# Freeze the app (set using run) and all middleware instances when building the application
# in to_app.
def freeze_app
@freeze_app = true
end
def to_app
app = @map ? generate_map(@run, @map) : @run
fail "missing run or map statement" unless app
app.freeze if @freeze_app
app = @use.reverse.inject(app) { |a, e| e[a].tap { |x| x.freeze if @freeze_app } }
@warmup.call(app) if @warmup
app
end
def call(env)
to_app.call(env)
end
private
def generate_map(default_app, mapping)
mapped = default_app ? { '/' => default_app } : {}
mapping.each { |r, b| mapped[r] = self.class.new(default_app, &b).to_app }
URLMap.new(mapped)
end
end
end
|