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   756 use base Exporter;
  3         4  
  3         191  
3 3     3   16 use Carp;
  3         5  
  3         180  
4 3     3   16 use strict;
  3         5  
  3         65  
5 3     3   12 use warnings;
  3         4  
  3         98  
6 3     3   2039 use overload '""' => 'as_string';
  3         1556  
  3         16  
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 51     51 1 625 my $class = shift;
16 51         62 my $self = {};
17 51         81 $self->{stmt}=[];
18 51 100       142 if (@_) {
19 2         4 my $nq = join('|',@_);
20 2         22 $self->{no_quote} = qr/$nq/;
21             }
22 51         184 bless $self, $class;
23             }
24              
25             sub pattern {
26 0     0 1 0 Neo4j::Cypher::Pattern->new(@_);
27             }
28              
29 50     50 1 109 sub ptn { Neo4j::Cypher::Pattern->new(@_); }
30              
31             sub path {
32 2     2 1 3 my $self = shift;
33 2 50 33     9 puke("Need arg1 => identifier") if (!defined $_[0] || ref($_[0]));
34 2         6 return "$_[0] = $self";
35             }
36              
37             # alias for path
38 2     2 1 5 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 94     94 1 176 my $self = shift;
46 94 100       180 unless (@_) {
47 6         11 push @{$self->{stmt}}, '()';
  6         12  
48 6         28 return $self;
49             }
50 88         147 my ($varname) = grep { !ref } @_;
  120         267  
51 88         131 my ($lbls) = grep { ref eq 'ARRAY' } @_;
  120         200  
52 88         120 my ($props) = grep { ref eq 'HASH' } @_;
  120         174  
53             # look for labels
54 88         107 my @l;
55 88         196 ($varname, @l) = split /:/, $varname;
56 88 100       169 if (@l) {
57 20   50     86 $lbls //= [];
58 20         39 push @$lbls, @l;
59             }
60 88 100       182 my $str = $lbls ? join(':',$varname, @$lbls) : $varname;
61 88 100       136 if ($props) {
62 28         34 my $p;
63 28         95 while (my($k,$v) = each %$props) {
64 32         185 push @$p, "$k:".$self->_quote($v);
65             }
66 28         82 $p = join(',',@$p);
67 28         64 $str .= " {$p}";
68             }
69 88         102 push @{$self->{stmt}}, "($str)";
  88         233  
70 88         348 return $self;
71             }
72              
73 86     86 1 153 sub N {shift->node(@_);}
74              
75             sub related_to {
76 32     32 1 44 my $self = shift;
77 32 100       59 unless (@_) {
78 3         4 push @{$self->{stmt}}, '--';
  3         8  
79 3         10 return $self;
80             }
81 29         40 my ($hops) = grep { ref eq 'ARRAY' } @_;
  44         82  
82 29         47 my ($props) = grep { ref eq 'HASH' } @_;
  44         69  
83 29         36 my ($varname,$type) = grep { !ref } @_;
  44         81  
84 29 100       48 if ($type) {
85 3         7 ($varname) = split /:/,$varname;
86             } else {
87 26         127 ($varname, $type) = $varname =~ /^([^:]*):?(.*)/;
88             }
89 29         47 my $dir;
90 29 100       47 if ($varname) {
91 27         71 $varname =~ s/^(<|>)//;
92 27         56 $dir = $1;
93 27         60 $varname =~ s/(<|>)$//;
94 27         43 $dir = $1;
95             }
96 29 100       55 unless ($dir) {
97 15 100       29 if ($type) {
98 11         19 $type =~ s/^(<|>)//;
99 11         17 $dir = $1;
100 11         34 $type =~ s/(<|>)$//;
101 11         22 $dir = $1;
102             }
103             }
104 29 100       67 my $str = $varname.($type ? ":$type" : "");
105              
106 29 100       49 if ($hops) {
107 9 100       24 if (@$hops == 0) {
    50          
108 2         4 $str.="*";
109             }
110             elsif (@$hops==1) {
111 0         0 $str .= "*$$hops[0]";
112             }
113             else {
114 7         18 $str .= "*$$hops[0]..$$hops[1]"
115             }
116             }
117 29 100       54 if ($props) {
118 3         4 my $p;
119 3         15 while (my($k,$v) = each %$props) {
120 3         15 push @$p, "$k:".$self->_quote($v);
121             }
122 3         9 $p = join(',',@$p);
123 3         7 $str .= " {$p}";
124             }
125 29 100       90 $str = ($str ? "-[$str]-" : '--');
126 29         48 $str =~ s/\[ \{/[{/;
127 29 100       59 if ($dir) {
128 22 100       46 if ($dir eq "<") {
    50          
129 7         15 $str = "<$str";
130             }
131             elsif ($dir eq ">") {
132 15         23 $str = "$str>";
133             }
134             else {
135 0         0 1; # huh?
136             }
137             }
138 29         38 push @{$self->{stmt}}, $str;
  29         51  
139 29         91 return $self;
140             }
141              
142 29     29 1 66 sub R {shift->related_to(@_)}
143              
144             # N('a')->toN('b') -> (a)-->(b)
145             # N('a')->fromN('b') -> (a)<--(b)
146 1     1   3 sub _N {shift->related_to->node(@_)}
147 1     1 1 2 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 9 my $self = shift;
154 5         15 return join(',',@_);
155             }
156              
157 5     5 1 14 sub C {shift->compound(@_)}
158              
159 17     17 0 52 sub clear { shift->{stmt}=[],1; }
160              
161             sub as_string {
162 163     163 0 512 my $self = shift;
163 163         191 return join('',@{$self->{stmt}});
  163         692  
164             }
165              
166             sub _quote {
167             return $_[1] if (
168 35 100 100 35   203 ($_[0]->{no_quote} and $_[1] =~ $_[0]->{no_quote}) or
      100        
169             $_[1] =~ /(?:^|\s)\$/ # no quote parameters
170             );
171 31 100       62 return ${$_[1]} if (ref $_[1] eq 'SCALAR');
  1         5  
172             # escape single quotes
173 30         55 my $v = $_[1];
174 30         52 $v =~ s/'/\\'/g;
175 30         129 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;