The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package DBIx::Class::ResultSet::WithMetaData;

use strict;
use warnings;

use Data::Alias;
use Moose;
use Method::Signatures::Simple;
extends 'DBIx::Class::ResultSet';

has '_row_info' => (
  is => 'rw',
  isa => 'HashRef'
);

has 'was_row' => (
  is => 'rw',
  isa => 'Int'
);

has 'id_cols' => (
  is => 'rw',
  isa => 'ArrayRef',
);

has '_hash_modifiers' => (
  is => 'rw',
  isa => 'ArrayRef',
);

has '_key_modifiers' => (
  is => 'rw',
  isa => 'ArrayRef',
);

has '_object_hash_modifiers' => (
  is => 'rw',
  isa => 'ArrayRef',
);

has '_object_key_modifiers' => (
  is => 'rw',
  isa => 'ArrayRef',
);

=head1 VERSION

Version 1.001000

=cut

our $VERSION = '1.001000';

=head1 NAME

DBIx::Class::ResultSet::WithMetaData

=head1 SYNOPSIS

  package MyApp::Schema::ResultSet::ObjectType;

  use Moose;
  use MooseX::Method::Signatures;
  extends 'DBIx::Class::ResultSet::WithMetaData;

  method with_substr () {
    return $self->_with_meta_key( 
      substr => sub {
        return substr(shift->{name}, 0, 3);
      }
    );
  }

  ...


  # then somewhere else

  my $object_type_arrayref = $object_type_rs->with_substr->display();

  # [{
  #    'artistid' => '1',
  #    'name' => 'Caterwauler McCrae',
  #    'substr' => 'Cat'
  #  },
  #  {
  #    'artistid' => '2',
  #    'name' => 'Random Boy Band',
  #    'substr' => 'Ran'
  #  },
  #  {
  #    'artistid' => '3',
  #    'name' => 'We Are Goth',
  #    'substr' => 'We '
  #  }]

=head1 DESCRIPTION

Attach metadata to rows by chaining ResultSet methods together. When the ResultSet is
flattened to an ArrayRef the metadata is merged with the row hashes to give
a combined 'hash-plus-other-stuff' representation.

=head1 METHODS

=cut

sub new {
  my $self = shift;

  my $new = $self->next::method(@_);
  foreach my $key (qw/_row_info was_row id_cols _key_modifiers _hash_modifiers _object_key_modifiers _object_hash_modifiers/) {
    alias $new->{$key} = $new->{attrs}{$key};
  }

  unless ($new->_row_info) {
    $new->_row_info({});
  }

  unless ($new->_key_modifiers) {
    $new->_key_modifiers([]);
  }
  unless ($new->_hash_modifiers) {
    $new->_hash_modifiers([]);
  }
  unless ($new->_object_key_modifiers) {
    $new->_object_key_modifiers([]);
  }
  unless ($new->_object_hash_modifiers) {
    $new->_object_hash_modifiers([]);
  }

  unless ($new->id_cols && scalar(@{$new->id_cols})) {
    $new->id_cols([sort $new->result_source->primary_columns]);
  }

  return $new;
}

=head2 display

=over 4

=item Arguments: none

=item Return Value: ArrayRef

=back

 $arrayref_of_row_hashrefs = $rs->display();

This method uses L<DBIx::Class::ResultClass::HashRefInflator> to convert all
rows in the ResultSet to HashRefs. Then the subrefs that were added via 
L</_with_meta_key> or L</_with_meta_hash> are run for each row and the
resulting data merged with them.

=cut

method display () {
  my $rs = $self->search({});
#  $rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
  $rs->result_class('DBIx::Class::WithMetaData::Inflator');
  my @rows;
  foreach my $row_rep ($rs->all) {
    # the custom inflator inflates to a arrayref with two versions of the row in it - hash and obj
    my ($row, $row_obj) = @{$row_rep};
    # THIS BLOCK IS DEPRECATED
    if (my $info = $self->row_info_for(id => $self->_mk_id(row => $row))) {
      $row = { %{$row}, %{$info} };
    }

    foreach my $modifier (@{$rs->_hash_modifiers}) {
      my $row_hash = $modifier->($row);
      if (ref $row_hash ne 'HASH') {
        die 'modifier subref (added via build_metadata) did not return hashref';
      }

      # simple merge for now, potentially needs to be more complex
      $row->{$_} = $row_hash->{$_} for keys %{$row_hash};
    }

    foreach my $modifier (@{$rs->_object_hash_modifiers}) {
      my $row_hash = $modifier->($row, $row_obj);
      if (ref $row_hash ne 'HASH') {
        die 'modifier subref (added via build_metadata) did not return hashref';
      }

      # simple merge for now, potentially needs to be more complex
      $row->{$_} = $row_hash->{$_} for keys %{$row_hash};
    }

    foreach my $params (@{$rs->_key_modifiers}) {
      my $modifier = $params->{modifier};
      my $key = $params->{key};

      if (my $val = $modifier->($row)) {
        $row->{$key} = $val;
      }
    }

    foreach my $params (@{$rs->_object_key_modifiers}) {
      my $modifier = $params->{modifier};
      my $key = $params->{key};

      if (my $val = $modifier->($row, $row_obj)) {
        $row->{$key} = $val;
      }
    }
    push(@rows, $row);
  }

  return ($self->was_row) ? $rows[0] : \@rows;
}

