jsonfmt: Add canonical option

Not as bad as I had expected it to be; managed to keep the
implementation a little bit simpler and cleaner than JSON::XS.
This commit is contained in:
Yorhel 2025-01-29 18:42:27 +01:00
parent 163a60b4ba
commit 1a0fb03205
5 changed files with 115 additions and 23 deletions

View file

@ -1,6 +1,7 @@
typedef struct {
fustr out;
UV depth;
int canon;
} fujson_fmt_ctx;
static void fujson_fmt(pTHX_ fujson_fmt_ctx *, SV *);
@ -126,19 +127,65 @@ static void fujson_fmt_av(pTHX_ fujson_fmt_ctx *ctx, AV *av) {
fustr_write(&ctx->out, "]", 1);
}
static int fujson_fmt_hvcmp(const void *pa, const void *pb) {
dTHX;
HE *a = *(HE **)pa;
HE *b = *(HE **)pb;
STRLEN alen, blen;
char *astr = HePV(a, alen);
char *bstr = HePV(b, blen);
int autf = HeUTF8(a);
int butf = HeUTF8(b);
if (autf == butf) {
int cmp = memcmp(bstr, astr, alen < blen ? alen : blen);
return cmp != 0 ? cmp : blen < alen ? -1 : blen == alen ? 0 : 1;
}
return autf ? bytes_cmp_utf8((const U8*)bstr, blen, (const U8*)astr, alen)
: -bytes_cmp_utf8((const U8*)astr, alen, (const U8*)bstr, blen);
}
static void fujson_fmt_hvkv(pTHX_ fujson_fmt_ctx *ctx, HV *hv, HE *he, char **hestr) {
STRLEN helen;
if (*hestr) fustr_write(&ctx->out, ",", 1);
*hestr = HePV(he, helen);
fujson_fmt_str(aTHX_ ctx, *hestr, helen, HeUTF8(he));
fustr_write(&ctx->out, ":", 1);
fujson_fmt(aTHX_ ctx, UNLIKELY(SvMAGICAL(hv)) ? hv_iterval(hv, he) : HeVAL(he));
}
static void fujson_fmt_hv(pTHX_ fujson_fmt_ctx *ctx, HV *hv) {
HE *he;
STRLEN helen;
char *hestr = NULL;
hv_iterinit(hv);
int numkeys = hv_iterinit(hv);
fustr_write(&ctx->out, "{", 1);
while ((he = hv_iternext(hv))) {
if (hestr) fustr_write(&ctx->out, ",", 1);
hestr = HePV(he, helen);
fujson_fmt_str(aTHX_ ctx, hestr, helen, HeUTF8(he));
fustr_write(&ctx->out, ":", 1);
fujson_fmt(aTHX_ ctx, UNLIKELY(SvMAGICAL(hv)) ? hv_iterval(hv, he) : HeVAL(he));
/* Canonical order on tied hashes is not supported. Cpanel::JSON::XS has
* code to deal with that case and it's absolutely horrifying. */
if (ctx->canon && !(SvMAGICAL(hv) && SvTIED_mg((SV*)hv, PERL_MAGIC_tied))) {
SAVETMPS;
if (numkeys < 4) numkeys = 4;
if (SvMAGICAL(hv)) numkeys = 32;
SV *keys_sv = sv_2mortal(newSV(numkeys * sizeof(HE*)));
HE **keys = (HE **)SvPVX(keys_sv);
int i = 0;
while ((he = hv_iternext(hv))) {
if (i >= numkeys) {
numkeys += numkeys >> 1;
keys = (HE **)SvGROW(keys_sv, numkeys * sizeof(HE*));
numkeys = SvLEN(keys_sv) / sizeof(HE*);
}
keys[i++] = he;
}
qsort(keys, i, sizeof(HE *), fujson_fmt_hvcmp);
while (i--) fujson_fmt_hvkv(aTHX_ ctx, hv, keys[i], &hestr);
FREETMPS;
} else {
while ((he = hv_iternext(hv))) fujson_fmt_hvkv(aTHX_ ctx, hv, he, &hestr);
}
fustr_write(&ctx->out, "}", 1);
}
@ -222,6 +269,7 @@ static SV *fujson_fmt_xs(pTHX_ I32 ax, I32 argc, SV *val) {
ctx.out.maxlen = 0;
ctx.depth = 0;
ctx.canon = 0;
while (i < argc) {
arg = SvPV_nolen(ST(i));
i++;
@ -229,7 +277,8 @@ static SV *fujson_fmt_xs(pTHX_ I32 ax, I32 argc, SV *val) {
r = ST(i);
i++;
if (strcmp(arg, "utf8") == 0) encutf8 = SvPVXtrue(r);
if (strcmp(arg, "canonical") == 0) ctx.canon = SvPVXtrue(r);
else if (strcmp(arg, "utf8") == 0) encutf8 = SvPVXtrue(r);
else if (strcmp(arg, "max_size") == 0) ctx.out.maxlen = SvUV(r);
else if (strcmp(arg, "max_depth") == 0) ctx.depth = SvUV(r);
else croak("Unknown flag: '%s'", arg);
@ -244,5 +293,4 @@ static SV *fujson_fmt_xs(pTHX_ I32 ax, I32 argc, SV *val) {
return r;
}
/* TODO: canonical */
/* TODO: pretty */