Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How does the route parsing work and what is the recommended way to split routes out into multiple files? #10

Open
davesag opened this issue Jun 5, 2012 · 8 comments

Comments

@davesag
Copy link

davesag commented Jun 5, 2012

Once a Sinatra project gets beyond a certain level of complexity the my_sinatra_app.rb file gets rather cluttered with route handling code. While the separation of helper code out into multiple files is well documented, I am yet to find a best practice approach to separating out route handlers into multiple files.

Having a clearer understanding how exactly how Sinatra parses routes in the first place would of course seem to be the way forward here, and then, based on this understanding, some example code and documentation could be written to cover this off canonically.

@davesag
Copy link
Author

davesag commented Jun 5, 2012

@ericgj
Copy link
Member

ericgj commented Jun 6, 2012

One approach that I find works quite well is to use Rack::URLMap to namespace your routes in config.ru. Then you can set up separate sinatra apps for different route sets, and not have to worry about name or other settings, etc. conflicts between them. And then it's completely natural to separate them into different source files. Any behavior you need to share between apps can be extracted into helper classes, or into shared models.

For instance, a typical setup:

require_relative 'blog_app'
require_relative 'user_app'
require_relative 'admin_app'
require_relative 'api'

map '/blog'
   run BlogApp   # Sinatra app whose routes are mounted under /blog
end

map '/user'
  run UserApp
end

map '/admin'
  run AdminApp
end

map '/api'
  run API
end

This is an ancient post, but describes the basics of the technique. To which I would add, get in the habit of using the to() route helper -- it translates relative paths in redirects and links, etc. and is aware of rack routing. See Sinatra readme

According to rkh, splitting up into multiple apps this way improves performance too (particularly if you have a lot of routes):

Routing in Sinatra is O(n) (with n being the number of routes), whilst Rack::URLMap and outer Rack routers route in O(log(n)).

(from an epic thread titled Should Sinatra deprecate classic? Dec 2010, sinatrarb google group)

@rkh
Copy link

rkh commented Jun 6, 2012

Note however that routing overhead is basically irrelevant for your app performance. Unless all your routes do is returning strings without any kind of computation, IO, etc.

@cbrito
Copy link
Member

cbrito commented Jun 6, 2012

In your master app, could you do something like:

class MySinatraApp < Sinatra::Application

    #Routes

    load 'routes/fake_controller_1.rb' # Or require?
    load 'routes/fake_controller_2.rb' # Or require?

end

Where fake_app_1 and fake_app_2 contain things like:

get '/con_1/route_1' do
    ...
end

I realize doing it in config.ru is probably cleaner, but this would be another way to keep the logic in your actual application, rather than at the Rack layer.

Note: This is untested (and probably very un-ruby-like) but valid?

!!!Edit!!!

Nevermind :)

That certainly won't work with the way load and require process through the interpreter.

@rkh
Copy link

rkh commented Jun 7, 2012

load and require do not respect lexical scope. '/con_1/route_1' would be added to Sinatra::Application, not MySinatraApp.

@ericgj
Copy link
Member

ericgj commented Jun 12, 2012

Going back to the question of how Sinatra compiles routes... anyone want to take a stab at explaining this?

      def compile!(verb, path, block, options = {})
        options.each_pair { |option, args| send(option, *args) }
        method_name             = "#{verb} #{path}"
        unbound_method          = generate_method(method_name, &block)
        pattern, keys           = compile path
        conditions, @conditions = @conditions, []

        [ pattern, keys, conditions, block.arity != 0 ?
            proc { |a,p| unbound_method.bind(a).call(*p) } :
            proc { |a,p| unbound_method.bind(a).call } ]
      end

      def compile(path)
        keys = []
        if path.respond_to? :to_str
          pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c) }
          pattern.gsub!(/((:\w+)|\*)/) do |match|
            if match == "*"
              keys << 'splat'
              "(.*?)"
            else
              keys << $2[1..-1]
              "([^/?#]+)"
            end
          end
          [/^#{pattern}$/, keys]
        elsif path.respond_to?(:keys) && path.respond_to?(:match)
          [path, path.keys]
        elsif path.respond_to?(:names) && path.respond_to?(:match)
          [path, path.names]
        elsif path.respond_to? :match
          [path, keys]
        else
          raise TypeError, path
        end
      end

      def generate_method(method_name, &block)
        define_method(method_name, &block)
        method = instance_method method_name
        remove_method method_name
        method
      end

@gwynforthewyn
Copy link

compile! is conceptually simple, though I'm futzing around in a detail or two.

generate_method is a piece of metaprogramming which returns an unboundmethod object and ensures that a dynamically generated method is cleaned up from the enclosing class.

compile() is kind of cute. It takes a path, which I thought was a string but then has to_str called on it. It santises the path once it's definitely a string.

There's some delegation to this:

def self.respond_to?(meth, *)
  meth.to_s !~ /^__|^to_str$/ and STRING.respond_to? meth unless super
end

which I don't recognise at all. If the method.to_s does not match (beginning with __||to_str) and STRING (??) .respond_to? tmethod, unless the super class has returned false, then return true.

@gwynforthewyn
Copy link

Oh, anyway, if either path responds_to :keys, or path responds_to :names, or path responds_to :match, then [path, path.{whatever}] is created as an anonymous array and returned to the pattern, keys variables in compile!.

compile then creates a copy of @conditions and blanks @conditions to [].

If "pattern, keys, conditions, block.arity " does not equal 0 (I'm not sure why the comparison with 0), then the unbound method is bound and called with either with or without the arguments *p. Presumably, *p is some class data.

So, how compile! works is to create a locally scoped method that corresponds to "GET /" or "HEAD /" or whatever and then called them with appropriate arguments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants