Wrong number of arguments on Rack::Builder.new - ruby

I apparently have a Rack::Builder misunderstanding. Inside my config.ru file i've got:
require 'rack'
require 'rack/lobster'
class Shrimp
SHRIMP_STRING = 'teste'
def initialize(app)
#app = app
end
def call(env)
status, headers, response = #app.call(env)
response_body = ""
response.each { |part| response_body += part }
response_body += "<pre>#{SHRIMP_STRING}</pre>"
headers["Content-Length"] = response_body.length.to_s
[status, headers, response_body]
end
end
app = Rack::Builder.new do
use Rack::Lobster
run Shrimp.new
end
Rack::Handler::WEBrick.run app
When I do a
rackup config.ru
I get a
/home/vagrant/config.ru:7:in `initialize': wrong number of arguments (0 for 1) (ArgumentError)
from /home/vagrant/config.ru:26:in `new'
Am I missing something? According to this tutorial Rack::Builder.new only receives a block as a parameter.
EDIT:
changing this line
run Shrimp.new
to:
run Shrimp
I still get a wrong number of arguments, but this time for Rack::Builder
ERROR ArgumentError: wrong number of arguments (1 for 0)
/home/vagrant/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/rack-1.5.2/lib/rack/builder.rb:86:in `initialize'

For Rack middleware, you don't need to do Shrimp.new, You just need to do use Shrimp and it should do.
You can find it's one example here.
As par this link you only need to do following:
# config.ru
require 'rack'
require 'rack/lobster'
require 'shrimp'
use Shrimp
run Rack::Lobster.new

Related

How to test a Ruby Roda app using RSpec to pass an argument to app.new with initialize

This question probably has a simple answer but I can't find any examples for using Roda with RSpec3, so it is difficult to troubleshoot.
I am using Marston and Dees "Effective Testing w/ RSpec3" book which uses Sinatra instead of Roda. I am having difficulty passing an object to API.new, and, from the book, this is what works with Sinatra but fails with a "wrong number of arguments" error when I substitute Roda.
Depending on whether I pass arguments with super or no arguments with super(), the error switches to indicate that the failure occurs either at the initialize method or in the call to Rack::Test::Methods post in the spec.
I see that in Rack::Test, in the Github repo README, I may have to use Rack::Builder.parse_file("config.ru") but that didn't help.
Here are the two errors that rspec shows when using super without brackets:
Failures:
1) MbrTrak::API POST /users when the user is successfully recorded returns the user id
Failure/Error: post '/users', JSON.generate(user)
ArgumentError:
wrong number of arguments (given 1, expected 0)
# ./spec/unit/app/api_spec.rb:21:in `block (4 levels) in <module:MbrTrak>'
And when using super():
1) MbrTrak::API POST /users when the user is successfully recorded returns the user id
Failure/Error: super()
ArgumentError:
wrong number of arguments (given 0, expected 1)
# ./app/api.rb:8:in `initialize'
# ./spec/unit/app/api_spec.rb:10:in `new'
# ./spec/unit/app/api_spec.rb:10:in `app'
# ./spec/unit/app/api_spec.rb:21:in `block (4 levels) in <module:MbrTrak>'
This is my api_spec.rb:
require_relative '../../../app/api'
require 'rack/test'
module MbrTrak
RecordResult = Struct.new(:success?, :expense_id, :error_message)
RSpec.describe API do
include Rack::Test::Methods
def app
API.new(directory: directory)
end
let(:directory) { instance_double('MbrTrak::Directory')}
describe 'POST /users' do
context 'when the user is successfully recorded' do
it 'returns the user id' do
user = { 'some' => 'user' }
allow(directory).to receive(:record)
.with(user)
.and_return(RecordResult.new(true, 417, nil))
post '/users', JSON.generate(user)
parsed = JSON.parse(last_response.body)
expect(parsed).to include('user_id' => 417)
end
end
end
end
end
And here is my api.rb file:
require 'roda'
require 'json'
module MbrTrak
class API < Roda
def initialize(directory: Directory.new)
#directory = directory
super()
end
plugin :render, escape: true
plugin :json
route do |r|
r.on "users" do
r.is Integer do |id|
r.get do
JSON.generate([])
end
end
r.post do
user = JSON.parse(request.body.read)
result = #directory.record(user)
JSON.generate('user_id' => result.user_id)
end
end
end
end
end
My config.ru is:
require "./app/api"
run MbrTrak::API
Well roda has defined initialize method that receives env as an argument which is being called by the app method of the class. Looks atm like this
def self.app
...
lambda{|env| new(env)._roda_handle_main_route}
...
end
And the constructor of the app looks like this
def initialize(env)
When you run your config.ru with run MbrTrack::API you are actually invoking the call method of the roda class which looks like this
def self.call(env)
app.call(env)
end
Because you have redefined the constructor to accept hash positional argument this no longer works and it throws the error you are receiving
ArgumentError:
wrong number of arguments (given 0, expected 1)
Now what problem are you trying to solve, if you want to make your API class configurable one way to go is to try out dry-configurable which is part of the great dry-ruby gem collection.
If you want to do something else feel free to ask.
It has been a long time since you posted your question so hope you will still find this helpful.

