json_writer: new routines to create JSON data

Add "struct json_writer" and a series of jw_ routines to compose JSON
data into a string buffer.  The resulting string may then be printed by
commands wanting to support a JSON-like output format.

The json_writer is limited to correctly formatting structured data for
output.  It does not attempt to build an object model of the JSON data.

We say "JSON-like" because we do not enforce the Unicode (usually UTF-8)
requirement on string fields.  Internally, Git does not necessarily have
Unicode/UTF-8 data for most fields, so it is currently unclear the best
way to enforce that requirement.  For example, on Linux pathnames can
contain arbitrary 8-bit character data, so a command like "status" would
not know how to encode the reported pathnames.  We may want to revisit
this (or double encode such strings) in the future.

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Helped-by: René Scharfe <l.s.r@web.de>
Helped-by: Wink Saville <wink@saville.com>
Helped-by: Ramsay Jones <ramsay@ramsayjones.plus.com>
Signed-off-by: Jeff Hostetler <jeffhost@microsoft.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Jeff Hostetler 2018-07-13 16:54:08 +00:00 committed by Junio C Hamano
parent b2453d3449
commit 75459410ed
8 changed files with 1471 additions and 0 deletions

View File

@ -709,6 +709,7 @@ TEST_BUILTINS_OBJS += test-example-decorate.o
TEST_BUILTINS_OBJS += test-genrandom.o
TEST_BUILTINS_OBJS += test-hashmap.o
TEST_BUILTINS_OBJS += test-index-version.o
TEST_BUILTINS_OBJS += test-json-writer.o
TEST_BUILTINS_OBJS += test-lazy-init-name-hash.o
TEST_BUILTINS_OBJS += test-match-trees.o
TEST_BUILTINS_OBJS += test-mergesort.o
@ -871,6 +872,7 @@ LIB_OBJS += hashmap.o
LIB_OBJS += help.o
LIB_OBJS += hex.o
LIB_OBJS += ident.o
LIB_OBJS += json-writer.o
LIB_OBJS += kwset.o
LIB_OBJS += levenshtein.o
LIB_OBJS += line-log.o

414
json-writer.c Normal file
View File

