File Coverage

blib/lib/Attean/API/Model.pm
Criterion Covered Total %
statement 209 238 87.8
branch 32 54 59.2
condition 24 46 52.1
subroutine 40 41 97.5
pod 11 12 91.6
total 316 391 80.8


line stmt bran cond sub pod time code
1 50     50   602 use v5.14;
  50         161  
2 50     50   253 use warnings;
  50         121  
  50         2303  
3              
4             =head1 NAME
5              
6             Attean::API::Model - RDF Model
7              
8             =head1 VERSION
9              
10             This document describes Attean::API::Model version 0.032
11              
12             =head1 DESCRIPTION
13              
14             The Attean::API::Model role defines a common API for all RDF models to conform
15             to. It is provides a consistent interface for probing, counting, and retrieving
16             L<Attean::API::Quad|Attean::API::Binding>s matching a pattern, as well as
17             related functionality such as enumerating the graph names, and extracting
18             structured data from the models' quads.
19              
20             =head1 REQUIRED METHODS
21              
22             The following methods are required by the L<Attean::API::Model> role:
23              
24             =over 4
25              
26             =item C<< get_quads( $subject, $predicate, $object, $graph ) >>
27              
28             Returns an L<Attean::API::Iterator> for quads in the model that match the
29             supplied C<< $subject >>, C<< $predicate >>, C<< $object >>, and C<< $graph >>.
30              
31             Any of these terms may be:
32              
33             * An L<Attean::API::Term> object, in which case matching is equality-based
34              
35             * A L<Attean::API::Variable> object or C<< undef >>, in which case that term
36             will be considered as a wildcard for the purposes of matching
37              
38             * An ARRAY reference of L<Attean::API::Term> objects, in which case the
39             matching will be equality-based on the disjunction of the supplied terms
40              
41             The returned iterator conforms to both L<Attean::API::Iterator> and
42             L<Attean::API::QuadIterator>.
43              
44             =item C<< count_quads( $subject, $predicate, $object, $graph ) >>
45              
46             Returns the number of quads in the model matching the supplied pattern
47             (using the same matching semantics as C<< get_quads >>).
48              
49             =item C<< count_quads_estimate( $subject, $predicate, $object, $graph ) >>
50              
51             Returns an estimate of the number of quads in the model matching the supplied
52             pattern (using the same matching semantics as C<< get_quads >>). This estimate
53             is guaranteed to non-zero if the count returned from an equivalent call to
54             `count_quads` would return a non-zero result.
55              
56             =item C<< get_graphs >>
57              
58             Returns an L<Attean::API::Iterator> of distinct L<Attean::API::Term> objects
59             that are used in the graph position of quads in the model.
60              
61             =back
62              
63             =head1 METHODS
64              
65             The L<Attean::API::Model> role provides default implementations of the
66             following methods:
67              
68             =over 4
69              
70             =item C<< get_bindings( $subject, $predicate, $object, $graph ) >>
71              
72             Returns an L<Attean::API::Iterator> of L<Attean::API::Result> objects
73             corresponding to quads in the model matching the supplied pattern. For each
74             L<Attean::API::Variable> in the pattern list, a mapping will be present in the
75             corresponding result object. For example,
76             C<< $model->get_bindings( variable('s') ) >> will return an iterator of results
77             containing just a mapping from C<< 's' >> to subjects of all quads in the
78             model.
79              
80             =item C<< get_list( $graph, $head ) >>
81              
82             Returns an L<Attean::API::Iterator> of L<Attean::API::Term> objects that are
83             members of the rdf:List with the specified C<< $head >> (and matching
84             restricted to only the specified C<< $graph >>).
85              
86             To check if a certain term is a list, the C<holds> method may be used, for example:
87              
88             $model->holds($head, iri('http://www.w3.org/1999/02/22-rdf-syntax-ns#first'), undef, $graph))
89              
90             will return true if a given term C<$head> is a list.
91              
92             =item C<< get_sequence( $graph, $head ) >>
93              
94             Returns an L<Attean::API::Iterator> of L<Attean::API::Term> objects that are
95             members of the rdf:Sequence with the specified C<< $head >> (and matching
96             restricted to only the specified C<< $graph >>).
97              
98             =item C<< subjects( $predicate, $object, $graph ) >>
99              
100             Returns an L<Attean::API::Iterator> of L<Attean::API::Term> objects of all
101             subjects of quads matching the supplied pattern (using the same matching
102             semantics as C<< get_quads >>).
103              
104             The objects returned will not necessarily be unique. It will instead be
105             equivalent to calling C<< get_quads >> and accessing C<< $quad->subject >>
106             for each C<< $quad >> value returned by the iterator. For an iterator of unique
107             subjects, use C<< $model->subjects->uniq >>.
108              
109             =item C<< predicates( $subject, $object, $graph ) >>
110              
111             Returns an L<Attean::API::Iterator> of L<Attean::API::Term> objects of all
112             predicates of quads matching the supplied pattern (using the same matching
113             semantics as C<< get_quads >> with an C<< undef >> predicate).
114              
115             The objects returned will not necessarily be unique
116             (see the note for C<< subjects >> above).
117              
118             =item C<< objects( $subject, $predicate, $graph ) >>
119              
120             Returns an L<Attean::API::Iterator> of L<Attean::API::Term> objects of all
121             objects of quads matching the supplied pattern (using the same matching
122             semantics as C<< get_quads >> with an C<< undef >> object).
123              
124             The objects returned will not necessarily be unique
125             (see the note for C<< subjects >> above).
126              
127             =item C<< graphs( $subject, $predicate, $object ) >>
128              
129             Returns an L<Attean::API::Iterator> of L<Attean::API::Term> objects of all
130             graphs of quads matching the supplied pattern (using the same matching
131             semantics as C<< get_quads >> with an C<< undef >> graph).
132              
133             The objects returned will not necessarily be unique
134             (see the note for C<< subjects >> above).
135              
136             =item C<< graph_nodes( $graph ) >>
137              
138             Returns an L<Attean::API::Iterator> of L<Attean::API::Term> objects of unique
139             subjects and objects present in the specified C<< $graph >>.
140              
141             =item C<< holds($s, $p, $o, $g) >>
142              
143             =item C<< holds($triple_pattern) >>
144              
145             =item C<< holds($quad_pattern) >>
146              
147             Returns true if the triple/quad pattern matches any data in the model, false
148             otherwise.
149              
150             =item C<< algebra_holds($algebra, $graph) >>
151              
152             =item C<< algebra_holds($algebra, \@graphs) >>
153              
154             Returns true if the algebra, evaluated with the supplied default graph(s)
155             matches any data in the model, false otherwise. This is equivalent to the
156             result of an ASK query over the supplied algebra.
157              
158             =item C<< evaluate($algebra, [ $default_graph | \@default_graphs ]) >>
159              
160             Returns an L<Attean::API::Iterator> of L<Attean::Result> objects which result
161             from evaluating the given query algebra (e.g. one obtained from parsing a query
162             with L<AtteanX::Parser::SPARQL>) with the supplied default graph(s) against data
163             in the model.
164              
165             =cut
166              
167 50     50   19186 use Attean::API::Binding;
  50         161  
  50         2203  
