One of the core ideas behind Moose is that of metaprogramming. That is; don't write programs - write programs which write programs.
For example, rather than defining our attributes the old-fashioned way, like:
sub some_attribute {
my $self = shift;
$self->{some_attribute} = shift if @_;
return $self->{some_attribute};
}
We just write:
has some_attribute => (is => 'rw');
The has function is a "program which writes programs". It makes our sub some_attribute for us!
So the solution is to write something that does the same sort of job as has, but has some domain-specific knowledge about grabbing raw data, unpacking it, etc. Here's an example (untested):
{
package Binary::Humax::HmtData;
use DateTime;
use Moose;
has raw_data_block => (
is => 'rw',
isa => 'Str',
required => 1,
);
# Shortcut function for defining attributes
#
sub _my_has
{
my ($name, %spec) = @_;
my $meta = __PACKAGE__->meta;
# Default attribute to 'rw'
$spec{is} //= 'rw';
# Set up lazy builder
if (my $unpack = delete $spec{unpack})
{
$spec{lazy} //= 1;
$spec{builder} //= "_build_$name";
if (my $postprocess = delete $spec{postprocess})
{
$meta->add_method($spec{builder}, sub {
my $self = shift;
local $_ = unpack($unpack, $self->raw_data_block);
$postprocess->();
});
}
else
{
$meta->add_method($spec{builder}, sub {
my $self = shift;
return unpack($unpack, $self->raw_data_block);
});
}
}
$meta->add_attribute($name, \%spec);
}
# Now use that shortcut to define each attribute.
#
_my_has last_play => (isa => 'Int', unpack => '@5 S');
_my_has chan_num => (isa => 'Int', unpack => '@17 S');
_my_has start_time => (
isa => 'DateTime',
unpack => '@5 S',
postprocess => sub {
DateTime->from_epoch(epoch => $_, time_zone => 'GMT');
},
);
_my_has file_name => (isa => 'Str', unpack => '@33 A512');
# Create an alternative constructor which wraps "new".
#
sub new_from_file
{
my ($class, $filename) = @_;
open my $fh, '>', $filename;
my $slurp = do { local $/ = <$fh> };
return $class->new(r);
}
}
#
# USAGE
#
my $hmt_data = Binary::Humax::HmtData->new_from_file($path_name);
my $field = $hmt_data->start_time;
"Secondly, when complete, I plan to upload the module to CPAN. Have I chosen a good name for it, or should it live in a different name space?"
No, it seems like a bad name. You're putting it in "Binary" because it's a binary file format. But presumably end users of your module won't care whether it's a binary file format, a text-based one, or XML-based; they don't care about the file format at all, because they've downloaded your module to abstract those sort of details away, haven't they?
I would have thought something in the "TV" namespace more fitting.
UPDATE:; we can go even "more meta" by replacing our has workalike with an attribute trait. This has the advantage of allowing introspection of each attribute to read back its "unpack" code.
{
package Binary::Humax::HmtData::Trait::Attribute;
use Moose::Role;
has unpack => (is => 'ro', isa => 'Str');
has postprocess => (is => 'ro', isa => 'CodeRef');
before _process_options => sub
{
my ($meta, $name, $spec) = @_;
if ($spec->{unpack})
{
$spec->{lazy} //= 1;
$spec->{builder} //= "_build_$name";
$spec->{is} //= 'rw';
}
};
after attach_to_class => sub
{
my $attr = shift;
my $class = $attr->associated_class;
my $unpack = $attr->unpack or return;
if (my $postprocess = $attr->postprocess)
{
$class->add_method($attr->builder, sub {
my $self = shift;
local $_ = unpack($unpack, $self->raw_data_block);
$postprocess->();
});
}
else
{
$class->add_method($attr->builder, sub {
my $self = shift;
return unpack($unpack, $self->raw_data_block);
});
}
};
}
{
package Binary::Humax::HmtData;
use DateTime;
use Moose;
use constant MAGIC => 'Binary::Humax::HmtData::Trait::Attribute';
has raw_data_block => (
is => 'rw',
isa => 'Str',
required => 1,
);
has last_play => (
traits => [ MAGIC ],
isa => 'Int',
unpack => '@5 S',
);
has chan_num => (
traits => [ MAGIC ],
isa => 'Int',
unpack => '@17 S',
);
has start_time => (
traits => [ MAGIC ],
isa => 'DateTime',
unpack => '@5 S',
postprocess => sub {
DateTime->from_epoch(epoch => $_, time_zone => 'GMT');
},
);
has file_name => (
traits => [ MAGIC ],
isa => 'Str',
unpack => '@33 A512',
);
}
# Attribute introspection
print Binary::Humax::HmtData->meta->get_attribute('start_time')->unpac
+k, "\n";
The slight ugliness with this method is that the attribute trait has some knowledge of the class it's being applied to - it knows that the class has a raw_data_block attribute. With a little more work that problem could be eliminated.
perl -E'sub Monkey::do{say$_,for@_,do{($monkey=[caller(0)]->[3])=~s{::}{ }and$monkey}}"Monkey say"->Monkey::do'
|