Detect which method called the routing condition in Sinatra - ruby

I'm using Sinatra with namespace.
When I tried to use condition, I met a problem.
Here's the snippet of code
class MainApp < Sinatra::Base
register Sinatra::Namespace
set(:role) do |role|
condition{
### DETECT WHERE THIS IS CALLED
p role
true
}
end
namespace '/api', :role => :admin do
before do
p "before"
end
get '/hoo' do
p "hoo"
end
end
namespace '/api' do
get '/bar' do
p "bar"
end
end
end
The above code outputs following message to console when accessing /api/hoo
:admin
:admin
"before"
:admin
"hoo"
I could not understand why :admin is displayed three times. However, maybe one is from namespace, and other twos are from before and get '/hoo'.
On the other hand, accessing /api/bar shows :admin two times.
I just want to do the filtering only before get '/hoo'. Is there any idea?
NOTE: I don't wan't to change URL from /api/hoo to something like /api/baz/hoo

You can debug the steps using the caller:
http://ruby-doc.org/core-2.0/Kernel.html#method-i-caller
(Note: I wouldn't recommend to leave caller in production code unless you absolutely need it for introspection, because it's quite slow.)
Re the Sinatra filters in particular, note that you can at the very least qualify the route and conditions they apply to:
http://www.sinatrarb.com/intro#Filters
before '/protected/*' do
authenticate!
end
before :agent => /Songbird/ do
# ...
end
I can't recollect how to get the http method, but if you look at the sinatra source code you'll likely find it -- last I looked, I recollect each of get, post, etc. to forward their call to the same function, with a method parameter.

Related

Sinatra route function call & helpers

So I have two classes like this.
class ApplicationController < Sinatra::Base
# don't enable logging when running tests
configure :production, :development do
enable :logging
end
get '/*' do
$request = request
PageController::render
end
end
and
class PageController < ApplicationController
def self.render()
#page = Page.find_by permalink: $request.path_info
if #page then
else
halt 400
end
end
end
All is well, until I reach the halt statement. Method not found. How could I use the Sinatra halt helper from inside this function call?
You've overcomplicated things. See the Helpers section of docs.
Put this in your Application controller:
helpers do
def render
#page = Page.find_by permalink: request.path_info
if #page then
else
halt 400
end
end
end
Now your route will be:
get '/*' do
render
end
Still, too complicated if you ask me, no need to ape Rails. Why not keep it simple?
require 'sinatra'
get '/*' do
#page = Page.find_by permalink: request.path_info
if #page then
haml :something
else
halt 400
end
end
That's it, that's the whole Sinatra app without recourse to inheritance and a structure that isn't required. Unless you're adding pages dynamically after the app is deployed then I'd also define the routes more explicitly.
Don't use globals. I actually can't remember the last time I saw one used, there are so many better alternatives. If you find you need one it's a clue you're going down the wrong path.

Sinatra pass anonymous block

I am new to Sinatra and am trying to implement the following:
REST service with get method whose action block can be provided. Something like:
class C1
get '/something' do
<some action to be provided later>
end
post '/something' do
<some action to be provided later>
end
end
C1.new
C1.get_block = { "Hello from get" }
C1.post_block = { "Hello from post" }
Is it possible to do something like above? I am designing an intercepting service which can be used to perform different actions depending on conditions
The following should do the trick. I've added arrays so you can register multiple blocks that should be executed upon a GET/POST requests.
class C1
##get_blocks = []
##post_blocks = []
def register_get(block)
##get_blocks << block
end
def register_post(block)
##post_blocks << block
end
get '/something' do
##get_blocks.each do |block|
block.call(params)
end
end
post '/something' do
##post_blocks.each do |block|
block.call(params)
end
end
end
C1.new
C1.register_get { |params| "Hello from get" }
C1.register_post { |params| "Hello from post" }
I think #MartinKonecny answered the question using a very nice dynamic and effective approach...
...But please allow me to suggest a different approach, assuming that the code itself is static and that it is activated according to a set of conditions.
I know that using the Plezi framework you could alternate controllers for the same route, so that if one controller doesn't answer the request, the next one is tested.
I believe that Sinatra could be used in the same way, so that when the first route fails, the second route is attempted.
A short demonstration of the concept (not an actual app code), using the Plezi framework would look something like this:
require 'plezi'
listen
class C1
def index
"Controller 1 answers"
end
def hello
false
end
end
class C2
def show
"Controller 2 shows your request for: #{params[:id]}"
end
def hello
'Hello World!'
end
end
route '/(:id)', C1
route '/(:id)', C2
exit # exit the terminal to test this code.
This way, for the path localhost:3000/, the C1 class answers. But, the route for C1 fails the restful request for the path localhost:3000/1, so the route for C2 answers that request, attempting to show object with id==1.
This is easy to see when accessing the localhost:3000/hello route - the first route fails, and the second one is then attempted.
If you don't need to define the blocks dynamically, perhaps this approach - which I assume to be available also in Sinatra - will be more easy to code and maintain.
If this isn't available using Sinatra, you could probably imitate this approach using a "routing" class as a controller on your Sinatra app.
Another approach would be to specify the conditions in a case statement, calling different methods according to each situation.
class C1
get '/something' do
case
when true # condition
# do something
when true # condition
# do something
when true # condition
# do something
when true # condition
# do something
else
# do something
end
end
end

