File Coverage

blib/lib/Cookieville/Client.pm
Criterion Covered Total %
statement 77 79 97.4
branch 30 40 75.0
condition 5 10 50.0
subroutine 20 20 100.0
pod 5 5 100.0
total 137 154 88.9


line stmt bran cond sub pod time code
1             package Cookieville::Client;
2              
3             =head1 NAME
4              
5             Cookieville::Client - Client that talks with Cookieville server
6              
7             =head1 DESCRIPTION
8              
9             L is a client that talks with the L server.
10              
11             =head1 SYNOPSIS
12              
13             use Cookieville::Client;
14             my $ua = Cookieville::Client->new(url => 'http://127.0.0.1/');
15             my $res;
16              
17             $res = $ua->search(
18             'SomeSource',
19             { col_a => { like => '%42' } },
20             { limit => 10 },
21             );
22              
23             $res = $ua->delete(SomeSource => 123);
24             $res = $ua->put(SomeSource => { col_a => 123 });
25             $res = $ua->patch(SomeSource => 42 => { col_a => 123 });
26              
27             =head2 Error handling
28              
29             Blocking requests will throw an exception on error, while all callbacks
30             receive the error as a string. Example:
31              
32             use Mojolicious::Lite;
33              
34             get '/artists' => sub {
35              
36             $c->delay(
37             sub {
38             my ($delay) = @_;
39             $c->cookieville_helper->search(Artist => {}, $delay->begin);
40             },
41             sub {
42             my ($delay, $err, $res) = @_;
43             return $c->reply->exception($err) if $err;
44             return $c->render(json => $res->{data});
45             },
46             );
47             };
48              
49             =cut
50              
51 5     5   3893815 use Mojo::Base -base;
  5         13  
  5         54  
52 5     5   1178 use Mojo::JSON 'encode_json';
  5         10  
  5         379  
53 5     5   31 use Mojo::UserAgent;
  5         12  
  5         63  
54 5     5   130 use Mojo::URL;
  5         10  
  5         44  
55 5     5   121 use Carp ();
  5         9  
  5         8506  
