README.md in angelo-0.1.7 vs README.md in angelo-0.1.8
- old
+ new
@@ -3,76 +3,243 @@
[![Build Status](https://travis-ci.org/kenichi/angelo.png?branch=master)](https://travis-ci.org/kenichi/angelo)
A [Sinatra](https://github.com/sinatra/sinatra)-esque DSL for [Reel](https://github.com/celluloid/reel).
-### Notes/Features
+### tl;dr
-* "easy" websocket support via `socket '/path' do |s|` route handler
-* "easy" websocket stashing via `websockets` helper
-* "easy" event handling via `async` helpers
+* websocket support via `websocket('/path'){|s| ... }` route builder
+* contextual websocket stashing via `websockets` helper
+* `task` handling via `async` and `future` helpers
* no rack
* optional tilt/erb support
* optional mustermann support
+### What is Angelo?
+
+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.
+
+There is also [Mustermann](#mustermann) support for full-on, Sinatra-like path
+matching and params.
+
+### Websockets!
+
+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.
+
+##### Route Builder
+
+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](https://github.com/celluloid/reel/blob/master/lib/reel/websocket.rb) 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.
+
+##### `websockets` helper
+
+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".
+
+##### Example!
+
+Here is an example of the `websocket` route builder, the `websockets` helper, and the context feature:
+
+```ruby
+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'.
+
+### Tasks + Async / Future
+
+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.
+
+##### `task` builder
+
+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`.
+
+```ruby
+# 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
+```
+
+##### `async` helper
+
+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.
+
+```ruby
+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
+```
+
+##### `future` helper
+
+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.
+
+```ruby
+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
+```
+
+### WORK LEFT TO DO
+
Lots of work left to do!
-### Quick example
+### Full-ish example
```ruby
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
- socket '/ws' do |ws|
+ # 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
- socket '/other/ws' do |ws|
+ # stash '/other/ws' connected websockets in the :other context
+ #
+ websocket '/other/ws' do |ws|
websockets[:other] << ws
end
- socket '/hearts' do |ws|
+ websocket '/hearts' do |ws|
- # this is a call to Base#async, actually calling
+ # this is a call to Base#async, actually calling
# the reactor to start the task
- #
+ #
async :hearts unless @@hearting
websockets[:hearts] << ws
end
@@ -85,16 +252,19 @@
end
post '/in/:sec/sec/:msg' do
# this is a call to Base#future, telling the reactor
- # do this thing and we'' want the value eventually
+ # 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
@@ -152,11 +322,9 @@
erb :index
end
end
```
-
-NOTE: this always sets the Mustermann object's `type` to `:sinatra`
### Contributing
YES, HAVE SOME