How to use DSL metaprogramming with Sinatra - ruby

I'm trying to work on a DSL to manage different locales within the same route, like get "/test". The
This is an exercise to learn how to extend Sinatra, therefore Rack::Locale or a similar tool is not a valid answer.
Based on the body of the request JSON body, assuming I receive JSON as POST or PUT, I want to respond with the specific locale.
I currently have a barebones script, of what I think I need:
class Locale
attr_reader :locale_id
attr_reader :described_class
alias :current_locale :locale_id
def initialize(locale_id, &block)
#locale_id = locale_id
instance_eval &block
end
end
def locale(locale_id, &block)
Locale.new(locale_id, &block)
end
I am missing the capability to respond based on the locale in the request.body JSON I receive as input, and the class here has something else I do not yet see that is needed or is missing.
An example of how this would get used would be:
get '/' do
locale 'cs-CS' do
"Czech"
#or db query or string
end
locale 'en-UK' do
"British english"
#or db query or string
end
end
Therefore to try to clarify even more clearly I will try with a TDD approach:
As User when I send a JSON that contains: "locale": "cs-CS" the result is Czech.

Have you read Extending The DSL and the Conditions section of the README?
Right now, you're not really extending the DSL. I'd redesign it slightly, because it looks like you'd want to match on a case statement but that would mean creating lots of classes or an ugly matching statement. But, Sinatra already has some really nice ways to match on routes and conditions. So, something like this would be more idiomatic:
post '/', :locale => "Czech" do
"Czech"
end
post '/', :locale => "British English" do
"British"
end
or
post '/', :locale => "en-GB" do
"cs-CS"
end
post '/', :locale => "cs-CS" do
"cs-CS"
end
How to do this? First, you'll need a filter to transform the JSON coming in:
before do
if request.media_type == "application/json"
request.body.rewind
#json = JSON.parse request.body.read
#locale = #json["locale"] && Locales[#json["locale"]]
end
end
and then you'll need a condition to check against:
set(:locale) {|value|
condition {
!!#locale && (#locale == value || #json["locale"] == value)
}
}
All together (app.rb):
require 'sinatra'
Locales = {
'cs-CS' => "Czech",
'en-GB' => "British English"
}
before do
if request.media_type == "application/json"
request.body.rewind
#json = JSON.parse request.body.read
#locale = #json["locale"] && Locales[#json["locale"]]
end
end
set(:locale) {|value|
condition {
!!#locale && (#locale == value || #json["locale"] == value)
}
}
post '/', :locale => "en-GB" do
"cs-CS"
end
post '/', :locale => "cs-CS" do
"cs-CS"
end
That works but it won't work as an extension. So, relying the docs I posted at the top:
require 'sinatra/base'
module Sinatra
module Localiser
Locales = {
'cs-CS' => "Czech",
'en-GB' => "British English"
}
def localise!(locales=Locales)
before do
if request.media_type == "application/json"
request.body.rewind
#json = JSON.parse request.body.read
#locale = #json["locale"] && locales[#json["locale"]]
end
end
set(:locale) {|value|
condition {
!!#locale && (#locale == value || #json["locale"] == value)
}
}
end
end
register Localiser
end
Now it will extend the DSL. For example:
require "sinatra/localiser"
class Localised < Sinatra::Base
register Sinatra::Localiser
localise!
post '/', :locale => "Czech" do
"Czech"
end
post '/', :locale => "British English" do
"British"
end
["get","post"].each{|verb|
send verb, "/*" do
"ELSE"
end
}
run! if app_file == $0
end
Hopefully that helps clarify a few things for you.

Related

is there a proper way to use is_a? with an instance_double?

I have real world code which does something like:
attr_reader :response
def initialize(response)
#response = response
end
def success?
response.is_a?(Net::HTTPOK)
end
and a test:
subject { described_class.new(response) }
let(:response) { instance_double(Net::HTTPOK, :body => 'nice body!', :code => 200) }
it 'should be successful' do
expect(subject).to be_success
end
This fails because #<InstanceDouble(Net::HTTPOK) (anonymous)> is not a Net::HTTPOK
... The only way I have been able to figure out how to get around this is with quite the hack attack:
let(:response) do
instance_double(Net::HTTPOK, :body => 'nice body!', :code => 200).tap do |dbl|
class << dbl
def is_a?(arg)
instance_variable_get('#doubled_module').send(:object) == arg
end
end
end
end
I can't imagine that I am the only one in the history ruby and rspec that is testing code being that performs introspection on a test double, and therefore think there has got to be a better way to do this-- There has to be a way that is_a? just will work out the box with a double?
I would do:
let(:response) { instance_double(Net::HTTPOK, :body => 'nice body!', :code => 200) }
before { allow(response).to receive(:is_a?).with(Net::HTTPOK).and_return(true) }

