NAME

UserAgent::Any – Wrapper above any UserAgent library, supporting sync and async calls

SYNOPSIS

my $ua = UserAgent::Any->new(LWP::UserAgent->new(%options));

my $res = $ua->get($url, %params);

DESCRIPTION

UserAgent::Any is to user agents what Log::Any is to loggers: it allows to write libraries making RPC calls without having to rely on one particular user agent implementation.

UserAgent::Any supports both synchronous and asynchronous calls (if supported by the underlying user agent).

The main goal of this library is to be used for Cloud API wrappers so that they can be written without imposing the use of one particular user agent on their users. As such, only a subset of the features usually exposed by full-fledged user agents is available for now in UserAgent::Any. Feel free to ask for or contribute new features if needed.

Supported user agents

LWP::UserAgent

When using an LWP::UserAgent, a UserAgent::Any object only implements the synchronous calls (without the _cb or _p suffixes).

Mojo::UserAgent

When using a Mojo::UserAgent, a UserAgent::Any object implements the asynchronous calls using the global singleton Mojo::IOLoop and the methods with the _p suffix return Mojo::Promise objects.

AnyEvent::UserAgent

When using a AnyEvent::UserAgent, a UserAgent::Any object implements the asynchronous calls using AnyEvent condvar and the methods with the _p suffix return Promise::XS objects (that module needs to be installed).

Note that you probably want to set the event loop used by the promise, which has global effect so is not done by this module. It can be achieved with:

Promise::XS::use_event('AnyEvent');

You can read more about that in "EVENT LOOPS" in Promise::XS.

If you need different promise objects (especially Future), feel free to ask for or contribute new implementations.

HTTP::Promise

When using a HTTP::Promise, a UserAgent::Any object implements the asynchronous calls using Promise::Me which execute the calls in forked processes. Because of that, there are some caveats with the use of this module and its usage is discouraged when another one can work.

UserAgent::Any

As a convenience, you can pass a UserAgent::Any to the constructor of the package and the exact same object will be returned.

Constructor

my $ua = UserAgent::Any->new($underlying_ua);

Builds a new UserAgent::Any object wrapping the given underlying user agent. The wrapped object must be an instance of a supported user agent. Feel free to ask for or contribute new implementations.

Note that UserAgent::Any is a Moo::Role and not a class. As such you can compose it or delegate to it, but you can’t extend it directly.

User agent methods

get

my $res = $ua->get($url, %params);

$ua->get_cb($url, %params)->($cb);

my $promise = $ua->get_p($url, %params);

Note that while the examples above are using %params, the parameters are actually treated as a list as the same key can appear multiple times to send the same header multiple time. But that list must be an even-sized list of alternating key-value pairs.

post

my $res = $ua->post($url, %params, $content);

$ua->post_cb($url, %params, $content)->($cb);

my $promise = $ua->post_p($url, %params, $content);

This is similar to the get method except that the call uses the POST HTTP verb. in addition to the $url and %params (which is still actually a @params), this method can take an optional $content scalar that will be sent as the body of the request.

delete

my $res = $ua->delete($url, %params);

$ua->delete_cb($url, %params)->($cb);

my $promise = $ua->delete_p($url, %params);

Same as the get method, but uses the DELETE HTTP verb for the request.

patch

my $res = $ua->patch($url, %params, $content);

$ua->patch_cb($url, %params, $content)->($cb);

my $promise = $ua->patch_p($url, %params, $content);

Same as the post method, but uses the PATCH HTTP verb for the request.

put

my $res = $ua->put($url, %params, $content);

$ua->put_cb($url, %params, $content)->($cb);

my $promise = $ua->put_p($url, %params, $content);

Same as the post method, but uses the PUT HTTP verb for the request.

my $res = $ua->head($url, %params);

$ua->head_cb($url, %params)->($cb);

my $promise = $ua->head_p($url, %params);

Same as the get method, but uses the HEAD HTTP verb for the request. Note that it means that in general the user agent will ignore the content returned by the server (except for the headers), even if some content is returned.

