File Coverage

lib/Neo4j/Driver/Result/Jolt.pm
Criterion Covered Total %
statement 182 192 94.7
branch 103 152 67.7
condition 19 29 65.5
subroutine 16 17 94.1
pod 0 1 100.0
total 320 391 82.1


line stmt bran cond sub pod time code
1 17     17   375 use 5.010;
  17         76  
2 17     17   98 use strict;
  17         60  
  17         389  
3 17     17   97 use warnings;
  17         43  
  17         511  
4 17     17   86 use utf8;
  17         41  
  17         104  
5              
6             package Neo4j::Driver::Result::Jolt;
7             # ABSTRACT: Jolt result handler
8             $Neo4j::Driver::Result::Jolt::VERSION = '0.40';
9              
10             # This package is not part of the public Neo4j::Driver API.
11              
12              
13 17     17   1120 use parent 'Neo4j::Driver::Result';
  17         42  
  17         100  
14              
15 17     17   1531 use Carp qw(carp croak);
  17         45  
  17         1441  
16             our @CARP_NOT = qw(Neo4j::Driver::Net::HTTP Neo4j::Driver::Result);
17              
18 17     17   130 use Neo4j::Error;
  17         51  
  17         43794  
19              
20             my ($TRUE, $FALSE);
21              
22             my $MEDIA_TYPE = "application/vnd.neo4j.jolt";
23             my $ACCEPT_HEADER = "$MEDIA_TYPE-v2+json-seq";
24             my $ACCEPT_HEADER_V1 = "$MEDIA_TYPE+json-seq";
25             my $ACCEPT_HEADER_STRICT = "$MEDIA_TYPE+json-seq;strict=true";
26             my $ACCEPT_HEADER_SPARSE = "$MEDIA_TYPE+json-seq;strict=false";
27             my $ACCEPT_HEADER_NDJSON = "$MEDIA_TYPE";
28              
29              
30             our $gather_results = 1; # 1: detach from the stream immediately (yields JSON-style result; used for testing)
31              
32              
33             sub new {
34             # uncoverable pod (private method)
35 125     125 0 276 my ($class, $params) = @_;
36            
37 125         650 my $jolt_v2 = $params->{http_header}->{content_type} =~ m/^\Q$MEDIA_TYPE\E-v2\b/i;
38             my $self = {
39             attached => 1, # 1: unbuffered records may exist on the stream
40             exhausted => 0, # 1: all records read by the client; fetch() will fail
41             buffer => [],
42             server_info => $params->{server_info},
43             json_coder => $params->{http_agent}->json_coder,
44             http_agent => $params->{http_agent},
45             cypher_types => $params->{cypher_types},
46 125 100       427 v2_id_prefix => $jolt_v2 ? 'element_' : '',
47             };
48 125         1052 bless $self, $class;
49            
50 125 100       483 ($TRUE, $FALSE) = @{ $self->{json_coder}->decode('[true,false]') } unless $TRUE;
  7         100  
51            
52 125 50       1214 return $self->_gather_results($params) if $gather_results;
53            
54 0         0 die "Unimplemented"; # $gather_results 0
55             }
56              
57              
58             sub _gather_results {
59 125     125   228 my ($self, $params) = @_;
60            
61 125         186 my $error = 'Neo4j::Error';
62 125         168 my @results = ();
63 125         239 my $columns = undef;
64 125         148 my @data = ();
65 125         226 $self->{result} = {};
66 125         213 my ($state, $prev) = (0, 'in first place');
67 125         246 my ($type, $event);
68 125         287 while ( ($type, $event) = $self->_next_event ) {
69 468 100       1330 if ($type eq 'header') { # StatementStartEvent
    100          
    100          
    100          
    50          
70 114 50 33     244 croak "Jolt error: unexpected header event $prev" unless $state == 0 || $state == 3;
71 114 0       225 croak "Jolt error: expected reference to HASH, received " . (ref $event ? "reference to " . ref $event : "scalar") . " in $type event $prev" unless ref $event eq 'HASH';
    50          
72 114         161 $state = 1;
73 114         171 $columns = $event->{fields};
74             }
75             elsif ($type eq 'data') { # RecordEvent
76 104 50 66     264 croak "Jolt error: unexpected data event $prev" unless $state == 1 || $state == 2;
77 104 0       230 croak "Jolt error: expected reference to ARRAY, received " . (ref $event ? "reference to " . ref $event : "scalar") . " in $type event $prev" unless ref $event eq 'ARRAY';
    50          
78 104         123 $state = 2;
79 104         337 push @data, { row => $event, meta => [] };
80             }
81             elsif ($type eq 'summary') { # StatementEndEvent
82 114 50 66     459 croak "Jolt error: unexpected summary event $prev" unless $state == 1 || $state == 2;
83 114 0       231 croak "Jolt error: expected reference to HASH, received " . (ref $event ? "reference to " . ref $event : "scalar") . " in $type event $prev" unless ref $event eq 'HASH';
    50          
84 114         149 $state = 3;
85             push @results, {
86             data => [@data],
87             stats => $event->{stats},
88             plan => $event->{plan},
89 114   100     523 columns => $columns // [],
90             };
91 114         191 @data = ();
92 114         157 $columns = undef;
93             }
94             elsif ($type eq 'info') { # TransactionInfoEvent
95 125 50 66     573 croak "Jolt error: unexpected info event $prev" unless $state == 0 || $state == 3 || $state == 4;
      66        
96 125 0       297 croak "Jolt error: expected reference to HASH, received " . (ref $event ? "reference to " . ref $event : "scalar") . " in $type event $prev" unless ref $event eq 'HASH';
    50          
97 125         222 $state += 10;
98 125         248 $self->{info} = $event;
99 125         226 $self->{notifications} = $event->{notifications};
100             }
101             elsif ($type eq 'error') { # FailureEvent
102             # If a rollback caused by a failure fails as well,
103             # two failure events may appear on the Jolt stream.
104             # Otherwise, there is always one at most.
105 11 0 33     30 carp "Jolt error: unexpected error event $prev" unless $state == 0 || $state == 3 || $state == 4;
      33        
106 11 0       26 croak "Jolt error: expected reference to HASH, received " . (ref $event ? "reference to " . ref $event : "scalar") . " in $type event $prev" unless ref $event eq 'HASH';
    50          
107 11         19 $state = 4;
108 11 50       13 $error = $error->append_new(Internal => "Jolt error: Jolt $type event with 0 errors $prev") unless @{$event->{errors}};
  11         39  
109 11         24 $error = $error->append_new(Server => $_) for @{$event->{errors}};
  11         57  
110             }
111             else {
112 0         0 croak "Jolt error: unsupported $type event $prev";
113             }
114 468         27339 $prev = "after $type event";
115             }
116 125 50       344 croak "Jolt error: unexpected end of event stream $prev" unless $state >= 10;
117            
118 125 50       261 if (! $params->{http_header}->{success}) {
119             $error = $error->append_new(Network => {
120             code => $params->{http_header}->{status},
121 0         0 as_string => sprintf("HTTP error: %s %s on %s to %s", $params->{http_header}->{status}, $params->{http_agent}->http_reason, $params->{http_method}, $params->{http_path}),
122             });
123             }
124            
125 125 100       255 $self->{info}->{_error} = $error if ref $error;
126 125         175 $self->{http_agent} = undef;
127            
128 125 100       246 if (@results == 1) {
129 114         180 $self->{result} = $results[0];
130 114         195 $self->{statement} = $params->{statements}->[0];
131 114         316 return $self->_as_fully_buffered;
132             }
133            
134             # If the number of Cypher statements run wasn't exactly one, provide a list
135             # of all results so that callers get a uniform interface for all of them.
136 11         32 @results = map { __PACKAGE__->_new_result($_, undef, $params) } @results;
  0         0  
137 11         45 $results[$_]->{statement} = $params->{statements}->[$_] for (0 .. $#results);
138 11         23 $self->{attached} = 0;
139 11         18 $self->{exhausted} = 1;
140 11 50       27 $self->{result_list} = \@results if @results;
141 11         44 return $self;
142             }
143              
144              
145             sub _new_result {
146 0     0   0 my ($class, $result, $json, $params) = @_;
147            
148 0         0 my $jolt_v2 = $params->{http_header}->{content_type} =~ m/^\Q$MEDIA_TYPE\E-v2\b/i;
149             my $self = {
150             attached => 0, # 1: unbuffered records may exist on the stream
151             exhausted => 0, # 1: all records read by the client; fetch() will fail
152             result => $result,
153             buffer => [],
154             columns => undef,
155             summary => undef,
156             cypher_types => $params->{cypher_types},
157             server_info => $params->{server_info},
158 0 0       0 v2_id_prefix => $jolt_v2 ? 'element_' : '',
159             };
160 0         0 bless $self, $class;
161            
162 0         0 return $self->_as_fully_buffered;
163             }
164              
165              
166             sub _next_event {
167 593     593   872 my ($self) = @_;
168            
169 593         1137 my $line = $self->{http_agent}->fetch_event;
170 593 100       1257 return unless defined $line;
171            
172 468         1973 my $json = $self->{json_coder}->decode($line);
173 468 0       984 croak "Jolt error: expected reference to HASH, received " . (ref $json ? "reference to " . ref $json : "scalar") unless ref $json eq 'HASH';
    50          
174            
175 468         1067 my @events = keys %$json;
176 468 50       827 croak "Jolt error: expected exactly 1 event, received " . scalar @events unless @events == 1;
177            
178 468         1722 return ( $events[0], $json->{$events[0]} );
179             }
180              
181              
182             # Return the full list of results this object represents.
183             sub _results {
184 110     110   229 my ($self) = @_;
185            
186 110 50       217 return @{ $self->{result_list} } if $self->{result_list};
  0         0  
187 110         326 return ($self);
188             }
189              
190              
191             # Return transaction status information (if available).
192             sub _info {
193 123     123   215 my ($self) = @_;
194 123         222 return $self->{info};
195             }
196              
197              
198             # Bless and initialise the given reference as a Record.
199             sub _init_record {
200 104     104   155 my ($self, $record) = @_;
201            
202 104         159 $record->{column_keys} = $self->{columns};
203 104         238 $self->_deep_bless( $record->{row} );
204 102         262 return bless $record, 'Neo4j::Driver::Record';
205             }
206              
207              
208             sub _deep_bless {
209 343     343   529 my ($self, $data) = @_;
210 343         413 my $cypher_types = $self->{cypher_types};
211            
212 343 100       639 if (ref $data eq 'ARRAY') { # List (sparse)
213 108         131 $data->[$_] = $self->_deep_bless($data->[$_]) for 0 .. $#{$data};
  108         455  
214 106         201 return $data;
215             }
216 235 100       378 if (ref $data eq '') { # Null or Integer (sparse) or String (sparse)
217 47         142 return $data;
218             }
219 188 100 100     474 if ($data == $TRUE || $data == $FALSE) { # Boolean (sparse)
220 2         26 return $data;
221             }
222            
223 186 50       1796 die "Assertion failed: unexpected type: " . ref $data unless ref $data eq 'HASH';
224 186 50       388 die "Assertion failed: sigil count: " . scalar keys %$data if scalar keys %$data != 1;
225 186         319 my $sigil = (keys %$data)[0];
226 186         265 my $value = $data->{$sigil};
227            
228 186 100       286 if ($sigil eq '?') { # Boolean (strict)
229 3 100       9 return $TRUE if $value eq 'true';
230 2 100       7 return $FALSE if $value eq 'false';
231 1         19 die "Assertion failed: unexpected bool value: " . $value;
232             }
233 183 100       265 if ($sigil eq 'Z') { # Integer (strict)
234 15         54 return 0 + $value;
235             }
236 168 100       267 if ($sigil eq 'R') { # Float
237 1         7 return 0 + $value;
238             }
239 167 100       241 if ($sigil eq 'U') { # String (strict)
240 113         376 return $value;
241             }
242 54 100       93 if ($sigil eq '[]') { # List (strict)
243 4 50       10 die "Assertion failed: unexpected list type: " . ref $value unless ref $value eq 'ARRAY';
244 4         7 $value->[$_] = $self->_deep_bless($value->[$_]) for 0 .. $#{$value};
  4         15  
245 4         11 return $value;
246             }
247 50 100       85 if ($sigil eq '{}') { # Map
248 6 50       14 die "Assertion failed: unexpected map type: " . ref $value unless ref $value eq 'HASH';
249 6         10 delete $data->{'{}'};
250 6         22 $data->{$_} = $self->_deep_bless($value->{$_}) for keys %$value;
251 6         17 return $data;
252             }
253 44 100       87 if ($sigil eq '()') { # Node
254 21 50       62 die "Assertion failed: unexpected node type: " . ref $value unless ref $value eq 'ARRAY';
255 21 50       61 die "Assertion failed: unexpected node fields: " . scalar @$value unless @$value == 3;
256 21 50       45 die "Assertion failed: unexpected prop type: " . ref $value->[2] unless ref $value->[2] eq 'HASH';
257 21         29 my $props = $value->[2];
258 21         67 $props->{$_} = $self->_deep_bless($props->{$_}) for keys %$props;
259 21         34 my $node = \( $props );
260 21         63 bless $node, $cypher_types->{node};
261             $$node->{_meta} = {
262 21         156 "$self->{v2_id_prefix}id" => $value->[0],
263             labels => $value->[1],
264             };
265 21 50       52 $cypher_types->{init}->($node) if $cypher_types->{init};
266 21         83 return $node;
267             }
268 23 100 100     73 if ($sigil eq '->' || $sigil eq '<-') { # Relationship
269 12 50       39 die "Assertion failed: unexpected rel type: " . ref $value unless ref $value eq 'ARRAY';
270 12 50       25 die "Assertion failed: unexpected rel fields: " . scalar @$value unless @$value == 5;
271 12 50       27 die "Assertion failed: unexpected prop type: " . ref $value->[4] unless ref $value->[4] eq 'HASH';
272 12         17 my $props = $value->[4];
273 12         39 $props->{$_} = $self->_deep_bless($props->{$_}) for keys %$props;
274 12         23 my $rel = \( $props );
275 12         31 bless $rel, $cypher_types->{relationship};
276             $$rel->{_meta} = {
277 12 100       195 "$self->{v2_id_prefix}id" => $value->[0],
    100          
278             type => $value->[2],
279             "$self->{v2_id_prefix}start" => $sigil eq '->' ? $value->[1] : $value->[3],
280             "$self->{v2_id_prefix}end" => $sigil eq '->' ? $value->[3] : $value->[1],
281             };
282 12 50       33 $cypher_types->{init}->($rel) if $cypher_types->{init};
283 12         43 return $rel;
284             }
285 11 100       28 if ($sigil eq '..') { # Path
286 7 50       19 die "Assertion failed: unexpected path type: " . ref $value unless ref $value eq 'ARRAY';
287 7 50       18 die "Assertion failed: unexpected path fields: " . scalar @$value unless @$value & 1;
288 7         11 $value->[$_] = $self->_deep_bless($value->[$_]) for 0 .. $#{$value};
  7         40  
289 7         31 my $path = bless { path => $value }, $cypher_types->{path};
290 7 50       18 $cypher_types->{init}->($path) if $cypher_types->{init};
291 7         20 return $path;
292             }
293 4 100       10 if ($sigil eq '@') { # Spatial
294             # TODO
295 1         11 bless $data, $cypher_types->{point};
296 1         3 return $data;
297             }
298 3 100       26 if ($sigil eq 'T') { # Temporal
299             # TODO
300 1         7 bless $data, $cypher_types->{temporal};
301 1         4 return $data;
302             }
303 2 100       8 if ($sigil eq '#') { # Bytes
304 1         9 $value =~ tr/ //d; # spaces were allowed in the Jolt draft, but aren't actually implemented in Neo4j 4.2's jolt.JoltModule
305 1         8 $value = pack 'H*', $value; # see neo4j#12660
306 1         4 utf8::downgrade($value); # UTF8 flag should be off already, but let's make sure
307 1         3 return $value;
308             }
309            
310 1         21 die "Assertion failed: unexpected sigil: " . $sigil;
311            
312             }
313              
314              
315             sub _accept_header {
316 209     209   458 my (undef, $want_jolt, $method) = @_;
317            
318 209 100       586 return unless $method eq 'POST'; # work around Neo4j HTTP Content Negotiation bug #12644
319            
320 127 100       326 if (defined $want_jolt) {
321 18 100       47 return if ! $want_jolt;
322 15 100       48 return ($ACCEPT_HEADER_V1) if $want_jolt eq 'v1';
323 8 100       25 return ($ACCEPT_HEADER_STRICT) if $want_jolt eq 'strict';
324 6 100       16 return ($ACCEPT_HEADER_SPARSE) if $want_jolt eq 'sparse';
325 4 100       21 return ($ACCEPT_HEADER_NDJSON) if $want_jolt eq 'ndjson';
326             }
327 111         328 return ($ACCEPT_HEADER);
328             }
329              
330              
331             sub _acceptable {
332 193     193   418 my (undef, $content_type) = @_;
333            
334 193         1403 return $content_type =~ m/^\Q$MEDIA_TYPE\E\b/i;
335             }
336              
337              
338             1;