File Coverage

lib/Neo4j/Driver/Result/Jolt.pm
Criterion Covered Total %
statement 176 192 91.6
branch 101 152 66.4
condition 19 29 65.5
subroutine 16 17 94.1
pod 0 1 100.0
total 312 391 80.0


line stmt bran cond sub pod time code
1 17     17   348 use 5.010;
  17         68  
2 17     17   97 use strict;
  17         28  
  17         382  
3 17     17   105 use warnings;
  17         78  
  17         501  
4 17     17   96 use utf8;
  17         46  
  17         111  
5              
6             package Neo4j::Driver::Result::Jolt;
7             # ABSTRACT: Jolt result handler
8             $Neo4j::Driver::Result::Jolt::VERSION = '0.38';
9              
10             # This package is not part of the public Neo4j::Driver API.
11              
12              
13 17     17   1072 use parent 'Neo4j::Driver::Result';
  17         57  
  17         100  
14              
15 17     17   1552 use Carp qw(carp croak);
  17         35  
  17         1553  
16             our @CARP_NOT = qw(Neo4j::Driver::Net::HTTP Neo4j::Driver::Result);
17              
18 17     17   144 use Neo4j::Error;
  17         48  
  17         44281  
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 253 my ($class, $params) = @_;
36            
37 125         629 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       428 v2_id_prefix => $jolt_v2 ? 'element_' : '',
47             };
48 125         936 bless $self, $class;
49            
50 125 100       556 ($TRUE, $FALSE) = @{ $self->{json_coder}->decode('[true,false]') } unless $TRUE;
  7         99  
