File Coverage

blib/lib/Clustericious/Client.pm
Criterion Covered Total %
statement 275 403 68.2
branch 79 176 44.8
condition 41 111 36.9
subroutine 48 65 73.8
pod 14 14 100.0
total 457 769 59.4


line stmt bran cond sub pod time code
1             package Clustericious::Client;
2              
3 27     27   43556 use strict;
  27         64  
  27         816  
4 27     27   145 use warnings;
  27         68  
  27         638  
5 27     27   458 use 5.010;
  27         171  
6 27     27   178 use Mojo::Base qw/-base/;
  27         60  
  27         196  
7 27     27   6929 use Mojo::UserAgent;
  27         1219633  
  27         226  
8 27     27   941 use Mojo::ByteStream qw/b/;
  27         66  
  27         1399  
9 27     27   243 use Mojo::Parameters;
  27         66  
  27         202  
10 27     27   2400 use Clustericious;
  27         64  
  27         742  
11 27     27   2267 use Clustericious::Config;
  27         58  
  27         645  
12 27     27   2353 use Clustericious::Client::Object;
  27         77  
  27         782  
13 27     27   1813 use Clustericious::Client::Meta;
  27         76  
  27         829  
14 27     27   2312 use Clustericious::Client::Meta::Route;
  27         272  
  27         255  
15 27     27   1034 use MojoX::Log::Log4perl;
  27         62  
  27         219  
16 27     27   710 use Log::Log4perl qw/:easy/;
  27         246  
  27         191  
17 27     27   17545 use File::Temp;
  27         61  
  27         2237  
18 27     27   167 use JSON::MaybeXS qw( encode_json decode_json );
  27         58  
  27         1303  
19 27     27   150 use Carp qw( carp );
  27         61  
  27         1096  
20 27     27   160 use Mojo::Util qw( monkey_patch );
  27         56  
  27         7170  
