File Coverage

lib/Web/NewsAPI.pm
Criterion Covered Total %
statement 65 70 92.8
branch 9 10 90.0
condition 4 6 66.6
subroutine 15 16 93.7
pod 3 3 100.0
total 96 105 91.4


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