Dancer Advent Calendar 2020 http://advent.perldancer.org/ Simple content negotiation with the mutable serializer. http://advent.perldancer.org/2020/24 perl http://advent.perldancer.org/2020/24 Thu, 24 Dec 2020 00:00:00 -0000 <div class="pod-document"><h1><a name="simple_content_negotiation_with_the_mutable_serializer_"></a>Simple content negotiation with the mutable serializer.</h1> <p>If you define a serializer in your Dancer2 app, that is what your app "speaks". All output will returned serialized. Use common frontend frameworks like react, vue, or angular, and you will need to be able to return JSON. However there are other options that may be suitable or be driven by business requirements.</p> <p>Out of the box Dancer2 ships with serializers for <a href="https://metacpan.org/module/YAML">YAML</a>, <a href="https://metacpan.org/module/JSON">JSON</a> and <a href="https://metacpan.org/module/Data::Dumper">Data::Dumper</a>. Others are avalable on CPAN for <a href="https://metacpan.org/module/Dancer2::Serializer::XML">XML</a> and <a href="https://metacpan.org/module/Dancer2::Serializer::CBOR">CBOR</a>. Why limit yourself to one when you can have them all?</p> <h2><a name="set_serializer___gt___mutable_"></a>set serializer =&gt; 'Mutable'</h2> <p>Dancer2 provides a mutable serializer as part of the core distribution. In this context "mutable" refers to being able to alter the serialization based on request headers. When a request is made, the HTTP request <code>Accept</code> header declares the mime type the clients wants for the response.</p> <p>Lets get things rolling with a example. The mapping of mime types to serializers is defined inline here to include XML and exclude (Data::)Dumper. To keep it simple there's only one route.</p> <pre class="prettyprint">package Example::App; use Dancer2; # https://github.com/PerlDancer/Dancer2/issues/1568 use YAML; use Dancer2::Serialier::XML; set engines =&gt; { serializer =&gt; { Mutable =&gt; { mapping =&gt; { 'text/x-yaml' =&gt; 'YAML', 'text/html' =&gt; 'YAML', 'text/x-json' =&gt; 'JSON', 'application/json' =&gt; 'JSON', 'text/xml' =&gt; 'XML', } } } }; set serializer =&gt; 'Mutable'; get '/' =&gt; sub { return { Australia =&gt; '+61-3-8652-1453', 'New Zealand' =&gt; '+64-9-886-0565', UK =&gt; '+44-11-7325-7425', USA =&gt; '+1-760-706-7425', }; }; 1;</pre> <p>Bring this app up via plackup and send some requests at it.</p> <p>For the YAML fans:</p> <pre class="prettyprint">curl -H 'Accept: text/x-yaml' http://localhost:5000 --- Australia: +61-3-8652-1453 New Zealand: +64-9-886-0565 UK: +44-11-7325-7425 USA: +1-760-706-7425</pre> <p>Or for the XML lovers:</p> <pre class="prettyprint">curl -H 'Accept: text/xml' http://localhost:5000 &lt;opt Australia="+61-3-8652-1453" New Zealand="+64-9-886-0565" UK="+44-11-7325-7425" USA="+1-760-706-7425" /&gt;</pre> <p>An <code>Accept</code> header with a mime type that doesn't match the mutable serialier mapping will return JSON.</p> <pre class="prettyprint">curl -H 'Accept: nosuch/mimetype' http://localhost:5000 {"Australia":"+61-3-8652-1453","New Zealand":"+64-9-886-0565", "UK":"+44-11-7325-7425","USA":"+1-760-706-7425"}</pre> <h2><a name="ins_and_outs"></a>Ins and Outs</h2> <p>Serializers also decode request content into body parameters. The mutable serializer uses the mime type from the request <code>Content-type</code> header to select how to decode the incomming data.</p> <p>Add a route that returns the body parameters as a hashref to the example app above</p> <pre class="prettyprint">POST '/babelfish' =&gt; sub { return body_parameters-&gt;as_hashref; };</pre> <p>Restart the app and POST a request where the body is JSON encoded and we accept XML back.</p> <pre class="prettyprint">curl -H 'Content-type: application/json' -H 'Accept: text/xml' \ -d '{"USA":"+1-760-706-7425","UK":"+44-11-7325-7425","Australia":"+61-3-8652-1453","New Zealand":"+64-9-886-0565"}' \ -X POST http://localhost:5000/babelfish &lt;opt Australia="+61-3-8652-1453" New Zealand="+64-9-886-0565" UK="+44-11-7325-7425" USA="+1-760-706-7425" /&gt;</pre> <p>While translation from JSON to XML or any other supported serialized formats may not give you up, it won't let you down either. Your clients may appreciate the ease in requesting data in the format they find easiest to use.</p> <h2><a name="beyond_serializer__mutable"></a>Beyond Serializer::Mutable</h2> <p>Content negotiation can go beyond a single mimetype requested by the client. Language choice, media selection, and weighted options are all defined in the HTTP specs, but are beyond the current capability of the mutable serializer. If this is something you require, <a href="https://metacpan.org/module/Dancer2::Plugin::HTTP::ContentNegotiation">Dancer2::Plugin::HTTP::ContentNegotiation</a> may fit your needs. Another possibility is a before hook using <a href="https://metacpan.org/module/HTTP::Negotiate">HTTP::Negotiate</a>.</p> <p>Use them to go make something awesome.</p> <h2><a name="author"></a>Author</h2> <p>This article has been written by Russell @veryrusty Jenkins for the Perl Dancer Advent Calendar 2020.</p> <h2><a name="copyright"></a>Copyright</h2> <p>No copyright retained. Enjoy.</p> </div> Asynchronous Dancer2 PT. 2 - Promises and a Bonus! http://advent.perldancer.org/2020/23 perl http://advent.perldancer.org/2020/23 Wed, 23 Dec 2020 00:00:00 -0000 <div class="pod-document"><h1><a name="asynchronous_dancer2_pt__2___promises_and_a_bonus_"></a>Asynchronous Dancer2 PT. 2 - Promises and a Bonus!</h1> <p>Now that you're familiar with <a href="https://advent.perldancer.org/2020/22">asynchronous programming in Dancer2</a>, we can delve into taking awkward async code and turning it into more manageable code using <a href="https://metacpan.org/module/Promises">Promises</a>.</p> <h2><a name="awkward"></a>Awkward?</h2> <p>Looking at the code in the <a href="https://advent.perldancer.org/2020/21">previous article</a>, you might see how our code begins to shift to the right with growing callbacks. Each async code requires providing anonymous subroutines that will get triggered once the async request (whether API request or DB call) returns. Our code will start to look like an arrow.</p> <pre class="prettyprint">use experimental qw&lt; postderef signatures &gt;; do_this_async( sub ($foo) { do_this_other_thing_async( $foo, sub ($bar) { foreach my $baz ( $bar-&gt;@* ) { do_more_async( $baz, sub ($quux) { ... }); } }); });</pre> <p>Eventually, this can become quite unwieldy. Promises is a pattern that can help us tame our code. <a href="https://metacpan.org/module/Promises">Promises</a> is an exceptional implementation of this pattern, so we will use it.</p> <h2><a name="so_what_are_promises"></a>So what are Promises?</h2> <p>Promises are a simple mechanism for turning that arrow pattern above into a clear straight chain where each callback is stored in an object using a method, and the eventual value can be used in another callback, again stored in the same object using a method.</p> <p><a href="https://metacpan.org/module/Promises">Promises</a> also provides us with comfortable method chaining..</p> <p>Let's take the above code and rewrite it using the Promises syntax:</p> <pre class="prettyprint">use experimental qw&lt; postderef signatures &gt;; do_this_async()-&gt;then( sub ($foo) { return do_this_other_thing_async($foo) })-&gt;then( sub ($bar) { my @promises = do_more_async($_) for $bar-&gt;@*; return collect(@promises); })-&gt;then( sub (@results) { ... });</pre> <p>Assuming that <code>do_this_async</code> returns a <a href="https://metacpan.org/module/Promise">Promise</a> object, we can call <code>then</code> to tack on a callback for the result that this method will eventually retrieve.</p> <p>We can then call another method (<code>call_this_other_thing_async</code>), which will also return a Promise, to which we tack another callback that will use the value we retrieved from the previous method.</p> <p>Notice everything here so far had been done with callbacks. We don't execute anything except the initial <code>do_this_async</code>. The rest simply registers callbacks. When it all gets executed at the end, this smart chaining will cause them to be triggered in the right order, connecting them together.</p> <p>Next, we call a method <code>do_more_async</code> which returns a Promise again, for each of our input. We collect those in a variable and call the <code>Promises</code> function <code>collect</code> which creates a single promise from a set of them. We can then tack a callback for it using <code>then</code>.</p> <p>Of course, we will need to have these functions return Promise objects, so you'll get to see that.</p> <h2><a name="okay__so_how_do_we_rewrite_this"></a>Okay, so how do we rewrite this?</h2> <p>You might not have fully understood how Promises - that's okay. But even if you did, you might not know how to move our previous code to this. No matter which category you fall in (maybe both?), we're now going to do it together.</p> <h3><a name="update_our_imports"></a>Update our imports</h3> <pre class="prettyprint">package CovidStats::Promises; use Dancer2; use experimental qw&lt; postderef signatures &gt;; use DateTime; use AnyEvent; use AnyEvent::HTTP; use Promises qw&lt; collect deferred &gt;; use Statistics::Descriptive::Full; use URI::Escape qw&lt; uri_escape &gt;; use constant { 'MAX_COUNTRIES' =&gt; 5, 'MAX_DAYS' =&gt; 14, 'SUMMARY_URL' =&gt; 'https://covid-api.mmediagroup.fr/v1/cases', 'COUNTRY_URL' =&gt; 'https://covid-api.mmediagroup.fr/v1/history?country=%s&amp;status=Confirmed', 'LTRIM' =&gt; '0.25', };</pre> <p>Similar to before, but this time we also load <a href="https://metacpan.org/module/Promises">Promises</a>.</p> <h3><a name="starting_with_the_initial_request"></a>Starting with the initial request</h3> <p>The big benefit of Promises is that it changes the order of writing (without changing the order of execution) so while we previous started with writing the code for the end result, we don't need to do this now. We can begin directly with the first request.</p> <p>We will need to turn this all to Promises, so we start with creating a deferred object:</p> <pre class="prettyprint">get '/' =&gt; sub { return delayed { flush(); my $def = deferred();</pre> <p>We can decide that this succeeded (using <code>resolve</code>) or failed (using <code>reject</code>) and we can retrieve the Promise object from it. (We're covering Promises loosely here, so you should probably read the <a href="https://metacpan.org/module/Promises">Promises</a> documentation to fully comprehend the syntax.)</p> <p>Now the request:</p> <pre class="prettyprint">http_get SUMMARY_URL(), sub ( $body, $hdrs ) { my $data = from_json($body); $def-&gt;resolve($data) };</pre> <p>What we do here is make a request. When it will respond (whenever that is), we make sure to mark the promise object as resolved. We also send it what we got so it could be used as the eventual data in the Promise.</p> <p>We can now use the Promise object by calling <code>promise</code> object on the deferred object. Once we have the Promise object, we can start tacking on the rest of the logic.</p> <pre class="prettyprint">$def-&gt;promise-&gt;then( sub ($data) { my @countries = ( sort { $data-&gt;{$b}{'All'}{'confirmed'} &lt;=&gt; $data-&gt;{$a}{'All'}{'confirmed'} } grep $_ ne 'Global', keys $data-&gt;%* )[ 0 .. MAX_COUNTRIES() ]; if ( !@countries ) { die "Sorry, failed to received countries\n"; } return \@countries; ))-&gt;then( delayed( sub ($countries) {...} ) );</pre> <p>We start by getting the Promise (by calling <code>promise</code> on the deferred) and from now on, we can just start chaining <code>then</code> methods. The <code>$data</code> variable will eventually (once it actually gets called) be the parameter that we sent to the <code>resolve</code> method above. That's the chaining that is happening here.</p> <p>We can define the top countries and return them. We can also just <code>die</code> when something fails because any <code>die</code> within Promises is caught and managed using an exception catching block. We'll get to it at the end.</p> <p>Notice this time, we don't use a <code>delayed</code> block because we're not calling any Dancer2 keywords (like <code>content</code> or <code>done</code>).</p> <p>This response will return and be wrapped in a Promise, which then allows us to chain another <code>then</code> to it with a callback. The parameter will be the countries.</p> <p>You might be wondering why we create two code blocks instead of keeping one big code block. Theoretically, we could, but when you separate to multiple blocks it's both more readable, as well as allows the event loop to run things between those code blocks if necessary. In short, this is just good practice.</p> <h3><a name="multiple_requests_with_promises"></a>Multiple requests with Promises</h3> <p>Here is a bit of a trick. A promise is meant for only one value, which normally means only one asynchronous action. We would need to collect multiple Promises - one Promise per API request (per country, based on our API). We can do that and then create a single Promise from all of them using... <code>collect</code>!</p> <pre class="prettyprint">}))-&gt;then( delayed( sub ($countries) { my $yesterday = DateTime-&gt;now-&gt;subtract( 'days' =&gt; 1 ); my @promises; foreach my $country ( $countries-&gt;@* ) { my $data_url = sprintf COUNTRY_URL(), uri_escape($country); my $def = deferred(); http_get $data_url, delayed( sub ( $body, $hdrs ) { my $country_data = from_json($body); my @period; my $day = $yesterday; foreach ( 0 .. MAX_DAYS() ) { push @period, $day-&gt;ymd(); $day = $day-&gt;subtract( 'days' =&gt; 1 ); } my @period_data = $country_data-&gt;{'All'}{'dates'}-&gt;@{@period}; my $stat = Statistics::Descriptive::Full-&gt;new(); $stat-&gt;add_data($_) for @period_data; $def-&gt;resolve( $country =&gt; $stat-&gt;trimmed_mean( LTRIM() ) ); }); push @promises, $def-&gt;promise(); } return collect(@promises); }))-&gt;then( delayed( sub (@stats_by_country) {...} ) );</pre> <p>Notice we repeat the same pattern with create a deferred Promise, then making all of these requests, adding the Promise from the deferred object into an array, and eventually calling <code>collect</code> to create a single Promise.</p> <p>The <code>then</code> callback will receive an array where each element represents the response from each Promise. The order will not be by execution, but by how we inserted them into the original <code>@promises</code> array.</p> <h3><a name="handle_the_result"></a>Handle the result</h3> <p>Simple enough:</p> <pre class="prettyprint">}))-&gt;then( delayed( sub (@stats_by_country) { content "By country (period of " . MAX_DAYS() . " days):\n"; content "- $_-&gt;[0]: $_-&gt;[1]\n" for @stats_by_country; }))...</pre> <h3><a name="add_an_exception_catch_block_and_our_final_block"></a>Add an exception catch block and our final block</h3> <p>Promises give us the <code>catch</code> block to catch exceptions and a <code>finally</code> block to handle the end, whether worked or not.</p> <p>In this case, we can use them as such:</p> <pre class="prettyprint">}))-&gt;catch( delayed( sub ($error) { content($error); }))-&gt;finally( delayed( sub (@args) { content "\nThank you for visiting our API\n"; done(); }));</pre> <p>We caught the error and sent it to the user. We end everything by calling the <code>done</code> in the <code>finally</code> block.</p> <h3><a name="full_program"></a>Full program</h3> <p>The full program is:</p> <pre class="prettyprint">package CovidStats::Promises; use Dancer2; use experimental qw&lt; postderef signatures &gt;; use DateTime; use AnyEvent; use AnyEvent::HTTP; use Promises qw&lt; collect deferred &gt;; use Statistics::Descriptive::Full; use URI::Escape qw&lt; uri_escape &gt;; use constant { 'MAX_COUNTRIES' =&gt; 5, 'MAX_DAYS' =&gt; 14, 'SUMMARY_URL' =&gt; 'https://covid-api.mmediagroup.fr/v1/cases', 'COUNTRY_URL' =&gt; 'https://covid-api.mmediagroup.fr/v1/history?country=%s&amp;status=Confirmed', 'LTRIM' =&gt; '0.25', }; get '/' =&gt; sub { return delayed { flush(); my $def = deferred(); http_get SUMMARY_URL(), delayed( sub ( $body, $hdrs ) { my $data = from_json($body); $def-&gt;resolve($data) }); $def-&gt;promise-&gt;then( delayed( sub ($data) { my @countries = ( sort { $data-&gt;{$b}{'All'}{'confirmed'} &lt;=&gt; $data-&gt;{$a}{'All'}{'confirmed'} } grep $_ ne 'Global', keys $data-&gt;%* )[ 0 .. MAX_COUNTRIES() ]; if ( !@countries ) { die "Sorry, failed to received countries\n"; } return \@countries; }))-&gt;then( delayed( sub ($countries) { my $yesterday = DateTime-&gt;now-&gt;subtract( 'days' =&gt; 1 ); my @promises; foreach my $country ( $countries-&gt;@* ) { my $data_url = sprintf COUNTRY_URL(), uri_escape($country); my $def = deferred(); http_get $data_url, delayed( sub ( $body, $hdrs ) { my $country_data = from_json($body); my @period; my $day = $yesterday; foreach ( 0 .. MAX_DAYS() ) { push @period, $day-&gt;ymd(); $day = $day-&gt;subtract( 'days' =&gt; 1 ); } my @period_data = $country_data-&gt;{'All'}{'dates'}-&gt;@{@period}; my $stat = Statistics::Descriptive::Full-&gt;new(); $stat-&gt;add_data($_) for @period_data; $def-&gt;resolve( $country =&gt; $stat-&gt;trimmed_mean( LTRIM() ) ); }); push @promises, $def-&gt;promise(); } return collect(@promises); }))-&gt;then( delayed( sub (@stats_by_country) { content "By country (period of " . MAX_DAYS() . " days):\n"; content "- $_-&gt;[0]: $_-&gt;[1]\n" for @stats_by_country; }))-&gt;catch( delayed( sub ($error) { content($error); }))-&gt;finally( delayed( sub (@args) { content "\nThank you for visiting our API\n"; done(); })); } ); }; 1;</pre> <h2><a name="application_runner_and_running"></a>Application runner and running</h2> <p>Our <code>app.psgi</code> file is simple enough:</p> <pre class="prettyprint">use CovidStats::Promises; CovidStats::Promises-&gt;to_app();</pre> <p>We can run this with <a href="https://metacpan.org/module/Twiggy">Twiggy</a> in the following manner:</p> <pre class="prettyprint">$ plackup -s Twiggy bin/app.psgi Twiggy: Accepting connections at http://0.0.0.0:5000/</pre> <p>Normally, <code>plackup</code> is very good at recognizing which server to use. If we didn't specify to use <a href="https://metacpan.org/module/Twiggy">Twiggy</a>, it will still get it right:</p> <pre class="prettyprint">$ plackup bin/app.psgi Twiggy: Accepting connections at http://0.0.0.0:5000/</pre> <p>Of course, on production you would set up something more elaborate instead of running this on a terminal. I suggest looking at <a href="https://metacpan.org/module/Dancer2::Manual::Deployment">Dancer2::Manual::Deployment</a> for production use.</p> <h2><a name="testing_out_our_application"></a>Testing out our application</h2> <p>On another terminal, we will run the following command:</p> <pre class="prettyprint">$ curl localhost:5000 By country (period of 7 days): - US: 16640229.25 - India: 9690261.25 - Brazil: 6294810 - France: 2118033.5 - Russia: 1854813.5 - United Kingdom: 1110655.5 Thank you for visiting our API</pre> <p>Done!</p> <h2><a name="where_did_the_condvar_go"></a>Where did the condvar go?</h2> <p>The studious might notice there's no condvar (<code>$cv</code>) used in our example. This is because <a href="https://metacpan.org/module/Promises">Promises</a> automatically handles that part. It also identified that we're using <a href="https://metacpan.org/module/AnyEvent">AnyEvent</a> so it used <a href="https://metacpan.org/module/AnyEvent">AnyEvent</a> and its condvars for implementing the event loop handling.</p> <h2><a name="you_said_something_about_a_surprise"></a>You said something about a surprise?</h2> <p>In the <a href="https://advent.perldancer.org/2020/21">previous article</a> we discussed other options than <a href="https://metacpan.org/module/AnyEvent">AnyEvent</a>. If you're interested in writing async code, I suggested look into <a href="https://metacpan.org/module/IO::Async">IO::Async</a>.</p> <p>To get you started, I implemented the same exercise using <a href="https://metacpan.org/module/IO::Async">IO::Async</a>. Luckily, because it already uses <a href="https://metacpan.org/module/Future">Future</a> (a more advanced version of the Promises pattern), we don't need to write one version with plain syntax and then another with Promises. It will provide us with this interface by default.</p> <pre class="prettyprint">package CovidStats::IOAsync; use Dancer2; use experimental qw&lt; postderef signatures &gt;; use DateTime; use IO::Async; use IO::Async::Loop; use Future::Utils qw&lt; fmap &gt;; use Net::Async::HTTP; use Statistics::Descriptive::Full; use URI::Escape qw&lt; uri_escape &gt;; use constant { 'MAX_COUNTRIES' =&gt; 5, 'MAX_DAYS' =&gt; 14, 'SUMMARY_URL' =&gt; 'https://covid-api.mmediagroup.fr/v1/cases', 'COUNTRY_URL' =&gt; 'https://covid-api.mmediagroup.fr/v1/history?country=%s&amp;status=Confirmed', 'LTRIM' =&gt; '0.25', }; get '/' =&gt; sub { return delayed { flush(); my $loop = IO::Async::Loop-&gt;new(); my $http = Net::Async::HTTP-&gt;new(); $loop-&gt;add($http); my $main_req = $http-&gt;GET( SUMMARY_URL() )-&gt;then( sub ($res) { my $data = from_json( $res-&gt;content ); my @countries = ( sort { $data-&gt;{$b}{'All'}{'confirmed'} &lt;=&gt; $data-&gt;{$a}{'All'}{'confirmed'} } grep $_ ne 'Global', keys $data-&gt;%* )[ 0 .. MAX_COUNTRIES() ]; if ( !@countries ) { content("Sorry, failed to received countries\n"); done(); return; } return \@countries; })-&gt;then( sub ($countries) { my $yesterday = DateTime-&gt;now-&gt;subtract( 'days' =&gt; 1 ); fmap( sub ($country) { my $data_url = sprintf COUNTRY_URL(), uri_escape($country); return $http-&gt;GET($data_url)-&gt;then( sub ($res) { my $country_data = from_json( $res-&gt;content ); my @period; my $day = $yesterday; foreach ( 0 .. MAX_DAYS() ) { push @period, $day-&gt;ymd(); $day = $day-&gt;subtract( 'days' =&gt; 1 ); } my @period_data = $country_data-&gt;{'All'}{'dates'}-&gt;@{@period}; my $stat = Statistics::Descriptive::Full-&gt;new(); $stat-&gt;add_data($_) for @period_data; return [ $country =&gt; $stat-&gt;trimmed_mean( LTRIM() ) ]; })-&gt;catch( sub ($error) { content("Sorry, failed to fetch data for $country: $error"); }); }, 'foreach' =&gt; $countries, 'concurrent' =&gt; scalar $countries-&gt;@*, )-&gt;then( sub (@results) { content( "By country (period of " . MAX_DAYS() . " days):\n" ); content( "- $_-&gt;[0]: $_-&gt;[1]\n" ) for sort { $b-&gt;[1] &lt;=&gt; $a-&gt;[1] } @results; content("\nThank you for visiting our API\n"); done(); }); })-&gt;catch( sub ($error) { content("Sorry, failed to receive data: $error\n"); done(); }); $loop-&gt;await($main_req); }; }; 1;</pre> <p>We can run it with <code>plackup</code> in a very similar way, but we will use a different web server. Luckily, the author for <a href="https://metacpan.org/module/IO::Async">IO::Async</a> had done us the service of writing a PSGI web server in <a href="https://metacpan.org/module/IO::Async">IO::Async</a>, so we can use that.</p> <p>Our <code>app.psgi</code> file:</p> <pre class="prettyprint">use CovidStats::IOAsync; CovidStats::IOAsync-&gt;to_app();</pre> <p>We can run this with <a href="https://metacpan.org/module/Net::Async::HTTP::Server">Net::Async::HTTP::Server</a> in the following manner:</p> <pre class="prettyprint">$ plackup -s Net::Async::HTTP::Server bin/app.psgi Plack::Handler::Net::Async::HTTP::Server: Accepting connections at http://0:5000/</pre> <p>I recommend checking out <a href="https://metacpan.org/module/IO::Async">IO::Async</a>. It comes with a host of useful, advanced, modern tools. It has a lot of thorough documentation. It has a lively community, and a responsive and supportive author.</p> <p>You can read more about <a href="https://metacpan.org/module/IO::Async">IO::Async</a> <a href="http://www.perladvent.org/2018/2018-12-14.html">here</a> and in Paul Evans' <a href="https://leonerds-code.blogspot.com/2020/12/">Advent Calendar 2020</a>.</p> <h2><a name="author"></a>Author</h2> <p>This article has been written by Sawyer X for the Perl Dancer Advent Calendar 2020.</p> <h2><a name="copyright"></a>Copyright</h2> <p>No copyright retained. Enjoy.</p> <p>Sawyer X.</p> </div> Asynchronous Dancer2 PT. 1 - What, Why, and How? http://advent.perldancer.org/2020/22 perl http://advent.perldancer.org/2020/22 Tue, 22 Dec 2020 00:00:00 -0000 <div class="pod-document"><h1><a name="asynchronous_dancer2_pt__1___what__why__and_how"></a>Asynchronous Dancer2 PT. 1 - What, Why, and How?</h1> <p>Dancer2 has support for asynchronous programming, allowing you to:</p> <ul> <li><a name="item_Write_code_that_can_run_multiple_operations_at_the_same_time"></a><b>Write code that can run multiple operations at the same time</b> </li> <li><a name="item_Write_code_that_streams_the_response_to_the_user"></a><b>Write code that streams the response to the user</b> </li> <li><a name="item_Run_code_in_the_background_after_the_user_finished_the_request"></a><b>Run code in the background after the user finished the request</b> </li> </ul> <p>These are provided by the Delayed Response capability, which you can also read about in a previous post on it.</p> <h2><a name="this_sounds_familiar___"></a>This sounds familiar...</h2> <p>If you think you've heard this before, it might be because of <a href="https://advent.perldancer.org/2016">this article</a> from the Perl Advent Calendar of 2016, in which we cover the asynchronous interface that Dancer2 provides.</p> <p>But just because we wrote about it in the past, it does not mean we can't write about it again. Some messages are worth repeating. :)</p> <h2><a name="so_what_s_this_about"></a>So what's this about?</h2> <p>This two-part article will teach you about writing asynchronous code using promises.</p> <p>This first part will focus on what asynchronous programming is, giving you some context, and we will even write some asynchronous code.</p> <p>In the next part, we will focus on rewriting this code using the Promises pattern, making use of the <a href="https://metacpan.org/module/Promises">Promises</a> module.</p> <h2><a name="but_wait__why_would_i_even_want_asynchronous_code"></a>But wait, why would I even want asynchronous code?</h2> <p>There are several cases in which asynchronous code execution is beneficial.</p> <ul> <li> <p>Imagine you need to make multiple database requests to calculate a response for the user. You need to request the user data, then the reservations for the user, and the latest messages they might have. Given the user ID from the session, these DB calls don't depend on each other, but alas, you must wait for each one to finish before you call the next one.</p> <p>If you're using asynchronous programming, you could trigger multiple DB calls at the same time and wait for them to finish.</p> </li> <li> <p>Most web applications are a set of transactions: User makes a request, the app figures out a response and returns it. However, some applications can make a full-blown conversation. For example, a chat app will continuously provide information to the user.</p> <p>When working as a sysadmin, we had one interface that had to be restarted every day. The restart required logging in, setting up the shell (old OS), finding the process, and issuing a process restart.</p> <p>I eventually wrote a small web interface that used SSH keys to connect to the server via SSH, run all the commands, and eventually restart. I used streamed responses to continuously show the progress as I was making it, instead of only showing the result at the end.</p> </li> <li> <p>Lastly, your application might be doing a lot of work at the end of a request, like logging everything that happened, or the timing of numerous operations during the request for analytical purposes. This would require calculations, comparisons, summarizing, and storing.</p> <p>With an asynchronous interface (that supports post-response actions), you could respond to the user and while they continue on their merry way, your app would continue with the logging (and possible cleanups) asynchronously.</p> </li> </ul> <p>Over time, we got used to web applications being transaction-based, so we created queue managers to help deal with it. We provide all work that should be done to the queue manager and it will run it in the background while the application continues.</p> <p>Queue managers are very useful, but they don't solve every situation, and in some cases, they are just unnecessary overhead.</p> <h2><a name="asynchronous__non_blocking__streaming__what"></a>Asynchronous, non-blocking, streaming, what?</h2> <p>There are multiple definitions floating around. There are differences between them, but for our purposes, they can all be viewed as similar enough to be used interchangeably.</p> <p>To be a slightly more technical (without the 100 lines of text I have decided to spare you), asynchronous code is an umbrella term. We will write asynchronous code that will work using an event loop and non-blocking calls. Streaming is the term for continuously feeding a stream of information versus a single response.</p> <h2><a name="what_event_loop_will_we_use"></a>What event loop will we use?</h2> <p>For this example, we will use the <a href="https://metacpan.org/module/AnyEvent">AnyEvent</a> event loop. However, there are other event loops you should also consider, mainly <a href="https://metacpan.org/module/IO::Async">IO::Async</a>.</p> <p>The example we use here can be equally written in <a href="https://metacpan.org/module/IO::Async">IO::Async</a> just the same. If you have any issues with <a href="https://metacpan.org/module/AnyEvent">AnyEvent</a>, we suggest researching <a href="https://metacpan.org/module/IO::Async">IO::Async</a>.</p> <p>The <a href="https://metacpan.org/module/Plack">Plack</a> web server we will use with <a href="https://metacpan.org/module/AnyEvent">AnyEvent</a> is <a href="https://metacpan.org/module/Twiggy">Twiggy</a>, but if you're using <a href="https://metacpan.org/module/IO::Async">IO::Async</a>, you can use <a href="https://metacpan.org/module/Net::Async::HTTP::Server">Net::Async::HTTP::Server</a>.</p> <h2><a name="so_what_example_are_we_using_here"></a>So what example are we using here?</h2> <p>For our example, we will build a small application that, when called, will reach a Covid-19 API to retrieve the top countries with confirmed cases. Then it will fetch each country's confirmed cases for the last period and create a trimmed mean / truncated mean.</p> <ul> <li><a name="item_Why_"></a><b>Why?</b> <p>This small app provides us with an example of making a single request (in our case, an API, but it can be a DB call just the same) and then making multiple concurrent requests (again, through an API, but could be a DB call).</p> </li> <li><a name="item_Could_this_be_cached_daily_"></a><b>Could this be cached daily?</b> <p>Theoretically yes. We're not looking at the most optimal code, but just enough contrived code to deliver the message but not be entirely useless.</p> </li> <li><a name="item_What_s_a_trimmed_mean_"></a><b>What's a trimmed mean?</b> <p>Mean (average) is not a very reliable metric, since it can be easily offset by outliers. You have ten good grades and one really bad one. With mean, you wouldn't look like such a good student. However, if we trimmed outliers, we would be able to see you as a good student with one crappy grade.</p> <p>I'm not a statistician, nor am I especially good with math, so if you disagree and have a better function, go ahead and use that.</p> </li> </ul> <h2><a name="where_s_the_code"></a>Where's the code?</h2> <h3><a name="the_basics"></a>The basics</h3> <p>First, our initial code:</p> <pre class="prettyprint">package CovidStats; use Dancer2; # This is still required on the version of Perl we're using # but this won't be "experimental" for much longer use experimental qw&lt; postderef signatures &gt;; # Some modules we'll be using use DateTime; use AnyEvent; use AnyEvent::HTTP; # http_get use Statistics::Descriptive::Full; use URI::Escape qw&lt; uri_escape &gt;; # A few constants, to keep things flexible use constant { 'MAX_COUNTRIES' =&gt; 5, 'MAX_DAYS' =&gt; 7, 'SUMMARY_URL' =&gt; 'https://covid-api.mmediagroup.fr/v1/cases', 'COUNTRY_URL' =&gt; 'https://covid-api.mmediagroup.fr/v1/history?country=%s&amp;status=Confirmed', 'LTRIM' =&gt; '0.25', };</pre> <h3><a name="now__the_routing"></a>Now, the routing</h3> <p>We will set up only one route (<code>/</code>) and it will respond in text instead of HTML, just for simpler interaction.</p> <pre class="prettyprint">get '/' =&gt; sub { return delayed { flush(); content("hi!"); done(); }; };</pre> <p>First, we return a <code>delayed</code> response. This means our code will be asynchronous. The <code>delayed</code> keyword is required for two things:</p> <ul> <li><a name="item_Create_the_initial_asynchronous_response"></a><b>Create the initial asynchronous response</b> <p>This allows Dancer2 to declare to the web server that it is running asynchronous code.</p> </li> <li><a name="item_Any_time_we_have_asynchronous_code_blocks"></a><b>Any time we have asynchronous code blocks</b> <p>Whenever we have a subroutine that needs access to the Dancer2 DSL, we need to change the <code>sub</code> into a <code>delayed</code> <code>sub</code>.</p> </li> </ul> <p>Using <code>delayed</code> can be done without parenthesis (just like other Dancer2 keywords) and without the <code>sub</code> keyword. You can also use it with parenthesis and the <code>sub</code> keyword, which will allow you to make use of subroutine signatures.</p> <p>The following are equivalent:</p> <pre class="prettyprint">return delayed {...}; return delayed( sub {...} );</pre> <p>We will be using both styles in this example.</p> <p>The <code>flush</code> keyword will start streaming our information. Each <code>content</code> call will send data. It can be used multiple times and, if we forgot to call <code>flush</code>, it will be called the first time we call <code>content</code>.</p> <p>The <code>done</code> keyword tells Dancer2 to tell the web server that we're done and it can close the connection with the user. We can run additional code afterward, but we don't have any in our example.</p> <h3><a name="expanding_it_to_make_the_first_request"></a>Expanding it to make the first request</h3> <p>What we want now is to make a request to retrieve the top countries with confirmed cases.</p> <pre class="prettyprint">get '/' =&gt; sub { return delayed { flush(); my $cv = AnyEvent-&gt;condvar(); $cv-&gt;cb( delayed { content("Retrieved countries"); done(); }); $cv-&gt;begin(); http_get SUMMARY_URL(), sub ( $body, $hdrs ) { $cv-&gt;end(); }; }; };</pre> <p>Here we set up a condvar (condition variable) to manage states. The <code>cb</code> with the <code>delayed</code> subroutine block indicate what to do when our following asynchronous code finishes running.</p> <p>Yes, we declare first what we do when code ends and only then write the code. Welcome to asynchronous code. You might also now understand why Promises is such a popular pattern, which we will see in the next part of this series.</p> <p>We then provide the async code to run, namely an HTTP request to our API. The subroutine calling <code>$cv-&gt;end</code> notes the end of the async code and will trigger the <code>$cv-&gt;cb</code> code we set up.</p> <h3><a name="let_s_add_some_data_validation"></a>Let's add some data validation</h3> <pre class="prettyprint">get '/' =&gt; sub { return delayed { flush(); my $cv = AnyEvent-&gt;condvar(); $cv-&gt;cb( delayed { done(); } ); $cv-&gt;begin(); http_get SUMMARY_URL(), delayed( sub ( $body, $hdrs ) { my $data; eval { $data = from_json($body); 1; } or do { content("Sorry, failed to fetch data: $!"); $cv-&gt;end(); return; }; ... }; };</pre> <p>In this case, we added some validation for our JSON response. We also moved to using <code>delayed</code> so we could access <code>content</code> and <code>done</code> keywords. You'll notice we're using <code>delayed</code> with parenthesis and the <code>sub</code> keyword, so we could continue using subroutine signatures.</p> <h3><a name="filtering_and_sorting"></a>Filtering and sorting</h3> <p>Our next goal is to pick the top countries based on the most confirmed cases using a simple sort. I won't go into the data structure the API returns because that's the least valuable part here.</p> <p>In short, we take all of our countries, excluding the "Global" category the API provides, then each country's <code>confirmed</code> key is compared and sorted, eventually picking only the amount we want.</p> <pre class="prettyprint">my @countries = ( sort { $data-&gt;{$b}{'All'}{'confirmed'} &lt;=&gt; $data-&gt;{$a}{'All'}{'confirmed'} } grep $_ ne 'Global', keys $data-&gt;%* )[ 0 .. MAX_COUNTRIES() ]; if ( !@countries ) { content("Sorry, failed to received countries\n"); done(); return; }</pre> <p>This code will be run within the <code>http_get</code> callback.</p> <h3><a name="introducing_multiple_concurrent_requests"></a>Introducing multiple concurrent requests</h3> <p>Once we get this list of top countries, we want to get the results for each country's history. Here is where the real magic happens.</p> <p>We want to make another <code>http_get</code> call for each country and we want these to run <b>concurrently</b>. This way, no matter how many we have, the time won't really change, since they are happening at the same time.</p> <p>(On larger-scale applications, you would likely defend against running too many concurrent requests. The API itself might throttle you as well.)</p> <pre class="prettyprint">my $yesterday = DateTime-&gt;now-&gt;subtract( 'days' =&gt; 1 ); foreach my $country (@countries) { $cv-&gt;begin(); my $data_url = sprintf COUNTRY_URL(), uri_escape($country); http_get $data_url, delayed( sub ( $body, $hdrs ) { my $country_data; eval { $country_data = from_json($body); 1; } or do { content("Sorry, failed to fetch data for $country: $!"); $cv-&gt;end(); return; }; my @period; my $day = $yesterday; foreach ( 0 .. MAX_DAYS() ) { push @period, $day-&gt;ymd(); $day = $day-&gt;subtract( 'days' =&gt; 1 ); } my @period_data = $country_data-&gt;{'All'}{'dates'}-&gt;@{@period}; my $stat = Statistics::Descriptive::Full-&gt;new(); $stat-&gt;add_data($_) for @period_data; $country_stat = $stat-&gt;trimmed_mean( LTRIM() ); $cv-&gt;end(); }); }</pre> <p>We start by calling <code>begin</code> for each request we intend to make. When reach request ends, we call <code>end</code>. This allows the <code>condvar</code> to track how many concurrent requests we make and when we finished all of them, to call the finishing callback we created at the beginning.</p> <p>We create a proper request URL with <code>sprintf</code> and make a request for each country's data. We take the last X amount of days (using our constant <code>MAX_DAYS</code>) and calculate it from yesterday (since the data for today is not yet available until today ends).</p> <p>We use <a href="https://metacpan.org/module/Statistics::Descriptive::Full">Statistics::Descriptive::Full</a> to calculate the trimmed mean.</p> <p>So far, however, we do nothing with this calculation. What we want is to do something when all of it ends, so let's adjust this a bit.</p> <h3><a name="updating_our_finishing_callback"></a>Updating our finishing callback</h3> <p>In the beginning, we set the <code>cb</code> to just send something to the user and close the connection. Instead, we intend to now store information and display it back to the user:</p> <pre class="prettyprint">my %country_weekly; $cv-&gt;cb( delayed { content( "By country (period of " . MAX_DAYS() . " days):\n" ); content( "- $_: $country_weekly{$_}\n" ) for sort { $country_weekly{$b} &lt;=&gt; $country_weekly{$a} } keys %country_weekly; content("\nThank you for visiting our API\n"); done(); });</pre> <p>Our code that calculates the trimmed mean can now use this variable:</p> <pre class="prettyprint">my $stat = Statistics::Descriptive::Full-&gt;new(); $stat-&gt;add_data($_) for @period_data; $country_weekly{$country} = $stat-&gt;trimmed_mean( LTRIM() );</pre> <h2><a name="full_program"></a>Full program</h2> <p>The full program is:</p> <pre class="prettyprint">package CovidStats; use Dancer2; use experimental qw&lt; postderef signatures &gt;; use DateTime; use AnyEvent; use AnyEvent::HTTP; use Statistics::Descriptive::Full; use URI::Escape qw&lt; uri_escape &gt;; use constant { 'MAX_COUNTRIES' =&gt; 5, 'MAX_DAYS' =&gt; 7, 'SUMMARY_URL' =&gt; 'https://covid-api.mmediagroup.fr/v1/cases', 'COUNTRY_URL' =&gt; 'https://covid-api.mmediagroup.fr/v1/history?country=%s&amp;status=Confirmed', 'LTRIM' =&gt; '0.25', }; get '/' =&gt; sub { return delayed { flush(); my $cv = AnyEvent-&gt;condvar(); my %country_weekly; $cv-&gt;cb( delayed { content( "By country (period of " . MAX_DAYS() . " days):\n" ); content( "- $_: $country_weekly{$_}\n" ) for sort { $country_weekly{$b} &lt;=&gt; $country_weekly{$a} } keys %country_weekly; content("\nThank you for visiting our API\n"); done(); }); $cv-&gt;begin(); http_get SUMMARY_URL(), delayed( sub ( $body, $hdrs ) { my $data; eval { $data = from_json($body); 1; } or do { content("Sorry, failed to fetch data: $!"); $cv-&gt;end(); return; }; my @countries = ( sort { $data-&gt;{$b}{'All'}{'confirmed'} &lt;=&gt; $data-&gt;{$a}{'All'}{'confirmed'} } grep $_ ne 'Global', keys $data-&gt;%* )[ 0 .. MAX_COUNTRIES() ]; if (!@countries) { content("Sorry, failed to received countries\n"); $cv-&gt;end(); return; } my $yesterday = DateTime-&gt;now-&gt;subtract( 'days' =&gt; 1 ); foreach my $country (@countries) { $cv-&gt;begin(); my $data_url = sprintf COUNTRY_URL(), uri_escape($country); http_get $data_url, delayed( sub ( $body, $hdrs ) { my $country_data; eval { $country_data = from_json($body); 1; } or do { content("Sorry, failed to fetch data for $country: $!"); $cv-&gt;end(); return; }; my @period; my $day = $yesterday; foreach ( 0 .. MAX_DAYS() ) { push @period, $day-&gt;ymd(); $day = $day-&gt;subtract( 'days' =&gt; 1 ); } my @period_data = $country_data-&gt;{'All'}{'dates'}-&gt;@{@period}; my $stat = Statistics::Descriptive::Full-&gt;new(); $stat-&gt;add_data($_) for @period_data; $country_weekly{$country} = $stat-&gt;trimmed_mean( LTRIM() ); $cv-&gt;end(); }); } $cv-&gt;end(); }); }; }; 1;</pre> <h2><a name="application_runner"></a>Application runner</h2> <p>Our <code>app.psgi</code> file is simple enough:</p> <pre class="prettyprint">use CovidStats; CovidStats-&gt;to_app();</pre> <h2><a name="running"></a>Running</h2> <p>We can run this with <a href="https://metacpan.org/module/Twiggy">Twiggy</a> in the following manner:</p> <pre class="prettyprint">$ plackup -s Twiggy bin/app.psgi Twiggy: Accepting connections at http://0.0.0.0:5000/</pre> <p>Normally, <code>plackup</code> is very good at recognizing which server to use. If we didn't specify to use <a href="https://metacpan.org/module/Twiggy">Twiggy</a>, it will still get it right:</p> <pre class="prettyprint">$ plackup bin/app.psgi Twiggy: Accepting connections at http://0.0.0.0:5000/</pre> <p>Of course, on production you would set up something more elaborate instead of running this on a terminal. I suggest looking at <a href="https://metacpan.org/module/Dancer2::Manual::Deployment">Dancer2::Manual::Deployment</a> for production use.</p> <h2><a name="testing_out_our_application"></a>Testing out our application</h2> <p>On another terminal, we will run the following command:</p> <pre class="prettyprint">$ curl localhost:5000 By country (period of 7 days): - US: 16640229.25 - India: 9690261.25 - Brazil: 6294810 - France: 2118033.5 - Russia: 1854813.5 - United Kingdom: 1110655.5 Thank you for visiting our API</pre> <p>Not bad at all.</p> <h2><a name="final_notes"></a>Final notes</h2> <p>There is a lot to say here:</p> <ul> <li><a name="item_The_example"></a><b>The example</b> <p>This example is fairly contrived. The <a href="https://github.com/M-Media-Group/Covid-19-API">M-Media-Group API</a> supports retrieving the history for all countries, so this two-step process is unnecessary.</p> <p>The calculation we do is not necessarily helpful. You might come up with a better calculation that is more useful and provides more insight.</p> </li> <li><a name="item_HTML_output"></a><b>HTML output</b> <p>The output in this example is purely text and includes newlines, which is in useful in the terminal, but not for the browser. But hey, contrived example!</p> <p>For streaming output, you would want self-contained message packets, like small JSON-structured messages. That way, your clients would be able to read each separately and use it.</p> </li> </ul> <h2><a name="author"></a>Author</h2> <p>This article has been written by Sawyer X for the Perl Dancer Advent Calendar 2020.</p> <h2><a name="copyright"></a>Copyright</h2> <p>No copyright retained. Enjoy.</p> <p>Sawyer X.</p> </div> Dancer Two-Factor Authentication Demo http://advent.perldancer.org/2020/21 perl http://advent.perldancer.org/2020/21 Mon, 21 Dec 2020 00:00:00 -0000 <div class="pod-document"><h1><a name="dancer_two_factor_authentication_demo"></a>Dancer Two-Factor Authentication Demo</h1> <p>This demo shows how Two-Factor Authentication (2FA) can be implemented with Dancer2, Dancer2::Plugin::Auth::Extensible and a couple of other Perl modules.</p> <p>It also shows how Dancer2::Plugin::Auth::Extensible enhances your Dancer application with user management using very little code.</p> <h2><a name="get_the_source_code_from_github"></a>Get the source code from GitHub</h2> <pre class="prettyprint">~# git clone https://github.com/racke/dancer2-tfa-demo.git</pre> <h2><a name="start_the_app"></a>Start the app</h2> <pre class="prettyprint">~# cd dancer2-tfa-demo ~# plackup -o 127.0.0.1 bin/app.psgi HTTP::Server::PSGI: Accepting connections at http://127.0.0.1:5000/</pre> <h2><a name="login_and_setup_2fa"></a>Login and Setup 2FA</h2> <p>Go to the browser and enter the login URL <a href="http://127.0.0.1:5000/login">http://127.0.0.1:5000/login</a>.</p> <img src="/images/2020/22/2FA_login.png"/> <p>The default credentials are <i>dancer2</i> as username and <i>2fanow</i> as password. You can change or add users in the <a href="https://metacpan.org/module//Configuration">configuration file</a>.</p> <img src="/images/2020/22/2FA_demo.png"/> <p>Now use an 2FA app like Authy, Google Authenticator or FreeOTP+ to scan the QR code and confirm the token.</p> <p>Finally log out and test your token.</p> <img src="/images/2020/22/2FA_relogin.png"/> <h2><a name="two_factor_authentication"></a>Two-Factor Authentication</h2> <h3><a name="creating_the_secret"></a>Creating the secret</h3> <p>The demo application creates the secret when the user is logged in and goes to the 2FA setup page for the first time. The secret is created with <a href="https://metacpan.org/module/Data::SimplePassword">Data::SimplePassword</a>, default length is 30 characters.</p> <h3><a name="generating_image_with_qr_code"></a>Generating image with QR code</h3> <p>First we create an object which is going to generate an image with the QR code.</p> <pre class="prettyprint">my $qr = Imager::QRCode-&gt;new( size =&gt; 6, margin =&gt; 2, version =&gt; 1, level =&gt; 'M', casesensitive =&gt; 1, );</pre> <p>Now we construct the label that is going to be used by the authentication app. It consists of the fixed string and the user name in parenthesis.</p> <pre class="prettyprint">my $instance = $self-&gt;qr_code_label; my $user_link = uri_escape("$instance (" . $username . ')'); my $data; my $img = $qr-&gt;plot("otpauth://totp/$user_link?secret=" . encode_base32($secret)); $img-&gt;write(data =&gt; \$data, type =&gt; 'png');</pre> <p>We send this back with:</p> <pre class="prettyprint">$self-&gt;plugin-&gt;app-&gt;send_file (\$data, content_type =&gt; 'image/png');</pre> <h3><a name="storing_the_secret"></a>Storing the secret</h3> <p>The secret is stored on the server and in the authentication app of the user. The demo keeps the secret in memory.</p> <h2><a name="authentication"></a>Authentication</h2> <p>The authentication code is split into two modules, a role with the logic for the specific routes and a demo provider which consumes that role and takes care of the secret's generation and the "storage".</p> <h3><a name="login"></a>Login</h3> <p>We intercept the standard authentication of Dancer2::Plugin::Auth::Extensible from the Demo provider using around:</p> <pre class="prettyprint">around authenticate_user =&gt; sub { my ($orig, $self, $username, $password, @args) = @_; my $ret = $orig-&gt;($self, $username, $password, @args); return unless $ret; if ($self-&gt;check_tfa($username, $self-&gt;plugin-&gt;app-&gt;request-&gt;param('token'))) { return $ret; } else { return; } };</pre> <p>So we first call the original authenticate_user method and only if that is successful we are checking the token.</p> <p>We determine the token that is valid at the current time and compare that with the token passed by the user:</p> <pre class="prettyprint">my $expected = Authen::OATH-&gt;new-&gt;totp($secret); if ($token eq $expected) { ... } else { ... }</pre> <h2><a name="configuration"></a>Configuration</h2> <p>We are using a fixed set of credentials in the configuration file <code>config.yml</code>.</p> <pre class="prettyprint">plugins: Auth::Extensible: realms: users: provider: Demo username_key_name: user users: - user: dancer2 pass: 2fanow</pre> <h1><a name="routes"></a>Routes</h1> <p>Dancer2::Plugin::Auth::Extensible supplements the demo with routes for login and logout, so we only need a few more routes specific to 2FA.</p> <h2><a name="2fa"></a>2FA</h2> <h3><a name="get__tfa_setup_"></a>GET /tfa/setup/</h3> <p>Shows the form for 2FA setup with the QR code.</p> <h3><a name="get__tfa_qrcode_png"></a>GET /tfa/qrcode.png</h3> <p>Produces QR code.</p> <h3><a name="post__tfa_setup_"></a>POST /tfa/setup/</h3> <p>Verifies token from 2FA setup form.</p> <h2><a name="from_plugin"></a>From plugin</h2> <p>The routes for 2FA are established by the plugin, e.g.</p> <pre class="prettyprint">$app-&gt;add_route( method =&gt; 'get', regexp =&gt; '/tfa/setup', code =&gt; sub { $self-&gt;tfa_setup } );</pre> <h1><a name="use_cases"></a>Use cases</h1> <p>We are using Two-Factor Authentication for a couple of websites for ECA and an online shop in Germany.</p> <h1><a name="limitations"></a>Limitations</h1> <p>As the secrets are stored into memory, this demo should be run only as a single instance. Maybe <a href="https://metacpan.org/module/Dancer2::Plugin::Cache::CHI">Dancer2::Plugin::Cache::CHI</a> could help here.</p> <h1><a name="author"></a>Author</h1> <p>This article has been written by Stefan Hornburg (Racke) for the Perl Dancer Advent Calendar 2020.</p> </div> Dancer2::Plugin::Minion (aka, Using Minion in Dancer Apps, Revisited) http://advent.perldancer.org/2020/20 perl http://advent.perldancer.org/2020/20 Sun, 20 Dec 2020 00:00:00 -0000 <div class="pod-document"><h1><a name="dancer2__plugin__minion__aka__using_minion_in_dancer_apps__revisited_"></a>Dancer2::Plugin::Minion (aka, Using Minion in Dancer Apps, Revisited)</h1> <p>In 2018, <a href="http://advent.perldancer.org/2018/16">I wrote about my experience using Minion</a> at my employer at that time. Since then, I changed to another employer, where again, <a href="https://metacpan.org/module/Minion">Minion</a> was the appropriate solution to the issue of long-running jobs in our application. They desired to have a more Dancer-ish, polished, and integrated solution, and thanks to them, <a href="https://metacpan.org/module/Dancer2::Plugin::Minion">Dancer2::Plugin::Minion</a> was born.</p> <p>I don't see a lot of value rehashing the rationale behind this - I think this was covered pretty well in the 2018 article - so feel free to pop back there if you're looking for some backstory and analysis. With that, let's move on to...</p> <h2><a name="the_code_"></a>The Code!</h2> <p>We're going to build an image uploader that lets a child upload a picture of an item on their Christmas list to Santa. We're going to save that image to a public image directory, and then it will generate a variety of sizes of thumbnails for that image. We don't want the client to have to wait on the generation to complete (they have other things to upload to Santa after all!), so we will use Minion to generate those thumbnails in the background.</p> <p>It requires a minimal amount of configuration to set up <a href="https://metacpan.org/module/Minion">Minion</a> in your <a href="https://metacpan.org/module/Dancer2">Dancer2</a> apps:</p> <pre class="prettyprint">plugins: Minion: dsn: sqlite:test.db backend: SQLite</pre> <p>Fill in whatever values for DSN and backend match your existing Minion setup. And that's it - your <a href="https://metacpan.org/module/Dancer2">Dancer2</a> app can now talk to <a href="https://metacpan.org/module/Minion">Minion</a>!</p> <p>The plugin exports a keyword, <code>minion</code>, that exposes all of <a href="https://metacpan.org/module/Minion">Minion</a> to your <a href="https://metacpan.org/module/Dancer2">Dancer2</a> application. I'm not promising it will be as seamless as an experience as the one when you are building <a href="https://metacpan.org/module/Mojolicious">Mojolicious</a> applications, but it is really powerful.</p> <p>Two more keywords are created by the plugin, <code>add_task</code> and <code>enqueue</code>, that map directly to the same methods available in <code>Minion</code>. These tasks are common enough to warrant having their own keywords to save you the little bit of additional typing to use them.</p> <p>Lastly, the keyword <code>minion_app</code> creates a Minion application, which you can then mount via <a href="https://metacpan.org/module/Plack">Plack</a> (if you want to have an admin dashboard for Minion in your Dancer app), or if you want to enable the Minion CLI in your Dancer apps. I will demonstrate this below.</p> <p>Let's see what this looks like:</p> <pre class="prettyprint">use Dancer2; use Dancer2::Plugin::Minion; use Plack::Builder; use File::Basename 'fileparse'; add_task thumbnails =&gt; sub { my ($job, $original) = @_; require Image::Imlib2::Thumbnail; my $thumb = Image::Imlib2::Thumbnail-&gt;new; my ($base, $dir, $ext) = fileparse( $original, qr/\.[^.]*?$/ ); $_-&gt;{name} = "$base-$_-&gt;{name}" for @{ $thumb-&gt;sizes }; my @generated = $thumb-&gt;generate($original, $dir); $job-&gt;finish(\@generated); };</pre> <p>This creates a task, <code>thumbnails</code>, to automatically generate multiple thumbnails when provided with an original image.</p> <pre class="prettyprint"># This exposes all of the minion commands if (@ARGV &amp;&amp; $ARGV[0] eq 'minion') { minion_app()-&gt;start; exit 0; }</pre> <p>This bit of magic exposes the entire <a href="https://metacpan.org/module/Minion">Minion</a> CLI to your app - so you can use the various subcommands like <code>worker</code> and <code>job</code>.</p> <pre class="prettyprint">set views =&gt; '.'; get '/' =&gt; sub { template 'upload'; };</pre> <p>This sets up a simple file uploader app. It also tells <a href="https://metacpan.org/module/Dancer2">Dancer2</a> to look for its templates in the same directory as the app.</p> <pre class="prettyprint">post '/' =&gt; sub { my $file = upload('file'); my $name = $file-&gt;basename; my $target = path('public', $name); $file-&gt;copy_to($target); enqueue(thumbnails =&gt; [$target]); redirect "/$name"; };</pre> <p>Here, we consume a file upload, stash it on disk, and call the <code>thumbnails</code> job to render multiple thumbnails. This might take a while, so we don't want to tie up the browser waiting for them to finish.</p> <pre class="prettyprint">builder { # mount the container app at /dashboard/ # note that the trailing slash is very important mount '/dashboard/' =&gt; minion_app( 'https://northpole.com/' )-&gt;start; mount '/' =&gt; start; };</pre> <p>Finally, we create the <a href="https://metacpan.org/module/Plack">Plack</a> application by mounting our <a href="https://metacpan.org/module/Dancer2">Dancer2</a> application to the root URL, and attaching the <a href="https://metacpan.org/module/Minion">Minion</a> dashboard to the <code>/dashboard/</code> URL (the trailing <code>/</code> is required!). It also sets the "Back to Site" link on the dashboard to the North Pole website (<code>https://northpole.com/</code>).</p> <h2><a name="now__let_s_put_it_all_together"></a>Now, Let's Put it All Together</h2> <p>Here's everything you need to make this example work (put all files in the same directory):</p> <pre class="prettyprint"># Save this as app.psgi use Dancer2; use Dancer2::Plugin::Minion; use Plack::Builder; use File::Basename 'fileparse'; add_task thumbnails =&gt; sub { my ($job, $original) = @_; require Image::Imlib2::Thumbnail; my $thumb = Image::Imlib2::Thumbnail-&gt;new; my ($base, $dir, $ext) = fileparse( $original, qr/\.[^.]*?$/ ); $_-&gt;{name} = "$base-$_-&gt;{name}" for @{ $thumb-&gt;sizes }; my @generated = $thumb-&gt;generate($original, $dir); $job-&gt;finish(\@generated); }; # This exposes all of the minion commands if (@ARGV &amp;&amp; $ARGV[0] eq 'minion') { minion_app()-&gt;start; exit 0; } set views =&gt; '.'; get '/' =&gt; sub { template 'upload'; }; post '/' =&gt; sub { my $file = upload('file'); my $name = $file-&gt;basename; my $target = path('public', $name); $file-&gt;copy_to($target); enqueue(thumbnails =&gt; [$target]); redirect "/$name"; }; builder { # mount the container app at /dashboard/ # note that the trailing slash is very important mount '/dashboard/' =&gt; minion_app( 'https://northpole.com/' )-&gt;start; mount '/' =&gt; start; }; # Save as cpanfile requires 'Dancer2'; requires 'Dancer2::Plugin::Minion'; requires 'Image::Imlib2::Thumbnail'; # Save as config.yml plugins: Minion: dsn: sqlite:test.db backend: SQLite # Save as upload.tt &lt;html&gt; &lt;head&gt; &lt;title&gt;Upload your wish pictures&lt;/title&gt; &lt;/head&gt; &lt;body&gt; &lt;h1&gt;Upload your wish pictures&lt;/h1&gt; &lt;form action="/" enctype="multipart/form-data" method="post"&gt; &lt;input type="file" name="file"&gt; &lt;input type="submit"&gt; &lt;/form&gt; &lt;/body&gt; &lt;/html&gt;</pre> <p>To run the web app:</p> <pre class="prettyprint">plackup app.psgi</pre> <p>And finally, to run the <a href="https://metacpan.org/module/Minion">Minion</a> worker:</p> <pre class="prettyprint">perl app.psgi minion worker</pre> <p>And remember, you can also run other <a href="https://metacpan.org/module/Minion">Minion</a> commands this way:</p> <pre class="prettyprint">perl app.psgi minion job perl app.psgi minion job -b kill ...</pre> <p>You can see a sample of the app running:</p> <img src="/images/2020/20/file-upload.png"> <p>And the dashboard, too:</p> <img src="/images/2020/20/minion-dashboard.png"> <h2><a name="future_plans"></a>Future Plans</h2> <p>To be honest, I don't know what the future holds for this module. My own uses of it have been pretty minimal compared to the potential of what you can use it for. In my mind, this leaves the future a pretty blank slate. Is there something you'd like to see? I'd love to hear from you! Reach out at <code>cromedome at cpan dot org</code> and let me know your thoughts and ideas for this plugin.</p> <h2><a name="giving_credit_where_credit_is_due"></a>Giving Credit Where Credit is Due</h2> <p><a href="https://metacpan.org/module/Dancer2::Plugin::Minion">Dancer2::Plugin::Minion</a> saw the light of day thanks to the wonderful people at <a href="https://clearbuilt.com">Clearbuilt</a>. They are the nicest group of people you could ever hope to work for/with, and I am extremely grateful for them giving me the time to not only build this module out, but so much more.</p> <h2><a name="other_notes"></a>Other Notes</h2> <p>There is no plugin for Dancer 1 at the time of this writing, nor do I expect there will ever be one, at least not of my doing.</p> <h2><a name="author"></a>Author</h2> <p>This article has been written by Jason Crome (CromeDome) (with <b>much</b> assistance from Joel Berger) for the Twelve Days of Dancer.</p> <h2><a name="copyright"></a>Copyright</h2> <p>No copyright retained. Enjoy.</p> <p>Jason A. Crome / CromeDome</p> </div> Making a one-page app using Dancer2::Plugin::Ajax http://advent.perldancer.org/2020/19 perl http://advent.perldancer.org/2020/19 Sat, 19 Dec 2020 00:00:00 -0000 <div class="pod-document"><h1><a name="making_a_one_page_app_using_dancer2__plugin__ajax"></a>Making a one-page app using Dancer2::Plugin::Ajax</h1> <p>It's possible to make bits of DOM on a Dancer2 template behave like a one-page application, auto-updating themselves using some clever JavaScript and <a href="https://metacpan.org/module/Dancer2::Plugin::Ajax">Dancer2::Plugin::Ajax</a>. In this article, I'll give you a quick runthrough of how to do that.</p> <h2><a name="the_example__messaging"></a>The example: messaging</h2> <p>In this example, I want the app to check every few minutes for messages coming from some other system. I'll have Dancer2 look for the actual messages, and the frontend of the app will call a route using <code>.ajax</code>, and if there's a message, update some DOM. I'll be using jQuery for this, though other tookits have similar sorts of constructs.</p> <p>Here's the HTML needed for the message block:</p> <pre class="prettyprint">&lt;div id='messageBlock' style='min-height: 50px; padding:10px; margin: 20px; border: 1px solid black'&gt; &lt;h2&gt;Messages from Beyond:&lt;/h2&gt; &lt;div id='messageResults'&gt;&lt;/div&gt; &lt;!-- Nothing in this div. Yet. --&gt; &lt;/div&gt;</pre> <p>And here's the route we'll be calling. In this example, I'm randomly generating the messages, but in a real app, you might be fetching them from a message box, a file, a database, or whatever. Because it's what I'm used to, I set the application-type in my config.yml to <code>'application/json'</code>, but this can do xml, or whatever you need; just make sure that's what you're returning!</p> <pre class="prettyprint">use Dancer2::Plugin::Ajax; ajax '/messages' =&gt; sub { my @messages = ( 'Boo! I betcha I scared you, ha-ha!', 'A girl has no name, but she still has a list.', 'Space, the final frontier.', '', '&lt;h1&gt;Really big text&lt;/h1&gt;', ); send_as JSON =&gt; { message =&gt; @messages[int(rand(5))] }; };</pre> <p>Now, here's where the wizardry comes in. Call a function that repeats an AJAX request every ten seconds, and on success, update the DOM in that empty div we created for these messages. If you've got the style right, the larger div with the border box will even enlarge and shrink in height as needed.</p> <pre class="prettyprint">$(document).ready( function() { var timer = setInterval( function() { $.ajax({ url: '/messages', type: 'GET', success: function(data) { // Update the DOM $('#messageResults').html(data.message); }, error: function(event, stat, error) { // throw a message into the browser's developer console var str = 'Request failed. Status: ' + stat + ' Error: ' + error; console.log(str); }, }); }, 10000); // in milliseconds. });</pre> <h2><a name="nice__but_what_can_you_do_with_it"></a>Nice, but what can you do with it?</h2> <p>Whatever you like, really; some obvious things for this little example would be to append the messages to a list until dismissed, or css treachery to make them fade in and out. On the Perl/Dancer2 side, you could have the route check for some event to happen--an okay from a manager, some outside processing finishing, you-name-it, and then letting the user know the success or failure. You could also cause growl or other notification blocks to appear as needed with a pattern like this; just change the JavaScript to do what you need.</p> <p>We've used patterns like this for auto-updating blocks of HTML based on the results of a request in a few places at my current job, and I'm using it on a pet project I'm tinkering with. It's easy to use and maintain, and works like a dream!</p> <h2><a name="author"></a>Author</h2> <p>This article has been written by D Ruth Holloway (GeekRuthie) for the Perl Dancer Advent Calendar 2020.</p> <h2><a name="copyright__amp__license"></a>Copyright &amp; License</h2> <p>Copyright (C) 2020 by D Ruth Holloway (GeekRuthie). This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>.</p> </div> Post-processing HTML with Mojo::DOM http://advent.perldancer.org/2020/18 perl http://advent.perldancer.org/2020/18 Fri, 18 Dec 2020 00:00:00 -0000 <div class="pod-document"><h1><a name="post_processing_html_with_mojo__dom"></a>Post-processing HTML with Mojo::DOM</h1> <p>Santa's elves were frantic! The big man had just noticed that the North Pole's website had links that didn't protect their users. They used <code>http://</code> rather than <code>https://</code> and that was very naughty. Santa wanted it fixed and he wanted it now!</p> <p>Some elves thought the best thing to do was to just sift through the site and find the naughty urls and fix them but others were worried it would take too long and Christmas would be ruined. Finally one proposed that they should just dynamically fix up any urls on the page right before sending it to the client.</p> <p>"It's a piece of cake," he said, "just use an 'after' hook". The other elves were still worried. "How do we know what to fix? After all we can't <a href="https://stackoverflow.com/a/1732454/468327">use a regex</a>," they complained. "Of course not, we can use a DOM parser, like <a href="https://metacpan.org/module/Mojo::DOM">Mojo::DOM</a>, even though the site itself uses <a href="https://metacpan.org/module/Dancer2">Dancer2</a>," he replied.</p> <p>He added the following bit of code to the app,</p> <pre class="prettyprint">hook after =&gt; sub { require Mojo::DOM; my $dom = Mojo::DOM-&gt;new(response-&gt;content); $dom-&gt;find('a[href^="http:"]') -&gt;each(sub{ $_-&gt;{href} =~ s/^http/https/ }); response-&gt;content("$dom"); };</pre> <p>"Now every time it renders a page, it will find anchor tags whose href attribute contains an insecure url starting with <code>http</code> and fix it! Here let's check. The welcome page looks something like this,"</p> <pre class="prettyprint">&lt;html&gt; &lt;head&gt; &lt;title&gt;Welcome to Santa's Workshop Online!&lt;/title&gt; &lt;/head&gt; &lt;body&gt; &lt;h1&gt;Welcome to Santa's Workshop Online!&lt;/h1&gt; This is the place to &lt;a id="check" href="/check"&gt;your naughty/nice status&lt;/a&gt; or send me &lt;a id="list" href="/list"&gt;your list&lt;/a&gt;. If you would like to request some of Mrs. Claus' famous homemade candies, please visit her at &lt;a id="mrsclaus" href="http://mrsclaus.example.org"&gt;her site&lt;/a&gt;. ... &lt;/body&gt; &lt;/html&gt;</pre> <p>"... and we can see that it works by writing some tests. We can use <a href="https://metacpan.org/module/Test::Mojo">Test::Mojo</a> since it <a href="http://advent.perldancer.org/2018/20">works with Dancer2 also</a>."</p> <pre class="prettyprint">use Mojo::Base -strict; use Test::More; use Test::Mojo; my $t = Test::Mojo-&gt;with_roles('+PSGI')-&gt;new('app.psgi'); $t-&gt;get_ok('/') -&gt;status_is(200) -&gt;text_like('html body h1' =&gt; qr|welcome|i) -&gt;attr_is('#list' =&gt; href =&gt; '/list') -&gt;attr_is('#check' =&gt; href =&gt; '/check') -&gt;attr_like('#mrsclaus' =&gt; href =&gt; qr|^https:|) -&gt;element_exists_not('[href^="http:"]'); done_testing;</pre> <p>They were careful to check that known urls were rewriten correctly while not accidentally changing others that shouldn't, like relative internal urls. Everyone was excited and the youngest elf, a spry 500 year old named Olaf, was about to slip something a little more festive into the eggnog when the big red and white striped phone rang!</p> <p>It was Santa again. He had decided that since 2020 had been a hard year for everyone, he wanted to wish everyone a happier 2021, and he wanted to do it in the footer on every page.</p> <p>The elves got back to work. Luckily they already had their HTML post-processor in place, they just needed to add another rule. They moved the url tranform to a subroutine named https and stubbed in a new one called footer,</p> <pre class="prettyprint">hook after =&gt; sub { require Mojo::DOM; my $dom = Mojo::DOM-&gt;new(response-&gt;content); https($dom); footer($dom); response-&gt;content("$dom"); };</pre> <p>but what should it do?</p> <p>Quickly the elves realized that this was going to be more complicated because some pages, like the welcome, didn't have a footer, but others, like the toyshop page already did.</p> <pre class="prettyprint">&lt;html&gt; &lt;head&gt; &lt;title&gt;Preview Next Year's Toys!&lt;/title&gt; &lt;/head&gt; &lt;body&gt; &lt;h1&gt;Preview Next Year's Toys!&lt;/h1&gt; &lt;ul&gt;&lt;li&gt;...&lt;/li&gt;&lt;/ul&gt; &lt;footer&gt; &lt;p&gt;Made with love by Elves!&lt;/p&gt; &lt;/footer&gt; &lt;/body&gt; &lt;/html&gt;</pre> <p>Still the solution wasn't hard, they'd simply try to find a footer tag and if they didn't get one, they'd add it as the last element in the <code>&lt;body&gt;</code>.</p> <pre class="prettyprint">sub footer { my $dom = shift; my $footer = $dom-&gt;at('body footer'); # no footer found, build one just before &lt;/body&gt; unless ($footer) { $dom-&gt;at('body')-&gt;append_content($dom-&gt;new_tag('footer')); $footer = $dom-&gt;at('body footer'); } my $wish = $dom-&gt;new_tag(p =&gt; id =&gt; 'wish' =&gt; 'Wishing you a happier 2021!'); $footer-&gt;append_content($wish); }</pre> <p>Now when they wrote the tests they had to be a little careful, when they create the footer, they can check that the footer is the last element in the body, but when they don't they can't be sure, so they make the test a little more generic.</p> <pre class="prettyprint">use Mojo::Base -strict; use Test::More; use Test::Mojo; my $t = Test::Mojo-&gt;with_roles('+PSGI')-&gt;new('app.psgi'); $t-&gt;get_ok('/') -&gt;status_is(200) -&gt;text_is('body &gt; footer:last-child &gt; p#wish:last-child', 'Wishing you a happier 2021!'); $t-&gt;get_ok('/toyshop') -&gt;status_is(200) -&gt;text_is('body footer &gt; p#wish:last-child', 'Wishing you a happier 2021!'); done_testing;</pre> <p>In either case, using Test::Mojo and CSS selectors made writing otherwise complex tests a breeze.</p> <p>Soon one of the senior elves addressed the crowd. "I just heard back from Santa," he said, "and he's feeling much more jolly. Great work everyone!" Just then some movement in the back of the room caught his eye. "Hey what's Olaf doing to that eggnog?!"</p> <p>---</p> <p>See entire finished app at <a href="https://gist.github.com/jberger/18990fc4d2197ce1ce61edb88e2ed6fa">https://gist.github.com/jberger/18990fc4d2197ce1ce61edb88e2ed6fa</a>.</p> </div> Dancer2::Template::Handlebars http://advent.perldancer.org/2020/17 perl http://advent.perldancer.org/2020/17 Thu, 17 Dec 2020 00:00:00 -0000 <div class="pod-document"><h1><a name="dancer2__template__handlebars"></a>Dancer2::Template::Handlebars</h1> <p>I know that our editor-in-chief this year, the amazing <a href="https://metacpan.org/author/CROMEDOME">cromedome</a>, is hoping for some deep wisdom here about why and how I created <a href="https://metacpan.org/module/Dancer2::Template::Handlebars">Dancer2::Template::Handlebars</a>.</p> <p>He's going to have to get used to disappointment.</p> <p>I realized a while back that, after nearly 20 years of writing Perl, I didn't have a single module on CPAN. It seemed to me that something simple-ish would be a good idea, just so I could work out the workflow, and I'd been playing around with some templating using <a href="https://handlebarsjs.com/">Handlebars</a> for another project of mine. To make it even easier, the underling <a href="https://metacpan.org/module/Text::Handlebars">Text::Handlebars</a> was already present, and our clever colleague <a href="https://metacpan.org/author/YANICK">Yanick</a> had already written <a href="https://metacpan.org/module/Dancer::Template::Handlebars">Dancer::Template::Handlebars</a>, from which I could crib some code as needed.</p> <p>All the bits were in place, it was just up to me to learn the bits and pieces of creating, testing, and submitting a module. But why this one?</p> <h2><a name="why_you_should_like_handlebars__and_use_it"></a>Why you should like Handlebars, and use it</h2> <p>Handlebars, like Template Toolkit and many others, can take objects handed to it from Dancer2's template-processing system, and insert data into pages, at your direction. So, if you have a list of people objects with names, you can get a list of them:</p> <pre class="prettyprint">{{#each people}} {{ lastname }}, {{ firstname }} {{/each}}</pre> <p>Handlebars also supports "partials", subtemplates you might want to call, similar in behavior to TT's <code>INCLUDE</code> or <code>PROCESS</code> tools. You can even dynamically call a partial by referencing a variable name containing the name of the partial!</p> <p>What sets Handlebars apart from Template Toolkit is its model of "helpers." There are a few built-in helpers, like <code>#each</code> and <code>#with</code>, which create loop iterators, <code>#if</code> and <code>#unless</code> that create conditionals, and a <code>lookup</code> <code>log</code> helper, which provide data access and logging, respectively. But everything else you might want to do is a helper you must include or write. Writing them is relatively straightforward JavaScript, like so:</p> <pre class="prettyprint">Handlebars.registerHelper('loud', function (aString) { return aString.toUpperCase() })</pre> <p>Then in your template,</p> <pre class="prettyprint">{{loud lastname }}, {{ firstname }}</pre> <p>And for me, you'd get HOLLOWAY, Ruth.</p> <p>By default, the "double-stache" construct used in Handlebars templates will escape your HTML, which may not be what you want. If you have a bit of HTML your Dancer application is generating, or storing in a database, you can tell Handlebars to skip the escaping with a "triple-stache": <code>{{{ generated_html_field }}}</code>.</p> <p>For me, at least, the aesthetics of Handlebars are clean and easy to read, and the helper format creates a nice easy-to-read layout. It's lightweight, and easy to read and code. Is it the next "best thing since sliced bread?" Not at all. But it's a good templating language, that some developers might find useful.</p> <h2><a name="so__how_did_i_turn_that_into_a_plugin_for_dancer2"></a>So, how did I turn that into a plugin for Dancer2?</h2> <p>As I mentioned before, Yanick had already done a Dancer plugin for Handlebars. He had also created <a href="https://metacpan.org/module/Dancer::Template::Mustache">Dancer::Template::Mustache</a>, and ported that to <a href="https://metacpan.org/module/Dancer2::Template::Mustache">Dancer2::Template::Mustache</a>, so I had a template to go by.</p> <p>I mercilessly plagarized. That's how I did it. I even told Yanick that I was plagarizing, and he just encouraged me. Then I used <a href="https://metacpan.org/module/Dist::Zilla">Dist::Zilla</a> to get it ready and release it.</p> <p>If you've got a Dancer plugin that you'd like to see ported to Dancer2, I strongly recommend this trick; take a look at a similar module where someone has already done that migration, and go and do likewise. It's not hard!</p> <h2><a name="author"></a>Author</h2> <p>This article has been written by D Ruth Holloway (GeekRuthie) for the Perl Dancer Advent Calendar 2020.</p> <h2><a name="copyright__amp__license"></a>Copyright &amp; License</h2> <p>Copyright (C) 2020 by D Ruth Holloway (GeekRuthie). This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>.</p> </div> Typed Route Parameters in Dancer2 http://advent.perldancer.org/2020/16 perl http://advent.perldancer.org/2020/16 Wed, 16 Dec 2020 00:00:00 -0000 <div class="pod-document"><h1><a name="typed_route_parameters_in_dancer2"></a>Typed Route Parameters in Dancer2</h1> <p>Dancer2 version 0.300000 introduced typed route parameters, allowing you to use <a href="https://metacpan.org/module/Type::Tiny">Type::Tiny</a> type constraints with your named route parameters.</p> <h2><a name="quick_intro"></a>Quick Intro</h2> <p>The default type library is the one shipped with Dancer2: <a href="https://metacpan.org/module/Dancer2::Core::Types">Dancer2::Core::Types</a>. This extends <a href="https://metacpan.org/module/Types::Standard">Types::Standard</a> with a small number of extra types, allowing simple type constraints like the following:</p> <pre class="prettyprint">get '/user/:id[Int]' =&gt; sub { # matches /user/34 but not /user/jamesdean my $user_id = route_parameters-&gt;get('id'); }; get '/user/:username[Str]' =&gt; sub { # matches /user/jamesdean but not /user/34 since that is caught # by previous route my $username = route_parameters-&gt;get('username'); };</pre> <p>You can even use type constraints to add a regexp check:</p> <pre class="prettyprint">get '/book/:date[StrMatch[qr{\d\d\d\d-\d\d-\d\d}]]' =&gt; sub { # matches /book/2014-02-04 my $date = route_parameters-&gt;get('date'); };</pre> <p>Constraints can be combined in the normal <a href="https://metacpan.org/module/Type::Tiny">Type::Tiny</a> way, such as:</p> <pre class="prettyprint">get '/some/:thing[Int|[StrMatch[qr{\d\d\d\d-\d\d-\d\d}]]' =&gt; sub { # matches Int like /some/234 and dates like /some/2020-12-08 my $thing = route_parameters-&gt;get('thing'); };</pre> <h2><a name="using_your_own_type_library"></a>Using Your Own Type Library</h2> <p>To access the full power of typed route parameters you will probably want to create your own custom types. Assuming your main app class is <code>MyApp</code>, then you might want to create <code>MyApp::Types</code> to hold your type library. For example:</p> <pre class="prettyprint">package MyApp::Types; # import Type::Library and declare our exported types use Type::Library -base, -declare =&gt; qw( IsoDate ItemAction Slug Username ); # import sugar from Type::Utils use Type::Utils -all; # here we import all of the type libraries whose types we want to include # in our library BEGIN { extends qw( Dancer2::Core::Types Types::Common::Numeric Types::Common::String ); } # An ISO date. # simplified example, you'd probably want something with better validation declare IsoDate, as NonEmptySimpleStr, where { $_ =~ /^(\d+)-(\d{2})-(\d{2})$/ }; # Enums are a nice way to constrain to a set of values enum ItemAction =&gt; [qw/ comsume drop equip repair unequip /]; # Valid slug: lowercase alphanumeric with hyphens declare Slug, as NonEmptySimpleStr, where { $_ =~ /^[a-z0-9-]*$/ }; # If usernames are length-constrained then is better than Str declare Username, as NonEmptySimpleStr, where { length($_) &gt; 6 &amp;&amp; length ($_) &lt; 50 }; 1;</pre> <p>You then need to add the following line to your <code>config.yml</code> so that Dancer2 knows which type library to use for typed parameters:</p> <pre class="prettyprint">type_library: MyApp::Types</pre> <p>Now you're ready to use your type checks in your route definitions:</p> <pre class="prettyprint">package MyApp; use Dancer2; get '/user/:id[PositiveInt]' =&gt; sub { # PositiveInt imported from Types::Common::Numeric gives us a better # check than simple Int my $user_id = route_parameters-&gt;get('id'); }; get '/user/:username[Username]' =&gt; sub { my $username = route_parameters-&gt;get('username'); }; get '/book/:date[IsoDate]' =&gt; sub { my $date = route_parameters-&gt;get('date'); }; get '/item/:action[ItemAction]/:item[Slug|PositiveInt]' =&gt; sub { # action constrained by enum # item by its symbolic slug, or its integer ID my $action = route_parameters-&gt;get('action'); my $item = route_parameters-&gt;get('item'); }; true;</pre> <h2><a name="using_other_type_libraries"></a>Using Other Type Libraries</h2> <p>You can always import other type libraries into your own library, as per the example in the previous example, but if you just want to use a type once you might not want to do that. In this case you can simply include the type library in the type definition of the route parameter:</p> <pre class="prettyprint">get '/user/:username[My::Type::Library::Username]' =&gt; sub { my $username = route_parameters-&gt;get('username'); };</pre> <h2><a name="need_typed_query_or_body_parameters"></a>Need Typed Query or Body Parameters?</h2> <p>For now core Dancer2 doesn't support this, but if you need it, then have a look at SawyerX's excellent <a href="https://metacpan.org/module/Dancer2::Plugin::ParamTypes">Dancer2::Plugin::ParamTypes</a>.</p> <h2><a name="author"></a>Author</h2> <p>This article has been written by Peter Mottram (SysPete) for the Twelve Days of Dancer 2020.</p> <h2><a name="copyright"></a>Copyright</h2> <p>No copyright retained. Enjoy, and keep Dancing!</p> </div> What's a Quarren? http://advent.perldancer.org/2020/15 perl http://advent.perldancer.org/2020/15 Tue, 15 Dec 2020 00:00:00 -0000 <div class="pod-document"><h1><a name="what_s_a_quarren"></a>What's a Quarren?</h1> <p>It is almost a rite of passage, it seems, and not just in Perl, but in any development ecosystem...a developer reaches a certain point in their career where they think they might have a better idea for a better CMS, so they write one.</p> <p>To be fair, some of these developer mid-life crises come earlier or later in the career. In my case, well, I've been at this for 30 years, so it's about time, I suppose. Some are built because of a specific itch that the developer wants to scratch, and for more than a few, "because I can" seems to be sufficient reason. For me, it's a little bit of both reasons.</p> <p>I have managed several web sites in the course of my career, from a single-page static, to big multi-page statics, to data-driven dynamic content. For each, it seems, you need a different sort of CMS.</p> <p>That irritated me, but for the longest time, I knew I didn't have the tools I needed to do the job I wanted, or the time to flesh it out. My current job gave me most all of the former; we use <a href="https://metacpan.org/module/Dancer2">Dancer2</a> for all of our sites, with <a href="https://metacpan.org/module/DBIx::Class">DBIx::Class</a> under the hood talking to databases.</p> <p>Quarren was born during the early days of the 2020 pandemic; my husband and I couldn't get out much, and I had some free time to scratch down some ideas--and the name? Well, this CMS was born in the Quarren-tine, of course.</p> <h2><a name="whence_quarren"></a>Whence Quarren?</h2> <p>Most of the very popular CMSes out there have one or more problems that make it unsuitable for all of the use cases I have. Either they are too big a hammer (a static site with Joomla?), too small a hammer (more than very simple CSV-based data on Jekyll) or the wrong hammer altogether (try setting up search on a Hugo site. No, really, I'll wait.)</p> <p>That's not to say that you can't shoehorn some of the use cases into a functional site. You can, for instance, set up a static site in WordPress, but then you have all kinds of lovely features that you don't need right at hand, which is a bit of a distraction. I want a CMS where you have the features you need for the site you're working on close to hand, but none of the features that you don't need.</p> <p>Now, lest you think I'm knocking WordPress, I'm not; it's a fine system, as are Joomla, Jekyll, Hugo, Drupal, and a host of other systems. But they're not the hammer I wanted for the nails I need to drive; in the case of WordPress, one of the side effects of its plugin ecosystem is that there are a lot of really skeevy operators out there writing plugins for pay. If you're lucky, you'll get one that merely spams your email box after you by their plugin. If your luck isn't all that good, well. It's an ugly world out there, and I didn't want to play their games.</p> <h2><a name="whereby_quarren"></a>Whereby Quarren?</h2> <p>My goals for the project are</p> <ul> <li><a name="item_One_CMS_for_everything_I_run__instead_of_the_current_three_"></a><b>One CMS for everything I run, instead of the current three.</b> </li> <li><a name="item_One_copy_of_the_code_running_per_OS_install_"></a><b>One copy of the code running per OS install.</b> </li> <li><a name="item_I_don_t_want_to_deal_with_skeevy_paid_plugin_shops_"></a><b>I don't want to deal with skeevy paid-plugin shops.</b> </li> <li><a name="item_Everything_available_on_a_relatively_easy_to_use_deployment_ecosystem__CPAN_apt_yum_"></a><b>Everything available on a relatively-easy-to-use deployment ecosystem (CPAN/apt/yum)</b> </li> <li><a name="item_Mostly_DB_agnostic__from_sqlite_up_to_Oracle__if_possible_"></a><b>Mostly DB-agnostic--from sqlite up to Oracle, if possible.</b> </li> <li><a name="item_Mostly_search_tool_agnostic__deploy_what_works_for_you__or_don_t_deploy_one_at_all_"></a><b>Mostly search-tool agnostic--deploy what works for you, or don't deploy one at all.</b> </li> <li><a name="item_I_don_t_want_to_see_features_I_m_not_using_in_the_admin_UI"></a><b>I don't want to see features I'm not using in the admin UI</b> </li> <li><a name="item_Code_I_can_be_proud_to_give_back_to_the_community_I_love"></a><b>Code I can be proud to give back to the community I love</b> </li> </ul> <h3><a name="the_core_of_quarren"></a>The "Core" of Quarren</h3> <p>The core of the system includes pages, users, and the plugin infrastructure to find and utilize plugins. Literally everything else is a plugin. A default renderer and theme is provided, to "pass through" raw HTML from a page, and a single default permalink system ("slug") is included in the core. With those bits in place, and no plugins, you can hand-hack HTML into a group of pages, and the system will properly render them. I'm working on a very basic admin UI that would let you do just that, and for a single-page site, that may even be all that you need. By default, Dancer is using CHI caching for pages, and for some system parameters, to provide snappy response time.</p> <p>...but you don't want just that, do you? Of course not. I don't either. The plugin infrastructure will automatically detect any plugins you have loaded on the system, and give the user options to turn them on or off. Each plugin may add additional renderers, taxonomy tools, editors, search tools, you name it. Some plugin namespaces I've already identified include:</p> <ul> <li><a name="item_Core"></a><b>Core</b> </li> <li><a name="item_PageRender"></a><b>PageRender</b> </li> <li><a name="item_UploadRender"></a><b>UploadRender</b> </li> <li><a name="item_DataRender"></a><b>DataRender</b> </li> <li><a name="item_Shortcode"></a><b>Shortcode</b> </li> <li><a name="item_Theme"></a><b>Theme</b> </li> <li><a name="item_Cron"></a><b>Cron</b> </li> <li><a name="item_Search"></a><b>Search</b> </li> </ul> <h3><a name="so__how_s_it_going__ruthie"></a>So, how's it going, Ruthie?</h3> <p>Not as well as I'd hoped for, by now. Life, as we all know, gets in the way. But the code is available on <a href="https://gitlab.com/GeekRuthie/quarren-cms">GitLab</a>, with an initial theme plugin <a href="https://gitlab.com/GeekRuthie/quarren-plugin-theme-barepages">here</a>. The main branch on the repo doesn't have an admin UI yet, but the code does function, if you're willing to force-feed your database a page or two. It's still very proof-of-concept, at this point.</p> <p>I have a couple of routes already set up and working. <code>get '/*'</code> will get a page by database ID, if present, or the default page specified in parameters, or lowest-id page in the database, if not, and pass it on to the chosen renderer. <code>get '/**'</code> calls up the permalink method defined in parameters, or the default of "slug", and finds and renders the page based on its defined renderer. Both of these routes are very short, less than 25 lines of code, yet they provide the basis for the entire non-admin side of the system.</p> <h3><a name="but_what_about_taxonomies"></a>But what about taxonomies?</h3> <p>A good example of how the plugin system will alter the basic behavior of those two routes is the Taxonomy plugin. A URL like <code>/author/ruthie</code> would be picked up by the <code>get '/**'</code> handler. The Core plugin Quarren::Plugin::Core::Taxonomy will have a Dancer2 <code>before</code> hook to see if the first term (<code>author</code>) is a known taxonomy, and if so, deal with it from there, passing on a Page schema object to the route using <code>vars</code>. If it's not a known taxonomy, or the taxonomy value is unknown, the hook will fall through to the regular route.</p> <h2><a name="whither_quarren"></a>Whither Quarren?</h2> <h3><a name="the_1_0_roadmap"></a>The 1.0 Roadmap</h3> <p>As of 1.0, you've got a minimally-useful system for a hard-coded HTML single-page site.</p> <ul> <li><a name="item_Remove_hard_coded_schema__go_dynamic__since_some_plugins_may_alter_the_schema"></a><b>Remove hard-coded schema, go dynamic, since some plugins may alter the schema</b> </li> <li><a name="item_Core_admin_UI"></a><b>Core admin UI</b> </li> <li><a name="item_Move_default_renderer_and_theme_out_of_separate_repos__and_ship_them_with_the_core"></a><b>Move default renderer and theme out of separate repos, and ship them with the core</b> </li> <li><a name="item_Tests"></a><b>Tests</b> </li> <li><a name="item_Documentation"></a><b>Documentation</b> </li> </ul> <h3><a name="the_1_02_roadmap"></a>The 1.02 Roadmap</h3> <p>At this point, you're starting to see the capabilities for a simple blogging engine.</p> <ul> <li><a name="item_More_permalink_methods__date__date_slug__etc__in_core"></a><b>More permalink methods (date, date/slug, etc) in core</b> </li> <li><a name="item_Quarren__Plugin__Core__Menu__for_adding_menus_to_theme_plugins"></a><b>Quarren::Plugin::Core::Menu, for adding menus to theme plugins</b> </li> <li><a name="item_Quarren__Plugin__Core__Taxonomy__for_adding_taxonomy_management"></a><b>Quarren::Plugin::Core::Taxonomy, for adding taxonomy management</b> </li> <li><a name="item_Quarren__Plugin__PageRender__Markdown__for_Markdown_in_the_DB"></a><b>Quarren::Plugin::PageRender::Markdown, for Markdown in the DB</b> </li> <li><a name="item_Quarren__Plugin__PageRender__File__Markdown__for_Markdown_on_disk"></a><b>Quarren::Plugin::PageRender::File::Markdown, for Markdown on disk</b> </li> <li><a name="item_Quarren__Plugin__Search___lt_something_gt___a_search_plugin"></a><b>Quarren::Plugin::Search::&lt;something&gt;, a search plugin</b> </li> <li><a name="item_Quarren__Plugin__Cron_infrastructure__and_Quarren__Plugin__Cron__Search__to_update_indexes"></a><b>Quarren::Plugin::Cron infrastructure, and Quarren::Plugin::Cron::Search, to update indexes</b> </li> <li><a name="item_At_least_one_more_theme_plugin"></a><b>At least one more theme plugin</b> </li> </ul> <h3><a name="the_1_04_roadmap"></a>The 1.04 Roadmap</h3> <p>In 1.04, features get added that let the user create simple data-driven pages, as well as more complicated themes and blogs.</p> <ul> <li><a name="item_Quarren__Plugin__Core__CustomFields__to_add_custom_fields_to_a_page_post"></a><b>Quarren::Plugin::Core::CustomFields, to add custom fields to a page/post</b> </li> <li><a name="item_Quarren__Plugin__Core__CustomCSS__so_users_can_customize_themes"></a><b>Quarren::Plugin::Core::CustomCSS, so users can customize themes</b> </li> <li><a name="item_Quarren__Plugin__DataRender__CSV__use_CSV_data_to_populate_a_page_"></a><b>Quarren::Plugin::DataRender::CSV (use CSV data to populate a page)</b> </li> <li><a name="item_Quarren__Plugin__Core__Upload"></a><b>Quarren::Plugin::Core::Upload</b> </li> <li><a name="item_Quarren__Plugin__Core__Shortcode__Gallery"></a><b>Quarren::Plugin::Core::Shortcode::Gallery</b> </li> <li><a name="item_At_least_one_more_theme"></a><b>At least one more theme</b> </li> </ul> <h3><a name="after_that"></a>After that?</h3> <p>The sky's the limit. Most everything that would need to happen after that is plugins, just waiting to be written. By 1.04, most of the plugin APIs should be pretty well-defined, and ready for folks to use. Releases for the core will only be needed for Core API changes, or changes to the Core UI.</p> <p>Here's some idea fodder:</p> <ul> <li><a name="item_Simple_analytic_plugin"></a><b>Simple analytic plugin</b> </li> <li><a name="item_Plugins_for_e_commerce"></a><b>Plugins for e-commerce</b> </li> <li><a name="item_Comments_system_and_or_plugins_to_interface_with_other_systems_"></a><b>Comments system and/or plugins to interface with other systems.</b> </li> <li><a name="item_CSV_or_RSS_ingestion_plugin_bulk_article_data"></a><b>CSV or RSS ingestion plugin bulk article data</b> </li> <li><a name="item_Cookie_handler_plugin"></a><b>Cookie-handler plugin</b> </li> </ul> <h2><a name="interested_in_helping"></a>Interested in helping?</h2> <p>I'd love to hear your thoughts. Open an issue on GitLab, or connect with me on irc.perl.org's #quarren channel.</p> <h2><a name="author"></a>Author</h2> <p>This article has been written by D Ruth Holloway (GeekRuthie) for the Perl Dancer Advent Calendar 2020.</p> <h2><a name="copyright__amp__license"></a>Copyright &amp; License</h2> <p>Copyright (C) 2020 by D Ruth Holloway (GeekRuthie). This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>.</p> </div> Collective Voice: Using Dancer2 at a Digital Marketing Agency http://advent.perldancer.org/2020/14 perl http://advent.perldancer.org/2020/14 Mon, 14 Dec 2020 00:00:00 -0000 <div class="pod-document"><h1><a name="collective_voice__using_dancer2_at_a_digital_marketing_agency"></a>Collective Voice: Using Dancer2 at a Digital Marketing Agency</h1> <p>At <a href="https://www.knowmad.com">work</a>, we have used Perl for over 20 years for web application development and automation. Recently, we have created, and are just about to open source, an application designed to collect online testimonials. There are similar paid services which include an online review portal such as <a href="https://gatherup.com">GatherUp</a>. The pricing plans for these services start at $75+ per month. As web developers, we decided to build our own solution. Thus was born Collective Voice.</p> <h2><a name="business_case"></a>Business Case</h2> <p>The growth of the web has empowered customers to have greater power than ever. Buyers often turn to customer testimonials and reviews to determine whether or not they trust a product, service, and even a seller. In fact, surveys show that buyers read <a href="https://www.brightlocal.com/research/local-consumer-review-survey/#Q17">seven reviews on average</a> before deciding to trust a business.</p> <p>But, getting reviews can be challenging. How do you make it easy for happy clients to leave reviews? And, when soliciting reviews from your customers, how do you prevent unhappy clients from leaving negative reviews? A testimonial review portal can benefit.</p> <h2><a name="testimonial_portal_requirements"></a>Testimonial Portal Requirements</h2> <p>Our immediate needs are far simpler than what the commercial solutions offer. We identified the following requirements for our software:</p> <ol> <li><a name="item_Make_it_easy_for_happy_customers_to_provide_5_star_reviews"></a><b>Make it easy for happy customers to provide 5-star reviews</b> </li> <li><a name="item_Provide_a_feedback_mechanism_for_less_satisfied_customers"></a><b>Provide a feedback mechanism for less satisfied customers</b> </li> <li><a name="item_Support_multiple_domains"></a><b>Support multiple domains</b> </li> <li><a name="item_Display_1_4_links_to_review_sites_for_4_5_stars"></a><b>Display 1-4 links to review sites for 4-5 stars</b> </li> <li><a name="item_Email_form_submissions_for_3_or_less_stars"></a><b>Email form submissions for 3 or less stars</b> </li> </ol> <h2><a name="tech_stack"></a>Tech Stack</h2> <h3><a name="back_end"></a>Back-End</h3> <p>Having a deep experience with Perl that goes back into the mid-90's, <a href="http://perldancer.org">Dancer</a> was an obvious choice for this project. It provides a simple but powerful web application framework that is quick to setup and get running so that I could focus more time and effort on the front-end design. As an adherent of test-driven development, I appreciate the built-in support for automated testing. Read more about <a href="https://metacpan.org/pod/distribution/Dancer2/lib/Dancer2/Manual.pod#TESTING">testing in the Dancer2::Manual</a>.</p> <h3><a name="front_end"></a>Front-End</h3> <p>As a long-time developer, I have my usual set of front-end frameworks that I'm comfortable using such as Bootstrap and jQuery. For this application, I wanted to experiment with newer tools that were light-weight and introduced a new way of building user interfaces.</p> <p>A programmer friend had recently suggested that I try out Tailwind CSS. This framework is built as a set of tiny components that you bring together to construct unique user interfaces. There is a site with pre-built components that includes both free and paid components at <a href="https://tailwindui.com">tailwindui.com</a>.</p> <p>It took a bit of trial and error to get into the hang of building interfaces up from simple utility classes, but I am hooked! It eliminates the need for writing custom CSS using id's and nested classes. The end result is less coding and more succinct designs. Check out an example at <a href="https://tailwindcss.com/docs/utility-first">tailwindcss.com/docs/utility-first</a>.</p> <p>One of the downsides of using minimalist frameworks is that you often need to pull in other libraries to provide additional support. In this case, I needed a Javascript library for the modal dialogs and form validation.</p> <p>Rather than pulling in a fat library like jQuery for this purpose, I learned that many Tailwind developers were opting for a lightweight alternative called Alpine JS. This library offers the reactive and declarative nature of big frameworks like Vue or React at a much lower resource cost.</p> <p>Phil Smith has a good <a href="https://www.smashingmagazine.com/2020/03/introduction-alpinejs-javascript-framework/">introductory article at Smashing Magazine</a>.</p> <p>Lastly, I found the <a href="https://www.alptail.com">Alptail</a> collection of open-source UI components invaluable as I built the UI. It provided the star rating and modal dialogs. Thanks to <a href="https://www.twitter.com/userlastname">Daniel Palmer</a> for putting that together.</p> <h2><a name="bringing_it_all_together"></a>Bringing it All Together</h2> <p>With the foundations laid, it's time to bring everything together for the big dance. There are 2 services that were needed</p> <p>For hosting the application, I chose a $5/month Linode server with Debian 10. In the repository, I have written a detailed description of how to get the application up and running within this environment that's available in the repository which will be released soon. Linode often offers free credit when signing up as a new user. If you'd like to support my work, please use <a href="https://www.linode.com/?r=9ec435ff3180734d255046be6e2b302f1d1f0992">my referral link</a>.</p> <p>For delivering the form submissions back to the company being reviewed, I opted for <a href="https://sendgrid.com">SendGrid</a>. Sure we could install an MTA to the server such as Postfix. And, debugging email delivery is not my idea of fun.</p> <h2><a name="getting_started"></a>Getting Started</h2> <s> <p>So far, I have been doing development in a private repository. I will be releasing v1.0 to the world via a public GitHub repository in the next couple of weeks.</p> </s> <p>This application is <a href="https://github.com/knowmad/CollectiveVoice">available on GitHub</a>. Follow the steps in README.md to get started and share your gratitude, frustrations or ideas for how to improve it. I welcome your comments!</p> <h2><a name="author"></a>Author</h2> <p>This article has been written by William McKee (knowmad) for the Perl Dancer Advent Calendar 2020. Follow me on <a href="https://github.com/knowmad">GitHub</a> or connect with me on <a href="https://www.linkedin.com/in/williammckee/">LinkedIn</a> or at <a href="https://www.knowmad.com">www.knowmad.com</a>.</p> <h2><a name="copyright__amp__license"></a>Copyright &amp; License</h2> <p>Copyright (C) 2020 by William McKee (knowmad). This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>.</p> </div> The Twelve Days of Dancer http://advent.perldancer.org/2020/13 perl http://advent.perldancer.org/2020/13 Sun, 13 Dec 2020 00:00:00 -0000 <div class="pod-document"><h1><a name="the_twelve_days_of_dancer"></a>The Twelve Days of Dancer</h1> <p>It's hard to believe that the decade of 2020 is almost behind us. This has been a challenging year for so many people in so many ways, and while many software projects have been relatively productive during this time, we Dancers have been pretty quiet. While we have been working to keep our families, jobs, and companies going, we've had to say goodbye to friends, families, and loved ones, and we had to survive the messes that were COVID-19 and the mess that was Hacktoberfest. So let me be the first to apologize for a really quiet year here.</p> <p>When we last ran a calendar in 2018, we made the decision to value quality over quantity, and ran an abbreviated calendar (The Twelve Days of Dancer) rather than a full advent calendar. This was received really well, and 2020 seemed like the perfect time to bring it back.</p> <p>This year, we revisit some old topics, introduce some exciting new web applications that have been built with Dancer2, and cover some of the most recent changes in the framework. In 2018, we featured some crossover articles with the Mojolicious team, and I'm pleased to say that Joel is back with yet another installment for us. We're really excited by this year's content, and we hope you will be, too.</p> <p>As David Bowie once said, "Let's Dance!"</p> <h2><a name="state_of_the_dancer"></a>State of the Dancer</h2> <p>This year brought a few new features, a lot of documentation patches, and a number of bug fixes and routine maintenance to Dancer2. If there were such a thing as the Golden Dancer award, it would certainly go to veryrusty this year - not only did he have the honor of closing one of the oldest open issues in Dancer2, but he also closed 4 issues with a single pull request.</p> <p>And that is not to say anyone else's work was any less important - the documentation continues to improve with the help of our awesome community. Bug reports are one of the most important contributions anyone can make - your reports help us make the framework better for everyone!</p> <p>There are a couple of specific items worth discussing in more detail:</p> <h3><a name="typed_parameters"></a>Typed Parameters</h3> <p>Thanks to SysPete, we now have typed route parameters in the core of Dancer2. This feature can help make your code more succinct and readable and less prone to bugs and other issues. This is one of the most exciting new features in Dancer2 - make sure to check out SysPete's article about it this year.</p> <h3><a name="dancer_1_"></a>Dancer(1)</h3> <p>David Precious (bigpresh) is still doing a great job maintaining Dancer, but has also switched employers, and is no longer developing and maintaining Dancer(1) as part of his day job.</p> <p>So, what does this mean for Dancer(1)? In the immediate future, probably nothing. Longer term, it means having some discussion among the core team and the community over how long to continue supporting the original version, and what that support might look like.</p> <p>As there are changes in the long-term status of Dancer(1), we will make every effort to keep the community informed.</p> <h2><a name="questions_for_the_community"></a>Questions for the Community</h2> <p>While there's not enough here for a formal survey, there are a couple of questions that I'd like to pose to our community:</p> <ul> <li><a name="item_GitHub_Discussions"></a><b>GitHub Discussions</b> <p>GitHub has a new discussions system, and it appears to be a modern replacement for what was Google Groups. The Dancer Core Team has yet to discuss how or if we might use these going forward. As a community, do you see value in us using this? Would you use it more or less than IRC or the mailing list when looking for help and information?</p> </li> <li><a name="item_Hacktoberfest"></a><b>Hacktoberfest</b> <p>Dear Reader, if only I could express the level of stress and frustration I encountered the first days of Hacktoberfest this year - the number of spam PRs we received was pretty ridiculous, and we are a much smaller project than some on GitHub, and I cannot imagine how they coped with the onslaught of spammy PRs, all for a lousy t-shirt (ok, the shirt was pretty cool...).</p> <p>To Digital Ocean's credit, I think they did an admirable job of trying to stop the bleeding. But for me, the spirit of Hacktoberfest has been ruined, and it makes me hesitant to set foot in those waters in the future.</p> <p>Dancer Community, what say you? Would you participate in a future Hacktoberfest given the chance?</p> </li> </ul> <p>Please, engage us on the mailing list, Twitter, IRC, or wherever you run into one of your friendly neighborhood Dancer Core Team members.</p> <h2><a name="dance__dance__dance_"></a>Dance, Dance, Dance!</h2> <p>And with that, let our mini advent calendar continue! Thanks for supporting us as much (or more) than we support you. Looking forward to 2021!</p> <h2><a name="author"></a>Author</h2> <p>This article has been written by Jason Crome (CromeDome) for the Twelve Days of Dancer.</p> <h2><a name="copyright"></a>Copyright</h2> <p>No copyright retained. Enjoy.</p> <p>Jason A. Crome / CromeDome</p> </div>