File Coverage

blib/lib/Amazon/API.pm
Criterion Covered Total %
statement 189 590 32.0
branch 31 212 14.6
condition 26 163 15.9
subroutine 44 95 46.3
pod 7 21 33.3
total 297 1081 27.4


line stmt bran cond sub pod time code
1             package Amazon::API;
2              
3             # Generic interface to Amazon APIs
4              
5 3     3   105234 use strict;
  3         16  
  3         88  
6 3     3   14 use warnings;
  3         6  
  3         67  
7              
8 3     3   62 use 5.010;
  3         9  
9              
10             our %LOG4PERL_LOG_LEVELS;
11              
12             BEGIN {
13 3     3   11 %LOG4PERL_LOG_LEVELS = eval {
14 3         1978 require Log::Log4perl::Level;
15              
16 3         5437 Log::Log4perl::Level->import();
17              
18 3     3   20 use vars qw($ERROR $WARN $INFO $DEBUG $TRACE $FATAL);
  3         5  
  3         343  
19              
20             return (
21 3         406 error => $ERROR,
22             warn => $WARN,
23             info => $INFO,
24             debug => $DEBUG,
25             trace => $TRACE,
26             fatal => $FATAL,
27             );
28             };
29             }
30              
31 3     3   1318 use parent qw( Exporter Class::Accessor::Fast);
  3         886  
  3         18  
32              
33 3     3   11819 use Amazon::API::Error;
  3         8  
  3         19  
34 3     3   1398 use Amazon::API::Signature4;
  3         20  
  3         173  
35 3     3   1265 use Amazon::API::Constants qw( :all );
  3         9  
  3         685  
36 3     3   2732 use Amazon::Credentials;
  3         337909  
  3         177  
37 3     3   1767 use Amazon::API::Botocore qw(paginator);
  3         13  
  3         242  
38 3     3   1560 use Amazon::API::Botocore::Shape::Serializer;
  3         10  
  3         30  
39 3         175 use Amazon::API::Botocore::Shape::Utils qw(
40             require_class
41             require_shape
42             create_module_name
43             get_service_from_class
44 3     3   154 );
  3         6  
45              
46 3     3   68 use Carp;
  3         73  
  3         148  
47 3     3   17 use Carp::Always;
  3         6  
  3         29  
48 3     3   104 use Data::Dumper;
  3         4  
  3         133  
49 3     3   17 use Date::Format;
  3         6  
  3         184  
50 3     3   19 use English qw( -no_match_vars);
  3         92  
  3         21  
51 3     3   1073 use HTTP::Request;
  3         6  
  3         56  
52 3     3   126 use JSON qw( encode_json decode_json );
  3         5  
  3         18  
53 3     3   323 use LWP::UserAgent;
  3         7  
  3         65  
54 3     3   85 use List::Util qw(any all pairs none);
  3         6  
  3         203  
55 3     3   21 use ReadonlyX;
  3         5  
  3         128  
56 3     3   22 use Scalar::Util qw( reftype blessed );
  3         4  
  3         156  
57 3     3   21 use Time::Local;
  3         11  
  3         181  
58 3     3   16 use Time::HiRes qw( gettimeofday tv_interval );
  3         14  
  3         36  
59 3     3   346 use XML::Simple;
  3         7  
  3         78  