Using UserAgent::Any in client APIs

wrap_method

Calling a class method

wrap_method($name => $delegate, sub ($self, ...) { ... }, sub ($self, $res, ...));

This method (which is the only one that can be exported by this module) is there to help implement API client library using UserAgent::Any and expose methods handling callback and promise without having to implement them all.

The call above will generate in your class a set of method named with $name and the (optional) suffix _cb and _p, that will call the methods named with $delegate and the same suffix on the same object, passing it the result of the first code reference and passing the result of that call to the second code reference.

For example, if you have a class that can handle methods foo, foo_cb, and foo_p with the same semantics as the user agent methods above (this will typically be the methods for UserAgent::Any itself) and you want to expose a method bar that depends on foo you can write:

wrap_method('bar' => 'foo', sub ($self, @args) { make_args_for_foo($@args) });

And this will expose in your package a set of bar, bar_cb, and bar_p methods with the same semantics that will use the provided method reference to build the arguments to foo. For the synchronous case, the method from the example above will be equivalent to:

sub bar ($self, @args) { $self->foo($self, make_args_for_foo(@args))}

You can optionally pass a second callback that will be called with the response from the wrapped method:

wrap_method($name => $delegate, $cb, sub ($self, $res, ...));

The second callback will be called with the current object, the response from the wrapped method and the arguments that were passed to the wrappers (the same that were already passed to the first callback). The wrapped method will be called in list context. If it returns exactly 1 result, then that result is passed as-is to the second callback; if it returns 0 result, then the callback will receive undef; otherwise the callback will receive an array reference with the result of the call.

If you don’t pass a second callback, then the callback, promise or method will return the default result from the invoked method, without any transformation.

Calling a method of a class member

wrap_method($name => \&method, $delegate, $cb1[, $cb2]);

Alternatively to the above, wrap_method can be used to wrap a method of a class member. Instead of calling the method named $delegate in your class, the call above will call the method named $delegate on the reference returned by the call to &method.

Example

Here is a minimal example on how to create a client library for a hypothetical service exposing a create call using the POST method.

Note in particular that, to bring the post method from UserAgent::Any in MyPackage, we are using Moo delegation to the UserAgent::Any package, which is an Moo::Role with the user agent methods.

Another class extending MyPackage would not need this trick and could directly derive from MyPackage without issues.

package MyPackage;

use 5.036;

use Moo;
use UserAgent::Any 'wrap_method';

use namespace::clean;

has ua => (
  is => 'ro',
  handles => 'UserAgent::Any',
  coerce => sub { UserAgent::Any->new($_[0]) },
  required => 1,
);

wrap_method(create_document => 'post', sub ($self, %opts) {
  return ('https://example.com/create/'.$opts{document_id}, $opts{content});
});

Or, if you don’t want to re-expose the UserAgent::Any method in your class directly (possibly because you want to re-use the same name), you can do:

package MyPackage;

use 5.036;

use Moo;
use UserAgent::Any 'wrap_method';

use namespace::clean;

has ua => (
  is => 'ro',
  coerce => sub { UserAgent::Any->new($_[0]) },
  required => 1,
);

wrap_method(create_document => \&ua => 'post', sub ($self, %opts) {
  return ('https://example.com/create/'.$opts{document_id}, $opts{content});
});

BUGS AND LIMITATIONS

  • AnyEvent::UserAgent does not properly support sending a single header multiple times: all the values will be concatenated (separated by , ) and sent as a single header. This is supposed to be equivalent but might give a different behavior from other implementations.

  • The message passing system used by HTTP::Promise (internally based on Promise::Me) appears to be unreliable and a program using it might dead-lock unexpectedly. If you only want to send requests in the background without waiting for their result, then this might not be an issue for you.

AUTHOR

Mathias Kende <mathias@cpan.org>

COPYRIGHT AND LICENSE

Copyright 2024 Mathias Kende

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

SEE ALSO