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
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   6640757 use warnings;
  50         463  
53 50     50   238 our $VERSION = '0.032';
  50         80  
  50         1746  
54             use Attean::API;
55 50     50   16947
  50         144  
  50         2015  
56             use Attean::Blank;
57 50     50   360 use Attean::Literal;
  50         92  
  50         1121  
58 50     50   19382 use Attean::Variable;
  50         155  
  50         1559  
59 50     50   358 use Attean::IRI;
  50         103  
  50         967  
60 50     50   227
  50         95  
  50         1061  
61             use Attean::Triple;
62 50     50   19172 use Attean::Quad;
  50         156  
  50         1479  
63 50     50   19307 use Attean::Result;
  50         146  
  50         1816  
64 50     50   19224
  50         201  
  50         1774  
65             use Attean::QuadModel;
66 50     50   18023 use Attean::TripleModel;
  50         168  
  50         1639  
67 50     50   21129 use Attean::BindingEqualityTest;
  50         201  
  50         2151  
68 50     50   21201
  50         148  
  50         1531  
69             use Attean::CodeIterator;
70 50     50   19378 use Attean::ListIterator;
  50         158  
  50         1559  
71 50     50   349 use Attean::IteratorSequence;
  50         121  
  50         1525  
72 50     50   19459  
  50         159  
  50         1758  
73             use Attean::IDPQueryPlanner;
74 50     50   17772
  50         187  
  50         1831  
75             use Attean::TermMap;
76 50     50   21251
  50         155  
  50         1549  
77             use HTTP::Negotiate qw(choose);
78 50     50   102152 use List::MoreUtils qw(any all);
  50         2115  
  50         3072  
79 50     50   355 use Module::Load::Conditional qw(can_load);
  50         113  
  50         407  
80 50     50   48104 use Role::Tiny ();
  50         114  
  50         2327  
81 50     50   272 use Sub::Util qw(set_subname);
  50         114  
  50         813  
82 50     50   250 use namespace::clean;
  50         98  
  50         1790  
83 50     50   308
  50         119  
  50         373  
84             use Module::Pluggable search_path => 'AtteanX::Parser', sub_name => 'parsers', max_depth => 3;
85 50     50   33791 use Module::Pluggable search_path => 'AtteanX::Serializer', sub_name => 'serializers', max_depth => 3;
  50         377018  
  50         377  
86 50     50   5059 use Module::Pluggable search_path => 'AtteanX::Store', sub_name => 'stores', max_depth => 3;
  50         116  
  50         192  
87 50     50   4277
  50         125  
  50         179  
88             my $class = shift;
89             if (scalar(@_)) {
90 138     138   602 my %args = @_;
91 138 100       25066 foreach my $p (@{ $args{parsers} || [] }) {
92 2         7 # warn "Loading $p parser...";
93 2 50       4 $class->get_parser($p) || die "Failed to load parser: $p";
  2         17  
94             }
95 2 50       8 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       5 $class->get_store($s) || die "Failed to load store: $s";
  2         103  
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 64651  
121 76         311 =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 572  
132 3         14  
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 76797 my $p = $self->_get_plugin('serializers', $name, $role);
154 70         186 return $p if $p;
155            
156 70 100       280 foreach my $type (qw'filename media_type') {
157 49         898 my $p = $self->get_serializer($type => $name);
158 49         212 return $p if $p;
159 49 100       683 }
160             return;
161 17         44 }
162 17         134 my $type = shift;
163 17 50       223 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         67 $value =~ s/;.*$// if ($type eq 'media_type');
168 21         96 foreach my $p ($self->serializers()) {
169 21 100       115 if (can_load( modules => { $p => 0 })) {
170 20         41 next unless ($p->does($role));
171 20 100       103 my @exts = @{ $p->$method() };
172 20 100       83 return $p if (any { $value eq $_ } @exts);
173 20         92 }
174 145 50       521741 }
175 145 100       29970 return;
176 129         2489 } else {
  129         704  
177 129 100   170   841 die "Not a valid constraint in get_serializer call: $type";
  170         811  
178             }
179             }
180 0         0
181             =item C<< get_parser( $NAME ) >>
182 1         15  
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 230273 return $p if $p;
204 190         448
205             foreach my $type (qw'filename media_type') {
206 190 100       825 my $p = $self->get_parser($type => $name);
207 175         392 return $p if $p;
208 175         745 }
209 175 100       4813 return;
210             }
211 11         34
212 11         120 while (my $type = shift) {
213 11 50       135 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         79 if (can_load( modules => { $p => 0 })) {
220 15 100       74 next unless ($p->can('does') and $p->does($role));
221 14         33 my @exts = @{ $p->$method() };
222 14 100       77 return $p if (any { $value eq $_ } @exts);
223 14 100       73 }
224 14         653 }
225 73 50       1073310 } else {
226 73 100 66     33002 die "Not a valid constraint in get_parser call: $type";
227 63         1289 }
  63         331  
228 63 100   78   414 }
  78         388  
