File Coverage

blib/lib/Async/Microservice.pm
Criterion Covered Total %
statement 35 98 35.7
branch 0 16 0.0
condition 0 11 0.0
subroutine 12 17 70.5
pod 0 1 0.0
total 47 143 32.8


line stmt bran cond sub pod time code
1             package Async::Microservice;
2              
3 2     2   1815 use strict;
  2         6  
  2         70  
4 2     2   12 use warnings;
  2         4  
  2         60  
5 2     2   58 use 5.010;
  2         9  
6 2     2   11 use utf8;
  2         5  
  2         17  
7              
8             our $VERSION = '0.02';
9              
10 2     2   1170 use Moose::Role;
  2         11552  
  2         10  
11             requires qw(get_routes service_name);
12              
13 2     2   14332 use Plack::Request;
  2         77222  
  2         85  
14 2     2   21 use Try::Tiny;
  2         5  
  2         152  
15 2     2   15 use Path::Class qw(dir file);
  2         5  
  2         114  
16 2     2   1355 use MooseX::Types::Path::Class;
  2         232400  
  2         18  
17 2     2   3620 use Path::Router;
  2         429397  
  2         109  
18 2     2   20 use FindBin qw($Bin);
  2         5  
  2         279  
19 2     2   1336 use Async::MicroserviceReq;
  2         11  
  2         3053  
