Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I provide an alternate init arg for an attribute in Moose?

Tags:

perl

moose

I of course know that I can rename the init arg for an attribute by setting init_arg (e.g)

package Test {
    use Moose;
    has attr => (
       is => 'ro',
       isa => 'Str',
       init_arg => 'attribute'
    );
}

which would allow me to

Test->new({ attribute => 'foo' });

but not

Test->new({ attr => 'foo' });

at the same time

MooseX::Aliases actually has this behavior, but creating an alias also creates accessors. I'm currently trying to understand the code in that module to see if I can't determine how it does it, so that I can replicate said functionality (in a way I understand). If someone could explain how to do it here with an example that'd be great.

update it appears that MX::Aliases is doing this by way of replacing what's actually passed to the constructor in an around initialize_instance_slot but I'm still not sure how that's actually getting called, because in my test code my around isn't actually getting executed.

update munging in BUILDARGS isn't really an option because what I'm trying to do allow setting of the accessor via the name of the label I'm adding to the attribute via Meta Recipe3. You might say I'm doing

has attr => (
   is => 'ro',
   isa => 'Str',
   alt_init_arg => 'attribute'
);

update

here's what I've managed to work out with what I'm trying to do so far.

use 5.014;
use warnings;

package MooseX::Meta::Attribute::Trait::OtherName {
    use Moose::Role;
    use Carp;

    has other_name => (
        isa       => 'Str',
        predicate => 'has_other_name',
        required  => 1,
        is        => 'ro',
    );

    around initialize_instance_slot => sub {
        my $orig = shift;
        my $self = shift;

        my ( $meta_instance, $instance, $params ) = @_;

        confess 'actually calling this code';

        return $self->$orig(@_)
            unless $self->has_other_name && $self->has_init_arg;

        if ( $self->has_other_name ) {
            $params->{ $self->init_arg }
                = delete $params->{ $self->other_name };
        }
    };
}

package Moose::Meta::Attribute::Custom::Trait::OtherName {
    sub register_implementation { 'MooseX::Meta::Attribute::Trait::OtherName' }
}

package Message {
    use Moose;
#   use MooseX::StrictConstructor;

    has attr => (
        traits    => [ 'OtherName' ],
        is        => 'ro',
        isa       => 'Str',
        other_name => 'Attr',
    );

    __PACKAGE__->meta->make_immutable;
}

package Client {
    use Moose;

    sub serialize {
        my ( $self, $message ) = @_;

        confess 'no message' unless defined $message;

        my %h;
        foreach my $attr ( $message->meta->get_all_attributes ) {
            if (
                    $attr->does('MooseX::Meta::Attribute::Trait::OtherName')
                    && $attr->has_other_name
                ) {
                $h{$attr->other_name} = $attr->get_value( $message );
            }
        }
        return \%h;
    }
    __PACKAGE__->meta->make_immutable;
}

my $message = Message->new( Attr => 'foo' );

my $ua = Client->new;

my %h = %{ $ua->serialize( $message )};

use Data::Dumper::Concise;

say Dumper \%h

problem is that my around block is never being run and I'm not sure why, maybe I'm wrapping it in the wrong place or something.

like image 622
xenoterracide Avatar asked Jan 17 '23 00:01

xenoterracide


2 Answers

MooseX::Aliases has several moving parts to make this functionality happen, that's because the behavior needs to be applied to several different places in the MOP. Your code here looks very close to the code in MooseX::Aliases's Trait attribute.

I suspect the reason your code isn't being called is due to something going wrong when you try to register your trait. MooseX::Aliases uses Moose::Util::meta_attribute_alias rather than the old fashioned way you're using here. Try replacing your Moose::Meta::Attribute::Custom::Trait::OtherName section with a call to Moose::Util::meta_attribute_alias 'OtherName'; inside your Role.

Second the code you have here won't work for immutable classes. You'll need to add a second trait to handle those because the immutability code is handled by the class's metaclass and not the attribute's metaclass. You'll need to add some more traits to handle attributes in Roles as well I think. Then you'll need to wire up an Moose::Exporter to make sure that all the traits are applied properly when everything is compiled.

I've gotten a simple version of this working up through immutable. This code is also on github.

First the Attribute trait:

