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

##----------------------------------------------------------------------------
## Module Generic - ~/lib/Module/Generic/JSON.pm
## Version v0.1.0
## Copyright(c) 2025 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2025/03/24
## Modified 2025/03/24
## 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( @EXPORT @EXPORT_OK $AUTOLOAD $DEBUG $VERSION );
use JSON ();
use Scalar::Util ();
our @ISA = qw( Module::Generic );
our @EXPORT = qw( from_json to_json encode_json decode_json );
our @EXPORT_OK = qw( new_json );
our %EXPORT_TAGS = ();
our $VERSION = 'v0.1.0';
};
use strict;
sub import
{
my $this = shift( @_ );
$this->export_to_level( 1, undef, ( @_, @EXPORT ) );
}
sub init
{
my $self = shift( @_ );
$self->{_init_strict_use_sub} = 1;
my $opts = $self->_get_args_as_hash( @_ );
# try-catch
local $@;
my $j = eval{ JSON->new };
if( $@ )
{
return( $self->error( "Error instantiating a JSON object: $@" ) );
}
my $equi =
{
order => 'canonical',
ordered => 'canonical',
sorted => 'canonical',
sort => 'canonical',
};
foreach my $opt ( keys( %$opts ) )
{
my $ref;
$ref = $j->can( exists( $equi->{ $opt } ) ? $equi->{ $opt } : $opt ) || do
{
warn( "Unknown JSON option '${opt}'\n" ) if( $self->_warnings_is_enabled( 'Module::Generic' ) );
next;
};
eval
{
$ref->( $j, $opts->{ $opt } );
};
if( $@ )
{
if( $@ =~ /perl[[:blank:]\h]+structure[[:blank:]\h]+exceeds[[:blank:]\h]+maximum[[:blank:]\h]+nesting[[:blank:]\h]+level/i )
{
my $max = $j->get_max_depth;
return( $self->error( "Unable to set json option ${opt}: $@ (max_depth value is ${max})" ) );
}
else
{
return( $self->error( "Unable to set json option ${opt}: $@" ) );
}
}
delete( $opts->{ $opt } );
}
$self->{_json} = $j;
# Pass the rest to our parent init for properties unique to our module.
$self->SUPER::init( %$opts ) || return( $self->pass_error );
return( $self );
}
# cache
my $JSON;
sub decode_json($)
{
my $rv = eval
{
( $JSON ||= __PACKAGE__->new->utf8 )->decode( @_ );
};
if( $@ )
{
return( $JSON->error( $@ ) );
}
return( $rv );
}
sub encode_json($)
{
my $rv = eval
{
( $JSON ||= __PACKAGE__->new->utf8 )->encode( @_ );
};
if( $@ )
{
return( $JSON->error( $@ ) );
}
return( $rv );
}
sub to_json($@)
{
if( ref($_[0]) eq __PACKAGE__ or
( @_ > 2 and $_[0] eq __PACKAGE__ ) )
{
return( __PACKAGE__->error( "to_json should not be called as a method." ) );
}
my $opts = {};
if( @_ == 2 and ref($_[1]) eq 'HASH' )
{
$opts = $_[1];
}
my $json = __PACKAGE__->new( %$opts ) ||
return( __PACKAGE__->pass_error );
return( $json->encode( $_[0] ) );
}
sub from_json($@)
{
if( ref( $_[0] ) eq __PACKAGE__ or $_[0] eq __PACKAGE__ )
{
return( __PACKAGE__->error( "from_json should not be called as a method." ) );
}
my $opts = {};
if( @_ == 2 and ref($_[1]) eq 'HASH' )
{
$opts = $_[1];
}
my $json = __PACKAGE__->new( %$opts ) ||
return( __PACKAGE__->pass_error );
return( $json->decode( $_[0] ) );
}
sub new_json
{
my $self = __PACKAGE__->new( @_ );
$self->debug( $DEBUG );
return( $self );
}
sub AUTOLOAD
{
my $self;
$self = shift( @_ ) if( Scalar::Util::blessed( $_[0] ) && $_[0]->isa( 'Module::Generic::JSON' ) );
my @args = @_;
my( $class, $meth, $code );
$class = ref( $self ) || $self;
$meth = $AUTOLOAD;
if( CORE::index( $meth, '::' ) != -1 )
{
my $idx = rindex( $meth, '::' );
$class = substr( $meth, 0, $idx );
$meth = substr( $meth, $idx + 2 );
}
if( $self )
{
my $j = $self->{_json} || return( $self->error( "No JSON object could be found! This should not happen." ) );
if( $code = $j->can( $meth ) )
{
local $@;
my $wantlist = wantarray();
my @rv = eval{ ( $wantlist // '' ) ? ( $code->( $j, scalar( @args ) ? @args : () ) ) : scalar( $code->( $j, scalar( @args ) ? @args : () ) ) };
if( $@ )
{
return( $self->error( $@ ) );
}
if( Scalar::Util::blessed( $rv[0] ) && $rv[0]->isa( 'JSON' ) )
{
return( $self );
}
else
{
return( ( $wantlist // '' ) ? @rv : $rv[0] );
}
}
else
{
return( $self->error( "Unknown JSON method '${meth}'" ) );
}
}
elsif( $code = JSON->can( $meth ) )
{
local $@;
my @rv = eval
{
$code->( scalar( @args ) ? @args : () );
};
if( $@ )
{
return( __PACKAGE__->error( $@ ) );
}
return( wantarray() ? @rv : $rv[0] );
}
else
{
die( "Unknown class function '${meth}' in JSON" );
}
}
1;
# NOTE: POD
__END__
=encoding utf-8
=head1 NAME
Module::Generic::JSON - A thin and reliable wrapper around JSON
=head1 SYNOPSIS
use Module::Generic::JSON;
my $j = Module::Generic::JSON->new(
utf8 => 1,
pretty => 1,
canonical => 1,
relaxed => 1,
allow_nonref => 1,
) || die( Module::Generic::JSON->error );
$j->encode( $some_ref ) || die( $j->error );
Or
my $j = Module::Generic::JSON->new;
$j->utf8->pretty->canonical->relaxed->allow_nonref->encode( $some_ref ) ||
die( $j->error );
Or, even simpler:
use Module::Generic::JSON qw( new_json );
my $j = new_json(
utf8 => 1,
pretty => 1,
canonical => 1,
relaxed => 1,
allow_nonref => 1,
) || die( Module::Generic::JSON->error );
$j->encode( $some_ref ) || die( $j->error );
=head1 VERSION
v0.1.0
=head1 DESCRIPTION
This is a thin and reliable wrapper around the otherwise excellent L<JSON> class. Its added value is:
=over 4
=item * Allow the setting of all the JSON properties upon object instantiation
As mentioned in the synopsis, you can do:
my $j = Module::Generic::JSON->new(
utf8 => 1,
pretty => 1,
canonical => 1,
relaxed => 1,
allow_nonref => 1,
) || die( Module::Generic::JSON->error );
instead of:
my $j = Module::Generic::JSON->new;
$j = $j->utf8->pretty->canonical->relaxed->allow_nonref;
=item * No fatal exception that would kill your process inadvertently.
This is important in a web application where you do not want some module killing your process, but rather you want the exception to be handled gracefully.
Thus, instead of having to do:
local $@;
my $ref = eval{ $j->decode( $payload ) };
if( $@ )
{
# Like returning a 500 or maybe 400 HTTP error
bailout_gracefully( $@ );
}
you can simply do:
my $ref = $j->decode( $payload ) || bailout_gracefully( $j->error );
=item * Upon error, it returns an L<exception object|Module::Generic::Exception>
=item * All methods calls are passed through to L<JSON>, and any exception is caught, and handled properly for you.
=back
For L<class functions|/"CLASS FUNCTIONS"> too, you can execute them safely and catch error, if any, by calling C<< Module::Generic::JSON->error >>, so for example:
decode_json( $some_data ) || die( Module::Generic::JSON->error );
=head1 CONSTRUCTOR
=head2 new
This takes an hash or an hash reference of options and it returns a new L<Module::Generic::JSON> object.
The options provided must be supported by L<JSON>.
Upon error, this sets an L<error object|Module::Generic::JSON>, and returns C<undef> in scalar context, or an empty list in list context.
=head1 METHODS
See the documentation for the module L<JSON> for more information, but below are the known methods supported by L<JSON>
=head2 allow_blessed
=head2 allow_nonref
=head2 allow_tags
=head2 allow_unknown
=head2 ascii
=head2 backend
=head2 boolean
=head2 boolean_values
=head2 canonical
=head2 convert_blessed
=head2 decode
=head2 decode_prefix
=head2 encode
=head2 filter_json_object
=head2 filter_json_single_key_object
=head2 indent
=head2 is_pp
=head2 is_xs
=head2 latin1
=head2 max_depth
=head2 max_size
=head2 pretty
=head2 property
=head2 relaxed
=head2 space_after
=head2 space_before
=head2 utf8
=head1 CLASS FUNCTIONS
=head2 decode_json
=head2 encode_json
=head2 from_json
=head2 to_json
=head1 AUTHOR
Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
=head1 SEE ALSO
L<JSON>, L<Module::Generic::Exception>
=head1 COPYRIGHT & LICENSE
Copyright(c) 2025 DEGUEST Pte. Ltd.
All rights reserved.
This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
=cut