File Coverage

blib/lib/Neo4j/Cypher/Pattern.pm
Criterion Covered Total %
statement 117 126 92.8
branch 41 44 93.1
condition 8 11 72.7
subroutine 21 25 84.0
pod 13 18 72.2
total 200 224 89.2


line stmt bran cond sub pod time code
1             package Neo4j::Cypher::Pattern;
2 3     3   1009 use base Exporter;
  3         9  
  3         279  
3 3     3   23 use Carp;
  3         8  
  3         210  
4 3     3   23 use strict;
  3         10  
  3         116  
5 3     3   21 use warnings;
  3         7  
  3         139  
6 3     3   2597 use overload '""' => 'as_string';
  3         2441  
  3         28  
7              
8             our $VERSION = '0.1002';
9             our @EXPORT_OK = qw/pattern ptn/;
10              
11             sub puke(@);
12             sub belch(@);
13              
14             sub new {
15 50     50 1 1501 my $class = shift;
16 50         121 my $self = {};
17 50         151 $self->{stmt}=[];
18 50 100       166 if (@_) {
19 2         5 my $nq = join('|',@_);
20 2         30 $self->{no_quote} = qr/$nq/;
21             }
22 50         295 bless $self, $class;
23             }
24              
25             sub pattern {
26 0     0 1 0 Neo4j::Cypher::Pattern->new(@_);
27             }
28              
29 49     49 1 191 sub ptn { Neo4j::Cypher::Pattern->new(@_); }
30              
31             sub path {
32 2     2 1 7 my $self = shift;
33 2 50 33     20 puke("Need arg1 => identifier") if (!defined $_[0] || ref($_[0]));
34 2         14 return "$_[0] = $self";
35             }
36              
37             # alias for path
38 2     2 1 14 sub as { shift->path(@_) }
39              
40             sub node {
41             # args:
42             # scalar string = varname
43             # array ref - array of labels
44             # hash ref - hash of props/values
45 93     93 1 300 my $self = shift;
46 93 100       295 unless (@_) {
47 6         18 push @{$self->{stmt}}, '()';
  6         22  
48 6         48 return $self;
49             }
50 87         237 my ($varname) = grep { !ref } @_;
  118         467  
51 87         225 my ($lbls) = grep { ref eq 'ARRAY' } @_;
  118         386  
52 87         218 my ($props) = grep { ref eq 'HASH' } @_;
  118         341  
53             # look for labels
54 87         185 my @l;
55 87         359 ($varname, @l) = split /:/, $varname;
56 87 100       310 if (@l) {
57 19   50     141 $lbls //= [];
58 19         61 push @$lbls, @l;
59             }
60 87 100       306 my $str = $lbls ? join(':',$varname, @$lbls) : $varname;
61 87 100       273 if ($props) {
62 27         58 my $p;
63 27         167 while (my($k,$v) = each %$props) {
64 31         351 push @$p, "$k:".$self->_quote($v);
65             }
66 27         150 $p = join(',',@$p);
67 27         102 $str .= " {$p}";
68             }
69 87         239 push @{$self->{stmt}}, "($str)";
  87         404  
70 87         599 return $self;
71             }
72              
73 85     85 1 307 sub N {shift->node(@_);}
74              
75             sub related_to {
76 32     32 1 78 my $self = shift;
77 32 100       108 unless (@_) {
78 3         7 push @{$self->{stmt}}, '--';
  3         12  
79 3         16 return $self;
80             }
81 29         78 my ($hops) = grep { ref eq 'ARRAY' } @_;
  44         174  
82 29         83 my ($props) = grep { ref eq 'HASH' } @_;
  44         152  
83 29         81 my ($varname,$type) = grep { !ref } @_;
  44         157  
84 29 100       96 if ($type) {
85 3         14 ($varname) = split /:/,$varname;
86             } else {
87 26         194 ($varname, $type) = $varname =~ /^([^:]*):?(.*)/;
88             }
89 29         87 my $dir;
90 29 100       99 if ($varname) {
91 27         123 $varname =~ s/^(<|>)//;
92 27         96 $dir = $1;
93 27         92 $varname =~ s/(<|>)$//;
94 27         79 $dir = $1;
95             }
96 29 100       111 unless ($dir) {
97 15 100       59 if ($type) {
98 11         44 $type =~ s/^(<|>)//;
99 11         31 $dir = $1;
100 11         66 $type =~ s/(<|>)$//;
101 11         39 $dir = $1;
102             }
103             }
104 29 100       123 my $str = $varname.($type ? ":$type" : "");
105              
106 29 100       98 if ($hops) {
107 9 100       47 if (@$hops == 0) {
    50          
108 2         8 $str.="*";
109             }
110             elsif (@$hops==1) {
111 0         0 $str .= "*$$hops[0]";
112             }
113             else {
114 7         33 $str .= "*$$hops[0]..$$hops[1]"
115             }
116             }
117 29 100       126 if ($props) {
118 3         10 my $p;
119 3         27 while (my($k,$v) = each %$props) {
120 3         22 push @$p, "$k:".$self->_quote($v);
121             }
122 3         14 $p = join(',',@$p);
123 3         16 $str .= " {$p}";
124             }
125 29 100       115 $str = ($str ? "-[$str]-" : '--');
126 29         90 $str =~ s/\[ \{/[{/;
127 29 100       96 if ($dir) {
128 22 100       88 if ($dir eq "<") {
    50          
129 7         23 $str = "<$str";
130             }
131             elsif ($dir eq ">") {
132 15         46 $str = "$str>";
133             }
134             else {
135 0         0 1; # huh?
136             }
137             }
138 29         70 push @{$self->{stmt}}, $str;
  29         97  
139 29         178 return $self;
140             }
141              
142 29     29 1 108 sub R {shift->related_to(@_)}
143              
144             # N('a')->toN('b') -> (a)-->(b)
145             # N('a')->fromN('b') -> (a)<--(b)
146 1     1   5 sub _N {shift->related_to->node(@_)}
147 1     1 1 5 sub to_N {shift->related_to('>')->node(@_)}
148 1     1 1 4 sub from_N {shift->related_to('<')->node(@_)}
149              
150             # 'class' method
151             # do pattern->C($pat1, $pat2)
152             sub compound {
153 5     5 1 16 my $self = shift;
154 5         24 return join(',',@_);
155             }
156              
157 5     5 1 23 sub C {shift->compound(@_)}
158              
159 17     17 0 123 sub clear { shift->{stmt}=[],1; }
160              
161             sub as_string {
162 160     160 0 1171 my $self = shift;
163 160         294 return join('',@{$self->{stmt}});
  160         1292  
164             }
165              
166             sub _quote {
167             return $_[1] if (
168 34 100 100 34   372 ($_[0]->{no_quote} and $_[1] =~ $_[0]->{no_quote}) or
      100        
169             $_[1] =~ /(?:^|\s)\$/ # no quote parameters
170             );
171 30 100       119 return ${$_[1]} if (ref $_[1] eq 'SCALAR');
  1         12  
172             # escape single quotes
173 29         91 my $v = $_[1];
174 29         111 $v =~ s/'/\\'/g;
175 29         229 return "'$v'";
176             }
177 0     0 0   sub pop { pop @{shift->{stmt}}; }
  0            
178              
179             sub belch (@) {
180 0     0 0   my($func) = (caller(1))[3];
181 0           Carp::carp "[$func] Warning: ", @_;
182             }
183              
184             sub puke (@) {
185 0     0 0   my($func) = (caller(1))[3];
186 0           Carp::croak "[$func] Fatal: ", @_;
187             }
188              
189             =head1 NAME
190              
191             Neo4j::Cypher::Pattern - Generate Cypher pattern strings
192              
193             =head1 SYNOPSIS
194              
195             # express a cypher pattern
196             use Neo4j::Cypher::Pattern qw/ptn/;
197              
198             ptn->node();
199             ptn->N(); #alias
200             ptn->N("varname");
201             ptn->N("varname",["label"],{prop => "value"});
202             ptn->N("varname:label");
203             ptn->N(["label"],{prop => "value"});
204              
205             ptn->node('a')->related_to()->node('b'); # (a)--(b)
206             ptn->N('a')->R()->N('b'); # alias
207             # additional forms
208             ptn->N('a')->R("varname","typename",[$minhops,$maxhops],{prop => "value"})
209             ->N('b'); # (a)-[varname:typename*minhops..maxhops { prop:"value }]-(b)
210             ptn->N('a')->R("varname:typename")->N('b'); # (a)-[varname:typename]-(b)
211             ptn->N('a')->R(":typename")->N('b'); # (a)-[:typename]-(b)
212             ptn->N('a')->R("", "typename")->N('b'); # (a)-[:typename]-(b)
213             # directed relns
214             ptn->N('a')->R("<:typename")->N('b'); # (a)<-[:typename]-(b)
215             ptn->N('a')->R("varname:typename>")->N('b'); # (a)-[varname:typename]->(b)
216              
217             # these return strings
218             $pattern->path('varname'); # path variable assigned to a pattern
219             $pattern->as('varname'); # alias
220             ptn->compound($pattern1, $pattern2); # comma separated patterns
221             ptn->C($pattern1, $pattern2); # alias
222              
223             =head1 DESCRIPTION
224              
225             The L
226             query language of the graph database L uses
227             L
228             to represent graph nodes and their relationships, for selecting and
229             matching in queries. C can be used to create
230             Cypher pattern productions in Perl in an intuitive way. It is part of
231             the L distribution.
232              
233             =head2 Basic idea : produce patterns by chaining method calls
234              
235             C objects possess methods to represent nodes
236             and relationships. Each method adds its portion of the pattern, with
237             arguments, to the object's internal queue. Every method returns the
238             object itself. When an object is rendered as a string, it concatenates
239             nodes and relationship productions to yield the entire query statement
240             as a string.
241              
242             These features add up to the following idiom. Suppose we want to
243             render the Cypher pattern
244              
245             (b {name:"Slate"})<-[:WORKS_FOR]-(a {name:"Fred"})-[:KNOWS]->(c {name:"Barney"})
246              
247             In C, we do
248              
249             $p = Neo4j::Cypher::Pattern->new()->N('b',{name=>'Slate'})
250             ->R('<:WORKS_FOR')->N('a',{name => 'Fred'})
251             ->R(':KNOWS>')->N('c',{name=>'Barney'});
252             print "$p\n"; # "" is overloaded by $p->as_string()
253              
254             Because you may create many patterns in a program, a short
255             alias for the constructor can be imported, and extra variable
256             assignments can be avoided.
257              
258             print ptn->N('b',{name=>'Slate'})
259             ->R('<:WORKS_FOR')->N('a',{name => 'Fred'})
260             ->R(':KNOWS>')->N('c',{name=>'Barney'}), "\n";
261              
262             =head2 Quoting
263              
264             In pattern productions, values for properties will be quoted by
265             default with single quotes (single quotes that are present will be
266             escaped) unless the values are numeric.
267              
268             To prevent quoting Cypher statement list variable names (for example),
269             make the name an argument to the pattern I:
270              
271             ptn('event')->N('y')->R("<:IN")->N('e:Event'=> { id => 'event.id' });
272              
273             # renders (y)<-[:IN]-(e:Event {id:event.id})
274             # rather than (y)<-[:IN]-(e:Event {id:"event.id"})
275              
276             =head1 METHODS
277              
278             =over
279              
280             =item Constructor new()
281              
282             =item pattern(), ptn()
283              
284             Exportable aliases for the constructor. Arguments are variable names
285             that should not be quoted in rendering values of properties.
286              
287             =item node(), N()
288              
289             Render a node. Arguments in any order:
290              
291             scalar string: variable name or variable:label
292             array ref: array of node labels
293             hash ref: hash of property => value
294              
295             =item related_to(), R()
296              
297             Render a relationship. Arguments in any order:
298              
299             scalar string: variable name or variable:type
300             array ref: variable-length pattern:
301             [$minhops, $maxhops]
302             [] (empty array)- any number of hops
303             [$hops] - exactly $hops
304             hash ref : hash of property => value
305              
306             =item path(), as()
307              
308             Render the pattern set equal to a path variable:
309              
310             $p = ptn->N('a')->_N('b');
311             print $p->as('pth'); # gives 'pth = (a)--(b)'
312              
313             =item compound(), C()
314              
315             Render multiple patterns separated by commas
316              
317             ptn->compound( ptn->N('a')->to_N('b'), ptn->N('a')->from_N('c'));
318             # (a)-->(b), (a)<--(c)
319              
320             =item Shortcuts _N, to_N, from_N
321              
322             ptn->N('a')->_N('b'); # (a)--(b)
323             ptn->N('a')->to_N('b'); # (a)-->(b)
324             pth->N('a')->from_N('b'); # (a)<--(b)
325              
326             =back
327              
328             =head1 SEE ALSO
329              
330             L
331              
332             =head1 AUTHOR
333              
334             Mark A. Jensen
335             CPAN: MAJENSEN
336             majensen -at- cpan -dot- org
337              
338             =head1 LICENSE
339              
340             This software is provided for use under the terms of Perl itself.
341              
342             =head1 COPYRIGHT
343              
344             (c) 2017 Mark A. Jensen
345              
346             =cut
347             1;