File Coverage

blib/lib/Mojo/UserAgent/Role/Cache.pm
Criterion Covered Total %
statement 22 66 33.3
branch 1 18 5.5
condition 2 4 50.0
subroutine 7 16 43.7
pod 1 1 100.0
total 33 105 31.4


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