text

Last week’s arti­cle received a com­ment on a pri­vate Facebook group that amount­ed to just use JavaScript’s built-​in for­mat­ting.” So what would that look like?

#!/usr/bin/env perl

use Mojolicious::Lite -signatures;
use DateTime;

get '/' =>
    sub ($c) { $c->render( template => 'index', date => DateTime->today ) };

helper localize_date => sub ( $c, $date = DateTime->today, $style = 'full' ) {
    my $date_params = join ',' => $date->year, $date->month_0, $date->day;
    return
        qq<new Date($date_params).toLocaleString( [], {dateStyle: "$style"})>;
};

app->start;
__DATA__
@@ index.html.ep
% layout 'default';
% title 'Today';
<ul>
    <li><script>
        document.write(<%== localize_date $date %>)
    </script></li>
    % for my $style ( qw(long medium short) ) {
    <li><script>
        document.write(<%== localize_date $date, $style %>)
    </script></li>
    % }
</ul>
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
    <head><title><%= title %></title></head>
    <body><%= content %></body>
</html>

It’s struc­tured much like the Perl-​only solu­tion, with a default "/" route and a localize_date Mojolicious helper to do the for­mat­ting. I opt­ed to out­put a piece of JavaScript from the helper on lines 11 through 14 since it could be repeat­ed sev­er­al times in a doc­u­ment. You could instead declare a func­tion in the default lay­out’s HTML <head> on line 38 that would receive a date and a for­mat­ting style, out­putting the result­ing for­mat­ted date.

In the tem­plate’s list from lines 22 through 31 I decid­ed to use JavaScript document.write method calls to add our gen­er­at­ed code. This has a slew of caveats but works for our exam­ple here.

Worth not­ing is the dou­ble equals sign (<%== %>) when embed­ding a Perl expres­sion. This pre­vents Mojolicious from XML-​escaping spe­cial char­ac­ters, e.g., replac­ing "quotes" with &quot;, <angle brack­ets> with &lt; and &gt;, etc.. This is impor­tant when return­ing HTML and JavaScript code.

I also chose to use the JavaScript Date objec­t’s toLocaleString() method for my for­mat­ting on line 12. There are oth­er ways to do this:

Note that line 10 builds the para­me­ters for JavaScript’s Date con­struc­tor using the year, month_0, and day meth­ods of our Perl DateTime object; month_0 because the Date con­struc­tor takes its month as an inte­ger from 0 to 11 rather than 1 to 12. JavaScript Dates can be con­struct­ed in many ways; this seemed the sim­plest with­out hav­ing to explain things like epochs and incon­sis­tent parsing.

Why are we using Perl DateTimes and a helper any­way? I’m assum­ing that our dates are com­ing from the back­end of our appli­ca­tion, pos­si­bly inflat­ed from a data­base col­umn. If your dates are strict­ly on the fron­tend, you might decide to put your for­mat­ting code there in a JavaScript func­tion, per­haps using a JavaScript-​based tem­plat­ing library.

The bot­tom line is to do what­ev­er makes sense for your sit­u­a­tion. I pre­fer the Perl solu­tion because I like the lan­guage and its ecosys­tem and per­haps have accli­mat­ed to its quirks. The com­pli­ca­tions of JavaScript brows­er sup­port, com­pet­ing frame­works, and lay­ers of tool­ing make my head hurt. Despite this, I’m still learn­ing; if you have any com­ments or sug­ges­tions, please leave them below.

Gábor’s already post­ed today’s video!

It’s real­ly inter­est­ing to see our dif­fer­ent devel­op­ment styles work togeth­er. Gábor’s all about try­ing things imme­di­ate­ly and search­ing the entire web for answers, while I like to read the doc­u­men­ta­tion and work out its writ­ers’ intent.

I’m also embar­rassed that we end­ed on a cliffhang­er” with an unre­solved error, though Gábor post­ed that he man­aged to resolve it.

Western and eastern hemispheres of the Earth

