Wrapping Web Services with Moo

178 views Asked by At

I recently took over the maintenance of a Perl client that wraps a Shipping web service. The project in its initial state uses Moo objects that map directly to object of the web service, for instance there are classes Parcel, Address, and Label.

In v2 of the API, you have to POST all of the data that represents one of these objects to the web service then you get back a unique id for that Object, that must be used for all subsequent transactions.

For instance if I posted:

{ name   => 'Hunter',
  street => '121 Baker St',
  city   => 'New York',
  state  => 'NY',
}

I would get back the same data, but with an id included:

{ id     => 'adr_xq1411',
  name   => 'Hunter',
  street => '121 Baker St',
  city   => 'New York',
  state  => 'NY',
}

I am having trouble deciding how to create these objects. Currently, I have this constructor that does the actual POSTing to get the ID then modifies the current object:

sub BUILD {
    my $self = shift;

    my $requestor = Net::Easypost::Request->new;
    my $resp = $requestor->post( 
        '/addresses', 
        $self->serialize( [qw(street1 street2 city state zip)] ) 
    );

    # save the id for this Address from Easypost   
    $self->id( $resp->{id} );

    return $self;
}

Is this a common approach when wrapping web services in Perl? It seems that the ideal method would be to POST to the web service and create all of the attributes of an Address object all at once, but in Moo(se) once you are in the BUILD method, the object has already been created.

I am not all that familiar with idioms for wrapping web services, is there a simpler way than this?

Any advice/comments/suggestions would be appreciated.

1

There are 1 answers

1
tobyink On BEST ANSWER

That's certainly one way to do it. It may be a good idea to factor out the BUILD method into a role. Something like:

package MyApp::PostOnBuild;

use Moo::Role;

has id          => (is => 'rwp');
has endpoint    => (is => 'ro', default => sub { '/addresses' });
has requestor   => (is => 'ro', default => sub { Net::Easypost::Request->new });
has field_names => (is => 'ro', builder => 1);

requires '_build_field_names';
requires 'serialize';  # or maybe just implement serialize within this role!

sub BUILD { }
after BUILD => sub {
    my $self = shift;
    my $resp = $self->requestor->post($self->endpoint, $self->serialize($self->field_names));
    $self->_set_id( $resp->{id} );
};

Now you classes don't need to define their own BUILD method. All they need to do is:

package MyApp::Address;

use Moo;
with 'MyApp::PostOnBuild';

my @fields = qw/ street1 street2 city state zip /;

has $_ => (is => 'ro') for @fields;

sub _build_field_names { \@fields }

sub serialize { ... }  # would this method be better defined in MyApp::PostOnBuild??

Note that requestor is now an attribute, so when you're testing the class you can do something like:

my $adr = MyApp::Address->new(
    street1   => '123 Example Lane',
    city      => 'Sydney',
    state     => 'NSW',
    zip       => '2035',
    requestor => Test::Requestor->new,
);