—##----------------------------------------------------------------------------
## 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.
##----------------------------------------------------------------------------
package
Module::Generic::JSON;
BEGIN
{
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;
use
warnings;
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