File Coverage

lib/Web/NewsAPI.pm
Criterion Covered Total %
statement 66 71 92.9
branch 9 10 90.0
condition 6 10 60.0
subroutine 16 17 94.1
pod 3 3 100.0
total 100 111 90.0


line stmt bran cond sub pod time code
1             package Web::NewsAPI;
2              
3             our $VERSION = '0.002';
4              
5 1     1   4946 use v5.10;
  1         4  
6 1     1   586 use Moose;
  1         453446  
  1         8  
7              
8 1     1   8655 use Readonly;
  1         4183  
  1         58  
9 1     1   11 use LWP;
  1         2  
  1         29  
10 1     1   747 use JSON;
  1         9087  
  1         6  
11 1     1   149 use Carp;
  1         3  
  1         68  
12 1     1   536 use DateTime::Format::ISO8601::Format;
  1         650  
  1         39  
13 1     1   8 use Scalar::Util qw(blessed);
  1         6  
  1         57  
14              
15 1     1   511 use Web::NewsAPI::Result;
  1         4  
  1         45  
16 1     1   641 use Web::NewsAPI::Source;
  1         5  
  1         834  
17              
18             Readonly my $API_BASE_URL => 'https://newsapi.org/v2/';
19              
20             has 'ua' => (
21             isa => 'LWP::UserAgent',
22             is => 'ro',
23             lazy_build => 1,
24             );
25              
26             has 'api_key' => (
27             required => 1,
28             is => 'ro',
29             isa => 'Str',
30             );
31              
32             sub top_headlines {
33 3     3 1 3657 my ($self, %args) = @_;
34              
35             # Collapse the source-param into a comma-separated string,
36             # if it's an array.
37 3         14 $self->_collapse_list( 'source', \%args );
38              
39 3         9 return $self->_make_article_set( 'top-headlines', \%args );
40             }
41              
42             sub everything {
43 2     2 1 2370 my ($self, %args) = @_;
44              
45             # Collapse each source/domain-param into a comma-separated string,
46             # if it's an array.
47 2         6 for my $param (qw(source domains excludeDomains) ) {
48 6         16 $self->_collapse_list( $param, \%args );
49             }
50              
51             # Convert time-params into ISO 8601 strings, if they are DateTime
52             # objects. (And throw an exception if they're something weird.)
53 2         15 my $dt_formatter = DateTime::Format::ISO8601::Format->new;
54 2         32 for my $param (qw(to from)) {
55 4 100 66     71 if ( $args{$param} && ( blessed $args{$param} ) ) {
56 1 50       92 if ($args{$param}->isa('DateTime')) {
57             $args{$param} =
58             $dt_formatter->format_datetime(
59 1         9 $args{$param}
60             );
61             }
62             else {
63 0         0 croak "The '$param' parameter of the 'everything' "
64             . "method must be either a DateTime object or an ISO 8601-"
65             . "formatted time-string. (Got: $args{$param}";
66             }
67             }
68             }
69              
70 2         107 return $self->_make_article_set( 'everything', \%args );
71              
72             }
73              
74             sub _make_article_set {
75 5     5   11 my ($self, $endpoint, $args) = @_;
76              
77             my $article_set = Web::NewsAPI::Result->new(
78             newsapi => $self,
79             api_endpoint => $endpoint,
80             api_args => $args,
81             page => $args->{page} || 1,
82 5   50     58 page_size => $args->{pageSize} || 20,
      50        
83             );
84              
85 5 100       45 if (wantarray) {
86 1         4 return $article_set->articles;
87             }
88             else {
89 4         21 return $article_set;
90             }
91             }
92              
93             sub _collapse_list {
94 9     9   45 my ($self, $param, $args) = @_;
95 9 100 66     44 if ( $args->{$param} && ( ref $args->{$param} eq 'ARRAY' ) ) {
96 1         2 $args->{$param} = join q{,}, @{$args->{$param}};
  1         5  
97             }
98             }
99              
100             sub sources {
101 1     1 1 1004 my ($self, %args) = @_;
102              
103 1         3 my @sources;
104 1         5 my ($source_data_list) = $self->_request( 'sources', 'sources', %args );
105 1         4 for my $source_data ( @$source_data_list ) {
106 5         4784 push @sources, Web::NewsAPI::Source->new( $source_data );
107             }
108              
109 1         1771 return @sources;
110             }
111              
112             sub _build_ua {
113 0     0   0 my $self = shift;
114              
115 0         0 my $ua = LWP::UserAgent->new;
116 0         0 $ua->default_header(
117             'X-Api-Key' => $self->api_key,
118             );
119              
120 0         0 return $ua;
121             }
122              
123             sub _request {
124 6     6   14 my $self = shift;
125 6         22 my ($endpoint, $container, %args) = @_;
126              
127 6         30 my $uri = URI->new( $API_BASE_URL . $endpoint );
128 6         8767 $uri->query( $uri->query_form( \%args ) );
129              
130 6         949 my $response = $self->ua->get( $uri );
131 6 100       10103 if ($response->is_success) {
132 5         44 my $data_ref = decode_json( $response->content );
133 5         233 return ($data_ref->{$container}, $data_ref->{totalResults});
134             }
135             else {
136 1         14 my $code = $response->code;
137 1         12 die "News API responded with an error ($code): " . $response->content;
138             }
139             }
140              
141             1;
142              
143             =head1 NAME
144              
145             Web::NewsAPI - Fetch and search news headlines and sources from News API
146              
147             =head1 SYNOPSIS
148              
149             use Web::NewsAPI;
150             use v5.10;
151              
152             # To use this module, you need to get a free API key from
153             # https://newsapi.org. (The following is a bogus example key that will
154             # not actually work. Try it with your own key instead!)
155             my $api_key = 'deadbeef1234567890f001f001deadbeef';
156              
157             my $newsapi = Web::NewsAPI->new(
158             api_key => $api_key,
159             );
160              
161             say "Here are the top ten headlines from American news sources...";
162             # This will be a Web::NewsAPI::Results object.
163             my $result = $newsapi->top_headlines( country => 'us', pageSize => 10 );
164             for my $article ( $result->articles ) {
165             # Each is a Web::NewsAPI::Article object.
166             say $article->title;
167             }
168              
169             say "Here are the top ten headlines worldwide containing 'chicken'...";
170             my $chicken_heds = $newsapi->everything( q => 'chicken', pageSize => 10 );
171             for my $article ( $chicken_heds->articles ) {
172             # Each is a Web::NewsAPI::Article object.
173             say $article->title;
174             }
175             say "The total number of chicken-flavor articles returned: "
176             . $chicken_heds->total_results;
177              
178              
179             say "Here are some sources for English-language technology news...";
180             my @sources = $newsapi->sources(
181             category => 'technology',
182             language => 'en'
183             );
184             for my $source ( @sources ) {
185             # Each is a Web::NewsAPI::Source object.
186             say $source->name;
187             }
188              
189             =head1 DESCRIPTION
190              
191             This module provides a simple, object-oriented interface to L<the News
192             API|https://newsapi.org>, version 2. It supports that API's three public
193             endpoints, allowing your code to fetch and search current news headlines
194             and sources.
195              
196             =head1 METHODS
197              
198             =head2 Class Methods
199              
200             =head3 new
201              
202             my $newsapi = Web::NewsAPI->new( api_key => $your_api_key );
203              
204             Object constructor. Takes a hash as an argument, whose only recognized
205             key is C<api_key>. This must be set to a valid News API key. You can
206             fetch a key for yourself by registering a free account with News API
207             L<at its website|https://newsapi.org>.
208              
209             Note that the validity of the API key you provide isn't checked until
210             this object (or one of its derivative objects) tries to send a query to
211             News API.
212              
213             =head2 Object Methods
214              
215             Each of these methods will attempt to call News API using the API key
216             you provided during construction. If the call fails, then this module
217             will throw an exception, sharing the error code and message passed back
218             from News API.
219              
220             =head3 everything
221              
222             # Call in scalar context to get a result object.
223             my $chicken_result-> = $newsapi->everything( q => 'chickens' );
224              
225             # Or call in array context to just get one page of articles.
226             my @chicken_stories = $newsapi->everything( q => 'chickens' );
227              
228             In scalar context, returns a L<Web::NewsAPI::Result> object representing
229             news articles matching the query parameters you provide. In array
230             context, it returns one page-worth of L<Web::NewsAPI::Article> objects
231             (equivalent to calling L<Web::NewsAPI::Result/"articles"> on the result
232             object, and then discarding it).
233              
234             In either case, the argument hash must contain I<at least one> of the
235             following keys:
236              
237             =over
238              
239             =item q
240              
241             Keywords or phrases to search for.
242              
243             See L<the News API docs|https://newsapi.org/docs/endpoints/everything>
244             for a complete description of what's allowed here.
245              
246             =item sources
247              
248             I<Either> a comma-separated string I<or> an array reference of News API
249             news source ID strings to limit results from.
250              
251             See L<the News API sources index|https://newsapi.org/sources> for a list
252             of valid source IDs.
253              
254             =item domains
255              
256             I<Either> a comma-separated string I<or> an array reference of domains
257             (e.g. "bbc.co.uk, techcrunch.com, engadget.com") to limit results from.
258              
259             =back
260              
261             You may also provide any of these optional keys:
262              
263             =over
264              
265             =item excludeDomains
266              
267             I<Either> a comma-separated string I<or> an array reference of domains
268             (e.g. "bbc.co.uk, techcrunch.com, engadget.com") to remove from the
269             results.
270              
271             =item from
272              
273             I<Either> an ISO 8601-formatted date-time string I<or> a L<DateTime>
274             object representing the timestamp of the oldest article allowed.
275              
276             =item to
277              
278             I<Either> an ISO 8601-formatted date-time string I<or> a L<DateTime>
279             object representing the timestamp of the most recent article allowed.
280              
281             =item language
282              
283             The 2-letter ISO-639-1 code of the language you want to get headlines
284             for. Possible options include C<ar>, C<de>, C<en>, C<es>, C<fr>, C<he>,
285             C<it>, C<nl>, C<no>, C<pt>, C<ru>, C<se>, C<ud>, and C<zh>.
286              
287             =item sortBy
288              
289             The order to sort the articles in. Possible options: C<relevancy>,
290             C<popularity>, C<publishedAt>. (Default: C<publishedAt>)
291              
292             =item pageSize
293              
294             The number of results to return per page. 20 is the default, 100 is the
295             maximum.
296              
297             =item page
298              
299             Which page of results to return. The default is 1.
300              
301             =back
302              
303             =head3 top_headlines
304              
305             # Call in scalar context to get a result object.
306             my $top_us_headlines = $newsapi->top_headlines( country => 'us' );
307              
308             # Or call in array context to just get one page of articles.
309             my @top_us_headlines = $newsapi->top_headlines( country => 'us' );
310              
311             Like L<"everything">, but limits results only to the latest articles,
312             sorted by recency. (Note that this arguments are a little different, as
313             well.)
314              
315             In scalar context, returns a L<Web::NewsAPI::Result> object representing
316             news articles matching the query parameters you provide. In array
317             context, it returns one page-worth of L<Web::NewsAPI::Article> objects
318             (equivalent to calling L<Web::NewsAPI::Result/"articles"> on the result
319             object, and then discarding it).
320              
321             In either case, the argument hash must contain I<at least one> of the
322             following keys:
323              
324             =over
325              
326             =item country
327              
328             Limit returned headlines to a single country, expressed as a 2-letter
329             ISO 3166-1 code. (See L<the News API
330             documentation|https://newsapi.org/docs/endpoints/top-headlines> for a
331             full list of country codes it supports.)
332              
333             News API will return an error if you mix this with C<sources>.
334              
335             =item category
336              
337             Limit returned headlines to a single category. Possible options include
338             C<business>, C<entertainment>, C<general>, C<health>, C<science>,
339             C<sports>, and C<technology>.
340              
341             News API will return an error if you mix this with C<sources>.
342              
343             =item sources
344              
345             I<Either> a comma-separated string I<or> an array reference of News API
346             news source ID strings to limit results from.
347              
348             See L<the News API sources index|https://newsapi.org/sources> for a list
349             of valid source IDs.
350              
351             News API will return an error if you mix this with C<country> or
352             C<category>.
353              
354             =item q
355              
356             Keywords or a phrase to search for.
357              
358             =back
359              
360             You may also provide either of these optional keys:
361              
362             =over
363              
364             =item pageSize
365              
366             The number of results to return per page (request). 20 is the default,
367             100 is the maximum.
368              
369             =item page
370              
371             Use this to page through the results if the total results found is
372             greater than the page size.
373              
374             =back
375              
376             =head3 sources
377              
378             my @sources = $newsapi->sources( language => 'en' );
379              
380             Returns a number of L<Web::NewsAPI::Source> objects reprsenting News
381             API's news sources.
382              
383             You may provide any of these optional parameters:
384              
385             =over
386              
387             =item category
388              
389             Limit sources to a single category. Possible options include
390             C<business>, C<entertainment>, C<general>, C<health>, C<science>,
391             C<sports>, and C<technology>.
392              
393             =item country
394              
395             Limit sources to a single country, expressed as a 2-letter ISO 3166-1
396             code. (See L<the News API
397             documentation|https://newsapi.org/docs/endpoints/sources> for a full
398             list of country codes it supports.)
399              
400             =item language
401              
402             Limit sources to a single language. Possible options include C<ar>,
403             C<de>, C<en>, C<es>, C<fr>, C<he>, C<it>, C<nl>, C<no>, C<pt>, C<ru>,
404             C<se>, C<ud>, and C<zh>.
405              
406             =back
407              
408             =head1 NOTES AND BUGS
409              
410             This is this module's first release (or nearly so). It works for the
411             author's own use-cases, but it's probably buggy beyond that. Please
412             report issues at L<the module's GitHub
413             site|https://github.com/jmacdotorg/newsapi-perl>. Code and documentation
414             pull requests are very welcome!
415              
416             =head1 AUTHOR
417              
418             Jason McIntosh (jmac@jmac.org)
419              
420             =head1 COPYRIGHT AND LICENSE
421              
422             This software is Copyright (c) 2019 by Jason McIntosh.
423              
424             This is free software, licensed under:
425              
426             The MIT (X11) License