our
@EXPORT_OK
= (
'analyze'
);
our
$VERSION
=
'1.6'
;
sub
_merge_type {
my
(
$c
,
$o
) =
@_
;
my
$n
=
$c
->{name}||
''
;
return
if
$o
->{type} eq
'int'
||
$o
->{type} eq
'bool'
;
$o
->{type} =
'int'
if
$n
eq
'int'
||
$n
eq
'uint'
;
$o
->{type} =
'bool'
if
$n
eq
'anybool'
||
$n
eq
'undefbool'
||
$n
eq
'jsonbool'
;
$o
->{type} =
'num'
if
$n
eq
'num'
;
}
sub
_merge {
my
(
$c
,
$o
) =
@_
;
_merge_type
$c
,
$o
;
$o
->{required} = 1
if
(
$c
->{name}||
''
) eq
'anybool'
;
$o
->{
values
} = _merge_toplevel(
$c
->{schema}{
values
},
$o
->{
values
}||{})
if
$c
->{schema}{
values
};
if
(
$c
->{schema}{
keys
}) {
$o
->{
keys
} ||= {};
$o
->{
keys
}{
$_
} = _merge_toplevel(
$c
->{schema}{
keys
}{
$_
},
$o
->{
keys
}{
$_
}||{})
for
keys
%{
$c
->{schema}{
keys
}};
}
$o
->{minlength} =
$c
->{schema}{_analyze_minlength}
if
defined
$c
->{schema}{_analyze_minlength} && (!
defined
$o
->{minlength} ||
$o
->{minlength} <
$c
->{schema}{_analyze_minlength});
$o
->{maxlength} =
$c
->{schema}{_analyze_maxlength}
if
defined
$c
->{schema}{_analyze_maxlength} && (!
defined
$o
->{maxlength} ||
$o
->{maxlength} >
$c
->{schema}{_analyze_maxlength});
$o
->{min} =
$c
->{schema}{_analyze_min}
if
defined
$c
->{schema}{_analyze_min} && (!
defined
$o
->{min} ||
$o
->{min} <
$c
->{schema}{_analyze_min} );
$o
->{max} =
$c
->{schema}{_analyze_max}
if
defined
$c
->{schema}{_analyze_max} && (!
defined
$o
->{max} ||
$o
->{max} >
$c
->{schema}{_analyze_max} );
push
@{
$o
->{regexes}},
$c
->{schema}{_analyze_regex}
if
defined
$c
->{schema}{_analyze_regex};
_merge(
$_
,
$o
)
for
@{
$c
->{validations}};
}
sub
_merge_toplevel {
my
(
$c
,
$o
) =
@_
;
$o
->{required} ||= !
exists
$c
->{schema}{
default
};
$o
->{unknown} ||=
$c
->{schema}{unknown};
$o
->{
default
} =
$c
->{schema}{
default
}
if
exists
$c
->{schema}{
default
};
$o
->{type} =
$c
->{schema}{type}
if
!
$o
->{type} ||
$o
->{type} eq
'any'
;
_merge
$c
,
$o
;
bless
$o
, __PACKAGE__;
}
sub
analyze {
my
$c
=
shift
;
$c
->{analysis} ||= _merge_toplevel
$c
, {};
$c
->{analysis}
}
sub
coerce_for_json {
my
(
$o
,
$obj
,
%opt
) =
@_
;
$opt
{unknown} ||=
$o
->{unknown};
return
undef
if
!
defined
$obj
;
return
$obj
+0
if
$o
->{type} eq
'num'
;
return
int
$obj
if
$o
->{type} eq
'int'
;
return
$obj
? \1 : \0
if
$o
->{type} eq
'bool'
;
return
"$obj"
if
$o
->{type} eq
'scalar'
;
return
[
map
$o
->{
values
}->coerce_for_json(
$_
,
%opt
),
@$obj
]
if
$o
->{type} eq
'array'
&&
$o
->{
values
};
return
{
map
{
$o
->{
keys
}{
$_
} ? (
$_
,
$o
->{
keys
}{
$_
}->coerce_for_json(
$obj
->{
$_
},
%opt
)) :
$opt
{unknown} eq
'pass'
? (
$_
,
$obj
->{
$_
}) :
$opt
{unknown} eq
'remove'
? ()
: croak
"Unknown key '$_' in hash in coerce_for_json()"
}
keys
%$obj
}
if
$o
->{type} eq
'hash'
&&
$o
->{
keys
};
$obj
}
sub
json_type {
my
$o
=
shift
;
return
Cpanel::JSON::XS::Type::JSON_TYPE_FLOAT_OR_NULL()
if
$o
->{type} eq
'num'
;
return
Cpanel::JSON::XS::Type::JSON_TYPE_INT_OR_NULL()
if
$o
->{type} eq
'int'
;
return
Cpanel::JSON::XS::Type::JSON_TYPE_BOOL_OR_NULL()
if
$o
->{type} eq
'bool'
;
return
Cpanel::JSON::XS::Type::JSON_TYPE_STRING_OR_NULL()
if
$o
->{type} eq
'scalar'
;
return
Cpanel::JSON::XS::Type::json_type_null_or_anyof(Cpanel::JSON::XS::Type::json_type_arrayof(
$o
->{
values
} ?
$o
->{
values
}->json_type :
undef
))
if
$o
->{type} eq
'array'
;
return
Cpanel::JSON::XS::Type::json_type_null_or_anyof({
map
+(
$_
,
$o
->{
keys
}{
$_
}->json_type),
keys
%{
$o
->{
keys
}} })
if
$o
->{type} eq
'hash'
&&
$o
->{
keys
};
return
Cpanel::JSON::XS::Type::json_type_null_or_anyof(Cpanel::JSON::XS::Type::json_type_hashof(
undef
))
if
$o
->{type} eq
'hash'
;
undef
}
sub
_re_compat {
local
$_
=
$_
[0];
s/\\@/@/g;
s{\(\?\^?[alupimnsx]*(?:-[imnsx]+)?(?=[:\)])}{(?}g;
$_
}
sub
_join_regexes {
my
%r
=
map
+(
$_
,1), @{
$_
[0]};
my
@r
=
sort
keys
%r
;
_re_compat
join
(
''
,
map
"(?=$_)"
,
@r
[0..
$#r
-1]).
$r
[
$#r
]
}
sub
html5_validation {
my
$o
=
shift
;
+(
$o
->{required} ? (
required
=>
'required'
) : (),
defined
$o
->{minlength} ? (
minlength
=>
$o
->{minlength}) : (),
defined
$o
->{maxlength} ? (
maxlength
=>
$o
->{maxlength}) : (),
defined
$o
->{min} ? (
min
=>
$o
->{min} ) : (),
defined
$o
->{max} ? (
max
=>
$o
->{max} ) : (),
$o
->{regexes} ? (
pattern
=> _join_regexes
$o
->{regexes}) : (),
);
}
sub
elm_type {
my
(
$o
,
%opt
) =
@_
;
my
$par
=
delete
$opt
{_need_parens} ?
sub
{
"($_[0])"
} :
sub
{
$_
[0] };
return
$par
->(
'Maybe '
.
$o
->elm_type(
%opt
,
required
=> 1,
_need_parens
=> 1))
if
(
ref
$o
->{
default
} eq
'CODE'
|| (!
$o
->{required} && !
defined
$o
->{
default
})) && !
$opt
{required};
delete
$opt
{required};
return
'String'
if
$o
->{type} eq
'scalar'
;
return
'Bool'
if
$o
->{type} eq
'bool'
;
return
'Float'
if
$o
->{type} eq
'num'
;
return
'Int'
if
$o
->{type} eq
'int'
;
return
$opt
{any}
if
$o
->{type} eq
'any'
&&
$opt
{any};
return
$par
->( (
$opt
{array} ||
'List'
) .
' '
. (
$opt
{
values
} ||
$o
->{
values
}->elm_type(
%opt
,
_need_parens
=> 1)) )
if
$o
->{type} eq
'array'
&& (
$opt
{
values
} ||
$o
->{
values
});
if
(
$o
->{type} eq
'hash'
&& (
$o
->{
keys
} ||
$opt
{
keys
})) {
$opt
{indent} //= 2;
$opt
{level} //= 1;
my
$len
= 0;
$len
=
length
$_
>
$len
?
length
$_
:
$len
for
keys
%{
$o
->{
keys
}};
my
$r
=
"\n{ "
.
join
(
"\n, "
,
map
{
sprintf
"%-*s : %s"
,
$len
,
$_
,
$opt
{
keys
}{
$_
} ||
$o
->{
keys
}{
$_
}->elm_type(
%opt
,
level
=>
$opt
{level}+1);
}
sort
keys
%{
$o
->{
keys
}}) .
"\n}"
;;
$r
=~ s/\n/
$opt
{indent} ?
"\n"
. (
' '
x(
$opt
{indent}
*$opt
{level})) :
''
/eg;
return
$r
;
}
croak
"Unknown type '$o->{type}' or missing option"
;
}
sub
elm_encoder {
my
(
$o
,
%opt
) =
@_
;
$opt
{json_encode} //=
''
;
$opt
{var_prefix} //=
'e'
;
$opt
{var_num} //= 0;
return
sprintf
'(Maybe.withDefault %snull << Maybe.map %s)'
,
$opt
{json_encode},
$opt
{
values
} ||
$o
->elm_encoder(
%opt
,
required
=> 1)
if
(
ref
$o
->{
default
} eq
'CODE'
|| (!
$o
->{required} && !
defined
$o
->{
default
})) && !
$opt
{required};
delete
$opt
{required};
return
"$opt{json_encode}string"
if
$o
->{type} eq
'scalar'
;
return
"$opt{json_encode}bool"
if
$o
->{type} eq
'bool'
;
return
"$opt{json_encode}float"
if
$o
->{type} eq
'num'
;
return
"$opt{json_encode}int"
if
$o
->{type} eq
'int'
;
return
$opt
{any}
if
$o
->{type} eq
'any'
&&
$opt
{any};
return
sprintf
'(%slist %s)'
,
$opt
{json_encode},
$opt
{
values
} ||
$o
->{
values
}->elm_encoder(
%opt
)
if
$o
->{type} eq
'array'
&& (
$opt
{
values
} ||
$o
->{
values
});
if
(
$o
->{type} eq
'hash'
&& (
$o
->{
keys
} ||
$opt
{
keys
})) {
$opt
{indent} //= 2;
$opt
{level} //= 1;
my
$len
= 0;
$len
=
length
$_
>
$len
?
length
$_
:
$len
for
keys
%{
$o
->{
keys
}};
my
$var
=
$opt
{var_prefix}.
$opt
{var_num};
my
$r
=
sprintf
"(\\%s -> %sobject\n[ %s\n])"
,
$var
,
$opt
{json_encode},
join
"\n, "
,
map
{
sprintf
'("%s",%s %s %s.%1$s)'
,
$_
,
' '
x(
$len
-(
length
$_
)),
$opt
{
keys
}{
$_
} ||
$o
->{
keys
}{
$_
}->elm_encoder(
%opt
,
level
=>
$opt
{level}+1,
var_num
=>
$opt
{var_num}+1),
$var
;
}
sort
keys
%{
$o
->{
keys
}};
$r
=~ s/\n/
$opt
{indent} ?
"\n"
. (
' '
x(
$opt
{indent}
*$opt
{level})) :
''
/eg;
return
$r
;
}
croak
"Unknown type '$o->{type}' or missing option"
;
}
sub
elm_decoder {
my
(
$o
,
%opt
) =
@_
;
$opt
{json_decode} //=
''
;
$opt
{var_prefix} //=
'd'
;
return
sprintf
'(%snullable %s)'
,
$opt
{json_decode},
$opt
{
values
} ||
$o
->elm_decoder(
%opt
,
required
=> 1)
if
!
$o
->{required} && !
defined
$o
->{
default
} && !
$opt
{required};
delete
$opt
{required};
return
"$opt{json_decode}string"
if
$o
->{type} eq
'scalar'
;
return
"$opt{json_decode}bool"
if
$o
->{type} eq
'bool'
;
return
"$opt{json_decode}float"
if
$o
->{type} eq
'num'
;
return
"$opt{json_decode}int"
if
$o
->{type} eq
'int'
;
return
$opt
{any}
if
$o
->{type} eq
'any'
&&
$opt
{any};
return
"$opt{json_decode}value"
if
$o
->{type} eq
'any'
;
return
sprintf
'(%slist %s)'
,
$opt
{json_decode},
$opt
{
values
} ||
$o
->{
values
}->elm_decoder(
%opt
)
if
$o
->{type} eq
'array'
&& (
$opt
{
values
} ||
$o
->{
values
});
if
(
$o
->{type} eq
'hash'
&& (
$o
->{
keys
} ||
$opt
{
keys
})) {
$opt
{indent} //= 2;
$opt
{level} //= 1;
my
$len
= 0;
$len
=
length
$_
>
$len
?
length
$_
:
$len
for
keys
%{
$o
->{
keys
}};
my
$r
;
my
$num
=
keys
%{
$o
->{
keys
}};
my
$varnum
= 1;
my
$getvar
=
sub
{
$opt
{var_prefix}.(
$varnum
++) };
if
(
$num
<= 8) {
my
(
@fnarg
,
@assign
,
@fetch
);
for
(
sort
keys
%{
$o
->{
keys
}}) {
my
$var
=
$getvar
->();
push
@fnarg
,
$var
;
push
@assign
,
"$_ = $var"
;
push
@fetch
,
sprintf
'(%sfield "%s"%s %s)'
,
$opt
{json_decode},
$_
,
' '
x(
$len
-(
length
$_
)),
$opt
{
keys
}{
$_
} ||
$o
->{
keys
}{
$_
}->elm_decoder(
%opt
,
var_prefix
=>
$var
,
level
=>
$opt
{level}+1);
}
$r
=
sprintf
"(%smap%s\n(\\%s -> { %s })\n%s)"
,
$opt
{json_decode},
$num
== 1 ?
''
:
$num
,
join
(
' '
,
@fnarg
),
join
(
', '
,
@assign
),
join
(
"\n"
,
@fetch
);
}
else
{
my
(
$dict
,
$fn
,
$name
,
$dec
,
$next
,
$cap
) =
map
$getvar
->(), 1..6;
my
(
@assign
,
@fn
);
for
(
sort
keys
%{
$o
->{
keys
}}) {
my
$var
=
$getvar
->();
push
@assign
,
"$_ = $var"
;
push
@fn
,
sprintf
'%s "%s"%s %s (\%s ->'
,
$fn
,
$_
,
' '
x(
$len
-(
length
$_
)),
$opt
{
keys
}{
$_
} ||
$o
->{
keys
}{
$_
}->elm_decoder(
%opt
,
var_prefix
=>
"${var}_"
,
level
=>
$opt
{level}+1),
$var
;
}
my
$spc
=
' '
x(12 +
length
(
$fn
) +
length
(
$name
) +
length
(
$dec
) +
length
(
$next
));
$r
=
"($opt{json_decode}andThen (\\$dict -> \n"
.
"let $fn $name $dec $next = case Maybe.map ($opt{json_decode}decodeValue $dec) (Dict.get $name $dict) of\n"
.
"${spc}Nothing -> $opt{json_decode}fail (\"Missing key '\"++$name++\"'\")\n"
.
"${spc}Just (Err $cap) -> $opt{json_decode}fail (\"Error decoding value of '\"++$name++\"': \"++($opt{json_decode}errorToString $cap))\n"
.
"${spc}Just (Ok $cap) -> $next $cap\n"
.
"in "
.
join
(
"\n "
,
@fn
).
"\n"
.
" $opt{json_decode}succeed { "
.
join
(
', '
,
@assign
).
" }\n"
.
')'
.(
')'
x
@fn
).
" ($opt{json_decode}dict $opt{json_decode}value))"
;
}
$r
=~ s/\n/
$opt
{indent} ?
"\n"
. (
' '
x(
$opt
{indent}
*$opt
{level})) :
''
/eg;
return
$r
;
}
croak
"Unknown type '$o->{type}' or missing option"
;
}
1;