File Coverage

blib/lib/WWW/Datafinder.pm
Criterion Covered Total %
statement 47 127 37.0
branch 0 48 0.0
condition 0 25 0.0
subroutine 16 22 72.7
pod 1 2 50.0
total 64 224 28.5


line stmt bran cond sub pod time code
1             package WWW::Datafinder;
2              
3 2     2   33345 use 5.010;
  2         5  
4 2     2   7 use strict;
  2         1  
  2         30  
5 2     2   5 use warnings;
  2         8  
  2         72  
6              
7             =head1 NAME
8              
9             WWW::Datafinder - Perl API for Datafinder L<< http://datafinder.com >> API for marketing data append
10              
11             =head1 VERSION
12              
13             Version 0.02
14              
15             =cut
16              
17             our $VERSION = '0.02';
18              
19 2     2   6 use Carp qw(cluck);
  2         2  
  2         715  
20 2     2   1017 use Data::Dumper;
  2         15775  
  2         112  
21 2     2   1022 use REST::Client;
  2         75361  
  2         52  
22 2     2   1182 use JSON::XS;
  2         7488  
  2         97  
23 2     2   10 use URI;
  2         2  
  2         38  
24 2     2   6 use Scalar::Util qw(blessed reftype);
  2         2  
  2         120  
25 2     2   925 use Readonly;
  2         5088  
  2         82  
26 2     2   10 use Exporter 'import';
  2         2  
  2         41  
27 2     2   6 use File::Path qw(make_path);
  2         2  
  2         89  
28 2     2   794 use File::Spec::Functions qw(catfile catdir splitpath);
  2         1073  
  2         108  
29 2     2   16 use Digest::MD5 qw(md5 md5_hex);
  2         2  
  2         83  
30 2     2   6 use Storable qw(nstore retrieve dclone);
  2         2  
  2         111  
31              
32 2     2   636 use Mouse;
  2         29547  
  2         6  
