Sinatra/Rack session.fetch producing unexpected results - ruby

I was writing a quick helper in Sinatra for redirect_to_next, where I redirect to the path provided by session[:next] if it exists, or a default.
In Sinatra, session really provided by Rack, and by spec, it is said to provide a hash like interface for fetch. I wrote the following error helper to explain my problem.
error 401 do
session[:next] = request.path
puts "get #{session[:next]}"
puts "fetch #{session.fetch(:next, '/')}"
redirect "/login"
end
When I attempt to access /settings when not logged in, I halt 401 which runs the above code. This is what it prints to my terminal:
get /settings
fetch /
:next exists as a key, so why is it giving me the default as if it does not?
Update
This minimal example shows the same behavior.
require 'sinatra'
set :sessions, true
get '/' do
session[:testing] = "hello"
puts "get #{session[:testing]}"
puts "fetch #{session.fetch(:testing, 'goodbye')}"
end
Logs
[2012-04-29 14:11:51] INFO WEBrick::HTTPServer#start: pid=1954 port=9292
get hello
fetch goodbye
10.0.2.2 - - [29/Apr/2012 14:11:54] "GET / HTTP/1.1" 200 - 0.0485
Software
ruby (1.9.3p194)
rack (1.4.1)
sinatra (1.3.2)

The session hash isn’t a normal Ruby Hash, it’s a Rack::Session::Abstract::SessionHash. SessionHash actually inherits from Hash, but it overrides the []= and [] methods, calling to_s on any keys before storing and retrieving them.
Extending your update example:
require 'sinatra'
set :sessions, true
get '/' do
session[:testing] = "hello"
puts "get #{session[:testing]}"
puts "fetch #{session.fetch(:testing, 'goodbye')}"
puts "fetch with string #{session.fetch(:testing.to_s, 'goodbye')}"
end
gives this output:
get hello
fetch goodbye
fetch with string hello
When you use Hash#fetch, passing a symbol, the method gets dispatched directly to the parent hash, without being converted to a string, and so the matching key isn’t found.
So, always use Strings as keys in your sessions and everything should work.

Related

Altering params in Sinatra before blocks

I am trying to alter the request params in a before block in Sinatra.
It seems that when using this syntax, it works:
before do
#params['global-before'] = 'yes'
end
But when using this syntax, it does not work:
before '/:id/test' do
#params['route-before'] = 'yes'
end
Here is a full example:
# test.rb
require 'sinatra'
require 'sinatra/reloader'
set :bind, '0.0.0.0'
set :port, 3000
before do
#params['global-before'] = 'yes'
end
before '/:id/test' do
#params['route-before'] = 'yes'
end
get '/' do
params.to_json
end
get '/:id/test' do
params.to_json
end
Then running:
$ ruby test.rb
$ curl localhost:3000
{"global-before":"yes"}
$ curl localhost:3000/123/test
{"global-before":"yes","id":"123"}
I was expecting to see the params['route-before'] populated as well.
I have tried using request.params instead of #params but that did not work at all.
Can anyone shed some light on this?
Update:
Opened an issue in Sinatra's issue tracker
The route filter goes first, and it has route parameters: {"id"=>"123"}, so this happens:
original, #params = #params, #params.merge(params) if params.any?
where original ends up as {} and #params as {"id"=>"123"}. When the global filter runs, there are no route parameters, so original remains unassigned (nil) and #params is the {} that was originally there.
After the filter processes, in the ensure clause, there is this:
#params = original if original
So global filter skips it, because original is nil, because there were no route parameters. The route filter resets the #params to whatever it was before the filter ran, because original is preserved, because there were route parameters.
I can't say whether this is a bug or an intended behaviour, but it's a "how" at least, if not a "why". It may make sense asking the Sinatra team (and reporting back here with the verdict).
tl;dr: #params are reset to pre-filter state if there are parameters in the filter's path pattern.
Note: you can hack around it by making your own instance variable:
before '/:id/test' do
#route_before = 'yes'
end
get '/:id/test' do
"Did we run route before? #{#route_before}"
end

How to use login credentials from yaml in ruby

