How do I use Sprockets with Sinatra without a rackup file? - sinatra

I'm writing a library that has an embedded Sinatra app launched via Thor. I want to mount instances of Sprockets::Environment at /css and /js and have the main app mapped to /. This would be easy using Rack::URLMap in a config.ru file, but in this case there isn't one because I'm starting the Sinatra app programmatically with Sinatra::Application.run!. How can I achieve this?

Actually, this is not that hard. All you need to do is assign an instance of Sprockets::Environment to a Sinatra configuration variable and define some paths to look up the assets you're interested in.
Here's a basic example:
require "sass"
require "haml"
require "erubis"
require "sinatra"
require "sprockets"
set :assets, Sprockets::Environment.new
# Configure sprockets
settings.assets.append_path "app/javascripts"
settings.assets.append_path "app/stylesheets"
# For compressed JS and CSS output
require "yui/compressor"
settings.assets.js_compressor = YUI::JavaScriptCompressor.new
settings.assets.css_compressor = YUI::CssCompressor.new
get "/" do
haml :index
end
get "/javascripts/:file.js" do
content_type "application/javascript"
settings.assets["#{params[:file]}.js"]
end
get "/stylesheets/:file.css" do
content_type "text/css"
settings.assets["#{params[:file]}.css"]
end
Happy sprocketing!

I ended up doing it by writing a custom middleware with some of the functionality from Rack::URLMap. It looks roughly like this:
require "sprockets"
require "sinatra/base"
class SprocketsMiddleware
attr_reader :app, :prefix, :sprockets
def initialize(app, prefix)
#app = app
#prefix = prefix
#sprockets = Sprockets::Environment.new
yield sprockets if block_given?
end
def call(env)
path_info = env["PATH_INFO"]
if path_info =~ prefix
env["PATH_INFO"].sub!(prefix, "")
sprockets.call(env)
else
app.call(env)
end
ensure
env["PATH_INFO"] = path_info
end
end
class App < Sinatra::Base
use SprocketsMiddleware, %r{/assets} do |env|
env.append_path "assets/css"
env.append_path "assets/js"
end
end
App.run!

Here is how I integrated Sprockets into Sinatra with Rails-like directory layout, helpers and minification for JS and CSS.
I chose to write a Sinatra extension. This extension encapsulates the configuration of sprockets (paths, minification, helpers) and can be registered by the application.
module Sinatra
module Assets
extend Sinatra::Extension
configure do
set :assets, Sprockets::Environment.new(root).tap { |assets|
%w(assets vendor/assets).each do |base|
%w(images javascripts stylesheets).each do |type|
assets.append_path File.join(base, type)
end
end
if production?
assets.js_compressor = Closure::Compiler.new
assets.css_compressor = YUI::CssCompressor.new
uid = Digest::MD5.hexdigest(File.dirname(__FILE__))[0,8]
assets.cache = Sprockets::Cache::FileStore.new("/tmp/sinatra-#{uid}")
else
assets.cache = nil
end
}
end
get "/assets/*" do
env["PATH_INFO"].sub!(%r{^/assets}, "")
expires Time.now + (365*24*60*60) if settings.production?
settings.assets.call(env)
end
helpers do
include Sprockets::Helpers
Sprockets::Helpers.configure do |config|
config.expand = development?
config.digest = production?
end
def assets_environment
settings.assets
end
end
end
end
Using the extension in your application is simple:
class App < Sinatra::Base
register Sinatra::Assets
# ...
end
Assets can be placed in assets, or vendor/assets. For example vendor/assets/jquery.js can be referenced by logical name, i.e. http://localhost/assets/jquery.js.
In the example above I'm using sprockets-helpers which provides helpers such as javascript_tag. The configuration given above assumes that in development, you want to expand assets required by the referenced asset (resulting in multiple tags per asset).

Related

How to switch from Sqlite to Postgres while installing Warden on Sinatra on Heroku