56              
57             =head1 ATTRIBUTES
58              
59             =head2 id
60              
61             $str = $self->id;
62             $self = $self->id($str);
63              
64             Used to set the "X-Cookieville-Auth-Id" HTTP header. This can be used by the
65             L plugin.
66              
67             Defaults to C environment variable.
68              
69             =head2 url
70              
71             $url_obj = $self->url;
72             $self = $self->url($url_obj);
73              
74             Holds the base URL to the L server.
75             Default to "http://127.0.0.1/".
76              
77             =cut
78              
79             has id => $ENV{COOKIEVILLE_AUTH_ID} || '';
80             has url => sub { Mojo::URL->new('http://127.0.0.1/'); };
81             has _ua => sub { Mojo::UserAgent->new; };
82              
83             =head1 METHODS
84              
85             =head2 new
86              
87             Will make sure "url" in constructor is indeed a L object.
88              
89             =cut
90              
91             sub new {
92 5     5 1 101 my $self = shift->SUPER::new(@_);
93              
94 5 50       201 $self->url(Mojo::URL->new($self->url)) if $self->url;
95 5         1585 $self;
96             }
97              
98             =head2 delete
99              
100             $res = $self->delete($source => $id);
101             $self = $self->delete($source => $id, $cb);
102              
103             Used to DELETE a single record from C<$source>, identified by C.
104              
105             =cut
106              
107             sub delete {
108 12     12 1 24896 my ($self, $source, $id, $cb) = @_;
109              
110 12 100       68 return $self->_blocking(delete => $source, $id) unless ref $cb eq 'CODE';
111 7 50 33     49 return $self->_abort('Invalid source or id.', $cb) unless $source and defined $id;
112              
113 7         25 Scalar::Util::weaken($self);
114             $self->_ua->delete(
115             $self->_url("/$source/$id"),
116             {'X-Cookieville-Auth-Id' => $self->id},
117 7     7   35570 sub { $self->$cb($self->_res_from_tx($_[1])); },
118 7         180 );
119              
120 7         18846 return $self;
121             }
122              
123             =head2 patch
124              
125             $res = $self->patch($source => $id => \%data);
126             $self = $self->patch($source => $id => \%data, $cb);
127              
128             Used to UPDATE a single record from C<$source>, identified by C.
129             C<%data> can be partial or full set of column/values.
130              
131             =cut
132              
133             sub patch {
134 14     14 1 33776 my ($self, $source, $id, $data, $cb) = @_;
135              
136 14 100       73 return $self->_blocking(patch => $source, $id, $data) unless ref $cb eq 'CODE';
137 8 50 33     69 return $self->_abort('Invalid source or id.', $cb) unless $source and defined $id;
138 8 100       34 return $self->_abort('Invalid data.', $cb) unless ref $data eq 'HASH';
139              
140 7         29 Scalar::Util::weaken($self);
141             $self->_ua->patch(
142             $self->_url("/$source/$id"), {'X-Cookieville-Auth-Id' => $self->id},
143 7     7   40849 encode_json($data), sub { $self->$cb($self->_res_from_tx($_[1])); },
144 7         199 );
145              
146 7         20724 return $self;
147             }
148              
149             =head2 put
150              
151             $res = $self->put($source => \%data);
152             $self = $self->put($source => \%data, $cb);
153              
154             Used to INSERT or UPDATE a single row. An UPDATE will be issued if C<%data>
155             contain an unique constraint a matching record in database.
156              
157             L v.s L: L will never INSERT a new record, while
158             L will make sure a given record exists.
159              
160             NOTE: C<%data> without any unique constraints will result in INSERT.
161              
162             =cut
163              
164             sub put {
165 14     14 1 16847 my ($self, $source, $data, $cb) = @_;
166              
167 14 100       65 return $self->_blocking(put => $source, $data) unless ref $cb eq 'CODE';
168 8 50       24 return $self->_abort('Invalid source.', $cb) unless $source;
169 8 100       28 return $self->_abort('Invalid data.', $cb) unless ref $data eq 'HASH';
170              
171 7         30 Scalar::Util::weaken($self);
172             $self->_ua->put(
173             $self->_url("/$source"), {'X-Cookieville-Auth-Id' => $self->id},
174 7     7   34968 encode_json($data), sub { $self->$cb($self->_res_from_tx($_[1])); },
175 7         196 );
176              
177 7         19090 return $self;
178             }
179              
180             =head2 search
181              
182             $res = $self->search($source => \%query, \%extra);
183             $self = $self->search($source => \%query, \%extra, $cb);
184              
185             Does a SELECT from the given C with a given C<%query> and C<%extra>
186             parameters. This method is very similar to L,
187             but with less C<%extra> options:
188              
189             =over 4
190              
191             =item * columns
192              
193             Only output the given columns. Example:
194              
195             $extra{columns} = [qw( id name )];
196              
197             =item * limit
198              
199             Used to limit the number of rows in the output.
200              
201             $extra{limit} = 10;
202              
203             =item * page=:int (optional)
204              
205             Used for pagination when C is specified.
206              
207             $extra{limit} = 2;
208              
209             =item * order_by
210              
211             Sort the result by column(s). Examples:
212              
213             $extra{order_by} = ["name"];
214             $extra{order_by} = { "-desc" => "name" };
215              
216             =back
217              
218             =cut
219              
220             sub search {
221 12 100   12 1 11280 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
222 12         18 my $self = shift;
223 12         15 my $source = shift;
224 12         18 my $query = shift;
225 12   100     45 my $extra = shift || {};
226              
227 12 100       52 return $self->_blocking(search => $source, $query, $extra) unless ref $cb eq 'CODE';
228 7 50       21 return $self->_abort('Invalid source.', $cb) unless $source;
229 7 50       65 return $self->_abort('Invalid query.', $cb) unless ref($query) =~ /HASH|ARRAY/;
230              
231 7 100       30 $extra->{columns} = encode_json $extra->{columns} if $extra->{columns};
232 7 100       64 $extra->{order_by} = encode_json $extra->{order_by} if $extra->{order_by};
233 7         43 $extra->{q} = encode_json $query;
234              
235 7         171 Scalar::Util::weaken($self);
236             $self->_ua->get(
237             $self->_url("/$source/search")->query($extra),
238             {'X-Cookieville-Auth-Id' => $self->id},
239 7     7   32393 sub { $self->$cb($self->_res_from_tx($_[1])); },
240 7         156 );
241              
242 7         17282 return $self;
243             }
244              
245             sub _blocking {
246 22     22   59 my ($self, $method, @args) = @_;
247 22         43 my ($err, $res);
248              
249             $self->$method(
250             @args,
251             sub {
252 22     22   42 my $self = shift;
253 22         54 ($err, $res) = @_;
254 22         438 $self->_ua->ioloop->stop;
255             }
256 22         192 );
257              
258 22         482 $self->_ua->ioloop->start;
259 22 100       7780 Carp::confess($err) if $err;
260 9         62 return $res;
261             }
262              
263             sub _abort {
264 2     2   4 my ($self, $err, $cb) = @_;
265              
266 2         11 Scalar::Util::weaken($self);
267 2 50   2   55 $self->_ua->ioloop->timer(0, sub { $self and $self->$cb($err) });
  2         628  
268 2         303 $self;
269             }
270              
271             sub _res_from_tx {
272 28     28   66 my ($self, $tx) = @_;
273 28         47 my ($err, $res);
274              
275 28         526 $res = $tx->res->json;
276              
277 28 50       7077 if (not ref $res) {
    100          
278 0         0 $err = $tx->error;
279 0 0       0 $err = $err->{message} if ref $err;
280             }
281             elsif ($tx->res->code != 200) {
282 15         419 $err = $res->{message};
283             }
284              
285 28   50     490 return $err, $res || {};
286             }
287              
288             sub _url {
289 28     28   840 $_[0]->url->clone->path($_[1]);
290             }
291              
292             =head1 COPYRIGHT AND LICENSE
293              
294             Copyright (C) 2014, Jan Henning Thorsen
295              
296             This program is free software, you can redistribute it and/or modify it under
297             the terms of the Artistic License version 2.0.
298              
299             =head1 AUTHOR
300              
301             Jan Henning Thorsen - C
302              
303             =cut
304              
305             1;