Implementing WWW::LastFM with XML::Rabbit - Part 3

In the previous article we looked at how XML::Rabbit does its magic to give us a very compact syntax for creating Moose attributes that mirror simple XML document values. In this article we'll look at some of the other sugar functions available in XML::Rabbit.

Implementing the <metros/> XML chunk extractor

So let's move on to the implementation of WWW::LastFM::Response::MetroList. Now we're starting to get to the juicy parts of the application. Use this code for lib/WWW/LastFM/Response/MetroList.pm:

package WWW::LastFM::Response::MetroList;
use XML::Rabbit;

use List::MoreUtils qw(uniq);

has_xpath_object_list '_locations' => './metro' => 'WWW::LastFM::Response::Metro',
    handles => {
        'locations'        => 'elements',
        'filter_locations' => 'grep',
    },
;

has_xpath_value_list '_countries_with_duplicates' => './metro/country';

has '_unique_countries' => (
    is         => 'ro',
    isa        => 'ArrayRef[Str]',
    traits     => ['Array'],
    handles    => {
        'countries'        => 'elements',
        'filter_countries' => 'grep',
    },
    lazy_build => 1,
);

sub _build__unique_countries {
    my ($self) = @_;
    return [
        uniq(
             @{ $self->_countries_with_duplicates }
        )
    ];
}

finalize_class();

Here you have another declaration method called has_xpath_object_list. It is similar to has_xpath_object, which we've already covered, but it returns an array of objects instead of just a single object. We also add some Moose native delegations for arrays. This makes the API more user friendly, as we don't need to dereference the array references all the time.

The next declaration is almost the same, but it works with strings instead of objects. As the XML document contains lots of duplicates, we create a separate attribute that takes care of getting rid of the duplicates and presents the API we're interested in.

Now we complete this with the implementation of WWW::LastFM::Response::Metro. Create the file lib/WWW/LastFM/Response/Metro.pm with the following content:

package WWW::LastFM::Response::Metro;
use XML::Rabbit;

has_xpath_value 'name'    => './name';
has_xpath_value 'country' => './country';

has 'country_and_name' => (
    is         => 'ro',
    isa        => 'Str',
    lazy_build => 1,
);

sub _build_country_and_name {
    my ($self) = @_;
    return $self->country . ": " . $self->name;
}

has 'name_and_country' => (
    is         => 'ro',
    isa        => 'Str',
    lazy_build => 1,
);

sub _build_name_and_country {
    my ($self) = @_;
    return $self->name . ", " . $self->country;
}

finalize_class();

This is a pretty straight-forward class. You have two string values from the <metro/> XML element extracted. Look at the use of relative XPath queries, which avoids having to construct elaborate queries based on the root of the XML document. As a convenience, I added two calculated attributes.

We can still test how it works with a one-liner, but it is starting to get a bit long.

$ perl -Ilib -MWWW::LastFM -E 'STDOUT->binmode(":utf8"); say join("\n", sort map { $_->name_and_country } WWW::LastFM->new->geo->get_metros->metros->filter_locations(sub { $_->country_and_name =~ /Norway/ }) )'
Bergen, Norway
Oslo, Norway

lastfm_locations.pl, a simple WWW::LastFM client

Let's create a script we can use to query locations available in the Last.FM service. Create bin/lastfm_locations.pl with this content:

#!/usr/bin/env perl

use strict;
use warnings;
use rlib;
use feature qw(say);

use WWW::LastFM;
use Encode qw(decode encode);

my $filter = shift;
my $filter_utf8 = $filter ? Encode::decode('UTF-8', $filter) : "";

my @locations = sort
        map { $_->name_and_country }
        WWW::LastFM->new->geo->get_metros->metros->filter_locations(
            sub { $_->country_and_name =~ /\Q$filter_utf8\E/i }
        )
;

if ( @locations > 0 ) {
    say Encode::encode('UTF-8', join("\n", @locations) );
}
else {
    say "No locations found matching '$filter'";
}

This is very similar to the one-liner, but with a bit more error checking and Unicode handling to ensure you can specify command line parameters in UTF8. I guess I've forgotten to mention this until now, but I have assumed that you are using a terminal software that uses UTF8 encoding. The only common operating system I know that doesn't do that by default nowadays is Windows. I'll leave dealing with that challenge for another article.

You should now have a complete application that allows you to ask a remote HTTP-based API for some information and display it in the way you want to. Adding more API calls is just a matter of adding additional method calls in WWW::LastFM::API::Geo and creating a new response class to handle the XML output. In the next article we will add the API call we're really interested in, geo.getEvents. Stay tuned!