├── Gemfile ├── app ├── tutorial_1 │ ├── classic.rb │ └── tutorial_1.md ├── tutorial_2 │ ├── classic.rb │ ├── classic.ru │ ├── modular.ru │ ├── modular.rb │ ├── classic_2.rb │ ├── middleware_stack.rb │ └── tutorial_2.md ├── tutorial_3 │ ├── classic.rb │ └── tutorial_3.md └── tutorial_4 │ └── tutorial_4.md ├── .gitmodules ├── Gemfile.lock └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | gem 'sinatra', :path => 'sinatra' -------------------------------------------------------------------------------- /app/tutorial_1/classic.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | 3 | get '/' do 4 | 'Hello world!' 5 | end -------------------------------------------------------------------------------- /app/tutorial_2/classic.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | 3 | get '/' do 4 | 'Hello world!' 5 | end -------------------------------------------------------------------------------- /app/tutorial_3/classic.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | 3 | get '/' do 4 | 'Hello world!' 5 | end -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "sinatra"] 2 | path = sinatra 3 | url = git://github.com/sinatra/sinatra.git 4 | -------------------------------------------------------------------------------- /app/tutorial_2/classic.ru: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/classic') 2 | run Sinatra::Application -------------------------------------------------------------------------------- /app/tutorial_2/modular.ru: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/modular') 2 | run Sinatra::Base 3 | # or run Modular -------------------------------------------------------------------------------- /app/tutorial_2/modular.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | 3 | class Modular < Sinatra::Base 4 | 5 | get '/' do 6 | 'Hello world!' 7 | end 8 | 9 | run! 10 | end -------------------------------------------------------------------------------- /app/tutorial_2/classic_2.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | 3 | include Sinatra::Delegator 4 | 5 | get '/' do 6 | 'Hello world!' 7 | end 8 | 9 | Sinatra::Application.run! 10 | # Sinatra::Base.run! -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: sinatra 3 | specs: 4 | sinatra (1.3.0.c) 5 | rack (~> 1.2) 6 | tilt (~> 1.2, >= 1.2.2) 7 | 8 | GEM 9 | specs: 10 | rack (1.2.2) 11 | tilt (1.2.2) 12 | 13 | PLATFORMS 14 | ruby 15 | 16 | DEPENDENCIES 17 | sinatra! 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | I write this while I dig into sinatra source code myself. Please correct me if you find any mistakes or unclarity. Since Sinatra is short and concise, I hope the tutorial makes sense and it can keep pace with sinatra development with contributions from the community. The tutorial is prepared based on Sinatra 1.3.0c. 2 | 3 | Sinatra is added as a git submodule in the sinatra folder. 4 | As this tutorial is for sinatra, I don't include the full rack source. I only list some relevant code based on rack 1.2.2 (https://github.com/rack/rack/tree/1.2.2). 5 | If you need I recommend to use tux to play with sinatra https://github.com/cldwalker/tux 6 | 7 | It's assumed that you have read the sinatra README. Sinatra is well documented so that's the only thing you need for this tutorial. 8 | 9 | Content: 10 | ________ 11 | 12 | * tutorial_1: sinatra startup (https://github.com/zhengjia/sinatra-explained/blob/master/app/tutorial_1/tutorial_1.md) 13 | * tutorial_2: extensions and middleware (https://github.com/zhengjia/sinatra-explained/blob/master/app/tutorial_2/tutorial_2.md) 14 | * tutorial_3: routing (https://github.com/zhengjia/sinatra-explained/blob/master/app/tutorial_3/tutorial_3.md) 15 | * tutorial_4: request and response (work in progress) 16 | * tutorial_5: request cycle 17 | * tutorial_6: sinatra helpers 18 | * tutorial_7: templates 19 | -------------------------------------------------------------------------------- /app/tutorial_4/tutorial_4.md: -------------------------------------------------------------------------------- 1 | A few attr_accessor defined on Sinatra::Base: 2 | 3 | ```ruby 4 | attr_accessor :env, :request, :response, :params 5 | ``` 6 | 7 | Request 8 | ------- 9 | 10 | Request.new is removed later. https://github.com/sinatra/sinatra/issues/239 11 | 12 | ```ruby 13 | # The request object. See Rack::Request for more info: 14 | # http://rack.rubyforge.org/doc/classes/Rack/Request.html 15 | class Request < Rack::Request 16 | def self.new(env) 17 | env['sinatra.request'] ||= super 18 | end 19 | 20 | # Returns an array of acceptable media types for the response 21 | def accept 22 | @env['sinatra.accept'] ||= begin 23 | entries = @env['HTTP_ACCEPT'].to_s.split(',') 24 | entries.map { |e| accept_entry(e) }.sort_by(&:last).map(&:first) 25 | end 26 | end 27 | 28 | def preferred_type(*types) 29 | return accept.first if types.empty? 30 | types.flatten! 31 | accept.detect do |pattern| 32 | type = types.detect { |t| File.fnmatch(pattern, t) } 33 | return type if type 34 | end 35 | end 36 | 37 | alias accept? preferred_type 38 | alias secure? ssl? 39 | 40 | def forwarded? 41 | @env.include? "HTTP_X_FORWARDED_HOST" 42 | end 43 | 44 | def route 45 | @route ||= Rack::Utils.unescape(path_info) 46 | end 47 | 48 | def path_info=(value) 49 | @route = nil 50 | super 51 | end 52 | 53 | private 54 | 55 | def accept_entry(entry) 56 | type, *options = entry.gsub(/\s/, '').split(';') 57 | quality = 0 # we sort smalles first 58 | options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' } 59 | [type, [quality, type.count('*'), 1 - options.size]] 60 | end 61 | end 62 | ``` 63 | 64 | Response 65 | -------- 66 | 67 | Let's see the response generated by sinatra error handling. 68 | 69 | NotFound is an exception class inherited from ruby's NameError whose parent is StandardError. It has one method `code` which just returns 404. 70 | 71 | ```ruby 72 | class NotFound < NameError #:nodoc: 73 | def code ; 404 ; end 74 | end 75 | ``` 76 | 77 | The NotFound is raised in `route_missing` if no routes can be matched. `route_missing` is called by `route!`, which is called by `dispatch!`. `dispatch!` rescues the NotFound. We have already covered the three methods in tutorial 3, but we still list them here for quick reference. 78 | 79 | ```ruby 80 | # No matching route was found or all routes passed. The default 81 | # implementation is to forward the request downstream when running 82 | # as middleware (@app is non-nil); when no downstream app is set, raise 83 | # a NotFound exception. Subclasses can override this method to perform 84 | # custom route miss logic. 85 | def route_missing 86 | if @app 87 | forward 88 | else 89 | raise NotFound 90 | end 91 | end 92 | ``` 93 | 94 | ```ruby 95 | # Run routes defined on the class and all superclasses. 96 | def route!(base = settings, pass_block=nil) 97 | if routes = base.routes[@request.request_method] 98 | routes.each do |pattern, keys, conditions, block| 99 | pass_block = process_route(pattern, keys, conditions) do 100 | route_eval(&block) 101 | end 102 | end 103 | end 104 | 105 | # Run routes defined in superclass. 106 | if base.superclass.respond_to?(:routes) 107 | return route!(base.superclass, pass_block) 108 | end 109 | 110 | route_eval(&pass_block) if pass_block 111 | route_missing 112 | end 113 | ``` 114 | 115 | ```ruby 116 | # Dispatch a request with error handling. 117 | def dispatch! 118 | static! if settings.static? && (request.get? || request.head?) 119 | filter! :before 120 | route! 121 | rescue NotFound => boom 122 | handle_not_found!(boom) 123 | rescue ::Exception => boom 124 | handle_exception!(boom) 125 | ensure 126 | filter! :after unless env['sinatra.static_file'] 127 | end 128 | ``` 129 | 130 | We can see `dispatch` also rescue general exceptions. It runs the after filters at last unless env['sinatra.static_file'] is set, which means the request is asking for a static file. 131 | 132 | After an exception is rescued `handle_not_found!` is called with the exception object as the parameter. env['sinatra.error'] is set to the exception object and available to the downstream app. `@response.status` is of course set to 404 and the `@response.body` is set to `['

Not Found

']`. @response.headers['X-Cascade'] is set to 'pass' to indicate the rest of the middleware stack that they can try to match the requested route and generate response. Then `error_block!` is called with the exception class as the first param and NotFound as the second param. Let's see what `error_block!` does. 133 | 134 | ```ruby 135 | # Special treatment for 404s in order to play nice with cascades. 136 | def handle_not_found!(boom) 137 | @env['sinatra.error'] = boom 138 | @response.status = 404 139 | @response.headers['X-Cascade'] = 'pass' 140 | @response.body = ['

Not Found

'] 141 | error_block! boom.class, NotFound 142 | end 143 | ``` 144 | 145 | ```ruby 146 | # Find an custom error block for the key(s) specified. 147 | def error_block!(*keys) 148 | keys.each do |key| 149 | base = settings 150 | while base.respond_to?(:errors) 151 | if block = base.errors[key] 152 | # found a handler, eval and return result 153 | return instance_eval(&block) 154 | else 155 | base = base.superclass 156 | end 157 | end 158 | end 159 | raise boom if settings.show_exceptions? and keys == Exception 160 | nil 161 | end 162 | ``` 163 | 164 | Next is the `not_found` method. There are two of them. One is an instance method in the Helper module: 165 | 166 | ```ruby 167 | # Halt processing and return a 404 Not Found. 168 | def not_found(body=nil) 169 | error 404, body 170 | end 171 | ``` 172 | 173 | This `not_found` calls the `error` method also in the Helper module. 174 | 175 | ``` 176 | # Halt processing and return the error status provided. 177 | def error(code, body=nil) 178 | code, body = 500, code.to_str if code.respond_to? :to_str 179 | response.body = body unless body.nil? 180 | halt code 181 | end 182 | ``` 183 | 184 | The Sinatra::Helper is included to the Sinatra::Base, so these two not_found and errors are available in the route handler and views. 185 | 186 | The other `not_found` is a singleton method on Sinatra::Base. 187 | 188 | # Sugar for `error(404) { ... }` 189 | def not_found(&block) 190 | error 404, &block 191 | end 192 | 193 | It calls the error method also on singleton class of Sinatra::Base 194 | 195 | ```ruby 196 | # Define a custom error handler. Optionally takes either an Exception 197 | # class, or an HTTP status code to specify which errors should be 198 | # handled. 199 | def error(codes=Exception, &block) 200 | Array(codes).each { |code| @errors[code] = block } 201 | end 202 | ``` 203 | 204 | 205 | error MyCustomError do 206 | 'So what happened was...' + request.env['sinatra.error'].message 207 | end 208 | 209 | get '/' do 210 | raise MyCustomError, 'something bad' 211 | end -------------------------------------------------------------------------------- /app/tutorial_2/middleware_stack.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | 3 | class MiddlewareStack < Sinatra::Base 4 | set :method_override, true 5 | end 6 | 7 | puts MiddlewareStack.build.inspect 8 | 9 | # result is something like 10 | 11 | #, #, #>]> 12 | 13 | 14 | puts MiddlewareStack.new.inspect 15 | 16 | # result is something like 17 | 18 | # => #>, @template=#\\n\\n\\n \\n \"\n\n\n\n; _erbout.concat((h exception.class ).to_s); _erbout.concat \" at \"; _erbout.concat((h path ).to_s); _erbout.concat \"\\n\\n \\n\\n\\n\\n\\n
\\n
\\n \\\"application\\n
\\n

\"\n\n; _erbout.concat((h exception.class ).to_s); _erbout.concat \" at \"; _erbout.concat((h path ).to_s); _erbout.concat \"\\n

\\n

\"\n\n; _erbout.concat((h exception.message ).to_s); _erbout.concat \"

\\n
    \\n
  • file: \\n \"\n\n\n; _erbout.concat((h frames.first.filename.split(\"/\").last ).to_s); _erbout.concat \"
  • \\n
  • location: \"\n; _erbout.concat((h frames.first.function ).to_s); _erbout.concat \"\\n
  • \\n
  • line:\\n \"\n\n\n; _erbout.concat((h frames.first.lineno ).to_s); _erbout.concat \"
  • \\n
