NAME
McBain - Framework for building portable, auto-validating and self-documenting APIs
VERSION
version 1.000000
SYNOPSIS
package MyAPI;
use McBain; # imports strict and warnings for you
get '/multiply' => (
description => 'Multiplies two integers',
params => {
one => { required => 1, integer => 1 },
two => { required => 1, integer => 1 }
},
cb => sub {
my ($api, $params) = @_;
return $params->{one} + $params->{two};
}
);
post '/factorial' => (
description => 'Calculates the factorial of an integer',
params => {
num => { required => 1, integer => 1, min_value => 0 }
},
cb => sub {
my ($api, $params) = @_;
# note how this route both uses another
# route and calls itself recursively
if ($params->{num} <= 1) {
return 1;
} else {
return $api->forward('GET:/multiply', {
one => $params->{num},
two => $api->forward('POST:/factorial', { num => $params->{num} - 1 })
});
}
}
);
1;
DESCRIPTION
McBain
is a framework for building powerful APIs and applications. Writing an API with McBain
provides the following benefits:
Lightweight-ness
McBain
is extremely lightweight, with minimal dependencies on non-core modules; only two packages; and a succinct, minimal syntax that is easy to remember. Your APIs and applications will require less resources and perform better. Maybe.Portability
McBain
APIs can be run/used in a variety of ways with absolutely no changes of code. For example, they can be used directly from Perl code (see McBain::Directly), as fully fledged RESTful PSGI web services (see McBain::WithPSGI), or as Gearman workers (see McBain::WithGearmanXS). Seriously, no change of code required. More McBain runners are yet to come, and you can create your own, god knows I don't have the time or motivation or talent. Why should I do it for you anyway?Auto-Validation
No more tedious input tests.
McBain
will handle input validation for you. All you need to do is define the parameters you expect to get with the simple and easy to remember syntax provided by Brannigan. When your API is used,McBain
will automatically validate input. If validation fails,McBain
will return appropriate errors and tell the users of your API that they suck.Self-Documentation
McBain
also eases the burden of having to document your APIs, so that other people can actually use it (and you, two weeks later when you're drunk and can't remember why you wrote the thing in the first place). Using simple descriptions you give to your API's methods, and the parameter definitions,McBain
can automatically create a manual document describing your API (see the mcbain2pod command line utility).Modularity and Flexibility
APIs written with
McBain
are modular and flexible. You can make them object oriented if you want, or not,McBain
won't care, it's unobtrusive like that. APIs are hierarchical, and every module in the API can be used as a complete API all by itself, detached from its siblings, so you can actually load only the parts of the API you need. Why is this useful? I don't know, maybe it isn't, what do I care? It happened by accident anyway.No More World Hunger
It'll do that too, just give it a chance.
FUNCTIONS
The following functions are exported:
provide( $method, $route, %opts )
Define a method and a route. $method
is one of GET
, POST
, PUT
or DELETE
. $route
is a string that starts with a forward slash, like a path in a URI. %opts
can hold the following keys (only cb
is required):
description
A short description of the method and what it does.
params
A hash-ref of parameters in the syntax of Brannigan (see Brannigan::Validations for a complete references).
cb
An anonymous subroutine (or a subroutine reference) to run when the route is called. The method will receive the root topic class (or object, if the topics are written in object oriented style), and a hash-ref of parameters.
get( $route, %opts )
Shortcut for provide( 'GET', $route, %opts )
post( $route, %opts )
Shortcut for provide( 'POST', $route, %opts )
put( $route, %opts )
Shortcut for provide( 'PUT', $route, %opts )
del( $route, %opts )
Shortcut for provide( 'DELETE', $route, %opts )
METHODS
The following methods will be available on importing classes/objects:
call( @args )
Calls the API, requesting the execution of a certain route. This is the main way your API is used. The arguments it expects to receive and its behavior are dependent on the McBain runner used. Refer to the docs of the runner you wish to use for more information.
forward( $namespace, [ \%params ] )
For usage from within API methods; this simply calls a method of the the API with the provided parameters (if any) and returns the result. With forward()
, an API method can call other API methods or even itself (for recursive operations).
$namespace
is the method and route to execute, in the format <METHOD>:<ROUTE>
, where METHOD
is one of GET
, POST
, PUT
, DELETE
, and ROUTE
starts with a forward slash.
is_root( )
Returns a true value if the module is the root topic of the API. Mostly used internally and in McBain runner modules.
MANUAL
ANATOMY OF AN API
Writing an API with McBain
is easy. The syntax is short and easy to remember, and the feature list is just what it needs to be - short and sweet.
The main idea of a McBain
API is this: a client requests the execution of a method provided by the API, sending a hash of parameters. The API then executes the method with the client's parameters, and produces a response. Every runner module will enforce a different response format (and even request format). When the API is used directly, for example, whatever the API produces is returned as is. The PSGI and Gearman::XS runners, however, are both JSON-in JSON-out interfaces.
A McBain
API is built of one or more topics, in a hierarchical structure. A topic is a class that provides methods that are categorically similar. For example, an API might have a topic called "math" that provides math-related methods such as add, multiply, divide, etc.
Since topics are hierarchical, every API will have a root topic, which may have zero or more child topics. The root topic if where your API begins, and it's decision how to utilize it. If your API is short and simple, with methods that cannot be categorized into different topics, then the entire API can live within the root topic itself, with no child topics at all. If, however, you're building a larger API, then the root topic might be empty, or it can provide general-purpose methods that do not particularly fit in a specific topic, for example maybe a status method that returns the status of the service, or an authentication method.
The name of a topic is calculated from the name of the package itself. The root topic is always called /
(forward slash), and its child topics are named like their package names, in lowercase, relative to the root topic, with /
as a separator instead of Perl's ::
, and starting with a slash. For example, lets look at the following API packages:
MyAPI - the root topic, will be called "/"
MyAPI::Math - a child topic, will be called "/math"
MyAPI::Math::Constants - a child-of-child, will be called "/math/constants"
MyAPI::Strings - a child topic, will be called "/strings"
You will notice that the naming of the topics is similar to paths in HTTP URIs. This is by design, since I wrote McBain
mostly for writing web applications (with the PSGI runner), and the RESTful architecture fits well with APIs whether they are HTTP-based or not.
CREATING TOPICS
To create a topic package, all you need to do is:
use McBain;
This will import McBain
functions into the package, register the package as a topic (possibly the root topic), and attempt to load all child topics, if there are any. For convenience, McBain
will also import strict and warnings for you.
Notice that using McBain
doesn't make your package an OO class. If you want your API to be object oriented, you are free to form your classes however you want, for example with Moo or Moose:
package MyAPI;
use McBain;
use Moo;
has 'some_attr' => ( is => 'ro' );
1;
CREATING ROUTES AND METHODS
The resemblance with HTTP continues as we delve further into methods themselves. An API topic defines routes, and one or more methods that can be executed on every route. Just like HTTP, these methods are GET
, POST
, PUT
and DELETE
.
Route names are like topic names. They begin with a slash, and every topic can have a root route which is just called /
. Every method defined on a route will have a complete name (or path, if you will), in the format <METHOD_NAME>:<TOPIC_NAME><ROUTE_NAME>
. For example, let's say we have a topic called /math
, and this topic has a route called /divide
, with one GET
method defined on this route. The complete name (or path) of this method will be GET:/math/divide
.
By using this structure and semantics, it is easy to create CRUD interfaces. Lets say your API has a topic called /articles
, that deals with articles in your blog. Every article has an integer ID. The /articles
topic can have the following routes and methods:
POST:/articles/ - Create a new article (root route /)
GET:/articles/(\d+) - Read an article
PUT:/articles/(\d+) - Update an article
DELETE:/articles/(\d+) - Delete an article
Methods are defined using the get(), post(), put() and del() subroutines. The syntax is similar to Moose's antlers:
get '/multiply' => (
description => 'Multiplies two integers',
params => {
a => { required => 1, integer => 1 },
b => { required => 1, integer => 1 }
},
cb => sub {
my ($api, $params) = @_;
return $params->{a} * $params->{b};
}
);
Of the three keys above (description
, params
and cb
), only cb
is required. It takes the actual subroutine to execute when the method is called. The subroutine will get two arguments: first, the root topic (either its package name, or its object, if you're creating an object oriented API), and a hash-ref of parameters provided to the method (if any).
You can provide McBain
with a short description
of the method, so that McBain
can use it when documenting the API with mcbain2pod.
You can also tell McBain
which parameters your method takes. The params
key will take a hash-ref of parameters, in the format defined by Brannigan (see Brannigan::Validations for a complete references). These will be both enforced and documented.
As you may have noticed in the /articles
example, routes can be defined using regular expressions. This is useful for creating proper RESTful URLs:
# in topic /articles
get '/(\d+)' => (
description => 'Returns an article by its integer ID',
cb => sub {
my ($api, $params, $id) = @_;
return $api->db->get_article($id);
}
);
If the regular expression contains captures, and a call to the API matches the regular expressions, the values captured will be passed to the method, after the parameters hash-ref (even if the method does not define parameters, in which case the parameters hash-ref will be empty - this may change in the future).
It is worth understanding how McBain
builds the regular expression. In the above example, the topic is /articles
, and the route is /(\d+)
. Internally, the generated regular expression will be ^/articles/(\d+)$
. Notice how the topic and route are concatenated, and how the ^
and $
metacharacters are added to the beginning and end of the regex, respectively. This means it is impossible to create partial regexes, which only pose problems in my experience.
CALLING METHODS FROM WITHIN METHODS
Methods are allowed to call other methods (whether in the same route or not), and even call themselves recursively. This can be accomplished easily with the forward() method. For example:
get '/factorial => (
description => 'Calculates the factorial of a number',
params => {
num => { required => 1, integer => 1 }
},
cb => sub {
my ($api, $params) = @_;
if ($params->{num} <= 1) {
return 1;
} else {
return $api->forward('GET:/multiply', {
one => $params->{num},
two => $api->forward('GET:/factorial', { num => $params->{num} - 1 })
});
}
}
);
In the above example, notice how the GET:/factorial
method calls both GET:/multiply
and itself.
EXCEPTIONS
McBain
APIs handle errors in a graceful way, returning proper error responses to callers. As always, the way errors are returned depends on the runner module used. When used directly from Perl code, McBain will confess (i.e. die) with a hash-ref consisting of two keys:
code
- An HTTP status code indicating the type of the error (for example 404 if the route doesn't exist, 405 if the route exists but the method is not allowed, 400 if parameters failed validation, etc.).error
- The text/description of the error.
Depending on the type of the error, more keys might be added to the exception. For example, the parameters failed validation error will also include a rejects
key holding Brannigan's standard rejects hash, describing which parameters failed validation.
When writing APIs, you are encouraged to return exceptions in this format to ensure proper handling by McBain
. If McBain
encounters an exception that does not conform to this format, it will generate an exception with code
500 (indicating "Internal Server Error"), and the error
key will hold the exception as is.
MCBAIN RUNNERS
A runner module is in charge of loading McBain
APIs in a specific way. The default runner, McBain::Directly, is the simplest runner there is, and is meant for using APIs directly from Perl code.
When a McBain
API is loaded, the selected runner module is actually set as the base class of McBain
, thus tweaking its behavior. The runner is in charge of whatever heavy lifting is required in order to turn your API into a "service", or an "app", or whatever it is you think your API needs to be.
The following runners are currently available:
McBain::Directly - Directly use an API from Perl code.
McBain::WithPSGI - Turn an API into a Plack based, JSON-to-JSON RESTful web application.
McBain::WithGearmanXS - Turn an API into a JSON-to-JSON Gearman worker.
The latter two completely change the way your API is used, and yet you can see their code is very short.
McBain
knows which runner module to use by the value of the MCBAIN_WITH
environment variable (i.e. $ENV{MCBAIN_WITH}
). For example, if the value of the variable is WithPSGI
, then McBain
will use McBain::WithPSGI
as the runner module.
You can easily create your own runner modules, so that your APIs can be used in different ways. A runner module needs to implement the following interface:
init( $runner_class, $target_class )
This method is called when McBain
is first imported into an API topic. $target_class
will hold the name of the class currently being imported to.
You can do whatever initializations you need to do here, possibly manipulating the target class directly. You will probably only want to do this on the root topic, which is why "is_root( )" is available on $target_class
.
You can look at WithPSGI
and WithGearmanXS
to see how they're using the init()
method. For example, in WithPSGI
, Plack::Component is added to the @ISA
array of the root topic, so that it turns into a Plack app. In WithGearmanXS
, the init()
method is used to define a work()
method on the root topic, so that your API can run as any standard Gearman worker.
generate_env( $runner_class, @call_args )
This method receives whatever arguments were passed to the "call( @args )" method. It is in charge of returning a standard hash-ref that McBain
can use in order to determine which route the caller wants to execute, and with what parameters. Remember that the way call()
is invoked depends on the runner used.
The hash-ref returned must have the following key-value pairs:
ROUTE - The route to execute (string).
METHOD - The method to call on the route (string).
PAYLOAD - A hash-ref of parameters to provide for the method. If no parameters are provided, an empty hash-ref should be given.
The returned hash-ref is called $env
, inspired by PSGI.
generate_res( $runner_class, \%env, $result )
This method formats the result from a route before returning it to the caller. It receives the $env
hash-ref (if needed), and the result from the route. In the WithPSGI
runner, for example, this method encodes the result into JSON and returns a proper PSGI response array-ref.
handle_exception( $runner_class, $error, @args )
This method will be called whenever a route raises an exception, or otherwise your code fails. The $error
variable will always be a standard exception hash-ref, with code
and error
keys, and possibly more. Read the discussion above.
The method should format the error before returning it to the user, similar to what generate_res()
above performs, but it allows you to handle exceptions gracefully.
Whatever arguments were provided to call()
will be provided to this method as-is, so that you can inspect or use them if need be. WithGearmanXS
, for example, will get the Gearman::XS::Job object and call the send_fail()
method on it, to properly indicate the job failed.
CONFIGURATION AND ENVIRONMENT
McBain
itself requires no configuration files or environment variables. However, when using/running APIs written with McBain
, the MCBAIN_WITH
environment variable might be needed to tell McBain
the name of the runner module to use. The default value is "Directly", so McBain::Directly is used. See the various McBain
runner modules for more information.
DEPENDENCIES
McBain
depends on the following CPAN modules:
The command line utility, mcbain2pod, depends on the following CPAN modules:
INCOMPATIBILITIES WITH OTHER MODULES
None reported.
BUGS AND LIMITATIONS
Please report any bugs or feature requests to bug-McBain@rt.cpan.org
, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=McBain.
SUPPORT
You can find documentation for this module with the perldoc command.
perldoc McBain
You can also look for information at:
RT: CPAN's request tracker
AnnoCPAN: Annotated CPAN documentation
CPAN Ratings
Search CPAN
AUTHOR
Ido Perlmuter <ido@ido50.net>
LICENSE AND COPYRIGHT
Copyright (c) 2013, Ido Perlmuter ido@ido50.net
.
This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either version 5.8.1 or any later version. See perlartistic and perlgpl.
The full text of the license can be found in the LICENSE file included with this module.
DISCLAIMER OF WARRANTY
BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.