An interface to WordsAPI

Weekly Challenge Thu 20 June 2019

One of the Perl weekly challenges this week is to use the WordsAPI to look up information about a word. An English word. I was curious about the API, so ended up having a play with it.

To use the API you have to sign up; they have a free level, but you still have to give them {d,cr}edit card details, which was a slight downer. Once you've signed up, you'll get a key.

A quick script

The main endpoint takes a word and returns a JSON structure with a bunch of information, including definition(s), synonyms, frequency in the English language, and more.

I started by writing a quick script to get details for a word, and just dump the JSON to stdout:

my $ua       = HTTP::Tiny->new();
my $url      = "https://wordsapiv1.p.mashape.com/words/$word";
my $headers  = {
                 "X-Mashape-Key" => $key,
                 "Accept"        => "application/json",
               };
my $response = $ua->get($url, { headers => $headers });

print $response->{content}, "\n";

There are other endpoints; for example to get a list of rhyming words:

my $url     = "https://wordsapiv1.p.mashape.com/words/$word/rhymes";

A simple class

Next I created a simple Moo class. Here's the skeleton:

package WebService::WordsAPI;
use Moo;
has key      => (...);
has ua       => (...);
has base_url => (...);

sub get_word_details {
    my ($self, $word) = @_;
    # pretty much the code from above
}

1;

Most of the methods are going to be pretty similar. And instead of return a JSON string, most of the time we'd like to get back a Perl data structure. But maybe we'd want our script to have a --json option, for dumping out the raw JSON from the API.

I created a private method that will do the heavy lifting, so that each public method will just be a matter of specifying the relative URL:

has return_type ( is => 'ro', default => sub { 'perl' } );

sub _request
{
    my ($self, $relurl) = @_;
    my $url             = $self->base_url.'/'.$relurl;
    my $headers         = {
                             "X-Mashape-Key" => $self->key,
                             "Accept"        => "application/json",
                          };
    my $response        = $self->ua->get($url, { headers => $headers });

    if (not $response->{success}) {
        die "failed $response->{status} $response->{reason}\n";
    }

    return $self->return_type eq 'json'
           ? decode_json($response->{content})
           : $response->{content}
           ;
}

And with that, we can define our first two public methods:

sub get_word_details {
    my ($self, $word) = @_;
    return $self->_request($word);
}

sub rhymes_with {
    my ($self, $word) = @_;
    return $self->_request("$word/rhymes");
}

Here's a script to display words that rhyme with a given word:

use WebService::WordsAPI;

my $key    = '...';
my $api    = WebService::WordsAPI->new(key => $key);
my $rhymes = $api->rhymes_with($ARGV[0])->{rhymes};

foreach my $pos (keys %$rhymes) {
    my @words = @{ $rhymes->{$pos} };
    print "\n$pos: @words\n";
}

You get back a hash from rhymes because a word like wind has different pronunciations depending on whether you're using it as a verb or noun:

./rhymes-with wind

verb: affined behind [...] trail behind

all: abscind downwind [...] leading wind

noun: abscind downwind [...] leading wind

The main method returns a lot of information, but there are also endpoints where you can just ask for one specific part of the information. For example, I've just added a definitions() method:

my $result = $api->definitions('wind');
foreach my $entry (@{ $result->{definitions} }) {
    printf "%s: %s\n",
        $entry->{partOfSpeech},
        $entry->{definition};
}

The JSON return for this is:

{
    "word": "wind",
    "definitions": [
        {
            "definition":   "catch the scent of; get wind of",
            "partOfSpeech": "verb"
        },
        ...
    ]
}

It feels like this has some redundant wrapping. It would be cleaner if the returned structure was just the array. Then you could just write:

my @definitions = $api->definitions('wind');
foreach my $entry (@definitions) {
    ...
}

But then it wouldn't match the full JSON returned, and would make the WordsAPI documentation less helpful.

Summary

That's what I've got done so far, which you can see in the github repo.

I've released this to CPAN, so you can see the documentation on MetaCPAN, and install the module to play with yourself.

I did think about some data classes and syntactic sugar, but I'm not convinced they wouldn't just muddy the water, for such a simple API.

comments powered by Disqus