File Coverage

blib/lib/Dancer2/Plugin/Map/Tube/API.pm
Criterion Covered Total %
statement 26 118 22.0
branch 0 62 0.0
condition 0 18 0.0
subroutine 9 17 52.9
pod 4 5 80.0
total 39 220 17.7


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::Map::Tube::API;
2              
3             $Dancer2::Plugin::Map::Tube::API::VERSION = '0.04';
4             $Dancer2::Plugin::Map::Tube::API::AUTHORITY = 'cpan:MANWAR';
5              
6             =head1 NAME
7              
8             Dancer2::Plugin::Map::Tube::API - API for Map::Tube.
9              
10             =head1 VERSION
11              
12             Version 0.04
13              
14             =cut
15              
16 1     1   13 use 5.006;
  1         3  
17 1     1   777 use JSON;
  1         12447  
  1         5  
18 1     1   202 use Data::Dumper;
  1         2  
  1         58  
19 1     1   522 use IO::Socket::INET;
  1         20960  
  1         7  
20 1     1   1198 use Cache::Memcached::Fast;
  1         64666  
  1         70  
21 1     1   10 use Time::HiRes qw(gettimeofday);
  1         1  
  1         11  
22 1     1   919 use Dancer2::Plugin::Map::Tube::Error;
  1         2  
  1         194  
23              
24 1     1   633 use Moo;
  1         7055  
  1         9  
25 1     1   2687 use namespace::autoclean;
  1         10457  
  1         4  