21              
22             # ABSTRACT: Construct command line and perl clients for RESTful services.
23             our $VERSION = '1.27'; # VERSION
24              
25              
26             has server_url => '';
27             has [qw(tx res userinfo ua )];
28             has _remote => ''; # Access via remote()
29             has _cache => sub { + {} }; # cache of credentials
30              
31             sub client
32             {
33 0     0 1 0 carp "Clustericious::Client->client is deprecated (use ua instead)";
34 0         0 shift->ua(@_);
35             }
36              
37             sub import
38             {
39 13     13   45 my($class) = @_;
40 13         43 my $caller = caller;
41              
42 13         85 monkey_patch $caller, route => \&route;
43 13         288 monkey_patch $caller, route_meta => \&route_meta;
44 13         198 monkey_patch $caller, route_args => \&route_args;
45             monkey_patch $caller, route_doc => sub {
46 4     4   17 Clustericious::Client::Meta->add_route( $caller, @_ );
        4      
47 13         207 };
48 13         200 monkey_patch $caller, object => \&object;
49 13     6   210 monkey_patch $caller, import => sub {};
        6      
50              
51 13         164 do {
52 27     27   179 no strict 'refs';
  27         64  
  27         23120  
53 13 50       283 push @{"${caller}::ISA"}, $class unless $caller->isa($class);
  13         4863  
54             };
55             }
56              
57              
58             sub _mojo_user_agent_factory
59             {
60 37     37   10143 my($class, $new) = @_;
61 37     2   98 state $factory = sub { Mojo::UserAgent->new };
  2         19  
62 37 100       213 $factory = $new if $new;
63 37 100       156 defined wantarray ? $factory->() : ();
64             }
65              
66             sub new
67             {
68 17     17 1 5869 my($class, %args) = @_;
69              
70 17         42 my $config = delete $args{config};
71            
72 17         77 my $self = $class->SUPER::new(%args);
73              
74 17 100       133 $self->{_base_config} = $config if $config;
75              
76 17 50       59 if($self->{app})
77             {
78 0         0 my $app = $self->{app};
79 0 0       0 $app = $app->new() unless ref($app);
80 0         0 my $ua = $self->_mojo_user_agent_factory();
81 0 0       0 return undef unless $ua;
82 0   0     0 eval { $ua->server->app($app) } // $ua->app($app);
  0         0  
83 0         0 $self->ua($ua);
84             }
85             else
86             {
87 17         60 $self->ua($self->_mojo_user_agent_factory());
88 17 100       51119 unless(length $self->server_url)
89             {
90 16         134 my $url = $self->config->url;
91 16         43 $url =~ s{/$}{};
92 16         40 $self->server_url($url);
93             }
94             }
95              
96 17         115 my $ua = $self->ua;
97 17         163 $ua->transactor->name($self->user_agent_string);
98 17   50     209 $ua->inactivity_timeout($ENV{CLUSTERICIOUS_KEEP_ALIVE_TIMEOUT} || 300);
99              
100 17 50       96 if(eval { require Clustericious::Client::Local; })
  17         345  
101             {
102 0         0 Clustericious::Client::Local->local($self);
103             }
104              
105 17         1229 $self;
106             }
107              
108              
109              
110             sub remote {
111 0     0 1 0 my $self = shift;
112 0 0       0 return $self->_remote unless @_ > 0;
113 0         0 my $remote = shift;
114 0 0       0 unless ($remote) { # reset to default
115 0         0 $self->{_remote} = '';
116 0         0 $self->server_url($self->config->url);
117 0         0 return;
118             }
119 0         0 my $info = $self->_base_config->remotes->$remote;
120 0         0 TRACE "Using remote url : ".$info->{url};
121 0         0 $self->server_url($info->{url});
122 0         0 $self->userinfo('');
123 0         0 $self->_remote($remote);
124             }
125              
126              
127             sub remotes {
128 0     0 1 0 my $self = shift;
129 0         0 my %found = $self->_base_config->remotes(default => {});
130 0         0 return keys %found;
131             }
132              
133              
134             sub login {
135 0     0 1 0 my $self = shift;
136 0         0 my %args = @_;
137             my ($user,$pw) =
138             @_==2 ? @_
139 0 0       0 : @_ ? @args{qw/username password/}
    0          
140             : map $self->config->$_, qw/username password/;
141 0         0 $self->userinfo(join ':', $user,$pw);
142             }
143              
144              
145             sub errorstring {
146 2     2 1 4 my $self = shift;
147 2 100       7 WARN "Missing response in ua object" unless $self->res;
148 2 100       33 return unless $self->res;
149 1 50 33     6 return if $self->res->code && $self->res->is_success;
150 1         29 my $error = $self->res->error;
151 1 50       11 if(defined $error->{advice})
    50          
152             {
153 0         0 return sprintf("[%d] %s", $error->{advice}, $error->{message});
154             }
155             elsif(defined $error->{code})
156             {
157 1         10 return sprintf( "(%d) %s", $error->{code}, $error->{message});
158             }
159             else
160             {
161 0   0     0 return $error->{message} // '';
162             }
163             }
164              
165              
166             sub has_error {
167 3     3 1 5 my $c = shift;
168 3 50 33     21 return unless $c->tx || $c->res;
169 0 0 0     0 return 1 if $c->tx && $c->tx->error;
170 0 0 0     0 return 1 if $c->res && !$c->res->is_success;
171 0         0 return 0;
172             }
173              
174              
175             sub user_agent_string {
176 17     17 1 309 my($self) = @_;
177 17         42 my $class = ref($self);
178 17   50     50 my $version1 = $Clustericious::Client::VERSION // 'dev';
179 17   100     29 my $version2 = do {
180 27     27   215 no strict 'refs';
  27         90  
  27         6944  
181 17         23 ${"${class}::VERSION"};
  17         116  
182             } // 'dev';
183 17         117 "Clustericious::Client/$version1 $class/$version2";
184             }
185              
186              
187             sub route {
188 17     17   171 my $subname = shift;
189 17 50       62 my $objclass = ref $_[0] eq 'ARRAY' ? shift->[0] : undef;
190 17 50       52 my $doc = ref $_[-1] eq 'SCALAR' ? ${ pop() } : "";
  0         0  
191 17   33     56 my $url = pop || "/$subname";
192 17   100     54 my $method = shift || 'GET';
193              
194 17         38 my $client_class = scalar caller();
195 17         164 my $meta = Clustericious::Client::Meta::Route->new(
196             client_class => scalar caller(),
197             route_name => $subname
198             );
199              
200 17         191 $meta->set(method => $method);
201 17         60 $meta->set(url => $url);
202 17         71 $meta->set_doc($doc);
203              
204 17 50       46 if ($objclass) {
205 0         0 eval "require $objclass";
206 0 0       0 if ($@) {
207 0 0       0 LOGDIE "Error loading $objclass : $@" unless $@ =~ /Can't locate/i;
208             }
209             }
210              
211             {
212 27     27   176 no strict 'refs';
  27         56  
  27         11290  
  17         28  
213 17         120 *{caller() . "::$subname"} = sub {
214 7     7   4067 my $self = shift;
215 7         33 my @args = $self->meta_for($subname)->process_args(@_);
216 7         49 my $got = $self->_doit($meta,$method,$url,@args);
217 7 50       92 return $objclass->new($got, $self) if $objclass;
218 7         35 return $got;
219 17         81 };
220             }
221              
222             }
223              
224              
225             sub route_meta {
226 2     2   9 my $name = shift;
227 2         4 my $attrs = shift;
228 2         8 my $meta = Clustericious::Client::Meta::Route->new(
229             client_class => scalar caller(),
230             route_name => $name
231             );
232              
233 2         20 $meta->set($_ => $attrs->{$_}) for keys %$attrs;
234             }
235              
236              
237             sub route_args {
238 9     9   88 my $name = shift;
239 9         10 my $args = shift;
240 9 50       19 die "args must be an array ref" unless ref $args eq 'ARRAY';
241 9         21 my $meta = Clustericious::Client::Meta::Route->new(
242             client_class => scalar caller(),
243             route_name => $name
244             );
245              
246 9         53 $meta->set(args => $args);
247             }
248              
249              
250             sub object {
251 1     1   2 my $objname = shift;
252 1   33     5 my $url = shift || "/$objname";
253 1 50       4 my $doc = ref $_[-1] eq 'SCALAR' ? ${ pop() } : '';
  0         0  
254 1         2 my $caller = caller;
255              
256             my $objclass = "${caller}::" .
257 1         5 join('', map { ucfirst } split('_', $objname)); # foo_bar => FooBar
  1         7  
258              
259 1         73 eval "require $objclass";
260 1 50       112 if ($@) {
261 1 50       10 LOGDIE "Error loading $objclass : $@" unless $@ =~ /Can't locate/i;
262             }
263              
264 1 50       19 $objclass = 'Clustericious::Client::Object' unless $objclass->can('new');
265              
266 1         12 Clustericious::Client::Meta->add_object(scalar caller(),$objname,$doc);
267              
268 27     27   179 no strict 'refs';
  27         59  
  27         73791  
269 1         59 *{"${caller}::$objname"} = sub {
270 2     2   5 my $self = shift;
271 2         22 my $meta = Clustericious::Client::Meta::Route->new(
272             client_class => $caller,
273             route_name => $objname
274             );
275 2         26 $meta->set( quiet_post => 1 );
276 2         12 my $data = $self->_doit( $meta, GET => $url, @_ );
277 2         107 $objclass->new( $data, $self );
278 1         5 };
279 1         6 *{"${caller}::${objname}_delete"} = sub {
280 0     0   0 my $meta = Clustericious::Client::Meta::Route->new(
281             client_class => $caller,
282             route_name => $objname.'_delete',
283             );
284 0         0 $meta->set(dont_read_files => 1);
285 0         0 shift->_doit( $meta, DELETE => $url, @_ );
286 1         5 };
287 1         5 *{"${caller}::${objname}_search"} = sub {
288 0     0   0 my $meta = Clustericious::Client::Meta::Route->new(
289             client_class => $caller,
290             route_name => $objname.'_search'
291             );
292 0         0 $meta->set(dont_read_files => 1);
293 0         0 shift->_doit( $meta, POST => "$url/search", @_ );
294 1         3 };
295             }
296              
297             sub _doit {
298 11     11   23 my $self = shift;
299 11         17 my $meta;
300 11 50       42 $meta = shift if ref $_[0] eq 'Clustericious::Client::Meta::Route';
301 11         40 my ($method, $url, @args) = @_;
302              
303 11         17 my $auto_failover;
304 11 50 33     56 $auto_failover = 1 if $meta && $meta->get('auto_failover');
305              
306 11 50 33     46 $url = $self->server_url . $url if $self->server_url && $url !~ /^http/;
307 11 50       160 return undef if $self->server_url eq 'http://0.0.0.0';
308              
309 11         58 my $cb;
310 11         22 my $body = '';
311 11         20 my $headers = {};
312              
313 11 50 66     73 if ($method eq 'POST' && grep /^--/, @args) {
314 0         0 s/^--// for @args;
315 0         0 @args = ( { @args } );
316             }
317              
318 11 50       83 $url = Mojo::URL->new($url) unless ref $url;
319 11         1188 my $parameters = $url->query;
320              
321             # Set up mappings from parameter names to modifier callbacks.
322 11         192 my %url_modifier;
323             my %payload_modifer;
324             my %gen_url_modifier = (
325 0     0   0 query => sub { my $name = shift;
326 0         0 sub { my ($u,$v) = @_; $u->query({$name => $v}) } },
  0         0  
  0         0  
327 13     13   18 append => sub { my $name = shift;
328 13         38 sub { my ($u,$v) = @_; push @{ $u->path->parts } , $v; $u; } },
  13         28  
  13         20  
  13         34  
  13         574  
329 11         94 );
330             my %gen_payload_modifier = (
331             array => sub {
332 1     1   2 my ( $name, $key ) = @_;
333 1 50       3 LOGDIE "missing key for array payload modifier" unless $key;
334 1   50     2 sub { my $body = shift; $body ||= {}; push @{ $body->{$key} }, ( $name => shift ); $body; }
  1         4  
  1         2  
  1         4  
  1         3  
335 1         4 },
336             hash => sub {
337 1     1   2 my $name = shift;
338 1   50     2 sub { my $body = shift; $body ||= {}; $body->{$name} = shift; $body; }
  1         6  
  1         2  
  1         4  
339 1         6 },
340 11         85 );
341 11 100 66     59 if ($meta && (my $arg_spec = $meta->get('args'))) {
342 4         11 for (@$arg_spec) {
343 15         21 my $name = $_->{name};
344 15 100       29 if (my $modifies_url = $_->{modifies_url}) {
345             $url_modifier{$name} =
346             ref($modifies_url) eq 'CODE' ? $modifies_url
347 13 50       35 : ($a = $gen_url_modifier{$modifies_url}) ? $a->($name)
    50          
348             : die "don't understand how to interpret modifies_url=$modifies_url";
349             }
350 15 100       34 if (my $modifies_payload = $_->{modifies_payload}) {
351             $payload_modifer{$name} =
352             ref($modifies_payload) eq 'CODE' ? $modifies_payload
353             : ($a = $gen_payload_modifier{$modifies_payload}) ? $a->($name,$_->{key})
354 2 50       13 : LOGDIE "don't understand how to interpret modifies_payload=$modifies_payload";
    50          
355             }
356             }
357             }
358              
359 11         47 while (defined(my $arg = shift @args)) {
360 17 100 33     81 if (ref $arg eq 'HASH') {
    50 33        
    100 33        
    100          
    50          
    50          
    50          
361 1         2 $method = 'POST';
362 1 50 33     5 $parameters->append(skip_existing => 1) if $meta && $meta->get("skip_existing");
363 1         12 $body = encode_json $arg;
364 1         6 $headers = { 'Content-Type' => 'application/json' };
365             } elsif (ref $arg eq 'CODE') {
366 0         0 $cb = $self->_mycallback($arg);
367             } elsif (my $code = $url_modifier{$arg}) {
368 13         29 $url = $code->($url, shift @args);
369             } elsif (my $code2 = $payload_modifer{$arg}) {
370 2         4 $body = $code2->($body, shift @args);
371             } elsif ($method eq "GET" && $arg =~ s/^--//) {
372 0         0 my $value = shift @args;
373 0         0 $parameters->append($arg => $value);
374             } elsif ($method eq "GET" && $arg =~ s/^-//) {
375             # example: $client->esdt(-range => [1 => 100]);
376 0         0 my $value = shift @args;
377 0 0       0 if (ref $value eq 'ARRAY') {
378 0         0 $value = "items=$value->[0]-$value->[1]";
379             }
380 0         0 $headers->{$arg} = $value;
381             } elsif ($method eq "POST" && !ref $arg) {
382 0         0 $body = $arg;
383 0 0 0     0 $headers = shift @args if $args[0] && ref $args[0] eq 'HASH';
384             } else {
385 1         3 push @{ $url->path->parts }, $arg;
  1         3  
386             }
387             }
388 11 0 33     121 $url = $url->to_abs unless $url->is_abs || $self->{app};
389 11 50       107 WARN "url $url is not absolute" unless $url =~ /^http/i;
390              
391 11 50       3561 $url->userinfo($self->userinfo) if $self->userinfo;
392              
393 11         88 DEBUG ( (ref $self)." : $method " ._sanitize_url($url));
394 11   50     1221 $headers->{Connection} ||= 'Close';
395 11   50     57 $headers->{Accept} ||= 'application/json';
396              
397 11 100 100     69 if($body && ref $body eq 'HASH' || ref $body eq 'ARRAY')
      66        
398             {
399 1         2 $headers->{'Content-Type'} = 'application/json';
400 1         13 $body = encode_json $body;
401             }
402              
403 11 50       28 return $self->ua->build_tx($method, $url, $headers, $body, $cb) if $cb;
404              
405 11         44 my $tx = $self->ua->build_tx($method, $url, $headers, $body);
406              
407 11         2476 $tx = $self->ua->start($tx);
408 11         25397 my $res = $tx->res;
409 11         99 $self->res($res);
410 11         85 $self->tx($tx);
411              
412 11         189 my $auth_header;
413 11 0 100     36 if (($tx->res->code||0) == 401 && ($auth_header = $tx->res->headers->www_authenticate)
      33        
      33        
      0        
      0        
414             && !$url->userinfo && ($self->_has_auth || $self->_can_auth)) {
415 0         0 DEBUG "received code 401, trying again with credentials";
416 0         0 my ($realm) = $auth_header =~ /realm=(.*)$/i;
417 0         0 my $host = $url->host;
418 0 0       0 $self->login( $self->_has_auth ? () : $self->_get_user_pw($host,$realm) );
419 0 0       0 return $self->_doit($meta ? $meta : (), @_);
420             }
421              
422 11 100       159 if ($res->is_success) {
423 6         120 TRACE "Got response : ".$res->to_string;
424 6   33     65 my $content_type = $res->headers->content_type || do {
425             WARN "No content-type from "._sanitize_url($url);
426             "text/plain";
427             };
428 6 100       180 return $method =~ /HEAD|DELETE/ ? 1
    50          
429             : $content_type =~ qr[application/json] ? decode_json($res->body)
430             : $res->body;
431             }
432              
433             # Failed.
434 5         63 my $err = $tx->error;
435 5         77 my ($msg, $code) = ($err->{message}, $err->{code});
436 5   50     15 $msg ||= 'unknown error';
437 5         14 my $s_url = _sanitize_url($url);
438              
439 5 100       30 if ($code) {
440 1 50       5 if ($code == 404) {
441             TRACE "$method $url : $code $msg"
442             unless $ENV{ACPS_SUPPRESS_404}
443 1 50 33     11 && $url =~ /$ENV{ACPS_SUPPRESS_404}/;
444             } else {
445 0         0 ERROR "Error trying to $method $s_url : $code $msg";
446 0 0       0 TRACE "Full error body : ".$res->body if $res->body;
447 0   0     0 my $brief = $res->body || '';
448 0         0 $brief =~ s/\n/ /g;
449 0 0       0 ERROR substr($brief,0,200) if $brief;
450             }
451             # No failover for legitimate status codes.
452 1         25 return undef;
453             }
454              
455 4 50       9 unless ($auto_failover) {
456 4         17 ERROR "Error trying to $method $s_url : $msg";
457 4 50       1213 ERROR $res->body if $res->body;
458 4         179 return undef;
459             }
460 0         0 my $failover_urls = $self->config->failover_urls(default => []);
461 0 0       0 unless (@$failover_urls) {
462 0         0 ERROR $msg;
463 0         0 return undef;
464             }
465 0         0 INFO "$msg but will try up to ".@$failover_urls." failover urls";
466 0         0 TRACE "Failover urls : @$failover_urls";
467 0         0 for my $url (@$failover_urls) {
468 0         0 DEBUG "Trying $url";
469 0         0 $self->server_url($url);
470 0         0 my $got = $self->_doit(@_);
471 0 0       0 return $got if $got;
472             }
473              
474 0         0 return undef;
475             }
476              
477             sub _mycallback
478             {
479 0     0   0 my $self = shift;
480 0         0 my $cb = shift;
481             sub
482             {
483 0     0   0 my ($ua, $tx) = @_;
484              
485 0         0 $self->res($tx->res);
486 0         0 $self->tx($tx);
487              
488 0 0       0 if ($tx->res->is_success)
489             {
490 0 0       0 my $body = $tx->res->headers->content_type =~ qr[application/json]
491             ? decode_json($tx->res->body) : $tx->res->body;
492              
493 0 0       0 $cb->($body ? $body : 1);
494             }
495             else
496             {
497 0         0 $cb->();
498             }
499             }
500 0         0 }
501              
502             sub _sanitize_url {
503             # Remove passwords from urls for displaying
504 16     16   29 my $url = shift;
505 16 50       48 $url = Mojo::URL->new($url) unless ref $url eq "Mojo::URL";
506 16 50       48 return $url unless $url->userinfo;
507 0         0 my $c = $url->clone;
508 0         0 $c->userinfo("user:*****");
509 0         0 $c;
510             }
511              
512             sub _appname
513             {
514 0     0   0 my($self) = @_;
515 0         0 (my $appname = ref $self) =~ s/:.*$//;
516 0         0 $appname;
517             }
518              
519              
520             sub config
521             {
522 24     24 1 46 my($self) = @_;
523 24         69 my $conf = $self->_base_config;
524 24 50       90 if (my $remote = $self->_remote)
525             {
526 0         0 return $conf->remotes->$remote;
527             }
528 24         211 $conf;
529             }
530              
531             sub _config
532             {
533 0     0   0 carp "Clustericious::Client->_config has been deprecated use config instead";
534 0         0 shift->config(@_);
535             }
536              
537             sub _base_config
538             {
539             # Independent of remotes
540 24     24   41 my($self) = @_;
541 24 100       188 unless(defined $self->{_base_config})
542             {
543 3         15 my $config_name = ref $self;
544 3         20 $config_name =~ s/::Client$//;
545 3         12 $config_name =~ s/::/-/;
546 3         25 $self->{_base_config} = Clustericious::Config->new($config_name);
547 3   66     30 $self->{_base_config}->{url} //= Clustericious->_default_url($config_name);
548             }
549            
550 24         53 $self->{_base_config};
551             }
552              
553             sub _has_auth
554             {
555 0     0   0 my($self) = @_;
556 0         0 my $config = $self->config;
557 0 0 0     0 $config->username(default => '') && password(default => '') ? 1 : 0;
558             }
559              
560             sub _can_auth
561             {
562 0     0   0 my $self = shift;
563 0 0       0 -t STDIN ? 1 : 0;
564             }
565              
566             sub _get_user_pw {
567 0     0   0 my $self = shift;
568 0         0 my $host = shift;
569 0         0 my $realm = shift;
570 0 0       0 $realm = '' unless defined $realm;
571 0 0       0 return @{ $self->_cache->{$host}{$realm} } if exists($self->_cache->{$host}{$realm});
  0         0  
572             # "use"ing causes too many warnings; load on demand.
573 0         0 require Term::Prompt;
574 0   0     0 my $user = Term::Prompt::prompt('x', "Username for $realm at $host : ", '', $ENV{USER} // $ENV{USERNAME});
575 0         0 my $pw = Term::Prompt::prompt('p', 'Password:', '', '');
576 0         0 $self->_cache->{$host}{$realm} = [ $user, $pw ];
577 0         0 return ($user,$pw);
578             }
579              
580              
581             sub meta_for {
582 23     23 1 4744 my $self = shift;
583 23   66     126 my $route_name = shift || [ caller 1 ]->[3];
584 23 100       115 if ( $route_name =~ /::([^:]+)$/ ){
585 10         22 $route_name = $1;
586             }
587 23         130 my $meta = Clustericious::Client::Meta::Route->new(
588             route_name => $route_name,
589             client_class => ref $self
590             );
591             }
592              
593              
594             sub version {
595 0     0 1 0 my $self = shift;
596 0         0 my $meta = $self->meta_for("version");
597 0         0 $meta->set(auto_failover => 1);
598 0         0 $self->_doit($meta, GET => '/version');
599             }
600              
601              
602             sub status {
603 2     2 1 27 my $self = shift;
604 2         11 my $meta = $self->meta_for("status");
605 2         24 $meta->set(auto_failover => 0);
606 2         11 $self->_doit($meta, GET => '/status');
607             }
608              
609              
610             sub api {
611 0     0 1   my $self = shift;
612 0           my $meta = $self->meta_for("api");
613 0           $meta->set( auto_failover => 1 );
614 0           $self->_doit( $meta, GET => '/api' );
615             }
616              
617              
618             sub logtail {
619 0     0 1   my $self = shift;
620 0           my $got = $self->_doit(GET => '/log', @_);
621 0           return { text => $got };
622             }
623              
624              
625             1;
626              
627             __END__
628              
629             =pod
630              
631             =encoding UTF-8
632              
633             =head1 NAME
634              
635             Clustericious::Client - Construct command line and perl clients for RESTful services.
636              
637             =head1 VERSION
638              
639             version 1.27
640              
641             =head1 SYNOPSIS
642              
643             tracks.pm :
644              
645             package Tracks;
646             use Clustericious::Client;
647              
648             route 'mixes' => '/mixes.json';
649             route_doc mixes => 'Get a list of mixes.';
650             route_args mixes => [
651             { name => 'api_key', type => '=s', modifies_url => "query", required => 1 },
652             { name => 'per_page',type => '=i', modifies_url => "query", },
653             { name => 'tags', type => '=s', modifies_url => "query" },
654             ];
655             # a 'mixes' method will be constructed automatically.
656             # a 'mixes' command line parameter will be recognized automatically.
657              
658             route 'play' => '/play.json';
659             route_args play => [
660             { name => 'token', type => '=s', modifies_url => 'query', required => 1 }
661             ];
662             sub play {
663             my $c = shift;
664             my %args = $c->meta_for->process_args(@_);
665             # do something with $args{token}
666             }
667             # A 'play' command line parameter will call the above method.
668              
669             tracks.pl :
670              
671             use lib '.';
672             use Log::Log4perl qw/:easy/;
673             Log::Log4perl->easy_init($TRACE);
674             use tracks;
675              
676             my $t = Tracks->new(server_url => 'http://8tracks.com' );
677             my $mixes = $t->mixes(
678             tags => 'jazz',
679             api_key => $api_key,
680             per_page => 2,
681             ) or die $t->errorstring;
682             print "Mix : $_->{name}\n" for @{ $mixes->{mixes} };
683              
684             tracks_cli :
685              
686             use lib '.';
687             use Clustericious::Client::Command;
688             use tracks;
689              
690             use Log::Log4perl qw/:easy/;
691             Log::Log4perl->easy_init($TRACE);
692              
693             Clustericious::Client::Command->run(Tracks->new, @ARGV);
694              
695             ~/etc/Tracks.conf :
696              
697             ---
698             url : 'http://8tracks.com'
699              
700             From the command line :
701              
702             $ perl tracks.pl
703             $ tracks_cli mixes --api_key foo --tags jazz
704              
705             =head1 DESCRIPTION
706              
707             Clustericious::Client is library for construction clients for RESTful
708             services. It provides a mapping between command line arguments, method
709             arguments, and URLs.
710              
711             The builder functions add methods to the client object that translate
712             into basic REST functions. All of the 'built' methods return undef on
713             failure of the REST/HTTP call, and auto-decode the returned body into
714             a data structure if it is application/json.
715              
716             =head1 ATTRIBUTES
717              
718             This class inherits from L<Mojo::Base>, and handles attributes like
719             that class. The following additional attributes are used.
720              
721             =head2 config
722              
723             Configuration object. Defaults to the appropriate L<Clustericious::Config>
724             class.
725              
726             =head2 ua
727              
728             User agent to process the HTTP stuff with. Defaults to a
729             L<Mojo::UserAgent>.
730              
731             =head2 client
732              
733             Deprecated alias for L</ua> above. Do not use in new code. May be removed
734             in the future.
735              
736             =head2 app
737              
738             For testing, you can specify a Mojolicious app name.
739              
740             =head2 server_url
741              
742             You can override the URL prefix for the client, otherwise it
743             will look it up in the config file.
744              
745             =head2 res, tx
746              
747             After an HTTP error, the built methods return undef. This function
748             will return the L<Mojo::Message::Response> from the server.
749              
750             res->code and res->message are the returned HTTP code and message.
751              
752             tx has the Mojo::Transaction::HTTP object.
753              
754             =head1 METHODS
755              
756             =head2 new
757              
758             my $f = Foo::Client->new();
759             my $f = Foo::Client->new(server_url => 'http://someurl');
760             my $f = Foo::Client->new(app => 'MyApp'); # For testing...
761              
762             If the configuration file has a "url" entry, this will
763             be used as the default url (first case above).
764              
765             =head2 userinfo
766              
767             Credentials currently stored.
768              
769             =head2 remote
770              
771             Tell the client to use the remote information in the configuration.
772             For instance, if the config has
773              
774             remotes :
775             test :
776             url: http://foo
777             bar :
778             url: http://baz
779             username : one
780             password : two
781              
782             Then setting remote("test") uses the first
783             url, and setting remote("bar") uses the
784             second one.
785              
786             =head2 remotes
787              
788             Return a list of available remotes.
789              
790             =head2 login
791              
792             Log in to the server. This will send basic auth info
793             along with every subsequent request.
794              
795             $f->login; # looks for username and password in $app.conf
796             $f->login("elmer", "fudd");
797             $f->login(username => "elmer", password => "fudd");
798              
799             =head2 errorstring
800              
801             After an error, this returns an error string made up of the server
802             error code and message. (use res->code and res->message to get the
803             parts)
804              
805             (e.g. "Error: (500) Internal Server Error")
806              
807             =head2 has_error
808              
809             Returns true if there was a recent error.
810              
811             =head2 user_agent_string
812              
813             Returns the user agent string for use in HTTP transactions.
814             By default this includes the clustericious and service
815             version numbers, but you can override it to be whatever
816             you want.
817              
818             =head1 FUNCTIONS
819              
820             =head2 route
821              
822             route 'subname'; # GET /subname
823             route subname => '/url'; # GET /url
824             route subname => GET => '/url'; # GET /url
825             route subname => POST => '/url'; # POST /url
826             route subname => DELETE => '/url'; # DELETE /url
827             route subname => ['SomeObjectClass'];
828             route subname \"<documentation> <for> <some> <args>";
829             route_args subname => [ { name => 'param', type => "=s", modifies_url => 'query' } ]
830             route_args subname => [ { name => 'param', type => "=i", modifies_url => 'append' } ]
831              
832             Makes a method subname() that does the REST action.
833              
834             route subname => $url => $doc
835              
836             is equivalent to
837              
838             route subname => $url
839             route_args subname => [ { name => 'all', positional => 'many', modifies_url => 'append' } ];
840             route_doc subname => $$doc
841              
842             with the additional differences that GET becomes a POST if the argument is
843             a hashref, and heuristics are used to read YAML files and STDIN.
844              
845             See route_args and route_doc below.
846              
847             =head2 route_meta
848              
849             Set metadata attributes for this route.
850              
851             route_meta 'bucket_map' => { auto_failover => 1 }
852             route_meta 'bucket_map' => { quiet_post => 1 }
853             route_meta 'bucket_map' => { skip_existing => 1 }
854              
855             =head2 route_args
856              
857             Set arguments for this route. This allows command line options
858             to be transformed into method arguments, and allows normalization
859             and validation of method arguments. route_args associates an array
860             ref with the name of a route. Each entry in the array ref is a hashref
861             which may have keys as shown in this example :
862              
863             route_args send => [
864             {
865             name => 'what', # name of the route
866             type => '=s', # type (see L<Getopt::Long>)
867             alt => 'long|extra|big', # alternative names
868             required => 0, # Is it required?
869             doc => 'get a full status', # brief documentation
870             },
871             {
872             name => 'items', # name of the route
873             type => '=s', # type (see L<Getopt::Long>)
874             doc => 'send a list of items' # brief docs
875             preprocess => 'list' # make an array ref from a list
876             },
877             ];
878              
879             The keys have the following effect :
880              
881             =over
882              
883             =item name
884              
885             The name of the option. This should be preceded by two dashes
886             on the command line. It is also sent as the named argument to the
887             method call.
888              
889             =item type
890              
891             A type, as described in L<Getopt::Long>. This will be appended to
892             the name to form the option specification.
893              
894             =item alt
895              
896             An alternative name or names (joined by |).
897              
898             =item required
899              
900             If this arg is required, set this to 1.
901              
902             =item doc
903              
904             A brief description to be printed in error messages and help documentation.
905              
906             =item preprocess
907              
908             Can be either C<yamldoc>, C<list> or C<datetime>.
909              
910             For yamldoc and list, the argument is expected
911             to refer to either a filename which exists, or else "-" for STDIN. The contents
912             are then transformed from YAML (for yamldoc), or split on carriage returns (for list)
913             to form either a data structure or an arrayref, respectively.
914              
915             For datetime the string is run through Date::Parse and turned into an ISO 8601 datetime.
916              
917             =item modifies_url
918              
919             Describes how the URL is affected by the arguments. Can be
920             'query', 'append', or a code reference.
921              
922             'query' adds to the query string, e.g.
923              
924             route subname '/url'
925             route_args subname => [ { name => 'foo', type => "=s", modifies_url => 'query' } ]
926              
927             This will cause this invocation :
928              
929             $foo->subname( "foo" => "bar" )
930              
931             to send a GET request to /url?foo=bar.
932              
933             Similarly, 'append' is equivalent to
934              
935             sub { my ($u,$v) = @_; push @{ $u->path->parts } , $v }
936              
937             i.e. append the parameter to the end of the URL path.
938              
939             If route_args is omitted for a route, then arguments with a '--'
940             are treated as part of the query string, and arguments with a '-'
941             are treated as HTTP headers (for a GET request). If a hash
942             reference is passed, the method changes to POST and the hash is
943             encoded into the body as application/json.
944              
945             =item modifies_payload, key
946              
947             Describes how the parameter modifies the payload.
948              
949             'hash' means set $body->{$name} to $value.
950             'array' means push ( $name => $value ) onto $body->{$key}.
951             (key should also be specified)
952              
953             =item positional
954              
955             Can be 'one' or 'many'.
956              
957             If set, this is a positional parameter, not a named parameter. i.e.
958             getopt will not be used to parse the command line, and
959             it will be take from a list sent to the method. For instance
960              
961             route_args name => [ { name => 'id', positional => 'one' } ];
962              
963             Then
964              
965             $client->name($id)
966              
967             or
968              
969             commandlineclient name id
970              
971             will result in the method receiving (id => $id).
972              
973             If set to 'many', multiple parameters may be sent, e.g.
974              
975             $client->name($id1, $id2,....)
976              
977             =back
978              
979             =head2 object
980              
981             object 'objname'; # defaults to URL /objname
982             object objname => '/some/url';
983              
984             Creates two methods, one named with the supplied objname() (used for
985             create, retrieve, update), and one named objname_delete().
986              
987             Any scalar arguments to the created functions are tacked onto the end
988             of the url. Performs a GET by default, but if you pass a hash
989             reference, the method changes to POST and the hash is encoded into the
990             body as application/json.
991              
992             The 'object' routes will automatically look for a class named with the
993             object name, but upper case first letter and first after any
994             underscores, which are removed:
995              
996             object 'myobj'; Foo::Client::Myobj;
997             object 'my_obj'; Foo::Client::MyObj;
998              
999             If such a class isn't found, object will default to returning a
1000             L<Clustericious::Client::Object>.
1001              
1002             =head2 meta_for
1003              
1004             Get the metadata for a route.
1005              
1006             $client->meta_for('welcome');
1007              
1008             Returns a Clustericious::Client::Meta::Route object.
1009              
1010             =head1 COMMON ROUTES
1011              
1012             These are routes that are automatically supported by all clients.
1013             See L<Clustericious::Plugin::CommonRoutes>.
1014              
1015             =head2 version
1016              
1017             Retrieve the version on the server.
1018              
1019             =head2 status
1020              
1021             Retrieve the status from the server.
1022              
1023             =head2 api
1024              
1025             Retrieve the API from the server
1026              
1027             =head2 logtail
1028              
1029             Get the last N lines of the server log file.
1030              
1031             =head1 EXAMPLES
1032              
1033             package Foo::Client;
1034             use Clustericious::Client;
1035              
1036             route 'welcome' => '/'; # GET /
1037             route status; # GET /status
1038             route myobj => [ 'MyObject' ]; # GET /myobj
1039             route something => GET => '/some/';
1040             route remove => DELETE => '/something/';
1041              
1042             object 'obj'; # Defaults to /obj
1043             object 'foo' => '/something/foo'; # Can override the URL
1044              
1045             route status => \"Get the status"; # Scalar refs are documentation
1046             route_doc status => "Get the status"; # or you can use route_doc
1047             route_args status => [ # route_args sets method or cli arguments
1048             {
1049             name => 'full',
1050             type => '=s',
1051             required => 0,
1052             doc => 'get a full status',
1053             },
1054             ];
1055              
1056             route_args wrinkle => [ # methods correspond to "route"s
1057             {
1058             name => 'time'
1059             }
1060             ];
1061              
1062             sub wrinkle { # provides cli command as well as a method
1063             my $c = shift;
1064             my %args = @_;
1065             if ($args{time}) {
1066             ...
1067             }
1068             }
1069              
1070             ----------------------------------------------------------------------
1071              
1072             use Foo::Client;
1073              
1074             my $f = Foo::Client->new();
1075             my $f = Foo::Client->new(server_url => 'http://someurl');
1076             my $f = Foo::Client->new(app => 'MyApp'); # For testing...
1077              
1078             my $welcome = $f->welcome(); # GET /
1079             my $status = $f->status(); # GET /status
1080             my $myobj = $f->myobj('key'); # GET /myobj/key, MyObject->new()
1081             my $something = $f->something('this'); # GET /some/this
1082             $f->remove('foo'); # DELETE /something/foo
1083              
1084             my $obj = $f->obj('this', 27); # GET /obj/this/27
1085             # Returns either 'Foo::Client::Obj' or 'Clustericious::Client::Object'
1086              
1087             $f->obj({ set => 'this' }); # POST /obj
1088             $f->obj('this', 27, { set => 'this' }); # POST /obj/this/27
1089             $f->obj_delete('this', 27); # DELETE /obj/this/27
1090             my $obj = $f->foo('this'); # GET /something/foo/this
1091              
1092             $f->status(full => "yes");
1093             $f->wrinkle( time => 1 );
1094              
1095             ----------------------
1096              
1097             #!/bin/sh
1098             fooclient status
1099             fooclient status --full yes
1100             fooclient wrinkle --time
1101              
1102             =head1 SEE ALSO
1103              
1104             L<Clustericious::Config>, L<Clustericious>, L<Mojolicious>
1105              
1106             =head1 AUTHOR
1107              
1108             Original author: Brian Duggan
1109              
1110             Current maintainer: Graham Ollis E<lt>plicease@cpan.orgE<gt>
1111              
1112             Contributors:
1113              
1114             Curt Tilmes
1115              
1116             Yanick Champoux
1117              
1118             =head1 COPYRIGHT AND LICENSE
1119              
1120             This software is copyright (c) 2013 by NASA GSFC.
1121              
1122             This is free software; you can redistribute it and/or modify it under
1123             the same terms as the Perl 5 programming language system itself.
1124              
1125             =cut