How to test if some specific rack middleware is being used?

To be more particular, I'm talking about sentry-raven and sinatra here. I saw examples testing sinatra applications, or middlewares. But I didn't see ones testing if some particular middleware is present. Or should I be testing behavior, not configuration (or how should I call it)?
The important thing (I'd say) is the behaviour, but if you wish to check for middleware there are 2 ways I'd suggest after taking a delve into the Sinatra source (there are possibly much easier/better ways):
The env
In the Sinatra source there's a method that uses the env to check if a middleware is already present:
# Behaves exactly like Rack::CommonLogger with the notable exception that it does nothing,
# if another CommonLogger is already in the middleware chain.
class CommonLogger < Rack::CommonLogger
def call(env)
env['sinatra.commonlogger'] ? #app.call(env) : super
end
You could do the same thing in a route, e.g.
get "/env-keys" do
env.keys.inspect
end
It'll only show you the middleware if it's inserted something in env hash, e.g.
class MyBad
def initialize app, options={}
#app = app
#options = options
end
def call env
#app.call env.merge("mybad" => "I'm sorry!")
end
end
output:
["SERVER_SOFTWARE", "SERVER_NAME", "rack.input", "rack.version", "rack.errors", "rack.multithread", "rack.multiprocess", "rack.run_once", "REQUEST_METHOD", "REQUEST_PATH", "PATH_INFO", "REQUEST_URI", "HTTP_VERSION", "HTTP_HOST", "HTTP_CONNECTION", "HTTP_CACHE_CONTROL", "HTTP_ACCEPT", "HTTP_USER_AGENT", "HTTP_DNT", "HTTP_ACCEPT_ENCODING", "HTTP_ACCEPT_LANGUAGE", "GATEWAY_INTERFACE", "SERVER_PORT", "QUERY_STRING", "SERVER_PROTOCOL", "rack.url_scheme", "SCRIPT_NAME", "REMOTE_ADDR", "async.callback", "async.close", "rack.logger", "mybad", "rack.request.query_string", "rack.request.query_hash", "sinatra.route"]
It's near the end of that list.
The middleware method
There's also a method called middleware in Sinatra::Base:
# Middleware used in this class and all superclasses.
def middleware
if superclass.respond_to?(:middleware)
superclass.middleware + #middleware
else
#middleware
end
end
Call it in the class definition of a modular app and you can get the middlewares in an array:
require 'sinatra/base'
class AnExample < Sinatra::Base
use MyBad
warn "self.middleware = #{self.middleware}"
output:
self.middleware = [[MyBad, [], nil]]
There may be a way to get it from Sinatra::Application, but I haven't looked.
With help from ruby-raven guys, we've got this:
ENV['RACK_ENV'] = 'test'
# the app: start
require 'sinatra'
require 'sentry-raven'
Raven.configure(true) do |config|
config.dsn = '...'
end
use Raven::Rack
get '/' do
'Hello, world!'
end
# the app: end
require 'rspec'
require 'rack/test'
Raven.configure do |config|
logger = ::Logger.new(STDOUT)
logger.level = ::Logger::WARN
config.logger = logger
end
describe 'app' do
include Rack::Test::Methods
def app
#app || Sinatra::Application
end
class TestRavenError < StandardError; end
it 'sends errors to sentry' do
#app = Class.new Sinatra::Application do
get '/' do
raise TestRavenError
end
end
allow(Raven.client).to receive(:send).and_return(true)
begin
get '/'
rescue TestRavenError
end
expect(Raven.client).to have_received(:send)
end
end
Or if raven sending requests is in the way (when tests fail because of raven sending requests, and not because of the underlying error), one can disable them:
Raven.configure(true) do |config|
config.should_send = Proc.new { false }
end
And mock Raven.send_or_skip instead:
...
allow(Raven).to receive(:send_or_skip).and_return(true)
begin
get '/'
rescue TestRavenError
end
expect(Raven).to have_received(:send_or_skip)
...

Set Up for RSpec in a Sinatra modular app

This is my first attempt with Sinatra. I built a simple classic app, set up RSpec for it, and got it working. Then, I tried to go modular, in a MVC fashion. Even though the app works in the browser, RSpec throws a NoMethodError. I've read Sinatra docs regarding RSpec, also searched a lot here in SO, but I can't find where the bug is. Any clue?
Thank you very much in advance.
Here are my relevant files:
config.ru
require 'sinatra/base'
Dir.glob('./{app/controllers}/*.rb') { |file| require file }
map('/') { run ApplicationController }
app.rb
require 'sinatra/base'
class ZerifApp < Sinatra::Base
# Only start the server if this file has been
# executed directly
run! if __FILE__ == $0
end
app/controllers/application_controller.rb
class ApplicationController < Sinatra::Base
set :views, File.expand_path('../../views', __FILE__)
set :public_dir, File.expand_path('../../../public', __FILE__)
get '/' do
erb :index
end
end
spec/spec_helper.rb
require 'rack/test'
# Also tried this
# Rack::Builder.parse_file(File.expand_path('../../config.ru', __FILE__))
require File.expand_path '../../app.rb', __FILE__
ENV['RACK_ENV'] = 'test'
module RSpecMixin
include Rack::Test::Methods
def app() described_class end
end
RSpec.configure { |c| c.include RSpecMixin }
spec/app_spec.rb
require File.expand_path '../spec_helper.rb', __FILE__
describe "My Sinatra Application" do
it "should allow accessing the home page" do
get '/'
expect(last_response).to be_ok
end
end
The error
My Sinatra Application should allow accessing the home page
Failure/Error: get '/'
NoMethodError:
undefined method `call' for nil:NilClass
# ./spec/app_spec.rb:5:in `block (2 levels) in <top (required)>'
I'm guessing you're following this recipe, correct?
The described_class in this line:
def app() described_class end
is meant to be the class under test, in this case ZerifApp. Try it like so:
def app() ZerifApp end
EDIT
It turns out the above answer is not correct about what described_class does. I assumed it was a placeholder -- actually it is an RSpec method that returns the class of the implicit subject, that is to say, the thing being tested.
The recipe at the link is misleading because of the way it recommends writing the describe block:
describe "My Sinatra Application" do
This is valid RSpec, but it does not define the subject class. Executing described_class in an example for this block will return nil. To make it work, replace the describe block:
describe ZerifApp do
Now described_class will return the expected value (ZerifApp)
https://pragprog.com/book/7web/seven-web-frameworks-in-seven-weeks
It has some source code to get some ideas from.
This has code example too. https://github.com/laser/sinatra-best-practices

Sinatra + Fibers + EventMachine

I would like to know to to pause a Root Fiber in ruby (if possible).
I have this Sinatra app, and I am making async calls to an external API with EventMachine.
I don't want to respond to the client until the api responds me.
For example, sleeping the Root Fiber in Sinatra until the EventMachine callback wake it up.
Thanks.
get '/some/route/' do
fib = Fiber.current
req = EM::SomeNonBlokingLib.request
req.callback do |response|
fib.resume(response)
end
req.errback do |err|
fib.resume(err)
end
Fiber.yield
end
EDIT
In your case you should spawn a Fiber for each request. So. Firstly create Rack config file and add some magick:
# config.ru
BOOT_PATH = File.expand_path('../http.rb', __FILE__)
require BOOT_PATH
class FiberSpawn
def initialize(app)
#app = app
end
def call(env)
fib = Fiber.new do
res = #app.call(env)
env['async.callback'].call(res)
end
EM.next_tick{ fib.resume }
throw :async
end
end
use FiberSpawn
run Http
Then your http Sinatra application:
# http.rb
require 'sinatra'
require 'fiber'
class Http < Sinatra::Base
get '/' do
f = Fiber.current
EM.add_timer(1) do
f.resume("Hello World")
end
Fiber.yield
end
end
Now you could run it under thin for example:
> thin start -R config.ru
Then if you will visit locakhost:3000 you'll see your Hello World message

Should I be using EM::Synchrony::Multi or EM::Synchrony::FiberIterator with Goliath?

Maybe this is the wrong approach, but I'm trying to parallelize em-hiredis puts and lookups in Goliath with EM::Synchrony::Multi or EM::Synchrony::FiberIterator. However, I can't seem to access basic values initialized in the config. I keep getting method_missing errors.
Here's the basic watered down version of what I'm trying to do:
/lib/config/try.rb
config['redisUri'] = 'redis://localhost:6379/0'
config['redis_db'] ||= EM::Hiredis.connect
config['user_agent'] = "MyCrawler Mozilla/5.0 Compat etc."
Here's the basic Goliath Setup
/try.rb
require "goliath"
require "em-hiredis"
require "em-synchrony/fiber_iterator"
require "em-synchrony/em-hiredis"
require "em-synchrony/em-multi"
class Try < Goliath::API
use Goliath::Rack::Params
use Goliath::Rack::DefaultMimeType
def response(env)
case env['REQUEST_PATH']
when "/start" then
start_crawl()
body = "STARTING"
[200, {}, body]
end
end
def start_crawl
urls = ["http://www.example.com/",
"http://www.example.com/photos/",
"http://www.example.com/video/",
]
EM::Synchrony::FiberIterator.new(urls, 3).each do |url|
p "#{user_agent}"
redis_db.sadd 'test_queue', url
end
# multi = EM::Synchrony::Multi.new
# urls.each_with_index do |url, index|
# p "#{user_agent}"
# multi.add index, redis_db.sadd('test_queue', url)
# end
end
end
However, I keep getting errors where Goliath doesn't know what user_agent is or redis_db which were initialized in the config.
[936:INFO] 2012-09-21 23:47:10 :: Starting server on 0.0.0.0:9000 in development mode. Watch out for stones.
/Users/ewu/.rvm/gems/ruby-1.9.3-p194#crawler/gems/goliath-1.0.0/lib/goliath/api.rb:143:in `method_missing': undefined local variable or method `user_agent' for #<Try:0x007ff5a431c4e0 #opts={}> (NameError)
from ./lib/try.rb:27:in `block in start_crawl'
from /Users/ewu/.rvm/gems/ruby-1.9.3-p194#crawler/gems/em-synchrony-1.0.2/lib/em-synchrony/fiber_iterator.rb:10:in `call'
from /Users/ewu/.rvm/gems/ruby-1.9.3-p194#crawler/gems/em-synchrony-1.0.2/lib/em-synchrony/fiber_iterator.rb:10:in `block (2 levels) in each'
...
...
...
Ideally I'd be able to get FiberIterator working, because I have additional conditionals to check for:
EM::Synchrony::FiberIterator.new(urls, 3).each do |new_url}
is_member = redis_db.sismember('crawled_urls', new_url)
is_member += redis_db.sismember('queued_urls', new_url)
if is_member == 0
redis_db.lpush 'crawl_queue', new_url
redis_db.sadd 'queued_urls', new_url
end
end
I don't think your config file is getting loaded. The name of try.rb needs to match the name of the robojin.rb file in the config directory.

Resources