codereading / sinatra

Classy web-development dressed in a DSL (official / canonical repo)
http://www.sinatrarb.com/
MIT License
12 stars 2 forks source link

You can execute a classic Sinatra app as a ruby program, how does this work? #4

Open ericgj opened 12 years ago

ericgj commented 12 years ago

Thought it might be helpful to have the code in front of us....

https://github.com/sinatra/sinatra/blob/master/lib/sinatra/main.rb

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 { File.expand_path($0) == File.expand_path(app_file) }

    if run? && ARGV.any?
      require 'optparse'
      OptionParser.new { |op|
        op.on('-p port',   'set the port (default is 4567)')                { |val| set :port, Integer(val) }
        op.on('-o addr',   'set the host (default is 0.0.0.0)')             { |val| set :bind, val }
        op.on('-e env',    'set the environment (default is development)')  { |val| set :environment, val.to_sym }
        op.on('-s server', 'specify rack server/handler (default is thin)') { |val| set :server, val }
        op.on('-x',        'turn on the mutex lock (default is off)')       {       set :lock, true }
      }.parse!(ARGV.dup)
    end
  end

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

# include would include the module in Object
# extend only extends the `main` object
extend Sinatra::Delegator
lyonsinbeta commented 12 years ago

If you write some ruby code that uses requie 'sinatra' then you run

$ ruby some_code_that_requires_sinatra.rb

it should automatically start the server and run the application on port 4567 by default.

ericgj commented 12 years ago

Yes - our aim is to explain how that works in the code above - can you explain it?

lyonsinbeta commented 12 years ago

Not all of it. But I'll try:

So skipping the require and module/class declarations...

set :app_file, caller_files.first || $0

