our
$VERSION
=
'0.607'
;
use
5.020;
no
autovivification
warn
=>
qw(fetch store exists delete)
;
use
if
"$]"
>= 5.022,
experimental
=>
're_strict'
;
no
if
"$]"
>= 5.031009,
feature
=>
'indirect'
;
no
if
"$]"
>= 5.033001,
feature
=>
'multidimensional'
;
no
if
"$]"
>= 5.033006,
feature
=>
'bareword_filehandles'
;
use
Ref::Util 0.100
qw(is_ref is_plain_arrayref is_plain_hashref)
;
our
@EXPORT_OK
=
qw(
is_type
get_type
is_bignum
is_equal
is_elements_unique
jsonp
unjsonp
local_annotations
canonical_uri
E
A
abort
assert_keyword_exists
assert_keyword_type
assert_pattern
assert_uri_reference
assert_uri
annotate_self
sprintf_num
true
false
)
;
use
constant {
true
=> JSON::PP::true,
false
=> JSON::PP::false };
sub
is_type (
$type
,
$value
,
$config
= {}) {
if
(
$type
eq
'null'
) {
return
!(
defined
$value
);
}
if
(
$type
eq
'boolean'
) {
return
is_bool(
$value
);
}
if
(
$type
eq
'object'
) {
return
is_plain_hashref(
$value
);
}
if
(
$type
eq
'array'
) {
return
is_plain_arrayref(
$value
);
}
if
(
$type
eq
'string'
or
$type
eq
'number'
or
$type
eq
'integer'
) {
return
0
if
not
defined
$value
;
my
$flags
= B::svref_2object(\
$value
)->FLAGS;
if
(
$type
eq
'string'
) {
return
!is_ref(
$value
)
&&
$flags
& B::SVf_POK
&& (!(
$flags
& (B::SVf_IOK | B::SVf_NOK))
||
do
{
no
warnings
'numeric'
; 0+
$value
eq
$value
});
}
if
(
$type
eq
'number'
) {
return
is_bignum(
$value
) || created_as_number(
$value
);
}
if
(
$type
eq
'integer'
) {
if
(
$config
->{legacy_ints}) {
return
ref
(
$value
) eq
'Math::BigInt'
|| (
$flags
& B::SVf_IOK) && !(
$flags
& B::SVf_NOK) && created_as_number(
$value
);
}
else
{
return
is_bignum(
$value
) &&
$value
->is_int
|| created_as_number(
$value
) &&
int
(
$value
) ==
$value
;
}
}
}
if
(
$type
=~ /^reference to (.+)$/) {
return
!blessed(
$value
) &&
ref
(
$value
) eq $1;
}
return
ref
(
$value
) eq
$type
;
}
sub
get_type (
$value
,
$config
= {}) {
return
'object'
if
is_plain_hashref(
$value
);
return
'boolean'
if
is_bool(
$value
);
return
'null'
if
not
defined
$value
;
return
'array'
if
is_plain_arrayref(
$value
);
if
(is_ref(
$value
)) {
my
$ref
=
ref
(
$value
);
return
$ref
eq
'Math::BigInt'
?
'integer'
:
$ref
eq
'Math::BigFloat'
? (
$value
->is_int ?
'integer'
:
'number'
)
: (
defined
blessed(
$value
) ?
''
:
'reference to '
).
$ref
;
}
my
$flags
= B::svref_2object(\
$value
)->FLAGS;
return
'string'
if
$flags
& B::SVf_POK
&& (!(
$flags
& (B::SVf_IOK | B::SVf_NOK))
||
do
{
no
warnings
'numeric'
; 0+
$value
eq
$value
});
if
(
$config
->{legacy_ints}) {
return
(
$flags
& B::SVf_IOK) && !(
$flags
& B::SVf_NOK) ?
'integer'
:
'number'
if
created_as_number(
$value
);
}
else
{
return
int
(
$value
) ==
$value
?
'integer'
:
'number'
if
created_as_number(
$value
);
}
return
'ambiguous type'
;
}
use
constant
HAVE_BUILTIN
=>
"$]"
>= 5.035010;
use
if
HAVE_BUILTIN,
experimental
=>
'builtin'
;
sub
is_bool (
$value
) {
HAVE_BUILTIN and builtin::is_bool(
$value
)
or
!!blessed(
$value
)
and (
$value
->isa(
'JSON::PP::Boolean'
)
or
$value
->isa(
'Cpanel::JSON::XS::Boolean'
)
or
$value
->isa(
'JSON::XS::Boolean'
));
}
sub
is_bignum (
$value
) {
ref
(
$value
) =~ /^Math::Big(?:Int|Float)$/;
}
sub
is_equal (
$x
,
$y
,
$state
= {}) {
$state
->{path} //=
''
;
my
@types
=
map
get_type(
$_
),
$x
,
$y
;
$state
->{error} =
'ambiguous type encountered'
,
return
0
if
grep
$types
[
$_
] eq
'ambiguous type'
, 0..1;
if
(
$state
->{scalarref_booleans}) {
(
$x
,
$types
[0]) = (0+!!
$$x
,
'boolean'
)
if
$types
[0] eq
'reference to SCALAR'
;
(
$y
,
$types
[1]) = (0+!!
$$y
,
'boolean'
)
if
$types
[1] eq
'reference to SCALAR'
;
}
if
(
$state
->{stringy_numbers}) {
(
$x
,
$types
[0]) = (0+
$x
,
int
(0+
$x
) ==
$x
?
'integer'
:
'number'
)
if
$types
[0] eq
'string'
and looks_like_number(
$x
);
(
$y
,
$types
[1]) = (0+
$y
,
int
(0+
$y
) ==
$y
?
'integer'
:
'number'
)
if
$types
[1] eq
'string'
and looks_like_number(
$y
);
}
$state
->{error} =
"wrong type: $types[0] vs $types[1]"
,
return
0
if
$types
[0] ne
$types
[1];
return
1
if
$types
[0] eq
'null'
;
(
$x
eq
$y
and
return
1),
$state
->{error} =
'strings not equal'
,
return
0
if
$types
[0] eq
'string'
;
(
$x
==
$y
and
return
1),
$state
->{error} =
"$types[0]s not equal"
,
return
0
if
grep
$types
[0] eq
$_
,
qw(boolean number integer)
;
my
$path
=
$state
->{path};
if
(
$types
[0] eq
'object'
) {
$state
->{error} =
'property count differs: '
.
keys
(
%$x
).
' vs '
.
keys
(
%$y
),
return
0
if
keys
%$x
!=
keys
%$y
;
if
(not is_equal(
my
$arr_x
= [
sort
keys
%$x
],
my
$arr_y
= [
sort
keys
%$y
],
my
$s
={})) {
my
$pos
=
substr
(
$s
->{path}, 1);
$state
->{error} =
'property names differ starting at position '
.
$pos
.
' ("'
.
$arr_x
->[
$pos
].
'" vs "'
.
$arr_y
->[
$pos
].
'")'
;
return
0;
}
foreach
my
$property
(
sort
keys
%$x
) {
$state
->{path} = jsonp(
$path
,
$property
);
return
0
if
not is_equal(
$x
->{
$property
},
$y
->{
$property
},
$state
);
}
return
1;
}
if
(
$types
[0] eq
'array'
) {
$state
->{error} =
'element count differs: '
.
@$x
.
' vs '
.
@$y
,
return
0
if
@$x
!=
@$y
;
foreach
my
$idx
(0 ..
$x
->$
$state
->{path} =
$path
.
'/'
.
$idx
;
return
0
if
not is_equal(
$x
->[
$idx
],
$y
->[
$idx
],
$state
);
}
return
1;
}
$state
->{error} =
'uh oh'
,
return
0;
}
sub
is_elements_unique (
$array
,
$equal_indices
=
undef
,
$state
= {}) {
my
%s
=
$state
->%{
qw(scalarref_booleans stringy_numbers)
};
foreach
my
$idx0
(0 ..
$array
->$
foreach
my
$idx1
(
$idx0
+1 ..
$array
->$
if
(is_equal(
$array
->[
$idx0
],
$array
->[
$idx1
], \
%s
)) {
push
@$equal_indices
,
$idx0
,
$idx1
if
defined
$equal_indices
;
return
0;
}
}
}
return
1;
}
sub
jsonp {
warn
q{first argument to jsonp should be '' or start with '/'}
if
length
(
$_
[0]) and
substr
(
$_
[0],0,1) ne
'/'
;
return
join
(
'/'
,
shift
,
map
s/~/~0/gr =~ s!/!~1!gr,
grep
defined
,
@_
);
}
sub
unjsonp (
$path
) {
return
map
s!~0!~!gr =~ s!~1!/!gr,
split
m!/!,
$path
;
}
sub
local_annotations (
$state
) {
grep
$_
->{instance_location} eq
$state
->{data_path},
$state
->{annotations}->@*;
}
sub
canonical_uri (
$state
,
@extra_path
) {
return
$state
->{initial_schema_uri}
if
not
@extra_path
and not
length
(
$state
->{schema_path});
my
$uri
=
$state
->{initial_schema_uri}->clone;
my
$fragment
= (
$uri
->fragment//
''
).(
@extra_path
? jsonp(
$state
->{schema_path},
@extra_path
) :
$state
->{schema_path});
undef
$fragment
if
not
length
(
$fragment
);
$uri
->fragment(
$fragment
);
$uri
;
}
sub
E (
$state
,
$error_string
,
@args
) {
croak
'E called in void context'
if
not
defined
wantarray
;
my
$sps
=
delete
$state
->{_schema_path_suffix};
my
@schema_path_suffix
=
defined
$sps
&& is_plain_arrayref(
$sps
) ?
$sps
->@* :
$sps
//();
my
$uri
= [
$state
->{initial_schema_uri},
$state
->{schema_path}, (
$state
->{keyword}//()),
@schema_path_suffix
,
$state
->{effective_base_uri} ];
my
$keyword_location
=
$state
->{traversed_schema_path}
.jsonp(
$state
->{schema_path},
$state
->{keyword},
@schema_path_suffix
);
push
$state
->{errors}->@*, JSON::Schema::Modern::Error->new(
depth
=>
$state
->{depth} // 0,
keyword
=>
$state
->{keyword},
instance_location
=>
$state
->{data_path},
keyword_location
=>
$keyword_location
,
_uri
=>
$uri
,
error
=>
@args
?
sprintf
(
$error_string
,
@args
) :
$error_string
,
$state
->{exception} ? (
exception
=>
$state
->{exception} ) : (),
$state
->{recommended_response} ? (
recommended_response
=>
$state
->{recommended_response} ) : (),
mode
=>
$state
->{traverse} ?
'traverse'
:
'evaluate'
,
);
return
0;
}
sub
A (
$state
,
$annotation
) {
return
1
if
not
$state
->{collect_annotations};
my
$uri
= [
$state
->{initial_schema_uri},
$state
->{schema_path},
$state
->{keyword},
$state
->{effective_base_uri} ];
my
$keyword_location
=
$state
->{traversed_schema_path}
.jsonp(
$state
->{schema_path},
$state
->{keyword});
push
$state
->{annotations}->@*, {
depth
=>
$state
->{depth} // 0,
keyword
=>
$state
->{keyword},
instance_location
=>
$state
->{data_path},
keyword_location
=>
$keyword_location
,
_uri
=>
$uri
,
annotation
=>
$annotation
,
$state
->{_unknown} ? (
unknown
=> 1 ) : (),
};
return
1;
}
sub
abort (
$state
,
$error_string
,
@args
) {
()= E({
%$state
,
exception
=> 1 },
$error_string
,
@args
);
croak
'abort() called during traverse'
if
$state
->{traverse};
die
pop
$state
->{errors}->@*;
}
sub
assert_keyword_exists (
$state
,
$schema
) {
croak
'assert_keyword_exists called in void context'
if
not
defined
wantarray
;
return
E(
$state
,
'%s keyword is required'
,
$state
->{keyword})
if
not
exists
$schema
->{
$state
->{keyword}};
return
1;
}
sub
assert_keyword_type (
$state
,
$schema
,
$type
) {
croak
'assert_keyword_type called in void context'
if
not
defined
wantarray
;
return
1
if
is_type(
$type
,
$schema
->{
$state
->{keyword}});
E(
$state
,
'%s value is not a%s %s'
,
$state
->{keyword}, (
$type
=~ /^[aeiou]/ ?
'n'
:
''
),
$type
);
}
sub
assert_pattern (
$state
,
$pattern
) {
croak
'assert_pattern called in void context'
if
not
defined
wantarray
;
try
{
local
$SIG
{__WARN__} =
sub
{
die
@_
};
qr/$pattern/
;
}
catch
(
$e
) {
return
E(
$state
,
$e
); };
return
1;
}
sub
assert_uri_reference (
$state
,
$schema
) {
croak
'assert_uri_reference called in void context'
if
not
defined
wantarray
;
my
$string
=
$schema
->{
$state
->{keyword}};
return
E(
$state
,
'%s value is not a valid URI reference'
,
$state
->{keyword})
if
fc(Mojo::URL->new(
$string
)->to_unsafe_string) ne fc(
$string
)
or
$string
=~ /[^[:ascii:]]/
or
$string
=~ /
and
$string
!~ m{
and
$string
!~ m{
and
$string
!~ m{
return
1;
}
sub
assert_uri (
$state
,
$schema
,
$override
=
undef
) {
croak
'assert_uri called in void context'
if
not
defined
wantarray
;
my
$string
=
$override
//
$schema
->{
$state
->{keyword}};
my
$uri
= Mojo::URL->new(
$string
);
return
E(
$state
,
'"%s" is not a valid URI'
,
$string
)
if
fc(
$uri
->to_unsafe_string) ne fc(
$string
)
or
$string
=~ /[^[:ascii:]]/
or not
$uri
->is_abs
or
$string
=~ /
and
$string
!~ m{
and
$string
!~ m{
and
$string
!~ m{
return
1;
}
sub
annotate_self (
$state
,
$schema
) {
A(
$state
, is_ref(
$schema
->{
$state
->{keyword}}) ? dclone(
$schema
->{
$state
->{keyword}})
:
$schema
->{
$state
->{keyword}});
}
sub
sprintf_num (
$value
) {
is_bignum(
$value
) ?
$value
->bstr :
sprintf
(
'%s'
,
$value
);
}
1;