diff --git a/FU/Util.pod b/FU/Util.pod index fd6bb5e..f597576 100644 --- a/FU/Util.pod +++ b/FU/Util.pod @@ -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 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 diff --git a/c/jsonfmt.c b/c/jsonfmt.c index 70dfbef..4c1cf8f 100644 --- a/c/jsonfmt.c +++ b/c/jsonfmt.c @@ -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; iout, ",", 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 */ diff --git a/t/json_format.t b/t/json_format.t index 4a5470f..a958468 100644 --- a/t/json_format.t +++ b/t/json_format.t @@ -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/;