I'm new in ruby and I can't move forward from using login cred. from a yml file for a ruby project .I have a basic yml file
login:
urls:
gmail: "https://accounts.google.com/signin"
users:
username: something
password: something_new
I've created a yml.rb with require yml ,and access the yml path & loading the file .
But I don't know how to go through users/username ... in my test.rb :( .I've added in the "it " a variable to store the yml class and at the end i'm trying with
expect data['valid_user']
expect data['login']['urls']['gmail']
expect data['login']['users']['username']
but in the terminal I receive th error "NoMethodError: undefined method `[]' for nil:NilClass "
Update
Here is my yml.rb
require 'rspec'
require 'yaml'
class YamlHelper
#env = {}
def initialize
file = "#{Dir.pwd}path of yml file"
#env = YAML.load_file(file)
end
def get_variables
#env
end
end
Here is my test.rb file
describe 'My behaviour' do
before(:each) do
#browser = Selenium::WebDriver.for :firefox
end
it 'verifies yml login' do
yaml_helper = YamlHelper.new
data = yaml_helper.get_variables
expect data['valid_user']
expect test_data['login']['urls']['gmail']
expect test_data['login']['users']['username']
expect test_data['login']['users']['password']
end
after(:each) do #browser.quit
end
Can anyone take a look ?
thanks in advance
Have a lovely day
It looks like the code is almost there. When I'm debugging this sort of thing I'll often try to distil it down to the most basic test first
Something like:
require 'yaml'
file = "#{Dir.pwd}/data.yml"
data = YAML.load_file(file)
data['valid_user']
#=> nil
data['login']['urls']['gmail']
#=> "https://accounts.google.com/signin"
data['login']['users']['username']
#=> "something"
From the above you can see there's probably a typo in your test.rb file: test_data should most likely be data. Also, your YAML file doesn't contain the valid_user key, so you probably want to remove it from the test, at least for now.
The other two keys load fine.
The error you're seeing NoMethodError: undefined method '[]' for nil:NilClass means that one of the hashes you're variables you're treating like a hash is actually nil. This sort of bug is fairly common when you're diving into nested hashes. It means one of two things:
You've correctly descended into the hash, but the data is not present in the YAML.
The data is present in the YAML, but you're not getting to it correctly.
One change you can make that will make this code a bit more resilient is to replace:
test_data['login']['users']['username']
with:
test_data.dig('login', 'users', 'username')
The latter uses dig, which delves into the data structure and tries to return the value you're after, but if it gets a nil back at any point it'll just return nil, rather than throwing an exception.
Finally, for the test you've pasted here, you don't need the before(:each) or after(:each) blocks – Selenium is only necessary for browser testing.

How do I get all requests in puma rack app?

I'm writing a dead-simple puma server,
and I keep getting 404 for every request.
How do I configure the '/' path without Sinatra?
I simply want to catch all requests.
#config.ru
module Moon
class HelloWorldApp
def call(env)
[200, {}, 'Hello World']
end
def each(env=nil)
env
end
end
end
run Rack::Cascade.new Moon::HelloWorldApp.new
I run it like this:
$ puma config.ru -p 9595
Use this curl:
$ curl http://0.0.0.0:9595/test
And get 404:
127.0.0.1 - - [07/Apr/2015:22:49:25 +0300] "GET /test HTTP/1.1" 404 - 0.0002
Rack::Cascade expects an explicit array of apps to be passed to the constructor (or at least something like an enumerable that responds to each and yields the apps). It then calls each on this array to get each app (it looks like you’ve hit the no method error and tried to work round it by adding an each method to your app).
Fix it by changing the run line to:
run Rack::Cascade.new [Moon::HelloWorldApp.new]
You will also need to change the body part of your returned array, it needs to be something that responds to each and yields Strings, the simplest way to fix that is to return an array:
[200, {}, ['Hello World']]

Getting Rack mounted path in Sinatra application

Suppose I have the following config.ru file
require './status.rb'
map "/status" do
run Sinatra::Application
end
and the status.rb is a simple
require 'sinatra'
get '/' do
'Some status here...'
end
I'd like to know where the Sinatra application is mounted inside status.rb (for example to provide proper paths to resources). Is there a way of retrieving that information from Rack?
To get where the app is mounted you can use request.script_name.
get '/' do
p request.script_name # will print "/status"
'Some status here...'
end
If you’re generating urls for resources, you might want to look at the url method instead. That will take into account things like proxies as well as where the app is mounted:
get '/' do
p url('foo') # will print "http://localhost:9292/status/foo"
'Some status here...'
end

Metaprogramming sinatra get

I have a list of words in a list, and I want to handle get requests to any of them (and respond the same way).
#words = ["foo","bar"....etc]
One of the ways I thought I could do this is to loop through the list and have a get directive generated for each word when sinatra is launched.
#words.each do |word|
get word do
# what to do
end
end
that doesn't work, but something in that fashion, maybe.
Another way of doing it might be responding to get %r{/(.+)} and then doing some processing inside there to see if it matches anything in the list and respond accordingly, but I'm interested nonetheless to see if there's a way I can do it as described above.
What you wrote does work, with a very minor change. For example:
require 'sinatra'
%w[foo bar].each do |word|
get "/#{word}" do
"You said #{word}!"
end
end
$ curl http://localhost:4567/bar
You said bar!
Instead of a catch-all regex route, you can craft a custom regexp dynamically to match only the words you want to match:
require 'sinatra'
words = %w[foo bar]
route_re = %r{^/(#{words.join '|'})$}i # case-insensitive
p route_re
#=> /^\/(foo|bar)$/i
get route_re do |name|
"You said #{name}!"
end
$ curl http://localhost:4567/FoO
You said FoO!
$ curl -I http://localhost:4567/jim
HTTP/1.1 404 Not Found
X-Cascade: pass
Content-Type: text/html;charset=utf-8
Content-Length: 413
Connection: keep-alive
Server: thin 1.2.7 codename No Hup
Depending on what you need, this might be enough:
get '/:word' do
# check if params[:word] is in your list
# and respond accordingly
if #words.include?(params[:word])
"yeeah"
else
pass
end
end
But keep in mind that this route matches everything.

Resources