Writing a Sinatra Extension using options in routes - ruby

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

Related

Sinatra API feature toggle

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.

Rails : jump to function chosen at realtime (variable name)

I would like to jump to an "import function" which can be one off the file I already wrote in lib/import/...
The user chooses an option in a select box and from this choice, I want to execute a specific portion of code on one uploaded file (import excel with different possible layouts)
I wrote this :
# Loading and executing the template chosen by user at step 1
template_path = 'import/'+params[:template]+'/import.rb'
require template_path
Import.action(filename, session, params, current_project)
I have several import.rb files located each in a separate directory. One of these is :
module Import
require 'spreadsheet'
def Import.action(filepath, session, params, project)
# My import code
end
end
The problem is that Rails is always calling the action method from the first directory in lib/firstdirectory/import.rb
I never reach another import.rb file located in lib/otherdirectory/import.rb
Is there a better way to execute a "jump to" functionnality in realtime ?
Why does Rails jump to always the same function ?
Edit :
My application.rb configuration file contains
config.autoload_paths += Dir["#{config.root}/lib/import/**/"]
Edit 2 :
# lib/importer/importer.rb
module Importer
class Base
# Whatever common logic the import.rb files have.
end
end
#lib/importer/Import_test/import_test.rb Note the Big letter for the directory (constant)
module Importer
class Import_test < Base
def self.import
logger.debug(">>>>>>>>>>>>> special function Test <<<<<<<<<<<<<<<<<<<<")
end
end
end
# Call from controller
logger.debug "------------------>> "+params[:template]
raise "Invalid input" unless params[:template].constantize.superclass == Importer::Base
params[:template].constantize.import()
The params[:template] returns the string Importer::Import_test (with capital letters)
I get the error :
NoMethodError (undefined method 'superclass' for Importer::Import_test:Module):
app/controllers/import_controller.rb:57:in `step2'
Your code using the first directory entry makes sense. When you reference a constant whose definition has not yet been loaded, Rails checks the entries in autoload_paths for a corresponding file. Since you already have that import.rb in your first directory, your application loads that file.
A better design for this IMHO would be something along:
config.autoload_paths += ["#{config.root}/lib"]
# lib/importer.rb
module Importer
class Base
# Whatever common logic the import.rb files have.
end
end
# lib/importer/foo.rb
module Importer
class Foo < Base
def self.import
# ...
end
end
end
# lib/importer/bar.rb
module Importer
class Bar < Base
def self.import
# ...
end
end
end
# In your view, a way to identify these:
select_tag :layout, options_for_select({
"Foo" => "Importer::Foo",
"Bar" => "Importer::Bar"
})
# In your controller:
raise "Invalid input" unless params[:layout].constantize.superclass == Importer::Base
params[:layout].constantize.import( ... )
Update:
Rails looks for files this way: Say you want to use FooBar::Baz. If it doesn't have FooBar yet, it will load lib/foo_bar.rb and there is supposed to be something there. Next, it will try to access FooBar::Baz. Only if it doesn't have that yet (already after loading lib/foo_bar.rb), it will load lib/foo_bar/baz.rb and there is supposed to be something there.
If you want to use autoload_paths and not require ruby files yourself, please use the convention of using proper camelcase that Rails can easily change to underscore. e.g. Use camelcase ImporterTest without the underscore and have lib/importer/importer_test.rb so the framework will be able to find the correct file and your definition.
:-) Good luck.
There are much better ways. I suggest having a hash of template names to objects that execute the action. Import all your importers, and construct the hash. Than use the hash to get the function, and execute it.

Rails 3: How to intercept any http request

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).

How can I refactor my Sinatra app?

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).

use parameters in routes definition in sinatra

I've been trying for hours and still got no way to configure my routes the way I want to.
What I want to achieve is:
http://fooo.bar/prefix1234
I want to capture everything that starts with specifix prefix.
Pretty easy:
get "/prefix:id" do
puts params[:id]
end
But I don't want it to be a fixed prefix. I want to put the prefix in a config file
settings.rb:
set :prefix, 'pre'
get "/#{settings.prefix}:id" do
puts params[:id]
end
this won't work (undefined method `prefix' for Sinatra::Application:Class (NoMethodError)). I also tried capturing with regex:
before do
#prefix = settings.prefix
end
get %r{#{#prefix}(\d+)} do |id|
puts "Params: #{id}"
end
This doesn't work either (URL will not be captured)
Anyone got anything?
I was able to get your example working almost without modification. This allowed me to do what you described:
before do
#prefix = "test"
end
get %r{#{#prefix}(\d+)} do |c|
puts "#{#prefix} #{c}"
erb :test, :locals => {:id => c}
end
I then ran shotgun to test the output and called /test123. The output was:
test 123
My view also reiterated that this was working properly. If the problem is that the URL is not being captured, you may need to reorganize your structure so that it is more like:
before do
#prefix = "test"
end
get "/#{#prefix}/:id" do
puts "#{#prefix} #{params[:id]}"
erb :test, :locals => {:id => params[:id]}
end
I don't know if the latter is feasible for your application, but if you are not specific enough in the routing, you are leaving yourself open for frequent bad matches. In my experience, the more RESTful your application is, the better off you will be when it comes time to writing these types of operations.
Alternatively, perhaps a YAML file to store your settings in, and then parsed by a script would give you better results for the route. For example, a YAML file with these contents:
prefix: test
And then a helper script that parses that, which would look something like this:
helpers do
def config
#config = YAML.load_file("config.yml")
end
end
You could then replace your before block with this:
before do
#prefix = config["prefix"]
end
My coding tastes make me lean toward using the YAML method, but I think any of these solutions should be viable.
The String/Regexp is generated right away. This works:
require 'sinatra'
set :prefix, '/foo'
get "#{settings.prefix}/bar" do
request.path_info
end

Resources