Introduction to Mojolicious

unicorn

About this talk

Getting Help

fail raptor

On with the talk...

oh hai

What is Mojolicious?

no raptor
  • An amazing real-time web framework
  • A powerful web development toolkit
  • Designed from the ground up
  • ... based on years of experience developing Catalyst
  • Portable
  • No non-core dependencies
  • Batteries included
  • Real-time and non-blocking
  • "... perfect for building highly scalable web services"
  • 8706 lines of code in lib
  • 11415 tests (92.8% coverage)
  • Easy to install (secure, only takes one minute!)
curl -L https://cpanmin.us \
| perl - -M https://cpan.metacpan.org -n Mojolicious

Mojolicious::Lite

"Hello World"


use Mojolicious::Lite;

any '/' => sub { shift->render( text => 'Hello World' ) };

app->start;
    
  • imports strict, warnings, utf8 and v5.10
  • handles the '/' route
    • renders the text (as text)
  • starts the app

Start the server

  • basic server: $ ./script daemon
  • development server, smooth auto-restarting on file change: $ morbo script
  • high performance preforking server, zero downtime redeployment: $ hypnotoad script
  • plack/psgi (no real-time features): $ plackup script or $ starman script
  • CGI (but why?)

Mojolicious::Lite

"Hello User"

  • handles all toplevel routes, including /
    • /Joel becomes 'Hello Joel'
    • / is special cased to /World
  • stashes route matches
  • stash some other useful values
  • renders from a template with layout
    • stash values are localized to Perl scalars

use Mojolicious::Lite;

any '/:name' => { name => 'World' } => sub { 
  my $self = shift;
  $self->stash( time => scalar localtime );
  $self->render( 'hello' );
};

app->start;

__DATA__

@@ layouts/basic.html.ep

<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
  </head>
  <body>
    %= content
  </body>
<html>

@@ hello.html.ep

% layout 'basic';
% title "Hello $name";

<p>Hello <%= $name %></p>

%= tag p => begin
  The time is now <%= $time %>
% end
    

ex/hello.pl

Testing "Hello User"


use Mojo::Base -strict;

use Test::More;
use Test::Mojo;

use FindBin '$Bin';
require "$Bin/hello.pl";

my $t = Test::Mojo->new;

$t->get_ok('/')
  ->status_is(200)
  ->content_like(qr/Hello World/)
  ->content_like(qr/\d{2}:\d{2}/);

$t->get_ok('/Joel')
  ->status_is(200)
  ->content_like(qr/Hello Joel/)
  ->content_like(qr/\d{2}:\d{2}/);

done_testing;

    

ex/hello1.t

  • Load the app into Test::Mojo
    • "Lite" apps must require the app
    • "Full" apps pass class name to Test::Mojo->new
  • Request content
  • Test status
  • Test response and content

... but do I really just have to regex the result???

orly

Aside: Mojo::DOM

The power of CSS3 selectors

  • HTML/XML parser
  • CSS3 selectors (all of them)
  • List of supported selectors in Mojo::DOM::CSS
  • First: $dom->at($selector) returns a Mojo::DOM
  • Multiple: $dom->find($selector) returns a Mojo::Collection

use Mojo::Base -strict;
use Mojo::DOM;

my $html = <<'END';
  <div class="something">
    <h2>Heading</h2>
    Bare text
    <p>Important</p>
  </div>
  <div class="else">Ignore</div>
END

my $dom = Mojo::DOM->new($html);
say $dom->at('.something p')->text;

    

ex/dom_example.pl

Testing "Hello User"

With selectors


use Mojo::Base -strict;

use Test::More;
use Test::Mojo;

use FindBin '$Bin';
require "$Bin/hello.pl";

my $t = Test::Mojo->new;

$t->get_ok('/')
  ->status_is(200)
  ->text_is( p => 'Hello World' )
  ->text_like( 'p:nth-of-type(2)' => qr/\d{2}:\d{2}/ );

$t->get_ok('/Joel')
  ->status_is(200)
  ->text_is( p => 'Hello Joel' )
  ->text_like( 'p:nth-of-type(2)' => qr/\d{2}:\d{2}/ );

done_testing;

    

ex/hello2.t

Helpers

Sessions

Session info is signed and stored in a cookie

i eated cookie lol

Login App Example


use Mojolicious::Lite;

helper validate => sub {
  my ($self, $user, $pass) = @_;
  state $users = {
    joel => 'mypass',
  };
  return unless $users->{$user} eq $pass;
  $self->session( user => $user );
};

any '/' => sub {
  my $self = shift;
  my ($user, $pass) = map {$self->param($_)} qw/user pass/;
  $self->validate($user, $pass) if $user;
  $self->render('index');
};

any '/logout' => sub {
  my $self = shift;
  $self->session( expires => 1 );
  $self->redirect_to('/');
};

app->start;

__DATA__

@@ index.html.ep

% my $user = session 'user';
% title $user ? "Welcome back \u$user" : "Not logged in";
% layout 'basic';

<h1><%= title %></h1>

% unless ($user) {
  %= form_for '/' => method => 'POST' => begin
    Username: <%= input_tag 'user' %> <br>
    Password <%= password_field 'pass' %> <br>
    %= submit_button
  % end
% }

    

ex/login.pl

Aside: Mojo::UserAgent

  • Full featured user agent
  • Built-in cookie jar
  • Handles redirects
  • SSL and proxy support
  • dom and json response methods
  • Pluggable content generators (form/json)
  • Non-blocking with callback

use Mojo::Base -strict;
use Mojo::UserAgent;

my $ua = Mojo::UserAgent->new(max_redirects => 10);

say $ua->get('api.metacpan.org/v0/release/Mojolicious')
       ->res->json->{version};

use Mojo::URL;
my $url = Mojo::URL->new('http://openlibrary.org/subjects/')
                   ->path('perl.json')
                   ->query( details => 'true' );
say $url;
say $ua->get($url)->res->json('/works/0/title');

    

ex/ua_example.pl

Testing Login Example

  • Test that the form is only shown when not authenticated
  • UserAgent generates form content from hash
  • UserAgent follows logout redirect

use Mojo::Base -strict;

use Test::More;
use Test::Mojo;

use FindBin '$Bin';
require "$Bin/login.pl";

my $t = Test::Mojo->new;
$t->ua->max_redirects( 2 );

$t->get_ok('/')
  ->status_is(200)
  ->text_is( h1 => 'Not logged in' )
  ->element_exists( 'form' );

my $login = { user => 'joel', pass => 'mypass' };
$t->post_ok( '/' => form => $login )
  ->status_is(200)
  ->text_like( h1 => qr/joel/i )
  ->element_exists_not( 'form' );

$t->get_ok('/logout')
  ->status_is(200)
  ->element_exists('form');

done_testing;

    

ex/login.t

Content Negotiation

Erm

Content Negotiation

  • RESTful apps often support many formats
  • the helper 'respond_to'
    • detects requested format
    • renders for that format
  • reply->not_found renders a 404 page
  • reply->exception renders a 500 page

use Mojolicious::Lite;

my %data = (
  1 => { foo => 'bar' },
  2 => { baz => 'bat' },
);

any '/:id' => sub {
  my $self = shift;
  return $self->reply->not_found
    unless my $item = $data{$self->stash('id')};

  $self->respond_to(
    txt => { text => join(', ', %$item) },
    json => { json => $item },
    any  => {
      format => 'html',
      template => 'page',
      foo => $item,
    },
  );
};

app->start;

__DATA__

@@ page.html.ep

% my $key = (keys %$foo)[0];
% title "You wanted $key";
% layout 'basic';

<p>
  All your <%= $key %> 
  are belong to <%= $foo->{$key} %>
</p>
    

ex/content.pl

Testing Content Negotiation

  • Test for formatted response by
    • default
    • query parameter
    • extension
    • Accept header
  • Test the 'not found' 404

use Mojo::Base -strict;

use Test::More;
use Test::Mojo;

use FindBin '$Bin';
require "$Bin/content.pl";

my $t = Test::Mojo->new;

$t->get_ok('/0')
  ->status_is(404);

$t->get_ok('/1')
  ->status_is(200)
  ->text_like( p => qr/foo.*?bar/s );

$t->get_ok('/2?format=txt')
  ->status_is(200)
  ->content_is('baz, bat');

$t->get_ok('/1.json')
  ->status_is(200)
  ->json_is({foo => 'bar'});

$t->get_ok('/2', { Accept => 'application/json' })
  ->status_is(200)
  ->json_is('/baz' => 'bat');

done_testing;

    

ex/content.t

Still not impressed?

Not impressed
sync

Let's write a POD website that

  • Requests module documentation from metacpan
  • Waits to render the response
  • Doesn't block while waiting

Non-blocking

UserAgent + Server


use Mojolicious::Lite;
use Mojo::UserAgent;
use Mojo::URL;

my $ua = Mojo::UserAgent->new;
my $url = Mojo::URL->new('http://api.metacpan.org/pod/')
                   ->query( 'content-type' => 'text/html' );

any '/:module' => { module => 'Mojolicious' } => sub {
  my $c = shift;
  $c->render_later;

  my $module = $c->stash('module');
  my $target = $url->clone;
  $target->path($module);

  $ua->get( $target => sub {
    my ($ua, $tx) = @_;
    $c->render( 'docs', pod => $tx->res->body );
  });
};

app->start;

__DATA__

@@ docs.html.ep

% layout 'basic';
% title $module;

<h1><%= $module %></h1>

%== $pod
    

ex/nb_doc_server.pl

WebSockets

  • Client opens websocket
  • Server responds with data every second
  • Client receives data and updates plot
  • Real app would get some more interesting data

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


any '/' => 'index';

websocket '/data' => sub {
  my $self = shift;
  my $timer = Mojo::IOLoop->recurring( 1 => sub {
    state $i = 0;
    $self->send({ json => gen_data($i++) }); 
  });

  $self->on( finish => sub {
    Mojo::IOLoop->remove($timer);
  });
};

sub gen_data { 
  my $x = shift; 
  return [ $x, sin( $x + 2*rand() - 2*rand() ) ]
}

app->start;

__DATA__

@@ index.html.ep

% layout 'basic';

%= javascript '/jquery-1.9.1.min.js'
%= javascript '/jquery.flot.js'

<div id="plot" style="width:600px;height:300px">
</div>

%= javascript begin
  var data = [];
  var plot = $.plot($('#plot'), [ data ]);

  var url = '<%= url_for('data')->to_abs %>';
  var ws = new WebSocket( url );
  ws.onmessage = function(e){
    var point = JSON.parse(e.data);
    data.push(point);
    plot.setData([data]);
    plot.setupGrid();
    plot.draw();
  };
% end
    

ex/websocket.pl

Testing WebSockets

  • Testing is just as easy!
  • Send a message
  • Wait for a response
  • Test response using JSON pointer
  • Repeat or finish
  • Many other websocket test methods

use Mojo::Base -strict;

use Test::More;
use Test::Mojo;

use FindBin '$Bin';
require "$Bin/websocket.pl";

my $t = Test::Mojo->new;

$t->get_ok('/')
  ->status_is(200)
  ->element_exists( '#plot' );

$t->websocket_ok('/data')
  ->message_ok
  ->json_message_is( '/0' => 0 )
  ->json_message_has( '/1' )
  ->message_ok
  ->json_message_is( '/0' => 1 )
  ->finish_ok;

done_testing;

    

ex/websocket.t

Validation

  • Mojolicious::Validator
  • Validating User Input

use Mojolicious::Lite;


any '/' => sub {
  my $self = shift;
  my $val=$self->validation;
  return $self->render unless $val->has_data;

  $val->required('user')->size(1, 20)->like(qr/^[a-z0-9]+$/);
  $val->required('pass_again')->equal_to('pass')
    if $val->optional('pass')->size(7, 500)->is_valid;


  $self->session( user => $self->param('user') ) unless $val->has_error;
} => 'index';

any '/logout' => sub {
  my $self = shift;
  $self->session( expires => 1 );
  $self->redirect_to('/');
};

app->start;

__DATA__

@@ index.html.ep

% my $user = session 'user';
% title $user ? "Registered as \u$user" : "Please register";
% layout 'basic';
<style>
  label.field-with-error { color: #dd7e5e }
  input.field-with-error { background-color: #fd9e7e }
</style>

<h1><%= title %></h1>

% unless ($user) {
  %= form_for '/' => method => 'POST' => begin
    %= label_for user => 'Username (required, 1-20 characters, a-z/0-9)'
    <br>
    %= text_field 'user', id => 'user'
    <br>
    %= label_for pass => 'Password (optional, 7-500 characters)'
    <br>
    %= password_field 'pass', id => 'pass'
    <br>
    %= label_for pass_again => 'Password again (equal to the value above)'
    <br>
    %= password_field 'pass_again', id => 'pass_again'
    %= submit_button
  % end
% }

    

ex/validation.pl

Testing Validation


use Mojo::Base -strict;

use Test::More;
use Test::Mojo;

use FindBin '$Bin';
require "$Bin/validation.pl";

my $t = Test::Mojo->new;
$t->ua->max_redirects( 2 );

$t->get_ok('/')
  ->status_is(200)
  ->text_is( h1 => 'Please register' )
  ->element_exists( 'form' );

my $registration= { user => 'joel', pass => 'mypass' };
$t->post_ok( '/' => form => $registration)
  ->status_is(200)
  ->element_exists( 'label.field-with-error' );

$registration= { user => 'marcus', pass => 'ohmypass', pass_again => 'ohmypass' };
$t->post_ok( '/' => form => $registration)
  ->status_is(200)
  ->text_is( h1 => 'Registered as Marcus' );

$t->get_ok('/logout')
  ->status_is(200)
  ->element_exists('form');

done_testing;
    

ex/validation.t

Mojolicious Apps are More Than Just Web Apps

command

Mojolicious::Commands

  • Fetch, parse and extract resources from the internet
    mojo get -r reddit.com/r/perl 'p.title > a.title' text
    mojo get fastapi.metacpan.org/v1/module/Mojo::Base /sloc
  • ... or from your own app!
    ./ex/hello.pl get / p 1 text
  • Examine the routes that your app defines
    ./ex/websocket.pl routes
  • Run some ad-hoc code against your app!
    ./ex/websocket.pl eval -v 'app->home'
    ./ex/websocket.pl eval -V 'app->secrets'
  • Generate a new app or plugin
    mojo generate lite_app
    mojo generate app
    mojo generate plugin
  • Add your own commands to Mojolicious:
    mojo nopaste gist myfile.pl
  • ... or to your own app:
    galileo setup
  • ... or import commands from CPAN:
    ./myapp.pl minion worker

Aside: Mojo::File (just added)


use Mojo::File qw(path tempfile);
my $passwords = path('/etc/passwd')->slurp;
my $tmpfile = tempfile
  ->spurt('This is her file')->move_to('/home/her/file');
    

$ perl -Mojo -E 'say r j f("geo.json")
  ->spurt(g("freegeoip.net/json/mojolicious.org")
  ->body)->slurp'

If you liked that, see also

  • Mojo::Pg - The Mojolicious Postgres driver wrapper
  • Mojo::Redis2 - Redis driver, great for messaging
  • Minion - The Mojolicious job queue and plugin
  • Galileo - My CPAN friendly Mojolicious-based CMS
  • Convos - The in-browser Mojolicious-based IRC client

Thanks for listening!

Cannot be unseen

Now go have fun with Mojolicious!

http://mojolicio.us

curl -L https://cpanmin.us |\ 
perl - -M https://cpan.metacpan.org -n Mojolicious
awesome