File Coverage

blib/lib/Catmandu/Importer.pm
Criterion Covered Total %
statement 102 107 95.3
branch 32 38 84.2
condition 18 20 90.0
subroutine 17 19 89.4
pod 0 2 0.0
total 169 186 90.8


line stmt bran cond sub pod time code
1              
2             use Catmandu::Sane;
3 35     35   175490  
  35         76  
  35         220  
4             our $VERSION = '1.2018';
5              
6             use Catmandu::Util qw(io is_value is_string is_array_ref is_hash_ref);
7 35     35   221 use Catmandu::Util::Path qw(as_path);
  35         63  
  35         2667  
8 35     35   9604 use LWP::UserAgent;
  35         177  
  35         1900  
9 35     35   21936 use HTTP::Request ();
  35         1253967  
  35         1193  
10 35     35   278 use URI ();
  35         74  
  35         438  
11 35     35   151 use URI::Template ();
  35         72  
  35         442  
12 35     35   15027 use Moo::Role;
  35         162817  
  35         995  
13 35     35   471 use namespace::clean;
  35         86  
  35         543  
14 35     35   14473  
  35         75  
  35         300  
15             with 'Catmandu::Logger';
16             with 'Catmandu::Iterable';
17             with 'Catmandu::IterableOnce';
18             with 'Catmandu::Fixable';
19             with 'Catmandu::Serializer';
20              
21             around generator => sub {
22             my ($orig, $self) = @_;
23              
24             my $generator = $orig->($self);
25              
26             if (my $fixer = $self->_fixer) {
27             $generator = $fixer->fix($generator);
28             }
29              
30             if (defined(my $path = $self->data_path)) {
31             my $getter = as_path($path)->getter;
32             return sub {
33             state @buf;
34             while (1) {
35             return shift @buf if @buf;
36             @buf = @{$getter->($generator->() // return)};
37             next;
38             }
39             };
40             }
41              
42             $generator;
43             };
44              
45             has file => (is => 'lazy', init_arg => undef);
46             has _file_template =>
47             (is => 'ro', predicate => 'has_file', init_arg => 'file');
48             has variables => (is => 'ro', predicate => 1);
49             has fh => (is => 'ro', lazy => 1, builder => 1);
50             has encoding => (is => 'ro', builder => 1);
51             has data_path => (is => 'ro');
52             has user_agent => (is => 'ro');
53             has http_method => (is => 'lazy');
54             has http_headers => (is => 'lazy');
55             has http_agent => (is => 'ro', predicate => 1);
56             has http_max_redirect => (is => 'ro', predicate => 1);
57             has http_timeout => (is => 'ro', default => sub {180}); # LWP default
58             has http_verify_hostname => (is => 'ro', default => sub {1});
59             has http_retry => (is => 'ro', predicate => 1);
60             has http_timing => (is => 'ro', predicate => 1);
61             has http_body => (is => 'ro', predicate => 1);
62             has _http_client => (
63             is => 'ro',
64             lazy => 1,
65             builder => '_build_http_client',
66             init_arg => 'user_agent'
67             );
68             has _http_timing_tries => (is => 'lazy');
69             has ignore_404 => (is => 'ro');
70              
71             ':utf8';
72             }
73 98     98   117897  
74             my ($self) = @_;
75             return \*STDIN unless $self->has_file;
76             my $file = $self->_file_template;
77 65     65   675 if (is_string($file) && $self->has_variables) {
78 65 50       228 my $template = URI::Template->new($file);
79 65         145 my $vars = $self->variables;
80 65 100 100     373 if (is_value($vars)) {
81 6         41 $vars = [split ',', $vars];
82 6         506 }
83 6 100       22 if (is_array_ref($vars)) {
84 3         11 my @keys = $template->variables;
85             my @vals = @$vars;
86 6 100       16 $vars = {};
87 4         11 $vars->{shift @keys} = shift @vals while @keys && @vals;
88 4         46 }
89 4         8 $file = $template->process_to_string(%$vars);
90 4   66     25 }
91             if (is_string($file) && $file !~ m!^https?://! && !-r $file) {
92 6         18 Catmandu::BadArg->throw("file '$file' doesn't exist");
93             }
94 65 100 100     1798 $file;
      100        
95 1         20 }
96              
97 64         295 my ($self) = @_;
98              
99             my $file = $self->file;
100              
101 62     62   1216 # get remote content
102             if (is_string($file) && $file =~ m!^https?://!) {
103 62         854 my $body;
104             if ($self->has_http_body) {
105             $body = $self->http_body;
106 61 100 100     319  
107 13         22 if (ref $body) {
108 13 100       41 $body = $self->serialize($body);
109 3         7 }
110              
111 3 50       8 if ($self->has_variables) {
112 0         0 my $vars = $self->variables;
113             if (is_hash_ref($vars)) { # named variables
114             for my $key (keys %$vars) {
115 3 50       7 my $var = $vars->{$key};
116 3         6 $body =~ s/{$key}/$var/;
117 3 100       10 }
118 1         3 }
119 1         2 else { # positional variables
120 1         17 if (is_value($vars)) {
121             $vars = [split ',', $vars];
122             }
123             for my $var (@$vars) {
124 2 50       6 $body =~ s/{\w+}/$var/;
125 2         6 }
126             }
127 2         6 }
128 6         15 }
129              
130             my $content = $self->_http_request(
131             $self->http_method, $file, $self->http_headers,
132             $body, $self->_http_timing_tries,
133             );
134 13         198  
135             return io(\$content, mode => 'r', binmode => $_[0]->encoding);
136             }
137              
138             io($file, mode => 'r', binmode => $_[0]->encoding);
139 8         86 }
140              
141             [];
142 48         253 }
143              
144             'GET';
145             }
146 13     13   317  
147             my ($self) = @_;
148              
149             if ($self->has_http_timing) {
150 10     10   208 my @timing_tries = $self->http_timing =~ /(\d+(?:\.\d+)*)/g;
151             return \@timing_tries;
152             }
153             elsif ($self->has_http_retry) {
154 13     13   103 my @timing_tries = (1) x $self->http_retry;
155             return \@timing_tries;
156 13 100       55 }
    100          
