Raisin - A REST API microframework for Perl.
use HTTP::Status qw(:constants); use List::Util qw(max); use Raisin::API; use Types::Standard qw(HashRef Any Int Str); my %USERS = ( 1 => { first_name => 'Darth', last_name => 'Wader', password => 'deathstar', email => 'darth@deathstar.com', }, 2 => { first_name => 'Luke', last_name => 'Skywalker', password => 'qwerty', email => 'l.skywalker@jedi.com', }, ); plugin 'Logger', fallback => 1; app->log( debug => 'Starting Raisin...' ); middleware 'CrossOrigin', origins => '*', methods => [qw/DELETE GET HEAD OPTIONS PATCH POST PUT/], headers => [qw/accept authorization content-type api_key_token/]; plugin 'Swagger'; swagger_setup( title => 'A POD synopsis API', description => 'An example of API documentation.', #terms_of_service => '', contact => { name => 'Artur Khabibullin', url => 'http://github.com/khrt', email => 'rtkh@cpan.org', }, license => { name => 'Perl license', url => 'http://dev.perl.org/licenses/', }, ); desc 'Users API'; resource users => sub { summary 'List users'; params( optional('start', type => Int, default => 0, desc => 'Pager (start)'), optional('count', type => Int, default => 10, desc => 'Pager (count)'), ); get sub { my $params = shift; my @users = map { { id => $_, %{ $USERS{$_} } } } sort { $a <=> $b } keys %USERS; my $max_count = scalar(@users) - 1; my $start = $params->{start} > $max_count ? $max_count : $params->{start}; my $count = $params->{count} > $max_count ? $max_count : $params->{count}; my @slice = @users[$start .. $count]; { data => \@slice } }; summary 'List all users at once'; get 'all' => sub { my @users = map { { id => $_, %{ $USERS{$_} } } } sort { $a <=> $b } keys %USERS; { data => \@users } }; summary 'Create new user'; params( requires('user', type => HashRef, desc => 'User object', group { requires('first_name', type => Str, desc => 'First name'), requires('last_name', type => Str, desc => 'Last name'), requires('password', type => Str, desc => 'User password'), optional('email', type => Str, default => undef, regex => qr/.+\@.+/, desc => 'User email'), }), ); post sub { my $params = shift; my $id = max(keys %USERS) + 1; $USERS{$id} = $params->{user}; res->status(HTTP_CREATED); { success => 1 } }; desc 'Actions on the user'; params requires('id', type => Int, desc => 'User ID'); route_param 'id' => sub { summary 'Show user'; get sub { my $params = shift; $USERS{ $params->{id} }; }; summary 'Delete user'; del sub { my $params = shift; delete $USERS{ $params->{id} }; res->status(HTTP_NO_CONTENT); undef; }; }; }; run;
Raisin is a REST API microframework for Perl. It's designed to run on Plack, providing a simple DSL to develop RESTful APIs easily. It was inspired by Grape.
Adds a route to an application.
resource user => sub { ... };
Defines a route parameter as a resource id which can be anything if type isn't specified for it.
id
route_param id => sub { ... };
Shortcuts to add a route restricted to the corresponding HTTP method.
route
get sub { 'GET' }; del 'all' => sub { 'OK' }; params( requires('id', type => Int), optional('key', type => Str), ); get sub { 'GET' }; desc 'Put data'; params( required('id', type => Int), optional('name', type => Str), ); put 'all' => sub { 'PUT' };
Adds a description to resource or any of the HTTP methods. Useful for OpenAPI as it's shown there as a description of an action.
resource
desc 'Some long explanation about an action'; put sub { ... }; desc 'Some exaplanation about a group of actions', resource => 'user' => sub { ... }
Same as "desc" but shorter.
summary 'Some summary'; put sub { ... };
Tags can be used for logical grouping of operations by resources or any other qualifier. Using in API description.
tags 'delete', 'user'; delete sub { ... };
By default tags are added automatically based on it's namespace but you always can overwrite it using the function.
Describes response object which will be used to generate OpenAPI description.
entity 'MusicApp::Entity::Album'; get { my $albums = $schema->resultset('Album'); present data => $albums, with => 'MusicApp::Entity::Album'; };
Defines validations and coercion options for your parameters. Can be applied to any HTTP method and/or "route_param" to describe parameters.
params( requires('name', type => Str), optional('start', type => Int, default => 0), optional('count', type => Int, default => 10), ); get sub { ... }; params( requires('id', type => Int, desc => 'User ID'), ); route_param 'id' => sub { ... };
For more see "Validation-and-coercion" in Raisin.
Specifies default API format mode when formatter isn't specified by API user. E.g. if URI is asked without an extension (json, yaml) or Accept header isn't specified the default format will be used.
json
yaml
Accept
Default value: YAML.
YAML
api_default_format 'json';
See also "API-FORMATS" in Raisin.
Restricts API to use only specified formatter to serialize and deserialize data.
Already exists Raisin::Encoder::JSON, Raisin::Encoder::YAML, and Raisin::Encoder::Text, but you can always register your own using "register_encoder".
api_format 'json';
Sets up an API version header.
api_version 1.23;
Loads a Raisin module. A module options may be specified after the module name. Compatible with Kelp modules.
plugin 'Swagger';
Adds a middleware to your application.
middleware '+Plack::Middleware::Session' => { store => 'File' }; middleware '+Plack::Middleware::ContentLength'; middleware 'Runtime'; # will be loaded Plack::Middleware::Runtime
Mounts multiple API implementations inside another one. These don't have to be different versions, but may be components of the same API.
In RaisinApp.pm:
RaisinApp.pm
package RaisinApp; use Raisin::API; api_format 'json'; mount 'RaisinApp::User'; mount 'RaisinApp::Host'; 1;
Registers a third-party parser (decoder).
register_decoder(xml => 'My::Parser::XML');
See also Raisin::Decoder.
Registers a third-party formatter (encoder).
register_encoder(xml => 'My::Formatter::XML');
See also Raisin::Encoder.
Returns the PSGI application.
PSGI
Provides quick access to the Raisin::Request object for the current route.
Use req to get access to request headers, params, etc.
req
use DDP; p req->headers; p req->params; say req->header('X-Header');
See also Plack::Request.
Provides quick access to the Raisin::Response object for the current route.
Use res to set up response parameters.
res
res->status(403); res->headers(['X-Application' => 'Raisin Application']);
See also Plack::Response.
Returns request parameters. Without an argument will return an array of all input parameters. Otherwise it will return the value of the requested parameter.
Returns Hash::MultiValue object.
say param('key'); # -> value say param(); # -> { key => 'value', foo => 'bar' }
Returns all declared parameters even if there is no value for a param.
See "Declared-parameters" in Raisin.
Returns psgix.session hash. When it exists, you can retrieve and store per-session data.
psgix.session
# store param session->{hello} = 'World!'; # read param say session->{name};
Raisin hash a built-in present method, which accepts two arguments: an object to be presented and an options associated with it. The options hash may include with key, which is defined the entity to expose. See Raisin::Entity.
present
with
my $artists = $schema->resultset('Artist'); present data => $artists, with => 'MusicApp::Entity::Artist'; present count => $artists->count;
Raisin::Entity supports DBIx::Class and Rose::DB::Object.
For details see examples in examples/music-app and Raisin::Entity.
When you add a route for a resource, a route for the OPTIONS method will also be added. The response to an OPTIONS request will include an "Allow" header listing the supported methods.
get 'count' => sub { { count => $count }; }; params( requires('num', type => Int, desc => 'Value to add to the count.'), ); put 'count' => sub { my $params = shift; $count += $params->{num}; { count: $count }; }; curl -v -X OPTIONS http://localhost:5000/count > OPTIONS /count HTTP/1.1 > Host: localhost:5000 > * HTTP 1.0, assume close after body < HTTP/1.1 204 No Content < Allow: GET, OPTIONS, PUT
If a request for a resource is made with an unsupported HTTP method, an HTTP 405 (Method Not Allowed) response will be returned.
curl -X DELETE -v http://localhost:3000/count > DELETE /count HTTP/1.1 > Host: localhost:5000 > * HTTP 1.0, assume close after body < HTTP/1.1 405 Method Not Allowed < Allow: OPTIONS, GET, PUT
Request parameters are available through the params HASH. This includes GET, POST and PUT parameters, along with any named parameters you specify in your route strings.
params
HASH
Parameters are automatically populated from the request body on POST and PUT for form input, JSON and YAML content-types.
POST
PUT
JSON
The request:
curl localhost:5000/data -H Content-Type:application/json -d '{"id": "14"}'
The Raisin endpoint:
post data => sub { param('id') };
Multipart POSTs and PUTs are supported as well.
In the case of conflict between either of:
path parameters;
GET, POST and PUT parameters;
contents of request body on POST and PUT;
Path parameters have precedence.
Query string and body parameters will be merged (see "parameters" in Plack::Request)
Raisin allows you to access only the parameters that have been declared by you in "params" in Raisin block.
By default you can get all declared parameter as a first argument passed to your route subroutine.
Application:
api_format 'json'; post data => sub { my $params = shift; { data => $params }; };
Request:
curl -X POST -H "Content-Type: application/json" localhost:5000/signup -d '{"id": 42}'
Response:
{ "data": nil }
Once we add parameters block, Raisin will start return only the declared parameters.
api_format 'json'; params( requires('id', type => Int), optional('email', type => Str) ); post data => sub { my $params = shift; { data => $params }; };
curl -X POST -H "Content-Type: application/json" localhost:5000/signup -d '{"id": 42, "key": "value"}'
{ "data": { "id": 42 } }
By default declared parameters don't contain parameters which have no value. If you want to return all parameters you can use the include_missing function.
include_missing
api_format 'json'; params( requires('id', type => Int), optional('email', type => Str) ); post data => sub { my $params = shift; { data => include_missing($params) }; };
{ "data": { "id": 42, "email": null } }
You can define validations and coercion options for your parameters using a "params" in Raisin block.
Parameters can requires value or can be optional. optional parameters can have default value.
requires
optional
params( requires('name', type => Str), optional('count', type => Int, default => 10), ); get sub { my $params = shift; "$params->{count}: $params->{name}"; };
Note that default values will NOT be passed through to any validation options specified.
Available arguments:
name
type
default
desc
regex
in
Use a keyword group to define a group of parameters which is enclosed to the parent HashRef parameter.
group
HashRef
params( requires('name', type => HashRef, group { requires('first_name', type => Str), requires('last_name', type => Str), }) )
Use ArrayRef[*] types from your compatible type library to define arrays.
ArrayRef[*]
requires('list', type => ArrayRef[Int], desc => 'List of integers')
Raisin supports Moo(se)-compatible type constraint so you can use any of the Moose, Moo or Type::Tiny type constraints.
By default Raisin depends on Type::Tiny and it's Types::Standard type contraint library.
You can create your own types as well. See Type::Tiny::Manual and Moose::Manual::Types.
Those blocks can be executed before or/and after every API call, using before, after, before_validation and after_validation.
before
after
before_validation
after_validation
Callbacks execute in the following order:
The block applies to every API call
before sub { my $self = shift; say $self->req->method . "\t" . $self->req->path; }; after_validation sub { my $self = shift; say $self->res->body; };
Steps after_validation and after are executed only if validation succeeds.
Every callback has only one argument as an input parameter which is Raisin object. For more information of available methods see "CONTROLLER" in Raisin.
By default, Raisin supports YAML, JSON, and TEXT content types. Default format is YAML.
TEXT
Response format can be determined by Accept header or route extension.
Accept header
route extension
Serialization takes place automatically. So, you do not have to call encode_json in each JSON API implementation.
encode_json
Your API can declare to support only one serializator by using "api_format" in Raisin.
Custom formatters for existing and additional types can be defined with a Raisin::Encoder/Raisin::Decoder.
Call JSON::encode_json and JSON::decode_json.
JSON::encode_json
JSON::decode_json
Call YAML::Dump and YAML::Load.
YAML::Dump
YAML::Load
Call Data::Dumper->Dump if output data is not a string.
Data::Dumper->Dump
The order for choosing the format is the following.
Use the route extension.
Use the value of the Accept header.
Fallback to default.
Raisin has a built-in logger and supports for Log::Dispatch. You can enable it by:
Log::Dispatch
plugin 'Logger', outputs => [['Screen', min_level => 'debug']];
Or use Raisin::Logger with a fallback option:
fallback
plugin 'Logger', fallback => 1;
The plugin registers a log subroutine to Raisin. Below are examples of how to use it.
log
app->log(debug => 'Debug!'); app->log(warn => 'Warn!'); app->log(error => 'Error!');
app is a Raisin instance, so you can use $self instead of app where it is possible.
app
$self
See Raisin::Plugin::Logger.
You can see application routes with the following command:
$ raisin examples/pod-synopsis-app/darth.pl GET /user GET /user/all POST /user GET /user/:id DELETE /user/:id PUT /user/:id GET /echo
Including parameters:
$ raisin --params examples/pod-synopsis-app/darth.pl GET /user start Int{0} count Int{10} GET /user/all POST /user *name Str *password Str email Str GET /user/:id *id Int DELETE /user/:id *id Int PUT /user/:id *id Int GET /echo *data Any{ёй}
Swagger compatible API documentations.
Documentation will be available on http://<url>/swagger.json URL. So you can use this URL in Swagger UI.
http://<url>/swagger.json
See Raisin::Plugin::Swagger.
You can easily add any Plack middleware to your application using middleware keyword. See "middleware" in Raisin.
middleware
Raisin can be extended using custom modules. Each new module must be a subclass of the Raisin::Plugin namespace. Modules' job is to initialize and register new methods into the web application class.
Raisin::Plugin
For more see "plugin" in Raisin and Raisin::Plugin.
See Plack::Test, Test::More and etc.
my $app = Plack::Util::load_psgi("$Bin/../script/raisinapp.pl"); test_psgi $app, sub { my $cb = shift; my $res = $cb->(GET '/user'); subtest 'GET /user' => sub { if (!is $res->code, 200) { diag $res->content; BAIL_OUT 'FAILED!'; } my $got = Load($res->content); isdeeply $got, $expected, 'Data!'; }; };
Deploying a Raisin application is done the same way any other Plack application is deployed:
$ plackup -E deployment -s Starman app.psgi
use Plack::Builder; use RaisinApp; use KelpApp; builder { mount '/' => KelpApp->new->run; mount '/api/rest' => RaisinApp->new; };
use Plack::Builder; use Dancer ':syntax'; use Dancer::Handler; use RaisinApp; my $dancer = sub { setting appdir => '/home/dotcloud/current'; load_app 'My::App'; Dancer::App->set_running_app('My::App'); my $env = shift; Dancer::Handler->init_request_headers($env); my $req = Dancer::Request->new(env => $env); Dancer->dance($req); }; builder { mount '/' => $dancer; mount '/api/rest' => RaisinApp->new; };
use Plack::Builder; use RaisinApp; builder { mount '/' => builder { enable 'Deflater'; require 'my_mojolicious-lite_app.pl'; }; mount '/api/rest' => RaisinApp->new; };
See also Plack::Builder, Plack::App::URLMap.
Raisin comes with three instance in example directory:
Basic example.
Shows the possibility of using "present" in Raisin with DBIx::Class and Rose::DB::Object.
Shows an example of complex application.
Versioning support;
Mount API's in any place of resource block;
https://github.com/khrt/Raisin
This module was inspired both by Grape and Kelp, which was inspired by Dancer, which in its turn was inspired by Sinatra.
Artur Khabibullin - rtkh@cpan.org
This module and all the modules in this package are governed by the same license as Perl itself.
To install Raisin, copy and paste the appropriate command in to your terminal.
cpanm
cpanm Raisin
CPAN shell
perl -MCPAN -e shell install Raisin
For more information on module installation, please visit the detailed CPAN module installation guide.