File Coverage

blib/lib/WWW/Datafinder.pm
Criterion Covered Total %
statement 47 131 35.8
branch 0 48 0.0
condition 0 25 0.0
subroutine 16 23 69.5
pod 3 3 100.0
total 66 230 28.7


line stmt bran cond sub pod time code
1             package WWW::Datafinder;
2              
3 2     2   39196 use 5.010;
  2         4  
4 2     2   7 use strict;
  2         1  
  2         30  
5 2     2   17 use warnings;
  2         6  
  2         96  
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.03
14              
15             =cut
16              
17             our $VERSION = '0.03';
18              
19 2     2   12 use Carp qw(cluck);
  2         3  
  2         731  
20 2     2   1155 use Data::Dumper;
  2         15093  
  2         89  
21 2     2   892 use REST::Client;
  2         66619  
  2         49  
22 2     2   1206 use JSON::XS;
  2         7542  
  2         98  
23 2     2   9 use URI;
  2         2  
  2         38  
24 2     2   5 use Scalar::Util qw(blessed reftype);
  2         2  
  2         116  
25 2     2   1017 use Readonly;
  2         5226  
  2         86  
26 2     2   9 use Exporter 'import';
  2         2  
  2         43  
27 2     2   6 use File::Path qw(make_path);
  2         2  
  2         88  
28 2     2   844 use File::Spec::Functions qw(catfile catdir splitpath);
  2         1313  
  2         135  
29 2     2   15 use Digest::MD5 qw(md5 md5_hex);
  2         3  
  2         94  
30 2     2   6 use Storable qw(nstore retrieve dclone);
  2         2  
  2         100  
