File Coverage

lib/Neo4j/Driver/Net/HTTP/Tiny.pm
Criterion Covered Total %
statement 73 73 100.0
branch 24 24 100.0
condition 16 16 100.0
subroutine 16 16 100.0
pod 0 9 100.0
total 129 138 100.0


line stmt bran cond sub pod time code
1 21     21   328643 use v5.14;
  21         79  
2 21     21   134 use warnings;
  21         38  
  21         1966  
3              
4             package Neo4j::Driver::Net::HTTP::Tiny 1.02;
5             # ABSTRACT: HTTP network adapter for HTTP::Tiny
6              
7              
8             # For documentation, see Neo4j::Driver::Plugin.
9              
10              
11 21     21   127 use Carp qw(croak);
  21         40  
  21         1953  
12             our @CARP_NOT = qw(Neo4j::Driver::Net::HTTP);
13              
14 21     21   15846 use HTTP::Tiny 0.034 ();
  21         1377862  
  21         1229  
15 21     21   12471 use JSON::MaybeXS 1.003003 ();
  21         250220  
  21         936  
16 21     21   3252 use URI 1.25 ();
  21         31463  
  21         647  
17 21     21   132 use URI::Escape 3.26 ();
  21         341  
  21         26443  
18              
19             my %DEFAULT_HEADERS = (
20             'Content-Type' => 'application/json',
21             'X-Stream' => 'true',
22             );
23              
24             # User-Agent: Neo4j-Driver/1.00 Perl HTTP-Tiny/0.090
25             my $AGENT = __PACKAGE__->VERSION;
26             $AGENT = sprintf "Neo4j-Driver%s Perl ", $AGENT ? "/$AGENT" : "";
27              
28              
29             sub new {
30             # uncoverable pod
31 19     19 0 359268 my ($class, $driver) = @_;
32            
33 19         42 my $config = $driver->{config};
34 19         153 my $self = bless {
35             json_coder => JSON::MaybeXS->new( utf8 => 1, allow_nonref => 0 ),
36             }, $class;
37            
38 19         9240 my $uri = $config->{uri};
39 19 100       81 if ( defined $config->{auth} ) {
40             croak "Only HTTP Basic Authentication is supported"
41 6 100       224 if $config->{auth}->{scheme} ne 'basic';
42 5   100     45 my $userid = URI::Escape::uri_escape_utf8 $config->{auth}->{principal} // '';
43 5   100     208 my $passwd = URI::Escape::uri_escape_utf8 $config->{auth}->{credentials} // '';
44 5         179 $uri = $uri->clone;
45 5         100 $uri->userinfo("$userid:$passwd");
46             }
47 18         775 $self->{uri_base} = $uri;
48            
49             my %http_attributes = (
50             agent => $AGENT,
51             default_headers => \%DEFAULT_HEADERS,
52             timeout => $config->{timeout},
53 18         89 );
54            
55 18 100       68 if ( $uri->scheme eq 'https' ) {
56             croak "HTTPS does not support unencrypted communication; use HTTP"
57 7 100 100     347 if defined $config->{encrypted} && ! $config->{encrypted};
58 5         11 $http_attributes{verify_SSL} = 1;
59 5 100       14 if ( defined $config->{trust_ca} ) {
60             croak sprintf "trust_ca file '%s' can't be used: %s", $config->{trust_ca}, $!
61 2 100       148 unless open my $fh, '<', $config->{trust_ca};
62 1         11 $http_attributes{SSL_options}->{SSL_ca_file} = $config->{trust_ca};
63             }
64             }
65             else {
66             croak "HTTP does not support encrypted communication; use HTTPS"
67 11 100       400 if $config->{encrypted};
68             }
69            
70 13         92 $self->{http} = HTTP::Tiny->new( %http_attributes );
71            
72 13         1698 return $self;
73             }
74              
75              
76             # Return server base URL as string or URI object (for ServerInfo).
77             sub uri {
78             # uncoverable pod
79 9     9 0 3789 my $self = shift;
80            
81 9         86 return $self->{uri_base};
82             }
83              
84              
85             # Return a JSON:XS-compatible coder object (for result parsers).
86             sub json_coder {
87             # uncoverable pod
88 2     2 0 213 my $self = shift;
89            
90 2         7 return $self->{json_coder};
91             }
92              
93              
94             # Return the HTTP Date header from the last response.
95             sub date_header {
96             # uncoverable pod
97 2     2 0 5 my $response = shift->{response};
98            
99 2   100     18 return $response->{headers}->{date} // '';
100             }
101              
102              
103             # Return a hashref with the following entries, representing
104             # headers and status of the last response:
105             # - content_type (eg "application/json")
106             # - location (URI reference)
107             # - status (eg "404")
108             # - success (truthy for 2xx status)
109             sub http_header {
110             # uncoverable pod
111 11     11 0 19 my $response = shift->{response};
112            
113             my %header = (
114             content_type => $response->{headers}->{'content-type'} // '',
115             location => $response->{headers}->{location} // '',
116             status => $response->{status},
117             success => $response->{success},
118 11   100     85 );
      100        
119 11 100       29 if ( $response->{status} eq '599' ) { # Internal Exception
120 3         6 $header{content_type} = '';
121 3         5 $header{status} = '';
122             }
123 11         60 return \%header;
124             }
125              
126              
127             # Return the HTTP reason phrase (eg "Not Found"), or the error
128             # message for HTTP::Tiny internal exceptions.
129             sub http_reason {
130             # uncoverable pod
131 4     4 0 8 my $response = shift->{response};
132            
133 4 100       12 if ( $response->{status} eq '599' ) {
134 2         43 return $response->{content} =~ s/\s+$//ra;
135             }
136 2         7 return $response->{reason};
137             }
138              
139              
140             # Return the next Jolt event from the response to the last network
141             # request as a string. When there are no further Jolt events on the
142             # response buffer, this method returns an empty string.
143             # This algorithm is not strictly compliant with the json-seq RFC 7464.
144             # Instead, it's an ndjson parser that accounts for leading RS bytes,
145             # which is what works best for all versions of Neo4j.
146             sub fetch_event {
147             # uncoverable pod
148 758     758 0 13577 my $response = shift->{response};
149            
150             # Jolt always uses LF as event separator. When there is no LF,
151             # return the entire buffer to terminate the event loop.
152 758         1485 my $length = 1 + index $response->{content}, chr 0x0a;
153 758   100     1841 $length ||= length $response->{content};
154            
155             # Chop the event off the front of the buffer and drop the RS byte.
156 758         1803 my $event = substr $response->{content}, 0, $length, '';
157 758 100       1722 substr $event, 0, 1, '' if ord $event == 0x1e;
158 758         2005 return $event;
159             }
160              
161              
162             # Return the entire remaining content of the response buffer.
163             sub fetch_all {
164             # uncoverable pod
165 3     3 0 7291 my $response = shift->{response};
166            
167 3         16 return $response->{content};
168             }
169              
170              
171             # Perform an HTTP request on the network and store the response.
172             # Will always block until the entire response has been received.
173             # The following positional parameters are given:
174             # - method (HTTP method, e. g. "POST")
175             # - url (string with request URL, relative to base)
176             # - json (reference to hash of JSON object, or undef)
177             # - accept (string with value for the Accept header)
178             # - mode (string with value for the Access-Mode header)
179             sub request {
180             # uncoverable pod
181 6     6 0 12149 my ($self, $method, $url, $json, $accept, $mode) = @_;
182            
183 6         28 $url = URI->new_abs( $url, $self->{uri_base} );
184 6 100       1643 if ( defined $json ) {
185             my %options = (
186 3         33 content => $self->{json_coder}->encode($json),
187             headers => { Accept => $accept },
188             );
189 3 100       16 $options{headers}->{'Access-Mode'} = $mode if defined $mode;
190 3         18 $self->{response} = $self->{http}->request( $method, $url, \%options );
191             }
192             else {
193 3         12 my %options = (
194             headers => { Accept => $accept },
195             );
196 3         13 $self->{response} = $self->{http}->request( $method, $url, \%options );
197             }
198             }
199              
200              
201             1;