File Coverage

blib/lib/Net/Async/WebSearch/Provider/Serper.pm
Criterion Covered Total %
statement 24 54 44.4
branch 0 20 0.0
condition 0 11 0.0
subroutine 8 13 61.5
pod 3 3 100.0
total 35 101 34.6


line stmt bran cond sub pod time code
1             package Net::Async::WebSearch::Provider::Serper;
2             our $VERSION = '0.002';
3             # ABSTRACT: Serper.dev Google Search API provider
4 1     1   977 use strict;
  1         2  
  1         26  
5 1     1   3 use warnings;
  1         2  
  1         35  
6 1     1   3 use parent 'Net::Async::WebSearch::Provider';
  1         1  
  1         5  
7              
8 1     1   48 use Carp qw( croak );
  1         1  
  1         50  
9 1     1   4 use Future;
  1         2  
  1         15  
10 1     1   3 use JSON::MaybeXS qw( encode_json decode_json );
  1         2  
  1         37  
11 1     1   3 use HTTP::Request ();
  1         2  
  1         49  
12 1     1   5 use Net::Async::WebSearch::Result;
  1         1  
  1         480  
13              
14             sub _init {
15 0     0     my ( $self ) = @_;
16 0 0         croak "Serper provider requires 'api_key'" unless $self->{api_key};
17 0   0       $self->{endpoint} ||= 'https://google.serper.dev/search';
18 0   0       $self->{name} ||= 'serper';
19             }
20              
21 0     0 1   sub endpoint { $_[0]->{endpoint} }
22 0     0 1   sub api_key { $_[0]->{api_key} }
23              
24             sub search {
25 0     0 1   my ( $self, $http, $query, $opts ) = @_;
26 0   0       $opts ||= {};
27 0   0       my $limit = $opts->{limit} || 10;
28              
29 0           my %body = ( q => $query, num => $limit );
30 0 0         $body{gl} = $opts->{region} if defined $opts->{region};
31 0 0         $body{hl} = $opts->{language} if defined $opts->{language};
32 0 0         $body{tbs} = $opts->{tbs} if defined $opts->{tbs};
33              
34 0           my $req = HTTP::Request->new(
35             POST => $self->endpoint,
36             [
37             'X-API-KEY' => $self->api_key,
38             'Content-Type' => 'application/json',
39             'Accept' => 'application/json',
40             'User-Agent' => $self->user_agent_string,
41             ],
42             encode_json(\%body),
43             );
44              
45             return $http->do_request( request => $req )->then(sub {
46 0     0     my ( $resp ) = @_;
47 0 0         unless ( $resp->is_success ) {
48 0           return Future->fail(
49             $self->name.": HTTP ".$resp->status_line, 'websearch', $self->name,
50             );
51             }
52 0           my $data = eval { decode_json( $resp->decoded_content ) };
  0            
53 0 0         if ( my $e = $@ ) {
54 0           return Future->fail( $self->name.": invalid JSON: $e", 'websearch', $self->name );
55             }
56 0           my @out;
57 0           my $rank = 0;
58 0 0         for my $r ( @{ $data->{organic} || [] } ) {
  0            
59 0           $rank++;
60             push @out, Net::Async::WebSearch::Result->new(
61             url => $r->{link},
62             title => $r->{title},
63             snippet => $r->{snippet},
64             provider => $self->name,
65             rank => $r->{position} // $rank,
66             published_at => $r->{date},
67             raw => $r,
68             extra => {
69             ( defined $r->{sitelinks} ? ( sitelinks => $r->{sitelinks} ) : () ),
70 0 0 0       ( defined $r->{imageUrl} ? ( imageUrl => $r->{imageUrl} ) : () ),
    0          
71             },
72             );
73 0 0         last if $rank >= $limit;
74             }
75 0           return Future->done(\@out);
76 0           });
77             }
78              
79             1;
80              
81             __END__