Is it possible to write a root route with a file extension in Sinatra? - ruby

I'm writing a JSON API with Sinatra and I'm separating the different resources into Sinatra::Base classes using the map command:
map('/people') { run Api::People }
Within Api::People, /people would be mapped as the root path /. I'd like /people.json to be handled via Api::People -- is this possible? I can't figure out how to write the route.

If you want a DRYer alternative:
%w(people people.json).each do |route|
map('/' + route) { run Api::People }
end
or you could include the slash in the array like %w(/path/to/api /path/to/api.json)

Looks like a second mapping is required:
map('/people') { run Api::People }
map('/people.json') { run Api::People }
When I add that, /people.json is sent to the root path of Api::People as I wanted.
The problem with this approach is that I have a lot of nested resources, with translates into a lot of repetitive mappings.
I've settled on a design that is both elegant and logically consistent. Did you know a Sinatra::Base class can mount other Sinatra::Base classes inside itself as middleware?
Once I figured that out, the solution is obvious:
config.ru
Dir['api/**/*.rb'].each {|file| require file }
run API::Router
api/router.rb
module API
class Router < Sinatra::Base
use Businesses
use People
use Users
get '*' do
not_found
end
end
end
api/businesses.rb
class API::Businesses < Sinatra::Base
use Locations
get '/businesses.json' do ... end
get '/businesses/:id.json' do ... end
end
api/businesses/locations.rb
class API::Businesses < Sinatra::Base
class Locations < Sinatra::Base
before { #business = Business.find_by_id( params[:business_id] ) }
get '/businesses/:business_id/locations.json' do ... end
get '/businesses/:business_id/locations/:id.json' do ... end
end
end
An additional benefit is that all routes are complete, so you don't have to constantly remember what '/' is actually mapping to.

Related

How to map routes to controllers in Sinatra?

I'd like to create a simple experimental MVC framework using Sinatra.
I'd like to define resources by name "pages" for example should resolve to:
/pages (index)
/pages/new
/pages/:id/show (show)
as WELL as map to app/controllers/PagesController.rb with corresponding get('/') to be responsible for the index, post('/pages/create') be responsible for creation, etc.
Trouble is even after reading the official documentation I'm terribly confused. I imagine I need to use non-classic Sinatra model for this, but could anyone point me in the right direction?
Thank you
If you want what I think you're wanting, I do this all the time. Initially for this scheme I used the travis-api source as a reference, but essentially what you want to do is extend Sinatra::Base in a "controller" class and then mount up your individual Sinatra "controllers" in rack, something like this:
module Endpoint
def self.included(base)
base.class_eval do
set(:prefix) { "/" << name[/[^:]+$/].downcase }
end
end
end
class Users < Sinatra::Base
include Endpoint
get '/' do
#logic here
end
get '/:id' do
#logic here
end
post '/' do
#logic here
end
patch '/:id' do
#logic here
end
end
class Posts < Sinatra::Base
include Endpoint
post '/' do
#logic here
end
end
and then something like this:
class App
require "lib/endpoints/users"
require "lib/endpoints/posts"
attr_reader :app
def initialize
#app = Rack::Builder.app do
[Users, Posts].each do |e|
map(e.prefix) { run(e.new) }
end
end
end
def call(env)
app.call(env)
end
end
You can adjust this to whatever you need, but the idea is the same, you separate your app into composable Sinatra applications that each have a prefix that they are mounted under using Rack. This particular example will give you routes for:
get '/users'
get '/users/:id'
post '/users'
patch '/users/:id'
get '/posts'
I'll give you a very simple example here:
Create a file controller.rb
get '/pages' do
#pages = Pages.all
erb :pages
end
Next create a views directory in the same folder as teh controller, and create a file named pages.html.erb
This is the corresponding view to your previously created controller action.
Here, you can type something like:
<% #pages.each do |p| %>
<%= p.title %>
<% end %>
Restart your server, visit localhost:PORT/pages and you will see a list of all your page titles.
You can check out this link for a simple sinatra tutorial - http://code.tutsplus.com/tutorials/singing-with-sinatra--net-18965
You can make this as complicated or as simple as you need. For example:
Rails makes a lot of magic happen under the hood, whereas Sinatra is more flexible at the cost of requiring you to implement some of this stuff yourself.
controller_map = {
'pages' => PagesController
}
post '/:controller/new' do
c = params[:controller]
module = controller_map[c]
module.create_new()
...
end
get '/:controller/:id/show' do
c = params[:controller]
id = params[:id]
module = controller_map[c]
module.get(id)
...
end

How can you set up scoped redirects for Sinatra apps

I have a series of sinatra applications that are set up such that each is responsible for one thing.
Let's say I have two apps like this:
class Foo < Sinatra::Base
get '/' do
'FOO!'
end
end
class Zoo < Sinatra::Base
get '/' do
'ZOO!'
end
get '/zoom' do
# do things
redirect '/'
end
end
Now let's say I have my config.ru as such:
require './application'
run Rack::URLMap.new('/' => Foo.new, '/zoo' => Zoo.new)
The problem I'm running into is when I try to do the redirect in the zoom action I get sent off to the index action of Foo instead of Zoo. Is there a clean way to do this such that my applications don't need to know how the routes are set up for the app?
You can use configurable redirects. See http://www.sinatrarb.com/2011/03/03/sinatra-1.2.0.html#configurable_redirects.
E.g.
class Zoo < Sinatra::Base
get '/' do
'ZOO!'
end
get '/zoom' do
# do things
redirect to('/')
end
end
Or alternatively, as mentioned in the link above, skip the to() call by enabling prefixed redirects in the Zoo app:
class Zoo < Sinatra::Base
enable :prefixed_redirects
...

Rails routing: Giving default values for path helpers

Is there some way to provide a default value to the url/path helpers?
I have an optional scope wrapping around all of my routes:
#config/routes.rb
Foo::Application.routes.draw do
scope "(:current_brand)", :constraints => { :current_brand => /(foo)|(bar)/ } do
# ... all other routes go here
end
end
I want users to be able to access the site using these URLs:
/foo/some-place
/bar/some-place
/some-place
For convenience, I'm setting up a #current_brand in my ApplicationController:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_filter :set_brand
def set_brand
if params.has_key?(:current_brand)
#current_brand = Brand.find_by_slug(params[:current_brand])
else
#current_brand = Brand.find_by_slug('blah')
end
end
end
So far so good, but now I must modify all *_path and *_url calls to include the :current_brand parameter, even though it is optional. This is really ugly, IMO.
Is there some way I can make the path helpers automagically pick up on #current_brand?
Or perhaps a better way to define the scope in routes.rb?
I think you will want to do something like this:
class ApplicationController < ActionController::Base
def url_options
{ :current_brand => #current_brand }.merge(super)
end
end
This method is called automatically every time url is constructed and it's result is merged into the parameters.
For more info on this, look at: default_url_options and rails 3
In addition to CMW's answer, to get it to work with rspec, I added this hack in spec/support/default_url_options.rb
ActionDispatch::Routing::RouteSet.class_eval do
undef_method :default_url_options
def default_url_options(options={})
{ :current_brand => default_brand }
end
end

Pass arguments to new sinatra app

Simple question: I want to be able to pass options into my sinatra app in config.ru. How is that possible? My config.ru looks like this:
run MyApp
But I want to have this in my MyApp class to take arguments:
class MyApp < Sinatra::Base
def initialize(config)
#config = config
end
end
But I can't figure out a way to do this. Ideas?
Use set/settings
require 'sinatra/base'
class MyApp < Sinatra::Base
get '/' do
settings.time_at_startup.to_s
end
end
# Just arbitrarily picking time as it'll be static but, diff for each run.
MyApp.set :time_at_startup, Time.now
run MyApp
Use a config file. See Sinatra::ConfigFile in contrib (which also uses set and settings, but loads params from a YAML file)
If you want to configure with params, I figured out that you could do this:
require 'sinatra/base'
class AwesomeApp < Sinatra::Base
def initialize(app = nil, params = {})
super(app)
#bootstrap = params.fetch(:bootstrap, false)
end
end
rnicholson's response will be the best answer in most cases but if what you want is to have access to an instance variable in your routes, you can set these up using the before filter as explained in the Sinatra README:
Before filters are evaluated before each request within the same context as the routes will be and can modify the request and response. Instance variables set in filters are accessible by routes and templates:
before do
#note = 'Hi!'
request.path_info = '/foo/bar/baz'
end
get '/foo/*' do
#note #=> 'Hi!'
params['splat'] #=> 'bar/baz'
end

Sharing filters between applications

I am building a modular Padrino application to mount mutiple applications.
I am mounting a base application to /
class BaseApp < Padrino::Application
...
end
I am then mounting other appilications to other endpoints, such as /clients and these applications inherit from the base application:
class ClientsApp < BaseApp
...
end
This inheritence allows me to define all my settings, error handling and any included rack middleware in the app.rb for the BaseApp class.
So far so good. But I would also like to share before and after routing between apps. For example in my BaseApp controller code I want to do this:
BaseApp.controller do
before do
...
end
after do
...
end
get :index do
...
end
end
Rather than repeating these filters in my ClientsApp controller code, like so:
ClientsApp.controller do
before do
...
end
after do
...
end
get :index do
...
end
end
Is there anyway I can DRY up this code and specify the filters once in BaseApp and have them somehow inherited? I understand these filters are methods calls rather than methods.
Thanks!
You can use standard sinatra extensions, put under lib:
# lib/common_filters.rb
module CommonFilters
def self.registered(app)
app.before do
...
end
app.after do
...
end
end
end
Then in yours apps:
# app/app.rb
class MyApp < Padrino::Application
register CommonFilters
end

Resources