@ -0,0 +1,414 @@
#include "cache.h"
#include "json-writer.h"
void jw_init(struct json_writer *jw)
{
strbuf_init(&jw->json, 0);
strbuf_init(&jw->open_stack, 0);
jw->need_comma = 0;
jw->pretty = 0;
}
void jw_release(struct json_writer *jw)
{
strbuf_release(&jw->json);
strbuf_release(&jw->open_stack);
}
/*
* Append JSON-quoted version of the given string to 'out'.
*/
static void append_quoted_string(struct strbuf *out, const char *in)
{
unsigned char c;
strbuf_addch(out, '"');
while ((c = *in++) != '\0') {
if (c == '"')
strbuf_addstr(out, "\\\"");
else if (c == '\\')
strbuf_addstr(out, "\\\\");
else if (c == '\n')
strbuf_addstr(out, "\\n");
else if (c == '\r')
strbuf_addstr(out, "\\r");
else if (c == '\t')
strbuf_addstr(out, "\\t");
else if (c == '\f')
strbuf_addstr(out, "\\f");
else if (c == '\b')
strbuf_addstr(out, "\\b");
else if (c < 0x20)
strbuf_addf(out, "\\u%04x", c);
else
strbuf_addch(out, c);
}
strbuf_addch(out, '"');
}
static void indent_pretty(struct json_writer *jw)
{
int k;
for (k = 0; k < jw->open_stack.len; k++)
strbuf_addstr(&jw->json, " ");
}
/*
* Begin an object or array (either top-level or nested within the currently
* open object or array).
*/
static void begin(struct json_writer *jw, char ch_open, int pretty)
{
jw->pretty = pretty;
strbuf_addch(&jw->json, ch_open);
strbuf_addch(&jw->open_stack, ch_open);
jw->need_comma = 0;
}
/*
* Assert that the top of the open-stack is an object.
*/
static void assert_in_object(const struct json_writer *jw, const char *key)
{
if (!jw->open_stack.len)
BUG("json-writer: object: missing jw_object_begin(): '%s'", key);
if (jw->open_stack.buf[jw->open_stack.len - 1] != '{')
BUG("json-writer: object: not in object: '%s'", key);
}
/*
* Assert that the top of the open-stack is an array.
*/
static void assert_in_array(const struct json_writer *jw)
{
if (!jw->open_stack.len)
BUG("json-writer: array: missing jw_array_begin()");
if (jw->open_stack.buf[jw->open_stack.len - 1] != '[')
BUG("json-writer: array: not in array");
}
/*
* Add comma if we have already seen a member at this level.
*/
static void maybe_add_comma(struct json_writer *jw)
{
if (jw->need_comma)
strbuf_addch(&jw->json, ',');
else
jw->need_comma = 1;
}
static void fmt_double(struct json_writer *jw, int precision,
double value)
{
if (precision < 0) {
strbuf_addf(&jw->json, "%f", value);
} else {
struct strbuf fmt = STRBUF_INIT;
strbuf_addf(&fmt, "%%.%df", precision);
strbuf_addf(&jw->json, fmt.buf, value);
strbuf_release(&fmt);
}
}
static void object_common(struct json_writer *jw, const char *key)
{
assert_in_object(jw, key);
maybe_add_comma(jw);
if (jw->pretty) {
strbuf_addch(&jw->json, '\n');
indent_pretty(jw);
}
append_quoted_string(&jw->json, key);
strbuf_addch(&jw->json, ':');
if (jw->pretty)
strbuf_addch(&jw->json, ' ');
}
static void array_common(struct json_writer *jw)
{
assert_in_array(jw);
maybe_add_comma(jw);
if (jw->pretty) {
strbuf_addch(&jw->json, '\n');
indent_pretty(jw);
}
}
/*
* Assert that the given JSON object or JSON array has been properly
* terminated. (Has closing bracket.)
*/
static void assert_is_terminated(const struct json_writer *jw)
{
if (jw->open_stack.len)
BUG("json-writer: object: missing jw_end(): '%s'",
jw->json.buf);
}
void jw_object_begin(struct json_writer *jw, int pretty)
{
begin(jw, '{', pretty);
}
void jw_object_string(struct json_writer *jw, const char *key, const char *value)
{
object_common(jw, key);
append_quoted_string(&jw->json, value);
}
void jw_object_intmax(struct json_writer *jw, const char *key, intmax_t value)
{
object_common(jw, key);
strbuf_addf(&jw->json, "%"PRIdMAX, value);
}
void jw_object_double(struct json_writer *jw, const char *key, int precision,
double value)
{
object_common(jw, key);
fmt_double(jw, precision, value);
}
void jw_object_true(struct json_writer *jw, const char *key)
{
object_common(jw, key);
strbuf_addstr(&jw->json, "true");
}
void jw_object_false(struct json_writer *jw, const char *key)
{
object_common(jw, key);
strbuf_addstr(&jw->json, "false");
}
void jw_object_bool(struct json_writer *jw, const char *key, int value)
{
if (value)
jw_object_true(jw, key);
else
jw_object_false(jw, key);
}
void jw_object_null(struct json_writer *jw, const char *key)
{
object_common(jw, key);
strbuf_addstr(&jw->json, "null");
}
static void increase_indent(struct strbuf *sb,
const struct json_writer *jw,
int indent)
{
int k;
strbuf_reset(sb);
for (k = 0; k < jw->json.len; k++) {
char ch = jw->json.buf[k];
strbuf_addch(sb, ch);
if (ch == '\n')
strbuf_addchars(sb, ' ', indent);
}
}
static void kill_indent(struct strbuf *sb,
const struct json_writer *jw)
{
int k;
int eat_it = 0;
strbuf_reset(sb);
for (k = 0; k < jw->json.len; k++) {
char ch = jw->json.buf[k];
if (eat_it && ch == ' ')
continue;
if (ch == '\n') {
eat_it = 1;
continue;
}
eat_it = 0;
strbuf_addch(sb, ch);
}
}
static void append_sub_jw(struct json_writer *jw,
const struct json_writer *value)
{
/*
* If both are pretty, increase the indentation of the sub_jw
* to better fit under the super.
*
* If the super is pretty, but the sub_jw is compact, leave the
* sub_jw compact. (We don't want to parse and rebuild the sub_jw
* for this debug-ish feature.)
*
* If the super is compact, and the sub_jw is pretty, convert
* the sub_jw to compact.
*
* If both are compact, keep the sub_jw compact.
*/
if (jw->pretty && jw->open_stack.len && value->pretty) {
struct strbuf sb = STRBUF_INIT;
increase_indent(&sb, value, jw->open_stack.len * 2);
strbuf_addbuf(&jw->json, &sb);
strbuf_release(&sb);
return;
}
if (!jw->pretty && value->pretty) {
struct strbuf sb = STRBUF_INIT;
kill_indent(&sb, value);
strbuf_addbuf(&jw->json, &sb);
strbuf_release(&sb);
return;
}
strbuf_addbuf(&jw->json, &value->json);
}
/*
* Append existing (properly terminated) JSON sub-data (object or array)
* as-is onto the given JSON data.
*/
void jw_object_sub_jw(struct json_writer *jw, const char *key,
const struct json_writer *value)
{
assert_is_terminated(value);
object_common(jw, key);
append_sub_jw(jw, value);
}
void jw_object_inline_begin_object(struct json_writer *jw, const char *key)
{
object_common(jw, key);
jw_object_begin(jw, jw->pretty);
}
void jw_object_inline_begin_array(struct json_writer *jw, const char *key)
{
object_common(jw, key);
jw_array_begin(jw, jw->pretty);
}
void jw_array_begin(struct json_writer *jw, int pretty)
{
begin(jw, '[', pretty);
}
void jw_array_string(struct json_writer *jw, const char *value)
{
array_common(jw);
append_quoted_string(&jw->json, value);
}
void jw_array_intmax(struct json_writer *jw, intmax_t value)
{
array_common(jw);
strbuf_addf(&jw->json, "%"PRIdMAX, value);
}
void jw_array_double(struct json_writer *jw, int precision, double value)
{
array_common(jw);
fmt_double(jw, precision, value);
}
void jw_array_true(struct json_writer *jw)
{
array_common(jw);
strbuf_addstr(&jw->json, "true");
}
void jw_array_false(struct json_writer *jw)
{
array_common(jw);
strbuf_addstr(&jw->json, "false");
}
void jw_array_bool(struct json_writer *jw, int value)
{
if (value)
jw_array_true(jw);
else
jw_array_false(jw);
}
void jw_array_null(struct json_writer *jw)
{
array_common(jw);
strbuf_addstr(&jw->json, "null");
}
void jw_array_sub_jw(struct json_writer *jw, const struct json_writer *value)
{
assert_is_terminated(value);
array_common(jw);
append_sub_jw(jw, value);
}
void jw_array_argc_argv(struct json_writer *jw, int argc, const char **argv)
{
int k;
for (k = 0; k < argc; k++)
jw_array_string(jw, argv[k]);
}
void jw_array_argv(struct json_writer *jw, const char **argv)
{
while (*argv)
jw_array_string(jw, *argv++);
}
void jw_array_inline_begin_object(struct json_writer *jw)
{
array_common(jw);
jw_object_begin(jw, jw->pretty);
}
void jw_array_inline_begin_array(struct json_writer *jw)
{
array_common(jw);
jw_array_begin(jw, jw->pretty);
}
int jw_is_terminated(const struct json_writer *jw)
{
return !jw->open_stack.len;
}
void jw_end(struct json_writer *jw)
{
char ch_open;
int len;
if (!jw->open_stack.len)
BUG("json-writer: too many jw_end(): '%s'", jw->json.buf);
len = jw->open_stack.len - 1;
ch_open = jw->open_stack.buf[len];
strbuf_setlen(&jw->open_stack, len);
jw->need_comma = 1;
if (jw->pretty) {
strbuf_addch(&jw->json, '\n');
indent_pretty(jw);
}
if (ch_open == '{')
strbuf_addch(&jw->json, '}');
else
strbuf_addch(&jw->json, ']');
}

