File Coverage

blib/lib/Net/ThreeScale/Client.pm
Criterion Covered Total %
statement 31 33 93.9
branch n/a
condition n/a
subroutine 11 11 100.0
pod n/a
total 42 44 95.4


line stmt bran cond sub pod time code
1             package Net::ThreeScale::Client;
2              
3 7     7   141405 use strict;
  7         11  
  7         176  
4 7     7   21 use warnings;
  7         9  
  7         201  
5 7     7   37 use vars qw($VERSION @ISA @EXPORT_OK %EXPORT_TAGS);
  7         10  
  7         491  
6              
7 7     7   22 use Carp;
  7         8  
  7         436  
8 7     7   1017 use Data::Dumper;
  7         11674  
  7         294  
9 7     7   26 use Exporter;
  7         9  
  7         172  
10 7     7   3921 use HTTP::Tiny;
  7         243979  
  7         285  
11 7     7   2592 use Net::ThreeScale::Response;
  7         11  
  7         151  
12 7     7   2713 use Try::Tiny;
  7         10723  
  7         327  
13              
14 7     7   2653 use URI::Escape::XS qw(uri_escape);
  7         11649  
  7         383  
15 7     7   5149 use XML::Parser;
  0            
  0            
16             use XML::Simple;
17              
18             my $DEFAULT_USER_AGENT;
19              
20             use constant {
21             TS_RC_SUCCESS => 'client.success',
22             TS_RC_AUTHORIZE_FAILED => 'provider_key_invalid',
23             TS_RC_UNKNOWN_ERROR => 'client.unknown_error'
24             };
25              
26             BEGIN {
27             @ISA = qw(Exporter);
28             $VERSION = "2.1.5";
29             @EXPORT_OK = qw();
30             %EXPORT_TAGS = (
31             'all' => \@EXPORT_OK,
32             'ALL' => \@EXPORT_OK,
33             );
34             $DEFAULT_USER_AGENT = "threescale_perl_client/$VERSION";
35              
36             }
37              
38             sub new {
39             my $class = shift;
40             my $params = ( $#_ == 0 ) ? { %{ (shift) } } : {@_};
41              
42             my $agent_string = $params->{user_agent} || $DEFAULT_USER_AGENT;
43              
44             croak("provider_key or service_token/service_id pair are required")
45             unless $params->{provider_key} xor ( $params->{service_token} && $params->{service_id});
46              
47             my $self = {};
48             $self->{provider_key} = $params->{provider_key} || undef;
49             $self->{service_token} = $params->{service_token} || undef;
50             $self->{service_id} = $params->{service_id} || undef;
51              
52             $self->{url} = $params->{url} || 'https://su1.3scale.net';
53             $self->{DEBUG} = $params->{DEBUG};
54             $self->{HTTPTiny} = HTTP::Tiny->new(
55             'agent' => $agent_string,
56             'keep_alive' => 1,
57             'timeout' => 5,
58             );
59              
60             return bless $self, $class;
61             }
62              
63             sub _authorize_given_url{
64             my $self = shift;
65             my $url = shift;
66              
67             $self->_debug( "start> sending GET request: ", $url );
68              
69             my $response = $self->{HTTPTiny}->get($url);
70             $self->_debug( "start> got response : ", $response->{content} );
71              
72             if (!$response->{success}){
73             return $self->_wrap_error($response);
74             }
75             # HTTP 409 = Conflict
76             if ($response->{status} == 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}->post_form($url, { Content=>$content });
198              
199             $self->_debug( "start> got response : ", $response->{content} );
200              
201             if ( !$response->{success} ) {
202             return $self->_wrap_error($response);
203             }
204              
205             $self->_debug( "success" );
206              
207             return Net::ThreeScale::Response->new(
208             error_code => TS_RC_SUCCESS,
209             success => 1,
210             );
211             }
212              
213             #Wraps an HTTP::Response message into a Net::ThreeScale::Response error return value
214             sub _wrap_error {
215             my $self = shift;
216             my $res = shift;
217             my $error_code;
218             my $message;
219              
220             try {
221             ( $error_code, $message ) = $self->_parse_errors( $res->{content});
222             } catch {
223             $error_code = TS_RC_UNKNOWN_ERROR;
224             $message = 'unknown_error';
225             };
226              
227             return Net::ThreeScale::Response->new(
228             success => 0,
229             error_code => $error_code,
230             error_message => $message
231             );
232             }
233              
234             # Parses an error document out of a response body
235             # If no sensible error messages are found in the response, insert the standard error value
236             sub _parse_errors {
237             my $self = shift;
238             my $body = shift;
239             my $cur_error;
240             my $in_error = 0;
241             my $errstring = undef;
242             my $errcode = TS_RC_UNKNOWN_ERROR;
243              
244             return undef if !defined($body);
245             my $parser = new XML::Parser(
246             Handlers => {
247             Start => sub {
248             my $expat = shift;
249             my $element = shift;
250             my %atts = @_;
251              
252             if ( $element eq 'error' ) {
253             $in_error = 1;
254             $cur_error = "";
255             if ( defined( $atts{code} ) ) {
256             $errcode = $atts{code};
257             }
258             }
259             },
260             End => sub {
261             if ( $_[1] eq 'error' ) {
262             $errstring = $cur_error;
263             $cur_error = undef;
264             $in_error = 0;
265             }
266             },
267             Char => sub {
268             if ($in_error) {
269             $cur_error .= $_[1];
270             }
271             }
272             }
273             );
274              
275             try {
276             $parser->parse($body);
277             }
278             catch {
279             $errstring = $_;
280             };
281              
282             return ( $errcode, $errstring );
283             }
284              
285             sub _parse_authorize_response {
286             my $self = shift;
287             my $response_body = shift;
288              
289             if (length($response_body)) {
290             my $xml = new XML::Simple(ForceArray=>['usage_report']);
291             return $xml->XMLin($response_body);
292             }
293             return {};
294             }
295              
296             sub _format_transactions {
297             my $self = shift;
298             my (@transactions) = @_;
299              
300             my $output = "";
301              
302             my $transNumber = 0;
303              
304             for my $trans (@transactions) {
305             die("Transactions should be given as hashes")
306             unless(ref($trans) eq 'HASH');
307              
308             die("Transactions need an 'app_id'")
309             unless(defined($trans->{app_id}));
310              
311             die("Transactions need a 'usage' hash")
312             unless(defined($trans->{usage}) and ref($trans->{usage}) eq 'HASH');
313              
314             die("Transactions need a 'timestamp'")
315             unless(defined($trans->{app_id}));
316              
317             my $pref = "transactions[$transNumber]";
318              
319             if ($transNumber > 0) {
320             $output .= "&";
321             }
322              
323             $output .= $pref . "[app_id]=" . $trans->{app_id};
324              
325             foreach my $k ( sort keys %{$trans->{usage}} ){
326             my $v = $trans->{usage}->{$k};
327             $k = uri_escape($k);
328             $v = uri_escape($v);
329             $output .= "&";
330             $output .= $pref . "[usage][$k]=$v";
331             }
332              
333             $output .= "&"
334             . $pref
335             . "[timestamp]="
336             . uri_escape($trans->{timestamp});
337              
338             $transNumber += 1;
339             }
340              
341             return $output;
342             }
343              
344             sub _debug {
345             my $self = shift;
346             if ( $self->{DEBUG} ) {
347             print STDERR "DBG:", @_, "\n";
348             }
349              
350             }
351             1;
352              
353             =head1 NAME
354              
355             Net::ThreeScale::Client - Client for 3Scale.com web API version 2.0
356              
357             =head1 SYNOPSIS
358              
359             use Net::ThreeScale::Client;
360            
361             my $client = new Net::ThreeScale::Client(provider_key=>"my_assigned_provider_key",
362             url=>"http://su1.3Scale.net");
363              
364             # Or initialize by service_token/service_id
365             # my $client = new Net::ThreeScale::Client(service_token=>"SERVICE_TOKEN",
366             # service_id=>"SERVICE_ID");
367              
368             my $response = $client->authorize(app_id => $app_id,
369             app_key => $app_key);
370            
371             if($response->is_success) {
372             print "authorized ", $response->transaction,"\"n";
373             ...
374              
375             my @transactions = (
376             {
377             app_id => $app_id,
378             usage => {
379             hits => 1,
380             },
381              
382             timestamp => "2010-09-01 09:01:00",
383             },
384              
385             {
386             app_id => $app_id,
387             usage => {
388             hits => 1,
389             },
390              
391             timestamp => "2010-09-02 09:02:00",
392             }
393             );
394              
395             my $report_response = $client->report(transactions=>\@transactions));
396             if($report_response->is_success){
397             print STDERR "Transactions reported\n";
398             } else {
399             print STDERR "Failed to report transactions",
400             $response->error_code(),":",
401             $response->error_message(),"\n";
402             }
403             } else {
404             print STDERR "authorize failed with error :",
405             $response->error_message,"\n";
406             if($response->error_code == TS_RC_AUTHORIZE_FAILED) {
407             print "Provider key is invalid";
408             } else {
409             ...
410             }
411             }
412              
413             =head1 CONSTRUCTOR
414            
415             The class method new(...) creates a new 3Scale client object. This may
416             be used to conduct transactions with the 3Scale service. The object is
417             stateless and transactions may span multiple clients. The following
418             parameters are recognised as arguments to new():
419              
420             =over 4
421            
422             =item provider_key
423              
424             (required) The provider key used to identify you with the 3Scale service
425              
426             =item service_token
427              
428             (required) Service API key with 3scale (also known as service token).
429              
430             =item service_id
431              
432             (required) Service id. Required.
433              
434             =item url
435              
436             (optional) The 3Scale service URL, usually this should be left to the
437             default value
438              
439             =back
440              
441             =head1 $response = $client->authorize(app_id=>$app_id, app_key=>$app_key)
442              
443             Starts a new client transaction the call must include a application id (as
444             a string) and (optionally) an application key (string), identifying the
445             application to use.
446            
447             Returns a Net::ThreeScale::Response object which indicates whether the
448             authorization was successful or indicates an error if one occured.
449            
450             =head1 $response = $client->report(transactions=>\@transactions)
451              
452             Reports a list of transactions to 3Scale.
453              
454             =over 4
455              
456             =item transactions=>{app_id=>value,...}
457              
458             Should be an array similar to the following:
459              
460             =over 4
461              
462             my @transactions = (
463             {
464             app_id => $app_id,
465             usage => {
466             hits => 1,
467             }
468             timestamp => "2010-09-01 09:01:00",
469             },
470             {
471             app_id => $app_id,
472             usage => {
473             hits => 1,
474             }
475             timestamp => "2010-09-01 09:02:00",
476             },
477             );
478              
479             =back
480              
481             =back
482              
483             =head1 EXPORTS / ERROR CODES
484              
485             The following constants are exported and correspond to error codes
486             which may appear in calls to Net::ThreeScale::Response::error_code
487              
488             =over 4
489              
490             =item TS_RC_SUCCESS
491              
492             The operation completed successfully
493              
494             =item TS_RC_AUTHORIZE_FAILED
495              
496             The passed provider key was invalid
497              
498             =item TS_RC_UNKNOWN_ERROR
499              
500             An unspecified error occurred. See the corresponding message for more detail.
501              
502             =back
503              
504             =head1 SUPPORT
505              
506             3scale support say,
507             I
508             certainly monitor pull requests and consider merging any useful contributions.>
509              
510             =head1 SEE ALSO
511              
512             =over 4
513              
514             =item Net::ThreeScale::Response
515              
516             Contains details of response contnet and values.
517            
518             =back
519              
520             =head1 AUTHOR
521              
522             (c) Owen Cliffe 2008, Eugene Oden 2010.
523              
524             =head1 CONTRIBUTORS
525              
526             =over
527              
528             =item *
529              
530             Dave Lambley
531              
532             =item *
533              
534             Ed Freyfogle
535              
536             =item *
537              
538             Marc Metten
539              
540             =head1 LICENSE
541              
542             Released under the MIT license. Please see the LICENSE file in the root
543             directory of the distribution.