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

I of course know that I can rename the init arg for an attribute by setting init_arg

(for example)

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

      

which would allow me

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

      

but not

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

      

in the same time

MooseX :: Aliases does have this behavior, but creating an alias also creates accessors. I am currently trying to understand the code in this module to see if I can figure out how it does it so that I can replicate the specified functionality (as I understand it). If someone can explain how to do this here, with an example that would be great.

update, it looks like MX :: Aliases does it by replacing what is actually passed to the constructor in around initialize_instance_slot

, but I'm still not sure how this is actually called, because in my test code, my environment isn In fact, execution is in progress.

update munging in BUILDARGS

is not really an option, because what I'm trying to do is to customize the accessory via the name of the label that I add to the attribute via Meta Recipe3 . You could tell what i do

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

      

Update

here is what I managed to figure out with what I am 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 block around

never runs and I'm not sure why, maybe I'm wrapping it in the wrong place or something.

+3


source to share


3 answers


MooseX::Aliases

has several moving parts to do this functionality because the behavior has to be applied to several different places in the MOP. Your code here is very close to the code in the MooseX::Aliases

Trait attribute .

I suspect the reason your code is not being called is because something is wrong when you try to register your trait. MooseX::Aliases

uses Moose::Util::meta_attribute_alias

rather than the old fashioned way of using it here. Try replacing the section Moose::Meta::Attribute::Custom::Trait::OtherName

with a call Moose::Util::meta_attribute_alias 'OtherName';

within your role.

The second code you have here will not work for immutable classes. You will need to add a second trait to handle them because immutability code is handled by the class metaclass, not the attribute metaclass. You will need to add some more traits to handle the attributes in Roles, I think. Then you need to hook up Moose :: Exporter to make sure all traits are applied correctly when everything is compiled.

I have a simple version of this work that cannot be changed. This code is also on github .

Attribute attribute first:

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 is a class property.



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 glue Moose::Exporter

.

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:

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 since there are many moving parts (many of which are purely about maximum efficiency), you bite off a lot of work when you dive.

Good luck.

+3


source


I could be wrong, but I think you could accomplish what I think you are trying to do using the BUILDARGS method . This allows you to argue constructor 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";

      

+3


source


So, I hear that:

  • At build time, the attribute must be set by its init_arg and any alternate init_args defined in the attribute.
  • An attribute should not be controlled by its alternate init_args except when instantiated; that is, in addition to the above, the attribute must behave "normally".

From that it looks like a good match for the MooseX :: MultiInitArg attribute attribute . Yes?:)

0


source







All Articles