| 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 |