31              
32 2     2   747 use Mouse;
  2         37417  
  2         7  
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,
142             substr($md5, 0, 2),
143             substr($md5, 2, 2));
144 0 0         unless ( -d $fname ) {
145 0           my $err;
146 0 0         unless (
147             make_path(
148             $fname,
149             {
150             mode => 0700,
151             error => \$err
152             }
153             )
154             )
155             {
156 0           warn(
157             "Cannot create cache directory : $fname ($err),".
158             " caching turned off");
159 0           $self->cache_time(0);
160             }
161             }
162            
163 0           $fname = catfile($fname, "$md5.stor");
164 0           return $fname;
165             }
166              
167             sub _transaction {
168 0     0     my ( $self, $query_params, $data ) = @_;
169              
170 0   0       $data //= {};
171 0 0         $query_params->{k2} = $self->api_key unless $query_params->{k2};
172 0           my $url = $self->_url($query_params);
173 0           my $headers = { 'Content-Type' => 'application/json' };
174 0           my $response;
175             # print "JSON data ".encode_json($data);
176              
177 0           my $f = $self->_cache_file_name($query_params, $data);
178 0 0 0       if ($self->cache_time && -s $f) {
179             # there is a cache file!
180 0           my $t = (stat($f))[9];
181 0 0         if ($t + $self->cache_time > time()) {
182             # recent enough
183 0           my $data = eval { retrieve($f); };
  0            
184 0 0 0       if (!$@ && $data) {
185 0 0 0       if ($data->{errors} || $data->{datafinder}->{errors}) {
186 0 0         print "Cached object $f contains error response - removing\n" if $ENV{DEBUG};
187 0           unlink($f);
188             } else {
189 0 0         print "Retrieved ".Dumper($query_params).Dumper($data)." from cache $f\n" if $ENV{DEBUG};
190 0           $data->{cached} = $t;
191 0           $data->{cache_object} = $f;
192 0           return $data;
193             }
194             }
195             } else {
196             # too old
197 0 0         print "Cached object $f is too old - removing\n" if $ENV{DEBUG};
198 0           unlink($f);
199             }
200             }
201              
202 0           for my $try ( 1 .. $self->retries ) {
203             $response =
204 0           eval {
205             print "Sent request to $url\n".
206             "Headers: ".
207             JSON::XS->new->pretty(1)->encode($headers)."\n".
208             "Post data: ".
209 0 0         JSON::XS->new->pretty(1)->encode($data) if $ENV{DEBUG};
210 0           $self->ua->POST( $url, encode_json($data), $headers );
211             };
212 0 0         if ($@) {
213 0           cluck($@);
214 0           sleep( int( 1 + rand() * 3 ) * $try );
215             } else {
216 0           last;
217             }
218             }
219            
220 0           my $res = $self->_process_response($response);
221              
222             # all is good, perhaps we should cache it?
223 0 0 0       if ($res && $self->cache_time) {
224              
225 0 0         unless ($res->{erros}) {
226 0           nstore($res, $f);
227 0 0         print "Stored result in cache file $f\n" if $ENV{DEBUG};
228             }
229             }
230              
231 0           return $res;
232             }
233              
234             =head1 SYNOPSIS
235              
236             use WWW::Datafinder;
237             use Text::CSV_XS;
238             use Data::Dumper;
239              
240             my $csv = Text::CSV_XS->new;
241             my $df = WWW::Datafinder->new( {
242             api_key => '456', # place a real API key here
243             cache_dir => '/var/tmp/datafinder',
244             cache_time => 3600 * 24 * 14
245             }) or die 'Cannot create Datafinder object';
246              
247             # process a CSV file with 6 columns:
248             # First Name, Last Name, Address, City, State, ZIP
249             while(<>) {
250             chomp;
251             my $status = $csv->parse($_);
252             unless ($status) {
253             warn qq{Cannot parse '$_':}.$csv->error_diag();
254             next;
255             }
256             my ($name, $surname, $addr, $city, $state, $zip) = $csv->fields();
257             my $data = {
258             d_first => $name,
259             d_last => $surname,
260             d_fulladdr => $addr,
261             d_city => $city,
262             d_state => $state,
263             d_zip => $zip
264             };
265             my $res = $df->append_email($data);
266             if ($res) {
267             if ( $res->{'num-results'} ) {
268             # there is a match!
269             print "Got a match for $name $surname: " . Dumper( $res->{results} );
270             }
271             }
272             }
273            
274             =head1 CONSTRUCTOR
275              
276             =head2 new( hashref )
277              
278             Creates a new object, acceptable parameters are:
279              
280             =over 16
281              
282             =item C - (required) the key to be used for read operations
283              
284             =item C - how many times retry the request upon error (e.g. timeout). Default is 5.
285              
286             =item C - for how long the cached result is valid(in seconds). 0 (default) turns caching off.
287              
288             =item C - directory where cache files are stored, default is /var/tmp/datafinder-cache
289              
290             =back
291              
292             =head1 METHODS
293              
294             =head2 append_email( $data )
295              
296             Attempts to append customer's email based on his/her name and address (or phone
297             number). Please see L<< https://datafinder.com/api/docs-demo >> for more
298             info regarding the parameter names and format of their values in C<$data>.
299             Returns a reference to a hash, which contains the response
300             received from the server.
301             Returns C on failure, application then may call
302             C method to get the detailed info about the error.
303              
304             my $res = $df->append_email(
305             {
306             d_fulladdr => $cust->{Address},
307             d_city => $cust->{City},
308             d_state => $cust->{State},
309             d_zip => $cust->{ZIP},
310             d_first => $cust->{Name},
311             d_last => $cust->{Surname}
312             }
313             );
314             if ( $res ) {
315             if ( $res->{'num-results'} ) {
316             # there is a match!
317             print "Got a match: " . Dumper( $res->{results} );
318             }
319             } else {
320             warn 'Something went wrong ' . $df->error_message();
321             }
322              
323             =cut
324              
325             sub append_email {
326 0     0 1   my ( $self, $data ) = @_;
327 0           $data->{service} = 'email';
328              
329 0           return $self->_transaction( $data, {} );
330             }
331              
332             =head2 append_phone( $data )
333              
334             Attempts to append customer's phone number based on his/her name and address
335             Please see L<< https://datafinder.com/api/docs-demo >> for more
336             info regarding the parameter names and format of their values in C<$data>.
337             Returns a reference to a hash, which contains the response
338             received from the server.
339             Returns C on failure, application then may call
340             C method to get the detailed info about the error.
341              
342             my $res = $df->append_phone(
343             {
344             d_fulladdr => $cust->{Address},
345             d_city => $cust->{City},
346             d_state => $cust->{State},
347             d_zip => $cust->{ZIP},
348             d_first => $cust->{Name},
349             d_last => $cust->{Surname}
350             }
351             );
352             if ( $res ) {
353             if ( $res->{'num-results'} ) {
354             # there is a match!
355             print "Got a match: " . Dumper( $res->{results} );
356             }
357             } else {
358             warn 'Something went wrong ' . $df->error_message();
359             }
360              
361             =cut
362              
363             sub append_phone {
364 0     0 1   my ( $self, $data ) = @_;
365 0           $data->{service} = 'phone';
366              
367 0           return $self->_transaction( $data, {} );
368             }
369              
370             =head2 append_demograph( $data )
371              
372             Attempts to append customer's demographic data on his/her name and address
373             Please see L<< https://datafinder.com/api/docs-demo >> for more
374             info regarding the parameter names and format of their values in C<$data>.
375             Returns a reference to a hash, which contains the response
376             received from the server.
377             Returns C on failure, application then may call
378             C method to get the detailed info about the error.
379              
380             my $res = $df->append_demograph(
381             {
382             d_fulladdr => $cust->{Address},
383             d_city => $cust->{City},
384             d_state => $cust->{State},
385             d_zip => $cust->{ZIP},
386             d_first => $cust->{Name},
387             d_last => $cust->{Surname}
388             }
389             );
390             if ( $res ) {
391             if ( $res->{'num-results'} ) {
392             # there is a match!
393             print "Got a match: " . Dumper( $res->{results} );
394             }
395             } else {
396             warn 'Something went wrong ' . $df->error_message();
397             }
398              
399             =cut
400              
401             sub append_demograph {
402 0     0 1   my ( $self, $data ) = @_;
403 0           $data->{service} = 'demograph';
404              
405 0           return $self->_transaction( $data, {} );
406             }
407              
408              
409             =head2 error_message()
410              
411             Returns the detailed explanation of the last error. Empty string if
412             everything went fine.
413              
414             my $res = $df->append_email($cust_data);
415             unless ($res) {
416             warn 'Something went wrong '.$df->error_message();
417             }
418              
419             If you want to troubleshoot the data being sent between the client and the
420             server - set environment variable DEBUG to a positive value.
421              
422             =cut
423              
424             =head1 CACHING
425              
426             The returned results can be cached in a local file, so the next time you
427             want to retrieve the data for the same person, you get a faster response and
428             do not have to spend money. Negative results (no match) are cached as well
429             to speed things up.
430              
431             The hash key for a request is calculated as MD5 of all request parameters
432             (including API key). The result is stored in a file (via Storable).
433             Files are organized in the two-level directory structure (so
434             fea5ffd3d65ee4d8bdf630677c0c5ff6.stor goes into /var/tmp/datafinder-cache/fe/a5/)
435             to accommodate potentially large amount of files.
436              
437             When a result is returned from the cache instead of the real server, it has
438             the C< cached > key set to the timestamp of the original data retrieval and
439             C< cache_object > set to the filename of the cache file.
440              
441             =head1 AUTHOR
442              
443             Andrew Zhilenko, C<< >>
444             (c) Putin Huylo LLC, 2017
445              
446             =head1 BUGS
447              
448             Please report any bugs or feature requests to C, or through
449             the web interface at L.
450             I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
451              
452             =head1 SUPPORT
453              
454             You can find documentation for this module with the perldoc command.
455              
456             perldoc WWW::Datafinder
457              
458              
459             You can also look for information at:
460              
461             =over 4
462              
463             =item * RT: CPAN's request tracker (report bugs here)
464              
465             L
466              
467             =item * AnnoCPAN: Annotated CPAN documentation
468              
469             L
470              
471             =back
472              
473             =head1 LICENSE AND COPYRIGHT
474              
475             Copyright 2017 Putin Huylo LLC
476              
477             This program is free software; you can redistribute it and/or modify it
478             under the terms of the the Artistic License (2.0). You may obtain a
479             copy of the full license at:
480              
481             L
482              
483             Any use, modification, and distribution of the Standard or Modified
484             Versions is governed by this Artistic License. By using, modifying or
485             distributing the Package, you accept this license. Do not use, modify,
486             or distribute the Package, if you do not accept this license.
487              
488             If your Modified Version has been derived from a Modified Version made
489             by someone other than you, you are nevertheless required to ensure that
490             your Modified Version complies with the requirements of this license.
491              
492             This license does not grant you the right to use any trademark, service
493             mark, tradename, or logo of the Copyright Holder.
494              
495             This license includes the non-exclusive, worldwide, free-of-charge
496             patent license to make, have made, use, offer to sell, sell, import and
497             otherwise transfer the Package with respect to any patent claims
498             licensable by the Copyright Holder that are necessarily infringed by the
499             Package. If you institute patent litigation (including a cross-claim or
500             counterclaim) against any party alleging that the Package constitutes
501             direct or contributory patent infringement, then this Artistic License
502             to you shall terminate on the date that such litigation is filed.
503              
504             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
505             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
506             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
507             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
508             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
509             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
510             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
511             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
512              
513              
514             =cut
515              
516             __PACKAGE__->meta->make_immutable;
517              
518             1; # End of WWW::Datafinder