Building a web app with Perl 6 and Cro

 

 

Lance Wicks

@lancew

 

https://slides.com/lancewicks/yapceu2018

Introduction

 

Building a web app

with Perl 6 and Cro

  • Lance Wicks
    • lw@judocoach.com
    • www.lancewicks.com
       
  • Perl Developer @ www.cv-library.co.uk
    • Modern Perl website
    • Started in 2000
    • Helps jobseekers find jobs
       
  • YAPC::EU 2017
    • Perl6 motivation
  • LPW 2017
    • Workshop on Perl6 + Bailador
  • YAPC::EU 2018
    • Perl6 + Bailador -> Perl6 + Cro

Bailador

  • "...A light-weight route-based web application framework for Perl 6..."
     
  • Similar to Dancer in Perl5
  • In 2017, was the first framework I found/liked
  • Familiar as a Dancer2 user

cro.services

  • "...Cro is a set of libraries for building reactive distributed systems, lovingly crafted to take advantage of all Perl 6 has to offer..."
     
  • Cro 
    • Active development
      • Jonathan Worthington & Edument
    • Modern
    • Perl6-centric

Bailador -> cro

  • Why port from Baildor -> Cro?
    • Baildor felt perl5-ish
    • Cro felt perl6-ish
    • Cro felt active
    • Tooling (Docker, Cro Run, Cro web)
    • Keep learning
    • Hype
    • Why the heck not?!

cro basics

use Cro::HTTP::Test;
use Routes;

test-service routes(), {
    test get('/'),
        status => 200,
        content-type => 'text/html',
        body => /example1/;
}

done-testing;
use Cro::HTTP::Router;
  
sub routes() is export {
    route {
        get -> {
            content 'text/html', "<h1> example1 </h1>";
        }
    }
}

example 1

 

Simple GET route

example 1

 

Simple GET route

prove6 -I=lib -v t/example1.t

    ok 1 - Status is acceptable
    ok 2 - Content type is acceptable
    ok 3 - Body is acceptable
    1..3
ok 1 - GET /
1..1
t/example1.t .. ok
All tests successful.
Files=1, Tests=1,  0 wallclock secs
Result: PASS
use Cro::HTTP::Test;
use Routes;

test-service routes(), {
    test get('/hello/world'),
        status => 200,
        content-type => 'text/html',
        body => /Hello world/;
}

done-testing;
use Cro::HTTP::Router;
  
sub routes() is export {
    route {
        get -> 'hello', $name {
            content 'text/html', "<h1> Hello $name </h1>";
        }
    }
}

example 2

 

GET route with params

prove6 -I=lib -v t/example2.t

    ok 1 - Status is acceptable
    ok 2 - Content type is acceptable
    ok 3 - Body is acceptable
    1..3
ok 1 - GET /hello/world
1..1
t/example2.t .. ok
All tests successful.
Files=1, Tests=1,  0 wallclock secs
Result: PASS

example 2

 

GET route with params

use Cro::HTTP::Test;
use Routes;

test-service routes(), {
    test-given '/add', {
        test post(json => { :x(37), :y(5) }),
            status => 200,
            json => { :result(42) };
    }
}


done-testing;
use Cro::HTTP::Router;
  
sub routes() is export {
    route {
        post -> 'add' {
            request-body 'application/json' => -> (:$x!, :$y!) {
                content 'application/json', { :result($x + $y) };
            }
        }
    }
}

example 3

 

POST route

$ prove6 -v -I=lib t/example3.t
    ok 1 - Status is acceptable
    ok 2 - Content type is recognized as a JSON one
    ok 3 - Body is acceptable
    1..3
ok 1 - POST /add
1..1
t/example3.t .. ok
All tests successful.
Files=1, Tests=1,  0 wallclock secs
Result: PASS
$ curl --header "Content-Type: application/json" \
--request POST \
--data '{"x":"2","y":"5"}' \
http://localhost:20000/add

{"result": 7}

example 3

 

POST route

use Cro::HTTP::Log::File;
use Cro::HTTP::Server;
use Routes;

my Cro::Service $http = Cro::HTTP::Server.new(
    http => <1.1>,
    host => %*ENV<EXAMPLE2_HOST>,
    port => %*ENV<EXAMPLE2_PORT>,
    application => routes(),
    after => [
        Cro::HTTP::Log::File.new(logs => $*OUT, errors => $*ERR)
    ]
);

$http.start;

service.p6

 

myjudo.net

  • Web application to tracking Judo training sessions.
  • Created to learn Perl6 + Bailador for LPW2017
  • Ported from Baildor -> Cro

  • "I speak Perl6 with a heavy Perl5 accent"

  • https://github.com/lancew/MyJudo