51            
52 125 50       1241 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   219 my ($self, $params) = @_;
60            
61 125         183 my $error = 'Neo4j::Error';
62 125         174 my @results = ();
63 125         154 my $columns = undef;
64 125         143 my @data = ();
65 125         243 $self->{result} = {};
66 125         213 my ($state, $prev) = (0, 'in first place');
67 125         174 my ($type, $event);
68 125         286 while ( ($type, $event) = $self->_next_event ) {
69 468 100       1277 if ($type eq 'header') { # StatementStartEvent
    100          
    100          
    100          
    50          
70 114 50 33     251 croak "Jolt error: unexpected header event $prev" unless $state == 0 || $state == 3;
71 114 0       237 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         150 $state = 1;
73 114         175 $columns = $event->{fields};
74             }
75             elsif ($type eq 'data') { # RecordEvent
76 104 50 66     277 croak "Jolt error: unexpected data event $prev" unless $state == 1 || $state == 2;
77 104 0       207 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         177 $state = 2;
79 104         272 push @data, { row => $event, meta => [] };
80             }
81             elsif ($type eq 'summary') { # StatementEndEvent
82 114 50 66     369 croak "Jolt error: unexpected summary event $prev" unless $state == 1 || $state == 2;
83 114 0       237 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         155 $state = 3;
85             push @results, {
86             data => [@data],
87             stats => $event->{stats},
88             plan => $event->{plan},
89 114   100     590 columns => $columns // [],
90             };
91 114         196 @data = ();
92 114         199 $columns = undef;
93             }
94             elsif ($type eq 'info') { # TransactionInfoEvent
95 125 50 66     523 croak "Jolt error: unexpected info event $prev" unless $state == 0 || $state == 3 || $state == 4;
      66        
96 125 0       278 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         246 $state += 10;
98 125         266 $self->{info} = $event;
99 125         233 $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     34 carp "Jolt error: unexpected error event $prev" unless $state == 0 || $state == 3 || $state == 4;
      33        
106 11 0       28 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         14 $state = 4;
108 11 50       12 $error = $error->append_new(Internal => "Jolt error: Jolt $type event with 0 errors $prev") unless @{$event->{errors}};
  11         39  
109 11         17 $error = $error->append_new(Server => $_) for @{$event->{errors}};
  11         62  
110             }
111             else {
112 0         0 croak "Jolt error: unsupported $type event $prev";
113             }
114 468         21907 $prev = "after $type event";
115             }
116 125 50       241 croak "Jolt error: unexpected end of event stream $prev" unless $state >= 10;
117            
118 125 50       242 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       279 $params->{error_handler}->($error) if ref $error;
126 114         147 $self->{http_agent} = undef;
127            
128 114 50       252 if (@results == 1) {
129 114         188 $self->{result} = $results[0];
130 114         271 $self->{statement} = $params->{statements}->[0];
131 114         405 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 0         0 @results = map { __PACKAGE__->_new_result($_, undef, $params) } @results;
  0         0  
137 0         0 $results[$_]->{statement} = $params->{statements}->[$_] for (0 .. $#results);
138 0         0 $self->{attached} = 0;
139 0         0 $self->{exhausted} = 1;
140 0 0       0 $self->{result_list} = \@results if @results;
141 0         0 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   808 my ($self) = @_;
168            
169 593         1169 my $line = $self->{http_agent}->fetch_event;
170 593 100       1263 return unless defined $line;
171            
172 468         2088 my $json = $self->{json_coder}->decode($line);
173 468 0       1059 croak "Jolt error: expected reference to HASH, received " . (ref $json ? "reference to " . ref $json : "scalar") unless ref $json eq 'HASH';
    50          
174            
175 468         1045 my @events = keys %$json;
176 468 50       817 croak "Jolt error: expected exactly 1 event, received " . scalar @events unless @events == 1;
177            
178 468         1678 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   211 my ($self) = @_;
185            
186 110 50       252 return @{ $self->{result_list} } if $self->{result_list};
  0         0  
187 110         333 return ($self);
188             }
189              
190              
191             # Return transaction status information (if available).
192             sub _info {
193 112     112   198 my ($self) = @_;
194 112         319 return $self->{info};
195             }
196              
197              
198             # Bless and initialise the given reference as a Record.
199             sub _init_record {
200 104     104   172 my ($self, $record) = @_;
201            
202 104         171 $record->{column_keys} = $self->{columns};
203 104         234 $self->_deep_bless( $record->{row} );
204 102         274 return bless $record, 'Neo4j::Driver::Record';
205             }
206              
207              
208             sub _deep_bless {
209 343     343   520 my ($self, $data) = @_;
210 343         396 my $cypher_types = $self->{cypher_types};
211            
212 343 100       637 if (ref $data eq 'ARRAY') { # List (sparse)
213 108         135 $data->[$_] = $self->_deep_bless($data->[$_]) for 0 .. $#{$data};
  108         404  
214 106         181 return $data;
215             }
216 235 100       400 if (ref $data eq '') { # Null or Integer (sparse) or String (sparse)
217 47         154 return $data;
218             }
219 188 100 100     477 if ($data == $TRUE || $data == $FALSE) { # Boolean (sparse)
220 2         26 return $data;
221             }
222            
223 186 50       1842 die "Assertion failed: unexpected type: " . ref $data unless ref $data eq 'HASH';
224 186 50       402 die "Assertion failed: sigil count: " . scalar keys %$data if scalar keys %$data != 1;
225 186         308 my $sigil = (keys %$data)[0];
226 186         264 my $value = $data->{$sigil};
227            
228 186 100       286 if ($sigil eq '?') { # Boolean (strict)
229 3 100       10 return $TRUE if $value eq 'true';
230 2 100       6 return $FALSE if $value eq 'false';
231 1         18 die "Assertion failed: unexpected bool value: " . $value;
232             }
233 183 100       300 if ($sigil eq 'Z') { # Integer (strict)
234 15         59 return 0 + $value;
235             }
236 168 100       261 if ($sigil eq 'R') { # Float
237 1         14 return 0 + $value;
238             }
239 167 100       262 if ($sigil eq 'U') { # String (strict)
240 113         353 return $value;
241             }
242 54 100       103 if ($sigil eq '[]') { # List (strict)
243 4 50       15 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         14  
245 4         10 return $value;
246             }
247 50 100       76 if ($sigil eq '{}') { # Map
248 6 50       18 die "Assertion failed: unexpected map type: " . ref $value unless ref $value eq 'HASH';
249 6         8 delete $data->{'{}'};
250 6         20 $data->{$_} = $self->_deep_bless($value->{$_}) for keys %$value;
251 6         17 return $data;
252             }
253 44 100       74 if ($sigil eq '()') { # Node
254 21 50       42 die "Assertion failed: unexpected node type: " . ref $value unless ref $value eq 'ARRAY';
255 21 50       41 die "Assertion failed: unexpected node fields: " . scalar @$value unless @$value == 3;
256 21 50       50 die "Assertion failed: unexpected prop type: " . ref $value->[2] unless ref $value->[2] eq 'HASH';
257 21         26 my $props = $value->[2];
258 21         85 $props->{$_} = $self->_deep_bless($props->{$_}) for keys %$props;
259 21         32 my $node = \( $props );
260 21         49 bless $node, $cypher_types->{node};
261             $$node->{_meta} = {
262 21         154 "$self->{v2_id_prefix}id" => $value->[0],
263             labels => $value->[1],
264             };
265 21 50       43 $cypher_types->{init}->($node) if $cypher_types->{init};
266 21         79 return $node;
267             }
268 23 100 100     73 if ($sigil eq '->' || $sigil eq '<-') { # Relationship
269 12 50       26 die "Assertion failed: unexpected rel type: " . ref $value unless ref $value eq 'ARRAY';
270 12 50       27 die "Assertion failed: unexpected rel fields: " . scalar @$value unless @$value == 5;
271 12 50       29 die "Assertion failed: unexpected prop type: " . ref $value->[4] unless ref $value->[4] eq 'HASH';
272 12         18 my $props = $value->[4];
273 12         33 $props->{$_} = $self->_deep_bless($props->{$_}) for keys %$props;
274 12         64 my $rel = \( $props );
275 12         30 bless $rel, $cypher_types->{relationship};
276             $$rel->{_meta} = {
277 12 100       178 "$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       50 $cypher_types->{init}->($rel) if $cypher_types->{init};
283 12         48 return $rel;
284             }
285 11 100       34 if ($sigil eq '..') { # Path
286 7 50       22 die "Assertion failed: unexpected path type: " . ref $value unless ref $value eq 'ARRAY';
287 7 50       19 die "Assertion failed: unexpected path fields: " . scalar @$value unless @$value & 1;
288 7         11 $value->[$_] = $self->_deep_bless($value->[$_]) for 0 .. $#{$value};
  7         39  
289 7         36 my $path = bless { path => $value }, $cypher_types->{path};
290 7 50       15 $cypher_types->{init}->($path) if $cypher_types->{init};
291 7         17 return $path;
292             }
293 4 100       7 if ($sigil eq '@') { # Spatial
294             # TODO
295 1         4 bless $data, $cypher_types->{point};
296 1         2 return $data;
297             }
298 3 100       6 if ($sigil eq 'T') { # Temporal
299             # TODO
300 1         8 bless $data, $cypher_types->{temporal};
301 1         3 return $data;
302             }
303 2 100       5 if ($sigil eq '#') { # Bytes
304 1         4 $value =~ tr/ //d; # spaces were allowed in the Jolt draft, but aren't actually implemented in Neo4j 4.2's jolt.JoltModule
305 1         6 $value = pack 'H*', $value; # see neo4j#12660
306 1         3 utf8::downgrade($value); # UTF8 flag should be off already, but let's make sure
307 1         3 return $value;
308             }
309            
310 1         18 die "Assertion failed: unexpected sigil: " . $sigil;
311            
312             }
313              
314              
315             sub _accept_header {
316 208     208   445 my (undef, $want_jolt, $method) = @_;
317            
318 208 100       586 return unless $method eq 'POST'; # work around Neo4j HTTP Content Negotiation bug #12644
319            
320 126 100       394 if (defined $want_jolt) {
321 18 100       44 return if ! $want_jolt;
322 15 100       45 return ($ACCEPT_HEADER_V1) if $want_jolt eq 'v1';
323 8 100       18 return ($ACCEPT_HEADER_STRICT) if $want_jolt eq 'strict';
324 6 100       21 return ($ACCEPT_HEADER_SPARSE) if $want_jolt eq 'sparse';
325 4 100       12 return ($ACCEPT_HEADER_NDJSON) if $want_jolt eq 'ndjson';
326             }
327 110         297 return ($ACCEPT_HEADER);
328             }
329              
330              
331             sub _acceptable {
332 192     192   384 my (undef, $content_type) = @_;
333            
334 192         1412 return $content_type =~ m/^\Q$MEDIA_TYPE\E\b/i;
335             }
336              
337              
338             1;