|  line  | 
 stmt  | 
 bran  | 
 cond  | 
 sub  | 
 pod  | 
 time  | 
 code  | 
| 
1
 | 
  
 
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 # ABSTRACT: PONAPI - Perl implementation of {JSON:API} (http://jsonapi.org/) v1.0  | 
| 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 package PONAPI::Server;  | 
| 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
4
 | 
3
 | 
 
 | 
 
 | 
  
3
  
 | 
 
 | 
357515
 | 
 use strict;  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
24
 | 
    | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
84
 | 
    | 
| 
5
 | 
3
 | 
 
 | 
 
 | 
  
3
  
 | 
 
 | 
17
 | 
 use warnings;  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
3
 | 
    | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
130
 | 
    | 
| 
6
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
7
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 our $VERSION = '0.003003';  | 
| 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
9
 | 
3
 | 
 
 | 
 
 | 
  
3
  
 | 
 
 | 
960
 | 
 use Plack::Request;  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
130760
 | 
    | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
84
 | 
    | 
| 
10
 | 
3
 | 
 
 | 
 
 | 
  
3
  
 | 
 
 | 
1230
 | 
 use Plack::Response;  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
4599
 | 
    | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
86
 | 
    | 
| 
11
 | 
3
 | 
 
 | 
 
 | 
  
3
  
 | 
 
 | 
1311
 | 
 use HTTP::Headers::ActionPack;  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
8907
 | 
    | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
89
 | 
    | 
| 
12
 | 
3
 | 
 
 | 
 
 | 
  
3
  
 | 
 
 | 
20
 | 
 use Module::Runtime ();  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
7
 | 
    | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
47
 | 
    | 
| 
13
 | 
3
 | 
 
 | 
 
 | 
  
3
  
 | 
 
 | 
15
 | 
 use JSON::MaybeXS;  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
7
 | 
    | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
166
 | 
    | 
| 
14
 | 
3
 | 
 
 | 
 
 | 
  
3
  
 | 
 
 | 
17
 | 
 use URI::Escape qw( uri_unescape );  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
7
 | 
    | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
131
 | 
    | 
| 
15
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
16
 | 
3
 | 
 
 | 
 
 | 
  
3
  
 | 
 
 | 
1247
 | 
 use PONAPI::Server::ConfigReader;  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
10
 | 
    | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
130
 | 
    | 
| 
17
 | 
3
 | 
 
 | 
 
 | 
  
3
  
 | 
 
 | 
1555
 | 
 use PONAPI::Names qw( check_name );  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
64765
 | 
    | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
228
 | 
    | 
| 
18
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
19
 | 
3
 | 
 
 | 
 
 | 
  
3
  
 | 
 
 | 
25
 | 
 use parent 'Plack::Component';  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
7
 | 
    | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
18
 | 
    | 
| 
20
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
21
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 use constant {  | 
| 
22
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
5722
 | 
     ERR_MISSING_MEDIA_TYPE   => +{ __error__ => +[ 415, "{JSON:API} No {json:api} Media-Type (Content-Type / Accept)" ] },  | 
| 
23
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     ERR_MISSING_CONTENT_TYPE => +{ __error__ => +[ 415, "{JSON:API} Missing Content-Type header" ] },  | 
| 
24
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     ERR_WRONG_CONTENT_TYPE   => +{ __error__ => +[ 415, "{JSON:API} Invalid Content-Type header" ] },  | 
| 
25
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     ERR_WRONG_HEADER_ACCEPT  => +{ __error__ => +[ 406, "{JSON:API} Invalid Accept header" ] },  | 
| 
26
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     ERR_BAD_REQ              => +{ __error__ => +[ 400, "{JSON:API} Bad request" ] },  | 
| 
27
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     ERR_BAD_REQ_INVALID_NAME => +{ __error__ => +[ 400, "{JSON:API} Bad request (invalid member-name)" ] },  | 
| 
28
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     ERR_BAD_REQ_PARAMS       => +{ __error__ => +[ 400, "{JSON:API} Bad request (unsupported parameters)" ] },  | 
| 
29
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     ERR_SORT_NOT_ALLOWED     => +{ __error__ => +[ 400, "{JSON:API} Server-side sorting not allowed" ] },  | 
| 
30
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     ERR_NO_MATCHING_ROUTE    => +{ __error__ => +[ 404, "{JSON:API} No matching route" ] },  | 
| 
31
 | 
3
 | 
 
 | 
 
 | 
  
3
  
 | 
 
 | 
7597
 | 
 };  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
8
 | 
    | 
