williamt
williamt

Reputation: 473

Better way to get all attributes from a Moose class as a hash

I want to get all attributes back from a class as a hash. Is there any better way to do it than this ? Ideally(?) I would like to be able to say something like:

my $hash = \%{ Diag->new( {range =>1, code => 'AB'} ) };

But will settle for:

my $d = Diag->new( {range =>1, code => 'AB'} );
my $hash = $d->hash;

package Diag;
use Moose;

my @attrs = qw/range code severity source message/;

has 'range'    => ( is => 'rw', isa => 'Int' );
has 'code'     => ( is => 'rw', isa => 'String' );
has 'severity' => ( is => 'rw', isa => 'Int' );
has 'source'   => ( is => 'rw', isa => 'String' );
has 'message'  => ( is => 'rw', isa => 'String' );

sub hash {
    my $self = shift;
    my $hash = {};
    for (@attrs) {
        $hash->{$_} = $self->$_;
    }
    return $hash;
}

no Moose;
1;

EDIT Hash with string output for pack/unpack:

# Combining this attribute and the record_format would be great.
# if $self->record->format worked that would be cool.
has 'record' => (
    is => 'ro',
    isa => 'HashRef',
    default => sub {
        {  
            foo => 'A5',
            foo2 => 'A16',
        }
);

sub record_format 
{
    my $self = shift;
    my @fields = qw( foo foo2 );

    return _build_format_string($self->record, \@fields);
}

sub _build_format_string {
    return join '', map { $_[1]->{$_} } @{ $_[2] };
}

EDIT2 I found that if I created an Attribute Trait I could make this a little nicer. This way the hash order is with the attribute and only one format method is needed.

package Order;
use Moose::Role;

  has order => (
      is        => 'ro',
      isa       => 'ArrayRef',
      predicate => 'has_order',
  );

Moose::Util::meta_attribute_alias('Order');
1;

package Record;
use Moose;

has 'record' => (
    traits  => [qw/Order/],
    is      => 'ro',
    isa     => 'HashRef',
    default => sub {
        {
            foo  => 'A5',
            foo2 => 'A16',
        },
        ;
    },
    order => [qw(foo foo2)]
);

sub format {
    my ( $self, $attr ) = @_;
    my $fields = $self->meta->get_attribute($attr)->order();
    return join '', map { $self->{$attr}{$_} } @$fields;
}

1;


my $r = Record->new();
print $r->format("record");
Outputs: A5A16

Upvotes: 3

Views: 1659

Answers (1)

zdim
zdim

Reputation: 66881

I would much rather pack that into a method, but your "ideal" case is almost there

my $data = { %{ Diag->new( {range =>1, code => 'AB'} ) } };

The %{...} returns a (key,value,...) list so you want {} to make a hashref out of it, not \ (which curiously turns it back into an object).

But really, that should be tucked away in a method

my $data = Diag->new(...)->get_data;

package Diag;
...
sub get_data { return { %{$_[0]} } };
...
1;

For purely presentational purposes – to print them out – consider using a module, so you don't have to worry about (or know) which attributes have what reference as a value. I use Data::Dump for conciseness of its output

my $obj = Diag->new(...);

say $obj->stringify();                           # whole object serialized

say for $obj->stringify('attr1', 'attr1', ...);  # serialized values for each

package Diag;
...
use Data::Dump qw(pp);
...
sub stringify {
    my $self = shift;
    return map { pp $self->{$_} } @_  if @_;
    return { pp %$self } }
}

If native OO is used and not Moo/Moose also overload "" for say $obj; use

use overload q("") => sub { return shift->stringify() }

In Moo and Moose the stringification of object under "" (implied in prints as well) is provided.


By further clarifications the code below doesn't address the actual problem. I'll edit but I am leaving this for now as it was deemed generally useful.

It has come up in comments and question edit that a part of the intent is to be able to retrieve values for attribute(s) as well, and packed. The added code does that, but as there is explicit dereferencing a check with ref should be added so to correctly retrieve all values, from either of arrayref, hashref, or string/number. For example

sub record_format {
    my ($self, @attrs) = @_;
    @attrs = qw(attr1 attr2 ...) if not @attrs;  # default list
    my $packed;
    foreach my $attr (@attrs) {
        my $val = $self->{$attr};
        my $rv = ref $val;
        if    (not $rv)        { $packed .= $val }
        elsif ($rv eq 'HASH')  { $packed .= join '', values %$val }
        elsif ($rv eq 'ARRAY') { $packed .= join '', @$val }   
    }
    return $packed;
}

This packs values of the passed attributes or of the listed defaults.

The desired $self->record->format can't work nicely since $self->record doesn't return an object so you can't string another method call. You can write an accessor but if you made it return an object under any circumstances that would likely be a surprising behavior, thus not good design.

Upvotes: 3

Related Questions