From 41becc0c9dfabbd59a1c96bdedc99a785539a93b Mon Sep 17 00:00:00 2001 From: "craven@gmx.net" Date: Mon, 23 Jul 2012 12:39:44 +0200 Subject: Add support for structured output formatters. This patch adds a new struct type sprinter_t, which is used for structured formatting, e.g. JSON or S-Expressions. The structure printer is heavily based on code from Austin Clements (id:87d34hsdx8.fsf@awakening.csail.mit.edu). It includes the following functions: /* Start a new map/dictionary structure. This should be followed by * a sequence of alternating calls to map_key and one of the * value-printing functions until the map is ended by end. */ void (*begin_map) (struct sprinter *); /* Start a new list/array structure. */ void (*begin_list) (struct sprinter *); /* End the last opened list or map structure. */ void (*end) (struct sprinter *); /* Print one string/integer/boolean/null element (possibly inside a * list or map, followed or preceded by separators). * For string, the char * must be UTF-8 encoded. */ void (*string) (struct sprinter *, const char *); void (*integer) (struct sprinter *, int); void (*boolean) (struct sprinter *, notmuch_bool_t); void (*null) (struct sprinter *); /* Print the key of a map's key/value pair. The char * must be UTF-8 * encoded. */ void (*map_key) (struct sprinter *, const char *); /* Insert a separator (usually extra whitespace) for improved * readability without affecting the abstract syntax of the * structure being printed. * For JSON, this could simply be a line break. */ void (*separator) (struct sprinter *); /* Set the current string prefix. This only affects the text * printer, which will print this string, followed by a colon, * before any string. For other printers, this does nothing. */ void (*set_prefix) (struct sprinter *, const char *); To support the plain text format properly, the following additional function must also be implemented: /* Set the current string prefix. This only affects the text * printer, which will print this string, followed by a colon, * before any string. For other printers, this does nothing. */ void (*set_prefix) (struct sprinter *, const char *); The structure also contains a flag that should be set to FALSE in all custom printers and to TRUE in the plain text formatter. /* True if this is the special-cased plain text printer. */ notmuch_bool_t is_text_printer; The printer can (and should) use internal state to insert delimiters and syntax at the correct places. Example: format->begin_map(format); format->map_key(format, "foo"); format->begin_list(format); format->integer(format, 1); format->integer(format, 2); format->integer(format, 3); format->end(format); format->map_key(format, "bar"); format->begin_map(format); format->map_key(format, "baaz"); format->string(format, "hello world"); format->end(format); format->end(format); would output JSON as follows: {"foo": [1, 2, 3], "bar": { "baaz": "hello world"}} --- sprinter.h | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 sprinter.h (limited to 'sprinter.h') diff --git a/sprinter.h b/sprinter.h new file mode 100644 index 0000000..77dc26f --- /dev/null +++ b/sprinter.h @@ -0,0 +1,58 @@ +#ifndef NOTMUCH_SPRINTER_H +#define NOTMUCH_SPRINTER_H + +/* Necessary for notmuch_bool_t */ +#include "notmuch-client.h" + +/* Structure printer interface. This is used to create output + * structured as maps (with key/value pairs), lists and primitives + * (strings, integers and booleans). + */ +typedef struct sprinter { + /* Start a new map/dictionary structure. This should be followed by + * a sequence of alternating calls to map_key and one of the + * value-printing functions until the map is ended by end. + */ + void (*begin_map) (struct sprinter *); + + /* Start a new list/array structure. + */ + void (*begin_list) (struct sprinter *); + + /* End the last opened list or map structure. + */ + void (*end) (struct sprinter *); + + /* Print one string/integer/boolean/null element (possibly inside a + * list or map, followed or preceded by separators). + * For string, the char * must be UTF-8 encoded. + */ + void (*string) (struct sprinter *, const char *); + void (*integer) (struct sprinter *, int); + void (*boolean) (struct sprinter *, notmuch_bool_t); + void (*null) (struct sprinter *); + + /* Print the key of a map's key/value pair. The char * must be UTF-8 + * encoded. + */ + void (*map_key) (struct sprinter *, const char *); + + /* Insert a separator (usually extra whitespace) for improved + * readability without affecting the abstract syntax of the + * structure being printed. + * For JSON, this could simply be a line break. + */ + void (*separator) (struct sprinter *); + + /* Set the current string prefix. This only affects the text + * printer, which will print this string, followed by a colon, + * before any string. For other printers, this does nothing. + */ + void (*set_prefix) (struct sprinter *, const char *); + + /* True if this is the special-cased plain text printer. + */ + notmuch_bool_t is_text_printer; +} sprinter_t; + +#endif // NOTMUCH_SPRINTER_H -- cgit v1.2.3 From 36522fca1cac6ca23c2c4c0280e3e20e96f7bfbb Mon Sep 17 00:00:00 2001 From: "craven@gmx.net" Date: Mon, 23 Jul 2012 12:39:45 +0200 Subject: Add structured output formatter for JSON and plain text (but don't use them yet). Using the new structured printer support in sprinter.h, implement sprinter_json_create, which returns a new JSON structured output formatter. The formatter prints output similar to the existing JSON, but with differences in whitespace (mostly newlines, --output=summary prints the entire message summary on one line, not split across multiple lines). Also implement a "structured" formatter for plain text that prints prefixed strings, to be used with notmuch-search.c plain text output. --- Makefile.local | 2 + sprinter-json.c | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ sprinter-text.c | 126 ++++++++++++++++++++++++++++++++++++++ sprinter.h | 10 +++ 4 files changed, 325 insertions(+) create mode 100644 sprinter-json.c create mode 100644 sprinter-text.c (limited to 'sprinter.h') diff --git a/Makefile.local b/Makefile.local index a890df2..296995d 100644 --- a/Makefile.local +++ b/Makefile.local @@ -290,6 +290,8 @@ notmuch_client_srcs = \ notmuch-show.c \ notmuch-tag.c \ notmuch-time.c \ + sprinter-json.c \ + sprinter-text.c \ query-string.c \ mime-node.c \ crypto.c \ diff --git a/sprinter-json.c b/sprinter-json.c new file mode 100644 index 0000000..4649655 --- /dev/null +++ b/sprinter-json.c @@ -0,0 +1,187 @@ +#include +#include +#include +#include "sprinter.h" + +struct sprinter_json { + struct sprinter vtable; + FILE *stream; + /* Top of the state stack, or NULL if the printer is not currently + * inside any aggregate types. */ + struct json_state *state; + + /* A flag to signify that a separator should be inserted in the + * output as soon as possible. + */ + notmuch_bool_t insert_separator; +}; + +struct json_state { + struct json_state *parent; + /* True if nothing has been printed in this aggregate yet. + * Suppresses the comma before a value. */ + notmuch_bool_t first; + /* The character that closes the current aggregate. */ + char close; +}; + +/* Helper function to set up the stream to print a value. If this + * value follows another value, prints a comma. */ +static struct sprinter_json * +json_begin_value (struct sprinter *sp) +{ + struct sprinter_json *spj = (struct sprinter_json *) sp; + + if (spj->state) { + if (! spj->state->first) { + fputc (',', spj->stream); + if (spj->insert_separator) { + fputc ('\n', spj->stream); + spj->insert_separator = FALSE; + } else { + fputc (' ', spj->stream); + } + } else { + spj->state->first = FALSE; + } + } + return spj; +} + +/* Helper function to begin an aggregate type. Prints the open + * character and pushes a new state frame. */ +static void +json_begin_aggregate (struct sprinter *sp, char open, char close) +{ + struct sprinter_json *spj = json_begin_value (sp); + struct json_state *state = talloc (spj, struct json_state); + + fputc (open, spj->stream); + state->parent = spj->state; + state->first = TRUE; + state->close = close; + spj->state = state; +} + +static void +json_begin_map (struct sprinter *sp) +{ + json_begin_aggregate (sp, '{', '}'); +} + +static void +json_begin_list (struct sprinter *sp) +{ + json_begin_aggregate (sp, '[', ']'); +} + +static void +json_end (struct sprinter *sp) +{ + struct sprinter_json *spj = (struct sprinter_json *) sp; + struct json_state *state = spj->state; + + fputc (spj->state->close, spj->stream); + spj->state = state->parent; + talloc_free (state); + if (spj->state == NULL) + fputc ('\n', spj->stream); +} + +static void +json_string (struct sprinter *sp, const char *val) +{ + static const char *const escapes[] = { + ['\"'] = "\\\"", ['\\'] = "\\\\", ['\b'] = "\\b", + ['\f'] = "\\f", ['\n'] = "\\n", ['\t'] = "\\t" + }; + struct sprinter_json *spj = json_begin_value (sp); + + fputc ('"', spj->stream); + for (; *val; ++val) { + unsigned char ch = *val; + if (ch < ARRAY_SIZE (escapes) && escapes[ch]) + fputs (escapes[ch], spj->stream); + else if (ch >= 32) + fputc (ch, spj->stream); + else + fprintf (spj->stream, "\\u%04x", ch); + } + fputc ('"', spj->stream); +} + +static void +json_integer (struct sprinter *sp, int val) +{ + struct sprinter_json *spj = json_begin_value (sp); + + fprintf (spj->stream, "%d", val); +} + +static void +json_boolean (struct sprinter *sp, notmuch_bool_t val) +{ + struct sprinter_json *spj = json_begin_value (sp); + + fputs (val ? "true" : "false", spj->stream); +} + +static void +json_null (struct sprinter *sp) +{ + struct sprinter_json *spj = json_begin_value (sp); + + fputs ("null", spj->stream); +} + +static void +json_map_key (struct sprinter *sp, const char *key) +{ + struct sprinter_json *spj = (struct sprinter_json *) sp; + + json_string (sp, key); + fputs (": ", spj->stream); + spj->state->first = TRUE; +} + +static void +json_set_prefix (unused (struct sprinter *sp), unused (const char *name)) +{ +} + +static void +json_separator (struct sprinter *sp) +{ + struct sprinter_json *spj = (struct sprinter_json *) sp; + + spj->insert_separator = TRUE; +} + +struct sprinter * +sprinter_json_create (const void *ctx, FILE *stream) +{ + static const struct sprinter_json template = { + .vtable = { + .begin_map = json_begin_map, + .begin_list = json_begin_list, + .end = json_end, + .string = json_string, + .integer = json_integer, + .boolean = json_boolean, + .null = json_null, + .map_key = json_map_key, + .separator = json_separator, + .set_prefix = json_set_prefix, + .is_text_printer = FALSE, + } + }; + struct sprinter_json *res; + + res = talloc (ctx, struct sprinter_json); + if (! res) + return NULL; + + *res = template; + res->stream = stream; + return &res->vtable; +} diff --git a/sprinter-text.c b/sprinter-text.c new file mode 100644 index 0000000..b208840 --- /dev/null +++ b/sprinter-text.c @@ -0,0 +1,126 @@ +#include +#include +#include +#include "sprinter.h" + +/* "Structured printer" interface for unstructured text printing. + * Note that --output=summary is dispatched and formatted in + * notmuch-search.c, the code in this file is only used for all other + * output types. + */ + +struct sprinter_text { + struct sprinter vtable; + FILE *stream; + + /* The current prefix to be printed with string/integer/boolean + * data. + */ + const char *current_prefix; + + /* A flag to indicate if this is the first tag. Used in list of tags + * for summary. + */ + notmuch_bool_t first_tag; +}; + +static void +text_string (struct sprinter *sp, const char *val) +{ + struct sprinter_text *sptxt = (struct sprinter_text *) sp; + + if (sptxt->current_prefix != NULL) + fprintf (sptxt->stream, "%s:", sptxt->current_prefix); + + fputs(val, sptxt->stream); +} + +static void +text_integer (struct sprinter *sp, int val) +{ + struct sprinter_text *sptxt = (struct sprinter_text *) sp; + + fprintf (sptxt->stream, "%d", val); +} + +static void +text_boolean (struct sprinter *sp, notmuch_bool_t val) +{ + struct sprinter_text *sptxt = (struct sprinter_text *) sp; + + fputs (val ? "true" : "false", sptxt->stream); +} + +static void +text_separator (struct sprinter *sp) +{ + struct sprinter_text *sptxt = (struct sprinter_text *) sp; + + fputc ('\n', sptxt->stream); +} + +static void +text_set_prefix (struct sprinter *sp, const char *prefix) +{ + struct sprinter_text *sptxt = (struct sprinter_text *) sp; + + sptxt->current_prefix = prefix; +} + +/* The structure functions begin_map, begin_list, end and map_key + * don't do anything in the text formatter. + */ + +static void +text_begin_map (unused (struct sprinter *sp)) +{ +} + +static void +text_begin_list (unused (struct sprinter *sp)) +{ +} + +static void +text_end (unused (struct sprinter *sp)) +{ +} + +static void +text_null (unused (struct sprinter *sp)) +{ +} + +static void +text_map_key (unused (struct sprinter *sp), unused (const char *key)) +{ +} + +struct sprinter * +sprinter_text_create (const void *ctx, FILE *stream) +{ + static const struct sprinter_text template = { + .vtable = { + .begin_map = text_begin_map, + .begin_list = text_begin_list, + .end = text_end, + .string = text_string, + .integer = text_integer, + .boolean = text_boolean, + .null = text_null, + .map_key = text_map_key, + .separator = text_separator, + .set_prefix = text_set_prefix, + .is_text_printer = TRUE, + }, + }; + struct sprinter_text *res; + + res = talloc (ctx, struct sprinter_text); + if (! res) + return NULL; + + *res = template; + res->stream = stream; + return &res->vtable; +} diff --git a/sprinter.h b/sprinter.h index 77dc26f..6680d41 100644 --- a/sprinter.h +++ b/sprinter.h @@ -55,4 +55,14 @@ typedef struct sprinter { notmuch_bool_t is_text_printer; } sprinter_t; + +/* Create a new unstructured printer that emits the default text format + * for "notmuch search". */ +struct sprinter * +sprinter_text_create (const void *ctx, FILE *stream); + +/* Create a new structure printer that emits JSON. */ +struct sprinter * +sprinter_json_create (const void *ctx, FILE *stream); + #endif // NOTMUCH_SPRINTER_H -- cgit v1.2.3 From 14883b07003b9ed4223cd8f2c03b301fddae07bd Mon Sep 17 00:00:00 2001 From: Austin Clements Date: Thu, 2 Aug 2012 21:14:49 -0400 Subject: sprinter: Add a string_len method This method allows callers to output strings with specific lengths. It's useful both for strings with embedded NULs (which JSON can represent, though parser support is apparently spotty), and non-terminated strings. --- sprinter-json.c | 16 ++++++++++++++-- sprinter-text.c | 11 +++++++++-- sprinter.h | 9 ++++++--- 3 files changed, 29 insertions(+), 7 deletions(-) (limited to 'sprinter.h') diff --git a/sprinter-json.c b/sprinter-json.c index 4649655..c9b6835 100644 --- a/sprinter-json.c +++ b/sprinter-json.c @@ -88,8 +88,13 @@ json_end (struct sprinter *sp) fputc ('\n', spj->stream); } +/* This implementation supports embedded NULs as allowed by the JSON + * specification and Unicode. Support for *parsing* embedded NULs + * varies, but is generally not a problem outside of C-based parsers + * (Python's json module and Emacs' json.el take embedded NULs in + * stride). */ static void -json_string (struct sprinter *sp, const char *val) +json_string_len (struct sprinter *sp, const char *val, size_t len) { static const char *const escapes[] = { ['\"'] = "\\\"", ['\\'] = "\\\\", ['\b'] = "\\b", @@ -98,7 +103,7 @@ json_string (struct sprinter *sp, const char *val) struct sprinter_json *spj = json_begin_value (sp); fputc ('"', spj->stream); - for (; *val; ++val) { + for (; len; ++val, --len) { unsigned char ch = *val; if (ch < ARRAY_SIZE (escapes) && escapes[ch]) fputs (escapes[ch], spj->stream); @@ -110,6 +115,12 @@ json_string (struct sprinter *sp, const char *val) fputc ('"', spj->stream); } +static void +json_string (struct sprinter *sp, const char *val) +{ + json_string_len (sp, val, strlen (val)); +} + static void json_integer (struct sprinter *sp, int val) { @@ -166,6 +177,7 @@ sprinter_json_create (const void *ctx, FILE *stream) .begin_list = json_begin_list, .end = json_end, .string = json_string, + .string_len = json_string_len, .integer = json_integer, .boolean = json_boolean, .null = json_null, diff --git a/sprinter-text.c b/sprinter-text.c index b208840..dfa54b5 100644 --- a/sprinter-text.c +++ b/sprinter-text.c @@ -25,14 +25,20 @@ struct sprinter_text { }; static void -text_string (struct sprinter *sp, const char *val) +text_string_len (struct sprinter *sp, const char *val, size_t len) { struct sprinter_text *sptxt = (struct sprinter_text *) sp; if (sptxt->current_prefix != NULL) fprintf (sptxt->stream, "%s:", sptxt->current_prefix); - fputs(val, sptxt->stream); + fwrite (val, len, 1, sptxt->stream); +} + +static void +text_string (struct sprinter *sp, const char *val) +{ + text_string_len (sp, val, strlen (val)); } static void @@ -105,6 +111,7 @@ sprinter_text_create (const void *ctx, FILE *stream) .begin_list = text_begin_list, .end = text_end, .string = text_string, + .string_len = text_string_len, .integer = text_integer, .boolean = text_boolean, .null = text_null, diff --git a/sprinter.h b/sprinter.h index 6680d41..5f43175 100644 --- a/sprinter.h +++ b/sprinter.h @@ -23,11 +23,14 @@ typedef struct sprinter { */ void (*end) (struct sprinter *); - /* Print one string/integer/boolean/null element (possibly inside a - * list or map, followed or preceded by separators). - * For string, the char * must be UTF-8 encoded. + /* Print one string/integer/boolean/null element (possibly inside + * a list or map, followed or preceded by separators). For string + * and string_len, the char * must be UTF-8 encoded. string_len + * allows non-terminated strings and strings with embedded NULs + * (though the handling of the latter is format-dependent). */ void (*string) (struct sprinter *, const char *); + void (*string_len) (struct sprinter *, const char *, size_t); void (*integer) (struct sprinter *, int); void (*boolean) (struct sprinter *, notmuch_bool_t); void (*null) (struct sprinter *); -- cgit v1.2.3