jsonfmt: Add pretty option

That completes the json_format() function for now. At least, it now does
everything I had planned for it.

Ended up at a bit over 300 LOC. That's larger than I had expected, but
still alright.
This commit is contained in:
Yorhel 2025-01-30 07:52:22 +01:00
parent 1a0fb03205
commit aebe5a93dc
3 changed files with 56 additions and 8 deletions

View file

@ -51,13 +51,20 @@ The following C<%options> are supported:
=item canonical
When set to a true value, write hash keys in deterministic (sorted) order. This
option currently has no effect on tied hashes.
Boolean, write hash keys in deterministic (sorted) order. This option currently
has no effect on tied hashes.
=item pretty
Boolean, format JSON with newlines and indentation for easier reading. Beauty
is in the eye of the beholder, this option currently follows the convention
used by L<JSON::XS> and others: 3 space indent and one space around the C<:>
separating object keys and values. The exact format might change in later
versions.
=item utf8
When set to a true value, returns a UTF-8 encoded byte string instead of a Perl
Unicode string.
Boolean, returns a UTF-8 encoded byte string instead of a Perl Unicode string.
=item max_size

View file

@ -2,10 +2,19 @@ typedef struct {
fustr out;
UV depth;
int canon;
int pretty; /* <0 when disabled, current nesting level otherwise */
} fujson_fmt_ctx;
static void fujson_fmt(pTHX_ fujson_fmt_ctx *, SV *);
static void fujson_fmt_indent(pTHX_ fujson_fmt_ctx *ctx) {
if (ctx->pretty >= 0) {
char *buf = fustr_write_buf(&ctx->out, 1 + ctx->pretty*3);
*buf = '\n';
memset(buf+1, ' ', ctx->pretty*3);
}
}
static void fujson_fmt_str(pTHX_ fujson_fmt_ctx *ctx, const char *stri, size_t len, int utf8) {
size_t off = 0, loff;
const unsigned char *str = (const unsigned char *)stri;
@ -118,12 +127,16 @@ static void fujson_fmt_int(pTHX_ fujson_fmt_ctx *ctx, SV *val) {
static void fujson_fmt_av(pTHX_ fujson_fmt_ctx *ctx, AV *av) {
int i, len = av_count(av);
fustr_write(&ctx->out, "[", 1);
ctx->pretty++;
for (i=0; i<len; i++) {
if (i) fustr_write(&ctx->out, ",", 1);
fujson_fmt_indent(aTHX_ ctx);
SV **sv = av_fetch(av, i, 0);
if (sv) fujson_fmt(aTHX_ ctx, *sv); /* sv will have magic if av is tied, but fujson_fmt() handles that. */
else fustr_write(&ctx->out, "null", 4);
}
ctx->pretty--;
if (i) fujson_fmt_indent(aTHX_ ctx);
fustr_write(&ctx->out, "]", 1);
}
@ -148,9 +161,11 @@ static int fujson_fmt_hvcmp(const void *pa, const void *pb) {
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);
fujson_fmt_indent(aTHX_ ctx);
*hestr = HePV(he, helen);
fujson_fmt_str(aTHX_ ctx, *hestr, helen, HeUTF8(he));
fustr_write(&ctx->out, ":", 1);
if (ctx->pretty > 0) fustr_write(&ctx->out, " : ", 3);
else fustr_write(&ctx->out, ":", 1);
fujson_fmt(aTHX_ ctx, UNLIKELY(SvMAGICAL(hv)) ? hv_iterval(hv, he) : HeVAL(he));
}
@ -160,6 +175,7 @@ static void fujson_fmt_hv(pTHX_ fujson_fmt_ctx *ctx, HV *hv) {
int numkeys = hv_iterinit(hv);
fustr_write(&ctx->out, "{", 1);
ctx->pretty++;
/* Canonical order on tied hashes is not supported. Cpanel::JSON::XS has
* code to deal with that case and it's absolutely horrifying. */
@ -187,6 +203,8 @@ static void fujson_fmt_hv(pTHX_ fujson_fmt_ctx *ctx, HV *hv) {
} else {
while ((he = hv_iternext(hv))) fujson_fmt_hvkv(aTHX_ ctx, hv, he, &hestr);
}
ctx->pretty--;
if (hestr) fujson_fmt_indent(aTHX_ ctx);
fustr_write(&ctx->out, "}", 1);
}
@ -269,6 +287,7 @@ static SV *fujson_fmt_xs(pTHX_ I32 ax, I32 argc, SV *val) {
ctx.out.maxlen = 0;
ctx.depth = 0;
ctx.pretty = INT_MIN;
ctx.canon = 0;
while (i < argc) {
arg = SvPV_nolen(ST(i));
@ -278,6 +297,7 @@ static SV *fujson_fmt_xs(pTHX_ I32 ax, I32 argc, SV *val) {
i++;
if (strcmp(arg, "canonical") == 0) ctx.canon = SvPVXtrue(r);
else if (strcmp(arg, "pretty") == 0) ctx.pretty = SvPVXtrue(r) ? 0 : INT_MIN;
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);
@ -288,9 +308,8 @@ static SV *fujson_fmt_xs(pTHX_ I32 ax, I32 argc, SV *val) {
fustr_init(&ctx.out, 128, ctx.out.maxlen);
fujson_fmt(aTHX_ &ctx, val);
if (ctx.pretty >= 0) fustr_write(&ctx.out, "\n", 1);
r = fustr_done(&ctx.out);
if (!encutf8) SvUTF8_on(r);
return r;
}
/* TODO: pretty */

View file

@ -68,7 +68,7 @@ my @errors = (
do { my $o = {}; bless $o, 'MyToJSONSelf' }, qr/MyToJSONSelf::TO_JSON method returned same object as was passed instead of a new one/,
);
plan tests => @tests*2 + @errors/2 + 9;
plan tests => @tests*2 + @errors/2 + 10;
for my($in, $exp) (@tests) {
my $out = json_format $in;
@ -90,6 +90,28 @@ for my ($in, $exp) (@errors) {
is json_format({qw/a 1 b 2 c 3 d 4 d1 5 d11 6/, do { use utf8; qw/ü 7 月 8 💩 9/ }}, canonical => 1),
do { use utf8; '{"a":"1","b":"2","c":"3","d":"4","d1":"5","d11":"6","ü":"7","月":"8","💩":"9"}' };
is json_format(
{ a => [], b => {}, c => { x => 1 }, d => { y => true, z => false }, e => [1,2,3] },
canonical => 1, pretty => 1
), <<_;
{
"a" : [],
"b" : {},
"c" : {
"x" : 1
},
"d" : {
"y" : true,
"z" : false
},
"e" : [
1,
2,
3
]
}
_
eval { json_format [[]], max_depth => 2 };
like $@, qr/max_depth exceeded while formatting JSON/;