60              
61             Readonly::Scalar my $DEFAULT_REGION => 'us-east-1';
62             Readonly::Scalar my $REGIONAL_URL_FMT => '%s://%s.%s.amazonaws.com';
63             Readonly::Scalar my $GLOBAL_URL_FMT => '%s://%s.amazonaws.com';
64              
65             Readonly::Scalar my $DEFAULT_LAYOUT_PATTERN => '[%d] %p (%R/%r) %M:%L - %m%n';
66              
67             local $Data::Dumper::Pair = $COLON;
68             local $Data::Dumper::Useqq = $TRUE;
69             local $Data::Dumper::Terse = $TRUE;
70              
71             __PACKAGE__->follow_best_practice;
72              
73             __PACKAGE__->mk_accessors(
74             qw(
75             action
76             api
77             api_methods
78             botocore_metadata
79             botocore_operations
80             botocore_shapes
81             content_type
82             credentials
83             debug
84             decode_always
85             endpoint_prefix
86             error
87             force_array
88             http_method
89             last_action
90             _log_level
91             logger
92             log_file
93             log_layout
94             paginators
95             print_error
96             protocol
97             raise_error
98             raise_serialization_errors
99             region
100             request_uri
101             response
102             service
103             serializer
104             service_url_base
105             target
106             target_prefix
107             url
108             user_agent
109             use_paginator
110             version
111             )
112             );
113              
114             our $VERSION = '2.0.8'; ## no critic (RequireInterpolationOfMetachars)
115              
116             our @EXPORT_OK = qw(
117             create_urlencoded_content
118             get_api_service
119             param_n
120             paginator
121             );
122              
123             our $START_TIME = [gettimeofday];
124             our $LAST_LOG_TIME = [gettimeofday];
125              
126             =begin 'ignore'
127              
128             The service categories were extracted from Botocore project:
129              
130             grep -ri '"protocol":"' botocore/botocore/data/* | grep 'xml' | \
131             cut -f 8 -d '/' | sort -u > xml.services
132              
133             grep -ri '"protocol":"' botocore/botocore/data/* | grep 'json' | \
134             cut -f 8 -d '/' | sort -u > json.services
135              
136             grep -ri '"protocol":"' botocore/botocore/data/* | grep 'query' | \
137             cut -f 8 -d '/' | sort -u > query.services
138              
139             These service categories are used to deduce content type for
140             parameters sent to methods for these APIs when Botocore metadata was
141             not used to create an API class. Content-Type can however be
142             overridden when invoking APIs if we guess wrong.
143              
144             rest-json => application/x-amz-1.1
145             rest-xml => application/xml
146             query => application/x-www-form-urlencoded
147              
148             =end 'ignore'
149              
150             =cut
151              
152             our %API_TYPES = (
153             query => [
154             qw(
155             autoscaling
156             cloudformation
157             cloudsearch
158             cloudwatch
159             docdb
160             ec2
161             elasticache
162             elasticbeanstalk
163             elb
164             elbv2
165             forecastquery
166             iam
167             importexport
168             neptune
169             rds
170             redshift
171             sdb
172             ses
173             sns
174             sqs
175             sts
176             timestream-query
177             )
178             ],
179             xml => [
180             qw(
181             cloudfront
182             route53
183             s3
184             s3control
185             )
186             ],
187              
188             json => [
189             qw(
190             accessanalyzer
191             account
192             acm
193             acm-pca
194             alexaforbusiness
195             amp
196             amplify
197             amplifyuibuilder
198             apigateway
199             appconfig
200             appconfigdata
201             appflow
202             appintegrations
203             application-autoscaling
204             applicationcostprofiler
205             application-insights
206             appmesh
207             apprunner
208             appstream
209             appsync
210             athena
211             auditmanager
212             autoscaling-plans
213             backup
214             backup-gateway
215             batch
216             billingconductor
217             braket
218             budgets
219             ce
220             chime
221             chime-sdk-identity
222             chime-sdk-media-pipelines
223             chime-sdk-meetings
224             chime-sdk-messaging
225             cloud9
226             cloudcontrol
227             clouddirectory
228             cloudhsm
229             cloudhsmv2
230             cloudsearchdomain
231             cloudtrail
232             codeartifact
233             codebuild
234             codecommit
235             codedeploy
236             codeguruprofiler
237             codeguru-reviewer
238             codepipeline
239             codestar
240             codestar-connections
241             codestar-notifications
242             cognito-identity
243             cognito-idp
244             cognito-sync
245             comprehend
246             comprehendmedical
247             compute-optimizer
248             config
249             connect
250             connectcampaigns
251             connect-contact-lens
252             connectparticipant
253             cur
254             customer-profiles
255             databrew
256             datapipeline
257             datasync
258             dax
259             detective
260             devicefarm
261             devops-guru
262             directconnect
263             discovery
264             dlm
265             dms
266             drs
267             ds
268             dynamodb
269             dynamodbstreams
270             ebs
271             ec2-instance-connect
272             ecr
273             ecr-public
274             ecs
275             efs
276             eks
277             elastic-inference
278             elastictranscoder
279             emr
280             emr-containers
281             emr-serverless
282             es
283             events
284             evidently
285             finspace
286             finspace-data
287             firehose
288             fis
289             fms
290             forecast
291             forecastquery
292             frauddetector
293             fsx
294             gamelift
295             gamesparks
296             glacier
297             globalaccelerator
298             glue
299             grafana
300             greengrassv2
301             groundstation
302             guardduty
303             health
304             healthlake
305             honeycode
306             identitystore
307             imagebuilder
308             inspector
309             inspector2
310             iot
311             iot1click-projects
312             iotanalytics
313             iot-data
314             iotdeviceadvisor
315             iotevents
316             iotevents-data
317             iotfleethub
318             iot-jobs-data
319             iotsecuretunneling
320             iotsitewise
321             iotthingsgraph
322             iottwinmaker
323             iotwireless
324             ivs
325             ivschat
326             kafkaconnect
327             kendra
328             keyspaces
329             kinesis
330             kinesisanalytics
331             kinesisanalyticsv2
332             kinesisvideo
333             kinesis-video-archived-media
334             kinesis-video-media
335             kinesis-video-signaling
336             kms
337             lakeformation
338             lambda
339             lex-models
340             lex-runtime
341             lexv2-models
342             lexv2-runtime
343             license-manager
344             license-manager-user-subscriptions
345             lightsail
346             location
347             logs
348             lookoutequipment
349             lookoutmetrics
350             lookoutvision
351             m2
352             machinelearning
353             macie
354             managedblockchain
355             marketplace-catalog
356             marketplacecommerceanalytics
357             marketplace-entitlement
358             mediastore
359             mediastore-data
360             memorydb
361             meteringmarketplace
362             mgh
363             mgn
364             migrationhub-config
365             migration-hub-refactor-spaces
366             migrationhubstrategy
367             mobile
368             mturk
369             mwaa
370             network-firewall
371             networkmanager
372             nimble
373             opensearch
374             opsworks
375             opsworkscm
376             organizations
377             outposts
378             panorama
379             personalize
380             personalize-events
381             personalize-runtime
382             pi
383             pinpoint-email
384             pinpoint-sms-voice-v2
385             polly
386             pricing
387             proton
388             qldb
389             qldb-session
390             quicksight
391             ram
392             rbin
393             rds-data
394             redshift-data
395             redshift-serverless
396             rekognition
397             resiliencehub
398             resource-groups
399             resourcegroupstaggingapi
400             robomaker
401             rolesanywhere
402             route53domains
403             route53-recovery-cluster
404             route53resolver
405             rum
406             s3outposts
407             sagemaker
408             sagemaker-a2i-runtime
409             sagemaker-edge
410             sagemaker-featurestore-runtime
411             sagemaker-runtime
412             savingsplans
413             secretsmanager
414             securityhub
415             servicecatalog
416             servicecatalog-appregistry
417             servicediscovery
418             service-quotas
419             sesv2
420             shield
421             signer
422             sms
423             snowball
424             snow-device-management
425             ssm
426             ssm-contacts
427             ssm-incidents
428             sso
429             sso-admin
430             sso-oidc
431             stepfunctions
432             storagegateway
433             support
434             swf
435             synthetics
436             textract
437             timestream-query
438             timestream-write
439             transcribe
440             transfer
441             translate
442             voice-id
443             waf
444             waf-regional
445             wafv2
446             wellarchitected
447             wisdom
448             workdocs
449             worklink
450             workmail
451             workmailmessageflow
452             workspaces
453             workspaces-web
454             xray
455             )
456             ],
457             );
458              
459             our @GLOBAL_SERVICES = qw(
460             cloudfront
461             iam
462             importexport
463             route53
464             s3
465             savingsplans
466             sts
467             );
468              
469             our @REQUIRED_KEYS = qw( aws_access_key_id aws_secret_access_key );
470              
471             ########################################################################
472             sub new {
473             ########################################################################
474 1     1 1 130 my ( $class, @options ) = @_;
475              
476 1   33     9 $class = ref $class || $class;
477              
478 1 50       4 my %options = ref $options[0] ? %{ $options[0] } : @options;
  1         6  
479              
480 1         15 my $self = $class->SUPER::new( \%options );
481              
482 1 50       35 if ( $self->get_service_url_base ) {
483 1         30 $self->set_service( $self->get_service_url_base );
484             }
485              
486 1 50       45 croak 'service is required'
487             if !$self->get_service;
488              
489 1         11 $self->_set_default_logger;
490              
491 1         7 $self->_set_defaults(%options);
492              
493 1         39 $self->_create_methods;
494              
495 1 50       5 if ( $self->is_botocore_api ) {
496 0         0 $self->set_serializer(
497             Amazon::API::Botocore::Shape::Serializer->new(
498             service => get_service_from_class( ref $self )
499             )
500             );
501             }
502              
503 1         22 return $self;
504             }
505              
506             ########################################################################
507             sub set_log_level {
508             ########################################################################
509 1     1 0 3 my ( $self, $log_level ) = @_;
510              
511 1         22 $self->set__log_level($log_level);
512              
513 1 50       12 if ( Log::Log4perl->initialized() ) {
514 1         7 Log::Log4perl->get_logger->level( $LOG4PERL_LOG_LEVELS{$log_level} );
515             }
516             else {
517 0         0 my $logger = $self->get_logger;
518              
519 0 0       0 if ( $logger->can('level') ) {
520 0         0 $logger->level($log_level);
521             }
522             }
523              
524 1         1443 return $self;
525             }
526              
527             ########################################################################
528             sub get_log_level {
529             ########################################################################
530 0     0 0 0 my ($self) = @_;
531              
532 0         0 return $self->get__log_level();
533             }
534              
535             ########################################################################
536             sub get_api_service {
537             ########################################################################
538 0     0 1 0 my ( $service, %args ) = @_;
539              
540 0         0 $service = create_module_name lc $service;
541              
542 0         0 my $class = sprintf 'Amazon::API::%s', $service;
543              
544 0         0 require_class $class;
545              
546 0         0 return $class->new(%args);
547             }
548              
549             ########################################################################
550             sub decode_response {
551             ########################################################################
552 0     0 1 0 my ( $self, $response ) = @_;
553              
554 0   0     0 $response = $response || $self->get_response;
555              
556             # could be Furl or HTTP?
557 0 0 0     0 if ( !ref $response && $response->can('content') ) {
558 0         0 croak q{can't decode response - not a response object: } . ref $response;
559             }
560              
561 0         0 my $content = $response->content;
562 0         0 my $content_type = $response->content_type;
563              
564 0         0 my $decoded_content;
565              
566 0 0       0 if ($content) {
567              
568 0         0 $decoded_content = eval {
569 0 0       0 if ( $content_type =~ /json/xmsi ) {
    0          
570 0         0 decode_json($content);
571             }
572             elsif ( $content_type =~ /xml/xmsi ) {
573              
574 0         0 XMLin(
575             $content,
576             ForceContent => $self->is_botocore_api,
577             ForceArray => ['item'],
578             # $self->get_force_array ? ( ForceArray => ['item'] ) : ()
579             );
580             }
581             };
582              
583             # disregard content_type (it might be misleading)
584 0 0 0     0 if ( !$decoded_content || $EVAL_ERROR ) {
585 0         0 $decoded_content = eval { return decode_json($content); };
  0         0  
586              
587 0 0 0     0 if ( !$decoded_content || $EVAL_ERROR ) {
588 0         0 $decoded_content = eval {
589 0 0       0 return XMLin(
590             $content,
591             SuppressEmpty => undef,
592             $self->get_force_array ? ( ForceArray => ['item'] ) : ()
593             );
594             };
595             }
596             }
597             }
598              
599 0   0     0 $content = $decoded_content || $content;
600              
601 0     0   0 DEBUG( sub { return Dumper( [ 'content', $content ] ) } );
  0         0  
602              
603             # we'll only have a "serializer" if this is a Botocore generated API
604 0         0 my $serializer = $self->get_serializer;
605              
606 0 0 0     0 if ( ref $content && $serializer ) {
607              
608 0         0 my $botocore_action = $self->get_botocore_action;
609              
610 0         0 my $output = $botocore_action->{'output'};
611              
612 0         0 my $orig_content = $content;
613              
614 0 0       0 if ( $output->{resultWrapper} ) {
615 0         0 $content = $content->{ $output->{resultWrapper} };
616             }
617              
618 0     0   0 DEBUG( sub { return Dumper( [ 'content', $content, $output ] ) } );
  0         0  
619              
620 0         0 $serializer->set_logger( $self->get_logger );
621              
622 0         0 $content = eval {
623             $serializer->serialize(
624             service => get_service_from_class( ref $self ),
625             shape => $output->{shape},
626 0         0 data => $content
627             );
628             };
629              
630             # ...but this isn't necessarily where things STB
631 0 0 0     0 if ( !$content || $EVAL_ERROR ) {
632 0 0       0 if ( $self->get_raise_serialization_errors ) {
633 0         0 die $EVAL_ERROR;
634             }
635             else {
636 0         0 warn
637             "error serializing content: please report this error\n$EVAL_ERROR";
638 0         0 $content = $orig_content;
639             }
640             }
641             }
642              
643 0         0 return $content;
644             }
645              
646             ########################################################################
647             sub get_botocore_action {
648             ########################################################################
649 0     0 0 0 my ($self) = @_;
650              
651 0         0 return $self->get_botocore_operations->{ $self->get_action };
652             }
653              
654             ########################################################################
655             sub is_botocore_shape {
656             ########################################################################
657 0     0 0 0 my ($request) = @_;
658              
659 0         0 my $shape_name = ref $request;
660              
661 0 0       0 if ( $shape_name =~ /Botocore::Shape::([^:]+)::([^:]+)$/xsm ) {
662 0         0 $shape_name = $2;
663             }
664             else {
665 0         0 $shape_name = undef;
666             }
667              
668 0         0 return $shape_name;
669             }
670              
671             ########################################################################
672             sub is_param_type {
673             ########################################################################
674 0     0 0 0 my ( $self, $shape_name, $param, $type ) = @_;
675              
676 0         0 my $members = $self->get_botocore_shapes->{$shape_name}->{members};
677 0         0 my $member = $members->{$param};
678              
679 0         0 my $location = $member->{location};
680              
681             TRACE(
682             sub {
683 0     0   0 return Dumper( $members, $member, $location, $param, $type,
684             $shape_name );
685             }
686 0         0 );
687              
688             return ( $location && $location eq $type )
689             ? $member->{locationName}
690 0 0 0     0 : $EMPTY;
691             }
692              
693             ########################################################################
694             sub is_query_param {
695             ########################################################################
696 0     0 0 0 my ( $self, $shape_name, $param ) = @_;
697              
698 0         0 return $self->is_param_type( $shape_name, $param, 'querystring' );
699             }
700              
701             ########################################################################
702             sub is_uri_param {
703             ########################################################################
704 0     0 0 0 my ( $self, $shape_name, $param ) = @_;
705              
706 0         0 return $self->is_param_type( $shape_name, $param, 'uri' );
707             }
708              
709             ########################################################################
710             sub create_botocore_request {
711             ########################################################################
712 0     0 0 0 my ( $self, %args ) = @_;
713              
714 0         0 my ( $parameters, $action ) = @args{qw(parameters action)};
715              
716 0   0     0 $action //= $self->get_action;
717              
718 0 0       0 croak 'no action'
719             if !$action;
720              
721 0 0       0 croak 'no parameters'
722             if !$parameters;
723              
724 0 0       0 croak 'not a botocore API'
725             if !$self->is_botocore_api($action);
726              
727 0         0 my $botocore_operations = $self->get_botocore_operations->{$action};
728              
729 0         0 my $input = $botocore_operations->{'input'};
730 0         0 my $shape = $input->{'shape'};
731              
732 0         0 my $class = require_shape( $shape, get_service_from_class($self) );
733              
734 0 0       0 croak "could not create request shape: $shape\n$EVAL_ERROR"
735             if !$class;
736              
737 0         0 my $request = $class->new($parameters); # request class
738              
739 0         0 return $request;
740             }
741              
742             ########################################################################
743             # init_botocore_request( $self, $request)
744             ########################################################################
745              
746             # This function will accept either an object which is a sub-class of
747             # Amazon::API::Botocore::Shape, or a hash if the parameters have been
748             # constructed "by-hand",
749             #
750             # The parameters are used to populate both the URL if some parameters
751             # are passed in the URL and either a JSON or XML payload depending on
752             # the API type (rest-json, rest-xml).
753              
754             ########################################################################
755             sub init_botocore_request {
756             ########################################################################
757 0     0 0 0 my ( $self, $request ) = @_;
758              
759 0   0     0 $request //= {};
760              
761 0         0 my $action = $self->get_action;
762              
763 0         0 my $botocore_operations = $self->get_botocore_operations->{$action};
764              
765             TRACE(
766             sub {
767 0     0   0 return Dumper(
768             [ 'init_botocore_request:', $botocore_operations,
769             $request, $self->get_botocore_metadata
770             ]
771             );
772             }
773 0         0 );
774              
775 0         0 my $protocol = $self->get_botocore_metadata->{'protocol'};
776              
777 0         0 my $input = $botocore_operations->{'input'};
778              
779 0         0 my $shape = $input->{'shape'};
780              
781 0         0 my $request_shape_name = is_botocore_shape($request);
782              
783             # if a shape object is passed, it must be the correct type
784 0 0 0     0 croak "$action requires a $shape object, not a $request_shape_name object"
785             if $request_shape_name && $request_shape_name ne $shape;
786              
787             # try to create a Botocore request shape
788 0         0 my $boto_request;
789              
790 0 0 0     0 if ( !$request_shape_name && $self->is_botocore_api ) {
791 0         0 $boto_request = $self->create_botocore_request( parameters => $request );
792 0 0       0 if ( !$boto_request ) {
793 0         0 carp "could not create a botocore request object\n$EVAL_ERROR\n";
794             }
795             else {
796 0         0 $request_shape_name = is_botocore_shape($boto_request);
797 0         0 $request = $boto_request;
798             }
799             }
800              
801 0         0 my $http = $botocore_operations->{'http'};
802 0         0 my $method = $http->{'method'};
803              
804 0         0 my %parameters;
805              
806             # is the request a Botocore::Shape object? if so we can use metadata
807             # to create URI and payload, otherwise it's up to the caller to make
808             # sure the URI and the payload are correct...good luck!
809              
810 0 0       0 if ( !$request_shape_name ) {
811 0         0 %parameters = %{$request};
  0         0  
812             }
813             else {
814 0         0 my $finalized_request = $request->finalize;
815              
816 0 0       0 if ( $protocol =~ /rest\-(xml|json)/xsm ) {
817 0         0 $finalized_request = { $request_shape_name => $finalized_request };
818             }
819              
820             DEBUG(
821 0     0   0 sub { return Dumper( [ 'finalized request:', $finalized_request ] ) } );
  0         0  
822              
823 0         0 %parameters = %{$finalized_request};
  0         0  
824              
825 0 0       0 if ( defined $input->{'xmlNamespace'} ) {
826             $parameters{$request_shape_name}->{'xmlns'}
827 0         0 = $input->{'xmlNamespace'}->{uri};
828             }
829              
830 0     0   0 DEBUG( sub { return Dumper( [ 'parameters:', \%parameters ] ) } );
  0         0  
831             }
832              
833 0   0     0 my $json_version = $self->get_botocore_metadata->{'jsonVersion'} // '1.0';
834              
835 0         0 my %content_types = (
836             'ec2' => 'application/x-www-form-urlencoded',
837             'query' => 'application/x-www-form-urlencoded',
838             'rest-json' => 'application/json',
839             'json' => 'application/x-amz-json-' . $json_version,
840             'rest-xml' => 'application/xml',
841             );
842              
843 0         0 $self->set_http_method($method);
844 0         0 $self->set_content_type( $content_types{$protocol} );
845              
846 0         0 DEBUG("protocol: $protocol");
847 0         0 DEBUG("method: $method");
848 0         0 DEBUG( 'content-type: ' . $self->get_content_type );
849              
850 0         0 my $uri;
851              
852 0 0       0 if ( $protocol =~ /^rest\-(json|xml)/xsm ) {
853 0         0 my @args = @{ $http->{'parsed_request_uri'}->{'parameters'} };
  0         0  
854              
855 0         0 my $request_uri_tpl = $http->{'parsed_request_uri'}->{'request_uri_tpl'};
856              
857             # if the request is a shape, we've already checked for required parameters
858 0 0       0 if ( !$request_shape_name ) {
859 0         0 foreach my $p (@args) {
860             croak 'required parameter ' . $p . ' not found.'
861 0 0       0 if !exists $parameters{$p};
862             }
863              
864 0         0 $uri = sprintf $request_uri_tpl, @parameters{@args};
865 0         0 $self->set_request_uri($uri);
866              
867 0         0 delete @parameters{@args};
868             }
869             else {
870              
871 0 0       0 if ($request_shape_name) {
872              
873 0         0 $uri = $http->{requestUri}; # use the Botocore template
874              
875 0         0 my $shape_parameters = $parameters{$shape};
876              
877             DEBUG(
878             sub {
879 0     0   0 return sprintf "requestUri: %s\nshape parameter: %s", $uri,
880             Dumper($shape_parameters);
881             }
882              
883 0         0 );
884              
885 0         0 foreach my $p ( keys %{$shape_parameters} ) {
  0         0  
886 0         0 DEBUG("parameter: $p");
887              
888 0 0       0 if ( my $var = $self->is_uri_param( $request_shape_name, $p ) ) {
889              
890 0         0 my $val = $shape_parameters->{$p};
891              
892 0         0 DEBUG("var: $var val: $val");
893              
894 0         0 $uri =~ s/[{]$var[}]/$val/xsm;
895              
896 0         0 delete $shape_parameters->{$p};
897             }
898             }
899             }
900             else {
901 0         0 $uri = sprintf $request_uri_tpl, @{ $parameters{$shape} }{@args};
  0         0  
902 0         0 delete @{ $parameters{$shape} }{@args};
  0         0  
903             }
904              
905             # we're not done yet...just to make things interesting, some
906             # APIs embed request parameters in the uri, payload AND query
907             # string!
908              
909 0 0       0 if ($request_shape_name) {
910 0         0 my %query_parameters;
911              
912 0         0 my $shape_parameters = $parameters{$shape};
913              
914 0         0 foreach my $p ( keys %{$shape_parameters} ) {
  0         0  
915 0 0       0 if ( my $var = $self->is_query_param( $request_shape_name, $p ) ) {
916 0         0 $query_parameters{$var} = $shape_parameters->{$p};
917 0         0 delete $shape_parameters->{$p};
918             }
919             }
920              
921             DEBUG(
922             sub {
923 0     0   0 return Dumper( [ 'query parameters: ', \%query_parameters ] );
924             }
925 0         0 );
926              
927 0 0       0 if ( keys %query_parameters ) {
928             # $self->set_content_type(undef);
929              
930 0         0 $uri = sprintf '%s?%s', $uri,
931             create_urlencoded_content( \%query_parameters );
932             }
933              
934 0 0       0 if ( !keys %{$shape_parameters} ) {
  0         0  
935 0         0 %parameters = ();
936             }
937             }
938             }
939              
940 0         0 $self->set_request_uri($uri);
941              
942             TRACE(
943             sub {
944 0     0   0 return Dumper [ 'rest API', \%parameters, $self->get_request_uri ];
945             }
946 0         0 );
947             }
948              
949 0         0 my $content = \%parameters;
950              
951 0   0     0 my $content_type = $self->get_content_type // $EMPTY;
952              
953 0 0 0     0 if ( $method ne 'POST' && !keys %parameters ) {
954 0         0 $content = undef;
955             }
956              
957 0     0   0 DEBUG( sub { return 'content ' . Dumper( [$content] ) } );
  0         0  
958              
959 0         0 return $content;
960             }
961              
962             ########################################################################
963             sub is_botocore_api {
964             ########################################################################
965 1     1 0 3 my ($self) = @_;
966              
967 1         19 return defined $self->get_botocore_metadata;
968             }
969              
970             ########################################################################
971             sub serialize_content {
972             ########################################################################
973 0     0 0 0 my ( $self, $parameters ) = @_;
974              
975 0         0 my $content = $parameters;
976 0         0 my $action = $self->get_action;
977 0         0 my $version = $self->get_version;
978 0         0 my $content_type = $self->get_content_type;
979              
980             DEBUG(
981             sub {
982 0     0   0 return Dumper(
983             [ 'serialize_content', $parameters, 'service', $self->get_service ] );
984             }
985 0         0 );
986              
987             # if the API is a query API, url encode parameters
988 0 0 0 0   0 if ( any { $_ eq lc $self->get_service } @{ $API_TYPES{query} } ) {
  0 0       0  
  0         0  
989 0         0 $content = create_urlencoded_content( $parameters, $action, $version );
990             }
991             elsif ( ref $parameters && reftype($parameters) eq 'HASH' ) {
992 0 0       0 if ( $content_type =~ /json/xsm ) {
    0          
993 0         0 $content = encode_json($parameters);
994             }
995             elsif ( $content_type =~ /xml/xms ) {
996             return
997 0 0 0     0 if !ref $content || !keys %{$content};
  0         0  
998              
999 0         0 $content = XMLout(
1000             $parameters,
1001             XMLDecl => $TRUE,
1002             KeepRoot => $TRUE,
1003             KeyAttr => ['xmlns'],
1004             );
1005             }
1006             }
1007              
1008 0         0 return $content;
1009             }
1010              
1011             ########################################################################
1012             # invoke_api( action, parameters, content-type, headers)
1013             ########################################################################
1014             sub invoke_api {
1015             ########################################################################
1016 0     0 1 0 my ( $self, @args ) = @_;
1017              
1018 0         0 my ( $action, $parameters, $content_type, $headers );
1019              
1020 0 0 0     0 if ( ref $args[0] && reftype( $args[0] ) eq 'HASH' ) {
1021             ( $action, $parameters, $content_type, $headers )
1022 0         0 = @{ $args[0] }{qw( action parameters content_type headers)};
  0         0  
1023             }
1024             else {
1025 0         0 ( $action, $parameters, $content_type, $headers ) = @args;
1026             }
1027              
1028             DEBUG(
1029             sub {
1030 0     0   0 return Dumper(
1031             [ 'parameters', $parameters, 'content-type', $content_type ] );
1032             }
1033 0         0 );
1034              
1035 0         0 $self->set_action($action);
1036 0         0 $self->set_last_action($action);
1037 0         0 $self->set_error(undef);
1038              
1039 0         0 my $decode_response = $self->get_decode_always;
1040              
1041 0 0 0     0 croak sprintf qq{"%s" was not generated with Botocore support.\n}
1042             . qq{Parameters should be simple objects, not blessed.\n}, ref $self
1043             if blessed($parameters) && !$self->is_botocore_api;
1044              
1045 0         0 my @paged_results;
1046              
1047 0   0     0 my $paginator = $self->get_paginators && $self->get_paginators->{$action};
1048 0   0     0 my $use_paginator = $paginator && $self->get_use_paginator;
1049              
1050 0         0 my $limit;
1051              
1052             # See if this was generated w/Botocore
1053 0 0       0 if ( $self->is_botocore_api ) {
    0          
1054 0         0 $parameters = $self->init_botocore_request($parameters);
1055              
1056 0         0 $content_type = $self->get_content_type;
1057              
1058 0 0       0 if ($use_paginator) {
1059              
1060 0   0     0 $paginator->{more_results} //= $paginator->{output_token};
1061              
1062 0         0 $limit = $parameters->{ $paginator->{limit_key} };
1063              
1064             DEBUG(
1065 0     0   0 sub { return Dumper( [ 'paginator', $paginator, 'limit', $limit ] ) }
1066 0         0 );
1067             }
1068             }
1069             elsif ( !$content_type ) {
1070 0         0 $content_type = $self->set_content_type( $self->_set_content_type );
1071             }
1072              
1073 0         0 my $serialized_content = $self->serialize_content($parameters);
1074              
1075             DEBUG(
1076             sub {
1077 0     0   0 Dumper [ $content_type, $parameters, $serialized_content ];
1078             }
1079 0         0 );
1080              
1081 0         0 my $page_count = 0;
1082              
1083             PAGINATE:
1084              
1085 0     0   0 DEBUG( sub { return Dumper( [ 'page', ++$page_count ] ) } );
  0         0  
1086              
1087 0         0 my $rsp = $self->submit(
1088             content => $serialized_content,
1089             content_type => $content_type,
1090             headers => $headers,
1091             );
1092              
1093 0         0 $self->set_response($rsp);
1094              
1095 0 0       0 if ( !$rsp->is_success ) {
1096              
1097 0         0 $self->set_error(
1098             Amazon::API::Error->new(
1099             { error => $rsp->code,
1100             message_raw => $rsp->content,
1101             content_type => scalar $rsp->content_type,
1102             api => ref $self,
1103             response => $rsp,
1104             }
1105             )
1106             );
1107              
1108 0 0       0 if ( $self->get_print_error ) {
1109 0         0 print {*STDERR} $self->print_error;
  0         0  
1110             }
1111              
1112 0 0       0 if ( $self->get_raise_error ) {
1113 0         0 die $self->get_error; ## no critic (RequireCarping)
1114             }
1115             }
1116              
1117             DEBUG(
1118             sub {
1119 0     0   0 return Dumper(
1120             [ 'content', $rsp->content,
1121             'decode always', $self->get_decode_always
1122             ]
1123             );
1124             }
1125 0         0 );
1126              
1127 0 0       0 return $rsp->content
1128             if !$self->get_decode_always;
1129              
1130 0         0 eval {
1131 0 0       0 if ($use_paginator) {
1132 0         0 my $result = $self->decode_response;
1133              
1134 0         0 push @paged_results, @{ $result->{ $paginator->{result_key} } };
  0         0  
1135              
1136             DEBUG(
1137             sub {
1138             return Dumper(
1139             [ 'paginating...',
1140             $result,
1141             'paged results',
1142             \@paged_results,
1143             $paginator->{more_results},
1144             !$result->{ $paginator->{more_results} }
1145 0     0   0 ]
1146             );
1147             }
1148 0         0 );
1149              
1150             return \@paged_results
1151 0 0       0 if !$result->{ $paginator->{more_results} };
1152              
1153 0   0     0 $limit = $limit // $result->{ $paginator->{limit_key} };
1154              
1155             my $parameters = $self->init_botocore_request(
1156             { $limit ? ( $paginator->{limit_key} => $limit ) : (),
1157             $paginator->{input_token} => $result->{ $paginator->{output_token} }
1158             }
1159 0 0       0 );
1160              
1161 0         0 $serialized_content = $self->serialize_content($parameters);
1162              
1163 0         0 goto PAGINATE;
1164             }
1165             };
1166              
1167 0 0       0 if ($EVAL_ERROR) {
1168 0 0       0 if ( $self->get_raise_serialization_errors ) {
1169 0         0 die $EVAL_ERROR;
1170             }
1171             else {
1172 0         0 warn "error serializing content: please report this error\n$EVAL_ERROR";
1173 0         0 return $rsp->content;
1174             }
1175             }
1176              
1177             return \@paged_results
1178 0 0       0 if $use_paginator;
1179              
1180 0         0 my $results = $self->decode_response;
1181              
1182 0 0 0     0 return $results
1183             if !$paginator || !$use_paginator;
1184              
1185             # you must have a paginator, but told me not to use it, delete
1186             # blank/null markers
1187 0 0       0 if ( !$results->{ $paginator->{more_results} } ) {
1188 0         0 delete $results->{ $paginator->{more_results} };
1189 0         0 delete $results->{ $paginator->{input_token} };
1190             }
1191              
1192 0         0 return $results;
1193             }
1194              
1195             ########################################################################
1196             sub print_error {
1197             ########################################################################
1198 0     0 1 0 my ($self) = @_;
1199              
1200 0         0 my $error = $self->get_error;
1201              
1202 0         0 my $err_str = 'API ERROR (' . $self->get_last_action . '): ';
1203              
1204 0 0 0     0 if ( $error && ref($error) =~ /Amazon::API::Error/xms ) {
1205 0         0 my $response = $error->get_response;
1206              
1207 0         0 local $Data::Dumper::Terse = $TRUE;
1208 0         0 local $Data::Dumper::Pair = $COLON;
1209 0         0 local $Data::Dumper::Useqq = $TRUE;
1210              
1211 0 0       0 $err_str .= sprintf q{[%s], %s},
1212             $error->get_error, Dumper( ref $response ? $response : [$response] );
1213             }
1214             else {
1215 0         0 $err_str .= '[' . $error . ']';
1216             }
1217              
1218 0         0 return $err_str;
1219             }
1220              
1221             ########################################################################
1222             sub get_valid_token {
1223             ########################################################################
1224 0     0 0 0 my ($self) = @_;
1225              
1226 0         0 my $credentials = $self->get_credentials;
1227              
1228 0 0       0 if ( $credentials->can('is_token_expired') ) {
1229 0 0       0 if ( $credentials->is_token_expired ) {
1230              
1231 0 0 0     0 if ( !$self->can('refresh_token') || !$credentials->refresh_token ) {
1232 0         0 croak 'token expired';
1233             }
1234             }
1235             }
1236              
1237 0     0   0 TRACE( sub { return Dumper( [ 'valid token:', $credentials->get_token ] ) }
1238 0         0 );
1239              
1240 0         0 return $credentials->get_token;
1241             }
1242              
1243             ########################################################################
1244             sub submit {
1245             ########################################################################
1246 0     0 1 0 my ( $self, %options ) = @_;
1247              
1248 0     0   0 DEBUG( sub { return Dumper [ 'submit:', \%options ] } );
  0         0  
1249              
1250 0   0     0 my $method = $self->get_http_method || 'POST';
1251 0   0     0 my $headers = $options{'headers'} || [];
1252              
1253 0         0 my $url = $self->get_url;
1254              
1255             my $botocore_protocol
1256 0   0     0 = eval { $self->get_botocore_metadata->{'protocol'} } // $EMPTY;
  0         0  
1257              
1258 0 0       0 if ( $botocore_protocol =~ /^rest\-(json|xml)/xsm ) {
1259 0 0       0 croak 'no request URI provided for rest-json call'
1260             if !$self->get_request_uri;
1261              
1262 0         0 $url .= $self->get_request_uri;
1263             }
1264              
1265 0     0   0 DEBUG( sub { return Dumper [ $method, $url, $headers ] } );
  0         0  
1266              
1267 0         0 my $request = HTTP::Request->new( $method, $url, $headers );
1268              
1269             # 1. set the header
1270             # 2. set the content
1271             # 3. sign the request
1272             # 4. send the request & return result
1273              
1274             # see IMPLEMENTATION NOTES for an explanation
1275 0 0 0     0 if ( $self->get_api || $self->get_target_prefix ) {
1276 0         0 $request = $self->_set_x_amz_target($request);
1277             }
1278              
1279 0         0 $self->_set_request_content( request => $request, %options );
1280 0         0 my $credentials = $self->get_credentials;
1281              
1282 0 0       0 if ( my $token = $self->get_valid_token ) {
1283 0         0 $request->header( 'X-Amz-Security-Token', $token );
1284             }
1285              
1286             # TODO: global end-points
1287 0         0 my $region = $self->get_region;
1288              
1289             # sign the request
1290 0   0     0 Amazon::API::Signature4->new(
1291             -access_key => $credentials->get_aws_access_key_id,
1292             -secret_key => $credentials->get_aws_secret_access_key,
1293             -security_token => $credentials->get_token || undef,
1294             service => $self->get_service,
1295             region => $region,
1296             )->sign( $request, $self->get_region );
1297              
1298 0     0   0 DEBUG( sub { return Dumper( [$request] ) } );
  0         0  
1299              
1300             # make the request, return response object
1301 0         0 my $ua = $self->get_user_agent;
1302 0         0 my $rsp = $ua->request($request);
1303              
1304 0     0   0 DEBUG( sub { return Dumper [$rsp] } );
  0         0  
1305              
1306 0         0 return $rsp;
1307             }
1308              
1309             # +------------------+
1310             # | EXPORTED METHODS |
1311             # +------------------+
1312              
1313             ########################################################################
1314             sub param_n {
1315             ########################################################################
1316 0     0 1 0 my (@args) = @_;
1317              
1318 0         0 return Amazon::API::Botocore::Shape::Utils::param_n(@args);
1319             }
1320              
1321             ########################################################################
1322             # create_urlencoded_content(parameters, action, version)
1323             # input:
1324             # parameters:
1325             # SCALAR - query string to encode (x=y&w=z...)
1326             # ARRAY - either an array of hashes or...
1327             # key/value pairs of the form x=y or...
1328             # key/values pairs
1329             # HASH - key/value pairs, if the value is an array then
1330             # it is assumed to be a list of hashes
1331             # action: API method
1332             # version: wsdl version for API
1333             #
1334             # output:
1335             # URL encodode query string
1336             #
1337             ########################################################################
1338             sub create_urlencoded_content {
1339             ########################################################################
1340 5     5 0 3400 my ( $parameters, $action, $version ) = @_;
1341              
1342 5         9 my @args;
1343              
1344 5 100 66     55 if ( $parameters && !ref $parameters ) {
    100 66        
    50 33        
1345 1         16 @args = map { split /=/xsm } split /&/xsm, $parameters;
  4         9  
1346             }
1347             elsif ( $parameters && reftype($parameters) eq 'HASH' ) {
1348 1         3 foreach my $key ( keys %{$parameters} ) {
  1         3  
1349 2 50 33     7 if ( ref $parameters->{$key}
1350             && reftype( $parameters->{$key} ) eq 'ARRAY' ) {
1351 0         0 push @args, map { %{$_} } @{ $parameters->{$key} };
  0         0  
  0         0  
  0         0  
1352             }
1353             else {
1354 2         7 push @args, $key, $parameters->{$key};
1355             }
1356             }
1357             }
1358             elsif ( $parameters && reftype($parameters) eq 'ARRAY' ) {
1359              
1360             # if any are refs then they should be hashes...
1361 3 100   7   12 if ( any {ref} @{$parameters} ) {
  7 100       15  
  3         11  
1362              
1363 1         28 @args = map { %{$_} } @{$parameters}; # list of hashes
  2         3  
  2         8  
  1         6  
1364             }
1365 5     5   11 elsif ( any {/=/xsm} @{$parameters} ) {
  2         4  
1366 1         3 @args = map { split /=/xsm } @{$parameters}; # formatted list
  2         6  
  1         3  
1367             }
1368             else {
1369 1         2 @args = @{$parameters}; # simple list
  1         3  
1370             }
1371             }
1372              
1373 5         15 my $content;
1374              
1375 5 100 66 17   28 if ( $action && !any {/Action/xsm} @args ) {
  17         34  
1376 4         9 push @args, 'Action', $action;
1377             }
1378              
1379 5 100 66 31   22 if ( $version && !any {/Version/xsm} @args ) {
  31         47  
1380 4         8 push @args, 'Version', $version;
1381             }
1382              
1383 5         37 return join $AMPERSAND, map { sprintf '%s=%s', @{$_} } pairs @args;
  20         26  
  20         80  
1384             }
1385              
1386             ########################################################################
1387             sub has_keys {
1388             ########################################################################
1389 0     0 0 0 my ( $self, %options ) = @_;
1390              
1391 0 0       0 my %creds = keys %options ? %options : @{$self}{@REQUIRED_KEYS};
  0         0  
1392              
1393 0   0     0 return $creds{'aws_secret_access_key'} && $creds{'aws_access_key_id'};
1394             }
1395              
1396             # +-----------------+
1397             # | PRIVATE METHODS |
1398             # +-----------------+
1399              
1400             # should not be called if we have botocore definition
1401             ########################################################################
1402             sub _set_content_type {
1403             ########################################################################
1404 0     0   0 my ($self) = @_;
1405              
1406 0         0 my $service = $self->get_service;
1407              
1408             # default content-type
1409 0         0 my $content_type = $self->get_content_type;
1410              
1411             return 'application/x-www-form-urlencoded'
1412 0 0   0   0 if any { $_ eq $service } @{ $API_TYPES{query} };
  0         0  
  0         0  
1413              
1414             return 'application/x-amz-json-1.1'
1415 0 0   0   0 if any { $_ eq $service } @{ $API_TYPES{json} };
  0         0  
  0         0  
1416              
1417             return 'application/xml'
1418 0 0   0   0 if any { $_ eq $service } @{ $API_TYPES{xml} };
  0         0  
  0         0  
1419              
1420 0         0 return $content_type;
1421             }
1422              
1423             ########################################################################
1424             sub _create_methods {
1425             ########################################################################
1426 1     1   5 my ($self) = @_;
1427              
1428 1   33     5 my $class = ref $self || $self;
1429              
1430 1 50       27 if ( $self->get_api_methods ) {
1431 3     3   18743 no strict 'refs'; ## no critic (TestingAndDebugging::ProhibitNoStrict)
  3         6  
  3         138  
1432 3     3   19 no warnings 'redefine'; ## no critic (TestingAndDebugging::ProhibitNoWarnings)
  3         6  
  3         962  
1433              
1434 0         0 my $stash = \%{ __PACKAGE__ . $DOUBLE_COLON };
  0         0  
1435              
1436 0         0 foreach my $api ( @{ $self->get_api_methods } ) {
  0         0  
1437              
1438 0         0 my $method = lcfirst $api;
1439              
1440 0         0 $method =~ s/([[:lower:]])([[:upper:]])/$1_$2/xmsg;
1441 0         0 $method = lc $method;
1442              
1443 0         0 my $snake_case_method = $class . $DOUBLE_COLON . $method;
1444 0         0 my $camel_case_method = $class . $DOUBLE_COLON . $api;
1445              
1446             # snake case rules the day
1447              
1448 0 0       0 if ( !$stash->{$method} ) {
1449 0         0 *{$snake_case_method} = sub {
1450 0     0   0 my $self = shift;
1451              
1452 0         0 $self->invoke_api( $api, @_ );
1453 0         0 };
1454             }
1455              
1456             # ...but some prefer camels
1457 0 0       0 if ( !$stash->{$api} ) {
1458 0         0 *{$camel_case_method} = sub {
1459 0     0   0 my $self = shift;
1460              
1461 0         0 $self->$method(@_);
1462 0         0 };
1463             }
1464             }
1465              
1466             }
1467              
1468 1         8 return $self;
1469             }
1470              
1471             ########################################################################
1472             sub _create_stealth_logger {
1473             ########################################################################
1474 0     0   0 my ( $self, $name, $level ) = @_;
1475              
1476 0         0 my $sub_name = sprintf '%s::%s', $name, uc $level;
1477              
1478 3     3   36 no strict 'refs'; ## no critic ProhibitNoStrict
  3         5  
  3         3675  
1479              
1480             return
1481 0 0       0 if defined *{$sub_name}{CODE};
  0         0  
1482              
1483 0         0 *{$sub_name} = sub {
1484 0 0 0 0   0 if ( $self->get_logger && $self->get_logger->can($level) ) {
1485 0         0 $self->get_logger->$level(@_);
1486             }
1487             else {
1488 0         0 return;
1489             }
1490 0         0 };
1491              
1492 0         0 return;
1493             }
1494              
1495             ########################################################################
1496             sub _set_default_logger {
1497             ########################################################################
1498 1     1   4 my ($self) = @_;
1499              
1500 1 50 33     21 if ( !$self->get_logger && !Log::Log4perl->initialized ) {
1501              
1502 1         23 eval {
1503 1         7 require Log::Log4perl;
1504              
1505 1         7 Log::Log4perl->import(':easy');
1506              
1507             Log::Log4perl->easy_init(
1508 1 50 33     797 { level => $LOG4PERL_LOG_LEVELS{'info'},
1509             layout => $self->get_log_layout // $DEFAULT_LAYOUT_PATTERN,
1510             ( $self->get_log_file ? ( file => $self->get_log_file ) : () ),
1511              
1512             }
1513             );
1514             };
1515             }
1516             else {
1517 0         0 for my $level (qw(debug trace info warn error )) {
1518 0         0 $self->_create_stealth_logger( __PACKAGE__, $level );
1519 0         0 $self->_create_stealth_logger( ref($self), $level );
1520             }
1521             }
1522              
1523 1         6046 return $self;
1524             }
1525              
1526             ########################################################################
1527             sub _set_defaults {
1528             ########################################################################
1529 1     1   5 my ( $self, %options ) = @_;
1530              
1531 1   33     32 $self->set_raise_error( $self->get_raise_error // $TRUE );
1532 1   33     59 $self->set_print_error( $self->get_print_error // $TRUE );
1533              
1534 1   33     55 $self->set_use_paginator( $self->get_use_paginator // $TRUE );
1535              
1536 1 50       51 if ( !$self->get_user_agent ) {
1537 1         17 $self->set_user_agent( LWP::UserAgent->new );
1538             }
1539              
1540 1 50       3747 if ( !defined $self->get_decode_always ) {
1541 1         30 $self->set_decode_always($TRUE);
1542              
1543 1 0 33     34 if ( !$self->get_decode_always && !defined $self->get_force_array ) {
1544 0         0 $self->set_force_array($FALSE);
1545             }
1546             }
1547              
1548             # most API services are POST, but using the Botocore metadata is best
1549 1   50     26 $self->set_http_method( $self->get_http_method // 'POST' );
1550              
1551 1   50     62 $self->set_protocol( $self->get_protocol() // 'https' );
1552              
1553             # note some APIs are global, hence an API may send '' to indicate global
1554 1 50       58 if ( !defined $self->get_region ) {
1555             $self->set_region( $self->get_region
1556             || $ENV{'AWS_REGION'}
1557 1   33     25 || $ENV{'AWS_DEFAULT_REGION'}
1558             || $DEFAULT_REGION );
1559             }
1560              
1561 1   33     47 my $debug //= ( $options{debug} || $ENV{DEBUG} );
      33        
1562              
1563 1 50 50     10 $options{log_level} = $debug ? 'debug' : $options{log_level} // 'info';
1564              
1565 1         6 $self->set_log_level( $options{log_level} );
1566              
1567 1 50       27 if ( !$self->get_credentials ) {
1568 0 0       0 if ( $self->has_keys(%options) ) {
1569             $self->set_credentials(
1570             Amazon::Credentials->new(
1571             { aws_secret_access_key => $options{'aws_secret_access_key'},
1572             aws_access_key_id => $options{'aws_access_key_id'},
1573 0         0 token => $options{'token'},
1574             region => $self->get_region,
1575             }
1576             )
1577             );
1578             }
1579             else {
1580             $self->set_credentials(
1581             Amazon::Credentials->new(
1582             order => $options{'order'},
1583 0         0 region => $options{'region'},
1584             )
1585             );
1586              
1587 0 0       0 if ( !defined $self->get_region ) {
1588 0         0 $self->set_region( $self->get_credentials->get_region );
1589             }
1590             }
1591             }
1592              
1593             # set URL last since it contains region
1594 1         12 $self->_set_url;
1595              
1596 1         3 return $self;
1597             }
1598              
1599             ########################################################################
1600             sub _create_service_url {
1601             ########################################################################
1602 1     1   3 my ($self) = @_;
1603              
1604 1         3 my $url;
1605              
1606 1         23 my $botocore_metadata = $self->get_botocore_metadata;
1607 1         23 my $service = $self->get_service;
1608              
1609 1 50 33     8 if ( $botocore_metadata && $botocore_metadata->{globalEndpoint} ) {
1610             $url = sprintf '%s://%s', $self->get_protocol,
1611 0         0 $botocore_metadata->{globalEndpoint};
1612             }
1613             else {
1614 1   33     20 my $endpoint = $self->get_endpoint_prefix || $service;
1615              
1616 1 50   7   20 if ( any { $_ eq $service } @GLOBAL_SERVICES ) {
  7 50       29  
1617 0         0 $url = sprintf $GLOBAL_URL_FMT, $self->get_protocol, $endpoint;
1618             }
1619             elsif ( $self->get_region ) {
1620 1         23 $url = sprintf $REGIONAL_URL_FMT, $self->get_protocol, $endpoint,
1621             $self->get_region;
1622             }
1623             else {
1624 0         0 $url = sprintf $REGIONAL_URL_FMT, $self->get_protocol, $endpoint,
1625             'us-east-1';
1626             }
1627             }
1628              
1629 1         31 return $url;
1630             }
1631              
1632             ########################################################################
1633             sub _set_url {
1634             ########################################################################
1635 1     1   3 my ($self) = @_;
1636              
1637 1         19 my $url = $self->get_url;
1638              
1639 1 50       20 if ( !$url ) {
1640 1         6 $url = $self->_create_service_url;
1641             }
1642             else {
1643 0 0       0 if ( $url !~ /^https?/xmsi ) {
1644 0         0 $url =~ s/^\///xms; # just remove leading slash...
1645 0         0 $url = $self->get_protocol . '://' . $url;
1646             }
1647              
1648             }
1649              
1650 1         21 $self->set_url($url);
1651              
1652 1         9 return $self;
1653             }
1654              
1655             ########################################################################
1656             sub _set_x_amz_target {
1657             ########################################################################
1658 0     0     my ( $self, $request ) = @_;
1659              
1660 0           my $target = $self->get_target_prefix;
1661 0           my $version = $self->get_version;
1662 0           my $api = $self->get_api;
1663 0           my $action = $self->get_action;
1664              
1665 0 0         if ( !$target ) {
1666 0 0         $target = $version ? $api . $UNDERSCORE . $version : $api;
1667             }
1668              
1669 0           $target = $target . $DOT . $action;
1670              
1671 0           $self->set_target($target);
1672              
1673 0           $request->header( 'X-Amz-Target', $target );
1674              
1675 0           return $request;
1676             }
1677              
1678             ########################################################################
1679             sub _set_request_content {
1680             ########################################################################
1681 0     0     my ( $self, %args ) = @_;
1682              
1683 0           my $request = $args{'request'};
1684 0           my $content = $args{'content'};
1685 0   0       my $content_type = $args{'content_type'} || $self->get_content_type;
1686              
1687             DEBUG(
1688             sub {
1689 0     0     return Dumper(
1690             [ '_set_request_content', $self->get_http_method, \%args ] );
1691             }
1692 0           );
1693              
1694 0 0 0       if ( $self->get_http_method ne 'GET' || !defined $content ) {
1695 0 0         if ($content_type) {
1696 0           $request->content_type( $content_type . '; charset=utf-8' );
1697             }
1698 0           $request->content($content);
1699             }
1700             else {
1701 0           $request->uri( $request->uri . $QUESTION_MARK . $content );
1702             }
1703              
1704 0           return $request;
1705             }
1706              
1707             1;
1708              
1709             __END__
1710              
1711             =pod
1712              
1713             =head1 NAME
1714              
1715             Amazon::API - A generic base class for AWS Services
1716              
1717             =head1 SYNOPSIS
1718              
1719             package Amazon::CloudWatchEvents;
1720              
1721             use parent qw( Amazon::API );
1722              
1723             # subset of methods I need
1724             our @API_METHODS = qw(
1725             ListRuleNamesByTarget
1726             ListRules
1727             ListTargetsByRule
1728             PutEvents
1729             PutRule
1730             );
1731              
1732             sub new {
1733             my $class = shift;
1734              
1735             $class->SUPER::new(
1736             service => 'events',
1737             api => 'AWSEvents',
1738             api_methods => \@API_METHODS,
1739             decode_always => 1
1740             );
1741             }
1742              
1743             1;
1744              
1745             Then...
1746              
1747             my $rules = Amazon::CloudWatchEvents->new->ListRules({});
1748              
1749             ...or
1750              
1751             my $rules = Amazon::API->new(
1752             { service => 'events',
1753             api => 'AWSEvents',
1754             }
1755             )->invoke_api( 'ListRules', {} );
1756              
1757             =head1 DESCRIPTION
1758              
1759             Generic class for constructing AWS API interfaces. Typically used as a
1760             parent class, but can be used directly. This package can also
1761             generates stubs for Amazon APIs using the Botocore project's
1762             metadata. (See L</BOTOCORE SUPPORT>).
1763              
1764             =over 5
1765              
1766             =item * See L</IMPLEMENTATION NOTES> for using C<Amazon::API>
1767             directly to call AWS services.
1768              
1769             =item * See
1770             L<Amazon::CloudWatchEvents|https://github.com/rlauer6/perl-Amazon-CloudWatchEvents/blob/master/src/main/perl/lib/Amazon/CloudWatchEvents.pm.in>
1771             for an example of how to use this module as a parent class.
1772              
1773             =item * See L<Amazon::API::Botocore::Pod> for information regarding
1774             how to automatically create Perl classes for AWS services using
1775             Botocore metadata.
1776              
1777             =back
1778              
1779             =head1 BACKGROUND AND MOTIVATION
1780              
1781             A comprehensive Perl interface to AWS services similar to the I<Botocore>
1782             library for Python has been a long time in coming. The PAWS project
1783             has been attempting to create an always up-to-date AWS interface with
1784             community support. Some however may find that project a little heavy
1785             in the dependency department. If you are looking for an extensible
1786             (albeit spartan) method of invoking a subset of services with a lower
1787             dependency count, you might want to consider C<Amazon::API>.
1788              
1789             Think of this class as a DIY kit for invoking B<only> the methods you
1790             need for your AWS project. Using the included C<amazon-api> utility
1791             however, you can also roll your own complete Amazon API classes that
1792             include support for serializing requests and responses based on
1793             metadata provided by the Botocore project. The classes you create with
1794             C<amazon-api> include full documentation as pod. (See L</BOTOCORE
1795             SUPPORT> for more details).
1796              
1797             =over 5
1798              
1799             I<NOTE:> The original L<Amazon::API> was written in 2017 as a I<very>
1800             lightweight way to call a handfull of APIs. The evolution of the
1801             module was based on discovering, without much documentation or help,
1802             the nature of Amazon APIs. In retrospect, even back then, it would
1803             have been easier to consult the Botocore project and decipher how that
1804             project managed to create a library from the metadata. Fast forward
1805             to 2022 and L<Amazon::API> is now capable of using the Botocore
1806             metadata in order to, in most cases, correctly call any AWS service.
1807             The L<Amazon::API> module can still be used without the assistance of
1808             Botocore metadata, but it works a heckuva lot better with it.
1809              
1810             =back
1811              
1812             You can use L<Amazon::API> in 3 different ways:
1813              
1814             =over 5
1815              
1816             =item Take the Luddite approach
1817              
1818             my $queues = Amazon::API->new(
1819             {
1820             service => 'sqs',
1821             http_method => 'GET'
1822             })->invoke_api('ListQueues');
1823              
1824             =item Build your own API classes with just what you need
1825              
1826             package Amazon::API::SQS;
1827            
1828             use strict;
1829             use warnings;
1830            
1831             use parent qw( Amazon::API );
1832            
1833             our @API_METHODS = qw(
1834             ListQueues
1835             PurgeQueue
1836             ReceiveMessage
1837             SendMessage
1838             );
1839            
1840             sub new {
1841             my ( $class, @options ) = @_;
1842             $class = ref($class) || $class;
1843            
1844             my %options = ref( $options[0] ) ? %{ $options[0] } : @options;
1845            
1846             return $class->SUPER::new(
1847             { service => 'sqs',
1848             http_method => 'GET',
1849             api_methods => \@API_METHODS,
1850             decode_always => 1,
1851             %options
1852             }
1853             );
1854             }
1855            
1856             1;
1857              
1858             use Amazon::API::SQS;
1859             use Data::Dumper;
1860              
1861             my $sqs = Amazon::API::SQS->new;
1862             print Dumper($sqs->ListQueues);
1863              
1864             =item Use the Botocore metadata to build classes for you
1865              
1866             amazon-api -s sqs create-stubs
1867             amazon-api -s sqs create-shapes
1868              
1869             perl -I . -MData::Dumper -MAmazon::API:SQS -e 'print Dumper(Amazon::API::SQS->new->ListQueues);'
1870              
1871             =over 5
1872              
1873             I<NOTE:> In order to use Botocore metadata you must clone the Botocore
1874             repository and point the utility to the repo.
1875              
1876             Clone the Botocore project from GitHub:
1877              
1878             mkdir ~/git
1879             cd git
1880             git clone https:://github.com/boto/botocore.git
1881              
1882             Generate stub classes for the API and shapes:
1883              
1884             amazon-api -b ~/git/botocore -s sqs -o ~/lib/perl5 create-stubs
1885             amazon-api -b ~/git/botocore -s sqs -o ~/lib/perl5 create-shapes
1886              
1887             perldoc Amazon::API::SQS
1888              
1889             See L<Amazon::API::Botocore::Pod> for more details regarding building
1890             stubs and shapes.
1891              
1892             =back
1893              
1894             =back
1895              
1896             =head1 THE APPROACH
1897              
1898             Essentially, most AWS APIs are RESTful services that adhere to a
1899             common protocol, but differences in services make a single solution
1900             difficult. All services more or less adhere to this framework:
1901              
1902             =over 5
1903              
1904             =item 1. Set HTTP headers (or query string) to indicate the API and
1905             method to be invoked
1906              
1907             =item 2. Set credentials in the header
1908              
1909             =item 3. Set API specific headers
1910              
1911             =item 4. Sign the request and set the signature in the header
1912              
1913             =item 5. Optionally send a payload of parameters for the method being invoked
1914              
1915             =back
1916              
1917             Specific details of the more recent AWS services are well documented,
1918             however early services were usually implemented as simple HTTP
1919             services that accepted a query string. This module attempts to account
1920             for most of the nuances involved in invoking AWS services and
1921             provide a fairly generic way of invoking these APIs in the most
1922             lightweight way possible.
1923              
1924             Using L<Amazon::API> as a generic, lightweight module, naturally does
1925             not provide nuanced support for individual AWS services. To use this
1926             class in that manner for invoking the AWS APIs, you need to be very
1927             familiar with the specific API requirements and responses and be
1928             willng to invest time reading the documentation on Amazon's website.
1929             The payoff is that you can probably use this class to call I<any> AWS
1930             API without installing a large number of dependencies.
1931              
1932             If you don't mind a few extra dependencies and overhead, you should
1933             generate the stub APIs and support classes using the C<amazon-api>
1934             utility. The stubs and shapes produced by the utility will serialize
1935             and deserialize requests and responses correctly by using the Botocore
1936             metadata. Botocore metadata provides the necessary information to
1937             create classes that can successfully invoke all of the Amazon APIs.
1938              
1939             A good example of creating a quick and dirty interface to CloudWatch
1940             Events can be found here:
1941              
1942             L<Amazon::CloudWatchEvents|https://github.com/rlauer6/perl-Amazon-CloudWatchEvents/blob/master/src/main/perl/lib/Amazon/CloudWatchEvents.pm.in>
1943              
1944             And invoking some of the APIs can be as easy as:
1945              
1946             Amazon::API->new(
1947             service => 'sqs',
1948             http_method => 'GET'
1949             }
1950             )->invoke_api('ListQueues');
1951              
1952             =head1 BOTOCORE SUPPORT
1953              
1954             A new experimental module ( L<Amazon::API::Botocore>) is now included
1955             in this project.
1956              
1957             B<!!CAUTION!!>
1958              
1959             I<Support for API calls using the Botocore metadata may be buggy and
1960             is subject to change. It may not be suitable for production
1961             environments at this time.>
1962              
1963             Using Botocore metadata and the utilities in this project, you can
1964             create Perl classes that simplify calling AWS services. After
1965             creating service classes and shape objects from the Botocore metadata
1966             calling AWS APIs will look something like this:
1967              
1968             use Amazon::API::SQS;
1969              
1970             my $sqs = Amazon::API::SQS->new;
1971             my $rsp = $sqs->ListQueues();
1972              
1973             The L<Amazon::API::Botocore> module augments L<Amazon::API> by using
1974             Botocore metadata for determining how to call individual services and
1975             serialize parameters passed to its API methods. A utility (C<amazon-api>)
1976             is provided that can generate Perl classes for all AWS services using
1977             the Botocore metadata.
1978              
1979             Perl classes that represent AWS data structures (aka shapes) that are
1980             passed to or returned from services can also be generated. These
1981             classes allow you to call all of the API methods for a given service
1982             using simple Perl objects that are serialized correctly for a specific
1983             method.
1984              
1985             Service classes are subclassed from C<Amazon::API> so their C<new()>
1986             constructor takes the same arguments as C<Amazon::API::new()>.
1987              
1988             my $credentials = Amazon::Credential->new();
1989              
1990             my $sqs = Amazon::API::SQS->new( credentials => $credentials );
1991              
1992             If you are going to use the Botocore support and automatically
1993             generate API classes you I<must also> create the data structure classes
1994             that are used by each service. The Botocore based APIs will use these
1995             classes to serialize requests and responses.
1996              
1997             For more information on generating API classes, see
1998             L<Amazon::API::Botocore::Pod>.
1999              
2000             =head2 Serialization Errors
2001              
2002             With little documentation to go on, Interpretting the Botocore
2003             metadata and deducing how to serialize shapes from Perl objects has
2004             been a difficult task. It's likely that there are still some edge
2005             cases and bugs lurking in the serialization methods. Accordingly,
2006             starting with version 1.4.5, serialization exceptions or exceptions
2007             that occur while attempting to decode a response, will result in the
2008             raw response being returned to the caller. The idea being that getting
2009             something back that allows you figure out what to do with the response
2010             might be better than receiving an error.
2011              
2012             OTOH, you might want to see the error, report it, or possibly
2013             contribute to its resolution. You can prevent errors from being
2014             surpressed by setting the C<raise_serializtion_errors> to a true
2015             value. The default is I<false>.
2016              
2017             I<Throughout the rest of this documentation a request made using one
2018             of the classes created by the Botocore support scripts will be
2019             referred to as a B<Botocore request> or B<Botocore API>.>
2020              
2021             =over 5
2022              
2023             =item *
2024              
2025             =back
2026              
2027             =head1 ERRORS
2028              
2029             When an error is returned from an API request, an exception class
2030             (C<Amazon::API::Error>) will be raised if C<raise_error> has been set
2031             to a true value. Additionally, a detailed error message will be
2032             displayed if C<print_error> is set to true.
2033              
2034             See L<Amazon::API::Error> for more details.
2035              
2036             =head1 METHODS AND SUBROUTINES
2037              
2038             I<Reminder: You can mostly ignore this part of the documentation when
2039             you are leveraging Botocore to generate your API classes.>
2040              
2041             =head2 new
2042              
2043             new(options)
2044              
2045             All options are described below. C<options> can be a list of
2046             key/values or hash reference.
2047              
2048             =over 5
2049              
2050             =item action
2051              
2052             The API method. Normally, you would not set C<action> when you
2053             construct your object. It is set when you call the C<invoke_api>
2054             method or automatically set when you call one of the API stubs created
2055             for you.
2056              
2057             Example: 'PutEvents'
2058              
2059             =item api
2060              
2061             The name of the AWS service. See L</IMPLEMENTATION NOTES> for a
2062             detailed explanation of when to set this value.
2063              
2064             Example: 'AWSEvents'
2065              
2066             =item api_methods
2067              
2068             A reference to an array of method names for the API. The new
2069             constructor will automatically create methods for each of the method
2070             names listed in the array.
2071              
2072             The methods that are created for you are nothing more than stubs that
2073             call C<invoke_api>. The stub is a convenience for calling the
2074             C<invoke_api> method as shown below.
2075              
2076            
2077             my $api = Amazon::CloudWatch->new;
2078              
2079             $api->PutEvents($events);
2080              
2081             ...is equivalent to:
2082              
2083             $api->invoke_api->('PutEvents', $events);
2084              
2085             Consult the Amazon API documentation for the service to determine what
2086             parameters each action requires.
2087              
2088             =item aws_access_key_id
2089              
2090             Your AWS access key. Both the access key and secret access key are
2091             required if either is passed. If no credentials are passed, an attempt
2092             will be made to find credentials using L<Amazon::Credentials>. Note
2093             that you may need to pass C<token> as well if you are using temporary
2094             credentials.
2095              
2096             =item aws_secret_access_key
2097              
2098             Your AWS secret access key.
2099              
2100             =item content_type
2101              
2102             Default content for parameters passed to the C<invoke_api()>
2103             method. If you do not provide this value, a default content type will
2104             be selected based on the service's protocol.
2105              
2106             query => application/x-www-form-urlencoded
2107             rest-json => application/x-amz-json-1.1
2108             json => application/json
2109             rest-xml => application/xml
2110              
2111             =item credentials (optional)
2112              
2113             Accessing AWS services requires credentials with sufficient privileges
2114             to make programmatic calls to the APIs that support a service. This
2115             module supports three ways that you can provide those credentials.
2116              
2117             =over 10
2118              
2119             =item 1. Pass the credentials directly.
2120              
2121             Pass the values for the credentials (C<aws_access_key_id>,
2122             C<aws_secaret_access_key>, C<token>) when you call the C<new> method.
2123             A session token is typically required when you have assumed
2124             a role, you are using the EC2's instance role or a container's role.
2125              
2126             =item 2. Pass a class that will provide the credential keys.
2127              
2128             Pass a reference to a class that has I<getters> for the credential
2129             keys. The class should supply I<getters> for all three credential keys.
2130              
2131             Pass the reference to the class as C<credentials> in the constructor
2132             as shown here:
2133              
2134             my $api = Amazon::API->new(credentials => $credentials_class, ... );
2135              
2136             =item 3. Use the default C<Amazon::Credentials> class.
2137              
2138             If you do not explicitly pass credentials or do not pass a class that
2139             will supply credentials, the module will use the
2140             C<Amazon::Credentials> class that attempts to find credentials in the
2141             I<environment>, your I<credentials file(s)>, or the I<container or
2142             instance role>. See L<Amazon::Credentials> for more details.
2143              
2144             I<NOTE: The latter method of obtaining credentials is probably the
2145             easiest to use and provides the most succinct and secure way of
2146             obtaining credentials.>
2147              
2148             =back
2149              
2150             =item debug
2151              
2152             Set debug to a true value to enable debug messages. Debug mode will
2153             dump the request and response from all API calls. You can also set the
2154             environment variable DEBUG to enable debugging output. Set the debug
2155             value to '2' to increase the logging level.
2156              
2157             default: false
2158              
2159             =item decode_always
2160              
2161             Set C<decode_always> to a true value to return Perl objects from API
2162             method calls. The default is to return the raw output from the call.
2163             Typically, API calls will return either XML or JSON encoded objects.
2164             Setting C<decode_always> will attempt to decode the content based on
2165             the returned content type.
2166              
2167             default: false
2168              
2169             =item error
2170              
2171             The most recent result of an API call. C<undef> indicates no error was
2172             encountered the last time C<invoke_api> was called.
2173              
2174             =item http_method
2175              
2176             Sets the HTTP method used to invoke the API. Consult the AWS
2177             documentation for each service to determine the method utilized. Most
2178             of the more recent services utilize the POST method, however older
2179             services like SQS or S3 utilize GET or a combination of methods
2180             depending on the specific method being invoked.
2181              
2182             default: POST
2183              
2184             =item last_action
2185              
2186             The last method call invoked.
2187              
2188             =item print_error
2189              
2190             Setting this value to true enables a detailed error message containing
2191             the error code and any messages returned by the API when errors occur.
2192              
2193             default: true
2194              
2195             =item protocol
2196              
2197             One of 'http' or 'https'. Some Amazon services do not support https
2198             (yet).
2199              
2200             default: https
2201              
2202             =item raise_error
2203              
2204             Setting this value to true will raise an exception when errors
2205             occur. If you set this value to false you can inspect the C<error>
2206             attribute to determine the success or failure of the last method call.
2207              
2208             $api->invoke_api('ListQueues');
2209              
2210             if ( $api->get_error ) {
2211             ...
2212             }
2213              
2214             default: true
2215              
2216             =item region
2217              
2218             The AWS region. Pass an empty string if the service is a global
2219             service that does not require or want a region.
2220              
2221             default: $ENV{'AWS_REGION'}, $ENV{'AWS_DEFAULT_REGION'}, 'us-east-1'
2222              
2223             =item response
2224              
2225             The HTTP response from the last API call.
2226              
2227             =item service
2228              
2229             The AWS service name. Example: C<sqs>. This value is used as a prefix
2230             when constructing the the service URL (if not C<url> attribute is set).
2231              
2232             =item service_url_base
2233              
2234             Deprecated, use C<service>
2235              
2236             =item token
2237              
2238             Session token for assumed roles.
2239              
2240             =item url
2241              
2242             The service url. Example: https://events.us-east-1.amazonaws.com
2243              
2244             Typically this will be constructed for you based on the region and the
2245             service being invoked. However, you may want to set this manually if
2246             for example you are using a service like
2247             <LocalStack|https://localstack.cloud/> that mocks AWS API calls.
2248              
2249             my $api = Amazon::API->new(service => 's3', url => 'http://localhost:4566/');
2250              
2251             =item user_agent
2252              
2253             Your own user agent object. Using
2254             C<Furl>, if you have it avaiable may result in faster response.
2255              
2256             default: C<LWP::UserAgent>
2257              
2258             =item version
2259              
2260             Sets the API version. Some APIs require a version. Consult the
2261             documentation for individual services.
2262              
2263             =back
2264              
2265             =head2 invoke_api
2266              
2267             invoke_api(action, [parameters], [content-type], [headers]);
2268              
2269             or using named parameters...
2270              
2271             invoke_api({ action => args, ... } )
2272              
2273             Invokes the API with the provided parameters.
2274              
2275             =over 5
2276              
2277             =item action
2278              
2279             API name.
2280              
2281             =item parameters
2282              
2283             Parameters to send to the API. C<parameters> can be a scalar, a hash
2284             reference or an array reference. See the discussion below regarding
2285             C<content-type> and how C<invoke_api()> formats parameters before
2286             sending them as a payload to the API.
2287              
2288             You can use the C<param_n()> method to format query string arguments
2289             that are required to be in the I<param.n> notation. This is about the
2290             best documentation I have seen for that format. From the AWS
2291             documentation...
2292              
2293             =over 10
2294              
2295             Some actions take lists of parameters. These lists are specified using
2296             the I<param.n> notation. Values of n are integers starting from 1. For
2297             example, a parameter list with two elements looks like this:
2298              
2299             &AttributeName.1=first
2300              
2301             &AttributeName.2=second
2302              
2303             =back
2304              
2305             An example of using this notation is to set queue attributes when
2306             creating an SQS queue.
2307              
2308             my $attributes = { Attributes => [ { Name => 'VisibilityTimeout', Value => '100' } ] };
2309             my @sqs_attributes= Amazon::API::param_n($attributes);
2310              
2311             eval {
2312             $sqs->CreateQueue([ 'QueueName=foo', @sqs_attributes ]);
2313             };
2314              
2315             See L</param_n> for more details.
2316              
2317             =item content-type
2318              
2319             If you pass the C<content-type> parameter, it is assumed that the parameters are
2320             the actual payload to be sent in the request (unless the parameter is a reference).
2321              
2322             The C<parameters> will be converted to a JSON string if the
2323             C<parameters> value is a hash reference. If the C<parameters> value
2324             is an array reference it will be converted to a query string (Name=Value&...).
2325              
2326             To pass a query string, you should send an array of key/value
2327             pairs, or an array of scalars of the form C<Name=Value>.
2328              
2329             [ { Action => 'DescribeInstances' } ]
2330             [ 'Action=DescribeInstances' ]
2331              
2332             =item headers
2333              
2334             Array reference of key/value pairs representing additional headers to
2335             send with the request.
2336              
2337             =back
2338              
2339             =head2 decode_response
2340              
2341             Boolean that indicates whether or not to deserialize the most recent
2342             response from an invoked API based on the I<Content-Type> header
2343             returned. If there is no I<Content-Type> header, then the method will
2344             try to decode it first as a JSON string and then as an XML string. If
2345             both of those fail, the raw content is returned.
2346              
2347             You can enable or disable deserializing responses globally by setting
2348             the C<decode_always> attribute when you call the C<new> constructor.
2349              
2350             default: true
2351              
2352             By default, `Amazon::API` will retrieve all results for Botocore based
2353             API calls that require pagination. To turn this behavior off, set
2354             C<use_paginator> to a false value when you instantiate the API
2355             service.
2356              
2357             my $ec2 = Amazon::API->new(use_paginator => 0);
2358              
2359             You can also use the L</paginator> method to retrieve all results from Botocore requests that implement pagination.
2360              
2361             =head2 submit
2362              
2363             submit(options)
2364              
2365             I<This method is used internally by C<invoke_api> and normally should
2366             not be called by your applications.>
2367              
2368             C<options> is a reference to a hash of options:
2369              
2370             =over 5
2371              
2372             =item content
2373              
2374             Payload to send.
2375              
2376             =item content_type
2377              
2378             Content types we have seen used to send values to AWS APIs:
2379              
2380             application/json
2381             application/x-amz-json-1.0
2382             application/x-amz-json-1.1
2383             application/x-www-form-urlencoded
2384              
2385             Check the documentation for the individual APIs for the correct
2386             content type.
2387              
2388             =item headers
2389              
2390             Array reference of key/value pairs that represent additional headers
2391             to send with the request.
2392              
2393             =back
2394              
2395             =head1 EXPORTED METHODS
2396              
2397             =head2 get_api_service
2398              
2399             get_api_service(api, options)
2400              
2401             Convenience routine that will return an API instance.
2402              
2403             my $sqs = get_api_service 'sqs';
2404              
2405             Equivalent to:
2406              
2407             require Amazon::API::SQS;
2408              
2409             my $sqs = Amazon::API::SQS->new(%options);
2410              
2411             =over 5
2412              
2413             =item api
2414              
2415             The service name. Example: route53, sqs, sns
2416              
2417             =item options
2418              
2419             list of key/value pairs passed to the new constructor as options
2420              
2421             =back
2422              
2423             =head2 create_url_encoded_content
2424              
2425             create_urlencoded_content(parameters, action, version)
2426              
2427             Returns a URL encoded query string. C<parameters> can be any of SCALAR, ARRAY, or HASH. See below.
2428              
2429             =over 5
2430              
2431             =item parameters
2432              
2433             =over 5
2434              
2435             =item SCALAR
2436              
2437             Query string to encode (x=y&w=z..)
2438              
2439             =item ARRAY
2440              
2441             Can be one of:
2442              
2443             =over 5
2444              
2445             =item * Array of hashes where the keys are the query string variable and the value is the value of that variable
2446              
2447             =item * Array of strings of the form "x=y"
2448              
2449             =item * An array of key/value pairs - qw( x y w z )
2450              
2451             =back
2452              
2453             =item HASH
2454              
2455             Key/value pairs. If value is an array it is assumed to be a list of hashes
2456              
2457             =back
2458              
2459             =item action
2460              
2461             The method being called. For some query type APIs an Action query variable is required.
2462              
2463             =item version
2464              
2465             The WSDL version for the API. Some query type APIs require a Version query variable.
2466              
2467             =back
2468              
2469             =head2 paginator
2470              
2471             paginator(service, api, request)
2472              
2473             Returns an array containing the results of an API call that requires
2474             pagination,
2475              
2476              
2477             my $result = paginator($ec2, 'DescribeInstances', { MaxResults => 10 });
2478              
2479             =head2 param_n
2480              
2481             param_n(parameters)
2482              
2483             Format parameters in the "param.n" notation.
2484              
2485             C<parameters> should be a hash or array reference.
2486              
2487             A good example of a service that uses this notation is the
2488             I<SendMessageBatch> SQS API call.
2489              
2490             The sample request can be found here:
2491              
2492             L<SendMessageBatch|https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessageBatch.html>
2493              
2494              
2495             https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue/
2496             ?Action=SendMessageBatch
2497             &SendMessageBatchRequestEntry.1.Id=test_msg_001
2498             &SendMessageBatchRequestEntry.1.MessageBody=test%20message%20body%201
2499             &SendMessageBatchRequestEntry.2.Id=test_msg_002
2500             &SendMessageBatchRequestEntry.2.MessageBody=test%20message%20body%202
2501             &SendMessageBatchRequestEntry.2.DelaySeconds=60
2502             &SendMessageBatchRequestEntry.2.MessageAttribute.1.Name=test_attribute_name_1
2503             &SendMessageBatchRequestEntry.2.MessageAttribute.1.Value.StringValue=test_attribute_value_1
2504             &SendMessageBatchRequestEntry.2.MessageAttribute.1.Value.DataType=String
2505             &Expires=2020-05-05T22%3A52%3A43PST
2506             &Version=2012-11-05
2507             &AUTHPARAMS
2508              
2509             To produce this message you would pass the Perl object below to C<param_n()>:
2510              
2511             my $message = {
2512             SendMessageBatchRequestEntry => [
2513             { Id => 'test_msg_001',
2514             MessageBody => 'test message body 1'
2515             },
2516             { Id => 'test_msg_002',
2517             MessageBody => 'test message body 2',
2518             DelaySeconds => 60,
2519             MessageAttribute => [
2520             { Name => 'test_attribute_name_1',
2521             Value =>
2522             { StringValue => 'test_attribute_value_1', DataType => 'String' }
2523             }
2524             ]
2525             }
2526             ]
2527             };
2528              
2529             =head1 CAVEATS
2530              
2531             =over 5
2532              
2533             =item If you are calling an API that does not expect parameters (or all of
2534             them are optional and you do not pass a parameter) the default is to
2535             pass an empty hash..
2536              
2537             $cwe->ListRules();
2538              
2539             would be equivalent to...
2540              
2541             $cwe->ListRules({});
2542              
2543             I<CAUTION! This may not be what the API expects! Always consult
2544             the AWS API for the service you are are calling.>
2545              
2546             =back
2547              
2548             =head1 GORY DETAILS
2549              
2550             If you using the Botocore APIs you can probably ignore this section.
2551              
2552             =head2 X-Amz-Target
2553              
2554             Most of the newer AWS APIs are invoked as HTTP POST operations and
2555             accept a header C<X-Amz-Target> in lieu of the CGI parameter C<Action>
2556             to specify the specific API action. Some APIs also want the version in
2557             the target, some don't. There is sparse documentation about the
2558             nuances of using the REST interface I<directly> to call AWS APIs.
2559              
2560             When invoking an API, the class uses the C<api> value to indicate
2561             that the action should be set in the C<X-Amz-Target> header. We also
2562             check to see if the version needs to be attached to the action value
2563             as required by some APIs.
2564              
2565             if ( $self->get_api ) {
2566             if ( $self->get_version) {
2567             $self->set_target(sprintf('%s_%s.%s', $self->get_api, $self->get_version, $self->get_action));
2568             }
2569             else {
2570             $self->set_target(sprintf('%s.%s', $self->get_api, $self->get_action));
2571             }
2572              
2573             $request->header('X-Amz-Target', $self->get_target());
2574             }
2575              
2576             DynamoDB and KMS seem to be able to use this in lieu of query
2577             variables C<Action> and C<Version>, although again, there seems to be
2578             a lot of inconsistency (and sometimes flexibility) in the APIs.
2579             DynamoDB uses DynamoDB_YYYYMMDD.Action while KMS does not require the
2580             version that way and prefers TrentService.Action (with no version).
2581             There is no explanation in any of the documentations I have been able
2582             to find as to what "TrentService" might actually mean. Again, your
2583             best approach is to read Amazon's documentation and look at their
2584             sample requests for guidance. You can also look to the L<Botocore
2585             project|https://github.com/boto/botocore> for information regarding
2586             the service. Checkout the F<service-2.json> file within the
2587             sub-directory F<botocore/botocore/data/{api-version}/{service-name}>
2588             which contains details for each service.
2589              
2590             In general, the AWS API ecosystem is very organic. Each service seems
2591             to have its own rules and protocol regarding what the content of the
2592             headers should be.
2593              
2594             As noted, this generic API interface tries to make it possible to use
2595             one class C<Amazon::API> as a sort of gateway to the APIs. The most
2596             generic interface is simply sending query variables and not much else
2597             in the header. Services like EC2 conform to that protocol and can be
2598             invoked with relatively little fanfare.
2599              
2600             use Amazon::API;
2601             use Data::Dumper;
2602              
2603             print Dumper(
2604             Amazon::API->new(
2605             service => 'ec2',
2606             version => '2016-11-15'
2607             )->invoke_api('DescribeInstances')
2608             );
2609              
2610             Note that invoking the API in this fashion, C<version> is
2611             required.
2612              
2613             For more hints regarding how to call a particular service, you can use
2614             the AWS CLI with the --debug option. Invoke the service using the CLI
2615             and examine the payloads sent by the botocore library.
2616              
2617             =head2 Rolling a New API
2618              
2619             The L<Amazon::API> class will stub out methods for the API if you pass
2620             an array of API method names. The stub is equivalent to:
2621              
2622             sub some_api {
2623             my $self = shift;
2624              
2625             $self->invoke_api('SomeApi', @_);
2626             }
2627              
2628             Some will also be happy to know that the class will create an
2629             equivalent I<CamelCase> version of the method.
2630              
2631             As an example, here is a possible implementation of
2632             C<Amazon::CloudWatchEvents> that implements one of the API calls.
2633              
2634             package Amazon::CloudWatchEvents;
2635              
2636             use parent qw/Amazon::API/;
2637              
2638             sub new {
2639             my ($class, $options) = @_;
2640              
2641             my $self = $class->SUPER::new(
2642             { %{$options},
2643             api => 'AWSEvents',
2644             service => 'events',
2645             api_methods => [qw( ListRules )],
2646             }
2647             );
2648              
2649             return $self;
2650             }
2651              
2652             Then...
2653              
2654             use Data::Dumper;
2655              
2656             print Dumper(Amazon::CloudWatchEvents->new->ListRules({}));
2657              
2658             Of course, creating a class for the service is optional. It may be
2659             desirable however to create higher level and more convenient methods
2660             that aid the developer in utilizing a particular API.
2661              
2662             =head2 Overriding Methods
2663              
2664             Because the class does some symbol table munging, you cannot easily
2665             override the methods in the usual way.
2666              
2667             sub ListRules {
2668             my $self = shift;
2669             ...
2670             $self->SUPER::ListRules(@_)
2671             }
2672              
2673             Instead, you should re-implement the method as implemented by this
2674             class.
2675              
2676             sub ListRules {
2677             my $self = shift;
2678             ...
2679             $self->invoke_api('ListRules', @_);
2680             }
2681              
2682             =head2 Content-Type
2683              
2684             Yet another piece of evidence that suggests the I<organic> nature of
2685             the Amazon API ecosystem is their use of different C<Content-Type>
2686             headers. Some of the variations include:
2687              
2688             application/json
2689             application/x-amz-json-1.0
2690             application/x-amz-json-1.1
2691             application/x-www-form-urlencoded
2692              
2693             Accordingly, the C<invoke_api()> method can be passed the
2694             C<Content-Type> or will try to make its I<best guess> based on the
2695             service protocol. It guesses using the following decision tree:
2696              
2697             You can also set the default content type used for the calling service
2698             by passing the C<content_type> option to the constructor.
2699              
2700             $class->SUPER::new(
2701             content_type => 'application/x-amz-json-1.1',
2702             api => 'AWSEvents',
2703             service => 'events'
2704             );
2705              
2706             =head2 ADDITIONAL HINTS
2707              
2708             =over 5
2709              
2710             =item * Bad Request
2711              
2712             If you send the wrong headers or payload you're liable to get a 400
2713             Bad Request. You may also get other errors that can be misleading when
2714             you send incorrect parameters. When in doubt compare your requests to
2715             requests from the AWS CLI using the C<--debug> option.
2716              
2717             =over 10
2718              
2719             =item 1. Set the C<debug> option to true to see the request object and
2720             the response object from C<Amazon::API>.
2721              
2722             =item 2. Excecute the AWS CLI with the --debug option and compare the
2723             request and response with that of your calls.
2724              
2725             =back
2726              
2727             =item * Payloads
2728              
2729             Pay attention to the payloads that are required by each service. B<Do
2730             not> assume that sending nothing when you have no parameters to pass
2731             is correct. For example, the C<ListSecrets> API of SecretsManager
2732             requires at least an empty JSON object.
2733              
2734             $api->invoke_api('ListSecrets', {});
2735              
2736             Failure to send at least an empty JSON object will result in a 400
2737             response.
2738              
2739             =back
2740              
2741             =head1 VERSION
2742              
2743             This documentation refers to version 2.0.8 of C<Amazon::API>.
2744              
2745             =head1 DIAGNOSTICS
2746              
2747             To enable diagnostic output set C<debug> to a true value when calling
2748             the constructor. You can also set the C<DEBUG> environment variable to a
2749             true value to enable diagnostics.
2750              
2751             =head2 Logging
2752              
2753             By default L<Amazon::API> uses L<Log::Log4perl>'s Stealth loggers to
2754             log at the DEBUG and TRACE levels. Setting the environment variable
2755             DEBUG to some value or passing a true value for C<debug> in the
2756             constructor will trigger verbose logging.
2757              
2758             If you pass a logger to the constructor, C<Amazon::API> will attempt
2759             to use that if it has the appropriate logging level methods (error,
2760             warn, info, debug, trace). If L<Log::Log4perl> is unavailable and you
2761             do not pass a logger, logging is essentially disabled at any level.
2762              
2763             If, for some reason you set the enviroment variable DEBUG to a true
2764             value but do not want C<Amazon::API> to log messages you can turn off
2765             logging as shown below:
2766              
2767             my $ec2 = Amazon::API::EC2->new();
2768              
2769             $ec2->set_log_level('fatal');
2770              
2771             =head1 DEPENDENCIES
2772              
2773             =over 5
2774              
2775             =item * L<Amazon::Signature4>
2776              
2777             =item * L<Amazon::Credentials>
2778              
2779             =item * L<Class::Accessor::Fast>
2780              
2781             =item * L<Date::Format>
2782              
2783             =item * L<HTTP::Request>
2784              
2785             =item * L<JSON>
2786              
2787             =item * L<LWP::UserAgent>
2788              
2789             =item * L<List::Util>
2790              
2791             =item * L<ReadonlyX>
2792              
2793             =item * L<Scalar::Util>
2794              
2795             =item * L<Time::Local>
2796              
2797             =item * L<XML::Simple>
2798              
2799             =back
2800              
2801             ...and possibly others.
2802              
2803             =head1 INCOMPATIBILITIES
2804              
2805             =head1 BUGS AND LIMITATIONS
2806              
2807             This module has not been tested on Windows OS. Please report any
2808             issues found by opening an issue here:
2809              
2810             L<https://github.com/rlauer6/perl-Amazon-API/issues>
2811              
2812             =head1 FAQs
2813              
2814             =head2 Why should I use this module instead of PAWS?
2815              
2816             Maybe you shouldn't. PAWS is a community supported project and
2817             may be a better choice for most people. The programmers who created
2818             PAWS are luminaries in the pantheon of Perl programming (alliteration
2819             intended). If you want to use something a little lighter in the
2820             dependency department however, and perhaps only need to invoke a
2821             single service, L<Amazon:API> may be the right choice.
2822              
2823             =head2 Does it perform better than PAWS?
2824              
2825             Probably. But individual API calls to Amazon services have their own
2826             performance characteristics and idiosyncracies. The overhead
2827             introduced by this module and PAWS may be insignificant compared to
2828             the API performance itself. YMMV.
2829              
2830             =head2 Does this work for all APIs?
2831              
2832             I don't know. Probably not. Would love to hear your
2833             feedback. L<Amazon::API> has been developed based on my needs.
2834             Although I have tested it on many APIs, there may still be some cases
2835             that are not handled properly and I am still deciphering the nuances
2836             of flattening, boxing and serializing objects to send to Amazon APIs. The newer versions of this module using Botocore metadata have become increasingly reliable.
2837              
2838             Amazon APIs are not created equal, homogenous or invoked in the the
2839             same manner. Some accept parameters as a query strings, some
2840             parameters are embedded in the URI, some are sent as JSON payloads and
2841             others as XML. Content types for payloads are all over the map.
2842             Likewise with return values.
2843              
2844             Luckily, the Botocore metadata describes the protocols, parameters and
2845             return values for all APIs. The Botocore metadata is quite amazing
2846             actually. It is used to provide information to the Botocore library
2847             for calling any of the AWS services and even for creating
2848             documentation!
2849              
2850             L<Amazon::API> can use that information for creating the Perl classes
2851             that invoke each API but may not interpret the metadata correctly in
2852             all circumstances. Bugs almost certainly exist. :-( Did I mention help
2853             is welcome?
2854              
2855             If you want to use this to invoke S3 APIs, don't. I
2856             haven't tried it and I'm pretty sure it would not work anyway. There are
2857             modules designed specifically for S3; L<Amazon::S3>,
2858             L<Net::Amazon::S3>. Use them.
2859              
2860             =head2 Do I have to create the shape classes when I generate stubs for
2861             a service?
2862              
2863             Possibly. If you create stubs manually, then you do not need the shape
2864             classes. If you use the scripts provide to create the API stubs using
2865             Botocore metadata, then yes, you must create the shapes so that the
2866             Botocore API methods know how to serialize requests. Note that you can
2867             create the shape stubs using the Botocore metadata while not creating
2868             the API services. You might want to do that if you want a
2869             lean stub but want the benefits of using the shape stubs for
2870             serialization of the parameters.
2871              
2872             If you produce your stubs manually and do not create the shape stubs,
2873             then you must pass parameters to your API methods that are ready to be
2874             serialized by L<Amazon::API>. Creating data structures that will be
2875             serialized correctly however is done for you if you use the shape
2876             classes. For example, to create an SQS queue using the shape stubs,
2877             you can call the C<CreateQueue> API method as describe in the Botocore
2878             documentation.
2879              
2880             $sqs->CreateQueue(
2881             { QueueName => $queue_name,
2882             Tag => [ { Name => 'my-new-queue' }, { Env => 'dev' } ],
2883             Attribute => [ { VisibilityTimeout => 40 }, { DelaySeconds => 60 } ]
2884             });
2885              
2886             If you do not use the shape classes, then you must pass the arguments
2887             in the form that will eventually be serialized in the correct manner
2888             as a query string.
2889              
2890             $sqs->CreateQueue([
2891             'QueueName=foo',
2892             'Attributes.1.Value=100',
2893             'Attributes.1.Name=VisibilityTimeout',
2894             'Tag.1.Key=Name',
2895             'Tag.1.Value=foo',
2896             'Tag.2.Key=Env',
2897             'Tag.2.Value=dev'
2898             ]);
2899              
2900             =head2 This code is wonky. Why?
2901              
2902             This code has evolved over the years from being I<ONLY> a way to make
2903             RESTful calls to Amazon APIs to incorporating the use of the Botocore
2904             metadata. It I<was> one person's effort to create a somewhat
2905             lightweight interface to a few AWS APIs. As my own needs have changed
2906             and my knowledge of AWS services has increased it became obvious that
2907             using the Botocore metadata would yield far superior results. Still,
2908             I'm grokking the Botocore metadata intuitively without any help. My
2909             interpretations may be off. Help? Pull requests welcomed.
2910              
2911             =head2 How do I pass AWS credentials to the API?
2912              
2913             There is a bit of magic here as L<Amazon::API> will use
2914             L<Amazon::Credentials> transparently if you do not explicitly pass the
2915             credentials object. I've taken great pains to try to make the
2916             aforementioned module somewhat useful and I<secure>.
2917              
2918             See L<Amazon::Credentials>.
2919              
2920             =head2 Can I use more than one set of credentials to invoke different APIs?
2921              
2922             Yes. See L<Amazon::Credentials>.
2923              
2924             =head2 OMG! You update this too frequently!
2925              
2926             Yeah, there are a lot of bugs. Should I stop fixing them?
2927              
2928             =head2 Why are you using XML::Simple when it clearly says "DO NOT"?
2929              
2930             It's simple. And it seems easier to build than other modules that
2931             almost do the same thing.
2932              
2933             =head2 I tried to use this with XYZ service and it barfed. What do I do?
2934              
2935             That's not too surprising. There are several reasons why your call
2936             might not have worked.
2937              
2938             Did you enable debugging? Tracing?
2939              
2940             =over 5
2941              
2942             =item * You passed bad data
2943              
2944             Take a look at the data you passed, how was it serialized and
2945             ultimately passed to the API?
2946              
2947             =item * You didn't read the docs and passed bad data
2948              
2949             amazon-api -s sqs CreateQueue
2950              
2951             =item * The serialization of Amazon::API::Botocore::Shape is busted
2952              
2953             I have not tested every class generated for every API. You may find
2954             that some API methods return C<Bad Request> or do not serialize the
2955             results in the manner expected. Requests are serialized based on the
2956             metadata found in the Botocore project. There lie the clues for each
2957             API (protocol, end points, etc) and the models (shapes) for requests
2958             and response elements.
2959              
2960             Some requests require a query string, some an XML or JSON payload. The
2961             Botocore based API classes use the metadata to determine how to send a
2962             request and how to interpret the results. This module uses
2963             L<XML::Simple> or L<JSON> to parse the results. It then uses the
2964             C<Amazon::API::Bottocore::Serializer> to turn the parsed results into
2965             a Perl object that respresents the response shape. It's likely that
2966             this module has bugs and the shapes returned might not exactly match the
2967             return response from the C<aws> command line interface version.
2968              
2969             What should be returned by an API request method is documented in
2970             these modules and Amazon's CLI.
2971              
2972             perldoc Amazon::API::Botocore::Shape::EC2:DescribeInstancesRequest
2973              
2974             If you find this project's serializer deficient, please log an issue
2975             and I will attempt to address it.
2976              
2977             =back
2978              
2979             =head1 LICENSE AND COPYRIGHT
2980              
2981             This module is free software. It may be used, redistributed and/or
2982             modified under the same terms as Perl itself.
2983              
2984             =head1 SEE OTHER
2985              
2986             L<Amazon::Credentials>, L<Amazon::API::Error>, L<AWS::Signature4>, L<Amazon::API::Botocore>
2987              
2988             =head1 AUTHOR
2989              
2990             Rob Lauer - <rlauer6@comcast.net>
2991              
2992             =cut