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

3

5

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.

Otto answered 7/4, 2012 at 2:14 Comment(6)
Could you give some examples of the desired behaviour? From what you describe, BUILDARGS mangling would do exactly the right things... That is: what readers/writers do you want for this attribute? What init_args?Heroics
an example of desired behavior is very close to what MooseX::Aliases actually does. But I haven't figured out what all it has to do to do it. Also it uses an array, and I don't want/need that. basically buildargs seems good when I need to do it once, but it's not really per attribute, it's per class. I want something per attribute.Otto
you can apply MooseX::Aliases on a per-attribute basis.Heroics
@ether yes but it does things I don't want, I said that but it'd requiring you read everything I wrote.Otto
@ether also because I really don't like having 3 or 4 MooseX* extensions in the same file because the load order becomes an issue...Otto
@ether and because aliases doesn't really provide the same metadata that I need, partially because it uses arrays, but partially because I want to ultimately differentiate between things I've aliased and things that I need to say has this other name in metadata.Otto
E
5

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.

Ephrayim answered 11/4, 2012 at 6:37 Comment(0)
E
5

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";
Enstatite answered 7/4, 2012 at 3:50 Comment(3)
yeah I could, but it's not as.... meta as I need. I didn't think to say as to why I wasn't doing thatOtto
yeah, when done at a meta level you can use this in multiple classes. still I think a future reader might benefit from this for a per-class mechanism.Enstatite
you can look at the source I wrote for a trait. Its job is to remember all the values that an attribute has had, its history. github.com/jberger/MooseX-RememberHistory/blob/master/lib/… Perhaps its logic can help you get started.Enstatite
E
5

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.

Ephrayim answered 11/4, 2012 at 6:37 Comment(0)
L
0

So what I'm hearing is that:

  • At construction time, an attribute should be able to be set by its init_arg and any alternate init_args defined on the attribute.
  • An attribute should not be able to be manipulated by its alternate init_args except at instance construction; that is, aside from the above, the attribute should behave "normally".

Based on that, this seems like a good match for the MooseX::MultiInitArg attribute trait. Yes? :)

Lais answered 12/4, 2012 at 17:26 Comment(3)
other modules are probably not a solution because I'm needing an extension that provides other kinds of special metadata, I was just getting hung up on the init arg part. Also multiple alternate init args would out of the scope of said other metadata. This might help someone else thoughOtto
To do what you want, you're going to need a BUILDARGS to handle the init_arg, or an attribute trait. If you're looking to handle everything (init_arg and your other metadata) at the same time, you're going to need a custom trait of some sort -- which is always "[an]other module", even if it's defined locally. :) Most modern MooseX will behave well with others, when written properly. I'd leverage the existing trait for the init_arg and write another one for additional metadata, but then, I'm pretty lazy :)Lais
sorry I meant an existing module. I'm lazier... I wrote my replacement with the idea in mind that I wouldn't have to write 2 lines of code per attribute, for which I have many.Otto

© 2022 - 2024 — McMap. All rights reserved.