Creating attribute defaults by calling a wrapped object - perl

I have WrapperClass object that has an InnerClass object as an attribute. The InnerClass object has a weight attribute. My WrapperClass object also has a weight attribute and I want its default value to be whatever the value of the InnerClass object's weight attribute is.
#!/usr/bin/perl
package InnerClass;
use Moose;
has 'weight' => (
is => 'rw',
);
package WrapperClass;
use Moose;
has 'wrapped' => (
is => 'rw',
lazy => 1,
default => sub {InnerClass->new(weight => 1)},
);
has 'weight' => (
is => 'rw',
default => sub {
my $self = shift;
$self->wrapped->weight()
},
lazy => 1,
);
The code above works, but in reality InnerClass has many attributes which WrapperClass needs to do the same thing for. Ideally I would do something like this when I'm writing WrapperClass:
use Moose;
has 'wrapped' => (
is => 'rw',
);
my #getDefaultsFromWrappers
= qw(weight height mass x y z label); # etc ...
foreach my $attr (#getDefaultsFromWrappers) {
has $attr => (
is => 'rw',
default => sub {
# Somehow tell the default which attribute
# it needs to call from wrapped object?
my $self = shift;
$self->wrapped->???()
},
lazy => 1,
);
}
However, there is no way of passing an argument to a default or builder to tell it which attribute it is building. I've considered using caller but this seems like a hack.
Does anyone know how I could accomplish this style of attribute declaration or is it a case of declaring each attribute and its default separately?

You can use $attr where your question marks are because it is still in scope when you declare the attributes.
foreach my $attr (#getDefaultsFromWrappers) {
has $attr => (
is => 'rw',
default => sub { shift->wrapped->$attr() },
lazy => 1,
);
}
The following is a possible alternative, which you might want to use if your attribute declarations are not uniform:
has weight => (
is => 'rw',
isa => 'Num',
default => _build_default_sub('weight'),
lazy => 1,
);
has label => (
is => 'rw',
isa => 'Str',
default => _build_default_sub('label'),
lazy => 1,
);
sub _build_default_sub {
my ($attr) = #_;
return sub { shift->wrapped->$attr };
}

This may be better handled by method delegation and default values in the inner object.
With these, the example you gave can be better written as:
#!/usr/bin/perl
use strict;
use warnings;
package InnerClass;
use Moose;
has weight => (
is => 'rw',
default => 1,
);
package WrapperClass;
use Moose;
has wrapped => (
is => 'rw',
isa => 'InnerClass',
lazy => 1,
default => sub { InnerClass->new },
handles => [ 'weight' ],
);
package main;
my $foo = WrapperClass->new;
print $foo->weight;
Any additional defaults would be added as default on the InnerClass, and within the WrapperClass, add to wrapped 'handles' array ref to indicate that it should be delegated to that object.
If don't want the defaults to be applied to all instances of InnerClass, then you can remove the default from there, specify all attributes required (to give better error detection), and specify all attributes in the default constructor.

Related

Attribute is => 'Maybe[SomeSubtype]' returns Attribute () does not pass type constraint