What does Sinatra::Base.condition actually do?

I've come across the sinatra condition method and am puzzled in how it works.
I have a piece of code:
def auth user
condition do
redirect '/login' unless user_logged_in?
end
end
Which checks to see if a user is logged for certain routes, an example route:
get '/', :auth => :user do
erb :index
end
The method user_logged_in? is defined in a helper file in the lib directory of the project:
def user_logged_in?
if session[:user]
#user = session[:user]
return #user
end
return nil
end
So, the question is:
How does the condition block know what the session[:user] contains, when at the get '/' route the session[:user] hasn't even been set?
The condition method is defined in the following GitHub page: sinatra base condition method
Thanks.
When you define a route, the key of each member of the options hash is called as a method, with the value passed as the arguments.
So when you do get '/', :auth => :user do ... the method auth gets called with the argument :user. This in turn calls the condition method with the block.
The condition method is actually defined just above where you link to which is a usage of it. It looks like this:
def condition(name = "#{caller.first[/`.*'/]} condition", &block)
#conditions << generate_method(name, &block)
end
The generate_method method converts the block into a method with the given name, and then this method is saved in the #conditions array. The contents of #conditions are then saved with the definition of the route, and #conditions is cleared ready for the next route definition.
At this point, the block of code passed to condition hasn't been executed. It has in effect been saved for later.
When an actual request comes in, if the request path matches the route, then each condition associated with that route is executed to check that it is fulfilled. In this example, this is when redirect '/login' unless user_logged_in? is first executed, so the session will have been set up and session[:user] will be available (or not if they're not logged in).
The important thing to understand about this is that when you pass a block to a method, the code in that block is not necessarily called right away. In this case the code in the block is only called when an actual request arrives.
Because Sinatra is responsible for calling both the condition methods and the route methods. Therefore, it should be safe to assume that whatever is set when your route method executes is also set when your condition execute.
Take a look at the code starting here: conditions are called one by one; if all conditions match, then the block gets called. Nothing much happens between checking conditions and calling the block: they are basically run with the same context.

How to have a gem controller handle multiple arbitrary models?

I have four models that I allow commenting on by four separate comment controllers. Those four comment controllers do essentially the same thing and vary only slightly.
In an attempt to remove duplication of the four commenting controllers which are essentially all the same, I've created a Rails Engine as a gem to arbitrarily handle commenting on any arbitrary model that I specify in the routes.rb.
So in my routes.rb file I can now use:
comments_on :articles, :by => :users
with comments_on implemented as follows in my gem:
def comments_on(*resources)
options = resources.extract_options!
[snip of some validation code]
topic_model = resources.first.to_s
user_model = options[:by].to_s
# Insert a nested route
Rails.application.routes.draw do
resources topic_model do
resources "comments"
end
end
end
The routes show up in 'rake routes' and requests correctly get routed to my gem's 'CommentsController' but that's where my gem's functionality ends.
What is the best way detect the context in my gem CommentsController so I can process requests specific to how comments_on was called?
More specifically, how would I implement an index action like the following, having it context aware?
def index
#article = Article.find(params[:article_id])
#comments = ArticleComment.find(:all, :conditions => { :article_id => #article.id })
end
Thanks for the help!
You could specify the topic as an extra parameter in your routes:
Rails.application.routes.draw do
resources topic_model do
resources "comments", :topic_model => topic_model.to_s
end
end
Then your controller could be written like this:
def index
#topic = topic
#comments = topic.comments
end
protected
def topic
m = params[:topic_model]
Kernel.const_get(m).find(params["#{m.underscore}_id"])
end
You could move a lot of the logic out of the controller and into the model as well. topic.comments could be a named scope that all of these models should implement.
I've done similar patterns in the past and there's usually an edge-case that breaks this idea down and you end up doing more 'meta' programming than is wise.
I'd recommend making a base controller, then making simplistic controllers that inherit from that, or try to split these common behaviors into modules.

Calling Sinatra from within Sinatra

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.
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
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
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.
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
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

Resources