File Coverage

blib/lib/RDF/KV/Patch.pm
Criterion Covered Total %
statement 55 177 31.0
branch 23 118 19.4
condition 6 23 26.0
subroutine 10 25 40.0
pod 9 9 100.0
total 103 352 29.2


line stmt bran cond sub pod time code
1             package RDF::KV::Patch;
2              
3 3     3   937 use 5.010;
  3         18  
4 3     3   15 use strict;
  3         8  
  3         80  
5 3     3   16 use warnings FATAL => 'all';
  3         28  
  3         144  
6              
7 3     3   21 use Moose;
  3         13  
  3         30  
8 3     3   23231 use namespace::autoclean;
  3         8403  
  3         18  
9              
10 3     3   1341 use RDF::Trine qw(iri blank literal);
  3         1511010  
  3         226  
11              
12 3     3   503 use URI::BNode;
  3         181755  
  3         8683  
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.14
21              
22             =cut
23              
24             our $VERSION = '0.14';
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         19  
126              
127 1 50       7 if (defined $s) {
128 1 50       4 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         4 $s = URI::BNode->new($s);
161 1 50       35 $s = $s->scheme eq '_' ? blank($s->opaque) : iri($s->as_string);
162             }
163             }
164              
165             # predicate will always be an iri
166 1 50       300 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         5 $p = iri("$p");
180             }
181             }
182              
183 1 50       43 if (defined $o) {
184 1 50       14 if (my $ref = ref $o) {
185 1 50       9 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       91  
    100          
247             $_->sse : ref $_ ? $_->uri_value : $_ : '' } ($g, $s, $p);
248              
249 1   50     65 $set->{$g} ||= {};
250 1   50     7 $set->{$g}{$s} ||= {};
251 1   50     8 $set->{$g}{$s}{$p} ||= [{}, {}];
252              
253 1 50       49 if ($o) {
254 1 50       96 if ($o->isa('RDF::Trine::Node::Literal')) {
    0          
255 1         3 my $l = $o->literal_value_language;
256 1         5 my $d = $o->literal_datatype;
257 1 0       6 my $ld = $d ? "^$d" : $l ? "\@$l" : '';
    50          
258 1   50     9 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 465 my $self = shift;
276 1         3 my ($s, $p, $o, $g) = _validate(@_);
277             Carp::croak('It makes no sense in this context to add a partial statement')
278 1 50       3 unless 3 == grep { ref $_ } ($s, $p, $o);
  3         7  
279              
280 1 50       20 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         59 _add_either($self->_pos, $s, $p, $o, $g);
285              
286 1         8 $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 affected_graphs
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 affected_graphs {
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 affected_subjects [$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 affected_subjects {
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           my $g = $stmt->graph;
364 0 0 0       my $x = $seen{$g ? $g->value : ''} ||= {};
365 0           $x->{$stmt->subject->value} = $stmt->subject;
366             }
367              
368 0 0         unless ($use_graphs) {
369             my %out = map {
370 0           map { $_->value => $_ } values %{$seen{$_}} } keys %seen;
  0            
  0            
  0            
371 0           return values %out;
372             }
373              
374 0           map { [_node($_), [values %{$seen{$_}} ] ] } keys %seen;
  0            
  0            
375             }
376              
377             =head2 to_add
378              
379             In list context, returns an array of statements to add to the
380             graph. In scalar context, returns an L<RDF::Trine::Iterator>.
381              
382             =cut
383              
384             sub to_add {
385 0     0 1   my $self = shift;
386 0           my @out;
387             _traverse($self->_pos, sub {
388 0 0   0     my $stmt = defined $_[3] ?
389             RDF::Trine::Statement::Quad->new(@_)
390             : RDF::Trine::Statement->new(@_[0..2]);
391 0           push @out, $stmt;
392 0           });
393 0 0         wantarray ? @out : RDF::Trine::Iterator->new(\@out, 'graph');
394             }
395              
396             =head2 to_remove
397              
398             In list context, returns an array of statements to remove from the
399             graph. In scalar context, returns an L<RDF::Trine::Iterator>.
400              
401             =cut
402              
403             sub to_remove {
404 0     0 1   my $self = shift;
405 0           my @out;
406             _traverse($self->_neg, sub {
407 0 0   0     my $stmt = defined $_[3] ?
408             RDF::Trine::Statement::Quad->new(@_)
409             : RDF::Trine::Statement->new(@_[0..2]);
410 0           push @out, $stmt;
411 0           });
412 0 0         wantarray ? @out : RDF::Trine::Iterator->new(\@out, 'bindings');
413             }
414              
415             =head2 apply { $model | $remove, $add }
416              
417             Apply the patch to an L<RDF::Trine::Model> object. Statements are
418             removed first, then added. Transactions
419             (i.e. L<RDF::Trine::Model/begin_bulk_ops>) are your responsibility.
420              
421             Alternatively, supply the C<remove> and C<add> functions directly:
422              
423             sub _remove_or_add {
424             my ($subject, $predicate, $object, $graph) = @_;
425              
426             # do stuff ...
427              
428             # return value is ignored
429             }
430              
431             Inputs will be either L<RDF::Trine::Node> objects, or C<undef>, in the
432             case of C<remove>.
433              
434             =cut
435              
436             sub _node {
437 0     0     my $x = shift;
438 0 0         return $x eq '' ? undef : $x =~ /^_:(.*)/ ? bnode($1) : iri($x);
    0          
439             }
440              
441             # holy lol @ this
442             sub _traverse {
443 0     0     my ($structure, $callback) = @_;
444              
445 0           for my $gg (keys %{$structure}) {
  0            
446 0           my $g = _node($gg);
447 0           for my $ss (keys %{$structure->{$gg}}) {
  0            
448 0           my $s = _node($ss);
449 0           for my $pp (keys %{$structure->{$gg}{$ss}}) {
  0            
450 0           my $gsp = $structure->{$gg}{$ss}{$pp};
451 0           my $p = _node($pp);
452 0 0 0       if (!ref $gsp or $gsp->[0]{''}) {
453             #warn 'lul';
454 0           $callback->($s, $p, undef, $g);
455             }
456             else {
457 0           for my $oo (keys %{$gsp->[0]}) {
  0            
458 0           my $o = _node($oo);
459             #warn "lul $o";
460 0           $callback->($s, $p, $o, $g);
461             }
462 0           for my $ld (keys %{$gsp->[1]}) {
  0            
463 0           my ($t, $v) = ($ld =~ /^(.)(.*)$/);
464 0 0         my @args = $t ? $t eq '@' ?
    0          
465             ($v, undef) : (undef, $v) : ();
466             # of course the datatype is always a string
467             # here so no need to check it for blessedness
468 0           for my $ll (keys %{$gsp->[1]{$ld}}) {
  0            
469 0           my $o = literal($ll, @args);
470             #warn 'lul';
471 0           $callback->($s, $p, $o, $g);
472             }
473             }
474             }
475             }
476             }
477             }
478             }
479              
480             sub _apply {
481 0     0     my ($self, $model) = @_;
482              
483 0           $model->begin_bulk_ops;
484              
485             _traverse($self->_neg, sub {
486             #warn join(' ', map { defined $_ ? $_ : '(undef)' } @_);
487             # fuuuuuuck this quad semantics shit
488 0     0     my @n = map { $_[$_] } (0..3);
  0            
489              
490             #warn "found context $n[3]" if defined $n[3];
491              
492 0 0         $model->remove_statements
493 0           (defined $n[3] ? @n[0..3] : @n[0..2]) });
494             _traverse($self->_pos,
495             sub {
496             #warn "found context $_[3]" if defined $_[3];
497 0 0   0     my $stmt = defined $_[3] ?
498             RDF::Trine::Statement::Quad->new(@_)
499             : RDF::Trine::Statement->new(@_[0..2]);
500             #warn $stmt->sse;
501 0           $model->add_statement($stmt);
502 0           });
503              
504 0           $model->end_bulk_ops;
505              
506 0           1;
507             }
508              
509             sub apply {
510 0     0 1   my ($self, $remove, $add) = @_;
511              
512             # note remove may be a coderef or a model
513 0 0         if (ref $remove eq 'CODE') {
514 0           _traverse($self->_neg, $remove);
515 0 0         _traverse($self->_pos, $add) if $add;
516 0           return 1;
517             }
518 0 0         $self->_apply($remove) if Scalar::Util::blessed($remove);
519             }
520              
521             =head1 SEE ALSO
522              
523             =over 4
524              
525             =item L<RDF::KV>
526              
527             =item L<RDF::Trine::Model>
528              
529             =item L<RDF::Trine::Statement>
530              
531             =item L<RDF::Trine::Node>
532              
533             =back
534              
535             =head1 LICENSE AND COPYRIGHT
536              
537             Copyright 2013 Dorian Taylor.
538              
539             Licensed under the Apache License, Version 2.0 (the "License"); you
540             may not use this file except in compliance with the License. You may
541             obtain a copy of the License at
542             L<http://www.apache.org/licenses/LICENSE-2.0>.
543              
544             Unless required by applicable law or agreed to in writing, software
545             distributed under the License is distributed on an "AS IS" BASIS,
546             WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
547             implied. See the License for the specific language governing
548             permissions and limitations under the License.
549              
550             =cut
551              
552             __PACKAGE__->meta->make_immutable;
553              
554             1;