Skip to content

Latest commit

 

History

History
309 lines (244 loc) · 21 KB

tutorial_1.md

File metadata and controls

309 lines (244 loc) · 21 KB

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?

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!

This brings out two areas we are going to cover: method lookup and server startup in sinatra.

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.)

If you look at sinatra/lib/sinatra.rb, which is the file that is required by the first line require 'sinatra'

  libdir = File.dirname(__FILE__)
  $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)

  require 'sinatra/base'
  require 'sinatra/main'

  enable :inline_templates

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.

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.

Sinatra::Delegator is a module defined in Sinatra::Base:

 # Sinatra delegation mixin. Mixing this module into an object causes all
 # methods to be delegated to the Sinatra::Application class. Used primarily
 # at the top-level.
  module Delegator #:nodoc:
    def self.delegate(*methods)
      methods.each do |method_name|
        eval <<-RUBY, binding, '(__DELEGATE__)', 1
          def #{method_name}(*args, &b)
            ::Sinatra::Delegator.target.send(#{method_name.inspect}, *args, &b)
          end
          private #{method_name.inspect}
        RUBY
      end
    end

    delegate :get, :patch, :put, :post, :delete, :head, :options, :template, :layout,
             :before, :after, :error, :not_found, :configure, :set, :mime_type,
             :enable, :disable, :use, :development?, :test?, :production?,
             :helpers, :settings

    class << self
      attr_accessor :target
    end

    self.target = Application
  end

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.

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.

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.

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.

  require 'sinatra/base'

  module Sinatra
    class Application < Base

      # we assume that the first file that requires 'sinatra' is the
      # app_file. all other path related options are calculated based
      # on this path by default.
      set :app_file, caller_files.first || $0

      set :run, Proc.new { $0 == app_file }

      if run? && ARGV.any?
        require 'optparse'
        OptionParser.new { |op|
          op.on('-x')        {       set :lock, true }
          op.on('-e env')    { |val| set :environment, val.to_sym }
          op.on('-s server') { |val| set :server, val }
          op.on('-p port')   { |val| set :port, val.to_i }
          op.on('-o addr')   { |val| set :bind, val }
        }.parse!(ARGV.dup)
      end
    end

    at_exit { Application.run! if $!.nil? && Application.run? }
  end

  include Sinatra::Delegator

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.

    # Sets an option to the given value.  If the value is a proc,
    # the proc will be called every time the option is accessed.
    def set(option, value=self, &block)
      raise ArgumentError if block && value != self
      value = block if block
      if value.kind_of?(Proc)
        metadef(option, &value)
        metadef("#{option}?") { !!__send__(option) }
        metadef("#{option}=") { |val| metadef(option, &Proc.new{val}) }
      elsif value == self && option.respond_to?(:each)
        option.each { |k,v| set(k, v) }
      elsif respond_to?("#{option}=")
        __send__ "#{option}=", value
      else
        set option, Proc.new{value}
      end
      self
    end

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.

set(:probability) { |value| condition { rand <= value } }

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.

    def metadef(message, &block)
      (class << self; self; end).
        send :define_method, message, &block
    end

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.

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.

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'

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.

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.

  # Access settings defined with Base.set.
  def self.settings
    self
  end
  # Access settings defined with Base.set.
  def settings
    self.class.settings
  end

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 '<main>'. 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 '<main>' it will match the two colons. Then /Users/zjia/code/ruby_test/caller/caller.rb:3:in '<main>' 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.

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.

  CALLERS_TO_IGNORE = [ # :nodoc:
    /\/sinatra(\/(base|main|showexceptions))?\.rb$/, # all sinatra code
    /lib\/tilt.*\.rb$/,                              # all tilt code
    /\(.*\)/,                                        # generated code
    /rubygems\/custom_require\.rb$/,                 # rubygems require hacks
    /active_support/,                                # active_support require hacks
    /bundler(\/runtime)?\.rb/,                       # bundler require hacks
    /<internal:/                                     # internal in ruby >= 1.9.2
  ]

  # add rubinius (and hopefully other VM impls) ignore patterns ...
  CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS) if defined?(RUBY_IGNORE_CALLERS)

  # Like Kernel#caller but excluding certain magic entries and without
  # line / method information; the resulting array contains filenames only.
  def caller_files
    caller_locations.
      map { |file,line| file }
  end

  # Like caller_files, but containing Arrays rather than strings with the
  # first element being the file, and the second being the line.
  def caller_locations
    caller(1).
      map    { |line| line.split(/:(?=\d|in )/)[0,2] }.
      reject { |file,line| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
  end

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.

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

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.

  # Run the Sinatra app as a self-hosted server using
  # Thin, Mongrel or WEBrick (in that order)
  def run!(options={})
    set options
    handler      = detect_rack_handler
    handler_name = handler.name.gsub(/.*::/, '')
    puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " +
      "on #{port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i
    handler.run self, :Host => bind, :Port => port do |server|
      [:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } }
      set :running, true
    end
  rescue Errno::EADDRINUSE => e
    puts "== Someone is already performing on port #{port}!"
  end

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.

  def detect_rack_handler
    servers = Array(server)
    servers.each do |server_name|
      begin
        return Rack::Handler.get(server_name.downcase)
      rescue LoadError
      rescue NameError
      end
    end
    fail "Server handler (#{servers.join(',')}) not found."
  end

@handlers is a hash contains all the server handlers defined by rack. Handlers are added to @handlers by the register method.

  def self.get(server)
    return unless server
    server = server.to_s

    if klass = @handlers[server]
      obj = Object
      klass.split("::").each { |x| obj = obj.const_get(x) }
      obj
    else
      try_require('rack/handler', server)
      const_get(server)
    end
  end

  def self.register(server, klass)
    @handlers ||= {}
    @handlers[server] = klass
  end

Following is how register is called and a list of all handlers

  register 'cgi', 'Rack::Handler::CGI'
  register 'fastcgi', 'Rack::Handler::FastCGI'
  register 'mongrel', 'Rack::Handler::Mongrel'
  register 'emongrel', 'Rack::Handler::EventedMongrel'
  register 'smongrel', 'Rack::Handler::SwiftipliedMongrel'
  register 'webrick', 'Rack::Handler::WEBrick'
  register 'lsws', 'Rack::Handler::LSWS'
  register 'scgi', 'Rack::Handler::SCGI'
  register 'thin', 'Rack::Handler::Thin'

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:

  require "thin"
  require "rack/content_length"
  require "rack/chunked"

  module Rack
    module Handler
      class Thin
        def self.run(app, options={})
          server = ::Thin::Server.new(options[:Host] || '0.0.0.0',
                                      options[:Port] || 8080,
                                      app)
          yield server if block_given?
          server.start
        end
      end
    end
  end

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.

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!

  handler.run self, :Host => bind, :Port => port do |server|
    [:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } }
    set :running, true
  end
  def quit!(server, handler_name)
    # Use Thin's hard #stop! if available, otherwise just #stop.
    server.respond_to?(:stop!) ? server.stop! : server.stop
    puts "\n== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i
  end

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.

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.