File Coverage

blib/lib/YATT/Lite/Connection.pm
Criterion Covered Total %
statement 194 279 69.5
branch 54 98 55.1
condition 19 56 33.9
subroutine 40 56 71.4
pod 0 44 0.0
total 307 533 57.6


line stmt bran cond sub pod time code
1             package YATT::Lite::Connection; sub PROP () {__PACKAGE__}
2 14     14   10980 use strict;
  14         30  
  14         443  
3 14     14   74 use warnings qw(FATAL all NONFATAL misc);
  14         29  
  14         429  
4 14     14   84 use Carp;
  14         32  
  14         720  
5              
6 14     14   108 use Hash::Util qw/lock_keys/;
  14         2644  
  14         127  
7              
8             # XXX: MFields may be ok.
9             use YATT::Lite::MFields
10             (# Incoming request. Should be filled by Dispatcher(Factory)
11 14         123 [cf_env => getter => [env => 'glob']]
12             , qw/cookies_in/
13              
14             # To debug liveness/leakage.
15             , qw/cf_debug/
16              
17             # Outgoing response. Should be written by YATT and *.yatt
18             , qw/cf_parent_fh cf_buffer
19             headers header_was_sent
20             cf_status cf_content_type cf_charset cf_encoding
21             cookies_out/
22              
23             # To suppress HTTP header, set this.
24             , 'cf_noheader'
25              
26             # To distinguish error state.
27             , qw/is_error raised oldbuf/
28              
29             # Session store
30             , qw/session stash debug_stash/
31              
32             # For logging, compatible to psgix.logger (I hope. Not yet used.)
33             , qw/cf_logger/
34              
35             # For poorman's logging logdump() series.
36             , qw/cf_logfh/
37              
38             # Invocation context
39             , qw/cf_system cf_yatt cf_backend cf_dbh/
40              
41             # Raw path_info. should match with env->{PATH_INFO}
42             , qw/cf_path_info/
43              
44             # Location quad and is_index flag
45             , qw/cf_dir cf_location cf_file cf_subpath
46             cf_is_index/
47              
48             # Not used..
49             , qw/cf_root/
50              
51             # User's choice of message language.
52             , qw/cf_lang/
53 14     14   2450 );
  14         34  
54              
55 14     14   150 use YATT::Lite::Util qw(globref lexpand fields_hash incr_opt terse_dump);
  14         39  
  14         950  
56 14     14   1587 use YATT::Lite::PSGIEnv;
  14         46  
  14         118  
57              
58 4872     4872 0 7482 sub prop { *{shift()}{HASH} }
  4872         11164  
