File Coverage

blib/lib/YATT/Lite/Connection.pm
Criterion Covered Total %
statement 174 270 64.4
branch 51 100 51.0
condition 19 53 35.8
subroutine 35 54 64.8
pod 0 43 0.0
total 279 520 53.6


line stmt bran cond sub pod time code
1             package YATT::Lite::Connection; sub PROP () {__PACKAGE__}
2 9     9   12456 use strict;
  9         23  
  9         292  
3 9     9   45 use warnings qw(FATAL all NONFATAL misc);
  9         16  
  9         327  
4 9     9   43 use Carp;
  9         23  
  9         538  
5              
6 9     9   85 use Hash::Util qw/lock_keys/;
  9         3682  
  9         70  
7              
8             # XXX: MFields may be ok.
9             use YATT::Lite::MFields
10             (# Incoming request. Should be filled by Dispatcher(Factory)
11 9         108 [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 9     9   2287 );
  9         19  
54              
55 9     9   59 use YATT::Lite::Util qw(globref lexpand fields_hash incr_opt terse_dump);
  9         15  
  9         629  
56 9     9   2196 use YATT::Lite::PSGIEnv;
  9         17  
  9         69  
57              
58 1220     1220 0 1483 sub prop { *{shift()}{HASH} }
  1220         2733  
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 69     69 0 557 my ($class, $self) = splice @_, 0, 2;
72 69         423 require IO::Handle;
73 69         15905 my ($prop, @task) = $class->build_prop(@_);
74 69         299 $class->build_fh_for($prop, $self);
75 69         276 $_->[0]->($self, $_->[1]) for @task;
76 69         891 $self->after_create;
77 69         552 $self;
78             }
79              
80       69 0   sub after_create {}
81              
82             sub build_prop {
83 69     69 0 115 my $class = shift;
84 69         219 my $fields = fields_hash($class);
85 69         773 my PROP $prop = lock_keys(my %prop, keys %$fields);
86 69         14615 my @task;
87 69         324 while (my ($name, $value) = splice @_, 0, 2) {
88 807 100       5437 if (my $sub = $class->can("configure_$name")) {
    50          
89 104         472 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 703         2984 $prop->{"cf_$name"} = $value;
94             }
95             }
96 69 50       296 wantarray ? ($prop, @task) : $prop;
97             }
98              
99             sub build_fh_for {
100 69     69 0 155 (my $class, my PROP $prop) = splice @_, 0, 2;
101 69 50       189 unless (defined $_[0]) {
    0          
102 69 50       197 my $enc = $$prop{cf_encoding} ? ":encoding($$prop{cf_encoding})" : '';
103 69   50     343 $prop->{cf_buffer} //= (\ my $str);
104 69   50     107 ${$prop->{cf_buffer}} //= "";
  69         306  
105 69 50       1111 open $_[0], ">$enc", $prop->{cf_buffer} or die $!;
106             } elsif ($$prop{cf_encoding}) {
107 0         0 binmode $_[0], ":encoding($$prop{cf_encoding})";
108             }
109 69         1318 bless $_[0], $class;
110 69         106 *{$_[0]} = $prop;
  69         205  
111 69         172 $_[0];
112             }
113              
114             sub configure_encoding {
115 106     106 0 256 my PROP $prop = prop(my $glob = shift);
116 106         187 my $enc = shift;
117 106         221 $prop->{cf_encoding} = $enc;
118 106     6   1000 binmode $glob, ":encoding($enc)";
  6         33  
  6         11  
  6         41  
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 21 50   21 0 181 confess "Not enough arguments" if @_ < 2;
130 21 50       75 confess "Too many arguments" if @_ > 3;
131 21         69 my PROP $prop = prop(my $glob = shift);
132 21         49 my ($name, $default) = @_;
133 21         74 my $fields = fields_hash($glob);
134 21 50       86 if (not exists $fields->{"cf_$name"}) {
135 0         0 confess "No such config item '$name' in class " . ref $glob;
136             }
137 21   66     153 $prop->{"cf_$name"} // $default;
138             }
139              
140             sub configure {
141 108     108 0 246 my PROP $prop = prop(my $glob = shift);
142 108         341 my $fields = fields_hash($glob);
143 108         180 my (@task);
144 108         406 while (my ($name, $value) = splice @_, 0, 2) {
145 108 50       246 unless (defined $name) {
146 0         0 croak "Undefined name given for @{[ref($glob)]}->configure(name=>value)!";
  0         0  
147             }
148 108         180 $name =~ s/^-//;
149 108 100       681 if (my $sub = $glob->can("configure_$name")) {
    50          
150 47         250 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 61         320 $prop->{"cf_$name"} = $value;
155             }
156             }
157 108 50       253 if (wantarray) {
158             # To delay configure_zzz.
159 0         0 @task;
160             } else {
161 108         312 $$_[0]->($glob, $$_[1]) for @task;
162 108         14592 $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             sub as_error {
178 0     0 0 0 my PROP $prop = prop(my $glob = shift);
179 0         0 $prop->{is_error} = 1;
180 0 0       0 if (my $buf = $prop->{cf_buffer}) {
181 0         0 $prop->{oldbuf} = $$buf;
182 0         0 $glob->rewind;
183             }
184 0 0       0 $glob->configure(@_) if @_;
185 0         0 $glob;
186             }
187              
188             sub error_with_status {
189 0     0 0 0 my ($glob, $code) = splice @_, 0, 2;
190 0         0 $glob->configure(status => $code)
191             ->raise(error => incr_opt(depth => \@_), @_);
192             }
193              
194             sub error {
195             # XXX: as_error?
196 2     2 0 14 shift->raise(error => incr_opt(depth => \@_), @_);
197             }
198              
199             sub raise {
200 4     4 0 908 my PROP $prop = prop(my $glob = shift);
201 4         14 my ($type, @err) = @_; # To keep args visible in backtrace.
202 4         10 $prop->{raised} = $type;
203 4 50       18 if (my $yatt = $prop->{cf_yatt}) {
    50          
204 0         0 $yatt->raise($type, incr_opt(depth => \@err), @err);
205             } elsif (my $system = $prop->{cf_system}) {
206 0         0 $system->raise($type, incr_opt(depth => \@err), @err);
207             } else {
208 4 100 66     25 shift @err if @err and ref $err[0] eq 'HASH'; # drop opts.
209 4         7 my $fmt = shift @err;
210 4         665 croak sprintf($fmt, @err);
211             }
212             }
213              
214             sub error_fh {
215 0     0 0 0 my PROP $prop = prop(my $glob = shift);
216 0 0       0 if (my Env $env = $prop->{cf_env}) {
    0          
217 0         0 $env->{'psgi.errors'}
218             } elsif (fileno(STDERR)) {
219 0         0 \*STDERR;
220             } else {
221 0         0 undef;
222             }
223             }
224              
225             # Simple level-less but tagged and serialized logging.
226             sub logdump {
227 4     4 0 20 my $self = shift;
228 4         25 $self->logemit($_[0], terse_dump(@_[1..$#_]));
229             }
230              
231             sub logbacktrace {
232 0     0 0 0 my $self = shift;
233 0         0 $self->logemit($_[0], terse_dump(@_[1..$#_]), Carp::longmess());
234             }
235              
236             sub logemit {
237 4     4 0 119 my PROP $prop = prop(my $glob = shift);
238 4   33     15 my $fh = $prop->{cf_logfh} || $glob->error_fh;
239 4         6 my $logger = $prop->{cf_logger};
240 4 50 33     16 return unless $fh || $logger;
241 4         5 my $tag = do {
242 4 100       24 unless (defined $_[0]) {
    100          
    100          
243 1         3 shift;
244 1         2 'undef'
245             } elsif (ref $_[0]) {
246 1         5 unshift @_, terse_dump(shift @_);
247 1         68 'debug';
248             } elsif ($_[0] =~ /^[\w\.\-]+$/) {
249 1         3 shift;
250             } else {
251 1         3 'debug';
252             }
253             };
254 4         11 my $msg = join(" ", map {(my $cp = $_) =~ s/\n/\n /g; $cp} @_);
  6         11  
  6         18  
255 4 50       12 if ($fh) {
256 4         16 print $fh uc($tag).": [", $glob->iso8601_datetime(), " #$$] $msg\n";
257             } else {
258 0         0 my ($level, $type) = $tag =~ /^(\w+)(?:\.([\w\-\.]+))?$/;
259 0   0     0 $logger->({level => $level || 'debug', message => $msg});
260             }
261             }
262              
263             # XXX: precise?
264             sub iso8601_datetime {
265 4     4 0 9 my ($glob, $time) = @_;
266 4   33     221 my ($S, $M, $H, $d, $m, $y) = localtime($time // time);
267 4         10 $y += 1900; $m++;
  4         6  
268 4         47 sprintf '%04d-%02d-%02dT%02d:%02d:%02d', ($y, $m, $d, $H, $M, $S);
269             }
270              
271             # Alternative, for more rich logging.
272             sub logger {
273 0     0 0 0 my PROP $prop = prop(my $glob = shift);
274 0         0 $prop->{cf_logger};
275             }
276              
277             #========================================
278              
279             DESTROY {
280             # Note: localizing $@ in DESTROY is not so good idea in general.
281             # But I do this here because I found some module stamps $@ in mkheader.
282             # Anyway, in usual case, $con lives along with entire request processing,
283             # so this may not be a problem.
284 69     69   11306 local $@;
285              
286 69         166 my PROP $prop = prop(my $glob = shift);
287 69         195 $glob->flush_headers;
288 69 100       212 if (my $backend = delete $prop->{cf_backend}) {
289 1 50 33     6 if ($prop->{cf_debug} and my $errfh = $glob->error_fh) {
290 0         0 print $errfh "DEBUG: Connection->backend is detached($backend)\n";
291             }
292             # DBSchema->DESTROY should be called automatically. <- Have tests for this!
293             #$backend->disconnect("Explicitly from Connection->DESTROY");
294             }
295 69 50 33     224 if ($prop->{cf_debug} and my $errfh = $glob->error_fh) {
296 0         0 print $errfh "DEBUG: Connection->DESTROY (glob=$glob, prop=$prop)\n";
297             }
298 69         2836 delete $prop->{$_} for keys %$prop;
299             #undef *$glob;
300             }
301              
302             sub header_was_sent {
303 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
304 0         0 $prop->{header_was_sent} = 1;
305 0         0 my $parent = $prop->{cf_parent_fh};
306 0 0 0     0 if ($parent and my $sub = $parent->can('header_was_sent')) {
307 0         0 $sub->($parent);
308             }
309             }
310              
311             sub flush_headers {
312 156     156 0 390 my PROP $prop = (my $glob = shift)->prop;
313              
314 156 100       611 return if $prop->{header_was_sent}++;
315              
316 66         190 $glob->finalize_headers;
317              
318 66 100       175 if (not $prop->{cf_noheader}) {
319 27   66     107 my $fh = $prop->{cf_parent_fh} // $glob;
320 27         87 print $fh $glob->mkheader;
321             }
322 66         1117 $glob->flush;
323             }
324              
325             sub finalize_headers {
326 69     69 0 160 my PROP $prop = (my $glob = shift)->prop;
327 69 100       362 $prop->{cf_yatt}->finalize_connection($glob) if $prop->{cf_yatt};
328 69 100       366 $prop->{cf_system}->finalize_connection($glob) if $prop->{cf_system};
329 69 50       200 $glob->finalize_cookies if $prop->{cookies_out};
330             }
331              
332             sub flush {
333 69     69 0 210 my PROP $prop = (my $glob = shift)->prop;
334 69         762 $glob->IO::Handle::flush();
335 69 100       300 if ($prop->{cf_parent_fh}) {
336 14         18 print {$prop->{cf_parent_fh}} ${$prop->{cf_buffer}};
  14         30  
  14         38  
337 14         24 ${$prop->{cf_buffer}} = '';
  14         29  
338 14         143 $prop->{cf_parent_fh}->IO::Handle::flush();
339             # XXX: flush 後は、 parent_fh の dup にするべき。
340             # XXX: でも、 multipart (server push) とか continue とかは?
341             }
342             }
343              
344             sub rewind {
345 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
346 0         0 seek *$glob, 0, 0;
347 0         0 ${$prop->{cf_buffer}} = '';
  0         0  
348 0         0 $glob;
349             }
350              
351             #========================================
352             # (Possibly obsoleted) Cookie support, based on CGI::Cookie (works under PSGI mode too)
353              
354             sub cookies_in {
355 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
356 0         0 my Env $env = $prop->{cf_env};
357 0   0     0 $prop->{cookies_in} ||= do {
358 0 0       0 if (defined $env->{HTTP_COOKIE}) {
359 0         0 require CGI::Cookie;
360 0         0 CGI::Cookie->parse($env->{HTTP_COOKIE});
361             } else {
362 0         0 +{};
363             }
364             };
365             }
366              
367             sub set_cookie {
368 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
369 0 0 0     0 if (@_ == 1 and ref $_[0]) {
370 0         0 my $cookie = shift;
371 0         0 my $name = $cookie->name;
372 0         0 $prop->{cookies_out}{$name} = $cookie;
373             } else {
374 0         0 my $name = shift;
375 0         0 $prop->{cookies_out}{$name} = $glob->new_cookie($name, @_);
376             }
377             }
378              
379             sub new_cookie {
380 0     0 0 0 my $glob = shift; # not used.
381 0         0 my ($name, $value) = splice @_, 0, 2;
382 0         0 require CGI::Cookie;
383 0         0 CGI::Cookie->new(-name => $name, -value => $value, @_);
384             }
385              
386             sub finalize_cookies {
387 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
388 0 0       0 return unless $prop->{cookies_out};
389 0         0 $prop->{headers}{'Set-Cookie'} = [map {$_->as_string}
390 0         0 values %{$prop->{cookies_out}}];
  0         0  
391             }
392             #========================================
393              
394             # XXX: Should be renamed to result, text, as_text, as_string or value;
395             sub buffer {
396 33     33 0 104 my PROP $prop = prop(my $glob = shift);
397 33         125 $glob->IO::Handle::flush();
398 33         54 ${$prop->{cf_buffer}}
  33         173  
399             }
400              
401             sub mkheader {
402 27     27 0 56 my PROP $prop = (my $glob = shift)->prop;
403 27   33     171 my ($code) = shift // $prop->{cf_status} // 200;
      50        
404 27         150 require HTTP::Headers;
405             my $headers = HTTP::Headers->new("Content-type", $glob->_mk_content_type
406             , map($_ ? %$_ : (), $prop->{headers})
407 27 100       109 , @_);
408 27         1585 YATT::Lite::Util::mk_http_status($code)
409             . $headers->as_string . "\015\012";
410             }
411              
412             sub _mk_content_type {
413 27     27   64 my PROP $prop = (my $glob = shift)->prop;
414 27   50     119 my $ct = $prop->{cf_content_type} || "text/html";
415 27 50 33     206 if ($ct =~ m{^text/} && $ct !~ /;\s*charset/) {
416 27   100     89 my $cs = $prop->{cf_charset} || "utf-8";
417 27         79 $ct .= qq|; charset=$cs|;
418             }
419 27         302 $ct;
420             }
421              
422             sub set_header {
423 4     4 0 10 my PROP $prop = prop(my $glob = shift);
424 4         7 my ($key, $value) = @_;
425 4         15 $prop->{headers}{$key} = $value;
426 4         7 $glob;
427             }
428              
429             sub set_header_list {
430 0     0 0 0 my PROP $prop = prop(my $glob = shift);
431 0         0 while (my ($k, $v) = splice @_, 0, 2) {
432 0         0 $prop->{headers}{$k} = $v;
433             }
434 0         0 $glob;
435             }
436              
437             sub append_header {
438 0     0 0 0 my PROP $prop = prop(my $glob = shift);
439 0         0 my ($key, @values) = @_;
440 0         0 push @{$prop->{headers}{$key}}, @values;
  0         0  
441             }
442              
443             # For PSGI only.
444             sub list_header {
445 32     32 0 138 my PROP $prop = prop(my $glob = shift);
446             my $headers = $prop->{headers}
447 32 100       247 or return;
448             map {
449 2         7 my $k = $_;
  4         7  
450 4         13 map {$k => $_} lexpand($headers->{$k});
  4         29  
451             } keys %$headers;
452             }
453              
454             sub content_type {
455 0     0 0 0 my PROP $prop = prop(my $glob = shift);
456             $prop->{cf_content_type}
457 0         0 }
458             sub set_content_type {
459 0     0 0 0 my PROP $prop = prop(my $glob = shift);
460 0         0 $prop->{cf_content_type} = shift;
461 0         0 $glob;
462             }
463              
464             sub charset {
465 0     0 0 0 my PROP $prop = prop(my $glob = shift);
466             $prop->{cf_charset}
467 0         0 }
468             sub set_charset {
469 45     45 0 119 my PROP $prop = prop(my $glob = shift);
470 45         108 $prop->{cf_charset} = shift;
471 45         119 $glob;
472             }
473              
474             #========================================
475              
476             sub stash {
477 0     0 0 0 my PROP $prop = prop(my $glob = shift);
478 0 0       0 unless (@_) {
    0          
479             $prop->{stash} //= {}
480 0   0     0 } elsif (@_ == 1) {
481 0         0 $prop->{stash}{$_[0]}
482             } else {
483 0         0 my $name = shift;
484 0         0 $prop->{stash}{$name} = shift;
485 0         0 $glob;
486             }
487             }
488              
489             #========================================
490              
491             sub gettext {
492 3     3 0 32 my PROP $prop = (my $glob = shift)->prop;
493 3         18 $prop->{cf_yatt}->lang_gettext($prop->{cf_lang}, @_);
494             }
495              
496             sub ngettext {
497 3     3 0 22 my PROP $prop = (my $glob = shift)->prop;
498 3         13 $prop->{cf_yatt}->lang_ngettext($prop->{cf_lang}, @_);
499             }
500              
501             #========================================
502              
503             sub backend {
504 1     1 0 9 my PROP $prop = (my $glob = shift)->prop;
505              
506             # XXX: Exposing bare backend may harm.
507             # But anyway, you can get backend via cget('backend').
508             #
509 1 50       5 return $prop->{cf_backend} unless @_;
510              
511 1         7 my $method = shift;
512 1 50       14 unless (defined $method) {
    50          
    50          
513 0         0 $glob->error("backend: null method is called");
514             } elsif (not $prop->{cf_backend}) {
515 0         0 $glob->error("backend is empty");
516             } elsif (not my $sub = $prop->{cf_backend}->can($method)) {
517 0         0 $glob->error("unknown method called for backend: %s", $method);
518             } else {
519 1         6 $sub->($prop->{cf_backend}, @_);
520             }
521             }
522              
523             {
524             foreach (qw/model resultset
525             txn_do txn_begin txn_commit txn_rollback
526             txn_scope_guard
527             /) {
528             my $method = $_;
529             *{globref(__PACKAGE__, $method)} = sub {
530 1     1   5 my PROP $prop = (my $glob = shift)->prop;
531 1         7 $prop->{cf_backend}->$method(@_);
532             };
533             }
534             }
535              
536             1;