n. a microframework for web development.
Meet us on IRC: #cuba.rb on freenode.net
Cuba is a microframework for web development originally inspired by Rum, a tiny but powerful mapper for Rack applications.
It integrates many templates via Tilt, and testing via Cutest and Capybara.
$ gem install cuba
Here's a simple application:
# cat hello_world.rb
require "cuba"
require "rack/protection"
require "securerandom"
Cuba.use Rack::Session::Cookie, :secret => SecureRandom.hex(64)
Cuba.use Rack::Protection
Cuba.define do
on get do
on "hello" do
res.write "Hello world!"
end
on root do
res.redirect "/hello"
end
end
end
And the test file:
# cat hello_world_test.rb
require "cuba/test"
require "./hello_world"
scope do
test "Homepage" do
get "/"
follow_redirect!
assert_equal "Hello world!", last_response.body
end
end
To run it, you can create a config.ru
file:
# cat config.ru
require "./hello_world"
run Cuba
You can now run rackup
and enjoy what you have just created.
Here's an example showcasing how different matchers work:
require "cuba"
require "rack/protection"
require "securerandom"
Cuba.use Rack::Session::Cookie, :secret => SecureRandom.hex(64)
Cuba.use Rack::Protection
Cuba.define do
# only GET requests
on get do
# /
on root do
res.write "Home"
end
# /about
on "about" do
res.write "About"
end
# /styles/basic.css
on "styles", extension("css") do |file|
res.write "Filename: #{file}" #=> "Filename: basic"
end
# /post/2011/02/16/hello
on "post/:y/:m/:d/:slug" do |y, m, d, slug|
res.write "#{y}-#{m}-#{d} #{slug}" #=> "2011-02-16 hello"
end
# /username/foobar
on "username/:username" do |username|
user = User.find_by_username(username) # username == "foobar"
# /username/foobar/posts
on "posts" do
# You can access `user` here, because the `on` blocks
# are closures.
res.write "Total Posts: #{user.posts.size}" #=> "Total Posts: 6"
end
# /username/foobar/following
on "following" do
res.write user.following.size #=> "1301"
end
end
# /search?q=barbaz
on "search", param("q") do |query|
res.write "Searched for #{query}" #=> "Searched for barbaz"
end
end
# only POST requests
on post do
on "login" do
# POST /login, user: foo, pass: baz
on param("user"), param("pass") do |user, pass|
res.write "#{user}:#{pass}" #=> "foo:baz"
end
# If the params `user` and `pass` are not provided, this block will
# get executed.
on true do
res.write "You need to provide user and pass!"
end
end
end
end
The favorite security layer for Cuba is Rack::Protection. It is not included by default because there are legitimate uses for plain Cuba (for instance, when designing an API).
If you are building a web application, by all means make sure to include a security layer. As it is the convention for unsafe operations, only POST, PUT and DELETE requests are monitored.
You should also always set a session secret to some undisclosed value. Keep in mind that the content in the session cookie is not encrypted.
require "cuba"
require "rack/protection"
require "securerandom"
Cuba.use Rack::Session::Cookie, :secret => SecureRandom.hex(64)
Cuba.use Rack::Protection
Cuba.use Rack::Protection::RemoteReferrer
Cuba.define do
# Now your app is protected against a wide range of attacks.
...
end
There are four matchers defined for HTTP Verbs: get
, post
, put
and
delete
. But the world doesn't end there, does it? As you have the whole
request available via the req
object, you can query it with helper methods
like req.options?
or req.head?
, or you can even go to a lower level
and inspect the environment via the env
object, and check for example if
env["REQUEST_METHOD"]
equals the obscure verb PATCH
.
What follows is an example of different ways of saying the same thing:
on env["REQUEST_METHOD"] == "GET", "api" do ... end
on req.get?, "api" do ... end
on get, "api" do ... end
Actually, get
is syntax sugar for req.get?
, which in turn is syntax sugar
for env["REQUEST_METHOD"] == "GET"
.
You may have noticed we use req
and res
a lot. Those variables are
instances of Rack::Request and Cuba::Response
respectively, and
Cuba::Response
is just an optimized version of
Rack::Response.
Those objects are helpers for accessing the request and for building
the response. Most of the time, you will just use res.write
.
If you want to use custom Request
or Response
objects, you can
set the new values as follows:
Cuba.settings[:req] = MyRequest
Cuba.settings[:res] = MyResponse
Make sure to provide classes compatible with those from Rack.
You may have noticed that some matchers yield a value to the block. The rules for determining if a matcher will yield a value are simple:
- Regex captures:
"posts/(\\d+)-(.*)"
will yield two values, corresponding to each capture. - Placeholders:
"users/:id"
will yield the value in the position of :id. - Symbols:
:foobar
will yield if a segment is available. - File extensions:
extension("css")
will yield the basename of the matched file. - Parameters:
param("user")
will yield the value of the parameter user, if present.
The first case is important because it shows the underlying effect of regex captures.
In the second case, the substring :id
gets replaced by ([^\\/]+)
and the
string becomes "users/([^\\/]+)"
before performing the match, thus it reverts
to the first form we saw.
In the third case, the symbol ––no matter what it says––gets replaced
by "([^\\/]+)"
, and again we are in presence of case 1.
The fourth case, again, reverts to the basic matcher: it generates the string
"([^\\/]+?)\.#{ext}\\z"
before performing the match.
The fifth case is different: it checks if the the parameter supplied is present in the request (via POST or QUERY_STRING) and it pushes the value as a capture.
You can mount a Cuba app, along with middlewares, inside another Cuba app:
class API < Cuba; end
API.use SomeMiddleware
API.define do
on param("url") do |url|
...
end
end
Cuba.define do
on "api" do
run API
end
end
Given that Cuba is essentially Rack, it is very easy to test with
Rack::Test
, Webrat
or Capybara
. Cuba's own tests are written
with a combination of Cutest and Rack::Test,
and if you want to use the same for your tests it is as easy as
requiring cuba/test
:
require "cuba/test"
require "your/app"
scope do
test "Homepage" do
get "/"
assert_equal "Hello world!", last_response.body
end
end
If you prefer to use Capybara, instead of requiring
cuba/test
you can require cuba/capybara
:
require "cuba/capybara"
require "your/app"
scope do
test "Homepage" do
visit "/"
assert has_content?("Hello world!")
end
end
To read more about testing, check the documentation for Cutest, Rack::Test and Capybara.
Each Cuba app can store settings in the Cuba.settings
hash. The settings are
inherited if you happen to subclass Cuba
Cuba.settings[:layout] = "guest"
class Users < Cuba; end
class Admin < Cuba; end
Admin.settings[:layout] = "admin"
assert_equal "guest", Users.settings[:layout]
assert_equal "admin", Admin.settings[:layout]
Feel free to store whatever you find convenient.
Cuba ships with a plugin that provides helpers for rendering templates. It uses Tilt, a gem that interfaces with many template engines.
require "cuba/render"
Cuba.plugin Cuba::Render
Cuba.define do
on default do
# Within the partial, you will have access to the local variable `content`,
# that will hold the value "hello, world".
res.write render("home.haml", content: "hello, world")
end
end
Note that in order to use this plugin you need to have Tilt installed, along with the templating engines you want to use.
Cuba provides a way to extend its functionality with plugins.
Authoring your own plugins is pretty straightforward.
module MyOwnHelper
def markdown(str)
BlueCloth.new(str).to_html
end
end
Cuba.plugin MyOwnHelper
That's the simplest kind of plugin you'll write. In fact, that's exactly how
the markdown
helper is written in Cuba::TextHelpers
.
A more complicated plugin can make use of Cuba.settings
to provide default
values. In the following example, note that if the module has a setup
method it will
be called as soon as it is included:
module Render
def self.setup(app)
app.settings[:template_engine] = "erb"
end
def partial(template, locals = {})
render("#{template}.#{settings[:template_engine]}", locals)
end
end
Cuba.plugin Render
This sample plugin actually resembles how Cuba::Render
works.
Finally, if a module called ClassMethods
is present, Cuba
will be extended
with it.
module GetSetter
module ClassMethods
def set(key, value)
settings[key] = value
end
def get(key)
settings[key]
end
end
end
Cuba.plugin GetSetter
Cuba.set(:foo, "bar")
assert_equal "bar", Cuba.get(:foo)
assert_equal "bar", Cuba.settings[:foo]