When we’re writ­ing soft­ware for a glob­al audi­ence, it’s nice if we can pro­vide it accord­ing to their native lan­guages and con­ven­tions. Translating all of the text can be a huge under­tak­ing, but we can start small by mak­ing sure that when we show the day and date it appears as the user expects. For exam­ple, to me it’s Tuesday, April 20, 2021; to my friend Paul in the UK it’s Tuesday, 20 April 2021 (note the dif­fer­ence in order), and to my oth­er friend Gabór in Israel it’s יום שלישי, 20 באפריל 2021 (note the dif­fer­ent direc­tion of the text, dif­fer­ent lan­guage, and char­ac­ter set).

Thankfully, we have a num­ber of tools to assist us:

  • The DateTime::Locale library, which enables our Perl soft­ware to rep­re­sent dates and times glob­al­ly and con­tains a cat­a­log of locales. It works with the DateTime library for stor­ing our dates as objects that can be eas­i­ly manip­u­lat­ed and formatted.
  • The HTTP Accept-​Language head­er, which lets a web brows­er com­mu­ni­cate to the serv­er what nat­ur­al lan­guages and locale vari­ants the user understands.
  • The HTTP::AcceptLanguage mod­ule, which helps us parse the Accept-​Language head­er and select a com­pat­i­ble locale.

Our sam­ple code uses the Mojolicious frame­work and is very sim­ple; almost half of it is just HTML web page tem­plates. You could eas­i­ly adapt it to oth­er frame­works or tem­plat­ing systems.

#!/usr/bin/env perl

use Mojolicious::Lite -signatures;
use DateTime;
use DateTime::Locale;
use HTTP::AcceptLanguage;

my %locales
    = map { $_ => DateTime::Locale->load($_) } DateTime::Locale->codes;

get '/' =>
    sub ($c) { $c->render( template => 'index', date => DateTime->today ) };

helper localize_date => sub ( $c, $date = DateTime->today, $format = 'full' )
{
    my $locale = $locales{ HTTP::AcceptLanguage->new(
            $c->req->headers->accept_language )->match( keys %locales ) };

    my $method_name = "date_format_$format";
    return $date->clone->set_locale($locale)
        ->format_cldr( $locale->$method_name );
};

app->start;
__DATA__
@@ index.html.ep
% layout 'default';
% title 'Today';
<ul>
    <li><%= localize_date $date %></li>
    % for my $format ( qw(long medium short) ) {
    <li><%= localize_date $date, $format %></li>
    % }
</ul>
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
    <head><title><%= title %></title></head>
    <body><%= content %></body>
</html>

Lines 1 through 5 tell our code to use the Perl inter­preter in our exe­cu­tion PATH and load our pre­req­ui­site mod­ules. Note we’re using the micro ver­sion of Mojolicious, Mojolicious::Lite; lat­er you can grow your appli­ca­tion into a well-​structured Mojolicious app. We’re also using Perl sub­rou­tine sig­na­tures, which requires Perl 5.20 or lat­er (released in 2014).

Lines 7 and 8 pre­load all of the avail­able DateTime::Locale objects so that we can serve requests faster with­out hav­ing to load a new locale every time. We cre­ate a hash where the keys are the locale iden­ti­fiers (for exam­ple, en-US for United States English), and the val­ues are the locale objects.

Line 10 begins our route han­dler for HTTP GET requests on the default / route in our web appli­ca­tion. When a brows­er hits the home page of our app, it will exe­cute the code in the anony­mous sub in line 11, which is passed the con­troller object as $c. It’s a very sim­ple han­dler that ren­ders a tem­plate called index (described below), pass­ing it a date object with today’s date.

Lines 13 through 23 are where the smarts of our appli­ca­tion lie. It’s a helper that we’ll call from our tem­plate to local­ize a date object, and it’s anoth­er anony­mous sub. This time it’s passed a Mojolicious con­troller as $c, a $date para­me­ter that defaults to today, and a $format para­me­ter that defaults to full’.