105
json-writer.h Normal file
View File

@ -0,0 +1,105 @@
#ifndef JSON_WRITER_H
#define JSON_WRITER_H
/*
* JSON data structures are defined at:
* [1] http://www.ietf.org/rfc/rfc7159.txt
* [2] http://json.org/
*
* The JSON-writer API allows one to build JSON data structures using a
* simple wrapper around a "struct strbuf" buffer. It is intended as a
* simple API to build output strings; it is not intended to be a general
* object model for JSON data. In particular, it does not re-order keys
* in an object (dictionary), it does not de-dup keys in an object, and
* it does not allow lookup or parsing of JSON data.
*
* All string values (both keys and string r-values) are properly quoted
* and escaped if they contain special characters.
*
* These routines create compact JSON data (with no unnecessary whitespace,
* newlines, or indenting). If you get an unexpected response, verify
* that you're not expecting a pretty JSON string.
*
* Both "JSON objects" (aka sets of k/v pairs) and "JSON array" can be
* constructed using a 'begin append* end' model.
*
* Nested objects and arrays can either be constructed bottom up (by
* creating sub object/arrays first and appending them to the super
* object/array) -or- by building them inline in one pass. This is a
* personal style and/or data shape choice.
*
* See t/helper/test-json-writer.c for various usage examples.
*
* LIMITATIONS:
* ============
*
* The JSON specification [1,2] defines string values as Unicode data
* and probably UTF-8 encoded. The current json-writer API does not
* enforce this and will write any string as received. However, it will
* properly quote and backslash-escape them as necessary. It is up to
* the caller to UTF-8 encode their strings *before* passing them to this
* API. This layer should not have to try to guess the encoding or locale
* of the given strings.
*/
struct json_writer
{
/*
* Buffer of the in-progress JSON currently being composed.
*/
struct strbuf json;
/*
* Simple stack of the currently open array and object forms.
* This is a string of '{' and '[' characters indicating the
* currently unterminated forms. This is used to ensure the
* properly closing character is used when popping a level and
* to know when the JSON is completely closed.
*/
struct strbuf open_stack;
unsigned int need_comma:1;
unsigned int pretty:1;
};
#define JSON_WRITER_INIT { STRBUF_INIT, STRBUF_INIT, 0, 0 }
void jw_init(struct json_writer *jw);
void jw_release(struct json_writer *jw);
void jw_object_begin(struct json_writer *jw, int pretty);
void jw_array_begin(struct json_writer *jw, int pretty);
void jw_object_string(struct json_writer *jw, const char *key,
const char *value);
void jw_object_intmax(struct json_writer *jw, const char *key, intmax_t value);
void jw_object_double(struct json_writer *jw, const char *key, int precision,
double value);
void jw_object_true(struct json_writer *jw, const char *key);
void jw_object_false(struct json_writer *jw, const char *key);
void jw_object_bool(struct json_writer *jw, const char *key, int value);
void jw_object_null(struct json_writer *jw, const char *key);
void jw_object_sub_jw(struct json_writer *jw, const char *key,
const struct json_writer *value);
void jw_object_inline_begin_object(struct json_writer *jw, const char *key);
void jw_object_inline_begin_array(struct json_writer *jw, const char *key);
void jw_array_string(struct json_writer *jw, const char *value);
void jw_array_intmax(struct json_writer *jw, intmax_t value);
void jw_array_double(struct json_writer *jw, int precision, double value);
void jw_array_true(struct json_writer *jw);
void jw_array_false(struct json_writer *jw);
void jw_array_bool(struct json_writer *jw, int value);
void jw_array_null(struct json_writer *jw);
void jw_array_sub_jw(struct json_writer *jw, const struct json_writer *value);
void jw_array_argc_argv(struct json_writer *jw, int argc, const char **argv);
void jw_array_argv(struct json_writer *jw, const char **argv);
void jw_array_inline_begin_object(struct json_writer *jw);
void jw_array_inline_begin_array(struct json_writer *jw);
int jw_is_terminated(const struct json_writer *jw);
void jw_end(struct json_writer *jw);
#endif /* JSON_WRITER_H */

