package Amazon::API;

# Generic interface to Amazon APIs

use strict;
use warnings;
use 5.010;

use parent qw( Class::Accessor::Fast Exporter );

use Amazon::API::Error;
use Amazon::API::Signature4;
use Amazon::API::Constants qw{ :all };
use Amazon::Credentials;

use Carp;
use Data::Dumper;
use Date::Format;
use English qw{ -no_match_vars};
use HTTP::Request;
use JSON::PP qw{ encode_json decode_json };
use LWP::UserAgent;
use List::Util qw{any all pairs none};
use ReadonlyX;
use Scalar::Util qw{ reftype };
use Time::Local;
use XML::LibXML::Simple qw{ XMLin };

Readonly my $DEFAULT_REGION => 'us-east-1';
Readonly my $URL_FMT        => '%s://%s.%s.amazonaws.com';

local $Data::Dumper::Pair  = $COLON;
local $Data::Dumper::Useqq = $TRUE;
local $Data::Dumper::Terse = $TRUE;

__PACKAGE__->follow_best_practice;

__PACKAGE__->mk_accessors(
  qw{
    action
    api
    api_methods
    botocore_metadata
    botocore_operations
    content_type
    credentials
    debug
    decode_always
    endpoint_prefix
    error
    http_method
    last_action
    logger
    print_error
    protocol
    raise_error
    region
    request_uri
    response
    service
    service_url_base
    target
    target_prefix
    url
    user_agent
    version
  }
);

our $VERSION = '1.3.1';

our @EXPORT_OK = qw{ param_n create_urlencoded_content };

our @REQUIRED_KEYS = qw{ aws_access_key_id aws_secret_access_key };

########################################################################
sub new {
########################################################################
  my ( $class, @options ) = @_;

  $class = ref $class || $class;

  my %options = ref $options[0] ? %{ $options[0] } : @options;

  my $self = $class->SUPER::new( \%options );

  if ( $self->get_service_url_base ) {
    $self->set_service( $self->get_service_url_base );
  }

  croak 'service is required'
    if !$self->get_service;

  $self->_set_defaults(%options);

  $self->_create_methods;

  $self->_set_default_logger;

  return $self;
} ## end sub new

