June 12th, 2008 cschneid
Cookies are a fairly simple thing to use in Sinatra, but they have a few quirks.
Lets first look at the simple use case:
require 'sinatra'
get '/' do
# Get the string representation
cookie = request.cookies["thing"]
# Set a default
cookie ||= 0
# Convert to an integer
cookie = cookie.to_i
# Do something with the value
cookie += 1
# Reset the cookie
set_cookie("thing", cookie)
# Render something
"Thing is now: #{cookie}"
end
Setting a path, expiration date, or domain gets a little more complicated - see the source code for set_cookie if you want to dig deeper.
set_cookie("thing", { :domain => myDomain,
:path => myPath,
:expires => Date.new } )
That’s the easy stuff with cookies - It can also serialize Array objects, separating them with ampersands (&), but when they come back, it doesn’t deserialize or split them in any way, it hands you the raw, encoded string for your parsing pleasure.
I’ll look into how serialization and data handling needs to occur in a later post. I’m guessing that it’s the user’s job to split and un-encode, but I haven’t verified that yet.
Posted in Programming, Ruby, Sinatra | 2 Comments »
June 9th, 2008 cschneid
After much poking and prodding it looks like Sinatra won’t work on top of JRuby Rack. It’s an unintended consequence of how everything was implemented, and it looks like some (hopefully minor) changes will have to be made to Sinatra.
Steps to get to where I am:
- Install JRuby 1.1.2
- Get tomcat - I used a 6.0.14 install I had laying around
- Install the JRuby gem “warble”, which will come with jruby-rack
- Create a new directory structure, described below
- Copy jruby-complete.jar, and jruby-rack.jar to the WEB-INF/lib directory
- Create a simple Sinatra hello world app
- Create your web.xml file as described
The directory structure
jruby-sinatra/
jruby-sinatra/WEB-INF
jruby-sinatra/WEB-INF/web.xml
jruby-sinatra/WEB-INF/hello_world.rb
jruby-sinatra/WEB-INF/sinatra # full copy of sinatra goes here
jruby-sinatra/WEB-INF/lib
jruby-sinatra/WEB-INF/lib/jruby-complete-1.1.1.jar
jruby-sinatra/WEB-INF/lib/jruby-rack-0.9.jar
My (non-working) current web.xml file.
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<context-param>
<param-name>public.root</param-name>
<param-value>/</param-value>
</context-param>
<context-param>
<param-name>rackup</param-name>
<param-value>
$:.unshift '/Users/cschneid/servers/apache-tomcat-6.0.14/webapps/jruby-sinatra/WEB-INF/'
$:.unshift '/Users/cschneid/servers/apache-tomcat-6.0.14/webapps/jruby-sinatra/WEB-INF/sinatra/lib'
puts "====================="
puts "====================="
puts "====================="
puts "Current file is: #{__FILE__}"
puts "Dollar 0 is: #{$0}"
puts "List of load paths:"
puts $:.map {|a| puts a }
puts "====================="
puts "====================="
puts "====================="
puts "Requiring sinatra"
require "sinatra"
puts "Setting up the sinatra app options"
Sinatra::Application.default_options.merge!(
:run => false,
:env => :production
)
puts "About to require test"
require 'test'
puts "About to run sinatra"
run Sinatra.application
</param-value>
</context-param>
<filter>
<filter-name>RackFilter</filter-name>
<filter-class>org.jruby.rack.RackFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>RackFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.jruby.rack.RackServletContextListener</listener-class>
</listener>
</web-app>
The problem - the load fails on the Sinatra::Application.default_options.merge line. The first time you access default_options, it generates the hash. Every subsequent time, it just hands back the already created instance. Since this is the first time, it attempts to create the default_options structure. It gets a “root” of the directory tree from $0. And then later combines it with ‘/views’ and ‘/public’. The problem lies in the fact that we have a ruby script embedded inside the web.xml file. It doesn’t have a reasonable $0 to even set, so it doesn’t. If you ask for __FILE__, it just reports back with “<script>”. End result: you get a simple “can’t convert nil into String” exception thrown.
Next Steps - I don’t know, I think we should probably wean Sinatra off of $0 if at all possible. Or at least figure out how to set it to something reasonable so that default_options doesn’t blow up. If anybody has any ideas on how to go about this, let me know, since this doesn’t have an obvious fix that I see. Mainly, I don’t want to break functionality elsewhere (for plain ruby).
Posted in Programming, Ruby, Sinatra | 3 Comments »
June 8th, 2008 cschneid
I don’t like deployment via FastCGI that much. It’s just too unstable, and random for my tastes. So when I saw that Passenger was going to support Rack, I knew I had to get that working on Dreamhost.
Once I started going at it, it only took me a few minutes to make everything work. You can find documentation at the Passenger Github repository.
Steps:
- Setup your account to support mod_rails
- Create the directory structure
- Make a rackup file (config.ru)
- Create a very simple Sinatra application
1) Setting up the account
- Domains -> Manage Domains -> Edit (web hosting column)
- Enable “Ruby on Rails Passenger (mod_rails)”
- Add the public directory to the web directory box. So if you were using “gittr.com”, it would change to “gittr.com/public”
- Save your changes
2) Creating the directory structure
domain.com/
domain.com/tmp
domain.com/public
# a vendored version of sinatra - not necessary if you use the gem
domain.com/sinatra
3) Creating a rackup file
# This file goes in domain.com/config.ru
require 'sinatra/lib/sinatra.rb'
require 'rubygems'
Sinatra::Application.default_options.merge!(
:run => false,
:env => :production
)
require 'test.rb'
run Sinatra.application
4) A very simple Sinatra application
# this is test.rb referred to above
get '/' do
"Worked on dreamhost"
end
get '/foo/:bar' do
"You asked for foo/#{params[:bar]}"
end
And that’s all there is to it! Once it’s all setup, point your browser at your domain, and you should see a “Worked on Dreamhost” page. To restart the application after making changes, you need to run “touch tmp/restart.txt”.
Posted in Programming, Ruby, Sinatra, System Admin | 16 Comments »
June 4th, 2008 cschneid
It’s nice having a chat channel as dead as #sinatra. It makes it trivial to go back through a day and read every message that went through. One of those messages yesterday was from SirSydAlot asking if Sinatra could bind to a certain IP address. I told him at the time that Rack should support it, but Sinatra didn’t yet, so I went to check that fact.
in Rack::Handler::Mongrel
server = ::Mongrel::HttpServer.new(options[:Host] || '0.0.0.0',
options[:Port] || 8080)
and in Rack::Handler::Thin
server = ::Thin::Server.new(options[:Host] || '0.0.0.0',
options[:Port] || 8080,
app)
So I made a patch to add a host option
. It’s pretty simple to use:
# That's it...
set :host, '192.168.1.1'
It sounds like the patch will be included in the mainline code soon, so look for that. Until then, be sure to use my host_setting branch, as that’s the only place this patch is.
Posted in Programming, Sinatra | No Comments »
June 3rd, 2008 cschneid
In my never ending quest to avoid doing anything useful, I decided to attempt to run Sinatra on top of JRuby. I had the original goal of getting it to run under JRuby-Rack and built into a war file that Tomcat or JBoss, or Weblogic, or… Glassfish(??) would take. Unfortunately, I didn’t get that far. What I saw was:
LocalJumpError - yield called out of block
My mini application that shows the issue:
$:.unshift File.dirname(__FILE__) + '/sinatra/lib'
require 'sinatra'
get '/' do
"Hello World"
end
get '/no_layout' do
erb :foo, :layout => false
end
get '/erb' do
erb :foo
end
get '/haml' do
haml :foo
end
Of course I have some views: - (just erb here, hamls are almost identical)
<!-- this is foo.erb -->
<p>Erb rendered</p>
<!-- this is layout.erb -->
<html>
<head></head>
<body>
<p>Layout rendered</p>
<%= yield %>
</body>
</html>
So… I filed a bug report against JRuby.. I’m impressed that Vladimir has already identified the narrow test case that I didn’t, and has written specs. I’m hoping that the next release of JRuby will have this fixed, and we can all move on with our lives.
Until then, layouts in Sinatra don’t work with JRuby.
Posted in Programming, Ruby, Sinatra | No Comments »
June 1st, 2008 cschneid
According to this post it looks like the version 2 release of Passenger will support Rack. Since Sinatra is built right on top of Rack, that means that Passenger will support Sinatra.
In practical terms, that means whenever Dreamhost (and other shared hosting providers) gets around to updating their Passenger installations, deployment of Sinatra apps will be as easy as writing a rackup file.
UPDATE: Sure enough, the newest release supports Rack, and I have a tutorial on getting sintra deployed on Dreamhost via passenger/mod_rails.
Posted in Programming, Ruby, Sinatra | No Comments »
May 30th, 2008 cschneid
To turn on sessions normally, you just have to run:
enable :sessions
# OR
set :sessions, true
Under the covers, that sets up Rack::Session::Cookie as a rack middleware component. If you were to do this manually, it would look like:
use Rack::Session::Cookie
The problem with relying on this is that the Cookie session store limits you to storing only 4k of data. It’s normally not a great idea to store a ton of data in the session, but it’s sometimes necessary. The other problem with using the cookie session stores is that the data is stored unencrypteted (but packed as base64). So you open your application up to a potential attack from a malicious user manipulated cookie.
So what we need to do in order to “use” the alternate session implementation is just to run:
use Rack::Session::Pool
# Or another session store that matches the rack API
# DON'T enable :session
# DON'T set :session, true
I haven’t seen many session implementations in Rack, but the edge Rack (and the next release presumably) will make it simple to create new implementations. Just check out Rack::Session::Abstract::Id for some helpers. Also, edge Rack does support a Memcached session adapter, which looks pretty promising as a usable session container.
Posted in Programming, Ruby, Sinatra | 1 Comment »
May 28th, 2008 cschneid
After my half-assed post about RSpec testing the Sinatra framework got several google hits, I figured I’d look into it a little further. Here’s what I found.
First, a very simple Sinatra application.
$:.unshift("../sinatra/lib")
require 'sinatra'
get '/' do
"Hello"
end
get '/:thingy' do
"Hello #{params[:thingy]}"
end
Now a test script.
$:.unshift("../sinatra/lib")
require 'sinatra'
require 'spec/interop/test'
require 'sinatra/test/unit'
describe 'Hello World' do
require 'hello'
specify "should render hello at /" do
get_it '/'
@response.should be_ok
@response.body.should == "Hello"
end
specify "should render argument at /anything" do
get_it '/foo'
@response.should be_ok
@response.body.should == "Hello foo"
get_it '/bar'
@response.should be_ok
@response.body.should == "Hello bar"
end
specify "should not respond to nested paths" do
get_it '/foo/bar'
@response.should_not be_ok
end
end
What I did - basically, all that needed to be done was to bypass the ‘sinatra/test/spec’ file, which in turn requires the test/spec library, which we need to avoid.
Instead, we require the RSpec interop library, and then get the ‘sinatra/test/unit’ file, which sets up a reasonable environment to start testing, including deferring to ‘sinatra/test/methods’ to define the helpers like ‘get_it’, ‘post_it’ and so on. The bonus is that you get to skip the require of test/spec, and get to stick to pure RSpec.
Where to go from here - I would like some input on what other helpers would be cool/useful. I haven’t worked with RSpec all that much (well.. at all), so I don’t know what’s helpful, and what’s not. Leave comments, or hit me up on IRC.
Posted in Programming, Ruby, Sinatra, Testing | 3 Comments »
May 28th, 2008 cschneid
Last night, two of my patches to Sinatra got merged into the main branch. I’ve already written about the erb_locals feature. The other one was a slightly older one to fix development reloading in situations where Sinatra was not called directly.
The situation which triggered the issue:
- $0 (the file actually run by ruby) was not the file where the Sinatra routing and application was defined
- The file that actually was $0 called ‘require’ on the Sinatra application.
- A Sinatra development environment reload happened.
Ruby prevents duplicate loading of libraries by only loading each require once per lifetime of the VM. If you really do want to reload the file, you have to use Kernel.load. Which of course is how Sinatra handles the development environment’s reloading.
Original code:
New code:
# app_file defaults to $0
Kernel.load Sinatra.options.app_file
Now that app_file is an option, it’s easily overridden in the file with your Sinatra application.
Now development reloading works easier, and Sinatra is a better embeddable framework. No changes are needed to existing applications since app_file defaults to $0, the default behavior is exactly the same as the old behavior.
Posted in Programming, Ruby, Sinatra | No Comments »
May 27th, 2008 cschneid
Today in #sinatra, a question came from jshsu about using a the helper defined in the wiki, with ERB, using locals like you would in Rails.
It looked something like:
<%= partial(:_post, :locals => {:post => post} %>;
In Rails, that passes a local variable called “post” into the template, with the value of whatever post is now. The problem is that ERB doesn’t support this nativlely. It’s sort of a hack to make it work.
The whole chunk of code:
def render_erb(content, options = {})
locals_opt = options.delete(:locals) || {}
locals_code = ""
locals_hash = {}
locals_opt.each do |key, value|
locals_code << "#{key} = locals_hash[:#{key}]\n"
locals_hash[:"#{key}"] = value
end
body = ::ERB.new(content).src
eval("#{locals_code}#{body}", binding)
end
The initialization part - the locals variable contains key/value mappings containing all the values to pass into the template. locals_code and locals_hash are place holders for the code generation that happens in the next step. They need to be here because if we defined them in the each block below, they’d be block scoped and disappear at the end of that block. We don’t want that, so we need to pre-define them up here.
locals_opt = options.delete(:locals) || {}
locals_code = ""
locals_hash = {}
Code generation and storage of values - For each key/value pair in the locals hash, store it off in a locals_hash variable indexed by a symbol, and define the code to pull that value out a little later. The locals_code variable will be included in the rendered code in the next step.
locals_opt.each do |key, value|
locals_code << "#{key} = locals_hash[:#{key}]\n"
locals_hash[:"#{key}"] = value
end
ERB rendering and eval() - The first statement here renders the erb template down to ruby code. Calling the src attribute on the rendered template gives back a string of ruby code matching that rendered template. Once we have that, we paste the locals_code to the top of the rendered template, and then calls eval with the current binding. The binding of course allows the locals_code’s references to locals_hash to resolve correctly.
body = ::ERB.new(content).src
eval("#{locals_code}#{body}", binding)
Posted in Programming, Sinatra | 2 Comments »