168              
169             use Sub::Install;
170 50     50   374 use Sub::Util qw(set_subname);
  50         106  
  50         491  
171 50     50   2094 use URI::Namespace;
  50         105  
  50         2007  
172 50     50   317 use Scalar::Util qw(blessed);
  50         94  
  50         1308  
173 50     50   264 use List::MoreUtils qw(uniq);
  50         93  
  50         1832  
174 50     50   306 use Math::Cartesian::Product;
  50         145  
  50         287  
175 50     50   69625 use Data::Dumper;
  50         33384  
  50         2280  
176 50     50   4280  
  50         135  
  50         2081  
177             use Moo::Role;
178 50     50   273
  50         99  
  50         306  
179             # get_quads($s, $p, $o, $g)
180             # or:
181             # get_quads([$s1, $s2, ...], \@p, \@o, \@g)
182             requires 'get_quads';
183            
184             my $self = shift;
185             my @nodes = @_;
186 50     50 1 214 my @pos = Attean::API::Quad->variables;
187 50         121 # my %vars;
188 50         186 my %bound;
189             my %projected_vars;
190 50         121 foreach my $i (0 .. $#nodes) {
191             my $n = $nodes[$i];
192 50         153 $bound{ $pos[ $i ] } = $n;
193 196         1734 if (blessed($n) and $n->does('Attean::API::Binding')) {
194 196         306 foreach my $v ($n->referenced_variables) {
195 196 50 66     661 $projected_vars{ $v }++;
    100 100        
196 0         0 }
197 0         0 } elsif (blessed($n) and $n->isa('Attean::Variable')) {
198             my $name = $n->value;
199             # $vars{ $pos[ $i ] } = $name;
200 84         1848 $projected_vars{ $name }++;
201             }
202 84         234 }
203            
204             my @patterns;
205             cartesian {
206 50         778 my %bound;
207             foreach my $i (0 .. $#_) {
208 50     50   3291 my $n = $_[$i];
209 50         124 $bound{ $pos[ $i ] } = $n;
210 196         230 }
211 196         353 push(@patterns, Attean::QuadPattern->new( %bound ));
212             } map { ref($_) eq 'ARRAY' ? $_ : [$_] } @nodes;
213 50         1106
214 50 100       269 my $quads = $self->get_quads(@nodes);
  196         595  
