diff options
Diffstat (limited to 'lib/Net/HTTP/API')
-rw-r--r-- | lib/Net/HTTP/API/Error.pm | 43 | ||||
-rw-r--r-- | lib/Net/HTTP/API/Meta/Class.pm | 16 | ||||
-rw-r--r-- | lib/Net/HTTP/API/Meta/Method.pm | 230 | ||||
-rw-r--r-- | lib/Net/HTTP/API/Meta/Method/APIDeclare.pm | 57 | ||||
-rw-r--r-- | lib/Net/HTTP/API/Meta/Method/APIMethod.pm | 115 | ||||
-rw-r--r-- | lib/Net/HTTP/API/Parser.pm | 15 | ||||
-rw-r--r-- | lib/Net/HTTP/API/Parser/JSON.pm | 23 | ||||
-rw-r--r-- | lib/Net/HTTP/API/Parser/XML.pm | 30 | ||||
-rw-r--r-- | lib/Net/HTTP/API/Parser/YAML.pm | 23 | ||||
-rw-r--r-- | lib/Net/HTTP/API/Role/Authentication.pm | 67 | ||||
-rw-r--r-- | lib/Net/HTTP/API/Role/Format.pm | 65 | ||||
-rw-r--r-- | lib/Net/HTTP/API/Role/Request.pm | 79 | ||||
-rw-r--r-- | lib/Net/HTTP/API/Role/Serialization.pm | 104 | ||||
-rw-r--r-- | lib/Net/HTTP/API/Role/UserAgent.pm | 36 |
14 files changed, 903 insertions, 0 deletions
diff --git a/lib/Net/HTTP/API/Error.pm b/lib/Net/HTTP/API/Error.pm new file mode 100644 index 0000000..43157b5 --- /dev/null +++ b/lib/Net/HTTP/API/Error.pm @@ -0,0 +1,43 @@ +package Net::HTTP::API::Error; + +# ABSTRACT: Throw error + +use Moose; +use JSON; +use Moose::Util::TypeConstraints; +use overload '""' => \&error; + +subtype error => as 'Str'; +coerce error => from 'HashRef' => via { JSON::encode_json $_}; + +has http_error => ( + is => 'ro', + isa => 'HTTP::Response', + handles => { http_message => 'message', http_code => 'code' } +); +has reason => ( + is => 'ro', + isa => 'error', + predicate => 'has_reason', + coerce => 1 +); + +sub error { + my $self = shift; + return + ( $self->has_reason && $self->reason ) + || ( $self->http_message . ": " . $self->http_code ) + || 'unknown'; +} + +1; + +=head1 SYNOPSIS + + Net::HTTP::API::Error->new(reason => "'useragent' is required"); + +or + + Net::HTTP::API::Error->new() + +=head1 DESCRIPTION diff --git a/lib/Net/HTTP/API/Meta/Class.pm b/lib/Net/HTTP/API/Meta/Class.pm new file mode 100644 index 0000000..8bc8c7b --- /dev/null +++ b/lib/Net/HTTP/API/Meta/Class.pm @@ -0,0 +1,16 @@ +package Net::HTTP::API::Meta::Class; + +# ABSTRACT: metaclass for all API client + +use Moose::Role; + +with qw/ + Net::HTTP::API::Meta::Method::APIMethod + Net::HTTP::API::Meta::Method::APIDeclare + /; + +1; + +=head1 SYNOPSIS + +=head1 DESCRIPTION diff --git a/lib/Net/HTTP/API/Meta/Method.pm b/lib/Net/HTTP/API/Meta/Method.pm new file mode 100644 index 0000000..adda12c --- /dev/null +++ b/lib/Net/HTTP/API/Meta/Method.pm @@ -0,0 +1,230 @@ +package Net::HTTP::API::Meta::Method; + +# ABSTRACT: create api method + +use Moose; +use Net::HTTP::API::Error; +use Moose::Util::TypeConstraints; + +use MooseX::Types::Moose qw/Str Int ArrayRef/; + +extends 'Moose::Meta::Method'; + +subtype UriPath + => as 'Str' + => where { $_ =~ m!^/! } + => message {"path must start with /"}; + +enum Method => qw(HEAD GET POST PUT DELETE); + +has path => (is => 'ro', isa => 'UriPath', required => 1, coerce => 1); +has method => (is => 'ro', isa => 'Method', required => 1); +has description => (is => 'ro', isa => 'Str', predicate => 'has_description'); +has strict => (is => 'ro', isa => 'Bool', default => 1,); +has authentication => ( + is => 'ro', + isa => 'Bool', + predicate => 'has_authentication', + default => 0 +); +has expected => ( + traits => ['Array'], + is => 'ro', + isa => ArrayRef [Int], + auto_deref => 1, + required => 0, + predicate => 'has_expected', + handles => {find_expected_code => 'grep',}, +); +has params => ( + traits => ['Array'], + is => 'ro', + isa => ArrayRef [Str], + required => 0, + default => sub { [] }, + auto_deref => 1, + handles => {find_request_parameter => 'first',} +); +has params_in_url => ( + traits => ['Array'], + is => 'ro', + isa => ArrayRef [Str], + required => 0, + default => sub { [] }, + auto_deref => 0, + handles => {find_request_url_parameters => 'first'} +); +has required => ( + traits => ['Array'], + is => 'ro', + isa => ArrayRef [Str], + default => sub { [] }, + auto_deref => 1, + required => 0, +); +has documentation => ( + is => 'ro', + isa => 'Str', + lazy => 1, + default => sub { + my $self = shift; + my $doc; + $doc .= "name: " . $self->name . "\n"; + $doc .= "description: " . $self->description . "\n" + if $self->has_description; + $doc .= "method: " . $self->method . "\n"; + $doc .= "path: " . $self->path . "\n"; + $doc .= "arguments: " . join(', ', $self->params) . "\n" + if $self->params; + $doc .= "required: " . join(', ', $self->required) . "\n" + if $self->required; + $doc; + } +); + +before wrap => sub { + my ($class, %args) = @_; + + if (!$args{params} && $args{required}) { + die Net::HTTP::API::Error->new( + reason => "You can't require a param that have not been declared"); + } + + if ( $args{required} ) { + foreach my $required ( @{ $args{required} } ) { + die Net::HTTP::API::Error->new( reason => + "$required is required but is not declared in params" ) + if ( !grep { $_ eq $required } @{ $args{params} }, @{$args{params_in_url}} ); + } + } +}; + +sub wrap { + my ($class, %args) = @_; + + if (!defined $args{body}) { + my $code = sub { + my ($self, %method_args) = @_; + + my $method = $self->meta->find_net_api_method_by_name($args{name}); + + $method->_validate_before_execute(\%method_args); + my $path = $method->_build_path(\%method_args); + my $local_url = $method->_build_uri($self, $path); + + my $result = $self->http_request( + $method->method => $local_url, + $method->params_in_url, \%method_args + ); + + my $code = $result->code; + + if ($method->has_expected + && !$method->find_expected_code(sub {/$code/})) + { + die Net::HTTP::API::Error->new( + reason => "unexpected code", + http_error => $result + ); + } + + my $content = $self->get_content($result);; + + if ($result->is_success) { + if (wantarray) { + return ($content, $result); + } + else { + return $content; + } + } + + die Net::HTTP::API::Error->new( + http_error => $result, + reason => $result->message, + ); + }; + $args{body} = $code; + } + + $class->SUPER::wrap(%args); +} + +sub _validate_before_execute { + my ($self, $args) = @_; + for my $method (qw/_check_params_before_run _check_required_before_run/) { + $self->$method($args); + } +} + +sub _check_params_before_run { + my ($self, $args) = @_; + + return if !$self->strict; + + # check if there is no undeclared param + foreach my $arg (keys %$args) { + if ( !$self->find_request_parameter(sub {/$arg/}) + && !$self->find_request_url_parameters(sub {/$arg/})) + { + die Net::HTTP::API::Error->new( + reason => "'$arg' is not declared as a param"); + } + } +} + +sub _check_required_before_run { + my ($self, $args) = @_; + + # check if all our params declared as required are present + foreach my $required ($self->required) { + if (!grep { $required eq $_ } keys %$args) { + die Net::HTTP::API::Error->new(reason => + "'$required' is declared as required, but is not present"); + } + } +} + +sub _build_path { + my ($self, $args) = @_; + my $path = $self->path; + + my $max_iter = keys %$args; + my $i = 0; + while ($path =~ /(?:\$|:)(\w+)/g) { + my $match = $1; + $i++; + if (my $value = delete $args->{$match}) { + $path =~ s/(?:\$|:)$match/$value/; + } + if ($max_iter > $i) { + $path =~ s/\/(?:(\$|\:).*)?$//; + } + } + $path =~ s/\/(?:(\$|\:).*)?$//; + return $path; +} + +sub _build_uri { + my ($method, $self, $path) = @_; + + my $local_url = $self->api_base_url->clone; + my $path_url_base = $local_url->path; + $path_url_base =~ s/\/$// if $path_url_base =~ m!/$!; + $path_url_base .= $path; + + if ($self->api_format && $self->api_format_mode eq 'append') { + my $format = $self->api_format; + $path_url_base .= "." . $format; + } + + $local_url->path($path_url_base); + return $local_url; +} + +1; + +=head1 SYNOPSIS + +=head1 DESCRIPTION + diff --git a/lib/Net/HTTP/API/Meta/Method/APIDeclare.pm b/lib/Net/HTTP/API/Meta/Method/APIDeclare.pm new file mode 100644 index 0000000..8718eaa --- /dev/null +++ b/lib/Net/HTTP/API/Meta/Method/APIDeclare.pm @@ -0,0 +1,57 @@ +package Net::HTTP::API::Meta::Method::APIDeclare; + +# ABSTRACT: declare API + +use Moose::Role; +use Net::HTTP::API::Error; + +my @accepted_options = qw/ + api_base_url + api_format + api_format_mode + api_username + api_password + authentication + authentication_method + /; + +has api_options => ( + is => 'ro', + traits => ['Hash'], + isa => 'HashRef[Str|CodeRef]', + default => sub { {} }, + lazy => 1, + handles => { + set_api_option => 'set', + get_api_option => 'get', + }, +); + +sub add_net_api_declare { + my ($meta, $name, %options) = @_; + + if ($options{useragent}) { + die Net::HTTP::API::Error->new( + reason => "'useragent' must be a CODE ref") + unless ref $options{useragent} eq 'CODE'; + $meta->set_api_option(useragent => delete $options{useragent}); + } + + # XXX for backward compatibility + for my $attr (qw/base_url format format_mode username password/) { + my $attr_name = "api_" . $attr; + if (exists $options{$attr} && !exists $options{$attr_name}) { + $options{$attr_name} = delete $options{$attr}; + } + } + + for my $attr (@accepted_options) { + $meta->set_api_option($attr => $options{$attr}) if defined $options{$attr}; + } +} + +1; + +=head1 SYNOPSIS + +=head1 DESCRIPTION diff --git a/lib/Net/HTTP/API/Meta/Method/APIMethod.pm b/lib/Net/HTTP/API/Meta/Method/APIMethod.pm new file mode 100644 index 0000000..8303770 --- /dev/null +++ b/lib/Net/HTTP/API/Meta/Method/APIMethod.pm @@ -0,0 +1,115 @@ +package Net::HTTP::API::Meta::Method::APIMethod; + +# ABSTRACT: declare API method + +use Moose::Role; +use Net::HTTP::API::Error; +use Net::HTTP::API::Meta::Method; +use MooseX::Types::Moose qw/Str ArrayRef/; + +has local_net_api_methods => ( + traits => ['Array'], + is => 'rw', + isa => ArrayRef [Str], + required => 1, + default => sub { [] }, + auto_deref => 1, + handles => { + _find_net_api_method_by_name => 'first', + _add_net_api_method => 'push', + get_all_net_api_methods => 'elements', + }, +); + +sub find_net_api_method_by_name { + my ($meta, $name) = @_; + my $method_name = $meta->_find_net_api_method_by_name(sub {/^$name$/}); + return unless $method_name; + my $method = $meta->find_method_by_name($method_name); + if ($method->isa('Class::MOP::Method::Wrapped')) { + return $method->get_original_method; + } + else { + return $method; + } +} + +sub remove_net_api_method { + my ($meta, $name) = @_; + my @methods = grep { !/$name/ } $meta->get_all_net_api_methods; + $meta->local_net_api_methods(\@methods); + $meta->remove_method($name); +} + +before add_net_api_method => sub { + my ($meta, $name) = @_; + if ($meta->_find_net_api_method_by_name(sub {/^$name$/})) { + die Net::HTTP::API::Error->new( + reason => "method '$name' is already declared in " . $meta->name); + } +}; + +sub add_net_api_method { + my ($meta, $name, %options) = @_; + + # XXX accept blessed method ? + + my $code = delete $options{code}; + + $meta->add_method( + $name, + Net::HTTP::API::Meta::Method->wrap( + name => $name, + package_name => $meta->name, + body => $code, + %options + ), + ); + $meta->_add_net_api_method($name); +} + +after add_net_api_method => sub { + my ($meta, $name) = @_; + $meta->add_before_method_modifier( + $name, + sub { + my $self = shift; + die Net::HTTP::API::Error->new( + reason => "'api_base_url' have not been defined") + unless $self->api_base_url; + } + ); +}; + +1; + +=head1 SYNOPSIS + + my $api_client = MyAPI->new; + + my @methods = $api_client->meta->get_all_api_methods(); + + my $method = $api_client->meta->find_net_api_method_by_name('users'); + + $api_client->meta->remove_net_api_method($method); + + $api_client->meta->add_net_api_method('users', sub {...}, + description => 'this method does...',); + +=head1 DESCRIPTION + +=method get_all_net_api_methods + +Return a list of net api methods + +=method find_net_api_method_by_name + +Return a net api method + +=method remove_net_api_method + +Remove a net api method + +=method add_net_api_method + +Add a net api method diff --git a/lib/Net/HTTP/API/Parser.pm b/lib/Net/HTTP/API/Parser.pm new file mode 100644 index 0000000..b77095b --- /dev/null +++ b/lib/Net/HTTP/API/Parser.pm @@ -0,0 +1,15 @@ +package Net::HTTP::API::Parser; + +# ABSTRACT: base class for all Net::HTTP::API::Parser + +use Moose; + +sub encode {die "must be implemented"} +sub decode {die "must be implemented"} + +1; + +=head1 SYNOPSIS + +=head1 DESCRIPTION + diff --git a/lib/Net/HTTP/API/Parser/JSON.pm b/lib/Net/HTTP/API/Parser/JSON.pm new file mode 100644 index 0000000..be8b30e --- /dev/null +++ b/lib/Net/HTTP/API/Parser/JSON.pm @@ -0,0 +1,23 @@ +package Net::HTTP::API::Parser::JSON; + +# ABSTRACT: Parse JSON + +use JSON; +use Moose; +extends 'Net::HTTP::API::Parser'; + +sub encode { + my ($self, $content) = @_; + return JSON::encode_json($content); +} + +sub decode { + my ($self, $content) = @_; + return JSON::decode_json($content); +} + +1; + +=head1 SYNOPSIS + +=head1 DESCRIPTION diff --git a/lib/Net/HTTP/API/Parser/XML.pm b/lib/Net/HTTP/API/Parser/XML.pm new file mode 100644 index 0000000..aaedfcd --- /dev/null +++ b/lib/Net/HTTP/API/Parser/XML.pm @@ -0,0 +1,30 @@ +package Net::HTTP::API::Parser::XML; + +# ABSTRACT: Parse XML result + +use XML::Simple; +use Moose; +extends 'Net::HTTP::API::Parser'; + +has _xml_parser => ( + is => 'rw', + isa => 'XML::Simple', + lazy => 1, + default => sub { XML::SImple->new(ForceArray => 0) } +); + +sub encode { + my ($self, $content) = @_; + return $self->_xml_parser->XMLin($content); +} + +sub decode { + my ($self, $content) = @_; + return $self->_xml_parser->XMLout($content); +} + +1; + +=head1 SYNOPSIS + +=head1 DESCRIPTION diff --git a/lib/Net/HTTP/API/Parser/YAML.pm b/lib/Net/HTTP/API/Parser/YAML.pm new file mode 100644 index 0000000..f18593a --- /dev/null +++ b/lib/Net/HTTP/API/Parser/YAML.pm @@ -0,0 +1,23 @@ +package Net::HTTP::API::Parser::YAML; + +# ABSTRACT: Parse YAML + +use YAML::Syck; +use Moose; +extends 'Net::HTTP::API::Parser'; + +sub encode { + my ($self, $content) = @_; + return Dump($content); +} + +sub decode { + my ($self, $content) = @_; + return Load($content); +} + +1; + +=head1 SYNOPSIS + +=head1 DESCRIPTION diff --git a/lib/Net/HTTP/API/Role/Authentication.pm b/lib/Net/HTTP/API/Role/Authentication.pm new file mode 100644 index 0000000..27728c3 --- /dev/null +++ b/lib/Net/HTTP/API/Role/Authentication.pm @@ -0,0 +1,67 @@ +package Net::HTTP::API::Role::Authentication; + +# ABSTRACT: Add authentication informations to request header + +use Moose::Role; + +has api_username => ( + is => 'rw', + isa => 'Str', + predicate => 'has_api_username', +); + +has api_password => ( + is => 'rw', + isa => 'Str', + predicate => 'has_api_password', +); + +# ugly :( +after BUILDALL => sub { + my $self = shift; + + for (qw/api_username api_password/) { + my $predicate = 'has_' . $_; + my $value = $self->meta->get_api_option($_); + $self->$_($value) if $value && !$self->$predicate; + } + + if ( $self->meta->get_api_option('authentication') + || $self->meta->get_api_option('authentication_method')) + { + my $auth_method = $self->meta->get_api_option('authentication_method'); + if ($auth_method) { + $self->api_useragent->add_handler( + request_prepare => sub { $self->$auth_method(@_) }); + } + else { + if ($self->has_api_username && $self->has_api_password) { + $self->api_useragent->add_handler( + request_prepare => sub { + my $req = shift; + $req->headers->authorization_basic($self->api_username, + $self->api_password); + } + ); + } + } + } +}; + +1; + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +=head2 ATTRIBUTES + +=over 4 + +=item B<api_password> + +=item B<api_username> + +=back +64: hit eof while in pod documentation (no =cut seen) + this can cause trouble with some pod utilities diff --git a/lib/Net/HTTP/API/Role/Format.pm b/lib/Net/HTTP/API/Role/Format.pm new file mode 100644 index 0000000..1d8c10f --- /dev/null +++ b/lib/Net/HTTP/API/Role/Format.pm @@ -0,0 +1,65 @@ +package Net::HTTP::API::Role::Format; + +# ABSTRACT: Set appropriate format to request header + +use Moose::Role; +use Moose::Util::TypeConstraints; + +sub content_type { + { json => {value => 'application/json', module => 'JSON',}, + yaml => {value => 'text/x-yaml', module => 'YAML'}, + xml => {value => 'text/xml', module => 'XML::Simple'}, + }; +} + +subtype Format => as 'Str' => where { + my $format = shift; + grep {/^$format$/} keys %{content_type()}; +}; + +enum 'FormatMode' => qw(content-type append); + +has api_format => ( + is => 'rw', + isa => 'Format', + lazy => 1, + default => sub { + my $self = shift; + $self->meta->get_api_option('api_format'); + } +); + +has api_format_mode => ( + is => 'rw', + isa => 'FormatMode', + lazy => 1, + default => sub { + my $self = shift; + my $mode = $self->meta->get_api_option('api_format_mode') || 'append'; + $mode; + } +); + +1; + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +=head2 METHODS + +=over 4 + +=item B<content_type> + +=back + +=head2 ATTRIBUTES + +=over 4 + +=item B<api_format> + +=item B<api_format_mode> + +=back diff --git a/lib/Net/HTTP/API/Role/Request.pm b/lib/Net/HTTP/API/Role/Request.pm new file mode 100644 index 0000000..c972b77 --- /dev/null +++ b/lib/Net/HTTP/API/Role/Request.pm @@ -0,0 +1,79 @@ +package Net::HTTP::API::Role::Request; + +# ABSTRACT: make HTTP request + +use Moose::Role; +use HTTP::Request; +use Net::HTTP::API::Error; +use MooseX::Types::URI qw(Uri); + +has api_base_url => ( + is => 'rw', + isa => Uri, + coerce => 1, + lazy => 1, + default => sub { + my $self = shift; + my $api_base_url = $self->meta->get_api_option('api_base_url'); + if (!$api_base_url) { + die Net::HTTP::API::Error->new( + reason => "'api_base_url' have not been defined"); + } + $api_base_url; + } +); + +sub http_request { + my ($self, $method, $uri, $params_in_url, $args) = @_; + + my $request; + + if ($method =~ /^(?:GET|DELETE)$/) { + $uri->query_form(%$args); + $request = HTTP::Request->new($method => $uri); + } + elsif ($method =~ /^(?:POST|PUT)$/) { + my $params = {}; + foreach my $key (@$params_in_url) { + $params->{$key} = $args->{$key} if exists $args->{$key}; + } + $uri->query_form(%$params) if $params; + + $request = HTTP::Request->new($method => $uri); + my $content = $self->serialize($args); + $request->content($content); + } + else { + die Net::HTTP::API::Error->new(reason => "$method is not defined"); + } + + $request->header( + 'Content-Type' => $self->content_type->{$self->api_format}->{value}) + if $self->api_format_mode eq 'content-type'; + + # XXX lwp hook! + my $result = $self->api_useragent->request($request); + return $result; +} + +1; + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +=head2 METHODS + +=over 4 + +=item B<http_request> + +=back + +=head2 ATTRIBUTES + +=over 4 + +=item B<api_base_url> + +=back diff --git a/lib/Net/HTTP/API/Role/Serialization.pm b/lib/Net/HTTP/API/Role/Serialization.pm new file mode 100644 index 0000000..37d2603 --- /dev/null +++ b/lib/Net/HTTP/API/Role/Serialization.pm @@ -0,0 +1,104 @@ +package Net::HTTP::API::Role::Serialization; + +# ABSTRACT: do (de)serialization + +use 5.010; + +use Try::Tiny; +use Moose::Role; +use Net::HTTP::API::Error; + +has serializers => ( + traits => ['Hash'], + is => 'rw', + isa => 'HashRef[Net::HTTP::API::Parser]', + default => sub { {} }, + auto_deref => 1, + handles => { + _add_serializer => 'set', + _get_serializer => 'get', + }, +); + +sub get_content { + my ($self, $result) = @_; + + return undef unless $result->content; + + my $content_type = $self->api_format // $result->header('Content-Type'); + $content_type =~ s/(;.+)$//; + + my $content; + if ($result->is_success && $result->code != 204) { + my @deserialize_order = ($content_type, $self->api_format); + $content = $self->deserialize($result->content, \@deserialize_order); + + if (!$content) { + die Net::HTTP::API::Error->new( + reason => "can't deserialize content", + http_error => $result, + ); + } + } + $content; +} + +sub deserialize { + my ($self, $content, $list_of_formats) = @_; + + foreach my $format (@$list_of_formats) { + my $s = $self->_get_serializer($format) + || $self->_load_serializer($format); + next unless $s; + my $result; + try { $result = $s->decode($content) }; + return $result if $result; + } +} + +sub serialize { + my ($self, $content) = @_; + my $s = $self->_get_serializer($self->api_format) + || $self->_load_serializer(); + my $result = try { $s->encode($content) }; + return $result if $result; +} + +sub _load_serializer { + my $self = shift; + my $format = shift || $self->api_format; + my $parser = "Net::HTTP::API::Parser::" . uc($format); + if (Class::MOP::load_class($parser)) { + my $o = $parser->new; + $self->_add_serializer($format => $o); + return $o; + } +} + +1; + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +=head2 ATTRIBUTES + +=over 4 + +=item B<serializers> + +=back + +=head2 METHODS + +=over 4 + +=item B<get_content> + +=item B<serialize> + +=item B<deserialize> + +=back +99: hit eof while in pod documentation (no =cut seen) + this can cause trouble with some pod utilities diff --git a/lib/Net/HTTP/API/Role/UserAgent.pm b/lib/Net/HTTP/API/Role/UserAgent.pm new file mode 100644 index 0000000..84f618d --- /dev/null +++ b/lib/Net/HTTP/API/Role/UserAgent.pm @@ -0,0 +1,36 @@ +package Net::HTTP::API::Role::UserAgent; + +# ABSTRACT: create UserAgent + +use Moose::Role; +use LWP::UserAgent; + +has api_useragent => ( + is => 'rw', + isa => 'LWP::UserAgent', + lazy => 1, + default => sub { + my $self = shift; + my $ua = $self->meta->get_api_option('useragent'); + return $ua->() if $ua; + $ua = LWP::UserAgent->new(); + $ua->agent( + "Net::HTTP::API " . $Net::HTTP::API::VERSION . " (Perl)"); + $ua->env_proxy; + return $ua; + } +); + +1; + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +=head2 ATTRIBUTES + +=over 4 + +=item B<api_useragent> + +=back |