File Coverage

blib/lib/WWW/MetaForge/Cache.pm
Criterion Covered Total %
statement 43 51 84.3
branch 9 14 64.2
condition 3 15 20.0
subroutine 11 12 91.6
pod 4 4 100.0
total 70 96 72.9


line stmt bran cond sub pod time code
1             package WWW::MetaForge::Cache;
2             our $AUTHORITY = 'cpan:GETTY';
3             # ABSTRACT: File-based caching for MetaForge APIs
4             our $VERSION = '0.002';
5              
6 7     7   546502 use Moo;
  7         20678  
  7         56  
7 7     7   11836 use Path::Tiny;
  7         101674  
  7         642  
8 7     7   5764 use JSON::MaybeXS;
  7         21579  
  7         578  
9 7     7   50 use Digest::MD5 qw(md5_hex);
  7         16  
  7         471  
10 7     7   1282 use namespace::clean;
  7         47544  
  7         57  
11              
12              
13             # Default: 0 = never expire (cache forever until manually cleared)
14             our %DEFAULT_TTL = ();
15              
16             has namespace => (
17             is => 'ro',
18             default => 'metaforge',
19             );
20              
21              
22             has cache_dir => (
23             is => 'ro',
24             lazy => 1,
25             builder => '_build_cache_dir',
26             coerce => sub { ref $_[0] ? $_[0] : path($_[0]) },
27             );
28              
29              
30             has ttl => (
31             is => 'ro',
32             default => sub { +{ %DEFAULT_TTL } },
33             );
34              
35              
36             has json => (
37             is => 'ro',
38             lazy => 1,
39             default => sub { JSON::MaybeXS->new(utf8 => 1, canonical => 1) },
40             );
41              
42              
43             sub _build_cache_dir {
44 0     0   0 my ($self) = @_;
45 0         0 my $dir;
46              
47 0 0       0 if ($^O eq 'MSWin32') {
48 0   0     0 $dir = path($ENV{LOCALAPPDATA} // $ENV{TEMP} // 'C:/Temp', $self->namespace);
      0        
49             } else {
50 0   0     0 my $base = $ENV{XDG_CACHE_HOME} // path($ENV{HOME}, '.cache');
51 0         0 $dir = path($base, $self->namespace);
52             }
53              
54 0 0       0 $dir->mkpath unless $dir->is_dir;
55 0         0 return $dir;
56             }
57              
58             sub _cache_key {
59 28     28   262 my ($self, $endpoint, $params) = @_;
60 28   50     598 my $param_str = $self->json->encode($params // {});
61 28         722 return $endpoint . '_' . md5_hex($param_str) . '.json';
62             }
63              
64             sub _cache_file {
65 28     28   54 my ($self, $endpoint, $params) = @_;
66 28         816 return path($self->cache_dir, $self->_cache_key($endpoint, $params));
67             }
68              
69             sub get {
70 16     16 1 4003737 my ($self, $endpoint, $params) = @_;
71              
72 16         50 my $file = $self->_cache_file($endpoint, $params);
73 16 100       874 return undef unless $file->is_file;
74              
75 10         262 my $cached = eval { $self->json->decode($file->slurp_utf8) };
  10         226  
76 10 50 33     2059 return undef unless $cached && ref $cached eq 'HASH';
77              
78             # TTL 0 or undef = never expire
79 10         53 my $ttl = $self->ttl->{$endpoint};
80 10 100       24 if ($ttl) {
81 2   50     31 my $age = time() - ($cached->{timestamp} // 0);
82 2 100       11 return undef if $age > $ttl;
83             }
84              
85 9         53 return $cached->{data};
86             }
87              
88              
89             sub set {
90 12     12 1 262 my ($self, $endpoint, $params, $data) = @_;
91              
92 12         41 my $file = $self->_cache_file($endpoint, $params);
93 12         673 my $cached = {
94             timestamp => time(),
95             endpoint => $endpoint,
96             params => $params,
97             data => $data,
98             };
99              
100 12         320 $file->spew_utf8($self->json->encode($cached));
101 12         15653 return $data;
102             }
103              
104              
105             sub clear {
106 4     4 1 21 my ($self, $endpoint) = @_;
107              
108 4 100       17 if (defined $endpoint) {
109 3         115 for my $file ($self->cache_dir->children(qr/^\Q$endpoint\E_/)) {
110 1         333 $file->remove;
111             }
112             } else {
113 1         38 $_->remove for $self->cache_dir->children(qr/\.json$/);
114             }
115             }
116              
117              
118             sub clear_all {
119 1     1 1 10 my ($self) = @_;
120 1         4 $self->clear();
121             }
122              
123              
124             1;
125              
126             __END__
127              
128             =pod
129              
130             =encoding UTF-8
131              
132             =head1 NAME
133              
134             WWW::MetaForge::Cache - File-based caching for MetaForge APIs
135              
136             =head1 VERSION
137              
138             version 0.002
139              
140             =head1 SYNOPSIS
141              
142             use WWW::MetaForge::Cache;
143              
144             my $cache = WWW::MetaForge::Cache->new;
145              
146             my $data = $cache->get('items', { search => 'Ferro' });
147             $cache->set('items', { search => 'Ferro' }, $response_data);
148             $cache->clear('items');
149              
150             =head1 DESCRIPTION
151              
152             File-based caching for MetaForge API responses. Cache files are stored following
153             XDG Base Directory Specification on Unix (C<~/.cache/metaforge/>) and
154             LOCALAPPDATA on Windows.
155              
156             =head2 namespace
157              
158             Directory name for cache. Defaults to C<metaforge>.
159              
160             =head2 cache_dir
161              
162             L<Path::Tiny> object for cache directory. Auto-detected based on OS.
163             Accepts string (coerced to Path::Tiny).
164              
165             =head2 ttl
166              
167             HashRef of TTL values per endpoint in seconds.
168             Default is empty (cache never expires). Use 0 or undef for infinite TTL.
169              
170             Example with expiration:
171              
172             my $cache = WWW::MetaForge::Cache->new(
173             ttl => { event_timers => 300 } # 5 minutes for events only
174             );
175              
176             =head2 json
177              
178             L<JSON::MaybeXS> instance for serialization.
179              
180             =head2 get
181              
182             my $data = $cache->get($endpoint, \%params);
183              
184             Returns cached data or undef if missing/expired.
185              
186             =head2 set
187              
188             $cache->set($endpoint, \%params, $data);
189              
190             Store data in cache with timestamp.
191              
192             =head2 clear
193              
194             $cache->clear('items'); # Clear specific endpoint
195             $cache->clear; # Clear all
196              
197             Remove cached files.
198              
199             =head2 clear_all
200              
201             Alias for C<< $cache->clear >>.
202              
203             =head1 SUPPORT
204              
205             =head2 Issues
206              
207             Please report bugs and feature requests on GitHub at
208             L<https://github.com/Getty/p5-www-metaforge/issues>.
209              
210             =head2 IRC
211              
212             You can reach Getty on C<irc.perl.org> for questions and support.
213              
214             =head1 CONTRIBUTING
215              
216             Contributions are welcome! Please fork the repository and submit a pull request.
217              
218             =head1 AUTHOR
219              
220             Torsten Raudssus <torsten@raudssus.de>
221              
222             =head1 COPYRIGHT AND LICENSE
223              
224             This software is copyright (c) 2026 by Torsten Raudssus.
225              
226             This is free software; you can redistribute it and/or modify it under
227             the same terms as the Perl 5 programming language system itself.
228              
229             =cut