59              
60             # # XXX: Experimental. This can slowdown 20%! the code like: print $CON (text);
61             # use overload qw/%{} as_hash
62             # bool as_bool/;
63             # sub as_hash { *{shift()}{HASH} }
64             # sub as_bool { defined $_[0] }
65              
66             #========================================
67             # Constructors
68             #========================================
69              
70             sub create {
71 241     241 0 1147 my ($class, $self) = splice @_, 0, 2;
72 241         1084 require IO::Handle;
73 241         11693 my ($prop, @task) = $class->build_prop(@_);
74 241         996 $class->build_fh_for($prop, $self);
75 241         899 $_->[0]->($self, $_->[1]) for @task;
76 241         3597 $self->after_create;
77 241         1946 $self;
78             }
79              
80       241 0   sub after_create {}
81              
82             sub build_prop {
83 241     241 0 500 my $class = shift;
84 241         758 my $fields = fields_hash($class);
85 241         2827 my PROP $prop = lock_keys(my %prop, keys %$fields);
86 241         53767 my @task;
87 241         1207 while (my ($name, $value) = splice @_, 0, 2) {
88 3386 100       16516 if (my $sub = $class->can("configure_$name")) {
    50          
89 408         1727 push @task, [$sub, $value];
90             } elsif (not exists $fields->{"cf_$name"}) {
91 0         0 confess "No such config item '$name' in class $class";
92             } else {
93 2978         10534 $prop->{"cf_$name"} = $value;
94             }
95             }
96 241 50       1192 wantarray ? ($prop, @task) : $prop;
97             }
98              
99             sub build_fh_for {
100 241     241 0 610 (my $class, my PROP $prop) = splice @_, 0, 2;
101 241 50 0     638 unless (defined $_[0]) {
102 241 50       712 my $enc = $$prop{cf_encoding} ? ":encoding($$prop{cf_encoding})" : '';
103 241   50     1479 $prop->{cf_buffer} //= (\ my $str);
104 241   50     416 ${$prop->{cf_buffer}} //= "";
  241         1376  
105 241 50   2   3497 open $_[0], ">$enc", $prop->{cf_buffer} or die $!;
  2         14  
  2         3  
  2         12  
106             } elsif ($$prop{cf_encoding}) {
107             binmode $_[0], ":encoding($$prop{cf_encoding})";
108             }
109 241         2411 bless $_[0], $class;
110 241         451 *{$_[0]} = $prop;
  241         697  
111 241         487 $_[0];
112             }
113              
114             sub configure_encoding {
115 410     410 0 1107 my PROP $prop = prop(my $glob = shift);
116 410         787 my $enc = shift;
117 410         943 $prop->{cf_encoding} = $enc;
118 5     5   40 binmode $glob, ":encoding($enc)";
  5         12  
  5         33  
  410         3594  
119             }
120              
121             sub get_encoding_layer {
122 0     0 0 0 my PROP $prop = prop(my $glob = shift);
123 0 0       0 $$prop{cf_encoding} ? ":encoding($$prop{cf_encoding})" : '';
124             }
125              
126             #========================================
127              
128             sub cget {
129 37 50   37 0 244 confess "Not enough arguments" if @_ < 2;
130 37 50       153 confess "Too many arguments" if @_ > 3;
131 37         107 my PROP $prop = prop(my $glob = shift);
132 37         97 my ($name, $default) = @_;
133 37         129 my $fields = fields_hash($glob);
134 37 50       148 if (not exists $fields->{"cf_$name"}) {
135 0         0 confess "No such config item '$name' in class " . ref $glob;
136             }
137 37   66     256 $prop->{"cf_$name"} // $default;
138             }
139              
140             sub configure {
141 383     383 0 920 my PROP $prop = prop(my $glob = shift);
142 383         1199 my $fields = fields_hash($glob);
143 383         787 my (@task);
144 383         1453 while (my ($name, $value) = splice @_, 0, 2) {
145 383 50       1017 unless (defined $name) {
146 0         0 croak "Undefined name given for @{[ref($glob)]}->configure(name=>value)!";
  0         0  
147             }
148 383         913 $name =~ s/^-//;
149 383 100       2183 if (my $sub = $glob->can("configure_$name")) {
    50          
150 180         860 push @task, [$sub, $value];
151             } elsif (not exists $fields->{"cf_$name"}) {
152 0         0 confess "No such config item '$name' in class " . ref $glob;
153             } else {
154 203         1024 $prop->{"cf_$name"} = $value;
155             }
156             }
157 383 50       942 if (wantarray) {
158             # To delay configure_zzz.
159 0         0 @task;
160             } else {
161 383         1099 $$_[0]->($glob, $$_[1]) for @task;
162 383         10049 $glob;
163             }
164             }
165              
166             # For debugging aid.
167             sub cf_pairs {
168 0     0 0 0 my PROP $prop = prop(my $glob = shift);
169 0         0 my $fields = fields_hash($glob);
170             map {
171 0         0 [substr($_, 3) => $prop->{$_}]
172 0 0       0 } grep {/^cf_/ && $_ ne 'cf_buffer'} keys %$fields;
  0         0  
173             }
174              
175             #========================================
176              
177             # Just a wrapper for die.
178             sub raise_response {
179 6     6 0 1016 my ($con, $response) = @_;
180 6         51 die $response;
181             }
182              
183             #========================================
184              
185             sub as_error {
186 11     11 0 32 my PROP $prop = prop(my $glob = shift);
187 11         31 $prop->{is_error} = 1;
188 11 50       44 if (my $buf = $prop->{cf_buffer}) {
189 11         33 $prop->{oldbuf} = $$buf;
190 11         40 $glob->rewind;
191             }
192 11 50       32 $glob->configure(@_) if @_;
193 11         46 $glob;
194             }
195              
196             sub error_with_status {
197 5     5 0 43 my ($glob, $code) = splice @_, 0, 2;
198 5         17 $glob->configure(status => $code)
199             ->raise(error => incr_opt(depth => \@_), @_);
200             }
201              
202             sub error {
203             # XXX: as_error?
204 2     2 0 9 shift->raise(error => incr_opt(depth => \@_), @_);
205             }
206              
207             sub raise {
208 9     9 0 940 my PROP $prop = prop(my $glob = shift);
209 9         30 my ($type, @err) = @_; # To keep args visible in backtrace.
210 9         28 $prop->{raised} = $type;
211 9 100       39 if (my $yatt = $prop->{cf_yatt}) {
    50          
212 5         18 $yatt->raise($type, incr_opt(depth => \@err), @err);
213             } elsif (my $system = $prop->{cf_system}) {
214 0         0 $system->raise($type, incr_opt(depth => \@err), @err);
215             } else {
216 4 100 66     22 shift @err if @err and ref $err[0] eq 'HASH'; # drop opts.
217 4         7 my $fmt = shift @err;
218 4         458 croak sprintf($fmt, @err);
219             }
220             }
221              
222             sub error_fh {
223 0     0 0 0 my PROP $prop = prop(my $glob = shift);
224 0 0       0 if (my Env $env = $prop->{cf_env}) {
    0          
225 0         0 $env->{'psgi.errors'}
226             } elsif (fileno(STDERR)) {
227 0         0 \*STDERR;
228             } else {
229 0         0 undef;
230             }
231             }
232              
233             # Simple level-less but tagged and serialized logging.
234             sub logdump {
235 4     4 0 18 my $self = shift;
236 4         19 $self->logemit($_[0], terse_dump(@_[1..$#_]));
237             }
238              
239             sub logbacktrace {
240 0     0 0 0 my $self = shift;
241 0         0 $self->logemit($_[0], terse_dump(@_[1..$#_]), Carp::longmess());
242             }
243              
244             sub logemit {
245 4     4 0 108 my PROP $prop = prop(my $glob = shift);
246 4   33     14 my $fh = $prop->{cf_logfh} || $glob->error_fh;
247 4         8 my $logger = $prop->{cf_logger};
248 4 50 33     12 return unless $fh || $logger;
249 4         6 my $tag = do {
250 4 100       20 unless (defined $_[0]) {
    100          
    100          
251 1         2 shift;
252 1         2 'undef'
253 0         0 } elsif (ref $_[0]) {
254 1         3 unshift @_, terse_dump(shift @_);
255 1         59 'debug';
256 0         0 } elsif ($_[0] =~ /^[\w\.\-]+$/) {
257 1         3 shift;
258             } else {
259 1         3 'debug';
260             }
261             };
262 4         10 my $msg = join(" ", map {(my $cp = $_) =~ s/\n/\n /g; $cp} @_);
  6         14  
  6         16  
263 4 50       10 if ($fh) {
264 4         15 print $fh uc($tag).": [", $glob->iso8601_datetime(), " #$$] $msg\n";
265             } else {
266 0         0 my ($level, $type) = $tag =~ /^(\w+)(?:\.([\w\-\.]+))?$/;
267 0   0     0 $logger->({level => $level || 'debug', message => $msg});
268             }
269             }
270              
271             # XXX: precise?
272             sub iso8601_datetime {
273 4     4 0 9 my ($glob, $time) = @_;
274 4   33     230 my ($S, $M, $H, $d, $m, $y) = localtime($time // time);
275 4         13 $y += 1900; $m++;
  4         9  
276 4         44 sprintf '%04d-%02d-%02dT%02d:%02d:%02d', ($y, $m, $d, $H, $M, $S);
277             }
278              
279             # Alternative, for more rich logging.
280             sub logger {
281 0     0 0 0 my PROP $prop = prop(my $glob = shift);
282 0         0 $prop->{cf_logger};
283             }
284              
285             #========================================
286              
287             DESTROY {
288             # Note: localizing $@ in DESTROY is not so good idea in general.
289             # But I do this here because I found some module stamps $@ in mkheader.
290             # Anyway, in usual case, $con lives along with entire request processing,
291             # so this may not be a problem.
292 241     241   23866 local $@;
293              
294 241         643 my PROP $prop = prop(my $glob = shift);
295 241         773 $glob->flush_headers;
296 241 100       810 if (my $backend = delete $prop->{cf_backend}) {
297 1 50 33     6 if ($prop->{cf_debug} and my $errfh = $glob->error_fh) {
298 0         0 print $errfh "DEBUG: Connection->backend is detached($backend)\n";
299             }
300             # DBSchema->DESTROY should be called automatically. <- Have tests for this!
301             #$backend->disconnect("Explicitly from Connection->DESTROY");
302             }
303 241 50 33     842 if ($prop->{cf_debug} and my $errfh = $glob->error_fh) {
304 0         0 print $errfh "DEBUG: Connection->DESTROY (glob=$glob, prop=$prop)\n";
305             }
306 241         7421 delete $prop->{$_} for keys %$prop;
307             #undef *$glob;
308             }
309              
310             sub header_was_sent {
311 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
312 0         0 $prop->{header_was_sent} = 1;
313 0         0 my $parent = $prop->{cf_parent_fh};
314 0 0 0     0 if ($parent and my $sub = $parent->can('header_was_sent')) {
315 0         0 $sub->($parent);
316             }
317             }
318              
319             sub flush_headers {
320 623     623 0 1700 my PROP $prop = (my $glob = shift)->prop;
321              
322 623 100       2256 return if $prop->{header_was_sent}++;
323              
324 238         841 $glob->finalize_headers;
325              
326 238 100       696 if (not $prop->{cf_noheader}) {
327 27   66     110 my $fh = $prop->{cf_parent_fh} // $glob;
328 27         98 print $fh $glob->mkheader;
329             }
330 238         2003 $glob->flush;
331             }
332              
333             sub finalize_headers {
334 241     241 0 620 my PROP $prop = (my $glob = shift)->prop;
335 241 100       1482 $prop->{cf_yatt}->finalize_connection($glob) if $prop->{cf_yatt};
336 241 100       1415 $prop->{cf_system}->finalize_connection($glob) if $prop->{cf_system};
337 241 50       686 $glob->finalize_cookies if $prop->{cookies_out};
338             }
339              
340             sub flush {
341 241     241 0 650 my PROP $prop = (my $glob = shift)->prop;
342 241         3777 $glob->IO::Handle::flush();
343 241 100       1262 if ($prop->{cf_parent_fh}) {
344 14         31 print {$prop->{cf_parent_fh}} ${$prop->{cf_buffer}};
  14         36  
  14         46  
345 14         34 ${$prop->{cf_buffer}} = '';
  14         40  
346 14         87 $prop->{cf_parent_fh}->IO::Handle::flush();
347             # XXX: flush 後は、 parent_fh の dup にするべき。
348             # XXX: でも、 multipart (server push) とか continue とかは?
349             }
350             }
351              
352             sub rewind {
353 11     11 0 32 my PROP $prop = (my $glob = shift)->prop;
354 11         57 seek *$glob, 0, 0;
355 11         23 ${$prop->{cf_buffer}} = '';
  11         26  
356 11         24 $glob;
357             }
358              
359             #========================================
360             # (Possibly obsoleted) Cookie support, based on CGI::Cookie (works under PSGI mode too)
361              
362             sub cookies_in {
363 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
364 0         0 my Env $env = $prop->{cf_env};
365 0   0     0 $prop->{cookies_in} ||= do {
366 0 0       0 if (defined $env->{HTTP_COOKIE}) {
367 0         0 require CGI::Cookie;
368 0         0 CGI::Cookie->parse($env->{HTTP_COOKIE});
369             } else {
370 0         0 +{};
371             }
372             };
373             }
374              
375             sub set_cookie {
376 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
377 0 0 0     0 if (@_ == 1 and ref $_[0]) {
378 0         0 my $cookie = shift;
379 0         0 my $name = $cookie->name;
380 0         0 $prop->{cookies_out}{$name} = $cookie;
381             } else {
382 0         0 my $name = shift;
383 0         0 $prop->{cookies_out}{$name} = $glob->new_cookie($name, @_);
384             }
385             }
386              
387             sub new_cookie {
388 0     0 0 0 my $glob = shift; # not used.
389 0         0 my ($name, $value) = splice @_, 0, 2;
390 0         0 require CGI::Cookie;
391 0         0 CGI::Cookie->new(-name => $name, -value => $value, @_);
392             }
393              
394             sub finalize_cookies {
395 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
396 0 0       0 return unless $prop->{cookies_out};
397 0         0 $prop->{headers}{'Set-Cookie'} = [map {$_->as_string}
398 0         0 values %{$prop->{cookies_out}}];
  0         0  
399             }
400             #========================================
401              
402             # XXX: Should be renamed to result, text, as_text, as_string or value;
403             sub buffer {
404 195     195 0 500 my PROP $prop = prop(my $glob = shift);
405 195         793 $glob->IO::Handle::flush();
406 195         405 ${$prop->{cf_buffer}}
  195         993  
407             }
408              
409             sub mkheader {
410 27     27 0 66 my PROP $prop = (my $glob = shift)->prop;
411 27   33     183 my ($code) = shift // $prop->{cf_status} // 200;
      50        
412 27         187 require HTTP::Headers;
413             my $headers = HTTP::Headers->new("Content-type", $glob->_mk_content_type
414             , map($_ ? %$_ : (), $prop->{headers})
415 27 100       95 , @_);
416 27         1964 YATT::Lite::Util::mk_http_status($code)
417             . $headers->as_string . "\015\012";
418             }
419              
420             sub _mk_content_type {
421 27     27   66 my PROP $prop = (my $glob = shift)->prop;
422 27   50     124 my $ct = $prop->{cf_content_type} || "text/html";
423 27 50 33     202 if ($ct =~ m{^text/} && $ct !~ /;\s*charset/) {
424 27   100     111 my $cs = $prop->{cf_charset} || "utf-8";
425 27         101 $ct .= qq|; charset=$cs|;
426             }
427 27         277 $ct;
428             }
429              
430             sub set_header {
431 4     4 0 11 my PROP $prop = prop(my $glob = shift);
432 4         6 my ($key, $value) = @_;
433 4         10 $prop->{headers}{$key} = $value;
434 4         8 $glob;
435             }
436              
437             sub set_header_list {
438 0     0 0 0 my PROP $prop = prop(my $glob = shift);
439 0         0 while (my ($k, $v) = splice @_, 0, 2) {
440 0         0 $prop->{headers}{$k} = $v;
441             }
442 0         0 $glob;
443             }
444              
445             sub append_header {
446 0     0 0 0 my PROP $prop = prop(my $glob = shift);
447 0         0 my ($key, @values) = @_;
448 0         0 push @{$prop->{headers}{$key}}, @values;
  0         0  
449             }
450              
451             # For PSGI only.
452             sub list_header {
453 147     147 0 420 my PROP $prop = prop(my $glob = shift);
454             my $headers = $prop->{headers}
455 147 100       842 or return;
456             map {
457 2         7 my $k = $_;
  4         6  
458 4         14 map {$k => $_} lexpand($headers->{$k});
  4         24  
459             } keys %$headers;
460             }
461              
462             sub content_type {
463 0     0 0 0 my PROP $prop = prop(my $glob = shift);
464             $prop->{cf_content_type}
465 0         0 }
466             sub set_content_type {
467 0     0 0 0 my PROP $prop = prop(my $glob = shift);
468 0         0 $prop->{cf_content_type} = shift;
469 0         0 $glob;
470             }
471              
472             sub charset {
473 0     0 0 0 my PROP $prop = prop(my $glob = shift);
474             $prop->{cf_charset}
475 0         0 }
476             sub set_charset {
477 178     178 0 638 my PROP $prop = prop(my $glob = shift);
478 178         482 $prop->{cf_charset} = shift;
479 178         496 $glob;
480             }
481              
482             #========================================
483              
484             sub stash {
485 0     0 0 0 my PROP $prop = prop(my $glob = shift);
486 0 0       0 unless (@_) {
    0          
487             $prop->{stash} //= {}
488 0   0     0 } elsif (@_ == 1) {
  0         0  
489 0         0 $prop->{stash}{$_[0]}
490             } else {
491 0         0 my $name = shift;
492 0         0 $prop->{stash}{$name} = shift;
493 0         0 $glob;
494             }
495             }
496              
497             #========================================
498              
499             sub gettext {
500 3     3 0 36 my PROP $prop = (my $glob = shift)->prop;
501 3         19 $prop->{cf_yatt}->lang_gettext($prop->{cf_lang}, @_);
502             }
503              
504             sub ngettext {
505 3     3 0 21 my PROP $prop = (my $glob = shift)->prop;
506 3         14 $prop->{cf_yatt}->lang_ngettext($prop->{cf_lang}, @_);
507             }
508              
509             #========================================
510              
511             sub backend {
512 1     1 0 7 my PROP $prop = (my $glob = shift)->prop;
513              
514             # XXX: Exposing bare backend may harm.
515             # But anyway, you can get backend via cget('backend').
516             #
517 1 50       4 return $prop->{cf_backend} unless @_;
518              
519 1         3 my $method = shift;
520 1 50       10 unless (defined $method) {
    50          
    50          
521 0         0 $glob->error("backend: null method is called");
522 0         0 } elsif (not $prop->{cf_backend}) {
523 0         0 $glob->error("backend is empty");
524 0         0 } elsif (not my $sub = $prop->{cf_backend}->can($method)) {
525 0         0 $glob->error("unknown method called for backend: %s", $method);
526             } else {
527 1         5 $sub->($prop->{cf_backend}, @_);
528             }
529             }
530              
531             {
532             foreach (qw/model resultset
533             txn_do txn_begin txn_commit txn_rollback
534             txn_scope_guard
535             /) {
536             my $method = $_;
537             *{globref(__PACKAGE__, $method)} = sub {
538 1     1   4 my PROP $prop = (my $glob = shift)->prop;
539 1         6 $prop->{cf_backend}->$method(@_);
540             };
541             }
542             }
543              
544             1;