RESTful APIs in Perl with Clustericious

(and Mojolicious)

Graham THE Ollis

2 July 2013

Overview

Network Service

220 ProFTPD 1.3.3a Server (Debian) [::ffff:10.10.16.1]
user ollisg
331 Password required for ollisg
pass secret
230 User ollisg logged in
help
214-The following commands are recognized (* =>'s unimplemented):
214-CWD     XCWD    CDUP    XCUP    SMNT*   QUIT    PORT    PASV    
214-EPRT    EPSV    ALLO*   RNFR    RNTO    DELE    MDTM    RMD     
214-XRMD    MKD     XMKD    PWD     XPWD    SIZE    SYST    HELP    
214-NOOP    FEAT    OPTS    AUTH*   CCC*    CONF*   ENC*    MIC*    
214-PBSZ*   PROT*   TYPE    STRU    MODE    RETR    STOR    STOU    
214-APPE    REST    ABOR    USER    PASS    ACCT*   REIN*   LIST    
214-NLST    STAT    SITE    MLSD    MLST    
214 Direct comments to root@web01.sydney.wdlabs.com
protocollayer
ftp, smtp, talk, finger, biff application
TCPtransport
Ethernetlink
IEEE 802.3uphysical

What is wrong with that?

Web Service

POST /InStock HTTP/1.1
Host: www.example.org
Content-Type: application/soap+xml; charset=utf-8
Content-Length: 299
SOAPAction: "http://www.w3.org/2003/05/soap-envelope"

<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Header>
</soap:Header>
<soap:Body>
<m:GetStockPrice xmlns:m="http://www.example.org/stock">
<m:StockName>IBM</m:StockName>
</m:GetStockPrice>
</soap:Body>
</soap:Envelope>
protocollayer
XML-RPC/SOAP application
HTTP
TCPtransport
Ethernetlink
IEEE 802.3uphysical

What is wrong with that?

RESTful Web Service

RESTful Web Service formats

GET /book/978-3-16-148410-0.json
GET /book/978-3-16-148410-0.yml
GET /book/978-3-16-148410-0.xml
GET /book/978-3-16-148410-0.html
GET /book/978-3-16-148410-0
get '/book/:isbn' => sub {
  my($c) = @_;
  my $book = lookup_book_by_isbn($c->param('isbn'));
  $c->stash->{autodata} = $book->as_hash;
}

RESTful Web Service

PUT /book/978-3-16-148410-0
GET /book/978-3-16-148410-0
POST /book/978-3-16-148410-0
DELETE /book/978-3-16-148410-0

Formats: JSON, XML, YAML, JPEG, etc.
(whatever is appropriate)
Content negotiation
protocollayer
REST/HTTPapplication
TCPtransport
Ethernetlink
IEEE 802.3uphysical

Web Services Options in Perl

Mojolicious

Mojolicious::Lite

#!/usr/bin/perl
use Mojolicious:Lite

get '/:foo' => sub {
  my $self = shift;
  my $foo  = $self->param('foo');
  $self->render(text => "Hello from $foo.");
};

app->start;

Mojolicious::Lite getting started

% mojo generate lite_app myapp
  [exist] /tmp
  [write] /tmp/myapp
  [chmod] myapp 744
#!/usr/bin/env perl
use Mojolicious::Lite;

# Documentation browser under "/perldoc"
plugin 'PODRenderer';

get '/' => sub {
  my $self = shift;
  $self->render('index');
};

app->start;
__DATA__

@@ index.html.ep
% layout 'default';
% title 'Welcome';
Welcome to the Mojolicious real-time web framework!

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head><title><%= title %></title></head>
  <body><%= content %></body>
</html>

Websocket/non-blocking example

websocket '/createrepo' => sub {
  my($self) = @_;

  my $data = RestYum::Data->new($self->config);
  my $json = Mojo::JSON->new;

  $self->on(finish => sub {
    INFO "connection closed";
  });

  $self->on(message => sub {
    my $self = shift;
    TRACE $_[0];
    my $payload = eval { $json->decode(shift) };
    if(my $error = $@ || !defined $payload)
    {
      ERROR $error;
      $self->send($json->encode({status => 'not ok'}));
      return;
    }

    my $repo_name = $payload->{repo};

    $data->createrepo($repo_name,
      error => sub { $self->send($json->encode({ status => 'not ok', 
                                                 repo => $repo_name })) },
      ok    => sub { $self->send($json->encode({ status => 'ok',
                                                 repo => $repo_name })) },
    );
    return;
  });
} => 'createrepo';

Mojolicious (full app)

package App::TrafficCone;
use Mojo::Base qw( Mojolicious );

sub startup {
  my($self, $config) = @_;
  $self->routes->any('/*x' => { x => 'y' } => sub {
    my $c = shift;
    $c->render( 
      text   => 'Down for maintenance', 
      status => 503,
    );
  }
}
#!/usr/bin/perl
use Mojolicious::Commands;
Mojolicious::Commands->start_app('App::TrafficCone');

Mojolicious::Lite getting started

% mojo generate app MyApp
  [mkdir] /tmp/my_app/script
  [write] /tmp/my_app/script/my_app
  [chmod] my_app/script/my_app 744
  [mkdir] /tmp/my_app/lib
  [write] /tmp/my_app/lib/MyApp.pm
  [mkdir] /tmp/my_app/lib/MyApp
  [write] /tmp/my_app/lib/MyApp/Example.pm
  [mkdir] /tmp/my_app/t
  [write] /tmp/my_app/t/basic.t
  [mkdir] /tmp/my_app/log
  [mkdir] /tmp/my_app/public
  [write] /tmp/my_app/public/index.html
  [mkdir] /tmp/my_app/templates/layouts
  [write] /tmp/my_app/templates/layouts/default.html.ep
  [mkdir] /tmp/my_app/templates/example
  [write] /tmp/my_app/templates/example/welcome.html.ep

Mojolicious migrating

Clustericious

Clustericious code generation

% clustericious generate app MyApp
  [mkdir] MyApp
  [write] MyApp/MANIFEST
  [exist] MyApp
  [write] MyApp/Build.PL
  [mkdir] MyApp/bin
  [write] MyApp/bin/myapp
  [chmod] MyApp/bin/myapp 744
  [mkdir] MyApp/eg
  [write] MyApp/eg/log4perl.conf
  [exist] MyApp/eg
  [write] MyApp/eg/nginx.conf
  [exist] MyApp/eg
  [write] MyApp/eg/hypnotoad.conf
  [exist] MyApp/eg
  [write] MyApp/eg/MyApp.conf
  [exist] MyApp/eg
  [write] MyApp/eg/MyApp.sample.conf
  [mkdir] MyApp/t
  [write] MyApp/t/001_basic.t
  [exist] MyApp/t
  [write] MyApp/t/999_pod_coverage.t
  [exist] MyApp/t
  [write] MyApp/t/999_pod.t
  [mkdir] MyApp/lib
  [write] MyApp/lib/MyApp.pm
  [mkdir] MyApp/lib/MyApp
  [write] MyApp/lib/MyApp/Routes.pm
% dzil new -P Clustericious MyApp
[DZ] making target dir /tmp/MyApp
[DZ] adding /Changes
[DZ] adding /bin/myapp
[DZ] adding /eg/log4perl.conf
[DZ] adding /eg/nginx.conf
[DZ] adding /eg/hypnotoad.conf
[DZ] adding /eg/MyApp.conf
[DZ] adding /eg/MyApp.sample.conf
[DZ] adding /t/001_basic.t
[DZ] adding /t/999_pod_coverage.t
[DZ] adding /t/999_pod.t
[DZ] adding /lib/MyApp/Routes.pm
[DZ] adding /lib/MyApp.pm
[DZ] writing files to /tmp/MyApp
[DZ] dist minted in ./MyApp

Clustericious deployment

Clustericious::Client

package YourApp::Client;
use Clustericious::Client;

route welcome => 'GET', '/';
route auth => 'GET', '/auth';
#!/usr/bin/perl
use YourApp;
use Clustericious::Client::Command;
Clustericious::Client::Command->run(
  YourApp::Client->new, @ARGV,
);

Clustericious::Config

---
% use File::HomeDir;
% my $data = File::HomeDir->my_home . '/var/plugauth';

% extends_config 'common', port => 3001, app => 'plugauth';

user_file: <%= $data %>/password
group_file: <%= $data %>/group
resource_file: <%= $data %>/resource
host_file: <%= $data %>/host


plugins:
  - PlugAuth::Plugin::WebUI: {}
  - PlugAuth::Plugin::Audit: {}

Clustericious::Config

---
% use File::HomeDir;
% my $ip = "198.61.169.5";

% my $run = File::HomeDir->my_home . "/var/run";
% system 'mkdir', '-p', $run;

% my $home = File::HomeDir->my_home . "/var/$app";
% system 'mkdir', '-p', $home;

simple_auth:
  url: http://<%= $ip %>:3001

url: http://<%= $ip %>:<%= $port %>

start_mode: hypnotoad

hypnotoad:
  heartbeat_timeout: 500
  pid_file: <%= $run %>/<%= $app %>-hypnotoad.pid
  listen:
    - http://<%= $ip %>:<%= $port %>
  env:
    MOJO_HOME: <%= $home %>

Clustericious::Config

[/home/ollisg/etc/PlugAuth.conf :: template]
---
% use File::HomeDir;
% my $data = File::HomeDir->my_home . '/var/plugauth';

% extends_config 'common', port => 3001, app => 'plugauth';

user_file: <%= $data %>/password
group_file: <%= $data %>/group
resource_file: <%= $data %>/resource
host_file: <%= $data %>/host


plugins:
  - PlugAuth::Plugin::WebUI: {}
  - PlugAuth::Plugin::Audit: {}


[/home/ollisg/etc/common.conf :: template]
---
% use File::HomeDir;
% my $ip = "198.61.169.5";

% my $run = File::HomeDir->my_home . "/var/run";
% system 'mkdir', '-p', $run;

% my $home = File::HomeDir->my_home . "/var/$app";
% system 'mkdir', '-p', $home;

simple_auth:
  url: http://<%= $ip %>:3001

url: http://<%= $ip %>:<%= $port %>

start_mode: hypnotoad

hypnotoad:
  heartbeat_timeout: 500
  pid_file: <%= $run %>/<%= $app %>-hypnotoad.pid
  listen:
    - http://<%= $ip %>:<%= $port %>
  env:
    MOJO_HOME: <%= $home %>


[/home/ollisg/etc/common.conf :: interpreted]
---



simple_auth:
  url: http://198.61.169.5:3001

url: http://198.61.169.5:3001

start_mode: hypnotoad

hypnotoad:
  heartbeat_timeout: 500
  pid_file: /home/ollisg/var/run/plugauth-hypnotoad.pid
  listen:
    - http://198.61.169.5:3001
  env:
    MOJO_HOME: /home/ollisg/var/plugauth


[/home/ollisg/etc/PlugAuth.conf :: interpreted]
---


user_file: /home/ollisg/var/plugauth/password
group_file: /home/ollisg/var/plugauth/group
resource_file: /home/ollisg/var/plugauth/resource
host_file: /home/ollisg/var/plugauth/host


plugins:
  - PlugAuth::Plugin::WebUI: {}
  - PlugAuth::Plugin::Audit: {}


[merged]
---
group_file: /home/ollisg/var/plugauth/group
host_file: /home/ollisg/var/plugauth/host
hypnotoad:
  env:
    MOJO_HOME: /home/ollisg/var/plugauth
  heartbeat_timeout: 500
  listen:
  - http://198.61.169.5:3001
  pid_file: /home/ollisg/var/run/plugauth-hypnotoad.pid
plugins:
- PlugAuth::Plugin::WebUI: {}
- PlugAuth::Plugin::Audit: {}
resource_file: /home/ollisg/var/plugauth/resource
simple_auth:
  url: http://198.61.169.5:3001
start_mode: hypnotoad
url: http://198.61.169.5:3001
user_file: /home/ollisg/var/plugauth/password

Clustericious testing

PlugAuth

Yars

See also