is setting the environment variable :app_file to either the file that called Sinatra or the name of that file (the global variable $0 is the name of the file.

set :run, Proc.new { File.expand_path($0) == File.expand_path(app_file) }

is setting the :run environment variable to the location of the app and the surrounding directory, I think. There may be more going on here.

if run? && ARGV.any?
      require 'optparse'
      OptionParser.new { |op|
        op.on('-p port',   'set the port (default is 4567)')                { |val| set :port, Integer(val) }
        op.on('-o addr',   'set the host (default is 0.0.0.0)')             { |val| set :bind, val }
        op.on('-e env',    'set the environment (default is development)')  { |val| set :environment, val.to_sym }
        op.on('-s server', 'specify rack server/handler (default is thin)') { |val| set :server, val }
        op.on('-x',        'turn on the mutex lock (default is off)')       {       set :lock, true }
      }.parse!(ARGV.dup)
    end
  end

This is the option parser for command line switches. The defaults must live elsewhere since I don't see them declared here. However I didn't know you could call "require" in the middle of a ruby file. I guess there's no reason you can't, but I hadn't seen it done this way before where it's declared directly before it's needed like this.

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

# include would include the module in Object
# extend only extends the `main` object
extend Sinatra::Delegator

These last few lines I'm not positive about. at_exit is apparently a kernel method according to the ruby docs that executes code (in reverse order multiples were declared!) at exit. In this case, it's running the application if there is no Global error messages (the $! is the global variable for the latest error message)

samnang commented 12 years ago
at_exit { Application.run! if $!.nil? && Application.run? }

This line is an interesting part, it registers a hook to execute this block of code before exiting your app. So when the app was executed the interpreter goes through all of program source code and finish the program, but this block of code was executed before exiting the program. It checks $1 to see there is no exception was raise before starting the Application instance.

I can't find where does Application.run? get defined? I try Sinatra::Application.method(:run?).source_location, but it doesn't help.

@codereading/readers

ericgj commented 12 years ago

Nice!

set :app_file, caller_files.first || $0

Didn't quite understand caller_files at first but it seems to be a hack to get the first file in the call stack that requires sinatra. The gory details are in sinatra/base. So as the comment says, this file is needed to set the relative paths -- in particular the root setting and settings based on root.

It seems like the difference from the Sinatra::Base behavior here is the || $0 -- although I'm not sure why caller_files would ever be empty, since you're always going to require 'sinatra' somewhere.

set :run, Proc.new { File.expand_path($0) == File.expand_path(app_file) }

This sets a condition which gets used in the at_exit hook -- namely, that it serves up the app only if the top-level file you executed from the command line is the same as the app_file.

So say you are testing your classic app, and you require the app file. When you ruby test_my_app.rb, it's not going to serve it up, but when you ruby my_app.rb, it will.

To answer Samnang's question, when you call set :x, you get a number of accessors, including x? . So in the at_exit, Application.run? evaluates the Proc.

Which brings up another nice feature, when you set something to a Proc, it gets lazily-evaluated. But it's not memoized -- the Proc gets evaluated each time you get the setting. Cf. sinatra/base

# include would include the module in Object
# extend only extends the `main` object
extend Sinatra::Delegator

A remaining question is what these lines above do? Sinatra::Delegator -- what's that about?

lyonsinbeta commented 12 years ago

Could caller_files be empty if you literally copied and pasted all of the Sinatra code into one massive file? So your app, and all the framework to run it would be in one giant .rb, so there would be no require and thus an empty caller_files array? It's certainly unlikely, but those 6 extra characters,|| $0, make it possible. People do weird stuff like that sometimes; just look at the smallest rails app.

kgrz commented 12 years ago

@samnang The Application class inherits Sinatra::Base. run! method is defined in Sinatra::Base at this line

adamakhtar commented 12 years ago

@thekungfuman @ericgj Yes I was wondering why

set :app_file, caller_files.first || $0

was guarding against a scenario of caller_files returning nil. I was just about to ask the group under what situation would caller_files return nil and I guess kungfumans scenario could be one - albeit a quite unusual one. Perhaps there is another case. Any ideas?

adamakhtar commented 12 years ago

If an exception has been raised, then I assume your sinatra app would never run anyway. So why check for an exception here

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

( for those unaware $! is a predefined global variable in ruby which holds the value of any exception raised inside of a rescue block )

lyonsinbeta commented 12 years ago

Exceptions can be suppressed can't they? Perhaps the developer suppressed an exception, but it's still logged in the $! so it would still get caught by this check.

adamakhtar commented 12 years ago

@ericgj re:

Which brings up another nice feature, when you set something to a Proc, it gets lazily-evaluated. But it's not memoized -- the Proc gets evaluated each time you get the setting.

thats a cool feature. Im wondering what benefit it would have here in

set :run, Proc.new { File.expand_path($0) == File.expand_path(app_file) }

during runtime would $0 and app_file ever change? Why not just set run like so

set :run, (File.expand_path($0) == File.expand_path(app_file) )`

probably not very important to the overall code reading session but it is something that puzzled me.

kgrz commented 12 years ago

I think the difference between $0 and app_file comes in when using an external config.ru. Trying to understand the information here

ericgj commented 12 years ago

@robodisco,

during runtime would $0 and app_file ever change? Why not just set run like so set :run, (File.expand_path($0) == File.expand_path(app_file) )

Yes - I think because your classic app could explicitly set app_file if it wanted to (although I can't think of a practical use for doing that, it's possible).

In general, (correct me if I'm wrong), it looks like the default settings that Sinatra makes that are based on other settings, are lazily-evaluated like this, so that the app could change the underlying settings.

adamakhtar commented 12 years ago

@ericgj @thekungfuman @kgrz Thanks for your input on those. Makes things a bit easier to understand.

Now as @ericgj said, what's this delegator all about?

# 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|
        define_method(method_name) do |*args, &block|
          return super(*args, &block) if respond_to? method_name
          Delegator.target.send(method_name, *args, &block)
        end
        private method_name
      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
ericgj commented 12 years ago

Yes, and why didn't they use the stdlib DelegateClass or some other tool in the delegate library?