App-CamelPKI

 view release on metacpan or  search on metacpan

lib/Class/Facet.pm  view on Meta::CPAN

            unless ($methname =~ m/^(from|on_error|delegate)$/);
        $class->$methname(scalar(caller),
                          (ref($args) eq "ARRAY" ? @$args : $args));
    }
}

=head2 from($facetclass, $origclass)

Indicates that $facetclass is to be a facet class from $origclass.
This method must be called first before any B<Class::Facet> operation
on $facetclass.

=cut

sub from {
    my ($class, $facetclass, $origclass) = @_;
    no strict "refs";
    *{"${facetclass}::_facet_of"} = sub { $origclass };
    foreach my $miranda (qw(rescind error)) {
        *{"${facetclass}::facet_$miranda"} = \&{"_miranda_$miranda"};
    }
    *{"${facetclass}::AUTOLOAD"} = \&_miranda_AUTOLOAD;
}

=head2 delegate($facetclass, $methodname)

Indicates that the method named $methodname is to be delegated to the
original object without altering the parameters or the return value.
Is mostly equivalent to declaring a sub like this:

=for My::Tests::Below "delegate equivalent" begin

  sub foo {
    my (undef, $origself) = Class::Facet->selves(\@_);
    unshift(@_, $origself);
    goto $origself->can("foo");
  }

=for My::Tests::Below "delegate equivalent" end

except that the error management is better.

=cut

sub delegate {
    my ($class, $facetclass, @methods) = @_;
    foreach my $methodname (@methods) {
        no strict "refs";
        *{"${facetclass}::${methodname}"} = sub {
            my (undef, $origself) = Class::Facet->selves(\@_);
            unshift(@_, $origself);
            goto $origself->can($methodname);
        };
    }
}

=head2 on_error($facetclass, $sub)

Installs $sub as the error management callback method for $facetclass.
$sub will always be called as a class method in void context, and
should throw an exception with L<perlfunc/die>, L<Exception::Class> or
some such, and not return.  As shown in L</SYNOPSIS>, $sub should
accept the following named parameters:

=over

=item B<-file>

=item B<-line>

The filename and line number of the place in the code that invoked the
faulty operation.

=item B<-facetclass>

The facet class against which the error sub is being invoked.  This
will be $facetclass, unless $sub is the error management routine for
several facets at once.

=item B<-reason>

The reason why the error is thrown, as the name of the method in
B<Class::Facet> that triggered the error, or one of the special values
C<facet_error> (meaning that L</facet_error> was invoked manually) or
C<forbidden_method> (if one tries to invoke a forbidden method through
the facet object).

=item B<-details> (optional)

A message in english explaining the reason of the error.

=item B<-method> (optional)

Set when trying to invoke a method through a facet object, but this
method is neither delegated (using L</delegate>) nor defined in the
facet package.

=back

The default implementation (if C<on_error()> is not called) is to
throw a text message in english using L<perlfunc/die> that contains a
subset of the aforementioned information.

=cut

# See the default implementation of the error handler in L</_carp>.
sub on_error {
    my ($class, $facetclass, $sub) = @_;
    unless (ref($sub) eq "CODE") {
        $sub = "an undefined value" if ! defined $sub;
        croak("Class::Facet: cannot use $sub as an error handler");
    }
    no warnings "redefine"; no strict "refs";
    *{"${facetclass}::_facet_die"} = $sub;
}

=head2 make($facetclass, $origobject)

Returns a facet of $object in class $facetclass.  The returned facet
object is an ordinary hashref-based object, constructed like this:

=for My::Tests::Below "make structure" begin

    bless { delegate => $origobject }, $facetclass;

=for My::Tests::Below "make structure" end

and B<Class::Facet> will never use any other field in blessed hash
besides C<delegate>.  The facet class and facet constructor are
therefore free to add their own fields into the facet object.

=cut

sub make {
    my ($class, $facetclass, $origobject) = @_;
    
#	use Class::ISA;
#	use UNIVERSAL::can; 
	#Making a facet from a facet is forbidden !!!!
#	for my $int (Class::ISA::super_path($facetclass)) {
# 		eval {($int->can("from") && $int->can("delegate"))};
# 		throw App::CamelPKI::Error::User
# 			("Subclassing a facet is forbidden")
# 				if ($int->can("from") && $int->can("delegate"))';
#	}
	
    return bless { delegate => $origobject }, $facetclass;
}

=head2 selves($argslistref)

Interprets $argslistref as a reference to the argument list (@_) of a
class method, and modifies it in place by removing the first argument
(as L<perlfunc/shift> would do).  Returns a a ($facetself, $origself)
pair where $facetself is the facet object, and $origself is the
original object.

This class method is useful for creating custom facet methods, such as
the C<get_substuff> example in L</SYNOPSIS>.

=cut

sub selves {
    my ($class, $argslistref) = @_;
    my $facetobject = shift @$argslistref;
    return ($facetobject, $facetobject->{delegate});
}

=head1 MIRANDA METHODS

These methods can be called from any facet object, regardless of how
restricted the facet class; in capability discipline parlance, they
can thus be interpreted as unremovable rights, just like those
enumerated in the I<miranda warning> given by the police officer upon
arresting you.

=head2 facet_rescind()

Turns the facet into a useless object, that will not accept any
further method call.

=cut

sub _miranda_rescind {
    my ($self) = @_;
    bless $self, ref($self) . "::Rescinded";
}

=head2 facet_error(%named_args)

