Compile-time sanity check provided by role - perl

I have a module that refuses to load unless a compile-time sanity check is met. Something like this:
package TopSecret;
use Moose;
die "Only Joe can use this!" unless $ENV{USER} eq 'joe';
1;
Now I would like to apply a similar sanity check to multiple modules, so my thought is to put it in a role. The consuming module would provide some information to customize the check a bit. So it might look something like:
package TopSecret;
use Moose;
with 'ForAuthorizedUser';
sub authorized_user { 'joe' }
1;
The problem is: how can I exercise TopSecret::authorized_user() from within ForAuthorizedUser, at compile time? Something like 'requires "authorized_user"' - except it would have to verify not just that the method exists, but execute it and check the return value.

I think that attribute overriding would be appropriate here. You declare the attribute in your Role and mark it as required, but don't provide a definition. Then the module that consumes the Role can supply the value for that attribute. Note that validation is typically done in the BUILD() subroutine.
package ForAuthorizedUser;
use Moose::Role;
use Carp qw(croak); # so you can see the line it fails on
has 'authorized_user' => (
is => 'ro',
required => 1,
);
sub BUILD {
my ($self) = #_;
croak "Only Joe can use this!"
unless $self->authorized_user eq 'joe';
}
1;
Now in your module that consumes ForAuthorizedUser, you supply the definition for the attribute:
package TopSecret;
use Moose;
with qw(ForAuthorizedUser);
has '+authorized_user' => (
default => 'joe',
);
__PACKAGE__->meta->make_immutable;
In a separate module you do the same thing, but with a different name (mine):
package TopSecret2;
use Moose;
with qw(ForAuthorizedUser);
has '+authorized_user' => (
default => 'hunter',
);
__PACKAGE__->meta->make_immutable;
Then you could test this like so:
use TopSecret;
use TopSecret2;
TopSecret->new; # lives
TopSecret2->new # croaks Only Joe can use this! at constructor TopSecret2::new (defined at Test.pm line 35) line 36.

Related

How to we dynamically create missing attributes in Moo or Moose?

