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