4. Sinatra
is
a
DSL
for
quickly
crea<ng
web
applica<ons
in
Ruby
5. # hi.rb
require 'rubygems’
require 'sinatra'
get '/' do
'Hello world!’
end
$ gem install sinatra
$ ruby hi.rb
== Sinatra has taken the stage ...
>> Listening on 0.0.0.0:4567
$ curl http://0.0.0.0:4567
Hello World
9. Rou?ng:
Splat
Support
get '/say/*/to/*' do
# matches /say/hello/to/world
params['splat'] # => ["hello", "world"]
...
end
get '/download/*.*' do
# matches /download/path/to/file.xml
params['splat'] # => ["path/to/file", "xml"]
...
end
10. Rou?ng:
Regex
Support
get /A/hello/([w]+)z/ do
"Hello, #{params['captures'].first}!”
...
end
11. Rou?ng:
Op?onal
Parameters
get '/posts.?:format?' do
# matches "GET /posts" and
# any extension "GET /posts.rss", "GET /posts.xml" etc.
end
12. Rou?ng:
URL
Query
Parameters
get '/posts' do
# matches "GET /posts?title=foo&author=bar"
title = params['title']
author = params['author']
# uses title and author variables;
# query is optional to the /posts route
End
13. Rou?ng:
Condi?onal
Matching
get '/', :host_name => /^admin./ do
"Admin Area, Access denied!"
end
get '/', :provides => 'html' do
haml :index
end
get '/', :provides => ['rss', 'atom', 'xml'] do
builder :feed
end
14. Rou?ng:
Custom
Condi?ons
set(:probability) { |value| condition { rand <= value } }
get '/win_a_car', :probability => 0.1 do
"You won!"
end
get '/win_a_car' do
"Sorry, you lost."
End
15. Returning
Results
# 1. String containing the body and default code of 200
get '/' do
'Hello world!’
end
# 2. Response code + body
get '/' do
[200, 'Hello world!’]
end
# 3. Response code + headers + body
get '/' do
[200, {'Content-Type' => 'text/plain'}, 'Hello world!’]
end
17. Hello
World
with
Rack
# hello_world.rb
require 'rack'
require 'rack/server’
class HelloWorldApp
def self.call(env)
[200, {}, 'Hello World’]
end
end
Rack::Server.start :app => HelloWorldApp
18. Rack
env
# hello_world.rb
require 'rack'
require 'rack/server’
class HelloWorldApp
def self.call(env)
[200, {},
"Hello World. You said: #{env['QUERY_STRING']}"]
end
end
Rack::Server.start :app => HelloWorldApp
20. Typical
env
(con’t)
...
"HTTP_USER_AGENT"=>
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4)
AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.47
Safari/536.11",
"HTTP_ACCEPT_ENCODING"=>"gzip,deflate,sdch",
"HTTP_ACCEPT_LANGUAGE"=>"en-US,en;q=0.8",
"HTTP_ACCEPT_CHARSET"=>"ISO-8859-1,utf-8;q=0.7,*;q=0.3",
"HTTP_COOKIE"=> "_gauges_unique_year=1;
_gauges_unique_month=1",
"GATEWAY_INTERFACE"=>"CGI/1.2",
"SERVER_PORT"=>"8080",
"QUERY_STRING"=>"",
"SERVER_PROTOCOL"=>"HTTP/1.1",
"rack.url_scheme"=>"http",
"SCRIPT_NAME"=>"",
"REMOTE_ADDR"=>"127.0.0.1",
...
}
21. The
Rack::Request
Wrapper
class HelloWorldApp
def self.call(env)
request = Rack::Request.new(env)
request.params # contains the union of GET and POST
params
request.xhr? # requested with AJAX
require.body # the incoming request IO stream
if request.params['message']
[200, {}, request.params['message']]
else
[200, {}, 'Say something to me!']
end
end
end
22. Rack
Middleware
u Rack
allows
for
chaining
mul?ple
call()
methods
u We
can
do
anything
we
want
within
each
call()
u This
includes
separa?ng
behavior
into
reusable
classes
(e.g.
across
Sinatra
and
Rails)
u SRP
(Single
Responsibility
Principle)
– Each
class
has
a
single
responsibility
– Our
app
is
composed
of
mul?ple
classes
that
each
do
one
thing
well
23. Rack::Builder
for
Middleware
# this returns an app that responds to call cascading down
# the list of middlewares.
app = Rack::Builder.new do
use Rack::Etag # Add an ETag
use Rack::ConditionalGet # Support Caching
use Rack::Deflator # GZip
run HelloWorldApp # Say Hello
end
Rack::Server.start :app => app
# Resulting call tree:
# Rack::Etag
# Rack::ConditionalGet
# Rack::Deflator
# HelloWorldApp
24. Using
the
Rackup
Command
u Combines
all
of
these
concepts
into
a
config
u Will
start
a
web
process
with
your
Rack
app
u Central
loca?on
for
requires,
bootstrapping
u Enables
middleware
to
be
configured
as
well
u Default
filename
is
config.ru
u Used
to
bootstrap
Rails
25. Using
Rackup
# config.ru
# HelloWorldApp defintion
# EnsureJsonResponse defintion
# Timer definition
use Timer
use EnsureJsonResponse
run HelloWorldApp
$ rackup –p 4567
26. Using
Mul?ple
Sinatra
Apps
u Rackup
allows
for
moun?ng
mul?ple
Sinatra
Apps
u This
allows
for
more
modular
APIs
u Recommend
one
Sinatra
app
per
top-‐level
resource
27. Moun?ng
Mul?ple
Sinatra
Apps
# config.ru
require 'sinatra'
require 'app/auth_api'
require 'app/users_api'
require 'app/organizations_api'
map "/auth" do
run AuthApi
end
map "/users" do
run UsersApi
end
map "/organizations" do
run OrganizationsApi
end
28. Important:
Require
!=
Automa?c
u Must
manage
your
own
requires
u No
free
ride
(like
with
Rails)
u This
means
order
of
requires
is
important!
30. Mul?ple
API
Design
Choices
u RPC-‐based
– Uses
HTTP
for
transport
only
– Endpoints
are
not
unique,
only
the
payload
– No
HTTP
caching
available
– e.g.
POST
/getUserDetails,
POST
/createUser
u Resource-‐based
– Unique
URLs
for
resources
and
collec?ons
– HTTP
caching
available
– e.g.
GET
/users/{userId}
and
GET
/users
31. Hypermedia
REST
u An
architectural
style,
with
constraints
u A
set
of
constraints,
usually
on
top
of
HTTP
u Not
a
standard;
builds
on
the
standard
of
HTTP
u Mul?ple
content
types
(e.g.
JSON,
XML,
CSV)
u The
response
is
a
representa?on
of
the
resource
state
(data)
plus
server-‐side
state
in
the
form
of
ac<ons/transi<ons
(links)
33. Resource
Lifecycle
using
Sinatra
get '/users' do
.. list a resource collection (and search) ..
end
get '/users/:id' do
.. resource instance details ..
end
post '/users' do
.. create new resource ..
end
put '/users/:id' do
.. replace resource ..
End
delete ’/users/:id' do
.. annihilate resource ..
end
34. List
Resources
Example
get '/users' do
# 1. capture any search filters using params[]
email_filter = params[:email]
# 2. build query and fetch results from database
if email_filter
users = User.where( email: email_filter ).all
else
users = User.all
# 3. marshal results to proper content type (e.g. JSON)
[200, users.to_json]
end
35. List
Resources
Example
get '/users' do
# 1. capture any search filters using params[]
email_filter = params[:email]
# 2. build query and fetch results from database
if email_filter
users = User.where( email: email_filter ).all
else
users = User.all
# 3. marshal results to proper content type (e.g. JSON)
[200, users.to_json]
# Q: Which ORM should we use with Sinatra?
# Q: Can we customize the results format easily?
end
37. Selec?ng
an
ORM
u Ac?veRecord
u DataMapper
u Sequel
(my
favorite)
– Flexible
as
it
supports
Datasets
and
Models
38. Sequel
Datasets
Example
require 'sequel'
DB = Sequel.sqlite # memory database
DB.create_table :items do
primary_key :id
String :name
Float :price
end
items = DB[:items] # Create a dataset
items.insert(:name => 'abc', :price => rand * 100)
items.insert(:name => 'def', :price => rand * 100)
items.insert(:name => 'ghi', :price => rand * 100)
puts "Item count: #{items.count}"
puts "The average price is: #{items.avg(:price)}”
39. Sequel
Model
Example
require 'sequel'
DB = Sequel.sqlite # memory database
class Post < Sequel::Model
end
post = Post[123]
post = Post.new
post.title = 'hello world'
post.save
40. Select
a
Marshaling
Library
u Ac?veModel::Serializers
(AMS)
– Works
with
Kaminari
and
WillPaginate
– Supported
by
Rails
core
team
– One-‐way
JSON
genera?on
only
u Roar+Representable
(my
favorite)
– Works
with
and
without
Rails
– Bi-‐direc?onal
marshaling
– Supports
JSON,
XML,
YAML,
hash
41. Representable
module SongRepresenter
include Representable::JSON
property :title
property :track
collection :composers
end
class Song < OpenStruct
end
song = Song.new(title: "Fallout", track: 1)
song.extend(SongRepresenter).to_json
> {"title":"Fallout","track":1}
song = Song.new.extend(SongRepresenter).from_json(%
{ {"title":"Roxanne"} })
> #<Song title="Roxanne">
42. Roar
+
Representable
module SongRepresenter
include Roar::JSON
include Roar::Hypermedia
property :title
property :track
collection :composers
link :self do
"/songs/#{title}"
end
end
song = Song.new(title: "Fallout", track: 1)
song.extend(SongRepresenter).to_json
> {"title":"Fallout","track":1,"links":
[{"rel":"self","href":"/songs/Fallout"}]}"
43. Tools
for
Tes?ng
Your
API
u Unit
–
RSpec
– Models,
helpers
u Integra?on
–
RSpec
– Make
HTTP
calls
to
a
running
Sinatra
process
– Controller-‐focused
u Acceptance/BDD
–
RSpec,
Cucumber
– Make
HTTP
calls
to
a
running
Sinatra
process
– Use-‐case/story
focused
45. Addi?onal
Gems
u faraday
–
HTTP
client
with
middleware
for
tes?ng
and
3rd
party
API
integra?on
u xml-‐simple
–
Easy
XML
parsing
and
genera?on
u faker
–
Generates
fake
names,
addresses,
etc.
u uuidtools
–
uuid
generator
when
incremen?ng
integers
aren’t
good
enough
u bcrypt
–
Ruby
binding
for
OpenBSD
hashing
algorithm,
to
secure
data
at
rest
46. Addi?onal
Gems
(part
2)
u rack-‐conneg
–
Content
nego?a?on
support
get '/hello' do
response = { :message => 'Hello, World!' }
respond_to do |wants|
wants.json { response.to_json }
wants.xml { response.to_xml }
wants.other {
content_type 'text/plain'
error 406, "Not Acceptable"
}
end
end
curl -H "Accept: application/json" http://localhost:4567/
hello
47. Addi?onal
Gems
(part
3)
u hirb
–
Console
formaing
of
data
from
CLI,
Rake
tasks
irb>> Tag.last
+-----+-------------------------+-------------+
| id | created_at | description |
+-----+-------------------------+-------------+
| 907 | 2009-03-06 21:10:41 UTC | blah |
+-----+-------------------------+-------------+
1 row in set
48. Reloading
with
Shotgun
Gem
u No
automa?c
reload
of
classes
with
Sinatra
u Instead,
use
the
shotgun
gem:
u Note:
Only
works
with
Ruby
MRI
where
fork()
is
available
(POSIX)
$ gem install shotgun
$ shotgun config.ru
49. Puma
+
JRuby
u Ruby
MRI
is
geing
beCer
u JVM
is
faster
(2-‐5x),
very
mature
(since
1997)
u High
performance
garbage
collectors,
na?ve
threading,
JMX
management
extensions
u JDBC
libraries
very
mature
and
performant
for
SQL-‐based
access
u Puma
is
recommended
over
unicorn
for
JRuby
50. From
Sinatra
to
Padrino
u Padrino
provides
Rails-‐like
environment
for
Sinatra
u Build
in
Sinatra,
move
to
Padrino
when
needed
u Generators,
pluggable
modules,
admin
generator