File Coverage

blib/lib/Webservice/Shipment/Carrier.pm
Criterion Covered Total %
statement 46 53 86.7
branch 8 14 57.1
condition 4 7 57.1
subroutine 10 17 58.8
pod 9 9 100.0
total 77 100 77.0


line stmt bran cond sub pod time code
1             package Webservice::Shipment::Carrier;
2              
3 4     4   1089 use Mojo::Base -base;
  4         8  
  4         31  
4              
5 4     4   1744 use Mojo::URL;
  4         20906  
  4         29  
6 4     4   1413 use Mojo::UserAgent;
  4         668317  
  4         30  
7 4     4   150 use Mojo::IOLoop;
  4         9  
  4         33  
8              
9 4     4   86 use Carp;
  4         8  
  4         2734  
10             our @CARP_NOT = ('Webservice::Shipment'); # don't carp from AUTOLOAD
11              
12             has api_url => sub { Mojo::URL->new };
13             has password => sub { croak 'password is required' };
14             has ua => sub { Mojo::UserAgent->new };
15             has username => sub { croak 'username is required' };
16             has [qw/date_format validation_regex/];
17             has carrier_description => sub { croak 'carrier_description is required' };
18              
19 0     0 1 0 sub extract_destination { '' }
20 0     0 1 0 sub extract_service { '' }
21 0     0 1 0 sub extract_status { return ('', '', 0) }
22 0     0 1 0 sub extract_weight { '' }
23              
24 0     0 1 0 sub human_url { Mojo::URL->new }
25              
26             sub parse {
27 8     8 1 52 my ($self, $id, $res) = @_;
28 8         15 my $ret = {};
29              
30 8         30 my @targets = (qw/postal_code state city country address1 address2/);
31 8         22 for my $target (@targets) {
32 48   100     20420 $ret->{destination}{$target} = $self->extract_destination($id, $res, $target) || '';
33             }
34              
35 8         5281 $ret->{weight} = $self->extract_weight($id, $res);
36              
37 8         1862 @{$ret->{status}}{qw/description date delivered/} = $self->extract_status($id, $res);
  8         44  
38 8   50     120 $ret->{status}{date} ||= '';
39 8 50 33     237 if ($ret->{status}{date} and my $fmt = $self->date_format) {
40 8         276 eval{ $ret->{status}{date} = $ret->{status}{date}->strftime($fmt) };
  8         38  
41             }
42 8 50       298 $ret->{status}{delivered} = $ret->{status}{delivered} ? 1 : 0;
43              
44 8         29 $ret->{service} = $self->extract_service($id, $res);
45              
46 8         35 $ret->{human_url} = $self->human_url($id, $res)->to_string;
47              
48 8         4528 return $ret;
49             }
50              
51 0     0 1 0 sub request { die 'to be overloaded by subclass' }
52              
53             sub track {
54 8     8 1 17589 my ($self, $id, $cb) = @_;
55 8         24 my $fail_msg = "No tracking information was available for $id";
56              
57 8 100       28 unless ($cb) {
58 6 50       27 croak $fail_msg unless my $res = $self->request($id);
59 6         975 return $self->parse($id, $res);
60             }
61              
62             Mojo::IOLoop->delay(
63 2     2   870 sub { $self->request($id, shift->begin) },
64             sub {
65 2     2   77 my ($delay, $err, $res) = @_;
66 2 50       7 die $err if $err;
67 2 50       8 die $fail_msg unless $res;
68 2         19 my $data = $self->parse($id, $res);
69 2         11 $self->$cb(undef, $data);
70             },
71 2     0   30 )->catch(sub{ $self->$cb(pop, undef) })->wait;
  0         0  
72             }
73              
74             sub validate {
75 6     6 1 18 my ($self, $id) = @_;
76 6 50       21 return undef unless my $re = $self->validation_regex;
77 6         103 return !!($id =~ $re);
78             }
79              
80             1;
81              
82             =head1 NAME
83              
84             Webservice::Shipment::Carrier - A base class for carrier objects used by Webservice::Shipment
85              
86             =head1 SYNOPSIS
87              
88             =head1 DESCRIPTION
89              
90             L is an abstract base class used to defined carrier objects which interact with external APIs.
91             For security, L requires that added carriers be a subclass of this one.
92              
93             =head1 ATTRIBUTES
94              
95             L inherits all of the attributes from L and implements the following new ones.
96              
97             =head2 api_url
98              
99             A L instance for specifying the base url of the api.
100             It is expected that this attribute will be overloaded in a subclass.
101              
102             =head2 date_format
103              
104             If specified, this format string is used to convert L objects to string representations.
105              
106             =head2 password
107              
108             Password for the external api call.
109             The default implementation dies if used without being specified.
110              
111             =head2 ua
112              
113             An instance of L, presumably used to make the L.
114             This is provided as a convenience for subclass implementations, and as an injection mechanism for a user agent which can mock the external service.
115             See L for more details.
116              
117             =head2 usename
118              
119             Username for the external api call.
120             The default implementation dies if used without being specified.
121              
122             =head2 validation_regex
123              
124             A regexp (C) applied to a tracking id to determine if the carrier can handle the request.
125             Currently, this is the only mechanism by which L determines this ability.
126             It is expected that this attribute will be overloaded in a subclass.
127             The default value is C which L interprets as false.
128              
129             =head1 METHODS
130              
131             =head2 extract_destination
132              
133             my $dest = $carrier->extract_destination($id, $res, $type);
134              
135             Returns a string of the response's destination field of a given type.
136             Currently those types are
137              
138             =over
139              
140             =item address1
141              
142             =item address2
143              
144             =item city
145              
146             =item state
147              
148             =item postal_code
149              
150             =item country
151              
152             =back
153              
154             An implementation should return an empty string if the type is not understood or if no information is available.
155              
156             =head2 extract_service
157              
158             my $service = $carrier->extract_service($id, $res);
159              
160             Returns a string representing the level of service the shipment as transported with.
161             By convention this string also should included the carrier name.
162             An example might be C.
163              
164             An implementation should return an empty string at the minimum if the information is unavailable.
165             That said, to follow the convention, most implementations should at least return the carrier name in any case.
166              
167             =head2 extract_status
168              
169             my ($description, $date, $delievered) = $carrier->extract_status($id, $res);
170              
171             Extract either the final or current status of the shipment.
172             Returns three values, the textual description of the current status, a L object, and a boolean C<1/0> representing whether the shipment has been delivered.
173             It is likely that if the shipment has been delivered that the description and date will correspond to that even, but it is not specifically guaranteed.
174              
175             =head2 extract_weight
176              
177             my $weight = $carrier->extract_weight($id, $res);
178              
179             Extract the shipping weight of the parcel.
180             An implementation should return an empty string if the information is not available.
181              
182             =head2 human_url
183              
184             my $url = $carrier->human_url($id, $res);
185              
186             Returns an instance of L which represents a url for to human interaction rather than API interaction.
187             An implementation should return a L object (presumably empty) even if information is not available.
188              
189             Note that though a response parameter is accepted, an implementation is likely able to generate a human_url from an id alone.
190              
191             =head2 parse
192              
193             my $info = $carrier->parse($id, $res);
194              
195             Returns a hash reference of data obtained from the id and the result obtained from L.
196             It contains the following structure with results obtained from many of the other methods.
197              
198             {
199             'status' => {
200             'description' => 'DELIVERED',
201             'date' => Time::Piece->new(...), # this is a string if date_format is set
202             'delivered' => 1,
203             },
204             'destination' => {
205             'address1' => '',
206             'address2' => '',
207             'city' => 'BEVERLY HILLS',
208             'state' => 'CA',
209             'country' => '',
210             'postal_code' => '90210',
211             },
212             'weight' => '0.70 LBS',
213             'service' => 'UPS NEXT DAY AIR',
214             'human_url' => 'http://wwwapps.ups.com/WebTracking/track?trackNums=1Z584056NW00605000&track.x=Track',
215             }
216              
217             =head2 request
218              
219             my $res = $carrier->request($id);
220              
221             # or nonblocking with callback
222             $carrier->request($id, sub { my ($carrier, $err, $res) = @_; ... });
223              
224             Given a valid id, this methods should return some native result for which other methods may extract or generate information.
225             The actual response will be carrier dependent and should not be relied upon other than the fact that it should be able to be passed to the extraction methods and function correctly.
226             Must be overridden by subclass, the default implementation throws an exception.
227              
228             =head2 track
229              
230             my $info = $carrier->track($id);
231              
232             # or nonblocking with callback
233             $carrier->track($id, sub { my ($carrier, $err, $info) = @_; ... });
234              
235             A shortcut for calling L and then L returning those results.
236             Note that this method throws an exception if L returns a falsey value.
237              
238             =head2 validate
239              
240             $bool = $carrier->validate($id);
241              
242             Given an id, check that the class can handle it.
243             The default implementation tests against the L.
244