Skip to content

Commit

Permalink
Provide type support when decoding JSON string
Browse files Browse the repository at this point in the history
JSON decoder fills optional third argument of Cpanel::JSON::XS::decode_json
function and optional second argument of ->decode method with original JSON
types from input string.

Examples:

my $value = decode_json('false', 1, my $type);
 $value is 0 and $type is JSON_TYPE_BOOL

my $value = decode_json('0', 1, my $type);
 $value is 0 and $type is JSON_TYPE_INT

my $value = decode_json('"0"', 1, my $type);
 $value is 0 and $type is JSON_TYPE_STRING

my $struct = Cpanel::JSON::XS->new->decode('[null,1,1.1,"1",[0],true]', my $type);
 $struct is [undef, 1, 1.1, '1', [0], 1]
 $value is [JSON_TYPE_NULL, JSON_TYPE_INT, JSON_TYPE_FLOAT, JSON_TYPE_STRING, [JSON_TYPE_INT], JSON_TYPE_BOOL]

This allows users to correctly distinguish between booleans, integers,
floats and strings in Perl language where perl interpreter can coerce
between these types at any time.
  • Loading branch information
pali committed Aug 10, 2018
1 parent b9f6d24 commit c0423f0
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 25 deletions.
111 changes: 87 additions & 24 deletions XS.xs
Expand Up @@ -231,6 +231,9 @@ mingw_modfl(long double x, long double *ip)
/* flags */
#define JSON_TYPE_CAN_BE_NULL 0x0100

/* null type */
#define JSON_TYPE_NULL JSON_TYPE_CAN_BE_NULL

/* classes */
#define JSON_TYPE_CLASS "Cpanel::JSON::XS::Type"
#define JSON_TYPE_ARRAYOF_CLASS "Cpanel::JSON::XS::Type::ArrayOf"
Expand Down Expand Up @@ -2190,7 +2193,7 @@ decode_ws (dec_t *dec)
#define DEC_INC_DEPTH if (++dec->depth > dec->json.max_depth) ERR (ERR_NESTING_EXCEEDED)
#define DEC_DEC_DEPTH --dec->depth

static SV *decode_sv (pTHX_ dec_t *dec);
static SV *decode_sv (pTHX_ dec_t *dec, SV *typesv);

/* #regen code
my $i;
Expand Down Expand Up @@ -3008,7 +3011,7 @@ decode_str_sq (pTHX_ dec_t *dec)
}

static SV *
decode_num (pTHX_ dec_t *dec)
decode_num (pTHX_ dec_t *dec, SV *typesv)
{
int is_nv = 0;
char *start = dec->cur;
Expand Down Expand Up @@ -3073,6 +3076,9 @@ decode_num (pTHX_ dec_t *dec)
{
int len = dec->cur - start;

if (typesv)
sv_setiv_mg (typesv, JSON_TYPE_INT);

/* special case the rather common 1..5-digit-int case */
if (*start == '-')
switch (len)
Expand Down Expand Up @@ -3136,6 +3142,9 @@ decode_num (pTHX_ dec_t *dec)
return newSVpvn (start, dec->cur - start);
}

if (typesv)
sv_setiv_mg (typesv, JSON_TYPE_FLOAT);

if (dec->json.flags & F_ALLOW_BIGNUM) {
SV* pv = newSVpvs("require Math::BigFloat && return Math::BigFloat->new(\"");
sv_catpvn(pv, start, dec->cur - start);
Expand All @@ -3158,21 +3167,37 @@ fail:
}

