17

I have a Sinatra based REST service app and I would like to call one of the resources from within one of the routes, effectively composing one resource from another. E.g.

get '/someresource' do
  otherresource = get '/otherresource'
  # do something with otherresource, return a new resource
end

get '/otherresource' do
  # etc.
end

A redirect will not work since I need to do some processing on the second resource and create a new one from it. Obviously I could a) use RestClient or some other client framework or b) structure my code so all of the logic for otherresource is in a method and just call that, however, it feels like it would be much cleaner if I could just re-use my resources from within Sinatra using their DSL.

Stephen Petschulat
  • 1,159
  • 2
  • 14
  • 23

5 Answers5

13

Another option (I know this isn't answering your actual question) is to put your common code (even the template render) within a helper method, for example:

helpers do
  def common_code( layout = true )
    @title = 'common'
    erb :common, :layout => layout
  end
end

get '/foo' do
  @subtitle = 'foo'
  common_code
end

get '/bar' do
  @subtitle = 'bar'
  common_code
end

get '/baz' do
  @subtitle = 'baz'
  @common_snippet = common_code( false )
  erb :large_page_with_common_snippet_injected
end
Clive Crous
  • 131
  • 1
  • 3
11

Sinatra's documentation covers this - essentially you use the underlying rack interface's call method:

http://www.sinatrarb.com/intro.html#Triggering%20Another%20Route

Triggering Another Route

Sometimes pass is not what you want, instead you would like to get the result of calling another route. Simply use call to achieve this:

get '/foo' do
  status, headers, body = call env.merge("PATH_INFO" => '/bar')
  [status, headers, body.map(&:upcase)]
end

get '/bar' do
  "bar"
end
Martin Konecny
  • 57,827
  • 19
  • 139
  • 159
9

I was able to hack something up by making a quick and dirty rack request and calling the Sinatra (a rack app) application directly. It's not pretty, but it works. Note that it would probably be better to extract the code that generates this resource into a helper method instead of doing something like this. But it is possible, and there might be better, cleaner ways of doing it than this.

#!/usr/bin/env ruby
require 'rubygems'
require 'stringio'
require 'sinatra'

get '/someresource' do
  resource = self.call(
    'REQUEST_METHOD' => 'GET',
    'PATH_INFO' => '/otherresource',
    'rack.input' => StringIO.new
  )[2].join('')

  resource.upcase
end

get '/otherresource' do
  "test"
end

If you want to know more about what's going on behind the scenes, I've written a few articles on the basics of Rack you can read. There is What is Rack? and Using Rack.

AboutRuby
  • 7,936
  • 2
  • 27
  • 20
  • Thanks, I like this better than using RestClient. Wrapper methods such as local_get, local_post, local_put, and local_delete using this technique would be a good addition to the Sinatra framework. – Stephen Petschulat Aug 24 '10 at 04:51
  • 1
    This answer does not preserve the environment, including the user's session. Here's a similar answer (but an even worse-seeming hack) that preserves the environment: http://stackoverflow.com/questions/5824736/sinatra-helper-to-fake-a-request/5825157#5825157 – Phrogz Apr 28 '11 at 21:19
  • Ruby 2.1.0, sinatra 1.4.4, I get `undefined method \`join' for #` – Jeff Ward Apr 03 '14 at 16:02
  • Ah, this apparently happens when the result is a file served out of `/public/`, workaround is: `return File.read(result[2].path) if result[2].is_a? Rack::File` – Jeff Ward Apr 03 '14 at 16:08
1

This may or may not apply in your case, but when I’ve needed to create routes like this, I usually try something along these lines:

%w(main other).each do |uri|
  get "/#{uri}" do
    @res = "hello"
    @res.upcase! if uri == "other"
    @res
  end
end
Todd Yandell
  • 14,656
  • 2
  • 50
  • 37
0

Building on AboutRuby's answer, I needed to support fetching static files in lib/public as well as query paramters and cookies (for maintaining authenticated sessions.) I also chose to raise exceptions on non-200 responses (and handle them in the calling functions).

If you trace Sinatra's self.call method in sinatra/base.rb, it takes an env parameter and builds a Rack::Request with it, so you can dig in there to see what parameters are supported.

I don't recall all the conditions of the return statements (I think there were some Ruby 2 changes), so feel free to tune to your requirements.

Here's the function I'm using:

  def get_route url
    fn = File.join(File.dirname(__FILE__), 'public'+url)
    return File.read(fn) if (File.exist?fn)

    base_url, query = url.split('?')
    begin
      result = self.call('REQUEST_METHOD' => 'GET',
                         'PATH_INFO' => base_url,
                         'QUERY_STRING' => query,
                         'rack.input' => StringIO.new,
                         'HTTP_COOKIE' => @env['HTTP_COOKIE'] # Pass auth credentials
                         )
    rescue Exception=>e
      puts "Exception when fetching self route: #{url}"
      raise e
    end
    raise "Error when fetching self route: #{url}" unless result[0]==200 # status

    return File.read(result[2].path) if result[2].is_a? Rack::File
    return result[2].join('') rescue result[2].to_json
  end
Community
  • 1
  • 1
Jeff Ward
  • 16,563
  • 6
  • 48
  • 57