API-Client
view release on metacpan or search on metacpan
lib/API/Client.pm view on Meta::CPAN
package API::Client;
use 5.014;
use strict;
use warnings;
use registry;
use routines;
use Data::Object::Class;
use Data::Object::ClassHas;
use FlightRecorder;
use Mojo::Transaction;
use Mojo::UserAgent;
use Mojo::URL;
with 'Data::Object::Role::Buildable';
with 'Data::Object::Role::Stashable';
with 'Data::Object::Role::Throwable';
our $VERSION = '0.12'; # VERSION
# ATTRIBUTES
has 'debug' => (
is => 'ro',
isa => 'Bool',
def => 0,
);
has 'fatal' => (
is => 'ro',
isa => 'Bool',
def => 0,
);
has 'logger' => (
is => 'ro',
isa => 'InstanceOf["FlightRecorder"]',
new => 1,
);
fun new_logger($self) {
FlightRecorder->new
}
has 'name' => (
is => 'ro',
isa => 'Str',
new => 1,
);
fun new_name($self) {
"@{[ref($self)]} (@{[$self->version]})"
}
has 'retries' => (
is => 'ro',
isa => 'Int',
def => 0,
);
has 'timeout' => (
is => 'ro',
isa => 'Int',
def => 10,
);
has 'url' => (
is => 'ro',
isa => 'InstanceOf["Mojo::URL"]',
opt => 1,
);
has 'user_agent' => (
is => 'ro',
isa => 'InstanceOf["Mojo::UserAgent"]',
new => 1,
);
fun new_user_agent($self) {
Mojo::UserAgent->new
}
has 'version' => (
lib/API/Client.pm view on Meta::CPAN
method dispatch(Str :$method = 'get', Any %args) {
my $log = $self->logger->info("@{[uc($method)]} @{[$self->url->to_string]}");
my $result = $self->execute(%args, method => $method);
$log->end;
return $result;
}
method fetch(Any %args) {
return $self->dispatch(%args, method => 'get');
}
method patch(Any %args) {
return $self->dispatch(%args, method => 'patch');
}
method update(Any %args) {
return $self->dispatch(%args, method => 'put');
}
method prepare(Object $ua, Object $tx, Any %args) {
$self->set_auth($ua, $tx, %args);
$self->set_headers($ua, $tx, %args);
$self->set_identity($ua, $tx, %args);
return $self;
}
method process(Object $ua, Object $tx, Any %args) {
return $self;
}
method resource(Str @segments) {
my $url;
if (@segments) {
$url = $self->url->clone;
$url->path->merge(
join '/', '', @{$self->url->path->parts}, @segments
);
}
my $object = ref($self)->new(
%{$self->serialize}, ($url ? ('url', $url) : ())
);
return $object;
}
method serialize() {
return {
debug => $self->debug,
fatal => $self->fatal,
name => $self->name,
retries => $self->retries,
timeout => $self->timeout,
url => $self->url->to_string,
};
}
method set_auth($ua, $tx, %args) {
if ($self->can('auth')) {
$tx->req->url->userinfo(join ':', @{$self->auth});
}
return $self;
}
method set_headers($ua, $tx, %args) {
if ($self->can('headers')) {
$tx->req->headers->header(@$_) for @{$self->headers};
} else {
$tx->req->headers->header('Content-Type' => 'application/json');
}
return $self;
}
method set_identity($ua, $tx, %args) {
$tx->req->headers->header('User-Agent' => $self->name);
return $self;
}
method execute(Str :$method = 'get', Str :$path = '', Any %args) {
delete $args{method};
my $ua = $self->user_agent;
my $url = $self->url->clone;
my $query = $args{query} || {};
my $headers = $args{headers} || {};
$url->path(join '/', $url->path, $path) if $path;
$url->query($url->query->merge(%$query)) if keys %$query;
my @args;
# data handlers
for my $type (sort keys %{$ua->transactor->generators}) {
push @args, $type, delete $args{$type} if $args{$type};
}
# handle raw body value
push @args, delete $args{body} if exists $args{body};
# transaction prepare hook
$ua->on(prepare => fun ($ua, $tx) {
$self->prepare($ua, $tx, %args);
});
# client timeouts
$ua->max_redirects(0);
$ua->connect_timeout($self->timeout);
$ua->request_timeout($self->timeout);
# transaction
my ($ok, $tx, $req, $res);
# times to retry failures
my $retries = $self->retries;
# transaction retry loop
for (my $i = 0; $i < ($retries || 1); $i++) {
# execute transaction
$tx = $ua->start($ua->build_tx($method, $url, $headers, @args));
$self->process($ua, $tx, %args);
# transaction objects
$req = $tx->req;
$res = $tx->res;
# determine success/failure
$ok = $res->code ? $res->code !~ /(4|5)\d\d/ : 0;
# log activity
if ($req && $res) {
my $log = $self->logger;
my $msg = join " ", "attempt", ("#".($i+1)), ": $method", $url->to_string;
$log->debug("req: $msg")->data({
request => $req->to_string =~ s/\s*$/\n\n\n/r
});
$log->debug("res: $msg")->data({
response => $res->to_string =~ s/\s*$/\n\n\n/r
});
# output to the console where applicable
$log->info("res: $msg [@{[$res->code]}]");
$log->output if $self->debug;
}
# no retry necessary
last if $ok;
}
# throw exception if fatal is truthy
if ($req && $res && $self->fatal && !$ok) {
my $code = $res->code;
$self->stash(tx => $tx);
$self->throw([$code, uc "${code}_http_response"]);
}
# return transaction
return $tx;
}
1;
=encoding utf8
=head1 NAME
API::Client
=cut
=head1 ABSTRACT
HTTP API Thin-Client Abstraction
=cut
=head1 SYNOPSIS
package main;
use API::Client;
my $client = API::Client->new(url => 'https://httpbin.org');
# $client->resource('post');
# $client->update(json => {...});
=cut
=head1 DESCRIPTION
This package provides an abstraction and method for rapidly developing HTTP API
clients. While this module can be used to interact with APIs directly,
API::Client was designed to be consumed (subclassed) by higher-level
purpose-specific API clients.
=head1 THIN CLIENT
The thin API client library is advantageous as it has complete API coverage and
can easily adapt to changes in the API with minimal effort. As a thin-client
superclass, this module does not map specific HTTP requests to specific
routines, nor does it provide parameter validation, pagination, or other
conventions found in typical API client implementations; Instead, it simply
provides a simple and consistent mechanism for dynamically generating HTTP
requests. Additionally, this module has support for debugging and retrying API
calls as well as throwing exceptions when 4xx and 5xx server response codes are
returned.
=cut
=head1 INTEGRATES
This package integrates behaviors from:
L<Data::Object::Role::Buildable>
L<Data::Object::Role::Stashable>
L<Data::Object::Role::Throwable>
=cut
=head1 LIBRARIES
This package uses type constraints from:
L<Types::Standard>
=cut
=head1 SCENARIOS
This package supports the following scenarios:
=cut
=head2 building
# given: synopsis
my $resource = $client->resource('get');
# GET /get
my $get = $client->resource('get')->dispatch;
# HEAD /head
my $head = $client->resource('head')->dispatch(
method => 'head'
);
# PATCH /patch
my $patch = $client->resource('patch')->dispatch(
method => 'patch'
);
[$get, $head, $patch]
Building up an HTTP request is extremely easy, simply call the L</resource> to
create a new object instance representing the API endpoint you wish to issue a
request against.
=cut
=head2 chaining
lib/API/Client.pm view on Meta::CPAN
useful hooks (i.e. methods, which if present are used to build up the client
object and requests), which are, the C<auth> hook, which should return a
C<Tuple[Str, Str]> which is used to configure the basic auth header, the
C<base> hook which should return a C<Tuple[Str]> which is used to configure the
base URL, and the C<headers> hook, which should return a
C<ArrayRef[Tuple[Str, Str]]> which are used to configure the HTTP request
headers.
=cut
=head2 transacting
# given: synopsis
my $tx1 = $client->resource('patch')->patch(
json => {active => 1}
);
# is equivalent to
my $tx2 = $client->resource('patch')->dispatch(
method => 'patch',
json => {active => 1}
);
[$tx1, $tx2]
An HTTP request is only issued when the L</dispatch> method is called, directly
or indirectly. Those calls return a L<Mojo::Transaction> object which provides
access to the C<request> and C<response> objects.
=cut
=head2 updating
# given: synopsis
my $tx1 = $client->resource('put')->update(
json => {active => 1}
);
# is equivalent to
my $tx2 = $client->resource('put')->dispatch(
method => 'put',
json => {active => 1}
);
[$tx1, $tx2]
This example illustrates how you might update a new API resource.
=cut
=head1 ATTRIBUTES
This package has the following attributes:
=cut
=head2 debug
debug(Bool)
This attribute is read-only, accepts C<(Bool)> values, and is optional.
=cut
=head2 fatal
fatal(Bool)
This attribute is read-only, accepts C<(Bool)> values, and is optional.
=cut
=head2 logger
logger(InstanceOf["FlightRecorder"])
This attribute is read-only, accepts C<(InstanceOf["FlightRecorder"])> values, and is optional.
=cut
=head2 name
name(Str)
This attribute is read-only, accepts C<(Str)> values, and is optional.
=cut
=head2 retries
retries(Int)
This attribute is read-only, accepts C<(Int)> values, and is optional.
=cut
=head2 timeout
timeout(Int)
This attribute is read-only, accepts C<(Int)> values, and is optional.
=cut
=head2 url
url(InstanceOf["Mojo::URL"])
This attribute is read-only, accepts C<(InstanceOf["Mojo::URL"])> values, and is optional.
=cut
=head2 user_agent
user_agent(InstanceOf["Mojo::UserAgent"])
This attribute is read-only, accepts C<(InstanceOf["Mojo::UserAgent"])> values, and is optional.
=cut
( run in 1.615 second using v1.01-cache-2.11-cpan-0bd6704ced7 )