Lines 14 through 18 in the helper get our locale. Working from the inside out, we get the HTTP Accept-​Language head­er from the request on line 16, cre­ate a new HTTP::AcceptLanguage object in line 15 for pars­ing that head­er, and then match it against the keys in our glob­al %locales hash in line 17. That matched key then looks up the appro­pri­ate DateTime::Locale object from the hash.

DateTime only allows you to set a locale at object con­struc­tion time, so in line 19 we cre­ate a new object from the old one, set­ting its locale to our newly-​discovered $locale object. Finally, in lines 21 and 22 we deter­mine what method to call on that object to retrieve the CLDR (Common Locale Data Repository) for­mat­ting pat­tern for the request­ed for­mat and then return the for­mat­ted date.

Finally, line 25 starts the appli­ca­tion. To run it using the devel­op­ment serv­er includ­ed with Mojolicious, do this at the com­mand line:

$ morbo perl_date_locale.pl

There are oth­er options for deploy­ing your appli­ca­tion, includ­ing Mojolicious’ built-​in web serv­er, inside a con­tain­er, using oth­er web servers, etc.

The rest of the above script is in the __DATA__ por­tion and con­tains two pseudo-​files that Mojolicious knows how to read in the absence of actu­al tem­plates and lay­outs. First on line 28 is the actu­al index.html.ep HTML page, which uses Mojolicious’ Embedded Perl (ep) tem­plat­ing sys­tem to select a lay­out of shared HTML to use (the layouts/default.html.ep file start­ing on line 39).

Lines 32 through 37 ren­der an HTML unordered list that runs through the var­i­ous for­mat­ting options avail­able to our localize_date helper, first with the default full’ for­mat­ting, and then a loop through long’, medi­um’, and short’. Note that we call our helper as an expres­sion, with an equals (=) sign after the per­cent (%) sign.

If you want to test dif­fer­ent locales with­out chang­ing your brows­er or oper­at­ing sys­tem set­tings, you can invoke the script from the com­mand line along with the HTTP request and head­ers to pass along. Here’s an exam­ple using German:

$ perl perl_date_locale.pl get -H 'Accept-Language: de' /
[2021-04-17 16:39:57.81379] [5425] [debug] [LcCSBKMVd90t] GET "/"
[2021-04-17 16:39:57.81408] [5425] [debug] [LcCSBKMVd90t] Routing to a callback
[2021-04-17 16:39:57.81610] [5425] [debug] [LcCSBKMVd90t] Rendering template "index.html.ep" from DATA section
[2021-04-17 16:39:57.81714] [5425] [debug] [LcCSBKMVd90t] Rendering template "layouts/default.html.ep" from DATA section
[2021-04-17 16:39:57.81792] [5425] [debug] [LcCSBKMVd90t] 200 OK (0.004118s, 242.836/s)
<!DOCTYPE html>
<html>
    <head><title>Today</title></head>
    <body>
<ul>
    <li>Sonntag, 18. April 2021</li>
    <li>18. April 2021</li>
    <li>18.04.2021</li>
    <li>18.04.21</li>
</ul>
</body>
</html>

And here’s Japanese:

$ perl perl_date_locale.pl get -H 'Accept-Language: ja' /
[2021-04-17 16:40:56.10840] [5478] [debug] [Wmr6cN5KUJlP] GET "/"
[2021-04-17 16:40:56.10874] [5478] [debug] [Wmr6cN5KUJlP] Routing to a callback
[2021-04-17 16:40:56.11101] [5478] [debug] [Wmr6cN5KUJlP] Rendering template "index.html.ep" from DATA section
[2021-04-17 16:40:56.11255] [5478] [debug] [Wmr6cN5KUJlP] Rendering template "layouts/default.html.ep" from DATA section
[2021-04-17 16:40:56.11360] [5478] [debug] [Wmr6cN5KUJlP] 200 OK (0.005164s, 193.648/s)
<!DOCTYPE html>
<html>
    <head><title>Today</title></head>
    <body>
