As part of the big counter example project, this example runs on the command line and uses a JSON file as back-end 'database'. It helps simplify the back-end part we saw in the multiple command line counter with TSV file case.

Front-end

The front end is the command line. We just run the script as perl json_counter.pl foo providing the name of the counter on the command line.

Back-end

In this example the "database" will be a file with JSON format in it. The nice thing about the JSON format is that a Perl hash can be easily converted to a JSON string, and the JSON string can be easily converted back to a Perl hash.

This eliminates the need to think about a format that suits our data structure. Of course in our simple case of a simple hash this might be less interesting, but in the general case, it is hard to map a complex, multi-dimensional data structure to a string. JSON is a great solution there.

Code

In this solution we also use Path::Tiny to make it easier to read from the file and to write back to it without calling open and close ourselves and we use Cpanel::JSON::XS to convert the Perl hash to JSON string and back again.

examples/json_counter.pl

use strict;
use warnings;

use Cpanel::JSON::XS qw(encode_json decode_json);
use Path::Tiny qw(path);

my $file = 'counters.json';

my ($name) = @ARGV;
die "Usage: $0 NAME\n" if not $name;

my $counter;

if (-e $file) {
    $counter = decode_json path($file)->slurp;
}

if ($name eq '--list') {
    foreach my $key (sort keys $counter) {
        print "$key: $counter->{$key}\n";
    }
    exit;
}


$counter->{$name}++;
print "$name: $counter->{$name}\n";

path($file)->spew(encode_json $counter);

First we load the modules and explicitly import the necessary functions. If for nothing else, importing the functions explicitly is useful for the next person who won't have to guess where do these functions come from.

use Cpanel::JSON::XS qw(encode_json decode_json);
use Path::Tiny qw(path);

Then we get the name of the counter from the command line and show a usage message if the user has not supplied the value. Just as we did in the case with the TSV file.

While the script is running we will keep the counters in a hash, or more specifically in a reference to a hash. We use a reference instead of a simple hash because the function that converts form Perl to JSON expects a reference, and the function converting JSON to Perl returns a reference. So we declare the my $counter; scalar variable that will later autovivify into a reference to a hash.

Before looking at the next two sections, let's jump to the end of the script to see how do we increment the proper counter and how do we save the hash reference as a JSON string.

This code will increment the counter stored in the $name variable.

Even if this is the fist time we run the script and the variable $counter still only contains an undef, when we access it as if it was a reference to a hash it will automatically turn itself into a hash reference. (This is called autovivification.)

$counter->{$name}++;
print "$name: $counter->{$name}\n";

Then the function encode_json will convert our hash reference into a JSON string and the spew method of the Path::Tiny object will save he given string to the file in the $file variable.

path($file)->spew(encode_json $counter);

We don't need to think about conversion at all, the encode_json function handles it for us.

Now that we know how the 'counters.json' file is generated after we have incremented the counter we can go back to the code in the middle of the script and see how do we load the content of the file.

Using the -e operator we check if the file exists, if it does, we read in the content of the file using the slurp method of the Path::Tiny object. This will read in (or slurp the whole content of the file). Then we use the decode_json function to convert this JSON string into a Perl data structure. (Specifically a reference to a Perl hash.) This is what we assign the $counter variable.

if (-e $file) {
    $counter = decode_json path($file)->slurp;
}

List all the counters

In this example I've added an extra functionality. If the user passes --list on the command line, then instead of using that as the name of yet another counter, we are going to list all the counters with their current count value.

For this we had to dereference the $counter reference of a hash using %$counter. Then we could call keys on this new hash. We then call sort on the list of keys, so we won't see the keys in a random order. Then we just print the key-value pairs to the console. Once we are done, we call exit to make sure we won't use the word '--list' as the name of another counter.

if ($name eq '--list') {
    foreach my $key (sort keys %$counter) {
        print "$key: $counter->{$key}\n";
    }
    exit;
}

keys on reference is experimental

Actually starting from perl 5.20, we don't even need to dereference the hash reference before using the keys function. We could just write:

    sort keys $counter

and it would work. It will give a warning keys on reference is experimental and at this point I'd recommend using only if you have plenty of unit-tests around the code and if you are ready to change it if this feature is changed in later versions of Perl.