\\n
\\n
\\n
\\n\\n
\\n

BACKTRACE

\\n

(expand)

\\n

JUMP TO:\\n GET\\n POST\\n COOKIES\\n ENV\\n

\\n
\\n\\n
    \\n\\n \"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n; id = 1 ; _erbout.concat \"\\n \"\n; frames.each do |frame| ; _erbout.concat \"\\n \"\n; if frame.context_line && frame.context_line != \"#\" ; _erbout.concat \"\\n\\n
  • \\n \"\n; _erbout.concat((h frame.filename ).to_s); _erbout.concat \" in\\n \"\n; _erbout.concat((h frame.function ).to_s); _erbout.concat \"\\n
  • \\n\\n
  • \\n \"\n; if frame.pre_context ; _erbout.concat \"\\n
      \\n \"\n; frame.pre_context.each do |line| ; _erbout.concat \"\\n
    1. \"\n; _erbout.concat((h line ).to_s); _erbout.concat \"
    2. \\n \"\n; end ; _erbout.concat \"\\n
    \\n \"\n\n; end ; _erbout.concat \"\\n\\n
      \\n
    1. \"; _erbout.concat((\n h frame.context_line ).to_s); _erbout.concat \"
    2. \\n
    \\n\\n \"\n\n\n; if frame.post_context ; _erbout.concat \"\\n
      \\n \"\n; frame.post_context.each do |line| ; _erbout.concat \"\\n
    1. \"\n; _erbout.concat((h line ).to_s); _erbout.concat \"
    2. \\n \"\n; end ; _erbout.concat \"\\n
    \\n \"\n\n; end ; _erbout.concat \"\\n
    \\n
  • \\n\\n \"\n\n\n\n; end ; _erbout.concat \"\\n\\n \"\n\n; id += 1 ; _erbout.concat \"\\n \"\n; end ; _erbout.concat \"\\n\\n
\\n
\\n\\n
\\n

GET

\\n \"\n\n\n\n\n\n\n; unless req.GET.empty? ; _erbout.concat \"\\n \\n \\n \\n \\n \\n \"\n\n\n\n\n\n; req.GET.sort_by { |k, v| k.to_s }.each { |key, val| ; _erbout.concat \"\\n \\n \\n \\n \\n \"\n\n; } ; _erbout.concat \"\\n
VariableValue
\"\n\n; _erbout.concat((h key ).to_s); _erbout.concat \"
\"\n; _erbout.concat((h val.inspect ).to_s); _erbout.concat \"
\\n \"\n\n; else ; _erbout.concat \"\\n

No GET data.

\\n \"\n\n; end ; _erbout.concat \"\\n
\\n
\\n\\n
\\n

POST

\\n \"\n\n\n\n\n\n; unless req.POST.empty? ; _erbout.concat \"\\n \\n \\n \\n \\n \\n \"\n\n\n\n\n\n; req.POST.sort_by { |k, v| k.to_s }.each { |key, val| ; _erbout.concat \"\\n \\n \\n \\n \\n \"\n\n; } ; _erbout.concat \"\\n
VariableValue
\"\n\n; _erbout.concat((h key ).to_s); _erbout.concat \"
\"\n; _erbout.concat((h val.inspect ).to_s); _erbout.concat \"
\\n \"\n\n; else ; _erbout.concat \"\\n

No POST data.

\\n \"\n\n; end ; _erbout.concat \"\\n
\\n
\\n\\n
\\n

COOKIES

\\n \"\n\n\n\n\n\n; unless req.cookies.empty? ; _erbout.concat \"\\n \\n \\n \\n \\n \\n \"\n\n\n\n\n\n; req.cookies.each { |key, val| ; _erbout.concat \"\\n \\n \\n \\n \\n \"\n\n; } ; _erbout.concat \"\\n
VariableValue
\"\n\n; _erbout.concat((h key ).to_s); _erbout.concat \"
\"\n; _erbout.concat((h val.inspect ).to_s); _erbout.concat \"
\\n \"\n\n; else ; _erbout.concat \"\\n

No cookie data.

\\n \"\n\n; end ; _erbout.concat \"\\n
\\n
\\n\\n
\\n

Rack ENV

\\n \\n \\n \\n \\n \\n \"\n\n\n\n\n\n\n\n\n\n\n; env.sort_by { |k, v| k.to_s }.each { |key, val| ; _erbout.concat \"\\n \\n \\n \\n \\n \"\n\n; } ; _erbout.concat \"\\n
VariableValue
\"\n\n; _erbout.concat((h key ).to_s); _erbout.concat \"
\"\n; _erbout.concat((h val ).to_s); _erbout.concat \"
\\n
\\n
\\n\\n

You're seeing this error because you have\\nenabled the show_exceptions setting.

