- websocket support via
websocket('/path'){|s| ... }
route builder - contextual websocket stashing via
websockets
helper task
handling viaasync
andfuture
helpers- no rack
- optional tilt/erb support
- optional mustermann support
Just like Sinatra, Angelo gives you an expressive DSL for creating web applications. There are some
notable differences, but the basics remain the same. It mostly follows the subclass style of Sinatra:
you must define a subclass of Angelo::Base
but also .run
on that class for the service to start.
In addition, and perhaps more importantly, Angelo is built upon Reel, which is, in turn, built upon
Celluloid::IO and gives you a reactor with evented IO in Ruby!
Note: There currently is no "standalone" capability where one can define route handlers at the top level.
Things will feel very familiar to anyone experienced with Sinatra. Inside the subclass, you can define route handlers denoted by HTTP verb and path. Unlike Sinatra, the only acceptable return value from a route block is the body of the response in full. Chunked response support was recently added to Reel, and look for that support in Angelo soon.
Angelo also features before
and after
blocks, just like Sinatra. The one difference lies in how
after blocks are handled. See the Errors section below for more info.
There is also Mustermann support for full-on, Sinatra-like path matching and params.
One of the main motivations for Angelo was the ability to define websocket handlers with ease. Through
the addition of a websocket
route builder and a websockets
helper, Angelo attempts to make it easy
for you to build real-time web applications.
The websocket
route builder accepts a path and a block, and passes the actual websocket to the block
as the only argument. This socket is an instance of Reel's
WebSocket class, and, as such,
responds to methods like on_message
and on_close
. A service-wide on_pong
handler (defined at the
class-level of the Angelo app) is available to customize the behavior when a pong frame comes back from
a connected websocket client.
Angelo includes a "stash" helper for connected websockets. One can <<
a websocket into websockets
from inside a websocket handler block. These can "later" be iterated over so one can do things like
emit a message on every connected websocket when the service receives a POST request.
The websockets
helper also includes a context ability, so you can stash connected websocket clients
into different "sections".
Here is an example of the websocket
route builder, the websockets
helper, and the context feature:
require 'angelo'
class Foo < Angelo::Base
websocket '/' do |ws|
websockets << ws
end
websocket '/bar' do |ws|
websockets[:bar] << ws
end
post '/' do
websockets.each {|ws| ws.write params[:foo]}
end
post '/bar' do
websockets[:bar].each {|ws| ws.write params[:bar]}
end
end
Foo.run
In this case, any clients that connected to a websocket at the path '/' would be stashed in the
default websockets array; clients that connected to '/bar' would be stashed into the :bar
section.
Each "section" can accessed with a familiar, Hash
-like syntax, and can be iterated over with
a .each
block.
When a POST /
with a 'foo' param is received, any value is messaged out to any '/' connected
websockets. When a POST /bar
with a 'bar' param is received, any value is messaged out to all
websockets that connected to '/bar'.
Angelo is built on Reel and Celluloid::IO, giving your web application class the ability to define
"tasks" and call them from route handler blocks in an async
or future
style.
You can define a task on the reactor using the task
class method and giving it a symbol and a
block. The block can take arguments that you can pass later, with async
or future
.
# defining a task on the reactor called `:in_sec` which will sleep for
# given number of seconds, then return the given message.
#
task :in_sec do |sec, msg|
sleep sec.to_i
msg
end
This helper is directly analogous to the Celluoid method of the same name. Once tasks are defined, you can call them with this helper method, passing the symbol of the task name and any arguments. The task will run on the reactor, asynchronously, and return immediately.
get '/' do
# run the task defined above asynchronously, return immediately
#
async :in_sec, params[:sec], params[:msg]
# NOTE: params[:msg] is discarded, the return value of tasks called with `async` is nil.
# return this response body while the task is still running
# assuming params[:sec] is > 0
#
'hi'
end
Just like async
, this comes from Celluloid as well. It behaves exactly like async
, with the
notable exception of returing a "future" object that you can call #value
on later to retreive
the return value of the task. Once #value
is called, things will "block" until the task is
finished.
get '/' do
# run the task defined above asynchronously, return immediately
#
f = future :in_sec, params[:sec], params[:msg]
# now, block until the task is finished and return the task's value
# as a response body
#
f.value
end
Angelo gives you two ordained methods of stopping route processing:
- raise an instance of
RequestError
halt
with a status code and message
The main difference is that halt
will still run an after
block, and raising RequestError
will bypass the after
block.
Any other exceptions or errors raised by your route handler will be handled with a 500 status code and the message will be the body of the response.
Raising an instance of Angelo::RequestError
causes a 400 status code response, and the message
in the instance is the body of the the response. If the route or class was set to respond with
JSON, the body is converted to a JSON object with one key, error
, that has a value of the message.
If the message is a Hash
, the hash is converted to a JSON object, or to a string for other content
types.
If you want to return a different status code, you can pass it as a second argument to
RequestError.new
. See example below.
You can halt
from within any route handler, optionally passing status code and a body. The
body is handled the same way as raising RequestError
.
get '/' do
raise RequestError.new '"foo" is a required parameter' unless params[:foo]
params[:foo]
end
get '/json' do
content_type :json
raise RequestError.new foo: "required!"
{foo: params[:foo]}
end
get '/not_found' do
raise RequestError.new 'not found', 404
end
get '/halt' do
halt 200, "everything's fine"
raise RequestError.new "won't get here"
end
$ curl -i http://127.0.0.1:4567/
HTTP/1.1 400 Bad Request
Content-Type: text/html
Connection: Keep-Alive
Content-Length: 29
"foo" is a required parameter
$ curl -i http://127.0.0.1:4567/?foo=bar
HTTP/1.1 200 OK
Content-Type: text/html
Connection: Keep-Alive
Content-Length: 3
bar
$ curl -i http://127.0.0.1:4567/json
HTTP/1.1 400 Bad Request
Content-Type: application/json
Connection: Keep-Alive
Content-Length: 29
{"error":{"foo":"required!"}}
$ curl -i http://127.0.0.1:4567/not_found
HTTP/1.1 404 Not Found
Content-Type: text/html
Connection: Keep-Alive
Content-Length: 9
not found
$ curl -i http://127.0.0.1:4567/halt
HTTP/1.1 200 OK
Content-Type: text/html
Connection: Keep-Alive
Content-Length: 18
everything's fine
Lots of work left to do!
require 'angelo'
require 'angelo/mustermann'
class Foo < Angelo::Base
include Angelo::Mustermann
# just some constants to use in routes later...
#
TEST = {foo: "bar", baz: 123, bat: false}.to_json
HEART = '<3'
# a flag to know if the :heart task is running
#
@@hearting = false
# you can define instance methods, just like Sinatra!
#
def pong; 'pong'; end
def foo; params[:foo]; end
# standard HTTP GET handler
#
get '/ping' do
pong
end
# standard HTTP POST handler
#
post '/foo' do
foo
end
post '/bar' do
params.to_json
end
# emit the TEST JSON value on all :emit_test websockets
# return the params posted as JSON
#
post '/emit' do
websockets[:emit_test].each {|ws| ws.write TEST}
params.to_json
end
# handle websocket requests at '/ws'
# stash them in the :emit_test context
# write 6 messages to the websocket whenever a message is received
#
websocket '/ws' do |ws|
websockets[:emit_test] << ws
ws.on_message do |msg|
5.times { ws.write TEST }
ws.write foo.to_json
end
end
# emit the TEST JSON value on all :other websockets
#
post '/other' do
websockets[:other].each {|ws| ws.write TEST}
''
end
# stash '/other/ws' connected websockets in the :other context
#
websocket '/other/ws' do |ws|
websockets[:other] << ws
end
websocket '/hearts' do |ws|
# this is a call to Base#async, actually calling
# the reactor to start the task
#
async :hearts unless @@hearting
websockets[:hearts] << ws
end
# this is a call to Base.task, defining the task
# to perform on the reactor
#
task :hearts do
@@hearting = true
every(10){ websockets[:hearts].each {|ws| ws.write HEART } }
end
post '/in/:sec/sec/:msg' do
# this is a call to Base#future, telling the reactor
# do this thing and we'll want the value eventually
#
f = future :in_sec params[:sec], params[:msg]
f.value
end
# define a task on the reactor that sleeps for the given number of
# seconds and returns the given message
#
task :in_sec do |sec, msg|
sleep sec.to_i
msg
end
end
Foo.run
Tilt / ERB
To make erb
available in route blocks
- add
tilt
to yourGemfile
:gem 'tilt'
- require
angelo/tilt/erb
- include
Angelo::Tilt::ERB
in your app
class Foo < Angelo::Base
include Angelo::Tilt::ERB
@@views = 'some/other/path' # defaults to './views'
get '/' do
erb :index
end
end
To make routes blocks match path with Mustermann patterns
- be using ruby >=2.0.0
- add 'mustermann' to to your
Gemfile
:platform(:ruby_20){ gem 'mustermann' }
- require
angelo/mustermann
- include
Angelo::Mustermann
in your app
class Foo < Angelo::Base
include Angelo::Tilt::ERB
include Angelo::Mustermann
get '/:foo/things/:bar' do
# `params` is merged with the Mustermann object#params hash, so
# a "GET /some/things/are_good?foo=other&bar=are_bad" would have:
# params: {
# 'foo' => 'some',
# 'bar' => 'are_good'
# }
@foo = params[:foo]
@bar = params[:bar]
erb :index
end
end
YES, HAVE SOME
- 🍴 Fork this repo, make changes, send PR!
if Good stuff?
see LICENSE