The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package List::Objects::WithUtils::Role::Hash;
{
  $List::Objects::WithUtils::Role::Hash::VERSION = '2.007001';
}
use strictures 1;

use Module::Runtime ();
use Scalar::Util ();
use List::MoreUtils ();

=pod

=for Pod::Coverage HASH_TYPE blessed_or_pkg

=cut

sub HASH_TYPE () { 'List::Objects::WithUtils::Hash' }
sub blessed_or_pkg { 
  Scalar::Util::blessed($_[0]) ?
    $_[0] : Module::Runtime::use_module(HASH_TYPE)
}

use Role::Tiny;

sub array_type       { 'List::Objects::WithUtils::Array' }
sub inflated_type    { 'List::Objects::WithUtils::Hash::Inflated' }
sub inflated_rw_type { 'List::Objects::WithUtils::Hash::Inflated::RW' }

=pod

=for Pod::Coverage TO_JSON type

=cut

sub is_mutable   { 1 }
sub is_immutable { ! $_[0]->is_mutable }

sub type { }

our %Required;
sub new {
  my $arraytype = $_[0]->array_type;
  unless (exists $Required{$arraytype}) {
    Module::Runtime::require_module($arraytype);
    $Required{$arraytype} = 1
  }
  bless +{ @_[1 .. $#_] }, Scalar::Util::blessed $_[0] || $_[0]
}

sub unbless { +{ %{ $_[0] } } }
{ no warnings 'once'; *TO_JSON = *unbless; }

sub clear { %{ $_[0] } = (); $_[0] }

sub copy { blessed_or_pkg($_[0])->new(%{ $_[0] }) }

sub inflate {
  my ($self, %params) = @_;
  my $type = $params{rw} ? 'inflated_rw_type' : 'inflated_type';
  my $pkg = blessed_or_pkg($self);
  Module::Runtime::require_module( $pkg->$type );
  $pkg->$type->new( %$self )
}

sub defined { CORE::defined $_[0]->{ $_[1] } }
sub exists  { CORE::exists  $_[0]->{ $_[1] } }

sub is_empty { ! keys %{ $_[0] } }

sub get {
  if (@_ > 2) {
    return blessed_or_pkg($_[0])->array_type->new(
      @{ $_[0] }{ @_[1 .. $#_] }
    )
  }
  $_[0]->{ $_[1] }
}

sub sliced {
  blessed_or_pkg($_[0])->new(
    map {;
      exists $_[0]->{$_} ? 
        ( $_ => $_[0]->{$_} )
        : ()
    } @_[1 .. $#_]
  )
}

sub set {
  my $self = shift;
  my @keysidx = grep {; not $_ % 2 } 0 .. $#_ ;
  my @valsidx = grep {; $_ % 2 }     0 .. $#_ ;

  @{$self}{ @_[@keysidx] } = @_[@valsidx];

  $self
}

sub maybe_set {
  my $self = shift;
  for (grep {; not $_ % 2 } 0 .. $#_) {
    $self->{ $_[$_] } = $_[$_ + 1] unless exists $self->{ $_[$_] }
  }
  $self
}

sub delete {
  blessed_or_pkg($_[0])->array_type->new(
    CORE::delete @{ $_[0] }{ @_[1 .. $#_] }
  )
}

sub keys {
  blessed_or_pkg($_[0])->array_type->new(
    CORE::keys %{ $_[0] }
  )
}

sub values {
  blessed_or_pkg($_[0])->array_type->new(
    CORE::values %{ $_[0] }
  )
}

sub intersection {
  my %seen;
  blessed_or_pkg($_[0])->array_type->new(
    List::MoreUtils::uniq
      grep {; ++$seen{$_} > $#_ } map {; CORE::keys %$_ } @_
  )
}

sub diff {
  my %seen;
  my @vals = map {; CORE::keys %$_ } @_;
  $seen{$_}++ for @vals;
  blessed_or_pkg($_[0])->array_type->new(
    grep {; $seen{$_} != @_ } List::MoreUtils::uniq @vals
  )
}

sub kv {
  blessed_or_pkg($_[0])->array_type->new(
    map {; [ $_, $_[0]->{ $_ } ] } CORE::keys %{ $_[0] }
  )
}

sub kv_sort {
  if (defined $_[1]) {
    return blessed_or_pkg($_[0])->array_type->new(
      map {; [ $_, $_[0]->{ $_ } ] }
        CORE::sort {; $_[1]->($a, $b) } CORE::keys %{ $_[0] }
    )
  }
  blessed_or_pkg($_[0])->array_type->new(
    map {; [ $_, $_[0]->{ $_ } ] } CORE::sort( CORE::keys %{ $_[0] } )
  )
}

sub export { %{ $_[0] } }

print
  qq[<Su-Shee> huf: I learned that from toyota via agile blahblah,],
  qq[ it's asking the five "why" questions.\n],
  qq[<mauke> WHY WHY WHY WHY GOD WHY\n]
unless caller;
1;


=pod

=head1 NAME

List::Objects::WithUtils::Role::Hash - Hash manipulation methods

=head1 SYNOPSIS

  ## Via List::Objects::WithUtils::Hash ->
  use List::Objects::WithUtils 'hash';

  my $hash = hash(foo => 'bar');

  $hash->set(
    foo => 'baz',
    pie => 'tasty',
  );

  my @matches = $hash->keys->grep(sub {
    $_[0] =~ /foo/
  })->all;

  my $pie = $hash->get('pie')
    if $hash->exists('pie');

  for my $pair ( $hash->kv->all ) {
    my ($key, $val) = @$pair;
    ...
  }

  my $obj = $hash->inflate;
  my $foo = $obj->foo;

  ## As a Role ->
  use Role::Tiny::With;
  with 'List::Objects::WithUtils::Role::Hash';

=head1 DESCRIPTION

A L<Role::Tiny> role defining methods for creating and manipulating HASH-type
objects.

In addition to the methods documented below, these objects provide a
C<TO_JSON> method exporting a plain HASH-type reference for convenience when
feeding L<JSON::Tiny> or similar.

=head2 new

Constructs a new HASH-type object.

=head2 export

  my %hash = $hash->export;

Returns a raw key/value list.

=head2 clear

Clears the current hash entirely.

Returns the hash object (as of version 1.013).

=head2 copy

Creates a shallow clone of the current object.

=head2 unbless

Returns a plain C</HASH> reference (shallow clone).

=head2 defined

  if ( $hash->defined($key) ) { ... }

Returns boolean true if the key has a defined value.

=head2 delete

  $hash->delete( @keys );

Deletes keys from the hash.

Returns an L</array_type> object containing the deleted values.

=head2 exists

  if ( $hash->exists($key) ) { ... }

Returns boolean true if the key exists.

=head2 get

  my $val  = $hash->get($key);
  my @vals = $hash->get(@keys)->all;

Retrieves a key or list of keys from the hash.

If we're taking a slice (multiple keys were specified), values are returned
as an L</array_type> object. (See L</sliced> if you'd rather generate a new
hash.)

=head2 inflate

  my $obj = hash(foo => 'bar', baz => 'quux')->inflate;
  my $baz = $obj->baz; 

Inflates a simple object providing accessors for a hash.

By default, accessors are read-only; specifying C<rw => 1> allows setting new
values:

  my $obj = hash(foo => 'bar', baz => 'quux')->inflate(rw => 1);
  $obj->foo('frobulate');

Returns an L</inflated_type> (or L</inflated_rw_type>) object.

The default objects provide a C<DEFLATE> method returning a
plain hash; this makes it easy to turn inflated objects back into a C<hash()>
for modification:

  my $first = hash( foo => 'bar', baz => 'quux' )->inflate;
  my $second = hash( $first->DEFLATE, frobulate => 1 )->inflate;

=head2 intersection

  my $first  = hash(a => 1, b => 2, c => 3);
  my $second = hash(b => 2, c => 3, d => 4);
  my $intersection = $first->intersection($second);
  my @common = $intersection->sort->all;

Returns the list of keys common between all given hash-type objects (including
the invocant) as an L</array_type> object.

=head2 diff

The opposite of L</intersection>; returns the list of keys that are not common
to all given hash-type objects (including the invocant) as an L</array_type>
object.

=head2 is_empty

Returns boolean true if the hash has no keys.

=head2 is_mutable

Returns boolean true if the hash is mutable; immutable subclasses can override
to provide a negative value.

=head2 is_immutable

The opposite of L</is_mutable>.

=head2 keys

  my @keys = $hash->keys->all;

Returns the list of keys in the hash as an L</array_type> object.

=head2 values

  my @vals = $hash->values->all;

Returns the list of values in the hash as an L</array_type> object.

=head2 kv

  for my $pair ($hash->kv->all) {
    my ($key, $val) = @$pair;
  }

Returns an L</array_type> object containing the key/value pairs in the HASH,
each of which is a two-element ARRAY.

=head2 kv_sort

  my $kvs = hash(a => 1, b => 2, c => 3)->kv_sort;
  # $kvs = array(
  #          [ a => 1 ], 
  #          [ b => 2 ], 
  #          [ c => 3 ]
  #        )

  my $reversed = hash(a => 1, b => 2, c => 3)
    ->kv_sort(sub { $_[1] cmp $_[0] });
  # Reverse result as above

Like L</kv>, but sorted by key. A sort routine can be provided; C<$_[0]> and
C<$_[1]> are equivalent to the usual sort variables C<$a> and C<$b>.

=head2 set

  $hash->set(
    key1 => $val,
    key2 => $other,
  )

Sets keys in the hash.

As of version 1.007, returns the current hash object.
The return value of prior versions is unreliable.

=head2 maybe_set

  my $hash = hash(foo => 1, bar => 2, baz => 3);
  $hash->maybe_set(foo => 2, bar => 3, quux => 4);
  # $hash = +{ foo => 1, bar => 2, baz => 3, quux => 4 }

Like L</set>, but only sets values that do not already exist in the hash.

Returns the hash object.

=head2 sliced

  my $newhash = $hash->sliced(@keys);

Returns a new hash object built from the specified set of keys.

(See L</get> if you only need the values.)

=head2 array_type

The class name of array-type objects that will be used to contain the results
of methods returning a list.

Defaults to L<List::Objects::WithUtils::Array>.

Subclasses can override C<array_type> to produce different types of array
objects; the method can also be queried to find out what kind of array object
will be returned:

  my $type = $hash->array_type;

=head2 inflated_type

The class name that objects are blessed into when calling L</inflate>.

Defaults to L<List::Objects::WithUtils::Hash::Inflated>.

=head2 inflated_rw_type

The class name that objects are blessed into when calling L</inflate> with
C<rw => 1>.

Defaults to L<List::Objects::WithUtils::Hash::Inflated::RW>, a subclass of
L<List::Objects::WithUtils::Hash::Inflated>.

=head1 SEE ALSO

L<List::Objects::WithUtils>

L<List::Objects::WithUtils::Hash>

L<List::Objects::WithUtils::Hash::Immutable>

L<List::Objects::WithUtils::Hash::Typed>

L<Data::Perl>

=head1 AUTHOR

Jon Portnoy <avenj@cobaltirc.org>

Portions of this code are derived from L<Data::Perl> by Matthew Phillips
(CPAN: MATTP), haarg et al

Licensed under the same terms as Perl.

=cut