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.032_01
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   6671527 use warnings;
  50         424  
53 50     50   231 our $VERSION = '0.032_01';
  50         101  
  50         1724  
54             use Attean::API;
55 50     50   17338
  50         141  
  50         2040  
56             use Attean::Blank;
57 50     50   365 use Attean::Literal;
  50         96  
  50         1039  
58 50     50   20020 use Attean::Variable;
  50         174  
  50         1476  
59 50     50   372 use Attean::IRI;
  50         108  
  50         930  
60 50     50   226
  50         99  
  50         1175  
61             use Attean::Triple;
62 50     50   19561 use Attean::Quad;
  50         167  
  50         1431  
63 50     50   18839 use Attean::Result;
  50         142  
  50         1718  
64 50     50   19101
  50         151  
  50         1660  
65             use Attean::QuadModel;
66 50     50   18400 use Attean::TripleModel;
  50         158  
  50         1555  
67 50     50   20890 use Attean::BindingEqualityTest;
  50         196  
  50         2056  
68 50     50   20647
  50         185  
  50         1488  
69             use Attean::CodeIterator;
70 50     50   19262 use Attean::ListIterator;
  50         167  
  50         1528  
71 50     50   358 use Attean::IteratorSequence;
  50         103  
  50         1313  
72 50     50   19732  
  50         149  
  50         1685  
73             use Attean::IDPQueryPlanner;
74 50     50   18577
  50         208  
  50         1641  
75             use Attean::TermMap;
76 50     50   21968
  50         175  
  50         1649  
77             use HTTP::Negotiate qw(choose);
78 50     50   104853 use List::MoreUtils qw(any all);
  50         2228  
  50         3254  
79 50     50   372 use Module::Load::Conditional qw(can_load);
  50         129  
  50         450  
80 50     50   49305 use Role::Tiny ();
  50         120  
  50         1995  
81 50     50   289 use Sub::Util qw(set_subname);
  50         106  
  50         850  
82 50     50   228 use namespace::clean;
  50         116  
  50         1860  
83 50     50   292
  50         116  
  50         384  
84             use Module::Pluggable search_path => 'AtteanX::Parser', sub_name => 'parsers', max_depth => 3;
85 50     50   35153 use Module::Pluggable search_path => 'AtteanX::Serializer', sub_name => 'serializers', max_depth => 3;
  50         388456  
  50         372  
86 50     50   5180 use Module::Pluggable search_path => 'AtteanX::Store', sub_name => 'stores', max_depth => 3;
  50         134  
  50         192  
87 50     50   4453
  50         133  
  50         200  
88             my $class = shift;
89             if (scalar(@_)) {
90 138     138   596 my %args = @_;
91 138 100       25767 foreach my $p (@{ $args{parsers} || [] }) {
92 2         6 # warn "Loading $p parser...";
93 2 50       3 $class->get_parser($p) || die "Failed to load parser: $p";
  2         14  
94             }
95 2 50       6 foreach my $s (@{ $args{serializers} || [] }) {
96             # warn "Loading $s serializer...";
97 2 50       4 $class->get_serializer($s) || die "Failed to load serializer: $s";
  2         14  
98             }
99 0 0       0 foreach my $s (@{ $args{stores} || [] }) {
100             # warn "Loading $s store...";
101 2 50       5 $class->get_store($s) || die "Failed to load store: $s";
  2         74  
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 52297  
121 76         322 =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 565  
132 3         13  
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 83016 my $p = $self->_get_plugin('serializers', $name, $role);
154 70         180 return $p if $p;
155            
156 70 100       333 foreach my $type (qw'filename media_type') {
157 49         126 my $p = $self->get_serializer($type => $name);
158 49         241 return $p if $p;
159 49 100       1057 }
160             return;
161 17         51 }
162 17         174 my $type = shift;
163 17 50       250 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         51 $value =~ s/;.*$// if ($type eq 'media_type');
168 21         102 foreach my $p ($self->serializers()) {
169 21 100       89 if (can_load( modules => { $p => 0 })) {
170 20         42 next unless ($p->does($role));
171 20 100       115 my @exts = @{ $p->$method() };
172 20 100       69 return $p if (any { $value eq $_ } @exts);
173 20         106 }
174 145 50       509551 }
175 145 100       28765 return;
176 129         2363 } else {
  129         730  
177 129 100   170   687 die "Not a valid constraint in get_serializer call: $type";
  170         714  
178             }
179             }
180 0         0
181             =item C<< get_parser( $NAME ) >>
182 1         14  
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 221793 return $p if $p;
204 190         427
205             foreach my $type (qw'filename media_type') {
206 190 100       677 my $p = $self->get_parser($type => $name);
207 175         341 return $p if $p;
208 175         610 }
209 175 100       4136 return;
210             }
211 11         28
212 11         86 while (my $type = shift) {
213 11 50       129 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         56 foreach my $p ($self->parsers()) {
219 15         79 if (can_load( modules => { $p => 0 })) {
220 15 100       57 next unless ($p->can('does') and $p->does($role));
221 14         30 my @exts = @{ $p->$method() };
222 14 100       69 return $p if (any { $value eq $_ } @exts);
223 14 100       50 }
224 14         64 }
225 73 50       1021500 } else {
226 73 100 66     31741 die "Not a valid constraint in get_parser call: $type";
227 63         1197 }
  63         281  