215             unless (blessed($quads)) {
216 50         2773 return Attean::ListIterator->new(values => [], item_type => 'Attean::API::Result', variables => []);
217 50 50       214 }
218 0         0 return $quads->map(sub {
219             my $q = shift;
220             # warn 'model got quad: ' . $q->as_string . "\n";
221 85     85   116 foreach my $pattern (@patterns) {
222             # warn 'model using pattern: ' . $pattern->as_string . "\n";
223 85         155 if (my $b = $pattern->unify($q)) {
224             # warn 'unified binding: ' . $b->as_string;
225 85 50       228 my $g = $pattern->ground($b);
226             # warn "get_bindings unification: " . $b->as_string;
227 85         2011 # warn "get_bindings ground: " . $g->as_string;
228             # warn 'project vars: ' . Dumper([keys %projected_vars]);
229             my $p = $b->project(keys %projected_vars);
230             # warn "get_bindings result: " . $p->as_string;
231 85         2055 return $p;
232             }
233 85         2107 }
234             return;
235             }, 'Attean::API::Result', variables => [keys %projected_vars]);
236 0         0 }
237 50         440
238             requires 'count_quads';
239             requires 'count_quads_estimate';
240             requires 'get_graphs';
241             requires 'holds';
242            
243             my $self = shift;
244             die "get_list called without a graph name" unless (scalar(@_));
245             my $graph = shift;
246 2     2 1 1504 die "get_list called without a list head" unless (scalar(@_));
247 2 50       8 my $head = shift;
248 2         4 my $rdf_first = Attean::IRI->new('http://www.w3.org/1999/02/22-rdf-syntax-ns#first');
249 2 100       12 my $rdf_rest = Attean::IRI->new('http://www.w3.org/1999/02/22-rdf-syntax-ns#rest');
250 1         2 my $rdf_nil = Attean::IRI->new('http://www.w3.org/1999/02/22-rdf-syntax-ns#nil');
251 1         23 my @elements;
252 1         265 my %seen;
253 1         182 while (blessed($head) and not($head->does('Attean::API::IRI') and $head->value eq $rdf_nil->value)) {
254 1         162 if ($seen{ $head->as_string }++) {
255             die "Loop found during rdf:List traversal";
256 1   66     11 }
      66        
257 3 50       83 my @n = $self->objects( $head, $rdf_first )->elements;
258 0         0 if (scalar(@n) != 1) {
259             die "Invalid structure found during rdf:List traversal";
260 3         33 }
261 3 50       19 push(@elements, @n);
262 0         0 ($head) = $self->objects( $head, $rdf_rest )->elements;
263             }
264 3         8 return Attean::ListIterator->new(values => \@elements, item_type => 'Attean::API::Term' );
265 3         9 }
266              
267 1         44 my $self = shift;
268             my $graph = shift;
269             my $head = shift;
270             my $rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
271 2     2 1 346 my @elements;
272 2         2 my $i = 1;
273 2         4 while (1) {
274 2         3 my $term = Attean::IRI->new("${rdf}_$i");
275 2         3 my @elem = $self->objects( $head, $term, $graph )->elements;
276 2         3 last unless (scalar(@elem));
277 2         3 if (scalar(@elem) > 1) {
278 6         110 my $count = scalar(@elem);
279 6         1135 die "Invalid structure found during rdf:Seq access: $count elements found for element $i";
280 6 100       36 }
281 5 100       13 my $elem = $elem[0];
282 1         2 last unless (blessed($elem));
283 1         15 push(@elements, $elem);
284             $i++;
285 4         8 }
286 4 50       13 return Attean::ListIterator->new(values => \@elements, item_type => 'Attean::API::Term' );
287 4         8 }
288 4         14  
289             {
290 1         18 # auto-generate methods subjects, predicates, objects, and graphs
291             my @pos = Attean::API::Quad->variables;
292             my %pos = map { $pos[$_] => $_ } (0 .. $#pos);
293             for my $method (@pos) {
294             my $code = sub {
295             my $self = shift;
296             my @nodes = @_;
297             $#nodes = 3;
298             splice(@nodes, $pos{$method}, 0, undef);
299 20     20   395 $#nodes = 3;
        20      
        20      
        20      
300 20         44 my $iter = $self->get_quads(@nodes);
301 20         51 my $nodes = $iter->map(
302 20         62 sub { $_->$method() },
303 20         35 'Attean::API::Term',
304 20         71 );
305             return $nodes;
306 33     33   117 };
307 20         153 Sub::Install::install_sub({
308             code => set_subname("${method}s", $code),
309 20         657 as => "${method}s"
310             });
311             }
312             }
313              
314             my $self = shift;
315             my $graph = shift;
316             my $s = $self->subjects(undef, undef, $graph);
317             my $o = $self->objects(undef, undef, $graph);
318             my $union = Attean::IteratorSequence->new( iterators => [$s, $o], item_type => 'Attean::API::Term' );
319 2     2 1 4 my %seen;
320 2         5 return $union->grep(sub {not($seen{shift->as_string}++)});
321 2         10 }
322 2         9  
323 2         39 my $self = shift;
324 2         61 my $algebra = shift || die "No algebra available in evaluate call";
325 2     12   34 my $default_graphs = shift || die "No default graphs available in evaluate call";
  12         31  
