The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

##----------------------------------------------------------------------------
## Module Generic - ~/lib/Module/Generic/Hash.pm
## Version v1.4.0
## Copyright(c) 2023 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2021/03/20
## Modified 2023/12/05
## All rights reserved
##
## This program is free software; you can redistribute it and/or modify it
## under the same terms as Perl itself.
##----------------------------------------------------------------------------
BEGIN
{
use strict;
use warnings;
use vars qw( $VERSION $DEBUG $KEY_OBJECT );
use Clone ();
use JSON;
use Want;
use overload (
# '""' => 'as_string',
'eq' => sub { _obj_eq(@_) },
'ne' => sub { !_obj_eq(@_) },
'<' => sub { _obj_comp( @_, '<') },
'>' => sub { _obj_comp( @_, '>') },
'<=' => sub { _obj_comp( @_, '<=') },
'>=' => sub { _obj_comp( @_, '>=') },
'==' => sub { _obj_comp( @_, '>=') },
'!=' => sub { _obj_comp( @_, '>=') },
'lt' => sub { _obj_comp( @_, 'lt') },
'gt' => sub { _obj_comp( @_, 'gt') },
'le' => sub { _obj_comp( @_, 'le') },
'ge' => sub { _obj_comp( @_, 'ge') },
'bool' => sub{$_[0]},
fallback => 1,
);
# Do we allow the use of object as hash keys?
our $KEY_OBJECT = 0;
our( $VERSION ) = 'v1.4.0';
};
use strict;
no warnings 'redefine';
sub new
{
my $that = shift( @_ );
my $class = ref( $that ) || $that;
my %hash = ();
# This enables access to the hash just like a real hash while still the user an call our object methods
my $obj = tie( %hash, 'Module::Generic::TieHash', {
# disable => ['Module::Generic'],
debug => $DEBUG,
enable => 0,
# Should we allow objects to be used as key? Default to false
key_object => $KEY_OBJECT,
});
my $self = bless( \%hash => $class );
$obj->enable(1);
if( scalar( @_ ) == 1 )
{
my $data = shift( @_ );
return( $that->error( "I was expecting an hash, but instead got '", ( $data // 'undef' ), "'." ) ) if( Scalar::Util::reftype( $data // '' ) ne 'HASH' );
my $tied = tied( %$data );
return( $that->error( "Hash provided is already tied to ", ref( $tied ), " and our package $class cannot use it, or it would disrupt the tie." ) ) if( $tied );
my @keys = CORE::keys( %$data );
@hash{ @keys } = @$data{ @keys };
}
elsif( scalar( @_ ) > 1 &&
!( @_ % 2 ) )
{
while( @_ )
{
$hash{ shift( @_ ) } = shift( @_ );
}
}
elsif( scalar( @_ ) )
{
return( $self->error( "Odd number (", scalar( @_ ), ") of hash keys and values provided." ) );
}
$obj->enable(0);
$self->SUPER::init( @_ );
$obj->enable(1);
return( $self );
}
# sub as_hash
# {
# my $self = CORE::shift( @_ );
# my $hash = {};
# $self->_tie_object->enable(1);
# my $keys = $self->keys;
# @$hash{ @$keys } = @$self{ @$keys };
# return( $hash );
# }
# We are already an hash, so no need to do anything.
# To convert to a regular hash as needed by JSON, the method TO_JSON can be used.
sub as_hash
{
my $self = shift( @_ );
if( @_ )
{
my $opts = $self->_get_args_as_hash( @_ );
if( $opts->{strict} )
{
my $ref = { %$self };
return( $ref );
}
}
return( $self );
}
sub as_json { return( shift->json(@_)->scalar ); }
sub as_string { return( shift->dump ); }
sub chomp
{
my $self = CORE::shift( @_ );
CORE::chomp( %$self );
return( $self );
}
sub clone
{
my $self = shift( @_ );
$self->_tie_object->enable(0);
my $data = $self->{data};
my $clone = Clone::clone( $data );
$self->_tie_object->enable(1);
return( $self->new( $clone ) );
}
sub debug { return( shift->_internal( 'debug', '_set_get_number', @_ ) ); }
sub defined { CORE::defined( $_[0]->{ $_[1] } ); }
sub delete { return( CORE::delete( shift->{ shift( @_ ) } ) ); }
sub dump
{
my $self = shift( @_ );
return( $self->_dumper( $self ) );
}
sub each
{
my $self = shift( @_ );
my $code = shift( @_ ) || return( $self->error( "No subroutine callback as provided for each" ) );
return( $self->error( "I was expecting a reference to a subroutine for the callback to each, but got '$code' instead." ) ) if( ref( $code ) ne 'CODE' );
while( my( $k, $v ) = CORE::each( %$self ) )
{
CORE::defined( $code->( $k, $v ) ) || CORE::last;
}
return( $self );
}
sub exists { return( CORE::exists( shift->{ shift( @_ ) } ) ); }
sub for { return( shift->foreach( @_ ) ); }
sub foreach
{
my $self = shift( @_ );
my $code = shift( @_ ) || return( $self->error( "No subroutine callback as provided for each" ) );
return( $self->error( "I was expecting a reference to a subroutine for the callback to each, but got '$code' instead." ) ) if( ref( $code ) ne 'CODE' );
CORE::foreach my $k ( CORE::keys( %$self ) )
{
local $_ = $self->{ $k };
CORE::defined( $code->( $k, $self->{ $k } ) ) || CORE::last;
}
return( $self );
}
sub get { return( $_[0]->{ $_[1] } ); }
sub has { return( shift->exists( @_ ) ); }
sub is_empty { return( scalar( CORE::keys( %{$_[0]} ) ) ? 0 : 1 ); }
sub json
{
my $self = shift( @_ );
my $opts = {};
if( ref( $_[-1] ) eq 'HASH' )
{
$opts = pop( @_ );
}
elsif( @_ && !( @_ % 2 ) )
{
$opts = { @_ };
}
$self->_tie_object->enable(0);
my $data = $self->{data};
# $opts->{utf8} = 1 if( !CORE::exists( $opts->{utf8} ) );
if( CORE::exists( $opts->{order} ) )
{
$opts->{canonical} = CORE::delete( $opts->{order} );
}
elsif( CORE::exists( $opts->{ordered} ) )
{
$opts->{canonical} = CORE::delete( $opts->{ordered} );
}
elsif( CORE::exists( $opts->{sort} ) )
{
$opts->{canonical} = CORE::delete( $opts->{sort} );
}
elsif( CORE::exists( $opts->{sorted} ) )
{
$opts->{canonical} = CORE::delete( $opts->{sorted} );
}
if( !CORE::exists( $opts->{canonical} ) && $opts->{pretty} )
{
$opts->{canonical} = 1;
}
if( !CORE::exists( $opts->{indent} ) && $opts->{pretty} )
{
$opts->{indent} = 1;
}
if( !CORE::exists( $opts->{relaxed} ) && $opts->{pretty} )
{
$opts->{relaxed} = 1;
}
my $j = JSON->new->allow_nonref;
my @keys = qw(
ascii latin1 utf8 pretty indent space_before space_after relaxed
canonical allow_nonref allow_unknown allow_blessed convert_blessed allow_tags
boolean_values filter_json_object filter_json_single_key_object max_depth max_size
);
foreach my $k ( @keys )
{
next unless( CORE::exists( $opts->{ $k } ) );
my $code = $j->can( $k );
if( defined( $code ) )
{
$code->( $j, $opts->{ $k } );
}
}
my $json = $j->encode( $data );
$self->_tie_object->enable(1);
return( Module::Generic::Scalar->new( $json ) );
}
# Allow hash keys as object
sub key_object
{
my $self = shift( @_ );
if( @_ )
{
$self->_tie_object->key_object( shift( @_ ) );
}
return( $self->_tie_object->key_object );
}
# $h->keys->sort
sub keys
{
my $self = shift( @_ );
$self->_tie_object->enable(1);
return( Module::Generic::Array->new( [ CORE::keys( %$self ) ] ) );
}
sub length { return( Module::Generic::Number->new( CORE::scalar( CORE::keys( %{$_[0]} ) ) ) ); }
sub map
{
my $self = shift( @_ );
my $code = CORE::shift( @_ );
return if( ref( $code ) ne 'CODE' );
return( CORE::map( $code->( $_, $self->{ $_ } ), CORE::keys( %$self ) ) );
}
sub map_array
{
my $self = shift( @_ );
my $code = CORE::shift( @_ );
return if( ref( $code ) ne 'CODE' );
return( Module::Generic::Array->new( [CORE::map( $code->( $_, $self->{ $_ } ), CORE::keys( %$self ) )] ) );
}
sub map_hash
{
my $self = shift( @_ );
my $code = CORE::shift( @_ );
return if( ref( $code ) ne 'CODE' );
return( $self->new( {CORE::map( $code->( $_, $self->{ $_ } ), CORE::keys( %$self ) )} ) );
}
sub merge
{
my $self = shift( @_ );
my $hash = {};
$hash = shift( @_ );
return( $self->error( "No valid hash provided." ) ) if( !$hash || ( Scalar::Util::reftype( $hash ) // '' ) ne 'HASH' );
my $opts = {};
$opts = pop( @_ ) if( @_ && ref( $_[-1] ) eq 'HASH' );
$opts->{overwrite} = 1 unless( CORE::exists( $opts->{overwrite} ) );
$self->_tie_object->enable(0);
my $data = $self->{data};
my $seen = {};
my $copy;
$copy = sub
{
my $this = shift( @_ );
my $to = shift( @_ );
my $p = {};
$p = shift( @_ ) if( @_ && ref( $_[-1] ) eq 'HASH' );
CORE::foreach my $k ( CORE::keys( %$this ) )
{
next if( CORE::exists( $to->{ $k } ) && !$p->{overwrite} );
if( ref( $this->{ $k } ) eq 'HASH' ||
( Scalar::Util::blessed( $this->{ $k } ) && $this->{ $k }->isa( 'Module::Generic::Hash' ) ) )
{
my $addr = Scalar::Util::refaddr( $this->{ $k } );
if( CORE::exists( $seen->{ $addr } ) )
{
$to->{ $k } = $seen->{ $addr };
next;
}
else
{
$to->{ $k } = {} unless( CORE::defined( $to->{ $k } ) && ( Scalar::Util::reftype( $to->{ $k } ) // '' ) eq 'HASH' );
$copy->( $this->{ $k }, $to->{ $k } );
}
$seen->{ $addr } = $this->{ $k };
}
else
{
$to->{ $k } = $this->{ $k };
}
}
};
$copy->( $hash, $data, $opts );
$self->_tie_object->enable(1);
return( $self );
}
sub remove { return( shift->delete( @_ ) ); }
sub reset { %{$_[0]} = () };
sub set { $_[0]->{ $_[1] } = $_[2]; }
sub size { return( shift->length ); }
sub undef { %{$_[0]} = () };
sub values
{
my $self = shift( @_ );
my $code;
$code = shift( @_ ) if( @_ && ref( $_[0] ) eq 'CODE' );
my $opts = {};
$opts = pop( @_ ) if( ( Scalar::Util::reftype( $_[-1] ) // '' ) eq 'HASH' );
if( $code )
{
if( $opts->{sort} )
{
return( Module::Generic::Array->new( [ CORE::map( $code->( $_ ), CORE::sort( CORE::values( %$self ) ) ) ] ) );
}
else
{
return( Module::Generic::Array->new( [ CORE::map( $code->( $_ ), CORE::values( %$self ) ) ] ) );
}
}
else
{
if( $opts->{sort} )
{
return( Module::Generic::Array->new( [ CORE::sort( CORE::values( %$self ) ) ] ) );
}
else
{
return( Module::Generic::Array->new( [ CORE::values( %$self ) ] ) );
}
}
}
sub _dumper
{
my $self = shift( @_ );
$self->_tie_object->enable(0);
my $data = $self->{data};
my $d = Data::Dumper->new( [ $data ] );
$d->Indent(1);
$d->Useqq(1);
$d->Terse(1);
$d->Sortkeys(1);
# $d->Freezer( '' );
$d->Bless( '' );
# return( $d->Dump );
my $str = $d->Dump;
$self->_tie_object->enable(1);
return( $str );
}
sub _internal
{
my $self = shift( @_ );
my $field = shift( @_ );
my $meth = shift( @_ );
$self->_tie_object->enable(0);
my( @resA, $resB );
if( wantarray )
{
@resA = $self->$meth( $field, @_ );
}
else
{
$resB = $self->$meth( $field, @_ );
}
$self->_tie_object->enable(1);
return( wantarray ? @resA : $resB );
}
sub _obj_comp
{
my( $self, $other, $swap, $op ) = @_;
my( $lA, $lB );
$lA = $self->length;
if( Scalar::Util::blessed( $other ) && $other->isa( 'Module::Generic::Hash' ) )
{
$lB = $other->length;
}
elsif( $other =~ /^$RE{num}{real}$/ )
{
$lB = $other;
}
else
{
return;
}
my $expr = $swap ? "$lB $op $lA" : "$lA $op $lB";
return( eval( $expr ) );
}
sub _printer { return( shift->printer( @_ ) ); }
sub _obj_eq
{
no overloading;
my $self = shift( @_ );
my $other = shift( @_ );
my $strA = $self->_dumper( $self );
my $strB;
if( Scalar::Util::blessed( $other ) && $other->isa( 'Module::Generic::Hash' ) )
{
$strB = $other->dump;
}
elsif( ( Scalar::Util::reftype( $other ) // '' ) eq 'HASH' )
{
$strB = $self->_dumper( $other )
}
else
{
return(0);
}
return( $strA eq $strB );
}
sub _tie_object
{
my $self = shift( @_ );
return( tied( %$self ) );
}
sub FREEZE
{
my $self = CORE::shift( @_ );
my $serialiser = CORE::shift( @_ ) // '';
my $class = CORE::ref( $self );
my $clone = $self->clone;
$clone->_tie_object->enable(0);
my %data = %{$clone->{data}};
$clone->_tie_object->enable(1);
# Return an array reference rather than a list so this works with Sereal and CBOR
# On or before Sereal version 4.023, Sereal did not support multiple values returned
CORE::return( [$class, \%data] ) if( $serialiser eq 'Sereal' && Sereal::Encoder->VERSION <= version->parse( '4.023' ) );
# But Storable want a list with the first element being the serialised element
CORE::return( $class, \%data );
}
sub STORABLE_freeze { CORE::return( CORE::shift->FREEZE( @_ ) ); }
sub STORABLE_thaw { CORE::return( CORE::shift->THAW( @_ ) ); }
# NOTE: CBOR will call the THAW method with the stored classname as first argument, the constant string CBOR as second argument, and all values returned by FREEZE as remaining arguments.
# NOTE: Storable calls it with a blessed object it created followed with $cloning and any other arguments initially provided by STORABLE_freeze
sub THAW
{
my( $self, undef, @args ) = @_;
my $ref = ( CORE::scalar( @args ) == 1 && CORE::ref( $args[0] ) eq 'ARRAY' ) ? CORE::shift( @args ) : \@args;
my $class = ( CORE::defined( $ref ) && CORE::ref( $ref ) eq 'ARRAY' && CORE::scalar( @$ref ) > 1 ) ? CORE::shift( @$ref ) : ( CORE::ref( $self ) || $self );
my $hash = CORE::ref( $ref ) eq 'ARRAY' ? CORE::shift( @$ref ) : {};
my $new;
# Storable pattern requires to modify the object it created rather than returning a new one
if( CORE::ref( $self ) )
{
foreach( CORE::keys( %$hash ) )
{
$self->{ $_ } = CORE::delete( $hash->{ $_ } );
}
$new = $self;
}
else
{
$new = $class->new( $hash );
}
CORE::return( $new );
}
sub TO_JSON
{
my $self = CORE::shift( @_ );
my $ref = { %$self };
CORE::return( $ref );
}
1;
__END__