package MooseX::AltInitArg::Meta::Trait::Attribute;
use Moose::Role;
use namespace::autoclean;
Moose::Util::meta_attribute_alias 'AltInitArg';


has alt_init_arg => (
    is         => 'ro',
    isa        => 'Str',
    predicate  => 'has_alt_init_arg',
);


around initialize_instance_slot => sub {
    my $orig = shift;
    my $self = shift;
    my ($meta_instance, $instance, $params) = @_;

    return $self->$orig(@_)
        # don't run if we haven't set any alt_init_args
        # don't run if init_arg is explicitly undef
        unless $self->has_alt_init_arg && $self->has_init_arg;

    if (my @alternates = grep { exists $params->{$_} } ($self->alt_init_arg)) {
        if (exists $params->{ $self->init_arg }) {
            push @alternates, $self->init_arg;
        }

        $self->associated_class->throw_error(
            'Conflicting init_args: (' . join(', ', @alternates) . ')'
        ) if @alternates > 1;

        $params->{ $self->init_arg } = delete $params->{ $alternates[0] };
    }
    $self->$orig(@_);
};

1;
__END__

Next the Class trait.

package MooseX::AltInitArg::Meta::Trait::Class;
use Moose::Role;
use namespace::autoclean;

around _inline_slot_initializer => sub {
    my $orig = shift;
    my $self = shift;
    my ($attr, $index) = @_;

    my @orig_source = $self->$orig(@_);
    return @orig_source
        # only run on aliased attributes
        unless $attr->meta->can('does_role')
            && $attr->meta->does_role('MooseX::AltInitArg::Meta::Trait::Attribute');
    return @orig_source
        # don't run if we haven't set any aliases
        # don't run if init_arg is explicitly undef
        unless $attr->has_alt_init_arg && $attr->has_init_arg;

    my $init_arg = $attr->init_arg;

    return (
        'if (my @aliases = grep { exists $params->{$_} } (qw('
          . $attr->alt_init_arg . '))) {',
            'if (exists $params->{' . $init_arg . '}) {',
                'push @aliases, \'' . $init_arg . '\';',
            '}',
            'if (@aliases > 1) {',
                $self->_inline_throw_error(
                    '"Conflicting init_args: (" . join(", ", @aliases) . ")"',
                ) . ';',
            '}',
            '$params->{' . $init_arg . '} = delete $params->{$aliases[0]};',
        '}',
        @orig_source,
    );
};
1;
__END__

Finally the Moose::Exporter glue.

package MooseX::AltInitArg;
use Moose();

use Moose::Exporter;
use MooseX::AltInitArg::Meta::Trait::Attribute;

Moose::Exporter->setup_import_methods(
    class_metaroles => { class => ['MooseX::AltInitArg::Meta::Trait::Class'] }
);

1;
__END__

An example of how this is used then:

package MyApp;
use 5.10.1;
use Moose;
use MooseX::AltInitArg;

has foo => (
    is            => 'ro',
    traits        => ['AltInitArg'],
    alt_init_arg => 'bar',
);


my $obj = MyApp->new( bar => 'bar' );
say $obj->foo; # prints bar

Meta-Programming in Moose is incredibly powerful, but because there are a lot of moving parts (many of which have solely to do with maximizing performance) you bite off a lot of work when you dive in.

Good luck.

like image 178
perigrin Avatar answered Jan 25 '23 23:01

perigrin


I could be wrong but I think you might be able to accomplish what I think you are trying to do using the BUILDARGS method. This lets you munge the contructor arguments before they are used to create the object.

#!/usr/bin/env perl

use strict;
use warnings;

{
  package MyClass;

  use Moose;
  has attr => (
     is => 'ro',
     isa => 'Str',
     required => 1,
  );

  around BUILDARGS => sub {
    my $orig = shift;
    my $self = shift;
    my %args = ref $_[0] ? %{shift()} : @_;

    if (exists $args{attribute}) {
      $args{attr} = delete $args{attribute};
    }

    $self->$orig(%args);
  };
}

my $one = MyClass->new(attribute => "Hi");
my $two = MyClass->new(attr => "Bye");

print $one->attr, "\n";
print $two->attr, "\n";
like image 45
Joel Berger Avatar answered Jan 26 '23 01:01

Joel Berger