I've created subtype Birth_d with coercion as shown below, and I'm trying to use it in combination with the built-in Maybe type, per Moose::Manual::Types.
I'm getting the error You cannot coerce an attribute (birth_d) unless its type (Maybe[Birth_d]) has a coercion. Here's complete test code:
package Student;
use Moose;
use Moose::Util::TypeConstraints;
use DateTime::Format::MySQL;
class_type 'Birth_d', { class => 'DateTime' };
coerce 'Birth_d',
from 'Str',
via { DateTime::Format::MySQL->parse_date( $_ ) };
has 'name' => (
isa => 'Str',
is => 'ro',
);
has 'birth_d' => (
isa => 'Maybe[Birth_d]', # This works: isa => 'Birth_d'
coerce => 1,
is => 'ro',
);
package main;
use Test::More;
my $student = Student->new(
name => 'Johnnie Appleseed',
birth_d => '2015-01-01'
);
is ( $student->birth_d->ymd(), '2015-01-01' );
my $student2 = Student->new(
name => 'Foo Bar',
birth_d => undef
);
is( $student2->birth_d, undef );
Replacing isa => 'Maybe[Birth_d]' with isa => 'Birth_d' works, but is not what is needed. I need to make the birth_d optional, and if not supplied, should be undef.
I should add, I tried using MooseX::Types to tuck this Birth_d type away in a separate place, but found its cavalier use of barewords a bit unorthodox, so I slowly backed away. I'm open to reconsidering it, if it makes sense to do so.
Moose does not do any chaining of coercions, in other words you have to tell it explicitly how to convert to a Maybe[Birth_d].
You can do this by reusing the existing coercion to Birth_d:
package Student;
use Moose;
use Moose::Util::TypeConstraints;
use DateTime::Format::MySQL;
# save the Moose::Meta::TypeConstraint object
# you can also get it with find_type_constraint('Birth_d')
my $birth_d = class_type 'Birth_d', { class => 'DateTime' };
coerce 'Birth_d',
from 'Str',
via { DateTime::Format::MySQL->parse_date( $_ ) };
subtype 'MaybeBirth_d',
as 'Maybe[Birth_d]';
coerce 'Maybe[Birth_d]',
from 'Str|Undef',
via { $birth_d->coerce($_) };
has 'name' => (
isa => 'Str',
is => 'ro',
);
has 'birth_d' => (
isa => 'Maybe[Birth_d]',
coerce => 1,
is => 'ro',
predicate => 'has_birth_d', # as per your comment
);
package main;
use Test::More;
my $student = Student->new(
name => 'Johnnie Appleseed',
birth_d => '2015-01-01'
);
is ( $student->birth_d->ymd(), '2015-01-01' );
my $student2 = Student->new(
name => 'Foo Bar',
birth_d => undef
);
is( $student2->birth_d, undef );
ok( $student2->has_birth_d );
done_testing;
I would find it more useful to not have a Maybe[Birth_d] type, but simply declare the attribute with the Birth_d type, and no "required" set.
That way, if a valid String is passed in, it will be accepted, an invalid String will lead to an error, and nothing just does not need to be passed in.
However, you can coerce to a maybe type:
subtype 'MaybeBirth_d',
as maybe_type(class_type('DateTime'));
coerce 'MaybeBirth_d',
from 'Str',
via { DateTime::Format::MySQL->parse_date( $_ ) };
has 'birth_d' => (
isa => 'MaybeBirth_d',
coerce => 1,
is => 'ro',
);
I just do not see the value of being able to pass in undef for a birthdate - how is that better than not setting it?
I would also like to suggest using no Moose::Util::TypeConstraints; and no Moose; at the end of your package, or namespace::autoclean; at the beginning, as well as __PACKAGE__->meta->make_immutable; at the end of your Student class.

How can one attribute's default sub access another attribute with Moose?

###############################################################################
# Attributes
###############################################################################
has 'primary_cluster' => (
is => 'ro',
isa => 'Str',
required => TRUE,
);
has 'secondary_cluster' => (
is => 'ro',
isa => 'Str',
required => FALSE,
default => sub {$_[0]->primary_cluster},
);
has '_connection' => (
is => 'ro',
required => FALSE,
init_arg => undef,
default => sub {
Core::mConnection->new(
primary_cluster => $_[0]->primary_cluster,
secondary_cluster => $_[0]->secondary_cluster,
);
},
);
I'm trying to have a private attribute _connection that uses other attributes to create a mConnection object. The problem I'm running into is that inside the default subroutine of _connection, $_[0]->primary_cluster is always undef. Is there any way to guarantee order to the attribute creation or is there a better way to go about doing this?
I do not want this attribute to be lazy; I need it to be created when the object is constructed.
The object is still being constructed! Delay your attribute's initialization until after it's constructed. the following delays its initialization until it's used:
lazy => 1
You could also use a BUILD method instead of a default.
sub BUILD {
my $self = shift;
$self->_connection(
Core::mConnection->new(
primary_cluster => $self->primary_cluster,
secondary_cluster => $self->secondary_cluster,
)
);
}
Of course, you'll need to make the attribute writable first.

Why isn't Moose Role exclude excluding particular role attributes?