326             $default_graphs = [$default_graphs] if (blessed($default_graphs));
327            
328             unless (blessed($algebra) and $algebra->does('Attean::API::Algebra')) {
329 2     2 1 18 die "Unexpected argument to evaluate: " . Dumper($algebra);
330 2   50     11 }
331 2   50     7
332 2 50       13 my $planner = Attean::IDPQueryPlanner->new();
333             my $plan = $planner->plan_for_algebra($algebra, $self, $default_graphs);
334 2 50 33     15 my $iter = $plan->evaluate($self);
335 0         0 return $iter;
336             }
337              
338 2         64 my $self = shift;
339 2         2983 my $algebra = shift || die "No algebra available in algebra_holds call";
340 2         14 my $default_graphs = shift || die "No default graphs available in algebra_holds call";
341 2         107 $default_graphs = [$default_graphs] if (blessed($default_graphs));
342            
343             unless (blessed($algebra) and $algebra->does('Attean::API::Algebra')) {
344             die "Unexpected argument to algebra_holds: " . Dumper($algebra);
345 6     6 1 17 }
346 6   50     20
347 6   50     21 unless ($algebra->isa('Attean::Algebra::Ask')) {
348 6 50       31 $algebra = Attean::Algebra::Ask->new(children => [$algebra]);
349             }
350 6 50 33     43 my $planner = Attean::IDPQueryPlanner->new();
351 0         0 my $plan = $planner->plan_for_algebra($algebra, $self, $default_graphs);
352             my $iter = $plan->evaluate($self);
353             my $r = $iter->next;
354 6 50       227 my $ebv = eval { $r->ebv };
355 6         92 return 0 if ($@);
356             return $ebv;
357 6         99 }
358 6         5685
359 6         37 my $self = shift;
360 6         308 return 0 unless scalar(@_);
361 6         14 if (not defined($_[0]) or (blessed($_[0]) and $_[0]->does('Attean::API::TermOrVariable'))) {
  6         42  
362 6 50       21 # firt argument is undef or a term/variable, so we assume this is a call with up to 3 term/variable/undef args
363 6         190 return ($self->count_quads_estimate(@_) > 0);
364             } elsif (blessed($_[0]) and $_[0]->does('Attean::API::TripleOrQuadPattern')) {
365             my $t = shift;
366             return ($self->count_quads_estimate($t->values) > 0);
367 16     16 1 1774 } else {
368 16 50       54 die "Unexpected argument to holds: " . Dumper($_[0]);
369 16 100 66     174 }
    50 66        
      33        
