File Coverage

blib/lib/Net/ThreeScale/Client.pm
Criterion Covered Total %
statement 34 36 94.4
branch n/a
condition n/a
subroutine 12 12 100.0
pod n/a
total 46 48 95.8


line stmt bran cond sub pod time code
1             package Net::ThreeScale::Client;
2              
3 7     7   528219 use strict;
  7         20  
  7         225  
4 7     7   43 use warnings;
  7         19  
  7         269  
5 7     7   45 use vars qw($VERSION @ISA @EXPORT_OK %EXPORT_TAGS);
  7         17  
  7         536  
6              
7 7     7   60 use Carp;
  7         28  
  7         453  
8 7     7   1185 use Data::Dumper;
  7         13204  
  7         419  
9 7     7   60 use Exporter;
  7         19  
  7         270  
10 7     7   5086 use HTTP::Tiny;
  7         384669  
  7         351  
11 7     7   3792 use Net::ThreeScale::Response;
  7         28  
  7         240  
12 7     7   3751 use Try::Tiny;
  7         15949  
  7         468  
13              
14 7     7   4070 use URI;
  7         50702  
  7         345  
15 7     7   4268 use URI::Escape::XS qw(uri_escape);
  7         21460  
  7         624  
16 7     7   8206 use XML::Parser;
  0            
  0            