33              
34             #** @attr public String $api_key API access key
35             #*
36             has api_key => ( isa => 'Str', is => 'rw', required => 1 );
37              
38             #** @attr public Int $cache_time How long locally cached results are valid
39             #*
40             has cache_time => ( isa => 'Int', is => 'rw', default => 0 );
41              
42             #** @attr public Int $cache_dir Whether to store locally cached results
43             #*
44             has cache_dir => ( isa => 'Str', is => 'rw', default => '/var/tmp/datafinder-cache' );
45              
46             #** @attr public Int $retries How many times retry upon timeout
47             #*
48             has retries => ( isa => 'Int', is => 'rw', default => 5 );
49              
50             #** @attr protected String $base_url Base REST URL
51             #*
52             has base_url => (
53             isa => 'Str',
54             is => 'rw',
55             default => 'http://api.datafinder.com/qdf.php'
56             );
57              
58             #** @attr protected CodeRef $ua Reference to the REST UA
59             #*
60             has ua => (
61             isa => 'Object',
62             is => 'rw',
63             lazy => 1,
64             init_arg => undef,
65             default => sub {
66             return REST::Client->new();
67             }
68             );
69              
70             #** @attr public String $error_message Error message regarding the last failed operation
71             #*
72             has error_message =>
73             ( isa => 'Str', is => 'rw', init_arg => undef, default => '' );
74              
75             sub _url {
76 0     0     my ( $self, $query_params ) = @_;
77              
78 0   0       $query_params //= {};
79 0           my $uri = URI->new( $self->base_url, 'http' );
80 0           $uri->query_form($query_params);
81             #print "URL=".$uri->as_string;
82 0           return $uri->as_string;
83             }
84              
85             sub _process_response {
86 0     0     my ( $self, $response ) = @_;
87              
88 0 0         if ($@) {
    0          
89 0           $self->error_message("Error $@");
90 0           return undef;
91             } elsif ( !blessed($response) ) {
92 0           $self->error_message(
93             "Unknown response $response from the REST client instead of object"
94             );
95 0           return undef;
96             }
97             print "Got response:"
98             . Dumper( $response->responseCode() ) . "/"
99             . Dumper( $response->responseContent() ) . "\n"
100 0 0         if $ENV{DEBUG};
101 0           my $code = $response->responseCode();
102 0           my $parsed_content = eval { decode_json( $response->responseContent() ) };
  0            
103 0 0         if ($@) {
104 0           cluck( "Cannot parse response content "
105             . $response->responseContent()
106             . ", error msg: $@. Is this JSON?" );
107 0           $parsed_content = {};
108             }
109 0 0         print "parsed " . Dumper($parsed_content) if $ENV{DEBUG};
110 0 0 0       if ( $code ne '200' && $code ne '201' ) {
111 0           my $err = "Received error code $code from the server instead of "
112             . 'expected 200/201';
113 0 0 0       if ( reftype($parsed_content) eq 'HASH'
114             && $parsed_content->{message} )
115             {
116             $err .=
117             "\nError message from server: "
118             . $parsed_content->{message}
119             . (
120             $parsed_content->{error_code}
121 0 0         ? ' (' . $parsed_content->{error_code} . ')'
122             : q{}
123             );
124              
125 0           $self->error_message($err);
126             }
127 0           return undef;
128             }
129              
130 0           $self->error_message(q{});
131 0 0 0       if (reftype($parsed_content) eq 'HASH' && $parsed_content->{datafinder}) {
132 0           return $parsed_content->{datafinder};
133             }
134 0           return $parsed_content;
135             }
136              
137             sub _cache_file_name {
138 0     0     my ( $self, $query_params, $data ) = @_;
139            
140 0           my $md5 = md5_hex(Dumper($query_params).Dumper($data));
141 0           my $fname = catdir($self->cache_dir, substr($md5, 0, 2));
142 0 0         unless ( -d $fname ) {
143 0           my $err;
144 0 0         unless (
145             make_path(
146             $fname,
147             {
148             mode => 0700,
149             error => \$err
150             }
151             )
152             )
153             {
154 0           warn(
155             "Cannot create cache directory : $fname ($err),".
156             " caching turned off");
157 0           $self->cache_time(0);
158             }
159             }
160            
161 0           $fname = catfile($fname, "$md5.stor");
162 0           return $fname;
163             }
164              
165             sub _transaction {
166 0     0     my ( $self, $query_params, $data ) = @_;
167              
168 0   0       $data //= {};
169 0 0         $query_params->{k2} = $self->api_key unless $query_params->{k2};
170 0           my $url = $self->_url($query_params);
171 0           my $headers = { 'Content-Type' => 'application/json' };
172 0           my $response;
173             # print "JSON data ".encode_json($data);
174              
175 0           my $f = $self->_cache_file_name($query_params, $data);
176 0 0 0       if ($self->cache_time && -s $f) {
177             # there is a cache file!
178 0           my $t = (stat($f))[9];
179 0 0         if ($t + $self->cache_time > time()) {
180             # recent enough
181 0           my $data = eval { retrieve($f); };
  0            
182 0 0 0       if (!$@ && $data) {
183 0 0 0       if ($data->{errors} || $data->{datafinder}->{errors}) {
184 0 0         print "Cached object $f contains error response - removing\n" if $ENV{DEBUG};
185 0           unlink($f);
186             } else {
187 0 0         print "Retrieved ".Dumper($query_params).Dumper($data)." from cache $f\n" if $ENV{DEBUG};
188 0           $data->{cached} = $t;
189 0           return $data;
190             }
191             }
192             } else {
193             # too old
194 0 0         print "Cached object $f is too old - removing\n" if $ENV{DEBUG};
195 0           unlink($f);
196             }
197             }
198              
199 0           for my $try ( 1 .. $self->retries ) {
200             $response =
201 0           eval {
202             print "Sent request to $url\n".
203             "Headers: ".
204             JSON::XS->new->pretty(1)->encode($headers)."\n".
205             "Post data: ".
206 0 0         JSON::XS->new->pretty(1)->encode($data) if $ENV{DEBUG};
207 0           $self->ua->POST( $url, encode_json($data), $headers );
208             };
209 0 0         if ($@) {
210 0           cluck($@);
211 0           sleep( int( 1 + rand() * 3 ) * $try );
212             } else {
213 0           last;
214             }
215             }
216            
217 0           my $res = $self->_process_response($response);
218              
219             # all is good, perhaps we should cache it?
220 0 0 0       if ($res && $self->cache_time) {
221              
222 0 0         unless ($res->{errros}) {
223 0           nstore($res, $f);
224 0 0         print "Stored result in cache file $f\n" if $ENV{DEBUG};
225             }
226             }
227              
228 0           return $res;
229             }
230              
231             =head1 SYNOPSIS
232              
233             use WWW::Datafinder;
234             use Text::CSV_XS;
235             use Data::Dumper;
236              
237             my $csv = Text::CSV_XS->new;
238             my $df = WWW::Datafinder->new( {
239             api_key => '456', # place real API key here
240             cache_dir => '/var/tmp/datafinder',
241             cache_time => 3600 * 24 * 14
242             }) or die 'Cannot create Datafinder object';
243              
244             # process a CSV file with 6 columns:
245             # First Name, Last Name, Address, City, State, ZIP
246             while(<>) {
247             chomp;
248             my $status = $csv->parse($_);
249             unless ($status) {
250             warn qq{Cannot parse '$_':}.$csv->error_diag();
251             next;
252             }
253             my ($name, $surname, $addr, $city, $state, $zip) = $csv->fields();
254             my $data = {
255             d_first => $name,
256             d_last => $surname,
257             d_fulladdr => $addr,
258             d_city => $city,
259             d_state => $state,
260             d_zip => $zip
261             };
262             my $res = $df->append_email($data);
263             if ($res) {
264             if ( $res->{'num-results'} ) {
265             # there is a match!
266             print "Got a match for $name $surname: " . Dumper( $res->{results} );
267             }
268             }
269             }
270            
271             =head1 CONSTRUCTOR
272              
273             =head2 new( hashref )
274              
275             Creates a new object, acceptable parameters are:
276              
277             =over 16
278              
279             =item C - (required) the key to be used for read operations
280              
281             =item C - how many times retry the request upon error (e.g. timeout). Default is 5.
282              
283             =item C - for how long the cached result is valid(in seconds). 0 (default) turns caching off.
284              
285             =item C - directory where cache files are stored, default is /var/tmp/datafinder-cache
286              
287             =back
288              
289             =head1 METHODS
290              
291             =head2 append_email( $data )
292              
293             Attempts to append customer's email based on his/her name and address (or phone
294             number). Please see L<< https://datafinder.com/api/docs-demo >> for more
295             info regarding the parameter names and format of their values in C<$data>.
296             Returns a reference to a hash, which contains the response
297             received from the server.
298             Returns C on failure, application then may call
299             C method to get the detailed info about the error.
300              
301             my $res = $df->append_email(
302             {
303             d_fulladdr => $cust->{Address},
304             d_city => $cust->{City},
305             d_state => $cust->{State},
306             d_zip => $cust->{ZIP},
307             d_first => $cust->{Name},
308             d_last => $cust->{Surname}
309             }
310             );
311             if ( $res ) {
312             if ( $res->{'num-results'} ) {
313             # there is a match!
314             print "Got a match: " . Dumper( $res->{results} );
315             }
316             } else {
317             warn 'Something went wrong ' . $df->error_message();
318             }
319              
320             =cut
321              
322             sub append_email {
323 0     0 1   my ( $self, $data ) = @_;
324 0           $data->{service} = 'email';
325              
326 0           return $self->_transaction( $data, {} );
327             }
328              
329             =head2 append_email( $data )
330              
331             Attempts to append customer's phone number based on his/her name and address
332             Please see L<< https://datafinder.com/api/docs-demo >> for more
333             info regarding the parameter names and format of their values in C<$data>.
334             Returns a reference to a hash, which contains the response
335             received from the server.
336             Returns C on failure, application then may call
337             C method to get the detailed info about the error.
338              
339             my $res = $df->append_phone(
340             {
341             d_fulladdr => $cust->{Address},
342             d_city => $cust->{City},
343             d_state => $cust->{State},
344             d_zip => $cust->{ZIP},
345             d_first => $cust->{Name},
346             d_last => $cust->{Surname}
347             }
348             );
349             if ( $res ) {
350             if ( $res->{'num-results'} ) {
351             # there is a match!
352             print "Got a match: " . Dumper( $res->{results} );
353             }
354             } else {
355             warn 'Something went wrong ' . $df->error_message();
356             }
357              
358             =cut
359              
360             sub append_phone {
361 0     0 0   my ( $self, $data ) = @_;
362 0           $data->{service} = 'phone';
363              
364 0           return $self->_transaction( $data, {} );
365             }
366              
367             =head2 error_message()
368              
369             Returns the detailed explanation of the last error. Empty string if
370             everything went fine.
371              
372             my $res = $df->append_email($cust_data);
373             unless ($res) {
374             warn 'Something went wrong '.$df->error_message();
375             }
376              
377             If you want to troubleshoot the data being sent between the client and the
378             server - set environment variable DEBUG to a positive value.
379              
380             =cut
381              
382             =head1 AUTHOR
383              
384             Andrew Zhilenko, C<< >>
385             (c) Putin Huylo LLC, 2017
386              
387             =head1 BUGS
388              
389             Please report any bugs or feature requests to C, or through
390             the web interface at L.
391             I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
392              
393             =head1 SUPPORT
394              
395             You can find documentation for this module with the perldoc command.
396              
397             perldoc WWW::Datafinder
398              
399              
400             You can also look for information at:
401              
402             =over 4
403              
404             =item * RT: CPAN's request tracker (report bugs here)
405              
406             L
407              
408             =item * AnnoCPAN: Annotated CPAN documentation
409              
410             L
411              
412             =back
413              
414             =head1 LICENSE AND COPYRIGHT
415              
416             Copyright 2017 Putin Huylo LLC
417              
418             This program is free software; you can redistribute it and/or modify it
419             under the terms of the the Artistic License (2.0). You may obtain a
420             copy of the full license at:
421              
422             L
423              
424             Any use, modification, and distribution of the Standard or Modified
425             Versions is governed by this Artistic License. By using, modifying or
426             distributing the Package, you accept this license. Do not use, modify,
427             or distribute the Package, if you do not accept this license.
428              
429             If your Modified Version has been derived from a Modified Version made
430             by someone other than you, you are nevertheless required to ensure that
431             your Modified Version complies with the requirements of this license.
432              
433             This license does not grant you the right to use any trademark, service
434             mark, tradename, or logo of the Copyright Holder.
435              
436             This license includes the non-exclusive, worldwide, free-of-charge
437             patent license to make, have made, use, offer to sell, sell, import and
438             otherwise transfer the Package with respect to any patent claims
439             licensable by the Copyright Holder that are necessarily infringed by the
440             Package. If you institute patent litigation (including a cross-claim or
441             counterclaim) against any party alleging that the Package constitutes
442             direct or contributory patent infringement, then this Artistic License
443             to you shall terminate on the date that such litigation is filed.
444              
445             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
446             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
447             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
448             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
449             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
450             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
451             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
452             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
453              
454              
455             =cut
456              
457             __PACKAGE__->meta->make_immutable;
458              
459             1; # End of WWW::Datafinder