<ul>
    <li>2021年4月18日日曜日</li>
    <li>2021年4月18日</li>
    <li>2021/04/18</li>
    <li>2021/04/18</li>
</ul>
</body>
</html>

A full list of sup­port­ed locales is pro­vid­ed in the DateTime::Locale::Catalog documentation.

I hope this arti­cle has helped demon­strate that it’s not too hard to make your Perl web appli­ca­tions respect glob­al audi­ences, if only with dates. For more on local­iza­tion and Perl, start with the Locale::Maketext framework.

I’ll be join­ing Perl Maven’s Gábor Szabó for a live cod­ing exer­cise via Zoom, work­ing on a web appli­ca­tion to keep track of exer­cis­es sub­mit­ted by stu­dents in a train­ing course. The first ses­sion is sched­uled for March 28 at 10:00 AM US Central Time; con­sult this site for the time in your time zone and reg­is­ter here.

I’m real­ly excit­ed as this is my first pair pro­gram­ming ses­sion out­side of work. Join us!

pexels-photo-3568518.jpeg

In part 1 we designed our API using OpenAPI/​Swagger. Now it’s time to write some tests and wire it up using Mojolicious::Plugin::OpenAPI. This is a much longer arti­cle; buck­le up!

If you haven’t already, you’ll need to install Perl. Using Linux or macOS? You’ve already got Perl installed on your sys­tem. On Windows? I rec­om­mend you install Strawberry Perl as it lets you devel­op Perl appli­ca­tions using the same tools that our Unix-​based brethren use. 

(Advanced users may want to inves­ti­gate using perl­brew, plenv, or berry­brew for man­ag­ing mul­ti­ple ver­sions of Perl and installing more recent ver­sions than are includ­ed on your system.)

Once you have Perl installed, it’s a sim­ple mat­ter of using either the cpan or cpanm tools to install Mojolicious and Mojolicious::Plugin::OpenAPI. The lat­ter will install some depen­den­cies as well. Here’s a tran­script of installing cpanm and the two modules:

% curl -L https://cpanmin.us | perl - App::cpanminus
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  295k  100  295k    0     0  97765      0  0:00:03  0:00:03 --:--:-- 97765
--> Working on App::cpanminus
Fetching http://www.cpan.org/authors/id/M/MI/MIYAGAWA/App-cpanminus-1.7044.tar.gz ... OK
Configuring App-cpanminus-1.7044 ... OK
Building and testing App-cpanminus-1.7044 ... OK
Successfully installed App-cpanminus-1.7044
1 distribution installed
% cpanm Mojolicious Mojolicious::Plugin::OpenAPI
--> Working on Mojolicious
Fetching http://www.cpan.org/authors/id/S/SR/SRI/Mojolicious-9.01.tar.gz ... OK
Configuring Mojolicious-9.01 ... OK
Building and testing Mojolicious-9.01 ... OK
Successfully installed Mojolicious-9.01
--> Working on Mojolicious::Plugin::OpenAPI
Fetching http://www.cpan.org/authors/id/J/JH/JHTHORSEN/Mojolicious-Plugin-OpenAPI-4.00.tar.gz ... OK
Configuring Mojolicious-Plugin-OpenAPI-4.00 ... OK
==> Found dependencies: JSON::Validator
--> Working on JSON::Validator
Fetching http://www.cpan.org/authors/id/J/JH/JHTHORSEN/JSON-Validator-4.14.tar.gz ... OK
Configuring JSON-Validator-4.14 ... OK
==> Found dependencies: YAML::PP, Test::Deep
--> Working on YAML::PP
Fetching http://www.cpan.org/authors/id/T/TI/TINITA/YAML-PP-0.026.tar.gz ... OK
Configuring YAML-PP-0.026 ... OK
==> Found dependencies: Test::Warn, Test::Deep
--> Working on Test::Warn
Fetching http://www.cpan.org/authors/id/B/BI/BIGJ/Test-Warn-0.36.tar.gz ... OK
Configuring Test-Warn-0.36 ... OK
==> Found dependencies: Sub::Uplevel
--> Working on Sub::Uplevel
Fetching http://www.cpan.org/authors/id/D/DA/DAGOLDEN/Sub-Uplevel-0.2800.tar.gz ... OK
Configuring Sub-Uplevel-0.2800 ... OK
Building and testing Sub-Uplevel-0.2800 ... OK
Successfully installed Sub-Uplevel-0.2800
Building and testing Test-Warn-0.36 ... OK
Successfully installed Test-Warn-0.36
--> Working on Test::Deep
Fetching http://www.cpan.org/authors/id/R/RJ/RJBS/Test-Deep-1.130.tar.gz ... OK
Configuring Test-Deep-1.130 ... OK
Building and testing Test-Deep-1.130 ... OK
Successfully installed Test-Deep-1.130
Building and testing YAML-PP-0.026 ... OK
Successfully installed YAML-PP-0.026
Building and testing JSON-Validator-4.14 ... OK
Successfully installed JSON-Validator-4.14
Building and testing Mojolicious-Plugin-OpenAPI-4.00 ... OK
Successfully installed Mojolicious-Plugin-OpenAPI-4.00
7 distributions installed

