Commit 2fd44331 authored by Rusty Russell's avatar Rusty Russell

json_out: new module for authoring JSON strings.

Signed-off-by: default avatarRusty Russell <rusty@rustcorp.com.au>
parent 3ceb24bf
../../licenses/BSD-MIT
\ No newline at end of file
#include "config.h"
#include <stdio.h>
#include <string.h>
/**
* json_out - Code for creating simple JSON output.
*
* This code helps you create well-formed JSON strings.
*
* Author: Rusty Russell <rusty@rustcorp.com.au>
* License: BSD-MIT
*
* Example:
* // Given "a 1 true" outputs {"argv1":"a","argv2":1,"argv3":true}
* // Print arguments as a JSON array.
* #include <ccan/json_out/json_out.h>
* #include <stdio.h>
* #include <string.h>
* #include <unistd.h>
*
* // Simplistic test to see if str needs quotes.
* static bool can_be_json_literal(const char *str)
* {
* char *endp;
* if (strtol(str, &endp, 10) != LONG_MIN
* && endp != str
* && *endp == '\0')
* return true;
* return !strcmp(str, "true")
* || !strcmp(str, "false")
* || !strcmp(str, "null");
* }
*
* int main(int argc, char *argv[])
* {
* struct json_out *jout = json_out_new(NULL);
* size_t len;
* const char *p;
*
* json_out_start(jout, NULL, '{');
* for (int i = 1; i < argc; i++) {
* char fieldname[80];
* sprintf(fieldname, "argv%i", i);
* json_out_add(jout, fieldname,
* !can_be_json_literal(argv[i]),
* "%s", argv[i]);
* }
* json_out_end(jout, '}');
* // Force appending of \n
* json_out_direct(jout, 1)[0] = '\n';
* json_out_finished(jout);
*
* // Now write it out.
* while ((p = json_out_contents(jout, &len)) != NULL) {
* int i = write(STDOUT_FILENO, p, len);
* if (i <= 0)
* exit(1);
* json_out_consume(jout, i);
* }
*
* tal_free(jout);
* return 0;
* }
*
*/
int main(int argc, char *argv[])
{
/* Expect exactly one argument */
if (argc != 2)
return 1;
if (strcmp(argv[1], "depends") == 0) {
printf("ccan/compiler\n");
printf("ccan/json_escape\n");
printf("ccan/membuf\n");
printf("ccan/tal\n");
printf("ccan/typesafe_cb\n");
return 0;
}
return 1;
}
/* MIT (BSD) license - see LICENSE file for details */
#include <ccan/json_escape/json_escape.h>
#include <ccan/json_out/json_out.h>
#include <ccan/membuf/membuf.h>
#include <stdarg.h>
#include <stdio.h>
struct json_out {
/* Callback if we reallocate. */
void (*move_cb)(struct json_out *jout, ptrdiff_t delta, void *arg);
void *cb_arg;
#ifdef CCAN_JSON_OUT_DEBUG
/* tal_arr of types ( or [ we're enclosed in. */
char *wrapping;
#endif
/* True if we haven't yet put an element in current wrapping */
bool empty;
/* Output. */
MEMBUF(char) outbuf;
};
/* Realloc helper for tal membufs */
static void *membuf_tal_realloc(struct membuf *mb,
void *rawelems, size_t newsize)
{
char *p = rawelems;
tal_resize(&p, newsize);
return p;
}
struct json_out *json_out_new(const tal_t *ctx)
{
struct json_out *jout = tal(ctx, struct json_out);
membuf_init(&jout->outbuf,
tal_arr(jout, char, 64), 64, membuf_tal_realloc);
#ifdef CCAN_JSON_OUT_DEBUG
jout->wrapping = tal_arr(jout, char, 0);
#endif
jout->empty = true;
jout->move_cb = NULL;
return jout;
}
void json_out_call_on_move_(struct json_out *jout,
void (*cb)(struct json_out *jout, ptrdiff_t delta,
void *arg),
void *arg)
{
if (cb)
assert(!jout->move_cb);
jout->move_cb = cb;
jout->cb_arg = arg;
}
struct json_out *json_out_dup(const tal_t *ctx, const struct json_out *src)
{
size_t num_elems = membuf_num_elems(&src->outbuf);
char *elems = membuf_elems(&src->outbuf);
struct json_out *jout = tal_dup(ctx, struct json_out, src);
membuf_init(&jout->outbuf,
tal_dup_arr(jout, char, elems, num_elems, 0),
num_elems, membuf_tal_realloc);
membuf_added(&jout->outbuf, num_elems);
#ifdef CCAN_JSON_OUT_DEBUG
jout->wrapping = tal_dup_arr(jout, char,
jout->wrapping, tal_count(jout->wrapping),
0);
#endif
return jout;
}
static void indent(struct json_out *jout, char type)
{
#ifdef CCAN_JSON_OUT_DEBUG
size_t n = tal_count(jout->wrapping);
tal_resize(&jout->wrapping, n+1);
jout->wrapping[n] = type;
#endif
jout->empty = true;
}
static void unindent(struct json_out *jout, char type)
{
#ifdef CCAN_JSON_OUT_DEBUG
size_t indent = tal_count(jout->wrapping);
assert(indent > 0);
/* Both [ and ] and { and } are two apart in ASCII */
assert(jout->wrapping[indent-1] == type - 2);
tal_resize(&jout->wrapping, indent-1);
#endif
jout->empty = false;
}
/* Make sure jout->outbuf has room for len: return pointer */
static char *mkroom(struct json_out *jout, size_t len)
{
ptrdiff_t delta = membuf_prepare_space(&jout->outbuf, len);
if (delta && jout->move_cb)
jout->move_cb(jout, delta, jout->cb_arg);
return membuf_space(&jout->outbuf);
}
static void check_fieldname(const struct json_out *jout,
const char *fieldname)
{
#ifdef CCAN_JSON_OUT_DEBUG
size_t n = tal_count(jout->wrapping);
if (n == 0)
/* Can't have a fieldname if not in anything! */
assert(!fieldname);
else if (jout->wrapping[n-1] == '[')
/* No fieldnames in arrays. */
assert(!fieldname);
else {
/* Must have fieldnames in objects. */
assert(fieldname);
/* We don't escape this for you */
assert(!json_escape_needed(fieldname, strlen(fieldname)));
}
#endif
}
char *json_out_member_direct(struct json_out *jout,
const char *fieldname, size_t extra)
{
char *dest;
/* Prepend comma if required. */
if (!jout->empty)
extra++;
check_fieldname(jout, fieldname);
if (fieldname)
extra += 1 + strlen(fieldname) + 2;
if (!extra) {
dest = NULL;
goto out;
}
dest = mkroom(jout, extra);
if (!jout->empty)
*(dest++) = ',';
if (fieldname) {
*(dest++) = '"';
memcpy(dest, fieldname, strlen(fieldname));
dest += strlen(fieldname);
*(dest++) = '"';
*(dest++) = ':';
}
membuf_added(&jout->outbuf, extra);
out:
jout->empty = false;
return dest;
}
void json_out_start(struct json_out *jout, const char *fieldname, char type)
{
assert(type == '[' || type == '{');
json_out_member_direct(jout, fieldname, 1)[0] = type;
indent(jout, type);
}
void json_out_end(struct json_out *jout, char type)
{
assert(type == '}' || type == ']');
json_out_direct(jout, 1)[0] = type;
unindent(jout, type);
}
void json_out_addv(struct json_out *jout,
const char *fieldname,
bool quote,
const char *fmt,
va_list ap)
{
size_t fmtlen, avail;
va_list ap2;
char *dst;
json_out_member_direct(jout, fieldname, 0);
/* Make a copy in case we need it below. */
va_copy(ap2, ap);
/* We can use any additional space, but need room for ". */
avail = membuf_num_space(&jout->outbuf);
if (quote) {
if (avail < 2)
avail = 0;
else
avail -= 2;
}
/* Try printing in place first. */
dst = membuf_space(&jout->outbuf);
fmtlen = vsnprintf(dst + quote, avail, fmt, ap);
/* Horrible subtlety: vsnprintf *will* NUL terminate, even if it means
* chopping off the last character. So if fmtlen ==
* membuf_num_space(&jout->outbuf), the result was truncated! */
if (fmtlen + (int)quote*2 >= membuf_num_space(&jout->outbuf)) {
/* Make room for NUL terminator, even though we don't want it */
dst = mkroom(jout, fmtlen + 1 + (int)quote*2);
vsprintf(dst + quote, fmt, ap2);
}
/* Of course, if we need to escape it, we have to redo it all. */
if (json_escape_needed(dst + quote, fmtlen)) {
struct json_escape *e;
e = json_escape_len(NULL, dst + quote, fmtlen);
fmtlen = strlen(e->s);
dst = mkroom(jout, fmtlen + (int)quote*2);
memcpy(dst + quote, e, fmtlen);
tal_free(e);
}
if (quote) {
dst[0] = '"';
dst[fmtlen+1] = '"';
}
membuf_added(&jout->outbuf, fmtlen + (int)quote*2);
va_end(ap2);
}
void json_out_add(struct json_out *jout,
const char *fieldname,
bool quote,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
json_out_addv(jout, fieldname, quote, fmt, ap);
va_end(ap);
}
void json_out_addstr(struct json_out *jout,
const char *fieldname,
const char *str)
{
size_t len = strlen(str);
char *p;
p = json_out_member_direct(jout, fieldname, len + 2);
p[0] = p[1+len] = '"';
memcpy(p+1, str, len);
}
void json_out_add_splice(struct json_out *jout,
const char *fieldname,
const struct json_out *src)
{
const char *p;
size_t len;
p = json_out_contents(src, &len);
memcpy(json_out_member_direct(jout, fieldname, len), p, len);
}
char *json_out_direct(struct json_out *jout, size_t len)
{
char *p = mkroom(jout, len);
membuf_added(&jout->outbuf, len);
return p;
}
void json_out_finished(const struct json_out *jout)
{
#ifdef CCAN_JSON_OUT_DEBUG
assert(tal_count(jout->wrapping) == 0);
#endif
}
const char *json_out_contents(const struct json_out *jout, size_t *len)
{
*len = membuf_num_elems(&jout->outbuf);
return *len ? membuf_elems(&jout->outbuf) : NULL;
}
void json_out_consume(struct json_out *jout, size_t len)
{
membuf_consume(&jout->outbuf, len);
}
/* MIT (BSD) license - see LICENSE file for details */
#ifndef CCAN_JSON_OUT_H
#define CCAN_JSON_OUT_H
#include <ccan/compiler/compiler.h>
#include <ccan/tal/tal.h>
#include <ccan/typesafe_cb/typesafe_cb.h>
#include <stddef.h>
struct json_out;
/**
* json_out_new - allocate a json_out stream.
* @ctx: the tal_context to allocate from, or NULL
*/
struct json_out *json_out_new(const tal_t *ctx);
/**
* json_out_call_on_move - callback for when buffer is reallocated.
* @jout: the json_out object to attach to.
* @cb: the callback to call.
* @arg: the argument to @cb (must match type).
*
* A NULL @cb disables. You can't currently have more than one callback.
* The @delta argument to @cb is the difference between the old location
* and the new one, and is never zero.
*/
#define json_out_call_on_move(jout, cb, arg) \
json_out_call_on_move_((jout), \
typesafe_cb_preargs(void, void *, \
(cb), (arg), \
struct json_out *, \
ptrdiff_t), \
(arg))
void json_out_call_on_move_(struct json_out *jout,
void (*cb)(struct json_out *jout, ptrdiff_t delta,
void *arg),
void *arg);
/**
* json_out_dup - duplicate a json_out stream.
* @ctx: the tal_context to allocate from, or NULL
* @src: the json_out to copy.
*/
struct json_out *json_out_dup(const tal_t *ctx, const struct json_out *src);
/**
* json_out_start - start an array or object.
* @jout: the json_out object to write into.
* @fieldname: the fieldname, if inside an object, or NULL if inside an array.
* @type: '[' or '{' to start an array or object, respectively.
*
* Literally writes '"@fieldname": @type' or '@type ' if fieldname is NULL.
* @fieldname must not need JSON escaping.
*/
void json_out_start(struct json_out *jout, const char *fieldname, char type);
/**
* json_out_end - end an array or object.
* @jout: the json_out object to write into.
* @type: '}' or ']' to end an array or object, respectively.
*
* Literally writes ']' or '}', keeping track of whether we need to append
* a comma.
*/
void json_out_end(struct json_out *jout, char type);
/**
* json_out_add - add a formatted member.
* @jout: the json_out object to write into.
* @fieldname: optional fieldname to prepend (must not need escaping).
* @quote: if true, surround fmt by " and ".
* @fmt...: the printf-style format
*
* If you're in an array, @fieldname must be NULL. If you're in an
* object, @fieldname must be non-NULL. This is checked if
* CCAN_JSON_OUT_DEBUG is defined.
* @fieldname must not need JSON escaping.
*
* If the resulting string requires escaping, we call json_escape().
*/
PRINTF_FMT(4,5)
void json_out_add(struct json_out *jout,
const char *fieldname,
bool quote,
const char *fmt, ...);
/**
* json_out_addv - add a formatted member (vararg variant)
* @jout: the json_out object to write into.
* @fieldname: optional fieldname to prepend.
* @quote: if true, surround fmt by " and ".
* @fmt: the printf-style format
* @ap: the argument list.
*
* See json_out_add() above.
*/
void json_out_addv(struct json_out *jout,
const char *fieldname,
bool quote,
const char *fmt,
va_list ap);
/**
* json_out_addstr - convenience helper to add a string field.
* @jout: the json_out object to write into.
* @fieldname: optional fieldname to prepend.
* @str: the string to add (must not be NULL).
*
* Equivalent to json_out_add(@jout, @fieldname, true, "%s", @str);
*/
void json_out_addstr(struct json_out *jout,
const char *fieldname,
const char *str);
/**
* json_out_member_direct - add a field, with direct access.
* @jout: the json_out object to write into.
* @fieldname: optional fieldname to prepend.
* @extra: how many bytes to allocate.
*
* @fieldname must not need JSON escaping. Returns a direct pointer into
* the @extra bytes.
*
* This allows you to write your own efficient type-specific helpers.
*/
char *json_out_member_direct(struct json_out *jout,
const char *fieldname, size_t extra);
/**
* json_out_direct - make room in output and access directly.
* @jout: the json_out object to write into.
* @len: the length to allocate.
*
* This lets you access the json_out stream directly, to save a copy,
* if you know exactly how much you will write.
*
* Returns a pointer to @len bytes at the end of @jout.
*
* This is dangerous, since it doesn't automatically prepend a ","
* like the internal logic does, but can be used (carefully) to add
* entire objects, or whitespace.
*/
char *json_out_direct(struct json_out *jout, size_t extra);
/**
* json_out_add_splice - copy a field from another json_out.
* @jout: the json_out object to write into.
* @fieldname: optional fieldname to prepend.
* @src: the json_out object to copy from.
*
* This asserts that @src is well-formed (as per json_out_finished()),
* then places it into @jout with optional @fieldname prepended. This
* can be used to assemble sub-objects for your JSON and then copy
* them in.
*
* Note that it will call json_out_contents(@src), so it expects that
* object to be unconsumed.
*/
void json_out_add_splice(struct json_out *jout,
const char *fieldname,
const struct json_out *src);
/**
* json_out_finished - assert that the json buffer is finished.
* @jout: the json_out object written to.
*
* This simply causes internal assertions that all arrays and objects are
* finished. It needs CCAN_JSON_OUT_DEBUG defined to have any effect.
*/
void json_out_finished(const struct json_out *jout);
/**
* json_out_contents - read contents from json_out stream.
* @jout: the json_out object we want to read from.
* @len: set to the length of the buffer returned.
*
* This returns a pointer into the JSON written so far. Returns NULL
* and sets @len to 0 if there's nothing left in the buffer.
*/
const char *json_out_contents(const struct json_out *jout, size_t *len);
/**
* json_out_consume - discard contents from json_out stream.
* @jout: the json_out object we read from.
* @len: the length to consume (must be <= @len from json_out_contents)
*/
void json_out_consume(struct json_out *jout, size_t len);
#endif /* CCAN_JSON_OUT_H */
#define CCAN_JSON_OUT_DEBUG 1
#include "run.c"
#include <ccan/json_out/json_out.h>
/* Include the C files directly. */
#include <ccan/json_out/json_out.c>
#include <ccan/tap/tap.h>
static const char *ptr;
static bool called = false;
static void move_cb(struct json_out *jout, ptrdiff_t delta,
struct json_out *arg)
{
ptr += delta;
called = true;
ok1(arg == jout);
}
int main(void)
{
const tal_t *ctx = tal(NULL, char);
struct json_out *jout;
char *p;
size_t len;
/* This is how many tests you plan to run */
plan_tests(3);
/* Test nested arrays. */
jout = json_out_new(ctx);
json_out_call_on_move(jout, move_cb, jout);
json_out_start(jout, NULL, '{');
ptr = json_out_contents(jout, &len);
p = json_out_member_direct(jout, "fieldname", 102);
p[0] = '"';
p[101] = '"';
memset(p+1, 'p', 100);
json_out_finished(jout);
ok1(called);
/* Contents should have moved correctly. */
ok1(json_out_contents(jout, &len) == ptr);
tal_free(ctx);
/* This exits depending on whether all tests passed */
return exit_status();
}
#include <ccan/json_out/json_out.h>
/* Include the C files directly. */
#include <ccan/json_out/json_out.c>
#include <ccan/tap/tap.h>
static void test_json_out_add(const tal_t *ctx,
char c, bool quote, const char *escaped)
{
/* 64 is the size of the initial buf, so we test that. */
for (size_t i = 1; i < 64; i++) {
struct json_out *jout;
char str[64 + 1];
const char *r;
size_t len;
char fieldname[64 + 1];
jout = json_out_new(ctx);
json_out_start(jout, NULL, '{');
memset(str, c, i);
str[i] = '\0';
memset(fieldname, 'f', i);
fieldname[i] = '\0';
json_out_add(jout, fieldname, quote, "%s", str);
json_out_end(jout, '}');
json_out_finished(jout);
r = json_out_contents(jout, &len);
ok1(len == strlen("{\"") + i + strlen("\":")
+ quote * 2 + strlen(escaped) * i + strlen("}"));
ok1(len > strlen("{\""));
ok1(memcmp(r, "{\"", strlen("{\"")) == 0);
json_out_consume(jout, strlen("{\""));
r = json_out_contents(jout, &len);
ok1(len > strlen(fieldname));
ok1(memcmp(r, fieldname, strlen(fieldname)) == 0);
json_out_consume(jout, strlen(fieldname));
r = json_out_contents(jout, &len);
ok1(len > strlen("\":"));
ok1(memcmp(r, "\":", strlen("\":")) == 0);
json_out_consume(jout, strlen("\":"));
r = json_out_contents(jout, &len);
if (quote) {
ok1(len > 0);
ok1(r[0] == '"');
json_out_consume(jout, 1);
}
for (size_t n = 0; n < i; n++) {
r = json_out_contents(jout, &len);
ok1(len > strlen(escaped));
ok1(memcmp(r, escaped, strlen(escaped)) == 0);
json_out_consume(jout, strlen(escaped));
}
r = json_out_contents(jout, &len);
if (quote) {
ok1(len > 0);
ok1(r[0] == '"');
json_out_consume(jout, 1);
}
r = json_out_contents(jout, &len);
ok1(len == 1);
ok1(memcmp(r, "}", 1) == 0);
json_out_consume(jout, 1);
ok1(!json_out_contents(jout, &len));
ok1(len == 0);
}
}
static void json_eq(const struct json_out *jout, const char *expect)
{
size_t len;
const char *p;
json_out_finished(jout);
p = json_out_contents(jout, &len);
ok1(len == strlen(expect));
ok1(memcmp(expect, p, len) == 0);
}
int main(void)
{
const tal_t *ctx = tal(NULL, char);
struct json_out *jout;
char *p;
/* This is how many tests you plan to run */
plan_tests(14689);
/* Simple tests */
test_json_out_add(ctx, '1', false, "1");
test_json_out_add(ctx, 'x', true, "x");
test_json_out_add(ctx, '\n', true, "\\n");
/* Test nested arrays. */
jout = json_out_new(ctx);
for (size_t i = 0; i < 64; i++)
json_out_start(jout, NULL, '[');
for (size_t i = 0; i < 64; i++)
json_out_end(jout, ']');
json_eq(jout, "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]");
/* Test nested objects. */
jout = json_out_new(ctx);
json_out_start(jout, NULL, '{');
for (size_t i = 0; i < 63; i++)
json_out_start(jout, "x", '{');
for (size_t i = 0; i < 64; i++)
json_out_end(jout, '}');
json_eq(jout, "{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{\"x\":{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}");
jout = json_out_new(ctx);
json_out_start(jout, NULL, '{');
p = json_out_member_direct(jout, "x", 7);
memcpy(p, "\"hello\"", 7);
json_out_end(jout, '}');
json_eq(jout, "{\"x\":\"hello\"}");
jout = json_out_new(ctx);
p = json_out_direct(jout, strlen("{\"x\":\"hello\"}\n"));
memcpy(p, "{\"x\":\"hello\"}\n", strlen("{\"x\":\"hello\"}\n"));
json_eq(jout, "{\"x\":\"hello\"}\n");
jout = json_out_new(ctx);
json_out_start(jout, NULL, '{');
struct json_out *jout2 = json_out_new(ctx);
json_out_start(jout2, NULL, '{');
json_out_addstr(jout2, "x", "hello");
json_out_end(jout2, '}');
json_out_finished(jout2);
json_out_add_splice(jout, "inner", jout2);
json_out_end(jout, '}');
json_eq(jout, "{\"inner\":{\"x\":\"hello\"}}");
tal_free(ctx);
/* This exits depending on whether all tests passed */
return exit_status();
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment