Writing simple webapps with Dancer

Writing simple webapps with Dancer

This talk provides a walkthrough of developing a simple web application with the Perl webapp framework Dancer. I gave this talk at the Dalhousie Faculty of Computer Science FreeSchool in March 2013

07feb1c4a2aaf22752c9924b95db944f?s=128

Mike Doherty

March 07, 2013
Tweet

Transcript

  1. Writing simple Perl webapps using Dancer Mike Doherty @mikedoherty_ca

  2. Our simple webapp  A pastebin – Accept a POST

    request containing some text to remember – Store it, and give the ID back – Respond to a GET for the ID with the paste content  If you want to follow along: – Start installing dependencies: • cpanm --installdeps WWW::Hashbang::Pastebin – Grab the starter kit: http://mikedoherty.ca/t/dalfcs-dancer.tar.gz
  3. Perl?  Perl is: – Old – Stable – Optimized

    – Designed for text manipulation – Not dead http://mikedoherty.ca/t/dalfcs-dancer.tar.gz
  4. Perl renaissance  New release cycle  Way better regexes

     autodie  Moose  cpanm, local::lib  perlbrew  Dist::Zilla  rxrx  Perl::Critic & Perl::Tidy  DBIx::Class  Plack  Mojo/Dancer/Catalyst I'd love to tell you more about these in another session. http://mikedoherty.ca/t/dalfcs-dancer.tar.gz
  5. Dancer  http://perldancer.org  Like Sinatra, but for Perl 

    Use Plack for deployment flexibility  Does everyone have the starter kit? http://mikedoherty.ca/t/dalfcs-dancer.tar.gz
  6. tar  Open it with tar xzvf perldancer.tar.gz  It

    is a git repository, so feel free to track your progress
  7. Front page package Pastebin; use 5.14.0; use strict; use warnings;

    use Dancer ':syntax'; get '/' => sub { return template 'index', { version => $^V }; };
  8. Templates <pre> WWW::Hashbang::Pastebin(3) NAME WWW::Hashbang::Pastebin - command line pastebin SYNOPSIS

    $ (hostname ; uptime) | curl -F 'p=&lt;-' <% request.uri_base %> <% request.uri_base %>/f4s2 $ chromium-browser <% request.uri_base %>/f4s2+#l2 DESCRIPTION This pastebin has no user interface - use "curl" to POST paste content. Your paste's ID is returned in the "X-Pastebin-ID" header; the URL in the "X-Pastebin-URL", as well as the response content. <a href="http://www.perl.org/">perl <% version %></a> WWW::Hashbang::Pastebin(3) </pre>
  9. Fire it up $ perl ./bin/app.pl [13008] core @0.000011> loading

    Dancer::Handler::Standalone handler in /home/mike/perl5/perlbrew/perls/demo-perl/lib/site_perl/5.16.2/ Dancer/Handler.pm l. 45 [13008] core @0.000219> loading handler 'Dancer::Handler::Standalone' in /home/mike/perl5/perlbrew/perls/demo-perl/lib/site_perl/5.16.2/ Dancer.pm l. 475 >> Dancer 1.3111 server 13008 listening on http://0.0.0.0:3000 >> Dancer::Plugin::DBIC (0.1802) == Entering the development dance floor ...
  10. DBIx::Class  Describes your DB schema  Abstracts away differences

    between RDBMSs  Object-Relational Mapping (ORM) – Allows you to access DB rows as objects – Inflates relationships (foreign keys) into objects  Write your own schema, or generate one from your DB
  11. Schema package WWW::Hashbang::Pastebin::Schema::Result::Paste; use strict; use warnings; use base qw/DBIx::Class::Core/;

    __PACKAGE__->table('paste'); __PACKAGE__->add_columns( ... );
  12. Schema __PACKAGE__->add_columns( 'paste_id' => { data_type => 'bigint', is_auto_increment =>

    1, accessor => 'id', }, 'paste_content' => { data_type => 'text', accessor => 'content', size => 'mediumtext', }, );
  13. Accept a POST use Dancer::Plugin::DBIC qw(schema); post '/' => sub

    { my $paste_content = param('p'); unless ($paste_content) { status 'bad_request'; return 'No paste content received'; } my $row = schema ->resultset('Paste') ->create({ paste_content => $paste_content, }); my $ext_id = ...; # get an ID };
  14. Generate an ID  Could be the autoincrement integer 

    But that's not compact enough – We have a whole alphabet of acceptable URL characters  Integer::Tiny represents integers compactly my $mapper = do { my $key = join ( 'a'..'z', 0..9 ); Integer::Tiny ->new($key); }; ... my $ext_id = $mapper ->encrypt($row->id );
  15. Webapp my $ext_id = $mapper->encrypt($row->id); my $ext_url = uri_for("/$ext_id"); headers

    'X-Pastebin-ID' => $ext_id, 'X-Pastebin-URL' => $ext_url; return "$ext_url\n";
  16. Test use Test::More tests => 2; use WWW::Hashbang::Pastebin; use Dancer::Plugin::DBIC;

    use Dancer::Test; my $rand = rand(); # paste content route_exists [POST => '/'], 'a route handler is defined for POST /'; my $response = dancer_response( 'POST', '/', { params => {p => $rand} } ); like $response->content => qr{^http://.+/.+} or diag explain $response;
  17. Run the tests $ prove t/create_paste.t t/create_paste.t .. ok All

    tests successful. Files=1, Tests=5, 1 wallclock secs ( 0.02 usr 0.02 sys + 0.64 cusr 0.17 csys = 0.85 CPU) Result: PASS
  18. Test-driven development  Write failing tests that should pass first

     Then make them pass 
  19. Let's test the retrieval of a paste 1. So, we

    need to send a POST with content • Safe to do this; we already tested that this works 2.Then, request the URL it gave us 3.Verify that the content matches what we sent in #1
  20. Retrieving content my $rand = rand(); # paste content my

    $response = dancer_response( 'POST', '/', { params => {p => $rand} } ); my $id = $response->header('X-Pastebin-ID'); route_exists [GET => "/$paste_id"], "route /$paste_id exists"; response_status_is [GET => "/$paste_id"], 200; response_content_like [GET => "/$paste_id"], qr{\Q$rand\E}, "$rand appears in the content";
  21. ACHIEVEMENT UNLOCKED: FAILURE $ prove t/create_paste.t t/create_paste.t .. 2/5 #

    Failed test 'route /b exists' # at t/create_paste.t line 16. # Failed test '200 for /b' # at t/create_paste.t line 17. # got: '404' # expected: '200' # Failed test '0\.814022216803046 appears in the content' # at t/create_paste.t line 20. # 'GLOB(0x39d78d8)' # doesn't match '(?^:0\.814022216803046)' # Looks like you failed 3 tests of 5. t/create_paste.t .. Dubious, test returned 3 (wstat 768, 0x300) Failed 3/5 subtests Test Summary Report ------------------- t/create_paste.t (Wstat: 768 Tests: 5 Failed: 3) Failed tests: 3-5 Non-zero exit status: 3 Files=1, Tests=5, 1 wallclock secs ( 0.03 usr 0.00 sys + 0.57 cusr 0.06 csys = 0.66 CPU) Result: FAIL
  22. Retrieval use Try::Tiny; get '/:id' => sub { my $ext_id

    = param('id'); my $int_id = try { $mapper->decrypt( $ext_id ) } catch { status 'bad_request'; return "'$ext_id' is an ivalid paste ID"; }; my $paste = schema ->resultset('Paste') ->find( $int_id );
  23. Retrieval my $paste = schema->resultset('Paste') ->find( $int_id ); unless ($paste)

    { content_type('text/plain'); status 'not_found'; return "No such paste as '$ext_id'"; } headers 'X-Pastebin-ID' => $ext_id; content_type('text/plain'); return $paste->content;
  24. Command-line usage mike@charron:~ $ (hostname;uptime)|curl -F 'p=<-' http://p.hashbang.ca http://p.hashbang.ca/7Q mike@charron:~

    $ curl http://p.hashbang.ca/7Q charron 21:25:45 up 2 days, 27 min, 3 users, load average: 0.35, 0.49, 0.44
  25. SUCCESS  The completed code is available at https://github.com/doherty/WWW-Hashbang-Pastebin 

    The slides are available at http://mikedoherty.ca/t/dalfcs-perldancer  Thanks for joining me  I'm happy to stick around for questions  More Perl sessions?