File Coverage

lib/Web/ComposableRequest/Base.pm
Criterion Covered Total %
statement 127 135 95.5
branch 26 40 65.0
condition 28 48 58.3
subroutine 45 47 100.0
pod 5 5 100.0
total 231 275 85.4


line stmt bran cond sub pod time code
1             package Web::ComposableRequest::Base;
2              
3 1     1   8 use namespace::autoclean;
  1         4  
  1         8  
4              
5 1     1   844 use HTTP::Body;
  1         57348  
  1         57  
6 1         207 use HTTP::Status qw( HTTP_EXPECTATION_FAILED
7             HTTP_INTERNAL_SERVER_ERROR
8 1     1   759 HTTP_REQUEST_ENTITY_TOO_LARGE );
  1         6224  
9 1     1   10 use Scalar::Util qw( weaken );
  1         3  
  1         62  
10 1     1   7 use Try::Tiny;
  1         2  
  1         65  
11 1     1   6 use Web::ComposableRequest::Constants qw( EXCEPTION_CLASS NUL TRUE );
  1         2  
  1         11  
12 1         9 use Web::ComposableRequest::Util qw( decode_array decode_hash first_char
13             is_arrayref is_hashref new_uri
14 1     1   659 throw );
  1         3  
15 1     1   1200 use Unexpected::Functions qw( Unspecified );
  1         3  
  1         12  
16 1         12 use Unexpected::Types qw( ArrayRef CodeRef HashRef LoadableClass
17             NonEmptySimpleStr NonZeroPositiveInt
18             Object PositiveInt SimpleStr Str
19 1     1   428 Undef );
  1         3  
20 1     1   5343 use Moo;
  1         3  
  1         8  