This is partly a problem-solving question, partly a "I'm trying to understand what's going on" question. I hope that's allowed. Basically, I'm trying to get Warden user authentication to work with Ruby/Sinatra and Postgres on Heroku.
I got a lot of help from this handy (but oldish) tutorial.
From some Rails experience I am a bit familiar with Active Record. The tutorial didn't mention anything about creating a migration for the User class. I went ahead and made my own migration, with my own properties ("name", "email", "password"), only to discover later that, lo and behold, the properties I put in that migration weren't being used by (and in fact were rejected by) the actual model in use. When I examined the object instances in the database, I found that they had only the properties Warden provided for me ("username" and "password").
I'm just trying to understand what happened here. I migrated down my (apparently unnecessary and ignored) Users migration, and nothing happened. I mean that I was able to create User instances and log in using them just as before.
Then it occurred to me that this old Warden tutorial (from 2012) uses something called DataMapper, which does what Active Record would do today. Is that right? They are both "ORMs"? I'm still confused about why Sinatra completely ignored the User migration I did. Maybe it's just using a different database--I did notice wht might be a new db.sqlite database in my main file. Pretty sure the one I created for Active Record was db/madlibs.sqlite3.
Although it works on my local machine, I'm pretty sure it won't work on Heroku, since they don't support sqlite (pretty sure). That then means I'll have to go back to the Warden documentation and figure out how to get it to work with my Postgres database...right? Any pointers on how to get started with that? Since this will be my first project using any authentication library like Warden, it's pretty intimidating.
Here's what I have so far (repo):
app.rb:
require 'sinatra'
require 'sinatra/activerecord'
require 'sinatra/base'
require './config/environment'
require 'bundler'
Bundler.require
require './model'
enable :sessions
class Madlib < ActiveRecord::Base
end
class SinatraWardenExample < Sinatra::Base
register Sinatra::Flash
end
use Warden::Manager do |config|
config.serialize_into_session{|user| user.id }
config.serialize_from_session{|id| User.get(id) }
config.scope_defaults :default,
strategies: [:password],
action: 'auth/unauthenticated'
config.failure_app = self
end
Warden::Manager.before_failure do |env,opts|
env['REQUEST_METHOD'] = 'POST'
end
Warden::Strategies.add(:password) do
def valid?
params['user']['username'] && params['user']['password']
end
def authenticate!
user = User.first(username: params['user']['username'])
if user.nil?
fail!("The username you entered does not exist.")
elsif user.authenticate(params['user']['password'])
success!(user)
else
fail!("Could not log in")
end
end
end
...non authentication routes...
post '/auth/login' do
env['warden'].authenticate!
flash[:success] = env['warden'].message
if session[:return_to].nil?
redirect '/'
else
redirect session[:return_to]
end
end
get '/auth/logout' do
env['warden'].raw_session.inspect
env['warden'].logout
flash[:success] = 'Successfully logged out'
redirect '/'
end
post '/auth/unauthenticated' do
session[:return_to] = env['warden.options'][:attempted_path]
puts env['warden.options'][:attempted_path]
flash[:error] = env['warden'].message || "You must log in"
redirect '/auth/login'
end
get '/protected' do
env['warden'].authenticate!
#current_user = env['warden'].user
erb :protected
end
model.rb (just the User model):
require 'rubygems'
require 'data_mapper'
require 'dm-sqlite-adapter'
require 'bcrypt'
DataMapper.setup(:default, "sqlite://#{Dir.pwd}/db.sqlite")
class User
include DataMapper::Resource
include BCrypt
property :id, Serial, :key => true
property :username, String, :length => 3..50
property :password, BCryptHash
def authenticate(attempted_password)
if self.password == attempted_password
true
else
false
end
end
end
DataMapper.finalize
DataMapper.auto_upgrade!
It seems like this repo might have solved the problems I'm facing now. Should I study that? The Warden documentation itself is pretty forbidding for a relative beginner. For example, it says "Warden must be downstream of some kind of session middleware. It must have a failure application declared, and you should declare which strategies to use by default." I don't understand that. And then it gives some code...which I also don't quite understand. Advice?? (Should I be working with a teacher/mentor, maybe?)

How to stub a method used in a Sinatra app with Minitest?

I want to test a small Sinatra application. And to do so, stub out some behaviour.
The relevant code in the Sinatra app is:
require 'sinatra'
require 'yaml'
get '/commands' do
YAML.load_file("commands.yml")["commands"].join("\n")
end
The minitest/spec code to test that is:
include Rack::Test::Methods
def app
Sinatra::Application
end
describe "/commands" do
describe "with two commands defined" do
it "must list the two commands" do
YAML.stub(:load_file, {"commands" => ["hostname", "uptime"]}) do
get '/commands'
last_response.must_be :ok?
last_response.body.must_equal "hostname\nuptime"
end
end
end
describe "with no commands defined" do
YAML.stub(:load_file, {"commands" => nil}) do
it "must list no commands" do
get '/commands'
last_response.body.must_equal ""
end
end
end
end
Using a small spec_helper:
ENV['RACK_ENV'] = 'test'
require 'minitest/autorun'
require 'minitest/spec'
require 'rack/test'
require_relative '../minion' #This is the sinatra-app.
The YAML.stub part is the most important, as it does not work as expected. It does stub out YAML.load_file inside the spec just fine. But in the context of the sinatra-app, YAML.load_file is unstubbed and loads the commands.yml, despite my attempt to stub it out.
How can I stub out a call to YAML.load_file with minitest/spec and have that stub being used in the Sinatra application?

