| File: | blib/lib/Class/Simple/Cached.pm |
| Coverage: | 95.2% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package Class::Simple::Cached; | |||||
| 2 | ||||||
| 3 | 13 13 13 | 792678 9 120 | use strict; | |||
| 4 | 13 13 13 | 18 7 195 | use warnings; | |||
| 5 | 13 13 13 | 1225 37607 20 | use autodie qw(:all); | |||
| 6 | ||||||
| 7 | 13 13 13 | 53952 12 99 | use Carp (); | |||
| 8 | 13 13 13 | 2055 20039 150 | use Class::Simple; | |||
| 9 | 13 13 13 | 1314 28877 184 | use Params::Get 0.15; | |||
| 10 | 13 13 13 | 21 9 72 | use Scalar::Util (); | |||
| 11 | 13 13 13 | 2050 172881 26 | use Sub::Protected; | |||
| 12 | ||||||
| 13 | # Stored in the cache to distinguish "the object returned undef" from | |||||
| 14 | # "this key has never been cached". Must never be a legitimate return value. | |||||
| 15 | 13 13 13 | 980 9 304 | use constant UNDEF_SENTINEL => __PACKAGE__ . '>UNDEF<'; | |||
| 16 | ||||||
| 17 | # Evaluated once at compile time so DESTROY never pays a string comparison | |||||
| 18 | # on every object teardown just to decide whether ${^GLOBAL_PHASE} exists. | |||||
| 19 | 13 13 13 | 19 4 4067 | use constant _GLOBAL_PHASE_AVAILABLE => defined($^V) && ($^V ge 'v5.14.0'); | |||
| 20 | ||||||
| 21 - 29 | =head1 NAME Class::Simple::Cached - cache getter results for any get/set object =head1 VERSION Version 0.06 =cut | |||||
| 30 | ||||||
| 31 | our $VERSION = '0.06'; | |||||
| 32 | ||||||
| 33 | =encoding UTF-8 | |||||
| 34 | ||||||
| 35 - 150 | =head1 SYNOPSIS
use CHI;
use Class::Simple::Cached;
# Wrap an existing object with a cache layer
my $cache = CHI->new(driver => 'RawMemory', global => 1);
my $obj = Class::Simple::Cached->new(
cache => $cache,
object => My::Expensive::Object->new(),
);
$obj->name('Alice'); # setter: delegates to wrapped object, updates cache
my $n = $obj->name(); # getter: returns cached 'Alice' without hitting object
# Or use a plain hash ref as the cache backend
my %store;
my $simple = Class::Simple::Cached->new(cache => \%store);
$simple->colour('blue');
print $simple->colour(); # 'blue', served from %store
=head1 DESCRIPTION
A subclass of L<Class::Simple> that transparently caches the return values of
getter calls, so that repeated reads of expensive-to-compute or
expensive-to-transport values hit the cache instead of the wrapped object.
Cache coherency is I<not> automatic. If the wrapped object's state changes
through a path other than the cached wrapper, callers must invalidate the cache
themselves.
=head1 SUBROUTINES/METHODS
=head2 new
Constructs a C<Class::Simple::Cached> instance that wraps C<object> behind
C<cache>.
=head3 ARGUMENTS
=over 4
=item C<cache> (mandatory)
Either a blessed object implementing C<get($key)>, C<set($key, $val, $expires)>,
and C<purge()> (e.g. any L<CHI> driver); or a plain hash reference used as an
in-process store.
=item C<object> (optional)
The object whose methods will be proxied and cached. Defaults to a fresh
C<Class::Simple> instance.
=back
Calling C<< ->new() >> on an already-blessed instance returns a shallow clone
(all stored fields are merged, including the existing cache handle).
=head3 RETURNS
A blessed C<Class::Simple::Cached> instance.
=head3 SIDE EFFECTS
None beyond allocating the new instance.
=head3 EXAMPLE
# CHI-backed cache
use CHI;
my $obj = Class::Simple::Cached->new(
cache => CHI->new(driver => 'RawMemory', global => 1),
object => My::Model->new(),
);
# Hash-ref cache (useful for tests or short-lived objects)
my $obj = Class::Simple::Cached->new(cache => {});
# Clone an existing wrapped object
my $clone = $obj->new();
=head3 API SPECIFICATION
Input:
cache => HashRef | CHICompatibleObject # required
object => Object # optional
Output:
Class::Simple::Cached instance
=head3 MESSAGES
Message Meaning Resolution
--------------------------------------------------- ----------------------------------- ----------------------------------------
"use ->new() not ::new() to instantiate" Called as Class::Simple::Cached::new Use $obj->new() or ClassName->new()
"Usage: $class->new(cache => \$cache)" No arguments supplied Pass at least cache => ...
"Cache must be ref to HASH or object" cache is a plain scalar or wrong ref Use a hashref or CHI-compatible object
"Cache object must implement get, set, purge" Blessed cache lacks required methods Use a fully CHI-compatible object
=head3 PSEUDOCODE
new(class, args):
IF class undefined â carp and return undef
IF class is blessed â merge fields, return shallow clone
IF no args â croak Usage message
PARSE args into hashref via Params::Get
IF params.object absent â params.object = Class::Simple->new()
IF params.cache is a blessed object:
VERIFY it can('get') AND can('set') AND can('purge')
IF not â croak capability message
RETURN bless params, class
IF params.cache is a HASH ref:
RETURN bless params, class
croak "Cache must be ref to HASH or object"
=cut | |||||
| 151 | ||||||
| 152 | sub new | |||||
| 153 | { | |||||
| 154 | 218 | 933195 | my $class = shift; | |||
| 155 | ||||||
| 156 | # Guard: always call as a method, not a bare function | |||||
| 157 | 218 | 205 | if(!defined($class)) { | |||
| 158 | 3 | 8 | Carp::carp(__PACKAGE__, ' use ->new() not ::new() to instantiate'); | |||
| 159 | 3 | 293 | return; | |||
| 160 | } | |||||
| 161 | ||||||
| 162 | # When called on a blessed instance, return a shallow clone | |||||
| 163 | 215 | 209 | if(Scalar::Util::blessed($class)) { | |||
| 164 | 16 | 14 | my $params = Params::Get::get_params(undef, \@_) || {}; | |||
| 165 | 16 16 16 | 135 14 21 | my %merged = (%{$class}, %{$params}); | |||
| 166 | # Always recalculate internal dispatch fields after the merge so that | |||||
| 167 | # caller-supplied _is_hash_cache or _cache_prefix args cannot corrupt | |||||
| 168 | # the cache-type flag or the key-prefix (internal field injection). | |||||
| 169 | 16 | 22 | $merged{'_is_hash_cache'} = ref($merged{'cache'}) eq 'HASH'; | |||
| 170 | 16 | 16 | $merged{'_cache_prefix'} = ref($class) . ':'; | |||
| 171 | ||||||
| 172 | # Validate the (potentially-replaced) cache object using the same rules | |||||
| 173 | # as new() â the clone path must not produce an unusable object. | |||||
| 174 | 16 | 21 | if(!$merged{'_is_hash_cache'}) { | |||
| 175 | 3 | 4 | if(Scalar::Util::blessed($merged{'cache'})) { | |||
| 176 | 1 | 4 | unless($merged{'cache'}->can('get') | |||
| 177 | && $merged{'cache'}->can('set') | |||||
| 178 | && $merged{'cache'}->can('purge')) | |||||
| 179 | { | |||||
| 180 | 1 | 3 | Carp::croak("Cache object must implement 'get', 'set', and 'purge' methods"); | |||
| 181 | } | |||||
| 182 | } else { | |||||
| 183 | 2 | 5 | Carp::croak(ref($class) . ': Cache must be ref to HASH or object'); | |||
| 184 | } | |||||
| 185 | } | |||||
| 186 | ||||||
| 187 | 13 | 24 | return bless \%merged, ref($class); | |||
| 188 | } | |||||
| 189 | ||||||
| 190 | # Require at least one argument so Params::Get's confess is never reached; | |||||
| 191 | # we want croak (Test::Carp-compatible) not confess. | |||||
| 192 | 199 | 142 | if(scalar(@_) == 0) { | |||
| 193 | 5 | 23 | Carp::croak('Usage: ', $class, '->new(cache => $cache)'); | |||
| 194 | } | |||||
| 195 | ||||||
| 196 | 194 | 203 | my $params = Params::Get::get_params('cache', @_) || {}; | |||
| 197 | ||||||
| 198 | # Default the wrapped object to a bare Class::Simple instance | |||||
| 199 | 194 | 1951 | $params->{'object'} ||= Class::Simple->new(); | |||
| 200 | ||||||
| 201 | # Precompute per-instance fields so AUTOLOAD (hot path) avoids ref() and | |||||
| 202 | # string concatenation on every call. | |||||
| 203 | 194 | 4129 | $params->{'_is_hash_cache'} = ref($params->{'cache'}) eq 'HASH'; | |||
| 204 | 194 | 135 | $params->{'_cache_prefix'} = "$class:"; | |||
| 205 | ||||||
| 206 | 194 | 193 | if(Scalar::Util::blessed($params->{'cache'})) { | |||
| 207 | # Verify the cache object speaks the required interface | |||||
| 208 | 36 | 147 | unless($params->{'cache'}->can('get') | |||
| 209 | && $params->{'cache'}->can('set') | |||||
| 210 | && $params->{'cache'}->can('purge')) | |||||
| 211 | { | |||||
| 212 | 14 | 41 | Carp::croak("Cache object must implement 'get', 'set', and 'purge' methods"); | |||
| 213 | } | |||||
| 214 | 22 | 29 | return bless $params, $class; | |||
| 215 | } | |||||
| 216 | ||||||
| 217 | # Transitive reduction: _is_hash_cache already holds ref($cache) eq 'HASH' | |||||
| 218 | # computed two statements earlier â no need to call ref() again. | |||||
| 219 | 158 | 110 | if($params->{'_is_hash_cache'}) { | |||
| 220 | 130 | 117 | return bless $params, $class; | |||
| 221 | } | |||||
| 222 | ||||||
| 223 | 28 | 81 | Carp::croak("$class: Cache must be ref to HASH or object"); | |||
| 224 | } | |||||
| 225 | ||||||
| 226 - 255 | =head2 can
Reports whether this wrapper (or its embedded object) can handle a method.
=head3 ARGUMENTS
=over 4
=item C<$method> â the method name to probe.
=back
=head3 RETURNS
True if the method is known; false otherwise.
=head3 EXAMPLE
$obj->can('name'); # true if the wrapped object has a name() method
=head3 API SPECIFICATION
Input: method_name : Str
Output: Bool
=head3 MESSAGES
None.
=cut | |||||
| 256 | ||||||
| 257 | sub can | |||||
| 258 | { | |||||
| 259 | 47 | 1878 | my ($self, $method) = @_; | |||
| 260 | ||||||
| 261 | # Undefined method name is outside the documented API; return undef cleanly. | |||||
| 262 | 47 | 46 | return unless defined $method; | |||
| 263 | ||||||
| 264 | # When called as a class method there is no wrapped object to probe | |||||
| 265 | 46 | 100 | return $self->SUPER::can($method) unless Scalar::Util::blessed($self); | |||
| 266 | ||||||
| 267 | return ($method eq 'new') | |||||
| 268 | 16 | 57 | || $self->{'object'}->can($method) | |||
| 269 | || $self->SUPER::can($method); | |||||
| 270 | } | |||||
| 271 | ||||||
| 272 - 301 | =head2 isa
Reports whether this wrapper or its embedded object is of a given class.
=head3 ARGUMENTS
=over 4
=item C<$class> â the class name to test.
=back
=head3 RETURNS
True if the wrapper or the wrapped object is-a C<$class>.
=head3 EXAMPLE
$obj->isa('My::Model'); # delegates to the wrapped object
=head3 API SPECIFICATION
Input: class_name : Str
Output: Bool
=head3 MESSAGES
None.
=cut | |||||
| 302 | ||||||
| 303 | sub isa | |||||
| 304 | { | |||||
| 305 | 39 | 2100 | my ($self, $class) = @_; | |||
| 306 | ||||||
| 307 | # Undefined class name is outside the documented API; return undef cleanly. | |||||
| 308 | 39 | 31 | return unless defined $class; | |||
| 309 | ||||||
| 310 | # When called as a class method there is no wrapped object to interrogate | |||||
| 311 | 38 | 52 | return $self->SUPER::isa($class) unless Scalar::Util::blessed($self); | |||
| 312 | ||||||
| 313 | 35 | 88 | return 1 if $class eq ref($self) | |||
| 314 | || $class eq __PACKAGE__ | |||||
| 315 | || $self->SUPER::isa($class); | |||||
| 316 | ||||||
| 317 | 11 | 25 | return $self->{'object'}->isa($class); | |||
| 318 | } | |||||
| 319 | ||||||
| 320 | # DESTROY â purge this instance's entries from the cache on object teardown. | |||||
| 321 | # | |||||
| 322 | # Purpose: Prevent stale entries from leaking into a shared cache after | |||||
| 323 | # the wrapper goes out of scope. | |||||
| 324 | # Entry: Called automatically by Perl's garbage collector. | |||||
| 325 | # Exit Status: None (void). | |||||
| 326 | # Side Effects: Removes all cache keys prefixed with ref($self) for hash caches; | |||||
| 327 | # calls purge() for CHI-style caches. | |||||
| 328 | # See https://github.com/Perl/perl5/issues/14673 for why an | |||||
| 329 | # explicit DESTROY is required even though AUTOLOAD could catch it. | |||||
| 330 | sub DESTROY | |||||
| 331 | { | |||||
| 332 | 168 | 56133 | my $self = shift; | |||
| 333 | ||||||
| 334 | 168 | 156 | my $cache = $self->{'cache'} or return; | |||
| 335 | ||||||
| 336 | 165 | 141 | if($self->{'_is_hash_cache'}) { | |||
| 337 | # Remove only this class's keys to avoid stomping on siblings sharing the hash | |||||
| 338 | 143 | 83 | my $prefix = $self->{'_cache_prefix'}; | |||
| 339 | 143 605 143 | 76 449 166 | delete $cache->{$_} for grep { index($_, $prefix) == 0 } keys %{$cache}; | |||
| 340 | 143 | 287 | return; | |||
| 341 | } | |||||
| 342 | ||||||
| 343 | # Skip purge during global destruction to avoid order-of-destruction crashes | |||||
| 344 | 22 | 27 | return if _GLOBAL_PHASE_AVAILABLE && ${^GLOBAL_PHASE} eq 'DESTRUCT'; | |||
| 345 | ||||||
| 346 | 22 | 26 | $cache->purge(); | |||
| 347 | } | |||||
| 348 | ||||||
| 349 | # _cache_get / _cache_set â unified read/write; hides hash-ref vs. CHI dispatch | |||||
| 350 | # so the rest of the code never needs to branch on ref($cache). | |||||
| 351 | sub _cache_get :Protected | |||||
| 352 | { | |||||
| 353 | my ($self, $key) = @_; | |||||
| 354 | my $cache = $self->{'cache'}; | |||||
| 355 | return $self->{'_is_hash_cache'} ? $cache->{$key} : $cache->get($key); | |||||
| 356 | 13 13 13 | 27 8 113 | } | |||
| 357 | ||||||
| 358 | sub _cache_set :Protected | |||||
| 359 | { | |||||
| 360 | my ($self, $key, $val) = @_; | |||||
| 361 | my $cache = $self->{'cache'}; | |||||
| 362 | if($self->{'_is_hash_cache'}) { | |||||
| 363 | $cache->{$key} = $val; | |||||
| 364 | } else { | |||||
| 365 | $cache->set($key, $val, 'never'); | |||||
| 366 | } | |||||
| 367 | 13 13 13 | 1047 10 89 | } | |||
| 368 | ||||||
| 369 - 459 | =head2 AUTOLOAD (getter/setter proxy)
Intercepts every method call that is not explicitly defined, proxying it to
the wrapped object with a caching layer for zero-argument (getter) calls.
Setter calls (one or more arguments) always pass through to the wrapped object
and update the cache with the new value.
=head3 ARGUMENTS
$method() â getter: returns cached value if present, else calls object
$method($scalar) â scalar setter: stores scalar, updates cache
$method(@list) â array setter: stores list, updates cache
=head3 RETURNS
The value returned by the wrapped object (or the cached copy thereof).
=head3 SIDE EFFECTS
=over 4
=item * Getter: may write to the cache on first call.
=item * Setter: writes to both the wrapped object and the cache.
=item * DESTROY: clears cache entries for this instance (see above).
=back
=head3 EXAMPLE
$obj->colour('red'); # setter â writes 'red' to object and cache
$obj->colour(); # getter â returns 'red' from cache
=head3 API SPECIFICATION
Input (getter): method_name : Str, args : ()
Input (setter): method_name : Str, args : (Scalar | List)
Output: the stored/retrieved value or list
=head3 MESSAGES
Message Meaning Resolution
-------------------------------- ---------------------------------- ----------------------------------------
"$method" (croak) Cached array's first element is Do not store the sentinel string
the UNDEF_SENTINEL string as a real value in the wrapped object
=head3 PSEUDOCODE
AUTOLOAD(method, args...):
key = ref(self) + ":" + method
IF no args (getter mode):
val = cache_get(key)
IF cache hit:
IF val is a plain string (not a ref):
IF val is UNDEF_SENTINEL â return undef
RETURN val
IF val is an arrayref:
IF first element is a plain string AND equals UNDEF_SENTINEL â croak
RETURN dereferenced list
RETURN val (blessed object)
# Cache miss â ask the wrapped object
IF list context:
result_list = object->method()
IF empty â return ()
cache_set(key, \result_list)
RETURN result_list
# Scalar context
result = object->method()
IF defined:
cache_set(key, result)
RETURN result
cache_set(key, UNDEF_SENTINEL)
RETURN undef
ELSE (setter mode):
IF more than one arg (array setter):
val = object->method(\@args) # wrapped object stores arrayref
IF defined:
cache_set(key, val)
RETURN @val
cache_set(key, UNDEF_SENTINEL)
RETURN undef
ELSE (scalar setter):
val = object->method(args[0])
cache_set(key, val // UNDEF_SENTINEL)
RETURN val
=cut | |||||
| 460 | ||||||
| 461 | sub AUTOLOAD | |||||
| 462 | { | |||||
| 463 | 735 | 19921 | our $AUTOLOAD; | |||
| 464 | # rindex+substr avoids regex engine startup on every method dispatch | |||||
| 465 | 735 | 448 | my $param = substr($AUTOLOAD, rindex($AUTOLOAD, '::') + 2); | |||
| 466 | ||||||
| 467 | 735 | 351 | my $self = shift; | |||
| 468 | # _cache_prefix is precomputed at new() time (= ref($self) . ":") to avoid | |||||
| 469 | # calling ref() and doing string concat on every AUTOLOAD invocation. | |||||
| 470 | 735 | 390 | my $key = $self->{'_cache_prefix'} . $param; | |||
| 471 | 735 | 322 | my $object = $self->{'object'}; | |||
| 472 | ||||||
| 473 | # Getter path âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ | |||||
| 474 | 735 | 467 | if(scalar(@_) == 0) { | |||
| 475 | 155 | 162 | my $rc = $self->_cache_get($key); | |||
| 476 | ||||||
| 477 | # Truthiness check: falsy-but-defined values (0, '') are treated as cache | |||||
| 478 | # misses so the object is re-invoked every call. This is a known | |||||
| 479 | # limitation; see the LIMITATIONS section in the POD. | |||||
| 480 | 154 | 723 | if($rc) { | |||
| 481 | # Premise 1: $rc is truthy (the if-guard guarantees this). | |||||
| 482 | # Premise 2: ref($rc) returns '' for plain scalars, 'ARRAY' for | |||||
| 483 | # arrayrefs, or a package name for blessed objects. | |||||
| 484 | # The three branches below are mutually exclusive and exhaustive. | |||||
| 485 | 103 | 90 | if(!ref($rc)) { | |||
| 486 | # Conclusion: plain scalar â only type that can equal the sentinel. | |||||
| 487 | 80 | 67 | return if $rc eq UNDEF_SENTINEL; | |||
| 488 | 68 | 100 | return $rc; | |||
| 489 | } elsif(ref($rc) eq 'ARRAY') { | |||||
| 490 | # Premise: !ref($rc) was false, so ref($rc) is truthy; no need | |||||
| 491 | # to re-test ref() â the elsif is the deductive next step. | |||||
| 492 | 18 | 51 | Carp::croak($param) | |||
| 493 | if !ref($rc->[0]) && $rc->[0] eq UNDEF_SENTINEL; | |||||
| 494 | 14 14 | 9 20 | return @{$rc}; | |||
| 495 | } | |||||
| 496 | # Conclusion: ref($rc) is truthy and â 'ARRAY' â blessed object. | |||||
| 497 | # Overloaded eq was never invoked; return the object as-is. | |||||
| 498 | 5 | 5 | return $rc; | |||
| 499 | } | |||||
| 500 | ||||||
| 501 | # Cache miss â call the real object | |||||
| 502 | 51 | 45 | if(wantarray) { | |||
| 503 | 19 | 21 | my @result = $object->$param(); | |||
| 504 | 19 | 78 | return unless scalar(@result); # empty list: don't cache, return () | |||
| 505 | 10 | 15 | $self->_cache_set($key, \@result); | |||
| 506 | 10 | 206 | return @result; | |||
| 507 | } | |||||
| 508 | ||||||
| 509 | # Scalar / void context: cache the value (or the sentinel for undef) | |||||
| 510 | 32 | 39 | my $val = $object->$param(); | |||
| 511 | 31 | 201 | $self->_cache_set($key, defined($val) ? $val : UNDEF_SENTINEL); | |||
| 512 | 30 | 274 | return $val; | |||
| 513 | } | |||||
| 514 | ||||||
| 515 | # Setter path â array âââââââââââââââââââââââââââââââââââââââââââââââââââââ | |||||
| 516 | 580 | 303 | if(scalar(@_) > 1) { | |||
| 517 | # Pass the list as an arrayref; the wrapped object stores it | |||||
| 518 | 12 | 25 | my $val = $object->$param(\@_); | |||
| 519 | 12 | 171 | $self->_cache_set($key, defined($val) ? $val : UNDEF_SENTINEL); | |||
| 520 | 12 10 | 149 13 | return defined($val) ? @{$val} : (); | |||
| 521 | } | |||||
| 522 | ||||||
| 523 | # Setter path â scalar ââââââââââââââââââââââââââââââââââââââââââââââââââââ | |||||
| 524 | # CHI's set() returns the cache object, not the stored value; | |||||
| 525 | # capture the value before the set call and return it directly. | |||||
| 526 | 568 | 751 | my $val = $object->$param($_[0]); | |||
| 527 | 567 | 1919 | $self->_cache_set($key, defined($val) ? $val : UNDEF_SENTINEL); | |||
| 528 | 566 | 533 | return $val; | |||
| 529 | } | |||||
| 530 | ||||||
| 531 - 690 | =head1 LIMITATIONS
=over 4
=item Setter arguments are not part of the cache key.
Calls like C<< $obj->method($arg) >> use the key C<ClassName:method> regardless
of C<$arg>. Setters with different arguments therefore overwrite each other's
cache entries. For read-only caching of parameterised methods, see
L<Class::Simple::Readonly::Cached>.
=item Falsy scalar return values are not cached.
Methods that return C<0> or C<''> (false but defined) are treated as a cache
miss on every call: the underlying object is re-invoked each time. Only
C<undef> is cached (via the UNDEF_SENTINEL). If you need to cache C<0>, wrap
it in a container object or use a different caching strategy.
=item Sentinel collision.
If the wrapped object ever returns the literal string
C<< "Class::Simple::Cached\>UNDEF\<" >> as a scalar, or as the first element of a
list, the module will misinterpret it as a cached-undef marker. This string is
deliberately unusual, but callers working with arbitrary string data should be
aware of the constraint.
=item Shared cache and multiple classes.
When two C<Class::Simple::Cached> instances of B<different> classes share the
same cache object, their keys are namespaced by class name and do not collide.
However, C<purge()> on a CHI-style cache is global: destroying one instance will
purge I<all> entries from a shared CHI cache, including those of the other
instance. Use per-instance cache objects, or a hash-ref cache, to avoid this.
=item Does not work with L<Memoize>.
=item Overloaded C<eq> on cached values.
If the wrapped object returns a blessed value that overloads the C<eq> operator,
the sentinel comparison in the getter uses a C<ref()> pre-check so that only
plain strings are compared against the sentinel. Array elements are subject to
the same pre-check. Callers wrapping objects that return sentinel-like strings
via overloading should be aware of this guard.
=back
=head1 AUTHOR
Nigel Horne, C<< <njh at nigelhorne.com> >>
=head1 BUGS
Please report bugs and feature requests at
L<https://github.com/nigelhorne/Class-Simple-Cached/issues>.
=head1 SEE ALSO
L<Class::Simple>, L<CHI>, L<Class::Simple::Readonly::Cached>
=over 4
=item * L<Test Dashboard|https://nigelhorne.github.io/Class-Simple-Cached/coverage/>
=back
=head1 SUPPORT
This module is provided as-is without any warranty.
You can find documentation for this module with the perldoc command:
perldoc Class::Simple::Cached
=over 4
=item * MetaCPAN: L<https://metacpan.org/release/Class-Simple-Cached>
=item * Source: L<https://github.com/nigelhorne/Class-Simple-Cached>
=item * CPANTS: L<http://cpants.cpanauthors.org/dist/Class-Simple-Cached>
=item * Testers Matrix: L<http://matrix.cpantesters.org/?dist=Class-Simple-Cached>
=back
=head1 FORMAL SPECIFICATION
=head2 new
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
[State]
cache : â(HashRef ⪠CHIObject)
object : Object
[CHIObject]
can_get : Method
can_set : Method
can_purge : Method
new ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Î(cache, object)
cache? : HashRef ⪠CHIObject
object? : Object ⪠{â
}
âââââââââââââââââââââââââââââââââââââââââââââââââ
cache? â â
cache â² = cache?
object â² = (object? â â
⹠object?) ⨠Class::Simple.new()
âââââââââââââââââââââââââââââââââââââââââââââââââ
=head2 can
can ââââââââââââââââââââââââââââââââââââââââââââââââââ
Î(cache, object)
method? : MethodName
âââââââââââââââââââââââââââââââââââââââââââââââââ
result! = (method? = 'new')
⨠object.can(method?)
⨠SUPER::can(method?)
=head2 isa
isa ââââââââââââââââââââââââââââââââââââââââââââââââââ
Î(cache, object)
class? : ClassName
âââââââââââââââââââââââââââââââââââââââââââââââââ
result! = (class? = ref(self))
⨠(class? = __PACKAGE__)
⨠SUPER::isa(class?)
⨠object.isa(class?)
=head2 AUTOLOAD
AUTOLOAD âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Î(cache)
method? : MethodName
args? : Seq(Any)
âââââââââââââââââââââââââââââââââââââââââââââââââ
key = ref(self) â ":" â method?
Getter (args? = â
):
(â v ⢠cache_hit(key, v) â§ v â UNDEF_SENTINEL â¹ result! = v)
⨠(cache_hit(key, UNDEF_SENTINEL) ⹠result! = undef)
⨠(¬cache_hit(key, _)
â§ result! = object.method?()
⧠cacheⲠ= cache ⪠{key ⦠encode(result!)})
Setter (args? â â
):
objectâ².method?(args?) = args?
cacheâ² = cache ⪠{key ⦠encode(objectâ².method?())}
result! = args?
=head1 LICENCE AND COPYRIGHT
Copyright (C) 2019-2026, Nigel Horne
Usage is subject to the GPL2 licence terms.
If you use it,
please let me know.
=cut | |||||
| 691 | ||||||
| 692 | 1; | |||||