Poncho
Poncho is an API to build APIs or, in other words, a DSL to build REST interfaces.
It'll validate input and output, coerce values and is easily extendable with custom data types.
It's compatible with any rack-based framework, such as Rails or Sinatra.
Installation
Add this line to your application's Gemfile:
gem 'poncho'
And then execute:
$ bundle
Or install it yourself as:
$ gem install poncho
TLDR Usage
class ChargeResource < Poncho::Resource
param :amount, :type => :integer
param :currency
def currency
super || 'USD'
end
end
class ChargeCreateMethod < Poncho::JSONMethod
param :amount, :type => :integer, :required => true
param :currency, :in => ['USD', 'GBP']
def invoke
charge = Charge.new
charge.amount = param(:amount)
charge.currency = param(:currency)
charge.save
ChargeResource.new(charge)
end
end
post '/charges', &ChargeCreateMethod
Getting started with Methods
Methods inherit from Poncho::Method
and override invoke
, where they perform any necessary logic.
In a similar vein to Sinatra, anything returned from invoke
is sent right back to the user. You can
return a http status code, a body string, or even a Rack response array.
class ChargeListMethod < Poncho::Method
def invoke
# Some DB shizzle
200
end
end
To invoke the method just add it to your routes.
Using Rails:
match '/users' => UsersListMethod, :via => :get
Using Sinatra:
get '/users', &UsersListMethod
Or invoke manually:
UsersListMethod.call(rack_env)
If you're writing a JSON API, you'll probably want to inherit the Method from Poncho::JSONMethod
instead of
Poncho::Method
, but we'll cover that later.
Params
You can get access to the request params, via the params
or param(name)
methods.
Before you can use a param though, you need to define it:
param :param_name
By default, param are of type 'string'. you can choose a different type via the :type
option:
param :amount, :type => :integer
There are a bunch of predefined types, such as :integer
, :array
, :boolean_string
etc, but you can
also easily define your own custom ones (covered later).
Poncho will automatically validate that if a paramter is provided it is in a valid format. Poncho will also handle type conversion for you.
So for example, in the case above, Poncho will automatically validate that the amount
param is
indeed an Integer or an Integer string, and will coerce the parameter into an integer when you try to access it.
Validation
As well as the default type validation, Poncho lets you validate presence, format, length and much more!
For example, to validate that a :currency
parameter is provided, pass in the `:presence' option:
param :currency, :presence => true
To validate that a currency is either 'USD' or 'GBP', use the :in
option.
param :currency, :in => ['USD', 'GBP']
The other supported validations out of the box are :format
, :not_in
, and :length
:
param :email, :format => /@/
param :password, :length => 5..20
Custom Validation
You can use a custom validator via the validate
method, passing in a block:
validate do
unless param(:customer_id) ~= /\Acus_/
errors.add(:customer_id, :invalid_customer)
end
end
# Or
validates :customer_id, :customer_validate
Alternatively, if your validation is being used in multiple places, you can wrap it up in a class and
pass it to the validates_with
method.
validates_with CustomValidator
For a good example of how to build validations, see the existing ones.
Custom Params
As your API grows you'll probably start to need custom parameter types. These can be useful to ensure parameters are both valid and converted into suitable values.
To define a custom parameter, simply inherit from Poncho::Param
. For example, let's define a new param called
CardHashParam
. It needs to validate input via overriding the validate_each
method, and convert input via
overriding the convert
method.
module Poncho
module Params
class CardHashParam < Param
def validate_each(method, attribute, value)
value = convert(value)
unless value.is_a?(Hash) && value.keys == [:number, :exp_month, :exp_year, :cvc]
method.errors.add(attribute, :invalid_card_hash, options.merge(:value => value))
end
end
def convert(value)
value && value.symbolize_keys
end
end
end
end
You can use custom parameters via the :type
option.
param :card, :type => Poncho::Params::CardHashParam
# Or the shortcut
param :card, :type => :card_hash
Request & Response
You can gain access to the rack request via the request
method, for example:
def invoke
accept = request.headers['Accept']
200
end
The same goes for the response object:
def invoke
response.body = ['Fee-fi-fo-fum']
200
end
There are some helper methods to set such things as the HTTP status response codes and body.
def invoke
status 201
body 'Created!'
end
Method filters
There are various filters you can apply to the request, for example:
class MyMethod < Poncho::Method
before_validation do
# Before validation
end
before do
# Before invoke
p params
end
after do
# After invocation
end
end
Error responses
You can provide custom responses to exceptions via the error
class method.
Pass error
a exception type or status code.
class MyMethod < Poncho::Method
error MyCustomClass do
'Sorry, something went wrong.'
end
error 403 do
'Not authorized.'
end
end
JSON APIs
If your API only returns JSON then Poncho has a convenient JSONMethod
class which
will ensure that all response bodies are converted into JSON and that the correct content type
header is set.
class TokenCreateMethod < Poncho::JSONMethod
param :number, :required => true
def invoke
{:token => '123'}
end
end
JSONMethod
also ensures that there's valid JSON error responses to 404s and 500s, as well
as returning a JSON error hash for validation errors.
$ curl http://localhost:4567/tokens -d number=
{"error":{"param":"number","type":"presence"}}
Resources
Resources are wrappers around other classes, such as models, providing a view representation of them.
You can specify attributes to be returned to the client using the same param
syntax as documented above.
class Card
attr_reader :number
def initialize(number)
@number = number
end
end
class CardResource < Poncho::Resource
param :number
param :description
def number
super[-4..-1]
end
end
As you can see in the example above, you can override params and return a custom response.
When the Resource
instance is converted into JSON the appropriate params will be used and serialized.
class ChargeResource < Poncho::Resource
param :amount, :type => :integer
param :currency
param :card, :resource => CardResource
def currency
super || 'USD'
end
end
class ChargeListMethod < Poncho::JSONMethod
def invoke
[
ChargeResource.new(Charge.new(1000, 'USD')),
ChargeResource.new(Charge.new(50, 'USD'))
]
end
end
If a particular param points to another resource, you can use the :type => :resource
option as demonstrated above.