File Coverage

blib/lib/Amazon/API.pm
Criterion Covered Total %
statement 191 596 32.0
branch 31 214 14.4
condition 26 163 15.9
subroutine 45 97 46.3
pod 7 21 33.3
total 300 1091 27.5


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