{"thread":{"id":"48669","subject":"[PATCH v8 0/2] json-writer V8","startedAt":"2018-06-07T14:12:36Z","lastAt":"2018-06-11T12:16:48Z","messageCount":13,"participants":["git@jeffhostetler.com","Eric Sunshine","René Scharfe","Jeff Hostetler"],"isPatch":true,"patchVersion":8,"patchTotal":2},"messages":[{"id":"349637","messageId":"20180607141209.24423-1-git@jeffhostetler.com","threadId":"48669","inReplyTo":null,"subject":"[PATCH v8 0/2] json-writer V8","fromName":"","fromEmail":"git@jeffhostetler.com","sentAt":"2018-06-07T14:12:07Z","receivedAt":"2018-06-07T14:12:36Z","isPatch":true,"sender":{"key":"git@jeffhostetler.com","avatar":null},"body":"From: Jeff Hostetler <jeffhost@microsoft.com>\n\nHere is V8 of my json-writer patches.  Please replace the existing V5/V6/V7\nversion of the jh/json-writer branch with this one.\n\nThis version uses perl rather than python to test the generated JSON.\n\nJeff Hostetler (2):\n  json_writer: new routines to create data in JSON format\n  json-writer: t0019: add perl unit test\n\n Makefile                    |   2 +\n json-writer.c               | 419 ++++++++++++++++++++++++++++++++\n json-writer.h               | 113 +++++++++\n t/helper/test-json-writer.c | 572 ++++++++++++++++++++++++++++++++++++++++++++\n t/t0019-json-writer.sh      | 274 +++++++++++++++++++++\n t/t0019/parse_json.perl     |  52 ++++\n 6 files changed, 1432 insertions(+)\n create mode 100644 json-writer.c\n create mode 100644 json-writer.h\n create mode 100644 t/helper/test-json-writer.c\n create mode 100755 t/t0019-json-writer.sh\n create mode 100644 t/t0019/parse_json.perl\n\n-- \n2.9.3\n\n"},{"id":"349638","messageId":"20180607141209.24423-2-git@jeffhostetler.com","threadId":"48669","inReplyTo":"20180607141209.24423-1-git@jeffhostetler.com","subject":"[PATCH v8 1/2] json_writer: new routines to create data in JSON format","fromName":"","fromEmail":"git@jeffhostetler.com","sentAt":"2018-06-07T14:12:08Z","receivedAt":"2018-06-07T14:12:42Z","isPatch":true,"sender":{"key":"git@jeffhostetler.com","avatar":null},"body":"From: Jeff Hostetler <jeffhost@microsoft.com>\n\nAdd a series of jw_ routines and \"struct json_writer\" structure to compose\nJSON data.  The resulting string data can then be output by commands wanting\nto support a JSON output format.\n\nThe json-writer routines can be used to generate structured data in a\nJSON-like format.  We say \"JSON-like\" because we do not enforce the Unicode\n(usually UTF-8) requirement on string fields.  Internally, Git does not\nnecessarily have Unicode/UTF-8 data for most fields, so it is currently\nunclear the best way to enforce that requirement.  For example, on Linx\npathnames can contain arbitrary 8-bit character data, so a command like\n\"status\" would not know how to encode the reported pathnames.  We may want\nto revisit this (or double encode such strings) in the future.\n\nThe initial use for the json-writer routines is for generating telemetry\ndata for executed Git commands.  Later, we may want to use them in other\ncommands, such as status.\n\nHelped-by: René Scharfe <l.s.r@web.de>\nHelped-by: Wink Saville <wink@saville.com>\nHelped-by: Ramsay Jones <ramsay@ramsayjones.plus.com>\nSigned-off-by: Jeff Hostetler <jeffhost@microsoft.com>\n---\n Makefile                    |   2 +\n json-writer.c               | 419 ++++++++++++++++++++++++++++++++\n json-writer.h               | 113 +++++++++\n t/helper/test-json-writer.c | 572 ++++++++++++++++++++++++++++++++++++++++++++\n t/t0019-json-writer.sh      | 236 ++++++++++++++++++\n 5 files changed, 1342 insertions(+)\n create mode 100644 json-writer.c\n create mode 100644 json-writer.h\n create mode 100644 t/helper/test-json-writer.c\n create mode 100755 t/t0019-json-writer.sh\n\ndiff --git a/Makefile b/Makefile\nindex a1d8775..4ae6946 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -666,6 +666,7 @@ TEST_PROGRAMS_NEED_X += test-fake-ssh\n TEST_PROGRAMS_NEED_X += test-genrandom\n TEST_PROGRAMS_NEED_X += test-hashmap\n TEST_PROGRAMS_NEED_X += test-index-version\n+TEST_PROGRAMS_NEED_X += test-json-writer\n TEST_PROGRAMS_NEED_X += test-lazy-init-name-hash\n TEST_PROGRAMS_NEED_X += test-line-buffer\n TEST_PROGRAMS_NEED_X += test-match-trees\n@@ -820,6 +821,7 @@ LIB_OBJS += hashmap.o\n LIB_OBJS += help.o\n LIB_OBJS += hex.o\n LIB_OBJS += ident.o\n+LIB_OBJS += json-writer.o\n LIB_OBJS += kwset.o\n LIB_OBJS += levenshtein.o\n LIB_OBJS += line-log.o\ndiff --git a/json-writer.c b/json-writer.c\nnew file mode 100644\nindex 0000000..f35ce19\n--- /dev/null\n+++ b/json-writer.c\n@@ -0,0 +1,419 @@\n+#include \"cache.h\"\n+#include \"json-writer.h\"\n+\n+void jw_init(struct json_writer *jw)\n+{\n+\tstrbuf_reset(&jw->json);\n+\tstrbuf_reset(&jw->open_stack);\n+\tstrbuf_reset(&jw->first_stack);\n+\tjw->pretty = 0;\n+}\n+\n+void jw_release(struct json_writer *jw)\n+{\n+\tstrbuf_release(&jw->json);\n+\tstrbuf_release(&jw->open_stack);\n+\tstrbuf_release(&jw->first_stack);\n+}\n+\n+/*\n+ * Append JSON-quoted version of the given string to 'out'.\n+ */\n+static void append_quoted_string(struct strbuf *out, const char *in)\n+{\n+\tunsigned char c;\n+\n+\tstrbuf_addch(out, '\"');\n+\twhile ((c = *in++) != '\\0') {\n+\t\tif (c == '\"')\n+\t\t\tstrbuf_addstr(out, \"\\\\\\\"\");\n+\t\telse if (c == '\\\\')\n+\t\t\tstrbuf_addstr(out, \"\\\\\\\\\");\n+\t\telse if (c == '\\n')\n+\t\t\tstrbuf_addstr(out, \"\\\\n\");\n+\t\telse if (c == '\\r')\n+\t\t\tstrbuf_addstr(out, \"\\\\r\");\n+\t\telse if (c == '\\t')\n+\t\t\tstrbuf_addstr(out, \"\\\\t\");\n+\t\telse if (c == '\\f')\n+\t\t\tstrbuf_addstr(out, \"\\\\f\");\n+\t\telse if (c == '\\b')\n+\t\t\tstrbuf_addstr(out, \"\\\\b\");\n+\t\telse if (c < 0x20)\n+\t\t\tstrbuf_addf(out, \"\\\\u%04x\", c);\n+\t\telse\n+\t\t\tstrbuf_addch(out, c);\n+\t}\n+\tstrbuf_addch(out, '\"');\n+}\n+\n+static inline void indent_pretty(struct json_writer *jw)\n+{\n+\tint k;\n+\n+\tif (!jw->pretty)\n+\t\treturn;\n+\n+\tfor (k = 0; k < jw->open_stack.len; k++)\n+\t\tstrbuf_addstr(&jw->json, \"  \");\n+}\n+\n+/*\n+ * Begin an object or array (either top-level or nested within the currently\n+ * open object or array).\n+ */\n+static inline void begin(struct json_writer *jw, char ch_open, int pretty)\n+{\n+\tjw->pretty = pretty;\n+\n+\tstrbuf_addch(&jw->json, ch_open);\n+\n+\tstrbuf_addch(&jw->open_stack, ch_open);\n+\tstrbuf_addch(&jw->first_stack, '1');\n+}\n+\n+/*\n+ * Assert that the top of the open-stack is an object.\n+ */\n+static inline void assert_in_object(const struct json_writer *jw, const char *key)\n+{\n+\tif (!jw->open_stack.len)\n+\t\tBUG(\"json-writer: object: missing jw_object_begin(): '%s'\", key);\n+\tif (jw->open_stack.buf[jw->open_stack.len - 1] != '{')\n+\t\tBUG(\"json-writer: object: not in object: '%s'\", key);\n+}\n+\n+/*\n+ * Assert that the top of the open-stack is an array.\n+ */\n+static inline void assert_in_array(const struct json_writer *jw)\n+{\n+\tif (!jw->open_stack.len)\n+\t\tBUG(\"json-writer: array: missing jw_array_begin()\");\n+\tif (jw->open_stack.buf[jw->open_stack.len - 1] != '[')\n+\t\tBUG(\"json-writer: array: not in array\");\n+}\n+\n+/*\n+ * Add comma if we have already seen a member at this level.\n+ */\n+static inline void maybe_add_comma(struct json_writer *jw)\n+{\n+\tif (!jw->open_stack.len)\n+\t\treturn;\n+\tif (jw->first_stack.buf[jw->first_stack.len - 1] == '1')\n+\t\tjw->first_stack.buf[jw->first_stack.len - 1] = '0';\n+\telse\n+\t\tstrbuf_addch(&jw->json, ',');\n+}\n+\n+static inline void fmt_double(struct json_writer *jw, int precision,\n+\t\t\t      double value)\n+{\n+\tif (precision < 0) {\n+\t\tstrbuf_addf(&jw->json, \"%f\", value);\n+\t} else {\n+\t\tstruct strbuf fmt = STRBUF_INIT;\n+\t\tstrbuf_addf(&fmt, \"%%.%df\", precision);\n+\t\tstrbuf_addf(&jw->json, fmt.buf, value);\n+\t\tstrbuf_release(&fmt);\n+\t}\n+}\n+\n+static inline void object_common(struct json_writer *jw, const char *key)\n+{\n+\tassert_in_object(jw, key);\n+\tmaybe_add_comma(jw);\n+\n+\tif (jw->pretty)\n+\t\tstrbuf_addch(&jw->json, '\\n');\n+\tindent_pretty(jw);\n+\n+\tappend_quoted_string(&jw->json, key);\n+\tstrbuf_addch(&jw->json, ':');\n+\tif (jw->pretty)\n+\t\tstrbuf_addch(&jw->json, ' ');\n+}\n+\n+static inline void array_common(struct json_writer *jw)\n+{\n+\tassert_in_array(jw);\n+\tmaybe_add_comma(jw);\n+\n+\tif (jw->pretty)\n+\t\tstrbuf_addch(&jw->json, '\\n');\n+\tindent_pretty(jw);\n+}\n+\n+/*\n+ * Assert that the given JSON object or JSON array has been properly\n+ * terminated.  (Has closing bracket.)\n+ */\n+static inline void assert_is_terminated(const struct json_writer *jw)\n+{\n+\tif (jw->open_stack.len)\n+\t\tBUG(\"json-writer: object: missing jw_end(): '%s'\",\n+\t\t    jw->json.buf);\n+}\n+\n+void jw_object_begin(struct json_writer *jw, int pretty)\n+{\n+\tbegin(jw, '{', pretty);\n+}\n+\n+void jw_object_string(struct json_writer *jw, const char *key, const char *value)\n+{\n+\tobject_common(jw, key);\n+\tappend_quoted_string(&jw->json, value);\n+}\n+\n+void jw_object_intmax(struct json_writer *jw, const char *key, intmax_t value)\n+{\n+\tobject_common(jw, key);\n+\tstrbuf_addf(&jw->json, \"%\"PRIdMAX, value);\n+}\n+\n+void jw_object_double(struct json_writer *jw, const char *key, int precision,\n+\t\t      double value)\n+{\n+\tobject_common(jw, key);\n+\tfmt_double(jw, precision, value);\n+}\n+\n+void jw_object_true(struct json_writer *jw, const char *key)\n+{\n+\tobject_common(jw, key);\n+\tstrbuf_addstr(&jw->json, \"true\");\n+}\n+\n+void jw_object_false(struct json_writer *jw, const char *key)\n+{\n+\tobject_common(jw, key);\n+\tstrbuf_addstr(&jw->json, \"false\");\n+}\n+\n+void jw_object_bool(struct json_writer *jw, const char *key, int value)\n+{\n+\tif (value)\n+\t\tjw_object_true(jw, key);\n+\telse\n+\t\tjw_object_false(jw, key);\n+}\n+\n+void jw_object_null(struct json_writer *jw, const char *key)\n+{\n+\tobject_common(jw, key);\n+\tstrbuf_addstr(&jw->json, \"null\");\n+}\n+\n+static void increase_indent(struct strbuf *sb,\n+\t\t\t    const struct json_writer *jw,\n+\t\t\t    int indent)\n+{\n+\tint k;\n+\n+\tstrbuf_reset(sb);\n+\tfor (k = 0; k < jw->json.len; k++) {\n+\t\tchar ch = jw->json.buf[k];\n+\t\tstrbuf_addch(sb, ch);\n+\t\tif (ch == '\\n')\n+\t\t\tstrbuf_addchars(sb, ' ', indent);\n+\t}\n+}\n+\n+static void kill_indent(struct strbuf *sb,\n+\t\t\tconst struct json_writer *jw)\n+{\n+\tint k;\n+\tint eat_it = 0;\n+\n+\tstrbuf_reset(sb);\n+\tfor (k = 0; k < jw->json.len; k++) {\n+\t\tchar ch = jw->json.buf[k];\n+\t\tif (eat_it && ch == ' ')\n+\t\t\tcontinue;\n+\t\tif (ch == '\\n') {\n+\t\t\teat_it = 1;\n+\t\t\tcontinue;\n+\t\t}\n+\t\teat_it = 0;\n+\t\tstrbuf_addch(sb, ch);\n+\t}\n+}\n+\n+static void append_sub_jw(struct json_writer *jw,\n+\t\t\t  const struct json_writer *value)\n+{\n+\t/*\n+\t * If both are pretty, increase the indentation of the sub_jw\n+\t * to better fit under the super.\n+\t *\n+\t * If the super is pretty, but the sub_jw is compact, leave the\n+\t * sub_jw compact.  (We don't want to parse and rebuild the sub_jw\n+\t * for this debug-ish feature.)\n+\t *\n+\t * If the super is compact, and the sub_jw is pretty, convert\n+\t * the sub_jw to compact.\n+\t *\n+\t * If both are compact, keep the sub_jw compact.\n+\t */\n+\tif (jw->pretty && jw->open_stack.len && value->pretty) {\n+\t\tstruct strbuf sb = STRBUF_INIT;\n+\t\tincrease_indent(&sb, value, jw->open_stack.len * 2);\n+\t\tstrbuf_addbuf(&jw->json, &sb);\n+\t\tstrbuf_release(&sb);\n+\t\treturn;\n+\t}\n+\tif (!jw->pretty && value->pretty) {\n+\t\tstruct strbuf sb = STRBUF_INIT;\n+\t\tkill_indent(&sb, value);\n+\t\tstrbuf_addbuf(&jw->json, &sb);\n+\t\tstrbuf_release(&sb);\n+\t\treturn;\n+\t}\n+\n+\tstrbuf_addbuf(&jw->json, &value->json);\n+}\n+\n+/*\n+ * Append existing (properly terminated) JSON sub-data (object or array)\n+ * as-is onto the given JSON data.\n+ */\n+void jw_object_sub_jw(struct json_writer *jw, const char *key,\n+\t\t      const struct json_writer *value)\n+{\n+\tassert_is_terminated(value);\n+\n+\tobject_common(jw, key);\n+\tappend_sub_jw(jw, value);\n+}\n+\n+void jw_object_inline_begin_object(struct json_writer *jw, const char *key)\n+{\n+\tobject_common(jw, key);\n+\n+\tjw_object_begin(jw, jw->pretty);\n+}\n+\n+void jw_object_inline_begin_array(struct json_writer *jw, const char *key)\n+{\n+\tobject_common(jw, key);\n+\n+\tjw_array_begin(jw, jw->pretty);\n+}\n+\n+void jw_array_begin(struct json_writer *jw, int pretty)\n+{\n+\tbegin(jw, '[', pretty);\n+}\n+\n+void jw_array_string(struct json_writer *jw, const char *value)\n+{\n+\tarray_common(jw);\n+\tappend_quoted_string(&jw->json, value);\n+}\n+\n+void jw_array_intmax(struct json_writer *jw, intmax_t value)\n+{\n+\tarray_common(jw);\n+\tstrbuf_addf(&jw->json, \"%\"PRIdMAX, value);\n+}\n+\n+void jw_array_double(struct json_writer *jw, int precision, double value)\n+{\n+\tarray_common(jw);\n+\tfmt_double(jw, precision, value);\n+}\n+\n+void jw_array_true(struct json_writer *jw)\n+{\n+\tarray_common(jw);\n+\tstrbuf_addstr(&jw->json, \"true\");\n+}\n+\n+void jw_array_false(struct json_writer *jw)\n+{\n+\tarray_common(jw);\n+\tstrbuf_addstr(&jw->json, \"false\");\n+}\n+\n+void jw_array_bool(struct json_writer *jw, int value)\n+{\n+\tif (value)\n+\t\tjw_array_true(jw);\n+\telse\n+\t\tjw_array_false(jw);\n+}\n+\n+void jw_array_null(struct json_writer *jw)\n+{\n+\tarray_common(jw);\n+\tstrbuf_addstr(&jw->json, \"null\");\n+}\n+\n+void jw_array_sub_jw(struct json_writer *jw, const struct json_writer *value)\n+{\n+\tassert_is_terminated(value);\n+\n+\tarray_common(jw);\n+\tappend_sub_jw(jw, value);\n+}\n+\n+void jw_array_argc_argv(struct json_writer *jw, int argc, const char **argv)\n+{\n+\tint k;\n+\n+\tfor (k = 0; k < argc; k++)\n+\t\tjw_array_string(jw, argv[k]);\n+}\n+\n+void jw_array_argv(struct json_writer *jw, const char **argv)\n+{\n+\twhile (*argv)\n+\t\tjw_array_string(jw, *argv++);\n+}\n+\n+void jw_array_inline_begin_object(struct json_writer *jw)\n+{\n+\tarray_common(jw);\n+\n+\tjw_object_begin(jw, jw->pretty);\n+}\n+\n+void jw_array_inline_begin_array(struct json_writer *jw)\n+{\n+\tarray_common(jw);\n+\n+\tjw_array_begin(jw, jw->pretty);\n+}\n+\n+int jw_is_terminated(const struct json_writer *jw)\n+{\n+\treturn !jw->open_stack.len;\n+}\n+\n+void jw_end(struct json_writer *jw)\n+{\n+\tchar ch_open;\n+\tint len;\n+\n+\tif (jw->open_stack.len != jw->first_stack.len)\n+\t\tBUG(\"jwon-writer: open_ and first_ stacks out of sync\");\n+\tif (!jw->open_stack.len)\n+\t\tBUG(\"json-writer: too many jw_end(): '%s'\", jw->json.buf);\n+\n+\tlen = jw->open_stack.len - 1;\n+\tch_open = jw->open_stack.buf[len];\n+\n+\tstrbuf_setlen(&jw->open_stack, len);\n+\tstrbuf_setlen(&jw->first_stack, len);\n+\n+\tif (jw->pretty)\n+\t\tstrbuf_addch(&jw->json, '\\n');\n+\tindent_pretty(jw);\n+\n+\tif (ch_open == '{')\n+\t\tstrbuf_addch(&jw->json, '}');\n+\telse\n+\t\tstrbuf_addch(&jw->json, ']');\n+}\ndiff --git a/json-writer.h b/json-writer.h\nnew file mode 100644\nindex 0000000..af9c261\n--- /dev/null\n+++ b/json-writer.h\n@@ -0,0 +1,113 @@\n+#ifndef JSON_WRITER_H\n+#define JSON_WRITER_H\n+\n+/*\n+ * JSON data structures are defined at:\n+ * [1] http://www.ietf.org/rfc/rfc7159.txt\n+ * [2] http://json.org/\n+ *\n+ * The JSON-writer API allows one to build JSON data structures using a\n+ * simple wrapper around a \"struct strbuf\" buffer.  It is intended as a\n+ * simple API to build output strings; it is not intended to be a general\n+ * object model for JSON data.  In particular, it does not re-order keys\n+ * in an object (dictionary), it does not de-dup keys in an object, and\n+ * it does not allow lookup or parsing of JSON data.\n+ *\n+ * All string values (both keys and string r-values) are properly quoted\n+ * and escaped if they contain special characters.\n+ *\n+ * These routines create compact JSON data (with no unnecessary whitespace,\n+ * newlines, or indenting).  If you get an unexpected response, verify\n+ * that you're not expecting a pretty JSON string.\n+ *\n+ * Both \"JSON objects\" (aka sets of k/v pairs) and \"JSON array\" can be\n+ * constructed using a 'begin append* end' model.\n+ *\n+ * Nested objects and arrays can either be constructed bottom up (by\n+ * creating sub object/arrays first and appending them to the super\n+ * object/array) -or- by building them inline in one pass.  This is a\n+ * personal style and/or data shape choice.\n+ *\n+ * See t/helper/test-json-writer.c for various usage examples.\n+ *\n+ * LIMITATIONS:\n+ * ============\n+ *\n+ * The JSON specification [1,2] defines string values as Unicode data\n+ * and probably UTF-8 encoded.  The current json-writer API does not\n+ * enforce this and will write any string as received.  However, it will\n+ * properly quote and backslash-escape them as necessary.  It is up to\n+ * the caller to UTF-8 encode their strings *before* passing them to this\n+ * API.  This layer should not have to try to guess the encoding or locale\n+ * of the given strings.\n+ */\n+\n+struct json_writer\n+{\n+\t/*\n+\t * Buffer of the in-progress JSON currently being composed.\n+\t */\n+\tstruct strbuf json;\n+\n+\t/*\n+\t * Simple stack of the currently open array and object forms.\n+\t * This is a string of '{' and '[' characters indicating the\n+\t * currently unterminated forms.  This is used to ensure the\n+\t * properly closing character is used when popping a level and\n+\t * to know when the JSON is completely closed.\n+\t */\n+\tstruct strbuf open_stack;\n+\n+\t/*\n+\t * Another simple stack of the currently open array and object\n+\t * forms.  This is used in parallel to \"open_stack\" (same length).\n+\t * It contains a string of '1' and '0' where '1' indicates that\n+\t * the next child-element seen will be the first child (and does\n+\t * not need a leading comma).\n+\t */\n+\tstruct strbuf first_stack;\n+\n+\tint pretty;\n+};\n+\n+#define JSON_WRITER_INIT { STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, 0 }\n+\n+void jw_init(struct json_writer *jw);\n+void jw_release(struct json_writer *jw);\n+\n+void jw_object_begin(struct json_writer *jw, int pretty);\n+void jw_array_begin(struct json_writer *jw, int pretty);\n+\n+void jw_object_string(struct json_writer *jw, const char *key,\n+\t\t      const char *value);\n+void jw_object_intmax(struct json_writer *jw, const char *key, intmax_t value);\n+void jw_object_double(struct json_writer *jw, const char *key, int precision,\n+\t\t      double value);\n+void jw_object_true(struct json_writer *jw, const char *key);\n+void jw_object_false(struct json_writer *jw, const char *key);\n+void jw_object_bool(struct json_writer *jw, const char *key, int value);\n+void jw_object_null(struct json_writer *jw, const char *key);\n+void jw_object_sub_jw(struct json_writer *jw, const char *key,\n+\t\t      const struct json_writer *value);\n+\n+void jw_object_inline_begin_object(struct json_writer *jw, const char *key);\n+void jw_object_inline_begin_array(struct json_writer *jw, const char *key);\n+\n+void jw_array_string(struct json_writer *jw, const char *value);\n+void jw_array_intmax(struct json_writer *jw, intmax_t value);\n+void jw_array_double(struct json_writer *jw, int precision, double value);\n+void jw_array_true(struct json_writer *jw);\n+void jw_array_false(struct json_writer *jw);\n+void jw_array_bool(struct json_writer *jw, int value);\n+void jw_array_null(struct json_writer *jw);\n+void jw_array_sub_jw(struct json_writer *jw, const struct json_writer *value);\n+void jw_array_argc_argv(struct json_writer *jw, int argc, const char **argv);\n+void jw_array_argv(struct json_writer *jw, const char **argv);\n+\n+void jw_array_inline_begin_object(struct json_writer *jw);\n+void jw_array_inline_begin_array(struct json_writer *jw);\n+\n+int jw_is_terminated(const struct json_writer *jw);\n+void jw_end(struct json_writer *jw);\n+\n+#endif /* JSON_WRITER_H */\ndiff --git a/t/helper/test-json-writer.c b/t/helper/test-json-writer.c\nnew file mode 100644\nindex 0000000..5bc117c\n--- /dev/null\n+++ b/t/helper/test-json-writer.c\n@@ -0,0 +1,572 @@\n+#include \"cache.h\"\n+#include \"json-writer.h\"\n+\n+static const char *expect_obj1 = \"{\\\"a\\\":\\\"abc\\\",\\\"b\\\":42,\\\"c\\\":true}\";\n+static const char *expect_obj2 = \"{\\\"a\\\":-1,\\\"b\\\":2147483647,\\\"c\\\":0}\";\n+static const char *expect_obj3 = \"{\\\"a\\\":0,\\\"b\\\":4294967295,\\\"c\\\":9223372036854775807}\";\n+static const char *expect_obj4 = \"{\\\"t\\\":true,\\\"f\\\":false,\\\"n\\\":null}\";\n+static const char *expect_obj5 = \"{\\\"abc\\\\tdef\\\":\\\"abc\\\\\\\\def\\\"}\";\n+static const char *expect_obj6 = \"{\\\"a\\\":3.14}\";\n+\n+static const char *pretty_obj1 = (\"{\\n\"\n+\t\t\t\t  \"  \\\"a\\\": \\\"abc\\\",\\n\"\n+\t\t\t\t  \"  \\\"b\\\": 42,\\n\"\n+\t\t\t\t  \"  \\\"c\\\": true\\n\"\n+\t\t\t\t  \"}\");\n+static const char *pretty_obj2 = (\"{\\n\"\n+\t\t\t\t  \"  \\\"a\\\": -1,\\n\"\n+\t\t\t\t  \"  \\\"b\\\": 2147483647,\\n\"\n+\t\t\t\t  \"  \\\"c\\\": 0\\n\"\n+\t\t\t\t  \"}\");\n+static const char *pretty_obj3 = (\"{\\n\"\n+\t\t\t\t  \"  \\\"a\\\": 0,\\n\"\n+\t\t\t\t  \"  \\\"b\\\": 4294967295,\\n\"\n+\t\t\t\t  \"  \\\"c\\\": 9223372036854775807\\n\"\n+\t\t\t\t  \"}\");\n+static const char *pretty_obj4 = (\"{\\n\"\n+\t\t\t\t  \"  \\\"t\\\": true,\\n\"\n+\t\t\t\t  \"  \\\"f\\\": false,\\n\"\n+\t\t\t\t  \"  \\\"n\\\": null\\n\"\n+\t\t\t\t  \"}\");\n+\n+static struct json_writer obj1 = JSON_WRITER_INIT;\n+static struct json_writer obj2 = JSON_WRITER_INIT;\n+static struct json_writer obj3 = JSON_WRITER_INIT;\n+static struct json_writer obj4 = JSON_WRITER_INIT;\n+static struct json_writer obj5 = JSON_WRITER_INIT;\n+static struct json_writer obj6 = JSON_WRITER_INIT;\n+\n+static void make_obj1(int pretty)\n+{\n+\tjw_object_begin(&obj1, pretty);\n+\t{\n+\t\tjw_object_string(&obj1, \"a\", \"abc\");\n+\t\tjw_object_intmax(&obj1, \"b\", 42);\n+\t\tjw_object_true(&obj1, \"c\");\n+\t}\n+\tjw_end(&obj1);\n+}\n+\n+static void make_obj2(int pretty)\n+{\n+\tjw_object_begin(&obj2, pretty);\n+\t{\n+\t\tjw_object_intmax(&obj2, \"a\", -1);\n+\t\tjw_object_intmax(&obj2, \"b\", 0x7fffffff);\n+\t\tjw_object_intmax(&obj2, \"c\", 0);\n+\t}\n+\tjw_end(&obj2);\n+}\n+\n+static void make_obj3(int pretty)\n+{\n+\tjw_object_begin(&obj3, pretty);\n+\t{\n+\t\tjw_object_intmax(&obj3, \"a\", 0);\n+\t\tjw_object_intmax(&obj3, \"b\", 0xffffffff);\n+\t\tjw_object_intmax(&obj3, \"c\", 0x7fffffffffffffffULL);\n+\t}\n+\tjw_end(&obj3);\n+}\n+\n+static void make_obj4(int pretty)\n+{\n+\tjw_object_begin(&obj4, pretty);\n+\t{\n+\t\tjw_object_true(&obj4, \"t\");\n+\t\tjw_object_false(&obj4, \"f\");\n+\t\tjw_object_null(&obj4, \"n\");\n+\t}\n+\tjw_end(&obj4);\n+}\n+\n+static void make_obj5(int pretty)\n+{\n+\tjw_object_begin(&obj5, pretty);\n+\t{\n+\t\tjw_object_string(&obj5, \"abc\" \"\\x09\" \"def\", \"abc\" \"\\\\\" \"def\");\n+\t}\n+\tjw_end(&obj5);\n+}\n+\n+static void make_obj6(int pretty)\n+{\n+\tjw_object_begin(&obj6, pretty);\n+\t{\n+\t\tjw_object_double(&obj6, \"a\", 2, 3.14159);\n+\t}\n+\tjw_end(&obj6);\n+}\n+\n+static const char *expect_arr1 = \"[\\\"abc\\\",42,true]\";\n+static const char *expect_arr2 = \"[-1,2147483647,0]\";\n+static const char *expect_arr3 = \"[0,4294967295,9223372036854775807]\";\n+static const char *expect_arr4 = \"[true,false,null]\";\n+\n+static const char *pretty_arr1 = (\"[\\n\"\n+\t\t\t\t  \"  \\\"abc\\\",\\n\"\n+\t\t\t\t  \"  42,\\n\"\n+\t\t\t\t  \"  true\\n\"\n+\t\t\t\t  \"]\");\n+static const char *pretty_arr2 = (\"[\\n\"\n+\t\t\t\t  \"  -1,\\n\"\n+\t\t\t\t  \"  2147483647,\\n\"\n+\t\t\t\t  \"  0\\n\"\n+\t\t\t\t  \"]\");\n+static const char *pretty_arr3 = (\"[\\n\"\n+\t\t\t\t  \"  0,\\n\"\n+\t\t\t\t  \"  4294967295,\\n\"\n+\t\t\t\t  \"  9223372036854775807\\n\"\n+\t\t\t\t  \"]\");\n+static const char *pretty_arr4 = (\"[\\n\"\n+\t\t\t\t  \"  true,\\n\"\n+\t\t\t\t  \"  false,\\n\"\n+\t\t\t\t  \"  null\\n\"\n+\t\t\t\t  \"]\");\n+\n+static struct json_writer arr1 = JSON_WRITER_INIT;\n+static struct json_writer arr2 = JSON_WRITER_INIT;\n+static struct json_writer arr3 = JSON_WRITER_INIT;\n+static struct json_writer arr4 = JSON_WRITER_INIT;\n+\n+static void make_arr1(int pretty)\n+{\n+\tjw_array_begin(&arr1, pretty);\n+\t{\n+\t\tjw_array_string(&arr1, \"abc\");\n+\t\tjw_array_intmax(&arr1, 42);\n+\t\tjw_array_true(&arr1);\n+\t}\n+\tjw_end(&arr1);\n+}\n+\n+static void make_arr2(int pretty)\n+{\n+\tjw_array_begin(&arr2, pretty);\n+\t{\n+\t\tjw_array_intmax(&arr2, -1);\n+\t\tjw_array_intmax(&arr2, 0x7fffffff);\n+\t\tjw_array_intmax(&arr2, 0);\n+\t}\n+\tjw_end(&arr2);\n+}\n+\n+static void make_arr3(int pretty)\n+{\n+\tjw_array_begin(&arr3, pretty);\n+\t{\n+\t\tjw_array_intmax(&arr3, 0);\n+\t\tjw_array_intmax(&arr3, 0xffffffff);\n+\t\tjw_array_intmax(&arr3, 0x7fffffffffffffffULL);\n+\t}\n+\tjw_end(&arr3);\n+}\n+\n+static void make_arr4(int pretty)\n+{\n+\tjw_array_begin(&arr4, pretty);\n+\t{\n+\t\tjw_array_true(&arr4);\n+\t\tjw_array_false(&arr4);\n+\t\tjw_array_null(&arr4);\n+\t}\n+\tjw_end(&arr4);\n+}\n+\n+static char *expect_nest1 =\n+\t\"{\\\"obj1\\\":{\\\"a\\\":\\\"abc\\\",\\\"b\\\":42,\\\"c\\\":true},\\\"arr1\\\":[\\\"abc\\\",42,true]}\";\n+\n+static struct json_writer nest1 = JSON_WRITER_INIT;\n+\n+static void make_nest1(int pretty)\n+{\n+\tjw_object_begin(&nest1, pretty);\n+\t{\n+\t\tjw_object_sub_jw(&nest1, \"obj1\", &obj1);\n+\t\tjw_object_sub_jw(&nest1, \"arr1\", &arr1);\n+\t}\n+\tjw_end(&nest1);\n+}\n+\n+static char *expect_inline1 =\n+\t\"{\\\"obj1\\\":{\\\"a\\\":\\\"abc\\\",\\\"b\\\":42,\\\"c\\\":true},\\\"arr1\\\":[\\\"abc\\\",42,true]}\";\n+\n+static char *pretty_inline1 =\n+\t(\"{\\n\"\n+\t \"  \\\"obj1\\\": {\\n\"\n+\t \"    \\\"a\\\": \\\"abc\\\",\\n\"\n+\t \"    \\\"b\\\": 42,\\n\"\n+\t \"    \\\"c\\\": true\\n\"\n+\t \"  },\\n\"\n+\t \"  \\\"arr1\\\": [\\n\"\n+\t \"    \\\"abc\\\",\\n\"\n+\t \"    42,\\n\"\n+\t \"    true\\n\"\n+\t \"  ]\\n\"\n+\t \"}\");\n+\n+static struct json_writer inline1 = JSON_WRITER_INIT;\n+\n+static void make_inline1(int pretty)\n+{\n+\tjw_object_begin(&inline1, pretty);\n+\t{\n+\t\tjw_object_inline_begin_object(&inline1, \"obj1\");\n+\t\t{\n+\t\t\tjw_object_string(&inline1, \"a\", \"abc\");\n+\t\t\tjw_object_intmax(&inline1, \"b\", 42);\n+\t\t\tjw_object_true(&inline1, \"c\");\n+\t\t}\n+\t\tjw_end(&inline1);\n+\t\tjw_object_inline_begin_array(&inline1, \"arr1\");\n+\t\t{\n+\t\t\tjw_array_string(&inline1, \"abc\");\n+\t\t\tjw_array_intmax(&inline1, 42);\n+\t\t\tjw_array_true(&inline1);\n+\t\t}\n+\t\tjw_end(&inline1);\n+\t}\n+\tjw_end(&inline1);\n+}\n+\n+static char *expect_inline2 =\n+\t\"[[1,2],[3,4],{\\\"a\\\":\\\"abc\\\"}]\";\n+\n+static char *pretty_inline2 =\n+\t(\"[\\n\"\n+\t \"  [\\n\"\n+\t \"    1,\\n\"\n+\t \"    2\\n\"\n+\t \"  ],\\n\"\n+\t \"  [\\n\"\n+\t \"    3,\\n\"\n+\t \"    4\\n\"\n+\t \"  ],\\n\"\n+\t \"  {\\n\"\n+\t \"    \\\"a\\\": \\\"abc\\\"\\n\"\n+\t \"  }\\n\"\n+\t \"]\");\n+\n+static struct json_writer inline2 = JSON_WRITER_INIT;\n+\n+static void make_inline2(int pretty)\n+{\n+\tjw_array_begin(&inline2, pretty);\n+\t{\n+\t\tjw_array_inline_begin_array(&inline2);\n+\t\t{\n+\t\t\tjw_array_intmax(&inline2, 1);\n+\t\t\tjw_array_intmax(&inline2, 2);\n+\t\t}\n+\t\tjw_end(&inline2);\n+\t\tjw_array_inline_begin_array(&inline2);\n+\t\t{\n+\t\t\tjw_array_intmax(&inline2, 3);\n+\t\t\tjw_array_intmax(&inline2, 4);\n+\t\t}\n+\t\tjw_end(&inline2);\n+\t\tjw_array_inline_begin_object(&inline2);\n+\t\t{\n+\t\t\tjw_object_string(&inline2, \"a\", \"abc\");\n+\t\t}\n+\t\tjw_end(&inline2);\n+\t}\n+\tjw_end(&inline2);\n+}\n+\n+/*\n+ * When super is compact, we expect subs to be compacted (even if originally\n+ * pretty).\n+ */\n+static const char *expect_mixed1 =\n+\t(\"{\\\"obj1\\\":{\\\"a\\\":\\\"abc\\\",\\\"b\\\":42,\\\"c\\\":true},\"\n+\t \"\\\"arr1\\\":[\\\"abc\\\",42,true]}\");\n+\n+/*\n+ * When super is pretty, a compact sub (obj1) is kept compact and a pretty\n+ * sub (arr1) is re-indented.\n+ */\n+static const char *pretty_mixed1 =\n+\t(\"{\\n\"\n+\t \"  \\\"obj1\\\": {\\\"a\\\":\\\"abc\\\",\\\"b\\\":42,\\\"c\\\":true},\\n\"\n+\t \"  \\\"arr1\\\": [\\n\"\n+\t \"    \\\"abc\\\",\\n\"\n+\t \"    42,\\n\"\n+\t \"    true\\n\"\n+\t \"  ]\\n\"\n+\t \"}\");\n+\n+static struct json_writer mixed1 = JSON_WRITER_INIT;\n+\n+static void make_mixed1(int pretty)\n+{\n+\tjw_init(&obj1);\n+\tjw_init(&arr1);\n+\n+\tmake_obj1(0); /* obj1 is compact */\n+\tmake_arr1(1); /* arr1 is pretty */\n+\n+\tjw_object_begin(&mixed1, pretty);\n+\t{\n+\t\tjw_object_sub_jw(&mixed1, \"obj1\", &obj1);\n+\t\tjw_object_sub_jw(&mixed1, \"arr1\", &arr1);\n+\t}\n+\tjw_end(&mixed1);\n+}\n+\n+static void cmp(const char *test, const struct json_writer *jw, const char *exp)\n+{\n+\tif (!strcmp(jw->json.buf, exp))\n+\t\treturn;\n+\n+\tprintf(\"error[%s]: observed '%s' expected '%s'\\n\",\n+\t       test, jw->json.buf, exp);\n+\texit(1);\n+}\n+\n+#define t(v) do { make_##v(0); cmp(#v, &v, expect_##v); } while (0)\n+#define p(v) do { make_##v(1); cmp(#v, &v, pretty_##v); } while (0)\n+\n+/*\n+ * Run some basic regression tests with some known patterns.\n+ * These tests also demonstrate how to use the jw_ API.\n+ */\n+static int unit_tests(void)\n+{\n+\t/* comptact (canonical) forms */\n+\tt(obj1);\n+\tt(obj2);\n+\tt(obj3);\n+\tt(obj4);\n+\tt(obj5);\n+\tt(obj6);\n+\n+\tt(arr1);\n+\tt(arr2);\n+\tt(arr3);\n+\tt(arr4);\n+\n+\tt(nest1);\n+\n+\tt(inline1);\n+\tt(inline2);\n+\n+\tjw_init(&obj1);\n+\tjw_init(&obj2);\n+\tjw_init(&obj3);\n+\tjw_init(&obj4);\n+\n+\tjw_init(&arr1);\n+\tjw_init(&arr2);\n+\tjw_init(&arr3);\n+\tjw_init(&arr4);\n+\n+\tjw_init(&inline1);\n+\tjw_init(&inline2);\n+\n+\t/* pretty forms */\n+\tp(obj1);\n+\tp(obj2);\n+\tp(obj3);\n+\tp(obj4);\n+\n+\tp(arr1);\n+\tp(arr2);\n+\tp(arr3);\n+\tp(arr4);\n+\n+\tp(inline1);\n+\tp(inline2);\n+\n+\t/* mixed forms */\n+\tt(mixed1);\n+\tjw_init(&mixed1);\n+\tp(mixed1);\n+\n+\treturn 0;\n+}\n+\n+#define STMT(s) do { s } while (0)\n+\n+#define PARAM(tok, lbl, p) \\\n+\tSTMT( if (!(p) || (*(p) == '@')) \\\n+\t\tdie(\"token '%s' requires '%s' parameter, but saw: '%s'\", \\\n+\t\t    tok, lbl, p); )\n+\n+#define KEY() \\\n+\tSTMT( key = argv[++k]; \\\n+\t      PARAM(a_k, \"key\", key); )\n+\n+#define VAL() \\\n+\tSTMT( val = argv[++k]; \\\n+\t      PARAM(a_k, \"val\", val); )\n+\n+#define PRECISION() \\\n+\tSTMT( const char *s = argv[++k]; \\\n+\t      PARAM(a_k, \"precision\", s); \\\n+\t      precision = strtol(s, &endptr, 10); \\\n+\t      if (*endptr || errno == ERANGE) \\\n+\t\tdie(\"invalid '%s' precision: '%s'\", a_k, s); )\n+\n+#define VAL_INTMAX() \\\n+\tSTMT( VAL(); \\\n+\t      v_int = strtol(val, &endptr, 10); \\\n+\t      if (*endptr || errno == ERANGE) \\\n+\t\tdie(\"invalid '%s' value: '%s'\",\ta_k, val); )\n+\n+#define VAL_DOUBLE() \\\n+\tSTMT( VAL(); \\\n+\t      v_double = strtod(val, &endptr); \\\n+\t      if (*endptr || errno == ERANGE) \\\n+\t\tdie(\"invalid '%s' value: '%s'\",\ta_k, val); )\n+\n+static int pretty;\n+\n+static inline int scripted(int argc, const char **argv)\n+{\n+\tstruct json_writer jw = JSON_WRITER_INIT;\n+\tint k;\n+\n+\tif (!strcmp(argv[0], \"@object\"))\n+\t\tjw_object_begin(&jw, pretty);\n+\telse if (!strcmp(argv[0], \"@array\"))\n+\t\tjw_array_begin(&jw, pretty);\n+\telse\n+\t\tdie(\"expected '@object' or '@array': '%s'\", argv[0]);\n+\n+\tfor (k = 1; k < argc; k++) {\n+\t\tconst char *a_k = argv[k];\n+\t\tconst char *key;\n+\t\tconst char *val;\n+\t\tchar *endptr;\n+\t\tint precision;\n+\t\tintmax_t v_int;\n+\t\tdouble v_double;\n+\n+\t\tif (!strcmp(a_k, \"@end\")) {\n+\t\t\tjw_end(&jw);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (!strcmp(a_k, \"@object-string\")) {\n+\t\t\tKEY();\n+\t\t\tVAL();\n+\t\t\tjw_object_string(&jw, key, val);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@object-int\")) {\n+\t\t\tKEY();\n+\t\t\tVAL_INTMAX();\n+\t\t\tjw_object_intmax(&jw, key, v_int);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@object-double\")) {\n+\t\t\tKEY();\n+\t\t\tPRECISION();\n+\t\t\tVAL_DOUBLE();\n+\t\t\tjw_object_double(&jw, key, precision, v_double);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@object-true\")) {\n+\t\t\tKEY();\n+\t\t\tjw_object_true(&jw, key);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@object-false\")) {\n+\t\t\tKEY();\n+\t\t\tjw_object_false(&jw, key);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@object-null\")) {\n+\t\t\tKEY();\n+\t\t\tjw_object_null(&jw, key);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@object-object\")) {\n+\t\t\tKEY();\n+\t\t\tjw_object_inline_begin_object(&jw, key);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@object-array\")) {\n+\t\t\tKEY();\n+\t\t\tjw_object_inline_begin_array(&jw, key);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (!strcmp(a_k, \"@array-string\")) {\n+\t\t\tVAL();\n+\t\t\tjw_array_string(&jw, val);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@array-int\")) {\n+\t\t\tVAL_INTMAX();\n+\t\t\tjw_array_intmax(&jw, v_int);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@array-double\")) {\n+\t\t\tPRECISION();\n+\t\t\tVAL_DOUBLE();\n+\t\t\tjw_array_double(&jw, precision, v_double);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@array-true\")) {\n+\t\t\tjw_array_true(&jw);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@array-false\")) {\n+\t\t\tjw_array_false(&jw);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@array-null\")) {\n+\t\t\tjw_array_null(&jw);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@array-object\")) {\n+\t\t\tjw_array_inline_begin_object(&jw);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(a_k, \"@array-array\")) {\n+\t\t\tjw_array_inline_begin_array(&jw);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tdie(\"unrecognized token: '%s'\", a_k);\n+\t}\n+\n+\tif (!jw_is_terminated(&jw))\n+\t\tdie(\"json not terminated: '%s'\", jw.json.buf);\n+\n+\tprintf(\"%s\\n\", jw.json.buf);\n+\n+\tstrbuf_release(&jw.json);\n+\treturn 0;\n+}\n+\n+static inline int my_usage(void)\n+{\n+\tdie(\"usage: '-u' | '[-p] @object ... @end' | '[-p] @array ... @end'\");\n+}\n+\n+int cmd_main(int argc, const char **argv)\n+{\n+\tif (argc <= 1)\n+\t\treturn my_usage();\n+\n+\targc--;\n+\targv++;\n+\n+\tif (argv[0][0] == '-') {\n+\t\tif (!strcmp(argv[0], \"-u\") || !strcmp(argv[0], \"--unit\"))\n+\t\t\treturn unit_tests();\n+\n+\t\tif (!strcmp(argv[0], \"-p\") || !strcmp(argv[0], \"--pretty\")) {\n+\t\t\tpretty = 1;\n+\t\t\targc--;\n+\t\t\targv++;\n+\t\t\tif (argc < 1)\n+\t\t\t\treturn my_usage();\n+\t\t}\n+\t}\n+\n+\treturn scripted(argc, argv);\n+}\ndiff --git a/t/t0019-json-writer.sh b/t/t0019-json-writer.sh\nnew file mode 100755\nindex 0000000..c9c2e23\n--- /dev/null\n+++ b/t/t0019-json-writer.sh\n@@ -0,0 +1,236 @@\n+#!/bin/sh\n+\n+test_description='test json-writer JSON generation'\n+. ./test-lib.sh\n+\n+test_expect_success 'unit test of json-writer routines' '\n+\ttest-json-writer -u\n+'\n+\n+test_expect_success 'trivial object' '\n+\tcat >expect <<-\\EOF &&\n+\t{}\n+\tEOF\n+\ttest-json-writer >actual \\\n+\t\t@object \\\n+\t\t@end &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'trivial array' '\n+\tcat >expect <<-\\EOF &&\n+\t[]\n+\tEOF\n+\ttest-json-writer >actual \\\n+\t\t@array \\\n+\t\t@end &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'simple object' '\n+\tcat >expect <<-\\EOF &&\n+\t{\"a\":\"abc\",\"b\":42,\"c\":3.14,\"d\":true,\"e\":false,\"f\":null}\n+\tEOF\n+\ttest-json-writer >actual \\\n+\t\t@object \\\n+\t\t\t@object-string a abc \\\n+\t\t\t@object-int b 42 \\\n+\t\t\t@object-double c 2 3.140 \\\n+\t\t\t@object-true d \\\n+\t\t\t@object-false e \\\n+\t\t\t@object-null f \\\n+\t\t@end &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'simple array' '\n+\tcat >expect <<-\\EOF &&\n+\t[\"abc\",42,3.14,true,false,null]\n+\tEOF\n+\ttest-json-writer >actual \\\n+\t\t@array \\\n+\t\t\t@array-string abc \\\n+\t\t\t@array-int 42 \\\n+\t\t\t@array-double 2 3.140 \\\n+\t\t\t@array-true \\\n+\t\t\t@array-false \\\n+\t\t\t@array-null \\\n+\t\t@end &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'escape quoting string' '\n+\tcat >expect <<-\\EOF &&\n+\t{\"a\":\"abc\\\\def\"}\n+\tEOF\n+\ttest-json-writer >actual \\\n+\t\t@object \\\n+\t\t\t@object-string a abc\\\\def \\\n+\t\t@end &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'escape quoting string 2' '\n+\tcat >expect <<-\\EOF &&\n+\t{\"a\":\"abc\\\"def\"}\n+\tEOF\n+\ttest-json-writer >actual \\\n+\t\t@object \\\n+\t\t\t@object-string a abc\\\"def \\\n+\t\t@end &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'nested inline object' '\n+\tcat >expect <<-\\EOF &&\n+\t{\"a\":\"abc\",\"b\":42,\"sub1\":{\"c\":3.14,\"d\":true,\"sub2\":{\"e\":false,\"f\":null}}}\n+\tEOF\n+\ttest-json-writer >actual \\\n+\t\t@object \\\n+\t\t\t@object-string a abc \\\n+\t\t\t@object-int b 42 \\\n+\t\t\t@object-object \"sub1\" \\\n+\t\t\t\t@object-double c 2 3.140 \\\n+\t\t\t\t@object-true d \\\n+\t\t\t\t@object-object \"sub2\" \\\n+\t\t\t\t\t@object-false e \\\n+\t\t\t\t\t@object-null f \\\n+\t\t\t\t@end \\\n+\t\t\t@end \\\n+\t\t@end &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'nested inline array' '\n+\tcat >expect <<-\\EOF &&\n+\t[\"abc\",42,[3.14,true,[false,null]]]\n+\tEOF\n+\ttest-json-writer >actual \\\n+\t\t@array \\\n+\t\t\t@array-string abc \\\n+\t\t\t@array-int 42 \\\n+\t\t\t@array-array \\\n+\t\t\t\t@array-double 2 3.140 \\\n+\t\t\t\t@array-true \\\n+\t\t\t\t@array-array \\\n+\t\t\t\t\t@array-false \\\n+\t\t\t\t\t@array-null \\\n+\t\t\t\t@end \\\n+\t\t\t@end \\\n+\t\t@end &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'nested inline object and array' '\n+\tcat >expect <<-\\EOF &&\n+\t{\"a\":\"abc\",\"b\":42,\"sub1\":{\"c\":3.14,\"d\":true,\"sub2\":[false,null]}}\n+\tEOF\n+\ttest-json-writer >actual \\\n+\t\t@object \\\n+\t\t\t@object-string a abc \\\n+\t\t\t@object-int b 42 \\\n+\t\t\t@object-object \"sub1\" \\\n+\t\t\t\t@object-double c 2 3.140 \\\n+\t\t\t\t@object-true d \\\n+\t\t\t\t@object-array \"sub2\" \\\n+\t\t\t\t\t@array-false \\\n+\t\t\t\t\t@array-null \\\n+\t\t\t\t@end \\\n+\t\t\t@end \\\n+\t\t@end &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'nested inline object and array 2' '\n+\tcat >expect <<-\\EOF &&\n+\t{\"a\":\"abc\",\"b\":42,\"sub1\":{\"c\":3.14,\"d\":true,\"sub2\":[false,{\"g\":0,\"h\":1},null]}}\n+\tEOF\n+\ttest-json-writer >actual \\\n+\t\t@object \\\n+\t\t\t@object-string a abc \\\n+\t\t\t@object-int b 42 \\\n+\t\t\t@object-object \"sub1\" \\\n+\t\t\t\t@object-double c 2 3.140 \\\n+\t\t\t\t@object-true d \\\n+\t\t\t\t@object-array \"sub2\" \\\n+\t\t\t\t\t@array-false \\\n+\t\t\t\t\t@array-object \\\n+\t\t\t\t\t\t@object-int g 0 \\\n+\t\t\t\t\t\t@object-int h 1 \\\n+\t\t\t\t\t@end \\\n+\t\t\t\t\t@array-null \\\n+\t\t\t\t@end \\\n+\t\t\t@end \\\n+\t\t@end &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'pretty nested inline object and array 2' '\n+\tsed -e \"s/^|//\" >expect <<-\\EOF &&\n+\t|{\n+\t|  \"a\": \"abc\",\n+\t|  \"b\": 42,\n+\t|  \"sub1\": {\n+\t|    \"c\": 3.14,\n+\t|    \"d\": true,\n+\t|    \"sub2\": [\n+\t|      false,\n+\t|      {\n+\t|        \"g\": 0,\n+\t|        \"h\": 1\n+\t|      },\n+\t|      null\n+\t|    ]\n+\t|  }\n+\t|}\n+\tEOF\n+\ttest-json-writer >actual \\\n+\t\t--pretty \\\n+\t\t@object \\\n+\t\t\t@object-string a abc \\\n+\t\t\t@object-int b 42 \\\n+\t\t\t@object-object \"sub1\" \\\n+\t\t\t\t@object-double c 2 3.140 \\\n+\t\t\t\t@object-true d \\\n+\t\t\t\t@object-array \"sub2\" \\\n+\t\t\t\t\t@array-false \\\n+\t\t\t\t\t@array-object \\\n+\t\t\t\t\t\t@object-int g 0 \\\n+\t\t\t\t\t\t@object-int h 1 \\\n+\t\t\t\t\t@end \\\n+\t\t\t\t\t@array-null \\\n+\t\t\t\t@end \\\n+\t\t\t@end \\\n+\t\t@end &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'inline object with no members' '\n+\tcat >expect <<-\\EOF &&\n+\t{\"a\":\"abc\",\"empty\":{},\"b\":42}\n+\tEOF\n+\ttest-json-writer >actual \\\n+\t\t@object \\\n+\t\t\t@object-string a abc \\\n+\t\t\t@object-object \"empty\" \\\n+\t\t\t@end \\\n+\t\t\t@object-int b 42 \\\n+\t\t@end &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'inline array with no members' '\n+\tcat >expect <<-\\EOF &&\n+\t{\"a\":\"abc\",\"empty\":[],\"b\":42}\n+\tEOF\n+\ttest-json-writer >actual \\\n+\t\t@object \\\n+\t\t\t@object-string a abc \\\n+\t\t\t@object-array \"empty\" \\\n+\t\t\t@end \\\n+\t\t\t@object-int b 42 \\\n+\t\t@end &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \n2.9.3\n\n"},{"id":"349639","messageId":"20180607141209.24423-3-git@jeffhostetler.com","threadId":"48669","inReplyTo":"20180607141209.24423-1-git@jeffhostetler.com","subject":"[PATCH v8 2/2] json-writer: t0019: add perl unit test","fromName":"","fromEmail":"git@jeffhostetler.com","sentAt":"2018-06-07T14:12:09Z","receivedAt":"2018-06-07T14:12:45Z","isPatch":true,"sender":{"key":"git@jeffhostetler.com","avatar":null},"body":"From: Jeff Hostetler <jeffhost@microsoft.com>\n\nTest json-writer output using perl script.\n\nSigned-off-by: Jeff Hostetler <jeffhost@microsoft.com>\n---\n t/t0019-json-writer.sh  | 38 ++++++++++++++++++++++++++++++++++++\n t/t0019/parse_json.perl | 52 +++++++++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 90 insertions(+)\n create mode 100644 t/t0019/parse_json.perl\n\ndiff --git a/t/t0019-json-writer.sh b/t/t0019-json-writer.sh\nindex c9c2e23..fd61fe4 100755\n--- a/t/t0019-json-writer.sh\n+++ b/t/t0019-json-writer.sh\n@@ -233,4 +233,42 @@ test_expect_success 'inline array with no members' '\n \ttest_cmp expect actual\n '\n \n+# As a sanity check, ask Perl to parse our generated JSON and recursively\n+# dump the resulting data in sorted order.  Confirm that that matches our\n+# expectations.\n+test_expect_success 'parse JSON using Perl' '\n+\tcat >expect <<-\\EOF &&\n+\trow[0].a abc\n+\trow[0].b 42\n+\trow[0].sub1 hash\n+\trow[0].sub1.c 3.14\n+\trow[0].sub1.d 1\n+\trow[0].sub1.sub2 array\n+\trow[0].sub1.sub2[0] 0\n+\trow[0].sub1.sub2[1] hash\n+\trow[0].sub1.sub2[1].g 0\n+\trow[0].sub1.sub2[1].h 1\n+\trow[0].sub1.sub2[2] null\n+\tEOF\n+\ttest-json-writer >output.json \\\n+\t\t@object \\\n+\t\t\t@object-string a abc \\\n+\t\t\t@object-int b 42 \\\n+\t\t\t@object-object \"sub1\" \\\n+\t\t\t\t@object-double c 2 3.140 \\\n+\t\t\t\t@object-true d \\\n+\t\t\t\t@object-array \"sub2\" \\\n+\t\t\t\t\t@array-false \\\n+\t\t\t\t\t@array-object \\\n+\t\t\t\t\t\t@object-int g 0 \\\n+\t\t\t\t\t\t@object-int h 1 \\\n+\t\t\t\t\t@end \\\n+\t\t\t\t\t@array-null \\\n+\t\t\t\t@end \\\n+\t\t\t@end \\\n+\t\t@end &&\n+\tperl \"$TEST_DIRECTORY\"/t0019/parse_json.perl <output.json >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\ndiff --git a/t/t0019/parse_json.perl b/t/t0019/parse_json.perl\nnew file mode 100644\nindex 0000000..ca4e5bf\n--- /dev/null\n+++ b/t/t0019/parse_json.perl\n@@ -0,0 +1,52 @@\n+#!/usr/bin/perl\n+use strict;\n+use warnings;\n+use JSON;\n+\n+sub dump_array {\n+    my ($label_in, $ary_ref) = @_;\n+    my @ary = @$ary_ref;\n+\n+    for ( my $i = 0; $i <= $#{ $ary_ref }; $i++ )\n+    {\n+\tmy $label = \"$label_in\\[$i\\]\";\n+\tdump_item($label, $ary[$i]);\n+    }\n+}\n+\n+sub dump_hash {\n+    my ($label_in, $obj_ref) = @_;\n+    my %obj = %$obj_ref;\n+\n+    foreach my $k (sort keys %obj) {\n+\tmy $label = (length($label_in) > 0) ? \"$label_in.$k\" : \"$k\";\n+\tmy $value = $obj{$k};\n+\n+\tdump_item($label, $value);\n+    }\n+}\n+\n+sub dump_item {\n+    my ($label_in, $value) = @_;\n+    if (ref($value) eq 'ARRAY') {\n+\tprint \"$label_in array\\n\";\n+\tdump_array($label_in, $value);\n+    } elsif (ref($value) eq 'HASH') {\n+\tprint \"$label_in hash\\n\";\n+\tdump_hash($label_in, $value);\n+    } elsif (defined $value) {\n+\tprint \"$label_in $value\\n\";\n+    } else {\n+\tprint \"$label_in null\\n\";\n+    }\n+}\n+\n+my $row = 0;\n+while (<>) {\n+    my $data = decode_json( $_ );\n+    my $label = \"row[$row]\";\n+\n+    dump_hash($label, $data);\n+    $row++;\n+}\n+\n-- \n2.9.3\n\n"},{"id":"349647","messageId":"CAPig+cS8MjQNKabrQwuWzO4DDYJcfP_81n9H37efgXOiRzv9TQ@mail.gmail.com","threadId":"48669","inReplyTo":"20180607141209.24423-3-git@jeffhostetler.com","subject":"Re: [PATCH v8 2/2] json-writer: t0019: add perl unit test","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2018-06-07T17:13:07Z","receivedAt":"2018-06-07T17:13:21Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Thu, Jun 7, 2018 at 10:12 AM,  <git@jeffhostetler.com> wrote:\n> Test json-writer output using perl script.\n>\n> Signed-off-by: Jeff Hostetler <jeffhost@microsoft.com>\n> ---\n> diff --git a/t/t0019/parse_json.perl b/t/t0019/parse_json.perl\n> @@ -0,0 +1,52 @@\n> +#!/usr/bin/perl\n> +use strict;\n> +use warnings;\n> +use JSON;\n\nThis new script is going to have to be protected by a prerequisite\nsince the JSON module is not part of the standard Perl installation,\nthus will not necessarily be installed everywhere (it isn't on any of\nmy machines, for instance). Something like:\n\n    test_lazy_prereq PERLJSON '\n        perl -MJSON -e \"exit 0\"\n    '\n\nwhich would be used like this:\n\n    test_expect_success PERLJSON 'parse JSON using Perl' '\n        ...\n    '\n"},{"id":"349653","messageId":"CAPig+cRBCr2dKOBFnUF_D1SXfPTE5SPbB4t=y51VGa_jLQGkcA@mail.gmail.com","threadId":"48669","inReplyTo":"20180607141209.24423-2-git@jeffhostetler.com","subject":"Re: [PATCH v8 1/2] json_writer: new routines to create data in JSON format","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2018-06-07T17:24:26Z","receivedAt":"2018-06-07T17:24:31Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Thu, Jun 7, 2018 at 10:12 AM,  <git@jeffhostetler.com> wrote:\n> Add a series of jw_ routines and \"struct json_writer\" structure to compose\n> JSON data.  The resulting string data can then be output by commands wanting\n> to support a JSON output format.\n> [...]\n> Signed-off-by: Jeff Hostetler <jeffhost@microsoft.com>\n> ---\n> diff --git a/t/t0019-json-writer.sh b/t/t0019-json-writer.sh\n> @@ -0,0 +1,236 @@\n> +test_expect_success 'simple object' '\n> +       cat >expect <<-\\EOF &&\n> +       {\"a\":\"abc\",\"b\":42,\"c\":3.14,\"d\":true,\"e\":false,\"f\":null}\n> +       EOF\n> +       test-json-writer >actual \\\n> +               @object \\\n> +                       @object-string a abc \\\n> +                       @object-int b 42 \\\n> +                       @object-double c 2 3.140 \\\n> +                       @object-true d \\\n> +                       @object-false e \\\n> +                       @object-null f \\\n> +               @end &&\n> +       test_cmp expect actual\n> +'\n\nTo make it easier on people writing these tests, it might be nice for\nthis to be less noisy by getting rid of \"@\" and \"\\\". To get rid of\n\"\\\", the test program could grab its script commands from stdin (one\ninstruction per line) rather than from argv[]. For instance:\n\n    test-json-writer >actual <<-\\EOF &&\n        object\n            object-string a abc\n            ...\n        end\n    EOF\n\nNot a big deal, and certainly not worth a re-roll.\n"},{"id":"349680","messageId":"f1154c86-9d11-f272-643f-4316be40e9b7@web.de","threadId":"48669","inReplyTo":"20180607141209.24423-2-git@jeffhostetler.com","subject":"Re: [PATCH v8 1/2] json_writer: new routines to create data in JSON format","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2018-06-08T06:05:39Z","receivedAt":"2018-06-08T06:06:02Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 07.06.2018 um 16:12 schrieb git@jeffhostetler.com:\n> From: Jeff Hostetler <jeffhost@microsoft.com>\n> \n> Add a series of jw_ routines and \"struct json_writer\" structure to compose\n> JSON data.  The resulting string data can then be output by commands wanting\n> to support a JSON output format.\n> \n> The json-writer routines can be used to generate structured data in a\n> JSON-like format.  We say \"JSON-like\" because we do not enforce the Unicode\n> (usually UTF-8) requirement on string fields.  Internally, Git does not\n> necessarily have Unicode/UTF-8 data for most fields, so it is currently\n> unclear the best way to enforce that requirement.  For example, on Linx\n> pathnames can contain arbitrary 8-bit character data, so a command like\n> \"status\" would not know how to encode the reported pathnames.  We may want\n> to revisit this (or double encode such strings) in the future.\n> \n> The initial use for the json-writer routines is for generating telemetry\n> data for executed Git commands.  Later, we may want to use them in other\n> commands, such as status.\n> \n> Helped-by: René Scharfe <l.s.r@web.de>\n> Helped-by: Wink Saville <wink@saville.com>\n> Helped-by: Ramsay Jones <ramsay@ramsayjones.plus.com>\n> Signed-off-by: Jeff Hostetler <jeffhost@microsoft.com>\n> ---\n>   Makefile                    |   2 +\n>   json-writer.c               | 419 ++++++++++++++++++++++++++++++++\n>   json-writer.h               | 113 +++++++++\n>   t/helper/test-json-writer.c | 572 ++++++++++++++++++++++++++++++++++++++++++++\n>   t/t0019-json-writer.sh      | 236 ++++++++++++++++++\n>   5 files changed, 1342 insertions(+)\n>   create mode 100644 json-writer.c\n>   create mode 100644 json-writer.h\n>   create mode 100644 t/helper/test-json-writer.c\n>   create mode 100755 t/t0019-json-writer.sh\n> \n> diff --git a/Makefile b/Makefile\n> index a1d8775..4ae6946 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -666,6 +666,7 @@ TEST_PROGRAMS_NEED_X += test-fake-ssh\n>  TEST_PROGRAMS_NEED_X += test-genrandom\n>  TEST_PROGRAMS_NEED_X += test-hashmap\n>  TEST_PROGRAMS_NEED_X += test-index-version\n> +TEST_PROGRAMS_NEED_X += test-json-writer>  TEST_PROGRAMS_NEED_X += test-lazy-init-name-hash\n>  TEST_PROGRAMS_NEED_X += test-line-buffer\n>  TEST_PROGRAMS_NEED_X += test-match-trees\n\nThis doesn't apply cleanly on master, because most test helpers have\nbeen combined into a single binary to reduce their disk footprint and\nlink times.  See efd71f8913 (t/helper: add an empty test-tool program)\nfor the overall rationale.\n\ntest-json-writer could become a built-in as well, I think.  You can see\ne.g. in c932a5ff28 (t/helper: merge test-string-list into test-tool)\nwhat needs to be done to convert a helper.\n\nRené\n"},{"id":"349728","messageId":"0690b5a8-6c1c-e664-bb89-5d4e48258ddb@web.de","threadId":"48669","inReplyTo":"20180607141209.24423-2-git@jeffhostetler.com","subject":"Re: [PATCH v8 1/2] json_writer: new routines to create data in JSON format","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2018-06-08T20:07:14Z","receivedAt":"2018-06-08T20:07:37Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 07.06.2018 um 16:12 schrieb git@jeffhostetler.com:\n> From: Jeff Hostetler <jeffhost@microsoft.com>\n> +/*\n> + * Add comma if we have already seen a member at this level.\n> + */\n> +static inline void maybe_add_comma(struct json_writer *jw)\n> +{\n> +\tif (!jw->open_stack.len)\n> +\t\treturn;\n\nThis is impossible.  maybe_add_comma() is only called directly after\nassert_in_object() and assert_in_object(), which abort if open_stack \nis empty.\n\n> +\tif (jw->first_stack.buf[jw->first_stack.len - 1] == '1')\n> +\t\tjw->first_stack.buf[jw->first_stack.len - 1] = '0';\n> +\telse\n> +\t\tstrbuf_addch(&jw->json, ',');\n\nYou only need a binary flag to indicate if a comma is needed, not a\nstack.  We need a comma at the current level if and only if we already\nwrote a child item.  If we finish a level then we do need a comma at the\nprevious level because we just wrote a sub-object or sub-array there.\nAnd that should cover all cases.  Am I missing something?\n\nI get a sense of déjà vu, by the way. :)\n\nHere's what I mean:\n---\n json-writer.c | 17 ++++++-----------\n json-writer.h | 13 +++++--------\n 2 files changed, 11 insertions(+), 19 deletions(-)\n\ndiff --git a/json-writer.c b/json-writer.c\nindex f35ce1912a..14a4e89188 100644\n--- a/json-writer.c\n+++ b/json-writer.c\n@@ -5,7 +5,7 @@ void jw_init(struct json_writer *jw)\n {\n \tstrbuf_reset(&jw->json);\n \tstrbuf_reset(&jw->open_stack);\n-\tstrbuf_reset(&jw->first_stack);\n+\tjw->need_comma = 0;\n \tjw->pretty = 0;\n }\n \n@@ -13,7 +13,6 @@ void jw_release(struct json_writer *jw)\n {\n \tstrbuf_release(&jw->json);\n \tstrbuf_release(&jw->open_stack);\n-\tstrbuf_release(&jw->first_stack);\n }\n \n /*\n@@ -69,7 +68,7 @@ static inline void begin(struct json_writer *jw, char ch_open, int pretty)\n \tstrbuf_addch(&jw->json, ch_open);\n \n \tstrbuf_addch(&jw->open_stack, ch_open);\n-\tstrbuf_addch(&jw->first_stack, '1');\n+\tjw->need_comma = 0;\n }\n \n /*\n@@ -99,12 +98,10 @@ static inline void assert_in_array(const struct json_writer *jw)\n  */\n static inline void maybe_add_comma(struct json_writer *jw)\n {\n-\tif (!jw->open_stack.len)\n-\t\treturn;\n-\tif (jw->first_stack.buf[jw->first_stack.len - 1] == '1')\n-\t\tjw->first_stack.buf[jw->first_stack.len - 1] = '0';\n-\telse\n+\tif (jw->need_comma)\n \t\tstrbuf_addch(&jw->json, ',');\n+\telse\n+\t\tjw->need_comma = 1;\n }\n \n static inline void fmt_double(struct json_writer *jw, int precision,\n@@ -397,8 +394,6 @@ void jw_end(struct json_writer *jw)\n \tchar ch_open;\n \tint len;\n \n-\tif (jw->open_stack.len != jw->first_stack.len)\n-\t\tBUG(\"jwon-writer: open_ and first_ stacks out of sync\");\n \tif (!jw->open_stack.len)\n \t\tBUG(\"json-writer: too many jw_end(): '%s'\", jw->json.buf);\n \n@@ -406,7 +401,7 @@ void jw_end(struct json_writer *jw)\n \tch_open = jw->open_stack.buf[len];\n \n \tstrbuf_setlen(&jw->open_stack, len);\n-\tstrbuf_setlen(&jw->first_stack, len);\n+\tjw->need_comma = 1;\n \n \tif (jw->pretty)\n \t\tstrbuf_addch(&jw->json, '\\n');\ndiff --git a/json-writer.h b/json-writer.h\nindex af9c2612f8..c437462ba8 100644\n--- a/json-writer.h\n+++ b/json-writer.h\n@@ -59,18 +59,15 @@ struct json_writer\n \tstruct strbuf open_stack;\n \n \t/*\n-\t * Another simple stack of the currently open array and object\n-\t * forms.  This is used in parallel to \"open_stack\" (same length).\n-\t * It contains a string of '1' and '0' where '1' indicates that\n-\t * the next child-element seen will be the first child (and does\n-\t * not need a leading comma).\n+\t * Indicates if the next child item needs a leading comma.\n+\t * Only the first item of arrays and objects doesn't need one.\n \t */\n-\tstruct strbuf first_stack;\n+\tunsigned int need_comma:1;\n \n-\tint pretty;\n+\tunsigned int pretty:1;\n };\n \n-#define JSON_WRITER_INIT { STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, 0 }\n+#define JSON_WRITER_INIT { STRBUF_INIT, STRBUF_INIT, 0, 0 }\n \n void jw_init(struct json_writer *jw);\n void jw_release(struct json_writer *jw);\n-- \n2.17.1\n"},{"id":"349729","messageId":"9cfabdad-e4eb-a334-5e6c-aad6dfee8b87@jeffhostetler.com","threadId":"48669","inReplyTo":"CAPig+cRBCr2dKOBFnUF_D1SXfPTE5SPbB4t=y51VGa_jLQGkcA@mail.gmail.com","subject":"Re: [PATCH v8 1/2] json_writer: new routines to create data in JSON format","fromName":"Jeff Hostetler","fromEmail":"git@jeffhostetler.com","sentAt":"2018-06-08T20:17:20Z","receivedAt":"2018-06-08T20:18:43Z","isPatch":true,"sender":{"key":"git@jeffhostetler.com","avatar":null},"body":"\n\nOn 6/7/2018 1:24 PM, Eric Sunshine wrote:\n> On Thu, Jun 7, 2018 at 10:12 AM,  <git@jeffhostetler.com> wrote:\n>> Add a series of jw_ routines and \"struct json_writer\" structure to compose\n>> JSON data.  The resulting string data can then be output by commands wanting\n>> to support a JSON output format.\n>> [...]\n>> Signed-off-by: Jeff Hostetler <jeffhost@microsoft.com>\n>> ---\n>> diff --git a/t/t0019-json-writer.sh b/t/t0019-json-writer.sh\n>> @@ -0,0 +1,236 @@\n>> +test_expect_success 'simple object' '\n>> +       cat >expect <<-\\EOF &&\n>> +       {\"a\":\"abc\",\"b\":42,\"c\":3.14,\"d\":true,\"e\":false,\"f\":null}\n>> +       EOF\n>> +       test-json-writer >actual \\\n>> +               @object \\\n>> +                       @object-string a abc \\\n>> +                       @object-int b 42 \\\n>> +                       @object-double c 2 3.140 \\\n>> +                       @object-true d \\\n>> +                       @object-false e \\\n>> +                       @object-null f \\\n>> +               @end &&\n>> +       test_cmp expect actual\n>> +'\n> \n> To make it easier on people writing these tests, it might be nice for\n> this to be less noisy by getting rid of \"@\" and \"\\\". To get rid of\n> \"\\\", the test program could grab its script commands from stdin (one\n> instruction per line) rather than from argv[]. For instance:\n> \n>      test-json-writer >actual <<-\\EOF &&\n>          object\n>              object-string a abc\n>              ...\n>          end\n>      EOF\n> \n> Not a big deal, and certainly not worth a re-roll.\n> \n\nI hadn't thought about doing it that way.  Might be a little easier\nto use.  Let me take a look and see if it would be much work to switch.\n\nThanks\nJeff\n"},{"id":"349731","messageId":"515709a2-3cfe-b147-6176-ff95455e1692@web.de","threadId":"48669","inReplyTo":"20180607141209.24423-2-git@jeffhostetler.com","subject":"Re: [PATCH v8 1/2] json_writer: new routines to create data in JSON format","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2018-06-08T20:32:41Z","receivedAt":"2018-06-08T20:33:01Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 07.06.2018 um 16:12 schrieb git@jeffhostetler.com:\n>   Makefile                    |   2 +\n>   json-writer.c               | 419 ++++++++++++++++++++++++++++++++\n>   json-writer.h               | 113 +++++++++\n>   t/helper/test-json-writer.c | 572 ++++++++++++++++++++++++++++++++++++++++++++\n>   t/t0019-json-writer.sh      | 236 ++++++++++++++++++\n>   5 files changed, 1342 insertions(+)\n>   create mode 100644 json-writer.c\n>   create mode 100644 json-writer.h\n>   create mode 100644 t/helper/test-json-writer.c\n>   create mode 100755 t/t0019-json-writer.sh\n\n$ git grep 'static inline' '*json*'\njson-writer.c:static inline void indent_pretty(struct json_writer *jw)\njson-writer.c:static inline void begin(struct json_writer *jw, char ch_open, int pretty)\njson-writer.c:static inline void assert_in_object(const struct json_writer *jw, const char *key)\njson-writer.c:static inline void assert_in_array(const struct json_writer *jw)\njson-writer.c:static inline void maybe_add_comma(struct json_writer *jw)\njson-writer.c:static inline void fmt_double(struct json_writer *jw, int precision,\njson-writer.c:static inline void object_common(struct json_writer *jw, const char *key)\njson-writer.c:static inline void array_common(struct json_writer *jw)\njson-writer.c:static inline void assert_is_terminated(const struct json_writer *jw)\nt/helper/test-json-writer.c:static inline int scripted(int argc, const char **argv)\nt/helper/test-json-writer.c:static inline int my_usage(void)\n\nDo you need all those inline keywords?  I'd rather leave the decision\nabout inlining to the compiler and (via optimization flags) the user\nas much as possible.  Not a biggie, but the high frequency of that\nword made me blink..\n\nRené\n"},{"id":"349890","messageId":"293b890c-3d8f-a6b9-7f3c-f14e76ce7687@jeffhostetler.com","threadId":"48669","inReplyTo":"515709a2-3cfe-b147-6176-ff95455e1692@web.de","subject":"Re: [PATCH v8 1/2] json_writer: new routines to create data in JSON format","fromName":"Jeff Hostetler","fromEmail":"git@jeffhostetler.com","sentAt":"2018-06-11T10:40:33Z","receivedAt":"2018-06-11T10:40:38Z","isPatch":true,"sender":{"key":"git@jeffhostetler.com","avatar":null},"body":"\n\nOn 6/8/2018 4:32 PM, René Scharfe wrote:\n> Am 07.06.2018 um 16:12 schrieb git@jeffhostetler.com:\n>>    Makefile                    |   2 +\n>>    json-writer.c               | 419 ++++++++++++++++++++++++++++++++\n>>    json-writer.h               | 113 +++++++++\n>>    t/helper/test-json-writer.c | 572 ++++++++++++++++++++++++++++++++++++++++++++\n>>    t/t0019-json-writer.sh      | 236 ++++++++++++++++++\n>>    5 files changed, 1342 insertions(+)\n>>    create mode 100644 json-writer.c\n>>    create mode 100644 json-writer.h\n>>    create mode 100644 t/helper/test-json-writer.c\n>>    create mode 100755 t/t0019-json-writer.sh\n> \n> $ git grep 'static inline' '*json*'\n> json-writer.c:static inline void indent_pretty(struct json_writer *jw)\n> json-writer.c:static inline void begin(struct json_writer *jw, char ch_open, int pretty)\n> json-writer.c:static inline void assert_in_object(const struct json_writer *jw, const char *key)\n> json-writer.c:static inline void assert_in_array(const struct json_writer *jw)\n> json-writer.c:static inline void maybe_add_comma(struct json_writer *jw)\n> json-writer.c:static inline void fmt_double(struct json_writer *jw, int precision,\n> json-writer.c:static inline void object_common(struct json_writer *jw, const char *key)\n> json-writer.c:static inline void array_common(struct json_writer *jw)\n> json-writer.c:static inline void assert_is_terminated(const struct json_writer *jw)\n> t/helper/test-json-writer.c:static inline int scripted(int argc, const char **argv)\n> t/helper/test-json-writer.c:static inline int my_usage(void)\n> \n> Do you need all those inline keywords?  I'd rather leave the decision\n> about inlining to the compiler and (via optimization flags) the user\n> as much as possible.  Not a biggie, but the high frequency of that\n> word made me blink..\n> \n> René\n> \n\nI was just trying to match the patterns I found elsewhere in the code.\n\n     $ grep \"static inline\" *.c builtin/*.c | wc -l\n     111\n\nBut yeah, it's no big deal.  I can remove them.\n\nJeff\n"},{"id":"349891","messageId":"134901ed-eab9-b921-9c81-4397430c0069@jeffhostetler.com","threadId":"48669","inReplyTo":"f1154c86-9d11-f272-643f-4316be40e9b7@web.de","subject":"Re: [PATCH v8 1/2] json_writer: new routines to create data in JSON format","fromName":"Jeff Hostetler","fromEmail":"git@jeffhostetler.com","sentAt":"2018-06-11T11:43:44Z","receivedAt":"2018-06-11T11:43:50Z","isPatch":true,"sender":{"key":"git@jeffhostetler.com","avatar":null},"body":"\n\nOn 6/8/2018 2:05 AM, René Scharfe wrote:\n> Am 07.06.2018 um 16:12 schrieb git@jeffhostetler.com:\n>> From: Jeff Hostetler <jeffhost@microsoft.com>\n>>\n>> Add a series of jw_ routines and \"struct json_writer\" structure to compose\n[...]\n>>   TEST_PROGRAMS_NEED_X += test-index-version\n>> +TEST_PROGRAMS_NEED_X += test-json-writer>  TEST_PROGRAMS_NEED_X += test-lazy-init-name-hash\n>>   TEST_PROGRAMS_NEED_X += test-line-buffer\n>>   TEST_PROGRAMS_NEED_X += test-match-trees\n> \n> This doesn't apply cleanly on master, because most test helpers have\n> been combined into a single binary to reduce their disk footprint and\n> link times.  See efd71f8913 (t/helper: add an empty test-tool program)\n> for the overall rationale.\n> \n> test-json-writer could become a built-in as well, I think.  You can see\n> e.g. in c932a5ff28 (t/helper: merge test-string-list into test-tool)\n> what needs to be done to convert a helper.\n> \n> René\n> \n\nYou're right, the test helper framework changed since I started this\npatch series.  I was trying to keep the same parent commit as my V1\nto make it easier to compare, but that's not working out so well.\n\nI'll move it forward to the current master and fix it up.\n\nThanks,\nJeff\n"},{"id":"349892","messageId":"eafb73e5-31f0-d34a-2179-6c4fef78656e@jeffhostetler.com","threadId":"48669","inReplyTo":"0690b5a8-6c1c-e664-bb89-5d4e48258ddb@web.de","subject":"Re: [PATCH v8 1/2] json_writer: new routines to create data in JSON format","fromName":"Jeff Hostetler","fromEmail":"git@jeffhostetler.com","sentAt":"2018-06-11T11:51:13Z","receivedAt":"2018-06-11T11:51:21Z","isPatch":true,"sender":{"key":"git@jeffhostetler.com","avatar":null},"body":"\n\nOn 6/8/2018 4:07 PM, René Scharfe wrote:\n> Am 07.06.2018 um 16:12 schrieb git@jeffhostetler.com:\n>> From: Jeff Hostetler <jeffhost@microsoft.com>\n[...]\n>> +\tif (jw->first_stack.buf[jw->first_stack.len - 1] == '1')\n>> +\t\tjw->first_stack.buf[jw->first_stack.len - 1] = '0';\n>> +\telse\n>> +\t\tstrbuf_addch(&jw->json, ',');\n> \n> You only need a binary flag to indicate if a comma is needed, not a\n> stack.  We need a comma at the current level if and only if we already\n> wrote a child item.  If we finish a level then we do need a comma at the\n> previous level because we just wrote a sub-object or sub-array there.\n> And that should cover all cases.  Am I missing something?\n> \n> I get a sense of déjà vu, by the way. :)\n[...]\n\nYes, your way is simpler.  I'll update and re-roll.\n(And yes, we did discuss this earlier.  I found a problem with\nmy first version where it wouldn't handle empty sub-items, but\nyou found the missing piece.)\n\nThanks\nJeff\n\n\n"},{"id":"349893","messageId":"9aeac3a6-ea04-a43e-3851-1083792dfb5d@jeffhostetler.com","threadId":"48669","inReplyTo":"CAPig+cS8MjQNKabrQwuWzO4DDYJcfP_81n9H37efgXOiRzv9TQ@mail.gmail.com","subject":"Re: [PATCH v8 2/2] json-writer: t0019: add perl unit test","fromName":"Jeff Hostetler","fromEmail":"git@jeffhostetler.com","sentAt":"2018-06-11T12:16:44Z","receivedAt":"2018-06-11T12:16:48Z","isPatch":true,"sender":{"key":"git@jeffhostetler.com","avatar":null},"body":"\n\nOn 6/7/2018 1:13 PM, Eric Sunshine wrote:\n> On Thu, Jun 7, 2018 at 10:12 AM,  <git@jeffhostetler.com> wrote:\n>> Test json-writer output using perl script.\n>>\n>> Signed-off-by: Jeff Hostetler <jeffhost@microsoft.com>\n>> ---\n>> diff --git a/t/t0019/parse_json.perl b/t/t0019/parse_json.perl\n>> @@ -0,0 +1,52 @@\n>> +#!/usr/bin/perl\n>> +use strict;\n>> +use warnings;\n>> +use JSON;\n> \n> This new script is going to have to be protected by a prerequisite\n> since the JSON module is not part of the standard Perl installation,\n> thus will not necessarily be installed everywhere (it isn't on any of\n> my machines, for instance). Something like:\n> \n>      test_lazy_prereq PERLJSON '\n>          perl -MJSON -e \"exit 0\"\n>      '\n> \n> which would be used like this:\n> \n>      test_expect_success PERLJSON 'parse JSON using Perl' '\n>          ...\n>      '\n> \n\nwill do.  thanks!\nJeff\n"}]}