File Coverage

blib/lib/WebService/Nestoria/Search.pm
Criterion Covered Total %
statement 121 137 88.3
branch 38 54 70.3
condition 20 33 60.6
subroutine 21 21 100.0
pod 9 9 100.0
total 209 254 82.2


line stmt bran cond sub pod time code
1 9     9   271499 use strict;
  9         21  
  9         274  
2 9     9   46 use warnings;
  9         21  
  9         236  
3 9     9   6420 use version;
  9         18982  
  9         61  
4              
5             package WebService::Nestoria::Search;
6             $WebService::Nestoria::Search::VERSION = '1.022010';
7 9     9   973 use Carp;
  9         19  
  9         668  
8 9     9   7871 use URI;
  9         75390  
  9         301  
9 9     9   5864 use WebService::Nestoria::Search::Request;
  9         38  
  9         323  
10 9     9   6207 use WebService::Nestoria::Search::MetadataResponse;
  9         26  
  9         23520  
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.022010
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              
179             my %GlobalDefaults = (
180             'warnings' => '1',
181             'country' => 'uk'
182             );
183              
184              
185             ##
186             ## import function allows 'Warnings' to be specified on the use line
187             ##
188              
189             sub import {
190 9     9   97 my $class = shift;
191 9         29 my %args = @_;
192              
193 9 100       52 if (defined $args{'Warnings'}) {
194 8         18 $args{'warnings'} = $args{'Warnings'};
195             }
196              
197 9 100       50 if (defined $args{'warnings'}) {
198 8         17073 $GlobalDefaults{'warnings'} = $args{'warnings'};
199             }
200             }
201              
202             ##
203             ## _carp_on_error helper function borrowed from Yahoo::Search
204             ##
205             sub _carp_on_error {
206 38     38   58 $@ = shift;
207 38 50       88 if ( $GlobalDefaults{warnings} ) {
208 38         339 carp $@;
209             }
210              
211 38         26690 return;
212             }
213              
214             ##
215             ## subs for validating arguments
216             ##
217              
218             my $validate_allow_all = sub {
219             ## allow any defined input
220             return defined shift;
221             };
222              
223             my $validate_lat_long = sub {
224             my $val = shift;
225             my ($lat, $long) = split (/,/, $val);
226             return _validate_lat($lat) && _validate_long($long);
227             };
228              
229             my $validate_radius = sub {
230             my $val = shift;
231             my ($lat,$long,$radius) = split(/,/, $val);
232             return _validate_lat($lat) && _validate_long($long) &&
233             ($radius =~ m/^\d+(km|mi)$/);
234             };
235              
236             ## latitude is a float between -180 and 180
237             sub _validate_lat {
238 15     15   32 my $val = shift;
239 15 50 33     146 if ( defined($val) && $val =~ /^[\+\-]?\d+\.?\d*$/ ) {
240 15   66     172 return -180 <= $val && $val <= 180;
241             }
242             else {
243 0         0 return;
244             }
245             }
246              
247             ## longitude is a float between -90 and 90
248             sub _validate_long {
249 13     13   30 my $val = shift;
250 13 50 33     105 if ( defined($val) && $val =~ /^[\+\-]?\d+\.?\d*$/ ) {
251 13   66     163 return -90 <= $val && $val <= 90;
252             }
253             else {
254 0         0 return;
255             }
256             }
257              
258             my $validate_positive_integer = sub {
259             my $val = shift;
260             return ( $val =~ /^\d+$/ && $val > 0 );
261             };
262              
263             my $validate_listing_type = sub {
264             my $val = shift;
265             return grep { $val eq $_ } qw(buy rent share);
266             };
267              
268             my $validate_property_type = sub {
269             my $val = shift;
270             return grep { $val eq $_ } qw(all house flat land);
271             };
272              
273             my $validate_max = sub {
274             my $val = shift;
275             return $val eq 'max' || $val =~ /^\d+$/;
276             };
277              
278             my $validate_min = sub {
279             my $val = shift;
280             return $val eq 'min' || $val =~ /^\d+$/;
281             };
282              
283             my $validate_sort = sub {
284             my $val = shift;
285             return grep { $val eq $_ } qw(bedroom_lowhigh bedroom_highlow
286             price_lowhigh price_highlow
287             newest oldest);
288             };
289              
290             my $validate_version = sub {
291             my $val = shift;
292             return $val =~ m/^[\d.]+$/;
293             };
294              
295             my $validate_action = sub {
296             my $val = shift;
297             return grep { $val eq $_ } qw(search_listings echo keywords metadata);
298             };
299              
300             my $validate_encoding = sub {
301             my $val = shift;
302             return grep { $val eq $_ } qw(json xml);
303             };
304              
305             my $validate_pretty = sub {
306             my $val = shift;
307             return $val == 0 || $val == 1;
308             };
309              
310             my $validate_country = sub {
311             my $val = shift;
312             return $Config{'Urls'}{$val};
313             };
314              
315             ## Mapping from arg name to validation sub
316             my %ValidateRoutine = (
317             'country' => $validate_country,
318             'place_name' => $validate_allow_all,
319             'south_west' => $validate_lat_long,
320             'north_east' => $validate_lat_long,
321             'centre_point' => $validate_lat_long,
322             'radius' => $validate_radius,
323             'number_of_results' => $validate_positive_integer,
324             'page' => $validate_positive_integer,
325             'listing_type' => $validate_listing_type,
326             'property_type' => $validate_property_type,
327             'price_max' => $validate_max,
328             'price_min' => $validate_min,
329             'bedroom_max' => $validate_max,
330             'bedroom_min' => $validate_min,
331             'bathroom_max' => $validate_max,
332             'bathroom_min' => $validate_min,
333             'room_max' => $validate_max,
334             'room_min' => $validate_min,
335             'size_max' => $validate_max,
336             'size_min' => $validate_min,
337             'sort' => $validate_sort,
338             'keywords' => $validate_allow_all,
339             'keywords_exclude' => $validate_allow_all,
340             'version' => $validate_version,
341             'action' => $validate_action,
342             'encoding' => $validate_encoding,
343             'pretty' => $validate_pretty,
344             'has_photo' => $validate_allow_all,
345             'guid' => $validate_allow_all,
346             );
347              
348             sub _validate {
349 768     768   1237 my $key = shift;
350 768         1213 my $val = shift;
351              
352 768 100 66     3970 unless ( defined $key && defined $val ) {
353 14         30 return "validation error";
354             }
355              
356 754 100       2024 if ( $key eq 'warnings' ) {
357 123         251 return;
358             }
359              
360 631 50       1780 unless ( $ValidateRoutine{$key} ) {
361 0         0 return "unknown argument '$key'";
362             }
363              
364 631 100       1676 if ( $ValidateRoutine{$key}->($val) ) {
365 607         1525 return;
366             }
367             else {
368 24         70 return "invalid value '$val' for '$key' argument";
369             }
370             }
371              
372             =head1 FUNCTIONS
373              
374             =head2 new
375              
376             Creates a WebService::Nestoria::Search object. On error sets C<$@> and returns C.
377              
378             If given 'request' parameters (eg. place_name, listing_type) these become defaults for all calls to the API.
379              
380             my %args = (warnings => 0, listing_type => 'rent', place_name => 'soho');
381             my $NS = WebService::Nestoria::Search->new(%args);
382              
383             =cut
384              
385             sub new {
386 16     16 1 1001 my $class = shift;
387 16         35 my $self;
388              
389 16 50       114 if ( @_ % 2 != 0 ) {
390 0         0 return _carp_on_error("wrong arg count to $class->new");
391             }
392              
393 16         95 $self->{Defaults} = { @_ };
394              
395 16         40 foreach my $key (keys %{ $self->{Defaults} }) {
  16         102  
396 21 50       172 if ($key =~ m/[A-Z]/) {
397 0         0 my $newkey = lc $key;
398 0         0 $self->{Defaults}{$newkey} = $self->{Defaults}{$key};
399 0         0 delete $self->{Defaults}{$key};
400             }
401             }
402              
403 16 100       87 if ( exists $self->{Defaults}{warnings} ) {
404 2         9 $GlobalDefaults{warnings} = $self->{Defaults}{warnings};
405 2         8 delete $self->{Defaults}{warnings};
406             }
407              
408 16 100       76 if ( exists $self->{Defaults}{country} ) {
409 9 50       74 unless ( exists $Config{Urls}->{$self->{Defaults}{country}} ) {
410 0         0 _carp_on_error("Invalid country");
411             }
412 9         43 $GlobalDefaults{country} = $self->{Defaults}{country};
413 9         37 delete $self->{Defaults}{country};
414             }
415              
416 16         45 my $defaults = $self->{Defaults};
417 16         61 foreach my $key (keys %$defaults) {
418 10         54 my $error = _validate($key, $self->{Defaults}{$key});
419 10 50       67 if ( $error ) {
420 0         0 return _carp_on_error("$error, in call to $class->new");
421             }
422             }
423              
424 16         69 foreach my $key (keys %GlobalDefaults) {
425 32   33     536 $self->{$key} ||= $GlobalDefaults{$key};
426             }
427              
428 16         78 return bless $self, $class;
429             }
430              
431             =head2 request
432              
433             Creates a WebService::Nestoria::Search::Request object. On error sets C<$@> and returns C
434              
435             my $request = WebService::Nestoria::Search->request(%args);
436              
437             =cut
438              
439             sub request {
440 142     142 1 46383 my $self = shift;
441              
442 142 50       576 if ( @_ % 2 != 0 ) {
443 0         0 return _carp_on_error("wrong arg count for request");
444             }
445              
446 142         476 my %args = @_;
447              
448 142 50       486 unless ( ref $self ) {
449 0         0 $self = $self->new;
450             }
451              
452 142         482 foreach my $key ( keys %GlobalDefaults ) {
453 284   66     1514 $args{$key} ||= $GlobalDefaults{$key};
454             }
455              
456 142         288 foreach my $key ( keys %{ $self->{Defaults} } ) {
  142         536  
457 36 50       137 next if grep { $key eq $_ } keys %GlobalDefaults;
  72         343  
458 36   66     362 $args{$key} ||= $self->{Defaults}{$key};
459             }
460              
461 142         263 foreach my $key ( keys %{ $Config{Defaults} } ) {
  142         1003  
462 3834 100       9610 if ( defined $Config{Defaults}{$key} ) {
463 426   100     2081 $args{$key} ||= $Config{Defaults}{$key};
464             }
465             }
466              
467 142         696 foreach my $key ( keys %args ) {
468 758         1869 my $error = _validate($key, $args{$key});
469 758 100       2295 if ( $error ) {
470 38         71 return _carp_on_error($error);
471             }
472             }
473              
474 104         234 my %params;
475 104         406 $params{ActionUrl} = $Config{Urls}->{$args{country}};
476 104         255 $params{AppId} = $Config{AppId};
477 104         261 $params{Params} = \%args;
478              
479 104 50 66     485 if (defined $args{number_of_results} && $args{number_of_results} > $Config{MaxResults}) {
480 0         0 return _carp_on_error("number_of_results $args{number_of_results} too large, maximum is $Config{MaxResults}");
481             }
482              
483 104         803 return WebService::Nestoria::Search::Request->new(\%params);
484             }
485              
486             =head2 query
487              
488             Queries the API and returns a WebService::Nestoria::Search::Response object. On error, sets C<$@> and returns C.
489              
490             my $response = $NS->query(%args);
491              
492             This is a shortcut for
493              
494             my $request = $NS->request(%args);
495             my $response = $request->fetch;
496              
497             =cut
498              
499             sub query {
500 26     26 1 13907 my $self = shift;
501              
502 26 100       144 unless ( ref $self ) {
503 7         32 $self = $self->new;
504             }
505              
506 26 50       130 if ( my $request = $self->request(@_) ) {
507 26         137 return $request->fetch();
508             }
509             else {
510 0         0 return;
511             }
512             }
513              
514             =head2 results
515              
516             Returns an array of WebService::Nestoria::Search::Result objects. On error, sets C<$@> and returns C.
517              
518             my @results = $NS->results(%args);
519              
520             This is a shortcut for
521              
522             my $request = $NS->request(%args);
523             my $response = $request->fetch;
524             my @results = $response->results;
525              
526             =cut
527              
528             sub results {
529 3     3 1 2254 my $self = shift;
530              
531 3 50       22 unless ( ref $self ) {
532 0         0 $self = $self->new;
533             }
534              
535 3         17 my $response = $self->query(@_);
536              
537 3 50       46 unless ( $response ) {
538 0         0 return;
539             }
540              
541 3         24 return $response->results();
542             }
543              
544             =head2 test_connection
545              
546             Uses the API feature 'action=echo' to test the connection.
547              
548             Returns 1 if the connection is successful and 0 otherwise.
549              
550             unless ($NS->test_connection) {
551             die "Cannot establish connection with Nestoria API\n";
552             }
553              
554             =cut
555              
556             sub test_connection {
557 7     7 1 73 my $self = shift;
558              
559 7         34 my %params = ( action => 'echo' );
560              
561 7         36 my $response = $self->query(%params);
562 7 100 66     247 if ( defined $response && $response->status_code == 200 ) {
563 6         140 return 1;
564             }
565             else {
566 1         10 return 0;
567             }
568             }
569              
570             =head2 keywords
571              
572             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.
573              
574             my @keywords = $NS->keywords;
575              
576             Taken from B.
577              
578             =cut
579              
580             sub keywords {
581 1     1 1 13 my $self = shift;
582              
583 1         7 my %params = ( action => 'keywords' );
584              
585 1         10 my $response = $self->query(%params);
586              
587 1         19 my $data = $response->get_hashref;
588              
589 1         10 return ( split(/,\s+/, $response->get_hashref->{'response'}{'keywords'}) );
590             }
591              
592             =head2 metadata
593              
594             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.
595              
596             my $metadata_response = WebService::Nestoria::Search->metadata(%args);
597              
598             =cut
599              
600             sub metadata {
601 2     2 1 1596 my $self = shift;
602              
603 2 50       18 unless ( ref $self ) {
604 0         0 $self = $self->new;
605             }
606              
607 2         12 my %params = ( action => 'metadata' );
608              
609 2         18 my $response = $self->query(%params, @_);
610              
611 2         33 return WebService::Nestoria::Search::MetadataResponse->new($response->get_hashref);
612             }
613              
614             =head2 last_request_uri
615              
616             Returns a URI object representing the URL that was last fetched by
617             WebService::Nestoria::Search::Request.
618              
619             =cut
620              
621             sub last_request_uri {
622 1     1 1 100 return URI->new($RecentRequestUrl);
623             }
624              
625             =head2 last_request_url
626              
627             Returns the URL that was last fetched by WebService::Nestoria::Search::Request.
628              
629             =cut
630              
631             sub last_request_url {
632 1     1 1 2037 return $RecentRequestUrl;
633             }
634              
635             =head1 Warnings
636              
637             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
638              
639             use WebService::Nestoria::Search Warnings => 0;
640              
641             or when calling C
642              
643             my $NS = WebService::Nestoria::Search->new(Warnings => 0);
644              
645             =head1 Country
646              
647             Country is an optional parameter which defaults to 'uk'. It affects the URL which is used for fetching results.
648              
649             Currently the available countries are:
650              
651             =over 4
652              
653             =item * br - Brazil
654              
655             =item * de - Germany
656              
657             =item * es - Spain
658              
659             =item * fr - France
660              
661             =item * in - India
662              
663             =item * uk - United Kingdom
664              
665             =back
666              
667             Australia (au) supports the metadata requests but not listings requests
668              
669             Mexico (mx) and Italy (it) currently have no API support.
670              
671             =head1 Non-OO
672              
673             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.
674              
675             my @results = WebService::Nestoria::Search->results(%args);
676              
677             =head1 Copyright
678              
679             Copyright (C) 2014 Lokku Ltd.
680              
681             =head1 Author
682              
683             Alex Balhatchet (alex@lokku.com)
684              
685             Patches supplied by Yoav Felberbaum, Alistair Francis, Ed Freyfogle
686              
687             =head1 Acknowledgements
688              
689             A lot of the ideas (and yes, very occasionally entire functions) for these modules were borrowed from Jeffrey Friedl's Yahoo::Search.
690              
691             This module would not exist without the public API available from Nestoria (B.)
692              
693             =cut
694              
695             1;