File Coverage

blib/lib/Attean.pm
Criterion Covered Total %
statement 235 257 91.4
branch 80 98 81.6
condition 19 23 82.6
subroutine 41 44 93.1
pod 10 10 100.0
total 385 432 89.1


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Attean - A Semantic Web Framework
4              
5             =head1 VERSION
6              
7             This document describes Attean version 0.033
8              
9             =head1 SYNOPSIS
10              
11             use Attean;
12             use Attean::RDF qw(iri);
13            
14             my $store = Attean->get_store('Memory')->new();
15             my $parser = Attean->get_parser('NTriples')->new();
16            
17             # iterator of triples and quads
18             my $iter = $parser->parse_iter_from_io(\*STDIN);
19            
20             # add a graph name to all triples
21             my $graph = iri('http://graph-name/');
22             my $quads = $iter->as_quads($graph);
23            
24             $store->add_iter($quads);
25             my $model = Attean::QuadModel->new( store => $store );
26             my $iter = $model->get_quads();
27             while (my $quad = $iter->next) {
28             say $quad->object->ntriples_string;
29             }
30              
31             # run a SPARQL query and iterate over the results
32             my $sparql = 'SELECT * WHERE { ?s ?p ?o }';
33             my $s = Attean->get_parser('SPARQL')->new();
34             my ($algebra) = $s->parse($sparql);
35             my $results = $model->evaluate($algebra, $graph);
36             while (my $r = $results->next) {
37             say $r->as_string;
38             }
39              
40             =head1 DESCRIPTION
41              
42             Attean provides APIs for parsing, storing, querying, and serializing
43             Semantic Web (RDF and SPARQL) data.
44              
45             =head1 METHODS
46              
47             =over 4
48              
49             =cut
50              
51             use v5.14;
52 50     50   6319147 use warnings;
  50         408  
53 50     50   216 our $VERSION = '0.033';
  50         74  
  50         1631  
54             use Attean::API;
55 50     50   16737
  50         137  
  50         2257  
56             use Attean::Blank;
57 50     50   392 use Attean::Literal;
  50         92  
  50         1025  
58 50     50   21163 use Attean::Variable;
  50         147  
  50         1574  
59 50     50   365 use Attean::IRI;
  50         111  
  50         897  
60 50     50   223
  50         88  
  50         1148  
61             use Attean::Triple;
62 50     50   20374 use Attean::Quad;
  50         154  
  50         1516  
63 50     50   19616 use Attean::Result;
  50         145  
  50         1758  
64 50     50   19871
  50         146  
  50         1739  
65             use Attean::QuadModel;
66 50     50   18288 use Attean::TripleModel;
  50         169  
  50         1671  
67 50     50   22091 use Attean::BindingEqualityTest;
  50         209  
  50         2147  
68 50     50   21444
  50         148  
  50         1488  
69             use Attean::CodeIterator;
70 50     50   20500 use Attean::ListIterator;
  50         147  
  50         1576  
71 50     50   359 use Attean::IteratorSequence;
  50         94  
  50         1370  
72 50     50   19816  
  50         159  
  50         1792  
73             use Attean::IDPQueryPlanner;
74 50     50   18830
  50         178  
  50         1620  
75             use Attean::TermMap;
76 50     50   20947
  50         163  
  50         1594  
77             use HTTP::Negotiate qw(choose);
78 50     50   101588 use List::MoreUtils qw(any all);
  50         2166  
  50         3492  
79 50     50   356 use Module::Load::Conditional qw(can_load);
  50         110  
  50         438  
80 50     50   46567 use Role::Tiny ();
  50         132  
  50         1914  
81 50     50   256 use Sub::Util qw(set_subname);
  50         106  
  50         752  
82 50     50   217 use namespace::clean;
  50         96  
  50         1587  
83 50     50   259
  50         107  
  50         361  
84             use Module::Pluggable search_path => 'AtteanX::Parser', sub_name => 'parsers', max_depth => 3;
85 50     50   33951 use Module::Pluggable search_path => 'AtteanX::Serializer', sub_name => 'serializers', max_depth => 3;
  50         373104  
  50         361  