26              
27             our $REQUEST_PERIOD = $ENV{'REQUEST_PERIOD'} || 60; # seconds.
28             our $REQUEST_THRESHOLD = $ENV{'REQUEST_THRESHOLD'} || 6; # API calls limit per REQUEST_PERIOD.
29             our $MEMCACHE_HOST = $ENV{'MEMCACHE_HOST'} || 'localhost';
30             our $MEMCACHE_PORT = $ENV{'MEMCACHE_PORT'} || 11211;
31              
32             has 'map_name' => (is => 'ro');
33             has 'user_maps' => (is => 'rw');
34             has 'user_error' => (is => 'rw');
35             has 'installed_maps' => (is => 'ro');
36             has 'map_names' => (is => 'ro');
37             has 'supported_maps' => (is => 'ro');
38             has 'request_period' => (is => 'ro', default => sub { $REQUEST_PERIOD });
39             has 'request_threshold' => (is => 'ro', default => sub { $REQUEST_THRESHOLD });
40             has 'memcache_host' => (is => 'ro', default => sub { $MEMCACHE_HOST });
41             has 'memcache_port' => (is => 'ro', default => sub { $MEMCACHE_PORT });
42             has 'memcached' => (is => 'rw');
43             has 'map_object' => (is => 'rw');
44              
45             =head1 DESCRIPTION
46              
47             It is the backbone for L and provides the core functionalities
48             for the REST API.
49              
50             This is part of Dancer2 plugin L distribution, which
51             makes most of work for L.
52              
53             =cut
54              
55             sub BUILD {
56 0     0 0   my ($self, $arg) = @_;
57              
58 0 0         return { error_code => $MEMCACHE_SERVER_ERROR,
59             error_message => $MEMCACHE_SERVER_UNREACHABLE,
60             } unless $self->_is_server_running();
61              
62 0           my $address = sprintf("%s:%d", $self->memcache_host, $self->memcache_port);
63 0           $self->{memcached} = Cache::Memcached::Fast->new(
64             { servers => [{ address => $address }],
65             close_on_error => 1,
66             max_failures => 3,
67             failure_timeout => 2,
68             utf8 => 1,
69             connect_timeout => 0.2,
70             max_size => 513 * 1024,
71             });
72              
73 0           my $map_name = $self->map_name;
74 0 0         if (defined $map_name) {
75 0 0         unless (exists $self->{map_names}->{lc($map_name)}) {
76             $self->{user_error} = {
77 0           error_code => $BAD_REQUEST,
78             error_message => $RECEIVED_INVALID_MAP_NAME,
79             };
80 0           return;
81             }
82              
83 0 0         unless (exists $self->{installed_maps}->{$self->{map_names}->{lc($map_name)}}) {
84             $self->{user_error} = {
85 0           error_code => $BAD_REQUEST,
86             error_message => $MAP_NOT_INSTALLED,
87             };
88 0           return;
89             }
90              
91 0           $self->{map_object} = $self->{installed_maps}->{$self->{map_names}->{lc($map_name)}};
92             }
93             else {
94             $self->{user_error} = {
95 0           error_code => $BAD_REQUEST,
96             error_message => $MISSING_MAP_NAME,
97             };
98             }
99             }
100              
101             =head1 METHODS
102              
103             =head2 shortest_route($client_ip, $start, $end)
104              
105             Returns ordered list of stations for the shortest route from C<$start> to C<$end>.
106              
107             =cut
108              
109             sub shortest_route {
110 0     0 1   my ($self, $client_ip, $start, $end) = @_;
111              
112 0 0         return { error_code => $MEMCACHE_SERVER_ERROR,
113             error_message => $MEMCACHE_SERVER_UNREACHABLE,
114             } unless $self->_is_server_running();
115              
116 0 0         return { error_code => $TOO_MANY_REQUEST,
117             error_message => $REACHED_REQUEST_LIMIT,
118             } unless $self->_is_authorized($client_ip);
119              
120 0           my $map_name = $self->{map_name};
121 0 0 0       return { error_code => $BAD_REQUEST,
122             error_message => $MISSING_MAP_NAME,
123             } unless (defined $map_name && ($map_name !~ /^$/));
124              
125 0 0 0       return { error_code => $BAD_REQUEST,
126             error_message => $MISSING_START_STATION_NAME,
127             } unless (defined $start && ($start !~ /^$/));
128              
129 0 0 0       return { error_code => $BAD_REQUEST,
130             error_message => $MISSING_END_STATION_NAME,
131             } unless (defined $end && ($end !~ /^$/));
132              
133 0           my $object = $self->map_object;
134 0 0         return { error_code => $BAD_REQUEST,
135             error_message => $RECEIVED_UNSUPPORTED_MAP_NAME,
136             } unless (defined $object);
137              
138 0           eval { $object->get_node_by_name($start) };
  0            
139 0 0         return { error_code => $BAD_REQUEST,
140             error_message => $RECEIVED_INVALID_START_STATION_NAME,
141             } if ($@);
142              
143 0           eval { $object->get_node_by_name($end) };
  0            
144 0 0         return { error_code => $BAD_REQUEST,
145             error_message => $RECEIVED_INVALID_END_STATION_NAME,
146             } if ($@);
147              
148 0           my $route = $object->get_shortest_route($start, $end);
149 0           my $stations = [ map { sprintf("%s", $_) } @{$route->nodes} ];
  0            
  0            
150              
151 0           return _jsonified_content($stations);
152             };
153              
154             =head2 line_stations($client_ip, $line)
155              
156             Returns the list of stations, indexed if it is available, in the given C<$line>.
157              
158             =cut
159              
160             sub line_stations {
161 0     0 1   my ($self, $client_ip, $line_name) = @_;
162              
163 0 0         return { error_code => $MEMCACHE_SERVER_ERROR,
164             error_message => $MEMCACHE_SERVER_UNREACHABLE,
165             } unless $self->_is_server_running();
166              
167 0 0         return { error_code => $TOO_MANY_REQUEST,
168             error_message => $REACHED_REQUEST_LIMIT,
169             } unless $self->_is_authorized($client_ip);
170              
171 0 0         return $self->{user_error} if (defined $self->{user_error});
172              
173 0           my $map_name = $self->{map_name};
174 0 0 0       return { error_code => $BAD_REQUEST,
175             error_message => $MISSING_MAP_NAME,
176             } unless (defined $map_name && ($map_name !~ /^$/));
177              
178 0 0 0       return { error_code => $BAD_REQUEST,
179             error_message => $MISSING_LINE_NAME,
180             } unless (defined $line_name && ($line_name !~ /^$/));
181              
182 0           my $object = $self->map_object;
183 0 0         return { error_code => $BAD_REQUEST,
184             error_message => $RECEIVED_UNSUPPORTED_MAP_NAME,
185             } unless (defined $object);
186              
187 0           eval { $object->get_line_by_name($line_name) };
  0            
188 0 0         return { error_code => $BAD_REQUEST,
189             error_message => $RECEIVED_INVALID_LINE_NAME,
190             } if ($@);
191              
192 0           my $stations = $object->get_stations($line_name);
193              
194 0           return _jsonified_content([ map { sprintf("%s", $_) } @{$stations} ]);
  0            
  0            
195             };
196              
197             =head2 map_stations($client_ip)
198              
199             Returns ordered list of stations in the map.
200              
201             =cut
202              
203             sub map_stations {
204 0     0 1   my ($self, $client_ip) = @_;
205              
206 0 0         return { error_code => $MEMCACHE_SERVER_ERROR,
207             error_message => $MEMCACHE_SERVER_UNREACHABLE,
208             } unless $self->_is_server_running();
209              
210 0 0         return { error_code => $TOO_MANY_REQUEST,
211             error_message => $REACHED_REQUEST_LIMIT,
212             } unless $self->_is_authorized($client_ip);
213              
214 0 0         return $self->{user_error} if (defined $self->{user_error});
215              
216 0           my $map_name = $self->{map_name};
217 0 0 0       return { error_code => $BAD_REQUEST,
218             error_message => $MISSING_MAP_NAME,
219             } unless (defined $map_name && ($map_name !~ /^$/));
220              
221 0           my $object = $self->map_object;
222 0 0         return { error_code => $BAD_REQUEST,
223             error_message => $RECEIVED_UNSUPPORTED_MAP_NAME,
224             } unless (defined $object);
225              
226 0           my $stations = {};
227 0           foreach my $station (@{$object->get_stations}) {
  0            
228 0           $stations->{sprintf("%s", $station)} = 1;
229             }
230              
231 0           return _jsonified_content([ sort keys %$stations ]);
232             };
233              
234             =head2 available_maps($client)
235              
236             Returns ordered list of available maps.
237              
238             =cut
239              
240             sub available_maps {
241 0     0 1   my ($self, $client_ip) = @_;
242              
243 0 0         return { error_code => $MEMCACHE_SERVER_ERROR,
244             error_message => $MEMCACHE_SERVER_UNREACHABLE,
245             } unless $self->_is_server_running();
246              
247 0 0         return { error_code => $TOO_MANY_REQUEST,
248             error_message => $REACHED_REQUEST_LIMIT,
249             } unless $self->_is_authorized($client_ip);
250              
251 0           my $maps = [ sort keys %{$self->{installed_maps}} ];
  0            
252              
253 0           return _jsonified_content($maps);
254             };
255              
256             #
257             #
258             # PRIVATE METHODS
259              
260             sub _jsonified_content {
261 0     0     my ($data) = @_;
262              
263 0           return { content => JSON->new->allow_nonref->utf8(1)->encode($data) };
264             }
265              
266             sub _is_server_running {
267 0     0     my ($self) = @_;
268              
269 0           my $socket = IO::Socket::INET->new(
270             PeerAddr => $self->memcache_host,
271             PeerPort => $self->memcache_port,
272             Proto => 'tcp',
273             Timeout => 2,
274             );
275              
276 0 0         return 0 unless $socket;
277              
278 0           close($socket);
279 0           return 1;
280             }
281              
282             sub _is_authorized {
283 0     0     my ($self, $client_ip) = @_;
284              
285 0 0         return 0 unless defined $client_ip;
286              
287 0           my $now = int(gettimeofday());
288 0           my $call_info = $self->memcached->get($client_ip);
289              
290 0 0         if (defined $call_info) {
291 0           my ($start_time, $count) = split /\:/, $call_info, 2;
292 0           my $elapsed_time = $now - $start_time;
293              
294 0 0         if ($elapsed_time >= $self->request_period) {
295 0           $start_time = $now;
296 0           $count = 1;
297             }
298             else {
299 0 0         if ($count >= $self->request_threshold) {
300 0           return 0;
301             }
302             else {
303 0           $count++;
304             }
305             }
306              
307 0           $self->memcached->replace($client_ip, sprintf("%d:%d", $start_time, $count));
308             }
309             else {
310 0           $self->memcached->add($client_ip, sprintf("%d:1", $now));
311             }
312              
313 0           return 1;
314             }
315              
316             =head1 AUTHOR
317              
318             Mohammad Sajid Anwar, C<< >>
319              
320             =head1 REPOSITORY
321              
322             L
323              
324             =head1 BUGS
325              
326             Please report any bugs or feature requests through the web interface at L.
327             I will be notified and then you'll automatically be notified of progress on your
328             bug as I make changes.
329              
330             =head1 SUPPORT
331              
332             You can find documentation for this module with the perldoc command.
333              
334             perldoc Dancer2::Plugin::Map::Tube::API
335              
336             You can also look for information at:
337              
338             =over 4
339              
340             =item * BUGS / ISSUES
341              
342             L
343              
344             =item * AnnoCPAN: Annotated CPAN documentation
345              
346             L
347              
348             =item * CPAN Ratings
349              
350             L
351              
352             =item * Search MetaCPAN
353              
354             L
355              
356             =back
357              
358             =head1 LICENSE AND COPYRIGHT
359              
360             Copyright (C) 2024 Mohammad Sajid Anwar.
361              
362             This program is free software; you can redistribute it and / or modify it under
363             the terms of the the Artistic License (2.0). You may obtain a copy of the full
364             license at:
365              
366             L
367              
368             Any use, modification, and distribution of the Standard or Modified Versions is
369             governed by this Artistic License.By using, modifying or distributing the Package,
370             you accept this license. Do not use, modify, or distribute the Package, if you do
371             not accept this license.
372              
373             If your Modified Version has been derived from a Modified Version made by someone
374             other than you,you are nevertheless required to ensure that your Modified Version
375             complies with the requirements of this license.
376              
377             This license does not grant you the right to use any trademark, service mark,
378             tradename, or logo of the Copyright Holder.
379              
380             This license includes the non-exclusive, worldwide, free-of-charge patent license
381             to make, have made, use, offer to sell, sell, import and otherwise transfer the
382             Package with respect to any patent claims licensable by the Copyright Holder that
383             are necessarily infringed by the Package. If you institute patent litigation
384             (including a cross-claim or counterclaim) against any party alleging that the
385             Package constitutes direct or contributory patent infringement,then this Artistic
386             License to you shall terminate on the date that such litigation is filed.
387              
388             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND
389             CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED
390             WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
391             NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS
392             REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT,
393             INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE
394             OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
395              
396             =cut
397              
398             1; # End of Dancer2::Plugin::Map::Tube::API