NAME
Perinci::Manual::Tutorial - Tutorial for using the Perinci modules
VERSION
version 0.29
ABOUT PERINCI
Perinci is a suite of Perl modules for providing implementation and tools for Rinci and Riap. Rinci is a metadata specification for your code entities, implementable in Perl but also other languages. Code entities include functions, variables, packages, classes, and so on. Metadata include summary, description, function argument specification, dependencies, features that a function has; basically anything that can be described about your function (or other entity type). Riap is a protocol for accessing code entity and its metadata, either locally or remotely and across language.
The main philosophy that guides the development of Rinci, Riap, and Perinci is laziness: not having to do much plumbing manually, not having to write needless code, not wanting to repeat yourself, not having to reimplement existing stuffs just because you switch language, not having to learn different ways to access API's for each web service. Another philosophy is to focus on functions (as opposed to classes or objects, or other kind of code entity). Function is the basic unit of reuse of a program. By making functions more powerful, reusable, flexible, we can greatly improve the development experience.
GETTING STARTED
Let us start this tutorial by writing an example function:
package MyApp;
use Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(add_array);
sub add_array {
my ($a1, $a2) = @_;
my @sum;
for (0..@$a1-1) {
push @sum, $a1->[$_] + $a2->[$_];
}
return \@sum;
}
1;
To use this function:
use MyApp qw(add_array);
add_array([1, 2, 3], [4, 5, 6]); # returns [5, 7, 9]
But suppose we want to check our input to make sure we are fed arrays of numbers:
use Scalar::Util qw(reftype looks_like_number);
sub add_array {
my ($a1, $a2) = @_;
# validate
defined($a1) or die "Please specify a1";
ref($a1) eq 'ARRAY' or die "Invalid a1, must be an array";
for (@$a1) {
die "Found a non-number element in a1" unless looks_like_number($_);
}
defined($a2) or die "Please specify a2";
ref($a2) eq 'ARRAY' or die "Invalid a2, must be an array";
for (@$a2) {
die "Found a non-number element in a2" unless looks_like_number($_);
}
my @sum;
for (0..@$a1-1) {
push @sum, $a1->[$_] + $a2->[$_];
}
return \@sum;
}
Here we see a problem: validating input can sometimes be tedious and boring. In the above example, it is even longer than the actual "business" routine.
What about if we just specify, in a higher-level language, what kind of input we want, and later just let some code generator generate the validator code for us? Let us write our first function metadata.
our %SPEC;
$SPEC{add_array} = {
v => 1.1,
result_naked => 1,
args_as => array,
args => {
a1 => { schema => ['array*' => {of=>'num*'}], pos => 0 },
a2 => { schema => ['array*' => {of=>'num*'}], pos => 1 },
},
};
sub add_array {
my ($a1, $a2) = @_;
my @sum;
for (0..@$a1-1) {
push @sum, $a1->[$_] + $a2->[$_];
}
return \@sum;
}
Metadata is a hashref, put in the %SPEC
package variable, under the key name that is the same as the function name. It actually doesn't have to be put there, but that is the default location.
Now our function is nice and short again. But at first glance, the metadata does contain quite a few items (some seem weird and needless). Let us go through them key by key (or, as it is called, property):
v => 1.1
This property is needed because there used to be an incompatible, first version of the metadata format (called 1.0). Specifications evolve, fact of life. To keep backward compatibility, if
v
is not specified, it is assumed to be 1.0.result_naked => 1
This property is needed simply because the metadata encourages result_naked => 0 (which is the default and does not have to be specified). We'll get to this a bit later.
args_as => 'array'
This property is needed also because the metadata encourages args_as => 'hash' (the default). We'll get to this a bit later.
args => HASH
This property specifies arguments of the function. It is a hashref, with each key being the name of the argument. Each argument specification is also a hash with the following known keys, among others:
schema
,pos
.schema
describes the schema of the argument value, written in Sah schema language.pos
specifies the position of the argument in the positional argument list, starting from 0.
EXPORTER AND WRAPPER
Adding a metadata to your function doesn't magically change your function, yet. The metadata is just a piece of data that we associate with the function, by itself it does not do anything. To actually do things, we need some tools. The first tool we shall use is the Perinci-specific exporter, to replace Exporter. Please install Perinci::Exporter first from CPAN before continuing (or, just install Task::Perinci to install everything, and ignore subsequent installation instructions until the end of the tutorial).
package MyApp;
use Perinci::Exporter;
our %SPEC;
$SPEC{add_array} = {
...
};
sub add_array {
...
}
1;
Here you can notice one thing: there is no longer any need to declare @ISA (a quirkiness of Exporter) and @EXPORT_OK. All functions which have metadata are assumed to be exportable.
Now let us use our function again:
use MyApp qw(add_array);
add_array([1, 2, 3], 3); # now dies: "a2 must be an array"
add_array(); # now dies: "Missing arguments a1, a2"
How does it work? Perinci::Exporter exports a wrapped version of the function. Function is wrapped using Perinci::Sub::Wrapper. It adds argument validation, among many other things like timeouts, automatic retries, post-processing of function result, exception handling, rate-limiting, etc. All wrapping is done using instructions specified in the metadata.
(Note: argument validation actually does not work yet, I'm still working on it, please check back the status of Perinci::Sub::Wrapper and Data::Sah regularly. Most of the other properties already work though.)
COMMAND-LINE PROGRAMS
Suppose you want to create a CLI program for your module. Some basic functionalities of a CLI program include command-line options processing and help/usage message. The "traditional" way of accomplishing this is with a module like Getopt::Long, yet this is one example of boring, plumbing code. With another tool, we can replace all that tedious work with just a single line of code. Please install Perinci::CmdLine first if you don't have it on your system.
But before we use Perinci::CmdLine, let us add some text to our metadata:
$SPEC{add_array} = {
v => 1.1,
result_naked => 1,
args_as => array,
summary => 'Add two arrays',
description => <<'EOT',
This function adds two array numerically. You *should* supply two arrays of the
same length containing only numbers.
EOT
args => {
a1 => {
schema => ['array*' => {of=>'num*'}],
pos => 0,
req => 1,
summary => 'The first array',
},
a2 => {
schema => ['array*' => {of=>'num*'}],
pos => 1,
req => 1,
summary => 'The second array',
},
},
};
Now create a script myapp
:
#!/usr/bin/perl
use Perinci::CmdLine;
Perinci::CmdLine->new(url => '/MyApp/add_array')->run;
To execute the program:
% myapp --a1 '[1,2,3]' --a2 '[4,5,6]'
% myapp '[1,2,3]' '[4,5,6]'; # ditto
.------.
| 5 |
| 7 |
| 9 |
'------'
To output in other formats:
% myapp '[1,2,3]' '[4,5,6]' --format json
[200","OK",[5,7,9]]
% myapp '[1,2,3]' '[4,5,6]' --format=yaml
- 200
- OK
-
- 5
- 7
- 9
To generate help message:
% myapp --help
myapp - Add two arrays
Usage:
myapp --help (or -h, -?)
myapp --version (or -v)
myapp [common options] [options]
Common options:
--format=FMT Choose output format
Options:
--a1 [array] (or as argument #1)
The first array.
--a2 [array] (or as argument #2)
The second array.
By just saying Perinci::CmdLine->new(url => '/MyApp/add_array')->run;
we have constructed a complete CLI program, which can parse command-line options (taken from function arguments), show help/usage message (using summary and other information from metadata), output result in a variety of formats (YAML, JSON, text, and more), among other things. Other features not demonstrated in this tutorial include subcommands, logging, Bash tab completion.
Several things worth noting:
Riap URLs
Instead of using Perl package and function names directly, Perinci::CmdLine refers to code entities using Riap URLs. This means
MyApp::add_array
becomes/MyApp/add_array
(actuallypl:/MyApp/add_array
). Other schemes are available, including http/https and tcp/unix/pipe. This means, Perinci::CmdLine can provide command-line interface for remote code entities. Many other Perinci tools also operate on URLs and thus share the remote access capability.Parsing argument value
Perinci::CmdLine can accept simple string values or complex structures. Complex structures will be parsed using JSON or YAML (in that order).
Markdown
The value of
description
property is in Markdown format.
POD DOCUMENTATION
Aside from generating help/usage message for a CLI program, the same information in metadata can also be used to generate POD documentation. Combined with tools like Dist::Zilla and Pod::Weaver, this means you do not have to write an API spec document in POD (and manually) ever again.
This section needs a separate tutorial on its own. For now, please take a look at Pod::Weaver::Plugin::Perinci.
HTTP API
Aside from exporting to a CLI program, exporting to an HTTP API is equally easy. Please install Perinci::Access::HTTP::Server if you do not already have it. Then run (make sure first that MyApp.pm is in Perl's @INC search path):
% peri-htserve MyApp
This will start a web server, by default at port 5000. To request to this server:
% curl -g 'http://localhost:5000/MyApp/add_array?a1=[1,2,3]&a2=[4,5,6]'
.------.
| 5 |
| 7 |
| 9 |
'------'
To pass special arguments:
% curl -g 'http://localhost:5000/MyApp/add_array?a1=[1,2,3]&a2=[4,5,6]&-riap-fmt=json'
[200,"OK",[5,7,9]]
Most things are configurable, from URL dispatching routes to parameter parsing. Perinci::Access::HTTP::Server is actually a set of PSGI middleware, which you can compose and customize, and deploy using any PSGI web server.
It is important to note that Riap is not just a protocol for calling function (a.k.a. the call
action), but also to access metadata and do other things. To get metadata for a code entity, use the meta
action:
$ curl http://localhost:5000/MyApp/add_array?-riap-action=meta
---
args:
a1:
pos: 0
req: 1
schema:
- array
- of: num*
req: 1
summary: The first array
a2:
pos: 1
req: 1
schema:
- array
- of: num*
req: 1
summary: The second array
args_as: hash
description: '
This function adds two array numerically. You should supply two arrays of the
same length containing only numbers.
'
result_naked: 0
result_postfilter:
code: str
date: epoch
re: str
summary: Add two arrays
v: 1.1
Let us add a couple more functions in MyApp, for demonstration (don't forget to restart the HTTP server afterwards):
$SPEC{foo} = {};
sub foo { }
$SPEC{bar} = {};
sub bar { }
To list code entities contained in a package entity (in this case, functions inside a package), we can use the list
action:
$ curl 'http://localhost:5000/MyApp/?-riap-action=list&-riap-fmt=json'
[200,"OK",["pl:/MyApp/add_array", "pl:/MyApp/foo", "pl:/MyApp/bar"]]
A host is viewed as a tree of code entities with the root package entity /. By using the Riap actions list
and meta
, we have a self-documenting and discoverable web service. This is applicable to every service implementing the Riap protocol.
ENVELOPED RESULT
If you see some of the above CLI curl outputs, we can see that function result is actually something like:
[200,"OK",[5,7,9]]
instead of just [5,7,9]. This is called an enveloped result, an array containing 3-digit HTTP status code as the first element, a string message as the second element, and the actual result as the third element.
Result envelope is useful for putting error message (and other stuffs) along with result. It is also designed like an HTTP message so it translates straightforwardly to HTTP API's.
You can return enveloped result from your function instead of just (naked) result:
our %SPEC;
$SPEC{add_array} = {
v => 1.1,
args_as => array,
args => {
a1 => { schema => ['array*' => {of=>'num*'}], pos => 0 },
a2 => { schema => ['array*' => {of=>'num*'}], pos => 1 },
},
};
sub add_array {
my ($a1, $a2) = @_;
# here we also want another check
return [400, "Arrays are not of same length"]
unless @$a1 == @$a2;
my @sum;
for (0..@$a1-1) {
push @sum, $a1->[$_] + $a2->[$_];
}
return [200, "OK", \@sum];
}
As you can see, if you do this, you no longer have to state that your results are naked. Let us see how this works in CLI and over HTTP:
% myapp --a1 '[]' --a2 '[1]'
ERROR 400: Arrays are not of same length
% curl -g 'http://localhost:5000/MyApp/add_array?a1=[]&a2=[1]&-riap-fmt=json'
[400,"Arrays are not of same length"]
Wrapper can convert your function from generating naked result to enveloped or vice versa. For example, a user do not like envelopes. She can export your function like this:
use MyApp add_array => {result_naked=>1};
my $res = add_array([1], [3]); # [4], instead of [200, "OK", [4]]
WHAT ELSE IS AVAILABLE?
What is covered in this tutorial is just the tip of the iceberg. Metadata can be as rich as you can. It can also be used to declare dependencies (e.g. checking whether rsync is on your PATH
, or whether some Perl modules are available, before running your function). It has also been used to specify: currying, declaring dropping OS privilege, declaring features like undo/transaction/idempotence, etc.
The metadata also facilitates putting text in different languages, to localize your generated documentation.
There are also other tools to generate functions and metadata for you, so creating a complex function is semi-automated. Take a look, for example at: Perinci::Sub::Gen::AccessTable, Perinci::Sub::Gen::Undoable, and other Perinci::Sub::Gen::* modules.
FAQ
What about OO?
Rinci metadata can also be added to classes, methods, and attributes. However, this is not specified in detail yet. Inputs and comments welcome.
TODO
args_as has not been discussed
SEE ALSO
AUTHOR
Steven Haryanto <stevenharyanto@gmail.com>
COPYRIGHT AND LICENSE
This software is copyright (c) 2013 by Steven Haryanto.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.