| File: | blib/lib/Sub/Abstract.pm |
| Coverage: | 99.3% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package Sub::Abstract; | |||||
| 2 | ||||||
| 3 | # Minimum required Perl version: 5.8 (Attribute::Handlers became core in 5.8). | |||||
| 4 | 13 13 | 1121399 23 | use 5.008; | |||
| 5 | 13 13 13 | 23 8 109 | use strict; | |||
| 6 | 13 13 13 | 28 13 275 | use warnings; | |||
| 7 | 13 13 13 | 2541 78278 25 | use autodie qw(:all); | |||
| 8 | ||||||
| 9 | 13 13 13 | 101137 23202 28 | use Attribute::Handlers; | |||
| 10 | 13 13 13 | 233 11 258 | use Carp qw(croak carp); # carp reserved for future non-fatal paths | |||
| 11 | 13 13 13 | 1407 10149 303 | use Readonly; | |||
| 12 | 13 13 13 | 2128 162364 351 | use Params::Validate::Strict 0.33 qw(validate_strict); | |||
| 13 | 13 13 13 | 1426 2003 1723 | use Return::Set qw(set_return); | |||
| 14 | ||||||
| 15 | # NOTE: Params::Get is available for any future method that accepts named | |||||
| 16 | # parameters. import() uses a positional calling convention imposed by | |||||
| 17 | # Perl's "use" mechanism and therefore cannot use named params. | |||||
| 18 | ||||||
| 19 - 27 | =head1 NAME Sub::Abstract - Abstract (virtual) methods for plain-Perl OO =head1 VERSION Version 0.01 =cut | |||||
| 28 | ||||||
| 29 | our $VERSION = '0.01'; | |||||
| 30 | ||||||
| 31 | # --------------------------------------------------------------------------- | |||||
| 32 | # Constants | |||||
| 33 | # --------------------------------------------------------------------------- | |||||
| 34 | ||||||
| 35 | # Self-referential constant: the canonical name of this package. | |||||
| 36 | Readonly::Scalar my $SELF => __PACKAGE__; | |||||
| 37 | ||||||
| 38 | # Validation schema for a single Perl sub name passed to import(). | |||||
| 39 | # Matches any legal Perl identifier: starts with _ or letter, then \w*. | |||||
| 40 | Readonly::Scalar my $SUB_NAME_SCHEMA => { | |||||
| 41 | name => { | |||||
| 42 | type => 'string', | |||||
| 43 | regex => qr/\A[_a-zA-Z]\w*\z/, | |||||
| 44 | } | |||||
| 45 | }; | |||||
| 46 | ||||||
| 47 | # --------------------------------------------------------------------------- | |||||
| 48 | # Public variables | |||||
| 49 | # --------------------------------------------------------------------------- | |||||
| 50 | ||||||
| 51 | # Set to a true value to suppress all abstract-method croaks globally. | |||||
| 52 | # Always use 'local' in tests to prevent state from bleeding between cases. | |||||
| 53 | our $BYPASS = 0; | |||||
| 54 | ||||||
| 55 | # Runtime tunables. Modify $config{harness_bypass} to control whether | |||||
| 56 | # HARNESS_ACTIVE suppresses enforcement. May be extended in future releases. | |||||
| 57 | our %config = ( | |||||
| 58 | harness_bypass => 1, # 1 = suppress croaks when HARNESS_ACTIVE is set | |||||
| 59 | ); | |||||
| 60 | ||||||
| 61 | # --------------------------------------------------------------------------- | |||||
| 62 | # Internal state | |||||
| 63 | # --------------------------------------------------------------------------- | |||||
| 64 | ||||||
| 65 | # Pending (owner_pkg, sub_name) pairs queued by import() during compilation. | |||||
| 66 | # Populated before CHECK fires; consumed and cleared by the CHECK block. | |||||
| 67 | my @_pending; | |||||
| 68 | ||||||
| 69 | # Becomes 1 once the CHECK block has fired. | |||||
| 70 | # import() consults this to decide whether to queue or wrap immediately. | |||||
| 71 | my $_post_check = 0; | |||||
| 72 | ||||||
| 73 | # --------------------------------------------------------------------------- | |||||
| 74 | # ATTRIBUTE HANDLER | |||||
| 75 | # --------------------------------------------------------------------------- | |||||
| 76 | ||||||
| 77 | # UNIVERSAL::Abstract :ATTR(CODE,CHECK) | |||||
| 78 | # Purpose : Replace any sub decorated with :Abstract with the croak | |||||
| 79 | # closure returned by _wrap(). Fires once per decorated sub | |||||
| 80 | # at CHECK time, after all subs in all packages are compiled. | |||||
| 81 | # Entry : Invoked by Attribute::Handlers with six positional args: | |||||
| 82 | # $package -- the package in which the sub was declared | |||||
| 83 | # $symbol -- the typeglob for the sub | |||||
| 84 | # $referent -- the CODE ref (stub body; discarded) | |||||
| 85 | # $attr -- the attribute name ('Abstract') | |||||
| 86 | # $data -- attribute arguments (undef for :Abstract) | |||||
| 87 | # $phase -- 'CHECK' | |||||
| 88 | # Exit status : Returns nothing (void); replaces *{$symbol} in the stash. | |||||
| 89 | # Side effects : Overwrites the CODE slot for $symbol in $package's stash. | |||||
| 90 | # Notes : Compiled inside Sub::Abstract so that caller(1) inside | |||||
| 91 | # _assert_private_caller resolves to Sub::Abstract rather | |||||
| 92 | # than the calling package -- allowing the guard to pass. | |||||
| 93 | # Installing :Abstract in UNIVERSAL gives all packages access | |||||
| 94 | # after a single "use Sub::Abstract", at the cost of global | |||||
| 95 | # namespace pollution (see KNOWN LIMITATIONS). | |||||
| 96 | # $referent, $attr, $data, $phase are received but unused; | |||||
| 97 | # they are named for documentation clarity only. | |||||
| 98 | sub UNIVERSAL::Abstract :ATTR(CODE,CHECK) { | |||||
| 99 | 38 | 1131 | my ($package, $symbol, undef, undef, undef, undef) = @_; | |||
| 100 | ||||||
| 101 | # Extract the bare sub name from the typeglob, then replace the glob. | |||||
| 102 | 38 38 | 17 22 | my $sub_name = *{$symbol}{NAME}; | |||
| 103 | 13 13 13 | 60 10 498 | no warnings 'redefine'; | |||
| 104 | 38 38 | 33 24 | *{$symbol} = _wrap($package, $sub_name); | |||
| 105 | 38 | 26 | return; | |||
| 106 | 13 13 13 | 24 10 255 | } | |||
| 107 | ||||||
| 108 | # --------------------------------------------------------------------------- | |||||
| 109 | # PUBLIC INTERFACE | |||||
| 110 | # --------------------------------------------------------------------------- | |||||
| 111 | ||||||
| 112 - 297 | =head1 SYNOPSIS
package Animal;
use Sub::Abstract;
# Attribute form (stub body required for Attribute::Handlers)
sub speak :Abstract { }
sub eat :Abstract { }
# Declarative form (no stub body needed)
use Sub::Abstract qw(speak eat);
package Dog;
our @ISA = ('Animal');
sub speak { 'Woof' } # satisfies the contract; wrapper never fires
# forgot eat -- runtime croak when called
=head1 DESCRIPTION
Enforces abstract (virtual) method contracts for plain-Perl OO without
requiring Moose or Moo. A subroutine decorated with C<:Abstract> (or
named in C<use Sub::Abstract qw(...)>) is replaced at C<CHECK> time with
a wrapper that C<Carp::croak>s whenever it is reached.
Perl's MRO ensures the wrapper is only reached when no subclass in the
call chain has provided an implementation: if C<Dog::speak> exists, the
wrapper installed in C<Animal::speak> is never called.
This module is only meaningful for plain-Perl OO or packages that do not
use a full object framework. Moo and Moose handle abstract/required
methods in their own object systems.
=head2 Two usage forms
=over 4
=item Attribute form (preferred)
sub speak :Abstract { }
The C<:Abstract> attribute is registered in C<UNIVERSAL> via
L<Attribute::Handlers> when C<Sub::Abstract> is loaded, so every package
has access to it without further C<use> or inheritance. A stub body
(even an empty one) is required because C<Attribute::Handlers> needs a
C<CODE> ref. The stub is replaced at C<CHECK> time.
=item Declarative form
use Sub::Abstract qw(speak eat);
Each named method is installed as an abstract-croak wrapper at C<CHECK>
time (or immediately if the module is loaded past C<CHECK>). No stub body
is needed.
=back
=head2 Bypass for testing
Either condition alone (OR logic) suppresses the croak at call time:
=over 4
=item * C<$Sub::Abstract::BYPASS> set to a true value. Use C<local> in tests.
Checked first; short-circuits the second condition.
=item * C<$ENV{HARNESS_ACTIVE}> set (the convention used by L<Test::Harness>/prove)
B<and> C<$config{harness_bypass}> is truthy (the default).
=back
The C<HARNESS_ACTIVE> bypass can be disabled:
$Sub::Abstract::config{harness_bypass} = 0;
B<Important:> setting C<$BYPASS> to any truthy value takes full precedence.
Even with C<harness_bypass = 0>, a truthy C<$BYPASS> still suppresses the
croak. The two guards use C<||> (short-circuit OR) and C<$BYPASS> is
checked first. See L</Bypass precedence> under KNOWN LIMITATIONS.
=head2 Error message format
speak() is an abstract method of Animal and must be implemented by Dog
=head1 PUBLIC INTERFACE
=head2 import
use Sub::Abstract; # attribute form -- no arguments
use Sub::Abstract qw(speak eat); # declarative form
=head3 Purpose
With B<no arguments>: loads the module and makes the C<:Abstract> attribute
globally available via C<UNIVERSAL>. No stash entries are modified.
With B<one or more method names>: installs abstract-croak wrappers for
those methods in the calling package. Wrappers are installed at C<CHECK>
time when called during compilation, or immediately when called after
C<CHECK> has fired. Validation is fail-fast and all-or-nothing: if any
name is invalid the entire call croaks before touching the stash.
=head3 Arguments
=over 4
=item C<$class> (required, implicit via C<use>)
The invocant. Always C<'Sub::Abstract'> in normal usage; not validated
because Perl's C<use> mechanism enforces it.
=item C<@methods> (optional)
Zero or more Perl sub names, each matching C</\A[_a-zA-Z]\w*\z/>.
An undef or reference in this list is coerced to the empty string before
validation, producing a clear identifier-mismatch error.
=back
=head3 Returns
The class name (C<'Sub::Abstract'>) as a plain string. All call paths
return this value, consistent with the sister modules C<Sub::Private>
and C<Sub::Protected>.
=head3 Example
package MyBase;
use Sub::Abstract qw(render serialize);
package MyConcrete;
our @ISA = ('MyBase');
sub render { ... } # satisfies render contract
sub serialize { ... } # satisfies serialize contract
# MyBase->new->render croaks; MyConcrete->new->render does not.
=head3 API SPECIFICATION
=head4 Input
# import() uses positional arguments imposed by Perl's "use" mechanism;
# named parameters are not applicable here.
# Each element of @methods is validated individually against:
{
name => {
type => 'string',
regex => qr/\A[_a-zA-Z]\w*\z/,
}
}
=head4 Output
{ type => 'string' } # always returns the class name ('Sub::Abstract')
=head3 PSEUDOCODE
import($class, @methods):
IF @methods is empty
RETURN class name # attribute form; nothing to install
FOR EACH name in @methods
coerce undef/ref to empty string
validate against /\A[_a-zA-Z]\w*\z/ via validate_strict()
CROAK if invalid # fail-fast; no stash modification
END FOR
owner_pkg <- caller package
IF post_check flag is set # CHECK has already fired
FOR EACH name: _process_one(owner_pkg, name)
ELSE # still compiling; queue for CHECK
FOR EACH name: push [owner_pkg, name] onto @_pending
END IF
RETURN class name
=head3 MESSAGES
Message Meaning / Action
------- ----------------
Sub::Abstract->import: 'NAME' is not a valid NAME failed the identifier regex
Perl identifier /\A[_a-zA-Z]\w*\z/. Common causes:
leading digit, hyphen, non-ASCII
character, undef passed where a name
was expected, or a reference in the
list. Action: inspect the argument
list passed to "use Sub::Abstract
qw(...)".
=cut | |||||
| 298 | ||||||
| 299 | sub import { | |||||
| 300 | 134 | 596531 | my ($class, @subs) = @_; | |||
| 301 | ||||||
| 302 | # Only do stash work when sub names were actually supplied. | |||||
| 303 | # With no arguments the :Abstract attribute is already globally available. | |||||
| 304 | 134 | 184 | if (@subs) { | |||
| 305 | # Validate every name before touching the stash. | |||||
| 306 | # Fail fast, all-or-nothing: no partial wrapping on bad input. | |||||
| 307 | 79 | 76 | for my $sub_name (@subs) { | |||
| 308 | # Coerce undef and references to empty string so the validator | |||||
| 309 | # produces a meaningful "'' is not a valid identifier" message. | |||||
| 310 | 88 | 225 | my $check = (defined $sub_name && !ref $sub_name) ? $sub_name : q{}; | |||
| 311 | ||||||
| 312 | # BUG FIX: use the "eval { ...; 1 } or do { $err = $@ }" pattern | |||||
| 313 | # rather than "eval { ... }; croak if $@". A DESTROY method fired | |||||
| 314 | # between the eval and the $@ test can overwrite $@, causing the | |||||
| 315 | # error to be silently swallowed or a wrong message to be reported. | |||||
| 316 | 88 | 57 | my $ok = eval { | |||
| 317 | 88 | 166 | validate_strict( | |||
| 318 | schema => $SUB_NAME_SCHEMA, | |||||
| 319 | input => { name => $check }, | |||||
| 320 | ); | |||||
| 321 | 49 | 6324 | 1; | |||
| 322 | }; | |||||
| 323 | 88 | 106015 | croak "$SELF->import: '$check' is not a valid Perl identifier" | |||
| 324 | unless $ok; | |||||
| 325 | } | |||||
| 326 | ||||||
| 327 | # Decide whether to wrap immediately or queue for CHECK. | |||||
| 328 | 40 | 51 | my $owner_pkg = caller; | |||
| 329 | 40 | 323 | if ($_post_check) { | |||
| 330 | # CHECK has already fired: install wrappers directly into the stash. | |||||
| 331 | 23 | 19 | for my $sub_name (@subs) { | |||
| 332 | 27 | 36 | _process_one($owner_pkg, $sub_name); | |||
| 333 | } | |||||
| 334 | } | |||||
| 335 | else { | |||||
| 336 | # Still in compilation: queue each pair for CHECK to drain. | |||||
| 337 | 17 | 14 | for my $sub_name (@subs) { | |||
| 338 | 19 | 19 | push @_pending, [ $owner_pkg, $sub_name ]; | |||
| 339 | } | |||||
| 340 | } | |||||
| 341 | } | |||||
| 342 | ||||||
| 343 | # Single return path: consistent with Sub::Private and Sub::Protected. | |||||
| 344 | 95 | 181 | return set_return($class, { type => 'string' }); | |||
| 345 | } | |||||
| 346 | ||||||
| 347 | # --------------------------------------------------------------------------- | |||||
| 348 | # CHECK-TIME PROCESSING | |||||
| 349 | # --------------------------------------------------------------------------- | |||||
| 350 | ||||||
| 351 | # CHECK block. | |||||
| 352 | # Purpose : Drain @_pending (all import() calls queued during compilation) | |||||
| 353 | # and mark the module post-CHECK so future import() calls wrap | |||||
| 354 | # immediately rather than queuing. | |||||
| 355 | # Ordering note: $_post_check is set to 1 BEFORE @_pending is drained. | |||||
| 356 | # This means if _process_one were ever to trigger import() | |||||
| 357 | # (it does not in practice), that nested import() would call | |||||
| 358 | # _process_one directly rather than re-queuing. | |||||
| 359 | # The _assert_private_caller guard inside _process_one is | |||||
| 360 | # satisfied because this CHECK block is compiled inside | |||||
| 361 | # Sub::Abstract: caller(1) resolves to Sub::Abstract. | |||||
| 362 | CHECK { | |||||
| 363 | 12 | 14569 | $_post_check = 1; | |||
| 364 | 12 19 | 16 22 | _process_one(@{$_}) for @_pending; | |||
| 365 | 12 | 34 | @_pending = (); | |||
| 366 | } | |||||
| 367 | ||||||
| 368 | # --------------------------------------------------------------------------- | |||||
| 369 | # PRIVATE SUBROUTINES | |||||
| 370 | # --------------------------------------------------------------------------- | |||||
| 371 | ||||||
| 372 | # _process_one | |||||
| 373 | # Purpose : Install an abstract-croak wrapper for one named method in | |||||
| 374 | # a given package. | |||||
| 375 | # Entry : $owner_pkg -- the package declaring the abstract method | |||||
| 376 | # $sub_name -- unqualified method name (a valid identifier) | |||||
| 377 | # Exit status : Returns nothing (void); the package stash is modified. | |||||
| 378 | # Side effects : Overwrites *{"${owner_pkg}::${sub_name}"} with the closure | |||||
| 379 | # returned by _wrap(). Creates the glob entry if it does not | |||||
| 380 | # yet exist (the declarative form requires no pre-existing stub). | |||||
| 381 | # Notes : Unlike Sub::Private::_process_one there is no pre-existence | |||||
| 382 | # check: abstract methods in the declarative form have no body. | |||||
| 383 | # Protected by _assert_private_caller (via the bypass guard) | |||||
| 384 | # so that external callers cannot invoke this directly. | |||||
| 385 | sub _process_one { | |||||
| 386 | 54 | 15330 | my ($owner_pkg, $sub_name) = @_; | |||
| 387 | ||||||
| 388 | # Guard: only Sub::Abstract itself may call this. | |||||
| 389 | # Bypass is active under the test harness or when $BYPASS is set. | |||||
| 390 | _assert_private_caller('_process_one') | |||||
| 391 | 54 | 173 | unless $BYPASS || ($config{harness_bypass} && $ENV{HARNESS_ACTIVE}); | |||
| 392 | ||||||
| 393 | # Install the wrapper; suppress redefine warnings for the attribute form. | |||||
| 394 | 13 13 13 | 4694 9 190 | no strict 'refs'; | |||
| 395 | 13 13 13 | 17 9 2134 | no warnings 'redefine'; | |||
| 396 | 51 51 | 87 184 | *{"${owner_pkg}::${sub_name}"} = _wrap($owner_pkg, $sub_name); | |||
| 397 | 51 | 53 | return; | |||
| 398 | } | |||||
| 399 | ||||||
| 400 | # _wrap | |||||
| 401 | # Purpose : Build and return the abstract-enforcement closure for one | |||||
| 402 | # method. The closure croaks with a message naming both the | |||||
| 403 | # abstract owner package and the concrete invocant class. | |||||
| 404 | # Entry : $owner_pkg -- the package declaring the abstract method | |||||
| 405 | # $sub_name -- unqualified method name (for error messages) | |||||
| 406 | # Exit status : Returns a new CODE ref (the enforcement wrapper). | |||||
| 407 | # Side effects : None. The closure captures $owner_pkg and $sub_name by | |||||
| 408 | # value; $BYPASS and %config are read at call time (not | |||||
| 409 | # capture time), so runtime changes to them take effect. | |||||
| 410 | # Notes : No $code argument and no delegation (unlike Sub::Private). | |||||
| 411 | # Calling an abstract method is always an error; there is | |||||
| 412 | # nothing to delegate to. | |||||
| 413 | # Invocant extraction: (ref($_[0]) || $_[0]) // '<undef>' | |||||
| 414 | # ref() -- for blessed objects: returns the concrete class | |||||
| 415 | # $_[0] -- for class-method calls: the package name string | |||||
| 416 | # '<undef>' -- guard against a completely absent invocant | |||||
| 417 | # $BYPASS is consulted first (short-circuit ||); setting | |||||
| 418 | # harness_bypass=0 does NOT re-enable enforcement while | |||||
| 419 | # $BYPASS is truthy (see KNOWN LIMITATIONS). | |||||
| 420 | sub _wrap { | |||||
| 421 | 114 | 114796 | my ($owner_pkg, $sub_name) = @_; | |||
| 422 | ||||||
| 423 | # Guard: same bypass semantics as _process_one. | |||||
| 424 | _assert_private_caller('_wrap') | |||||
| 425 | 114 | 248 | unless $BYPASS || ($config{harness_bypass} && $ENV{HARNESS_ACTIVE}); | |||
| 426 | ||||||
| 427 | # Return a closure that enforces the abstract contract at call time. | |||||
| 428 | return sub { | |||||
| 429 | 181 | 686678 | return if $BYPASS; | |||
| 430 | 163 | 416 | return if $config{harness_bypass} && $ENV{HARNESS_ACTIVE}; | |||
| 431 | 152 | 264 | my $invocant = ref($_[0]) || $_[0] // '<undef>'; | |||
| 432 | 152 | 925 | croak "${sub_name}() is an abstract method of ${owner_pkg}" | |||
| 433 | . " and must be implemented by ${invocant}"; | |||||
| 434 | 109 | 234 | }; | |||
| 435 | } | |||||
| 436 | ||||||
| 437 | # _assert_private_caller | |||||
| 438 | # Purpose : Croak if the guarded private method (_wrap or _process_one) | |||||
| 439 | # was invoked from outside Sub::Abstract. | |||||
| 440 | # Entry : $method_name -- the name of the guarded method (used only | |||||
| 441 | # in the error message; not validated). | |||||
| 442 | # Exit status : Returns normally when the immediate caller of the guarded | |||||
| 443 | # function is Sub::Abstract. Croaks otherwise. | |||||
| 444 | # Side effects : May croak. | |||||
| 445 | # Notes : Uses caller(1), not caller(0). | |||||
| 446 | # Inside _assert_private_caller: | |||||
| 447 | # caller(0) = Sub::Abstract (this sub's own frame) | |||||
| 448 | # caller(1) = the package that called _wrap/_process_one | |||||
| 449 | # For calls from CHECK{} or UNIVERSAL::Abstract (both compiled | |||||
| 450 | # inside Sub::Abstract), caller(1) = Sub::Abstract -- allowed. | |||||
| 451 | # For direct external calls to _wrap or _process_one, caller(1) | |||||
| 452 | # is the calling package -- denied unless bypass is active. | |||||
| 453 | # The '//' fallback to q{} handles the pathological case where | |||||
| 454 | # caller(1) returns undef (top-level call outside any sub); | |||||
| 455 | # this path is practically unreachable in production. | |||||
| 456 | # This guard is a lint tool, not a security fence: code that | |||||
| 457 | # injects a sub into the Sub::Abstract namespace can defeat it | |||||
| 458 | # (see KNOWN LIMITATIONS). | |||||
| 459 | sub _assert_private_caller { | |||||
| 460 | 18 | 7837 | my $method_name = $_[0]; | |||
| 461 | 18 | 27 | my $caller = (caller(1))[0] // q{}; | |||
| 462 | ||||||
| 463 | # Single conditional croak; one return at the bottom. | |||||
| 464 | 18 | 288 | croak "${method_name}() is a private method of $SELF" | |||
| 465 | . " and cannot be called from ${caller}" | |||||
| 466 | unless $caller eq $SELF; | |||||
| 467 | 6 | 7 | return; | |||
| 468 | } | |||||
| 469 | ||||||
| 470 | 1; | |||||
| 471 | ||||||