static SV *
decode_av (pTHX_ dec_t *dec)
decode_av (pTHX_ dec_t *dec, SV *typesv)
{
AV *av = newAV ();
AV *typeav = NULL;
SV *typerv;

DEC_INC_DEPTH;
decode_ws (dec);

if (typesv)
{
typeav = newAV ();
typerv = newRV_noinc ((SV *)typeav);
SvSetMagicSV (typesv, typerv);
}

if (*dec->cur == ']')
++dec->cur;
else
for (;;)
{
SV *value;
SV *value_typesv = NULL;

if (typesv)
{
value_typesv = newSV (0);
av_push (typeav, value_typesv);
}

value = decode_sv (aTHX_ dec);
value = decode_sv (aTHX_ dec, value_typesv);
if (!value)
goto fail;

Expand Down Expand Up @@ -3210,10 +3235,12 @@ fail:
}

static SV *
decode_hv (pTHX_ dec_t *dec)
decode_hv (pTHX_ dec_t *dec, SV *typesv)
{
SV *sv;
HV *hv = newHV ();
HV *typehv = NULL;
SV *typerv;
int allow_squote = dec->json.flags & F_ALLOW_SQUOTE;
int allow_barekey = dec->json.flags & F_ALLOW_BAREKEY;
int relaxed = dec->json.flags & F_RELAXED;
Expand All @@ -3222,6 +3249,13 @@ decode_hv (pTHX_ dec_t *dec)
DEC_INC_DEPTH;
decode_ws (dec);

if (typesv)
{
typehv = newHV ();
typerv = newRV_noinc ((SV *)typehv);
SvSetMagicSV (typesv, typerv);
}

if (*dec->cur == '}')
++dec->cur;
else
Expand Down Expand Up @@ -3252,6 +3286,7 @@ decode_hv (pTHX_ dec_t *dec)
/* the overhead of decode_str + hv_store_ent. */
{
SV *value;
SV *value_typesv = NULL;
char *p = dec->cur;
char *e = p + 24; /* only try up to 24 bytes */

Expand All @@ -3271,7 +3306,14 @@ decode_hv (pTHX_ dec_t *dec)
decode_ws (dec); EXPECT_CH (':');

decode_ws (dec);
value = decode_sv (aTHX_ dec);

if (typesv)
{
value_typesv = newSV (0);
hv_store_ent (typehv, key, value_typesv, 0);
}

value = decode_sv (aTHX_ dec, value_typesv);
if (!value)
{
SvREFCNT_dec (key);
Expand Down Expand Up @@ -3307,7 +3349,14 @@ decode_hv (pTHX_ dec_t *dec)
decode_ws (dec); if (*p != ':') EXPECT_CH (':');

decode_ws (dec);
value = decode_sv (aTHX_ dec);

if (typesv)
{
value_typesv = newSV (0);
hv_store (typehv, key, len, value_typesv, 0);
}

value = decode_sv (aTHX_ dec, value_typesv);
if (!value)
goto fail;

Expand Down Expand Up @@ -3434,7 +3483,7 @@ decode_tag (pTHX_ dec_t *dec)

decode_ws (dec);

tag = decode_sv (aTHX_ dec);
tag = decode_sv (aTHX_ dec, NULL);
if (!tag)
goto fail;

Expand All @@ -3450,7 +3499,7 @@ decode_tag (pTHX_ dec_t *dec)

decode_ws (dec);

val = decode_sv (aTHX_ dec);
val = decode_sv (aTHX_ dec, NULL);
if (!val)
goto fail;

Expand Down Expand Up @@ -3505,33 +3554,42 @@ fail:
}

static SV *
decode_sv (pTHX_ dec_t *dec)
decode_sv (pTHX_ dec_t *dec, SV *typesv)
{
/* the beauty of JSON: you need exactly one character lookahead */
/* to parse everything. */
switch (*dec->cur)
{
case '"': ++dec->cur; return decode_str (aTHX_ dec);
case '"':
++dec->cur;
if (typesv)
sv_setiv_mg (typesv, JSON_TYPE_STRING);
return decode_str (aTHX_ dec);
case 0x27:
if (dec->json.flags & F_ALLOW_SQUOTE) {
++dec->cur; return decode_str_sq (aTHX_ dec);
++dec->cur;
if (typesv)
sv_setiv_mg (typesv, JSON_TYPE_STRING);
return decode_str_sq (aTHX_ dec);
}
ERR ("malformed JSON string, neither tag, array, object, number, string or atom");
break;
case '[': ++dec->cur; return decode_av (aTHX_ dec);
case '{': ++dec->cur; return decode_hv (aTHX_ dec);
case '[': ++dec->cur; return decode_av (aTHX_ dec, typesv);
case '{': ++dec->cur; return decode_hv (aTHX_ dec, typesv);
case '(': return decode_tag (aTHX_ dec);

case '-':
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
return decode_num (aTHX_ dec);
return decode_num (aTHX_ dec, typesv);

case 't':
if (dec->end - dec->cur >= 4 && memEQc(dec->cur, "true"))
{
dMY_CXT;
dec->cur += 4;
if (typesv)
sv_setiv_mg (typesv, JSON_TYPE_BOOL);
return newSVsv(MY_CXT.json_true);
}
else
Expand All @@ -3544,6 +3602,8 @@ decode_sv (pTHX_ dec_t *dec)
{
dMY_CXT;
dec->cur += 5;
if (typesv)
sv_setiv_mg (typesv, JSON_TYPE_BOOL);
return newSVsv(MY_CXT.json_false);
}
else
Expand All @@ -3555,6 +3615,8 @@ decode_sv (pTHX_ dec_t *dec)
if (dec->end - dec->cur >= 4 && memEQc(dec->cur, "null"))
{
dec->cur += 4;
if (typesv)
sv_setiv_mg (typesv, JSON_TYPE_NULL);
return newSVsv(&PL_sv_undef);
}
else
Expand Down Expand Up @@ -3620,7 +3682,7 @@ decode_bom(pTHX_ const char* encoding, SV* string, STRLEN offset)
}

static SV *
decode_json (pTHX_ SV *string, JSON *json, STRLEN *offset_return)
decode_json (pTHX_ SV *string, JSON *json, STRLEN *offset_return, SV *typesv)
{
dec_t dec;
SV *sv;
Expand Down Expand Up @@ -3724,7 +3786,7 @@ decode_json (pTHX_ SV *string, JSON *json, STRLEN *offset_return)
*dec.end = 0; /* this should basically be a nop, too, but make sure it's there */

decode_ws (&dec);
sv = decode_sv (aTHX_ &dec);
sv = decode_sv (aTHX_ &dec, typesv);

if (offset_return) {
if (dec.cur < SvPVX (string) || dec.cur > SvEND (string))
Expand Down Expand Up @@ -3956,6 +4018,7 @@ BOOT:
newCONSTSUB(stash, "JSON_TYPE_INT", newSViv(JSON_TYPE_INT));
newCONSTSUB(stash, "JSON_TYPE_FLOAT", newSViv(JSON_TYPE_FLOAT));
newCONSTSUB(stash, "JSON_TYPE_STRING", newSViv(JSON_TYPE_STRING));
newCONSTSUB(stash, "JSON_TYPE_NULL", newSViv(JSON_TYPE_NULL));
newCONSTSUB(stash, "JSON_TYPE_INT_OR_NULL", newSViv(JSON_TYPE_INT | JSON_TYPE_CAN_BE_NULL));
newCONSTSUB(stash, "JSON_TYPE_BOOL_OR_NULL", newSViv(JSON_TYPE_BOOL | JSON_TYPE_CAN_BE_NULL));
newCONSTSUB(stash, "JSON_TYPE_FLOAT_OR_NULL", newSViv(JSON_TYPE_FLOAT | JSON_TYPE_CAN_BE_NULL));
Expand Down Expand Up @@ -4161,17 +4224,17 @@ void encode (JSON *self, SV *scalar, SV *typesv = &PL_sv_undef)
PUTBACK; scalar = encode_json (aTHX_ scalar, self, typesv); SPAGAIN;
XPUSHs (scalar);

void decode (JSON *self, SV *jsonstr)
void decode (JSON *self, SV *jsonstr, SV *typesv = NULL)
PPCODE:
PUTBACK; jsonstr = decode_json (aTHX_ jsonstr, self, 0); SPAGAIN;
PUTBACK; jsonstr = decode_json (aTHX_ jsonstr, self, 0, typesv); SPAGAIN;
XPUSHs (jsonstr);

void decode_prefix (JSON *self, SV *jsonstr)
void decode_prefix (JSON *self, SV *jsonstr, SV *typesv = NULL)
PPCODE:
{
SV *sv;
STRLEN offset;
PUTBACK; sv = decode_json (aTHX_ jsonstr, self, &offset); SPAGAIN;
PUTBACK; sv = decode_json (aTHX_ jsonstr, self, &offset, typesv); SPAGAIN;
EXTEND (SP, 2);
PUSHs (sv);
PUSHs (sv_2mortal (newSVuv (ptr_to_index (aTHX_ jsonstr, offset))));
Expand Down Expand Up @@ -4257,7 +4320,7 @@ void incr_parse (JSON *self, SV *jsonstr = 0)
}
}

PUTBACK; sv = decode_json (aTHX_ self->incr_text, self, &offset); SPAGAIN;
PUTBACK; sv = decode_json (aTHX_ self->incr_text, self, &offset, NULL); SPAGAIN;
XPUSHs (sv);

endp = SvPVX(self->incr_text) + offset;
Expand Down Expand Up @@ -4346,7 +4409,7 @@ void encode_json (SV *scalar, SV *typesv = &PL_sv_undef)
XPUSHs (scalar);
}

void decode_json (SV *jsonstr, SV *allow_nonref = NULL)
void decode_json (SV *jsonstr, SV *allow_nonref = NULL, SV *typesv = NULL)
ALIAS:
_from_json = 0
decode_json = F_UTF8
Expand All @@ -4357,7 +4420,7 @@ void decode_json (SV *jsonstr, SV *allow_nonref = NULL)
json.flags |= ix;
if (ix && allow_nonref)
json.flags |= F_ALLOW_NONREF;
PUTBACK; jsonstr = decode_json (aTHX_ jsonstr, &json, 0); SPAGAIN;
PUTBACK; jsonstr = decode_json (aTHX_ jsonstr, &json, 0, typesv); SPAGAIN;
XPUSHs (jsonstr);
}

26 changes: 25 additions & 1 deletion XS/Type.pm
Expand Up @@ -11,6 +11,7 @@ Cpanel::JSON::XS::Type - Type support for JSON encode
use Cpanel::JSON::XS;
use Cpanel::JSON::XS::Type;
encode_json([10, "10", 10.25], [JSON_TYPE_INT, JSON_TYPE_INT, JSON_TYPE_STRING]);
# '[10,10,"10.25"]'
Expand All @@ -35,11 +36,27 @@ Cpanel::JSON::XS::Type - Type support for JSON encode
my $json_string = encode_json($perl_struct, $type_spec);
# '{"key1":{"key2":[10,10,10]},"key3":10.5}'
my $value = decode_json('false', 1, my $type);
# $value is 0 and $type is JSON_TYPE_BOOL
my $value = decode_json('0', 1, my $type);
# $value is 0 and $type is JSON_TYPE_INT
my $value = decode_json('"0"', 1, my $type);
# $value is 0 and $type is JSON_TYPE_STRING
my $json_string = '{"key1":{"key2":[10,"10",10.6]},"key3":"10.5"}';
my $perl_struct = decode_json($json_string, 0, my $type_spec);
# $perl_struct is { key1 => { key2 => [ 10, 10, 10.6 ] }, key3 => 10.5 }
# $type_spec is { key1 => { key2 => [ JSON_TYPE_INT, JSON_TYPE_STRING, JSON_TYPE_FLOAT ] }, key3 => JSON_TYPE_STRING }
=head1 DESCRIPTION
This module provides stable JSON type support for the
L<Cpanel::JSON::XS|Cpanel::JSON::XS> encoder which doesn't depend on
any internal perl scalar flags or characteristics.
any internal perl scalar flags or characteristics. Also it provides
real JSON types for L<Cpanel::JSON::XS|Cpanel::JSON::XS> decoder.
In most cases perl structures passed to
L<encode_json|Cpanel::JSON::XS/encode_json> come from other functions
Expand All @@ -48,6 +65,12 @@ have control of internals or they are subject of change. So it is not
easy to support enforcing types as described in the
L<simple scalars|Cpanel::JSON::XS/simple scalars> section.
For services based on JSON contents it is sometimes needed to correctly
process and enforce JSON types.
The function L<decode_json|Cpanel::JSON::XS/decode_json> takes optional
third scalar parameter and fills it with specification of json types.
The function L<encode_json|Cpanel::JSON::XS/encode_json> takes a perl
structure as its input and optionally also a json type specification in
the second parameter.
Expand Down Expand Up @@ -196,6 +219,7 @@ our @EXPORT = our @EXPORT_OK = qw(
json_type_anyof
json_type_null_or_anyof
json_type_weaken
JSON_TYPE_NULL
JSON_TYPE_BOOL
JSON_TYPE_INT
JSON_TYPE_FLOAT
Expand Down

0 comments on commit c0423f0

Please sign in to comment.