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   294400 use strict;
  9         21  
  9         239  
2 9     9   50 use warnings;
  9         16  
  9         232  
3 9     9   6139 use version;
  9         18350  
  9         56  
4              
5             package WebService::Nestoria::Search;
6             $WebService::Nestoria::Search::VERSION = '1.022012';
7 9     9   871 use Carp;
  9         17  
  9         629  
8 9     9   19066 use URI;
  9         72467  
  9         278  
9 9     9   5395 use WebService::Nestoria::Search::Request;
  9         36  
  9         334  
10 9     9   5788 use WebService::Nestoria::Search::MetadataResponse;
  9         26  
  9         24477  
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.022012
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   99 my $class = shift;
192 9         27 my %args = @_;
193              
194 9 100       52 if (defined $args{'Warnings'}) {
195 8         19 $args{'warnings'} = $args{'Warnings'};
196             }
197              
198 9 100       43 if (defined $args{'warnings'}) {
199 8         16721 $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   57 $@ = shift;
208 38 50       85 if ( $GlobalDefaults{warnings} ) {
209 38         349 carp $@;
210             }
211              
212 38         27857 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   25 my $val = shift;
240 15 50 33     125 if ( defined($val) && $val =~ /^[\+\-]?\d+\.?\d*$/ ) {
241 15   66     156 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   25 my $val = shift;
251 13 50 33     95 if ( defined($val) && $val =~ /^[\+\-]?\d+\.?\d*$/ ) {
252 13   66     132 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 826     826   1123 my $key = shift;
351 826         1073 my $val = shift;
352              
353 826 100 66     3622 unless ( defined $key && defined $val ) {
354 14         30 return "validation error";
355             }
356              
357 812 100       1729 if ( $key eq 'warnings' ) {
358 127         226 return;
359             }
360              
361 685 50       1452 unless ( $ValidateRoutine{$key} ) {
362 0         0 return "unknown argument '$key'";
363             }
364              
365 685 100       1413 if ( $ValidateRoutine{$key}->($val) ) {
366 661         1096 return;
367             }
368             else {
369 24         71 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 6326 my $class = shift;
388 22         33 my $self;
389              
390 22 50       90 if ( @_ % 2 != 0 ) {
391 0         0 return _carp_on_error("wrong arg count to $class->new");
392             }
393              
394 22         106 $self->{Defaults} = { @_ };
395              
396 22         37 foreach my $key (keys %{ $self->{Defaults} }) {
  22         84  
397 53 50       244 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       80 if ( exists $self->{Defaults}{warnings} ) {
405 2         8 $GlobalDefaults{warnings} = $self->{Defaults}{warnings};
406 2         6 delete $self->{Defaults}{warnings};
407             }
408              
409 22 50       75 if ( exists $self->{Defaults}{country} ) {
410 22 50       96 unless ( exists $Config{Urls}->{$self->{Defaults}{country}} ) {
411 0         0 _carp_on_error("Invalid country");
412             }
413 22         55 $GlobalDefaults{country} = $self->{Defaults}{country};
414 22         50 delete $self->{Defaults}{country};
415             }
416              
417 22         35 my $defaults = $self->{Defaults};
418 22         65 foreach my $key (keys %$defaults) {
419 29         81 my $error = _validate($key, $self->{Defaults}{$key});
420 29 50       93 if ( $error ) {
421 0         0 return _carp_on_error("$error, in call to $class->new");
422             }
423             }
424              
425 22         61 foreach my $key (keys %GlobalDefaults) {
426 44   33     250 $self->{$key} ||= $GlobalDefaults{$key};
427             }
428              
429 22         78 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 54088 my $self = shift;
442              
443 149 50       469 if ( @_ % 2 != 0 ) {
444 0         0 return _carp_on_error("wrong arg count for request");
445             }
446              
447 149         416 my %args = @_;
448              
449 149 50       352 unless ( ref $self ) {
450 0         0 $self = $self->new;
451             }
452              
453 149         427 foreach my $key ( keys %GlobalDefaults ) {
454 298   66     1367 $args{$key} ||= $GlobalDefaults{$key};
455             }
456              
457 149         249 foreach my $key ( keys %{ $self->{Defaults} } ) {
  149         416  
458 57 50       119 next if grep { $key eq $_ } keys %GlobalDefaults;
  114         331  
459 57   66     295 $args{$key} ||= $self->{Defaults}{$key};
460             }
461              
462 149         222 foreach my $key ( keys %{ $Config{Defaults} } ) {
  149         831  
463 4023 100       8843 if ( defined $Config{Defaults}{$key} ) {
464 447   100     1721 $args{$key} ||= $Config{Defaults}{$key};
465             }
466             }
467              
468 149         624 foreach my $key ( keys %args ) {
469 797         1572 my $error = _validate($key, $args{$key});
470 797 100       1955 if ( $error ) {
471 38         79 return _carp_on_error($error);
472             }
473             }
474              
475 111         200 my %params;
476 111         305 $params{ActionUrl} = $Config{Urls}->{$args{country}};
477 111         200 $params{AppId} = $Config{AppId};
478 111         197 $params{Params} = \%args;
479              
480 111 50 66     360 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         665 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 5152 my $self = shift;
502              
503 33 50       93 unless ( ref $self ) {
504 0         0 $self = $self->new;
505             }
506              
507 33 50       92 if ( my $request = $self->request(@_) ) {
508 33         121 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 1254 my $self = shift;
531              
532 10 50       32 unless ( ref $self ) {
533 0         0 $self = $self->new;
534             }
535              
536 10         32 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 6 my $self = shift;
583              
584 1         4 my %params = ( action => 'keywords' );
585              
586 1         5 my $response = $self->query(%params);
587              
588 1         8 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 439 my $self = shift;
603              
604 9 50       26 unless ( ref $self ) {
605 0         0 $self = $self->new;
606             }
607              
608 9         26 my %params = ( action => 'metadata' );
609              
610 9         29 my $response = $self->query(%params, @_);
611              
612 9         72 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 90 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 2112 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 81 my $class = shift;
649 7         17 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     43 if (!$UA && $class->can("get")) {
654 0         0 $UA = $class;
655             }
656              
657 7         16 $WebService::Nestoria::Search::Request::UA = $UA;
658              
659 7         19 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 46 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     46 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         21 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;