20              
21             has 'api_version' => (
22             is => 'ro',
23             isa => 'Int',
24             default => 1,
25             );
26             has 'static_dir' => (
27             is => 'ro',
28             isa => 'Path::Class::Dir',
29             required => 1,
30             coerce => 1,
31             default => sub {
32             my $static_dir = $ENV{STATIC_DIR} // dir($Bin, '..', 'root', 'static');
33             die 'static dir "' . $static_dir . '" not found (check $ENV{STATIC_DIR})'
34             if !$static_dir || !-d $static_dir;
35             return $static_dir;
36             },
37             lazy => 1,
38             );
39              
40             has 'router' => (
41             is => 'ro',
42             isa => 'Path::Router',
43             lazy => 1,
44             builder => '_build_router'
45             );
46              
47             our $start_time = time();
48             our $req_count = 0;
49              
50             sub _build_router {
51 0     0     my ($self) = @_;
52              
53 0           my $router = Path::Router->new;
54 0           my @routes = $self->get_routes();
55 0           while (@routes) {
56 0           my ($path, $opts) = splice(@routes, 0, 2);
57 0           $router->add_route($path, %$opts);
58             }
59              
60 0           return $router;
61             }
62              
63             sub plack_handler {
64 0     0 0   my ($self, $env) = @_;
65              
66 0           $req_count++;
67              
68 0           my $plack_req = Plack::Request->new($env);
69 0           my $this_req = Async::MicroserviceReq->new(
70             method => $plack_req->method,
71             headers => $plack_req->headers,
72             content => $plack_req->content,
73             path => $plack_req->path_info,
74             params => $plack_req->parameters,
75             static_dir => $self->static_dir,
76             );
77              
78             # set process name and last requested path for debug/troubleshooting
79 0           $0 = $self->service_name . ' ' . $this_req->path;
80              
81             my $plack_handler_sub = sub {
82 0     0     my ($plack_respond) = @_;
83 0           $this_req->plack_respond($plack_respond);
84              
85             # API version
86 0           my ($version, $sub_path_info);
87 0 0         if ($this_req->path =~ qr{^/v(\d+?)(/.*)$}) {
88 0           $version = $1;
89 0           $sub_path_info = $2;
90             }
91              
92             # without version path redirect to the latest version
93 0 0         return $this_req->redirect('/v' . $self->api_version . '/')
94             unless $version;
95              
96             # handle static/
97 0 0         return $this_req->static($1)
98             if ($sub_path_info =~ qr{^/static(/.+)$});
99              
100             # dispatch request
101             state $path_dispatch = {
102             '/' => sub {
103 0           $this_req->static('index.html', sub {$self->_update_openapi_html(@_)});
  0            
104             },
105             '/edit' => sub {
106 0           $this_req->static('edit.html', sub {$self->_update_openapi_html(@_)});
  0            
107             },
108             '/hcheck' => sub {
109 0           $this_req->text_plain(
110             'Service-Name: ' . $self->service_name,
111             "API-Version: " . $self->api_version,
112             'Uptime: ' . (time() - $start_time),
113             'Request-Count: ' . $req_count,
114             'Pending-Requests: ' . Async::MicroserviceReq->get_pending_req,
115             );
116             },
117             '' => sub {
118 0 0         if (my $match = $self->router->match($sub_path_info)) {
119 0           my $func = $match->{mapping}->{$this_req->method};
120 0 0 0       if ($func && (my $misc_fn = $self->can($func))) {
121 0           %{$this_req->params} = (
122 0           %{$this_req->params},
123 0           %{$match->{mapping}}
  0            
124             );
125 0           my $resp = $misc_fn->($self, $this_req, $match);
126 0 0 0       if (blessed($resp) && $resp->isa('Future')) {
    0          
127 0           $resp->retain;
128             $resp->on_done(
129             sub {
130 0           my ($resp_data) = @_;
131 0 0         if ( ref($resp_data) eq 'ARRAY' ) {
132 0           $this_req->respond(@$resp_data);
133             }
134             else {
135 0           $this_req->respond( 200,
136             [], $resp_data );
137             }
138             }
139 0           );
140             $resp->on_fail(sub {
141 0           my ($err_msg) = @_;
142 0   0       $err_msg ||= 'unknown';
143 0           $this_req->respond(
144             503, [], 'internal server error calling '.$func.': ' . $err_msg
145             );
146 0           });
147             $resp->on_cancel(sub {
148 0           $this_req->respond(
149             429, [], 'request for '.$func.' canceled'
150             );
151 0           });
152             }
153             elsif (ref($resp) eq 'ARRAY') {
154 0           $this_req->respond(@$resp);
155             }
156 0           return;
157             }
158             }
159 0           return $this_req->respond(404, [], 'not found');
160             },
161 0           };
162 0   0       my $dispatch_fn = $path_dispatch->{$sub_path_info} // $path_dispatch->{''};
163              
164 0           return $dispatch_fn->();
165 0           };
166              
167             return sub {
168 0     0     my $respond = shift;
169             my $response = try {
170 0           $plack_handler_sub->($respond);
171             }
172             catch {
173 0           $this_req->respond(503, [], 'internal server error: ' . $_);
174 0           };
175 0           return $response;
176 0           };
177             }
178              
179             sub _update_openapi_html {
180 0     0     my ($self, $content) = @_;
181 0           my $service_name = $self->service_name;
182 0           $content =~ s/ASYNC-SERVICE-NAME/$service_name/g;
183 0           return $content;
184             }
185              
186             1;
187              
188             __END__
189              
190             =head1 NAME
191              
192             Async::Microservice - Async HTTP Microservice Moose Role
193              
194             =head1 SYNOPSYS
195              
196             # lib/Async/Microservice/HelloWorld.pm
197             package Async::Microservice::HelloWorld;
198             use Moose;
199             with qw(Async::Microservice);
200             sub service_name {return 'asmi-helloworld';}
201             sub get_routes {return ('hello' => {defaults => {GET => 'GET_hello'}});}
202             sub GET_hello {
203             my ( $self, $this_req ) = @_;
204             return [ 200, [], 'Hello world!' ];
205             }
206             1;
207              
208             # bin/async-microservice-helloworld.psgi
209             use Async::Microservice::HelloWorld;
210             my $mise = Async::Microservice::HelloWorld->new();
211             return sub { $mise->plack_handler(@_) };
212              
213             $ plackup -Ilib --port 8089 --server Twiggy bin/async-microservice-helloworld.psgi
214              
215             $ curl http://localhost:8089/v1/hello
216             Hello world!
217              
218             =head1 DESCRIPTION
219              
220             This L<Moose::Role> helps to quicly bootstrap async http service that is
221             including OpenAPI documentation.
222              
223             See L<https://time.meon.eu/> and L<Async::Microservice::Time> code.
224              
225             =head2 To bootstrap new async service
226              
227             Create new package for your APIs from current examples
228             C<lib/Async/Microservice/*>. Inside set return value of C<service_name>.
229             This string will be used to set process name and to read/locate
230             OpenAPI yaml definition for the documentation. Any GET/POST processing
231             funtions must be defined in C<get_routes> funtion.
232              
233             Copy one of the C<bin/*.psgi> update it with your new package name.
234              
235             Copy one of the C<root/static/*.yaml> to have the same name as
236             C<service_name>.
237              
238             Now you are able to lauch the http service with:
239              
240             plackup -Ilib --port 8089 --server Twiggy bin/async-microservice-YOUNAME.psgi
241              
242             In your broser you can read the OpenAPI documentation: L<http://0.0.0.0:8089/v1/>
243             and also use editor to extend it: L<http://0.0.0.0:8089/v1/edit>
244              
245             =head1 SEE ALSO
246              
247             OpenAPI Specification: L<https://github.com/OAI/OpenAPI-Specification/tree/master/versions>
248             or L<https://swagger.io/docs/specification/about/>
249              
250             L<Async::MicroserviceReq>
251             L<Twiggy>
252              
253             =head1 TODO
254              
255             - graceful termination (finish all requests before terminating on sigterm/hup)
256             - systemd service file examples
257             - static/index.html and static/edit.html are not really static, should be moved
258              
259             =head1 CONTRIBUTORS & CREDITS
260              
261             The following people have contributed to this distribution by committing their
262             code, sending patches, reporting bugs, asking questions, suggesting useful
263             advice, nitpicking, chatting on IRC or commenting on my blog (in no particular
264             order):
265              
266             you?
267              
268             Also thanks to my current day-job-employer L<https://www.apa-it.at/>.
269              
270             =head1 BUGS
271              
272             Please report any bugs or feature requests via L<https://github.com/jozef/Async-Microservice/issues>.
273              
274             =head1 AUTHOR
275              
276             Jozef Kutej
277              
278             =head1 COPYRIGHT & LICENSE
279              
280             Copyright 2020 Jozef Kutej, all rights reserved.
281              
282             This program is free software; you can redistribute it and/or modify it
283             under the same terms as Perl itself.
284              
285             =cut