(The ver­sions list­ed above may dif­fer as this arti­cle gets pro­gres­sive­ly out of date.)

Next, make a fold­er some­where to place our new microser­vice project. We’ll call it ~/Projects/blog here, but any place will do as long as you know how to get to it from your text edi­tor and your com­mand line.

After that, go into that direc­to­ry and use the newly-​installed mojo com­mand to build the basics of our project:

% cd ~/Projects/blog
% mojo generate app Local::Dictionary::Microservice
  [mkdir] /Users/mgardner/Projects/blog/local_dictionary_microservice/script
  [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/script/local_dictionary_microservice
  [chmod] /Users/mgardner/Projects/blog/local_dictionary_microservice/script/local_dictionary_microservice 744
  [mkdir] /Users/mgardner/Projects/blog/local_dictionary_microservice/lib/Local/Dictionary
  [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/lib/Local/Dictionary/Microservice.pm
  [exist] /Users/mgardner/Projects/blog/local_dictionary_microservice
  [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/local-dictionary-microservice.yml
  [mkdir] /Users/mgardner/Projects/blog/local_dictionary_microservice/lib/Local/Dictionary/Microservice/Controller
  [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/lib/Local/Dictionary/Microservice/Controller/Example.pm
  [mkdir] /Users/mgardner/Projects/blog/local_dictionary_microservice/t
  [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/t/basic.t
  [mkdir] /Users/mgardner/Projects/blog/local_dictionary_microservice/public
  [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/public/index.html
  [mkdir] /Users/mgardner/Projects/local_dictionary_microservice/templates/layouts
  [write] /Users/mgardner/Projects/local_dictionary_microservice/templates/layouts/default.html.ep
  [mkdir] /Users/mgardner/Projects/blog/local_dictionary_microservice/templates/example
  [write] /Users/mgardner/Projects/blog/local_dictionary_microservice/templates/example/welcome.html.ep

This com­mand builds the scaf­fold­ing for our new project. We won’t be using all of it, but it does pro­vide some use­ful start­ing points.

(Why did we put Local:: in the begin­ning of the name? Because noth­ing you place there will con­flict with oth­er mod­ules you install from CPAN.)

Next, put the OpenAPI doc­u­ment we devel­oped in part 1 some­where in the project. We’ll choose openapi/dictionary_openapi.yml. Create a fold­er called openapi in the project, and then open your text edi­tor and paste the fol­low­ing doc­u­ment into a file called dictionary_openapi.yml.

openapi: 3.0.3
info:
  title: Dictionary
  description: The PhoenixTrap.com dictionary microservice
  version: 1.0.0
  license:
    name: Artistic License 2.0
    url: https://www.perlfoundation.org/artistic-license-20.html
paths:
  /health:
    get:
      summary: Check if this service is online
      x-mojo-to: monitoring#heartbeat
      responses:
        200:
          description: All systems operational
          content:
            application/json:
              schema:
                type: object
        500:
          description: Something is wrong
  /word/{word}:
    get:
      summary: Get the definition of a word
      x-mojo-to: word#define
      parameters:
      - $ref: '#/components/parameters/word'
      responses:
        200:
          description: Found word
          content:
            application/json:
              schema:
                type: string
        404:
          description: Could not find word
    post:
      summary: Add or replace the definition of a word
      x-mojo-to: word#save
      parameters:
      - $ref: '#/components/parameters/word'
      requestBody:
        description: Definition of a word
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                definition:
                  type: string
      responses:
        200:
          description: Word saved
    delete:
      summary: Delete an entry from the dictionary
      x-mojo-to: word#remove
      parameters:
      - $ref: '#/components/parameters/word'
      responses:
        200:
          description: Word deleted
components:
  parameters:
    word:
      description: A word in the dictionary
      in: path
      name: word
      required: true
      schema:
        type: string

Calling the plugin

Right now all our appli­ca­tion does is serve a demon­stra­tion page; you can test it by run­ning the fol­low­ing command:

% script/local_dictionary_microservice daemon
[2021-02-28 16:17:09.76863] [74167] [info] Listening at "http://*:3000"
Web application available at http://127.0.0.1:3000

…and then going to that URL on the last line.

After you’ve ver­i­fied that it works, hold down the Control key and type the let­ter C in your ter­mi­nal to stop the script. Edit the lib/Local/Dictionary/Microservice.pm file in your text edi­tor. Replace it with this:

package Local::Dictionary::Microservice;
use Mojo::Base 'Mojolicious', -signatures;

# This method will run once at server start
sub startup($self) {

    # Load configuration from config file
    my $config = $self->plugin('NotYAMLConfig');

    # Configure the application
    $self->secrets( $config->{secrets} );
    $self->plugin( OpenAPI => {
        url => $self->home
          ->rel_file('openapi/dictionary_openapi.yml'),
    } );
}

1;

Now when you run the script as a dae­mon, the URL it reports responds with the JSON ver­sion of our OpenAPI doc­u­ment. Progress!

Note the $self->plugin() call towards the end of our class above. That’s the secret sauce that loads our OpenAPI doc­u­ment and tells Mojolicious to cre­ate respons­es from it.

Writing our first tests

Next, we’ll write a cou­ple of test scripts. (Why are we writ­ing tests before actu­al­ly cod­ing our microser­vice? Because we’re prac­tic­ing test-​driven devel­op­ment, in which we write our tests first, see that they fail, and then write the code to make them suc­ceed.) Here’s a mod­i­fied t/basic.t script that should suc­ceed right off the bat:

use Mojo::Base -strict;

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

my $t = Test::Mojo->new('Local::Dictionary::Microservice');
$t->get_ok('/')->status_is(200)->json_has('/openapi');

done_testing();

Run it with prove:

% prove -vl t/basic.t
t/basic.t .. [2021-02-28 16:32:54.79287] [74611] [debug] [3kzjyqgi] GET "/"
[2021-02-28 16:32:54.79321] [74611] [debug] [3kzjyqgi] Routing to a callback
[2021-02-28 16:32:54.79536] [74611] [debug] [3kzjyqgi] 200 OK (0.002493s, 401.123/s)
ok 1 - GET /
ok 2 - 200 OK
ok 3 - has value for JSON Pointer "/openapi"
1..3
ok
All tests successful.
Files=1, Tests=3,  0 wallclock secs ( 0.02 usr  0.01 sys +  0.45 cusr  0.07 csys =  0.55 CPU)
Result: PASS

Now for a test that will fail at first. Put this in t/word.t:

use Mojo::Base -strict;

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

my $t = Test::Mojo->new('Local::Dictionary::Microservice');

$t->post_ok(
    '/word/foo' =>
      form => { definition => 'A metasyntactic variable' },
)->status_is(200);

done_testing();

And prove it:

% prove -vl t/word.t
t/word.t .. [2021-02-28 16:48:53.26826] [75751] [debug] [SlDSHWVE] POST "/word/foo"
[2021-02-28 16:48:53.26897] [75751] [debug] [SlDSHWVE] Routing to controller "Local::Dictionary::Microservice::Controller::Word" and action "save"
[2021-02-28 16:48:53.27278] [75751] [debug] [SlDSHWVE] Template "word/save.html.ep" not found
[2021-02-28 16:48:53.27287] [75751] [debug] [SlDSHWVE] Nothing has been rendered, expecting delayed response
[2021-02-28 16:49:23.27553] [75751] [debug] Inactivity timeout
# Premature connection close
not ok 1 - POST /word/foo
not ok 2 - 200 OK
#   Failed test 'POST /word/foo'
#   at t/word.t line 8.
#   Failed test '200 OK'
#   at t/word.t line 8.
#          got: undef
#     expected: '200'
1..2
# Looks like you failed 2 tests of 2.
Dubious, test returned 2 (wstat 512, 0x200)
Failed 2/2 subtests
Test Summary Report
-------------------
t/word.t (Wstat: 512 Tests: 2 Failed: 2)
  Failed tests:  1-2
  Non-zero exit status: 2
Files=1, Tests=2, 31 wallclock secs ( 0.02 usr  0.01 sys +  0.45 cusr  0.07 csys =  0.55 CPU)
Result: FAIL

Note in the sec­ond line of out­put that Mojolicious tried to route to a con­troller class at Local::Dictionary::Microservice::Controller::Word with an action of save. That’s what our OpenAPI doc­u­ment told it to do with its x-mojo-to: word#save line.

Adding methods to make the tests pass

Now we’ll write the con­troller class at lib/Local/Dictionary/Microservice/Controller/Word.pm:

package Local::Dictionary::Microservice::Controller::Word;
use Mojo::Base 'Mojolicious::Controller', -signatures;
use DB_File;

sub save($self) {
    return if not $self->openapi->valid_input;
    my ( $word, $definition ) = map { $self->param($_) }
      qw(word definition);

    tie my %definition, 'DB_File',
      $self->app->home->child('definitions.db');
    $definition{$word} = $definition;

    return $self->render( openapi => {} );
}

1;

The save method above retrieves the word and definition para­me­ters from the URL path and POST data, respec­tive­ly, and then saves them into a hash that is tied to a Berkeley DB file stored in definitions.db. It then ren­ders an emp­ty response back to the client.

Now run the test:

% prove -vl t/word.t
t/word.t .. [2021-02-28 18:06:33.77551] [78923] [debug] [VV0L-5rU] POST "/word/foo"
[2021-02-28 18:06:33.77623] [78923] [debug] [VV0L-5rU] Routing to controller "Local::Dictionary::Microservice::Controller::Word" and action "save"
[2021-02-28 18:06:33.77751] [78923] [debug] [VV0L-5rU] 200 OK (0.001994s, 501.505/s)
ok 1 - POST /word/foo
ok 2 - 200 OK
1..2
ok
All tests successful.
Files=1, Tests=2,  0 wallclock secs ( 0.02 usr  0.01 sys +  0.43 cusr  0.07 csys =  0.53 CPU)
Result: PASS

Woohoo, our method works! Let’s add some tests to the same t/word.t script, under­neath the first one:


$t->get_ok('/word/foo')->status_is(200)
  ->content_is('"A metasyntactic variable"');

$t->delete_ok('/word/foo')->status_is(200);

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

Here we’re test­ing that we get back the def­i­n­i­tion we saved, that we can delete the def­i­n­i­tion, and that when we try to retrieve it again we get a 404 Not Found error.

The test script will fail again, but we can fix that by adding the define and remove meth­ods to our con­troller class in lib/Local/Dictionary/Microservice/Controller/Word.pm:

sub define($self) {
    return if not $self->openapi->valid_input;
    my $word = $self->param('word');

    tie my %definition, 'DB_File',
      $self->app->home->child('definitions.db');

    return $self->render( openapi => $definition{$word},
      exists $definition{$word} ? () : (status => 404),
    );
}

sub remove($self) {
    return if not $self->openapi->valid_input;
    my $word = $self->param('word');

    tie my %definition, 'DB_File',
      $self->app->home->child('definitions.db');
    delete $definition{$word};

    return $self->render( openapi => {} );
}

Run the test script one last time:

% prove -vl t/word.t
t/word.t .. [2021-02-28 18:13:50.82904] [79162] [debug] [7PprxDOz] POST "/word/foo"
[2021-02-28 18:13:50.82978] [79162] [debug] [7PprxDOz] Routing to controller "Local::Dictionary::Microservice::Controller::Word" and action "save"
[2021-02-28 18:13:50.83225] [79162] [debug] [7PprxDOz] 200 OK (0.003191s, 313.381/s)
ok 1 - POST /word/foo
ok 2 - 200 OK
[2021-02-28 18:13:50.83531] [79162] [debug] [G0qyFA0G] GET "/word/foo"
[2021-02-28 18:13:50.83568] [79162] [debug] [G0qyFA0G] Routing to controller "Local::Dictionary::Microservice::Controller::Word" and action "define"
[2021-02-28 18:13:50.85745] [79162] [debug] [G0qyFA0G] 200 OK (0.022096s, 45.257/s)
ok 3 - GET /word/foo
ok 4 - 200 OK
ok 5 - exact match for content
[2021-02-28 18:13:50.86182] [79162] [debug] [MoedWcOi] DELETE "/word/foo"
[2021-02-28 18:13:50.86224] [79162] [debug] [MoedWcOi] Routing to controller "Local::Dictionary::Microservice::Controller::Word" and action "remove"
[2021-02-28 18:13:50.86337] [79162] [debug] [MoedWcOi] 200 OK (0.001535s, 651.466/s)
ok 6 - DELETE /word/foo
ok 7 - 200 OK
[2021-02-28 18:13:50.86662] [79162] [debug] [G2lH8gyw] GET "/word/foo"
[2021-02-28 18:13:50.86684] [79162] [debug] [G2lH8gyw] Routing to controller "Local::Dictionary::Microservice::Controller::Word" and action "define"
[2021-02-28 18:13:50.86964] [79162] [debug] [G2lH8gyw] 404 Not Found (0.002989s, 334.560/s)
ok 8 - GET /word/foo
ok 9 - 404 Not Found
1..9
ok
All tests successful.
Files=1, Tests=9,  0 wallclock secs ( 0.02 usr  0.01 sys +  0.46 cusr  0.10 csys =  0.59 CPU)
Result: PASS

Congratulations! You now have a sim­ple microser­vice for stor­ing and retriev­ing def­i­n­i­tions of words. You can now write a web front-​end in HTML and JavaScript, or per­haps anoth­er microser­vice that con­sumes this one.

(As an exer­cise, write the /health route defined in our OpenAPI doc­u­ment. It calls a heartbeat method in a con­troller class named Monitoring.)

What next?

  • You may not want to store your def­i­n­i­tions in a DB file in your project; con­sid­er mak­ing it a con­fig­urable option. Or use one of the many mod­ules on CPAN to choose a com­plete­ly dif­fer­ent back­end.
  • Add meth­ods to list some or all def­i­n­i­tions, using hyper­text as the engine of appli­ca­tion state (HATEOAS) to ren­der links where appropriate.
  • Support mul­ti­ple def­i­n­i­tions of the same word, or expand the API to respond with syn­onyms and antonyms. (Just make sure to incre­ment the ver­sion in the OpenAPI doc­u­ment and add some way for clients to spec­i­fy the ver­sion to indi­cate you’re mak­ing a break­ing change!)

Did you have any trou­ble fol­low­ing along? Got stuck on the instal­la­tion steps or some­where else? Please leave a com­ment below and I’ll try to help.