File Coverage

blib/lib/Mojo/UserAgent/Role/Cache.pm
Criterion Covered Total %
statement 42 60 70.0
branch 8 18 44.4
condition 1 2 50.0
subroutine 10 14 71.4
pod 1 1 100.0
total 62 95 65.2


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