\\n
\\n \\n\\n\"\n\n\n\n\n\n\n\n\n\n; _erbout.force_encoding(__ENCODING__)", @enc=#, @filename=nil>>> -------------------------------------------------------------------------------- /app/tutorial_1/tutorial_1.md: -------------------------------------------------------------------------------- 1 | We will start the tutorials with a simple four-line sinatra app as shown in classic.rb. The question we are going to solve in the first tutorial is: what will happen when we require sinatra? 2 | 3 | To get a hint, looking at the four line app, apparently the get method is available in the context of the current app. So some methods are "imported" from sinatra to the current app. To get another hint, we create a file which only has one line: `require 'sinatra'`, and run it. We can see a server starts! 4 | 5 | This brings out two areas we are going to cover: method lookup and server startup in sinatra. 6 | 7 | First let's see where the get method is defined. get is a class method of Sinatra::Base. Since it's available at the top level in the current app, it can either be a class method or an instance method defined at the top level main object. Let's see which case it is and how get becomes available in the current app. (In case you don't know, methods defined on the top level becomes private instance methods of Object class; class methods defined on top level become singleton methods on the main object, which is an instance of Object.) 8 | 9 | If you look at `sinatra/lib/sinatra.rb`, which is the file that is required by the first line `require 'sinatra'` 10 | 11 | ```ruby 12 | libdir = File.dirname(__FILE__) 13 | $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir) 14 | 15 | require 'sinatra/base' 16 | require 'sinatra/main' 17 | 18 | enable :inline_templates 19 | ``` 20 | 21 | First two lines add sinatra/lib to $LOAD_PATH. Then `sinatra/lib/sinatra/base.rb`, which is the file contains majority of the code, and `sinatra/lib/sinatra/main.rb` are required. `sinatra/lib/sinatra/base.rb` has a lot of classes and modules defined: `Sinatra::Base`, `Sinatra::Request`, `Sinatra::Response`, `Sinatra::NotFound`, `Sinatra::Helpers`, `Sinatra::Templates`, `Sinatra::Application`, and `Sinatra::Delegator`; among them `Sinatra::Application` is a subclass of `Sinatra::Base`, and it is opened and further defined by `sinatra/lib/sinatra/main.rb`. 22 | 23 | By looking at the top level code, the whole sinatra/base is inside the Sinatra module, so it can be safely passed at this step because it's in its own scope and can't be automatically hooked to our app. There are two other possibilities: `enable :inline_templates` on the last line of `sinatra/lib/sinatra.rb`, and `include Sinatra::Delegator` on the last line of `sinatra/lib/sinatra/main.rb`. If you grep on 'def enable', it's a class methods of Sinatra::Base. It looks like it has nothing to do with the get method. The only hope is this line: `include Sinatra::Delegator`. We can see :get is passed in as a parameter to the `delegate` method, which looks promising. Let's look at this module in detail. 24 | 25 | Sinatra::Delegator is a module defined in Sinatra::Base: 26 | 27 | ```ruby 28 | # Sinatra delegation mixin. Mixing this module into an object causes all 29 | # methods to be delegated to the Sinatra::Application class. Used primarily 30 | # at the top-level. 31 | module Delegator #:nodoc: 32 | def self.delegate(*methods) 33 | methods.each do |method_name| 34 | eval <<-RUBY, binding, '(__DELEGATE__)', 1 35 | def #{method_name}(*args, &b) 36 | ::Sinatra::Delegator.target.send(#{method_name.inspect}, *args, &b) 37 | end 38 | private #{method_name.inspect} 39 | RUBY 40 | end 41 | end 42 | 43 | delegate :get, :patch, :put, :post, :delete, :head, :options, :template, :layout, 44 | :before, :after, :error, :not_found, :configure, :set, :mime_type, 45 | :enable, :disable, :use, :development?, :test?, :production?, 46 | :helpers, :settings 47 | 48 | class << self 49 | attr_accessor :target 50 | end 51 | 52 | self.target = Application 53 | end 54 | ``` 55 | 56 | When I try to find the executing path of an app or library, I'd look at the hook methods like `Module#included`, `Class#inherited`. But Sinatra::Delegator doesn't have any of those. We know when include 'SomeModule' is called, all instance methods of SomeModule are included in the calling class. At a glance it seems there is no instance methods in Sinatra::Delegator, which is false. I would then look at code that's run immediately, i.e., code not in method definitions. Here the delegate method is run when `require 'sinatra/base'` is called. The delegate method defines on Sinatra::Delegator a bunch of private instance methods including the get method. Note that the scope inside `self.delegate` is still the Delegator class; the newly defined methods become instance methods of Sinatra::Delegator, instead of class methods of `Sinatra::Delegator`. When include Sinatra::Delegator is called these instance methods are included to the top level of current app. This answers the question we asked: the `get` method is available to the current app as an instance method. 57 | 58 | The technique used here is to dynamically define a new set of instance methods, include them as instance methods to the current app, and then delegate calls to them to the corresponding class methods on `Sinatra::Delegator.target`, i.e. `Sinatra::Application`. Since Sinatra::Application is a subclass of Sinatra::Base, it has all the class methods of Sinatra::Base. If you are not familiar with the eval syntax, here is a good reference http://olabini.com/blog/2008/01/ruby-antipattern-using-eval-without-positioning-information/. Otherwise the syntax in the Delegator module is straightforward. The reason we have the Sinatra::Delegator module is that it picks some of the class methods from Sinatra::Base and make them available to the current app. Finally the source code annotation at the top of the Delegator module makes sense, and we know why get is available in the current app. We will explain other delegated methods defined here in later tutorials as we encounter them. 59 | 60 | After we define a route with the get method, the server starts. Let's see how that happens. There are a lot of default settings going on and we only look at some of them for now. 61 | 62 | It all starts with the `at_exit` method in `sinatra/lib/sintra/main.rb`. at_exit is a `Kernal` method that runs the block when the current app exits. 63 | 64 | ```ruby 65 | require 'sinatra/base' 66 | 67 | module Sinatra 68 | class Application < Base 69 | 70 | # we assume that the first file that requires 'sinatra' is the 71 | # app_file. all other path related options are calculated based 72 | # on this path by default. 73 | set :app_file, caller_files.first || $0 74 | 75 | set :run, Proc.new { $0 == app_file } 76 | 77 | if run? && ARGV.any? 78 | require 'optparse' 79 | OptionParser.new { |op| 80 | op.on('-x') { set :lock, true } 81 | op.on('-e env') { |val| set :environment, val.to_sym } 82 | op.on('-s server') { |val| set :server, val } 83 | op.on('-p port') { |val| set :port, val.to_i } 84 | op.on('-o addr') { |val| set :bind, val } 85 | }.parse!(ARGV.dup) 86 | end 87 | end 88 | 89 | at_exit { Application.run! if $!.nil? && Application.run? } 90 | end 91 | 92 | include Sinatra::Delegator 93 | ``` 94 | 95 | In `sinatra/lib/sintra/main.rb`, it first calls `require 'sinatra/base'` to make sure Sinatra::Base is available to it. We need to explain two methods: `set` and `caller_files``. set is a class method on Sinatra::Base and is also delegated from current app to Sinatra::Application. 96 | 97 | ```ruby 98 | # Sets an option to the given value. If the value is a proc, 99 | # the proc will be called every time the option is accessed. 100 | def set(option, value=self, &block) 101 | raise ArgumentError if block && value != self 102 | value = block if block 103 | if value.kind_of?(Proc) 104 | metadef(option, &value) 105 | metadef("#{option}?") { !!__send__(option) } 106 | metadef("#{option}=") { |val| metadef(option, &Proc.new{val}) } 107 | elsif value == self && option.respond_to?(:each) 108 | option.each { |k,v| set(k, v) } 109 | elsif respond_to?("#{option}=") 110 | __send__ "#{option}=", value 111 | else 112 | set option, Proc.new{value} 113 | end 114 | self 115 | end 116 | ``` 117 | 118 | set method is interesting but a bit complicated. There are several forms to use the set method, which helps to explain it. First one is just `set :some_option, "some_value"`. It will just be translated to `set option, Proc.new{value}`, which is exactly the second form. A variation is passing a block to set like the following example. The `value` parameter defaults to self, and will be reassigned to the proc. 119 | 120 | set(:probability) { |value| condition { rand <= value } } 121 | 122 | For the second form, it open the singleton class of the current class. In the case our classic.rb, since set is delegated Sinatra::Application, it will define three new methods on Sinatra::Application using the `metadef` private class method on Sinatra::Base. In metadef, (class << self; self; end) opens the singleton class of Sinatra::Base and defines methods there. Just remember sinatra settings are defined on Sinatra::Base and not on our app. 123 | 124 | ```ruby 125 | def metadef(message, &block) 126 | (class << self; self; end). 127 | send :define_method, message, &block 128 | end 129 | ``` 130 | 131 | The three class methods are used as setter, getter, and question mark method. The getter is lazy evaluated, meaning content of the block is used as the method body and isn't called until the getter is called. The question mark method uses double bang to get the true/false value based on the truth of the result of the getter method. The third form is that when the setter is already defined by previous calls to the set method, then when we use set method in the first form it doesn't go through the second form and defines the getter setter and question mark element again; instead it just used the already defined setter. 132 | 133 | As an example, if we have `set :inline_templates, true`, then we will have three class methods available on the Sinatra::Application: `inline_templates` which returns true, `inline_templates?` which returns true also, and `inline_templates=` which sets inline_templates to a new value. We will look at how the set method is typically used in later tutorials. 134 | 135 | The last form of set method accepts a hash and split the hash to set individual element. For example, `set :a => 'value1', :b => 'value2'` equals to two calls: `set :a => 'value1'`, and `set :b => 'value2'` 136 | 137 | Finally the set method returns self, which is the current class Sinatra::Application so other methods can be chained to set method. However I've never seen any cases this can be useful. 138 | 139 | Related to `set`, two `setting` methods are defined as instance method and class method of Sinatra::Base. The instance `setting` method does nothing but calls the class method `setting`. The class `setting` method just returns the current class. When we can call a setting getter method on current class it will go to its superclass Sinatra::Base which will fetch the setting defined on the singleton class of Sinatra::Base. 140 | 141 | ```ruby 142 | # Access settings defined with Base.set. 143 | def self.settings 144 | self 145 | end 146 | ``` 147 | 148 | ```ruby 149 | # Access settings defined with Base.set. 150 | def settings 151 | self.class.settings 152 | end 153 | ``` 154 | 155 | Then we come to the `caller_files` and it's associated code. caller_files is a public class method of Sinatra::Base. `CALLERS_TO_IGNORE` is a constant that defines the patterns that should be ignored from result of the `Kernel#caller`. The first regular expression is kind of special. It matches `/sinatra.rb`, `/sinatra/base.rb`, `/sinatra/main.rb`, and `/sinatra/showexceptions.rb`. `RUBY_IGNORE_CALLERS` is added to CALLERS_TO_IGNORE if it's available. caller_locations calls the Kernel#caller method, which basically returns the calling stack in the format like `/Users/zjia/code/ruby_test/caller/caller.rb:3:in '
'`. The `caller(1)` will ignore the top level of the calling stack, i.e., the `sinatra/lib/sinatra/main.rb` itself. Regex `/:(?=\d|in )/` matches a colon preceding a number or a string 'in', but not including the number or 'in'. For example in `/Users/zjia/code/ruby_test/caller/caller.rb:3:in '
'` it will match the two colons. Then `/Users/zjia/code/ruby_test/caller/caller.rb:3:in '
'` is splitted at the two colons and [0,2] get the first two elements of the array returned by the split, i.e., the pure file location and the line number. Finally the reject method uses the patterns in CALLERS_TO_IGNORE to remove the unwanted lines of the calling stack. The `caller_files` further removes the line number and returns only the pure file location. 156 | 157 | We return to the line `set :app_file, caller_files.first || $0`. As the source annotation says, `caller_files.first` is the file that calls `require 'sinatra'`. As we talked, when `require 'sinatra'` is called, it requires sinatra/lib/sinatra.rb, which requires sinatra/lib/sinatra/main.rb. sinatra/lib/sinatra.rb and /sinatra/lib/sinatra/main.rb are in the ignored patterns so they are removed from caller_files. Then the first element in the array should be the one that contains the `requires 'sinatra'`. Here I think `caller(1)` in caller_locations is not necessary because the top level of the calling stack sinatra/lib/sinatra/main.rb is in the ignored pattern. If caller_files is an empty array, which is possible when the file is located in the ignored paths, then the current running file stored in $0 is set as the `app_file`. `app_file` stores the root path of the sinatra project and locations of other files are based on it. 158 | 159 | ```ruby 160 | CALLERS_TO_IGNORE = [ # :nodoc: 161 | /\/sinatra(\/(base|main|showexceptions))?\.rb$/, # all sinatra code 162 | /lib\/tilt.*\.rb$/, # all tilt code 163 | /\(.*\)/, # generated code 164 | /rubygems\/custom_require\.rb$/, # rubygems require hacks 165 | /active_support/, # active_support require hacks 166 | /bundler(\/runtime)?\.rb/, # bundler require hacks 167 | /= 1.9.2 168 | ] 169 | 170 | # add rubinius (and hopefully other VM impls) ignore patterns ... 171 | CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS) if defined?(RUBY_IGNORE_CALLERS) 172 | 173 | # Like Kernel#caller but excluding certain magic entries and without 174 | # line / method information; the resulting array contains filenames only. 175 | def caller_files 176 | caller_locations. 177 | map { |file,line| file } 178 | end 179 | 180 | # Like caller_files, but containing Arrays rather than strings with the 181 | # first element being the file, and the second being the line. 182 | def caller_locations 183 | caller(1). 184 | map { |line| line.split(/:(?=\d|in )/)[0,2] }. 185 | reject { |file,line| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } } 186 | end 187 | ``` 188 | 189 | Next line `set :run, Proc.new { $0 == app_file }` defines three singleton methods on Sinatra::Base. The `run` and `run?` methods do the same thing: if the current running file is the `app_file` we just set, i.e. the current file does `require 'sinatra'`, then it will return true. `run=` setter is also defined on Sinatra::Base, but I don't think it's used. In fact only `run?` is used to determine whether to run the app now or not. The reason to have `run?` is that it's possible that one app can be used as a middleware and should not be run when it requires sinatra, or in the case a project has multiple files that require sinatra, only the file run in the command line should run the server. 190 | 191 | Now we come to the option parsing. If the current app is supposed to be run and any arguments are passed in to run it, sinatra will set those settings based on the passed in arguments. You can refer to the full list of the available settings in the "Available Settings" section in sinatra doc. The option parsing is pretty standard and I will just include a reference here http://ruby-doc.org/stdlib/libdoc/optparse/rdoc/classes/OptionParser.html 192 | 193 | Finally we come to `at_exit { Application.run! if $!.nil? && Application.run? }`. $!.nil? ensures there is no exceptions raised at this point. Let's look at the run! method. It's defined as a class method of Sinatra::Base. It can optionally accept a hash of options and set them on Sinatra::Base. 194 | 195 | ```ruby 196 | # Run the Sinatra app as a self-hosted server using 197 | # Thin, Mongrel or WEBrick (in that order) 198 | def run!(options={}) 199 | set options 200 | handler = detect_rack_handler 201 | handler_name = handler.name.gsub(/.*::/, '') 202 | puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " + 203 | "on #{port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i 204 | handler.run self, :Host => bind, :Port => port do |server| 205 | [:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } } 206 | set :running, true 207 | end 208 | rescue Errno::EADDRINUSE => e 209 | puts "== Someone is already performing on port #{port}!" 210 | end 211 | ``` 212 | 213 | Then it tries to get a rack compatible server to run the app by calling `detect_rack_handler`. detect_rack_handler uses either the default array defined by `set :server, %w[thin mongrel webrick]`, or the server option passed in by arguments when running the app, as the parameter to `Rack::Handler.get`. A set of server handlers are predefined by rack to abstract the difference of servers so any rack server can be just run by calling `some_handler.run(myapp)`. You can also define your customized server handler. We will see an example of handler below. As soon as a server handler is found Rack::Handler.get will return it. Assuming we are running `Rack::Handler.get('thin')` and let's see what does it do. 214 | 215 | ```ruby 216 | def detect_rack_handler 217 | servers = Array(server) 218 | servers.each do |server_name| 219 | begin 220 | return Rack::Handler.get(server_name.downcase) 221 | rescue LoadError 222 | rescue NameError 223 | end 224 | end 225 | fail "Server handler (#{servers.join(',')}) not found." 226 | end 227 | ``` 228 | 229 | @handlers is a hash contains all the server handlers defined by rack. Handlers are added to @handlers by the register method. 230 | 231 | ```ruby 232 | def self.get(server) 233 | return unless server 234 | server = server.to_s 235 | 236 | if klass = @handlers[server] 237 | obj = Object 238 | klass.split("::").each { |x| obj = obj.const_get(x) } 239 | obj 240 | else 241 | try_require('rack/handler', server) 242 | const_get(server) 243 | end 244 | end 245 | 246 | def self.register(server, klass) 247 | @handlers ||= {} 248 | @handlers[server] = klass 249 | end 250 | ``` 251 | 252 | Following is how register is called and a list of all handlers 253 | 254 | ```ruby 255 | register 'cgi', 'Rack::Handler::CGI' 256 | register 'fastcgi', 'Rack::Handler::FastCGI' 257 | register 'mongrel', 'Rack::Handler::Mongrel' 258 | register 'emongrel', 'Rack::Handler::EventedMongrel' 259 | register 'smongrel', 'Rack::Handler::SwiftipliedMongrel' 260 | register 'webrick', 'Rack::Handler::WEBrick' 261 | register 'lsws', 'Rack::Handler::LSWS' 262 | register 'scgi', 'Rack::Handler::SCGI' 263 | register 'thin', 'Rack::Handler::Thin' 264 | ``` 265 | 266 | In the case of Rack::Handler.get('thin'), `@handlers[server]` is the string `'Rack::Handler::Thin'`. `klass.split("::").each { |x| obj = obj.const_get(x) }` loop through modules Rack to Handler and then to Thin class in `rack/handler/thin.rb`, which is defined as following: 267 | 268 | ```ruby 269 | require "thin" 270 | require "rack/content_length" 271 | require "rack/chunked" 272 | 273 | module Rack 274 | module Handler 275 | class Thin 276 | def self.run(app, options={}) 277 | server = ::Thin::Server.new(options[:Host] || '0.0.0.0', 278 | options[:Port] || 8080, 279 | app) 280 | yield server if block_given? 281 | server.start 282 | end 283 | end 284 | end 285 | end 286 | ``` 287 | 288 | The thin handler is a class and it has a single class method `run`. We pass `self`, which is Sinatra::Application, to the `run` method; remember in `at_exit` method `run!` is called with `Application.run!`, hence `self` here is Sinatra::Application. When `Rack::Handler::Thin.run` is called it creates a new server instance with the app we passed in as the parameter, yield to the app and let it do something, and starts the thin server with `server.start`. 289 | 290 | Let's return to the `run!` method on Sinatra::Base. It outputs the information about the server it got and calls the `run` method on the handler, passing in the default binding(0.0.0.0) and port(4567) and a block. Inside the block, we specify that two signal that can end the server by calling the `quit!` method on Sinatra::Base, and then set the `running` to true which indicate the server is running. Then the control returns to run method on the Thin handler. It starts the server instance for handling requests using our app! 291 | 292 | ```ruby 293 | handler.run self, :Host => bind, :Port => port do |server| 294 | [:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } } 295 | set :running, true 296 | end 297 | ``` 298 | 299 | ```ruby 300 | def quit!(server, handler_name) 301 | # Use Thin's hard #stop! if available, otherwise just #stop. 302 | server.respond_to?(:stop!) ? server.stop! : server.stop 303 | puts "\n== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i 304 | end 305 | ``` 306 | 307 | To sum up the sinatra DSL is available to our app through method delegation to Sinatra::Application. The server is started by passing Sinatra::Application to a rack server. Although we define routes in our app, everything happens in Sinatra::Application. 308 | 309 | This concludes our first tutorial. There are still some topics need to be talked about the server, like how requests are picked up by the server and passed to our app. We will resolve this in later tutorials. In tutorial_2.rb, we will look at high level architecture of sinatra apps and the code that support it, including other forms of sinatra apps, sinatra extensions and middleware. -------------------------------------------------------------------------------- /app/tutorial_2/tutorial_2.md: -------------------------------------------------------------------------------- 1 | In this tutorial, we will look at different styles of sinatra app, sinatra extension system, and sinatra middlware. You may find that the sections "Rack Middleware", "Sinatra::Base - Middleware, Libraries, and Modular Apps" and "Scopes and Binding" in sinatra README may help to understand the topic. 2 | 3 | In tutorial_1 we learned the basic form of sinatra apps: require the sinatra library and define routes directly in the same file. This is referred to as the "classic" style (see classic.rb). The other style is called "modular" style (see modular.rb). 4 | 5 | Let's see an example of a modular app. In modular.rb, first Sinatra::Base is imported by `require 'sinatra/base'`. We define our app inside the Modular class, which is a subclass of Sinatra::Base. When subclassing `Sinatra::Base.inherited` is triggered and it calls the `subclass.reset!`. Note `subclass.reset!` is calling the inherited `Sinatra::Base.reset!`. Even the subclass has its own reset! method it won't be called because the content of the subclass is empty at this point. The `inherited` method then calls `super` which triggers the `inherited` method on the super class if there is one. In the case of our Modular class, it inherits from Sinatra::Base, which doesn't inherit from other classes, so the super in the `inherited` method does not do anything. If another app Modular2 inherits from class AnotherClass that in turn inherits from Sinatra::Base, then the `inherits` method on Sinatra::Base would first reset AnotherClass; after AnotherClass is defined, Modular2 is reset by calling AnotherClass.reset!. 6 | 7 | ```ruby 8 | def inherited(subclass) 9 | subclass.reset! 10 | super 11 | end 12 | ``` 13 | 14 | Now let's see what does `reset!` do. As the name suggests it resets everything and make current app a blank state. This is important because Sinatra::Base can be the super class of a number of modular apps or middleware. We know the `set` method defines settings on the app's singleton class, so settings are unique for each app; however, as we will see in the next tutorial, instance variables like @routes are defined on Sinatra::Base's singleton class, 15 | so multiple apps subclassing from Sinatra::Base may share states, which we don't want. We will explain what those instance variables mean in later tutorials. For now knowing `reset!` empty them is enough. 16 | 17 | ```ruby 18 | attr_reader :routes, :filters, :templates, :errors 19 | 20 | # Removes all routes, filters, middleware and extension hooks from the 21 | # current class (not routes/filters/... defined by its superclass). 22 | def reset! 23 | @conditions = [] 24 | @routes = {} 25 | @filters = {:before => [], :after => []} 26 | @errors = {} 27 | @middleware = [] 28 | @prototype = nil 29 | @extensions = [] 30 | 31 | if superclass.respond_to?(:templates) 32 | @templates = Hash.new { |hash,key| superclass.templates[key] } 33 | else 34 | @templates = {} 35 | end 36 | end 37 | ``` 38 | 39 | We continue to look at the route definition in modular.rb. We define routes as instance methods inside the Modular class. It makes perfect sense because the route definition methods like `get` are defined as class methods on Sinatra::Base, so they are available in the class scope on the Modular. 40 | 41 | Next let's see how to start a modular app. There are two ways. First we can use the `run!` method as we talked in tutorial_1 and just throw it in after the routes like this: 42 | 43 | ```ruby 44 | require 'sinatra/base' 45 | class Modular < Sinatra::Base 46 | get '/' do 47 | 'Hello world!' 48 | end 49 | run! 50 | end 51 | ``` 52 | 53 | Then `run!` will fire up a rack handler by calling its run method and pass in `self`, i.e. Sinatra::Base. 54 | 55 | Second, we can use a ru file to start it. A ru file is also called the rackup file that is used to configure for example the rack middlware, mapping url to rack endpoints, and start the rack server etc. We will just use the very basic ru file for now. If you look at modular.ru, we just require the modular app we defined and call `Rack::Builder.run` with Sinatra::Base or Modular as the parameter. 56 | 57 | ```ruby 58 | require File.expand_path(File.dirname(__FILE__) + '/modular') 59 | run Sinatra::Base 60 | # or run Modular 61 | ``` 62 | 63 | Of course we can have a ru file for classic sinatra app like classic.ru. 64 | 65 | Here it's a bit different from how we run a regular rack app. Normally a rack app is a class that has an instance method `call`. handler would expect an instance of a rack app; when we run the rack app, we make an instance of the class and run it like this `run SomeRackApp.new`. In ru file we run the class like run Modular instead of the instance of the class. We will see why is that later in this tutorial. 66 | 67 | Now we finish the definition of a modular app, and our conclusion is that the modular style apps have nothing to do with the Sinatra::Application. A modular app is self-contained in its own scope. As a contrast the classic style app delegates its calls to Sinatra::Application, the subclass of Sinatra::Base. 68 | 69 | There is a third way of defining a sinatra app. Sinatra.new overrides Object.new. It takes a base class , and a options hash, and a block as parameters. It looks like the options hash is never used though. The base defaults to Sinatra::Base, which makes the app a modular app, and also can be Sinatra::Application, which makes the app a classic app. Nothing special with this form, just a syntactic sugar. 70 | 71 | ```ruby 72 | def self.new(base=Base, options={}, &block) 73 | base = Class.new(base) 74 | base.class_eval(&block) if block_given? 75 | base 76 | end 77 | ``` 78 | 79 | Since we are here let's look at other class methods defined on the Sinatra module: 80 | 81 | ```ruby 82 | # Extend the top-level DSL with the modules provided. 83 | def self.register(*extensions, &block) 84 | Delegator.target.register(*extensions, &block) 85 | end 86 | 87 | # Include the helper modules provided in Sinatra's request context. 88 | def self.helpers(*extensions, &block) 89 | Delegator.target.helpers(*extensions, &block) 90 | end 91 | 92 | # Use the middleware for classic applications. 93 | def self.use(*args, &block) 94 | Delegator.target.use(*args, &block) 95 | end 96 | ``` 97 | 98 | They are just convenient methods that are delegating the `Sinatra.register`, `Sinatra.helpers` and `Sinatra.use` methods to the classic form app. 99 | 100 | As we discussed Sinatra::Application is split in two files. Let's list the full Sinatra::Application code here. Following code is in sinatra/lib/sinatra/main.rb, which we already discussed in detail in tutorial_1. 101 | 102 | ```ruby 103 | module Sinatra 104 | class Application < Base 105 | 106 | # we assume that the first file that requires 'sinatra' is the 107 | # app_file. all other path related options are calculated based 108 | # on this path by default. 109 | set :app_file, caller_files.first || $0 110 | 111 | set :run, Proc.new { $0 == app_file } 112 | 113 | if run? && ARGV.any? 114 | require 'optparse' 115 | OptionParser.new { |op| 116 | op.on('-x') { set :lock, true } 117 | op.on('-e env') { |val| set :environment, val.to_sym } 118 | op.on('-s server') { |val| set :server, val } 119 | op.on('-p port') { |val| set :port, val.to_i } 120 | op.on('-o addr') { |val| set :bind, val } 121 | }.parse!(ARGV.dup) 122 | end 123 | end 124 | 125 | at_exit { Application.run! if $!.nil? && Application.run? } 126 | end 127 | ``` 128 | 129 | Following code is in sinatra/lib/sinatra/base.rb. Let's look at it in detail. `set :logging, Proc.new { ! test? }` determines whether or not to do logging based on result of the test? method. Note that development?, test?, production? are methods defined on Sinatra::Base and are delegated in the classic style sinatra apps. 130 | 131 | ```ruby 132 | # Execution context for classic style (top-level) applications. All 133 | # DSL methods executed on main are delegated to this class. 134 | # 135 | # The Application class should not be subclassed, unless you want to 136 | # inherit all settings, routes, handlers, and error pages from the 137 | # top-level. Subclassing Sinatra::Base is heavily recommended for 138 | # modular applications. 139 | class Application < Base 140 | set :logging, Proc.new { ! test? } 141 | set :method_override, true 142 | set :run, Proc.new { ! test? } 143 | 144 | def self.register(*extensions, &block) #:nodoc: 145 | added_methods = extensions.map {|m| m.public_instance_methods }.flatten 146 | Delegator.delegate(*added_methods) 147 | super(*extensions, &block) 148 | end 149 | end 150 | ``` 151 | 152 | The definitions of development?, test?, production? are pretty simple. The environment is another setting `set :environment, (ENV['RACK_ENV'] || :development).to_sym`, which will default to 'development' if ENV['RACK_ENV'] is not set. 153 | 154 | ```ruby 155 | def development?; environment == :development end 156 | def production?; environment == :production end 157 | def test?; environment == :test end 158 | ``` 159 | 160 | `set :method_override, true` will determine whether the sinatra app will use `Rack::MethodOverride` as a middleware. What Rack::MethodOverride does is just detect the _method param passed in by browsers to support HTTP method like PUT and DELETE. 161 | 162 | `set :run, Proc.new { ! test? }` defines the run setting. As we have seen in sinatra/lib/sinatra/main.rb the line `set :run, Proc.new { $0 == app_file }` has already set the `run` setting; the run setting is set twice in two spots. And why Sinatra::Application is separated in two files in the first place? Users can do something like in classic_2.rb, which is also a classic sinatra app. The difference than classic.rb is that in classic_2.rb you have to do `include Sinatra::Delegator` and run the server by calling `Sinatra::Application.run!` explicitly. Let's look back at the Sinatra::Application in sinatra/lib/sinatra/main.rb. What it does is just parsing the command line arguments and run the server. So we can think sinatra/lib/sinatra/main.rb is just a convenient way of defining a sinatra app and get it running. Back to our original question, the reason that `run` is set twice is that they are used in different context. The `set :run, Proc.new { ! test? }` will be overridden if sinatra/lib/sinatra/main.rb is required after sinatra/lib/sinatra/base.rb, and by setting the `run` as true if it's not test environment, it will prevent another classic sinatra app from running. 163 | 164 | Now let's see the `Sinatra::Applocation.register`. To summarize what it does, it gets the all public instance methods of the extension array passed to it and delegate them to the top level, i.e. defines instance methods on top level and delegate them to Sinatra::Application. Then it calls the `register` method of its super class Sinatra::Base shown below. 165 | 166 | ```ruby 167 | # Register an extension. Alternatively take a block from which an 168 | # extension will be created and registered on the fly. 169 | def register(*extensions, &block) 170 | extensions << Module.new(&block) if block_given? 171 | @extensions += extensions 172 | extensions.each do |extension| 173 | extend extension 174 | extension.registered(self) if extension.respond_to?(:registered) 175 | end 176 | end 177 | ``` 178 | 179 | The register method on Sinatra::Base basically extends all the extensions, i.e. add the instance methods of the extensions as class methods to Sinatra::Base. If any blocks are passed in to the register method, the methods defined inside the blocks are also added as class methods to Sinatra::Base. It then calls the `registered` method on each of the extensions as sort of callbacks. Note the self i.e. the current class is passed to the `registered` method. So if an extension defines a `registered` class method, it can do something with the current app like set some settings, define some routes etc. 180 | 181 | A sinatra app uses an @extension instance variable to store all the extensions that are used in the current app. `extension` method gets all its super classes' extensions including its own extensions, which means all extensions used by super classes are added as class methods to the current app. 182 | 183 | ```ruby 184 | # Extension modules registered on this class and all superclasses. 185 | def extensions 186 | if superclass.respond_to?(:extensions) 187 | (@extensions + superclass.extensions).uniq 188 | else 189 | @extensions 190 | end 191 | end 192 | ``` 193 | 194 | Let's see what a sinatra extension looks like. A sinatra extension is just a module with sinatra DSL available to it. I take an example from sinatra documentation. 195 | 196 | ```ruby 197 | require 'sinatra/base' 198 | 199 | module Sinatra 200 | module LinkBlocker 201 | def block_links_from(host) 202 | before { 203 | halt 403, "Go Away!" if request.referer.match(host) 204 | } 205 | end 206 | end 207 | 208 | register LinkBlocker 209 | end 210 | ``` 211 | 212 | This is how to use it in a classic sinatra app: 213 | 214 | ```ruby 215 | require 'sinatra' 216 | require 'sinatra/linkblocker' 217 | 218 | block_links_from 'digg.com' 219 | 220 | get '/' do 221 | "Hello World" 222 | end 223 | ``` 224 | 225 | We can see that the register method in the extension will be evaluated when the extension is required by `require 'sinatra/linkblocker'`, and when it's required it will define `block_links_from` on the top level, and also define `block_links_from` as class methods on Sinatra::Base. 226 | 227 | This is how to use it in a modular sinatra app: 228 | 229 | ```ruby 230 | require 'sinatra/base' 231 | require 'sinatra/diggblocker' 232 | 233 | class Hello < Sinatra::Base 234 | register Sinatra::LinkBlocker 235 | 236 | block_links_from 'digg.com' 237 | 238 | get '/' do 239 | "Hello World" 240 | end 241 | end 242 | ``` 243 | 244 | Here the `regisiter` method in the extension doesn't have effect to the modular app in that they are not in the same scope. So the modular app calls the register method on Sinatra::Base which defines `block_links_from` as class methods on Sinatra::Base. 245 | 246 | Now we know the `register` and extensions, it's easier to understand a similar concept sinatra Helpers. 247 | 248 | ```ruby 249 | # Makes the methods defined in the block and in the Modules given 250 | # in `extensions` available to the handlers and templates 251 | def helpers(*extensions, &block) 252 | class_eval(&block) if block_given? 253 | include(*extensions) if extensions.any? 254 | end 255 | ``` 256 | 257 | As you may already guess `helpers` just add the instance methods on extensions as well as the methods defined in the block passed to helpers method call as instance methods to the current app so that they can be used in routing handlers, filters, templates and other helpers etc. 258 | 259 | Let's explore the question we just asked: why we do `run Modular` instead of `run Modular.new` in the ru file. Let's see how a sinatra app acts as a rack app. By rack app I mean the class that defines the rack app. We know an instance of rack app responds to call method. Take our modular.rb as an example, rack handler would expect Modular.new responds to `call`. There are several call methods defined on Sinatra::Base. First one is an instance method that is used as the regular rack interface. It duplicates the instance of current app and call the `call!` method, which is the actual place requests are routed and response is generated. `call!` is a rather long method and we will explain it in detail in later tutorials. So our app does have a `call` instance method. 260 | 261 | ```ruby 262 | # Rack call interface. 263 | def call(env) 264 | dup.call!(env) 265 | end 266 | ``` 267 | 268 | Before we continue, why the current app needs to be duplicated before routes are processed and response is generated? We know `dup` method make a copy of all instance variables, so apparently we are trying to avoid messing up the instance variables here. How instance variables can be possibly messed up? 269 | 270 | I am not really sure at this time. This is when tests may help to figure out. So I remove the dup and run the test with `rack test`. The modified `call` method is like this: 271 | 272 | ```ruby 273 | def call(env) 274 | call!(env) 275 | end 276 | ``` 277 | 278 | There are two failures: 279 | 280 | 1) Failure: 281 | test_does_not_maintain_state_between_requests(BaseTest::TestSinatraBaseSubclasses) [/Users/zjia/code/sinatra-explained/sinatra/test/base_test.rb:42]: 282 | <"Foo: new"> expected but was 283 | <"Foo: discard">. 284 | 285 | 2) Failure: 286 | test_allows_custom_route_conditions_to_be_set_via_route_options(RoutingTest) [/Users/zjia/code/sinatra-explained/sinatra/test/routing_test.rb:941]: 287 | Failed assertion, no message given. 288 | 289 | Let's just look at the first failure. The last assertation in base_test.rb failed: 290 | 291 | ```ruby 292 | it 'does not maintain state between requests' do 293 | request = Rack::MockRequest.new(TestApp) 294 | 2.times do 295 | response = request.get('/state') 296 | assert response.ok? 297 | assert_equal 'Foo: new', response.body 298 | end 299 | end 300 | ``` 301 | 302 | This is how TestApp defined: 303 | 304 | ```ruby 305 | class TestApp < Sinatra::Base 306 | get '/state' do 307 | @foo ||= "new" 308 | body = "Foo: #{@foo}" 309 | @foo = 'discard' 310 | body 311 | end 312 | end 313 | ``` 314 | 315 | The failure is because the @foo instance variable is shared between two requests. On the first request, @foo is assigned to "discard" after the request is processed; on the second request, since @foo has a value, it's not assigned to "new" again. Now we know the cause of the failure, it's clear that the `dup` method makes sure that each request has its own set of instance variables. 316 | 317 | There is another `call` class method on Sinatra::Base. Remember in the ru file, we run the app by something like `run Sinatra::Base`. The rack handler actually calls this `call` method. 318 | 319 | ```ruby 320 | def call(env) 321 | synchronize { prototype.call(env) } 322 | end 323 | ``` 324 | 325 | It uses the `synchronize` method on Sinatra::Base. Mutex is imported by `require 'thread'`. We make an instance of Mutex as a class variable. The reason is that class variable is inherited by subclasses so all of them share the same @@mutex, which ensures that only one lock exists on the class hierarchy. 326 | 327 | ```ruby 328 | @@mutex = Mutex.new 329 | def synchronize(&block) 330 | if lock? 331 | @@mutex.synchronize(&block) 332 | else 333 | yield 334 | end 335 | end 336 | ``` 337 | 338 | We can see that if the lock? setting is true, then it will use Mutex#synchronize method to place a lock on every request to avoid race conditions among threads. If your sinatra app is multithreaded and not thread safe, or any gems you use is not thread safe, you would want to do `set :lock, true` so that only one request is processed at a given time. I don't have a good example for demonstration at the moment. Otherwise by default `lock` is false, which means the `synchronize` would yield to the block directly. 339 | 340 | Inside the block, the class method call uses `prototype` method. 341 | 342 | ```ruby 343 | # The prototype instance used to process requests. 344 | def prototype 345 | @prototype ||= new 346 | end 347 | ``` 348 | 349 | Inside the `prototype` method it calls the `new` method if our app isn't already initialized. The `Sinatra::Base.new` uses the `build` method to initialize a middleware stack that is used to process requests. 350 | 351 | ```ruby 352 | # Create a new instance of the class fronted by its middleware 353 | # pipeline. The object is guaranteed to respond to #call but may not be 354 | # an instance of the class new was called on. 355 | def new(*args, &bk) 356 | build(*args, &bk).to_app 357 | end 358 | ``` 359 | 360 | We can see the build method first initializes a Rack::Builder. 361 | 362 | ```ruby 363 | # Creates a Rack::Builder instance with all the middleware set up and 364 | # an instance of this class as end point. 365 | def build(*args, &bk) 366 | builder = Rack::Builder.new 367 | builder.use Rack::MethodOverride if method_override? 368 | builder.use ShowExceptions if show_exceptions? 369 | setup_logging builder 370 | setup_sessions builder 371 | middleware.each { |c,a,b| builder.use(c, *a, &b) } 372 | builder.run new!(*args, &bk) 373 | builder 374 | end 375 | ``` 376 | 377 | To understand what the build method does, I list an abridged version of Rack::Builder here all at once. 378 | 379 | ```ruby 380 | module Rack 381 | 382 | class Builder 383 | 384 | def initialize(&block) 385 | @ins = [] 386 | instance_eval(&block) if block_given? 387 | end 388 | 389 | def self.app(&block) 390 | self.new(&block).to_app 391 | end 392 | 393 | def use(middleware, *args, &block) 394 | @ins << lambda { |app| middleware.new(app, *args, &block) } 395 | end 396 | 397 | def run(app) 398 | @ins << app #lambda { |nothing| app } 399 | end 400 | 401 | def map(path, &block) 402 | if @ins.last.kind_of? Hash 403 | @ins.last[path] = self.class.new(&block).to_app 404 | else 405 | @ins << {} 406 | map(path, &block) 407 | end 408 | end 409 | 410 | def to_app 411 | @ins[-1] = Rack::URLMap.new(@ins.last) if Hash === @ins.last 412 | inner_app = @ins.last 413 | @ins[0...-1].reverse.inject(inner_app) { |a, e| e.call(a) } 414 | end 415 | 416 | end 417 | end 418 | ``` 419 | 420 | As we know middlewares are in stack and process requests in layers. When Rack::Builder is initialized, it assigns an empty array to the instance variable @ins. If any block is given it's also evaluated on the Rack::Builder instance. Next if the setting `method_override?` is true then our app will use Rack::MethodOverride middleware by calling `builder.use Rack::MethodOverride`. By default, in classic form sinatra app the `method_override?` is enabled, while in modular form sinatra app, the setting is disabled. The `use` method basically wraps the `middleware.new` in a proc and lazy evaluates the initialization of the middleware it uses. If any arguments or block are passed to the `use`, it will be passed to the middleware initialization process. Then if the `show_exceptions?` setting is true then we use the ShowExceptions middleware defined in `sinatra/lib/sinatra/showexceptions.rb`. By default :show_exceptions is true in development mode. 421 | 422 | Note here `builder.use ShowExceptions if show_exceptions?` is calling Rack::Builder#use. There is also a `use` method on Sinatra::Base 423 | 424 | ```ruby 425 | def use(middleware, *args, &block) 426 | @prototype = nil 427 | @middleware << [middleware, args, block] 428 | end 429 | ``` 430 | 431 | So Sinatra::Base.use collects an array of [middleware, args, block] and store it in @middleware. 432 | 433 | Then we come to this line: `middleware.each { |c,a,b| builder.use(c, *a, &b) }`. For each of the middleware in the @middleware we call Rack::Builder#use to use it. The question is instead of using Rack::Builder#use directly, why do we have an additional step? This is because when we use a new middleware in our sinatra app we want to re-initialize our app so the middleware stack can be rebuilt without restarting the app. 434 | 435 | If the logging setting is true, then it will use the Rack::CommonLogger middleware to generates logs. Further if a logging level is given in the logging setting it will be used to set `env['rack.logger']` 436 | 437 | ```ruby 438 | def setup_logging(builder) 439 | if logging? 440 | builder.use Rack::CommonLogger 441 | if logging.respond_to? :to_int 442 | builder.use Rack::Logger, logging 443 | else 444 | builder.use Rack::Logger 445 | end 446 | else 447 | builder.use Rack::NullLogger 448 | end 449 | end 450 | ``` 451 | 452 | setup_sessions just uses the Rack::Session::Cookie middleware if sessions setting is enabled. 453 | 454 | ```ruby 455 | def setup_sessions(builder) 456 | return unless sessions? 457 | options = { :secret => session_secret } 458 | options.merge! sessions.to_hash if sessions.respond_to? :to_hash 459 | builder.use Rack::Session::Cookie, options 460 | end 461 | ``` 462 | 463 | Next `builder.run new!(*args, &bk)` calls the `new!` method, which is an alias method of original `new` method. It just create an instance of the current app without building the middleware stack. So the parameter passed to `builder.run` is an instance of of our app. 464 | 465 | ```ruby 466 | # Create a new instance without middleware in front of it. 467 | alias new! new unless method_defined? :new! 468 | ``` 469 | 470 | The `run` method just adds our app instance to the @ins array, and then it returns the `builder` variable containing the @ins array to the `Sinatra::Base.new` method. `Sinatra::Base.new` calls `to_app` on the returned `builder` to build the middleware calling stack using the @ins array. Here is how `to_app` works. Suppose we have the @ins has middleware proc array [m1, m2, m3]. It first check whether the last element of the @ins array, i.e., our app instance is a hash. Let's assume it's not for now. We will see a bit later how it can be a hash. If it's not a hash, we just get the last element as the inner_app, and for the remaining middleware, we do `@ins[0...-1].reverse.inject(inner_app) { |a, e| e.call(a) }`. What does this do is reversing the middleware sequence, and generating a call something like m1.call(m2.call(m3.call(inner_app))). When this is executed, middleware are initialized in sequence, setting their inner app, and the outermost middleware instance is returned. We can see example outputs of Sinatra::Base.build and Sinatra::Base.new in middleware_stack.rb. 471 | 472 | Now let's briefly see how the last element of @ins can be a hash. If in the ru file we have something like 473 | 474 | ```ruby 475 | use Middleware1 476 | Rack::Builder.app do 477 | map '/' do 478 | use Middleware2 479 | run Heartbeat 480 | end 481 | end 482 | ``` 483 | 484 | The `Rack::Builder.app` take a block and initialize a Rack::Builder instance, evaluate the block on Rack::Builder, and convert the Rack::Builder instance to a middleware stack with the `to_app` method. Let's see the `Rack::Builder#map` method inside the block. It takes a path parameter and a block. If first check whether the last element is a hash. In our case it is not. If it's not it will make add an empty hash as the last element of @ins, and then call itself `map(path, &block)`. Now the last element is a hash, so it will key on the path parameter and the value is a middleware stack by evaluating the block on Rack::Builder and call `to_app`. 485 | 486 | Back to the ru file, it uses Middleware1 at the top, and the remaining is a just a hash. Then back to the `to_app` method. If the last element of @ins is a hash, it will initialize a Rack::URLMap, which basically does the routing directly in the ru file based on the key of the hash, i.e. the path parameter. 487 | 488 | In conclusion the `builder` method ends up with an array with an instance of current app as the last element; `Sinatra::Base.new` ends up with a middleware stack, and the Sinatra::Base.call ends up to `Sinatra::Base#call`. 489 | 490 | In the next tutorial, let's see how routing is done. -------------------------------------------------------------------------------- /app/tutorial_3/tutorial_3.md: -------------------------------------------------------------------------------- 1 | The question we are going to look at in this tutorial is: how routing is handled in Sinatra. First we need a little background on routing conditions. A routing condition means that a route is only picked if the condition is met. For example, we have a route like: 2 | 3 | ```ruby 4 | get '/', :host_name => /^admin\./ do 5 | "Admin Area, Access denied!" 6 | end 7 | ``` 8 | 9 | We can see we use a hash `:host_name => /^admin\./` to define a condition. `host_name` is actually a method and we use the name of the method as the key a regular expression that represents a route as the value. The route '/' will only be picked if the host name starts with "admin". You can refer to the "Conditions" section in the sinatra README for further information. Don't worry about the meaning of the `host_name` condition. You just need to know the concept of routing conditions. We will explain host_name below in this tutorial. 10 | 11 | Quite a few related methods are covered in this tutorial and routing itself is rather complex. Now let's get started. We use the same sinatra app as in tutorial_1/classic.rb. 12 | 13 | We know `get '/'` defines a route to the root path. Let's see the get method: 14 | 15 | ```ruby 16 | # Defining a `GET` handler also automatically defines a `HEAD` handler. 17 | def get(path, opts={}, &block) 18 | conditions = @conditions.dup 19 | route('GET', path, opts, &block) 20 | @conditions = conditions 21 | route('HEAD', path, opts, &block) 22 | end 23 | ``` 24 | 25 | The get method takes a path, an optional condition hash and a block. The questions we have with this method are: 1. what is the instance variable @conditions? 2. why it's duplicated(@conditions.dup) and then set back(on line 4)? 3. what does the `route` method do? 26 | 27 | Let's grep on @conditions, and we find the following `condition` method relevant. The `condition` method takes a block as a proc and add it to @conditions, an array of procs. With this information, although we don't know exactly how, we can guess that `route('GET', path, opts, &block)` will modify @conditions, and we want to use the same @conditions that we used to for 'GET' to define 'HEAD', so we need to set it back. Initially both in classic and modular sinatra app, the @conditions is set to an empty array by `Sinatra::Base.reset!`. 28 | 29 | ```ruby 30 | # Add a route condition. The route is considered non-matching when the block returns false. 31 | def condition(&block) 32 | @conditions << block 33 | end 34 | ``` 35 | 36 | Similarly other http methods are defined. 37 | 38 | ```ruby 39 | def put(path, opts={}, &bk) route 'PUT', path, opts, &bk end 40 | def post(path, opts={}, &bk) route 'POST', path, opts, &bk end 41 | def delete(path, opts={}, &bk) route 'DELETE', path, opts, &bk end 42 | def head(path, opts={}, &bk) route 'HEAD', path, opts, &bk end 43 | def options(path, opts={}, &bk) route 'OPTIONS', path, opts, &bk end 44 | def patch(path, opts={}, &bk) route 'PATCH', path, opts, &bk end 45 | ``` 46 | 47 | `route` is a private instance method of Sinatra::Base. It takes a HTTP verb("GET", "POST" etc) and the same parameters passed to the get method: 48 | 49 | ```ruby 50 | def route(verb, path, options={}, &block) 51 | # Because of self.options.host 52 | host_name(options.delete(:host)) if options.key?(:host) 53 | enable :empty_path_info if path == "" and empty_path_info.nil? 54 | 55 | block, pattern, keys, conditions = compile! verb, path, block, options 56 | invoke_hook(:route_added, verb, path, block) 57 | 58 | (@routes[verb] ||= []). 59 | push([pattern, keys, conditions, block]).last 60 | end 61 | ``` 62 | 63 | Before we delve into the route method, let's look at the methods it uses. 64 | 65 | `Sinatra::Base.host_name` is a private method that defines a routing condition by using the `condition` method we just discussed. If you look at the source code annotation for the condition method: "The route is considered non-matching when the block returns false", we can know that if the block { pattern === request.host } returns true, then the condition is considered satisfied, and vice versa. In the block, it references request, which is an attr_accessor on Sinatra::Base, and an instance of the `Sinatra::Request < Rack::Request`. We will look at Sinatra::Request in detail in other tutorials. `request.host` is the host part without port number of user's requested url. As a simple example, if we specify a host option in the get like `get '/', :host => 'test.smokyapp.com'`, it will only match the route '/' if the request is something like http://test.smokyapp.com. It will not match '/' if the request is http://test2.smokyapp.com. So when `host_name` is called with a regular expression as the path pattern, proc{ pattern === request.host } will be added to the @conditions. 66 | 67 | ```ruby 68 | # Condition for matching host name. Parameter might be String or Regexp. 69 | def host_name(pattern) 70 | condition { pattern === request.host } 71 | end 72 | ``` 73 | 74 | Next is the enable method. As we have seen in tutorial 1, it's a class method in Sinatra::Base and is delegated in Sinatra::Delegator. It's just a convenient method to the `set` method that sets an array of settings as true. 75 | 76 | ```ruby 77 | # Same as calling `set :option, true` for each of the given options. 78 | def enable(*opts) 79 | opts.each { |key| set(key, true) } 80 | end 81 | ``` 82 | 83 | One interesting thing to note is that in sinatra/sinatra.rb, `enable :inline_templates` is called so it defines `inline_templates=` on the singleton class of Sinatra::Base, but Sinatra::Base also defines `inline_templates=` class method itself. As we know class methods are actually methods defined on class's singleton class. The `inline_templates=` defined by Sinatra::Base will overwrite the one defined by `enable :inline_templates`. 84 | 85 | Now we know what the enable method does, we come back to the route method. We've already seen host_name defines a routing condition. Then it calls `enable :empty_path_info`, i.e., set empty_path_info to true to if the path param is an empty string and if `empty_path_info` setting is not already true. Note `set :empty_path_info, nil` is called Sinatra::Base's class definition, so by default empty_path_info is nil. empty_path_info is set to true the first time you give an empty string as the path param. Then as we can see later when routing is processed if empty_path_info is true it will use '/' as the route. 86 | 87 | Next the Sinatra::Base.compile! is called with all the params passed to route. 88 | 89 | ```ruby 90 | def compile!(verb, path, block, options = {}) 91 | options.each_pair { |option, args| send(option, *args) } 92 | method_name = "#{verb} #{path}" 93 | 94 | define_method(method_name, &block) 95 | unbound_method = instance_method method_name 96 | pattern, keys = compile(path) 97 | conditions, @conditions = @conditions, [] 98 | remove_method method_name 99 | 100 | [ block.arity != 0 ? 101 | proc { unbound_method.bind(self).call(*@block_params) } : 102 | proc { unbound_method.bind(self).call }, 103 | pattern, keys, conditions ] 104 | end 105 | ``` 106 | 107 | You may wonder what the `options.each_pair` does. For each element of the option hash, i.e. the condition hash like `:host_name => /^admin\./`, it calls the method with the key as the method name and the value as the parameters to the method. It turns out it's one usage of the `set` method. Take an example from Sinatra doc: 108 | 109 | ```ruby 110 | set(:probability) { |value| condition { rand <= value } } 111 | get '/win_a_car', :probability => 0.1 do 112 | "You won!" 113 | end 114 | ``` 115 | 116 | Here we first use set to define a routing condition named probability. A method named `probability` is defined on the singleton class on Sinatra::Base. When we pass the :probability => 0.1 as the option to `get`, 0.1 is passed in as the value parameter to the block, and then a condition is set by adding the { rand <= 0.1 } to the @conditions. Using set with a block that contains a condition makes the condition reusable. 117 | 118 | Next it defines a method with method names like "GET /" and with the method body as the block passed in. The method is defined as class methods on Sinatra::Base. The line unbound_method = instance_method method_name is interesting. Before I see it I only know instance_method can extract an unbound method from a class's instance methods. But it actually just finds and extracts a method from the current scope, no matter it's an instance method or a class methods. Here we extract the method that's just defined to the local variable unbound_method. The method is later removed with remove_method method_name. Before we look into why it does that, let's first look at the `compile` method. The `compile(path)` returns an array with two elements path and keys. As we can see path and keys are returned from the `compile` method and are stored as part of the route information. Let's see what does `compile` do in detail. 119 | 120 | According to the method, the path can be of 4 forms: it responds to to_str, indicating it's a string, responds to keys and match, responds to names and match, and responds to match only, indicating it's a regular expression. 121 | 122 | First let's look at the most common case where path is a string. To know what does the compile method do, it's good to see some examples first. We know compile accepts a path param, and return array of pattern and keys. Let's pop up irb and see four examples: 123 | 124 | ruby-1.9.2-p180 :002 > Sinatra::Base.send(:compile, "/") 125 | => [/^\/$/, []] 126 | ruby-1.9.2-p180 :003 > Sinatra::Base.send(:compile, "/a*") 127 | => [/^\/a(.*?)$/, ["splat"]] 128 | ruby-1.9.2-p180 :004 > Sinatra::Base.send(:compile, "/a/:boo") 129 | => [/^\/a\/([^\/?#]+)$/, ["boo"]] 130 | ruby-1.9.2-p180 :005 > Sinatra::Base.send(:compile,"/a/:boo/*.pdf") 131 | => [/^\/a\/([^\/?#]+)\/(.*?)\.pdf$/, ["boo", "splat"]] 132 | 133 | If you haven't already realize it, the returned array contains a regular expression as the pattern that will be used to match a request url to a route, and keys are the name of the params. We can verify it with the last example. /^\/a\/([^\/?#]+)\/(.*?)\.pdf$/ matches requests start with '/a/' and then all string that's not in '\/?#' as the param[:boo] and then a '/' preceding any string preceding '.pdf' as the param[:splat]. 134 | 135 | Let's see how the matched string in a url is stored into keys. The regular expression /((:\w+)|[\*#{special_chars.join}])/ is the key here. It equals to /((:\w+)|[\*.+()$])/, which will match a word starting with semicolon, or any of the following punctuations '*.+()$'. gsub tries to match the string as many times as it can. If the match is *, like the case when path is "/a*", then 'splat' is added to the key, and * is substituded for (.*?). If any of the special_chars is matched, then the key is not changed, and pattern is the escaped special_char. Otherwise, the key is the matched word without the starting semicolon, and the matched word is substituted for ([^/?#]+). Notice that if there are multiple matches for *, they are added in order to param[:splat]. Examples from sinatra doc: 136 | 137 | ```ruby 138 | get '/say/*/to/*' do 139 | # matches /say/hello/to/world 140 | params[:splat] # => ["hello", "world"] 141 | end 142 | 143 | get '/download/*.*' do 144 | # matches /download/path/to/file.xml 145 | params[:splat] # => ["path/to/file", "xml"] 146 | end 147 | ``` 148 | 149 | In the case of path is a regular expression, it will just return the regular expression itself as the first element 'pattern' of the array, and the empty array 'keys' as the second element of the array. 150 | 151 | ```ruby 152 | def compile(path) 153 | keys = [] 154 | if path.respond_to? :to_str 155 | special_chars = %w{. + ( ) $} 156 | pattern = 157 | path.to_str.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match| 158 | case match 159 | when "*" 160 | keys << 'splat' 161 | "(.*?)" 162 | when *special_chars 163 | Regexp.escape(match) 164 | else 165 | keys << $2[1..-1] 166 | "([^/?#]+)" 167 | end 168 | end 169 | [/^#{pattern}$/, keys] 170 | elsif path.respond_to?(:keys) && path.respond_to?(:match) 171 | [path, path.keys] 172 | elsif path.respond_to?(:names) && path.respond_to?(:match) 173 | [path, path.names] 174 | elsif path.respond_to? :match 175 | [path, keys] 176 | else 177 | raise TypeError, path 178 | end 179 | end 180 | ``` 181 | 182 | The other two cases are kind of special. They are used for paths that are defined with custom classes. If you look at test/routing_test.rb, you can find a class RegexpLookAlike. The objects of this class respond to match and keys method. It also has a MatchData class. MatchData defines the object returned by the match method, and the capture instance method converts the matches to an array. Refer to http://ruby-doc.org/core/classes/MatchData.html for further information. 183 | 184 | You can define a custom class like RegexpLookAlike and the path like RegexpLookAlike.new will be a valid path parameter. In the case of RegexpLookAlike, RegexpLookAlike.new will be the path, and ["one", "two", "three", "four"] will be the keys. 185 | 186 | ```ruby 187 | class RegexpLookAlike 188 | class MatchData 189 | def captures 190 | ["this", "is", "a", "test"] 191 | end 192 | end 193 | 194 | def match(string) 195 | ::RegexpLookAlike::MatchData.new if string == "/this/is/a/test/" 196 | end 197 | 198 | def keys 199 | ["one", "two", "three", "four"] 200 | end 201 | end 202 | ``` 203 | 204 | The last case which path responds to name method is similar to this one. 205 | 206 | After `compile` finishes, path and keys are returned to the `route` method and assigned to the corresponding variable. Now we get to the line `conditions, @conditions = @conditions, []`. We see here that the local variable `conditions` is a copy of @conditions, and @conditions is reset to empty array. We sure want the routing conditions to be independent of each route. Now we know why in the `get` method duplicates the @conditions because it's reset here and we want to use the same routing conditions for the `GET` and `HEAD`. 207 | 208 | Next the method "GET /" that's just defined is removed. We could, for example, use the proc directly to define a route. For example, we can have a simplified `get` method: 209 | 210 | ```ruby 211 | class Base 212 | class << self 213 | attr_accessor :routes 214 | end 215 | @routes = {} 216 | class << self 217 | def get path, &block 218 | @routes[path] = block 219 | end 220 | end 221 | end 222 | 223 | Base.get '/' do 224 | puts "hello world" 225 | end 226 | 227 | Base.routes.each_pair do |k, v| 228 | puts "Route #{k}:" 229 | puts v.call 230 | end 231 | ``` 232 | 233 | This works fine. However if we add a helper, then the helper is undefined in `Base.get '/'`: 234 | 235 | ```ruby 236 | class Base 237 | class << self 238 | attr_accessor :routes 239 | end 240 | @routes = {} 241 | 242 | def a_helper 243 | "a message from a_helper" 244 | end 245 | 246 | class << self 247 | def get path, &block 248 | @routes[path] = block 249 | end 250 | end 251 | 252 | end 253 | 254 | Base.get '/' do 255 | puts "hello world #{a_helper}" 256 | end 257 | 258 | Base.routes.each_pair do |k, v| 259 | puts "Route #{k}:" 260 | puts v.call 261 | end 262 | ``` 263 | 264 | If we lazy evaluate it by using a unbound_method, then it works fine. Now we know why we define and then remove the `GET /` method. 265 | 266 | ```ruby 267 | class Base 268 | class << self 269 | attr_accessor :routes 270 | end 271 | @routes = {} 272 | 273 | def a_helper 274 | "a message from a_helper" 275 | end 276 | 277 | class << self 278 | def get path, &block 279 | define_method "a_route", &block 280 | unbound_method = instance_method "a_route" 281 | @routes[path] = proc { unbound_method.bind(self).call } 282 | end 283 | end 284 | 285 | end 286 | 287 | Base.get '/' do 288 | puts "hello world #{a_helper}" 289 | end 290 | 291 | Base.routes.each_pair do |k, v| 292 | puts "Route #{k}:" 293 | puts Base.new.instance_eval &v 294 | end 295 | ``` 296 | 297 | Since the unbound_method is an instance method, `unbound_method.bind(self)` binds to an instance of current class, and wrapped in a proc object with `call` method on it. When the proc is evaluated then the unbound_method is run. If arguments are passed to the block, for example 298 | 299 | get '/hello/:name' do |n| 300 | "Hello #{n}!" 301 | end 302 | 303 | *@block_params will be passed in as parameter. Note the @block_params is not available here, but that's fine because it's not evaluated yet. 304 | 305 | The compile! method finishes and four pieces of information is returned to the`route` method: the route method which wrapped in a proc, route pattern, parameter keys, conditions which is an array of proc. The four pieces are assigned to variable block, pattern, keys, conditions respectively. 306 | 307 | Let's see an example of the `compile!`. I use the sourcify gem https://github.com/ngty/sourcify to lookup the proc. 308 | ruby-1.9.2-p180 :001 > require 'sinatra' 309 | => true 310 | ruby-1.9.2-p180 :002 > require 'sourcify' 311 | => true 312 | ruby-1.9.2-p180 :003 > arr = Sinatra::Base.send :compile!,'GET', '/', proc{"abc"}, :host_name => /admin/ 313 | => [#, /^\/$/, [], [#]] 314 | ruby-1.9.2-p180 :004 > arr.first.to_source 315 | => "proc { unbound_method.bind(self).call }" 316 | ruby-1.9.2-p180 :005 > arr.last.collect(&:to_source) 317 | => ["proc { pattern.===(request.host) }"] 318 | ruby-1.9.2-p180 :006 > 319 | 320 | Next let's look at the `invoke_hook` method. The first argument name is :route_added, and args is an array [verb, path, block]. What does `invoke_hook` do is to call the `extensions` method to get all extensions on the current app and its superclasses. For each of the extensions if the `route_added` method exists on the extension then it calls it as a callback method. 321 | 322 | ```ruby 323 | def invoke_hook(name, *args) 324 | extensions.each { |e| e.send(name, *args) if e.respond_to?(name) } 325 | end 326 | ``` 327 | 328 | Then the`route` method adds the array [pattern, keys, conditions, block] to the @routes hash which is keyed on the HTTP verb like 'GET', 'POST', 'HEAD' etc, and returns the [pattern, keys, conditions, block] as the result of the `route` method. With the same conditions a HEAD route is added. 329 | 330 | Let's see an example of the routes added. 331 | 332 | ruby-1.9.2-p180 :002 > get '/:action', :host_name => /^admin\./ do 333 | ruby-1.9.2-p180 :003 > "Admin Area, Access denied!" 334 | ruby-1.9.2-p180 :004?> end 335 | => [/^\/([^\/?#]+)$/, ["action"], [#], #] 336 | ruby-1.9.2-p180 :005 > get '/download/*.*' do 337 | ruby-1.9.2-p180 :006 > params[:splat] 338 | ruby-1.9.2-p180 :007?> end 339 | => [/^\/download\/(.*?)\.(.*?)$/, ["splat", "splat"], [], #] 340 | ruby-1.9.2-p180 :008 > Sinatra::Application.routes 341 | => {"GET"=>[[/^\/([^\/?#]+)$/, ["action"], [#], #], [/^\/download\/(.*?)\.(.*?)$/, ["splat", "splat"], [], #]], "HEAD"=>[[/^\/([^\/?#]+)$/, ["action"], [#], #], [/^\/download\/(.*?)\.(.*?)$/, ["splat", "splat"], [], #]]} 342 | 343 | Besides routes we can define filters that are processed before or after a request. A filter is like a route in that it can also be matched if a path or a condition is given. There are two types filters - before filter and after filter. Both of then use the `add_filter` method. `filters` is a attr_reader of the Sinatra::Base singleton class: `@filters = {:before => [], :after => []}`. 344 | 345 | ```ruby 346 | # Define a before filter; runs before all requests within the same 347 | # context as route handlers and may access/modify the request and 348 | # response. 349 | def before(path = nil, options = {}, &block) 350 | add_filter(:before, path, options, &block) 351 | end 352 | 353 | # Define an after filter; runs after all requests within the same 354 | # context as route handlers and may access/modify the request and 355 | # response. 356 | def after(path = nil, options = {}, &block) 357 | add_filter(:after, path, options, &block) 358 | end 359 | 360 | # add a filter 361 | def add_filter(type, path = nil, options = {}, &block) 362 | return filters[type] << block unless path 363 | path, options = //, path if path.respond_to?(:each_pair) 364 | block, *arguments = compile!(type, path, block, options) 365 | add_filter(type) do 366 | process_route(*arguments) { instance_eval(&block) } 367 | end 368 | end 369 | ``` 370 | 371 | Suppose we add a before filter without a path, so that it will be run before every request. 372 | 373 | ```ruby 374 | before '/foo/*' do 375 | @note = 'Hi!' 376 | request.path_info = '/foo/bar/baz' 377 | end 378 | ``` 379 | 380 | In the `add_filter` the block of the before filter is added to the filters[:before] hash. If there is no path, which means it doesn't need to be routed, then `add_filter` is just returned with the filters[:before] hash. Next if path is a hash, which means the path is a condition, then the path is set to `//` which will match any routes, and options is set to the condition. Then `compile!(type, path, block, options)` is called. We get the pattern, keys, conditions from `compile!` and assign them to an array `argument`. After that `add_filter` is called again with only the `type` and a block as the parameters. `add_filter` then adds the block to `filters[type]` and returns. So the filters[:before] and filters[:after] are two proc arrays. Now let's see the block passed to `add_filter`. In the block it calls `process_route(*arguments) { instance_eval(&block) }`. `process_route` is a pretty big method. 381 | 382 | It first make a copy of `@params`. We can see `@params` is assigned back to `original_params` in the ensure block at the end of `process_route` method. So `params` will be modified in `process_route` and we want it to be the same after `process_route` returns. `:params` is an attr_accessor defined on Sinatra::Base: `attr_accessor :env, :request, :response, :params`. 383 | 384 | ```ruby 385 | # If the current request matches pattern and conditions, fill params 386 | # with keys and call the given block. 387 | # Revert params afterwards. 388 | # 389 | # Returns pass block. 390 | def process_route(pattern, keys, conditions) 391 | @original_params ||= @params 392 | route = @request.route 393 | route = '/' if route.empty? and not settings.empty_path_info? 394 | if match = pattern.match(route) 395 | values = match.captures.to_a 396 | params = 397 | if keys.any? 398 | keys.zip(values).inject({}) do |hash,(k,v)| 399 | if k == 'splat' 400 | (hash[k] ||= []) << v 401 | else 402 | hash[k] = v 403 | end 404 | hash 405 | end 406 | elsif values.any? 407 | {'captures' => values} 408 | else 409 | {} 410 | end 411 | @params = @original_params.merge(params) 412 | @block_params = values 413 | catch(:pass) do 414 | conditions.each { |cond| 415 | throw :pass if instance_eval(&cond) == false } 416 | yield 417 | end 418 | end 419 | ensure 420 | @params = @original_params 421 | end 422 | ``` 423 | 424 | `params` is assigned in the `Sinatra::Base#call!` method: `@params = indifferent_params(@request.params)`. We've seen `call!` is run when a request comes in and our app is initialized. `@request` is an instance of Sinatra::Request and is also an attr_accessor on Sinatra::Base. `@request.params` just returns a hash of parameters passed in by the request. For example in the request to `http://127.0.0.1:4567/?a[name]=1&b=2` the request.params is `{"a"=>{"name"=>"1"}, "b"=>"2"}`. In `indifferent_params` an `indifferent_hash` is called and returns a hash that will automatically convert symbol key to string key when accessing it. We merge the params to that hash so we can access the top level params with both symbol key and string key. Next params is looped and convert the nested params using `indifferent_params`. 425 | 426 | ```ruby 427 | # Enable string or symbol key access to the nested params hash. 428 | def indifferent_params(params) 429 | params = indifferent_hash.merge(params) 430 | params.each do |key, value| 431 | next unless value.is_a?(Hash) 432 | params[key] = indifferent_params(value) 433 | end 434 | end 435 | ``` 436 | 437 | ```ruby 438 | # Creates a Hash with indifferent access. 439 | def indifferent_hash 440 | Hash.new {|hash,key| hash[key.to_s] if Symbol === key } 441 | end 442 | ``` 443 | 444 | Next `route = @request.route` takes a copy of the unescaped the requested path stored in `Sinatra::Base#path_info` and assigns it to the instance variable `route`. I list the slick method Rack::Utils.unescape here but don't explain it. 445 | 446 | ```ruby 447 | def route 448 | @route ||= Rack::Utils.unescape(path_info) 449 | end 450 | ``` 451 | 452 | ```ruby 453 | # Unescapes a URI escaped string. (Stolen from Camping). 454 | def unescape(s) 455 | s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){ 456 | [$1.delete('%')].pack('H*') 457 | } 458 | end 459 | module_function :unescape 460 | ``` 461 | 462 | Let's see an example of Rack::Utils.unescape. 463 | 464 | ruby-1.9.2-p180 :012 > Rack::Utils.unescape('https%3A%2F%2Fapi.dropbox.com%2F0%2Ffileops%2Fcreate_folder&path%3D%2Ftest%26root%3Ddropbox') 465 | => "https://api.dropbox.com/0/fileops/create_folder&path=/test&root=dropbox" 466 | 467 | The path_info returns `/0/fileops/create_folder&path=/test&root=dropbox` 468 | 469 | Next line basically says that if you have define an empty route "" and the `path_info` is empty then it will use `/` as the route. I don't find the usage of an empty route yet so let's ignore it. 470 | 471 | The `route` is matched against the pattern. Remember the pattern is the regular expression that represents the route. If a match it found, `values = match.captures.to_a` assigns all of the matched components to the variable `value`. Here `to_a` isn't necessary since `captures` already returns an array. 472 | 473 | Next chunk of code just tries to populate the `params` variable if there is any element in `keys`. `keys.zip(values)` drops each key in `keys` to each of the element in values array to for a 2-dimension array. For example, our keys array is `['splat', 'foo']`, and the value array is ['bar', 'foobar'], then `keys.zip(values)` returns [['splat', 'bar'], ['foo', 'foobar']. Then constructs a hash using the first element of the 2-dimension array as the key and the last element as the value. In the case of above example, the final hash is {"splat" => 'bar, 'foo', 'foobar'}. If there is no elements in `keys` and there is any `values`, which means the route defines unnamed parameters like the following example, then we just assign all the values to hash keyed on 'captures'. Otherwise an empty hash is returned. 474 | 475 | ```ruby 476 | get %r{/hello/([\w]+)} do 477 | "Hello, #{params[:captures].first}!" 478 | end 479 | ``` 480 | 481 | Then we merge the `params` hash to the existing @params: `@params = @original_params.merge(params)`. We assign `values` to @block_params which as we discussed will be used as parameters to the routes defined like: 482 | 483 | ```ruby 484 | get '/hello/:name' do |n| 485 | "Hello #{n}!" 486 | end 487 | ``` 488 | 489 | Next we examine whether the conditions are all satisfied. If the any of condition procs is evaluated as false by `instance_eval(&cond) == false`, i.e. the condition isn't satisfied, then `yield` isn't called and no further processing is done; otherwise `process_route` yields to the block passed to it. 490 | 491 | Now let's return to our `add_filter` method. The block passed to `process_route` is `{ instance_eval(&block) }`, which basically runs the block we passed to the filter. We can imagine when a request comes in, for each of the filters we try to match it with the request; if a match is found we run the filter. 492 | 493 | After routes and filters are defined, let's see how routing is handled. Suppose classic.rb is running and we have a request to '/' coming in. As we have discussed in tutorial_2, the Sinatra::Base.call initializes the app and calls Sinatra::Base#call which in turn calls Sinatra::Base#call!. In Sinatra::Base#call! this line triggers the routing process: `invoke { dispatch! }`. `dispatch!` is where the routing happens. 494 | 495 | ```ruby 496 | # Dispatch a request with error handling. 497 | def dispatch! 498 | static! if settings.static? && (request.get? || request.head?) 499 | filter! :before 500 | route! 501 | rescue NotFound => boom 502 | handle_not_found!(boom) 503 | rescue ::Exception => boom 504 | handle_exception!(boom) 505 | ensure 506 | filter! :after unless env['sinatra.static_file'] 507 | end 508 | ``` 509 | 510 | First off a sequence settings. By default, in classic sinatra app `app_file` is the file that is being run. In modular sinatra app it's nil unless set. `root` and `public `are true if app_file is set. `static` is true if the public folder in the `root` exists. 511 | 512 | ```ruby 513 | set :app_file, nil 514 | set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) } 515 | set :public, Proc.new { root && File.join(root, 'public') } 516 | set :static, Proc.new { public && File.exist?(public) } 517 | ``` 518 | 519 | If `static` is true, and the request is either get or head request, then `static!` is called. `request.get?` and `request.head?` are instance methods on Rack::Request: 520 | 521 | ```ruby 522 | def get?; request_method == "GET" end 523 | def head?; request_method == "HEAD" end 524 | ``` 525 | 526 | In `static!` the first line double-checks settings.public is not nil. You may wonder that `settings.public` should not be nil since `static!` is already called. otherwise `static?` would return false. However it's possible after the current app is run we monkeypatch the app and set the public to nil. So the check is necessary. 527 | 528 | If `public` exists, we construct the absolute path to the `path_info` by combining the absolute path to the public folder and the unescaped `request.path_info`. Note unescaped is imported to Sinatra::Base by `include Rack::Utils`. The check of `path.start_with?(public_dir)` is important because we don't want the any request to access files outside the public folder. For example the request can be '/../../../etc/passwd'. If the file exists。 then we set the env['sinatra.static_file'] to the path to the file, and use `send_file` to generate the response object. `send_file` is inside the Sinatra::Helper. We will learn it in later tutorials. Note even the requested file is found it's not the end; we still need to run `filter!` and `route!` 529 | 530 | ```ruby 531 | # Attempt to serve static files from public directory. Throws :halt when 532 | # a matching file is found, returns nil otherwise. 533 | def static! 534 | return if (public_dir = settings.public).nil? 535 | public_dir = File.expand_path(public_dir) 536 | 537 | path = File.expand_path(public_dir + unescape(request.path_info)) 538 | return unless path.start_with?(public_dir) and File.file?(path) 539 | 540 | env['sinatra.static_file'] = path 541 | send_file path, :disposition => nil 542 | end 543 | ``` 544 | 545 | Then `Sinatra::Base#filter!` is run. We pass in :before as the first parameter, which means we want the before filters run. The `filter!` iteratively gets the before filters on current app and all its superclasses and evaluates them on the instance level of current app, i.e. `process_route(*arguments) { instance_eval(&block) }` will be run. We already know `process_route` stuffs the params hash, and run the block if a route is matched. 546 | 547 | ```ruby 548 | # Run filters defined on the class and all superclasses. 549 | def filter!(type, base = settings) 550 | filter! type, base.superclass if base.superclass.respond_to?(:filters) 551 | base.filters[type].each { |block| instance_eval(&block) } 552 | end 553 | ``` 554 | 555 | After before filters are run, `route!` is called. It first tries to match routes defined in the current class. We have already learned `process_route` will yield to the block if it finds a matching route; so and all superclasses if a route in the current class can be matched. Otherwise we the superclasses of the current class and try to find a matching route on the superclass. 556 | 557 | ```ruby 558 | # Run routes defined on the class and all superclasses. 559 | def route!(base = settings, pass_block=nil) 560 | if routes = base.routes[@request.request_method] 561 | routes.each do |pattern, keys, conditions, block| 562 | pass_block = process_route(pattern, keys, conditions) do 563 | route_eval(&block) 564 | end 565 | end 566 | end 567 | 568 | # Run routes defined in superclass. 569 | if base.superclass.respond_to?(:routes) 570 | return route!(base.superclass, pass_block) 571 | end 572 | 573 | route_eval(&pass_block) if pass_block 574 | route_missing 575 | end 576 | 577 | Based on the `Rack::Request#request_method`, it gets the corresponding values of the @routes hash, and calls `process_route` on each of the routes. 578 | 579 | ```ruby 580 | def request_method; @env["REQUEST_METHOD"] end 581 | ``` 582 | 583 | If a route is matched, `route_eval` is called, which evaluates the route processing proc and throws :halt with the result of the `instance_eval(&block)`. 584 | 585 | ```ruby 586 | # Run a route block and throw :halt with the result. 587 | def route_eval(&block) 588 | throw :halt, instance_eval(&block) 589 | end 590 | ``` 591 | 592 | The :halt terminates further processing of other possible routes. :halt is caught in the `Sinatra::Base#invoke`. Remember `dispatch!` is wrapped in `invoke { dispatch! }`. So the block passed to invoke is run by `instance_eval` and the result is used to generate the response. 593 | 594 | ```ruby 595 | # Run the block with 'throw :halt' support and apply result to the response. 596 | def invoke(&block) 597 | res = catch(:halt) { instance_eval(&block) } 598 | return if res.nil? 599 | 600 | case 601 | when res.respond_to?(:to_str) 602 | @response.body = [res] 603 | when res.respond_to?(:to_ary) 604 | res = res.to_ary 605 | if Fixnum === res.first 606 | if res.length == 3 607 | @response.status, headers, body = res 608 | @response.body = body if body 609 | headers.each { |k, v| @response.headers[k] = v } if headers 610 | elsif res.length == 2 611 | @response.status = res.first 612 | @response.body = res.last 613 | else 614 | raise TypeError, "#{res.inspect} not supported" 615 | end 616 | else 617 | @response.body = res 618 | end 619 | when res.respond_to?(:each) 620 | @response.body = res 621 | when (100..599) === res 622 | @response.status = res 623 | end 624 | 625 | res 626 | end 627 | ``` 628 | 629 | The `route!` is called recursively on superclass until a route is found or the superclass does respond to the `routes` method, i.e it does not define routes. In that case `route_missing` is called. `route_missing` checks if the current app is a middleware of an inner app. If it is then it just forwards the request to the inner app and let the downstream app process the request. Otherwise it raise a NotFound exception as the response to the request. We will learn response in detail in the next tutorial. 630 | 631 | ```ruby 632 | # No matching route was found or all routes passed. The default 633 | # implementation is to forward the request downstream when running 634 | # as middleware (@app is non-nil); when no downstream app is set, raise 635 | # a NotFound exception. Subclasses can override this method to perform 636 | # custom route miss logic. 637 | def route_missing 638 | if @app 639 | forward 640 | else 641 | raise NotFound 642 | end 643 | end 644 | ``` 645 | 646 | ```ruby 647 | # Forward the request to the downstream app -- middleware only. 648 | def forward 649 | fail "downstream app not set" unless @app.respond_to? :call 650 | status, headers, body = @app.call env 651 | @response.status = status 652 | @response.body = body 653 | @response.headers.merge! headers 654 | nil 655 | end 656 | ``` --------------------------------------------------------------------------------