86 50     50   4966 use Module::Pluggable search_path => 'AtteanX::Store', sub_name => 'stores', max_depth => 3;
  50         110  
  50         184  
87 50     50   4191
  50         119  
  50         223  
88             my $class = shift;
89             if (scalar(@_)) {
90 138     138   571 my %args = @_;
91 138 100       23564 foreach my $p (@{ $args{parsers} || [] }) {
92 2         9 # warn "Loading $p parser...";
93 2 50       5 $class->get_parser($p) || die "Failed to load parser: $p";
  2         19  
94             }
95 2 50       9 foreach my $s (@{ $args{serializers} || [] }) {
96             # warn "Loading $s serializer...";
97 2 50       6 $class->get_serializer($s) || die "Failed to load serializer: $s";
  2         16  
98             }
99 0 0       0 foreach my $s (@{ $args{stores} || [] }) {
100             # warn "Loading $s store...";
101 2 50       6 $class->get_store($s) || die "Failed to load store: $s";
  2         118  
102             }
103 0 0       0 }
104             }
105            
106             =item C<< get_store( $NAME ) >>
107              
108             Attempts to find a L<Attean::API::Store> implementation with the
109             given C<< $NAME >>. This is done using L<Module::Pluggable> and will generally
110             be searching for class names C<< AtteanX::Store::$NAME >>.
111              
112             Returns the full class name if a matching implementation is found, otherwise
113             returns undef.
114              
115             =cut
116              
117             my $self = shift;
118             return $self->_get_plugin('stores', shift);
119             }
120 76     76 1 49290  
121 76         271 =item C<< temporary_model >>
122              
123             Returns a temporary, mutable quad model based on a L<AtteanX::Store::Memory> store.
124              
125             =cut
126              
127             my $self = shift;
128             return Attean::MutableQuadModel->new( store => $self->get_store('Memory')->new() )
129             }
130              
131 3     3 1 714  
132 3         18  
133             =item C<< get_serializer( $NAME ) >>
134              
135             =item C<< get_serializer( filename => $FILENAME ) >>
136              
137             =item C<< get_serializer( media_type => $MEDIA_TYPE ) >>
138              
139             Attempts to find a L<Attean::API::Serializer> serializer class with the given
140             C<< $NAME >>, or that can serialize files with the C<< $MEDIA_TYPE >> media
141             type.
142              
143             Returns the full class name if a matching implementation is found, otherwise
144             returns undef.
145              
146             =cut
147              
148             my $self = shift;
149             my $role = 'Attean::API::Serializer';
150              
151             if (scalar(@_) == 1) {
152             my $name = shift;
153 70     70 1 74851 my $p = $self->_get_plugin('serializers', $name, $role);
154 70         127 return $p if $p;
155            
156 70 100       250 foreach my $type (qw'filename media_type') {
157 49         116 my $p = $self->get_serializer($type => $name);
158 49         183 return $p if $p;
159 49 100       688 }
160             return;
161 17         47 }
162 17         114 my $type = shift;
163 17 50       196 my %method = (filename => 'file_extensions', media_type => 'media_types');
164             if (my $method = $method{ $type }) {
165 0         0 my $value = shift;
166             $value =~ s/^.*[.]// if ($type eq 'filename');
167 21         45 $value =~ s/;.*$// if ($type eq 'media_type');
168 21         102 foreach my $p ($self->serializers()) {
169 21 100       92 if (can_load( modules => { $p => 0 })) {
170 20         48 next unless ($p->does($role));
171 20 100       100 my @exts = @{ $p->$method() };
172 20 100       77 return $p if (any { $value eq $_ } @exts);
173 20         98 }
174 145 50       507271 }
175 145 100       29703 return;
176 129         2404 } else {
  129         617  
177 129 100   170   723 die "Not a valid constraint in get_serializer call: $type";
  170         720  
178             }
179             }
180 0         0
181             =item C<< get_parser( $NAME ) >>
182 1         12  
183             =item C<< get_parser( filename => $FILENAME ) >>
184              
185             =item C<< get_parser( media_type => $MEDIA_TYPE ) >>
186              
187             Attempts to find a L<Attean::API::Parser> parser class with the given
188             C<< $NAME >>, or that can parse files with the same extension as
189             C<< $FILENAME >>, or that can parse files with the C<< $MEDIA_TYPE >> media
190             type.
191              
192             Returns the full class name if a matching implementation is found, otherwise
193             returns undef.
194              
195             =cut
196              
197             my $self = shift;
198             my $role = 'Attean::API::Parser';
199            
200             if (scalar(@_) == 1) {
201             my $name = shift;
202             my $p = $self->_get_plugin('parsers', $name, $role);
203 190     190 1 200155 return $p if $p;
204 190         405
205             foreach my $type (qw'filename media_type') {
206 190 100       645 my $p = $self->get_parser($type => $name);
207 175         300 return $p if $p;
208 175         644 }
209 175 100       3982 return;
210             }
211 11         27
212 11         88 while (my $type = shift) {
213 11 50       125 my %method = (filename => 'file_extensions', media_type => 'media_types');
214             if (my $method = $method{ $type }) {
215 0         0 my $value = shift;
216             $value =~ s/^.*[.]// if ($type eq 'filename');
217             $value =~ s/;.*$// if ($type eq 'media_type');
218 15         63 foreach my $p ($self->parsers()) {
219 15         72 if (can_load( modules => { $p => 0 })) {
220 15 100       56 next unless ($p->can('does') and $p->does($role));
221 14         32 my @exts = @{ $p->$method() };
222 14 100       67 return $p if (any { $value eq $_ } @exts);
223 14 100       50 }
224 14         73 }
225 73 50       1040663 } else {
226 73 100 66     32754 die "Not a valid constraint in get_parser call: $type";
227 63         1363 }
  63         270  
228 63 100   78   516 }
  78         406  
