From 9ad766544325d2e39f9e3e3cecc44903174ba4de Mon Sep 17 00:00:00 2001 From: Austin Clements Date: Sat, 30 Jun 2012 22:59:15 +2000 Subject: [PATCH] Proposed structure formatter API --- 68/043d304414bb0f0ffabca0b18bedfdeb78f2a2 | 341 ++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 68/043d304414bb0f0ffabca0b18bedfdeb78f2a2 diff --git a/68/043d304414bb0f0ffabca0b18bedfdeb78f2a2 b/68/043d304414bb0f0ffabca0b18bedfdeb78f2a2 new file mode 100644 index 000000000..8160210e2 --- /dev/null +++ b/68/043d304414bb0f0ffabca0b18bedfdeb78f2a2 @@ -0,0 +1,341 @@ +Return-Path: +X-Original-To: notmuch@notmuchmail.org +Delivered-To: notmuch@notmuchmail.org +Received: from localhost (localhost [127.0.0.1]) + by olra.theworths.org (Postfix) with ESMTP id E1014431FB6 + for ; Fri, 29 Jun 2012 19:59:20 -0700 (PDT) +X-Virus-Scanned: Debian amavisd-new at olra.theworths.org +X-Spam-Flag: NO +X-Spam-Score: -0.7 +X-Spam-Level: +X-Spam-Status: No, score=-0.7 tagged_above=-999 required=5 + tests=[RCVD_IN_DNSWL_LOW=-0.7] autolearn=disabled +Received: from olra.theworths.org ([127.0.0.1]) + by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id ojSSQFZQVlNL for ; + Fri, 29 Jun 2012 19:59:20 -0700 (PDT) +Received: from dmz-mailsec-scanner-5.mit.edu (DMZ-MAILSEC-SCANNER-5.MIT.EDU + [18.7.68.34]) + by olra.theworths.org (Postfix) with ESMTP id DC0DD431FAF + for ; Fri, 29 Jun 2012 19:59:19 -0700 (PDT) +X-AuditID: 12074422-b7f1f6d00000090b-38-4fee6b85923a +Received: from mailhub-auth-4.mit.edu ( [18.7.62.39]) + by dmz-mailsec-scanner-5.mit.edu (Symantec Messaging Gateway) with SMTP + id FE.1E.02315.58B6EEF4; Fri, 29 Jun 2012 22:59:17 -0400 (EDT) +Received: from outgoing.mit.edu (OUTGOING-AUTH.MIT.EDU [18.7.22.103]) + by mailhub-auth-4.mit.edu (8.13.8/8.9.2) with ESMTP id q5U2xGiF030961; + Fri, 29 Jun 2012 22:59:17 -0400 +Received: from awakening.csail.mit.edu (awakening.csail.mit.edu [18.26.4.91]) + (authenticated bits=0) + (User authenticated as amdragon@ATHENA.MIT.EDU) + by outgoing.mit.edu (8.13.6/8.12.4) with ESMTP id q5U2xFLm007059 + (version=TLSv1/SSLv3 cipher=AES256-SHA bits=256 verify=NOT); + Fri, 29 Jun 2012 22:59:16 -0400 (EDT) +Received: from amthrax by awakening.csail.mit.edu with local (Exim 4.77) + (envelope-from ) + id 1Skntz-0000P7-7D; Fri, 29 Jun 2012 22:59:15 -0400 +From: Austin Clements +To: Peter Feigl , + Jameson Graef Rollins +Subject: Proposed structure formatter API +User-Agent: Notmuch/0.13.2+59~g78b98ba (http://notmuchmail.org) Emacs/23.3.1 + (i486-pc-linux-gnu) +Date: Fri, 29 Jun 2012 22:59:15 -0400 +Message-ID: <87d34hsdx8.fsf@awakening.csail.mit.edu> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="=-=-=" +X-Brightmail-Tracker: + H4sIAAAAAAAAA+NgFvrMIsWRmVeSWpSXmKPExsUixG6nrtua/c7fYM9kbYu9De2MFnv2eVlc + vzmT2YHZ4+5pLo/Fm/azeTxbdYs5gDmKyyYlNSezLLVI3y6BK+P1+VXMBU9tK1pfH2VsYLyo + 38XIySEhYCLRuOo3O4QtJnHh3nq2LkYuDiGBfYwSB+dOZoJwNjBKrJg1hRHCOckk8XvNGqjM + EkaJrUsfMoL0swloSGzbvxzMFhGIlZj7bi8ziM0sIC3x7XczE4gtLKApsfPwU6AdHByiAgkS + B4/XgJgsAqoS07qcQUxeoIsev7UHKeYVEJQ4OfMJC8QQfYnZ07tZJzDyz0KSmoUkBWFLSpya + tpQNwjaQeLX/IuMCRpZVjLIpuVW6uYmZOcWpybrFyYl5ealFuqZ6uZkleqkppZsYwWHrorSD + 8edBpUOMAhyMSjy8UZff+guxJpYVV+YeYpTkYFIS5T2T+s5fiC8pP6UyI7E4I76oNCe1+BCj + BAezkgjvw8VA5bwpiZVVqUX5MClpDhYlcd5rKTf9hQTSE0tSs1NTC1KLYLIyHBxKEry/soCG + ChalpqdWpGXmlCCkmTg4QYbzAA1/AFLDW1yQmFucmQ6RP8WoKCXOex0kIQCSyCjNg+uFpZVX + jOJArwjzvgSp4gGmJLjuV0CDmYAGOwW8BhlckoiQkmpgrM+bsm/W5LxHz8ILV796dumAnGHB + N0GmV2cdni2x+H8gZ92i9sSLqv8fBjy6eZ/dQ1SdSftc5PnIGQr9H/8dT9c2exZ3JlXpzLor + +zxuPf/1asH565+3qUwW4Z7BaqcidOTBr0Win6/rrD4l4bjPj2fCGlbtWx/OFk/xzb3EftEn + Ofht8UIergYlluKMREMt5qLiRADJaRFhBgMAAA== +Cc: notmuch@notmuchmail.org +X-BeenThere: notmuch@notmuchmail.org +X-Mailman-Version: 2.1.13 +Precedence: list +List-Id: "Use and development of the notmuch mail system." + +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +X-List-Received-Date: Sat, 30 Jun 2012 02:59:21 -0000 + +--=-=-= + +On IRC today, Peter renewed discussions of supporting an S-expression +format, which I think would be a fantastic thing to have. + +A while back [1] I tried to explain an API I thought would form a good +foundation for this by abstracting away the differences between JSON and +S-expression output so both structured formats can use the same code, +but I didn't express the idea very clearly. I built essentially this +API for another project recently and it worked well (and I learned a few +things), so I figured I would give it another shot. It's simple enough +that I tossed together an example implementation, which is attached. + +The idea is similar to the existing format structures, but instead of +dealing with complex, high-level concepts like threads or messages, this +API deals with syntax-level concepts like lists and strings. All of the +structured output formats can then be supported by a single high-level +formatter backed by different syntax formatters. + +There are a few ways something like this could be integrated with the +high-level formatters. For show, the least invasive thing would be to +have two notmuch_show_format structures with different part functions. +These part functions would be thin wrappers that simply create the right +structure printer and then pass it to a common part function. + +It would be even nicer if we could get rid of the +message_set_{start,sep,end} fields, since those duplicate functionality +from the structure printer and their use is haphazard and overly +complicated. We could do this by changing notmuch_show_format to +something like + +typedef struct notmuch_show_format { + struct sprinter *(*new_sprinter) (const void *ctx); + notmuch_status_t (*part) (const void *ctx, struct sprinter *sp, + struct mime_node *node, + const struct notmuch_show_params *params); +} notmuch_show_format_t; + +For the JSON and S-expression show formats, new_sprinter would be +sprinter_json_new and sprinter_sexp_new, respectively, and they could +share a part function. For the text, mbox, and raw formatters, +new_sprinter could simply be NULL, or we could provide a "NULL structure +printer" implementation that does nothing. + +We could do something similar for reply. + +search_format is more complicated, but might also benefit more. Most of +those fields have to do with how to print lists and objects and could be +removed if the format simply provided a new_sprinter method like the +notmuch_show_format I suggested above. + +[1] id:"20120121220407.GK16740@mit.edu" + + +--=-=-= +Content-Type: text/x-csrc +Content-Disposition: inline; filename=sprinter.c + +#include +#include +#include +#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0])) + +/* Structure printer interface */ +struct sprinter +{ + void (*begin_map) (struct sprinter *); + void (*begin_list) (struct sprinter *); + void (*end) (struct sprinter *); + + void (*string) (struct sprinter *, const char *); + void (*integer) (struct sprinter *, int); + void (*boolean) (struct sprinter *, bool); + void (*null) (struct sprinter *); + void (*map_key) (struct sprinter *, const char *); + + void (*frame) (struct sprinter *); +}; + +/* Create a new structure printer that emits JSON */ +struct sprinter * +sprinter_json_new(const void *ctx, FILE *stream); + +int +main(int argc, char **argv) +{ + struct sprinter *test = sprinter_json_new (NULL, stdout); + test->begin_list (test); + test->string (test, "test"); + test->integer (test, 42); + test->boolean (test, true); + test->null (test); + test->frame (test); + test->begin_map (test); + test->map_key (test, "Hello"); + test->string (test, "world\n"); + test->end (test); + test->frame (test); + test->end (test); + talloc_free (test); +} + +/* + * Every below here is private implementation. + */ + +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; +}; + +struct json_state +{ + struct json_state *parent; + /* True if nothing has been printed in this aggregate yet. + * Suppresses the comma before a value. */ + bool 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) + fputs (", ", 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) +{ + const static 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, bool 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_frame(struct sprinter *sp) +{ + struct sprinter_json *spj = (struct sprinter_json*)sp; + fputc ('\n', spj->stream); +} + +struct sprinter * +sprinter_json_new(const void *ctx, FILE *stream) +{ + const static 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, + .frame = json_frame, + } + }; + struct sprinter_json *res; + + res = talloc (ctx, struct sprinter_json); + if (!res) + return NULL; + + *res = template; + res->stream = stream; + return &res->vtable; +} + +--=-=-=-- -- 2.26.2