=head2 _with_meta_key

=over 4

=item Arguments: key_name => subref($row_hash)

=item Return Value: ResultSet

=back

 $self->_with_meta_key( substr => sub ($row) { 
   return substr(shift->{name}, 0, 3);
 });

This method allows you populate a certain key for each row hash at  L</display> time.

=cut

method _with_meta_key ($key, $modifier) {
  my $rs = $self->search({});
  unless ($key) {
    die 'build_metadata called without key';
  }

  unless ($modifier && (ref $modifier eq 'CODE')) {
    die 'build_metadata called without modifier param';
  }

  push( @{$rs->_key_modifiers}, { key => $key, modifier => $modifier });
  return $rs;
}

=head2 _with_object_meta_key

=over 4

=item Arguments: key_name => subref($row_hash, $row_obj)

=item Return Value: ResultSet

=back

 $self->_with_object_meta_key( substr => sub { 
   my ($row_hash, $row_obj) = @_;
   return substr($row_obj->row_method, 0, 3);
 });

The same as L</_with_meta_key> but the subref gets the row object
as well as the row hash. This should only be used when you need to
access row methods as it's slower to inflate objects.

=cut

method _with_object_meta_key ($key, $modifier) {
  my $rs = $self->search({});
  unless ($key) {
    die '_with_object_meta_key called without key';
  }

  unless ($modifier && (ref $modifier eq 'CODE')) {
    die '_with_object_meta_key called without modifier param';
  }

  push( @{$rs->_object_key_modifiers}, { key => $key, modifier => $modifier });
  return $rs;
}

=head2 _with_meta_hash

=over 4

=item Arguments: subref($row_hash)

=item Return Value: ResultSet

=back

 $self->_with_meta_hash( sub ($row) { 
   my $row = shift;
   my $return_hash = { substr => substr($row->{name}, 0, 3), substr2 => substr($row->{name}, 0, 4) };
   return $return_hash;
 });

Use this method when you want to populate multiple keys of the hash at the same time. If you just want to 
populate one key, use L</_with_meta_key>.

=cut

method _with_meta_hash ($modifier) {
  my $rs = $self->search({});
  unless ($modifier && (ref $modifier eq 'CODE')) {
    die 'build_metadata called without modifier param';
  }

  push( @{$rs->_hash_modifiers}, $modifier );
  return $rs;
}

=head2 _with_object_meta_hash

=over 4

=item Arguments: subref($row_hash, $row_object)

=item Return Value: ResultSet

=back

 $self->_with_object_meta_hash( sub { 
   my ($row_hash, $row_object) = @_;

   my $return_hash = { substr => substr($row_object->name, 0, 3), substr2 => substr($row_hash->{name}, 0, 4) };
   return $return_hash;
 });

Like L</_with_meta_hash> but the subref gets the row object
as well as the row hash. This should only be used when you need to
access row methods as it's slower to inflate objects.

=cut

method _with_object_meta_hash ($modifier) {
  my $rs = $self->search({});
  unless ($modifier && (ref $modifier eq 'CODE')) {
    die 'build_metadata called without modifier param';
  }

  push( @{$rs->_object_hash_modifiers}, $modifier );
  return $rs;
}

=head2 add_row_info (DEPRECATED)

=over 4

=item Arguments: row => DBIx::Class::Row object, info => HashRef to attach to the row

=item Return Value: ResultSet

=back

 $rs = $rs->add_row_info(row => $row, info => { dates => [qw/mon weds fri/] } );

DEPRECATED - this method is quite slow as it requires that you iterate through 
the resultset each time you want to add metadata. Replaced by L</build_metadata>.

=cut

method add_row_info (%opts) {
  my ($row, $id, $info) = map { $opts{$_} } qw/row id info/;

  warn 'DEPRECATED - add_row_info is deprecated in favour of build_metadata';
  if ($row) {
    $id = $self->_mk_id(row => { $row->get_columns });
  }

  unless ($row || $self->find($id)) {
    die 'invalid id passed to add_row_info';
  }

  if (my $existing = $self->_row_info->{$id}) {
    $info = { %{$existing}, %{$info} };
  }

  $self->_row_info->{$id} = $info;  
}

# DEPRECATED
method row_info_for (%opts) {
  my $id = $opts{id};
  return $self->_row_info->{$id};
}

# DEPRECATED
method _mk_id (%opts) {
  my $row = $opts{row};
  return join('-', map { $row->{$_} } @{$self->id_cols});
}

=head1 AUTHOR

  Luke Saunders <luke.saunders@gmail.com>

=head1 THANKS

As usual, thanks to Matt S Trout for the sanity check.

=head1 LICENSE

  This library is free software under the same license as perl itself

=cut

1;