File Coverage

blib/lib/WebService/Nestoria/Search.pm
Criterion Covered Total %
statement 124 149 83.2
branch 36 58 62.0
condition 21 42 50.0
subroutine 22 23 95.6
pod 11 11 100.0
total 214 283 75.6


line stmt bran cond sub pod time code
1 9     9   274527 use strict;
  9         24  
  9         246  
2 9     9   46 use warnings;
  9         17  
  9         229  
3 9     9   6196 use version;
  9         18648  
  9         56  
4              
5             package WebService::Nestoria::Search;
6             $WebService::Nestoria::Search::VERSION = '1.022011';
7 9     9   921 use Carp;
  9         17  
  9         682  
8 9     9   7465 use URI;
  9         73716  
  9         300  
9 9     9   5798 use WebService::Nestoria::Search::Request;
  9         39  
  9         358  
10 9     9   6286 use WebService::Nestoria::Search::MetadataResponse;
  9         24  
  9         24533  
11              
12             =head1 NAME
13              
14             WebService::Nestoria::Search - Perl interface to the Nestoria Search public API.
15              
16             =head1 VERSION
17              
18             version 1.022011
19              
20             =head1 SYNOPSIS
21              
22             WebService::Nestoria::Search provides a Perl interface to the public API of Nestoria, a vertical search engine for property listings.
23              
24             WebService::Nestoria::Search is currently written to be used with v1.22 of the Nestoria API.
25              
26             Functions and documentation are split over WebService::Nestoria::Search, WebService::Nestoria::Search::Request, WebService::Nestoria::Search::Response and WeebService::Nestoria::Search::Result. However you need only ever use WebService::Nestoria::Search, and the others will be used as necessary.
27              
28             A Request object stores the parameters of the request, a Response object stores the data retrieved from the API (in JSON and Perl hashref formats), and a Result represents an individual listing.
29              
30             =head2 Parameters
31              
32             The possible parameters and their defaults are as follows:
33              
34             country (default: 'uk')
35             warnings (default: 1)
36             action (default: 'search_listings')
37             version
38             encoding (default: 'json')
39             pretty (default: 0)
40             number_of_results
41             page
42             place_name
43             south_west
44             north_east
45             centre_point
46             radius
47             listing_type
48             property_type
49             price_max
50             price_min
51             bedroom_max
52             bedroom_min
53             bathroom_max
54             bathroom_min
55             room_max
56             room_min
57             size_max
58             size_min
59             sort
60             keywords
61             keywords_exclude
62              
63             If parameters are passed to C they are used as the defaults for all calls to the API. Otherwise they can be passed to the querying functions (eg. C) as per-search parameters.
64              
65             You should never have to set the 'action' parameter yourself, it is implied by the method you choose to use to run the query.
66              
67             =head2 Simple Example
68              
69             use WebService::Nestoria::Search;
70              
71             my $NS = WebService::Nestoria::Search->new(
72             place_name => 'soho',
73             listing_type => 'rent',
74             property_type => 'flat',
75             price_max => '500',
76             number_of_results => '10',
77             );
78              
79             my @results = $NS->results(
80             keywords => 'garden,hot_tub,mews',
81             keywords_exclude => 'cottage,wood_floor'
82             );
83              
84             foreach my $result (@results) {
85             print $result->get_title, "\n";
86             }
87              
88             C<@listings> is an array of WebService::Nestoria::Search::Result objects.
89              
90             =head2 Using the Request object
91              
92             my $request = $NS->request;
93              
94             print "Will fetch: ", $request->url, "\n";
95              
96             my $response = $request->fetch;
97              
98             =head2 Using the Response object
99              
100             my $response = $NS->query;
101              
102             if ($response->status_code == 200) {
103             print "Success! Got ", $response->count, " results\n";
104             }
105              
106             print "Raw JSON\n", $response->get_json, "\n";
107              
108             while (my $result = $response->next_result) {
109             print $result->get_thumb_url, "\n";
110             }
111              
112             =head2 Using a bounding box
113              
114             my @bound_results = $ns->results('south_west' => '51.473685,-0.148315', 'north_east' => '50.473685,-0.248315');
115              
116             foreach my $result (@bound_results) {
117             print $result->get_title, "\n";
118             }
119              
120             =cut
121              
122             ##
123             ## Configuration details for searching the Nestoria listings database
124             ##
125             my %Config = (
126             'AppId' => sprintf("%s %s",
127             __PACKAGE__,
128             $WebService::Nestoria::Search::VERSION
129             ? version->parse($WebService::Nestoria::Search::VERSION)->normal
130             : ""
131             ),
132             'MaxResults' => '1000',
133              
134             ## keys indicate the universe of allowable arguments
135             'Defaults' => {
136             'action' => 'search_listings',
137             'version' => undef, # defaults to the latest version
138             'encoding' => 'json',
139             'pretty' => '0', # pretty JSON results not needed
140             'number_of_results' => undef, # defaults to 20 their end
141             'page' => undef, # defautls to 1 on their end
142             'place_name' => undef,
143             'south_west' => undef,
144             'north_east' => undef,
145             'centre_point' => undef,
146             'radius' => undef,
147             'listing_type' => undef, # defaults to 'buy'
148             'property_type' => undef, # defaults to 'all'
149             'price_max' => undef, # defaults to 'max'
150             'price_min' => undef, # defaults to 'min'
151             'bedroom_max' => undef, # defaults to 'max'
152             'bedroom_min' => undef, # defaults to 'min'
153             'bathroom_max' => undef, # defaults to 'max'
154             'bathroom_min' => undef, # defaults to 'min'
155             'room_max' => undef, # defaults to 'max'
156             'room_min' => undef, # defaults to 'min'
157             'size_max' => undef, # defaults to 'max'
158             'size_min' => undef, # defaults to 'min'
159             'sort' => undef, # defaults to 'nestoria_rank'
160             'keywords' => undef, # defaults to an empty list
161             'keywords_exclude' => undef, # defaults to an empty list
162             'callback' => undef,
163             },
164              
165             'Urls' => {
166             'au' => 'http://api.nestoria.com.au/api',
167             'br' => 'http://api.nestoria.com.br/api',
168             'de' => 'http://api.nestoria.de/api',
169             'es' => 'http://api.nestoria.es/api',
170             'fr' => 'http://api.nestoria.fr/api',
171             'in' => 'http://api.nestoria.in/api',
172             'uk' => 'http://api.nestoria.co.uk/api',
173             },
174             );
175              
176             ## filled in Search/Request.pm
177             our $RecentRequestUrl;
178             our $SleepTime = 2;
179              
180             my %GlobalDefaults = (
181             'warnings' => '1',
182             'country' => 'uk'
183             );
184              
185              
186             ##
187             ## import function allows 'Warnings' to be specified on the use line
188             ##
189              
190             sub import {
191 9     9   93 my $class = shift;
192 9         28 my %args = @_;
193              
194 9 100       48 if (defined $args{'Warnings'}) {
195 8         18 $args{'warnings'} = $args{'Warnings'};
196             }
197              
198 9 100       45 if (defined $args{'warnings'}) {
199 8         17089 $GlobalDefaults{'warnings'} = $args{'warnings'};
200             }
201             }
202              
203             ##
204             ## _carp_on_error helper function borrowed from Yahoo::Search
205             ##
206             sub _carp_on_error {
207 38     38   56 $@ = shift;
208 38 50       93 if ( $GlobalDefaults{warnings} ) {
209 38         346 carp $@;
210             }
211              
212 38         26925 return;
213             }
214              
215             ##
216             ## subs for validating arguments
217             ##
218              
219             my $validate_allow_all = sub {
220             ## allow any defined input
221             return defined shift;
222             };
223              
224             my $validate_lat_long = sub {
225             my $val = shift;
226             my ($lat, $long) = split (/,/, $val);
227             return _validate_lat($lat) && _validate_long($long);
228             };
229              
230             my $validate_radius = sub {
231             my $val = shift;
232             my ($lat,$long,$radius) = split(/,/, $val);
233             return _validate_lat($lat) && _validate_long($long) &&
234             ($radius =~ m/^\d+(km|mi)$/);
235             };
236              
237             ## latitude is a float between -180 and 180
238             sub _validate_lat {
239 15     15   23 my $val = shift;
240 15 50 33     120 if ( defined($val) && $val =~ /^[\+\-]?\d+\.?\d*$/ ) {
241 15   66     160 return -180 <= $val && $val <= 180;
242             }
243             else {
244 0         0 return;
245             }
246             }
247              
248             ## longitude is a float between -90 and 90
249             sub _validate_long {
250 13     13   36 my $val = shift;
251 13 50 33     86 if ( defined($val) && $val =~ /^[\+\-]?\d+\.?\d*$/ ) {
252 13   66     136 return -90 <= $val && $val <= 90;
253             }
254             else {
255 0         0 return;
256             }
257             }
258              
259             my $validate_positive_integer = sub {
260             my $val = shift;
261             return ( $val =~ /^\d+$/ && $val > 0 );
262             };
263              
264             my $validate_listing_type = sub {
265             my $val = shift;
266             return grep { $val eq $_ } qw(buy rent share);
267             };
268              
269             my $validate_property_type = sub {
270             my $val = shift;
271             return grep { $val eq $_ } qw(all house flat land);
272             };
273              
274             my $validate_max = sub {
275             my $val = shift;
276             return $val eq 'max' || $val =~ /^\d+$/;
277             };
278              
279             my $validate_min = sub {
280             my $val = shift;
281             return $val eq 'min' || $val =~ /^\d+$/;
282             };
283              
284             my $validate_sort = sub {
285             my $val = shift;
286             return grep { $val eq $_ } qw(bedroom_lowhigh bedroom_highlow
287             price_lowhigh price_highlow
288             newest oldest);
289             };
290              
291             my $validate_version = sub {
292             my $val = shift;
293             return $val =~ m/^[\d.]+$/;
294             };
295              
296             my $validate_action = sub {
297             my $val = shift;
298             return grep { $val eq $_ } qw(search_listings echo keywords metadata);
299             };
300              
301             my $validate_encoding = sub {
302             my $val = shift;
303             return grep { $val eq $_ } qw(json xml);
304             };
305              
306             my $validate_pretty = sub {
307             my $val = shift;
308             return $val == 0 || $val == 1;
309             };
310              
311             my $validate_country = sub {
312             my $val = shift;
313             return $Config{'Urls'}{$val};
314             };
315              
316             ## Mapping from arg name to validation sub
317             my %ValidateRoutine = (
318             'country' => $validate_country,
319             'place_name' => $validate_allow_all,
320             'south_west' => $validate_lat_long,
321             'north_east' => $validate_lat_long,
322             'centre_point' => $validate_lat_long,
323             'radius' => $validate_radius,
324             'number_of_results' => $validate_positive_integer,
325             'page' => $validate_positive_integer,
326             'listing_type' => $validate_listing_type,
327             'property_type' => $validate_property_type,
328             'price_max' => $validate_max,
329             'price_min' => $validate_min,
330             'bedroom_max' => $validate_max,
331             'bedroom_min' => $validate_min,
332             'bathroom_max' => $validate_max,
333             'bathroom_min' => $validate_min,
334             'room_max' => $validate_max,
335             'room_min' => $validate_min,
336             'size_max' => $validate_max,
337             'size_min' => $validate_min,
338             'sort' => $validate_sort,
339             'keywords' => $validate_allow_all,
340             'keywords_exclude' => $validate_allow_all,
341             'version' => $validate_version,
342             'action' => $validate_action,
343             'encoding' => $validate_encoding,
344             'pretty' => $validate_pretty,
345             'has_photo' => $validate_allow_all,
346             'guid' => $validate_allow_all,
347             );
348              
349             sub _validate {
350 828     828   1090 my $key = shift;
351 828         1099 my $val = shift;
352              
353 828 100 66     3643 unless ( defined $key && defined $val ) {
354 14         28 return "validation error";
355             }
356              
357 814 100       1665 if ( $key eq 'warnings' ) {
358 127         226 return;
359             }
360              
361 687 50       1527 unless ( $ValidateRoutine{$key} ) {
362 0         0 return "unknown argument '$key'";
363             }
364              
365 687 100       1330 if ( $ValidateRoutine{$key}->($val) ) {
366 663         1141 return;
367             }
368             else {
369 24         73 return "invalid value '$val' for '$key' argument";
370             }
371             }
372              
373             =head1 FUNCTIONS
374              
375             =head2 new
376              
377             Creates a WebService::Nestoria::Search object. On error sets C<$@> and returns C.
378              
379             If given 'request' parameters (eg. place_name, listing_type) these become defaults for all calls to the API.
380              
381             my %args = (warnings => 0, listing_type => 'rent', place_name => 'soho');
382             my $NS = WebService::Nestoria::Search->new(%args);
383              
384             =cut
385              
386             sub new {
387 22     22 1 8140 my $class = shift;
388 22         40 my $self;
389              
390 22 50       91 if ( @_ % 2 != 0 ) {
391 0         0 return _carp_on_error("wrong arg count to $class->new");
392             }
393              
394 22         103 $self->{Defaults} = { @_ };
395              
396 22         41 foreach my $key (keys %{ $self->{Defaults} }) {
  22         76  
397 53 50       195 if ($key =~ m/[A-Z]/) {
398 0         0 my $newkey = lc $key;
399 0         0 $self->{Defaults}{$newkey} = $self->{Defaults}{$key};
400 0         0 delete $self->{Defaults}{$key};
401             }
402             }
403              
404 22 100       78 if ( exists $self->{Defaults}{warnings} ) {
405 2         6 $GlobalDefaults{warnings} = $self->{Defaults}{warnings};
406 2         4 delete $self->{Defaults}{warnings};
407             }
408              
409 22 50       73 if ( exists $self->{Defaults}{country} ) {
410 22 50       92 unless ( exists $Config{Urls}->{$self->{Defaults}{country}} ) {
411 0         0 _carp_on_error("Invalid country");
412             }
413 22         50 $GlobalDefaults{country} = $self->{Defaults}{country};
414 22         44 delete $self->{Defaults}{country};
415             }
416              
417 22         39 my $defaults = $self->{Defaults};
418 22         58 foreach my $key (keys %$defaults) {
419 29         96 my $error = _validate($key, $self->{Defaults}{$key});
420 29 50       105 if ( $error ) {
421 0         0 return _carp_on_error("$error, in call to $class->new");
422             }
423             }
424              
425 22         67 foreach my $key (keys %GlobalDefaults) {
426 44   33     285 $self->{$key} ||= $GlobalDefaults{$key};
427             }
428              
429 22         84 return bless $self, $class;
430             }
431              
432             =head2 request
433              
434             Creates a WebService::Nestoria::Search::Request object. On error sets C<$@> and returns C
435              
436             my $request = WebService::Nestoria::Search->request(%args);
437              
438             =cut
439              
440             sub request {
441 149     149 1 52471 my $self = shift;
442              
443 149 50       434 if ( @_ % 2 != 0 ) {
444 0         0 return _carp_on_error("wrong arg count for request");
445             }
446              
447 149         398 my %args = @_;
448              
449 149 50       374 unless ( ref $self ) {
450 0         0 $self = $self->new;
451             }
452              
453 149         341 foreach my $key ( keys %GlobalDefaults ) {
454 298   66     1375 $args{$key} ||= $GlobalDefaults{$key};
455             }
456              
457 149         231 foreach my $key ( keys %{ $self->{Defaults} } ) {
  149         445  
458 57 50       123 next if grep { $key eq $_ } keys %GlobalDefaults;
  114         301  
459 57   66     284 $args{$key} ||= $self->{Defaults}{$key};
460             }
461              
462 149         233 foreach my $key ( keys %{ $Config{Defaults} } ) {
  149         830  
463 4023 100       8977 if ( defined $Config{Defaults}{$key} ) {
464 447   100     1790 $args{$key} ||= $Config{Defaults}{$key};
465             }
466             }
467              
468 149         619 foreach my $key ( keys %args ) {
469 799         1617 my $error = _validate($key, $args{$key});
470 799 100       2052 if ( $error ) {
471 38         71 return _carp_on_error($error);
472             }
473             }
474              
475 111         194 my %params;
476 111         299 $params{ActionUrl} = $Config{Urls}->{$args{country}};
477 111         191 $params{AppId} = $Config{AppId};
478 111         203 $params{Params} = \%args;
479              
480 111 50 66     427 if (defined $args{number_of_results} && $args{number_of_results} > $Config{MaxResults}) {
481 0         0 return _carp_on_error("number_of_results $args{number_of_results} too large, maximum is $Config{MaxResults}");
482             }
483              
484 111         677 return WebService::Nestoria::Search::Request->new(\%params);
485             }
486              
487             =head2 query
488              
489             Queries the API and returns a WebService::Nestoria::Search::Response object. On error, sets C<$@> and returns C.
490              
491             my $response = $NS->query(%args);
492              
493             This is a shortcut for
494              
495             my $request = $NS->request(%args);
496             my $response = $request->fetch;
497              
498             =cut
499              
500             sub query {
501 33     33 1 7199 my $self = shift;
502              
503 33 50       93 unless ( ref $self ) {
504 0         0 $self = $self->new;
505             }
506              
507 33 50       85 if ( my $request = $self->request(@_) ) {
508 33         119 return $request->fetch();
509             }
510             else {
511 0         0 return;
512             }
513             }
514              
515             =head2 results
516              
517             Returns an array of WebService::Nestoria::Search::Result objects. On error, sets C<$@> and returns C.
518              
519             my @results = $NS->results(%args);
520              
521             This is a shortcut for
522              
523             my $request = $NS->request(%args);
524             my $response = $request->fetch;
525             my @results = $response->results;
526              
527             =cut
528              
529             sub results {
530 10     10 1 1233 my $self = shift;
531              
532 10 50       38 unless ( ref $self ) {
533 0         0 $self = $self->new;
534             }
535              
536 10         29 my $response = $self->query(@_);
537              
538 10 50       103 unless ( $response ) {
539 0         0 return;
540             }
541              
542 10         43 return $response->results();
543             }
544              
545             =head2 test_connection
546              
547             Uses the API feature 'action=echo' to test the connection.
548              
549             Returns 1 if the connection is successful and 0 otherwise.
550              
551             unless ($NS->test_connection) {
552             die "Cannot establish connection with Nestoria API\n";
553             }
554              
555             =cut
556              
557             sub test_connection {
558 0     0 1 0 my $self = shift;
559              
560 0         0 my %params = ( action => 'echo' );
561              
562 0         0 my $response = $self->query(%params);
563 0 0 0     0 if ( defined $response && $response->status_code == 200 ) {
564 0         0 return 1;
565             }
566             else {
567 0         0 return 0;
568             }
569             }
570              
571             =head2 keywords
572              
573             Uses the API feature 'action=keywords' to return a list of valid keywords. A current list of keywords can be found at the below URL, but do not hardcode the list of keywords in your code as it is occasionally subject to change.
574              
575             my @keywords = $NS->keywords;
576              
577             Taken from B.
578              
579             =cut
580              
581             sub keywords {
582 1     1 1 7 my $self = shift;
583              
584 1         4 my %params = ( action => 'keywords' );
585              
586 1         4 my $response = $self->query(%params);
587              
588 1         7 my $data = $response->get_hashref;
589              
590 1         4 return ( split(/,\s+/, $response->get_hashref->{'response'}{'keywords'}) );
591             }
592              
593             =head2 metadata
594              
595             Uses the API feature 'action=metadata' to return metadata about the listings. Returns a WebService::Nestoria::Search::MetadataResponse object with average house, flat and property prices aggregated monthly and quarterly.
596              
597             my $metadata_response = WebService::Nestoria::Search->metadata(%args);
598              
599             =cut
600              
601             sub metadata {
602 9     9 1 828 my $self = shift;
603              
604 9 50       28 unless ( ref $self ) {
605 0         0 $self = $self->new;
606             }
607              
608 9         27 my %params = ( action => 'metadata' );
609              
610 9         31 my $response = $self->query(%params, @_);
611              
612 9         68 return WebService::Nestoria::Search::MetadataResponse->new($response->get_hashref);
613             }
614              
615             =head2 last_request_uri
616              
617             Returns a URI object representing the URL that was last fetched by
618             WebService::Nestoria::Search::Request.
619              
620             =cut
621              
622             sub last_request_uri {
623 1     1 1 98 return URI->new($RecentRequestUrl);
624             }
625              
626             =head2 last_request_url
627              
628             Returns the URL that was last fetched by WebService::Nestoria::Search::Request.
629              
630             =cut
631              
632             sub last_request_url {
633 1     1 1 1973 return $RecentRequestUrl;
634             }
635              
636             =head2 override_user_agent
637              
638             Takes an LWP::UserAgent-ish object and uses that object for all requests.
639              
640             Note this is a class method so will affect all instances being used in your
641             process.
642              
643             WebService::Nestoria::Search->override_user_agent($UA);
644              
645             =cut
646              
647             sub override_user_agent {
648 7     7 1 85 my $class = shift;
649 7         19 my $UA = shift;
650              
651             # allow getting called as ::override_user_agent() instead of
652             # ->override_user_agent() - we're nice like that :-)
653 7 50 33     49 if (!$UA && $class->can("get")) {
654 0         0 $UA = $class;
655             }
656              
657 7         18 $WebService::Nestoria::Search::Request::UA = $UA;
658              
659 7         20 return;
660             }
661              
662             =head2 override_sleep_time
663              
664             Takes a time in seconds to sleep between each request. The default is two
665             (2) seconds.
666              
667             Note that the T&Cs of the Nestoria API explicitly request 1 second between
668             requests. To live this requirement contact Nestoria.
669              
670             WebService::Nestoria::Search->override_sleep_time(1);
671              
672             =cut
673              
674             sub override_sleep_time {
675 7     7 1 52 my $class = shift;
676 7         16 my $sleep_time = shift;
677              
678             # allow getting called as ::override_user_agent() instead of
679             # ->override_user_agent() - we're nice like that :-)
680 7 50 33     105 if (!defined($sleep_time) && !ref($class) && $class =~ m/^\d+$/) {
      33        
681 0         0 $sleep_time = $class;
682             }
683              
684 7         15 $WebService::Nestoria::Search::SleepTime = $sleep_time;
685              
686 7         17 return;
687             }
688              
689              
690             =head1 Warnings
691              
692             Warnings is true by default, and means that errors are output to STDERR as well as being returned via $@. This can be turned off either on the C line
693              
694             use WebService::Nestoria::Search Warnings => 0;
695              
696             or when calling C
697              
698             my $NS = WebService::Nestoria::Search->new(Warnings => 0);
699              
700             =head1 Country
701              
702             Country is an optional parameter which defaults to 'uk'. It affects the URL which is used for fetching results.
703              
704             Currently the available countries are:
705              
706             =over 4
707              
708             =item * br - Brazil
709              
710             =item * de - Germany
711              
712             =item * es - Spain
713              
714             =item * fr - France
715              
716             =item * in - India
717              
718             =item * uk - United Kingdom
719              
720             =back
721              
722             Australia (au) supports the metadata requests but not listings requests
723              
724             Mexico (mx) and Italy (it) currently have no API support.
725              
726             =head1 Non-OO
727              
728             It is possible to run WebService::Nestoria::Search functions without creating an object. However, no functions are exported (by default or otherwise) so the full name must be used.
729              
730             my @results = WebService::Nestoria::Search->results(%args);
731              
732             =head1 Copyright
733              
734             Copyright (C) 2014 Lokku Ltd.
735              
736             =head1 Author
737              
738             Alex Balhatchet (alex@lokku.com)
739              
740             Patches supplied by Yoav Felberbaum, Alistair Francis, Ed Freyfogle
741              
742             =head1 Acknowledgements
743              
744             A lot of the ideas (and yes, very occasionally entire functions) for these modules were borrowed from Jeffrey Friedl's Yahoo::Search.
745              
746             This module would not exist without the public API available from Nestoria (B.)
747              
748             =cut
749              
750             1;