157 2         19 return;
158 2         14 }
159              
160             my ($self) = @_;
161 2         10 my $ua = LWP::UserAgent->new;
162 2         9 $ua->timeout($self->http_timeout);
163             $ua->agent($self->http_agent) if $self->has_http_agent;
164 9         46 $ua->max_redirect($self->http_max_redirect)
165             if $self->has_http_max_redirect;
166             $ua->ssl_opts(verify_hostname => $self->http_verify_hostname);
167             $ua->protocols_allowed([qw(http https)]);
168 2     2   39 $ua->env_proxy;
169 2         17 $ua;
170 2         3954 }
171 2 50       49  
172 2 50       13 my ($self, $method, $url, $headers, $body, $timing_tries) = @_;
173              
174 2         17 my $client = $self->_http_client;
175 2         66  
176 2         27 my $req = HTTP::Request->new($method, $url, $headers || []);
177 2         15736 $req->content($body) if defined $body;
178              
179             my $res = $client->request($req);
180              
181 13     13   36 if ($res->code =~ /^408|500|502|503|504$/ && $timing_tries) {
182             my @tries = @$timing_tries;
183 13         183 while (my $sleep = shift @tries) {
184             sleep $sleep;
185 13   50     150 $res = $client->request($req->clone);
186 13 100       14876 last if $res->code !~ /^408|500|502|503|504$/;
187             }
188 13         95 }
189              
190 13 100 100     136997 my $res_body = $res->decoded_content;
191 4         65  
192 4         12 unless ($res->is_success) {
193 6         6001155 my $res_headers = [];
194 6         312 for my $header ($res->header_field_names) {
195 6 100       17379 my $val = $res->header($header);
196             push @$res_headers, $header, $val;
197             }
198             Catmandu::HTTPError->throw(
199 13         284 {
200             code => $res->code,
201 13 100       9716 message => $res->status_line,
202 5         73 url => $url,
203 5         41 method => $method,
204 18         355 request_headers => $headers,
205 18         699 request_body => $body,
206             response_headers => $res_headers,
207             response_body => $res_body,
208             }
209 5         23 );
210             }
211              
212             $res_body;
213             }
214              
215             warnings::warnif("deprecated",
216             "readline is deprecated, fh->getline instead");
217             $_[0]->fh->getline;
218             }
219              
220             warnings::warnif("deprecated",
221 8         111 "readall is deprecated, join('',fh->getlines) instead");
222             join '', $_[0]->fh->getlines;
223             }
224              
225 0     0 0   1;
226              
227 0            
228             =pod
229              
230             =head1 NAME
231 0     0 0    
232             Catmandu::Importer - Namespace for packages that can import
233 0            
234             =head1 SYNOPSIS
235              
236             # From the command line
237              
238             # JSON is an importer and YAML an exporter
239             $ catmandu convert JSON to YAML < data.json
240              
241             # OAI is an importer and JSON an exporter
242             $ catmandu convert OAI --url http://biblio.ugent.be/oai to JSON
243              
244             # Fetch remote content
245             $ catmandu convert JSON --file http://example.com/data.json to YAML
246            
247             # From Perl
248            
249             use Catmandu;
250             use Data::Dumper;
251              
252             my $importer = Catmandu->importer('JSON', file => 'data.json');
253              
254             $importer->each(sub {
255             my $item = shift;
256             print Dumper($item);
257             });
258              
259             my $num = $importer->count;
260              
261             my $first_item = $importer->first;
262              
263             # Convert OAI to JSON in Perl
264             my $importer = Catmandu->importer('OAI', url => 'http://biblio.ugent.be/oai');
265             my $exporter = Catmandu->exporter('JSON');
266              
267             $exporter->add_many($importer);
268              
269             =head1 DESCRIPTION
270              
271             A Catmandu::Importer is a Perl package that can generate structured data from
272             sources such as JSON, YAML, XML, RDF or network protocols such as Atom, OAI-PMH,
273             SRU and even DBI databases. Given an Catmandu::Importer a programmer can read
274             data from using one of the many L<Catmandu::Iterable> methods:
275              
276              
277             $importer->to_array;
278             $importer->count;
279             $importer->each(\&callback);
280             $importer->first;
281             $importer->rest;
282             ...etc...
283              
284             Every Catmandu::Importer is also L<Catmandu::Fixable> and thus inherits a 'fix'
285             parameter that can be set in the constructor. When given a 'fix' parameter, then each
286             item returned by the generator will be automatically Fixed using one or
287             more L<Catmandu::Fix>es.
288             E.g.
289            
290             my $importer = Catmandu->importer('JSON',fix => ['upcase(title)']);
291             $importer->each( sub {
292             my $item = shift ; # Every $item->{title} is now upcased...
293              
294             });
295              
296             # or via a Fix file
297             my $importer = Catmandu->importer('JSON',fix => ['/my/fixes.txt']);
298             $importer->each( sub {
299             my $item = shift ; # Every $item->{title} is now upcased...
300              
301             });
302              
303             =head1 CONFIGURATION
304              
305             =over
306              
307             =item file
308              
309             Read input from a local file given by its path. If the path looks like a
310             url, the content will be fetched first and then passed to the importer.
311             Alternatively a scalar reference can be passed to read from a string.
312              
313             =item fh
314              
315             Read input from an L<IO::Handle>. If not specified, L<Catmandu::Util::io> is used to
316             create the input stream from the C<file> argument or by using STDIN.
317              
318             =item encoding
319              
320             Binmode of the input stream C<fh>. Set to C<:utf8> by default.
321              
322             =item fix
323              
324             An ARRAY of one or more Fix-es or Fix scripts to be applied to imported items.
325              
326             =item data_path
327              
328             The data at C<data_path> is imported instead of the original data.
329              
330             # given this imported item:
331             {abc => [{a=>1},{b=>2},{c=>3}]}
332             # with data_path 'abc', this item gets imported instead:
333             [{a=>1},{b=>2},{c=>3}]
334             # with data_path 'abc.*', 3 items get imported:
335             {a=>1}
336             {b=>2}
337             {c=>3}
338              
339             =item variables
340              
341             Variables given here will interpolate the C<file> and C<http_body> options. The
342             syntax is the same as L<URI::Template>.
343              
344             # named arguments
345             my $importer = Catmandu->importer('JSON',
346             file => 'http://{server}/{path}',
347             variables => {server => 'biblio.ugent.be', path => 'file.json'},
348             );
349              
350             # positional arguments
351             my $importer = Catmandu->importer('JSON',
352             file => 'http://{server}/{path}',
353             variables => 'biblio.ugent.be,file.json',
354             );
355              
356             # or
357             my $importer = Catmandu->importer('JSON',
358             url => 'http://{server}/{path}',
359             variables => ['biblio.ugent.be','file.json'],
360             );
361              
362             # or via the command line
363             $ catmandu convert JSON --file 'http://{server}/{path}' --variables 'biblio.ugent.be,file.json'
364              
365             =back
366              
367             =head1 HTTP CONFIGURATION
368              
369             These options are only relevant if C<file> is a url. See L<LWP::UserAgent> for details about these options.
370              
371             =over
372              
373             =item http_body
374              
375             Set the GET/POST message body.
376              
377             =item http_method
378              
379             Set the type of HTTP request 'GET', 'POST' , ...
380              
381             =item http_headers
382              
383             A reference to a HTTP::Headers objects.
384              
385             =back
386              
387             =head2 Set an own HTTP client
388              
389             =over
390              
391             =item user_agent(LWP::UserAgent->new(...))
392              
393             Set an own HTTP client
394              
395             =back
396              
397             =head2 Alternative set the parameters of the default client
398              
399             =over
400              
401             =item http_agent
402              
403             A string containing the name of the HTTP client.
404              
405             =item http_max_redirect
406              
407             Maximum number of HTTP redirects allowed.
408              
409             =item http_timeout
410              
411             Maximum execution time.
412              
413             =item http_verify_hostname
414              
415             Verify the SSL certificate.
416              
417             =item http_retry
418              
419             Maximum times to retry the HTTP request if it temporarily fails. Default is not
420             to retry. See L<LWP::UserAgent::Determined> for the HTTP status codes
421             that initiate a retry.
422              
423             =item http_timing
424              
425             Maximum times and timeouts to retry the HTTP request if it temporarily fails. Default is not
426             to retry. See L<LWP::UserAgent::Determined> for the HTTP status codes
427             that initiate a retry and the format of the timing value.
428              
429             =back
430              
431             =head1 METHODS
432              
433             =head2 first, each, rest , ...
434              
435             See L<Catmandu::Iterable> for all inherited methods.
436              
437             =head1 CODING
438              
439             Create your own importer by creating a Perl package in the Catmandu::Importer namespace that
440             implements C<Catmandu::Importer>. Basically, you need to create a method 'generate' which
441             returns a callback that creates one Perl hash for each call:
442              
443             my $importer = Catmandu::Importer::Hello->new;
444              
445             $importer->generate(); # record
446             $importer->generate(); # next record
447             $importer->generate(); # undef = end of stream
448              
449             Here is an example of a simple C<Hello> importer:
450              
451             package Catmandu::Importer::Hello;
452              
453             use Catmandu::Sane;
454             use Moo;
455              
456             with 'Catmandu::Importer';
457              
458             sub generator {
459             my ($self) = @_;
460             state $fh = $self->fh;
461             my $n = 0;
462             return sub {
463             $self->log->debug("generating record " . ++$n);
464             my $name = $self->fh->readline;
465             return defined $name ? { "hello" => $name } : undef;
466             };
467             }
468              
469             1;
470              
471             This importer can be called via the command line as:
472              
473             $ catmandu convert Hello to JSON < /tmp/names.txt
474             $ catmandu convert Hello to YAML < /tmp/names.txt
475             $ catmandu import Hello to MongoDB --database_name test < /tmp/names.txt
476              
477             Or, via Perl
478              
479             use Catmandu;
480              
481             my $importer = Catmandu->importer('Hello', file => '/tmp/names.txt');
482             $importer->each(sub {
483             my $items = shift;
484             });
485              
486             =head1 SEE ALSO
487              
488             L<Catmandu::Iterable> , L<Catmandu::Fix> ,
489             L<Catmandu::Importer::CSV>, L<Catmandu::Importer::JSON> , L<Catmandu::Importer::YAML>
490              
491             =cut