21              
22             # Attribute constructors
23             my $_build_body = sub {
24 5     5   68 my $self = shift;
25 5         132 my $content = $self->_content;
26 5         259 my $len = length $content;
27 5         92 my $body = HTTP::Body->new($self->content_type, $len);
28              
29 5         645 $body->cleanup(TRUE);
30 5         80 $body->tmpdir($self->_config->tempdir);
31              
32 5 100       97 return $body unless $len;
33              
34 2     2   125 try { $self->_decode_body($body, $content) }
35             catch {
36             # uncoverable subroutine
37             # uncoverable statement
38 0     0   0 $self->_log->({ level => 'error', message => $_ });
39 2         20 };
40              
41 2         114 return $body;
42             };
43              
44             my $_build__content = sub {
45 5     5   62 my $self = shift;
46 5 100       121 my $cl = $self->content_length or return NUL;
47 2 50       86 my $fh = $self->_env->{ 'psgi.input' } or return NUL;
48 2         6 my $content = NUL;
49              
50             try {
51 2 50   2   168 $fh->can( 'seek' ) and $fh->seek( 0, 0 );
52 2         68 $fh->read( $content, $cl, 0 );
53 2 50       52 $fh->can( 'seek' ) and $fh->seek( 0, 0 );
54             }
55             catch {
56             # uncoverable subroutine
57             # uncoverable statement
58 0     0   0 $self->_log->( { level => 'error', message => $_ } );
59 2         30 };
60              
61 2         115 return $content;
62             };
63              
64             my $_build_tunnel_method = sub {
65 3   100 3   952 return $_[ 0 ]->body_params->( '_method', { optional => TRUE } )
66             || $_[ 0 ]->query_params->( '_method', { optional => TRUE } )
67             || 'not_found';
68             };
69              
70             # Public attributes
71             has 'address' => is => 'lazy', isa => SimpleStr,
72 1   50 1   4779 builder => sub { $_[ 0 ]->_env->{ 'REMOTE_ADDR' } // NUL };
73              
74             has 'base' => is => 'lazy', isa => Object,
75 1     1   1207 builder => sub { new_uri $_[ 0 ]->scheme, $_[ 0 ]->_base },
76             init_arg => undef;
77              
78             has 'body' => is => 'lazy', isa => Object, builder => $_build_body;
79              
80             has 'content_length' => is => 'lazy', isa => PositiveInt,
81 5   100 5   2187 builder => sub { $_[ 0 ]->_env->{ 'CONTENT_LENGTH' } // 0 };
82              
83             has 'content_type' => is => 'lazy', isa => SimpleStr,
84 5   100 5   151 builder => sub { $_[ 0 ]->_env->{ 'CONTENT_TYPE' } // NUL };
85              
86             has 'host' => is => 'lazy', isa => NonEmptySimpleStr,
87 1     1   1275 builder => sub { (split m{ : }mx, $_[ 0 ]->hostport)[ 0 ] };
88              
89             has 'hostport' => is => 'lazy', isa => NonEmptySimpleStr,
90 2   50 2   158 builder => sub { $_[ 0 ]->_env->{ 'HTTP_HOST' } // 'localhost' };
91              
92             has 'method' => is => 'lazy', isa => SimpleStr,
93 1   50 1   1148 builder => sub { lc( $_[ 0 ]->_env->{ 'REQUEST_METHOD' } // NUL )};
94              
95             has 'path' => is => 'lazy', isa => SimpleStr, builder => sub {
96 2   50 2   97 my $v = $_[ 0 ]->_env->{ 'PATH_INFO' } // '/';
97 2         13 $v =~ s{ \A / }{}mx; $v =~ s{ \? .* \z }{}mx; $v };
  2         8  
  2         35  
98              
99             has 'port' => is => 'lazy', isa => NonZeroPositiveInt,
100 1   50 1   1246 builder => sub { $_[ 0 ]->_env->{ 'SERVER_PORT' } // 80 };
101              
102             has 'protocol' => is => 'lazy', isa => NonEmptySimpleStr,
103 1     1   1150 builder => sub { $_[ 0 ]->_env->{ 'SERVER_PROTOCOL' } };
104              
105             has 'query' => is => 'lazy', isa => Str, builder => sub {
106 1 50   1   1118 my $v = $_[ 0 ]->_env->{ 'QUERY_STRING' }; $v ? "?${v}" : NUL };
  1         31  
107              
108             has 'referer' => is => 'lazy', isa => Str,
109 1   50 1   1124 builder => sub { $_[ 0 ]->_env->{ 'HTTP_REFERER' } // NUL };
110              
111             has 'remote_host' => is => 'lazy', isa => SimpleStr,
112 1   50 1   1078 builder => sub { $_[ 0 ]->_env->{ 'REMOTE_HOST' } // NUL };
113              
114             has 'scheme' => is => 'lazy', isa => NonEmptySimpleStr,
115 3   50 3   120 builder => sub { $_[ 0 ]->_env->{ 'psgi.url_scheme' } // 'http' };
116              
117             has 'script' => is => 'lazy', isa => SimpleStr, builder => sub {
118 2   50 2   149 my $v = $_[ 0 ]->_env->{ 'SCRIPT_NAME' } // '/';
119 2         28 $v =~ s{ / \z }{}gmx; $v };
  2         44  
120              
121             has 'tunnel_method' => is => 'lazy', isa => NonEmptySimpleStr,
122             builder => $_build_tunnel_method;
123              
124             has 'upload' => is => 'lazy', isa => Object | Undef,
125             predicate => TRUE;
126              
127             has 'uri' => is => 'lazy', isa => Object, builder => sub {
128 1     1   30 new_uri $_[ 0 ]->scheme, $_[ 0 ]->_base.$_[ 0 ]->path.$_[ 0 ]->query };
129              
130             # Private attributes
131             has '_args' => is => 'ro', isa => ArrayRef,
132 5     5   223 builder => sub { [] }, init_arg => 'args';
133              
134             has '_base' => is => 'lazy', isa => NonEmptySimpleStr, builder => sub {
135 2     2   61 $_[ 0 ]->scheme.'://'.$_[ 0 ]->hostport.$_[ 0 ]->script.'/' };
136              
137             has '_config' => is => 'ro', isa => Object,
138             required => TRUE, init_arg => 'config';
139              
140             has '_content' => is => 'lazy', isa => Str,
141             builder => $_build__content;
142              
143             has '_env' => is => 'ro', isa => HashRef,
144             init_arg => 'env', required => TRUE;
145              
146             has '_log' => is => 'lazy', isa => CodeRef,
147 1   33 1   149 builder => sub { $_[ 0 ]->_env->{ 'psgix.logger' } // sub {} },
        1      
148             init_arg => 'log';
149              
150             has '_params' => is => 'ro', isa => HashRef,
151 2     2   134 builder => sub { {} }, init_arg => 'params';
152              
153             # Construction
154             sub BUILD {
155 7     7 1 34241 my $self = shift;
156 7         46 my $enc = $self->_config->encoding;
157              
158 7         53 decode_array $enc, $self->_args;
159 7         37 decode_hash $enc, $self->_params;
160              
161 7         195 return;
162             }
163              
164             # Private functions
165             my $_defined_or_throw = sub {
166             my ($k, $v, $opts) = @_; $opts->{optional} and return $v;
167              
168             $k =~ m{ \A \d+ \z }mx and $k = "arg[${k}]";
169              
170             defined $v or throw 'Parameter [_1] undefined value', [ $k ],
171             level => 6, rv => HTTP_EXPECTATION_FAILED;
172              
173             return $v;
174             };
175              
176             my $_get_last_value = sub {
177             my ($k, $v, $opts) = @_; return $_defined_or_throw->( $k, $v->[-1], $opts );
178             };
179              
180             my $_get_value_or_values = sub {
181             my ($params, $name, $opts) = @_;
182              
183             defined $name or throw Unspecified, [ 'name' ],
184             level => 5, rv => HTTP_INTERNAL_SERVER_ERROR;
185              
186             my $v = (is_arrayref $params and $name eq '-1') ? [ @{ $params } ]
187             : (is_arrayref $params ) ? $params->[ $name ]
188             : ( $name eq '-1') ? { %{ $params } }
189             : $params->{ $name };
190              
191             return $_defined_or_throw->( $name, $v, $opts );
192             };
193              
194             my $_get_defined_value = sub {
195             my ($params, $name, $opts) = @_;
196              
197             my $v = $_get_value_or_values->( $params, $name, $opts );
198              
199             return (is_arrayref $v) ? $_get_last_value->( $name, $v, $opts ) : $v;
200             };
201              
202             my $_get_defined_values = sub {
203             my ($params, $name, $opts) = @_;
204              
205             my $v = $_get_value_or_values->( $params, $name, $opts );
206              
207             return (is_arrayref $v) ? $v : [ $v ];
208             };
209              
210             my $_scrub_value = sub {
211             my ($name, $v, $opts) = @_; my $pattern = $opts->{scrubber}; my $len;
212              
213             $pattern and defined $v and $v =~ s{ $pattern }{}gmx;
214              
215             $name =~ m{ \A [\-]? \d+ \z }mx and $name = "arg[${name}]";
216              
217             $opts->{optional} or $opts->{allow_null} or $len = length $v
218             or throw Unspecified, [ $name ], level => 4,
219             rv => HTTP_EXPECTATION_FAILED;
220              
221             $len and $len > $opts->{max_length}
222             and throw 'Parameter [_1] size [_2] too big', [ $name, $len ], level => 4,
223             rv => HTTP_REQUEST_ENTITY_TOO_LARGE;
224             return $v;
225             };
226              
227             my $_scrub_hash = sub {
228             my ($params, $opts) = @_;
229              
230             my $hash = $_get_defined_value->( $params, -1, $opts );
231             my @keys = keys %{ $hash };
232              
233             for my $k (@keys) {
234             my $v = delete $hash->{ $k };
235              
236             $hash->{ $_scrub_value->( 'key', $k, $opts ) }
237             = (is_arrayref $v && $opts->{multiple}) ?
238             [ map { $_scrub_value->( $k, $_, $opts ) } @{ $v } ]
239             : (is_arrayref $v) ? $_get_last_value->( $k, $v, $opts )
240             : $_scrub_value->( $k, $v, $opts );
241             }
242              
243             return $hash;
244             };
245              
246             my $_get_scrubbed_param = sub {
247             my ($self, $params, $name, $opts) = @_; $opts = { %{ $opts // {} } };
248              
249             $opts->{max_length} //= $self->_config->max_asset_size;
250             $opts->{scrubber } //= $self->_config->scrubber;
251             $opts->{hashref } and return $_scrub_hash->( $params, $opts );
252             $opts->{multiple } and return
253             [ map { $opts->{raw} ? $_ : $_scrub_value->( $name, $_, $opts ) }
254             @{ $_get_defined_values->( $params, $name, $opts ) } ];
255              
256             my $v = $_get_defined_value->( $params, $name, $opts );
257              
258             return $opts->{raw} ? $v : $_scrub_value->( $name, $v, $opts );
259             };
260              
261             # Private methods
262             sub _decode_body {
263 1     1   4 my ($self, $body, $content) = @_;
264              
265 1         11 $body->add( $content );
266 1         1429 decode_hash $self->_config->encoding, $body->param;
267              
268 1         5 return;
269             }
270              
271             # Public methods
272             sub body_params {
273 8     8 1 1993 my $self = shift; weaken( $self );
  8         17  
274              
275 8         189 my $params = $self->body->param;
276              
277 8 50       177 weaken($params) if ref $params;
278              
279             return sub {
280             return $_get_scrubbed_param->(
281             $self, $params, (defined $_[0] && !is_hashref $_[0])
282 8 100 66 8   35 ? @_ : (-1, { hashref => TRUE, %{ $_[0] // {} } })
  2   50     14  
283             );
284 8         62 };
285             }
286              
287             sub query_params {
288 27     27 1 82 my $self = shift; weaken( $self );
  27         47  
289              
290 27         73 my $params = $self->_params; weaken( $params );
  27         67  
291              
292             return sub {
293             return $_get_scrubbed_param->(
294             $self, $params, (defined $_[0] && !is_hashref $_[0])
295 27 100 66 27   134 ? @_ : (-1, { hashref => TRUE, %{ $_[0] // {} } })
  2   50     12  
296             );
297 27         149 };
298             }
299              
300             sub uri_for {
301 3   50 3 1 11 my ($self, $path, @args) = @_; $path //= NUL;
  3         9  
302              
303 3         174 my $base = $self->_base;
304 3         82 my @query_params = ();
305 3         5 my $uri_params = [];
306              
307 3 100       10 if (is_arrayref $args[0]) {
    100          
308 1         4 $uri_params = shift @args;
309 1         25 @query_params = @args;
310             }
311             elsif (is_hashref $args[0]) {
312 1   50     7 $uri_params = $args[0]->{uri_params } // [];
313 1   50     2 @query_params = @{ $args[0]->{query_params} // [] };
  1         8  
314              
315 1 50       4 $base = $args[0]->{base} if $args[0]->{base};
316             }
317              
318 3 50       13 $path = $base.$path if first_char $path ne '/';
319              
320 3 100       10 if ($uri_params->[0]) {
321 2 50       6 if ($path =~ m{ \* }mx) {
322 0 0       0 for my $arg (grep { defined and length } @{$uri_params}) {
  0         0  
  0         0  
323 0 0       0 if ($path =~ m{ \* }mx) { $path =~ s{ \* }{$arg}mx }
  0         0  
324 0         0 else { $path = "${path}/${arg}" }
325             }
326             }
327             else {
328 2 50       3 $path = join '/', $path, grep { defined and length } @{$uri_params};
  2         13  
  2         5  
329             }
330             }
331              
332 3         60 my $uri = new_uri $self->scheme, $path;
333              
334 3 50       12 $uri->query_form(@query_params) if $query_params[0];
335              
336 3         24 return $uri;
337             }
338              
339             sub uri_params {
340 4     4 1 8 my $self = shift; weaken( $self );
  4         8  
341              
342 4         12 my $params = $self->_args; weaken( $params );
  4         5  
343              
344             return sub {
345             return $_get_scrubbed_param->
346             ( $self, $params, (defined $_[ 0 ] && !is_hashref $_[ 0 ])
347 4 100 66 4   21 ? @_ : (-1, { %{ $_[ 0 ] // {} }, multiple => TRUE }) );
  1   50     4  
348 4         20 };
349             }
350              
351             1;
352              
353             __END__