README.md in angelo-0.3.3 vs README.md in angelo-0.4.0

- old
+ new

@@ -1,47 +1,48 @@ Angelo ====== [![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). +A [Sinatra](https://github.com/sinatra/sinatra)-like DSL for [Reel](https://github.com/celluloid/reel). ### tl;dr * websocket support via `websocket('/path'){|s| ... }` route builder * SSE support via `eventsource('/path'){|s| ... }` route builder * contextual websocket/sse stashing via `websockets` and `sses` helpers * `task` handling via `async` and `future` helpers * no rack -* optional tilt/erb support -* optional mustermann support +* tilt/erb support +* 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. +notable differences, but the basics remain the same: you can either create a "classic" application +by requiring 'angelo/main' and using the DSL at the top level of your script, or a "modular" +application by requiring 'angelo', subclassing `Angelo::Base`, and calling `.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. You can define +route handlers denoted by HTTP verb and path with parameters set from path matching (using +[Mustermann](#mustermann)), the query string, and post body. +A route block may return: -Things will feel very familiar to anyone experienced with Sinatra. Inside the subclass, you can define -route handlers denoted by HTTP verb and path. One acceptable return value from a route block is the body -of the response in full as a `String`. Another is a `Hash` if the content type is set to `:json`. Finally, -you may return any object that responds to `#each(&block)` if the transfer encoding is set to `:chunked`. +* The body of the response in full as a `String`. +* A `Hash` (or anything that `respond_to? :to_json`) if the content type is set to `:json`. +* Any object that responds to `#each(&block)` if the transfer encoding is set to `:chunked`. There is also a `chunked_response` helper that will take a block, set the transfer encoding, and return -an appropriate object for you. +an appropriate object. -There is also [Mustermann](#mustermann) support for full-on, Sinatra-like path -matching and params. - -Angelo also features `before` and `after` blocks, just like Sinatra. Filters are ordered as defined, +Angelo also features `before` and `after` filter blocks, just like Sinatra. Filters are ordered as defined, and called in that order. When defined without a path, they run for all matched requests. With a path, -the path must match exactly for the block to be called. If `Angelo::Mustermann` is included, the paths -are interpreted as a Mustermann pattern and params are merged. For more info on the difference in how -after blocks are handled, see the Errors section below for more info. +the path is interpreted as a Mustermann pattern and params are merged. `before` filters can set instance +variables which can be used in the route block and the `after` filter. +For more info on the difference in how after blocks are handled, see the Errors section below for more info. ### 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 @@ -50,14 +51,13 @@ ##### 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 +responds to methods like `on_message` and `on_close`. A service-wide `on_pong` handler may be defined +to customize the behavior when a pong frame comes back from a connected websocket client. -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. @@ -94,19 +94,19 @@ 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. +In this case, any clients that connect to a websocket at the path '/' will be stashed in the +default websockets array; clients that connect to '/bar' will be stashed in the `:bar` section. -Each "section" can accessed with a familiar, `Hash`-like syntax, and can be iterated over with +Each "section" is 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 +When a `POST /` with a 'foo' param is received, any value is messaged out to all '/' connected websockets. When a `POST /bar` with a 'bar' param is received, any value is messaged out to all -websockets that connected to '/bar'. +websockets connected to '/bar'. ### SSE - Server-Sent Events The `eventsource` route builder also accepts a path and a block, and passes the socket to the block, just like the `websocket` builder. This socket is actually the raw `Celluloid::IO::TCPSocket` and is @@ -174,16 +174,16 @@ ``` ##### `sses` helper Angelo also includes a "stash" helper for SSE connections. One can `<<` a socket into `sses` from -inside an `eventsource` handler block. These can also be "later" be iterated over so one can do things +inside an `eventsource` handler block. These can "later" be iterated over so one can do things like emit a message on every SSE connection when the service receives a POST request. -The `sses` helper includes the same a context ability as the `websockets` helper. Also, by default, +The `sses` helper includes the same context ability as the `websockets` helper. Also, by default, the helper will `reject!` any closed sockets before returning, just like `websockets`. You may -optionally pass `false` to the helper to skip this step.In addition, the `sses` stash includes +optionally pass `false` to the helper to skip this step. In addition, the `sses` stash includes methods for easily sending events or messages to all stashed connections. **Note that the `Stash::SSE#event` method only works on non-default contexts and uses the context name as the event name.** ```ruby @@ -225,11 +225,11 @@ do not support EventSource yet, since this will respond with a non-"text/event-stream" Content-Type if 'sse' is not present in the params. ##### `EventSource#on_close` helper -When inside an eventsource block, you can use may want to do something specific when a client closes the +When inside an eventsource block, you may want to do something specific when a client closes the connection. For this case, there are `on_close` and `on_close=` methods on the object passed to the block that will get called if the client closes the socket. The assignment method takes a proc object and the other one takes a block: ```ruby @@ -250,27 +250,27 @@ sses << es end ``` -Note the use of the optional parameter the stashes here; by default, stash accessors (`websockets` and +Note the use of the optional parameter of the stashes here; by default, stash accessors (`websockets` and `sses`) will `reject!` any closed sockets before letting you in. If you pass `false` to the stash accessors, they will skip the `reject!` step. ### Tasks + Async / Future -Angelo is built on Reel and Celluloid::IO, giving your web application class the ability to define +Angelo is built on Reel and Celluloid::IO, giving your web application 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. +# the given number of seconds, then return the given message. # task :in_sec do |sec, msg| sleep sec.to_i msg end @@ -298,13 +298,13 @@ ``` ##### `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. +notable exception of returning a "future" object that you can call `#value` on later to retrieve +the return value of the task. Calling `#value` will "block" until the task is finished, while the +reactor continues to process requests. ```ruby get '/' do # run the task defined above asynchronously, return immediately # @@ -322,21 +322,21 @@ 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. +The main difference is that `halt` will still run `after` blocks, and raising `RequestError` +will bypass `after` blocks. 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. #### RequestError 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. +JSON, the body is converted to a JSON object with one key, `error`, that has the 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. @@ -412,19 +412,12 @@ everything's fine ``` ### [Tilt](https://github.com/rtomayko/tilt) / ERB -To make `erb` available in route blocks - -1. add `tilt` to your `Gemfile`: `gem 'tilt'` -2. require `angelo/tilt/erb` -3. include `Angelo::Tilt::ERB` in your app - ```ruby class Foo < Angelo::Base - include Angelo::Tilt::ERB views_dir 'some/other/path' # defaults to './views' get '/' do erb :index @@ -446,21 +439,12 @@ See [views](https://github.com/kenichi/angelo/tree/master/test/test_app_root/views) for examples. ### [Mustermann](https://github.com/rkh/mustermann) -To make routes blocks match path with Mustermann patterns - -1. be using ruby &gt;=2.0.0 -2. add 'mustermann' to to your `Gemfile`: `platform(:ruby_20){ gem 'mustermann' }` -3. require `angelo/mustermann` -4. include `Angelo::Mustermann` in your app - ```ruby 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: @@ -484,21 +468,81 @@ end end ``` +### Classic vs. modular apps + +Like Sinatra, Angelo apps can be written in either "classic" style or +so-called "modular" style. Which style you use is more of a personal +preference than anything else. + +A classic-style app requires "angelo/main" and defines the app +directly at the top level using the DSL. In addition, classic apps: + +* Can use a `helpers` block to define methods that can be called from +filters and route handlers. The `helpers` method can also include methods +from one or more modules passed as arguments instead of or in addition +to taking a block. +* Parse optional command-line options "-o addr" and "-p port" to set +the bind address and listen port, respectively. +* Are run automatically. + +*Note: unlike Sinatra, define a classic app by requiring "angelo/main" +and a modular app by requiring "angelo". Sinatra uses "sinatra" and +"sinatra/base" to do the same things.* + +Here's a classic app: + +```ruby +require 'angelo/main' + +helpers do + def say_hello + "Hello" + end +end + +get "/hello" do + "#{say_hello} to you, too." +end +``` + +And the same app in modular style: + +```ruby +require 'angelo' + +class HelloApp < Angelo::Base + def say_hello + "Hello" + end + + get "/hello" do + "#{say_hello} to you, too." + end +end + +HelloApp.run! +``` + +### Documentation + +**I'm bad at documentation and I feel bad.** + +Others have helped, and there is a YaRD plugin for Angelo [here](https://github.com/artcom/yard-angelo) +if you would like to document your apps built with Angelo. (thanks: @katjaeinsfeld, @artcom) + ### WORK LEFT TO DO Lots of work left to do! ### 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'