228 63 100   78   376 }
  78         368  
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   24 return @classes;
        15      
        15      
248 15         20 };
249 15         64 Sub::Install::install_sub({
250 192 50       672634 code => set_subname("list_${method}", $code),
251 192 100 66     40005 as => "list_${method}"
252             });
253 15         264 }
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   627 warn $Module::Load::Conditional::ERROR;
264 300         540 return;
265 300         572 }
266 300         736
267 300         1827 foreach (@roles) {
268 1896 100       13957352 unless ($p->does($_)) {
269 272 50       2191 die ucfirst($type) . " class $p failed validation for role $_";
270 0         0 }
271 0         0 }
272             return $p;
273             }
274 272         76155 }
275 196 50       1589 }
276 0         0
277             =item C<< negotiate_serializer ( request_headers => $request_headers, restrict => \@serializer_names, extend => \%media_types ) >>
278              
279 272         7603 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 7398 my $name = $sclass =~ s/^.*://r;
300 9         25 $serializer_names{lc($name)} = $sclass;
301 9         20 for (@{ $sclass->media_types }) {
302 9         15 push(@{ $media_types{$_} }, $sclass);
303 9   100     36 }
304 9         17 }
305             my %sclasses;
306 9         21 if (ref($restrict) && ref($restrict) eq 'ARRAY') {
307 117         349 foreach (@$restrict) {
308 117         201 if (my $sclass = $serializer_names{lc($_)}) {
309 117         116 $sclasses{ $sclass } = 1;
  117         335  
310 135         143 }
  135         278  
311             }
312             } else {
313 9         20 %sclasses = reverse %serializer_names;
314 9 100 66     39 }
315 3         11 my @default_variants;
316 4 100       16 while (my($type, $sclasses) = each(%media_types)) {
317 3         7 foreach my $sclass (@$sclasses) {
318             next unless $sclasses{$sclass};
319             my $qv;
320             # slightly prefer turtle as a readable format to others
321 6         39 # 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         26 } elsif ($type eq 'text/plain') {
325 99         121 $qv = 0.2;
326 135 100       232 } else {
327 95         93 $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       130 }
    100          
331 14         17 push(@default_variants, [$type, $qv, $type]);
332             }
333 20         23 }
334            
335 61         61 my %custom_thunks;
336 61 50       97 my @custom_variants;
337 61 100       108 while (my($type,$thunk) = each(%$extend)) {
338             push(@custom_variants, [$thunk, 1.0, $type]);
339 95         236 $custom_thunks{ $thunk } = [$type, $thunk];
340             }
341            
342             # remove variants with media types that are in custom_variants from @variants
343 9         11 my @variants = grep { not exists $extend->{ $_->[2] } } @default_variants;
344             push(@variants, @custom_variants);
345 9         34
346 3         7 my $stype = choose( \@variants, $headers );
347 3         9 if (defined($stype) and $custom_thunks{ $stype }) {
348             my $thunk = $stype;
349             my $type = $custom_thunks{ $stype }[0];
350             return ($type, $thunk);
351 9         15 }
  95         137  
352 9         13
353             if (defined($stype) and my $sclasses = $media_types{ $stype }) {
354 9         30 return ($stype, $sclasses->[0]);
355 9 100 100     5358 } else {
356 2         7 die "No appropriate serializer found for content-negotiation: " . Data::Dumper->Dump([$headers, $restrict, $extend], [qw(headers restrict extend)]);
357 2         5 }
358 2         24 }
359            
360             =item C<< acceptable_parsers ( handles => $item_role, prefer => $parser_role ) >>
361 7 100 66     25  
362 5         66 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         18 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 30147 if (defined($prefer) and $prefer !~ /::/) {
390 6         15 $prefer = "Attean::API::" . ucfirst($prefer);
391 6         12 $prefer = "${prefer}Parser" unless ($prefer =~ /Parser$/);
392 6         11 }
393            
394 6 100 100     22 my %media_types;
395 1         4 foreach my $pclass ($class->list_parsers) {
396 1         2 if (defined($handles)) {
397             my $type = $pclass->handled_type;
398 6 100 100     26 next unless ($type->can('role'));
399 2         8 my $role = $type->role;
400 2 50       8 next unless Role::Tiny::does_role($handles, $role);
401             }
402            
403 6         7 if (defined($prefer)) {
404 6         18 next unless ($pclass->does($prefer));
405 60 100       455 }
406 20         91
407 20 50       44 my $q = 0.5;
408 20         513 if ($pclass->does('Attean::API::PullParser')) {
409 20 100       96 $q += 0.25;
410             } elsif ($pclass->does('Attean::API::AtOnceParser')) {
411             $q -= 0.25;
412 48 100       182 }
413 30 100       58
414             for (@{ $pclass->media_types }) {
415             my $mt = "$_;q=$q";
416 28         129 $media_types{$mt} = $q;
417 28 100       68 }
    100          
418 11         124 }
419            
420 5         122 my @sorted = sort { $media_types{$b} <=> $media_types{$a} } keys %media_types;
421             return join(',', @sorted);
422             }
423 28         742
  28         131  
424 39         127
425 39         97 our %global_functions;
426              
427             =item C<< register_global_function( %uri_to_func ) >>
428              
429 6         67 =cut
  74         99  
430 6         45 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         8 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