The gist
Is it possible to bake feature-toggle-like functionality into a Sinatra application?
A bit about feature toggles, just in-case ;)
Back story
I've set up a modular Sinatra project, and I tend to implement a GET/POST/PUT/DELETE endpoint for all my resources; it makes it easier to test the app and manipulate the data while in development.
Problem
When I go into production I don't want the unneeded endpoints to exist (e.g DELETE '/users').
Question
Can I annotate the methods with some kind of a :development flag, or maybe intercept the request in a before block? Would you do this using a helper? I'm not sure if I'm heading down the right path here, I'm probably over complicating it(?)
How would one go about this?
If you've done something like this it would be great if you can share your findings with the nation.
You can use the current environment to decide whether you define an action. For example:
class MyApp < Sinatra::Application
if settings.development?
get '/admin' do
'VIPs only'
end
end
end
If you have a lot to toggle, you might want to isolate them in one file that you can decide to require or not:
# routes/init.rb
require_relative 'main'
require_relative 'debug' if settings.development?
# routes/main.rb
class MyApp < Sinatra::Application
get '/' do
'Hello!'
end
end
# routes/debug.rb
class MyApp < Sinatra::Application
get '/admin' do
'VIPs only'
end
end
Or if you want to list your development-only paths in one place, here's a filter version:
class MyApp < Sinatra::Application
DEVELOPMENT_PATHS = %w[
/admin
]
before do
unless settings.development? || !DEVELOPMENT_PATHS.include?(request.path)
halt 404
end
end
end
Then you could also build some decorator-like methods that add to the list:
class MyApp < Sinatra::Application
def self.development_only(path)
DEVELOPMENT_PATHS << path
end
get '/admin' do
'VIPs only'
end
development_only '/admin
end
In general, I'd recommend caution when introducing significant differences between the code that runs in development vs. production. Inevitably, the dev code is either left untested or becomes cumbersome to maintain properly. In this case, there's the danger that you miss a route you intended to hide and it becomes available to everyone in production. I'd tend towards not having these routes at all and manipulating my dev environment from a console, or going all the way to the other end and building fully-tested and production-ready user permissions with something like sinatra-authentication.
Related
Lets say I have an image at app/assets/images/privateimages/myrestrictedimage1.jpg
If I try to go directly to the image via url say with something like
http://localhost:5555/assets/privateimages/myrestrictedimage1.jpg
I am able to view the image.
I would like to have a way to inspect any http request to decide if the user is allowed access to it.
I know I can use before_filter in controllers to do some preprocessing before continuing onto any of the controller actions but I dont think this will help me because I need to be attempting to do a controller action for this to take effect.
I have heard I might be able to do it with a rake task but after much searching I haven't found anything like what I am trying to do. Perhaps I have to create a ruby gem to do this but I have no clue how to do this.
Can anyone point me in the right direction? Thanks.
I used Rack Middleware
The middleware class looks like this:
class MyChecker
def initialize(app)
#app = app
end
def call(env)
if (docheck)
#do stuff here such as check the path.
#For example #path = env['PATH_INFO'] and compare against your okay paths
#if youre good and are able to continue then
#app.call(env)
else
#redirect such as
[301, {"Location" => /somewhere, "Content-Type" => "text/html"}, []]
end
end
end
make sure to make your middleware visible by adding the following to application.rb
class Application < Rails::Application
...
config.autoload_paths += %W(#{config.root}/lib) #if MyChecker is located in lib othewise substitute lib with wherever you have your middleware class
config.middleware.use "MyChecker"
end
You want to look at Rack (not rake).
I've just started writing a reasonably straightforward site using sinatra. My problem is that I wanted to refactor the main app.rb file but am getting errors trying to access the url params.
In my get '/' action, Sinatra's looking at which params are set and then needs to do a few different things depending on what's in the url. Something like this.
class App < Sinatra::Application
...
get '/' do
if params['code1']
#network = 'code1'
mode code here
elsif params['called'] && params['mac']
#network = 'code2'
mode code here
elsif params['code3']
#network = 'code3'
mode code here
end
end
The problem is that I need to require a file that also uses the params.
I've put the following in the above code:
require File.dirname(__FILE__) + '/lib/networks/code1.rb'
Where code1.rb includes:
class App < Sinatra::Application
if params['login'] # == 'login'
pass = 'uampass'
elsif
...
But that gives me the following error:
undefined local variable or method `params' for main:Object
How can I refactor this without causing an error
As far as i know you can't use two (or more) Sinatra applications in, well one application. Since both files define a Sinatra::Application descendant this isn't possible.
Also if you want to use values from the params-hash you should define helper methods Helper Documentation, which you call when processing the route, or you just create Class which has class or instance methods which take params-values as parameters. Actually calling params from another file/class doesn't seem like good practice.
To put this in context: Sinatra applications are organised as handlers. The Sinatra::Application descendant is something like the main handler which uses support methods(helpers and instance methods of the Sinatra::Application descendant) or support Classes, which are usually defined in other files, but do not descend from Sinatra::Application.
To make this a little bit more clearly:
Your main Sinatra file:
require_relative 'another_file.rb'
class App < Sinatra::Application
# ...
#a_handler = MyHandler.new
get '/' do
if params['something'] == 'wanted_value'
#a_handler.handle_it(params)
end
end
Another file ('another_file.rb'):
class MyHandler
def initialize
#an_instance_variable = 'foobar'
end
def handle_it(params_hash)
if params_hash['login'] # == 'login'
pass = 'uampass'
elsif
# ...
end
# ...
# do some stuff
# ....
return pass
end
end
Actual code would of course depend on the real problem you're trying to solve, so if you would elaborate i could be more precise...
The error message contains everything you need to know, and it's nothing to do with Sinatra.
You are requiring code1.rb, which contains this (slightly edited so it will run):
require 'sinatra'
class App < Sinatra::Application
if params['login'] # == 'login'
pass = 'uampass'
end
end
Ruby evaluates code as it encounters it. In this case, you've required 'code1.rb', so it evaluates the code in that file. It encounters 'params' and asks "is there a local variable or method with that name?". There isn't, so it fails as you've seen. Open an irb session and check it out.
Class definitions in ruby are just an expression with a scope.
In relation to Sinatra: params is available in the block that a route declaration takes.
I'd recommend reading Sinatra: Up and Running, which explains some of the 'magic' that is going on (a good companion to the Sinatra Book).
I want to be able to follow a convention closer to what Rails does with resourceful routing. For example, I'm considering "signups" to be a resource, with it's own controller containing "new" and "create" actions.
In app/controllers/signup.rb I have:
MyApp.controllers :signups do
get :index do
# ...
end
post :index do
# ...
end
end
Is there any way I can use these route names, while actually responding on a path other than '/signups'? It feels like Padrino's route naming system is very tightly coupled with the URLs the routes map to.
I've tried:
MyApp.controllers :signups, :map => '/another-path' do
# ...
end
Among various other things without success. Perhaps I should just go back to using Rails... I was just getting frustrated with the startup overhead in TDD and I'm embarking on a new project at the moment (please don't refer me to Spork... that has it's own issues).
This is how I would do what you are asking
# in app/controller/signups.rb
MyApp.controllers :'another-path' do
get '/' do
# ...
end
end
Lets say I'm writing a sinatra extension which mounts a second public directory at a given mount point.
require 'sinatra'
require 'sinatra/moar-public'
set :moar_local, './downloads/'
set :moar_remote, 'dls'
I now expect a user going to http://myapp.com/downloads/thing.bin to be given the file at [sinatra_root]/dls/thing.bin.
Writing this extension (obviously, it's a simplified example) I have something like this:
require 'sinatra/base'
module Sinatra
module MoarPublic
def self.registered(app)
app.set :moar_local, './downloads/'
app.set :moar_remote, 'downloads'
app.get "/#{app.options.moar_remote}/:filename" do
# Logic
end
end
end
register MoarPublic
end
But app.get has already been called with the default value for moar_remote so the download files are available at /downloads/thing.bin, not at /dls/thing.bin as I'd like. Any ideas?
You're asking for dynamic routes, but Sinatra compiles the route information so it won't work the way you're looking for.
As a work around, you might consider defining a catch-all route, and checking the route information inside the catch-all, e.g.
get %r{^/(*)/bar$} do |capture|
if settings.url_prefix == capture # or perhaps check against request.path_info
# get file
else
status 404
end
end
Obviously, there are still many things to be done there, but you get the drift.
I had no problem registering an extension explicitily in a modular configuration. Illustration below.
class Service < Sinatra::Base
set :url_prefix, 'foo'
register Common
end
module Common
def self.registered(app)
app.get "/#{app.options.url_prefix}/bar" do
"hello world"
end
end
end
I'm writing a Sinatra app which needs to render different layouts based on whether the user is using an iPhone or a regular browser. I can detect the browser type using Rack-Mobile-Detect but I'm not sure of the best way to tell Sinatra which layout to use.
Also, I have a feeling that how I choose to do this may also break page caching. Is that true?
Example code:
require 'sinatra/base'
require 'haml'
require 'rack/mobile-detect'
class Orca < Sinatra::Base
use Rack::MobileDetect
helpers do
def choose_layout
if request.env['X_MOBILE_DEVICE'] == :iPhone
# use iPhone layout
else
# use normal layout
end
end
end
before do
# should I use a before filter?
choose_layout()
end
get '/' do
haml :home # with proper layout
end
end #Class Orca
This is what I ended up doing:
require 'sinatra/base'
require 'haml'
require 'rack/mobile-detect'
class Orca < Sinatra::Base
use Rack::MobileDetect
# HAML template options
# Use HTML5 doctype
set :haml, {:format => :html5 }
helpers do
def get_layout
# For AJAX (XMLHttpRequest) requests, don't use a layout
if request.xhr? then
#layout = false
exit
end
# For non-AJAX (XMLHttpRequest) requests, choose correct layout
# For each mobile device, you will need a layout_<device>.haml file
# in the Views directory
#layout = case request.env['X_MOBILE_DEVICE']
when /iPhone|iPod/ then :layout_iphone
# when "Android" then :layout_android
else true # use default Sinatra layout
end
end
end # helpers
before do
get_layout()
end # before filter
get '/' do
# Will use iPhone layout for iPhone|iPod,
# Sinatra default layout for desktop browsers
haml :home, :layout => #layout
end
end # Class
I believe the standard way to handle specific user agents in Sinatra is directly on the route...
get '/', :agent => /iPhone/ do
# render for iPhone
end
get '/' do
# render standard layout
end
See The Sinatra Book.
Re: caching, I guess it would depend on what caching layers are fronting your site, but, yes, you may need to account for this.
I wrote a blog post about this topic that might be helpful to someone using Padrino with Sinatra. If you're not using Padrino, this still might be useful if you find the right place to extend Sinatra.
http://blog.joshdzielak.com/override-padrino-locale-based-template-resolu
http://dzello.com/blog/2011/06/22/override-padrino-locale-based-template-resolu/
The summary - I use rack-mobile-detect to tell me if a request is 'mobile', and I patch out Padrino's locale-based rendering support to render based on the mobile detection instead of the locale.
In this way I'm able to have foo.mobile.haml render for mobile and foo.haml for non-mobile, without any application code. As a bonus, it works both for the template file and the layout.
I had a similar problem always we cant relay on Routes . Sinatra provides filters to handle these kind of problems
#your_layout_name = ''
before :agent => /iPhone/ do
#your_layout_name = "initialize with desired iphone template"
end
get '/' do
# use #your_layout_name variable to initialize layout
end
I posting this since I had a problem where I was not in a position to filter at route level
so This may help others who are looking for similar solutions