File Coverage

blib/lib/RDF/KV/Patch.pm
Criterion Covered Total %
statement 55 176 31.2
branch 23 116 19.8
condition 6 25 24.0
subroutine 10 25 40.0
pod 9 9 100.0
total 103 351 29.3


line stmt bran cond sub pod time code
1             package RDF::KV::Patch;
2              
3 3     3   906 use 5.010;
  3         15  
4 3     3   16 use strict;
  3         8  
  3         76  
5 3     3   15 use warnings FATAL => 'all';
  3         15  
  3         124  
6              
7 3     3   17 use Moose;
  3         10  
  3         32  
8 3     3   22894 use namespace::autoclean;
  3         8281  
  3         20  
9              
10 3     3   1326 use RDF::Trine qw(iri blank literal);
  3         1498438  
  3         245  
11              
12 3     3   520 use URI::BNode;
  3         180578  
  3         8592  
13              
14             =head1 NAME
15              
16             RDF::KV::Patch - Representation of RDF statements to be added or removed
17              
18             =head1 VERSION
19              
20             Version 0.11
21              
22             =cut
23              
24             our $VERSION = '0.11';
25              
26             =head1 SYNOPSIS
27              
28             my $patch = RDF::KV::Patch->new;
29              
30             $patch->add_this($s, $p, $o, $g);
31             $patch->remove_this($s, $p, undef, $g); # a wildcard
32              
33             $patch->apply($model); # an RDF::Trine::Model instance
34              
35             =head1 DESCRIPTION
36              
37             This module is designed to represent a I<diff> for RDF graphs. You add
38             statements to its I<add> or I<remove> sides, then you L</apply> them
39             to a L<RDF::Trine::Model> object. This should probably be part of
40             L<RDF::Trine> if there isn't something like this in there already.
41              
42             =cut
43              
44             # positive statements
45             has _pos => (
46             is => 'ro',
47             isa => 'HashRef',
48             default => sub { {} },
49             );
50              
51             # negative statements/wildcards
52             has _neg => (
53             is => 'ro',
54             isa => 'HashRef',
55             default => sub { {} },
56             );
57              
58             =head1 METHODS
59              
60             =head2 new
61              
62             Stub constructor, nothing of interest.
63              
64             =cut
65              
66             =head2 add_this { $S, $P, $O | $statement } [, $graph ]
67              
68             Add a statement, or set of terms, to the I<add> side of the patch.
69              
70             This method and its siblings are fairly robust, and can take a wide
71             variety of inputs, from triple and quad statements, to individual
72             subject/predicate/object/graph objects, to string literals, to
73             variable nodes, to undef. They are processed by the following scheme:
74              
75             =over 4
76              
77             =item
78              
79             If passed a L<RDF::Trine::Statement>, it will be unwound into its
80             respective components. The C<graph> attribute of a L<quad
81             statement|RDF::Trine::Statement::Quad> supersedes any given graph
82             parameter.
83              
84             =item
85              
86             The empty string, C<undef>, and L<RDF::Trine::Node::Variable> objects
87             are considered to be wildcards, which are only legal in the
88             I<negative> side of the patch, since they don't make sense on the
89             positive side. Placing wildcards in all three of the subject,
90             predicate and object positions will raise an exception, because if
91             carried out it would completely empty the graph. If you're sure you
92             want to do that, you should use another mechanism.
93              
94             =item
95              
96             I<Subjects> are coerced from string literals to L<URI> and
97             L<URI::BNode> instances and from there to
98             L<RDF::Trine::Node::Resource> and L<RDF::Trine::Node::Blank>
99             instances, respectively.
100              
101             =item
102              
103             I<Predicates> are always coerced from string literals or L<URI> objects
104             into L<RDF::Trine::Node::Resource> objects.
105              
106             =item
107              
108             I<Objects> are coerced from either string literals or C<ARRAY>
109             references into L<RDF::Trine::Node::Literal> instances, the latter
110             case mimicking L<that class's
111             constructor|RDF::Trine::Node::Literal/new>. URIs or blank nodes must
112             already be at least instances of L<URI> or L<URI::BNode>, if not
113             L<RDF::Trine::Node::Resource> or L<RDF::Trine::Node::Blank>. Note: the
114             empty string is considered a wildcard, so if you want an actual empty
115             string, you will need to pass in an L<RDF::Trine::Node::Literal> with
116             that value.
117              
118             =back
119              
120             =cut
121              
122             sub _validate {
123             # oh undef vs empty string, you're so cute.
124             my ($s, $p, $o, $g) =
125 1 50 66 1   3 map { defined $_ && !ref $_ && $_ eq '' ? undef : $_ } @_;
  3         22  
126              
127 1 50       6 if (defined $s) {
128 1 50       5 if (Scalar::Util::blessed($s)) {
129 0 0       0 if ($s->isa('RDF::Trine::Statement')) {
    0          
    0          
    0          
130             # move $p to $g
131 0 0       0 if (defined $p) {
132 0         0 $g = $p;
133 0         0 undef $p;
134             }
135              
136             # unpack statement
137 0 0       0 if ($s->isa('RDF::Trine::Statement::Quad')) {
138             ($s, $p, $o, $g) =
139 0         0 map { $s->$_ } qw(subject predicate object graph);
  0         0  
140             }
141             else {
142 0         0 ($s, $p, $o) = map { $s->$_ } qw(subject predicate object);
  0         0  
143             }
144             }
145             elsif ($s->isa('URI::BNode')) {
146 0         0 $s = blank($s->opaque);
147             }
148             elsif ($s->isa('URI')) {
149 0         0 $s = iri($s->as_string);
150             }
151             elsif ($s->isa('RDF::Trine::Node::Variable')) {
152 0         0 $s = undef;
153             }
154             else {
155             # dunno
156             }
157             }
158             else {
159             # dunno
160 1         5 $s = URI::BNode->new($s);
161 1 50       38 $s = $s->scheme eq '_' ? blank($s->opaque) : iri($s->as_string);
162             }
163             }
164              
165             # predicate will always be an iri
166 1 50       330 if (defined $p) {
167 1 50       5 if (Scalar::Util::blessed($p)) {
168 0 0       0 if ($p->isa('URI')) {
    0          
169 0         0 $p = iri($p->as_string);
170             }
171             elsif ($p->isa('RDF::Trine::Node::Variable')) {
172 0         0 $p = undef;
173             }
174             else {
175             # dunno
176             }
177             }
178             else {
179 1         7 $p = iri("$p");
180             }
181             }
182              
183 1 50       40 if (defined $o) {
184 1 50       3 if (my $ref = ref $o) {
185 1 50       4 if (Scalar::Util::blessed($o)) {
    0          
186 1 50       19 if ($o->isa('URI::BNode')) {
    50          
    50          
187 0         0 $o = blank($o->opaque);
188             }
189             elsif ($o->isa('URI')) {
190 0         0 $o = iri($o->as_string);
191             }
192             elsif ($o->isa('RDF::Trine::Node::Variable')) {
193 0         0 $o = undef;
194             }
195             else {
196             # dunno
197             }
198             }
199             elsif ($ref eq 'ARRAY') {
200 0         0 my ($lv, $lang, $dt) = @$o;
201 0 0 0     0 if (ref $dt and Scalar::Util::blessed($dt)) {
202 0 0       0 $dt = $dt->can('uri_value') ? $dt->uri_value :
    0          
203             $dt->can('as_string') ? $dt->as_string : "$dt";
204             }
205 0         0 $o = literal($lv, $lang, $dt);
206             }
207             else {
208             # dunno
209             }
210             }
211             else {
212 0         0 $o = literal($o);
213             }
214             }
215              
216 1 50       3 if (defined $g) {
217 0 0       0 if (Scalar::Util::blessed($g)) {
218 0 0       0 if ($g->isa('RDF::Trine::Node')) {
    0          
219             # do nothing
220             }
221             elsif ($g->isa('URI')) {
222             # scheme is not guaranteed to be present
223 0 0 0     0 $g = ($g->scheme || '') eq '_' ?
224             blank($g->opaque) : iri($g->as_string);
225             }
226             else {
227             # dunno
228             }
229             }
230             else {
231             # apparently rdf 1.1 graph identifiers can be bnodes
232 0         0 $g = URI::BNode->new($g);
233             # ditto scheme
234 0 0 0     0 $g = ($g->scheme || '') eq '_' ?
235             blank($g->opaque) : iri($g->as_string);
236             }
237             }
238              
239 1         4 return ($s, $p, $o, $g);
240             }
241              
242             sub _add_either {
243 1     1   3 my ($set, $s, $p, $o, $g) = @_;
244             # clobber graph, subject and predicate to strings; bnode will be _:
245             ($g, $s, $p) = map {
246 1 50       2 defined $_ ? $_->isa('RDF::Trine::Node::Blank') ?
  3 100       80  
    100          
247             $_->sse : ref $_ ? $_->uri_value : $_ : '' } ($g, $s, $p);
248              
249 1   50     61 $set->{$g} ||= {};
250 1   50     14 $set->{$g}{$s} ||= {};
251 1   50     11 $set->{$g}{$s}{$p} ||= [{}, {}];
252              
253 1 50       41 if ($o) {
254 1 50       90 if ($o->isa('RDF::Trine::Node::Literal')) {
    0          
255 1         5 my $l = $o->literal_value_language;
256 1         6 my $d = $o->literal_datatype;
257 1 0       8 my $ld = $d ? "^$d" : $l ? "\@$l" : '';
    50          
258 1   50     7 my $x = $set->{$g}{$s}{$p}[1]{$ld} ||= {};
259 1         4 $x->{$o->literal_value} = 1;
260             }
261             elsif ($o->isa('RDF::Trine::Node::Variable')) {
262 0         0 $set->{$g}{$s}{$p} = 1;
263             }
264             else {
265 0 0       0 $o = $o->isa('RDF::Trine::Node::Blank') ? $o->sse : $o->uri_value;
266 0         0 $set->{$g}{$s}{$p}[0]{$o} = 1;
267             }
268             }
269             else {
270 0         0 $set->{$g}{$s}{$p} = 1;
271             }
272             }
273              
274             sub add_this {
275 1     1 1 460 my $self = shift;
276 1         4 my ($s, $p, $o, $g) = _validate(@_);
277             Carp::croak('It makes no sense in this context to add a partial statement')
278 1 50       4 unless 3 == grep { ref $_ } ($s, $p, $o);
  3         7  
279              
280 1 50       23 my $ret = $g ? RDF::Trine::Statement::Quad->new($s, $p, $o, $g) :
281             RDF::Trine::Statement->new($s, $p, $o);
282             #warn $ret;
283              
284 1         58 _add_either($self->_pos, $s, $p, $o, $g);
285              
286 1         9 $ret;
287             }
288              
289             =head2 dont_add_this { $S, $P, $O | $statement } [, $graph ]
290              
291             Remove a statement, or set of terms, from the I<add> side of the patch.
292              
293             =cut
294              
295             sub dont_add_this {
296 0     0 1   my $self = shift;
297 0           my ($s, $p, $o, $g) = _validate(@_);
298             }
299              
300             =head2 remove_this { $S, $P, $O | $statement } [, $graph ]
301              
302             Add a statement, or set of terms, to the I<remove> side of the patch.
303              
304             =cut
305              
306             sub remove_this {
307 0     0 1   my $self = shift;
308 0           my ($s, $p, $o, $g) = _validate(@_);
309              
310             #warn Data::Dumper::Dumper([$s, $p, $o, $g]);
311              
312             Carp::croak('If you want to nuke the whole graph, just do that directly')
313 0 0         unless 1 <= grep { ref $_ } ($s, $p, $o);
  0            
314              
315 0 0         my $ret = $g ? RDF::Trine::Statement::Quad->new($s, $p, $o, $g) :
316             RDF::Trine::Statement->new($s, $p, $o);
317              
318 0           _add_either($self->_neg, $s, $p, $o, $g);
319              
320 0           $ret;
321             }
322              
323             =head2 dont_remove_this { $S, $P, $O | $statement } [, $graph ]
324              
325             Remove a statement, or set of terms, from the I<remove> side of the
326             patch.
327              
328             =cut
329              
330             sub dont_remove_this {
331 0     0 1   my $self = shift;
332 0           my ($s, $p, $o, $g) = _validate(@_);
333             }
334              
335             =head2 graphs_affected
336              
337             Return the set of graph identifiers affected by the patch, including
338             C<undef> for the null graph.
339              
340             =cut
341              
342             sub graphs_affected {
343 0     0 1   my $self = shift;
344 0           my %seen;
345              
346 0           map { $seen{$_} = 1 } (keys %{$self->_pos}, keys %{$self->_neg});
  0            
  0            
  0            
347              
348 0           map { _node($_) } keys %seen;
  0            
349             }
350              
351             =head2 subjects_affected [$use_graphs]
352              
353             Return the set of subjects that are affected by the patch,
354             irrespective of graph.
355              
356             =cut
357              
358             sub subjects_affected {
359 0     0 1   my ($self, $use_graphs) = @_;
360 0           my %seen;
361              
362 0           for my $stmt ($self->to_add, $self->to_remove) {
363 0   0       my $x = $seen{$stmt->graph // ''} ||= {};
      0        
364 0           $x->{$stmt->subject->value} = $stmt->subject;
365             }
366              
367 0 0         unless ($use_graphs) {
368             my %out = map {
369 0           map { $_->value => $_ } values %{$seen{$_}} } keys %seen;
  0            
  0            
  0            
370 0           return values %out;
371             }
372              
373 0           map { [$_, [values %{$seen{$_}} ] ] } keys %seen;
  0            
  0            
374             }
375              
376             =head2 to_add
377              
378             In list context, returns an array of statements to add to the
379             graph. In scalar context, returns an L<RDF::Trine::Iterator>.
380              
381             =cut
382              
383             sub to_add {
384 0     0 1   my $self = shift;
385 0           my @out;
386             _traverse($self->_pos, sub {
387 0 0   0     my $stmt = defined $_[3] ?
388             RDF::Trine::Statement::Quad->new(@_)
389             : RDF::Trine::Statement->new(@_[0..2]);
390 0           push @out, $stmt;
391 0           });
392 0 0         wantarray ? @out : RDF::Trine::Iterator->new(\@out, 'graph');
393             }
394              
395             =head2 to_remove
396              
397             In list context, returns an array of statements to remove from the
398             graph. In scalar context, returns an L<RDF::Trine::Iterator>.
399              
400             =cut
401              
402             sub to_remove {
403 0     0 1   my $self = shift;
404 0           my @out;
405             _traverse($self->_neg, sub {
406 0 0   0     my $stmt = defined $_[3] ?
407             RDF::Trine::Statement::Quad->new(@_)
408             : RDF::Trine::Statement->new(@_[0..2]);
409 0           push @out, $stmt;
410 0           });
411 0 0         wantarray ? @out : RDF::Trine::Iterator->new(\@out, 'bindings');
412             }
413              
414             =head2 apply { $model | $remove, $add }
415              
416             Apply the patch to an L<RDF::Trine::Model> object. Statements are
417             removed first, then added. Transactions
418             (i.e. L<RDF::Trine::Model/begin_bulk_ops>) are your responsibility.
419              
420             Alternatively, supply the C<remove> and C<add> functions directly:
421              
422             sub _remove_or_add {
423             my ($subject, $predicate, $object, $graph) = @_;
424              
425             # do stuff ...
426              
427             # return value is ignored
428             }
429              
430             Inputs will be either L<RDF::Trine::Node> objects, or C<undef>, in the
431             case of C<remove>.
432              
433             =cut
434              
435             sub _node {
436 0     0     my $x = shift;
437 0 0         return $x eq '' ? undef : $x =~ /^_:(.*)/ ? bnode($1) : iri($x);
    0          
438             }
439              
440             # holy lol @ this
441             sub _traverse {
442 0     0     my ($structure, $callback) = @_;
443              
444 0           for my $gg (keys %{$structure}) {
  0            
445 0           my $g = _node($gg);
446 0           for my $ss (keys %{$structure->{$gg}}) {
  0            
447 0           my $s = _node($ss);
448 0           for my $pp (keys %{$structure->{$gg}{$ss}}) {
  0            
449 0           my $gsp = $structure->{$gg}{$ss}{$pp};
450 0           my $p = _node($pp);
451 0 0 0       if (!ref $gsp or $gsp->[0]{''}) {
452             #warn 'lul';
453 0           $callback->($s, $p, undef, $g);
454             }
455             else {
456 0           for my $oo (keys %{$gsp->[0]}) {
  0            
457 0           my $o = _node($oo);
458             #warn "lul $o";
459 0           $callback->($s, $p, $o, $g);
460             }
461 0           for my $ld (keys %{$gsp->[1]}) {
  0            
462 0           my ($t, $v) = ($ld =~ /^(.)(.*)$/);
463 0 0         my @args = $t ? $t eq '@' ?
    0          
464             ($v, undef) : (undef, $v) : ();
465             # of course the datatype is always a string
466             # here so no need to check it for blessedness
467 0           for my $ll (keys %{$gsp->[1]{$ld}}) {
  0            
468 0           my $o = literal($ll, @args);
469             #warn 'lul';
470 0           $callback->($s, $p, $o, $g);
471             }
472             }
473             }
474             }
475             }
476             }
477             }
478              
479             sub _apply {
480 0     0     my ($self, $model) = @_;
481              
482 0           $model->begin_bulk_ops;
483              
484             _traverse($self->_neg, sub {
485             #warn join(' ', map { defined $_ ? $_ : '(undef)' } @_);
486             # fuuuuuuck this quad semantics shit
487 0     0     my @n = map { $_[$_] } (0..3);
  0            
488              
489             #warn "found context $n[3]" if defined $n[3];
490              
491 0 0         $model->remove_statements
492 0           (defined $n[3] ? @n[0..3] : @n[0..2]) });
493             _traverse($self->_pos,
494             sub {
495             #warn "found context $_[3]" if defined $_[3];
496 0 0   0     my $stmt = defined $_[3] ?
497             RDF::Trine::Statement::Quad->new(@_)
498             : RDF::Trine::Statement->new(@_[0..2]);
499             #warn $stmt->sse;
500 0           $model->add_statement($stmt);
501 0           });
502              
503 0           $model->end_bulk_ops;
504              
505 0           1;
506             }
507              
508             sub apply {
509 0     0 1   my ($self, $remove, $add) = @_;
510              
511             # note remove may be a coderef or a model
512 0 0         if (ref $remove eq 'CODE') {
513 0           _traverse($self->_neg, $remove);
514 0 0         _traverse($self->_pos, $add) if $add;
515 0           return 1;
516             }
517 0 0         $self->_apply($remove) if Scalar::Util::blessed($remove);
518             }
519              
520             =head1 SEE ALSO
521              
522             =over 4
523              
524             =item L<RDF::KV>
525              
526             =item L<RDF::Trine::Model>
527              
528             =item L<RDF::Trine::Statement>
529              
530             =item L<RDF::Trine::Node>
531              
532             =back
533              
534             =head1 LICENSE AND COPYRIGHT
535              
536             Copyright 2013 Dorian Taylor.
537              
538             Licensed under the Apache License, Version 2.0 (the "License"); you
539             may not use this file except in compliance with the License. You may
540             obtain a copy of the License at
541             L<http://www.apache.org/licenses/LICENSE-2.0>.
542              
543             Unless required by applicable law or agreed to in writing, software
544             distributed under the License is distributed on an "AS IS" BASIS,
545             WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
546             implied. See the License for the specific language governing
547             permissions and limitations under the License.
548              
549             =cut
550              
551             __PACKAGE__->meta->make_immutable;
552              
553             1;