17             use XML::Simple;
18              
19             my $DEFAULT_USER_AGENT;
20              
21             use constant {
22             TS_RC_SUCCESS => 'client.success',
23             TS_RC_AUTHORIZE_FAILED => 'provider_key_invalid',
24             TS_RC_UNKNOWN_ERROR => 'client.unknown_error'
25             };
26              
27             BEGIN {
28             @ISA = qw(Exporter);
29             $VERSION = "2.1.7";
30             @EXPORT_OK = qw();
31             %EXPORT_TAGS = (
32             'all' => \@EXPORT_OK,
33             'ALL' => \@EXPORT_OK,
34             );
35             $DEFAULT_USER_AGENT = "threescale_perl_client/$VERSION";
36              
37             }
38              
39             sub new {
40             my $class = shift;
41             my $params = ( $#_ == 0 ) ? { %{ (shift) } } : {@_};
42              
43             my $agent_string = $params->{user_agent} || $DEFAULT_USER_AGENT;
44              
45             croak("provider_key or service_token/service_id pair are required")
46             unless $params->{provider_key} xor ( $params->{service_token} && $params->{service_id});
47              
48             my $self = {};
49             $self->{provider_key} = $params->{provider_key} || undef;
50             $self->{service_token} = $params->{service_token} || undef;
51             $self->{service_id} = $params->{service_id} || undef;
52              
53             $self->{url} = $params->{url} || 'https://su1.3scale.net';
54             $self->{DEBUG} = $params->{DEBUG};
55             $self->{HTTPTiny} = HTTP::Tiny->new(
56             'agent' => $agent_string,
57             'keep_alive' => 1,
58             'timeout' => 5,
59             );
60              
61             return bless $self, $class;
62             }
63              
64             sub _authorize_given_url{
65             my $self = shift;
66             my $url = shift;
67              
68             $self->_debug( "start> sending GET request: ", $url );
69              
70             my $response = $self->{HTTPTiny}->get($url);
71             $self->_debug( "start> got response : ", $response->{content} );
72              
73             # Valid HTTP response codes in which we should still have XML that may
74             # be parsed.
75             # https://support.3scale.net/docs/3scale-apis-activedocs
76             if (!$response->{success} && ($response->{status} !~ /^403|404|409$/)) {
77             return $self->_wrap_error($response);
78             }
79              
80             my $data = $self->_parse_authorize_response( $response->{content} );
81            
82             if ($data->{authorized} ne "true") {
83            
84             my $reason = $data->{reason};
85             $self->_debug("authorization failed: $reason");
86            
87             return Net::ThreeScale::Response->new(
88             success => 0,
89             error_code => TS_RC_UNKNOWN_ERROR,
90             error_message => $reason,
91             usage_reports => \@{$data->{usage_reports}->{usage_report}},
92             )
93             }
94              
95             $self->_debug( "success" );
96             return Net::ThreeScale::Response->new(
97             error_code => TS_RC_SUCCESS,
98             success => 1,
99             usage_reports => \@{$data->{usage_reports}->{usage_report}},
100             application_plan => $data->{plan},
101             );
102             }
103              
104             sub authorize {
105             my $self = shift;
106             my $p = ( $#_ == 0 ) ? { %{ (shift) } } : {@_};
107              
108             die("app_id is required") unless defined($p->{app_id});
109              
110             my %query = (
111             (provider_key => $self->{provider_key})x!! $self->{provider_key},
112             (service_token => $self->{service_token})x!! $self->{service_token},
113             (service_id => $self->{service_id})x!! $self->{service_id},
114             );
115              
116             while (my ($k, $v) = each(%{$p})) {
117             $query{$k} = $v;
118             }
119              
120             my $url = URI->new($self->{url} . "/transactions/authorize.xml");
121              
122             $url->query_form(%query);
123             return $self->_authorize_given_url( $url );
124             }
125              
126              
127             sub authrep {
128             my $self = shift;
129             my $p = ( $#_ == 0 ) ? { %{ (shift) } } : {@_};
130              
131             die("user_key is required") unless defined($p->{user_key});
132              
133             my %query = (
134             (provider_key => $self->{provider_key})x!! $self->{provider_key},
135             (service_token => $self->{service_token})x!! $self->{service_token},
136             (service_id => $self->{service_id})x!! $self->{service_id},
137             );
138              
139             while (my ($k, $v) = each(%{$p})) {
140             $query{$k} = $v;
141             }
142              
143             if ( $query{'usage'} ){
144             while (my ($metric_name, $value) = each %{$query{'usage'}} ){
145             $query{"usage[$metric_name]"} = $value;
146             }
147             delete $query{'usage'};
148             }
149              
150             my $url = URI->new($self->{url} . "/transactions/authrep.xml");
151             $url->query_form(%query);
152              
153             return $self->_authorize_given_url( $url );
154             }
155              
156              
157             sub report {
158             my $self = shift;
159             my $p = ( $#_ == 0 ) ? { %{ (shift) } } : {@_};
160              
161             die("transactions is a required parameter") unless defined($p->{transactions});
162             die("transactions parameter must be a list")
163             unless (ref($p->{transactions}) eq 'ARRAY');
164              
165             my %query = (
166             (provider_key => $self->{provider_key})x!! $self->{provider_key},
167             (service_token => $self->{service_token})x!! $self->{service_token},
168             (service_id => $self->{service_id})x!! $self->{service_id},
169             );
170              
171             while (my ($k, $v) = each(%{$p})) {
172             if ($k eq "transactions") {
173             next;
174             }
175              
176             $query{$k} = $v;
177             }
178              
179             my $content = "";
180              
181             while (my ($k, $v) = each(%query)) {
182             if (length($content)) {
183             $content .= "&\r\n";
184             }
185              
186             $content .= "$k=" . uri_escape($v);
187             }
188              
189             my $txnString = $self->_format_transactions(@{$p->{transactions}});
190              
191             $content .= "&" . $txnString;
192              
193             my $url = $self->{url} . "/transactions.xml";
194              
195             $self->_debug( "start> sending request: ", $url );
196              
197             my $response = $self->{HTTPTiny}->request("POST", $url, {
198             content => $content,
199             headers => {
200             'Content-Type' => 'application/x-www-form-urlencoded',
201             },
202             });
203              
204             $self->_debug( "start> got response : ", $response->{content} );
205              
206             if ( !$response->{success} ) {
207             return $self->_wrap_error($response);
208             }
209              
210             $self->_debug( "success" );
211              
212             return Net::ThreeScale::Response->new(
213             error_code => TS_RC_SUCCESS,
214             success => 1,
215             );
216             }
217              
218             #Wraps an HTTP::Response message into a Net::ThreeScale::Response error return value
219             sub _wrap_error {
220             my $self = shift;
221             my $res = shift;
222             my $error_code;
223             my $message;
224              
225             try {
226             ( $error_code, $message ) = $self->_parse_errors( $res->{content});
227             } catch {
228             $error_code = TS_RC_UNKNOWN_ERROR;
229             $message = 'unknown_error';
230             };
231              
232             return Net::ThreeScale::Response->new(
233             success => 0,
234             error_code => $error_code,
235             error_message => $message
236             );
237             }
238              
239             # Parses an error document out of a response body
240             # If no sensible error messages are found in the response, insert the standard error value
241             sub _parse_errors {
242             my $self = shift;
243             my $body = shift;
244             my $cur_error;
245             my $in_error = 0;
246             my $errstring = undef;
247             my $errcode = TS_RC_UNKNOWN_ERROR;
248              
249             return undef if !defined($body);
250             my $parser = new XML::Parser(
251             Handlers => {
252             Start => sub {
253             my $expat = shift;
254             my $element = shift;
255             my %atts = @_;
256              
257             if ( $element eq 'error' ) {
258             $in_error = 1;
259             $cur_error = "";
260             if ( defined( $atts{code} ) ) {
261             $errcode = $atts{code};
262             }
263             }
264             },
265             End => sub {
266             if ( $_[1] eq 'error' ) {
267             $errstring = $cur_error;
268             $cur_error = undef;
269             $in_error = 0;
270             }
271             },
272             Char => sub {
273             if ($in_error) {
274             $cur_error .= $_[1];
275             }
276             }
277             }
278             );
279              
280             try {
281             $parser->parse($body);
282             }
283             catch {
284             $errstring = $_;
285             };
286              
287             return ( $errcode, $errstring );
288             }
289              
290             sub _parse_authorize_response {
291             my $self = shift;
292             my $response_body = shift;
293              
294             if (length($response_body)) {
295             my $xml = new XML::Simple(ForceArray=>['usage_report']);
296             return $xml->XMLin($response_body);
297             }
298             return {};
299             }
300              
301             sub _format_transactions {
302             my $self = shift;
303             my (@transactions) = @_;
304              
305             my $output = "";
306              
307             my $transNumber = 0;
308              
309             for my $trans (@transactions) {
310             die("Transactions should be given as hashes")
311             unless(ref($trans) eq 'HASH');
312              
313             die("Transactions need an 'app_id'")
314             unless(defined($trans->{app_id}));
315              
316             die("Transactions need a 'usage' hash")
317             unless(defined($trans->{usage}) and ref($trans->{usage}) eq 'HASH');
318              
319             die("Transactions need a 'timestamp'")
320             unless(defined($trans->{app_id}));
321              
322             my $pref = "transactions[$transNumber]";
323              
324             if ($transNumber > 0) {
325             $output .= "&";
326             }
327              
328             $output .= $pref . "[app_id]=" . $trans->{app_id};
329              
330             foreach my $k ( sort keys %{$trans->{usage}} ){
331             my $v = $trans->{usage}->{$k};
332             $k = uri_escape($k);
333             $v = uri_escape($v);
334             $output .= "&";
335             $output .= $pref . "[usage][$k]=$v";
336             }
337              
338             $output .= "&"
339             . $pref
340             . "[timestamp]="
341             . uri_escape($trans->{timestamp});
342              
343             $transNumber += 1;
344             }
345              
346             return $output;
347             }
348              
349             sub _debug {
350             my $self = shift;
351             if ( $self->{DEBUG} ) {
352             print STDERR "DBG:", @_, "\n";
353             }
354              
355             }
356             1;
357              
358             =head1 NAME
359              
360             Net::ThreeScale::Client - Client for 3Scale.com web API version 2.0
361              
362             =head1 SYNOPSIS
363              
364             use Net::ThreeScale::Client;
365            
366             my $client = new Net::ThreeScale::Client(provider_key=>"my_assigned_provider_key",
367             url=>"http://su1.3Scale.net");
368              
369             # Or initialize by service_token/service_id
370             # my $client = new Net::ThreeScale::Client(service_token=>"SERVICE_TOKEN",
371             # service_id=>"SERVICE_ID");
372              
373             my $response = $client->authorize(app_id => $app_id,
374             app_key => $app_key);
375            
376             if($response->is_success) {
377             print "authorized ", $response->transaction,"\"n";
378             ...
379              
380             my @transactions = (
381             {
382             app_id => $app_id,
383             usage => {
384             hits => 1,
385             },
386              
387             timestamp => "2010-09-01 09:01:00",
388             },
389              
390             {
391             app_id => $app_id,
392             usage => {
393             hits => 1,
394             },
395              
396             timestamp => "2010-09-02 09:02:00",
397             }
398             );
399              
400             my $report_response = $client->report(transactions=>\@transactions));
401             if($report_response->is_success){
402             print STDERR "Transactions reported\n";
403             } else {
404             print STDERR "Failed to report transactions",
405             $response->error_code(),":",
406             $response->error_message(),"\n";
407             }
408             } else {
409             print STDERR "authorize failed with error :",
410             $response->error_message,"\n";
411             if($response->error_code == TS_RC_AUTHORIZE_FAILED) {
412             print "Provider key is invalid";
413             } else {
414             ...
415             }
416             }
417              
418             =head1 CONSTRUCTOR
419            
420             The class method new(...) creates a new 3Scale client object. This may
421             be used to conduct transactions with the 3Scale service. The object is
422             stateless and transactions may span multiple clients. The following
423             parameters are recognised as arguments to new():
424              
425             =over 4
426            
427             =item provider_key
428              
429             (required) The provider key used to identify you with the 3Scale service
430              
431             =item service_token
432              
433             (required) Service API key with 3scale (also known as service token).
434              
435             =item service_id
436              
437             (required) Service id. Required.
438              
439             =item url
440              
441             (optional) The 3Scale service URL, usually this should be left to the
442             default value
443              
444             =back
445              
446             =head1 $response = $client->authorize(app_id=>$app_id, app_key=>$app_key)
447              
448             Starts a new client transaction the call must include a application id (as
449             a string) and (optionally) an application key (string), identifying the
450             application to use.
451            
452             Returns a Net::ThreeScale::Response object which indicates whether the
453             authorization was successful or indicates an error if one occured.
454            
455             =head1 $response = $client->report(transactions=>\@transactions)
456              
457             Reports a list of transactions to 3Scale.
458              
459             =over 4
460              
461             =item transactions=>{app_id=>value,...}
462              
463             Should be an array similar to the following:
464              
465             =over 4
466              
467             my @transactions = (
468             {
469             app_id => $app_id,
470             usage => {
471             hits => 1,
472             }
473             timestamp => "2010-09-01 09:01:00",
474             },
475             {
476             app_id => $app_id,
477             usage => {
478             hits => 1,
479             }
480             timestamp => "2010-09-01 09:02:00",
481             },
482             );
483              
484             =back
485              
486             =back
487              
488             =head1 EXPORTS / ERROR CODES
489              
490             The following constants are exported and correspond to error codes
491             which may appear in calls to Net::ThreeScale::Response::error_code
492              
493             =over 4
494              
495             =item TS_RC_SUCCESS
496              
497             The operation completed successfully
498              
499             =item TS_RC_AUTHORIZE_FAILED
500              
501             The passed provider key was invalid
502              
503             =item TS_RC_UNKNOWN_ERROR
504              
505             An unspecified error occurred. See the corresponding message for more detail.
506              
507             =back
508              
509             =head1 SUPPORT
510              
511             3scale support say,
512             I
513             certainly monitor pull requests and consider merging any useful contributions.>
514              
515             =head1 SEE ALSO
516              
517             =over 4
518              
519             =item L
520              
521             Contains details of response content and values.
522              
523             =item L
524              
525             The service with which this package integrates.
526            
527             =back
528              
529             =head1 AUTHOR
530              
531             (c) Owen Cliffe 2008, Eugene Oden 2010.
532              
533             =head1 CONTRIBUTORS
534              
535             =over
536              
537             =item *
538              
539             Dave Lambley
540              
541             =item *
542              
543             Ed Freyfogle
544              
545             =item *
546              
547             Marc Metten
548              
549             =back
550              
551             =head1 LICENSE
552              
553             Released under the MIT license. Please see the LICENSE file in the root
554             directory of the distribution.
555              
556             =cut