We have a sample code like below. Is it possible to to capture all missing attributes invoked in package FooBar and create it dynamically? This is something like PHP's __call.
test.pl
package Person;
use feature qw(say);
use Moo;
has name => (is => "ro");
my $p = Person->new(name => "John");
say $p->name;
# The missing attribute method will be dynamically created when
# invoked even it's not declared in Person.
say $p->lalala;
$ perl test.pl
John
Can't locate object method "lalala" via package "Test" at test.pl line 13.
It's possible using AUTOLOAD and metaprogramming, the question remains Why.
There might be nicer ways using parameterized roles, but I just wanted to quickly show how to do it. I would reject such code in a review (I'd expect at least a comment explaining why autoloading is needed).
#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };
{ package MyObj;
use Moose;
sub AUTOLOAD {
my ($self) = #_;
( my $method = our $AUTOLOAD ) =~ s/.*:://;
(ref $self)->meta->add_attribute($method, is => 'rw');
goto &$method
}
}
say 'MyObj'->can('lalala'); # No, it can't.
my $o = 'MyObj'->new;
$o->lalala(12); # Attribute created.
say $o->lalala; # 12.
Update: Previously, my code was more complex, as it replied to #simbabque's comment to the question: it showed how to add the attribute to an instance, not the whole class.

Composing Roles into a Moose class not working

Aloha!
I have a role that I'm busy defining in a Moose class called Authable that is essentially composed into any class that might potentially require some form of authentication in the future; it's a rather simple role, here's the entirety:
package Trello::API::Roles::Authable;
use Moose::Role;
#authentication information
has key => (
is => "rw",
isa => "Str",
);
has token => (
is => "rw",
isa => "Str",
);
1;
For whatever reason, when I attempt to compose it into a class using multiple different statements, i.e.,
with "Trello::API::Roles::Authable";
or
with "Roles::Authable";
I consistently get this same error message: You can only consume roles, Roles::Authable is not a Moose role.
Any idea why this might be happening?
Edit!
Just a side note, I checked the actual source for Moose::Role, and saw this bit:
unless ($meta && $meta->isa('Moose::Meta::Role') ) {
require Moose;
Moose->throw_error( "You can only consume roles, "
. $role->[0]
. " is not a Moose role" );
}
This seems to be where the error is occuring, so it almost seems that for some reason, the role I'm implementing isn't stating that it's a role in the metaclass. Though I could be mistaken! Any help would be appreciated.
Another convenient EDIT!
Bonus: Code context wherein the with routine is called.
package Trello::API::Resource;
use Moose;
use URI::Escape;
use LWP::UserAgent;
with 'Roles::Authable';
which when I do this, it intelligently knows to try and consume Roles/Authable.pm but for whatever reason, it just fails to function!
First of all, I have to agree with Piers that technically, you really should be calling it as with 'Trello::API::Roles::Authable'.
So, you're asking for something that I don't find to be implemented in basic Moose.
I have used the ideas of generic namespace pools before. They are sort of universal namespaces to which you can offer your
semi-anonymous services--without the lock-in of a fixed namespace. I refined my basic idea of the namespace pool with Moose (really MOP) support.
In the Wild West days of Perl, all you would have to do is assign one stash to the symbol for the other, like so:
{ no strict 'refs';
*{$short_pkg_name.'::'} = \*{$full_pkg_name.'::'};
};
And, those two packages were exactly the same things!
But now, we guard our data with lexicals a bit more. And because Class::MOP jealously guards its meta objects in a lexical hash, you have to add something else:
Class::MOP::store_metaclass_by_name(
$short_pkg_name
, Class::MOP::get_metaclass_by_name( $full_pkg_name )
);
Now they are the exact same thing to Perl and to MOP.
Thus you can create packages that are simply a namespace repository for other packages
-- Now with MOP support!
package Namespace::Pool;
use strict;
use warnings;
use Params::Util qw<_POSINT>;
sub import {
shift; # It's just me.
my $full_pkg_name = caller();
Carp::croak( "'$full_pkg_name' is short enough!" )
unless my $pool_name
= shift // [ split /::/, $full_pkg_name ]->[-2]
;
Carp::croak( "'::$pool_name\::' not found in '$full_pkg_name'" )
unless ( _POSINT( my $pos = rindex( $full_pkg_name, "::$pool_name\::" ))
or my $is_short = _POSINT( index( $pool_name, '::' ))
);
my $short_pkg_name
= $is_short ? $poll_name
: substr( $full_pkg_name, $pos + 2 )
;
{ no strict 'refs';
if ( %{$short_pkg_name.'::'} ) {
Carp::croak( "You have already defined $short_pkg_name!" );
}
*{$short_pkg_name.'::'} = \*{$full_pkg_name.'::'};
};
if ( my $meta = Class::MOP::get_metaclass_by_name( $full_pkg_name )) {
Class::MOP::store_metaclass_by_name( $short_pkg_name, $meta );
}
return;
}
Thus in your Role package you can do the following:
package Trello::API::Roles::Authable;
use strict;
use warnings;
use Moose::Role;
use Namespace::Pool 'Roles';
...
And know that it will be available from the namespace of 'Roles'.
In my case I'd simply accidentally named my role 'Test', but there was already an installed module on my system called 'Test' and so Moose thought I wanted to consume that module rather than the new Moose role I'd created. Once I renamed by role to 'Testable' it all worked fine.

Use a single module and get Moose plus several MooseX extensions

Let's say I have a codebase with a bunch of Moose-based classes and I want them all to use a common set of MooseX::* extension modules. But I don't want each Moose-based class to have to start like this:
package My::Class;
use Moose;
use MooseX::Aliases;
use MooseX::HasDefaults::RO;
use MooseX::StrictConstructor;
...
Instead, I want each class to begin like this:
package MyClass;
use My::Moose;
and have it be exactly equivalent to the above.
My first attempt at implementing this was based on the approach used by Mason::Moose (source):
package My::Moose;
use Moose;
use Moose::Exporter;
use MooseX::Aliases();
use MooseX::StrictConstructor();
use MooseX::HasDefaults::RO();
use Moose::Util::MetaRole;
Moose::Exporter->setup_import_methods(also => [ 'Moose' ]);
sub init_meta {
my $class = shift;
my %params = #_;
my $for_class = $params{for_class};
Moose->init_meta(#_);
MooseX::Aliases->init_meta(#_);
MooseX::StrictConstructor->init_meta(#_);
MooseX::HasDefaults::RO->init_meta(#_);
return $for_class->meta();
}
But this approach is not recommended by the folks in the #moose IRC channel on irc.perl.org, and it doesn't always work, depending on the mix of MooseX::* modules. For example, trying to use the My::Moose class above to make My::Class like this:
package My::Class;
use My::Moose;
has foo => (isa => 'Str');
Results in the following error when the class is loaded:
Attribute (foo) of class My::Class has no associated methods (did you mean to provide an "is" argument?)
at /usr/local/lib/perl5/site_perl/5.12.1/darwin-2level/Moose/Meta/Attribute.pm line 1020.
Moose::Meta::Attribute::_check_associated_methods('Moose::Meta::Class::__ANON__::SERIAL::2=HASH(0x100bd6f00)') called at /usr/local/lib/perl5/site_perl/5.12.1/darwin-2level/Moose/Meta/Class.pm line 573
Moose::Meta::Class::add_attribute('Moose::Meta::Class::__ANON__::SERIAL::1=HASH(0x100be2f10)', 'foo', 'isa', 'Str', 'definition_context', 'HASH(0x100bd2eb8)') called at /usr/local/lib/perl5/site_perl/5.12.1/darwin-2level/Moose.pm line 79
Moose::has('Moose::Meta::Class::__ANON__::SERIAL::1=HASH(0x100be2f10)', 'foo', 'isa', 'Str') called at /usr/local/lib/perl5/site_perl/5.12.1/darwin-2level/Moose/Exporter.pm line 370
Moose::has('foo', 'isa', 'Str') called at lib/My/Class.pm line 5
require My/Class.pm called at t.pl line 1
main::BEGIN() called at lib/My/Class.pm line 0
eval {...} called at lib/My/Class.pm line 0
The MooseX::HasDefaults::RO should be preventing this error, but it's apparently not being called upon to do its job. Commenting out the MooseX::Aliases->init_meta(#_); line "fixes" the problem, but a) that's one of the modules I want to use, and b) that just further emphasizes the wrongness of this solution. (In particular, init_meta() should only be called once.)
So, I'm open to suggestions, totally ignoring my failed attempt to implement this. Any strategy is welcome as long as if gives the results described at the start of this question.
Based on #Ether's answer, I now have the following (which also doesn't work):
package My::Moose;
use Moose();
use Moose::Exporter;
use MooseX::Aliases();
use MooseX::StrictConstructor();
use MooseX::HasDefaults::RO();
my %class_metaroles = (
class => [
'MooseX::StrictConstructor::Trait::Class',
],
attribute => [
'MooseX::Aliases::Meta::Trait::Attribute',
'MooseX::HasDefaults::Meta::IsRO',
],
);
my %role_metaroles = (
role =>
[ 'MooseX::Aliases::Meta::Trait::Role' ],
application_to_class =>
[ 'MooseX::Aliases::Meta::Trait::Role::ApplicationToClass' ],
application_to_role =>
[ 'MooseX::Aliases::Meta::Trait::Role::ApplicationToRole' ],
);
if (Moose->VERSION >= 1.9900) {
push(#{$class_metaroles{class}},
'MooseX::Aliases::Meta::Trait::Class');
push(#{$role_metaroles{applied_attribute}},
'MooseX::Aliases::Meta::Trait::Attribute',
'MooseX::HasDefaults::Meta::IsRO');
}
else {
push(#{$class_metaroles{constructor}},
'MooseX::StrictConstructor::Trait::Method::Constructor',
'MooseX::Aliases::Meta::Trait::Constructor');
}
*alias = \&MooseX::Aliases::alias;
Moose::Exporter->setup_import_methods(
also => [ 'Moose' ],
with_meta => ['alias'],
class_metaroles => \%class_metaroles,
role_metaroles => \%role_metaroles,
);
With a sample class like this:
package My::Class;
use My::Moose;
has foo => (isa => 'Str');
I get this error:
Attribute (foo) of class My::Class has no associated methods (did you mean to provide an "is" argument?) at ...
With a sample class like this:
package My::Class;
use My::Moose;
has foo => (isa => 'Str', alias => 'bar');
I get this error:
Found unknown argument(s) passed to 'foo' attribute constructor in 'Moose::Meta::Attribute': alias at ...
I might get raked over the coals for this, but when in doubt, lie :)
package MyMoose;
use strict;
use warnings;
use Carp 'confess';
sub import {
my $caller = caller;
eval <<"END" or confess("Loading MyMoose failed: $#");
package $caller;
use Moose;
use MooseX::StrictConstructor;
use MooseX::FollowPBP;
1;
END
}
1;
By doing that, you're evaling the use statements into the calling package. In other words, you're lying to them about what class they are used in.
And here you declare your person:
package MyPerson;
use MyMoose;
has first_name => ( is => 'ro', required => 1 );
has last_name => ( is => 'rw', required => 1 );
1;
And tests!
use lib 'lib';
use MyPerson;
use Test::Most;
throws_ok { MyPerson->new( first_name => 'Bob' ) }
qr/\QAttribute (last_name) is required/,
'Required attributes should be required';
throws_ok {
MyPerson->new(
first_name => 'Billy',
last_name => 'Bob',
what => '?',
);
}
qr/\Qunknown attribute(s) init_arg passed to the constructor: what/,
'... and unknown keys should throw an error';
my $person;
lives_ok { $person = MyPerson->new( first_name => 'Billy', last_name => 'Bob' ) }
'Calling the constructor with valid arguments should succeed';
isa_ok $person, 'MyPerson';
can_ok $person, qw/get_first_name get_last_name set_last_name/;
ok !$person->can("set_first_name"),
'... but we should not be able to set the first name';
done_testing;
And the test results:
ok 1 - Required attributes should be required
ok 2 - ... and unknown keys should throw an error
ok 3 - Calling the constructor with valid arguments should succeed
ok 4 - The object isa MyPerson
ok 5 - MyPerson->can(...)
ok 6 - ... but we should not be able to set the first name
1..6
Let's keep this our little secret, shall we? :)
As discussed, you shouldn't be calling other extensions' init_meta methods directly. Instead, you should essentially inline those extensions' init_meta methods: combine what all those methods do, into your own init_meta. This is fragile because now you are tying your module to other modules' innards, which are subject to change at any time.
e.g. to combine MooseX::HasDefaults::IsRO, MooseX::StrictConstructor and MooseX::Aliases, you'd do something like this (warning: untested) (now tested!):
package Mooseish;
use Moose ();
use Moose::Exporter;
use MooseX::StrictConstructor ();
use MooseX::Aliases ();
my %class_metaroles = (
class => ['MooseX::StrictConstructor::Trait::Class'],
attribute => [
'MooseX::Aliases::Meta::Trait::Attribute',
'MooseX::HasDefaults::Meta::IsRO',
],
);
my %role_metaroles = (
role =>
['MooseX::Aliases::Meta::Trait::Role'],
application_to_class =>
['MooseX::Aliases::Meta::Trait::Role::ApplicationToClass'],
application_to_role =>
['MooseX::Aliases::Meta::Trait::Role::ApplicationToRole'],
);
if (Moose->VERSION >= 1.9900) {
push #{$class_metaroles{class}}, 'MooseX::Aliases::Meta::Trait::Class';
push #{$role_metaroles{applied_attribute}}, 'MooseX::Aliases::Meta::Trait::Attribute';
}
else {
push #{$class_metaroles{constructor}},
'MooseX::StrictConstructor::Trait::Method::Constructor',
'MooseX::Aliases::Meta::Trait::Constructor';
}
*alias = \&MooseX::Aliases::alias;
Moose::Exporter->setup_import_methods(
also => ['Moose'],
with_meta => ['alias'],
class_metaroles => \%class_metaroles,
role_metaroles => \%role_metaroles,
);
1;
This can be tested with this class and tests:
package MyObject;
use Mooseish;
sub foo { 1 }
has this => (
isa => 'Str',
alias => 'that',
);
1;
use strict;
use warnings;
use MyObject;
use Test::More;
use Test::Fatal;
like(
exception { MyObject->new(does_not_exist => 1) },
qr/unknown attribute.*does_not_exist/,
'strict constructor behaviour is present',
);
can_ok('MyObject', qw(alias this that has with foo));
my $obj = MyObject->new(this => 'thing');
is($obj->that, 'thing', 'can access attribute by its aliased name');
like(
exception { $obj->this('new value') },
qr/Cannot assign a value to a read-only accessor/,
'attribute defaults to read-only',
);
done_testing;
Which prints:
ok 1 - strict constructor behaviour is present
ok 2 - MyObject->can(...)
ok 3 - can access attribute by its aliased name
ok 4 - attribute defaults to read-only
1..4
So long as the MooseX you want to use are all well-behaved and use Moose::Exporter, you can use Moose::Exporter to create a package that will behave like Moose for you:
package MyMoose;
use strict;
use warnings;
use Moose::Exporter;
use MooseX::One ();
use MooseX::Two ();
Moose::Exporter->setup_import_methods(
also => [ qw{ Moose MooseX::One MooseX::Two } ],
);
1;
Note that in also we're using the name of the package that the Moose extension using Moose::Exporter (generally the main package from the extension), and NOT using any of the trait application bits. Moose::Exporter handles that all behind the scenes.
The advantage here? Everything works as expected, all sugar from Moose and extensions is installed and can be removed via 'no MyMoose;'.
I should point out here that some extensions do not play well with others, usually due to their not anticipating that they'll be required to coexist in harmony with others. Luckily, these are becoming increasingly uncommon.
For a larger scale example, check out Reindeer on the CPAN, which collects several extensions and integrates them together in a coherent, consistent fashion.

How to extend Class::Multimethods::Pure to recognise Moose Roles?

I need multemethod dispatch with Moose objects. I'm doing this with Class::Multimethods::Pure. I chose this instead of MooseX::MultiMethods because it depends on MooseX::Method::Signatures which can't install on my system because it fails its tests. I don't mind if you have an alternative approach to suggest.
The following works fine with types and subtypes:
package Foo::Type;
use Moose;
package Foo::SubType;
use Moose;
extends 'Foo::Type';
package main;
use Class::Multimethods::Pure;
multi hello => ('Foo::Type') => sub {
my ( $foo ) = #_;
print $foo;
};
hello( Foo::SubType->new );
But the scenario I now need to handle is where the declared type is actually a Moose Role:
package Foo::Role;
use Moose::Role;
package Foo::Type;
use Moose;
with 'Foo::Role';
package main;
use Class::Multimethods::Pure;
multi hello => ('Foo') => sub {
my ( $foo ) = #_;
print $foo;
};
hello( Foo::Type->new );
But this can't recognise the role:
No method found for args (Foo::Type=HASH(0x22ac854))
The documentation says it can be extended in various ways, including adding Perl 6-ish roles. But it's a little sketchy for me and I'm looking for a more detailed example. Has anyone tried this?
My solution was to convert the roles to abstract base classes using MooseX::ABC. In this way, they could be recognised as a class type.
On a side note, I managed to get MooseX::MultiMethods working on another system. It does work with roles, but it can't figure out which to use if we define a multimethod that takes the class and another multimethod that takes the role. Incidentally, MooseX::ABC resolved this issue also since it gave me a hierarchical structure which the roles did not really have.
package Foo::Role;
use Moose::Role;
package Foo::Type;
use Moose;
with 'Foo::Role';
package Merger;
use Moose;
use MooseX::MultiMethods;
multi method hello (Foo::Role $foo) {
print 'Foo::Role: '.$foo;
}
multi method hello (Foo::Type $foo) {
print 'Foo::Type: '.$foo;
}
package main;
my $merger = Merger->new;
my $f = Foo::Type->new;
$merger->hello( $f );
# Ambiguous match for multi method hello: (Foo::Role $foo), (Foo::Type $foo)
# with value [ Merger{ }, Foo::Type{ } ]

Moose::Error::Croak error reporting not from perspective of caller

I just recently started out on Moose and its a great OO framework not only to use but also to learn new OO concepts. One of the things I wanted to do was to do error reporting from perspective of caller during object creation. I saw that Moose has the module Moose::Error::Croak which tells Moose to override the default error reporting by croak call. I used it but it did not seem to help
Moose code - Foo.pm
package Foo;
use metaclass (
metaclass => 'Moose::Meta::Class',
error_class => 'Moose::Error::Croak',
);
use Moose;
has 'attr1' => (
is => 'rw',
isa => 'Str',
required => '1',
);
no Moose;
1;
Moose code - fooser.pl
#!/usr/bin/perl
use strict;
use warnings;
use Foo;
my $foobj = Foo->new();
This fails with error:
Attribute (attr1) is required at /usr/local/lib/perl/5.8.8/Class/MOP/Class.pm line 364
which is terse than the actual stack trace if Moose::Error::Croak is not used. But it does not report it from perspective of caller. If this were a Perl 5 OO code and I had Foo.pm as:
package Foo;
use strict;
use warnings;
use Carp;
sub new {
my ($class, %args) = #_;
my $self = {};
if (! exists $args{'attr1'}) {
croak "ERR: did not provide attr1";
}
$self->{'attr1'} = $args{attr1};
bless $self, $class;
return $self;
}
1;
And if fooser.pl was executed I would have got the error:
"ERR: did not provide attr1 at fooser.pl line 6"
which is from the perspective of the caller as it points to line no. 6 of fooser.pl rather than MOP.pm's line no. 364.
How can I do this in Moose? Or am I misunderstanding something here?
One option is to use MooseX::Constructor::AllErrors.
This module collects constructor errors and prints them all together (default behaviour is to die as soon as the first error is encountered). It has a side-effect of printing from the caller's perspective.
Replace your use metaclass with use MooseX::Constructor::AllErrors; and the error becomes:
Attribute (attr1) is required at ./fooser.pl line 5
However, if you call Foo->new from inside another module (e.g. Bar) and call Bar->new in your script, the error would be more like:
Attribute (attr1) is required at Bar.pm line 8
...so it's not a complete solution.