How to check response code use rest-client Resource

I'm fairly new to Ruby. I'm trying to write a RSpec test against the following class:
require 'rest-client'
class Query
def initialize
##log = Logger.new(STDOUT)
RestClient.log = ##log
##user = "joe#example.com"
##password = "joe123"
end
def get_details
begin
url = "http://api.example.com/sample/12345"
resource = RestClient::Resource.new(url, :user => ##user,
:password => ##password, :content_type => :json, :accept => :json)
details = resource.get
rescue => e
throw e # TODO: something more intelligent
end
end
end
I've discovered that unlike RestClient.get which returns a Response, Resource.get returns the body of the response as a String. I'd like to get Response working, because I will want to expand this to make different sub-resource calls.
Is there a way that I can find out the HTTP status code of the GET call response? That would allow me to write a test like:
require 'rspec'
require_relative 'query'
describe "Query site" do
before :all do
#query = Query.new
end
it "should connect to site" do
details = #query.get_details
expect(details).to_not be_nil
expect(details.code).to eq(200)
expect(details.body).to match /description12345/
end
end
Get returns an instance of the class RestClient::Response that inherits from the String class.
You can still check the return code by calling the method code details.code. Other methods are for example details.headers and details.cookies

Before filter on condition

I have a Sinatra app where all routes require a user login by default. Something like this:
before do
env['warden'].authenticate!
end
get :index do
render :index
end
Now I would like to use a custom Sinatra condition to make exceptions, but I cannot find a way to read if the condition is true/false/nil
def self.public(enable)
condition {
if enable
puts 'yes'
else
puts 'no'
end
}
end
before do
# unless public?
env['warden'].authenticate!
end
get :index do
render :index
end
get :foo, :public => true do
render :index
end
Since the authentication check must be done even if the condition is not defined, I guess I still must use a before filter, but I am not sure how to access my custom condition.
I was able to solve this using Sinatra's helpers and some digging into Sinatra's internals. I think this should work for you:
helpers do
def skip_authentication?
possible_routes = self.class.routes[request.request_method]
possible_routes.any? do |pattern, _, conditions, _|
pattern.match(request.path_info) &&
conditions.any? {|c| c.name == :authentication }
end
end
end
before do
skip_authentication? || env['warden'].authenticate!
end
set(:authentication) do |enabled|
condition(:authentication) { true } unless enabled
end
get :index do
render :index
end
get :foo, authentication: false do
render :index
end

Ruby JSON issue

I know the title is a bit vague, but I dont know what to put on there.
I'm developing an API with Sinatra for our backend in Ruby. The thing is that I need to be able to pass JSON to the service representing a User. The problem I'm facing is that when I run my tests it does not work, but doing it manually against the service it does work. I'm guessing there is an issue with the JSON format.
I've updated my User model to rely on the helpers from ActiveModel for the JSON serialization. I was running in too much problems with manual conversions. This is what the base User model looks like:
class User
include ActiveModel::Serializers::JSON
attr_accessor :login, :email, :birthday, :created_at, :updated_at, :password_sha, :password_salt
# Creates a new instance of the class using the information stored
# in the hash. If data is missing then nill will be assigned to the
# corresponding property.
def initialize(params = {})
return if params.nil?
self.login = params[:login] if params.key?("login")
self.email = params[:email] if params.key?("email")
self.birthday = Time.parse(params[:birthday]) rescue Time.now
if params.key?("password_salt") && params.key?("password_sha")
self.password_salt = params["password_salt"]
self.password_sha = params["password_sha"]
elsif params.key?("password")
self.set_password(params[:password])
end
self.created_at = Time.now
end
def attributes
{:login => self.login, :email => self.email, :birthday => self.birthday, :created_at => self.created_at, :updated_at => self.updated_at, :password_sha => self.password_sha, :password_salt => self.password_salt}
end
def attributes=(params = {})
self.login = params['login']
self.email = params['email']
self.birthday = params['birthday']
self.created_at = params['created_at']
self.updated_at = params['updated_at']
self.password_sha = params['password_sha']
self.password_salt = params['password_salt']
end
end
I'm using Cucumber, Rack::Test and Capybara to test my API implementation.
The code of the API application looks like this:
# This action will respond to POST request on the /users URI,
# and is responsible for creating a User in the various systems.
post '/users' do
begin
user = User.new.from_json(request.body.read)
201
rescue
400
end
end
In the above piece I expect the json representation in the request body. For some reason the params hash is empty here, don't know why
The test section that makes the actuall post looks like this:
When /^I send a POST request to "([^\"]*)" with the following:$/ do |path, body|
post path, User.new(body.hashes.first).to_json, "CONTENT_TYPE" => "application/json"
end
The example output JSON string generated by the User.rb file looks like this:
"{"user":{"birthday":"1985-02-14T00:00:00+01:00","created_at":"2012-03-23T12:54:11+01:00","email":"arne.de.herdt#gmail.com","login":"airslash","password_salt":"x9fOmBOt","password_sha":"2d3afc55aee8d97cc63b3d4c985040d35147a4a1d312e6450ebee05edcb8e037","updated_at":null}}"
The output is copied from the Rubymine IDE, but when I submit this to the application, I cannot parse it because:
The params hash is empty when using the tests
doing it manually gives me the error about needing at least 2 octets.

RSpec mock or stub super in a model

How do I test this tiny part of the module, with super? (superclass is action_dispatch-3.0.1 testing/integration...) The module is included within spec/requests to intercept post:
module ApiDoc
def post(path, parameters = nil, headers = nil)
super
document_request("post", path, parameters, headers) if ENV['API_DOC'] == "true"
end
...
end
I don't want it to run the ActionDispatch::Integration-whatever, but I don't know how to mock or stub super to unit test it.
The module is only used within specs, and will have 100% test coverage, which proves those kinds of metrics as useless. I need to unit test it.
An example, if needed, this is how I use the module ApiDoc
require 'spec_helper'
describe "Products API" do
include ApiDoc ############## <---- This is my module
context "POST product" do
before do
#hash = {:product => {:name => "Test Name 1", :description => "Some data for testing"}}
end
it "can be done with JSON" do
valid_json = #hash.to_json
############### the following 'post' is overriden by ApiDoc
post("/products.json",valid_json,
{"CONTENT_TYPE" => "application/json",
"HTTP_AUTHORIZATION" => ActionController::HttpAuthentication::Basic.encode_credentials("user", "secret")})
response.should be_success
end
end
end
You can check if the method is called on the 'super' class
ActionDispatch::Integration.any_instance.should_receive(:post)
Since ApiDock is only required for your tests you could also overwrite the post method with alias_method_chain:
ActionDispatch::Integration.instance_eval do
def post_with_apidoc(path, parameters = nil, headers = nil)
post_without_apidoc
if ENV['API_DOC'] == "true"
document_request("post", path, parameters, headers)
end
end
alias_method_chain :post, :apidoc
end
This is merely a supplement to the answer. This is how I ended up testing it
require 'spec_helper'
describe 'ApiDoc' do
include ApiDoc
it "should pass the post to super, ActionDispatch" do
#path = "path"
#parameters = {:param1 => "someparam"}
#headers = {:aheader => "someheaders"}
ActionDispatch::Integration::Session.any_instance.expects(:post).with(#path, #parameters, #headers)
post(#path, #parameters, #headers)
end
end
class DummySuper
def post(path, parameters=nil, headers=nil)
#How to verify this is called?
end
end
class Dummy < DummySuper
include ApiDoc
end
describe Dummy do
it "should call super" do
subject.expects(:enabled?).once.returns(true)
#how to expect super, the DummySuper.post ?
path = "path"
parameters = {:param1 => "someparam"}
headers = {:aheader => "someheaders"}
subject.expects(:document_request).with("post", path, parameters, headers)
subject.post(path, parameters, headers)
end
end
and the slightly modified ApiDoc.
module ApiDoc
def enabled?
ENV['API_DOC'] == "true"
end
def post(path, parameters = nil, headers = nil)
super
document_request("post", path, parameters, headers) if enabled?
end
private
def document_request(verb, path, parameters, headers)
...
end
end
I could verify the super.post in the first test, but I still can't figure out how to do just that with my Dummy class specs.

Resources