File Coverage

blib/lib/SolarBeam.pm
Criterion Covered Total %
statement 119 121 98.3
branch 30 32 93.7
condition 7 10 70.0
subroutine 22 22 100.0
pod 4 4 100.0
total 182 189 96.3


line stmt bran cond sub pod time code
1             package SolarBeam;
2 2     2   18198 use Mojo::Base -base;
  2         8813  
  2         19  
3              
4 2     2   900 use Mojo::UserAgent;
  2         221433  
  2         20  
5 2     2   58 use Mojo::Parameters;
  2         3  
  2         19  
6 2     2   53 use Mojo::URL;
  2         4  
  2         11  
7 2     2   980 use SolarBeam::Query;
  2         6  
  2         14  
8 2     2   948 use SolarBeam::Response;
  2         6  
  2         20  
9 2     2   75 use SolarBeam::Util 'escape';
  2         2  
  2         2883  
10              
11             our $VERSION = '0.04';
12              
13             has ua => sub { Mojo::UserAgent->new };
14             has default_query => sub { {} };
15              
16             sub autocomplete {
17 2     2 1 605 my $cb = pop;
18 2         14 my ($self, $prefix, %options) = @_;
19 2   100     17 my $postfix = delete $options{'-postfix'} || '\w+';
20              
21 2         5 $options{'regex.flag'} = 'case_insensitive';
22 2         7 $options{'regex'} = quotemeta($prefix) . $postfix;
23 2         8 my $options = {terms => \%options, -endpoint => 'terms'};
24 2         10 my $url = $self->_build_url($options);
25              
26             Mojo::IOLoop->delay(
27 2     2   250 sub { $self->ua->get($url, shift->begin) },
28             sub {
29 2     2   79388 my ($delay, $tx) = @_;
30 2         19 $self->$cb(SolarBeam::Response->new->parse($tx));
31             }
32 2         23 );
33              
34 2         258 return $self;
35             }
36              
37             sub new {
38 3     3 1 812 my $self = shift->SUPER::new(@_);
39 3 100       42 $self->url($self->{url}) if $self->{url};
40 3         7 $self;
41             }
42              
43             sub search {
44 1     1 1 14 my $cb = pop;
45 1         2 my ($self, $query, %options) = @_;
46 1         3 my $options = \%options;
47 1         2 my $page = $options->{page};
48              
49 1         4 $options->{-query} = $query;
50 1         11 my $url = $self->_build_url($options);
51 1         4 my $q = $url->query;
52 1         8 $url->query(Mojo::Parameters->new);
53              
54             Mojo::IOLoop->delay(
55 1     1   148 sub { $self->ua->post($url, form => $q->to_hash, shift->begin) },
56             sub {
57 1     1   9346 my ($delay, $tx) = @_;
58 1         13 my $res = SolarBeam::Response->new->parse($tx);
59              
60 1 50 33     6 if ($page && !$res->error) {
61 0         0 $res->pager->current_page($page);
62 0         0 $res->pager->entries_per_page($options->{rows});
63             }
64              
65 1         5 $self->$cb($res);
66             }
67 1         25 );
68              
69 1         137 return $self;
70             }
71              
72             sub url {
73 19     19 1 18811 my $self = shift;
74 19 100 66     120 return $self->{url} ||= Mojo::URL->new('http://localhost:8983/solr') unless @_;
75 3         15 $self->{url} = Mojo::URL->new(shift);
76 3         392 return $self;
77             }
78              
79             sub _build_hash {
80 3     3   8 my ($self, %fields) = @_;
81 3         3 my @query;
82              
83 3         7 for my $field (keys %fields) {
84 3         5 my $val = $fields{$field};
85 3 100       9 my @vals = ref($val) eq 'ARRAY' ? @{$val} : $val;
  1         3  
86 3         8 push @query, join(' OR ', map { $field . ':(' . escape($_) . ')' } @vals);
  4         13  
87             }
88              
89 3         18 return '(' . join(' AND ', @query) . ')';
90             }
91              
92             sub _build_query {
93 10     10   15 my ($self, $query) = @_;
94              
95 10         18 my $type = ref($query);
96 10 100       34 if ($type eq 'HASH') {
    100          
97 3         5 $self->_build_hash(%{$query});
  3         11  
98             }
99             elsif ($type eq 'ARRAY') {
100 3         7 my ($raw, @params) = @$query;
101 3         10 $raw =~ s|%@|escape(shift @params)|ge;
  2         9  
102 3         11 my %params = @params;
103 3         15 $raw =~ s|%([a-z]+)|escape($params{$1})|ge;
  3         13  
104 3         17 $raw;
105             }
106             else {
107 4         11 $query;
108             }
109             }
110              
111             sub _build_url {
112 12     12   30 my ($self, $options) = @_;
113 12         36 my $endpoint = delete $options->{-endpoint};
114 12         25 my $query = delete $options->{-query};
115 12         32 my $url = $self->url->clone;
116              
117 12   100     792 $url->path($endpoint || 'select');
118 12 100       1747 $url->query(q => $self->_build_query($query)) if $query;
119 12         81 $url->query($self->default_query);
120 12         443 $url->query({wt => 'json'});
121              
122 12 100       328 if ($options->{page}) {
123 1         3 $self->_handle_page($options->{page}, $options);
124             }
125              
126 12 100       35 if ($options->{fq}) {
127 3         7 $self->_handle_fq($options->{fq}, $options);
128             }
129              
130 12 100       34 if ($options->{facet}) {
131 2         9 $self->_handle_facet($options->{facet}, $options);
132             }
133              
134 12 100       27 if ($options->{terms}) {
135 3         12 $self->_handle_nested_hash('terms', $options->{terms}, $options);
136             }
137              
138 12         36 $url->query($options);
139              
140 12         424 return $url;
141             }
142              
143             sub _handle_fq {
144 3     3   5 my ($self, $fq, $options) = @_;
145              
146 3 100       9 if (ref($fq) eq 'ARRAY') {
147 2         4 my @queries = map { $self->_build_query($_) } @{$fq};
  4         8  
  2         4  
148 2         8 $options->{fq} = \@queries;
149             }
150             else {
151 1         3 $options->{fq} = $self->_build_query($fq);
152             }
153 3         5 return;
154             }
155              
156             sub _handle_facet {
157 2     2   5 my ($self, $facet, $options) = @_;
158 2         7 $self->_handle_nested_hash('facet', $facet, $options);
159             }
160              
161             sub _handle_nested_hash {
162 25     25   39 my ($self, $prefix, $content, $options) = @_;
163 25         31 my $type = ref $content;
164              
165 25 100       43 if ($type eq 'HASH') {
166 6 100       31 $content->{-value} or $content->{-value} = 'true';
167              
168 6         7 for my $key (keys %{$content}) {
  6         20  
169 20         25 my $name = $prefix;
170 20 100       47 $name .= '.' . $key if $key ne '-value';
171 20         44 $self->_handle_nested_hash($name, $content->{$key}, $options);
172             }
173             }
174             else {
175 19         54 $options->{$prefix} = $content;
176             }
177             }
178              
179             sub _handle_page {
180 1     1   3 my ($self, $page, $options) = @_;
181 1 50       4 die "You must provide both page and rows" unless $options->{rows};
182 1         4 $options->{start} = ($page - 1) * $options->{rows};
183 1         2 return delete $options->{page};
184             }
185              
186             1;
187              
188             =encoding utf8
189              
190             =head1 NAME
191              
192             SolarBeam - Async Solr search driver
193              
194             =head1 VERSION
195              
196             0.04
197              
198             =head1 SYNOPSIS
199              
200             use SolarBeam;
201             my $solr = SolarBeam->new;
202             $solr->search(...);
203              
204             =head1 DESCRIPTION
205              
206             Interface to acquire Solr index engine connections.
207              
208             L is currently EXPERIMENTAL.
209              
210             =head1 ATTRIBUTES
211              
212             L implements the the following attributes.
213              
214             =head2 ua
215              
216             $ua = $self->ua
217             $self = $self->ua(Mojo::UserAgent->new);
218              
219             A L compatible object.
220              
221             =head2 url
222              
223             $url = $self->url;
224              
225             Solr endpoint as a L object. Note that passing in L as a
226             string to L also works.
227              
228             =head2 default_query
229              
230             A hashref with default parameters used for every query.
231              
232             =head1 METHODS
233              
234             =head2 new
235              
236             $self = SolarBeam->new(%attributes);
237              
238             Object constructor.
239              
240             =head2 search
241              
242             $self = $self->search($query, [%options], sub { my ($self, $res) = @_; });
243              
244             Used to search for data in Solr. C<$res> is a L object.
245              
246             Example C<$query>:
247              
248             =over 2
249              
250             =item * Hash
251              
252             $self->search({surname => q("Thorsen"), age => [33, 34]});
253              
254             The query above will result in this Solr query:
255              
256             (surname:("Thorsen") AND age:(33) OR age:(34))
257              
258             =item * String
259              
260             $self->search("active:1");
261              
262             The query above will result in this Solr query:
263              
264             active:1
265              
266             =back
267              
268             C<%options> can hold Solr query parameters and some special instuctions
269             to this module, such a "page" and "rows".
270              
271             =over 2
272              
273             =item * page
274              
275             Used to calculate the offset together with L. Will also be used to set
276             L attributes in L:
277              
278             $res->pager->current_page($page);
279              
280             =item * rows
281              
282             Used to calculate the offset together with L. Will also be used to set
283             L attributes in L:
284              
285             $res->pager->entries_per_page($rows);
286              
287             =back
288              
289             =head2 autocomplete
290              
291             $self = $self->autocomplete($prefix, [%options], sub { my ($self, $res) = @_; });
292              
293             TODO.
294              
295             C<$res> is a L object.
296              
297             C<%options> can be:
298              
299             =over 2
300              
301             =item * -postfix - defaults to \w+
302              
303             =item * regex.flag -
304              
305             =item * regex -
306              
307             =back
308              
309             =head1 COPYRIGHT AND LICENSE
310              
311             Copyright (C) 2011-2016, Magnus Holm
312              
313             This program is free software, you can redistribute it and/or modify it under
314             the terms of the Artistic License version 2.0.
315              
316             =head1 AUTHOR
317              
318             Magnus Holm - C
319              
320             Jan Henning Thorsen - C
321              
322             Nicolas Mendoza - C
323              
324             =cut