File Coverage

blib/lib/Webservice/Shipment/Carrier.pm
Criterion Covered Total %
statement 45 52 86.5
branch 8 14 57.1
condition 4 7 57.1
subroutine 10 17 58.8
pod 9 9 100.0
total 76 99 76.7


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