File Coverage

blib/lib/Webservice/Shipment/Carrier.pm
Criterion Covered Total %
statement 49 56 87.5
branch 8 14 57.1
condition 4 7 57.1
subroutine 11 18 61.1
pod 9 9 100.0
total 81 104 77.8


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