File Coverage

blib/lib/REST/Neo4p/Constraint.pm
Criterion Covered Total %
statement 144 163 88.3
branch 35 50 70.0
condition 9 20 45.0
subroutine 29 34 85.2
pod 17 22 77.2
total 234 289 80.9


line stmt bran cond sub pod time code
1             #$Id$
2             package REST::Neo4p::Constraint;
3 4     4   29360 use base 'Exporter';
  4         20  
  4         829  
4 4     4   37 use REST::Neo4p;
  4         10  
  4         142  
5 4     4   40 use REST::Neo4p::Exceptions;
  4         12  
  4         135  
6 4     4   23 use JSON;
  4         8  
  4         85  
7 4     4   1224 use Data::Dumper;
  4         13  
  4         399  
8              
9 4     4   39 use Scalar::Util qw(looks_like_number);
  4         16  
  4         341  
10 4     4   39 use strict;
  4         12  
  4         155  
11 4     4   36 use warnings;
  4         13  
  4         1087  
12              
13             our @EXPORT = qw(serialize_constraints load_constraints);
14             our @VALIDATE = qw(validate_properties validate_relationship validate_relationship_type);
15             our @EXPORT_OK = (@VALIDATE);
16             our %EXPORT_TAGS = (
17             validate => \@VALIDATE,
18             auto => [@EXPORT],
19             all => [@EXPORT,@EXPORT_OK]
20             );
21              
22             our $jobj = JSON->new->utf8;
23             $jobj->allow_blessed(1);
24             $jobj->convert_blessed(1);
25             my $regex_to_json = sub {
26 3     3   6 my $qr = shift;
27 3         12 local $Data::Dumper::Terse=1;
28 3         22 $qr = Dumper $qr;
29 3         309 chomp $qr;
30 3         18 return $qr;
31             };
32              
33             BEGIN {
34 4     4   1561 $REST::Neo4p::Constraint::VERSION = '0.4000';
35             }
36              
37             # valid constraint types
38             our @CONSTRAINT_TYPES = qw( node_property relationship_property
39             relationship_type relationship );
40             our $CONSTRAINT_TABLE = {};
41              
42              
43             # flag - when set, disallow relationships that are not allowed by current
44             # relationship types
45             # default strict
46             $REST::Neo4p::Constraint::STRICT_RELN_TYPES = 1;
47              
48             # flag - when set, require strict checking of relationship properties when
49             # validating relationships -- i.e., a relationship with no properties is
50             # disallowed unless there is a specific relationship_property constraint
51             # allow this
52             # default relaxed
53              
54             $REST::Neo4p::Constraint::STRICT_RELN_PROPS = 0;
55              
56             # flag - when set, use the database to store constraints
57             $REST::Neo4p::Constraint::USE_NEO4J = 0;
58              
59              
60             sub new {
61 34     34 1 2890 my $class = shift;
62 34         74 my ($tag, $constraints) = @_;
63 34         71 my $self = bless {}, $class;
64 34 100       88 unless (defined $tag) {
65 1         54 REST::Neo4p::LocalException->throw("New constraint requires tag as arg 1\n");
66             }
67 33 100       145 unless ($tag =~ /^[a-z0-9_.]+$/i) {
68 1         5 REST::Neo4p::LocalException->throw("Constraint tag may contain only alphanumerics chars, underscore and period\n");
69             }
70 32 50       519 if ( !grep /^$tag$/,keys %$CONSTRAINT_TABLE ) {
71 32         143 $self->{_tag} = $tag;
72             }
73             else {
74 0         0 REST::Neo4p::LocalException->throw("Constraint with tag '$tag' is already defined\n");
75             }
76 32         129 $self->new_from_constraint_hash($constraints);
77 31         101 $CONSTRAINT_TABLE->{$tag} = $self;
78             }
79              
80             sub new_from_constraint_hash {
81 0     0 0 0 REST::Neo4p::AbstractMethodException->throw("new_from_constraint_hash() is an abstract method of ".__PACKAGE__."\n");
82             }
83              
84             sub TO_JSON {
85 4     4   39 no warnings qw(redefine);
  4         12  
  4         8214  
86 11     11 0 21 my $self = shift;
87 11         14 my $store;
88 11         20 my $old = *Regexp::TO_JSON{CODE};
89 11         20 *Regexp::TO_JSON = $regex_to_json;
90 11         26 $store = $self->constraints;
91 11         30 $store->{_condition} = $self->condition;
92 11         21 $store->{_priority} = $self->priority;
93 11 100       65 $store->{_relationship_type} = $self->rtype if $self->can('rtype');
94 11         25 my $ret = $jobj->encode({tag => $self->tag, type => $self->type,
95             _constraint_hash => $store });
96 11 100       38 *Regexp::TO_JSON = $old if $old;
97 11         69 return $ret;
98             }
99              
100             sub new_from_json {
101 11     11 0 321 my $class = shift;
102 11         23 my ($json) = @_;
103 11 50       25 unless (ref($json)) {
104 11         112 $json = decode_json($json);
105             }
106 11 50 33     55 unless ( $json->{tag} && $json->{type} ) {
107 0         0 REST::Neo4p::LocalException->throw("json does not correctly specify a constraint object\n");
108             }
109 11         21 my $subclass = $json->{type};
110 11         30 _fix_constraints($json->{_constraint_hash});
111 11         72 $subclass =~ s/^(.)/\U$1\E/;
112 11         43 $subclass =~ s/_(.)/\U$1\E/;
113 11         32 $subclass = 'REST::Neo4p::Constraint::'.$subclass;
114 11         52 $subclass->new($json->{tag}, $json->{_constraint_hash});
115             }
116              
117             sub _fix_constraints {
118             # make qr// strings into Regexp objects
119 78     78   115 local $_ = shift;
120 78 100       249 if (ref eq 'HASH') {
    100          
121 17         61 while (my ($k, $v) = each %$_) {
122 50 100 100     180 if ($v && ($v =~ /^qr\//)) {
123 3 50       11 if ($v =~ /\(\?(\^|-[a-z]+):.*\)/) {
124 0         0 $v =~ s{/\(\?(\^|-[a-z]+):}{/}; # kludge - eval wants to wrap (?:^...) around a qr string
125 0         0 $v =~ s{\)/}{/}; # kludge - even if one is there already
126             }
127 3         263 $_->{$k} = eval $v; # replace with Regexp
128             }
129             else {
130 47         89 _fix_constraints($v);
131             }
132             }
133             }
134             elsif (ref eq 'ARRAY') {
135 11         27 foreach my $v (@$_) {
136 20         33 _fix_constraints($v);
137             }
138             }
139             }
140              
141 88     88 1 351 sub tag { shift->{_tag} }
142 281     281 1 911 sub type { shift->{_type} }
143 109     109 1 2111 sub condition { shift->{_constraints}{_condition} } ##
144 338     338 1 556 sub priority { shift->{_constraints}{_priority} } ##
145 483     483 1 1385 sub constraints { shift->{_constraints} }
146              
147             sub set_priority {
148 3     3 1 9 my $self = shift;
149 3         7 my ($priority_value) = @_;
150 3 50       14 unless (looks_like_number($priority_value)) {
151 0         0 REST::Neo4p::LocalException->throw("Priority value must be numeric\n");
152             }
153 3         16 return $self->{_constraints}{_priority} = $priority_value;
154             }
155              
156             sub get_constraint {
157 4     4 1 69 my $class = shift;
158 4 100       19 if (ref $class) {
159 1         32 REST::Neo4p::ClassOnlyException->throw("get_constraint is a class method only\n");
160             }
161 3         12 my ($tag) = @_;
162 3         17 return $CONSTRAINT_TABLE->{$tag};
163             }
164              
165             sub get_all_constraints {
166 0     0 1 0 my $class = shift;
167 0 0       0 if (ref $class) {
168 0         0 REST::Neo4p::ClassOnlyException->throw("get_constraint is a class method only\n");
169             }
170 0         0 return %{$CONSTRAINT_TABLE};
  0         0  
171             }
172              
173             sub drop {
174 11     11 0 2557 my $self = shift;
175 11         34 delete $CONSTRAINT_TABLE->{$self->tag};
176             }
177              
178             sub drop_constraint {
179 1     1 0 3 my $class = shift;
180 1 50       3 if (ref $class) {
181 0         0 REST::Neo4p::ClassOnlyException->throw("get_constraint is a class method only\n");
182             }
183 1         2 my ($tag) = @_;
184 1         17 delete $CONSTRAINT_TABLE->{$tag};
185             }
186              
187             sub add_constraint {
188 0     0 1 0 REST::Neo4p::AbstractMethodException->throw("Cannot call add_constraint() from the Constraint parent class\n");
189             }
190              
191             sub remove_constraint {
192 0     0 1 0 REST::Neo4p::AbstractMethodException->throw("Cannot call remove_constraint() from the Constraint parent class\n");
193             }
194              
195             sub set_condition {
196 0     0 1 0 REST::Neo4p::AbstractMethodException->throw("Cannot call set_condition() from the Constraint parent class\n");
197             }
198              
199             # return the first property constraint according to priority
200             # that the property hash arg satisfies, or false if no match
201              
202             sub validate_properties {
203             # my $class = shift;
204             # Exported
205 20     20 1 31 my ($properties) = @_;
206 20 50       39 return unless defined $properties;
207             # if (ref $class) {
208             # REST::Neo4p::ClassOnlyException->throw("validate_properties() is a class-only method\n");
209             # }
210              
211 20 50 33     88 unless ( (ref($properties) =~ /Neo4p::(Node|Relationship)$/) ||
212             (ref($properties) eq 'HASH') ) {
213 0         0 REST::Neo4p::LocalException->throw("Arg to validate_properties() must be a hashref, a Node object, or a Relationship object");
214             }
215             my $type = (ref($properties) =~ /Neo4p/) ? $properties->entity_type :
216 20 50 50     85 (delete $properties->{__type} || '');
217 20         49 my @prop_constraints = grep { $_->type =~ /${type}_property$/ } values %$CONSTRAINT_TABLE;
  160         299  
218 20         64 @prop_constraints = sort {$b->priority <=> $a->priority} @prop_constraints;
  160         271  
219 20         32 my $ret;
220 20         36 foreach (@prop_constraints) {
221 50 100       108 if ($_->validate($properties)) {
222 19         31 $ret = $_;
223 19         27 last;
224             }
225             }
226 20         57 return $ret;
227             }
228              
229             sub validate_relationship {
230             # my $class = shift;
231             # Exported
232 2     2 1 12 my ($from, $to, $reln_type, $reln_props) = @_;
233 2         7 my ($reln) = @_;
234             # if (ref $class) {
235             # REST::Neo4p::ClassOnlyException->throw("validate_relationship() is a class-only method\n");
236             # }
237 2 50       7 return unless defined $from;
238 2 50 33     34 unless ( (ref($reln) =~ /Neo4p::Relationship$/) ||
      33        
      33        
239             ( (ref($from) =~ /Neo4p::Node|HASH$/) && (ref($to) =~ /Neo4p::Node|HASH$/) &&
240             defined $reln_type ) ) {
241 0         0 REST::Neo4p::LocalException->throw("validate_relationship() requires a Relationship object, or two property hashrefs or nodes followed by a relationship type\n");
242             }
243 2         10 my @reln_constraints = grep {$_->type eq 'relationship'} values %$CONSTRAINT_TABLE;
  16         28  
244 2         9 @reln_constraints = sort {$a->priority <=> $b->priority} @reln_constraints;
  2         9  
245 2         4 my $ret;
246 2         5 foreach (@reln_constraints) {
247 3 100       11 if ($_->validate($from => $to, $reln_type, $reln_props)) {
248 1         3 $ret = $_;
249 1         2 last;
250             }
251             }
252 2         11 return $ret;
253             }
254              
255             sub validate_relationship_type {
256             # my $class = shift;
257             # Exported
258 11     11 1 23 my ($reln_type) = @_;
259             # if (ref $class) {
260             # REST::Neo4p::ClassOnlyException->throw("validate_relationhip_type() is a class-only method\n");
261             # }
262 11 50       23 return unless defined $reln_type;
263 11         33 my @type_constraints = grep {$_->type eq 'relationship_type'} values %$CONSTRAINT_TABLE;
  88         158  
264 11         29 @type_constraints = sort {$a->priority <=> $b->priority} @type_constraints;
  0         0  
265 11         18 my $ret;
266 11         21 foreach (@type_constraints) {
267 11 100       32 if ($_->validate($reln_type)) {
268 10         20 $ret = $_;
269 10         19 last;
270             }
271             }
272 11         38 return $ret;
273             }
274              
275             sub serialize_constraints {
276 1     1 1 4 my $json = sprintf "%s", join(", ", map { $jobj->encode($_) } values %$CONSTRAINT_TABLE);
  6         30  
277 1         7 return "[$json]";
278             }
279              
280             sub load_constraints {
281 1     1 1 561 my ($json) = @_;
282 1         3 eval {
283 1         24 $json = decode_json($json);
284             };
285 1 50       17 if (my $e = Exception::Class->caught()) {
286 0         0 REST::Neo4p::LocalException->throw("JSON error: $e");
287             }
288 1         12 for (@$json) {
289 6         17 REST::Neo4p::Constraint->new_from_json($_);
290             }
291 1         4 return 1;
292             }
293              
294             =head1 NAME
295              
296             REST::Neo4p::Constraint - Application-level Neo4j Constraints
297              
298             =head1 SYNOPSIS
299              
300             See L,
301             L,
302             L for examples.
303              
304             =head1 DESCRIPTION
305              
306             Objects of class REST::Neo4p::Constraint are used to capture and
307             organize L application level constraints on Neo4j Node
308             and Relationship content.
309              
310             The L module provides a more convenient
311             factory for REST::Neo4p::Constraint subclasses that specify L
312             property|REST::Neo4p::Constraint::Property>, L
313             property|REST::Neo4p::Property>,
314             L, and
315             L
316             constraints.
317              
318             =head1 FLAGS
319              
320             =over
321              
322             =item C<$REST::Neo4p::Constraint::STRICT_RELN_TYPES>
323              
324             When true, relationships are disallowed if the relationship type does
325             not meet any current relationship type constraint. Default is true.
326              
327             =item C<$REST::Neo4p::Constraint::STRICT_RELN_PROPS>
328              
329             When true, relationships are disallowed if their relationship
330             properties do not meet any current relationship property constraint.
331              
332             Default is false. This is so relationships without properties can be
333             made freely. When relationship property checking is strict, you can
334             allow relationships without properties by setting the following
335             constraint:
336              
337             create_constraint(
338             tag => 'free_reln_prop',
339             type => 'relationship_property',
340             rtype => '*',
341             condition => 'all',
342             constraints => {}
343             );
344              
345             =back
346              
347             =head1 METHODS
348              
349             =head2 Class Methods
350              
351             =over
352              
353             =item new()
354              
355             $reln_pc = REST::Neo4p::Constraint::RelationshipProperty->new($constraints);
356              
357             Constructor. Construction also registers the constraint for
358             validation. See subclass pod for details.
359              
360             =item get_constraint()
361            
362             $c = REST::Neo4p::Constraint->get_constraint('spiffy_node');
363              
364             Get a registered constraint by constraint tag. Returns false if none found.
365              
366             =item get_all_constraints()
367              
368             %constraints = REST::Neo4p::Constraint->get_all_constraints();
369              
370             Get a hash of all registered constraint objects, keyed by constraint tag.
371              
372             =back
373              
374             =head2 Instance Methods
375              
376             =over
377              
378             =item tag()
379              
380             =item type()
381              
382             =item condition()
383              
384             =item set_condition()
385              
386             $reln_c->set_condition('only');
387              
388             Set the group condition for the constraint. See subclass pod for details.
389              
390             =item priority()
391              
392             =item set_priority()
393              
394             $node_pc->set_priority(10);
395              
396             Constraints with larger priority values are checked before those with
397             smaller values by the L|/Functional interface for
398             validation> functions.
399              
400             =item constraints()
401              
402             Returns the hashref of constraints. Format depends on the subclass.
403              
404             =item add_constraint()
405              
406             $node_pc->add_constraint( 'warning_level' => qr/^[0-9]$/ );
407             $reln_c->add_constraint( { 'species' => 'genus' } );
408              
409             Add an individual constraint specification to an existing constraint
410             object. See subclass pod for details.
411              
412             =item remove_constraint()
413              
414             $node_pc->remove_constraint( 'warning_level' );
415             $reln_c->remove_constraint( { 'genus' => 'species' } );
416              
417             Remove an individual constraint specification from an existing
418             constraint object. See subclass pod for details.
419              
420             =back
421              
422             =head2 Functional interface for validation
423              
424             =over
425              
426             =item validate_properties()
427              
428             validate_properties( $node_object )
429             validate_properties( $relationship_object );
430             validate_properties( { name => 'Steve', instrument => 'banjo' } );
431              
432             =item validate_relationship()
433              
434             validate_relationship ( $relationship_object );
435             validate_relationship ( $node_object1 => $node_object2,
436             $reln_type );
437             validate_relationship ( { name => 'Steve', instrument => 'banjo' } =>
438             { name => 'Marcia', instrument => 'blunt' },
439             'avoids' );
440              
441             =item validate_relationship_type()
442              
443             validate_relationship_type( 'avoids' )
444              
445             =back
446              
447             Functional interface. Returns the registered constraint object with
448             the highest priority that the argument satisfies, or false if none is
449             satisfied.
450              
451             These methods can be exported as follows:
452              
453             use REST::Neo4p::Constraint qw(:validate)
454              
455             They can also be exported from L:
456              
457             use REST::Neo4p::Constrain qw(:validate)
458              
459             =head2 Serializing and loading constraints
460              
461             =over
462              
463             =item serialize_constraints()
464              
465             open $f, ">constraints.json";
466             print $f serialize_constraints();
467              
468             Returns a JSON-formatted representation of all currently registered
469             constraints.
470              
471             =item load_constraints()
472              
473             open $f, "constraints.json";
474             {
475             local $/ = undef;
476             load_constraints(<$f>);
477             }
478              
479             Creates and registers a list of constraints specified by a JSON string
480             as produced by L.
481              
482             =back
483              
484             =head1 SEE ALSO
485              
486             L,L,
487             L, L,
488             L. L, L,
489              
490             =head1 AUTHOR
491              
492             Mark A. Jensen
493             CPAN ID: MAJENSEN
494             majensen -at- cpan -dot- org
495              
496             =head1 LICENSE
497              
498             Copyright (c) 2012-2020 Mark A. Jensen. This program is free software; you
499             can redistribute it and/or modify it under the same terms as Perl
500             itself.
501              
502             =cut
503              
504             1;