File Coverage

blib/lib/Dancer2/Plugin/Map/Tube/API.pm
Criterion Covered Total %
statement 20 106 18.8
branch 0 50 0.0
condition 0 18 0.0
subroutine 7 14 50.0
pod 4 5 80.0
total 31 193 16.0


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