I have a Moose::Role that has (among other things):
package My::Role;
use strict;
use warnings;
use Moose::Role;
use MooseX::ClassAttribute;
class_has table => (
is => 'ro'
isa => 'Str',
lazy => 1,
);
has id => (
is => 'ro',
isa => 'Int',
predicate => 'has_id',
writer => '_id',
required => 0,
);
has other => (
is => 'rw',
isa => 'Int',
);
...
1;
Then, in a module that consumes that Role,
package Some::Module;
with 'My::Role' => {
-excludes => [qw( id table )]
};
has module_id => (
is => 'ro',
isa => 'Int',
);
...
1;
Then, in a script I'm instantiating an instance of Some::Module:
my $some_module = Some::Module->new({ other => 3 });
and I'm able to call
$some_module->id; # I'd expect this to die but returns undef.
However, I'm unable to call
$some_module->table; # this dies as I'd expect
As I'd expect calling $some_module->table causes the script to cease. Calling
$some_module->id doesn't.
When I use Data::Dumper to dump out the attribute list of the $some_module meta
class it show that the id attribute is defined but the table attribute is not.
Does anyone know why the 'id' attribute defined in the Role would not be excluded
from the meta class but the 'table' class_attribute would? The problem being, as
described above, is that users of Some::Module can call id() when they should be
required to call module_id().
Furthermore, when dumping $some_module object, the 'id' doesn't show up in the dump.
Edit:
Here's a sample that illustrates the problem. I've defined a role
that implements an id then I'm consuming the role in the package My::Product.
I'm excluding the id when consuming it however. When I print the attribute
from the meta object it shows that it is in fact there. I was under the impression
that excluding the id from a role when consuming it wouldn't allow it to be called.
I'd expect that it would not only be NOT in the meta object but also to die on
an attempt to call it.
#!/usr/bin/perl
package My::Model;
use Moose::Role;
use MooseX::ClassAttribute;
class_has first_name => (
is => 'rw',
isa => 'Str',
);
class_has last_name => (
is => 'rw',
isa => 'Str',
);
has id => (
is => 'rw',
isa => 'Int',
predicate => 'has_id',
writer => '_id',
required => 0,
);
1;
package My::Product;
use Moose;
use Class::MOP::Class;
use Data::Dumper;
with 'My::Model' => { -excludes => [ qw( first_name id ) ], };
has count => (
is => 'rw',
isa => 'Int',
);
has product_id => (
is => 'ro',
isa => 'Int',
required => 0,
predicate => 'has_product_id'
);
sub create_classes {
my #list = ();
foreach my $subclass (qw( one two three )) {
Class::MOP::Class->create(
"My::Product::"
. $subclass => (
superclasses => ["My::Product"],
)
);
push #list, "My::Product::$subclass";
}
return \#list;
}
__PACKAGE__->meta()->make_immutable;
1;
package main;
use strict;
use warnings;
use Data::Dumper;
my $product = My::Product->new();
my $classes = $product->create_classes();
my #class_list;
foreach my $class ( #{ $classes } ) {
my $temp = $class->new( { count => time } );
$temp->first_name('Don');
$temp->last_name('MouseCop');
push #class_list, $temp;
}
warn "what is the id for the first obj => " . $class_list[0]->id ;
warn "what is the first_name for the first obj => " . $class_list[0]->first_name ;
warn "what is the last_name for the first obj => " . $class_list[0]->last_name ;
warn "\nAttribute list:\n";
foreach my $attr ( $class_list[2]->meta->get_all_attributes ) {
warn "name => " . $attr->name;
# warn Dumper( $attr );
}
Edit 2:
Upon dumping the $attr I am seeing that first_name and id are in the method_exclusions.
'role_applications' => [
bless( {
'class' => $VAR1->{'associated_class'},
'role' => $VAR1->{'associated_class'}{'roles'}[0],
'method_aliases' => {},
'method_exclusions' => [
'first_name',
'id'
]
}, 'Moose::Meta::Class::__ANON__::SERIAL::8' )
]
I have no idea how the innards of this works but I believe this is to do with the fact that the two methods you are excluding are attribute methods. The only relevant article I can find is here, where it says:
A roles attributes are similar to those of a class, except
that they are not actually applied. This means that methods that are
generated by an attributes accessor will not be generated in the role,
but only created once the role is applied to a class.
Therefore I'm guessing the problem is that when your classes are being constructed, the role is applied (and the methods are excluded), but after that the role's attributes are applied and the accessor methods (including id and first_name) are constructed.
To demonstrate, change the id attribute to _id, give it a different writer and create an id sub to access it:
# This replaces id
has _id => (
is => 'rw',
isa => 'Int',
writer => 'set_id',
required => 0,
);
sub id {
my $self = shift;
return $self->_id();
}
The script will now die with an exception:
Can't locate object method "id" via package "My::Product::one" at ./module.pm line 89.

Moose - modify default attribute position in object hash