In Sinatra, is there a way to forward a request to another app?

I'm trying to do something like this:
class Dispatcher < Sinatra::Base
def initialize
#foo = Foo.new
end
get '/foo/*' do
#foo.call(params[:splat])
end
end
So that URL /foo/abc/def?xxx=yyy would be like calling the Foo app with /abc/def?xxx=yyy.
This seems like it should be easy, but I haven't seen any example of it.
I ended up doing this in a Rack config.ru file:
map "/abc" do
run Foo.new('abc')
end
map "/def" do
run Foo.new('def')
end
Not exactly what I wanted, but saves me from modifying the underlying app.
I'm not sure why you would use Sinatra for that. If I understand you right, and you use Apache with Proxy-Rewrite rules you just do:
The .htaccess file
RewriteEngine On
RewriteRule ^foo/(.*) http://localhost:61000/$1 [P]
So all your domain.tdl/foo get redirected to your local running app athttp://localhost:61000/ with all Post and Get Parameters.

Sprockets asset_path error on Sinatra

I'm working on a Sinatra project that's using Sprockets. When I add a new stylesheet in the assets folder, I get this error:
Error compiling CSS asset
Sprockets::NotImplementedError: Custom asset_path helper is not
implemented
Extend your environment context with a custom method.
environment.context_class.class_eval do
def asset_path(path, options = {})
end
end
What am I doing wrong?
I followed the error message's suggestion: I defined the asset_path method in config.ru:
environment.context_class.class_eval do
def asset_path(path, options = {})
"/assets/#{path}"
end
end
I'm still not sure why this is needed, but it made the error go away.
To fix the same error in Padrino, I defined this method in my app.rb and changed environment to assets:
assets.context_class.class_eval do
def asset_path(path, options = {})
"/assets/#{path}"
end
end
Has already proposed, you need to define the method asset_path for your environment.
This method is used by helpers like image_url, ... commonly used in CSS files. You might want to make it a little different depending on the options[:type] then.
Example:
environment.context_class.class_eval do
def asset_path(path, options = {})
if type = options[:type]
"/assets/#{type.to_s.pluralize}/#{path}"
else
"/assets/#{path}"
end
end
end
Doing so mill make the asset_url to return /assets/path/to/your/file while the image_url helper will return /assets/images/path/to/your/file
Don't we need an instance of sprockets call?
For a Rails app that I'm playing with I used this in the config.ru to get rid of the error.
map '/assets' do
environment = Sprockets::Environment.new
environment.context_class.class_eval do
def asset_path(path, options = {})
"app/assets/#{path}"
end
end
environment.append_path 'app/assets/javascripts'
environment.append_path 'app/assets/stylesheets'
run environment
end

Testing view helpers

I'm currently working on a Rails plugin used for generating iPhone specific HTML meta-tags. I'm trying to use ActionView::TestCase for unit tests but keep getting the same error. See file contents and error below. Any ideas or help would be much appreciated.
test_helper.rb
require 'rubygems'
require 'test/unit'
require 'active_support'
require 'action_view'
require File.join(File.dirname(__FILE__), '..', 'lib', 'iphone_helper')
iphone_test_helper.rb
require 'test_helper'
class IphoneHelperTest < ActionView::TestCase
test 'br' do
tag = tag('br')
assert_tag_in tag, '<br />'
end
end
error
RuntimeError: In order to use #url_for, you must include routing helpers explicitly. For instance, `include Rails.application.routes.url_helpers
Awful and hacky workaround that worked for me (since I am working on a gem and not in a full rails environment):
require 'ostruct'
module ActionController::UrlFor
def _routes
helpers = OpenStruct.new
helpers.url_helpers = Module.new
helpers
end
end
Did you try to include the respective Module in an old-fashioned way?:
include ActionDispatch::Routing::RouteSet
If a NameError is raised telling you that ActionDispatch is unknown you might have to require 'action_dispatch'.
Maybe a stupid question, but is the fact that the class name and the file name don't match possibly a problem (IphoneHelperTest vs. iphone_test_helper.rb)? Sometimes that leads to classes not being loaded.