565
t/helper/test-json-writer.c Normal file
View File

@ -0,0 +1,565 @@
#include "test-tool.h"
#include "cache.h"
#include "json-writer.h"
static const char *expect_obj1 = "{\"a\":\"abc\",\"b\":42,\"c\":true}";
static const char *expect_obj2 = "{\"a\":-1,\"b\":2147483647,\"c\":0}";
static const char *expect_obj3 = "{\"a\":0,\"b\":4294967295,\"c\":9223372036854775807}";
static const char *expect_obj4 = "{\"t\":true,\"f\":false,\"n\":null}";
static const char *expect_obj5 = "{\"abc\\tdef\":\"abc\\\\def\"}";
static const char *expect_obj6 = "{\"a\":3.14}";
static const char *pretty_obj1 = ("{\n"
" \"a\": \"abc\",\n"
" \"b\": 42,\n"
" \"c\": true\n"
"}");
static const char *pretty_obj2 = ("{\n"
" \"a\": -1,\n"
" \"b\": 2147483647,\n"
" \"c\": 0\n"
"}");
static const char *pretty_obj3 = ("{\n"
" \"a\": 0,\n"
" \"b\": 4294967295,\n"
" \"c\": 9223372036854775807\n"
"}");
static const char *pretty_obj4 = ("{\n"
" \"t\": true,\n"
" \"f\": false,\n"
" \"n\": null\n"
"}");
static struct json_writer obj1 = JSON_WRITER_INIT;
static struct json_writer obj2 = JSON_WRITER_INIT;
static struct json_writer obj3 = JSON_WRITER_INIT;
static struct json_writer obj4 = JSON_WRITER_INIT;
static struct json_writer obj5 = JSON_WRITER_INIT;
static struct json_writer obj6 = JSON_WRITER_INIT;
static void make_obj1(int pretty)
{
jw_object_begin(&obj1, pretty);
{
jw_object_string(&obj1, "a", "abc");
jw_object_intmax(&obj1, "b", 42);
jw_object_true(&obj1, "c");
}
jw_end(&obj1);
}
static void make_obj2(int pretty)
{
jw_object_begin(&obj2, pretty);
{
jw_object_intmax(&obj2, "a", -1);
jw_object_intmax(&obj2, "b", 0x7fffffff);
jw_object_intmax(&obj2, "c", 0);
}
jw_end(&obj2);
}
static void make_obj3(int pretty)
{
jw_object_begin(&obj3, pretty);
{
jw_object_intmax(&obj3, "a", 0);
jw_object_intmax(&obj3, "b", 0xffffffff);
jw_object_intmax(&obj3, "c", 0x7fffffffffffffffULL);
}
jw_end(&obj3);
}
static void make_obj4(int pretty)
{
jw_object_begin(&obj4, pretty);
{
jw_object_true(&obj4, "t");
jw_object_false(&obj4, "f");
jw_object_null(&obj4, "n");
}
jw_end(&obj4);
}
static void make_obj5(int pretty)
{
jw_object_begin(&obj5, pretty);
{
jw_object_string(&obj5, "abc" "\x09" "def", "abc" "\\" "def");
}
jw_end(&obj5);
}
static void make_obj6(int pretty)
{
jw_object_begin(&obj6, pretty);
{
jw_object_double(&obj6, "a", 2, 3.14159);
}
jw_end(&obj6);
}
static const char *expect_arr1 = "[\"abc\",42,true]";
static const char *expect_arr2 = "[-1,2147483647,0]";
static const char *expect_arr3 = "[0,4294967295,9223372036854775807]";
static const char *expect_arr4 = "[true,false,null]";
static const char *pretty_arr1 = ("[\n"
" \"abc\",\n"
" 42,\n"
" true\n"
"]");
static const char *pretty_arr2 = ("[\n"
" -1,\n"
" 2147483647,\n"
" 0\n"
"]");
static const char *pretty_arr3 = ("[\n"
" 0,\n"
" 4294967295,\n"
" 9223372036854775807\n"
"]");
static const char *pretty_arr4 = ("[\n"
" true,\n"
" false,\n"
" null\n"
"]");
static struct json_writer arr1 = JSON_WRITER_INIT;
static struct json_writer arr2 = JSON_WRITER_INIT;
static struct json_writer arr3 = JSON_WRITER_INIT;
static struct json_writer arr4 = JSON_WRITER_INIT;
static void make_arr1(int pretty)
{
jw_array_begin(&arr1, pretty);
{
jw_array_string(&arr1, "abc");
jw_array_intmax(&arr1, 42);
jw_array_true(&arr1);
}
jw_end(&arr1);
}
static void make_arr2(int pretty)
{
jw_array_begin(&arr2, pretty);
{
jw_array_intmax(&arr2, -1);
jw_array_intmax(&arr2, 0x7fffffff);
jw_array_intmax(&arr2, 0);
}
jw_end(&arr2);
}
static void make_arr3(int pretty)
{
jw_array_begin(&arr3, pretty);
{
jw_array_intmax(&arr3, 0);
jw_array_intmax(&arr3, 0xffffffff);
jw_array_intmax(&arr3, 0x7fffffffffffffffULL);
}
jw_end(&arr3);
}
static void make_arr4(int pretty)
{
jw_array_begin(&arr4, pretty);
{
jw_array_true(&arr4);
jw_array_false(&arr4);
jw_array_null(&arr4);
}
jw_end(&arr4);
}
static char *expect_nest1 =
"{\"obj1\":{\"a\":\"abc\",\"b\":42,\"c\":true},\"arr1\":[\"abc\",42,true]}";
static struct json_writer nest1 = JSON_WRITER_INIT;
static void make_nest1(int pretty)
{
jw_object_begin(&nest1, pretty);
{
jw_object_sub_jw(&nest1, "obj1", &obj1);
jw_object_sub_jw(&nest1, "arr1", &arr1);
}
jw_end(&nest1);
}
static char *expect_inline1 =
"{\"obj1\":{\"a\":\"abc\",\"b\":42,\"c\":true},\"arr1\":[\"abc\",42,true]}";
static char *pretty_inline1 =
("{\n"
" \"obj1\": {\n"
" \"a\": \"abc\",\n"
" \"b\": 42,\n"
" \"c\": true\n"
" },\n"
" \"arr1\": [\n"
" \"abc\",\n"
" 42,\n"
" true\n"
" ]\n"
"}");
static struct json_writer inline1 = JSON_WRITER_INIT;
static void make_inline1(int pretty)
{
jw_object_begin(&inline1, pretty);
{
jw_object_inline_begin_object(&inline1, "obj1");
{
jw_object_string(&inline1, "a", "abc");
jw_object_intmax(&inline1, "b", 42);
jw_object_true(&inline1, "c");
}
jw_end(&inline1);
jw_object_inline_begin_array(&inline1, "arr1");
{
jw_array_string(&inline1, "abc");
jw_array_intmax(&inline1, 42);
jw_array_true(&inline1);
}
jw_end(&inline1);
}
jw_end(&inline1);
}
static char *expect_inline2 =
"[[1,2],[3,4],{\"a\":\"abc\"}]";
static char *pretty_inline2 =
("[\n"
" [\n"
" 1,\n"
" 2\n"
" ],\n"
" [\n"
" 3,\n"
" 4\n"
" ],\n"
" {\n"
" \"a\": \"abc\"\n"
" }\n"
"]");
static struct json_writer inline2 = JSON_WRITER_INIT;
static void make_inline2(int pretty)
{
jw_array_begin(&inline2, pretty);
{
jw_array_inline_begin_array(&inline2);
{
jw_array_intmax(&inline2, 1);
jw_array_intmax(&inline2, 2);
}
jw_end(&inline2);
jw_array_inline_begin_array(&inline2);
{
jw_array_intmax(&inline2, 3);
jw_array_intmax(&inline2, 4);
}
jw_end(&inline2);
jw_array_inline_begin_object(&inline2);
{
jw_object_string(&inline2, "a", "abc");
}
jw_end(&inline2);
}
jw_end(&inline2);
}
/*
* When super is compact, we expect subs to be compacted (even if originally
* pretty).
*/
static const char *expect_mixed1 =
("{\"obj1\":{\"a\":\"abc\",\"b\":42,\"c\":true},"
"\"arr1\":[\"abc\",42,true]}");
/*
* When super is pretty, a compact sub (obj1) is kept compact and a pretty
* sub (arr1) is re-indented.
*/
static const char *pretty_mixed1 =
("{\n"
" \"obj1\": {\"a\":\"abc\",\"b\":42,\"c\":true},\n"
" \"arr1\": [\n"
" \"abc\",\n"
" 42,\n"
" true\n"
" ]\n"
"}");
static struct json_writer mixed1 = JSON_WRITER_INIT;
static void make_mixed1(int pretty)
{
jw_init(&obj1);
jw_init(&arr1);
make_obj1(0); /* obj1 is compact */
make_arr1(1); /* arr1 is pretty */
jw_object_begin(&mixed1, pretty);
{
jw_object_sub_jw(&mixed1, "obj1", &obj1);
jw_object_sub_jw(&mixed1, "arr1", &arr1);
}
jw_end(&mixed1);
}
static void cmp(const char *test, const struct json_writer *jw, const char *exp)
{
if (!strcmp(jw->json.buf, exp))
return;
printf("error[%s]: observed '%s' expected '%s'\n",
test, jw->json.buf, exp);
exit(1);
}
#define t(v) do { make_##v(0); cmp(#v, &v, expect_##v); } while (0)
#define p(v) do { make_##v(1); cmp(#v, &v, pretty_##v); } while (0)
/*
* Run some basic regression tests with some known patterns.
* These tests also demonstrate how to use the jw_ API.
*/
static int unit_tests(void)
{
/* comptact (canonical) forms */
t(obj1);
t(obj2);
t(obj3);
t(obj4);
t(obj5);
t(obj6);
t(arr1);
t(arr2);
t(arr3);
t(arr4);
t(nest1);
t(inline1);
t(inline2);
jw_init(&obj1);
jw_init(&obj2);
jw_init(&obj3);
jw_init(&obj4);
jw_init(&arr1);
jw_init(&arr2);
jw_init(&arr3);
jw_init(&arr4);
jw_init(&inline1);
jw_init(&inline2);
/* pretty forms */
p(obj1);
p(obj2);
p(obj3);
p(obj4);
p(arr1);
p(arr2);
p(arr3);
p(arr4);
p(inline1);
p(inline2);
/* mixed forms */
t(mixed1);
jw_init(&mixed1);
p(mixed1);
return 0;
}
static void get_s(int line_nr, char **s_in)
{
*s_in = strtok(NULL, " ");
if (!*s_in)
die("line[%d]: expected: <s>", line_nr);
}
static void get_i(int line_nr, intmax_t *s_in)
{
char *s;
char *endptr;
get_s(line_nr, &s);
*s_in = strtol(s, &endptr, 10);
if (*endptr || errno == ERANGE)
die("line[%d]: invalid integer value", line_nr);
}
static void get_d(int line_nr, double *s_in)
{
char *s;
char *endptr;
get_s(line_nr, &s);
*s_in = strtod(s, &endptr);
if (*endptr || errno == ERANGE)
die("line[%d]: invalid float value", line_nr);
}
static int pretty;
#define MAX_LINE_LENGTH (64 * 1024)
static char *get_trimmed_line(char *buf, int buf_size)
{
int len;
if (!fgets(buf, buf_size, stdin))
return NULL;
len = strlen(buf);
while (len > 0) {
char c = buf[len - 1];
if (c == '\n' || c == '\r' || c == ' ' || c == '\t')
buf[--len] = 0;
else
break;
}
while (*buf == ' ' || *buf == '\t')
buf++;
return buf;
}
static int scripted(void)
{
struct json_writer jw = JSON_WRITER_INIT;
char buf[MAX_LINE_LENGTH];
char *line;
int line_nr = 0;
line = get_trimmed_line(buf, MAX_LINE_LENGTH);
if (!line)
return 0;
if (!strcmp(line, "object"))
jw_object_begin(&jw, pretty);
else if (!strcmp(line, "array"))
jw_array_begin(&jw, pretty);
else
die("expected first line to be 'object' or 'array'");
while ((line = get_trimmed_line(buf, MAX_LINE_LENGTH)) != NULL) {
char *verb;
char *key;
char *s_value;
intmax_t i_value;
double d_value;
line_nr++;
verb = strtok(line, " ");
if (!strcmp(verb, "end")) {
jw_end(&jw);
}
else if (!strcmp(verb, "object-string")) {
get_s(line_nr, &key);
get_s(line_nr, &s_value);
jw_object_string(&jw, key, s_value);
}
else if (!strcmp(verb, "object-int")) {
get_s(line_nr, &key);
get_i(line_nr, &i_value);
jw_object_intmax(&jw, key, i_value);
}
else if (!strcmp(verb, "object-double")) {
get_s(line_nr, &key);
get_i(line_nr, &i_value);
get_d(line_nr, &d_value);
jw_object_double(&jw, key, i_value, d_value);
}
else if (!strcmp(verb, "object-true")) {
get_s(line_nr, &key);
jw_object_true(&jw, key);
}
else if (!strcmp(verb, "object-false")) {
get_s(line_nr, &key);
jw_object_false(&jw, key);
}
else if (!strcmp(verb, "object-null")) {
get_s(line_nr, &key);
jw_object_null(&jw, key);
}
else if (!strcmp(verb, "object-object")) {
get_s(line_nr, &key);
jw_object_inline_begin_object(&jw, key);
}
else if (!strcmp(verb, "object-array")) {
get_s(line_nr, &key);
jw_object_inline_begin_array(&jw, key);
}
else if (!strcmp(verb, "array-string")) {
get_s(line_nr, &s_value);
jw_array_string(&jw, s_value);
}
else if (!strcmp(verb, "array-int")) {
get_i(line_nr, &i_value);
jw_array_intmax(&jw, i_value);
}
else if (!strcmp(verb, "array-double")) {
get_i(line_nr, &i_value);
get_d(line_nr, &d_value);
jw_array_double(&jw, i_value, d_value);
}
else if (!strcmp(verb, "array-true"))
jw_array_true(&jw);
else if (!strcmp(verb, "array-false"))
jw_array_false(&jw);
else if (!strcmp(verb, "array-null"))
jw_array_null(&jw);
else if (!strcmp(verb, "array-object"))
jw_array_inline_begin_object(&jw);
else if (!strcmp(verb, "array-array"))
jw_array_inline_begin_array(&jw);
else
die("unrecognized token: '%s'", verb);
}
if (!jw_is_terminated(&jw))
die("json not terminated: '%s'", jw.json.buf);
printf("%s\n", jw.json.buf);
strbuf_release(&jw.json);
return 0;
}
int cmd__json_writer(int argc, const char **argv)
{
argc--; /* skip over "json-writer" arg */
argv++;
if (argc > 0 && argv[0][0] == '-') {
if (!strcmp(argv[0], "-u") || !strcmp(argv[0], "--unit"))
return unit_tests();
if (!strcmp(argv[0], "-p") || !strcmp(argv[0], "--pretty"))
pretty = 1;
}
return scripted();
}

