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.
HEAD
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.