########################################################################
sub decode_response {
########################################################################
  my ( $self, $response ) = @_;

  $response = $response || $self->get_response;

  if ( !ref $response =~ /HTTP::Response/xms ) {
    croak q{can't decode response - not a response object};
  }

  my $decoded_content;

  my $content      = $response->content;
  my $content_type = $response->content_type;

  if ($content) {

    $decoded_content = eval {
      if ( $content_type =~ /json/xmsi ) {
        decode_json($content);
      }
      elsif ( $content_type =~ /xml/xmsi ) {
        XMLin( $content, ForceArray => ['item'] );
      }
    };

    # disregard content_type (it might be misleading)
    if ( !$decoded_content || $EVAL_ERROR ) {
      $decoded_content = eval { return decode_json($content); };

      if ( !$decoded_content || $EVAL_ERROR ) {
        $decoded_content
          = eval { return XMLin( $content, ForceArray => ['item'] ); };
      }
    } ## end if ( !$decoded_content...)
  } ## end if ($content)

  return $decoded_content || $content;
} ## end sub decode_response

########################################################################
sub init_botocore_request {
########################################################################
  my ( $self, $botocore_def, %parameters ) = @_;

  my $json_version = $self->get_botocore_metadata->{'jsonVersion'} // '1.0';

  my %content_types = (
    'ec2'       => 'application/x-www-form-urlencoded',
    'query'     => 'application/x-www-form-urlencoded',
    'rest-json' => 'application/json',
    'json'      => 'application/x-amz-json-' . $json_version,
  );

  my $protocol = $self->get_botocore_metadata->{'protocol'};

  my $http   = $botocore_def->{'http'};
  my $method = $http->{'method'};

  $self->set_http_method($method);
  $self->set_content_type( $content_types{$protocol} );

  if ( $protocol eq 'rest-json' ) {
    my @args            = $http->{'parsed_request_uri'}->{'parameters'};
    my $request_uri_tpl = $http->{'parsed_request_uri'}->{'request_uri_tpl'};

    foreach my $p (@args) {
      croak 'required parameter ' . $p . ' not found.'
        if !exists $parameters{$p};
    }

    $self->set_request_uri( sprintf $request_uri_tpl, @parameters{@args} );

    delete @parameters{@args};

    DEBUG( sub { return Dumper [ \%parameters, $self->get_request_uri ]; } );
  } ## end if ( $protocol eq 'rest-json')

  return \%parameters;
} ## end sub init_botocore_request

########################################################################
sub invoke_api {
########################################################################
  my ( $self, @args ) = @_;
  my ( $action, $parameters, $content_type, $headers );

  if ( ref $args[0] && reftype( $args[0] ) eq 'HASH' ) {
    ( $action, $parameters, $content_type, $headers )
      = @{ $args[0] }{qw{ action parameters content_type headers}};
  }
  else {
    ( $action, $parameters, $content_type, $headers ) = @args;
  }

  $self->set_action($action);
  $self->set_last_action($action);
  $self->set_error(undef);

  my $content         = $parameters;                # default
  my $decode_response = $self->get_decode_always;

  # Botocore definition?
  my $botocore_def = eval { $self->get_botocore_operations->{$action}; };

  if ($botocore_def) {
    $content = $self->init_botocore_request( $botocore_def, %{$parameters} );
    $content_type = $self->get_content_type;
  }
  elsif ( !$content_type ) {
    $content_type = $self->_set_content_type($parameters);
  }

  DEBUG( sub { Dumper [ $content_type, $parameters, $content ] } );

  if ( $content_type =~ /urlencoded/xms && !$self->get_api ) {
    my $version = $self->get_version;
    $content = create_urlencoded_content( $parameters, $action, $version );
  }
  elsif ( ref $content && reftype($content) eq 'HASH' ) {
    $content = encode_json($parameters);
  }

  DEBUG( sub { Dumper [ $content_type, $content ] } );

  my $rsp = $self->submit(
    content      => $content,
    content_type => $content_type,
    headers      => $headers,
  );

  $self->set_response($rsp);

  if ( !$rsp->is_success ) {

    $self->set_error(
      Amazon::API::Error->new(
        { error        => $rsp->code,
          message_raw  => $rsp->content,
          content_type => join $EMPTY,
          $rsp->content_type,
          api      => ref $self,
          response => $rsp,
        }
      )
    );

    if ( $self->get_print_error ) {
      print {*STDERR} $self->print_error;
    }

    if ( $self->get_raise_error ) {
      die $self->get_error;    ## no critic (ErrorHandling::RequireCarping)
    }
  } ## end if ( !$rsp->is_success)

  # legacy behavior for GET...was decode?
  $decode_response = ( defined $decode_response ) ? $decode_response : 1;

  return $decode_response ? $self->decode_response : $rsp->content;
} ## end sub invoke_api

########################################################################
sub print_error {
########################################################################
  my ($self) = @_;

  my $error = $self->get_error;

  my $err_str = 'API ERROR (' . $self->get_last_action . '): ';

  if ( $error && ref $error =~ /Amazon::API::Error/xms ) {
    my $response = $error->get_response;

    local $Data::Dumper::Terse = 1;
    local $Data::Dumper::Pair  = $COLON;
    local $Data::Dumper::Useqq = 1;

    $err_str .= sprintf q{[%s], %s},
      $error->get_error, Dumper( ref $response ? $response : [$response] );
  } ## end if ( $error && ref $error...)
  else {
    $err_str .= '[' . $error . ']';
  }

  return $err_str;
} ## end sub print_error

########################################################################
sub get_valid_token {
########################################################################
  my ($self) = @_;

  my $credentials = $self->get_credentials;

  if ( $credentials->can('is_token_expired') ) {
    if ( $credentials->is_token_expired ) {
      if ( !$self->can('refresh_token') || !$credentials->refresh_token ) {
        croak 'token expired';
      }
    }
  } ## end if ( $credentials->can...)

  return $credentials->get_token;
} ## end sub get_valid_token

########################################################################
sub submit {
########################################################################
  my ( $self, %options ) = @_;

  DEBUG( sub { return Dumper [ \%options ] } );

  my $method  = $self->get_http_method || 'POST';
  my $headers = $options{'headers'}    || [];

  my $url = $self->get_url;

  my $botocore_protocol
    = eval { $self->get_botocore_metadata->{'protocol'} } // $EMPTY;

  if ( $botocore_protocol eq 'rest-json' ) {
    croak 'no request URI provided for rest-json call'
      if !$self->get_request_uri;

    $url .= $self->get_request_uri;
  } ## end if ( $botocore_protocol...)

  DEBUG( sub { return Dumper [ $method, $url, $headers ] } );

  my $request = HTTP::Request->new( $method, $url, $headers );

  # 1. set the header
  # 2. set the content
  # 3. sign the request
  # 4. send the request & return result

  # see IMPLEMENTATION NOTES for an explanation
  if ( $self->get_api || $self->get_target_prefix ) {
    $request = $self->_set_x_amz_target($request);
  }

  $self->_set_request_content( request => $request, %options );
  my $credentials = $self->get_credentials;

  if ( my $token = $self->get_valid_token ) {
    $request->header( 'X-Amz-Security-Token', $token );
  }

  # sign the request
  Amazon::API::Signature4->new(
    -access_key     => $self->get_credentials->get_aws_access_key_id,
    -secret_key     => $self->get_credentials->get_aws_secret_access_key,
    -security_token => $self->get_credentials->get_token || undef,
    service         => $self->get_service,
    region          => $self->get_region
  )->sign( $request, $self->get_region );

  DEBUG( sub { return Dumper( [$request] ) } );

  # make the request, return response object
  my $ua  = $self->get_user_agent;
  my $rsp = $ua->request($request);

  DEBUG( sub { return Dumper [$rsp] } );

  return $rsp;
} ## end sub submit

# +------------------+
# | EXPORTED METHODS |
# +------------------+

########################################################################
sub param_n {
########################################################################
  my ( $message, $prefix, $idx ) = @_;

  if ( !defined $idx ) {    # first call, check args
    croak 'message argument must be reference'
      if !ref $message;
  }

  my @param_n;

  if ( ref $message ) {
    if ( reftype($message) eq 'HASH' ) {
      foreach my $k ( keys %{$message} ) {
        push @param_n,
          param_n( $message->{$k}, $prefix ? "$prefix.$k" : $k, $idx );
      }
    } ## end if ( reftype($message)...)
    else {
      $idx = 1;
      foreach my $e ( @{$message} ) {
        push @param_n,
          param_n( $e, $prefix ? "$prefix.$idx" : $EMPTY, $idx++ );
      }
    } ## end else [ if ( reftype($message)...)]
  } ## end if ( ref $message )
  else {
    croak 'missing value'
      if !defined $message || $message eq $EMPTY;

    return "$prefix=$message";
  } ## end else [ if ( ref $message ) ]

  return @param_n;
} ## end sub param_n

########################################################################
sub create_urlencoded_content {
########################################################################
  my ( $parameters, $action, $version ) = @_;

  my @args;

  if ( $parameters && !ref $parameters ) {
    @args = map { split /=/xsm } split /&/xsm, $parameters;
  }
  elsif ( $parameters && reftype($parameters) eq 'HASH' ) {
    @args = map { ( $_ => $parameters->{$_} ) } keys %{$parameters};
  }
  elsif ( $parameters && reftype($parameters) eq 'ARRAY' ) {
    if ( any {ref} @{$parameters} ) {
      @args = map { %{$_} } @{$parameters};    # list of hashes
    }
    elsif ( any {/=/xsm} @{$parameters} ) {
      @args = map { split /=/xsm } @{$parameters};    # formatted list
    }
    else {
      @args = @{$parameters};                         # simple list
    }
  } ## end elsif ( $parameters && reftype...)

  my $content;

  if ( $action && !any {/Action/xsm} @args ) {
    push @args, 'Action', $action;
  }

  if ( $version && !any {/Version/xsm} @args ) {
    push @args, 'Version', $version;
  }

  return join $AMPERSAND, map { sprintf '%s=%s', @{$_} } pairs @args;
} ## end sub create_urlencoded_content

########################################################################
sub has_keys {
########################################################################
  my ( $self, %options ) = @_;

  my %creds = keys %options ? %options : @{$self}{@REQUIRED_KEYS};

  return $creds{'aws_secret_access_key'} && $creds{'aws_access_key_id'};
} ## end sub has_keys

# +-----------------+
# | PRIVATE METHODS |
# +-----------------+

# should not be called if we have botocore definition
########################################################################
sub _set_content_type {
########################################################################
  my ( $self, $parameters ) = @_;

  # default content-type
  my $content_type = $self->get_content_type;

  if ( ref $parameters && reftype($parameters) eq 'ARRAY' ) {
    if ( $self->get_http_method ne 'GET' ) {
      $content_type = 'application/x-www-form-urlencoded';
    }
  }
  elsif ( !ref $parameters ) {
    if ( $self->get_http_method ne 'GET' && !$self->get_api ) {
      $content_type = 'application/x-www-form-urlencoded';
    }
  }

  return $content_type;
} ## end sub _set_content_type

########################################################################
sub _create_methods {
########################################################################
  my ($self) = @_;

  my $class = ref $self || $self;

  if ( $self->get_api_methods ) {
    no strict 'refs';          ## no critic (TestingAndDebugging::ProhibitNoStrict)
    no warnings 'redefine';    ## no critic (TestingAndDebugging::ProhibitNoWarnings)

    my $stash = \%{ __PACKAGE__ . $DOUBLE_COLON };

    foreach my $api ( @{ $self->get_api_methods } ) {

      my $method = lcfirst $api;

      $method =~ s/([[:lower:]])([[:upper:]])/$1_$2/xmsg;
      $method = lc $method;

      my $snake_case_method = $class . $DOUBLE_COLON . $method;
      my $camel_case_method = $class . $DOUBLE_COLON . $api;

      # snake case rules the day

      if ( !$stash->{$method} ) {
        *{$snake_case_method} = sub {
          my $self = shift;

          $self->invoke_api( $api, @_ );
        };
      } ## end if ( !$stash->{$method...})

      # ...but some prefer camels
      if ( !$stash->{$api} ) {
        *{$camel_case_method} = sub {
          my $self = shift;

          $self->$method(@_);
        };
      } ## end if ( !$stash->{$api} )
    } ## end foreach my $api ( @{ $self->...})

  } ## end if ( $self->get_api_methods)

  return $self;
} ## end sub _create_methods

########################################################################
sub _set_default_logger {
########################################################################
  my ($self) = @_;

  {
    no strict 'refs';          ## no critic (TestingAndDebugging::ProhibitNoStrict)
    no warnings 'redefine';    ## no critic (TestingAndDebugging::ProhibitNoWarnings)

    *{ __PACKAGE__ . $DOUBLE_COLON . 'DEBUG' } = sub {
      return $TRUE if !$self->get_debug;

      my @message = @_;

      my @tm = localtime time;
      my $err_msg;

      if ( ref $message[0] && reftype( $message[0] ) eq 'CODE' ) {
        $err_msg = $message[0]->();
      }
      else {
        $err_msg = join $EMPTY, @message;
      }

      print {*STDERR} sprintf "%s [%s] %s\n", strftime( '%c', @tm ),
        $PROCESS_ID, $err_msg;

      return $TRUE;
    };
  }

  return $self;
} ## end sub _set_default_logger

########################################################################
sub _set_defaults {
########################################################################
  my ( $self, %options ) = @_;

  $self->set_raise_error( $self->get_raise_error // $TRUE );
  $self->set_print_error( $self->get_print_error // $TRUE );

  if ( !$self->get_content_type ) {
    $self->set_content_type('application/x-amz-json-1.1');
  }

  if ( !$self->get_user_agent ) {
    $self->set_user_agent( LWP::UserAgent->new );
  }

  # legacy behavior is to never decode... :-(
  # setting to undef vs FALSE to indicate unfortunate legacy behavior
  $self->set_decode_always( $self->get_decode_always // $TRUE );

  $self->set_http_method( $self->get_http_method // 'POST' );

  $self->set_protocol( $self->get_protocol() // 'https' );

  # note some APIs are global, hence an API may send '' to indicate global
  if ( !defined $self->get_region ) {
    $self->set_region( $self->get_region
        || $ENV{'AWS_REGION'}
        || $ENV{'AWS_DEFAULT_REGION'}
        || $DEFAULT_REGION );
  } ## end if ( !defined $self->get_region)

  if ( !$self->get_credentials ) {
    if ( $self->has_keys(%options) ) {
      $self->set_credentials(
        Amazon::Credentials->new(
          { aws_secret_access_key => $options{'aws_secret_access_key'},
            aws_access_key_id     => $options{'aws_access_key_id'},
            token                 => $options{'token'},
            region                => $self->get_region,
          }
        )
      );
    } ## end if ( $self->has_keys(%options...))
    else {
      $self->set_credentials(
        Amazon::Credentials->new(
          order  => $options{'order'},
          region => $options{'region'},
        )
      );

      $self->set_region( $self->get_credentials->get_region );
    } ## end else [ if ( $self->has_keys(%options...))]
  } ## end if ( !$self->get_credentials)

  # set URL last since it contains region
  $self->_set_url;

  return $self;
} ## end sub _set_defaults

########################################################################
sub _create_service_url {
########################################################################
  my ($self) = @_;

  my $endpoint = $self->get_endpoint_prefix || $self->get_service;

  return sprintf $URL_FMT, $self->get_protocol, $endpoint, $self->get_region;
} ## end sub _create_service_url

########################################################################
sub _set_url {
########################################################################
  my ($self) = @_;

  my $url = $self->get_url;

  if ( !$url ) {
    $url = $self->_create_service_url;
  }
  else {
    if ( $url !~ /^https?/xmsi ) {
      $url =~ s/^\///xms;    # just remove leading slash...
      $url = $self->get_protocol . '://' . $url;
    }

  } ## end else [ if ( !$url ) ]

  $self->set_url($url);

  return $self;
} ## end sub _set_url

########################################################################
sub _set_x_amz_target {
########################################################################
  my ( $self, $request ) = @_;

  my $target  = $self->get_target_prefix;
  my $version = $self->get_version;
  my $api     = $self->get_api;
  my $action  = $self->get_action;

  if ( !$target ) {
    $target = $version ? $api . $UNDERSCORE . $version : $api;
  }

  $target = $target . $DOT . $action;

  $self->set_target($target);

  $request->header( 'X-Amz-Target', $target );

  return $request;
} ## end sub _set_x_amz_target

########################################################################
sub _set_request_content {
########################################################################
  my ( $self, %args ) = @_;

  my $request      = $args{'request'};
  my $content      = $args{'content'};
  my $content_type = $args{'content_type'} || $self->get_content_type;

  if ( $self->get_http_method ne 'GET' ) {
    $request->content_type( $content_type . '; charset=utf-8' );

    $request->content($content);
  }
  else {
    $request->uri( $request->uri . $QUESTION_MARK . $content );
  }

  return $request;
} ## end sub _set_request_content

1;

__END__

=pod

=head1 NAME

Amazon::API - A generic base class for AWS Services

=head1 SYNOPSIS

 package Amazon::CloudWatchEvents;

 use parent qw{ Amazon::API };

 our @API_METHODS = qw{
  DeleteRule
  DescribeEventBus
  DescribeRule
  DisableRule
  EnableRule
  ListRuleNamesByTarget
  ListRules
  ListTargetsByRule
  PutEvents
  PutPermission
  PutRule
  PutTargets
  RemovePermission
  RemoveTargets
  TestEventPattern
 };

 sub new {
   my $class = shift;

   $class->SUPER::new(
     service       => 'events',
     api           => 'AWSEvents',
     api_methods   => \@API_METHODS,
     decode_always => 1
   );
 }

 1;

Then...

 my $rules = Amazon::CloudWatchEvents->new->ListRules({});

=head1 DESCRIPTION

Generic class for constructing AWS API interfaces. Typically used as
the parent class, but can be used directly.

=over 5

=item * See L</IMPLEMENTATION NOTES> for using C<Amazon::API>
directly to call AWS services.

=item * See L<Amazon::CloudWatchEvents> for an example of how to use
this module as a parent class.

=back

=head1 BACKGROUND AND MOTIVATION

A comprehensive Perl interface to AWS services similar to the I<boto>
library for Python has been a long time in coming. The PAWS project
has been attempting to create an always up-to-date AWS interface with
community support.  Some however may find that project a little heavy
in the dependency department. If you are looking for an extensible
(albeit spartan) method of invoking a subset of services with a lower
dependency count, you might want to consider C<Amazon::API>.

=head1 THE APPROACH

Essentially, most AWS APIs are RESTful services that adhere to a
common protocol, but differences in services make a single solution
difficult. All services more or less adhere to this framework:

=over 5

=item 1. Set HTTP headers (or query string) to indicate the API and
method to be invoked

=item 2. Set credentials in the header

=item 3. Set API specific headers

=item 4. Sign the request and set the signature in the header

=item 5. Optionally send a payload of parameters for the method being invoked

=back

Specific details of the more recent AWS services are well documented,
however early services were usually implemented as simple HTTP
services that accepted a query string. This module attempts to account
for most if not all of those nuances of invoking AWS services and
provide a fairly generic way of invoking these APIs in the most
lightweight way possible.

As a generic, lightweight module, it naturally does not provide
support for individual AWS services. To use this class for invoking
the AWS APIs, you need to be very familiar with the specific API
requirements and responses and be willng to invest time reading the
documentation on Amazon's website.  The payoff is that you can
probably use this class to call I<any> AWS API without installing a large
number of dependencies.

Think of this class as a DIY kit to invoke B<only> the methods you
need for your AWS project. A good example of creating a quick and
dirty interface to CloudWatch Events can be found here:

L<Amazon::CloudWatchEvents>

And invoking some of the APIs is as easy as:

  Amazon::API->new(
    service     => 'sqs',
    http_method => 'GET'
  }
  )->invoke_api('ListQueues');


=head1 ERRORS

When an error is encountered an exception class (C<Amazon::API::Error>)
will be raised if C<raise_error> has been set to a true
value. Additionally, a detailed error message will be displayed if
C<print_error> is set to true.

See L<Amazon::API::Error> for more details.

=head1 METHODS AND SUBROUTINES

=head2 new

 new(options)

All options are described below. C<options> can be a list of
key/values or hash reference.

=over 5

=item action

The API method. Normally, you would not set C<action> when you
construct your object. It is set when you call the C<invoke_api>
method or automatically set when you call one of the API stubs created
for you.

Example: 'PutEvents'

=item api

The name of the AWS service. See L</IMPLEMENTATION NOTES> for a
detailed explanation of when to set this value.

Example: 'AWSEvents'

=item api_methods

A reference to an array of method names for the API.  The new
constructor will create methods for each of the method names listed in
the array.

The methods that are created for you are nothing more than stubs that
call C<invoke_api>. The stub is a convenience for calling the
C<invoke_api> method as shown below.

  
 my $api = Amazon::CloudWatch->new;

 $api->PutEvents($events);

...is equivalent to:

 $api->invoke_api->('PutEvents', $events);

Consult the Amazon API documentation for the service to determine what
parameters each action requires.

=item aws_access_key_id

Your AWS access key. Both the access key and secret access key are
required if either is passed. If no credentials are passed, an attempt
will be made to find credentials using L<Amazon::Credentials>. Note
that you may need to pass C<token> as well if you are using temporary
credentials.

=item aws_secret_access_key

Your AWS secret access key.

=item content_type

Default content for parameters passed to the C<invoke_api()> method.
The default is C<application/x-amz-json-1.1>.

If you are calling an API that does not expect parameters (or all of
them are optional and you do not pass a parameter) the default is to
pass an empty hash.

  $cwe->ListRules();

would be equivalent to...

  $cwe->ListRules({});

I<CAUTION! This may not be what the API expects! Always consult
the AWS API for the service you are are calling.>

=item credentials (optional)

Accessing AWS services requires credentials with sufficient privileges
to make programmatic calls to the APIs that support a service.  This
module supports three ways that you can provide those credentials.

=over 10

=item 1. Pass the credentials directly.

Pass the values for the credentials (C<aws_access_key_id>,
C<aws_secaret_access_key>, C<token>) when you call the C<new> method.
A session token is typically required when you have assumed
a role, you are using the EC2's instance role or a container's role.

=item 2. Pass a class that will provide the credential keys.

Pass a reference to a class that has I<getters> for the credential
keys. The class should supply I<getters> for all three credential keys.

Pass the reference to the class as C<credentials> in the constructor
as shown here:

 my $api = Amazon::API->new(credentials => $credentials_class, ... );

=item 3. Use the default C<Amazon::Credentials> class.

If you do not explicitly pass credentials or do not pass a class that
will supply credentials, the module will use the
C<Amazon::Credentials> class that attempts to find credentials in the
I<environment>, your I<credentials file(s)>, or the I<container or
instance role>.  See L<Amazon::Credentials> for more details.

I<NOTE: The latter method of obtaining credentials is probably the
easiest to use and provides the most succinct and secure way of
obtaining credentials.>

=back

=item debug

Set debug to a true value to enable debug messages. Debug mode will
dump the request and response from all API calls. You can also set the
environment variable DEBUG to enable debugging output.

I<NOTE: By default this value will not be passed to
C<Amazon::Credentials> to prevent accidental output of credentials in
logs. If you want to explicitly pass this value, set the debug option
to 2 or 'insecure'.>

default: false

=item decode_always

Set C<decode_always> to a true value to return Perl objects from API
method calls. The default is to return the raw output from the call.
Typically, API calls will return either XML or JSON encoded objects.
Setting C<decode_always> will attempt to decode the content based on
the returned content type.

default: false

=item error

The most recent result of an API call. C<undef> indicates no error was
encountered the last time C<invoke_api> was called.

=item http_method

Sets the HTTP method used to invoke the API. Consult the AWS
documentation for each service to determine the method utilized. Most
of the more recent services utilize the POST method, however older
services like SQS or S3 utilize GET or a combination of methods
depending on the specific method being invoked.

default: POST

=item last_action

The last method call invoked.

=item print_error

Setting this value to true enables a detailed error message containing
the error code and any messages returned by the API when errors occur.

default: true

=item protocol

One of 'http' or 'https'.  Some Amazon services do not support https
(yet).

default: https

=item raise_error

Setting this value to true will raise an exception when errors
occur. If you set this value to false you can inspect the C<error>
attribute to determine the success or failure of the last method call.

 $api->invoke_api('ListQueues');

 if ( $api->get_error ) {
   ...
 }

default: true

=item region

The AWS region. Pass an empty string if the service is a global
service that does not require or want a region.

default: $ENV{'AWS_REGION'}, $ENV{'AWS_DEFAULT_REGION'}, 'us-east-1'

=item response

The HTTP response from the last API call.

=item service

The AWS service name. Example: C<sqs>. This value is used as a prefix
when constructing the the service URL (if not C<url> attribute is set).

=item service_url_base

Deprecated, use C<service>

=item token

Session token for assumed roles.

=item url

The service url.  Example: https://events.us-east-1.amazonaws.com

Typically this will be constructed for you based on the region and the
service being invoked. However, you may want to set this manually if
for example you are using a service like
<LocalStack|https://localstack.cloud/> that mocks AWS API calls.

 my $api = Amazon::API->new(service => 's3', url => 'http://localhost:4566/');

=item user_agent

Your own user agent object.  Using
C<Furl>, if you have it avaiable may result in faster response.

default: C<LWP::UserAgent>

=item version

Sets the API version.  Some APIs require a version. Consult the
documentation for individual services.

=back

=head2 invoke_api

 invoke_api(action, [parameters], [content-type], [headers]);

or using named parameters...

 invoke_api({ action => args, ... } )

Invokes the API with the provided parameters.

=over 5

=item action

API name.

=item parameters

Parameters to send to the API. C<parameters> can be a scalar, a hash
reference or an array reference. See the discussion below regarding
C<content-type> and how C<invoke_api()> formats parameters before
sending them as a payload to the API.

You can use the C<param_n()> method to format query string arguments
that are required to be in the I<param.n> notation. This is about the
best documentation I have seen for that format. From the AWS
documentation...

=over 10

Some actions take lists of parameters. These lists are specified using
the I<param.n> notation. Values of n are integers starting from 1. For
example, a parameter list with two elements looks like this:

&AttributeName.1=first

&AttributeName.2=second

=back

An example of using this notation is to set queue attributes when
creating an SQS queue.

 my $attributes = { Attributes => [ { Name => 'VisibilityTimeout', Value => '100' } ] };
 my @sqs_attributes= Amazon::API::param_n($attributes);

 eval {
   $sqs->CreateQueue([ 'QueueName=foo', @sqs_attributes ]);
 };

See L</param_n> for more details.

=item content-type

If you pass the C<content-type> parameter, it is assumed that the parameters are
the actual payload to be sent in the request (unless the parameter is a reference).

The C<parameters> will be converted to a JSON string if the
C<parameters> value is a hash reference.  If the C<parameters> value
is an array reference it will be converted to a query string (Name=Value&...).

To pass a query string, you should send an array of key/value
pairs, or an array of scalars of the form C<Name=Value>.

 [ { Action => 'DescribeInstances' } ]
 [ 'Action=DescribeInstances' ]

=item headers

Array reference of key/value pairs representing additional headers to
send with the request.

=back

=head2 decode_response

Attempts to decode the most recent response from an invoked API based
on the I<Content-Type> header returned.  If there is no
I<Content-Type> header, then the method will try to decode it first as
a JSON string and then as an XML string. If both of those fail, the
raw content is returned.

You can enable decoded responses globally by setting the
C<decode_always> attribute when you call the C<new>
constructor. Legacy behavior of this API was to always decode GET
responses. You can explicitly disable this behavior by setting
C<decode_always> to 0.

=head2 param_n

 param_n(parameters)

Format parameters in the "param.n" notation.

C<parameters> should be a hash or array reference.

A good example of a service that uses this notation is the
I<SendMessageBatch> SQS API call.

The sample request can be found here:

L<SendMessageBatch|https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessageBatch.html>


 https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue/
 ?Action=SendMessageBatch
 &SendMessageBatchRequestEntry.1.Id=test_msg_001
 &SendMessageBatchRequestEntry.1.MessageBody=test%20message%20body%201
 &SendMessageBatchRequestEntry.2.Id=test_msg_002
 &SendMessageBatchRequestEntry.2.MessageBody=test%20message%20body%202
 &SendMessageBatchRequestEntry.2.DelaySeconds=60
 &SendMessageBatchRequestEntry.2.MessageAttribute.1.Name=test_attribute_name_1
 &SendMessageBatchRequestEntry.2.MessageAttribute.1.Value.StringValue=test_attribute_value_1
 &SendMessageBatchRequestEntry.2.MessageAttribute.1.Value.DataType=String
 &Expires=2020-05-05T22%3A52%3A43PST
 &Version=2012-11-05
 &AUTHPARAMS

To produce this message you would pass the Perl object below to C<param_n()>:

 my $message = {
   SendMessageBatchRequestEntry => [
     { Id          => 'test_msg_001',
       MessageBody => 'test message body 1'
     },
     { Id               => 'test_msg_002',
       MessageBody      => 'test message body 2',
       DelaySeconds     => 60,
       MessageAttribute => [
         { Name  => 'test_attribute_name_1',
           Value =>
             { StringValue => 'test_attribute_value_1', DataType => 'String' }
         }
       ]
     }
   ]
 };


=head2 submit

 submit(options)

I<This method is used internally by C<invoke_api> and normally should
not be called by your applications.>

C<options> is a reference to a hash of options:

=over 5

=item content

Payload to send.

=item content_type

Content types we have seen used to send values to AWS APIs:

 application/json
 application/x-amz-json-1.0
 application/x-amz-json-1.1
 application/x-www-form-urlencoded

Check the documentation for the individual APIs for the correct
content type.

=item headers

Array reference of key/value pairs that represent additional headers
to send with the request.

=back

=head1 IMPLEMENTATION NOTES

=head2 X-Amz-Target

Most of the newer AWS APIs are invoked as HTTP POST operations and
accept a header C<X-Amz-Target> in lieu of the CGI parameter C<Action>
to specify the specific API action. Some APIs also want the version in
the target, some don't. There is sparse documentation about the
nuances of using the REST interface directly to call AWS APIs.

When invoking an API, the class uses the C<api> value to indicate
that the action should be set in the C<X-Amz-Target> header.  We also
check to see if the version needs to be attached to the action value
as required by some APIs.

  if ( $self->get_api ) {
    if ( $self->get_version) {
      $self->set_target(sprintf('%s_%s.%s', $self->get_api, $self->get_version, $self->get_action));
    }
    else {
      $self->set_target(sprintf('%s.%s', $self->get_api, $self->get_action));
    }

    $request->header('X-Amz-Target', $self->get_target());
  }

DynamoDB and KMS seem to be able to use this in lieu of query
variables C<Action> and C<Version>, although again, there seems to be
a lot of inconsisitency (and sometimes flexibility) in the APIs.
DynamoDB uses DynamoDB_YYYYMMDD.Action while KMS does not require the
version that way and prefers TrentService.Action (with no version).
There is no explanation in any of the documentations I have been able
to find as to what "TrentService" might actually mean.  Again, your
best approach is to read Amazon's documentation and look at their
sample requests for guidance.

In general, the AWS API ecosystem is very organic. Each service seems
to have its own rules and protocol regarding what the content of the
headers should be.

As noted, this generic API interface tries to make it possible to use
one class C<Amazon::API> as a sort of gateway to the APIs. The most
generic interface is simply sending query variables and not much else
in the header.  Services like EC2 conform to that protocol and can be
invoked with relatively little fanfare.

 use Amazon::API;
 use Data::Dumper;

 print Dumper(
   Amazon::API->new(
     service => 'ec2',
     version => '2016-11-15'
   )->invoke_api('DescribeInstances')
 );

Note that invoking the API in this fashion, C<version> is
required.

For more hints regarding how to call a particular service, you can use
the AWS CLI with the --debug option.  Invoke the service using the CLI
and examine the payloads sent by the boto library.

=head2 Rolling a New API

The class will stub out methods for the API if you pass an array of
API method names.  The stub is equivalent to:

 sub some_api {
   my $self = shift;

   $self->invoke_api('SomeApi', @_);
 }

Some will also be happy to know that the class will create an
equivalent I<CamelCase> version of the method.

As an example, here is a possible implementation of
C<Amazon::CloudWatchEvents> that implements one of the API calls.

 package Amazon::CloudWatchEvents;

 use parent qw/Amazon::API/;

 sub new {
   my ($class, $options) = @_;

   my $self = $class->SUPER::new(
     { %{$options},
       api         => 'AWSEvents',
       service     => 'events',
       api_methods => [qw{ ListRules }],
     }
   );

   return $self;
 }

Then...

 use Data::Dumper;

 print Dumper(Amazon::CloudWatchEvents->new->ListRules({}));

Of course, creating a class for the service is optional. It may be
desirable however to create higher level and more convenient methods
that aid the developer in utilizing a particular API.

=head2 Overriding Methods

Because the class does some symbol table munging, you cannot easily
override the methods in the usual way.

 sub ListRules {
   my $self = shift;
   ...
   $self->SUPER::ListRules(@_)
 }

Instead, you should re-implement the method as implemented by this
class.

 sub ListRules {
   my $self = shift;
   ...
   $self->invoke_api('ListRules', @_);
 }

=head2 Content-Type

Yet another piece of evidence that suggests the I<organic> nature of
the Amazon API ecosystem is their use of different C<Content-Type>
headers.  Some of the variations include:

 application/json
 application/x-amz-json-1.0
 application/x-amz-json-1.1
 application/x-www-form-urlencoded

Accordingly, the C<invoke_api()> method can be passed the
C<Content-Type> or will try to make its I<best guess> based on the
input parameters you passed. It guesses using the following decision tree:

=over 5

=item * If the Content-Type parameter is passed as the third argument,
that is used.  Full stop.

=item * If the C<parameters> value to C<invoke_api()> is a reference,
then the Content-Type is either the value of C<get_content_type> or
C<application/x-amzn-json-1.1>.

=item * If the C<parameters> value to C<invoke_api()> is a scalar,
then the Content-Type is C<application/x-www-form-urlencoded>.

=back

You can set the default Content-Type used for the calling service when
a reference is passed to the C<invoke_api()> method by passing the
C<content_type> option to the constructor. The default is
'application/x-amz-json-1.1'.

  $class->SUPER::new(
    content_type => 'application/x-amz-json-1.1',
    api          => 'AWSEvents',
    service      => 'events'
  );

=head2 ADDITIONAL HINTS

=over 5

=item * Bad Request

If you send the wrong headers or payload you're liable to get a 400
Bad Request. You may also get other errors that can be misleading when
you send incorrect parameters. When in doubt compare your requests to
requests from the AWS CLI using the C<--debug> option.

=over 10 

=item 1. Set the C<debug> option to true to see the request object and
the response object from C<Amazon::API>.

=item 2. Excecute the AWS CLI with the --debug option and compare the
request and response with that of your calls.

=back

=item * Payloads

Pay attention to the payloads that are required by each service.  B<Do
not> assume that sending nothing when you have no parameters to pass
is correct. For example, the C<ListSecrets> API of SecretsManager
requires at least an empty JSON object.

 $api->invoke_api('ListSecrets', {});

Failure to send at least an empty JSON object will result in a 400
response. 

=back

=head1 VERSION

This documentation refers to version 1.3.1 of C<Amazon::API>.

=head1 DIAGNOSTICS

To enable diagnostic output set C<debug> to a true value when calling
the constructor. You can also set the C<DEBUG> environment variable to a
true value to enable diagnostics.

=head1 CONFIGURATION AND ENVIRONMENT

=head1 DEPENDENCIES

=over 5

=item * L<Amazon::Signature4>

=item * L<Amazon::Credentials>

=item * L<Class::Accessor::Fast>

=item * L<Date::Format>

=item * L<HTTP::Request>

=item * L<JSON::PP>

=item * L<LWP::UserAgent>

=item * L<List::Util>

=item * L<ReadonlyX>

=item * L<Scalar::Util>

=item * L<Time::Local>

=item * L<XML::LibXML::Simple>

=back

...and possibly others.

=head1 INCOMPATIBILITIES

=head1 BUGS AND LIMITATIONS

This module has not been tested on Windows OS.

=head1 LICENSE AND COPYRIGHT

This module is free software. It may be used, redistributed and/or
modified under the same terms as Perl itself.

=head1 SEE OTHER

C<Amazon::Credentials>, C<Amazon::API::Error>, C<AWS::Signature4>

=head1 AUTHOR

Rob Lauer - <rlauer6@comcast.net>

=cut