View File

@ -19,6 +19,7 @@ static struct test_cmd cmds[] = {
{ "genrandom", cmd__genrandom },
{ "hashmap", cmd__hashmap },
{ "index-version", cmd__index_version },
{ "json-writer", cmd__json_writer },
{ "lazy-init-name-hash", cmd__lazy_init_name_hash },
{ "match-trees", cmd__match_trees },
{ "mergesort", cmd__mergesort },

View File

@ -13,6 +13,7 @@ int cmd__example_decorate(int argc, const char **argv);
int cmd__genrandom(int argc, const char **argv);
int cmd__hashmap(int argc, const char **argv);
int cmd__index_version(int argc, const char **argv);
int cmd__json_writer(int argc, const char **argv);
int cmd__lazy_init_name_hash(int argc, const char **argv);
int cmd__match_trees(int argc, const char **argv);
int cmd__mergesort(int argc, const char **argv);

331
t/t0019-json-writer.sh Executable file
View File

@ -0,0 +1,331 @@
#!/bin/sh
test_description='test json-writer JSON generation'
. ./test-lib.sh
test_expect_success 'unit test of json-writer routines' '
test-tool json-writer -u
'
test_expect_success 'trivial object' '
cat >expect <<-\EOF &&
{}
EOF
cat >input <<-\EOF &&
object
end
EOF
test-tool json-writer <input >actual &&
test_cmp expect actual
'
test_expect_success 'trivial array' '
cat >expect <<-\EOF &&
[]
EOF
cat >input <<-\EOF &&
array
end
EOF
test-tool json-writer <input >actual &&
test_cmp expect actual
'
test_expect_success 'simple object' '
cat >expect <<-\EOF &&
{"a":"abc","b":42,"c":3.14,"d":true,"e":false,"f":null}
EOF
cat >input <<-\EOF &&
object
object-string a abc
object-int b 42
object-double c 2 3.140
object-true d
object-false e
object-null f
end
EOF
test-tool json-writer <input >actual &&
test_cmp expect actual
'
test_expect_success 'simple array' '
cat >expect <<-\EOF &&
["abc",42,3.14,true,false,null]
EOF
cat >input <<-\EOF &&
array
array-string abc
array-int 42
array-double 2 3.140
array-true
array-false
array-null
end
EOF
test-tool json-writer <input >actual &&
test_cmp expect actual
'
test_expect_success 'escape quoting string' '
cat >expect <<-\EOF &&
{"a":"abc\\def"}
EOF
cat >input <<-\EOF &&
object
object-string a abc\def
end
EOF
test-tool json-writer <input >actual &&
test_cmp expect actual
'
test_expect_success 'escape quoting string 2' '
cat >expect <<-\EOF &&
{"a":"abc\"def"}
EOF
cat >input <<-\EOF &&
object
object-string a abc"def
end
EOF
test-tool json-writer <input >actual &&
test_cmp expect actual
'
test_expect_success 'nested inline object' '
cat >expect <<-\EOF &&
{"a":"abc","b":42,"sub1":{"c":3.14,"d":true,"sub2":{"e":false,"f":null}}}
EOF
cat >input <<-\EOF &&
object
object-string a abc
object-int b 42
object-object sub1
object-double c 2 3.140
object-true d
object-object sub2
object-false e
object-null f
end
end
end
EOF
test-tool json-writer <input >actual &&
test_cmp expect actual
'
test_expect_success 'nested inline array' '
cat >expect <<-\EOF &&
["abc",42,[3.14,true,[false,null]]]
EOF
cat >input <<-\EOF &&
array
array-string abc
array-int 42
array-array
array-double 2 3.140
array-true
array-array
array-false
array-null
end
end
end
EOF
test-tool json-writer <input >actual &&
test_cmp expect actual
'
test_expect_success 'nested inline object and array' '
cat >expect <<-\EOF &&
{"a":"abc","b":42,"sub1":{"c":3.14,"d":true,"sub2":[false,null]}}
EOF
cat >input <<-\EOF &&
object
object-string a abc
object-int b 42
object-object sub1
object-double c 2 3.140
object-true d
object-array sub2
array-false
array-null
end
end
end
EOF
test-tool json-writer <input >actual &&
test_cmp expect actual
'
test_expect_success 'nested inline object and array 2' '
cat >expect <<-\EOF &&
{"a":"abc","b":42,"sub1":{"c":3.14,"d":true,"sub2":[false,{"g":0,"h":1},null]}}
EOF
cat >input <<-\EOF &&
object
object-string a abc
object-int b 42
object-object sub1
object-double c 2 3.140
object-true d
object-array sub2
array-false
array-object
object-int g 0
object-int h 1
end
array-null
end
end
end
EOF
test-tool json-writer <input >actual &&
test_cmp expect actual
'
test_expect_success 'pretty nested inline object and array 2' '
sed -e "s/^|//" >expect <<-\EOF &&
|{
| "a": "abc",
| "b": 42,
| "sub1": {
| "c": 3.14,
| "d": true,
| "sub2": [
| false,
| {
| "g": 0,
| "h": 1
| },
| null
| ]
| }
|}
EOF
cat >input <<-\EOF &&
object
object-string a abc
object-int b 42
object-object sub1
object-double c 2 3.140
object-true d
object-array sub2
array-false
array-object
object-int g 0
object-int h 1
end
array-null
end
end
end
EOF
test-tool json-writer -p <input >actual &&
test_cmp expect actual
'
test_expect_success 'inline object with no members' '
cat >expect <<-\EOF &&
{"a":"abc","empty":{},"b":42}
EOF
cat >input <<-\EOF &&
object
object-string a abc
object-object empty
end
object-int b 42
end
EOF
test-tool json-writer <input >actual &&
test_cmp expect actual
'
test_expect_success 'inline array with no members' '
cat >expect <<-\EOF &&
{"a":"abc","empty":[],"b":42}
EOF
cat >input <<-\EOF &&
object
object-string a abc
object-array empty
end
object-int b 42
end
EOF
test-tool json-writer <input >actual &&
test_cmp expect actual
'
test_expect_success 'larger empty example' '
cat >expect <<-\EOF &&
{"a":"abc","empty":[{},{},{},[],{}],"b":42}
EOF
cat >input <<-\EOF &&
object
object-string a abc
object-array empty
array-object
end
array-object
end
array-object
end
array-array
end
array-object
end
end
object-int b 42
end
EOF
test-tool json-writer <input >actual &&
test_cmp expect actual
'
test_lazy_prereq PERLJSON '
perl -MJSON -e "exit 0"
'
# As a sanity check, ask Perl to parse our generated JSON and recursively
# dump the resulting data in sorted order. Confirm that that matches our
# expectations.
test_expect_success PERLJSON 'parse JSON using Perl' '
cat >expect <<-\EOF &&
row[0].a abc
row[0].b 42
row[0].sub1 hash
row[0].sub1.c 3.14
row[0].sub1.d 1
row[0].sub1.sub2 array
row[0].sub1.sub2[0] 0
row[0].sub1.sub2[1] hash
row[0].sub1.sub2[1].g 0
row[0].sub1.sub2[1].h 1
row[0].sub1.sub2[2] null
EOF
cat >input <<-\EOF &&
object
object-string a abc
object-int b 42
object-object sub1
object-double c 2 3.140
object-true d
object-array sub2
array-false
array-object
object-int g 0
object-int h 1
end
array-null
end
end
end
EOF
test-tool json-writer <input >output.json &&
perl "$TEST_DIRECTORY"/t0019/parse_json.perl <output.json >actual &&
test_cmp expect actual
'
test_done

52
t/t0019/parse_json.perl Normal file
View File

@ -0,0 +1,52 @@
#!/usr/bin/perl
use strict;
use warnings;
use JSON;
sub dump_array {
my ($label_in, $ary_ref) = @_;
my @ary = @$ary_ref;
for ( my $i = 0; $i <= $#{ $ary_ref }; $i++ )
{
my $label = "$label_in\[$i\]";
dump_item($label, $ary[$i]);
}
}
sub dump_hash {
my ($label_in, $obj_ref) = @_;
my %obj = %$obj_ref;
foreach my $k (sort keys %obj) {
my $label = (length($label_in) > 0) ? "$label_in.$k" : "$k";
my $value = $obj{$k};
dump_item($label, $value);
}
}
sub dump_item {
my ($label_in, $value) = @_;
if (ref($value) eq 'ARRAY') {
print "$label_in array\n";
dump_array($label_in, $value);
} elsif (ref($value) eq 'HASH') {
print "$label_in hash\n";
dump_hash($label_in, $value);
} elsif (defined $value) {
print "$label_in $value\n";
} else {
print "$label_in null\n";
}
}
my $row = 0;
while (<>) {
my $data = decode_json( $_ );
my $label = "row[$row]";
dump_hash($label, $data);
$row++;
}