File Coverage

File:blib/lib/Sub/Abstract.pm
Coverage:99.3%

linestmtbrancondsubtimecode
1package 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
29our $VERSION = '0.01';
30
31# ---------------------------------------------------------------------------
32# Constants
33# ---------------------------------------------------------------------------
34
35# Self-referential constant: the canonical name of this package.
36Readonly::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*.
40Readonly::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.
53our $BYPASS = 0;
54
55# Runtime tunables.  Modify $config{harness_bypass} to control whether
56# HARNESS_ACTIVE suppresses enforcement.  May be extended in future releases.
57our %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.
67my @_pending;
68
69# Becomes 1 once the CHECK block has fired.
70# import() consults this to decide whether to queue or wrap immediately.
71my $_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.
98sub 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
299sub 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.
362CHECK {
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.
385sub _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).
420sub _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).
459sub _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
4701;
471