229             return;
230             }
231            
232 1         11 {
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   28 return @classes;
        15      
        15      
248 15         24 };
249 15         71 Sub::Install::install_sub({
250 192 50       690230 code => set_subname("list_${method}", $code),
251 192 100 66     41437 as => "list_${method}"
252             });
253 15         266 }
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   1165 warn $Module::Load::Conditional::ERROR;
264 300         590 return;
265 300         591 }
266 300         768
267 300         1876 foreach (@roles) {
268 1896 100       14272051 unless ($p->does($_)) {
269 272 50       2364 die ucfirst($type) . " class $p failed validation for role $_";
270 0         0 }
271 0         0 }
272             return $p;
273             }
274 272         78387 }
275 196 50       2125 }
276 0         0
277             =item C<< negotiate_serializer ( request_headers => $request_headers, restrict => \@serializer_names, extend => \%media_types ) >>
278              
279 272         8321 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 8862 my $name = $sclass =~ s/^.*://r;
300 9         29 $serializer_names{lc($name)} = $sclass;
301 9         16 for (@{ $sclass->media_types }) {
302 9         17 push(@{ $media_types{$_} }, $sclass);
303 9   100     37 }
304 9         19 }
305             my %sclasses;
306 9         21 if (ref($restrict) && ref($restrict) eq 'ARRAY') {
307 117         318 foreach (@$restrict) {
308 117         195 if (my $sclass = $serializer_names{lc($_)}) {
309 117         120 $sclasses{ $sclass } = 1;
  117         358  
310 135         125 }
  135         281  
311             }
312             } else {
313 9         14 %sclasses = reverse %serializer_names;
314 9 100 66     33 }
315 3         14 my @default_variants;
316 4 100       17 while (my($type, $sclasses) = each(%media_types)) {
317 3         6 foreach my $sclass (@$sclasses) {
318             next unless $sclasses{$sclass};
319             my $qv;
320             # slightly prefer turtle as a readable format to others
321 6         34 # try hard to avoid using ntriples as 'text/plain' isn't very useful for conneg
322             if ($type eq 'application/n-triples') {
323 9         12 $qv = 1.0;
324 9         30 } elsif ($type eq 'text/plain') {
325 99         115 $qv = 0.2;
326 135 100       204 } else {
327 95         84 $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       135 }
    100          
331 14         16 push(@default_variants, [$type, $qv, $type]);
332             }
333 20         25 }
334            
335 61         55 my %custom_thunks;
336 61 50       104 my @custom_variants;
337 61 100       102 while (my($type,$thunk) = each(%$extend)) {
338             push(@custom_variants, [$thunk, 1.0, $type]);
339 95         231 $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         31
346 3         6 my $stype = choose( \@variants, $headers );
347 3         11 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         152  
352 9         17
353             if (defined($stype) and my $sclasses = $media_types{ $stype }) {
354 9         27 return ($stype, $sclasses->[0]);
355 9 100 100     5345 } else {
356 2         6 die "No appropriate serializer found for content-negotiation: " . Data::Dumper->Dump([$headers, $restrict, $extend], [qw(headers restrict extend)]);
357 2         3 }
358 2         29 }
359            
360             =item C<< acceptable_parsers ( handles => $item_role, prefer => $parser_role ) >>
361 7 100 66     27  
362 5         78 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 33410 if (defined($prefer) and $prefer !~ /::/) {
390 6         17 $prefer = "Attean::API::" . ucfirst($prefer);
391 6         14 $prefer = "${prefer}Parser" unless ($prefer =~ /Parser$/);
392 6         13 }
393            
394 6 100 100     31 my %media_types;
395 1         6 foreach my $pclass ($class->list_parsers) {
396 1         3 if (defined($handles)) {
397             my $type = $pclass->handled_type;
398 6 100 100     39 next unless ($type->can('role'));
399 2         9 my $role = $type->role;
400 2 50       8 next unless Role::Tiny::does_role($handles, $role);
401             }
402            
403 6         13 if (defined($prefer)) {
404 6         22 next unless ($pclass->does($prefer));
405 60 100       472 }
406 20         67
407 20 50       50 my $q = 0.5;
408 20         163 if ($pclass->does('Attean::API::PullParser')) {
409 20 100       66 $q += 0.25;
410             } elsif ($pclass->does('Attean::API::AtOnceParser')) {
411             $q -= 0.25;
412 48 100       143 }
413 30 100       55
414             for (@{ $pclass->media_types }) {
415             my $mt = "$_;q=$q";
416 28         142 $media_types{$mt} = $q;
417 28 100       69 }
    100          
418 11         169 }
419            
420 5         197 my @sorted = sort { $media_types{$b} <=> $media_types{$a} } keys %media_types;
421             return join(',', @sorted);
422             }
423 28         371
  28         113  
424 39         149
425 39         106 our %global_functions;
426              
427             =item C<< register_global_function( %uri_to_func ) >>
428              
429 6         91 =cut
  75         668  
430 6         51 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 6 =head1 BUGS
476 2         3  
477 2         12 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