# Jersey
Because [worse is better](http://en.wikipedia.org/wiki/Worse_is_better).
Jersey is a gem for people who want to write excellent APIs but have their
own opinions on how to structure their code and projects.
Jersey provides sensible defaults that are composed with sensible pieces, making
it easy to compose your own stack or use Jesery's compositions.
## Features
- env-conf for easy ENV based configuration
- request context aware request logging
- structured data loggers - json and logfmt
- unified exception handling
### `Jersey.setup`
Setup embodies a few opinions we have about apps:
- Having a notion of 'environment' where the configuration lives
- Using a dependency manager
- Always using UTC for the Time zone
- Always printing times in ISO8601 format
`Jersey.setup` allows any app that has Gemfiles and `.env` files to
"just work" in development *and* production with just:
```ruby
require 'jersey'
Jersey.setup
```
Which is usually included as the first line of code in a library consuming
Jersey. For tests, you will want to set `RACK_ENV` to `test` before
requiring your library that uses the above.
### `Jersey::API::Base`
Combines all of the Jersey middleware with a few standard middleware
from Rack and sinatra-contrib.
```ruby
class API < Jersey::API::Base
get '/hello' do
Jersey.log(at: "hello")
'hello'
end
get '/not_found' do
raise NotFound, "y u no here?"
end
end
```
```
$ curl http://localhost:9292/hello
```
Server logs:
```
at=start method=GET path=/hello request_id=2c018557-d246-4b04-a7d7-fc74bae67ec0 now=2014-11-11T18:04:25+00:00
at=hello now=2014-11-11T18:04:25+00:00 request_id=2c018557-d246-4b04-a7d7-fc74bae67ec0
at=finish method=GET path=/hello status=200 size#bytes=5 route_signature=/hello elapsed=0.000 request_id=2c018557-d246-4b04-a7d7-fc74bae67ec0 now=2014-11-11T18:04:25+00:00
```
Unified, structured logging with all the info you will wish you had:
```
$ curl http://localhost:9292/not_found | jq '.'
```
Server logs:
```
at=start method=GET path=/not_found request_id=f3085630-05cf-4314-a7dd-5855e752594b now=2014-11-11T18:05:15+00:00
at=finish method=GET path=/not_found status=404 size#bytes=6212 route_signature=/not_found elapsed=0.001 request_id=f3085630-05cf-4314-a7dd-5855e752594b now=2014-11-11T18:05:15+00:00
```
Response:
```json
{
"error": {
"type": "NotFound",
"message": "y u no here?",
"backtrace": [
"/Users/csquared/projects/jersey/examples/readme.ru:11:in `block in '",
"/Users/csquared/projects/jersey/.bundle/bundle/ruby/2.1.0/gems/sinatra-1.4.5/lib/sinatra/base.rb:1603:in `call'",
....
]
}
}
```
Unified, strucutred error handling. Notice how all we needed to do was raise `NotFound`
and we get a 404 response code (in the server logs) and our error message as part of the JSON payload.
#### `Jersey::HTTP::Errors`
Includes Ruby `Error` objects named with camel case for all of the HTTP 4xx and 5xx
errors. This allows you to raise `NotFound` as an error that has the `STATUS_CODE`
defined.
Allows uniform HTTP error handling when combined with the `ErrorHandler` sinatra extension.
#### Usage
Mix-in to any class that wants to raise HTTP errors, usually an API class.
```ruby
class API < Sinatra::Base
include Jersey::HTTP::Errors
end
```
### `Jersey::Extensions::ErrorHandler`
Unifies error responses. If the error object's class has a `STATUS_CODE` defined (such as the
errors in `Jersey::HTTP::Errors`), this will use that as the HTTP return status. The error
message and backtraces are included in responses assuming that this in for an internal API
over secured channels and therefore favors ease of debugging over the security risk of
including the backtrace. This is something I may want to configure.
#### Usage
Register as a Sinatra extension
```ruby
class API < Sinatra::Base
register Jersey::Extensions::ErrorHandler
end
```
#### `Jersey::Extensions::RouteSignature`
Adds a `ROUTE_SIGNATURE` to the `env` for each request, which is the name of an api endpoint
as it is *defined* versus the path that reaches your app.
For example, when you define a route such as ` get "/hello/:id"`, the `ROUTE_SIGNATURE` would
equal `"/hello/:id"`.
When combined with the `RequestLogger`,
it greatly simplifies creating aggregate statistics about the traffic hitting various api endpoints.
*Note:* this is considered a hack and something that sinatra should, but does not, handle.
#### Usage
Register as a Sinatra extension
```ruby
class API < Sinatra::Base
register Jersey::Extensions::RouteSignature
end
```
#### `Jersey::Middleware::AutoJson`
Uses content type matching regex `/json/` or a request body that
begins with a `{` to detect and parse json. Exposes json
via `request.body` and `params` so you can transparently
accept form-encoded and json bodies.
Adds a `#json` method to the `Rack::Request` object.
#### Usage
Use as a Rack middleware
```ruby
class API < Sinatra::Base
use Jersey::Middleware::AutoJson
post '/test-json' do
request.json == env['rack.json']
end
end
```
#### `Jersey::Helpers::AutoJsonParams`
Merges sinatra params Hash with json data parsed by a rack middleware
that has set `rack.json` on the rack env.
If the parsed data is an array, merges by using the array index as a hash key.
Json data gets precendence in naming collisions.
#### Usage
Use as a Sinatra Helper
Note: works with any Rack middleware that populates `env['rack.json']`
```ruby
class API < Sinatra::Base
helpers Jersey::Helpers::AutoJsonParams
post "/test-array-params" do
params[0]
end
end
```
#### `Jersey::Middleware::RequestID`
Creates a random request id for every http request, stored both in thread local storage
via the `RequestStore` and in the Rack `env`.
Works with or without explicitly including `RequestStore::Middleware`.
#### Usage
Use as a Rack middleware
```ruby
class API < Sinatra::Base
use Jersey::Middleware::RequestID
end
```
#### `Jersey::Middleware::RequestLogger`
Logs http start and finish and errors in a structured logging format.
It defaults to using the `Jersey.logger` singleton which is `RequestStore`-aware.
Anything in `RequestStore[:log]` will get appended to the log data. (This is how request ids
are made available to logger calls outside of HTTP blocks but within HTTP request lifecycles).
Because I think request_ids are important, the logger will try to get them from either the
`RequestStore` or the `env`.
Logs at request start:
at: "start",
request_id: env['REQUEST_ID'],
method: request.request_method,
path: request.path_info,
content_type: request.content_type,
content_length: request.content_length
Logs at request finish:
at: "finish",
method: request.request_method,
path: request.path_info,
status: status,
content_length: headers['Content-Length'],
route_signature: env['ROUTE_SIGNATURE'],
elapsed: (Time.now - @request_start).to_f,
request_id: env['REQUEST_ID']
#### Usage
Use as a Rack middleware
```ruby
class API < Sinatra::Base
use Jersey::Middleware::RequestLogger
end
```
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'jersey'
```
And then execute:
$ bundle
Or install it yourself as:
$ gem install jersey
## Contributing
1. Fork it ( https://github.com/[my-github-username]/jersey/fork )
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request