Throws an exception by invoking the error mechanism configured with
L</on_error> for this facet class.  This may be used from inside a
facetized method, so as to make error handling uniform.

=cut

sub _miranda_error {
    my $self = shift;
    push(@_, "???") if (@_ % 2);
    Class::Facet->_carp(ref($self), @_);
}

lib/Class/Facet.pm  view on Meta::CPAN

use Test::More qw(no_plan);
use Test::Group;

test "import" => sub {
    no warnings "redefine";
    our @calls;
    local *Class::Facet::from = sub { shift; push (@calls, "from", \@_) };
    local *Class::Facet::delegate = sub
        { shift; push (@calls, "delegate", \@_) };
    foreach my $snip ("import use simple",
                      "import converted into method calls",
                      "import use multiple") {
        @calls = ();
        my $callerpackage = "Foo::ReadOnlyFacet";
        eval "package $callerpackage; " .
            My::Tests::Below->pod_code_snippet($snip); die $@ if $@;
        is_deeply(\@calls,
                  [ from => [ "Foo::ReadOnlyFacet", "foo" ],
                    delegate => [ "Foo::ReadOnlyFacet", "bar", "baz" ] ]);
    }
};

=head2 Foo::TheRealOne

The bogus original class defined in L</SYNOPSIS> is the basis for all
tests.  The methods are all stubbed down to simply pushing a marker
into @Foo::TheRealOne::calls whenever they are called.

=cut

my $synopsis = My::Tests::Below->pod_code_snippet("synopsis main class");
$synopsis =~ s/sub get_substuff.*//g;
$synopsis =~ s|sub (.*) { \.\.\. }|sub $1 { push(our \@calls, "$1"); }|g;
eval $synopsis; die $@ if $@;

sub Foo::TheRealOne::new { bless {}, shift }
sub Foo::TheRealOne::in_a_bad_mood { 0 } # Lucky you!

=head2 Foo::TheRealOne::SubStuff

The class of the object returned by C<Foo::TheRealOne::get_substuff>.
Obviously no less bogus than the rest of the test fixture.

=cut

sub Foo::TheRealOne::get_substuff {
    return bless { }, "Foo::TheRealOne::SubStuff";
}

sub Foo::TheRealOne::SubStuff::facet_readonly { shift }

test "synopsis, BEGIN style" => sub {
    eval My::Tests::Below->pod_code_snippet("synopsis facet class");
    die $@ if $@;
    @Foo::TheRealOne::calls = ();
    my $facet = Foo::TheRealOne->new->facet_readonly;
    $facet->get_this();
    is_deeply(\@Foo::TheRealOne::calls, ["get_this"]);
    eval {
        $facet->set_that;
        fail("method should have thrown");
    };
    isnt($@, undef);
    is_deeply(\@Foo::TheRealOne::calls, ["get_this"]);
};

test 'synopsis, "use Class::Facet" style' => sub {
    eval "package Foo::ReadOnlyFacetToo;" .
        My::Tests::Below->pod_code_snippet("synopsis without BEGIN");
    die $@ if $@;
    @Foo::TheRealOne::calls = ();
    my $facet = Class::Facet->make
        ("Foo::ReadOnlyFacetToo", Foo::TheRealOne->new);
    $facet->get_this();
    is_deeply(\@Foo::TheRealOne::calls, ["get_this"]);
    eval {
        $facet->set_that;
        fail("method should have thrown");
    };
    isnt($@, undef);
    is_deeply(\@Foo::TheRealOne::calls, ["get_this"]);
};

test "facet structure" => sub {
    my $origobject = Foo::TheRealOne->new;
    my $facet = $origobject->facet_readonly;
    my $facetclass = "Foo::ReadOnlyFacet";
    my $facettoo = eval My::Tests::Below->pod_code_snippet
        ("make structure");
    die $@ if $@;
    is_deeply($facet, $facettoo);
};

test "transparently delegated method" => sub {
    local *Foo::TheRealOne::foo = sub { pass };
    eval "package Foo::ReadOnlyFacet; " .
        My::Tests::Below->pod_code_snippet("delegate equivalent");
    die $@ if $@;
    Foo::TheRealOne->new->foo();
};

test "bogus method calls in the facet look real" => sub {
    my $real = Foo::TheRealOne->new;
    my @errors;
    foreach my $object ($real, $real->facet_readonly) {
        eval { $object->glork(); };
        push(@errors, $@);
    }
    is(scalar(grep { defined } @errors), 2);
    ok($errors[1] =~ s/ReadOnlyFacet/TheRealOne/);
    is($errors[0], $errors[1]);
};

test "on_error and faceted-out methods" => sub {
    eval {
        Foo::TheRealOne->new->facet_readonly->set_this();
        fail;
    };
    like($@, qr/^forbidden method set_this/);
};

TODO:{
	local $TODO = "Defensiveness not implemented";
test "make defensiveness" => sub {
    @Bogus::SubFacet::ISA = qw(Foo::ReadOnlyFacet);
    my $object = Foo::TheRealOne->new;

    eval {
        Class::Facet->make("Bogus::SubFacet", $object);
        fail("subclassing a facet is a no-no");
    };

    @Foo::SubReal::ISA = qw(Foo::TheRealOne);
    Class::Facet->make("Foo::ReadOnlyFacet", Foo::SubReal->new);
    pass("->make works for subclasses too");
};
};



( run in 1.695 second using v1.01-cache-2.11-cpan-437f7b0c052 )