Before filter on condition - filter

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

Related

How to use DSL metaprogramming with Sinatra

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.

Hanami parameters whitelisting

Following the hanami docs, in order to block a admin parameter inside an action, I can use the following configuration:
params do
required(:email).filled
required(:address).schema do
required(:country).filled
end
end
def call(params)
puts params[:email] # => "alice#example.org"
puts params[:address][:country] # => "Italy"
puts params[:admin] # => nil
end
But this does not work for nested parameters, i.e.:
params do
required(:email).filled
required(:address).schema do
required(:country).filled
end
end
def call(params)
puts params[:email] # => "alice#example.org"
puts params[:address] # => { country: "Italy", admin: true }
puts params[:address][:admin] # => true
end
I was able to solve this by using select to filter out the undesirable parameters with a private method, but this does not seems like the Hanami way. What would be the proper way to do this whitelisting of nested parameters?
I have never had this issue when using Hanami Validations. Within the app directory there should be a validations folder which should have the same directory structure as your controllers, views, templates etc. Your validation file should look something like this:
# apps/web/validations/users/create.rb
module Web
module Validations
module Users
class Create < Web::Action::Params
predicates Web::Validations::CommonPredicates
validations do
required(:email).filled
required(:address).schema do
required(:country).filled
end
end
end
end
end
end
And then your controller should set the params to be filtered through the validation:
module Web
module Controllers
module Users
class Create
include Web::Action
params Web::Validations::Users::Create
def call(params); end
end
end
end
end

How to make this Sinatra code DRYer?

These are my Sinatra routes. They pass the storename as an ActiveRecord class and the params with which my helpermethods do what they have to.
def self.get_or_post(url,&block)
get(url,&block)
post(url,&block)
end
get_or_post "/read" do
jsonp(read(params[:store].constantize, params))
end
get_or_post "/create" do
jsonp(create(params[:store].constantize, params))
end
get_or_post "/update" do
jsonp(update(params[:store].constantize, params))
end
get_or_post "/destroy" do
jsonp(destroy(params[:store].constantize, params))
end
It seems to me this can be made DRYer doing something like:
case route
when read, create, update, destroy
jsonp(method(route).call(params[:store].constantize, params))
else
# neglect or give error
end
How can I get this route variable, and is my use of method(route).call correct?
How about:
%w(read create update destroy).each do |action|
[:get, :post].each do |method|
send(method, "/#{action}") do
jsonp(send(action, params[:store].constantize, params))
end
end
end

Test helpers with RSpec in Padrino (Sinatra)

I'm trying to test a helper in a Padrino (Sinatra) app. My helper method is itself calling Padrino core helper methods but they are undefined. The error appears only in RSpec, while the app works fine. So the way I'm including my helper in RSpec makes it loose "Padrino scope" but I don't know how to bring Padrino helper's scope properly in my RSpec environment.
My helper:
module AdminHelper
Sort = Struct.new(:column, :order)
def sort_link(model, column)
order = sorted_by_this?(column) ? 'desc' : 'asc'
link_to mat(model, column), url(:pages, :index, sort: column, order: order)
end
def sorted_by_this?(column)
column.to_s == #sort.column && #sort.order == 'asc'
end
end
Lenstroy::Admin.helpers AdminHelper
My spec:
describe AdminHelper do
before(:all) do
class AdminHelperClass
include AdminHelper
end
end
subject(:helper) { AdminHelperClass.new }
describe '#sort_link' do
context "with :pages and :title parameters" do
before do
sort = AdminHelperClass::Sort.new('title', 'asc')
helper.instance_variable_set('#sort', sort)
end
subject { helper.sort_link(:pages, :title) }
it { should match(/<a href=([^ ]+)pages/) }
end
end
end
Results in error:
1) AdminHelper#sort_link with :pages and :title parameters
Failure/Error: subject { helper.sort_link(:pages, :title) }
NoMethodError:
undefined method `mat' for #<AdminHelperClass:0x007f1d951dc4a0>
Including a helper where mat is defined doesn't work, as one method is dependent on another helper and it goes on and on...
Update
In my spec helper I have:
def app(app = nil, &blk)
#app ||= block_given? ? app.instance_eval(&blk) : app
#app ||= Lenstroy::Admin
#app.register Padrino::Helpers
#app.register Padrino::Rendering
#app
end
in my spec I have:
it "returns link to resource with sort parameters" do
app do
get '/' do
sort_link(:pages, :title)
end
end
get "/"
last_response.body.should =~ /<a href=([^ >]+)pages/
end
And now tests fail, last_response.body is ''.
Method #mat is defined in Padrino::Admin::Helpers::ViewHelpers. You can do
class AdminHelperClass
include Padrino::Admin::Helpers::ViewHelpers
include AdminHelper
end
Update:
If your methods are really dependent on all these routes and helpers you should consider doing full mockup of your app like this:
def mock_app(base=Padrino::Application, &block)
#app = Sinatra.new(base, &block)
#app.register Padrino::Helpers
#app.register Padrino::Rendering
# register other things
end
def app
Rack::Lint.new(#app)
end
mock_app do
get '/' do
sort_link(my_model, my_column)
end
end
get "/"
assert_equal "some test text", body
Here's how it's done in padrino-admin: https://github.com/padrino/padrino-framework/blob/master/padrino-admin/test/test_admin_application.rb
I was having the same problem (and getting very frustrated tracking down the modules and including them). So far, I've got my specs working by:
1) Explicitly defining my module (as explained in how to use padrino helper methods in rspec)
module MyHelper
...
end
MyApp::App.helpers MyHelper
2) Automatically including helpers at the top of my spec. (Right now I only have one helper spec, but in the future I might try to move this into spec_helper.rb.)
describe MyHelper do
let(:helpers) { Class.new }
before { MyApp::App.included_modules.each { |m| helpers.extend m } }
subject { helpers }
it 'blah' do
expect(subject.helper_method).to eq 'foo'
end
end

MongoMapper custom validation

I have this ruby class with an array of links. As it is now I'm able to save a Paper object even if the array contains links that are not valid urls. I have a method that runs through the array and validates the urls and returns false if a url is invalid. But I want to get an error message when I try to call Paper.save. Is that possible?
class Paper
include MongoMapper::Document
key :links, Array
validates_presence_of :links
def validate_urls
reg = /^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$/ix
status = []
links.each do |link|
if link.match(reg)
status.push('true')
else
if "http://#{link}".match(reg)
status.push('true')
else
status.push('false')
end
end
end
if status.include?('false')
return false
else
return true
end
end
end
If you're using MongoMapper from GitHub (which supports ActiveModel), see http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validate
class Paper
include MongoMapper::Document
key :links, Array
validates_presence_of :links
validate :validate_urls
def validate_urls
reg = /^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$/ix
status = []
links.each do |link|
if link.match(reg)
status.push('true')
else
if "http://#{link}".match(reg)
status.push('true')
else
status.push('false')
end
end
end
if status.include?('false')
# add errors to make the save fail
errors.add :links, 'must all be valid urls'
end
end
end
Not sure if that code works with the 0.8.6 gem but it might.
Also, it doesn't apply in this case but if it weren't an array you could smash it all into a single line:
key :link, String, :format => /your regex here/

Resources