| 
32
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
33
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 my $qr_member_name_prefix = qr/^[a-zA-Z0-9]/;  | 
| 
34
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
35
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub prepare_app {  | 
| 
36
 | 
9
 | 
 
 | 
 
 | 
  
9
  
 | 
  
1
  
 | 
15646
 | 
     my $self = shift;  | 
| 
37
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
38
 | 
9
 | 
 
 | 
 
 | 
 
 | 
 
 | 
25
 | 
     my %conf;  | 
| 
39
 | 
9
 | 
 
 | 
 
 | 
 
 | 
 
 | 
18
 | 
     local $@;  | 
| 
40
 | 
9
 | 
 
 | 
 
 | 
 
 | 
 
 | 
23
 | 
     eval {  | 
| 
41
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         %conf = PONAPI::Server::ConfigReader->new(  | 
| 
42
 | 
9
 | 
 
 | 
  
 50
  
 | 
 
 | 
 
 | 
584
 | 
             dir => $self->{'ponapi.config_dir'} || 'conf'  | 
| 
43
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         )->read_config;  | 
| 
44
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     };  | 
| 
45
 | 
9
 | 
 
 | 
  
100
  
 | 
 
 | 
 
 | 
292
 | 
     $self->{$_} //= $conf{$_} for keys %conf;  | 
| 
46
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
47
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # Some defaults  | 
| 
48
 | 
9
 | 
 
 | 
 
 | 
 
 | 
 
 | 
120
 | 
     my $default_media_type           = 'application/vnd.api+json';  | 
| 
49
 | 
9
 | 
 
 | 
  
 50
  
 | 
 
 | 
 
 | 
33
 | 
     $self->{'ponapi.spec_version'} //= '1.0';  | 
| 
50
 | 
9
 | 
 
 | 
  
 33
  
 | 
 
 | 
 
 | 
30
 | 
     $self->{'ponapi.mediatype'}    //= $default_media_type;  | 
| 
51
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
52
 | 
9
 | 
 
 | 
 
 | 
 
 | 
 
 | 
35
 | 
     $self->_load_dao();  | 
| 
53
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
54
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
55
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub call {  | 
| 
56
 | 
50
 | 
 
 | 
 
 | 
  
50
  
 | 
  
1
  
 | 
313114
 | 
     my ( $self, $env ) = @_;  | 
| 
57
 | 
50
 | 
 
 | 
 
 | 
 
 | 
 
 | 
323
 | 
     my $req = Plack::Request->new($env);  | 
| 
58
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
59
 | 
50
 | 
 
 | 
 
 | 
 
 | 
 
 | 
496
 | 
     my $ponapi_params;  | 
| 
60
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     eval {  | 
| 
61
 | 
50
 | 
 
 | 
 
 | 
 
 | 
 
 | 
161
 | 
         $ponapi_params = $self->_ponapi_params( $req );  | 
| 
62
 | 
25
 | 
 
 | 
 
 | 
 
 | 
 
 | 
108
 | 
         1;  | 
| 
63
 | 
50
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
103
 | 
     } or do {  | 
| 
64
 | 
25
 | 
 
 | 
 
 | 
 
 | 
 
 | 
161
 | 
         $ponapi_params = $@;  | 
| 
65
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     };  | 
| 
66
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
67
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     return $self->_options_response( $ponapi_params->{__options__} )  | 
| 
68
 | 
50
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
155
 | 
         if $ponapi_params->{__options__};  | 
| 
69
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
70
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     return $self->_error_response( $ponapi_params->{__error__} )  | 
| 
71
 | 
50
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
182
 | 
         if $ponapi_params->{__error__};  | 
| 
72
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
73
 | 
25
 | 
 
 | 
 
 | 
 
 | 
 
 | 
67
 | 
     my $action = delete $ponapi_params->{action};  | 
| 
74
 | 
25
 | 
 
 | 
 
 | 
 
 | 
 
 | 
181
 | 
     my ( $status, $headers, $res ) = $self->{'ponapi.DAO'}->$action($ponapi_params);  | 
| 
75
 | 
25
 | 
 
 | 
 
 | 
 
 | 
 
 | 
173
 | 
     return $self->_response( $status, $headers, $res, $req->method eq 'HEAD' );  | 
| 
76
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
77
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
78
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
79
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 ### ...  | 
| 
80
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
81
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _load_dao {  | 
| 
82
 | 
9
 | 
 
 | 
 
 | 
  
9
  
 | 
 
 | 
18
 | 
     my $self = shift;  | 
| 
83
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
84
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     my $repository =  | 
| 
85
 | 
9
 | 
 
 | 
  
 50
  
 | 
 
 | 
 
 | 
45
 | 
         Module::Runtime::use_module( $self->{'repository.class'} )->new( @{ $self->{'repository.args'} } )  | 
| 
86
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
           || die "[PONAPI Server] failed to create a repository object\n";  | 
| 
87
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
88
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     $self->{'ponapi.DAO'} = PONAPI::DAO->new(  | 
| 
89
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         repository => $repository,  | 
| 
90
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
551
 | 
         version    => $self->{'ponapi.spec_version'},  | 
| 
91
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     );  | 
| 
92
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
93
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
94
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _is_get_like {  | 
| 
95
 | 
141
 | 
 
 | 
 
 | 
  
141
  
 | 
 
 | 
221
 | 
     my $req = shift;  | 
| 
96
 | 
141
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
298
 | 
     return 1 if $req->method =~ /^(?:GET|HEAD)$/;  | 
| 
97
 | 
52
 | 
 
 | 
 
 | 
 
 | 
 
 | 
424
 | 
     return;  | 
| 
98
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
99
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
100
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _ponapi_params {  | 
| 
101
 | 
50
 | 
 
 | 
 
 | 
  
50
  
 | 
 
 | 
142
 | 
     my ( $self, $req ) = @_;  | 
| 
102
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
103
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # THE HEADERS  | 
| 
104
 | 
50
 | 
 
 | 
 
 | 
 
 | 
 
 | 
169
 | 
     $self->_ponapi_check_headers($req);  | 
| 
105
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
106
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # THE PATH --> route matching  | 
| 
107
 | 
46
 | 
 
 | 
 
 | 
 
 | 
 
 | 
155
 | 
     my @ponapi_route_params = $self->_ponapi_route_match($req);  | 
| 
108
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
109
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # THE QUERY  | 
| 
110
 | 
40
 | 
 
 | 
 
 | 
 
 | 
 
 | 
127
 | 
     my @ponapi_query_params = $self->_ponapi_query_params($req);  | 
| 
111
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
112
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # THE BODY CONTENT  | 
| 
113
 | 
33
 | 
 
 | 
 
 | 
 
 | 
 
 | 
117
 | 
     my @ponapi_data = $self->_ponapi_data($req);  | 
| 
114
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
115
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # misc.  | 
| 
116
 | 
25
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
273
 | 
     my $req_base      = $self->{'ponapi.relative_links'} eq 'full' ? "".$req->base : '/';  | 
| 
117
 | 
25
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
123
 | 
     my $req_path      = $self->{'ponapi.relative_links'} eq 'full' ? "".$req->uri : $req->request_uri;  | 
| 
118
 | 
25
 | 
 
 | 
 
 | 
 
 | 
 
 | 
198
 | 
     my $update_200    = !!$self->{'ponapi.respond_to_updates_with_200'};  | 
| 
119
 | 
25
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
66
 | 
     my $doc_self_link = _is_get_like($req) ? !!$self->{'ponapi.doc_auto_self_link'} : 0;  | 
| 
120
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
121
 | 
25
 | 
 
 | 
 
 | 
 
 | 
 
 | 
394
 | 
     my %params = (  | 
| 
122
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         @ponapi_route_params,  | 
| 
123
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         @ponapi_query_params,  | 
| 
124
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         @ponapi_data,  | 
| 
125
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         req_base                    => $req_base,  | 
| 
126
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         req_path                    => $req_path,  | 
| 
127
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         respond_to_updates_with_200 => $update_200,  | 
| 
128
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         send_doc_self_link          => $doc_self_link,  | 
| 
129
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     );  | 
| 
130
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
131
 | 
25
 | 
 
 | 
 
 | 
 
 | 
 
 | 
98
 | 
     return \%params;  | 
| 
132
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
133
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
134
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _ponapi_route_match {  | 
| 
135
 | 
46
 | 
 
 | 
 
 | 
  
46
  
 | 
 
 | 
111
 | 
     my ( $self, $req ) = @_;  | 
| 
136
 | 
46
 | 
 
 | 
 
 | 
 
 | 
 
 | 
148
 | 
     my $method = $req->method;  | 
| 
137
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
138
 | 
46
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
313
 | 
     die(ERR_BAD_REQ) unless grep { $_ eq $method } qw< GET POST PATCH DELETE HEAD OPTIONS >;  | 
| 
 
 | 
276
 | 
 
 | 
 
 | 
 
 | 
 
 | 
549
 | 
    | 
| 
139
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
140
 | 
46
 | 
 
 | 
 
 | 
 
 | 
 
 | 
156
 | 
     my ( $type, $id, $relationships, $rel_type ) = split '/' => substr($req->path_info,1);  | 
| 
141
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
142
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # validate `type`  | 
| 
143
 | 
46
 | 
  
100
  
 | 
  
100
  
 | 
 
 | 
 
 | 
814
 | 
     die(ERR_BAD_REQ) unless defined $type and $type =~ /$qr_member_name_prefix/ ;  | 
| 
144
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
145
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # validate `rel_type`  | 
| 
146
 | 
43
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
150
 | 
     if ( defined $rel_type ) {  | 
| 
 
 | 
 
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
147
 | 
7
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
23
 | 
         die(ERR_BAD_REQ) if $relationships ne 'relationships';  | 
| 
148
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
149
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     elsif ( $relationships ) {  | 
| 
150
 | 
1
 | 
 
 | 
 
 | 
 
 | 
 
 | 
3
 | 
         $rel_type = $relationships;  | 
| 
151
 | 
1
 | 
 
 | 
 
 | 
 
 | 
 
 | 
3
 | 
         undef $relationships;  | 
| 
152
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
153
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
154
 | 
43
 | 
 
 | 
 
 | 
 
 | 
 
 | 
95
 | 
     my $def_rel_type = defined $rel_type;  | 
| 
155
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
156
 | 
43
 | 
  
100
  
 | 
  
100
  
 | 
 
 | 
 
 | 
168
 | 
     die(ERR_BAD_REQ) if $def_rel_type and $rel_type !~ /$qr_member_name_prefix/;  | 
| 
157
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
158
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # set `action`  | 
| 
159
 | 
42
 | 
 
 | 
 
 | 
 
 | 
 
 | 
67
 | 
     my $action;  | 
| 
160
 | 
42
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
100
 | 
     if ( defined $id ) {  | 
| 
161
 | 
31
 | 
  
100
  
 | 
  
100
  
 | 
 
 | 
 
 | 
122
 | 
         $action = 'create_relationships'     if $method eq 'POST'   and $relationships  and $def_rel_type;  | 
| 
 
 | 
 
 | 
 
 | 
  
 66
  
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
162
 | 
31
 | 
  
100
  
 | 
  
100
  
 | 
 
 | 
 
 | 
86
 | 
         $action = 'retrieve'                 if _is_get_like($req)  and !$relationships and !$def_rel_type;  | 
| 
 
 | 
 
 | 
 
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
163
 | 
31
 | 
  
100
  
 | 
  
100
  
 | 
 
 | 
 
 | 
336
 | 
         $action = 'retrieve_by_relationship' if _is_get_like($req)  and !$relationships and $def_rel_type;  | 
| 
 
 | 
 
 | 
 
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
164
 | 
31
 | 
  
100
  
 | 
  
100
  
 | 
 
 | 
 
 | 
269
 | 
         $action = 'retrieve_relationships'   if _is_get_like($req)  and $relationships  and $def_rel_type;  | 
| 
 
 | 
 
 | 
 
 | 
  
 66
  
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
165
 | 
31
 | 
  
 50
  
 | 
  
 66
  
 | 
 
 | 
 
 | 
275
 | 
         $action = 'update'                   if $method eq 'PATCH'  and !$relationships and !$def_rel_type;  | 
| 
 
 | 
 
 | 
 
 | 
  
 33
  
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
166
 | 
31
 | 
  
 50
  
 | 
  
 66
  
 | 
 
 | 
 
 | 
90
 | 
         $action = 'update_relationships'     if $method eq 'PATCH'  and $relationships  and $def_rel_type;  | 
| 
 
 | 
 
 | 
 
 | 
  
 66
  
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
167
 | 
31
 | 
  
100
  
 | 
  
100
  
 | 
 
 | 
 
 | 
91
 | 
         $action = 'delete'                   if $method eq 'DELETE' and !$relationships and !$def_rel_type;  | 
| 
 
 | 
 
 | 
 
 | 
  
 66
  
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
168
 | 
31
 | 
  
100
  
 | 
  
100
  
 | 
 
 | 
 
 | 
89
 | 
         $action = 'delete_relationships'     if $method eq 'DELETE' and $relationships  and $def_rel_type;  | 
| 
 
 | 
 
 | 
 
 | 
  
 66
  
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
169
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
170
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     else {  | 
| 
171
 | 
11
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
48
 | 
         $action = 'retrieve_all'             if _is_get_like($req);  | 
| 
172
 | 
11
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
84
 | 
         $action = 'create'                   if $method eq 'POST';  | 
| 
173
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
174
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
175
 | 
42
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
136
 | 
     if ( $method eq 'OPTIONS' ) {  | 
| 
176
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
0
 | 
         my @options = (qw< GET HEAD > );  | 
| 
177
 | 
  
0
  
 | 
  
  0
  
 | 
 
 | 
 
 | 
 
 | 
0
 | 
         if ( defined $id ) {  | 
| 
178
 | 
  
0
  
 | 
  
  0
  
 | 
 
 | 
 
 | 
 
 | 
0
 | 
             push @options => (qw< PATCH DELETE >) unless $def_rel_type;  | 
| 
179
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         }  | 
| 
180
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         else {  | 
| 
181
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
0
 | 
             push @options => 'POST';  | 
| 
182
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         }  | 
| 
183
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
0
 | 
         die( +{ __options__ => \@options } );  | 
| 
184
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
185
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
186
 | 
42
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
127
 | 
     die(ERR_NO_MATCHING_ROUTE) unless $action;  | 
| 
187
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
188
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # return ( action, type, id?, rel_type? )  | 
| 
189
 | 
40
 | 
 
 | 
 
 | 
 
 | 
 
 | 
117
 | 
     my @ret = ( action => $action, type => $type );  | 
| 
190
 | 
40
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
121
 | 
     defined $id   and push @ret => id => $id;  | 
| 
191
 | 
40
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
93
 | 
     $def_rel_type and push @ret => rel_type => $rel_type;  | 
| 
192
 | 
40
 | 
 
 | 
 
 | 
 
 | 
 
 | 
172
 | 
     return @ret;  | 
| 
193
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
194
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
195
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _ponapi_check_headers {  | 
| 
196
 | 
50
 | 
 
 | 
 
 | 
  
50
  
 | 
 
 | 
114
 | 
     my ( $self, $req ) = @_;  | 
| 
197
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
198
 | 
50
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
157
 | 
     return if $req->method eq 'OPTIONS';  | 
| 
199
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
200
 | 
50
 | 
 
 | 
 
 | 
 
 | 
 
 | 
485
 | 
     my $mt = $self->{'ponapi.mediatype'};  | 
| 
201
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
202
 | 
50
 | 
 
 | 
 
 | 
 
 | 
 
 | 
96
 | 
     my $has_mediatype = 0;  | 
| 
203
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
204
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # check Content-Type  | 
| 
205
 | 
50
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
155
 | 
     if ( $req->content_length ) {  | 
| 
206
 | 
13
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
98
 | 
         if ( my $content_type = $req->headers->header('Content-Type') ) {  | 
| 
207
 | 
13
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
2468
 | 
             die(ERR_WRONG_CONTENT_TYPE) unless $content_type eq $mt;  | 
| 
208
 | 
13
 | 
 
 | 
 
 | 
 
 | 
 
 | 
28
 | 
             $has_mediatype++;  | 
| 
209
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         } else {  | 
| 
210
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
0
 | 
             die(ERR_MISSING_CONTENT_TYPE)  | 
| 
211
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         }  | 
| 
212
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
213
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
214
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # check Accept  | 
| 
215
 | 
50
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
345
 | 
     if ( my $accept = $req->headers->header('Accept') ) {  | 
| 
216
 | 
35
 | 
 
 | 
 
 | 
 
 | 
 
 | 
7788
 | 
         my $pack = HTTP::Headers::ActionPack->new;  | 
| 
217
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
218
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         my @jsonapi_accept =  | 
| 
219
 | 
35
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
2330
 | 
             map { ( $_->[1]->type eq $mt ) ? $_->[1] : () }  | 
| 
 
 | 
35
 | 
 
 | 
 
 | 
 
 | 
 
 | 
78664
 | 
    | 
| 
220
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             $pack->create_header( 'Accept' => $accept )->iterable;  | 
| 
221
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
222
 | 
35
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
498
 | 
         if ( @jsonapi_accept ) {  | 
| 
223
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             die(ERR_WRONG_HEADER_ACCEPT)  | 
| 
224
 | 
34
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
76
 | 
                 unless grep { $_->params_are_empty } @jsonapi_accept;  | 
| 
 
 | 
34
 | 
 
 | 
 
 | 
 
 | 
 
 | 
127
 | 
    | 
| 
225
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
226
 | 
33
 | 
 
 | 
 
 | 
 
 | 
 
 | 
612
 | 
             $has_mediatype++;  | 
| 
227
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         }  | 
| 
228
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
229
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
230
 | 
49
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
940
 | 
     die(ERR_MISSING_MEDIA_TYPE) unless $has_mediatype;  | 
| 
231
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
232
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
233
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _ponapi_query_params {  | 
| 
234
 | 
40
 | 
 
 | 
 
 | 
  
40
  
 | 
 
 | 
93
 | 
     my ( $self, $req ) = @_;  | 
| 
235
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
236
 | 
40
 | 
 
 | 
 
 | 
 
 | 
 
 | 
63
 | 
     my %params;  | 
| 
237
 | 
40
 | 
 
 | 
 
 | 
 
 | 
 
 | 
144
 | 
     my $query_params = $req->query_parameters;  | 
| 
238
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
239
 | 
40
 | 
 
 | 
 
 | 
 
 | 
 
 | 
3538
 | 
     my $unesacpe_values = !!$req->headers->header('X-PONAPI-Escaped-Values');  | 
| 
240
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
241
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # loop over query parameters (unique keys)  | 
| 
242
 | 
40
 | 
 
 | 
 
 | 
 
 | 
 
 | 
1503
 | 
     for my $k ( sort keys %{ $query_params } ) {  | 
| 
 
 | 
40
 | 
 
 | 
 
 | 
 
 | 
 
 | 
189
 | 
    | 
| 
243
 | 
17
 | 
 
 | 
 
 | 
 
 | 
 
 | 
136
 | 
         my ( $p, $f ) = $k =~ /^ (\w+?) (?:\[(\w+)\])? $/x;  | 
| 
244
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
245
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # key not matched  | 
| 
246
 | 
17
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
53
 | 
         die(ERR_BAD_REQ_PARAMS) unless defined $p;  | 
| 
247
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
248
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # valid parameter names  | 
| 
249
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         die(ERR_BAD_REQ_PARAMS)  | 
| 
250
 | 
17
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
38
 | 
             unless grep { $p eq $_ } qw< fields filter page include sort >;  | 
| 
 
 | 
85
 | 
 
 | 
 
 | 
 
 | 
 
 | 
169
 | 
    | 
| 
251
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
252
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # "complex" parameters have the correct structre  | 
| 
253
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         die(ERR_BAD_REQ)  | 
| 
254
 | 
17
 | 
  
100
  
 | 
  
100
  
 | 
 
 | 
 
 | 
60
 | 
             if !defined $f and grep { $p eq $_ } qw< page fields filter >;  | 
| 
 
 | 
39
 | 
 
 | 
 
 | 
 
 | 
 
 | 
111
 | 
    | 
| 
255
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
256
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # 'sort' requested but not supported  | 
| 
257
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         die(ERR_SORT_NOT_ALLOWED)  | 
| 
258
 | 
13
 | 
  
 50
  
 | 
  
 33
  
 | 
 
 | 
 
 | 
50
 | 
             if $p eq 'sort' and !$self->{'ponapi.sort_allowed'};  | 
| 
259
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
260
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # values can be passed as CSV  | 
| 
261
 | 
11
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
47
 | 
         my @values = map { $unesacpe_values ? uri_unescape($_) : $_ }  | 
| 
262
 | 
13
 | 
 
 | 
 
 | 
 
 | 
 
 | 
55
 | 
                      map { split /,/ } $query_params->get_all($k);  | 
| 
 
 | 
13
 | 
 
 | 
 
 | 
 
 | 
 
 | 
213
 | 
    | 
| 
263
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
264
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # check we have values for a given key  | 
| 
265
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # (for 'fields' an empty list is valid)  | 
| 
266
 | 
13
 | 
  
100
  
 | 
  
100
  
 | 
 
 | 
 
 | 
75
 | 
         die(ERR_BAD_REQ) if $p ne 'fields' and !@values;  | 
| 
267
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
268
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # values passed on in array-ref  | 
| 
269
 | 
20
 | 
 
 | 
 
 | 
 
 | 
 
 | 
53
 | 
         grep { $p eq $_ } qw< fields filter >  | 
| 
270
 | 
10
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
22
 | 
             and $params{$p}{$f} = \@values;  | 
| 
271
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
272
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # page info has one value per request  | 
| 
273
 | 
10
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
25
 | 
         $p eq 'page' and $params{$p}{$f} = $values[0];  | 
| 
274
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
275
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # values passed on in hash-ref  | 
| 
276
 | 
10
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
30
 | 
         $p eq 'include' and $params{include} = \@values;  | 
| 
277
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
278
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # sort values: indicate direction  | 
| 
279
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # Not doing any processing here to allow repos to support  | 
| 
280
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # complex sorting, if they want to.  | 
| 
281
 | 
10
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
40
 | 
         $p eq 'sort' and $params{'sort'} = \@values;  | 
| 
282
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
283
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
284
 | 
33
 | 
 
 | 
 
 | 
 
 | 
 
 | 
123
 | 
     return %params;  | 
| 
285
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
286
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
287
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _ponapi_data {  | 
| 
288
 | 
33
 | 
 
 | 
 
 | 
  
33
  
 | 
 
 | 
78
 | 
     my ( $self, $req ) = @_;  | 
| 
289
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
290
 | 
33
 | 
  
100
  
 | 
  
 66
  
 | 
 
 | 
 
 | 
95
 | 
     return unless $req->content_length and $req->content_length > 0;  | 
| 
291
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
292
 | 
12
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
163
 | 
     die(ERR_BAD_REQ) if _is_get_like($req);  | 
| 
293
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
294
 | 
12
 | 
 
 | 
 
 | 
 
 | 
 
 | 
25
 | 
     my $body;  | 
| 
295
 | 
12
 | 
 
 | 
 
 | 
 
 | 
 
 | 
22
 | 
     eval { $body = JSON::MaybeXS::decode_json( $req->content ); 1 };  | 
| 
 
 | 
12
 | 
 
 | 
 
 | 
 
 | 
 
 | 
36
 | 
    | 
| 
 
 | 
11
 | 
 
 | 
 
 | 
 
 | 
 
 | 
3356
 | 
    | 
| 
296
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
297
 | 
12
 | 
  
 50
  
 | 
  
 66
  
 | 
 
 | 
 
 | 
513
 | 
     die(ERR_BAD_REQ) unless $body and ref $body eq 'HASH' and exists $body->{data};  | 
| 
 
 | 
 
 | 
 
 | 
  
 66
  
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
298
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
299
 | 
11
 | 
 
 | 
 
 | 
 
 | 
 
 | 
26
 | 
     my $data = $body->{data};  | 
| 
300
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
301
 | 
11
 | 
  
100
  
 | 
  
 66
  
 | 
 
 | 
 
 | 
104
 | 
     die(ERR_BAD_REQ) if defined $data and ref($data) !~ /^(?:ARRAY|HASH)$/;  | 
| 
302
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
303
 | 
10
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
49
 | 
     $self->_validate_data_members($data) if defined $data;  | 
| 
304
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
305
 | 
4
 | 
 
 | 
 
 | 
 
 | 
 
 | 
18
 | 
     return ( data => $data );  | 
| 
306
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
307
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
308
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _validate_data_members {  | 
| 
309
 | 
10
 | 
 
 | 
 
 | 
  
10
  
 | 
 
 | 
26
 | 
     my ( $self, $data ) = @_;  | 
| 
310
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
311
 | 
10
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
36
 | 
     my @recs = ref $data eq 'ARRAY' ? @{$data} : $data;  | 
| 
 
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
7
 | 
    | 
| 
312
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
313
 | 
10
 | 
 
 | 
 
 | 
 
 | 
 
 | 
24
 | 
     for my $r ( @recs ) {  | 
| 
314
 | 
10
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
18
 | 
         return unless keys %{$r};  | 
| 
 
 | 
10
 | 
 
 | 
 
 | 
 
 | 
 
 | 
43
 | 
    | 
| 
315
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
316
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # `type`  | 
| 
317
 | 
10
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
33
 | 
         die(ERR_BAD_REQ)              unless $r->{type};  | 
| 
318
 | 
10
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
43
 | 
         die(ERR_BAD_REQ_INVALID_NAME) unless check_name( $r->{type} );  | 
| 
319
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
320
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # `attributes`  | 
| 
321
 | 
9
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
1443
 | 
         if ( exists $r->{attributes} ) {  | 
| 
322
 | 
3
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
15
 | 
             die(ERR_BAD_REQ) unless ref( $r->{attributes} ) eq 'HASH';  | 
| 
323
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             die(ERR_BAD_REQ_INVALID_NAME)  | 
| 
324
 | 
2
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
6
 | 
                 if grep { !check_name($_) } keys %{ $r->{attributes} };  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
19
 | 
    | 
| 
 
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
7
 | 
    | 
| 
325
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         }  | 
| 
326
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
327
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         # `relationships`  | 
| 
328
 | 
7
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
53
 | 
         if ( exists $r->{relationships} ) {  | 
| 
329
 | 
3
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
18
 | 
             die(ERR_BAD_REQ) unless ref( $r->{relationships} ) eq 'HASH';  | 
| 
330
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
331
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
4
 | 
             for my $k ( keys %{ $r->{relationships} } ) {  | 
| 
 
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
7
 | 
    | 
| 
332
 | 
2
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
6
 | 
                 die(ERR_BAD_REQ_INVALID_NAME) unless check_name($k);  | 
| 
333
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
334
 | 
1
 | 
 
 | 
 
 | 
 
 | 
 
 | 
15
 | 
                 my $rel  = $r->{relationships}{$k};  | 
| 
335
 | 
1
 | 
  
 50
  
 | 
  
 50
  
 | 
 
 | 
 
 | 
7
 | 
                 my @rels = ref($rel||'') eq 'ARRAY' ? @$rel : $rel;  | 
| 
336
 | 
1
 | 
 
 | 
 
 | 
 
 | 
 
 | 
4
 | 
                 foreach my $relationship ( @rels ) {  | 
| 
337
 | 
1
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
3
 | 
                     next unless defined $relationship;  | 
| 
338
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     # Some requests have relationships => { blah },  | 
| 
339
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     # others have relationships => { data => { blah } }  | 
| 
340
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     $relationship = $relationship->{data}  | 
| 
341
 | 
1
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
4
 | 
                         if exists $relationship->{data};  | 
| 
342
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
343
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     die(ERR_BAD_REQ) unless  | 
| 
344
 | 
1
 | 
  
 50
  
 | 
  
 33
  
 | 
 
 | 
 
 | 
8
 | 
                         ref($relationship) eq 'HASH' and exists $relationship->{type};  | 
| 
345
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
346
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     die(ERR_BAD_REQ_INVALID_NAME)  | 
| 
347
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                         if !check_name( $relationship->{type} )  | 
| 
348
 | 
1
 | 
  
 50
  
 | 
  
 33
  
 | 
 
 | 
 
 | 
5
 | 
                             or grep { !check_name($_) } keys %$relationship;  | 
| 
 
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
0
 | 
    | 
| 
349
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 }  | 
| 
350
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             }  | 
| 
351
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         }  | 
| 
352
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
353
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
354
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
355
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _response {  | 
| 
356
 | 
50
 | 
 
 | 
 
 | 
  
50
  
 | 
 
 | 
361
 | 
     my ( $self, $status, $headers, $content, $is_head ) = @_;  | 
| 
357
 | 
50
 | 
 
 | 
  
 50
  
 | 
 
 | 
 
 | 
364
 | 
     my $res = Plack::Response->new( $status || 200 );  | 
| 
358
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
359
 | 
50
 | 
 
 | 
 
 | 
 
 | 
 
 | 
1022
 | 
     $res->headers($headers);  | 
| 
360
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     $res->header( 'X-PONAPI-Server-Version' => $self->{'ponapi.spec_version'} )  | 
| 
361
 | 
50
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
1320
 | 
         if $self->{'ponapi.send_version_header'};  | 
| 
362
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
363
 | 
50
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
2295
 | 
     if ( ref $content ) {  | 
| 
364
 | 
50
 | 
 
 | 
 
 | 
 
 | 
 
 | 
630
 | 
         my $enc_content = JSON::MaybeXS::encode_json $content;  | 
| 
365
 | 
50
 | 
 
 | 
 
 | 
 
 | 
 
 | 
209
 | 
         $res->content_length( length($enc_content) );  | 
| 
366
 | 
50
 | 
 
 | 
 
 | 
 
 | 
 
 | 
1754
 | 
         $res->content_type( $self->{'ponapi.mediatype'} );  | 
| 
367
 | 
50
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
923
 | 
         $res->content($enc_content) unless $is_head;  | 
| 
368
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
369
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
370
 | 
50
 | 
 
 | 
 
 | 
 
 | 
 
 | 
529
 | 
     $res->finalize;  | 
| 
371
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
372
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
373
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _options_response {  | 
| 
374
 | 
  
0
  
 | 
 
 | 
 
 | 
  
0
  
 | 
 
 | 
0
 | 
     my ( $self, $options ) = @_;  | 
| 
375
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
0
 | 
     return +[ 200, [ Allow => join( ', ' => @{$options} ) ], [] ];  | 
| 
 
 | 
0
 | 
 
 | 
 
 | 
 
 | 
 
 | 
0
 | 
    | 
| 
376
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
377
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
378
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _error_response {  | 
| 
379
 | 
25
 | 
 
 | 
 
 | 
  
25
  
 | 
 
 | 
56
 | 
     my ( $self, $args ) = @_;  | 
| 
380
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
381
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     return $self->_response( $args->[0], [], +{  | 
| 
382
 | 
25
 | 
 
 | 
 
 | 
 
 | 
 
 | 
194
 | 
         jsonapi => { version => $self->{'ponapi.spec_version'} },  | 
| 
383
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         errors  => [ { detail => $args->[1], status => $args->[0] } ],  | 
| 
384
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     });  | 
| 
385
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
386
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
387
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 1;  | 
| 
388
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
389
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 __END__  | 
| 
390
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
391
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =pod  | 
| 
392
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
393
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =encoding UTF-8  | 
| 
394
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
395
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =head1 NAME  | 
| 
396
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
397
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 PONAPI::Server - PONAPI - Perl implementation of {JSON:API} (http://jsonapi.org/) v1.0  | 
| 
398
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
399
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =head1 VERSION  | 
| 
400
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
401
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 version 0.003003  | 
| 
402
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
403
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =head1 SYNOPSIS  | 
| 
404
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
405
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # Run the server  | 
| 
406
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     $ plackup -MPONAPI::Server -e 'PONAPI::Server->new("repository.class" => "Test::PONAPI::Repository::MockDB")->to_app'  | 
| 
407
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
408
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     $ perl -MPONAPI::Client -E 'say Dumper(PONAPI::Client->new->retrieve(type => "people", id => 88))'  | 
| 
409
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
410
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # Or with cURL:  | 
| 
411
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     $ curl -X GET -H "Content-Type: application/vnd.api+json" 'http://0:5000/people/88'  | 
| 
412
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
413
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =head1 DESCRIPTION  | 
| 
414
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
415
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 C<PONAPI::Server> is a small plack server that implements the  | 
| 
416
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 L<{json:api}|http://jsonapi.org/> specification.  | 
| 
417
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
418
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 You'll have to set up a repository (to provide access to the data  | 
| 
419
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 you want to serve) and tweak some server configurations, so  | 
| 
420
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 hop over to L<PONAPI::Manual> for the next steps!  | 
| 
421
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
422
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =head1 BUGS, CONTACT AND SUPPORT  | 
| 
423
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
424
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 For reporting bugs or submitting patches, please use the github  | 
| 
425
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 bug tracker at L<https://github.com/mickeyn/PONAPI>.  | 
| 
426
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
427
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =head1 AUTHORS  | 
| 
428
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
429
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =over 4  | 
| 
430
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
431
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =item *  | 
| 
432
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
433
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 Mickey Nasriachi <mickey@cpan.org>  | 
| 
434
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
435
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =item *  | 
| 
436
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
437
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 Stevan Little <stevan@cpan.org>  | 
| 
438
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
439
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =item *  | 
| 
440
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
441
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 Brian Fraser <hugmeir@cpan.org>  | 
| 
442
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
443
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =back  | 
| 
444
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
445
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =head1 COPYRIGHT AND LICENSE  | 
| 
446
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
447
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 This software is copyright (c) 2019 by Mickey Nasriachi, Stevan Little, Brian Fraser.  | 
| 
448
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
449
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 This is free software; you can redistribute it and/or modify it under  | 
| 
450
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 the same terms as the Perl 5 programming language system itself.  | 
| 
451
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
452
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 =cut  |