229             return;
230             }
231            
232 1         13 {
233             my %roles = (
234             serializers => 'Attean::API::Serializer',
235 0         0 parsers => 'Attean::API::Parser',
236             stores => 'Attean::API::Store',
237             );
238             for my $method (keys %roles) {
239             my $role = $roles{$method};
240             my $code = sub {
241             my $self = shift;
242             my @classes;
243             foreach my $class ($self->$method()) {
244             next unless (can_load( modules => { $class => 0 }));
245             push(@classes, $class) if ($class->can('does') and $class->does($role));
246             }
247 15     15   29 return @classes;
        15      
        15      
248 15         30 };
249 15         96 Sub::Install::install_sub({
250 192 50       683177 code => set_subname("list_${method}", $code),
251 192 100 66     42996 as => "list_${method}"
252             });
253 15         315 }
254             }
255            
256             my $self = shift;
257             my $type = shift;
258             my $name = shift;
259             my @roles = @_;
260             foreach my $p ($self->$type()) {
261             if (lc(substr($p, -(length($name)+2))) eq lc("::$name")) {
262             unless (can_load( modules => { $p => 0 })) {
263 300     300   539 warn $Module::Load::Conditional::ERROR;
264 300         480 return;
265 300         553 }
266 300         672
267 300         1630 foreach (@roles) {
268 1896 100       12904232 unless ($p->does($_)) {
269 272 50       1794 die ucfirst($type) . " class $p failed validation for role $_";
270 0         0 }
271 0         0 }
272             return $p;
273             }
274 272         69375 }
275 196 50       1338 }
276 0         0
277             =item C<< negotiate_serializer ( request_headers => $request_headers, restrict => \@serializer_names, extend => \%media_types ) >>
278              
279 272         7039 Returns a two-element list containing an appropriate media type and
280             L<Attean::API::Serializer> class as decided by L<HTTP::Negotiate>. If the
281             C<< 'request_headers' >> key-value is supplied, the C<< $request_headers >> is
282             passed to C<< HTTP::Negotiate::choose >>. The option C<< 'restrict' >>, set to
283             a list of serializer names, can be used to limit the serializers to choose from.
284             Finally, an C<<'extend'>> option can be set to a hashref that contains
285             MIME-types as keys and a custom variant as value. This will enable the user to
286             use this negotiator to return a type that isn't supported by any serializers.
287             The subsequent code will have to find out how to return a representation.
288              
289             =cut
290              
291             my $class = shift;
292             my %options = @_;
293             my $headers = delete $options{ 'request_headers' };
294             my $restrict = delete $options{ 'restrict' };
295             my $extend = delete $options{ 'extend' } || {};
296             my %serializer_names;
297             my %media_types;
298             foreach my $sclass ($class->list_serializers) {
299 9     9 1 9769 my $name = $sclass =~ s/^.*://r;
300 9         43 $serializer_names{lc($name)} = $sclass;
301 9         25 for (@{ $sclass->media_types }) {
302 9         67 push(@{ $media_types{$_} }, $sclass);
303 9   100     60 }
304 9         23 }
305             my %sclasses;
306 9         44 if (ref($restrict) && ref($restrict) eq 'ARRAY') {
307 117         372 foreach (@$restrict) {
308 117         320 if (my $sclass = $serializer_names{lc($_)}) {
309 117         128 $sclasses{ $sclass } = 1;
  117         609  
310 135         135 }
  135         329  
311             }
312             } else {
313 9         26 %sclasses = reverse %serializer_names;
314 9 100 66     49 }
315 3         6 my @default_variants;
316 4 100       25 while (my($type, $sclasses) = each(%media_types)) {
317 3         8 foreach my $sclass (@$sclasses) {
318             next unless $sclasses{$sclass};
319             my $qv;
320             # slightly prefer turtle as a readable format to others
321 6         38 # try hard to avoid using ntriples as 'text/plain' isn't very useful for conneg
322             if ($type eq 'application/n-triples') {
323 9         15 $qv = 1.0;
324 9         46 } elsif ($type eq 'text/plain') {
325 99         124 $qv = 0.2;
326 135 100       218 } else {
327 95         85 $qv = 0.99;
328             $qv -= 0.01 if ($type =~ m#/x-#); # prefer non experimental media types
329             $qv -= 0.01 if ($type =~ m#^application/(?!rdf[+]xml)#); # prefer standard rdf/xml to other application/* formats
330 95 100       155 }
    100          
331 14         25 push(@default_variants, [$type, $qv, $type]);
332             }
333 20         23 }
334            
335 61         62 my %custom_thunks;
336 61 50       99 my @custom_variants;
337 61 100       119 while (my($type,$thunk) = each(%$extend)) {
338             push(@custom_variants, [$thunk, 1.0, $type]);
339 95         250 $custom_thunks{ $thunk } = [$type, $thunk];
340             }
341            
342             # remove variants with media types that are in custom_variants from @variants
343 9         25 my @variants = grep { not exists $extend->{ $_->[2] } } @default_variants;
344             push(@variants, @custom_variants);
345 9         44
346 3         9 my $stype = choose( \@variants, $headers );
347 3         17 if (defined($stype) and $custom_thunks{ $stype }) {
348             my $thunk = $stype;
349             my $type = $custom_thunks{ $stype }[0];
350             return ($type, $thunk);
351 9         31 }
  95         146  
352 9         19
353             if (defined($stype) and my $sclasses = $media_types{ $stype }) {
354 9         58 return ($stype, $sclasses->[0]);
355 9 100 100     6059 } else {
356 2         6 die "No appropriate serializer found for content-negotiation: " . Data::Dumper->Dump([$headers, $restrict, $extend], [qw(headers restrict extend)]);
357 2         5 }
358 2         29 }
359            
360             =item C<< acceptable_parsers ( handles => $item_role, prefer => $parser_role ) >>
361 7 100 66     31  
362 5         73 Returns a string value expressing the media types that are acceptable to the
363             parsers available to the system. This string may be used as an 'Accept' HTTP
364 2         28 header value.
365              
366             If a C<< handles >> role is supplied, only parsers that produce objects that
367             conform to C<< $item_role >> will be included.
368              
369             If a C<< prefer >> role is supplied, only parsers that conform to
370             C<< $parser_role >> will be included.
371              
372             Parsers are given a quality-value (expressing a preferred order or use) based
373             on the roles each parser consumes. Parsers consuming L<Attean::API::PullParser>
374             are preferred, while those consuming L<Attean::API::AtOnceParser> are not
375             preferred. An exact ordering between parsers consuming similar roles is
376             currently undefined.
377              
378             =cut
379              
380             my $class = shift;
381             my %options = @_;
382             my $handles = delete $options{ 'handles' };
383             my $prefer = delete $options{ 'prefer' };
384              
385             if (defined($handles) and $handles !~ /::/) {
386             $handles = ucfirst(lc($handles));
387             $handles = "Attean::API::$handles";
388             }
389 6     6 1 32793 if (defined($prefer) and $prefer !~ /::/) {
390 6         15 $prefer = "Attean::API::" . ucfirst($prefer);
391 6         14 $prefer = "${prefer}Parser" unless ($prefer =~ /Parser$/);
392 6         11 }
393            
394 6 100 100     28 my %media_types;
395 1         4 foreach my $pclass ($class->list_parsers) {
396 1         3 if (defined($handles)) {
397             my $type = $pclass->handled_type;
398 6 100 100     27 next unless ($type->can('role'));
399 2         9 my $role = $type->role;
400 2 50       7 next unless Role::Tiny::does_role($handles, $role);
401             }
402            
403 6         11 if (defined($prefer)) {
404 6         17 next unless ($pclass->does($prefer));
405 60 100       509 }
406 20         59
407 20 50       41 my $q = 0.5;
408 20         157 if ($pclass->does('Attean::API::PullParser')) {
409 20 100       72 $q += 0.25;
410             } elsif ($pclass->does('Attean::API::AtOnceParser')) {
411             $q -= 0.25;
412 48 100       141 }
413 30 100       61
414             for (@{ $pclass->media_types }) {
415             my $mt = "$_;q=$q";
416 28         146 $media_types{$mt} = $q;
417 28 100       66 }
    100          
418 11         124 }
419            
420 5         158 my @sorted = sort { $media_types{$b} <=> $media_types{$a} } keys %media_types;
421             return join(',', @sorted);
422             }
423 28         753
  28         106  
