Open davesag opened 12 years ago
Note I found this in Stackoverflow. http://stackoverflow.com/questions/5015471/using-sinatra-for-larger-projects-via-multiple-files
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)
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.
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.
load
and require
do not respect lexical scope. '/con_1/route_1'
would be added to Sinatra::Application
, not MySinatraApp
.
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
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.
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.
Once a
Sinatra
project gets beyond a certain level of complexity themy_sinatra_app.rb
file gets rather cluttered with route handling code. While the separation ofhelper
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.