370             }
371 13         427 }
372              
373 3         149  
374 3         18 use Attean::RDF;
375             use LWP::UserAgent;
376 0         0 use Encode qw(encode);
377             use Scalar::Util qw(blessed);
378             use Role::Tiny ();
379              
380             use Moo::Role;
381            
382             requires 'add_quad';
383 50     50   86744 requires 'remove_quad';
  50         145  
  50         415  
384 50     50   67634 requires 'create_graph';
  50         1595194  
  50         1832  
385 50     50   584 requires 'drop_graph';
  50         5411  
  50         2771  
386 50     50   323 requires 'clear_graph';
  50         102  
  50         1574  
387 50     50   381 requires 'add_iter';
  50         91  
  50         692  
388            
389 50     50   237 with 'Attean::API::Model';
  50         89  
  50         409  
390            
391             my $self = shift;
392             my $graph = shift;
393             my @urls = @_;
394             my $ua = LWP::UserAgent->new();
395             my $accept = Attean->acceptable_parsers( handles => 'Attean::API::Triple' );
396             $ua->default_headers->push_header( 'Accept' => $accept );
397            
398             foreach my $u (@urls) {
399             my $url = blessed($u) ? $u->value : $u;
400             my $resp = $ua->get($url);
401 0     0 0 0 if ($resp->is_success) {
402 0         0 my $ct = $resp->header('Content-Type');
403 0         0 my $pclass = Attean->get_parser( media_type => $ct, filename => $url ) // Attean->get_parser('ntriples');
404 0         0 if ($pclass) {
405 0         0 my $p = $pclass->new(base => iri($url));
406 0         0 my $str = $resp->decoded_content;
407             my $bytes = encode('UTF-8', $str, Encode::FB_CROAK);
408 0         0 my $iter = $p->parse_iter_from_bytes( $bytes );
409 0 0       0 $self->add_iter($iter->as_quads($graph));
410 0         0 } else {
411 0 0       0 die "No parser found for content type $ct: $url";
412 0         0 }
413 0   0     0 } else {
414 0 0       0 die $resp->status_line;
415 0         0 }
416 0         0 }
417 0         0 }
418 0         0
419 0         0 # $model->load_triples( 'turtle', iri('http://example.org/graph1') => "@prefix foaf: ...", iri('http://example.org/graph2') => "@prefix foaf: ..." );
420             my $self = shift;
421 0         0 my $format = shift;
422             my $class = Attean->get_parser($format) || die "Failed to load parser for '$format'";
423             my $parser = $class->new() || die "Failed to construct parser for '$format'";
424 0         0 while (scalar(@_)) {
425             my ($graph, $string) = splice(@_, 0, 2);
426             my $iter = $parser->parse_iter_from_bytes(encode('UTF-8', $string, Encode::FB_CROAK));
427             my $quads = $iter->as_quads($graph);
428             $self->add_iter($quads);
429             }
430             }
431 6     6 1 1678
432 6         13 my $self = shift;
433 6   50     42 my $format = shift;
434 6   50     114 my $class = Attean->get_parser($format) || die "Failed to load parser for '$format'";
435 6         261 my $parser = $class->new() || die "Failed to construct parser for '$format'";
436 6         28 while (scalar(@_)) {
437 6         75 my ($graph, $fh) = splice(@_, 0, 2);
438 6         3302 my $iter = $parser->parse_iter_from_io($fh);
439 6         318 my $quads = $iter->as_quads($graph);
440             $self->add_iter($quads);
441             }
442             }
443            
444 1     1 1 77 my $self = shift;
445 1         2 my $iter = shift;
446 1   50     9 my $type = $iter->item_type;
447 1   50     37 die "Iterator type $type isn't quads" unless (Role::Tiny::does_role($type, 'Attean::API::Quad'));
448 1         52 while (my $q = $iter->next) {
449 1         4 $self->add_quad($q);
450 1         7 }
451 1         143 }
452 1         54  
453             my $self = shift;
454             die "add_list called without a graph name" unless (scalar(@_));
455             my $graph = shift;
456             my @elements = @_;
457 2     2 1 50 my $rdf_first = Attean::IRI->new('http://www.w3.org/1999/02/22-rdf-syntax-ns#first');
458 2         5 my $rdf_rest = Attean::IRI->new('http://www.w3.org/1999/02/22-rdf-syntax-ns#rest');
459 2         22 my $rdf_nil = Attean::IRI->new('http://www.w3.org/1999/02/22-rdf-syntax-ns#nil');
460 2 50       10 if (scalar(@elements) == 0) {
461 2         49 return $rdf_nil;
462 10         41 } else {
463             my $head = Attean::Blank->new();
464             my $node = shift(@elements);
465             my $rest = $self->add_list($graph, @elements);
466             $self->add_quad( Attean::Quad->new($head, $rdf_first, $node, $graph) );
467 5     5 1 189 $self->add_quad( Attean::Quad->new($head, $rdf_rest, $rest, $graph) );
468 5 100       22 return $head;
469 4         9 }
470 4         6 }
471 4         64 }
472 4         736  
473 4         743  
474 4 100       635 use Moo::Role;
475 1         6
476             requires 'etag_value_for_quads';
477 3         46 }
478 3         166  
479 3         14  
480 3         49 use Moo::Role;
481 3         42
482 3         17 requires 'mtime_for_quads';
483             }
484              
485              
486             use Moo::Role;
487            
488             with 'Attean::API::MutableModel';
489 50     50   52805
  50         102  
  50         355  
