| 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 |