File Coverage

blib/lib/Mojo/UserAgent/Role/Cache.pm
Criterion Covered Total %
statement 15 59 25.4
branch 1 18 5.5
condition 1 2 50.0
subroutine 5 14 35.7
pod 1 1 100.0
total 23 94 24.4


line stmt bran cond sub pod time code
1             package Mojo::UserAgent::Role::Cache;
2 2     2   1340 use Mojo::Base -role;
  2         4  
  2         18  
3              
4 2     2   1804 use Mojo::UserAgent::Role::Cache::Driver::File;
  2         6  
  2         17  
5 2     2   110 use Mojo::Util 'term_escape';
  2         5  
  2         113  
6              
7 2   50 2   14 use constant DEBUG => $ENV{MOJO_CLIENT_DEBUG} || 0;
  2         5  
  2         3122  
8              
9             our $VERSION = '0.02';
10              
11             my $DEFAULT_STRATEGY = 'playback_or_record';
12              
13             has cache_driver => sub { shift->cache_driver_singleton };
14              
15             has cache_key => sub {
16             return sub {
17             my $req = shift->req;
18             my $url = $req->url;
19             my @keys = (lc $req->method);
20              
21             push @keys, $url->host // '';
22             push @keys, $url->path_query;
23             push @keys, Mojo::Util::md5_sum($req->body) if length $req->body // '';
24              
25             return \@keys;
26             };
27             };
28              
29             has cache_strategy => sub {
30             my $strategy = $ENV{MOJO_USERAGENT_CACHE_STRATEGY} || $DEFAULT_STRATEGY;
31             my @strategies = map { split /=/, $_, 2 } split '&', $strategy;
32             my %strategies = @strategies == 1 ? () : @strategies;
33              
34             return !%strategies ? sub {$strategy} : sub {
35             my $method = uc shift->req->method;
36             return $strategies{$method} || $strategies{DEFAULT} || $DEFAULT_STRATEGY;
37             };
38             };
39              
40             sub cache_driver_singleton {
41 2     2 1 8 my $class = shift;
42 2         10 state $driver = Mojo::UserAgent::Role::Cache::Driver::File->new;
43 2 50       16 return $driver unless @_;
44 0           $driver = shift;
45 0           return $class;
46             }
47              
48             around start => sub {
49             my ($orig, $self, $tx) = (shift, shift, shift);
50              
51             my $strategy = $self->cache_strategy->($tx);
52             warn qq(-- Cache strategy is "$strategy" (@{[_url($tx)]})\n) if DEBUG and !$self->{cache_passthrough};
53             return $self->$orig($tx, @_) if $strategy eq 'passthrough' or delete $self->{cache_passthrough};
54              
55             my $method = $self->can("_cache_start_$strategy");
56             Carp::confess(qq([Mojo::UserAgent::Role::Cache] Invalid strategy "$strategy".)) unless $method;
57             return $self->$method($tx, @_);
58             };
59              
60 0     0     sub _url { shift->req->url->to_abs }
61              
62             sub _cache_get_tx {
63 0     0     my ($self, $tx_input) = @_;
64 0           my $key = $self->cache_key->($tx_input);
65              
66 0           my $buffer = $self->cache_driver->get($key);
67 0 0         return undef unless defined $buffer;
68              
69 0           $tx_input->res->parse($buffer);
70 0           return $tx_input;
71             }
72              
73             sub _cache_set_tx {
74 0     0     my ($self, $tx_input, $tx_output) = @_;
75 0           $self->cache_driver->set($self->cache_key->($tx_input), $tx_output->res->to_string);
76 0           return $self;
77             }
78              
79             sub _cache_start_playback {
80 0     0     my ($self, $tx_input, $cb) = @_;
81 0           my $tx_output = $self->_cache_get_tx($tx_input);
82 0 0         my $status = $tx_output ? '<<<' : '!!!';
83              
84             # Not in cache
85 0 0         unless ($tx_output) {
86 0           $tx_output = $tx_input;
87 0           $tx_output->res->error({message => 'Not in cache.'});
88             }
89              
90 0           warn term_escape "-- Client >>> Cache (@{[_url($tx_input)]})\n@{[$tx_input->req->to_string]}\n" if DEBUG;
91 0           warn term_escape "-- Client $status Cache (@{[_url($tx_input)]})\n@{[$tx_output->res->to_string]}\n" if DEBUG;
92              
93             # Blocking
94 0 0         return $tx_output unless $cb;
95              
96             # Non-blocking
97 0     0     Mojo::IOLoop->next_tick(sub { $self->$cb($tx_input) });
  0            
98 0           return $self;
99             }
100              
101             sub _cache_start_playback_or_record {
102 0     0     my ($self, $tx_input, $cb) = @_;
103 0           my $tx_output = $self->_cache_get_tx($tx_input);
104              
105             # Not cached
106 0 0         unless ($tx_output) {
107 0           warn term_escape "-- Client !!! Cache (@{[_url($tx_input)]}) - Start recording...\n" if DEBUG;
108 0 0         return $self->_cache_start_record($tx_input, $cb ? ($cb) : ());
109             }
110              
111 0           warn term_escape "-- Client >>> Cache (@{[_url($tx_input)]})\n@{[$tx_input->req->to_string]}\n" if DEBUG;
112 0           warn term_escape "-- Client <<< Cache (@{[_url($tx_input)]})\n@{[$tx_output->res->to_string]}\n" if DEBUG;
113              
114             # Blocking
115 0 0         return $tx_output unless $cb;
116              
117             # Non-blocking
118 0     0     Mojo::IOLoop->next_tick(sub { $self->$cb($tx_output) });
  0            
119 0           return $self;
120             }
121              
122             sub _cache_start_record {
123 0     0     my ($self, $tx_input, $cb) = @_;
124              
125             # Make sure we perform the actual request when calling start();
126 0           $self->{cache_passthrough} = 1;
127              
128             # Blocking
129 0 0         unless ($cb) {
130 0           my $tx_output = $self->start($tx_input);
131 0           $self->_cache_set_tx($tx_input, $tx_output);
132 0           return $tx_output;
133             }
134              
135             # Non-blocking
136 0     0     $self->start($tx_input, sub { $_[0]->_cache_set_tx($tx_input, $_[1])->$cb($_[1]) });
  0            
137 0           return $self;
138             }
139              
140             1;
141              
142             =encoding utf8
143              
144             =head1 NAME
145              
146             Mojo::UserAgent::Role::Cache - Role for Mojo::UserAgent that provides caching
147              
148             =head1 SYNOPSIS
149              
150             =head2 General
151              
152             # Apply the role
153             my $ua_class_with_cache = Mojo::UserAgent->with_roles('+Cache');
154             my $ua = $ua_class_with_cache->new;
155              
156             # Change the global cache driver
157             use CHI;
158             $ua_class_with_cache->cache_driver_singleton(CHI->new(driver => "Memory", datastore => {}));
159              
160             # Or change the driver for the instance
161             $ua->cache_driver(CHI->new(driver => "Memory", datastore => {}));
162              
163             # The rest is like a normal Mojo::UserAgent
164             my $tx = $ua->get($url)->error;
165              
166             =head2 Module
167              
168             package MyCoolModule;
169             use Mojo::Base -base;
170              
171             has ua => sub {
172             return $ENV{MOJO_USERAGENT_CACHE_STRATEGY}
173             ? Mojo::UserAgent->with_roles('+Cache') : Mojo::UserAgent->new;
174             };
175              
176             sub get_mojolicious_org {
177             return shift->ua->get("https://mojolicious.org/");
178             }
179              
180             Using the C inside the module is a very
181             effective way to either use the global cache set up by a unit test, or run with
182             the default L without caching.
183              
184             =head2 Test
185              
186             use Mojo::Base -strict;
187             use Mojo::UserAgent::Role::Cache;
188             use MyCoolModule;
189             use Test::More;
190              
191             # Set up the environment and change the global cache_driver before running
192             # the tests
193             $ENV{MOJO_USERAGENT_CACHE_STRATEGY} ||= "playback";
194             Mojo::UserAgent::Role::Cache->cache_driver_singleton->root_dir("/some/path");
195              
196             # Run the tests
197             my $cool = MyCoolModule->new;
198             is $cool->get_mojolicious_org->res->code, 200, "mojolicious.org works";
199              
200             done_testing;
201              
202             =head1 DESCRIPTION
203              
204             L is a role for the full featured non-blocking
205             I/O HTTP and WebSocket user agent L, that provides caching.
206              
207             The L shows how to use this in with tests, but there's nothing wrong
208             with using it for other things as well, where you want caching.
209              
210             By default, this module caches everything without any expiration. This is
211             because L is very basic and
212             actually just meant for unit testing. If you want something more complex, you
213             can use L or another L that implements the logic you want.
214              
215             One exotic hack that is possible, is to make L return the whole
216             L<$tx> object and then implement a wrapper around L that will investigate
217             the transaction and see if it wants to cache the request at all.
218              
219             =head1 WARNING
220              
221             L is still under development, so there will be
222             changes and there is probably bugs that needs fixing. Please report in if you
223             find a bug or find this role interesting.
224              
225             L
226              
227             =head1 ATTRIBUTES
228              
229             =head2 cache_driver
230              
231             $obj = $self->cache_driver;
232             $self = $self->cache_driver(CHI->new);
233              
234             Holds an object that will get/set the HTTP messages. Default is
235             L, but any backend that supports
236             C and C should do. This means that you can use L if you
237             like.
238              
239             =head2 cache_key
240              
241             $code = $self->cache_key;
242             $self = $self->cache_key(sub { my $tx = shift; return $tx->req->url });
243              
244             Holds a code ref that returns an array-ref of the key parts that is passed on
245             to C or C in the L.
246              
247             This works with L as well, since CHI will serialize the key if it is a
248             reference.
249              
250             The default is EXPERIMENTAL, but returns this value for now:
251              
252             [
253             $http_method, # get, post, ...
254             $host, # no port
255             $path_query, # /foo?x=42
256             md5($body), # but not for GET
257             ]
258              
259             =head2 cache_strategy
260              
261             $code = $self->cache_strategy;
262             $self = $self->cache_strategy(sub { my $tx = shift; return "passthrough" });
263              
264             Used to set up a callback to return a cache strategy. Default value is read
265             from the C environment variable or
266             "playback_or_record".
267              
268             The return value from the C<$code> can be one of:
269              
270             =over 2
271              
272             =item * passthrough
273              
274             Will disable any caching.
275              
276             =item * playback
277              
278             Will never send a request to the remote server, but only look for recorded
279             messages.
280              
281             =item * playback_or_record
282              
283             Will return a recorded message if it exists, or fetch one from the remote
284             server and store the response.
285              
286             =item * record
287              
288             Will always fetch a new response from the remote server and store the response.
289              
290             =back
291              
292             =head1 METHODS
293              
294             =head2 cache_driver_singleton
295              
296             $obj = Mojo::UserAgent::Role::Cache->cache_driver_singleton;
297             Mojo::UserAgent::Role::Cache->cache_driver_singleton($obj);
298              
299             Used to retrieve or set the default L. Useful for setting up
300             caching globally in unit tests.
301              
302             =head1 AUTHOR
303              
304             Jan Henning Thorsen
305              
306             =head1 COPYRIGHT AND LICENSE
307              
308             This program is free software, you can redistribute it and/or modify it under
309             the terms of the Artistic License version 2.0.
310              
311             =head1 SEE ALSO
312              
313             L,
314             L and
315             L.
316              
317             =cut