Bailador

vs.

Cro

 

myjudo.net

 

# Cro
sub routes() is export {
    route {
        get -> LoggedIn $user, 'user', $user_name {
            my %user_data = $mj.get_user_data( user_name => $user.username );

             my $t = Template::Mojo.from-file('views/user/home.tm');

            content 'text/html', $t.render( {
                user_data => $(%user_data),
                } );
        };
}
# Bailador
prefix '/user' => sub {
    get "/:user" => sub ($user){
        my $session = session();
        redirect '/' unless $session<user>:exists && $session<user> eq $user;

        my %user_data = $mj.get_user_data( user_name => $session<user> );

        redirect '/' unless %user_data;

        template 'user/home.tt', {
            user_data => $(%user_data),
            };
    }
}

Tooling

 

Docker

FROM croservices/cro-http:0.7.5
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN zef install --deps-only . && perl6 -c -Ilib service.p6
ENV EXAMPLE1_HOST="0.0.0.0" EXAMPLE1_PORT="10000"
EXPOSE 10000
CMD perl6 -Ilib service.p6

Tooling

myjudo.net

 

Docker

FROM alpine:3.8

RUN apk add --no-cache curl gcc git libressl-dev linux-headers make musl-dev perl sqlite-libs

# Install Perl 6
RUN curl -L https://github.com/rakudo/rakudo/archive/2018.06.tar.gz | tar xzf -                   \
 && cd rakudo-2018.06                                                                             \
 && CFLAGS=-flto ./Configure.pl --backend=moar --gen-moar --moar-option=--ar=gcc-ar --prefix=/usr \
 && make -j`nproc` install

# Install zef
RUN git clone https://github.com/ugexe/zef.git \
 && cd zef                                     \
 && perl6 -Ilib bin/zef install --/test .

WORKDIR /app

COPY META6.json .

RUN /usr/share/perl6/site/bin/zef install --deps-only --/test .

# Avoid having "binaries" in the final image
RUN rm -r /usr/share/perl6/site/bin

FROM scratch

COPY --from=0 /bin/sh                  /bin/
COPY --from=0 /lib/ld-musl-x86_64.so.1 \
              /lib/libz.*              /lib/
COPY --from=0 /usr/bin/moar            \
              /usr/bin/perl6           /usr/bin/
COPY --from=0 /usr/lib/libmoar.so      \
              /usr/lib/libcrypto.*     \
              /usr/lib/libsqlite3.*    \
              /usr/lib/libssl.*        /usr/lib/
COPY --from=0 /usr/share/nqp           /usr/share/nqp
COPY --from=0 /usr/share/perl6         /usr/share/perl6

WORKDIR /app

COPY . /app

CMD ["perl6", "-Ilib", "service.p6"]

Tooling

 

cro stub

$ cro stub http example2 example2
Stubbing a HTTP Service 'example2' in 'example2'...

First, please provide a little more information.

Secure (HTTPS) (yes/no) [no]: 
Support HTTP/1.1 (yes/no) [yes]: 
Support HTTP/2.0 (yes/no) [no]: 
Support Web Sockets (yes/no) [no]: 


---

$ cro stub web example2 example2
Couldn't find template 'web'. Available templates:
zeromq-worksink, http, zeromq-worker, react-redux-spa

Tooling

 

cro run

$ cro run
▶ Starting example2 (example2)
🔌 Endpoint HTTP will be at http://localhost:20000/
♻ Restarting example2 (example2)
📓 example2 Listening at http://localhost:20000
⚠ example2 [ERROR] 404 /favicon.ico - ::1
📓 example2 [OK] 200 / - ::1
📓 example2 Shutting down...
♻ Restarting example2 (example2)
📓 example2 Listening at http://localhost:20000

Tooling

 

cro trace

$ cro trace
▶ Starting example2 (example2)
🔌 Endpoint HTTP will be at http://localhost:20000/
📓 example2 Listening at http://localhost:20000
👓 example2 ⚡ EMIT [HTTP(20000)] Cro::TCP::Listener
             Cro::TCP::ServerConnection.new(replier => Cro::TCP::Replier.new)
👓 example2 ⚡ EMIT [HTTP(20000)] Cro::TCP::Listener
             Cro::TCP::ServerConnection.new(replier => Cro::TCP::Replier.new)
👓 example2 ⚡ EMIT [HTTP(20000)] Cro::TCP::ServerConnection
             TCP Message
               47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a  GET / HTTP/1.1..
               48 6f 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 74 3a  Host: localhost:
               32 30 30 30 30 0d 0a 43 6f 6e 6e 65 63 74 69 6f  20000..Connectio
               6e 3a 20 6b 65 65 70 2d 61 6c 69 76 65 0d 0a 43  n: keep-alive..C
               61 63 68 65 2d 43 6f 6e 74 72 6f 6c 3a 20 6d 61  ache-Control: ma
               78 2d 61 67 65 3d 30 0d 0a 55 70 67 72 61 64 65  x-age=0..Upgrade
               2d 49 6e 73 65 63 75 72 65 2d 52 65 71 75 65 73  -Insecure-Reques
               74 73 3a 20 31 0d 0a 55 73 65 72 2d 41 67 65 6e  ts: 1..User-Agen
               74 3a 20 4d 6f 7a 69 6c 6c 61 2f 35 2e 30 20 28  t: Mozilla/5.0 (
               58 31 31 3b 20 4c 69 6e 75 78 20 78 38 36 5f 36  X11; Linux x86_6
               34 29 20 41 70 70 6c 65 57 65 62 4b 69 74 2f 35  4) AppleWebKit/5
               33 37 2e 33 36 20 28 4b 48 54 4d 4c 2c 20 6c 69  37.36 (KHTML, li
               6b 65 20 47 65 63 6b 6f 29 20 43 68 72 6f 6d 65  ke Gecko) Chrome
               2f 37 30 2e 30 2e 33 35 31 34 2e 30 20 53 61 66  /70.0.3514.0 Saf
               61 72 69 2f 35 33 37 2e 33 36 0d 0a 44 4e 54 3a  ari/537.36..DNT:
               20 31 0d 0a 41 63 63 65 70 74 3a 20 74 65 78 74   1..Accept: text
               2f 68 74 6d 6c 2c 61 70 70 6c 69 63 61 74 69 6f  /html,applicatio
               6e 2f 78 68 74 6d 6c 2b 78 6d 6c 2c 61 70 70 6c  n/xhtml+xml,appl
               69 63 61 74 69 6f 6e 2f 78 6d 6c 3b 71 3d 30 2e  ication/xml;q=0.
               39 2c 69 6d 61 67 65 2f 77 65 62 70 2c 69 6d 61  9,image/webp,ima
               67 65 2f 61 70 6e 67 2c 2a 2f 2a 3b 71 3d 30 2e  ge/apng,*/*;q=0.
               38 0d 0a 41 63 63 65 70 74 2d 45 6e 63 6f 64 69  8..Accept-Encodi
               6e 67 3a 20 67 7a 69 70 2c 20 64 65 66 6c 61 74  ng: gzip, deflat
               65 2c 20 62 72 0d 0a 41 63 63 65 70 74 2d 4c 61  e, br..Accept-La
               6e 67 75 61 67 65 3a 20 65 6e 2d 55 53 2c 65 6e  nguage: en-US,en
               3b 71 3d 30 2e 39 0d 0a 0d 0a                    ;q=0.9....

Tooling

 

cro web

$ cro web
Cro web interface running at http://localhost:10203/

example 5

 

myjudo.net

 

SSL

my $https = Cro::HTTP::Server.new(
    :1443port,
    :host<0.0.0.0>,
    http => <1.1>,
    before => [
        Cro::HTTP::Session::InMemory[UserSession].new;
    ],
    tls => %(
        private-key-file => %*ENV<MYJUDO_TLS_KEY>,
        certificate-file => %*ENV<MYJUDO_TLS_CERT>,
        ciphers => 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256',
    ),
    application => routes(),
    after => [
        Cro::HTTP::Log::File.new(logs => $*OUT, errors => $*ERR),
    ]
);


$https.start;

example 6

 

myjudo.net

 

Middleware

class StrictTransportSecurity does Cro::Transform {
        has Duration:D $.max-age is required;

        method consumes() { Cro::HTTP::Response }
        method produces() { Cro::HTTP::Response }

        method transformer(Supply $pipeline --> Supply) {
            supply {
                whenever $pipeline -> $response {
                    $response.append-header:
                        'Strict-Transport-Security',
                        "max-age=$!max-age; includeSubDomains; preload";
                    emit $response;
                }
            }
        }
    }

my $https = Cro::HTTP::Server.new(
    ...
    application => routes(),
    after => [
        Cro::HTTP::Log::File.new(logs => $*OUT, errors => $*ERR),
        # set max age to be one year and one day, 366 days
        StrictTransportSecurity.new(max-age => Duration.new(366 * 24 * 60 * 60)),
    ]
);


$https.start;

Summary

 

https://slides.com/lancewicks/yapceu2018

Questions?

 

Thank You!

 

https://slides.com/lancewicks/yapceu2018

Building a web app with Perl 6 and Cro

 

 

Lance Wicks

@lancew