File Coverage

blib/lib/Plack/App/unAPI.pm
Criterion Covered Total %
statement 97 98 98.9
branch 25 28 89.2
condition 16 21 76.1
subroutine 19 19 100.0
pod 4 6 66.6
total 161 172 93.6


line stmt bran cond sub pod time code
1 4     4   307489 use strict;
  4         11  
  4         252  
2             package Plack::App::unAPI;
3             #ABSTRACT: Serve via unAPI
4             our $VERSION = '0.61'; #VERSION
5              
6 4     4   65 use v5.10.1;
  4         13  
  4         206  
7              
8 4     4   22 use parent 'Plack::Component';
  4         11  
  4         126  
9 4     4   58159 use Plack::Util::Accessor qw(formats);
  4         1006  
  4         26  
10              
11 4     4   3799 use Plack::Request;
  4         207074  
  4         162  
12 4     4   44 use Carp;
  4         10  
  4         4452  
13              
14             sub prepare_app {
15 6     6 1 41694 my ($self) = @_;
16 6         35 $self->_trigger_formats( $self->formats );
17             }
18              
19             sub call {
20 23     23 1 133402 my ($self, $env) = @_;
21              
22 23         178 my $req = Plack::Request->new($env);
23 23   100     260 my $format = $req->param('format') // '';
24 23   100     10247 my $id = $req->param('id') // '';
25            
26             # TODO: here we could first lookup the resource at the server
27             # and sent 404 if no known format was specified
28             # return 404 unless $self->available_formats($id);
29            
30 23 100 100     440 return $self->formats_as_psgi($id)
31             if $format eq '' or $format eq '_';
32              
33 16         74 my $route = $self->formats->{$format};
34 16 100 66     191 if ( !$route || !$route->{app} ) {
35 1         7 my $res = $self->formats_as_psgi($id);
36 1         3 $res->[0] = 406; # Not Acceptable
37 1         19 return $res;
38             }
39              
40 15 100 66     78 return $self->formats_as_psgi('')
      100        
41             if $id eq '' and !($route->{always} // $self->formats->{_}->{always});
42              
43 13         27 my $res = eval { $route->{app}->($env) };
  13         58  
44 13         241 my $error = $@;
45              
46 13 100       54 if ($error) {
    100          
47 1         4 $error = "Internal crash with format=$format and id=$id: $error";
48             } elsif (!_is_psgi_response($res)) {
49             # we may also check response type...
50 1         3 $error = "No PSGI response for format=$format and id=$id";
51             }
52              
53 13 100       39 if ($error) { # TODO: catch only on request
54 2         30 return [ 500, [ 'Content-Type' => 'text/plain' ], [ $error ] ];
55             }
56              
57 11         93 $res;
58             }
59              
60             # can return a subset of formats for some identifiers in a subclass
61             sub available_formats {
62 10     10 0 22 my ($self, $id) = @_;
63 10         17 return keys %{$self->formats};
  10         33  
64             }
65              
66             sub formats_as_psgi {
67 10     10 0 45 my ($self, $id) = @_;
68              
69 10         34 my $formats = $self->formats;
70              
71 10         52 my $status = 300; # Multiple Choices
72 10         21 my $type = 'application/xml; charset: utf-8';
73 10         25 my @xml = '';
74              
75 10 100       119 push @xml, _xmltag(' ($id eq '' ? undef : $id ) ).">";
76              
77 10         101 foreach my $name (sort $self->available_formats($id)) {
78 52 100       221 next if $name eq '_';
79 42         149 push @xml, _xmltag('
80             name => $name,
81             type => $formats->{$name}->{type},
82             docs => $formats->{$name}->{docs})." />";
83             }
84              
85 10         27 push @xml, '';
86              
87 10         140 return [ $status, [ 'Content-Type' => $type ], [ join "\n", @xml] ];
88             }
89              
90             # TODO: better force lookup functions to return full/partial PSGI???
91             sub _lookup2psgi {
92 3     3   7 my ($method, $type) = @_;
93             # TODO: error response in corresponding content type and more headers
94             sub {
95 5     5   29 my ($env) = @_;
96 5   50     47 my $id = Plack::Request->new($env)->param('id') // '';
97 5         117 my $content = $method->( $id, $env );
98 5 100       61 return defined $content
99             ? [ 200, [ 'Content-Type' => $type ], [ $content ] ]
100             : [ 404, [ 'Content-Type' => 'text/plain' ], [ 'not found' ] ];
101 3         17 };
102             }
103              
104             # convert [ $app => $type, %about ] to { type => $type, %about }
105             # convert [ $type, %about ] to { type => $type, %about }
106             sub _trigger_formats { # TODO: make Plack::App::unAPI::Format
107 6     6   330 my ($self, $formats) = @_;
108              
109 6         23 $self->{formats} = { };
110              
111 6         28 foreach my $name (grep { $_ ne '_' } keys %$formats) {
  34         56  
112 34         54 my $spec = $formats->{$name};
113 34 50       88 if (ref $spec eq 'ARRAY') {
114            
115 34 100       101 my ($app, $type, %about) = @$spec % 2 ? (undef,@$spec) : @$spec;
116 34 50       71 croak "unAPI format required MIME type" unless $type;
117              
118 34 100       157 if (!$app) {
119 2         3 my $lookup = do {
120 2         6 my $method = "format_$name";
121 2 50       20 if (!$self->can($method)) {
122 0         0 croak __PACKAGE__." must implement method $method";
123             }
124 2     3   11 sub { $self->$method(@_); };
  3         15  
125             };
126              
127 2         6 $app = _lookup2psgi( $lookup, $type );
128             }
129              
130 34         178 $self->{formats}->{$name} = { type => $type, %about, app => $app };
131             } # TODO: keep { ... }
132             }
133              
134 6         34 $self->{formats}->{_} = $formats->{_};
135             }
136              
137              
138             ###### FUNCTIONS
139              
140 4     4   32 use parent 'Exporter';
  4         9  
  4         58  
141             our @EXPORT = qw(unAPI wrAPI);
142              
143             ## no critic
144             sub unAPI(@) {
145 4     4 1 1292 Plack::App::unAPI->new( formats => { @_ } )->to_app;
146             }
147             ## use critic
148              
149             sub wrAPI {
150 1     1 1 21 my ($code, $type, %about) = @_;
151              
152 1         5 my $app = _lookup2psgi( $code, $type );
153              
154 1         8 return [ $app => $type, %about ];
155             }
156              
157             ###### Utility
158              
159             # checks whether PSGI conforms to PSGI specification
160             sub _is_psgi_response {
161 12     12   23 my $res = shift;
162 12   33     375 return (ref($res) and ref($res) eq 'ARRAY' and
163             (@$res == 3 or @$res == 2) and
164             $res->[0] =~ /^\d+$/ and $res->[0] >= 100 and
165             ref $res->[1] and ref $res->[1] eq 'ARRAY');
166             }
167              
168             sub _xmltag {
169 52     52   64 my $name = shift;
170 52         158 my %attr = @_;
171              
172 95         128 return $name . join '', map {
173 136         217 my $val = $attr{$_};
174 95         162 $val =~ s/\&/\&\;/g;
175 95         108 $val =~ s/\
176 95         111 $val =~ s/"/\"\;/g;
177 95         399 " $_=\"$val\"";
178 272         254 } grep { defined $attr{$_} }
179 52         82 grep { state $n=0; ++$n % 2; } @_;
  272         339  
180             }
181              
182             1;
183              
184             __END__