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   356 use 5.010;
  17         65  
2 17     17   93 use strict;
  17         41  
  17         380  
3 17     17   85 use warnings;
  17         37  
  17         503  
4 17     17   88 use utf8;
  17         42  
  17         127  
5              
6             package Neo4j::Driver::Result::Jolt;
7             # ABSTRACT: Jolt result handler
8             $Neo4j::Driver::Result::Jolt::VERSION = '0.39';
9              
10             # This package is not part of the public Neo4j::Driver API.
11              
12              
13 17     17   1032 use parent 'Neo4j::Driver::Result';
  17         48  
  17         91  
14              
15 17     17   1554 use Carp qw(carp croak);
  17         67  
  17         1387  
16             our @CARP_NOT = qw(Neo4j::Driver::Net::HTTP Neo4j::Driver::Result);
17              
18 17     17   256 use Neo4j::Error;
  17         45  
  17         42702  
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 255 my ($class, $params) = @_;
36            
37 125         659 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       438 v2_id_prefix => $jolt_v2 ? 'element_' : '',
47             };
48 125         983 bless $self, $class;
49            
50 125 100       493 ($TRUE, $FALSE) = @{ $self->{json_coder}->decode('[true,false]') } unless $TRUE;
  7         94  
51            
52 125 50       1305 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   269 my ($self, $params) = @_;
60            
61 125         199 my $error = 'Neo4j::Error';
62 125         185 my @results = ();
63 125         263 my $columns = undef;
64 125         170 my @data = ();
65 125         231 $self->{result} = {};
66 125         231 my ($state, $prev) = (0, 'in first place');
67 125         258 my ($type, $event);
68 125         290 while ( ($type, $event) = $self->_next_event ) {
69 468 100       1279 if ($type eq 'header') { # StatementStartEvent
    100          
    100          
    100          
    50          
70 114 50 33     262 croak "Jolt error: unexpected header event $prev" unless $state == 0 || $state == 3;
71 114 0       249 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         159 $state = 1;
73 114         176 $columns = $event->{fields};
74             }
75             elsif ($type eq 'data') { # RecordEvent
76 104 50 66     286 croak "Jolt error: unexpected data event $prev" unless $state == 1 || $state == 2;
77 104 0       225 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         142 $state = 2;
79 104         365 push @data, { row => $event, meta => [] };
80             }
81             elsif ($type eq 'summary') { # StatementEndEvent
82 114 50 66     490 croak "Jolt error: unexpected summary event $prev" unless $state == 1 || $state == 2;
83 114 0       277 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         160 $state = 3;
85             push @results, {
86             data => [@data],
87             stats => $event->{stats},
88             plan => $event->{plan},
89 114   100     553 columns => $columns // [],
90             };
91 114         198 @data = ();
92 114         146 $columns = undef;
93             }
94             elsif ($type eq 'info') { # TransactionInfoEvent
95 125 50 66     552 croak "Jolt error: unexpected info event $prev" unless $state == 0 || $state == 3 || $state == 4;
      66        
96 125 0       268 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         242 $state += 10;
98 125         274 $self->{info} = $event;
99 125         214 $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       32 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         21 $state = 4;
108 11 50       16 $error = $error->append_new(Internal => "Jolt error: Jolt $type event with 0 errors $prev") unless @{$event->{errors}};
  11         45  
109 11         16 $error = $error->append_new(Server => $_) for @{$event->{errors}};
  11         67  
110             }
111             else {
112 0         0 croak "Jolt error: unsupported $type event $prev";
113             }
114 468         29337 $prev = "after $type event";
115             }
116 125 50       338 croak "Jolt error: unexpected end of event stream $prev" unless $state >= 10;
117            
118 125 50       282 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       247 $self->{info}->{_error} = $error if ref $error;
126 125         185 $self->{http_agent} = undef;
127            
128 125 100       247 if (@results == 1) {
129 114         188 $self->{result} = $results[0];
130 114         249 $self->{statement} = $params->{statements}->[0];
131 114         335 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         28 @results = map { __PACKAGE__->_new_result($_, undef, $params) } @results;
  0         0  
137 11         45 $results[$_]->{statement} = $params->{statements}->[$_] for (0 .. $#results);
138 11         22 $self->{attached} = 0;
139 11         19 $self->{exhausted} = 1;
140 11 50       23 $self->{result_list} = \@results if @results;
141 11         117 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   908 my ($self) = @_;
168            
169 593         1152 my $line = $self->{http_agent}->fetch_event;
170 593 100       1321 return unless defined $line;
171            
172 468         2035 my $json = $self->{json_coder}->decode($line);
173 468 0       973 croak "Jolt error: expected reference to HASH, received " . (ref $json ? "reference to " . ref $json : "scalar") unless ref $json eq 'HASH';
    50          
174            
175 468         1145 my @events = keys %$json;
176 468 50       914 croak "Jolt error: expected exactly 1 event, received " . scalar @events unless @events == 1;
177            
178 468         1786 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   188 my ($self) = @_;
185            
186 110 50       227 return @{ $self->{result_list} } if $self->{result_list};
  0         0  
187 110         309 return ($self);
188             }
189              
190              
191             # Return transaction status information (if available).
192             sub _info {
193 123     123   209 my ($self) = @_;
194 123         215 return $self->{info};
195             }
196              
197              
198             # Bless and initialise the given reference as a Record.
199             sub _init_record {
200 104     104   171 my ($self, $record) = @_;
201            
202 104         173 $record->{column_keys} = $self->{columns};
203 104         237 $self->_deep_bless( $record->{row} );
204 102         272 return bless $record, 'Neo4j::Driver::Record';
205             }
206              
207              
208             sub _deep_bless {
209 343     343   527 my ($self, $data) = @_;
210 343         400 my $cypher_types = $self->{cypher_types};
211            
212 343 100       640 if (ref $data eq 'ARRAY') { # List (sparse)
213 108         128 $data->[$_] = $self->_deep_bless($data->[$_]) for 0 .. $#{$data};
  108         474  
214 106         181 return $data;
215             }
216 235 100       392 if (ref $data eq '') { # Null or Integer (sparse) or String (sparse)
217 47         143 return $data;
218             }
219 188 100 100     508 if ($data == $TRUE || $data == $FALSE) { # Boolean (sparse)
220 2         25 return $data;
221             }
222            
223 186 50       1853 die "Assertion failed: unexpected type: " . ref $data unless ref $data eq 'HASH';
224 186 50       426 die "Assertion failed: sigil count: " . scalar keys %$data if scalar keys %$data != 1;
225 186         305 my $sigil = (keys %$data)[0];
226 186         267 my $value = $data->{$sigil};
227            
228 186 100       300 if ($sigil eq '?') { # Boolean (strict)
229 3 100       9 return $TRUE if $value eq 'true';
230 2 100       27 return $FALSE if $value eq 'false';
231 1         17 die "Assertion failed: unexpected bool value: " . $value;
232             }
233 183 100       267 if ($sigil eq 'Z') { # Integer (strict)
234 15         69 return 0 + $value;
235             }
236 168 100       250 if ($sigil eq 'R') { # Float
237 1         5 return 0 + $value;
238             }
239 167 100       228 if ($sigil eq 'U') { # String (strict)
240 113         366 return $value;
241             }
242 54 100       82 if ($sigil eq '[]') { # List (strict)
243 4 50       11 die "Assertion failed: unexpected list type: " . ref $value unless ref $value eq 'ARRAY';
244 4         6 $value->[$_] = $self->_deep_bless($value->[$_]) for 0 .. $#{$value};
  4         16  
245 4         12 return $value;
246             }
247 50 100       79 if ($sigil eq '{}') { # Map
248 6 50       21 die "Assertion failed: unexpected map type: " . ref $value unless ref $value eq 'HASH';
249 6         9 delete $data->{'{}'};
250 6         23 $data->{$_} = $self->_deep_bless($value->{$_}) for keys %$value;
251 6         18 return $data;
252             }
253 44 100       69 if ($sigil eq '()') { # Node
254 21 50       44 die "Assertion failed: unexpected node type: " . ref $value unless ref $value eq 'ARRAY';
255 21 50       35 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         26 my $props = $value->[2];
258 21         52 $props->{$_} = $self->_deep_bless($props->{$_}) for keys %$props;
259 21         35 my $node = \( $props );
260 21         44 bless $node, $cypher_types->{node};
261             $$node->{_meta} = {
262 21         151 "$self->{v2_id_prefix}id" => $value->[0],
263             labels => $value->[1],
264             };
265 21 50       47 $cypher_types->{init}->($node) if $cypher_types->{init};
266 21         75 return $node;
267             }
268 23 100 100     61 if ($sigil eq '->' || $sigil eq '<-') { # Relationship
269 12 50       27 die "Assertion failed: unexpected rel type: " . ref $value unless ref $value eq 'ARRAY';
270 12 50       35 die "Assertion failed: unexpected rel fields: " . scalar @$value unless @$value == 5;
271 12 50       30 die "Assertion failed: unexpected prop type: " . ref $value->[4] unless ref $value->[4] eq 'HASH';
272 12         19 my $props = $value->[4];
273 12         30 $props->{$_} = $self->_deep_bless($props->{$_}) for keys %$props;
274 12         22 my $rel = \( $props );
275 12         33 bless $rel, $cypher_types->{relationship};
276             $$rel->{_meta} = {
277 12 100       159 "$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       29 $cypher_types->{init}->($rel) if $cypher_types->{init};
283 12         43 return $rel;
284             }
285 11 100       25 if ($sigil eq '..') { # Path
286 7 50       20 die "Assertion failed: unexpected path type: " . ref $value unless ref $value eq 'ARRAY';
287 7 50       34 die "Assertion failed: unexpected path fields: " . scalar @$value unless @$value & 1;
288 7         10 $value->[$_] = $self->_deep_bless($value->[$_]) for 0 .. $#{$value};
  7         37  
289 7         38 my $path = bless { path => $value }, $cypher_types->{path};
290 7 50       16 $cypher_types->{init}->($path) if $cypher_types->{init};
291 7         30 return $path;
292             }
293 4 100       8 if ($sigil eq '@') { # Spatial
294             # TODO
295 1         5 bless $data, $cypher_types->{point};
296 1         3 return $data;
297             }
298 3 100       20 if ($sigil eq 'T') { # Temporal
299             # TODO
300 1         10 bless $data, $cypher_types->{temporal};
301 1         3 return $data;
302             }
303 2 100       6 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         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         4 return $value;
308             }
309            
310 1         18 die "Assertion failed: unexpected sigil: " . $sigil;
311            
312             }
313              
314              
315             sub _accept_header {
316 209     209   510 my (undef, $want_jolt, $method) = @_;
317            
318 209 100       638 return unless $method eq 'POST'; # work around Neo4j HTTP Content Negotiation bug #12644
319            
320 127 100       307 if (defined $want_jolt) {
321 18 100       43 return if ! $want_jolt;
322 15 100       39 return ($ACCEPT_HEADER_V1) if $want_jolt eq 'v1';
323 8 100       21 return ($ACCEPT_HEADER_STRICT) if $want_jolt eq 'strict';
324 6 100       14 return ($ACCEPT_HEADER_SPARSE) if $want_jolt eq 'sparse';
325 4 100       11 return ($ACCEPT_HEADER_NDJSON) if $want_jolt eq 'ndjson';
326             }
327 111         345 return ($ACCEPT_HEADER);
328             }
329              
330              
331             sub _acceptable {
332 193     193   391 my (undef, $content_type) = @_;
333            
334 193         1450 return $content_type =~ m/^\Q$MEDIA_TYPE\E\b/i;
335             }
336              
337              
338             1;