490             requires 'begin_bulk_updates';
491             requires 'end_bulk_updates';
492            
493             around [qw(load_triples load_triples_from_io add_iter add_list)] => sub {
494             my $orig = shift;
495             my $self = shift;
496 50     50   17433 $self->begin_bulk_updates();
  50         112  
  50         275  
497             $self->$orig(@_);
498             $self->end_bulk_updates();
499             };
500              
501             # End bulk updates the moment a read operation is performed...
502             before [qw(get_quads get_bindings count_quads count_quads_estimate get_graphs subject predicate object graph)] => sub {
503 50     50   18379 my $self = shift;
  50         207  
  50         224  
504             $self->end_bulk_updates();
505             };
506             }
507              
508             use Moo::Role;
509              
510             with 'Attean::API::Model';
511             }
512              
513              
514              
515             1;
516              
517              
518             =back
519              
520             =head1 BUGS
521              
522             Please report any bugs or feature requests to through the GitHub web interface
523             at L<https://github.com/kasei/attean/issues>.
524              
525             =head1 SEE ALSO
526 50     50   21228  
  50         116  
  50         220  
527              
528              
529             =head1 AUTHOR
530              
531             Gregory Todd Williams C<< <gwilliams@cpan.org> >>
532              
533             =head1 COPYRIGHT
534              
535             Copyright (c) 2014--2022 Gregory Todd Williams.
536             This program is free software; you can redistribute it and/or modify it under
537             the same terms as Perl itself.
538              
539             =cut