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.
Add this line to your application's Gemfile:
gem 'poncho'And then execute:
$ bundle
Or install it yourself as:
$ gem install poncho
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', &ChargeCreateMethodMethods 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
endTo invoke the method just add it to your routes.
Using Rails:
match '/users' => UsersListMethod, :via => :getUsing Sinatra:
get '/users', &UsersListMethodOr 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.
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_nameBy default, param are of type 'string'. you can choose a different type via the :type option:
param :amount, :type => :integerThere 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.
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 => trueTo 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..20You 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_validateAlternatively, 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 CustomValidatorFor a good example of how to build validations, see the existing ones.
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
endYou can use custom parameters via the :type option.
param :card, :type => Poncho::Params::CardHashParam
# Or the shortcut
param :card, :type => :card_hashYou can gain access to the rack request via the request method, for example:
def invoke
accept = request.headers['Accept']
200
endThe same goes for the response object:
def invoke
response.body = ['Fee-fi-fo-fum']
200
endThere are some helper methods to set such things as the HTTP status response codes and body.
def invoke
status 201
body 'Created!'
endThere 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
endYou 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
endIf 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
endJSONMethod 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 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
endAs 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
endIf a particular param points to another resource, you can use the :type => :resource option as demonstrated above.