File Coverage

blib/lib/Tatsumaki/Handler.pm
Criterion Covered Total %
statement 71 155 45.8
branch 7 38 18.4
condition 5 15 33.3
subroutine 23 41 56.1
pod 0 21 0.0
total 106 270 39.2


line stmt bran cond sub pod time code
1             package Tatsumaki::Handler;
2 4     4   1869 use strict;
  4         8  
  4         149  
3 4     4   4047 use AnyEvent;
  4         19238  
  4         99  
4 4     4   30 use Carp ();
  4         8  
  4         57  
5 4     4   6031 use Encode ();
  4         48835  
  4         101  
6 4     4   1494 use Any::Moose;
  4         77343  
  4         32  
7 4     4   6461 use MIME::Base64 ();
  4         3425  
  4         125  
8 4     4   5095 use JSON;
  4         72353  
  4         22  
9 4     4   4273 use Try::Tiny;
  4         6242  
  4         245  
10 4     4   1892 use Tatsumaki::Error;
  4         46  
  4         8353  
11              
12             has application => (is => 'rw', isa => 'Tatsumaki::Application');
13             has condvar => (is => 'rw', isa => 'AnyEvent::CondVar');
14             has request => (is => 'rw', isa => 'Tatsumaki::Request');
15             has response => (is => 'rw', isa => 'Tatsumaki::Response', lazy_build => 1);
16             has args => (is => 'rw', isa => 'ArrayRef');
17             has writer => (is => 'rw');
18             has mxhr => (is => 'rw', isa => 'Bool');
19             has mxhr_boundary => (is => 'rw', isa => 'Str', lazy => 1, lazy_build => 1);
20             has json => (is => 'rw', isa => 'JSON', lazy => 1, default => sub { JSON->new->utf8 });
21             has binary => (is => 'rw', isa => 'Bool');
22              
23             has _write_buffer => (is => 'rw', isa => 'ArrayRef', lazy => 1, default => sub { [] });
24              
25 0     0 0 0 sub head { shift->get(@_) }
26 0     0 0 0 sub get { Tatsumaki::Error::HTTP->throw(405) }
27 2     2 0 28 sub post { Tatsumaki::Error::HTTP->throw(405) }
28 0     0 0 0 sub put { Tatsumaki::Error::HTTP->throw(405) }
29 0     0 0 0 sub delete { Tatsumaki::Error::HTTP->throw(405) }
30              
31 4     4 0 7 sub prepare { }
32              
33             my $class_attr = {};
34              
35             sub is_asynchronous {
36 6   33 6 0 29 my $class = ref $_[0] || $_[0];
37 6         49 return $class_attr->{$class}{is_asynchronous};
38             }
39              
40             sub asynchronous {
41 2     2 0 23 my $class = shift;
42 2         10 $class_attr->{$class}{is_asynchronous} = shift;
43             }
44              
45 0     0 0 0 sub nonblocking { shift->asynchronous(@_) } # alias
46              
47             sub multipart_xhr_push {
48 0     0 0 0 my $self = shift;
49 0 0       0 if ($_[0]) {
50 0 0       0 Carp::croak("asynchronous should be set to do multipart XHR push")
51             unless $self->is_asynchronous;
52 0         0 $self->response->content_type('multipart/mixed; boundary="' . $self->mxhr_boundary . '"');
53              
54             # HACK: Always write a boundary for the next event, so client JS can fire the event immediately
55             # Maybe DUI.Stream should respect the Content-Length header to look at the endFlag
56 0         0 $self->stream_write("--" . $self->mxhr_boundary. "\n");
57              
58 0         0 return $self->mxhr(1);
59             } else {
60 0         0 return $self->mxhr;
61             }
62             }
63              
64             sub _build_mxhr_boundary {
65 0     0   0 my $size = 2;
66 0         0 my $b = MIME::Base64::encode(join("", map chr(rand(256)), 1..$size*3), "");
67 0         0 $b =~ s/[\W]/X/g; # ensure alnum only
68 0         0 $b;
69             }
70              
71             sub _build_response {
72 2     2   7 my $self = shift;
73 2         33 $self->request->new_response(200, [ 'Content-Type' => 'text/html; charset=utf-8' ]);
74             }
75              
76             my $supported;
77              
78             sub supported_method {
79 4     4 0 11 my($self, $method) = @_;
80 4   100     37 $supported ||= +{ map { $_ => 1 } qw( head get post put delete ) };
  10         63  
81 4         21 return $supported->{$method};
82             }
83              
84             sub async_cb {
85 0     0 0 0 my $self = shift;
86 0         0 my $cb = shift;
87             return sub {
88 0     0   0 my @args = @_;
89 0         0 try { $cb->(@args) }
90 0         0 catch { $self->condvar->croak($_) };
  0         0  
91 0         0 };
92             }
93              
94             sub run {
95 4     4 0 10 my $self = shift;
96              
97 4         68 my $method = lc $self->request->method;
98 4 50       57 unless ($self->supported_method($method)) {
99 0         0 Tatsumaki::Error::HTTP->throw(400);
100             }
101              
102             my $catch = sub {
103 2 50   2   29 if ($_->isa('Tatsumaki::Error::HTTP')) {
104 2         747 return [ $_->code, [ 'Content-Type' => 'text/plain' ], [ $_->message ] ];
105             } else {
106 0         0 $self->log($_);
107 0         0 return [ 500, [ 'Content-Type' => 'text/plain' ], [ "Internal Server Error" ] ];
108             }
109 4         30 };
110              
111 4 50       24 if ($self->is_asynchronous) {
112 0         0 $self->condvar(my $cv = AE::cv);
113             return sub {
114 0     0   0 my $start_response = shift;
115             $cv->cb(sub {
116 0         0 my $cv = shift;
117             try {
118 0         0 my $res = $cv->recv;
119 0         0 my $w = $start_response->($res);
120 0 0 0     0 if (!$res->[2] && $w) {
121 0         0 $self->writer($w);
122 0         0 $self->condvar(my $cv2 = AE::cv);
123             }
124             } catch {
125 0         0 $start_response->($catch->());
126 0         0 };
127 0         0 });
128              
129             try {
130 0         0 $self->prepare;
131 0         0 $self->$method(@{$self->args});
  0         0  
132             } catch {
133 0         0 $cv->croak($_);
134 0         0 };
135              
136 0 0       0 unless ($self->request->env->{'psgi.nonblocking'}) {
137 0         0 $self->log("Running an async handler in a blocking server. MQ based app should cause a deadlock.\n");
138 0         0 $self->condvar->recv for 1..2; # response and writer
139             }
140 0         0 };
141             } else {
142             my $res = try {
143 4     4   160 $self->prepare;
144 4         7 $self->$method(@{$self->args});
  4         80  
145 2         84 $self->flush;
146 2         40 return $self->response->finalize;
147             } catch {
148 2     2   28 return $catch->();
149 4         42 };
150              
151 4         394 return $res;
152             }
153             }
154              
155             sub log {
156 0     0 0 0 my($self, @stuff) = @_;
157 0         0 $self->request->env->{'psgi.errors'}->print(join '', @stuff);
158             }
159              
160             sub get_writer {
161 0     0 0 0 my $self = shift;
162 0 0       0 $self->flush unless $self->writer;
163 0         0 return $self->writer;
164             }
165              
166             sub get_chunk {
167 2     2 0 7 my $self = shift;
168 2 50       29 if (ref $_[0]) {
    50          
169 0 0       0 if ($self->mxhr) {
170 0         0 my $json = $self->json->encode($_[0]);
171 0         0 return "Content-Type: application/json\n\n$json\n--" . $self->mxhr_boundary. "\n";
172             } else {
173 0         0 $self->response->content_type('application/json');
174 0         0 return $self->json->encode($_[0]);
175             }
176             } elsif ($self->binary) {
177 0         0 join '', @_;
178             } else {
179 2         15 join '', map Encode::encode_utf8($_), @_;
180             }
181             }
182              
183             sub _write {
184 0     0   0 my $self = shift;
185 0         0 my @buf = @_;
186             try {
187 0     0   0 $self->get_writer->write(@buf);
188             } catch {
189 0 0   0   0 /Broken pipe/ and Tatsumaki::Error::ClientDisconnect->throw;
190 0         0 die $_;
191             }
192 0         0 }
193              
194             sub stream_write {
195 0     0 0 0 my $self = shift;
196 0         0 $self->_write($self->get_chunk(@_));
197             }
198              
199             sub write {
200 2     2 0 21 my $self = shift;
201 2         7 push @{$self->_write_buffer}, $self->get_chunk(@_);
  2         23  
202             }
203              
204             sub flush {
205 2     2 0 6 my $self = shift;
206 2         4 my($is_final) = @_;
207              
208 2 50 33     34 if ($self->writer) {
    50          
209 0 0       0 $self->_write(join '', @{$self->_write_buffer}) if @{$self->_write_buffer};
  0         0  
  0         0  
210 0         0 $self->_write_buffer([]);
211             } elsif (!$self->is_asynchronous || $is_final) {
212 2   50     33 my $body = $self->response->body || [];
213 2         265 push @$body, @{$self->_write_buffer};
  2         11  
214 2         10 $self->_write_buffer([]);
215 2         12 $self->response->body($body);
216             } else {
217 0           my $res = $self->response->finalize;
218 0           delete $res->[2]; # gimme a writer
219 0           $self->condvar->send($res);
220 0 0         $self->writer or Carp::croak("Can't get a writer object back: you need servers with psgi.streaming");
221 0           $self->flush();
222             }
223             }
224              
225             sub finish {
226 0     0 0   my($self, $chunk) = @_;
227 0 0         $self->write($chunk) if defined $chunk;
228 0           $self->flush(1);
229 0 0         if ($self->writer) {
    0          
230 0           $self->writer->close;
231 0           $self->condvar->send;
232             } elsif ($self->condvar) {
233 0           $self->condvar->send($self->response->finalize);
234             }
235             }
236              
237             sub render {
238 0     0 0   my($self, $file, $args) = @_;
239 0   0       $args ||= {};
240 0           $self->finish($self->application->render_file($file, { %$args, handler => $self })->as_string);
241             }
242              
243 4     4   38 no Any::Moose;
  4         8  
  4         42  
244             __PACKAGE__->meta->make_immutable;
245              
246             1;
247             __END__