File Coverage

blib/lib/Async/MicroserviceReq.pm
Criterion Covered Total %
statement 32 76 42.1
branch 0 20 0.0
condition 0 10 0.0
subroutine 11 25 44.0
pod 5 7 71.4
total 48 138 34.7


line stmt bran cond sub pod time code
1             package Async::MicroserviceReq;
2              
3 2     2   49 use strict;
  2         7  
  2         79  
4 2     2   13 use warnings;
  2         5  
  2         66  
5 2     2   49 use 5.010;
  2         7  
6              
7 2     2   25 use Moose;
  2         6  
  2         23  
8 2     2   16277 use namespace::autoclean;
  2         7  
  2         24  
9 2     2   260 use URI;
  2         7  
  2         78  
10 2     2   1603 use AnyEvent::IO qw(aio_load);
  2         12832  
  2         216  
11 2     2   19 use Try::Tiny;
  2         5  
  2         134  
12 2     2   797 use JSON::XS;
  2         3827  
  2         127  
13 2     2   1141 use Plack::MIME;
  2         1874  
  2         79  
14 2     2   18 use MooseX::Types::Path::Class;
  2         4  
  2         25  
15              
16             our $json = JSON::XS->new->utf8->pretty->canonical;
17             our @no_cache_headers = ('Cache-Control' => 'private, max-age=0', 'Expires' => '-1');
18             our $pending_req = 0;
19              
20             has 'method' => (is => 'ro', isa => 'Str', required => 1);
21             has 'headers' => (is => 'ro', isa => 'Object', required => 1);
22             has 'path' => (is => 'ro', isa => 'Str', required => 1);
23             has 'content' => (is => 'ro', isa => 'Str', required => 1);
24             has 'json_content' =>
25             (is => 'ro', isa => 'Ref', required => 0, lazy => 1, builder => '_build_json_content');
26             has 'params' => (is => 'ro', isa => 'Object', required => 1);
27             has 'plack_respond' => (is => 'rw', isa => 'CodeRef', required => 0);
28             has 'static_dir' => (is => 'ro', isa => 'Path::Class::Dir', required => 1, coerce => 1);
29              
30             has 'base_url' => (
31             is => 'ro',
32             isa => 'URI',
33             required => 1,
34             lazy => 1,
35             builder => '_build_base_url'
36             );
37             has 'want_json' => (
38             is => 'ro',
39             isa => 'Bool',
40             required => 1,
41             lazy => 1,
42             builder => '_build_want_json'
43             );
44              
45             sub _build_base_url {
46 0     0     my ($self) = @_;
47 0           return URI->new('http://' . $self->headers->header('Host') . '/'),;
48             }
49              
50             sub _build_want_json {
51 0     0     my ($self) = @_;
52             return (
53 0 0 0       ($self->headers->header('Accept') // '') eq 'application/json'
54             ? 1
55             : 0
56             );
57             }
58              
59             sub _build_json_content {
60 0     0     my ($self) = @_;
61 0           return $json->decode($self->content);
62             }
63              
64             sub BUILD {
65 0     0 0   $pending_req++;
66             }
67              
68             sub DEMOLISH {
69 0     0 0   $pending_req--;
70             }
71              
72             sub get_pending_req {
73 0     0 1   return $pending_req;
74             }
75              
76             sub text_plain {
77 0     0 1   my ($self, @text) = @_;
78 0           return $self->respond(200, [], join("\n", @text));
79             }
80              
81             sub respond {
82 0     0 1   my ($self, $status, $headers, $payload) = @_;
83 0 0         my %headers_as_hash = map {defined($_) ? lc($_) : $_} @$headers;
  0            
84              
85 0 0         unless ($headers_as_hash{'content-type'}) {
86 0 0         my $content_type = ($self->want_json ? 'application/json' : 'text/plain');
87 0           push(@$headers, ('Content-Type' => $content_type));
88             }
89              
90 0 0 0       if ($self->want_json # json wanted via accept headerts
      0        
91             && !ref($payload) # payload not a reference
92             && !$headers_as_hash{'content-type'} # and content type is not forced (statics for example)
93             ) {
94 0 0         if ($status < 400) {
95 0           $payload = {'data' => $payload};
96             }
97             else {
98 0           $payload = {
99             'error' => {
100             err_status => $status,
101             err_msg => $payload,
102             }
103             };
104             }
105             }
106              
107             # encode any reference as json
108 0 0         if (ref($payload)) {
109             try {
110 0     0     $payload = $json->encode($payload);
111             }
112             catch {
113 0     0     $payload =
114             $json->encode(CRS::Exception::Internal->as_data('failed to serialize json: ' . $_));
115 0           };
116             }
117 0           return $self->plack_respond->([$status, [@no_cache_headers, @$headers], [$payload]]);
118             }
119              
120             sub redirect {
121 0     0 1   my ($self, $location_path) = @_;
122 0           my $location = $self->base_url->clone;
123 0           $location->path($location_path);
124 0           return $self->respond(302, ["Location" => $location], "redirect to " . $location);
125             }
126              
127             sub static {
128 0     0 1   my ($self, $file_name, $content_cb) = @_;
129              
130 0           my $static_file = $self->static_dir->file($file_name)->stringify;
131 0 0         unless (-r $static_file) {
132 0           return $self->respond(404, [], $file_name . ' not found');
133             }
134              
135 0   0       my $content_type = Plack::MIME->mime_type($static_file) || 'text/plain';
136 0           my ($content) = _fetch_file($static_file);
137 0 0         $content = $content_cb->($content)
138             if $content_cb;
139              
140 0           return $self->respond(200, ['Content-Type' => $content_type], $content);
141             }
142              
143             sub _fetch_file {
144 0     0     my ($file) = @_;
145              
146 0           my $filedata = AE::cv;
147             aio_load(
148             $file,
149             sub {
150 0 0   0     my ($content) = @_
151             or die('failed to slurp "' . $file . '"');
152 0           $filedata->($content);
153             }
154 0           );
155              
156 0           return $filedata->recv;
157             }
158              
159             __PACKAGE__->meta->make_immutable;
160              
161             1;
162              
163             __END__
164              
165             =head1 NAME
166              
167             Async::MicroserviceReq - async microservice request class
168              
169             =head1 SYNOPSYS
170              
171             my $this_req = Async::MicroserviceReq->new(
172             method => $plack_req->method,
173             headers => $plack_req->headers,
174             content => $plack_req->content,
175             path => $plack_req->path_info,
176             params => $plack_req->parameters,
177             static_dir => $self->static_dir,
178             );
179              
180             ...
181              
182             my $plack_handler_sub = sub {
183             my ($plack_respond) = @_;
184             $this_req->plack_respond($plack_respond);
185             ...
186              
187             =head1 DESCRIPTION
188              
189             This is an object created for each request handled by L<Async::Microservice>.
190             It is passed to all request handling functions as first argument and
191             it provides some request info and response helper methods.
192              
193             =head1 ATTRIBUTES
194              
195             method
196             headers
197             path
198             params
199             plack_respond
200             static_dir
201             base_url
202             want_json
203             content
204             json_content
205              
206             =head1 METHODS
207              
208             =head2 text_plain(@text_lines)
209              
210             Send text plain response.
211              
212             =head2 respond($status, $headers, $payload)
213              
214             Send plack response.
215              
216             =head2 redirect($location_path)
217              
218             Send redirect.
219              
220             =head2 static($file_name, $content_cb)
221              
222             Send static file, can be updated/modified using optional callback.
223              
224             =head2 get_pending_req
225              
226             Returns number of currently pending async requests.
227              
228             =cut