package Data::MultiValued::AttributeTrait; use Moose::Role; use namespace::autoclean; use Data::MultiValued::AttributeAccessors; use MooseX::Types::Moose qw(Str); use Try::Tiny; use namespace::autoclean; # ABSTRACT: "base role" for traits of multi-valued Moose attributes =head1 DESCRIPTION Don't use this role directly, use L, L or L. This role (together with L) defines all the basic plumbing to glue C etc into Moose attributes. =head2 Implementation details The multi-value object is stored in the instance slot named by the L attribute attribute. C modifiers on getters load the appropriate value from the multi-value object into the regular instance slot, C modifiers on setters store the value from the regular instance slot into the multi-value object. =head2 Attributes This trait adds some attributes to the attribute declarations in your class. Example: has stuff => ( is => 'rw', isa => 'Int', traits => ['MultiValued::Tags'], predicate => 'has_stuff', multi_accessor => 'stuff_tagged', multi_predicate => 'has_stuff_tagged', ); =attr C The instance slot to use to store the C or similar object. Defaults to C<"${name}__MULTIVALUED_STORAGE__">, where C<$name> is the attribute name. =cut has 'full_storage_slot' => ( is => 'ro', isa => Str, lazy_build => 1, ); sub _build_full_storage_slot { shift->name . '__MULTIVALUED_STORAGE__' } =attr C =attr C =attr C =attr C =attr C The names to use for the various additional accessors. See L for details. These default to C<"${name}_multi"> where C<$name> is the name of the corresponding non-multi accessor. So, for example, has stuff => ( is => 'rw', traits => ['MultiValued::Tags'], ); will create a C read / write accessor and a C read / write tagged accessor. =cut my @accs_to_multiply=qw(accessor reader writer predicate clearer); for my $acc (@accs_to_multiply) { has "multi_$acc" => ( is => 'ro', isa => Str, predicate => "has_multi_$acc", ); } =head1 REQUIREMENTS These methods must be provided by any class consuming this role. See L etc. for examples. =head2 C The class to use to create the multi-value objects. =cut requires 'multivalue_storage_class'; =head2 C Which options to pass from the multi-value accessors to the C method of the multi-value object. =cut requires 'opts_to_pass_set'; =head2 C Which options to pass from the multi-value accessors to the C method of the multi-value object. =cut requires 'opts_to_pass_get'; =method C Adds the L to the list of used slots. =cut around slots => sub { my ($orig, $self) = @_; return ($self->$orig(), $self->full_storage_slot); }; =method C Stores a new instance of L into the L of the instance. =cut sub set_full_storage { my ($self,$instance) = @_; my $ret = $self->multivalue_storage_class->new(); $self->associated_class->get_meta_instance->set_slot_value( $instance, $self->full_storage_slot, $ret, ); return $ret; } =method C Retrieves the value of the L of the instance. =cut sub get_full_storage { my ($self,$instance) = @_; return $self->associated_class->get_meta_instance ->get_slot_value( $instance, $self->full_storage_slot, ); } =method C Returns an instance of L, either by retrieving it from the instance, or by creating one (and setting it in the instance). Calls L and L. =cut sub full_storage { my ($self,$instance) = @_; return $self->get_full_storage($instance) || $self->set_full_storage($instance); } =method C Makes sure that all accessors for this attribute are created via the L method meta class. =cut sub accessor_metaclass { 'Data::MultiValued::AttributeAccessors' } =method C After the regular L method, installs the multi-value accessors. Each installed normal accessor gets a multi-value version You can add or rename the multi-value version by using the attributes described above If you are passing explicit subrefs for your accessors, things won't work. =cut after install_accessors => sub { my ($self) = @_; my $class = $self->associated_class; for my $meth (@accs_to_multiply) { my $type = "multi_$meth"; my $check = "has_$meth"; my $multi_check = "has_$type"; next unless $self->$check || $self->$multi_check; my $name = $self->$type; if (!$name) { my $basename = $self->$meth; die 'MultiValued attribute trait is not compatible with subref accessors' if ref($basename); $name = "${basename}_multi"; } $class->add_method( $self->_process_accessors($type => $name,0) ); } }; sub _filter_opts { my ($hr,@fields) = @_; my %ret; for my $f (@fields) { if (exists $hr->{$f}) { $ret{$f}=$hr->{$f}; } } return \%ret; } =method C Retrieves a value from the multi-value object, and stores it in the regular slot in the instance. If the value is not found, clears the slot. This traps the L exception that may be thrown by the multi-value object, but re-throws any other exception. =cut sub load_multi_value { my ($self,$instance,$opts) = @_; my $opts_passed = _filter_opts($opts, $self->opts_to_pass_get); my $value;my $found=1; try { $value = $self->full_storage($instance)->get($opts_passed); } catch { unless (ref($_) && $_->isa('Data::MultiValued::Exceptions::NotFound')) { die $_; } $found = 0; }; if ($found) { $self->set_raw_value($instance,$value); } else { $self->raw_clear_value($instance); } } =method C Clears the instance slot. Does the same as L, but we need this method because the other one gets changed by this trait. =cut sub raw_clear_value { my ($self,$instance) = @_; $self->associated_class->get_meta_instance ->deinitialize_slot( $instance, $self->name, ); } =method C Gets the value from the regular slot in the instance, and stores it into the multi-value object. =cut sub store_multi_value { my ($self,$instance,$opts) = @_; my $opts_passed = _filter_opts($opts, $self->opts_to_pass_set); $opts_passed->{value} = $self->get_raw_value($instance); $self->full_storage($instance)->set($opts_passed); } our $dyn_opts = {}; =method C Before the normal method, calls L. Normally, no options will be passed to the multi-value object C method. =cut before get_value => sub { my ($self,$instance) = @_; $self->load_multi_value($instance,$dyn_opts); }; =method C Sets the options that L will use, then calls L. The options are passed via an ugly Cised package variable. There might be a better way. =cut sub get_multi_value { my ($self,$instance,$opts) = @_; local $dyn_opts = $opts; return $self->get_value($instance); } =method C After the normal method, calls L. =cut after set_initial_value => sub { my ($self,$instance,$value) = @_; $self->store_multi_value($instance,$dyn_opts); }; =method C =method C Just like L and L, but calling L after the regular C =cut after set_value => sub { my ($self,$instance,$value) = @_; $self->store_multi_value($instance,$dyn_opts); }; sub set_multi_value { my ($self,$instance,$opts,$value) = @_; local $dyn_opts = $opts; return $self->set_value($instance,$value); } =method C =method C Just like L and L. =cut before has_value => sub { my ($self,$instance) = @_; $self->load_multi_value($instance,$dyn_opts); }; sub has_multi_value { my ($self,$instance,$opts) = @_; local $dyn_opts = $opts; return $self->has_value($instance); } =method C =method C Call the C method on the multi-value object. =cut after clear_value => sub { my ($self,$instance) = @_; $self->full_storage($instance)->clear($dyn_opts); return; }; sub clear_multi_value { my ($self,$instance,$opts) = @_; local $dyn_opts = $opts; return $self->clear_value($instance); } =method C =method C Return the name of the reader or writer method, honoring L, L and L. =cut sub get_multi_read_method { my $self = shift; return $self->multi_reader || $self->multi_accessor || $self->get_read_method . '_multi'; } sub get_multi_write_method { my $self = shift; return $self->multi_writer || $self->multi_accessor || $self->get_write_method . '_multi'; } =head1 Serialisation helpers These are used through L. =head2 C<_rebless_slot> Blesses the value inside the L of the instance into L, then calls C<_rebless_storage> on it. =cut sub _rebless_slot { my ($self,$instance) = @_; my $st = $self->get_full_storage($instance); return unless $st; bless $st, $self->multivalue_storage_class; $st->_rebless_storage; } =head2 C<_as_hash> Returns the result of calling C<_as_hash> on the value inside the L of the instance. Returns nothing if the slot does not have a value. =cut sub _as_hash { my ($self,$instance) = @_; my $st = $self->get_full_storage($instance); return unless $st; return $st->_as_hash; } 1;