424 39         120
425 39         102 our %global_functions;
426              
427             =item C<< register_global_function( %uri_to_func ) >>
428              
429 6         71 =cut
  72         100  
430 6         47 my $class = shift;
431             my %args = @_;
432             foreach my $uri (keys %args) {
433             my $func = $args{ $uri };
434             $global_functions{ $uri } = $func;
435             }
436             }
437              
438             =item C<< get_global_function( $uri ) >>
439              
440 0     0 1 0 =cut
441 0         0 my $class = shift;
442 0         0 my $uri = shift;
443 0         0 return $global_functions{ $uri };
444 0         0 }
445            
446             our %global_aggregates;
447              
448             =item C<< register_global_aggregate( %uri_to_hash ) >>
449              
450             =cut
451             my $class = shift;
452 0     0 1 0 my %args = @_;
453 0         0 foreach my $uri (keys %args) {
454 0         0 my $funcs = $args{ $uri };
455             $global_aggregates{ $uri } = $funcs;
456             }
457             }
458              
459             =item C<< get_global_aggregate( $uri ) >>
460              
461             =cut
462             my $class = shift;
463 0     0 1 0 my $uri = shift;
464 0         0 return $global_aggregates{ $uri };
465 0         0 }
466 0         0
467 0         0
468             }
469              
470             1;
471              
472              
473             =back
474              
475 2     2 1 4 =head1 BUGS
476 2         4  
477 2         14 Please report any bugs or feature requests to through the GitHub web interface
478             at L<https://github.com/kasei/attean/issues>.
479              
480             =head1 SEE ALSO
481              
482              
483              
484             =head1 AUTHOR
485              
486             Gregory Todd Williams C<< <gwilliams@cpan.org> >>
487              
488             =head1 COPYRIGHT
489              
490             Copyright (c) 2014--2022 Gregory Todd Williams.
491             This program is free software; you can redistribute it and/or modify it under
492             the same terms as Perl itself.
493              
494             =cut