I'm dealing with some non-Moose legacy code and I want to extend it with a Moose class. This is a simplification of the legacy code:
package My::Legacy;
sub create {
my ($class, $args) = #_;
my $fields = { _fields => {}};
foreach my $key ( keys %$args ) {
$fields->{_fields}->{$key} = $args->{$key}
}
bless $fields, $class;
}
1;
The My::Legacy class handles all the CRUD operations, caching and other stuff. All the operations are performed on the values contained in the internal _field hash, so, for example, if you want to update a value it has to be in the _field hash. The My::Legacy class provides setter/getter for this.
The My::Legacy is subclassed by several classes that need the "sugar" provided by it: My::Legacy::ObjectA, My::Legacy::ObjectB, etc.
I need to add a further one and I want to extend it using Moose. The problem is that every time I will set an attribute, I will have to keep its value in sync in the internal _fields hash, so for example if I have...
package My::Legacy::MyMooseObj;
use Moose;
use MooseX::NonMoose;
use namespace::autoclean;
has _fields => (
isa => HashRef,
is => 'rw',
default => sub { {} },
);
has attr_a => (
isa => 'Int',
is => 'ro',
);
has attr_b => (
isa => 'Str',
is => 'ro',
);
__PACKAGE__->meta->make_immutable;
...and I do:
my $MyMooseObj = My::Legacy::MyMooseObj->new();
$MyMooseObj->attr_a(15);
...I want attr_a to be set in _fields as well, so if I dump out the object it will look like:
bless( {
'_fields' => {
'attr_a' => 15,
},
'attr_a' => 15,
}, 'My::Legacy::MyMooseObj' );
The way I come up to achieve this is add a trigger to each attribute in order to write its value in the _fields hash every time is set:
has attr_b => (
isa => 'Str',
is => 'ro',
trigger => sub { # Write in the _fields attribute attr_b value! },
);
This is a bit annoying because every time I add a new attribute I have to make sure it has the trigger set :/
Can you think of a better way of doing it ? Is there any way of telling Moose to read/write the attribute not in the "root" of the object hash by default (so in my case to read/write attributes from _fields) ?
This more or less does what you want...
use strict;
use warnings;
{
package My::Legacy::MyMooseObj;
use Moose;
use MooseX::FunkyAttributes;
use namespace::autoclean;
has _fields => (
isa => 'HashRef',
is => 'rw',
default => sub { {} },
lazy => 1, # you want this, for the rest to work
);
has attr_a => (
isa => 'Int',
is => 'ro',
traits => [ FunkyAttribute ],
custom_get => sub { $_->_fields->{attr_a} },
custom_set => sub { $_->_fields->{attr_a} = $_[-1] },
custom_has => sub { exists($_->_fields->{attr_a}) },
);
has attr_b => (
isa => 'Str',
is => 'rw',
traits => [ FunkyAttribute ],
custom_get => sub { $_->_fields->{attr_b} },
custom_set => sub { $_->_fields->{attr_b} = $_[-1] },
custom_has => sub { exists($_->_fields->{attr_b}) },
);
}
my $obj = My::Legacy::MyMooseObj->new( attr_a => 42 );
$obj->attr_b(666);
print $obj->dump;
With the current version of MooseX::FunkyAttributes, the constructor will not work correctly if you do the whole __PACKAGE__->meta->make_immutable though. :-(
Delving slightly deeper into metaprogramming...
use strict;
use warnings;
{
package My::Legacy::MyMooseObj;
use Moose;
use MooseX::FunkyAttributes;
use namespace::autoclean;
has _fields => (
isa => 'HashRef',
is => 'rw',
default => sub { {} },
lazy => 1, # you want this, for the rest to work
);
sub funky_has {
my ($attr, %opts) = #_;
has $attr => (
is => 'ro',
traits => [ FunkyAttribute ],
custom_get => sub { $_->_fields->{$attr} },
custom_set => sub { $_->_fields->{$attr} = $_[-1] },
custom_has => sub { exists($_->_fields->{$attr}) },
%opts,
);
}
funky_has attr_a => (isa => 'Int');
funky_has attr_b => (isa => 'Str', is => 'rw');
}
my $obj = My::Legacy::MyMooseObj->new( attr_a => 42 );
$obj->attr_b(666);
print $obj->dump;

How do I define default attribute property values in Moose?

As the title suggests, I'd like to be able to do something like this in my class:
use MooseX::Declare;
class MyClass {
default_attribute_propeties(
is => 'ro',
lazy => 1,
required => 1,
);
has [qw( some standard props )] => ();
has 'override_default_props' => (
is => 'rw',
required => 0,
...
);
...
}
That is, define some default property values that will apply to all attribute definitions unless overridden.
It sounds like you want to write some custom attribute declarations, that provide some default options. This is covered in Moose::Cookbook::Extending::Recipe1, e.g.:
package MyApp::Mooseish;
use Moose ();
use Moose::Exporter;
Moose::Exporter->setup_import_methods(
install => [ qw(import unimport init_meta) ],
with_meta => ['has_table'],
also => 'Moose',
);
sub has_table
{
my ($meta, $name, %config) = #_;
$meta->add_attribute(
$name,
# overridable defaults.
is => 'rw',
isa => 'Value', # any defined non-reference; hopefully the caller
# passed their own type, which will override
# this one.
# other options you may wish to supply, or calculate based on
# other arguments passed to this function...
%config,
);
}
And then in your class:
package MyApp::SomeObject;
use MyApp::Moosish;
has_table => (
# any normal 'has' options;
# will override the defaults.
);
# remaining class definition as normal.