File Coverage

blib/lib/Dezi/Client.pm
Criterion Covered Total %
statement 30 143 20.9
branch 0 64 0.0
condition 0 51 0.0
subroutine 10 17 58.8
pod 7 7 100.0
total 47 282 16.6


line stmt bran cond sub pod time code
1             package Dezi::Client;
2              
3 2     2   37684 use warnings;
  2         5  
  2         80  
4 2     2   13 use strict;
  2         4  
  2         93  
5              
6             our $VERSION = '0.003004';
7              
8 2     2   12 use Carp;
  2         11  
  2         175  
9 2     2   12788 use LWP::UserAgent;
  2         139626  
  2         66  
10 2     2   1596 use LWP::MediaTypes;
  2         83009  
  2         220  
11 2     2   20 use HTTP::Request;
  2         3  
  2         68  
12 2     2   1810 use URI::Query;
  2         4159  
  2         57  
13 2     2   1982 use JSON;
  2         16245  
  2         17  
14 2     2   2339 use Search::Tools;
  2         3702  
  2         88  
15 2     2   1484 use Dezi::Response;
  2         9  
  2         3600  
16              
17             =head1 NAME
18              
19             Dezi::Client - interact with a Dezi server
20              
21             =head1 SYNOPSIS
22              
23             use Dezi::Client;
24            
25             # open a connection
26             my $client = Dezi::Client->new(
27             server => 'http://localhost:5000',
28             );
29            
30             # add/update a filesystem document to the index
31             $client->index( 'path/to/file.html' );
32            
33             # add/update an in-memory document to the index
34             $client->index( \$html_doc, 'foo/bar.html' );
35            
36             # add/update a Dezi::Doc to the index
37             $client->index( $dezi_doc );
38            
39             # remove a document from the index
40             $client->delete( '/doc/uri/relative/to/index' );
41            
42             # search the index
43             my $response = $client->search( q => 'foo' );
44            
45             # iterate over results
46             for my $result (@{ $response->results }) {
47             printf("--\n uri: %s\n title: %s\n score: %s\n",
48             $result->uri, $result->title, $result->score);
49             }
50            
51             # print stats
52             printf(" hits: %d\n", $response->total);
53             printf("search time: %s\n", $response->search_time);
54             printf(" build time: %s\n", $response->build_time);
55             printf(" query: %s\n", $response->query);
56            
57            
58             =head1 DESCRIPTION
59              
60             Dezi::Client is a client for the Dezi search platform.
61              
62             =head1 METHODS
63              
64             =head2 new( I )
65              
66             Instantiate a Client instance. Expects the following params:
67              
68             =over
69              
70             =item server I
71              
72             The I of the Dezi server. If the B or B
73             params are not passed to new(), then the server will be
74             interrogated at initial connect for the correct paths
75             for searching and indexing.
76              
77             =item server_params I
78              
79             Passed internally to URI::Query and appended to server I.
80              
81             =item search I
82              
83             The URI path for searching. Dezi defaults to B.
84              
85             =item index I
86              
87             The URI path for indexing. Dezi defaults to B.
88              
89             =item username I
90              
91             =item password I
92              
93             If present, the username and password credentials will
94             be set in each internal HTTP::Request object for any
95             non-idempotent action (delete(), index(), commit(), rollback()).
96            
97             =back
98              
99             =cut
100              
101             sub new {
102 0     0 1   my $class = shift;
103 0           my %args = @_;
104 0 0 0       if ( !%args or !exists $args{server} ) {
105 0           croak "server param required";
106             }
107 0           my $self = bless { server => delete $args{server} }, $class;
108              
109 0   0       $self->{debug} = delete $args{debug} || 0;
110 0 0         if ( $self->{debug} ) {
111 0           require Data::Dump;
112             }
113              
114 0           $self->{ua} = LWP::UserAgent->new();
115 0 0 0       if ( $args{search} and $args{index} ) {
116 0           $self->{search_uri} = $self->{server} . delete $args{search};
117 0           $self->{index_uri} = $self->{server} . delete $args{index};
118             $self->{commit_uri}
119 0   0       = $self->{server} . ( delete $args{commit} || 'commit' );
120             $self->{rollback_uri}
121 0   0       = $self->{server} . ( delete $args{rollback} || 'rollback' );
122             }
123             else {
124 0           my $uri = $self->{server};
125 0 0         if ( $args{server_params} ) {
126             $self->{server_params}
127 0           = URI::Query->new( delete $args{server_params} );
128 0           $uri .= '?' . $self->{server_params};
129             }
130 0           my $resp = $self->{ua}->get($uri);
131 0 0         if ( !$resp->is_success ) {
132 0           croak $resp->status_line;
133             }
134 0           my $paths = from_json( $resp->decoded_content );
135 0 0 0       if ( !$resp->is_success
      0        
      0        
136             or !$paths
137             or !$paths->{search}
138             or !$paths->{index} )
139             {
140 0           croak "Bad response from server $self->{server}: "
141             . $resp->status_line . " "
142             . $resp->decoded_content;
143             }
144 0           $self->{search_uri} = $paths->{search};
145 0           $self->{index_uri} = $paths->{index};
146 0           $self->{commit_uri} = $paths->{commit};
147 0           $self->{rollback_uri} = $paths->{rollback};
148 0           $self->{fields} = $paths->{fields};
149 0           $self->{facets} = $paths->{facets};
150             }
151              
152             $self->{_creds} = {
153             username => delete $args{username},
154             password => delete $args{password},
155 0           };
156              
157 0 0         if (%args) {
158 0           croak "Invalid params to new(): " . join( ", ", keys %args );
159             }
160              
161 0           return $self;
162             }
163              
164             =head2 index( I [, I, I, I] )
165              
166             Add or update a document. I should be one of:
167              
168             =over
169              
170             =item I
171              
172             I should be a readable file on an accessible filesystem.
173             I will be read with Search::Tools->slurp.
174              
175             =item I
176              
177             I should be a reference to a string representing
178             the document to be indexed. If this is the case, then I
179             must be passed as the second argument.
180              
181             =item I
182              
183             A Dezi::Doc object.
184              
185             =back
186              
187             I and I are optional, except in the
188             I case, where I is required. If specified,
189             the values are passed explicitly in the HTTP headers to the Dezi
190             server. If not specified, they are (hopefully intelligently) guessed at.
191              
192             Returns a L object which can be interrogated to
193             determine the result. Example:
194              
195             my $resp = $client->index( file => 'path/to/foo.html' );
196             if (!$resp->is_success) {
197             die "Failed to add path/to/foo.html to the Dezi index!";
198             }
199              
200             I is an optional value. It is passed to URI::Query->new()
201             internally and appended to the search_server/index URL.
202              
203             =cut
204              
205             sub index {
206 0     0 1   my $self = shift;
207 0 0         my $doc = shift or croak "doc required";
208 0           my $uri = shift; # optional
209 0           my $content_type = shift; # optional
210 0           my $payload_params = shift; # optional
211              
212 0           my $body_ref;
213              
214 0 0 0       if ( !ref $doc ) {
    0          
    0          
215 0           my $buf = Search::Tools->slurp($doc);
216 0 0         if ( !defined $buf ) {
217 0           croak "unable to read $doc: $!";
218             }
219 0           $body_ref = \$buf;
220 0   0       $uri ||= $doc;
221             }
222             elsif ( ref $doc eq 'SCALAR' ) {
223 0 0 0       if ( !defined $uri and !length $uri ) {
224 0           croak "uri required when passing scalar ref";
225             }
226 0           $body_ref = $doc;
227             }
228             elsif ( ref $doc and $doc->isa('Dezi::Doc') ) {
229 0           $body_ref = $doc->as_string_ref;
230 0   0       $uri ||= $doc->uri;
231 0   0       $content_type ||= $doc->mime_type;
232             }
233             else {
234 0           croak "doc must be a scalar string, scalar ref or Dezi::Doc object";
235             }
236              
237 0           my $server_uri = $self->{index_uri} . '/' . $uri;
238 0 0         if ($payload_params) {
    0          
239 0           $server_uri .= '?' . URI::Query->new($payload_params);
240             }
241             elsif ( $self->{server_params} ) {
242 0           $server_uri .= '?' . $self->{server_params};
243             }
244 0           my $req = HTTP::Request->new( 'POST', $server_uri );
245 0   0       $content_type ||= guess_media_type( $uri, $req );
246 0 0         $req->header( 'Content-Type' => $content_type )
247             unless $req->header('Content-Type');
248 0           $req->content($$body_ref); # TODO encode into bytes ??
249              
250 0 0 0       if ( defined $self->{_creds}->{username}
251             && defined $self->{_creds}->{password} )
252             {
253             $req->authorization_basic( $self->{_creds}->{username},
254 0           $self->{_creds}->{password} );
255             }
256              
257 0 0         $self->{debug} and Data::Dump::dump $req;
258              
259 0           return $self->{ua}->request($req);
260              
261             }
262              
263             =head2 search( I )
264              
265             Fetch search results from a Dezi server. I can be
266             any key/value pair as described in L. The only
267             required key is B for the query string.
268              
269             Returns a L object on success, or 0 on failure. Check
270             the last_response() accessor for the raw L object.
271              
272             my $resp = $client->search('q' => 'foo')
273             or die "search failed: " . $client->last_response->status_line;
274              
275             =cut
276              
277             sub search {
278 0     0 1   my $self = shift;
279 0           my %args = @_;
280 0 0         if ( !exists $args{q} ) {
281 0           croak "q required";
282             }
283 0           my $search_uri = $self->{search_uri};
284 0           my $query = URI::Query->new(%args);
285 0           $query->replace( t => 'JSON' ); # force json response
286 0           $query->strip('format'); # old-style name
287 0 0         if ( $self->{server_params} ) {
288 0           $query .= $self->{server_params};
289             }
290 0           my $resp = $self->{ua}->get( $search_uri . '?' . $query );
291 0 0         if ( !$resp->is_success ) {
292 0           $self->{last_response} = $resp;
293 0           return 0;
294             }
295 0 0         $self->{debug} and Data::Dump::dump $resp;
296 0           return Dezi::Response->new(http_response => $resp);
297             }
298              
299             =head2 last_response
300              
301             Returns the last L object that the Client object
302             interacted with. Useful when search() returns false (HTTP failure).
303             Example:
304              
305             my $resp = $client->search( q => 'foo' );
306             if (!$resp) {
307             die "Dezi search failed: " . $client->last_response->status_line;
308             }
309              
310             =cut
311              
312             sub last_response {
313 0     0 1   return shift->{last_response};
314             }
315              
316             =head2 delete( I )
317              
318             Remove a document from the server. I must be the document's URI.
319              
320             Returns a L object which can be interrogated to
321             determine the result. A 200 response indicates success.
322              
323             =cut
324              
325             sub delete {
326 0     0 1   my $self = shift;
327 0 0         my $uri = shift or croak "uri required";
328              
329 0           my $server_uri = $self->{index_uri} . '/' . $uri;
330 0 0         if ( $self->{server_params} ) {
331 0           $server_uri .= '?' . $self->{server_params};
332             }
333 0           my $req = HTTP::Request->new( 'DELETE', $server_uri );
334 0 0 0       if ( defined $self->{_creds}->{username}
335             && defined $self->{_creds}->{password} )
336             {
337             $req->authorization_basic( $self->{_creds}->{username},
338 0           $self->{_creds}->{password} );
339             }
340 0 0         $self->{debug} and Data::Dump::dump $req;
341 0           $self->{last_response} = $self->{ua}->request($req);
342 0           return $self->{last_response};
343             }
344              
345             =head2 commit
346              
347             Send a COMMIT HTTP request to the server. This is only
348             useful if the server has been configured with:
349              
350             engine_config => {
351             auto_commit => 0,
352             }
353              
354             Otherwise the server will not act on the index
355             and will return a 400 response, indicating an
356             invalid request.
357              
358             If successful and at least one document
359             was committed, returns a 200 response.
360              
361             If successful and no documents were committed,
362             returns a 204, indicating zero un-committed changes
363             were pending.
364              
365             commit() returns a L object which can be interrogated to
366             determine the result.
367              
368             =cut
369              
370             sub commit {
371 0     0 1   my $self = shift;
372 0           my $server_uri = $self->{commit_uri} . '/';
373 0 0         if ( $self->{server_params} ) {
374 0           $server_uri .= '?' . $self->{server_params};
375             }
376 0           my $req = HTTP::Request->new( 'POST', $server_uri );
377 0 0 0       if ( defined $self->{_creds}->{username}
378             && defined $self->{_creds}->{password} )
379             {
380             $req->authorization_basic( $self->{_creds}->{username},
381 0           $self->{_creds}->{password} );
382             }
383 0 0         $self->{debug} and Data::Dump::dump $req;
384 0           $self->{last_response} = $self->{ua}->request($req);
385 0           return $self->{last_response};
386             }
387              
388             =head2 rollback
389              
390             Send a ROLLBACK HTTP request to the server. This is only
391             useful if the server has been configured with:
392              
393             engine_config => {
394             auto_commit => 0,
395             }
396              
397             Otherwise the server will not act on the index
398             and will return a 400 response, indicating an
399             invalid request.
400              
401             If successful the server returns a 200 response.
402              
403             rollback() returns a L object which can be interrogated to
404             determine the result.
405              
406             =cut
407              
408             sub rollback {
409 0     0 1   my $self = shift;
410 0           my $server_uri = $self->{rollback_uri} . '/';
411 0 0         if ( $self->{server_params} ) {
412 0           $server_uri .= '?' . $self->{server_params};
413             }
414 0           my $req = HTTP::Request->new( 'POST', $server_uri );
415 0 0 0       if ( defined $self->{_creds}->{username}
416             && defined $self->{_creds}->{password} )
417             {
418             $req->authorization_basic( $self->{_creds}->{username},
419 0           $self->{_creds}->{password} );
420             }
421 0 0         $self->{debug} and Data::Dump::dump $req;
422 0           $self->{last_response} = $self->{ua}->request($req);
423 0           return $self->{last_response};
424             }
425              
426             1;
427              
428             __END__