unofficial mirror of notmuch@notmuchmail.org
 help / color / mirror / code / Atom feed
* [PATCH] Added better support for multiple structured output formats.
@ 2012-07-10 10:02 craven
  2012-07-10 10:48 ` [PATCH] FIXED: " craven
  0 siblings, 1 reply; 39+ messages in thread
From: craven @ 2012-07-10 10:02 UTC (permalink / raw)
  To: notmuch

As discussed in <id:20120121220407.GK16740@mit.edu>, this patch adds
support for new structured output formats (like s-expressions) by using
stateful structure_printers. An implementation of the JSON structure
printer that passes all tests is included.

Structure printers have functions for starting a map, a list, closing
any number of these two, printing a map key, a number value, a string
value, and boolean values. By threading a state through the
invocations, arbitrary structured formatting can be achieved.

In a second patch, the structured output code should be isolated in a
separate file, and also used in all other parts of notmuch.
---
 notmuch-search.c | 458 ++++++++++++++++++++++++++++++++++++++-----------------
 1 file changed, 322 insertions(+), 136 deletions(-)

diff --git a/notmuch-search.c b/notmuch-search.c
index fa5086e..3413b79 100644
--- a/notmuch-search.c
+++ b/notmuch-search.c
@@ -28,19 +28,181 @@ typedef enum {
     OUTPUT_TAGS
 } output_t;
 
+typedef void * structure_printer_state_t;
+
 typedef struct structure_printer {
-    int (*map)(struct structure_printer_t *sp);
-    int (*list)(struct structure_printer_t *sp);
-    void (*pop)(struct structure_printer_t *sp, int level);
-    void (*map_key)(struct structure_printer_t *sp, const char *key);
-    void (*number)(struct structure_printer_t *sp, int val);
-    void (*string)(struct structure_printer_t *sp, const char *val);
-    void (*bool)(struct structure_printer_t *sp, notmuch_bool_t val);
-    void *(*initial_context)(struct structure_printer_t *sp);
+    int (*map)(void *state);
+    int (*list)(void *state);
+    void (*pop)(void *state, int level);
+    void (*map_key)(void *state, const char *key);
+    void (*number)(void *state, int val);
+    void (*string)(void *state, const char *val);
+    void (*bool)(void *state, notmuch_bool_t val);
+    void *(*initial_state)(const struct structure_printer *sp, FILE *output);
 } structure_printer_t;
 
-struct structure_printer_t *new_search_format_structure_printer(FILE *stream, struct search_format_t *search_format);
+/* JSON structure printer */
+
+typedef struct json_list {
+    int type;
+    int first_already_seen;
+    struct json_list *rest;
+} json_list_t;
+
+#define TYPE_JSON_MAP 1
+#define TYPE_JSON_ARRAY 2
+
+typedef struct json_state {
+    FILE *output;
+    json_list_t *stack;
+    int level;
+} json_state_t;
+
+int json_map(void *state);
+int json_list(void *state);
+void json_pop(void *state, int level);
+void json_map_key(void *state, const char *key);
+void json_number(void *state, int val);
+void json_string(void *state, const char *val);
+void json_bool(void *state, notmuch_bool_t val);
+void *json_initial_state(const struct structure_printer *sp, FILE *output);
+
+int json_map(void *st) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
+	fputs(",", output);
+	if(state->level == 1)
+	    fputs("\n", output);
+	else
+	    fputs(" ", output);
+    }
+    if(state->stack != NULL) {
+	state->stack->first_already_seen = TRUE;
+    }
+    fputs("{", output);
+    void *ctx_json_map = talloc_new (0);
+    json_list_t *el = talloc(ctx_json_map, json_list_t);
+    el->type = TYPE_JSON_MAP;
+    el->first_already_seen = FALSE;
+    el->rest = state->stack;
+    state->stack = el;
+    return state->level++;
+}
+
+int json_list(void *st) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
+	fputs(",", output);
+	if(state->level == 1)
+	    fputs("\n", output);
+	else
+	    fputs(" ", output);
+    }
+    if(state->stack != NULL) {
+	state->stack->first_already_seen = TRUE;
+    }
+    fputs("[", output);
+    void *ctx_json_map = talloc_new (0);
+    json_list_t *el = talloc(ctx_json_map, json_list_t);
+    el->type = TYPE_JSON_ARRAY;
+    el->first_already_seen = FALSE;
+    el->rest = state->stack;
+    state->stack = el;
+    return state->level++;
+}
+
+void json_pop(void *st, int level) {
+    int i;
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    for(i = state->level; i > level; i--) {
+	json_list_t *tos = state->stack;
+	if(tos->type == TYPE_JSON_MAP) {
+	    fputs("}", output);
+	} 
+	if(tos->type == TYPE_JSON_ARRAY) {
+	    fputs("]", output);
+	}
+	state->stack = tos->rest;
+	state->level--;
+	talloc_free(tos);
+    }
+    if(state->level == 0)
+	fputs("\n", output);
+}
+
+void json_map_key(void *st, const char *key) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(state->stack != NULL && state->stack->first_already_seen) {
+	fputs(",\n", output);
+    }
+    fputs("\"", output);
+    fputs(key, output);
+    fputs("\": ", output);
+}
+
+void json_number(void *st, int val) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
+	fputs(", ", output);
+    }
+    state->stack->first_already_seen = TRUE;
+    fprintf(output, "%i", val);
+}
+
+void json_string(void *st, const char *val) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    void *ctx = talloc_new(0);
+    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
+	fputs(",", output);
+	if(state->level == 1)
+	    fputs("\n", output);
+	else
+	    fputs(" ", output);
+    }
+
+    state->stack->first_already_seen = TRUE;
+    fprintf(output, "%s", json_quote_str(ctx, val));
+    talloc_free(ctx);
+}
+
+void json_bool(void *st, notmuch_bool_t val) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(val)
+	fputs("true", output);
+    else
+	fputs("false", output);
+}
+
+void *json_initial_state(const struct structure_printer *sp, FILE *output) {
+    (void)sp;
+    json_state_t *st = talloc(0, json_state_t);
+    st->level = 0;
+    st->stack = NULL;
+    st->output = output;
+    return st;
+}
+
+structure_printer_t json_structure_printer = {
+    &json_map,
+    &json_list,
+    &json_pop,
+    &json_map_key,
+    &json_number,
+    &json_string,
+    &json_bool,
+    &json_initial_state
+};
 
+structure_printer_t *text_structure_printer = NULL;
+
+/* legacy, only needed for non-structured text output */
 typedef struct search_format {
     const char *results_start;
     const char *item_start;
@@ -64,6 +226,7 @@ typedef struct search_format {
     const char *results_null;
 } search_format_t;
 
+
 static void
 format_item_id_text (const void *ctx,
 		     const char *item_type,
@@ -77,6 +240,7 @@ format_thread_text (const void *ctx,
 		    const int total,
 		    const char *authors,
 		    const char *subject);
+
 static const search_format_t format_text = {
     "",
 	"",
@@ -91,35 +255,6 @@ static const search_format_t format_text = {
 };
 
 static void
-format_item_id_json (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject);
-
-/* Any changes to the JSON format should be reflected in the file
- * devel/schemata. */
-static const search_format_t format_json = {
-    "[",
-	"{",
-	    format_item_id_json,
-	    format_thread_json,
-	    "\"tags\": [",
-		"\"%s\"", ", ",
-	    "]", ",\n",
-	"}",
-    "]\n",
-    "]\n",
-};
-
-static void
 format_item_id_text (unused (const void *ctx),
 		     const char *item_type,
 		     const char *item_id)
@@ -166,50 +301,9 @@ format_thread_text (const void *ctx,
     talloc_free (ctx_quote);
 }
 
-static void
-format_item_id_json (const void *ctx,
-		     unused (const char *item_type),
-		     const char *item_id)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("%s", json_quote_str (ctx_quote, item_id));
-
-    talloc_free (ctx_quote);
-    
-}
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("\"thread\": %s,\n"
-	    "\"timestamp\": %ld,\n"
-	    "\"date_relative\": \"%s\",\n"
-	    "\"matched\": %d,\n"
-	    "\"total\": %d,\n"
-	    "\"authors\": %s,\n"
-	    "\"subject\": %s,\n",
-	    json_quote_str (ctx_quote, thread_id),
-	    date,
-	    notmuch_time_relative_date (ctx, date),
-	    matched,
-	    total,
-	    json_quote_str (ctx_quote, authors),
-	    json_quote_str (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
 static int
-do_search_threads (const search_format_t *format,
+do_search_threads (const structure_printer_t *format,
+		   void *state,
 		   notmuch_query_t *query,
 		   notmuch_sort_t sort,
 		   output_t output,
@@ -222,7 +316,8 @@ do_search_threads (const search_format_t *format,
     time_t date;
     int first_thread = 1;
     int i;
-
+    int outermost_level = 0;
+    int items_level = 0;
     if (offset < 0) {
 	offset += notmuch_query_count_threads (query);
 	if (offset < 0)
@@ -233,7 +328,10 @@ do_search_threads (const search_format_t *format,
     if (threads == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    if(format == text_structure_printer)
+	fputs(format_text.results_start, stdout);
+    else
+	outermost_level = format->list(state);
 
     for (i = 0;
 	 notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
@@ -248,43 +346,93 @@ do_search_threads (const search_format_t *format,
 	    continue;
 	}
 
-	if (! first_thread)
-	    fputs (format->item_sep, stdout);
+	if (format == text_structure_printer && ! first_thread)
+	    fputs (format_text.item_sep, stdout);
 
 	if (output == OUTPUT_THREADS) {
-	    format->item_id (thread, "thread:",
-			     notmuch_thread_get_thread_id (thread));
+	    if(format == text_structure_printer) {
+		format_text.item_id (thread, "thread:",
+				     notmuch_thread_get_thread_id (thread));
+	    }
+	    else {
+		char buffer[128];
+		strncpy(buffer, "thread:", 1 + strlen("thread:"));
+		strncat(buffer, notmuch_thread_get_thread_id (thread), 128 - strlen("thread:"));
+		format->string(state, buffer);
+	    }
+	    
 	} else { /* output == OUTPUT_SUMMARY */
-	    fputs (format->item_start, stdout);
+	    int tags_level = 0;
+	    void *ctx = talloc_new (0);
+
+	    if(format == text_structure_printer)
+		fputs (format_text.item_start, stdout);
+	    else
+		items_level = format->map(state);
 
 	    if (sort == NOTMUCH_SORT_OLDEST_FIRST)
 		date = notmuch_thread_get_oldest_date (thread);
 	    else
 		date = notmuch_thread_get_newest_date (thread);
 
-	    format->thread_summary (thread,
-				    notmuch_thread_get_thread_id (thread),
-				    date,
-				    notmuch_thread_get_matched_messages (thread),
-				    notmuch_thread_get_total_messages (thread),
-				    notmuch_thread_get_authors (thread),
-				    notmuch_thread_get_subject (thread));
+	    if(format == text_structure_printer) {
+		format_text.thread_summary (thread,
+					    notmuch_thread_get_thread_id (thread),
+					    date,
+					    notmuch_thread_get_matched_messages (thread),
+					    notmuch_thread_get_total_messages (thread),
+					    notmuch_thread_get_authors (thread),
+					    notmuch_thread_get_subject (thread));
+	    } else {
+		format->map_key(state, "thread");
+		format->string(state, notmuch_thread_get_thread_id (thread));
+		format->map_key(state, "timestamp");
+		format->number(state, date);
+		format->map_key(state, "date_relative");
+		format->string(state, notmuch_time_relative_date(ctx, date));
+		format->map_key(state, "matched");
+		format->number(state, notmuch_thread_get_matched_messages(thread));
+		format->map_key(state, "total");
+		format->number(state, notmuch_thread_get_total_messages(thread));
+		format->map_key(state, "authors");
+		format->string(state, notmuch_thread_get_authors(thread));
+		format->map_key(state, "subject");
+		format->string(state, notmuch_thread_get_subject(thread));
+	    }
+
+	    if(format == text_structure_printer) {
+		fputs (format_text.tag_start, stdout);
+	    } else {
+		format->map_key(state, "tags");
+
+		tags_level = format->list(state);
+	    }
 
-	    fputs (format->tag_start, stdout);
 
 	    for (tags = notmuch_thread_get_tags (thread);
 		 notmuch_tags_valid (tags);
 		 notmuch_tags_move_to_next (tags))
 	    {
-		if (! first_tag)
-		    fputs (format->tag_sep, stdout);
-		printf (format->tag, notmuch_tags_get (tags));
+		if (format == text_structure_printer && ! first_tag) {
+		    fputs (format_text.tag_sep, stdout);
+		}
+		if(format == text_structure_printer) {
+		    printf (format_text.tag, notmuch_tags_get (tags));
+		} else {
+		    format->string(state, notmuch_tags_get(tags));
+		}
 		first_tag = 0;
 	    }
 
-	    fputs (format->tag_end, stdout);
+	    if(format == text_structure_printer)
+		fputs (format_text.tag_end, stdout);
+	    else
+		format->pop(state, tags_level);
 
-	    fputs (format->item_end, stdout);
+	    if(format == text_structure_printer)
+		fputs (format_text.item_end, stdout);
+	    else
+		format->pop(state, items_level);
 	}
 
 	first_thread = 0;
@@ -292,16 +440,21 @@ do_search_threads (const search_format_t *format,
 	notmuch_thread_destroy (thread);
     }
 
-    if (first_thread)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    if(format == text_structure_printer) {
+	if (first_thread)
+	    fputs (format_text.results_null, stdout);
+	else
+	    fputs (format_text.results_end, stdout);
+    } else {
+	format->pop(state, outermost_level);
+    }
 
     return 0;
 }
 
 static int
-do_search_messages (const search_format_t *format,
+do_search_messages (const structure_printer_t *format,
+		    void *state,
 		    notmuch_query_t *query,
 		    output_t output,
 		    int offset,
@@ -312,6 +465,7 @@ do_search_messages (const search_format_t *format,
     notmuch_filenames_t *filenames;
     int first_message = 1;
     int i;
+    int outermost_level = 0;
 
     if (offset < 0) {
 	offset += notmuch_query_count_messages (query);
@@ -323,7 +477,10 @@ do_search_messages (const search_format_t *format,
     if (messages == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    if(format == text_structure_printer)
+	fputs (format_text.results_start, stdout);
+    else
+	outermost_level = format->list(state);
 
     for (i = 0;
 	 notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
@@ -341,23 +498,32 @@ do_search_messages (const search_format_t *format,
 		 notmuch_filenames_valid (filenames);
 		 notmuch_filenames_move_to_next (filenames))
 	    {
-		if (! first_message)
-		    fputs (format->item_sep, stdout);
-
-		format->item_id (message, "",
-				 notmuch_filenames_get (filenames));
-
+		if(format == text_structure_printer) {
+		    if (! first_message)
+			fputs (format_text.item_sep, stdout);
+
+		    format_text.item_id (message, "",
+					 notmuch_filenames_get (filenames));
+		} else {
+		format->string(state, notmuch_filenames_get (filenames));
+		}
+		
 		first_message = 0;
 	    }
 	    
 	    notmuch_filenames_destroy( filenames );
 
 	} else { /* output == OUTPUT_MESSAGES */
-	    if (! first_message)
-		fputs (format->item_sep, stdout);
+	    if(format == text_structure_printer) {
+		if (! first_message)
+		    fputs (format_text.item_sep, stdout);
+
+		format_text.item_id (message, "id:",
+				     notmuch_message_get_message_id (message));
+	    } else {
+		format->string(state, notmuch_message_get_message_id (message));
+	    }
 
-	    format->item_id (message, "id:",
-			     notmuch_message_get_message_id (message));
 	    first_message = 0;
 	}
 
@@ -366,23 +532,29 @@ do_search_messages (const search_format_t *format,
 
     notmuch_messages_destroy (messages);
 
-    if (first_message)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    if(format == text_structure_printer) {
+	if (first_message)
+	    fputs (format_text.results_null, stdout);
+	else
+	    fputs (format_text.results_end, stdout);
+    } else {
+	format->pop(state, outermost_level);
+    }
 
     return 0;
 }
 
 static int
 do_search_tags (notmuch_database_t *notmuch,
-		const search_format_t *format,
+		const structure_printer_t *format,
+		void *state,
 		notmuch_query_t *query)
 {
     notmuch_messages_t *messages = NULL;
     notmuch_tags_t *tags;
     const char *tag;
     int first_tag = 1;
+    int outermost_level = 0;
 
     /* should the following only special case if no excluded terms
      * specified? */
@@ -400,7 +572,10 @@ do_search_tags (notmuch_database_t *notmuch,
     if (tags == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    if(format == text_structure_printer)
+	fputs (format_text.results_start, stdout);
+    else
+	outermost_level = format->list(state);
 
     for (;
 	 notmuch_tags_valid (tags);
@@ -408,10 +583,14 @@ do_search_tags (notmuch_database_t *notmuch,
     {
 	tag = notmuch_tags_get (tags);
 
-	if (! first_tag)
-	    fputs (format->item_sep, stdout);
+	if(format == text_structure_printer) {
+	    if (! first_tag)
+		fputs (format_text.item_sep, stdout);
 
-	format->item_id (tags, "", tag);
+	    format_text.item_id (tags, "", tag);
+	} else {
+	    format->string(state, tag);
+	}
 
 	first_tag = 0;
     }
@@ -421,10 +600,14 @@ do_search_tags (notmuch_database_t *notmuch,
     if (messages)
 	notmuch_messages_destroy (messages);
 
-    if (first_tag)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    if(format == text_structure_printer) {
+	if (first_tag)
+	    fputs (format_text.results_null, stdout);
+	else
+	    fputs (format_text.results_end, stdout);
+    } else {
+	format->pop(state, outermost_level);
+    }
 
     return 0;
 }
@@ -443,7 +626,8 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_t *query;
     char *query_str;
     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
-    const search_format_t *format = &format_text;
+    const structure_printer_t *format = text_structure_printer;
+    void *state = NULL;
     int opt_index, ret;
     output_t output = OUTPUT_SUMMARY;
     int offset = 0;
@@ -488,10 +672,12 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 
     switch (format_sel) {
     case NOTMUCH_FORMAT_TEXT:
-	format = &format_text;
+	format = text_structure_printer;
+	state = 0;
 	break;
     case NOTMUCH_FORMAT_JSON:
-	format = &format_json;
+	format = &json_structure_printer;
+	state = format->initial_state(format, stdout);
 	break;
     }
 
@@ -545,14 +731,14 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     default:
     case OUTPUT_SUMMARY:
     case OUTPUT_THREADS:
-	ret = do_search_threads (format, query, sort, output, offset, limit);
+	ret = do_search_threads (format, state, query, sort, output, offset, limit);
 	break;
     case OUTPUT_MESSAGES:
     case OUTPUT_FILES:
-	ret = do_search_messages (format, query, output, offset, limit);
+	ret = do_search_messages (format, state, query, output, offset, limit);
 	break;
     case OUTPUT_TAGS:
-	ret = do_search_tags (notmuch, format, query);
+	ret = do_search_tags (notmuch, format, state, query);
 	break;
     }
 
-- 
1.7.11.1

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH] FIXED: Added better support for multiple structured output formats.
  2012-07-10 10:02 [PATCH] Added better support for multiple structured output formats craven
@ 2012-07-10 10:48 ` craven
  2012-07-10 12:45   ` Mark Walters
  2012-07-10 16:58   ` [PATCH] FIXED: Added better support for multiple structured output formats Jameson Graef Rollins
  0 siblings, 2 replies; 39+ messages in thread
From: craven @ 2012-07-10 10:48 UTC (permalink / raw)
  To: notmuch

Sorry, the original patch I sent was missing a small part, here the full
patch:

As discussed in <id:20120121220407.GK16740@mit.edu>, this patch adds
support for new structured output formats (like s-expressions) by using
stateful structure_printers. An implementation of the JSON structure
printer that passes all tests is included.

Structure printers have functions for starting a map, a list, closing
any number of these two, printing a map key, a number value, a string
value, and boolean values. By threading a state through the
invocations, arbitrary structured formatting can be achieved.

In a second patch, the structured output code should be isolated in a
separate file, and also used in all other parts of notmuch.
---
 notmuch-search.c | 453 +++++++++++++++++++++++++++++++++++++++----------------
 1 file changed, 326 insertions(+), 127 deletions(-)

diff --git a/notmuch-search.c b/notmuch-search.c
index 3be296d..3413b79 100644
--- a/notmuch-search.c
+++ b/notmuch-search.c
@@ -28,6 +28,181 @@ typedef enum {
     OUTPUT_TAGS
 } output_t;
 
+typedef void * structure_printer_state_t;
+
+typedef struct structure_printer {
+    int (*map)(void *state);
+    int (*list)(void *state);
+    void (*pop)(void *state, int level);
+    void (*map_key)(void *state, const char *key);
+    void (*number)(void *state, int val);
+    void (*string)(void *state, const char *val);
+    void (*bool)(void *state, notmuch_bool_t val);
+    void *(*initial_state)(const struct structure_printer *sp, FILE *output);
+} structure_printer_t;
+
+/* JSON structure printer */
+
+typedef struct json_list {
+    int type;
+    int first_already_seen;
+    struct json_list *rest;
+} json_list_t;
+
+#define TYPE_JSON_MAP 1
+#define TYPE_JSON_ARRAY 2
+
+typedef struct json_state {
+    FILE *output;
+    json_list_t *stack;
+    int level;
+} json_state_t;
+
+int json_map(void *state);
+int json_list(void *state);
+void json_pop(void *state, int level);
+void json_map_key(void *state, const char *key);
+void json_number(void *state, int val);
+void json_string(void *state, const char *val);
+void json_bool(void *state, notmuch_bool_t val);
+void *json_initial_state(const struct structure_printer *sp, FILE *output);
+
+int json_map(void *st) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
+	fputs(",", output);
+	if(state->level == 1)
+	    fputs("\n", output);
+	else
+	    fputs(" ", output);
+    }
+    if(state->stack != NULL) {
+	state->stack->first_already_seen = TRUE;
+    }
+    fputs("{", output);
+    void *ctx_json_map = talloc_new (0);
+    json_list_t *el = talloc(ctx_json_map, json_list_t);
+    el->type = TYPE_JSON_MAP;
+    el->first_already_seen = FALSE;
+    el->rest = state->stack;
+    state->stack = el;
+    return state->level++;
+}
+
+int json_list(void *st) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
+	fputs(",", output);
+	if(state->level == 1)
+	    fputs("\n", output);
+	else
+	    fputs(" ", output);
+    }
+    if(state->stack != NULL) {
+	state->stack->first_already_seen = TRUE;
+    }
+    fputs("[", output);
+    void *ctx_json_map = talloc_new (0);
+    json_list_t *el = talloc(ctx_json_map, json_list_t);
+    el->type = TYPE_JSON_ARRAY;
+    el->first_already_seen = FALSE;
+    el->rest = state->stack;
+    state->stack = el;
+    return state->level++;
+}
+
+void json_pop(void *st, int level) {
+    int i;
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    for(i = state->level; i > level; i--) {
+	json_list_t *tos = state->stack;
+	if(tos->type == TYPE_JSON_MAP) {
+	    fputs("}", output);
+	} 
+	if(tos->type == TYPE_JSON_ARRAY) {
+	    fputs("]", output);
+	}
+	state->stack = tos->rest;
+	state->level--;
+	talloc_free(tos);
+    }
+    if(state->level == 0)
+	fputs("\n", output);
+}
+
+void json_map_key(void *st, const char *key) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(state->stack != NULL && state->stack->first_already_seen) {
+	fputs(",\n", output);
+    }
+    fputs("\"", output);
+    fputs(key, output);
+    fputs("\": ", output);
+}
+
+void json_number(void *st, int val) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
+	fputs(", ", output);
+    }
+    state->stack->first_already_seen = TRUE;
+    fprintf(output, "%i", val);
+}
+
+void json_string(void *st, const char *val) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    void *ctx = talloc_new(0);
+    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
+	fputs(",", output);
+	if(state->level == 1)
+	    fputs("\n", output);
+	else
+	    fputs(" ", output);
+    }
+
+    state->stack->first_already_seen = TRUE;
+    fprintf(output, "%s", json_quote_str(ctx, val));
+    talloc_free(ctx);
+}
+
+void json_bool(void *st, notmuch_bool_t val) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(val)
+	fputs("true", output);
+    else
+	fputs("false", output);
+}
+
+void *json_initial_state(const struct structure_printer *sp, FILE *output) {
+    (void)sp;
+    json_state_t *st = talloc(0, json_state_t);
+    st->level = 0;
+    st->stack = NULL;
+    st->output = output;
+    return st;
+}
+
+structure_printer_t json_structure_printer = {
+    &json_map,
+    &json_list,
+    &json_pop,
+    &json_map_key,
+    &json_number,
+    &json_string,
+    &json_bool,
+    &json_initial_state
+};
+
+structure_printer_t *text_structure_printer = NULL;
+
+/* legacy, only needed for non-structured text output */
 typedef struct search_format {
     const char *results_start;
     const char *item_start;
@@ -51,6 +226,7 @@ typedef struct search_format {
     const char *results_null;
 } search_format_t;
 
+
 static void
 format_item_id_text (const void *ctx,
 		     const char *item_type,
@@ -64,6 +240,7 @@ format_thread_text (const void *ctx,
 		    const int total,
 		    const char *authors,
 		    const char *subject);
+
 static const search_format_t format_text = {
     "",
 	"",
@@ -78,35 +255,6 @@ static const search_format_t format_text = {
 };
 
 static void
-format_item_id_json (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject);
-
-/* Any changes to the JSON format should be reflected in the file
- * devel/schemata. */
-static const search_format_t format_json = {
-    "[",
-	"{",
-	    format_item_id_json,
-	    format_thread_json,
-	    "\"tags\": [",
-		"\"%s\"", ", ",
-	    "]", ",\n",
-	"}",
-    "]\n",
-    "]\n",
-};
-
-static void
 format_item_id_text (unused (const void *ctx),
 		     const char *item_type,
 		     const char *item_id)
@@ -153,50 +301,9 @@ format_thread_text (const void *ctx,
     talloc_free (ctx_quote);
 }
 
-static void
-format_item_id_json (const void *ctx,
-		     unused (const char *item_type),
-		     const char *item_id)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("%s", json_quote_str (ctx_quote, item_id));
-
-    talloc_free (ctx_quote);
-    
-}
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("\"thread\": %s,\n"
-	    "\"timestamp\": %ld,\n"
-	    "\"date_relative\": \"%s\",\n"
-	    "\"matched\": %d,\n"
-	    "\"total\": %d,\n"
-	    "\"authors\": %s,\n"
-	    "\"subject\": %s,\n",
-	    json_quote_str (ctx_quote, thread_id),
-	    date,
-	    notmuch_time_relative_date (ctx, date),
-	    matched,
-	    total,
-	    json_quote_str (ctx_quote, authors),
-	    json_quote_str (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
 static int
-do_search_threads (const search_format_t *format,
+do_search_threads (const structure_printer_t *format,
+		   void *state,
 		   notmuch_query_t *query,
 		   notmuch_sort_t sort,
 		   output_t output,
@@ -209,7 +316,8 @@ do_search_threads (const search_format_t *format,
     time_t date;
     int first_thread = 1;
     int i;
-
+    int outermost_level = 0;
+    int items_level = 0;
     if (offset < 0) {
 	offset += notmuch_query_count_threads (query);
 	if (offset < 0)
@@ -220,7 +328,10 @@ do_search_threads (const search_format_t *format,
     if (threads == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    if(format == text_structure_printer)
+	fputs(format_text.results_start, stdout);
+    else
+	outermost_level = format->list(state);
 
     for (i = 0;
 	 notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
@@ -235,43 +346,93 @@ do_search_threads (const search_format_t *format,
 	    continue;
 	}
 
-	if (! first_thread)
-	    fputs (format->item_sep, stdout);
+	if (format == text_structure_printer && ! first_thread)
+	    fputs (format_text.item_sep, stdout);
 
 	if (output == OUTPUT_THREADS) {
-	    format->item_id (thread, "thread:",
-			     notmuch_thread_get_thread_id (thread));
+	    if(format == text_structure_printer) {
+		format_text.item_id (thread, "thread:",
+				     notmuch_thread_get_thread_id (thread));
+	    }
+	    else {
+		char buffer[128];
+		strncpy(buffer, "thread:", 1 + strlen("thread:"));
+		strncat(buffer, notmuch_thread_get_thread_id (thread), 128 - strlen("thread:"));
+		format->string(state, buffer);
+	    }
+	    
 	} else { /* output == OUTPUT_SUMMARY */
-	    fputs (format->item_start, stdout);
+	    int tags_level = 0;
+	    void *ctx = talloc_new (0);
+
+	    if(format == text_structure_printer)
+		fputs (format_text.item_start, stdout);
+	    else
+		items_level = format->map(state);
 
 	    if (sort == NOTMUCH_SORT_OLDEST_FIRST)
 		date = notmuch_thread_get_oldest_date (thread);
 	    else
 		date = notmuch_thread_get_newest_date (thread);
 
-	    format->thread_summary (thread,
-				    notmuch_thread_get_thread_id (thread),
-				    date,
-				    notmuch_thread_get_matched_messages (thread),
-				    notmuch_thread_get_total_messages (thread),
-				    notmuch_thread_get_authors (thread),
-				    notmuch_thread_get_subject (thread));
+	    if(format == text_structure_printer) {
+		format_text.thread_summary (thread,
+					    notmuch_thread_get_thread_id (thread),
+					    date,
+					    notmuch_thread_get_matched_messages (thread),
+					    notmuch_thread_get_total_messages (thread),
+					    notmuch_thread_get_authors (thread),
+					    notmuch_thread_get_subject (thread));
+	    } else {
+		format->map_key(state, "thread");
+		format->string(state, notmuch_thread_get_thread_id (thread));
+		format->map_key(state, "timestamp");
+		format->number(state, date);
+		format->map_key(state, "date_relative");
+		format->string(state, notmuch_time_relative_date(ctx, date));
+		format->map_key(state, "matched");
+		format->number(state, notmuch_thread_get_matched_messages(thread));
+		format->map_key(state, "total");
+		format->number(state, notmuch_thread_get_total_messages(thread));
+		format->map_key(state, "authors");
+		format->string(state, notmuch_thread_get_authors(thread));
+		format->map_key(state, "subject");
+		format->string(state, notmuch_thread_get_subject(thread));
+	    }
+
+	    if(format == text_structure_printer) {
+		fputs (format_text.tag_start, stdout);
+	    } else {
+		format->map_key(state, "tags");
+
+		tags_level = format->list(state);
+	    }
 
-	    fputs (format->tag_start, stdout);
 
 	    for (tags = notmuch_thread_get_tags (thread);
 		 notmuch_tags_valid (tags);
 		 notmuch_tags_move_to_next (tags))
 	    {
-		if (! first_tag)
-		    fputs (format->tag_sep, stdout);
-		printf (format->tag, notmuch_tags_get (tags));
+		if (format == text_structure_printer && ! first_tag) {
+		    fputs (format_text.tag_sep, stdout);
+		}
+		if(format == text_structure_printer) {
+		    printf (format_text.tag, notmuch_tags_get (tags));
+		} else {
+		    format->string(state, notmuch_tags_get(tags));
+		}
 		first_tag = 0;
 	    }
 
-	    fputs (format->tag_end, stdout);
+	    if(format == text_structure_printer)
+		fputs (format_text.tag_end, stdout);
+	    else
+		format->pop(state, tags_level);
 
-	    fputs (format->item_end, stdout);
+	    if(format == text_structure_printer)
+		fputs (format_text.item_end, stdout);
+	    else
+		format->pop(state, items_level);
 	}
 
 	first_thread = 0;
@@ -279,16 +440,21 @@ do_search_threads (const search_format_t *format,
 	notmuch_thread_destroy (thread);
     }
 
-    if (first_thread)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    if(format == text_structure_printer) {
+	if (first_thread)
+	    fputs (format_text.results_null, stdout);
+	else
+	    fputs (format_text.results_end, stdout);
+    } else {
+	format->pop(state, outermost_level);
+    }
 
     return 0;
 }
 
 static int
-do_search_messages (const search_format_t *format,
+do_search_messages (const structure_printer_t *format,
+		    void *state,
 		    notmuch_query_t *query,
 		    output_t output,
 		    int offset,
@@ -299,6 +465,7 @@ do_search_messages (const search_format_t *format,
     notmuch_filenames_t *filenames;
     int first_message = 1;
     int i;
+    int outermost_level = 0;
 
     if (offset < 0) {
 	offset += notmuch_query_count_messages (query);
@@ -310,7 +477,10 @@ do_search_messages (const search_format_t *format,
     if (messages == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    if(format == text_structure_printer)
+	fputs (format_text.results_start, stdout);
+    else
+	outermost_level = format->list(state);
 
     for (i = 0;
 	 notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
@@ -328,23 +498,32 @@ do_search_messages (const search_format_t *format,
 		 notmuch_filenames_valid (filenames);
 		 notmuch_filenames_move_to_next (filenames))
 	    {
-		if (! first_message)
-		    fputs (format->item_sep, stdout);
-
-		format->item_id (message, "",
-				 notmuch_filenames_get (filenames));
-
+		if(format == text_structure_printer) {
+		    if (! first_message)
+			fputs (format_text.item_sep, stdout);
+
+		    format_text.item_id (message, "",
+					 notmuch_filenames_get (filenames));
+		} else {
+		format->string(state, notmuch_filenames_get (filenames));
+		}
+		
 		first_message = 0;
 	    }
 	    
 	    notmuch_filenames_destroy( filenames );
 
 	} else { /* output == OUTPUT_MESSAGES */
-	    if (! first_message)
-		fputs (format->item_sep, stdout);
+	    if(format == text_structure_printer) {
+		if (! first_message)
+		    fputs (format_text.item_sep, stdout);
+
+		format_text.item_id (message, "id:",
+				     notmuch_message_get_message_id (message));
+	    } else {
+		format->string(state, notmuch_message_get_message_id (message));
+	    }
 
-	    format->item_id (message, "id:",
-			     notmuch_message_get_message_id (message));
 	    first_message = 0;
 	}
 
@@ -353,23 +532,29 @@ do_search_messages (const search_format_t *format,
 
     notmuch_messages_destroy (messages);
 
-    if (first_message)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    if(format == text_structure_printer) {
+	if (first_message)
+	    fputs (format_text.results_null, stdout);
+	else
+	    fputs (format_text.results_end, stdout);
+    } else {
+	format->pop(state, outermost_level);
+    }
 
     return 0;
 }
 
 static int
 do_search_tags (notmuch_database_t *notmuch,
-		const search_format_t *format,
+		const structure_printer_t *format,
+		void *state,
 		notmuch_query_t *query)
 {
     notmuch_messages_t *messages = NULL;
     notmuch_tags_t *tags;
     const char *tag;
     int first_tag = 1;
+    int outermost_level = 0;
 
     /* should the following only special case if no excluded terms
      * specified? */
@@ -387,7 +572,10 @@ do_search_tags (notmuch_database_t *notmuch,
     if (tags == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    if(format == text_structure_printer)
+	fputs (format_text.results_start, stdout);
+    else
+	outermost_level = format->list(state);
 
     for (;
 	 notmuch_tags_valid (tags);
@@ -395,10 +583,14 @@ do_search_tags (notmuch_database_t *notmuch,
     {
 	tag = notmuch_tags_get (tags);
 
-	if (! first_tag)
-	    fputs (format->item_sep, stdout);
+	if(format == text_structure_printer) {
+	    if (! first_tag)
+		fputs (format_text.item_sep, stdout);
 
-	format->item_id (tags, "", tag);
+	    format_text.item_id (tags, "", tag);
+	} else {
+	    format->string(state, tag);
+	}
 
 	first_tag = 0;
     }
@@ -408,10 +600,14 @@ do_search_tags (notmuch_database_t *notmuch,
     if (messages)
 	notmuch_messages_destroy (messages);
 
-    if (first_tag)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    if(format == text_structure_printer) {
+	if (first_tag)
+	    fputs (format_text.results_null, stdout);
+	else
+	    fputs (format_text.results_end, stdout);
+    } else {
+	format->pop(state, outermost_level);
+    }
 
     return 0;
 }
@@ -430,7 +626,8 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_t *query;
     char *query_str;
     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
-    const search_format_t *format = &format_text;
+    const structure_printer_t *format = text_structure_printer;
+    void *state = NULL;
     int opt_index, ret;
     output_t output = OUTPUT_SUMMARY;
     int offset = 0;
@@ -475,10 +672,12 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 
     switch (format_sel) {
     case NOTMUCH_FORMAT_TEXT:
-	format = &format_text;
+	format = text_structure_printer;
+	state = 0;
 	break;
     case NOTMUCH_FORMAT_JSON:
-	format = &format_json;
+	format = &json_structure_printer;
+	state = format->initial_state(format, stdout);
 	break;
     }
 
@@ -532,14 +731,14 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     default:
     case OUTPUT_SUMMARY:
     case OUTPUT_THREADS:
-	ret = do_search_threads (format, query, sort, output, offset, limit);
+	ret = do_search_threads (format, state, query, sort, output, offset, limit);
 	break;
     case OUTPUT_MESSAGES:
     case OUTPUT_FILES:
-	ret = do_search_messages (format, query, output, offset, limit);
+	ret = do_search_messages (format, state, query, output, offset, limit);
 	break;
     case OUTPUT_TAGS:
-	ret = do_search_tags (notmuch, format, query);
+	ret = do_search_tags (notmuch, format, state, query);
 	break;
     }
 
-- 
1.7.11.1

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* Re: [PATCH] FIXED: Added better support for multiple structured output formats.
  2012-07-10 10:48 ` [PATCH] FIXED: " craven
@ 2012-07-10 12:45   ` Mark Walters
  2012-07-10 13:30     ` [PATCH] v2: " craven
  2012-07-10 17:04     ` [PATCH] FIXED: Added better support for multiple structured output formats Jameson Graef Rollins
  2012-07-10 16:58   ` [PATCH] FIXED: Added better support for multiple structured output formats Jameson Graef Rollins
  1 sibling, 2 replies; 39+ messages in thread
From: Mark Walters @ 2012-07-10 12:45 UTC (permalink / raw)
  To: craven, notmuch


>  notmuch-search.c | 453 +++++++++++++++++++++++++++++++++++++++----------------
>  1 file changed, 326 insertions(+), 127 deletions(-)

Hi for such a large patch this was surprisingly easy to follow (though I
am not saying I have worked through all the details). However, there are
some preliminary comments below which I think are worth fixing before a
full review as they will make it easier for other reviewers. One thing
that would help a lot is some comments.

Best wishes

Mark

> As discussed in <id:20120121220407.GK16740@mit.edu>, this patch adds
> support for new structured output formats (like s-expressions) by using
> stateful structure_printers. An implementation of the JSON structure
> printer that passes all tests is included.

If I understand it correctly, the output should be identical to before?
If that is the case I think it's worth saying. 

> Structure printers have functions for starting a map, a list, closing
> any number of these two, printing a map key, a number value, a string
> value, and boolean values. By threading a state through the
> invocations, arbitrary structured formatting can be achieved.

I think I would also add something saying that the text format is just
different (and that a significant chunk of the patch is just that).

> In a second patch, the structured output code should be isolated in a
> separate file, and also used in all other parts of notmuch.
> ---
>
> diff --git a/notmuch-search.c b/notmuch-search.c
> index 3be296d..3413b79 100644
> --- a/notmuch-search.c
> +++ b/notmuch-search.c
> @@ -28,6 +28,181 @@ typedef enum {
>      OUTPUT_TAGS
>  } output_t;
>  
> +typedef void * structure_printer_state_t;
> +
> +typedef struct structure_printer {
> +    int (*map)(void *state);
> +    int (*list)(void *state);
> +    void (*pop)(void *state, int level);
> +    void (*map_key)(void *state, const char *key);
> +    void (*number)(void *state, int val);
> +    void (*string)(void *state, const char *val);
> +    void (*bool)(void *state, notmuch_bool_t val);
> +    void *(*initial_state)(const struct structure_printer *sp, FILE *output);
> +} structure_printer_t;

I think this needs some comments on what these functions do. number,
string and boolean are relatively clear (but saying "output a number"
etc is worthwhile). But what map and list do is much less clear and
definitely deserves a comment. And it is definitely unclear what they
are meant to return.

I would also say something about the variable state: eg "the variable
`state` can contain any state the structure_printer wishes to maintain".

> +
> +/* JSON structure printer */
> +
> +typedef struct json_list {
> +    int type;
> +    int first_already_seen;
> +    struct json_list *rest;
> +} json_list_t;
> +
> +#define TYPE_JSON_MAP 1
> +#define TYPE_JSON_ARRAY 2
> +
> +typedef struct json_state {
> +    FILE *output;
> +    json_list_t *stack;
> +    int level;
> +} json_state_t;
> +
> +int json_map(void *state);
> +int json_list(void *state);
> +void json_pop(void *state, int level);
> +void json_map_key(void *state, const char *key);
> +void json_number(void *state, int val);
> +void json_string(void *state, const char *val);
> +void json_bool(void *state, notmuch_bool_t val);
> +void *json_initial_state(const struct structure_printer *sp, FILE *output);
> +
> +int json_map(void *st) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
> +	fputs(",", output);
> +	if(state->level == 1)
> +	    fputs("\n", output);
> +	else
> +	    fputs(" ", output);
> +    }
> +    if(state->stack != NULL) {
> +	state->stack->first_already_seen = TRUE;
> +    }
> +    fputs("{", output);
> +    void *ctx_json_map = talloc_new (0);
> +    json_list_t *el = talloc(ctx_json_map, json_list_t);
> +    el->type = TYPE_JSON_MAP;
> +    el->first_already_seen = FALSE;
> +    el->rest = state->stack;
> +    state->stack = el;
> +    return state->level++;
> +}
> +
> +int json_list(void *st) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
> +	fputs(",", output);
> +	if(state->level == 1)
> +	    fputs("\n", output);
> +	else
> +	    fputs(" ", output);
> +    }
> +    if(state->stack != NULL) {
> +	state->stack->first_already_seen = TRUE;
> +    }
> +    fputs("[", output);
> +    void *ctx_json_map = talloc_new (0);
> +    json_list_t *el = talloc(ctx_json_map, json_list_t);
> +    el->type = TYPE_JSON_ARRAY;
> +    el->first_already_seen = FALSE;
> +    el->rest = state->stack;
> +    state->stack = el;
> +    return state->level++;
> +}
> +
> +void json_pop(void *st, int level) {
> +    int i;
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    for(i = state->level; i > level; i--) {
> +	json_list_t *tos = state->stack;
> +	if(tos->type == TYPE_JSON_MAP) {
> +	    fputs("}", output);
> +	} 
> +	if(tos->type == TYPE_JSON_ARRAY) {
> +	    fputs("]", output);
> +	}
> +	state->stack = tos->rest;
> +	state->level--;
> +	talloc_free(tos);
> +    }
> +    if(state->level == 0)
> +	fputs("\n", output);
> +}
> +
> +void json_map_key(void *st, const char *key) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(state->stack != NULL && state->stack->first_already_seen) {
> +	fputs(",\n", output);
> +    }
> +    fputs("\"", output);
> +    fputs(key, output);
> +    fputs("\": ", output);
> +}
> +
> +void json_number(void *st, int val) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
> +	fputs(", ", output);
> +    }
> +    state->stack->first_already_seen = TRUE;
> +    fprintf(output, "%i", val);
> +}
> +
> +void json_string(void *st, const char *val) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    void *ctx = talloc_new(0);
> +    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
> +	fputs(",", output);
> +	if(state->level == 1)
> +	    fputs("\n", output);
> +	else
> +	    fputs(" ", output);
> +    }
> +
> +    state->stack->first_already_seen = TRUE;
> +    fprintf(output, "%s", json_quote_str(ctx, val));
> +    talloc_free(ctx);
> +}
> +
> +void json_bool(void *st, notmuch_bool_t val) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(val)
> +	fputs("true", output);
> +    else
> +	fputs("false", output);
> +}
> +
> +void *json_initial_state(const struct structure_printer *sp, FILE *output) {
> +    (void)sp;
> +    json_state_t *st = talloc(0, json_state_t);
> +    st->level = 0;
> +    st->stack = NULL;
> +    st->output = output;
> +    return st;
> +}
> +
> +structure_printer_t json_structure_printer = {
> +    &json_map,
> +    &json_list,
> +    &json_pop,
> +    &json_map_key,
> +    &json_number,
> +    &json_string,
> +    &json_bool,
> +    &json_initial_state
> +};

Since you forward declare all these functions I think this would be more
natural before the full declarations.

> +structure_printer_t *text_structure_printer = NULL;

I would rename this particularly given the next comment: it makes it
seem like there is non-structured text output and structured text
output. Maybe `unstructured_text_printer`? 
> +
> +/* legacy, only needed for non-structured text output */
>  typedef struct search_format {
>      const char *results_start;
>      const char *item_start;
> @@ -51,6 +226,7 @@ typedef struct search_format {
>      const char *results_null;
>  } search_format_t;
>  
> +
>  static void
>  format_item_id_text (const void *ctx,
>  		     const char *item_type,

just a whitespace change so omit.

> @@ -64,6 +240,7 @@ format_thread_text (const void *ctx,
>  		    const int total,
>  		    const char *authors,
>  		    const char *subject);
> +
>  static const search_format_t format_text = {
>      "",
>  	"",

just a whitespace change so omit.

(there are also some trailing whitespaces in various of the lines that
should be omitted).

> @@ -78,35 +255,6 @@ static const search_format_t format_text = {
>  };
>  
>  static void
> -format_item_id_json (const void *ctx,
> -		     const char *item_type,
> -		     const char *item_id);
> -
> -static void
> -format_thread_json (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject);
> -
> -/* Any changes to the JSON format should be reflected in the file
> - * devel/schemata. */
> -static const search_format_t format_json = {
> -    "[",
> -	"{",
> -	    format_item_id_json,
> -	    format_thread_json,
> -	    "\"tags\": [",
> -		"\"%s\"", ", ",
> -	    "]", ",\n",
> -	"}",
> -    "]\n",
> -    "]\n",
> -};
> -
> -static void
>  format_item_id_text (unused (const void *ctx),
>  		     const char *item_type,
>  		     const char *item_id)
> @@ -153,50 +301,9 @@ format_thread_text (const void *ctx,
>      talloc_free (ctx_quote);
>  }
>  
> -static void
> -format_item_id_json (const void *ctx,
> -		     unused (const char *item_type),
> -		     const char *item_id)
> -{
> -    void *ctx_quote = talloc_new (ctx);
> -
> -    printf ("%s", json_quote_str (ctx_quote, item_id));
> -
> -    talloc_free (ctx_quote);
> -    
> -}
> -
> -static void
> -format_thread_json (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject)
> -{
> -    void *ctx_quote = talloc_new (ctx);
> -
> -    printf ("\"thread\": %s,\n"
> -	    "\"timestamp\": %ld,\n"
> -	    "\"date_relative\": \"%s\",\n"
> -	    "\"matched\": %d,\n"
> -	    "\"total\": %d,\n"
> -	    "\"authors\": %s,\n"
> -	    "\"subject\": %s,\n",
> -	    json_quote_str (ctx_quote, thread_id),
> -	    date,
> -	    notmuch_time_relative_date (ctx, date),
> -	    matched,
> -	    total,
> -	    json_quote_str (ctx_quote, authors),
> -	    json_quote_str (ctx_quote, subject));
> -
> -    talloc_free (ctx_quote);
> -}
> -
>  static int
> -do_search_threads (const search_format_t *format,
> +do_search_threads (const structure_printer_t *format,
> +		   void *state,
>  		   notmuch_query_t *query,
>  		   notmuch_sort_t sort,
>  		   output_t output,
> @@ -209,7 +316,8 @@ do_search_threads (const search_format_t *format,
>      time_t date;
>      int first_thread = 1;
>      int i;
> -
> +    int outermost_level = 0;
> +    int items_level = 0;
>      if (offset < 0) {
>  	offset += notmuch_query_count_threads (query);
>  	if (offset < 0)
> @@ -220,7 +328,10 @@ do_search_threads (const search_format_t *format,
>      if (threads == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    if(format == text_structure_printer)
> +	fputs(format_text.results_start, stdout);
> +    else
> +	outermost_level = format->list(state);
>  
>      for (i = 0;
>  	 notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
> @@ -235,43 +346,93 @@ do_search_threads (const search_format_t *format,
>  	    continue;
>  	}
>  
> -	if (! first_thread)
> -	    fputs (format->item_sep, stdout);
> +	if (format == text_structure_printer && ! first_thread)
> +	    fputs (format_text.item_sep, stdout);
>  
>  	if (output == OUTPUT_THREADS) {
> -	    format->item_id (thread, "thread:",
> -			     notmuch_thread_get_thread_id (thread));
> +	    if(format == text_structure_printer) {
> +		format_text.item_id (thread, "thread:",
> +				     notmuch_thread_get_thread_id (thread));
> +	    }
> +	    else {
> +		char buffer[128];
> +		strncpy(buffer, "thread:", 1 + strlen("thread:"));
> +		strncat(buffer, notmuch_thread_get_thread_id (thread), 128 - strlen("thread:"));
> +		format->string(state, buffer);

This seems rather ugly: wouldn't a snprintf or possibly a talloc_printf
or something be nicer?

> +	    }
> +	    
>  	} else { /* output == OUTPUT_SUMMARY */
> -	    fputs (format->item_start, stdout);
> +	    int tags_level = 0;
> +	    void *ctx = talloc_new (0);
> +
> +	    if(format == text_structure_printer)
> +		fputs (format_text.item_start, stdout);
> +	    else
> +		items_level = format->map(state);
>  
>  	    if (sort == NOTMUCH_SORT_OLDEST_FIRST)
>  		date = notmuch_thread_get_oldest_date (thread);
>  	    else
>  		date = notmuch_thread_get_newest_date (thread);
>  
> -	    format->thread_summary (thread,
> -				    notmuch_thread_get_thread_id (thread),
> -				    date,
> -				    notmuch_thread_get_matched_messages (thread),
> -				    notmuch_thread_get_total_messages (thread),
> -				    notmuch_thread_get_authors (thread),
> -				    notmuch_thread_get_subject (thread));
> +	    if(format == text_structure_printer) {
> +		format_text.thread_summary (thread,
> +					    notmuch_thread_get_thread_id (thread),
> +					    date,
> +					    notmuch_thread_get_matched_messages (thread),
> +					    notmuch_thread_get_total_messages (thread),
> +					    notmuch_thread_get_authors (thread),
> +					    notmuch_thread_get_subject (thread));
> +	    } else {
> +		format->map_key(state, "thread");
> +		format->string(state, notmuch_thread_get_thread_id (thread));
> +		format->map_key(state, "timestamp");
> +		format->number(state, date);
> +		format->map_key(state, "date_relative");
> +		format->string(state, notmuch_time_relative_date(ctx, date));
> +		format->map_key(state, "matched");
> +		format->number(state, notmuch_thread_get_matched_messages(thread));
> +		format->map_key(state, "total");
> +		format->number(state, notmuch_thread_get_total_messages(thread));
> +		format->map_key(state, "authors");
> +		format->string(state, notmuch_thread_get_authors(thread));
> +		format->map_key(state, "subject");
> +		format->string(state, notmuch_thread_get_subject(thread));
> +	    }
> +
> +	    if(format == text_structure_printer) {
> +		fputs (format_text.tag_start, stdout);
> +	    } else {
> +		format->map_key(state, "tags");
> +
> +		tags_level = format->list(state);
> +	    }
>  
> -	    fputs (format->tag_start, stdout);
>  
>  	    for (tags = notmuch_thread_get_tags (thread);
>  		 notmuch_tags_valid (tags);
>  		 notmuch_tags_move_to_next (tags))
>  	    {
> -		if (! first_tag)
> -		    fputs (format->tag_sep, stdout);
> -		printf (format->tag, notmuch_tags_get (tags));
> +		if (format == text_structure_printer && ! first_tag) {
> +		    fputs (format_text.tag_sep, stdout);
> +		}
> +		if(format == text_structure_printer) {
> +		    printf (format_text.tag, notmuch_tags_get (tags));
> +		} else {
> +		    format->string(state, notmuch_tags_get(tags));
> +		}
>  		first_tag = 0;
>  	    }
>  
> -	    fputs (format->tag_end, stdout);
> +	    if(format == text_structure_printer)
> +		fputs (format_text.tag_end, stdout);
> +	    else
> +		format->pop(state, tags_level);
>  
> -	    fputs (format->item_end, stdout);
> +	    if(format == text_structure_printer)
> +		fputs (format_text.item_end, stdout);
> +	    else
> +		format->pop(state, items_level);
>  	}
>  
>  	first_thread = 0;
> @@ -279,16 +440,21 @@ do_search_threads (const search_format_t *format,
>  	notmuch_thread_destroy (thread);
>      }
>  
> -    if (first_thread)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    if(format == text_structure_printer) {
> +	if (first_thread)
> +	    fputs (format_text.results_null, stdout);
> +	else
> +	    fputs (format_text.results_end, stdout);
> +    } else {
> +	format->pop(state, outermost_level);
> +    }
>  
>      return 0;
>  }
>  
>  static int
> -do_search_messages (const search_format_t *format,
> +do_search_messages (const structure_printer_t *format,
> +		    void *state,
>  		    notmuch_query_t *query,
>  		    output_t output,
>  		    int offset,
> @@ -299,6 +465,7 @@ do_search_messages (const search_format_t *format,
>      notmuch_filenames_t *filenames;
>      int first_message = 1;
>      int i;
> +    int outermost_level = 0;
>  
>      if (offset < 0) {
>  	offset += notmuch_query_count_messages (query);
> @@ -310,7 +477,10 @@ do_search_messages (const search_format_t *format,
>      if (messages == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    if(format == text_structure_printer)
> +	fputs (format_text.results_start, stdout);
> +    else
> +	outermost_level = format->list(state);
>  
>      for (i = 0;
>  	 notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
> @@ -328,23 +498,32 @@ do_search_messages (const search_format_t *format,
>  		 notmuch_filenames_valid (filenames);
>  		 notmuch_filenames_move_to_next (filenames))
>  	    {
> -		if (! first_message)
> -		    fputs (format->item_sep, stdout);
> -
> -		format->item_id (message, "",
> -				 notmuch_filenames_get (filenames));
> -
> +		if(format == text_structure_printer) {
> +		    if (! first_message)
> +			fputs (format_text.item_sep, stdout);
> +
> +		    format_text.item_id (message, "",
> +					 notmuch_filenames_get (filenames));
> +		} else {
> +		format->string(state, notmuch_filenames_get (filenames));
> +		}
> +		
>  		first_message = 0;
>  	    }
>  	    
>  	    notmuch_filenames_destroy( filenames );
>  
>  	} else { /* output == OUTPUT_MESSAGES */
> -	    if (! first_message)
> -		fputs (format->item_sep, stdout);
> +	    if(format == text_structure_printer) {
> +		if (! first_message)
> +		    fputs (format_text.item_sep, stdout);
> +
> +		format_text.item_id (message, "id:",
> +				     notmuch_message_get_message_id (message));
> +	    } else {
> +		format->string(state, notmuch_message_get_message_id (message));
> +	    }
>  
> -	    format->item_id (message, "id:",
> -			     notmuch_message_get_message_id (message));
>  	    first_message = 0;
>  	}
>  
> @@ -353,23 +532,29 @@ do_search_messages (const search_format_t *format,
>  
>      notmuch_messages_destroy (messages);
>  
> -    if (first_message)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    if(format == text_structure_printer) {
> +	if (first_message)
> +	    fputs (format_text.results_null, stdout);
> +	else
> +	    fputs (format_text.results_end, stdout);
> +    } else {
> +	format->pop(state, outermost_level);
> +    }
>  
>      return 0;
>  }
>  
>  static int
>  do_search_tags (notmuch_database_t *notmuch,
> -		const search_format_t *format,
> +		const structure_printer_t *format,
> +		void *state,
>  		notmuch_query_t *query)
>  {
>      notmuch_messages_t *messages = NULL;
>      notmuch_tags_t *tags;
>      const char *tag;
>      int first_tag = 1;
> +    int outermost_level = 0;
>  
>      /* should the following only special case if no excluded terms
>       * specified? */
> @@ -387,7 +572,10 @@ do_search_tags (notmuch_database_t *notmuch,
>      if (tags == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    if(format == text_structure_printer)
> +	fputs (format_text.results_start, stdout);
> +    else
> +	outermost_level = format->list(state);
>  
>      for (;
>  	 notmuch_tags_valid (tags);
> @@ -395,10 +583,14 @@ do_search_tags (notmuch_database_t *notmuch,
>      {
>  	tag = notmuch_tags_get (tags);
>  
> -	if (! first_tag)
> -	    fputs (format->item_sep, stdout);
> +	if(format == text_structure_printer) {
> +	    if (! first_tag)
> +		fputs (format_text.item_sep, stdout);
>  
> -	format->item_id (tags, "", tag);
> +	    format_text.item_id (tags, "", tag);
> +	} else {
> +	    format->string(state, tag);
> +	}
>  
>  	first_tag = 0;
>      }
> @@ -408,10 +600,14 @@ do_search_tags (notmuch_database_t *notmuch,
>      if (messages)
>  	notmuch_messages_destroy (messages);
>  
> -    if (first_tag)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    if(format == text_structure_printer) {
> +	if (first_tag)
> +	    fputs (format_text.results_null, stdout);
> +	else
> +	    fputs (format_text.results_end, stdout);
> +    } else {
> +	format->pop(state, outermost_level);
> +    }
>  
>      return 0;
>  }
> @@ -430,7 +626,8 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>      notmuch_query_t *query;
>      char *query_str;
>      notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
> -    const search_format_t *format = &format_text;
> +    const structure_printer_t *format = text_structure_printer;
> +    void *state = NULL;
>      int opt_index, ret;
>      output_t output = OUTPUT_SUMMARY;
>      int offset = 0;
> @@ -475,10 +672,12 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>  
>      switch (format_sel) {
>      case NOTMUCH_FORMAT_TEXT:
> -	format = &format_text;
> +	format = text_structure_printer;
> +	state = 0;
>  	break;
>      case NOTMUCH_FORMAT_JSON:
> -	format = &format_json;
> +	format = &json_structure_printer;
> +	state = format->initial_state(format, stdout);
>  	break;
>      }
>  
> @@ -532,14 +731,14 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>      default:
>      case OUTPUT_SUMMARY:
>      case OUTPUT_THREADS:
> -	ret = do_search_threads (format, query, sort, output, offset, limit);
> +	ret = do_search_threads (format, state, query, sort, output, offset, limit);
>  	break;
>      case OUTPUT_MESSAGES:
>      case OUTPUT_FILES:
> -	ret = do_search_messages (format, query, output, offset, limit);
> +	ret = do_search_messages (format, state, query, output, offset, limit);
>  	break;
>      case OUTPUT_TAGS:
> -	ret = do_search_tags (notmuch, format, query);
> +	ret = do_search_tags (notmuch, format, state, query);
>  	break;
>      }
>  

One final comment: you have quite a lot of things of the form


    if(format == text_structure_printer) {
        do_something
    } else {
	do_something_else
    }

in some of the cases they are obviously analagous things (eg output a
string) but in other cases they look like they might really just be
different. If there are some of the latter (and I haven't worked through
it carefully enough to be sure) then my preference would be to have them
as

    if(format == text_structure_printer) {
        do_something
    } 

    if (format != text_structure_printer) {
	do_something_else
    }

but that is a personal preference and others (and you) may easily disagree.

Best wishes 

Mark

^ permalink raw reply	[flat|nested] 39+ messages in thread

* [PATCH] v2: Added better support for multiple structured output formats.
  2012-07-10 12:45   ` Mark Walters
@ 2012-07-10 13:30     ` craven
  2012-07-10 17:34       ` Mark Walters
  2012-07-10 19:13       ` Austin Clements
  2012-07-10 17:04     ` [PATCH] FIXED: Added better support for multiple structured output formats Jameson Graef Rollins
  1 sibling, 2 replies; 39+ messages in thread
From: craven @ 2012-07-10 13:30 UTC (permalink / raw)
  To: notmuch

As discussed in <id:20120121220407.GK16740@mit.edu>, this patch adds
support for new structured output formats (like s-expressions) by using
stateful structure_printers. An implementation of the JSON structure
printer that passes all tests is included. The output for JSON (and
text) is identical to the current output. S-Expressions will be added in
a later patch.

A large part of this patch just implements the differentiation between
structured and non-structured output (all the code within 
"if(format == unstructured_text_printer)").

In a second patch, the structured output code should be isolated in a
separate file, and also used in all other parts of notmuch.

The interface is a structure structure_printer, which contains the following methods:

- initial_state: is called to create a state object, that is passed to all invocations. This should be used to keep track of the output file and everything else necessary to correctly format output.
- map: is called when a new map (associative array, dictionary) is started. map_key and the primitives (string, number, bool) are used alternatingly to add key/value pairs. pop is used to close the map (see there). This function must return a nesting level identifier that can be used to close all nested structures (maps and lists), backing out to the returned nesting level.
- list: is called when a new list (array, vector) is started. the primitives (string, number, bool) are used consecutively to add values to the list. pop is used to close the list. This function must return a nesting level identifier that can be used to close all nested structures (maps and lists), backing out to the returned nesting level.
- pop: is called to return to a given nesting level. All lists and maps with a deeper nesting level must be closed.
- number, string, bool: output one element of the specific type.

All functions should use the state object to insert delimiters etc. automatically when appropriate.

Example:
int top, one;
top = map(state);
map_key(state, "foo");
one = list(state);
number(state, 1);
number(state, 2);
number(state, 3);
pop(state, i);
map_key(state, "bar");
map(state);
map_key(state, "baaz");
string(state, "hello world");
pop(state, top);

would output JSON as follows:

{"foo": [1, 2, 3], "bar": { "baaz": "hello world"}}
---
 notmuch-search.c | 491 ++++++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 361 insertions(+), 130 deletions(-)

diff --git a/notmuch-search.c b/notmuch-search.c
index 3be296d..4127777 100644
--- a/notmuch-search.c
+++ b/notmuch-search.c
@@ -28,6 +28,210 @@ typedef enum {
     OUTPUT_TAGS
 } output_t;
 
+/* structured formatting, useful for JSON, S-Expressions, ...
+
+- initial_state: is called to create a state object, that is passed to all invocations. This should be used to keep track of the output file and everything else necessary to correctly format output.
+- map: is called when a new map (associative array, dictionary) is started. map_key and the primitives (string, number, bool) are used alternatingly to add key/value pairs. pop is used to close the map (see there). This function must return a nesting level identifier that can be used to close all nested structures (maps and lists), backing out to the returned nesting level.
+- list: is called when a new list (array, vector) is started. the primitives (string, number, bool) are used consecutively to add values to the list. pop is used to close the list. This function must return a nesting level identifier that can be used to close all nested structures (maps and lists), backing out to the returned nesting level.
+- pop: is called to return to a given nesting level. All lists and maps with a deeper nesting level must be closed.
+- number, string, bool: output one element of the specific type.
+
+All functions should use state to insert delimiters etc. automatically when appropriate.
+
+Example:
+int top, one;
+top = map(state);
+map_key(state, "foo");
+one = list(state);
+number(state, 1);
+number(state, 2);
+number(state, 3);
+pop(state, i);
+map_key(state, "bar");
+map(state);
+map_key(state, "baaz");
+string(state, "hello world");
+pop(state, top);
+
+would output JSON as follows:
+
+{"foo": [1, 2, 3], "bar": { "baaz": "hello world"}}
+
+ */
+typedef struct structure_printer {
+    int (*map)(void *state);
+    int (*list)(void *state);
+    void (*pop)(void *state, int level);
+    void (*map_key)(void *state, const char *key);
+    void (*number)(void *state, int val);
+    void (*string)(void *state, const char *val);
+    void (*bool)(void *state, notmuch_bool_t val);
+    void *(*initial_state)(const struct structure_printer *sp, FILE *output);
+} structure_printer_t;
+
+/* JSON structure printer */
+
+/* single linked list implementation for keeping track of the array/map nesting state */
+typedef struct json_list {
+    int type;
+    int first_already_seen;
+    struct json_list *rest;
+} json_list_t;
+
+#define TYPE_JSON_MAP 1
+#define TYPE_JSON_ARRAY 2
+
+typedef struct json_state {
+    FILE *output;
+    json_list_t *stack;
+    int level;
+} json_state_t;
+
+int json_map(void *state);
+int json_list(void *state);
+void json_pop(void *state, int level);
+void json_map_key(void *state, const char *key);
+void json_number(void *state, int val);
+void json_string(void *state, const char *val);
+void json_bool(void *state, notmuch_bool_t val);
+void *json_initial_state(const struct structure_printer *sp, FILE *output);
+
+structure_printer_t json_structure_printer = {
+    &json_map,
+    &json_list,
+    &json_pop,
+    &json_map_key,
+    &json_number,
+    &json_string,
+    &json_bool,
+    &json_initial_state
+};
+
+int json_map(void *st) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
+	fputs(",", output);
+	if(state->level == 1)
+	    fputs("\n", output);
+	else
+	    fputs(" ", output);
+    }
+    if(state->stack != NULL) {
+	state->stack->first_already_seen = TRUE;
+    }
+    fputs("{", output);
+    void *ctx_json_map = talloc_new (0);
+    json_list_t *el = talloc(ctx_json_map, json_list_t);
+    el->type = TYPE_JSON_MAP;
+    el->first_already_seen = FALSE;
+    el->rest = state->stack;
+    state->stack = el;
+    return state->level++;
+}
+
+int json_list(void *st) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
+	fputs(",", output);
+	if(state->level == 1)
+	    fputs("\n", output);
+	else
+	    fputs(" ", output);
+    }
+    if(state->stack != NULL) {
+	state->stack->first_already_seen = TRUE;
+    }
+    fputs("[", output);
+    void *ctx_json_map = talloc_new (0);
+    json_list_t *el = talloc(ctx_json_map, json_list_t);
+    el->type = TYPE_JSON_ARRAY;
+    el->first_already_seen = FALSE;
+    el->rest = state->stack;
+    state->stack = el;
+    return state->level++;
+}
+
+void json_pop(void *st, int level) {
+    int i;
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    for(i = state->level; i > level; i--) {
+	json_list_t *tos = state->stack;
+	if(tos->type == TYPE_JSON_MAP) {
+	    fputs("}", output);
+	}
+	if(tos->type == TYPE_JSON_ARRAY) {
+	    fputs("]", output);
+	}
+	state->stack = tos->rest;
+	state->level--;
+	talloc_free(tos);
+    }
+    if(state->level == 0)
+	fputs("\n", output);
+}
+
+void json_map_key(void *st, const char *key) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(state->stack != NULL && state->stack->first_already_seen) {
+	fputs(",\n", output);
+    }
+    fputs("\"", output);
+    fputs(key, output);
+    fputs("\": ", output);
+}
+
+void json_number(void *st, int val) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
+	fputs(", ", output);
+    }
+    state->stack->first_already_seen = TRUE;
+    fprintf(output, "%i", val);
+}
+
+void json_string(void *st, const char *val) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    void *ctx = talloc_new(0);
+    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
+	fputs(",", output);
+	if(state->level == 1)
+	    fputs("\n", output);
+	else
+	    fputs(" ", output);
+    }
+
+    state->stack->first_already_seen = TRUE;
+    fprintf(output, "%s", json_quote_str(ctx, val));
+    talloc_free(ctx);
+}
+
+void json_bool(void *st, notmuch_bool_t val) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if(val)
+	fputs("true", output);
+    else
+	fputs("false", output);
+}
+
+void *json_initial_state(const struct structure_printer *sp, FILE *output) {
+    (void)sp;
+    json_state_t *st = talloc(0, json_state_t);
+    st->level = 0;
+    st->stack = NULL;
+    st->output = output;
+    return st;
+}
+
+structure_printer_t *unstructured_text_printer = NULL;
+
+/* legacy, only needed for non-structured text output */
 typedef struct search_format {
     const char *results_start;
     const char *item_start;
@@ -51,6 +255,7 @@ typedef struct search_format {
     const char *results_null;
 } search_format_t;
 
+
 static void
 format_item_id_text (const void *ctx,
 		     const char *item_type,
@@ -64,6 +269,7 @@ format_thread_text (const void *ctx,
 		    const int total,
 		    const char *authors,
 		    const char *subject);
+
 static const search_format_t format_text = {
     "",
 	"",
@@ -78,35 +284,6 @@ static const search_format_t format_text = {
 };
 
 static void
-format_item_id_json (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject);
-
-/* Any changes to the JSON format should be reflected in the file
- * devel/schemata. */
-static const search_format_t format_json = {
-    "[",
-	"{",
-	    format_item_id_json,
-	    format_thread_json,
-	    "\"tags\": [",
-		"\"%s\"", ", ",
-	    "]", ",\n",
-	"}",
-    "]\n",
-    "]\n",
-};
-
-static void
 format_item_id_text (unused (const void *ctx),
 		     const char *item_type,
 		     const char *item_id)
@@ -153,50 +330,9 @@ format_thread_text (const void *ctx,
     talloc_free (ctx_quote);
 }
 
-static void
-format_item_id_json (const void *ctx,
-		     unused (const char *item_type),
-		     const char *item_id)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("%s", json_quote_str (ctx_quote, item_id));
-
-    talloc_free (ctx_quote);
-    
-}
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("\"thread\": %s,\n"
-	    "\"timestamp\": %ld,\n"
-	    "\"date_relative\": \"%s\",\n"
-	    "\"matched\": %d,\n"
-	    "\"total\": %d,\n"
-	    "\"authors\": %s,\n"
-	    "\"subject\": %s,\n",
-	    json_quote_str (ctx_quote, thread_id),
-	    date,
-	    notmuch_time_relative_date (ctx, date),
-	    matched,
-	    total,
-	    json_quote_str (ctx_quote, authors),
-	    json_quote_str (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
 static int
-do_search_threads (const search_format_t *format,
+do_search_threads (const structure_printer_t *format,
+		   void *state,
 		   notmuch_query_t *query,
 		   notmuch_sort_t sort,
 		   output_t output,
@@ -210,6 +346,8 @@ do_search_threads (const search_format_t *format,
     int first_thread = 1;
     int i;
 
+    int outermost_level = 0;
+    int items_level = 0;
     if (offset < 0) {
 	offset += notmuch_query_count_threads (query);
 	if (offset < 0)
@@ -220,7 +358,11 @@ do_search_threads (const search_format_t *format,
     if (threads == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    if(format == unstructured_text_printer) {
+	fputs(format_text.results_start, stdout);
+    } else { /* structured output */
+	outermost_level = format->list(state);
+    }
 
     for (i = 0;
 	 notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
@@ -235,43 +377,92 @@ do_search_threads (const search_format_t *format,
 	    continue;
 	}
 
-	if (! first_thread)
-	    fputs (format->item_sep, stdout);
+	if (format == unstructured_text_printer && ! first_thread)
+	    fputs (format_text.item_sep, stdout);
 
 	if (output == OUTPUT_THREADS) {
-	    format->item_id (thread, "thread:",
-			     notmuch_thread_get_thread_id (thread));
+	    if(format == unstructured_text_printer) {
+		format_text.item_id (thread, "thread:",
+				     notmuch_thread_get_thread_id (thread));
+	    } else { /* structured output */
+		char buffer[128];
+		snprintf(buffer, 128, "thread:%s", notmuch_thread_get_thread_id (thread));
+		format->string(state, buffer);
+	    }
 	} else { /* output == OUTPUT_SUMMARY */
-	    fputs (format->item_start, stdout);
+	    int tags_level = 0;
+	    void *ctx = talloc_new (0);
+
+	    if(format == unstructured_text_printer) {
+		fputs (format_text.item_start, stdout);
+	    } else { /* structured output */
+		items_level = format->map(state);
+	    }
 
 	    if (sort == NOTMUCH_SORT_OLDEST_FIRST)
 		date = notmuch_thread_get_oldest_date (thread);
 	    else
 		date = notmuch_thread_get_newest_date (thread);
 
-	    format->thread_summary (thread,
-				    notmuch_thread_get_thread_id (thread),
-				    date,
-				    notmuch_thread_get_matched_messages (thread),
-				    notmuch_thread_get_total_messages (thread),
-				    notmuch_thread_get_authors (thread),
-				    notmuch_thread_get_subject (thread));
+	    if(format == unstructured_text_printer) {
+		format_text.thread_summary (thread,
+					    notmuch_thread_get_thread_id (thread),
+					    date,
+					    notmuch_thread_get_matched_messages (thread),
+					    notmuch_thread_get_total_messages (thread),
+					    notmuch_thread_get_authors (thread),
+					    notmuch_thread_get_subject (thread));
+	    } else { /* structured output */
+		format->map_key(state, "thread");
+		format->string(state, notmuch_thread_get_thread_id (thread));
+		format->map_key(state, "timestamp");
+		format->number(state, date);
+		format->map_key(state, "date_relative");
+		format->string(state, notmuch_time_relative_date(ctx, date));
+		format->map_key(state, "matched");
+		format->number(state, notmuch_thread_get_matched_messages(thread));
+		format->map_key(state, "total");
+		format->number(state, notmuch_thread_get_total_messages(thread));
+		format->map_key(state, "authors");
+		format->string(state, notmuch_thread_get_authors(thread));
+		format->map_key(state, "subject");
+		format->string(state, notmuch_thread_get_subject(thread));
+	    }
+
+	    if(format == unstructured_text_printer) {
+		fputs (format_text.tag_start, stdout);
+	    } else { /* structured output */
+		format->map_key(state, "tags");
+		tags_level = format->list(state);
+	    }
 
-	    fputs (format->tag_start, stdout);
 
 	    for (tags = notmuch_thread_get_tags (thread);
 		 notmuch_tags_valid (tags);
 		 notmuch_tags_move_to_next (tags))
 	    {
-		if (! first_tag)
-		    fputs (format->tag_sep, stdout);
-		printf (format->tag, notmuch_tags_get (tags));
+		if (format == unstructured_text_printer && ! first_tag) {
+		    fputs (format_text.tag_sep, stdout);
+		}
+		if(format == unstructured_text_printer) {
+		    printf (format_text.tag, notmuch_tags_get (tags));
+		} else { /* structured output */
+		    format->string(state, notmuch_tags_get(tags));
+		}
 		first_tag = 0;
 	    }
 
-	    fputs (format->tag_end, stdout);
+	    if(format == unstructured_text_printer) {
+		fputs (format_text.tag_end, stdout);
+	    } else { /* structured output */
+		format->pop(state, tags_level);
+	    }
 
-	    fputs (format->item_end, stdout);
+	    if(format == unstructured_text_printer) {
+		fputs (format_text.item_end, stdout);
+	    } else { /* structured output */
+		format->pop(state, items_level);
+	    }
 	}
 
 	first_thread = 0;
@@ -279,16 +470,21 @@ do_search_threads (const search_format_t *format,
 	notmuch_thread_destroy (thread);
     }
 
-    if (first_thread)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    if(format == unstructured_text_printer) {
+	if (first_thread)
+	    fputs (format_text.results_null, stdout);
+	else
+	    fputs (format_text.results_end, stdout);
+    } else { /* structured output */
+	format->pop(state, outermost_level);
+    }
 
     return 0;
 }
 
 static int
-do_search_messages (const search_format_t *format,
+do_search_messages (const structure_printer_t *format,
+		    void *state,
 		    notmuch_query_t *query,
 		    output_t output,
 		    int offset,
@@ -299,6 +495,7 @@ do_search_messages (const search_format_t *format,
     notmuch_filenames_t *filenames;
     int first_message = 1;
     int i;
+    int outermost_level = 0;
 
     if (offset < 0) {
 	offset += notmuch_query_count_messages (query);
@@ -310,7 +507,11 @@ do_search_messages (const search_format_t *format,
     if (messages == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    if(format == unstructured_text_printer) {
+	fputs (format_text.results_start, stdout);
+    } else { /* structured output */
+	outermost_level = format->list(state);
+    }
 
     for (i = 0;
 	 notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
@@ -328,23 +529,32 @@ do_search_messages (const search_format_t *format,
 		 notmuch_filenames_valid (filenames);
 		 notmuch_filenames_move_to_next (filenames))
 	    {
-		if (! first_message)
-		    fputs (format->item_sep, stdout);
+		if(format == unstructured_text_printer) {
+		    if (! first_message)
+			fputs (format_text.item_sep, stdout);
 
-		format->item_id (message, "",
-				 notmuch_filenames_get (filenames));
+		    format_text.item_id (message, "",
+					 notmuch_filenames_get (filenames));
+		} else { /* structured output */
+		format->string(state, notmuch_filenames_get (filenames));
+		}
 
 		first_message = 0;
 	    }
-	    
+
 	    notmuch_filenames_destroy( filenames );
 
 	} else { /* output == OUTPUT_MESSAGES */
-	    if (! first_message)
-		fputs (format->item_sep, stdout);
+	    if(format == unstructured_text_printer) {
+		if (! first_message)
+		    fputs (format_text.item_sep, stdout);
+
+		format_text.item_id (message, "id:",
+				     notmuch_message_get_message_id (message));
+	    } else { /* structured output */
+		format->string(state, notmuch_message_get_message_id (message));
+	    }
 
-	    format->item_id (message, "id:",
-			     notmuch_message_get_message_id (message));
 	    first_message = 0;
 	}
 
@@ -353,23 +563,29 @@ do_search_messages (const search_format_t *format,
 
     notmuch_messages_destroy (messages);
 
-    if (first_message)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    if(format == unstructured_text_printer) {
+	if (first_message)
+	    fputs (format_text.results_null, stdout);
+	else
+	    fputs (format_text.results_end, stdout);
+    } else { /* structured output */
+	format->pop(state, outermost_level);
+    }
 
     return 0;
 }
 
 static int
 do_search_tags (notmuch_database_t *notmuch,
-		const search_format_t *format,
+		const structure_printer_t *format,
+		void *state,
 		notmuch_query_t *query)
 {
     notmuch_messages_t *messages = NULL;
     notmuch_tags_t *tags;
     const char *tag;
     int first_tag = 1;
+    int outermost_level = 0;
 
     /* should the following only special case if no excluded terms
      * specified? */
@@ -387,7 +603,11 @@ do_search_tags (notmuch_database_t *notmuch,
     if (tags == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    if(format == unstructured_text_printer) {
+	fputs (format_text.results_start, stdout);
+    } else { /* structured output */
+	outermost_level = format->list(state);
+    }
 
     for (;
 	 notmuch_tags_valid (tags);
@@ -395,10 +615,14 @@ do_search_tags (notmuch_database_t *notmuch,
     {
 	tag = notmuch_tags_get (tags);
 
-	if (! first_tag)
-	    fputs (format->item_sep, stdout);
+	if(format == unstructured_text_printer) {
+	    if (! first_tag)
+		fputs (format_text.item_sep, stdout);
 
-	format->item_id (tags, "", tag);
+	    format_text.item_id (tags, "", tag);
+	} else { /* structured output */
+	    format->string(state, tag);
+	}
 
 	first_tag = 0;
     }
@@ -408,10 +632,14 @@ do_search_tags (notmuch_database_t *notmuch,
     if (messages)
 	notmuch_messages_destroy (messages);
 
-    if (first_tag)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    if(format == unstructured_text_printer) {
+	if (first_tag)
+	    fputs (format_text.results_null, stdout);
+	else
+	    fputs (format_text.results_end, stdout);
+    } else { /* structured output */
+	format->pop(state, outermost_level);
+    }
 
     return 0;
 }
@@ -430,7 +658,8 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_t *query;
     char *query_str;
     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
-    const search_format_t *format = &format_text;
+    const structure_printer_t *format = unstructured_text_printer;
+    void *state = NULL;
     int opt_index, ret;
     output_t output = OUTPUT_SUMMARY;
     int offset = 0;
@@ -457,11 +686,11 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 				  { "files", OUTPUT_FILES },
 				  { "tags", OUTPUT_TAGS },
 				  { 0, 0 } } },
-        { NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
-          (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE },
-                                  { "false", EXCLUDE_FALSE },
-                                  { "flag", EXCLUDE_FLAG },
-                                  { 0, 0 } } },
+	{ NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
+	  (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE },
+				  { "false", EXCLUDE_FALSE },
+				  { "flag", EXCLUDE_FLAG },
+				  { 0, 0 } } },
 	{ NOTMUCH_OPT_INT, &offset, "offset", 'O', 0 },
 	{ NOTMUCH_OPT_INT, &limit, "limit", 'L', 0  },
 	{ 0, 0, 0, 0, 0 }
@@ -475,10 +704,12 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 
     switch (format_sel) {
     case NOTMUCH_FORMAT_TEXT:
-	format = &format_text;
+	format = unstructured_text_printer;
+	state = 0;
 	break;
     case NOTMUCH_FORMAT_JSON:
-	format = &format_json;
+	format = &json_structure_printer;
+	state = format->initial_state(format, stdout);
 	break;
     }
 
@@ -532,14 +763,14 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     default:
     case OUTPUT_SUMMARY:
     case OUTPUT_THREADS:
-	ret = do_search_threads (format, query, sort, output, offset, limit);
+	ret = do_search_threads (format, state, query, sort, output, offset, limit);
 	break;
     case OUTPUT_MESSAGES:
     case OUTPUT_FILES:
-	ret = do_search_messages (format, query, output, offset, limit);
+	ret = do_search_messages (format, state, query, output, offset, limit);
 	break;
     case OUTPUT_TAGS:
-	ret = do_search_tags (notmuch, format, query);
+	ret = do_search_tags (notmuch, format, state, query);
 	break;
     }
 
-- 
1.7.11.1

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* Re: [PATCH] FIXED: Added better support for multiple structured output formats.
  2012-07-10 10:48 ` [PATCH] FIXED: " craven
  2012-07-10 12:45   ` Mark Walters
@ 2012-07-10 16:58   ` Jameson Graef Rollins
  1 sibling, 0 replies; 39+ messages in thread
From: Jameson Graef Rollins @ 2012-07-10 16:58 UTC (permalink / raw)
  To: craven, notmuch

[-- Attachment #1: Type: text/plain, Size: 1352 bytes --]

On Tue, Jul 10 2012, craven@gmx.net wrote:
> Sorry, the original patch I sent was missing a small part, here the full
> patch:
>
> As discussed in <id:20120121220407.GK16740@mit.edu>, this patch adds
> support for new structured output formats (like s-expressions) by using
> stateful structure_printers. An implementation of the JSON structure
> printer that passes all tests is included.
>
> Structure printers have functions for starting a map, a list, closing
> any number of these two, printing a map key, a number value, a string
> value, and boolean values. By threading a state through the
> invocations, arbitrary structured formatting can be achieved.
>
> In a second patch, the structured output code should be isolated in a
> separate file, and also used in all other parts of notmuch.

Hi, Peter.  Thanks for submitting this.

I would really like to see this patch broken up into multiple smaller
and more atomic patches.  Smaller patches are much easier to read and
review.  For instance, all the new formatting functions could be added
in their own patch (and already in their own file if that's
appropriate).  Then the existing json output could then be modified to
use the new formatters.

There is also json output in notmuch show and reply as well, so they
should probably also be modified to use the new formatter.

Thanks!

jamie.

[-- Attachment #2: Type: application/pgp-signature, Size: 835 bytes --]

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH] FIXED: Added better support for multiple structured output formats.
  2012-07-10 12:45   ` Mark Walters
  2012-07-10 13:30     ` [PATCH] v2: " craven
@ 2012-07-10 17:04     ` Jameson Graef Rollins
  2012-07-10 17:28       ` Austin Clements
  2012-07-10 22:45       ` deprecating legacy text output David Bremner
  1 sibling, 2 replies; 39+ messages in thread
From: Jameson Graef Rollins @ 2012-07-10 17:04 UTC (permalink / raw)
  To: Mark Walters, craven, notmuch

[-- Attachment #1: Type: text/plain, Size: 366 bytes --]

On Tue, Jul 10 2012, Mark Walters <markwalters1009@gmail.com> wrote:
> I think I would also add something saying that the text format is just
> different (and that a significant chunk of the patch is just that).

Can we not just dump this output format once and for all?  Does anything
use it?  And if so can we just modify it to use a more sensible format?

jamie.

[-- Attachment #2: Type: application/pgp-signature, Size: 835 bytes --]

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH] FIXED: Added better support for multiple structured output formats.
  2012-07-10 17:04     ` [PATCH] FIXED: Added better support for multiple structured output formats Jameson Graef Rollins
@ 2012-07-10 17:28       ` Austin Clements
  2012-07-10 17:40         ` Jameson Graef Rollins
  2012-07-10 22:45       ` deprecating legacy text output David Bremner
  1 sibling, 1 reply; 39+ messages in thread
From: Austin Clements @ 2012-07-10 17:28 UTC (permalink / raw)
  To: Jameson Graef Rollins; +Cc: notmuch

Quoth Jameson Graef Rollins on Jul 10 at 10:04 am:
> On Tue, Jul 10 2012, Mark Walters <markwalters1009@gmail.com> wrote:
> > I think I would also add something saying that the text format is just
> > different (and that a significant chunk of the patch is just that).
> 
> Can we not just dump this output format once and for all?  Does anything
> use it?  And if so can we just modify it to use a more sensible format?

Currently Emacs uses this output format (though
id:"1341870162-17782-1-git-send-email-amdragon@mit.edu" aims to fix
this).  Also, I assume most scripted uses of notmuch use the text
format (probably not the --output=summary text format, but certainly
files, messages, and tags).

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH] v2: Added better support for multiple structured output formats.
  2012-07-10 13:30     ` [PATCH] v2: " craven
@ 2012-07-10 17:34       ` Mark Walters
  2012-07-10 19:13       ` Austin Clements
  1 sibling, 0 replies; 39+ messages in thread
From: Mark Walters @ 2012-07-10 17:34 UTC (permalink / raw)
  To: craven, notmuch

On Tue, 10 Jul 2012, craven@gmx.net wrote:
> As discussed in <id:20120121220407.GK16740@mit.edu>, this patch adds
> support for new structured output formats (like s-expressions) by using
> stateful structure_printers. An implementation of the JSON structure
> printer that passes all tests is included. The output for JSON (and
> text) is identical to the current output. S-Expressions will be added in
> a later patch.
>

Hi I have some more comments inline below. I agree with Jamie that it
would be nice to split it into smaller chunks but I am not sure that is
practical to do.

> A large part of this patch just implements the differentiation between
> structured and non-structured output (all the code within 
> "if(format == unstructured_text_printer)").

I also agree with Jamie that the text output is annoying. However, at
least until Austin's async json parser goes in even the emacs interface
is using it so I think we have to live with it for a while (at the very
least we would need to give clients a reasonable time to migrate).

Anyway, I think this patch has to support it.

> In a second patch, the structured output code should be isolated in a
> separate file, and also used in all other parts of notmuch.
>
> The interface is a structure structure_printer, which contains the following methods:
>
> - initial_state: is called to create a state object, that is passed to all invocations. This should be used to keep track of the output file and everything else necessary to correctly format output.
> - map: is called when a new map (associative array, dictionary) is started. map_key and the primitives (string, number, bool) are used alternatingly to add key/value pairs. pop is used to close the map (see there). This function must return a nesting level identifier that can be used to close all nested structures (maps and lists), backing out to the returned nesting level.
> - list: is called when a new list (array, vector) is started. the primitives (string, number, bool) are used consecutively to add values to the list. pop is used to close the list. This function must return a nesting level identifier that can be used to close all nested structures (maps and lists), backing out to the returned nesting level.
> - pop: is called to return to a given nesting level. All lists and maps with a deeper nesting level must be closed.
> - number, string, bool: output one element of the specific type.
>
> All functions should use the state object to insert delimiters etc. automatically when appropriate.
>
> Example:
> int top, one;
> top = map(state);
> map_key(state, "foo");
> one = list(state);
> number(state, 1);
> number(state, 2);
> number(state, 3);
> pop(state, i);
> map_key(state, "bar");
> map(state);
> map_key(state, "baaz");
> string(state, "hello world");
> pop(state, top);
>
> would output JSON as follows:
>
> {"foo": [1, 2, 3], "bar": { "baaz": "hello world"}}
> ---
>  notmuch-search.c | 491 ++++++++++++++++++++++++++++++++++++++++---------------
>  1 file changed, 361 insertions(+), 130 deletions(-)
>
> diff --git a/notmuch-search.c b/notmuch-search.c
> index 3be296d..4127777 100644
> --- a/notmuch-search.c
> +++ b/notmuch-search.c
> @@ -28,6 +28,210 @@ typedef enum {
>      OUTPUT_TAGS
>  } output_t;
>  
> +/* structured formatting, useful for JSON, S-Expressions, ...
> +
> +- initial_state: is called to create a state object, that is passed to all invocations. This should be used to keep track of the output file and everything else necessary to correctly format output.
> +- map: is called when a new map (associative array, dictionary) is started. map_key and the primitives (string, number, bool) are used alternatingly to add key/value pairs. pop is used to close the map (see there). This function must return a nesting level identifier that can be used to close all nested structures (maps and lists), backing out to the returned nesting level.
> +- list: is called when a new list (array, vector) is started. the primitives (string, number, bool) are used consecutively to add values to the list. pop is used to close the list. This function must return a nesting level identifier that can be used to close all nested structures (maps and lists), backing out to the returned nesting level.
> +- pop: is called to return to a given nesting level. All lists and maps with a deeper nesting level must be closed.
> +- number, string, bool: output one element of the specific type.
> +
> +All functions should use state to insert delimiters etc. automatically when appropriate.
> +
> +Example:
> +int top, one;
> +top = map(state);
> +map_key(state, "foo");
> +one = list(state);
> +number(state, 1);
> +number(state, 2);
> +number(state, 3);
> +pop(state, i);

I think `i` should be `one` or vice versa (and the same in the commit message)? 

> +map_key(state, "bar");
> +map(state);
> +map_key(state, "baaz");
> +string(state, "hello world");
> +pop(state, top);
> +
> +would output JSON as follows:
> +
> +{"foo": [1, 2, 3], "bar": { "baaz": "hello world"}}
> +
> + */
> +typedef struct structure_printer {
> +    int (*map)(void *state);
> +    int (*list)(void *state);
> +    void (*pop)(void *state, int level);
> +    void (*map_key)(void *state, const char *key);
> +    void (*number)(void *state, int val);
> +    void (*string)(void *state, const char *val);
> +    void (*bool)(void *state, notmuch_bool_t val);
> +    void *(*initial_state)(const struct structure_printer *sp, FILE *output);
> +} structure_printer_t;
> +
> +/* JSON structure printer */
> +
> +/* single linked list implementation for keeping track of the array/map nesting state */
> +typedef struct json_list {
> +    int type;
> +    int first_already_seen;
> +    struct json_list *rest;
> +} json_list_t;
> +
> +#define TYPE_JSON_MAP 1
> +#define TYPE_JSON_ARRAY 2
> +
> +typedef struct json_state {
> +    FILE *output;
> +    json_list_t *stack;
> +    int level;
> +} json_state_t;
> +
> +int json_map(void *state);
> +int json_list(void *state);
> +void json_pop(void *state, int level);
> +void json_map_key(void *state, const char *key);
> +void json_number(void *state, int val);
> +void json_string(void *state, const char *val);
> +void json_bool(void *state, notmuch_bool_t val);
> +void *json_initial_state(const struct structure_printer *sp, FILE *output);
> +
> +structure_printer_t json_structure_printer = {
> +    &json_map,
> +    &json_list,
> +    &json_pop,
> +    &json_map_key,
> +    &json_number,
> +    &json_string,
> +    &json_bool,
> +    &json_initial_state
> +};
> +
> +int json_map(void *st) {

I think these should be of the form (see devel/STYLE)

int
json_map(void *st) 
{

> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
> +	fputs(",", output);
> +	if(state->level == 1)
> +	    fputs("\n", output);
> +	else
> +	    fputs(" ", output);
> +    }
> +    if(state->stack != NULL) {
> +	state->stack->first_already_seen = TRUE;
> +    }
> +    fputs("{", output);
> +    void *ctx_json_map = talloc_new (0);
> +    json_list_t *el = talloc(ctx_json_map, json_list_t);
> +    el->type = TYPE_JSON_MAP;
> +    el->first_already_seen = FALSE;
> +    el->rest = state->stack;
> +    state->stack = el;
> +    return state->level++;
> +}
> +
> +int json_list(void *st) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
> +	fputs(",", output);
> +	if(state->level == 1)
> +	    fputs("\n", output);
> +	else
> +	    fputs(" ", output);
> +    }
> +    if(state->stack != NULL) {
> +	state->stack->first_already_seen = TRUE;
> +    }
> +    fputs("[", output);
> +    void *ctx_json_map = talloc_new (0);
> +    json_list_t *el = talloc(ctx_json_map, json_list_t);
> +    el->type = TYPE_JSON_ARRAY;
> +    el->first_already_seen = FALSE;
> +    el->rest = state->stack;
> +    state->stack = el;
> +    return state->level++;
> +}
> +
> +void json_pop(void *st, int level) {
> +    int i;
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    for(i = state->level; i > level; i--) {
> +	json_list_t *tos = state->stack;
> +	if(tos->type == TYPE_JSON_MAP) {
> +	    fputs("}", output);
> +	}
> +	if(tos->type == TYPE_JSON_ARRAY) {
> +	    fputs("]", output);
> +	}
> +	state->stack = tos->rest;
> +	state->level--;
> +	talloc_free(tos);
> +    }
> +    if(state->level == 0)
> +	fputs("\n", output);
> +}
> +
> +void json_map_key(void *st, const char *key) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(state->stack != NULL && state->stack->first_already_seen) {
> +	fputs(",\n", output);
> +    }
> +    fputs("\"", output);
> +    fputs(key, output);
> +    fputs("\": ", output);
> +}
> +
> +void json_number(void *st, int val) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
> +	fputs(", ", output);
> +    }
> +    state->stack->first_already_seen = TRUE;
> +    fprintf(output, "%i", val);
> +}
> +
> +void json_string(void *st, const char *val) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    void *ctx = talloc_new(0);
> +    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
> +	fputs(",", output);
> +	if(state->level == 1)
> +	    fputs("\n", output);
> +	else
> +	    fputs(" ", output);
> +    }
> +
> +    state->stack->first_already_seen = TRUE;
> +    fprintf(output, "%s", json_quote_str(ctx, val));
> +    talloc_free(ctx);
> +}
> +
> +void json_bool(void *st, notmuch_bool_t val) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(val)
> +	fputs("true", output);
> +    else
> +	fputs("false", output);
> +}

These three functions are all different but I feel they should all be
the same. We may not have arrays of booleans but that looks like it
would be legal JSON. This might suggest that it is worth extracting a
small helper function to do the

if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
	fputs(",", output);
	if(state->level == 1)
	    fputs("\n", output);
	else
	    fputs(" ", output);
    }

bit.


> +
> +void *json_initial_state(const struct structure_printer *sp, FILE *output) {
> +    (void)sp;
> +    json_state_t *st = talloc(0, json_state_t);
> +    st->level = 0;
> +    st->stack = NULL;
> +    st->output = output;
> +    return st;
> +}
> +
> +structure_printer_t *unstructured_text_printer = NULL;
> +
> +/* legacy, only needed for non-structured text output */
>  typedef struct search_format {
>      const char *results_start;
>      const char *item_start;
> @@ -51,6 +255,7 @@ typedef struct search_format {
>      const char *results_null;
>  } search_format_t;
>  
> +
>  static void
>  format_item_id_text (const void *ctx,
>  		     const char *item_type,
> @@ -64,6 +269,7 @@ format_thread_text (const void *ctx,
>  		    const int total,
>  		    const char *authors,
>  		    const char *subject);
> +
>  static const search_format_t format_text = {
>      "",
>  	"",
> @@ -78,35 +284,6 @@ static const search_format_t format_text = {
>  };
>  
>  static void
> -format_item_id_json (const void *ctx,
> -		     const char *item_type,
> -		     const char *item_id);
> -
> -static void
> -format_thread_json (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject);
> -
> -/* Any changes to the JSON format should be reflected in the file
> - * devel/schemata. */
> -static const search_format_t format_json = {
> -    "[",
> -	"{",
> -	    format_item_id_json,
> -	    format_thread_json,
> -	    "\"tags\": [",
> -		"\"%s\"", ", ",
> -	    "]", ",\n",
> -	"}",
> -    "]\n",
> -    "]\n",
> -};
> -
> -static void
>  format_item_id_text (unused (const void *ctx),
>  		     const char *item_type,
>  		     const char *item_id)
> @@ -153,50 +330,9 @@ format_thread_text (const void *ctx,
>      talloc_free (ctx_quote);
>  }
>  
> -static void
> -format_item_id_json (const void *ctx,
> -		     unused (const char *item_type),
> -		     const char *item_id)
> -{
> -    void *ctx_quote = talloc_new (ctx);
> -
> -    printf ("%s", json_quote_str (ctx_quote, item_id));
> -
> -    talloc_free (ctx_quote);
> -    
> -}
> -
> -static void
> -format_thread_json (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject)
> -{
> -    void *ctx_quote = talloc_new (ctx);
> -
> -    printf ("\"thread\": %s,\n"
> -	    "\"timestamp\": %ld,\n"
> -	    "\"date_relative\": \"%s\",\n"
> -	    "\"matched\": %d,\n"
> -	    "\"total\": %d,\n"
> -	    "\"authors\": %s,\n"
> -	    "\"subject\": %s,\n",
> -	    json_quote_str (ctx_quote, thread_id),
> -	    date,
> -	    notmuch_time_relative_date (ctx, date),
> -	    matched,
> -	    total,
> -	    json_quote_str (ctx_quote, authors),
> -	    json_quote_str (ctx_quote, subject));
> -
> -    talloc_free (ctx_quote);
> -}
> -
>  static int
> -do_search_threads (const search_format_t *format,
> +do_search_threads (const structure_printer_t *format,
> +		   void *state,
>  		   notmuch_query_t *query,
>  		   notmuch_sort_t sort,
>  		   output_t output,
> @@ -210,6 +346,8 @@ do_search_threads (const search_format_t *format,
>      int first_thread = 1;
>      int i;
>  
> +    int outermost_level = 0;
> +    int items_level = 0;
>      if (offset < 0) {
>  	offset += notmuch_query_count_threads (query);
>  	if (offset < 0)
> @@ -220,7 +358,11 @@ do_search_threads (const search_format_t *format,
>      if (threads == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    if(format == unstructured_text_printer) {
> +	fputs(format_text.results_start, stdout);
> +    } else { /* structured output */
> +	outermost_level = format->list(state);
> +    }
>  
>      for (i = 0;
>  	 notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
> @@ -235,43 +377,92 @@ do_search_threads (const search_format_t *format,
>  	    continue;
>  	}
>  
> -	if (! first_thread)
> -	    fputs (format->item_sep, stdout);
> +	if (format == unstructured_text_printer && ! first_thread)
> +	    fputs (format_text.item_sep, stdout);
>  
>  	if (output == OUTPUT_THREADS) {
> -	    format->item_id (thread, "thread:",
> -			     notmuch_thread_get_thread_id (thread));
> +	    if(format == unstructured_text_printer) {
> +		format_text.item_id (thread, "thread:",
> +				     notmuch_thread_get_thread_id (thread));
> +	    } else { /* structured output */
> +		char buffer[128];
> +		snprintf(buffer, 128, "thread:%s", notmuch_thread_get_thread_id (thread));
> +		format->string(state, buffer);

As mentioned on irc I think you don't need the thread: bit for the
current JSON output in this case.

Best wishes

Mark


>  	} else { /* output == OUTPUT_SUMMARY */
> -	    fputs (format->item_start, stdout);
> +	    int tags_level = 0;
> +	    void *ctx = talloc_new (0);
> +
> +	    if(format == unstructured_text_printer) {
> +		fputs (format_text.item_start, stdout);
> +	    } else { /* structured output */
> +		items_level = format->map(state);
> +	    }
>  
>  	    if (sort == NOTMUCH_SORT_OLDEST_FIRST)
>  		date = notmuch_thread_get_oldest_date (thread);
>  	    else
>  		date = notmuch_thread_get_newest_date (thread);
>  
> -	    format->thread_summary (thread,
> -				    notmuch_thread_get_thread_id (thread),
> -				    date,
> -				    notmuch_thread_get_matched_messages (thread),
> -				    notmuch_thread_get_total_messages (thread),
> -				    notmuch_thread_get_authors (thread),
> -				    notmuch_thread_get_subject (thread));
> +	    if(format == unstructured_text_printer) {
> +		format_text.thread_summary (thread,
> +					    notmuch_thread_get_thread_id (thread),
> +					    date,
> +					    notmuch_thread_get_matched_messages (thread),
> +					    notmuch_thread_get_total_messages (thread),
> +					    notmuch_thread_get_authors (thread),
> +					    notmuch_thread_get_subject (thread));
> +	    } else { /* structured output */
> +		format->map_key(state, "thread");
> +		format->string(state, notmuch_thread_get_thread_id (thread));
> +		format->map_key(state, "timestamp");
> +		format->number(state, date);
> +		format->map_key(state, "date_relative");
> +		format->string(state, notmuch_time_relative_date(ctx, date));
> +		format->map_key(state, "matched");
> +		format->number(state, notmuch_thread_get_matched_messages(thread));
> +		format->map_key(state, "total");
> +		format->number(state, notmuch_thread_get_total_messages(thread));
> +		format->map_key(state, "authors");
> +		format->string(state, notmuch_thread_get_authors(thread));
> +		format->map_key(state, "subject");
> +		format->string(state, notmuch_thread_get_subject(thread));
> +	    }
> +
> +	    if(format == unstructured_text_printer) {
> +		fputs (format_text.tag_start, stdout);
> +	    } else { /* structured output */
> +		format->map_key(state, "tags");
> +		tags_level = format->list(state);
> +	    }
>  
> -	    fputs (format->tag_start, stdout);
>  
>  	    for (tags = notmuch_thread_get_tags (thread);
>  		 notmuch_tags_valid (tags);
>  		 notmuch_tags_move_to_next (tags))
>  	    {
> -		if (! first_tag)
> -		    fputs (format->tag_sep, stdout);
> -		printf (format->tag, notmuch_tags_get (tags));
> +		if (format == unstructured_text_printer && ! first_tag) {
> +		    fputs (format_text.tag_sep, stdout);
> +		}
> +		if(format == unstructured_text_printer) {
> +		    printf (format_text.tag, notmuch_tags_get (tags));
> +		} else { /* structured output */
> +		    format->string(state, notmuch_tags_get(tags));
> +		}
>  		first_tag = 0;
>  	    }
>  
> -	    fputs (format->tag_end, stdout);
> +	    if(format == unstructured_text_printer) {
> +		fputs (format_text.tag_end, stdout);
> +	    } else { /* structured output */
> +		format->pop(state, tags_level);
> +	    }
>  
> -	    fputs (format->item_end, stdout);
> +	    if(format == unstructured_text_printer) {
> +		fputs (format_text.item_end, stdout);
> +	    } else { /* structured output */
> +		format->pop(state, items_level);
> +	    }
>  	}
>  
>  	first_thread = 0;
> @@ -279,16 +470,21 @@ do_search_threads (const search_format_t *format,
>  	notmuch_thread_destroy (thread);
>      }
>  
> -    if (first_thread)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    if(format == unstructured_text_printer) {
> +	if (first_thread)
> +	    fputs (format_text.results_null, stdout);
> +	else
> +	    fputs (format_text.results_end, stdout);
> +    } else { /* structured output */
> +	format->pop(state, outermost_level);
> +    }
>  
>      return 0;
>  }
>  
>  static int
> -do_search_messages (const search_format_t *format,
> +do_search_messages (const structure_printer_t *format,
> +		    void *state,
>  		    notmuch_query_t *query,
>  		    output_t output,
>  		    int offset,
> @@ -299,6 +495,7 @@ do_search_messages (const search_format_t *format,
>      notmuch_filenames_t *filenames;
>      int first_message = 1;
>      int i;
> +    int outermost_level = 0;
>  
>      if (offset < 0) {
>  	offset += notmuch_query_count_messages (query);
> @@ -310,7 +507,11 @@ do_search_messages (const search_format_t *format,
>      if (messages == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    if(format == unstructured_text_printer) {
> +	fputs (format_text.results_start, stdout);
> +    } else { /* structured output */
> +	outermost_level = format->list(state);
> +    }
>  
>      for (i = 0;
>  	 notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
> @@ -328,23 +529,32 @@ do_search_messages (const search_format_t *format,
>  		 notmuch_filenames_valid (filenames);
>  		 notmuch_filenames_move_to_next (filenames))
>  	    {
> -		if (! first_message)
> -		    fputs (format->item_sep, stdout);
> +		if(format == unstructured_text_printer) {
> +		    if (! first_message)
> +			fputs (format_text.item_sep, stdout);
>  
> -		format->item_id (message, "",
> -				 notmuch_filenames_get (filenames));
> +		    format_text.item_id (message, "",
> +					 notmuch_filenames_get (filenames));
> +		} else { /* structured output */
> +		format->string(state, notmuch_filenames_get (filenames));
> +		}
>  
>  		first_message = 0;
>  	    }
> -	    
> +
>  	    notmuch_filenames_destroy( filenames );
>  
>  	} else { /* output == OUTPUT_MESSAGES */
> -	    if (! first_message)
> -		fputs (format->item_sep, stdout);
> +	    if(format == unstructured_text_printer) {
> +		if (! first_message)
> +		    fputs (format_text.item_sep, stdout);
> +
> +		format_text.item_id (message, "id:",
> +				     notmuch_message_get_message_id (message));
> +	    } else { /* structured output */
> +		format->string(state, notmuch_message_get_message_id (message));
> +	    }
>  
> -	    format->item_id (message, "id:",
> -			     notmuch_message_get_message_id (message));
>  	    first_message = 0;
>  	}
>  
> @@ -353,23 +563,29 @@ do_search_messages (const search_format_t *format,
>  
>      notmuch_messages_destroy (messages);
>  
> -    if (first_message)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    if(format == unstructured_text_printer) {
> +	if (first_message)
> +	    fputs (format_text.results_null, stdout);
> +	else
> +	    fputs (format_text.results_end, stdout);
> +    } else { /* structured output */
> +	format->pop(state, outermost_level);
> +    }
>  
>      return 0;
>  }
>  
>  static int
>  do_search_tags (notmuch_database_t *notmuch,
> -		const search_format_t *format,
> +		const structure_printer_t *format,
> +		void *state,
>  		notmuch_query_t *query)
>  {
>      notmuch_messages_t *messages = NULL;
>      notmuch_tags_t *tags;
>      const char *tag;
>      int first_tag = 1;
> +    int outermost_level = 0;
>  
>      /* should the following only special case if no excluded terms
>       * specified? */
> @@ -387,7 +603,11 @@ do_search_tags (notmuch_database_t *notmuch,
>      if (tags == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    if(format == unstructured_text_printer) {
> +	fputs (format_text.results_start, stdout);
> +    } else { /* structured output */
> +	outermost_level = format->list(state);
> +    }
>  
>      for (;
>  	 notmuch_tags_valid (tags);
> @@ -395,10 +615,14 @@ do_search_tags (notmuch_database_t *notmuch,
>      {
>  	tag = notmuch_tags_get (tags);
>  
> -	if (! first_tag)
> -	    fputs (format->item_sep, stdout);
> +	if(format == unstructured_text_printer) {
> +	    if (! first_tag)
> +		fputs (format_text.item_sep, stdout);
>  
> -	format->item_id (tags, "", tag);
> +	    format_text.item_id (tags, "", tag);
> +	} else { /* structured output */
> +	    format->string(state, tag);
> +	}
>  
>  	first_tag = 0;
>      }
> @@ -408,10 +632,14 @@ do_search_tags (notmuch_database_t *notmuch,
>      if (messages)
>  	notmuch_messages_destroy (messages);
>  
> -    if (first_tag)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    if(format == unstructured_text_printer) {
> +	if (first_tag)
> +	    fputs (format_text.results_null, stdout);
> +	else
> +	    fputs (format_text.results_end, stdout);
> +    } else { /* structured output */
> +	format->pop(state, outermost_level);
> +    }
>  
>      return 0;
>  }
> @@ -430,7 +658,8 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>      notmuch_query_t *query;
>      char *query_str;
>      notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
> -    const search_format_t *format = &format_text;
> +    const structure_printer_t *format = unstructured_text_printer;
> +    void *state = NULL;
>      int opt_index, ret;
>      output_t output = OUTPUT_SUMMARY;
>      int offset = 0;
> @@ -457,11 +686,11 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>  				  { "files", OUTPUT_FILES },
>  				  { "tags", OUTPUT_TAGS },
>  				  { 0, 0 } } },
> -        { NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
> -          (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE },
> -                                  { "false", EXCLUDE_FALSE },
> -                                  { "flag", EXCLUDE_FLAG },
> -                                  { 0, 0 } } },
> +	{ NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
> +	  (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE },
> +				  { "false", EXCLUDE_FALSE },
> +				  { "flag", EXCLUDE_FLAG },
> +				  { 0, 0 } } },
>  	{ NOTMUCH_OPT_INT, &offset, "offset", 'O', 0 },
>  	{ NOTMUCH_OPT_INT, &limit, "limit", 'L', 0  },
>  	{ 0, 0, 0, 0, 0 }
> @@ -475,10 +704,12 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>  
>      switch (format_sel) {
>      case NOTMUCH_FORMAT_TEXT:
> -	format = &format_text;
> +	format = unstructured_text_printer;
> +	state = 0;
>  	break;
>      case NOTMUCH_FORMAT_JSON:
> -	format = &format_json;
> +	format = &json_structure_printer;
> +	state = format->initial_state(format, stdout);
>  	break;
>      }
>  
> @@ -532,14 +763,14 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>      default:
>      case OUTPUT_SUMMARY:
>      case OUTPUT_THREADS:
> -	ret = do_search_threads (format, query, sort, output, offset, limit);
> +	ret = do_search_threads (format, state, query, sort, output, offset, limit);
>  	break;
>      case OUTPUT_MESSAGES:
>      case OUTPUT_FILES:
> -	ret = do_search_messages (format, query, output, offset, limit);
> +	ret = do_search_messages (format, state, query, output, offset, limit);
>  	break;
>      case OUTPUT_TAGS:
> -	ret = do_search_tags (notmuch, format, query);
> +	ret = do_search_tags (notmuch, format, state, query);
>  	break;
>      }
>  
> -- 
> 1.7.11.1
>
> _______________________________________________
> notmuch mailing list
> notmuch@notmuchmail.org
> http://notmuchmail.org/mailman/listinfo/notmuch

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH] FIXED: Added better support for multiple structured output formats.
  2012-07-10 17:28       ` Austin Clements
@ 2012-07-10 17:40         ` Jameson Graef Rollins
  0 siblings, 0 replies; 39+ messages in thread
From: Jameson Graef Rollins @ 2012-07-10 17:40 UTC (permalink / raw)
  To: Austin Clements; +Cc: notmuch

[-- Attachment #1: Type: text/plain, Size: 953 bytes --]

On Tue, Jul 10 2012, Austin Clements <amdragon@MIT.EDU> wrote:
> Quoth Jameson Graef Rollins on Jul 10 at 10:04 am:
>> On Tue, Jul 10 2012, Mark Walters <markwalters1009@gmail.com> wrote:
>> > I think I would also add something saying that the text format is just
>> > different (and that a significant chunk of the patch is just that).
>> 
>> Can we not just dump this output format once and for all?  Does anything
>> use it?  And if so can we just modify it to use a more sensible format?
>
> Currently Emacs uses this output format (though
> id:"1341870162-17782-1-git-send-email-amdragon@mit.edu" aims to fix
> this).  Also, I assume most scripted uses of notmuch use the text
> format (probably not the --output=summary text format, but certainly
> files, messages, and tags).

Sorry, in a momentary brain freeze I confused the search and show text
output.  You're right that the text search output is more widely used.

jamie.

[-- Attachment #2: Type: application/pgp-signature, Size: 835 bytes --]

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH] v2: Added better support for multiple structured output formats.
  2012-07-10 13:30     ` [PATCH] v2: " craven
  2012-07-10 17:34       ` Mark Walters
@ 2012-07-10 19:13       ` Austin Clements
  2012-07-11  8:26         ` [PATCH v3 0/3] Structured Formatters craven
  2012-07-13  8:11         ` [PATCH v5 0/3] notmuch-reply: Structured Formatters Peter Feigl
  1 sibling, 2 replies; 39+ messages in thread
From: Austin Clements @ 2012-07-10 19:13 UTC (permalink / raw)
  To: craven; +Cc: notmuch

Since it would be great to use the structure printer for show as well,
it would make sense to put it in its own source file, where it can
easily be shared between commands.

There are a few systematic code formatting problems in your patch.  To
be consistent with other notmuch code, there should be a space between
function names and parameter lists (in both declarations and calls)
and there should be a space between a keyword and a paren (e.g., "if
(" and "for (").  In a function definition, the function name should
start on a new line (this makes it easy to grep for a function's
definition).  Also, in general, try to keep lines under 80 characters
wide (we're not very consistent about this one, but it would be nice
to not become even less consistent about it).

More detailed comments below.

Quoth craven@gmx.net on Jul 10 at  3:30 pm:
> As discussed in <id:20120121220407.GK16740@mit.edu>, this patch adds
> support for new structured output formats (like s-expressions) by using
> stateful structure_printers. An implementation of the JSON structure
> printer that passes all tests is included. The output for JSON (and
> text) is identical to the current output. S-Expressions will be added in
> a later patch.
> 
> A large part of this patch just implements the differentiation between
> structured and non-structured output (all the code within 
> "if(format == unstructured_text_printer)").
> 
> In a second patch, the structured output code should be isolated in a
> separate file, and also used in all other parts of notmuch.
> 
> The interface is a structure structure_printer, which contains the following methods:
> 
> - initial_state: is called to create a state object, that is passed to all invocations. This should be used to keep track of the output file and everything else necessary to correctly format output.
> - map: is called when a new map (associative array, dictionary) is started. map_key and the primitives (string, number, bool) are used alternatingly to add key/value pairs. pop is used to close the map (see there). This function must return a nesting level identifier that can be used to close all nested structures (maps and lists), backing out to the returned nesting level.
> - list: is called when a new list (array, vector) is started. the primitives (string, number, bool) are used consecutively to add values to the list. pop is used to close the list. This function must return a nesting level identifier that can be used to close all nested structures (maps and lists), backing out to the returned nesting level.
> - pop: is called to return to a given nesting level. All lists and maps with a deeper nesting level must be closed.
> - number, string, bool: output one element of the specific type.
> 
> All functions should use the state object to insert delimiters etc. automatically when appropriate.
> 
> Example:
> int top, one;
> top = map(state);
> map_key(state, "foo");
> one = list(state);
> number(state, 1);
> number(state, 2);
> number(state, 3);
> pop(state, i);
> map_key(state, "bar");
> map(state);
> map_key(state, "baaz");
> string(state, "hello world");
> pop(state, top);
> 
> would output JSON as follows:
> 
> {"foo": [1, 2, 3], "bar": { "baaz": "hello world"}}
> ---
>  notmuch-search.c | 491 ++++++++++++++++++++++++++++++++++++++++---------------
>  1 file changed, 361 insertions(+), 130 deletions(-)
> 
> diff --git a/notmuch-search.c b/notmuch-search.c
> index 3be296d..4127777 100644
> --- a/notmuch-search.c
> +++ b/notmuch-search.c
> @@ -28,6 +28,210 @@ typedef enum {
>      OUTPUT_TAGS
>  } output_t;
>  
> +/* structured formatting, useful for JSON, S-Expressions, ...
> +
> +- initial_state: is called to create a state object, that is passed to all invocations. This should be used to keep track of the output file and everything else necessary to correctly format output.
> +- map: is called when a new map (associative array, dictionary) is started. map_key and the primitives (string, number, bool) are used alternatingly to add key/value pairs. pop is used to close the map (see there). This function must return a nesting level identifier that can be used to close all nested structures (maps and lists), backing out to the returned nesting level.
> +- list: is called when a new list (array, vector) is started. the primitives (string, number, bool) are used consecutively to add values to the list. pop is used to close the list. This function must return a nesting level identifier that can be used to close all nested structures (maps and lists), backing out to the returned nesting level.
> +- pop: is called to return to a given nesting level. All lists and maps with a deeper nesting level must be closed.
> +- number, string, bool: output one element of the specific type.
> +
> +All functions should use state to insert delimiters etc. automatically when appropriate.
> +
> +Example:
> +int top, one;
> +top = map(state);
> +map_key(state, "foo");
> +one = list(state);
> +number(state, 1);
> +number(state, 2);
> +number(state, 3);
> +pop(state, i);
> +map_key(state, "bar");
> +map(state);
> +map_key(state, "baaz");
> +string(state, "hello world");
> +pop(state, top);
> +
> +would output JSON as follows:
> +
> +{"foo": [1, 2, 3], "bar": { "baaz": "hello world"}}
> +
> + */

The above comment should follow the style of other notmuch comments:

/* Put a star before every line, use full sentences starting with a
 * capital letter, and wrap the comment at 72 columns.
 */

For the descriptions of the function pointers, it probably makes sense
to comment them inline in the struct itself so the reader doesn't have
to match things up.  E.g.,

typedef struct structure_printer {
    /* (Description of map.) */
    int (*map) (void *state);
    ...
} structure_printer_t;

> +typedef struct structure_printer {
> +    int (*map)(void *state);

This is a rather less obvious application of notmuch code style, but
there should be a space before the parameter list here, too:
    int (*map) (void *state);
Likewise for the other fields, of course.

> +    int (*list)(void *state);
> +    void (*pop)(void *state, int level);
> +    void (*map_key)(void *state, const char *key);
> +    void (*number)(void *state, int val);
> +    void (*string)(void *state, const char *val);
> +    void (*bool)(void *state, notmuch_bool_t val);
> +    void *(*initial_state)(const struct structure_printer *sp, FILE *output);
> +} structure_printer_t;

Is there a reason to separate the structure printer's vtable from its
state?  It seems like this forces the caller to keep track of two
things instead of just one.  For show this isn't too bad because the
structure printer is the formatter, but I think it will be cumbersome
for the search code.  Keeping them together would also be at least
nominally more typesafe.

> +
> +/* JSON structure printer */
> +
> +/* single linked list implementation for keeping track of the array/map nesting state */

Same comment formatting comment here.

> +typedef struct json_list {
> +    int type;
> +    int first_already_seen;

Maybe just "first_seen"?  The "already" doesn't add anything.  Also,
this should probably be a notmuch_bool_t.

> +    struct json_list *rest;
> +} json_list_t;
> +
> +#define TYPE_JSON_MAP 1
> +#define TYPE_JSON_ARRAY 2

An enum would be preferable here.

> +
> +typedef struct json_state {
> +    FILE *output;
> +    json_list_t *stack;
> +    int level;
> +} json_state_t;
> +
> +int json_map(void *state);
> +int json_list(void *state);
> +void json_pop(void *state, int level);
> +void json_map_key(void *state, const char *key);
> +void json_number(void *state, int val);
> +void json_string(void *state, const char *val);
> +void json_bool(void *state, notmuch_bool_t val);
> +void *json_initial_state(const struct structure_printer *sp, FILE *output);
> +
> +structure_printer_t json_structure_printer = {
> +    &json_map,
> +    &json_list,
> +    &json_pop,
> +    &json_map_key,
> +    &json_number,
> +    &json_string,
> +    &json_bool,
> +    &json_initial_state
> +};
> +
> +int json_map(void *st) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
> +	fputs(",", output);
> +	if(state->level == 1)
> +	    fputs("\n", output);
> +	else
> +	    fputs(" ", output);
> +    }
> +    if(state->stack != NULL) {
> +	state->stack->first_already_seen = TRUE;
> +    }

You repeat the above code (or something very similar) in several
places.  It would make sense to put it in a separate function,
possibly with an additional char argument to control whether to follow
the comma with a newline or a space.

> +    fputs("{", output);
> +    void *ctx_json_map = talloc_new (0);
> +    json_list_t *el = talloc(ctx_json_map, json_list_t);

You're leaking ctx_json_map here.  It's also not necessary: you should
use st as the talloc context for el.  It still makes sense to free
these as you pop, but that way if the caller needs to abort it can
free the whole state structure in one swoop.

> +    el->type = TYPE_JSON_MAP;
> +    el->first_already_seen = FALSE;
> +    el->rest = state->stack;
> +    state->stack = el;
> +    return state->level++;
> +}
> +
> +int json_list(void *st) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
> +	fputs(",", output);
> +	if(state->level == 1)
> +	    fputs("\n", output);
> +	else
> +	    fputs(" ", output);
> +    }
> +    if(state->stack != NULL) {
> +	state->stack->first_already_seen = TRUE;
> +    }
> +    fputs("[", output);
> +    void *ctx_json_map = talloc_new (0);
> +    json_list_t *el = talloc(ctx_json_map, json_list_t);
> +    el->type = TYPE_JSON_ARRAY;
> +    el->first_already_seen = FALSE;
> +    el->rest = state->stack;
> +    state->stack = el;
> +    return state->level++;
> +}
> +
> +void json_pop(void *st, int level) {
> +    int i;
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    for(i = state->level; i > level; i--) {

Maybe "while (state->level > level)"?

> +	json_list_t *tos = state->stack;
> +	if(tos->type == TYPE_JSON_MAP) {
> +	    fputs("}", output);
> +	}
> +	if(tos->type == TYPE_JSON_ARRAY) {

Maybe "else if"?

> +	    fputs("]", output);
> +	}
> +	state->stack = tos->rest;
> +	state->level--;
> +	talloc_free(tos);
> +    }
> +    if(state->level == 0)
> +	fputs("\n", output);
> +}
> +
> +void json_map_key(void *st, const char *key) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(state->stack != NULL && state->stack->first_already_seen) {
> +	fputs(",\n", output);
> +    }
> +    fputs("\"", output);
> +    fputs(key, output);

The key needs to be escaped like any JSON string.

> +    fputs("\": ", output);
> +}
> +
> +void json_number(void *st, int val) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
> +	fputs(", ", output);
> +    }
> +    state->stack->first_already_seen = TRUE;
> +    fprintf(output, "%i", val);

"%d" is much more common (I had to look up %i!)

> +}
> +
> +void json_string(void *st, const char *val) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;
> +    void *ctx = talloc_new(0);
> +    if(state->stack != NULL && state->stack->type == TYPE_JSON_ARRAY && state->stack->first_already_seen) {
> +	fputs(",", output);
> +	if(state->level == 1)
> +	    fputs("\n", output);
> +	else
> +	    fputs(" ", output);
> +    }
> +
> +    state->stack->first_already_seen = TRUE;
> +    fprintf(output, "%s", json_quote_str(ctx, val));

Take a look at the string quoting function in
id:"87d34hsdx8.fsf@awakening.csail.mit.edu".  It quotes directly to a
FILE * without allocating an intermediate buffer.  It's also actually
correct, unlike json_quote_str.

> +    talloc_free(ctx);
> +}
> +
> +void json_bool(void *st, notmuch_bool_t val) {
> +    json_state_t *state = (json_state_t*)st;
> +    FILE *output = state->output;

This needs to insert a comma like the other output functions.

> +    if(val)
> +	fputs("true", output);
> +    else
> +	fputs("false", output);

Maybe fputs(val ? "true" : "false", output)?

> +}
> +
> +void *json_initial_state(const struct structure_printer *sp, FILE *output) {
> +    (void)sp;
> +    json_state_t *st = talloc(0, json_state_t);
> +    st->level = 0;
> +    st->stack = NULL;
> +    st->output = output;
> +    return st;
> +}

This is as far as I've gotten.  I'll switch to your newer version and
review the rest when I get a chance.

^ permalink raw reply	[flat|nested] 39+ messages in thread

* deprecating legacy text output
  2012-07-10 17:04     ` [PATCH] FIXED: Added better support for multiple structured output formats Jameson Graef Rollins
  2012-07-10 17:28       ` Austin Clements
@ 2012-07-10 22:45       ` David Bremner
  1 sibling, 0 replies; 39+ messages in thread
From: David Bremner @ 2012-07-10 22:45 UTC (permalink / raw)
  To: Jameson Graef Rollins, notmuch

On Tue, 10 Jul 2012 10:04:10 -0700, Jameson Graef Rollins <jrollins@finestructure.net> wrote:
> On Tue, Jul 10 2012, Mark Walters <markwalters1009@gmail.com> wrote:
> > I think I would also add something saying that the text format is just
> > different (and that a significant chunk of the patch is just that).
> 
> Can we not just dump this output format once and for all?  Does anything
> use it?  And if so can we just modify it to use a more sensible format?

I think we need to at least deprecate the format for a release or so, as
there are definitely people using it in the wild.  As far as I know the
at least the Vim interface uses it (what about notmuch-mutt?), in
addition to whatever homebrewed scripts might use it. Since
scriptability is one of the "notmuch core values", I guess we should
also provide something more script friendly than json. As you say, it
doesn't have to be exactly like what we have now.

d

^ permalink raw reply	[flat|nested] 39+ messages in thread

* [PATCH v3 0/3] Structured Formatters
  2012-07-10 19:13       ` Austin Clements
@ 2012-07-11  8:26         ` craven
  2012-07-11  8:26           ` [PATCH v3 1/3] Add support for structured output formatters craven
                             ` (2 more replies)
  2012-07-13  8:11         ` [PATCH v5 0/3] notmuch-reply: Structured Formatters Peter Feigl
  1 sibling, 3 replies; 39+ messages in thread
From: craven @ 2012-07-11  8:26 UTC (permalink / raw)
  To: notmuch

Currently there is no easy way to add support for different structured
formatters (like JSON). For example, adding support for S-Expressions
would result in code duplication.

This patch series amends the situation by introducing structured
formatters, which allow different implementations of structures like
lists, maps, strings and numbers.

The new code in structured-output.h and structured-output.c can be used
instead of the current ad-hoc output in all parts of notmuch, a patch
for notmuch-search.c is included.

In a later patch, all other parts of notmuch should be adapted to the
structured formatters, and the creation of formatters should be
centralised (to make adding new formatters easier).

^ permalink raw reply	[flat|nested] 39+ messages in thread

* [PATCH v3 1/3] Add support for structured output formatters.
  2012-07-11  8:26         ` [PATCH v3 0/3] Structured Formatters craven
@ 2012-07-11  8:26           ` craven
  2012-07-11  8:26           ` [PATCH v3 2/3] Adding a structured formatter for JSON craven
  2012-07-11  8:26           ` [PATCH v3 3/3] Use the JSON structure printer in notmuch-search craven
  2 siblings, 0 replies; 39+ messages in thread
From: craven @ 2012-07-11  8:26 UTC (permalink / raw)
  To: notmuch

This patch adds a new type structure_printer, which is used for
structured formatting, e.g. JSON or S-Expressions.

The structure contains the following function pointers:

- initial_state: is called to create a state object, that is passed to
  all invocations. This should be used to keep track of the output file
  and everything else necessary to correctly format output.
- map: is called when a new map (associative array, dictionary) is
  started. map_key and the primitives (string, number, bool) are used
  alternatingly to add key/value pairs. pop is used to close the map
  (see there). This function must return a nesting level identifier that
  can be used to close all nested structures (maps and lists), backing
  out to the returned nesting level.
- list: is called when a new list (array, vector) is started. the
  primitives (string, number, bool) are used consecutively to add values
  to the list. pop is used to close the list. This function must return
  a nesting level identifier that can be used to close all nested
  structures (maps and lists), backing out to the returned nesting
  level.
- map_key: is called to write the key of a key/value pair.
- pop: is called to return to a given nesting level. All lists and maps
  with a deeper nesting level must be closed.
- number, string, bool: output one element of the specific type.

All functions should use state to insert delimiters etc. automatically
when appropriate. State is a user-defined object/data that can contain
arbitrary information. Initial state is constructed by a call to
initial_state.

Example:
int top, one;
top = map(state);
map_key(state, "foo");
one = list(state);
number(state, 1);
number(state, 2);
number(state, 3);
pop(state, one);
map_key(state, "bar");
map(state);
map_key(state, "baaz");
string(state, "hello world");
pop(state, top);

would output JSON as follows:

{"foo": [1, 2, 3], "bar": { "baaz": "hello world"}}
---
 structured-output.h | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 90 insertions(+)
 create mode 100644 structured-output.h

diff --git a/structured-output.h b/structured-output.h
new file mode 100644
index 0000000..73029f1
--- /dev/null
+++ b/structured-output.h
@@ -0,0 +1,90 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see http://www.gnu.org/licenses/ .
+ *
+ * Author: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-client.h"
+
+/* structured formatting, useful for JSON, S-Expressions, ...
+ *
+ * All functions should use state to insert delimiters
+ * etc. automatically when appropriate. State is a user-defined
+ * object/data that can contain arbitrary information. Initial state is
+ * constructed by a call to initial_state.
+ *
+ * Example:
+ * int top, one;
+ * top = map(state);
+ * map_key(state, "foo");
+ * one = list(state);
+ * number(state, 1);
+ * number(state, 2);
+ * number(state, 3);
+ * pop(state, one);
+ * map_key(state, "bar");
+ * map(state);
+ * map_key(state, "baaz");
+ * string(state, "hello world");
+ * pop(state, top);
+ *
+ * would output JSON as follows:
+ *
+ * {"foo": [1, 2, 3], "bar": { "baaz": "hello world"}}
+ */
+typedef struct structure_printer {
+    /* map: is called when a new map (associative array, dictionary) is
+     * started. map_key and the primitives (string, number, bool) are
+     * used alternatingly to add key/value pairs. pop is used to close
+     * the map (see there). This function must return a nesting level
+     * identifier number that can be used to close all nested structures
+     * (maps and lists), backing out to the returned nesting level.
+     */
+    int (*map) (void *state);
+
+    /* list: is called when a new list (array, vector) is started. the
+     * primitives (string, number, bool) are used consecutively to add
+     * values to the list. pop is used to close the list. This function
+     * must return a nesting level identifier number that can be used to
+     * close all nested structures (maps and lists), backing out to the
+     * returned nesting level.
+     */
+    int (*list) (void *state);
+
+    /* pop: is called to return to a given nesting level. All lists and
+     * maps with a deeper nesting level must be closed.
+     */
+    void (*pop) (void *state, int level);
+
+    /* map_key: is called to write the key of a key/value pair.
+     */
+    void (*map_key) (void *state, const char *key);
+
+    /* number, string, bool: output one element of the specific type. */
+    void (*number) (void *state, int val);
+    void (*string) (void *state, const char *val);
+    void (*bool) (void *state, notmuch_bool_t val);
+
+    /* initial_state: is called to create a state object, that is passed
+     * to all invocations. This should be used to keep track of the
+     * output file and everything else necessary to correctly format
+     * output.
+     */
+    void *(*initial_state) (const struct structure_printer *sp,
+			    FILE *output);
+
+} structure_printer_t;
-- 
1.7.11.1

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH v3 2/3] Adding a structured formatter for JSON.
  2012-07-11  8:26         ` [PATCH v3 0/3] Structured Formatters craven
  2012-07-11  8:26           ` [PATCH v3 1/3] Add support for structured output formatters craven
@ 2012-07-11  8:26           ` craven
  2012-07-11  8:26           ` [PATCH v3 3/3] Use the JSON structure printer in notmuch-search craven
  2 siblings, 0 replies; 39+ messages in thread
From: craven @ 2012-07-11  8:26 UTC (permalink / raw)
  To: notmuch

This patch adds a structured formatter that prints exactly the same JSON as the built-in JSON printer. All tests pass without change.

Notice that this formatter could be simpler by changing the whitespace
and line-breaks in the generated JSON.
---
 Makefile.local      |   1 +
 structured-output.c | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 structured-output.h |  59 +++++++++++++++++++
 3 files changed, 227 insertions(+)
 create mode 100644 structured-output.c

diff --git a/Makefile.local b/Makefile.local
index a890df2..9b989dc 100644
--- a/Makefile.local
+++ b/Makefile.local
@@ -291,6 +291,7 @@ notmuch_client_srcs =		\
 	notmuch-tag.c		\
 	notmuch-time.c		\
 	query-string.c		\
+	structured-output.c	\
 	mime-node.c		\
 	crypto.c		\
 	json.c
diff --git a/structured-output.c b/structured-output.c
new file mode 100644
index 0000000..18a7306
--- /dev/null
+++ b/structured-output.c
@@ -0,0 +1,167 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see http://www.gnu.org/licenses/ .
+ *
+ * Author: Carl Worth <cworth@cworth.org>
+ */
+
+#include "structured-output.h"
+#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
+
+structure_printer_t *
+unstructured_text_printer = NULL;
+
+structure_printer_t
+json_structure_printer = {
+    &json_map,
+    &json_list,
+    &json_pop,
+    &json_map_key,
+    &json_number,
+    &json_string,
+    &json_bool,
+    &json_initial_state
+};
+
+static int
+enter_level (void *st, const char *marker, int type) {
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    json_list_t *el = talloc (st, json_list_t);
+
+    json_item_separator (state, (state->level == 1) ? "\n" : " ");
+    fputs (marker, output);
+
+    el->type = type;
+    el->first_seen = FALSE;
+    el->rest = state->stack;
+    state->stack = el;
+    return state->level++;
+}
+
+void
+json_print_escaped_string (FILE *output, const char *val)
+{
+    static const char * const escapes[] = {
+        ['\"'] = "\\\"", ['\\'] = "\\\\", ['\b'] = "\\b",
+        ['\f'] = "\\f",  ['\n'] = "\\n",  ['\t'] = "\\t"
+    };
+    fputc ('"', output);
+    for (; *val; ++val) {
+        unsigned char ch = *val;
+        if (ch < ARRAY_SIZE(escapes) && escapes[ch])
+            fputs (escapes[ch], output);
+        else if (ch >= 32)
+            fputc (ch, output);
+        else
+            fprintf (output, "\\u%04x", ch);
+    }
+    fputc ('"', output);
+}
+
+int
+json_map (void *st)
+{
+    return enter_level (st, "{", TYPE_JSON_MAP);
+}
+
+int
+json_list (void *st)
+{
+    return enter_level (st, "[", TYPE_JSON_ARRAY);
+}
+
+void
+json_pop (void *st, int level)
+{
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    while (state->level > level) {
+	json_list_t *tos = state->stack;
+	fputs (tos->type == TYPE_JSON_MAP ? "}" : "]", output);
+	state->stack = tos->rest;
+	state->level--;
+	talloc_free (tos);
+    }
+    if (state->level == 0) {
+	fputs ("\n", output);
+    }
+}
+
+void
+json_map_key (void *st, const char *key)
+{
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    if (state->stack != NULL && state->stack->first_seen) {
+	fputs (",\n", output);
+    }
+    json_print_escaped_string (output, key);
+    fputs (": ", output);
+}
+
+void
+json_item_separator (json_state_t *state, const char *suffix)
+{
+    FILE *output = state->output;
+    if (state->stack != NULL
+	&& state->stack->type == TYPE_JSON_ARRAY
+	&& state->stack->first_seen) {
+
+	fputs (",", output);
+	fputs (suffix, output);
+    }
+    if (state->stack != NULL)
+	state->stack->first_seen = TRUE;
+}
+
+void
+json_number (void *st, int val)
+{
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    json_item_separator (state, state->level == 1 ? "\n" : " ");
+    fprintf (output, "%d", val);
+}
+
+void
+json_string (void *st, const char *val)
+{
+    json_state_t *state = (json_state_t *)st;
+    FILE *output = state->output;
+    json_item_separator (state, state->level == 1 ? "\n" : " ");
+    json_print_escaped_string (output, val);
+}
+
+void
+json_bool (void *st, notmuch_bool_t val)
+{
+    json_state_t *state = (json_state_t*)st;
+    FILE *output = state->output;
+    json_item_separator (state, state->level == 1 ? "\n" : " ");
+    fputs (val ? "true" : "false", output);
+}
+
+void *
+json_initial_state (const struct structure_printer *sp, FILE *output)
+{
+    (void)sp;
+    json_state_t *st = talloc (0, json_state_t);
+    st->level = 0;
+    st->stack = NULL;
+    st->output = output;
+    return st;
+}
diff --git a/structured-output.h b/structured-output.h
index 73029f1..b211ac6 100644
--- a/structured-output.h
+++ b/structured-output.h
@@ -88,3 +88,62 @@ typedef struct structure_printer {
 			    FILE *output);
 
 } structure_printer_t;
+
+/* dummy object to differentiate plain text from structured output */
+structure_printer_t *
+unstructured_text_printer;
+
+/* JSON structure printer
+ * An implementation of the JSON structure printer that produces
+ * exactly the same output as the previous JSON printer.
+ */
+
+/* single linked list implementation for keeping track of the array/map
+ * nesting state.
+ */
+typedef enum {TYPE_JSON_MAP, TYPE_JSON_ARRAY} JSON_TYPE;
+
+typedef struct json_list {
+    int type;
+    notmuch_bool_t first_seen;
+    struct json_list *rest;
+} json_list_t;
+
+typedef struct json_state {
+    FILE *output;
+    json_list_t *stack;
+    int level;
+} json_state_t;
+
+int
+json_map(void *state);
+
+int
+json_list(void *state);
+
+void
+json_pop(void *state, int level);
+
+void
+json_map_key(void *state, const char *key);
+
+void
+json_number(void *state, int val);
+
+void
+json_string(void *state, const char *val);
+
+void
+json_bool(void *state, notmuch_bool_t val);
+
+void *
+json_initial_state(const struct structure_printer *sp, FILE *output);
+
+void
+json_print_escaped_string(FILE *output, const char *val);
+
+void
+json_item_separator(json_state_t *state, const char *suffix);
+
+structure_printer_t
+json_structure_printer;
-- 
1.7.11.1

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH v3 3/3] Use the JSON structure printer in notmuch-search.
  2012-07-11  8:26         ` [PATCH v3 0/3] Structured Formatters craven
  2012-07-11  8:26           ` [PATCH v3 1/3] Add support for structured output formatters craven
  2012-07-11  8:26           ` [PATCH v3 2/3] Adding a structured formatter for JSON craven
@ 2012-07-11  8:26           ` craven
  2 siblings, 0 replies; 39+ messages in thread
From: craven @ 2012-07-11  8:26 UTC (permalink / raw)
  To: notmuch

This patch uses the JSON structure printer for notmuch search. The
changes necessary for switching to the structure printer are minor, a
large part of this patch is concerned with keeping the normal
unstructured text output.
---
 notmuch-search.c | 278 ++++++++++++++++++++++++++++++-------------------------
 1 file changed, 150 insertions(+), 128 deletions(-)

diff --git a/notmuch-search.c b/notmuch-search.c
index 3be296d..84e4cbc 100644
--- a/notmuch-search.c
+++ b/notmuch-search.c
@@ -19,6 +19,7 @@
  */
 
 #include "notmuch-client.h"
+#include "structured-output.h"
 
 typedef enum {
     OUTPUT_SUMMARY,
@@ -28,6 +29,7 @@ typedef enum {
     OUTPUT_TAGS
 } output_t;
 
+/* legacy, only needed for non-structured text output */
 typedef struct search_format {
     const char *results_start;
     const char *item_start;
@@ -64,6 +66,7 @@ format_thread_text (const void *ctx,
 		    const int total,
 		    const char *authors,
 		    const char *subject);
+
 static const search_format_t format_text = {
     "",
 	"",
@@ -78,35 +81,6 @@ static const search_format_t format_text = {
 };
 
 static void
-format_item_id_json (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject);
-
-/* Any changes to the JSON format should be reflected in the file
- * devel/schemata. */
-static const search_format_t format_json = {
-    "[",
-	"{",
-	    format_item_id_json,
-	    format_thread_json,
-	    "\"tags\": [",
-		"\"%s\"", ", ",
-	    "]", ",\n",
-	"}",
-    "]\n",
-    "]\n",
-};
-
-static void
 format_item_id_text (unused (const void *ctx),
 		     const char *item_type,
 		     const char *item_id)
@@ -153,50 +127,9 @@ format_thread_text (const void *ctx,
     talloc_free (ctx_quote);
 }
 
-static void
-format_item_id_json (const void *ctx,
-		     unused (const char *item_type),
-		     const char *item_id)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("%s", json_quote_str (ctx_quote, item_id));
-
-    talloc_free (ctx_quote);
-    
-}
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("\"thread\": %s,\n"
-	    "\"timestamp\": %ld,\n"
-	    "\"date_relative\": \"%s\",\n"
-	    "\"matched\": %d,\n"
-	    "\"total\": %d,\n"
-	    "\"authors\": %s,\n"
-	    "\"subject\": %s,\n",
-	    json_quote_str (ctx_quote, thread_id),
-	    date,
-	    notmuch_time_relative_date (ctx, date),
-	    matched,
-	    total,
-	    json_quote_str (ctx_quote, authors),
-	    json_quote_str (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
 static int
-do_search_threads (const search_format_t *format,
+do_search_threads (const structure_printer_t *format,
+		   void *state,
 		   notmuch_query_t *query,
 		   notmuch_sort_t sort,
 		   output_t output,
@@ -209,6 +142,8 @@ do_search_threads (const search_format_t *format,
     time_t date;
     int first_thread = 1;
     int i;
+    int outermost_level = 0;
+    int items_level = 0;
 
     if (offset < 0) {
 	offset += notmuch_query_count_threads (query);
@@ -220,7 +155,11 @@ do_search_threads (const search_format_t *format,
     if (threads == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    if (format == unstructured_text_printer) {
+	fputs(format_text.results_start, stdout);
+    } else { /* structured output */
+	outermost_level = format->list(state);
+    }
 
     for (i = 0;
 	 notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
@@ -235,43 +174,85 @@ do_search_threads (const search_format_t *format,
 	    continue;
 	}
 
-	if (! first_thread)
-	    fputs (format->item_sep, stdout);
+	if (format == unstructured_text_printer && ! first_thread)
+	    fputs (format_text.item_sep, stdout);
 
 	if (output == OUTPUT_THREADS) {
-	    format->item_id (thread, "thread:",
-			     notmuch_thread_get_thread_id (thread));
+	    if (format == unstructured_text_printer) {
+		format_text.item_id (thread, "thread:",
+				     notmuch_thread_get_thread_id (thread));
+	    } else { /* structured output */
+		format->string(state, notmuch_thread_get_thread_id (thread));
+	    }
 	} else { /* output == OUTPUT_SUMMARY */
-	    fputs (format->item_start, stdout);
+
+	    if (format == unstructured_text_printer) {
+		fputs (format_text.item_start, stdout);
+	    } else { /* structured output */
+		items_level = format->map(state);
+	    }
 
 	    if (sort == NOTMUCH_SORT_OLDEST_FIRST)
 		date = notmuch_thread_get_oldest_date (thread);
 	    else
 		date = notmuch_thread_get_newest_date (thread);
 
-	    format->thread_summary (thread,
-				    notmuch_thread_get_thread_id (thread),
-				    date,
-				    notmuch_thread_get_matched_messages (thread),
-				    notmuch_thread_get_total_messages (thread),
-				    notmuch_thread_get_authors (thread),
-				    notmuch_thread_get_subject (thread));
-
-	    fputs (format->tag_start, stdout);
+	    if (format == unstructured_text_printer) {
+		format_text.thread_summary (thread,
+					    notmuch_thread_get_thread_id (thread),
+					    date,
+					    notmuch_thread_get_matched_messages (thread),
+					    notmuch_thread_get_total_messages (thread),
+					    notmuch_thread_get_authors (thread),
+					    notmuch_thread_get_subject (thread));
+	    } else { /* structured output */
+		void *ctx = talloc_new (0);
+		format->map_key(state, "thread");
+		format->string(state, notmuch_thread_get_thread_id (thread));
+		format->map_key(state, "timestamp");
+		format->number(state, date);
+		format->map_key(state, "date_relative");
+		format->string(state, notmuch_time_relative_date(ctx, date));
+		format->map_key(state, "matched");
+		format->number(state, notmuch_thread_get_matched_messages(thread));
+		format->map_key(state, "total");
+		format->number(state, notmuch_thread_get_total_messages(thread));
+		format->map_key(state, "authors");
+		format->string(state, notmuch_thread_get_authors(thread));
+		format->map_key(state, "subject");
+		format->string(state, notmuch_thread_get_subject(thread));
+		talloc_free(ctx);
+	    };
+
+	    if (format == unstructured_text_printer) {
+		fputs (format_text.tag_start, stdout);
+	    } else { /* structured output */
+		format->map_key(state, "tags");
+		format->list(state);
+	    }
 
 	    for (tags = notmuch_thread_get_tags (thread);
 		 notmuch_tags_valid (tags);
 		 notmuch_tags_move_to_next (tags))
 	    {
-		if (! first_tag)
-		    fputs (format->tag_sep, stdout);
-		printf (format->tag, notmuch_tags_get (tags));
+		if (format == unstructured_text_printer && ! first_tag) {
+		    fputs (format_text.tag_sep, stdout);
+		}
+
+		if(format == unstructured_text_printer) {
+		    printf (format_text.tag, notmuch_tags_get (tags));
+		} else {
+		    format->string(state, notmuch_tags_get(tags));
+		}
 		first_tag = 0;
 	    }
 
-	    fputs (format->tag_end, stdout);
-
-	    fputs (format->item_end, stdout);
+	    if(format == unstructured_text_printer) {
+		fputs (format_text.tag_end, stdout);
+		fputs (format_text.item_end, stdout);
+	    } else { /* structured output */
+		format->pop(state, items_level);
+	    }
 	}
 
 	first_thread = 0;
@@ -279,16 +260,21 @@ do_search_threads (const search_format_t *format,
 	notmuch_thread_destroy (thread);
     }
 
-    if (first_thread)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    if (format == unstructured_text_printer) {
+	if (first_thread)
+	    fputs (format_text.results_null, stdout);
+	else
+	    fputs (format_text.results_end, stdout);
+    } else { /* structured output */
+	format->pop(state, outermost_level);
+    }
 
     return 0;
 }
 
 static int
-do_search_messages (const search_format_t *format,
+do_search_messages (const structure_printer_t *format,
+		    void *state,
 		    notmuch_query_t *query,
 		    output_t output,
 		    int offset,
@@ -299,6 +285,7 @@ do_search_messages (const search_format_t *format,
     notmuch_filenames_t *filenames;
     int first_message = 1;
     int i;
+    int outermost_level = 0;
 
     if (offset < 0) {
 	offset += notmuch_query_count_messages (query);
@@ -310,7 +297,11 @@ do_search_messages (const search_format_t *format,
     if (messages == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    if(format == unstructured_text_printer) {
+	fputs (format_text.results_start, stdout);
+    } else { /* structured output */
+	outermost_level = format->list(state);
+    }
 
     for (i = 0;
 	 notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
@@ -328,23 +319,32 @@ do_search_messages (const search_format_t *format,
 		 notmuch_filenames_valid (filenames);
 		 notmuch_filenames_move_to_next (filenames))
 	    {
-		if (! first_message)
-		    fputs (format->item_sep, stdout);
+		if (format == unstructured_text_printer) {
+		    if (! first_message)
+			fputs (format_text.item_sep, stdout);
 
-		format->item_id (message, "",
-				 notmuch_filenames_get (filenames));
+		    format_text.item_id (message, "",
+					 notmuch_filenames_get (filenames));
+		} else { /* structured output */
+		format->string(state, notmuch_filenames_get (filenames));
+		}
 
 		first_message = 0;
 	    }
-	    
+
 	    notmuch_filenames_destroy( filenames );
 
 	} else { /* output == OUTPUT_MESSAGES */
-	    if (! first_message)
-		fputs (format->item_sep, stdout);
+	    if (format == unstructured_text_printer) {
+		if (! first_message)
+		    fputs (format_text.item_sep, stdout);
+
+		format_text.item_id (message, "id:",
+				     notmuch_message_get_message_id (message));
+	    } else { /* structured output */
+		format->string(state, notmuch_message_get_message_id (message));
+	    }
 
-	    format->item_id (message, "id:",
-			     notmuch_message_get_message_id (message));
 	    first_message = 0;
 	}
 
@@ -353,23 +353,29 @@ do_search_messages (const search_format_t *format,
 
     notmuch_messages_destroy (messages);
 
-    if (first_message)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    if (format == unstructured_text_printer) {
+	if (first_message)
+	    fputs (format_text.results_null, stdout);
+	else
+	    fputs (format_text.results_end, stdout);
+    } else { /* structured output */
+	format->pop(state, outermost_level);
+    };
 
     return 0;
 }
 
 static int
 do_search_tags (notmuch_database_t *notmuch,
-		const search_format_t *format,
+		const structure_printer_t *format,
+		void *state,
 		notmuch_query_t *query)
 {
     notmuch_messages_t *messages = NULL;
     notmuch_tags_t *tags;
     const char *tag;
     int first_tag = 1;
+    int outermost_level = 0;
 
     /* should the following only special case if no excluded terms
      * specified? */
@@ -387,7 +393,11 @@ do_search_tags (notmuch_database_t *notmuch,
     if (tags == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    if (format == unstructured_text_printer) {
+	fputs (format_text.results_start, stdout);
+    } else { /* structured output */
+	outermost_level = format->list(state);
+    }
 
     for (;
 	 notmuch_tags_valid (tags);
@@ -395,10 +405,14 @@ do_search_tags (notmuch_database_t *notmuch,
     {
 	tag = notmuch_tags_get (tags);
 
-	if (! first_tag)
-	    fputs (format->item_sep, stdout);
-
-	format->item_id (tags, "", tag);
+	if (format == unstructured_text_printer) {
+	    if (! first_tag) {
+		fputs (format_text.item_sep, stdout);
+	    }
+	    format_text.item_id (tags, "", tag);
+	} else { /* structured output */
+	    format->string(state, tag);
+	}
 
 	first_tag = 0;
     }
@@ -408,10 +422,14 @@ do_search_tags (notmuch_database_t *notmuch,
     if (messages)
 	notmuch_messages_destroy (messages);
 
-    if (first_tag)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    if (format == unstructured_text_printer) {
+	if (first_tag)
+	    fputs (format_text.results_null, stdout);
+	else
+	    fputs (format_text.results_end, stdout);
+    } else { /* structured output */
+	format->pop(state, outermost_level);
+    };
 
     return 0;
 }
@@ -430,7 +448,8 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_t *query;
     char *query_str;
     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
-    const search_format_t *format = &format_text;
+    const structure_printer_t *format = unstructured_text_printer;
+    void *state = NULL;
     int opt_index, ret;
     output_t output = OUTPUT_SUMMARY;
     int offset = 0;
@@ -475,13 +494,16 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 
     switch (format_sel) {
     case NOTMUCH_FORMAT_TEXT:
-	format = &format_text;
+	format = unstructured_text_printer;
 	break;
     case NOTMUCH_FORMAT_JSON:
-	format = &format_json;
+	format = &json_structure_printer;
 	break;
     }
 
+    if(format != unstructured_text_printer)
+	state = format->initial_state(format, stdout);
+
     config = notmuch_config_open (ctx, NULL, NULL);
     if (config == NULL)
 	return 1;
@@ -532,14 +554,14 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     default:
     case OUTPUT_SUMMARY:
     case OUTPUT_THREADS:
-	ret = do_search_threads (format, query, sort, output, offset, limit);
+	ret = do_search_threads (format, state, query, sort, output, offset, limit);
 	break;
     case OUTPUT_MESSAGES:
     case OUTPUT_FILES:
-	ret = do_search_messages (format, query, output, offset, limit);
+	ret = do_search_messages (format, state, query, output, offset, limit);
 	break;
     case OUTPUT_TAGS:
-	ret = do_search_tags (notmuch, format, query);
+	ret = do_search_tags (notmuch, format, state, query);
 	break;
     }
 
-- 
1.7.11.1

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH v5 0/3] notmuch-reply: Structured Formatters
  2012-07-10 19:13       ` Austin Clements
  2012-07-11  8:26         ` [PATCH v3 0/3] Structured Formatters craven
@ 2012-07-13  8:11         ` Peter Feigl
  2012-07-13  8:11           ` [PATCH v5 1/3] Add support for structured output formatters Peter Feigl
                             ` (3 more replies)
  1 sibling, 4 replies; 39+ messages in thread
From: Peter Feigl @ 2012-07-13  8:11 UTC (permalink / raw)
  To: notmuch

Currently there is no easy way to add support for different structured
formatters (like JSON). For example, adding support for S-Expressions
would result in code duplication.

This patch series amends the situation by introducing structured
formatters, which allow different implementations of structures like
lists, maps, strings and numbers.

The new code in sprinter.h and sprinter-json.c can be used instead of
the current ad-hoc output in all parts of notmuch, a patch for
notmuch-search.c is included.

In a later patch, all other parts of notmuch should be adapted to the
structured formatters, and the creation of formatters should be
centralised (to make adding new formatters easier).

A "structured" formatter is provided for notmuch-search that prints the
current text format. This removes all special-casing from
notmuch-search.c, only structured output is used (and the formatter
discards unnecessary parts or introduces extra formatting).

^ permalink raw reply	[flat|nested] 39+ messages in thread

* [PATCH v5 1/3] Add support for structured output formatters.
  2012-07-13  8:11         ` [PATCH v5 0/3] notmuch-reply: Structured Formatters Peter Feigl
@ 2012-07-13  8:11           ` Peter Feigl
  2012-07-14  1:46             ` Austin Clements
  2012-07-13  8:11           ` [PATCH v5 2/3] Add structured output formatter for JSON and text Peter Feigl
                             ` (2 subsequent siblings)
  3 siblings, 1 reply; 39+ messages in thread
From: Peter Feigl @ 2012-07-13  8:11 UTC (permalink / raw)
  To: notmuch

From: <craven@gmx.net>

This patch adds a new type sprinter_t, which is used for structured
formatting, e.g. JSON or S-Expressions. The structure printer is the
code from Austin Clements (id:87d34hsdx8.fsf@awakening.csail.mit.edu)
with minor modifications.

The structure printer contains the following function pointers:

/* 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 *);

/* Set the prefix of the next component. This is purely for
 * debugging purposes and for the unstructured text formatter.
 */
void (*set_prefix)(struct sprinter *, const char *);

/* 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 *);

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 | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 50 insertions(+)
 create mode 100644 sprinter.h

diff --git a/sprinter.h b/sprinter.h
new file mode 100644
index 0000000..c9cd6a6
--- /dev/null
+++ b/sprinter.h
@@ -0,0 +1,50 @@
+#ifndef NOTMUCH_SPRINTER_H
+#define NOTMUCH_SPRINTER_H
+
+/* Necessary for notmuch_bool_t */
+#include "notmuch-client.h"
+
+/* Structure printer interface */
+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 *);
+
+    /* Set the prefix of the next component. This is purely for
+     * debugging purposes and for the unstructured text formatter.
+     */
+    void (*set_prefix)(struct sprinter *, const char *);
+
+    /* 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 *);
+} sprinter_t;
+
+#endif // NOTMUCH_SPRINTER_H
-- 
1.7.11.1

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH v5 2/3] Add structured output formatter for JSON and text.
  2012-07-13  8:11         ` [PATCH v5 0/3] notmuch-reply: Structured Formatters Peter Feigl
  2012-07-13  8:11           ` [PATCH v5 1/3] Add support for structured output formatters Peter Feigl
@ 2012-07-13  8:11           ` Peter Feigl
  2012-07-14  2:02             ` Austin Clements
  2012-07-13  8:11           ` [PATCH v5 3/3] Use the structured formatters in notmuch-search.c Peter Feigl
  2012-07-13  8:17           ` Proof of concept: S-Expression format craven
  3 siblings, 1 reply; 39+ messages in thread
From: Peter Feigl @ 2012-07-13  8:11 UTC (permalink / raw)
  To: notmuch

From: <craven@gmx.net>

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 that prints the current plain
text format. This passes all tests, but the exact formatting is probably
specific to notmuch-search and cannot easily (if at all) be adapted to
be used across all of notmuch-{search,reply,show,...}.
---
 Makefile.local         |   2 +
 sprinter-json.c        | 184 +++++++++++++++++++++++++++++++++++++++++++++++
 sprinter-text-search.c | 190 +++++++++++++++++++++++++++++++++++++++++++++++++
 sprinter.h             |   8 +++
 4 files changed, 384 insertions(+)
 create mode 100644 sprinter-json.c
 create mode 100644 sprinter-text-search.c

diff --git a/Makefile.local b/Makefile.local
index a890df2..b6c7e0c 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-search.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..215151d
--- /dev/null
+++ b/sprinter-json.c
@@ -0,0 +1,184 @@
+#include <stdbool.h>
+#include <stdio.h>
+#include <talloc.h>
+#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,
+	}
+    };
+    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-search.c b/sprinter-text-search.c
new file mode 100644
index 0000000..95ed9cb
--- /dev/null
+++ b/sprinter-text-search.c
@@ -0,0 +1,190 @@
+#include <stdbool.h>
+#include <stdio.h>
+#include <talloc.h>
+#include "sprinter.h"
+
+/* "Structured printer" interface for unstructured text printing.
+ * This is at best a misuse of the interface, but it simplifies the code
+ * in notmuch-search.c considerably.
+ */
+
+struct sprinter_text_search {
+    struct sprinter vtable;
+    FILE *stream;
+
+    /* The current name or prefix to be printed with string/integer/boolean
+     * data.
+     */
+    const char *current_name;
+
+    /* A flag to indicate if this is the first tag. Used in list of tags
+     * for summary.
+     */
+    notmuch_bool_t first_tag;
+};
+
+/* struct text_search_state { */
+/*     struct text_search_state *parent; */
+/* }; */
+
+static notmuch_bool_t
+current_context (struct sprinter_text_search *sptxt, const char *marker)
+{
+    return (sptxt->current_name != NULL
+	    && ! strncmp (marker, sptxt->current_name, strlen (marker)));
+}
+
+static void
+print_sanitized_string (FILE *stream, const char *str)
+{
+    if (NULL == str)
+	return;
+
+    for (; *str; str++) {
+	if ((unsigned char) (*str) < 32)
+	    fputc ('?', stream);
+	else
+	    fputc (*str, stream);
+    }
+}
+
+static void
+text_search_begin_map (unused (struct sprinter *sp))
+{
+}
+
+static void
+text_search_begin_list (struct sprinter *sp)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    if (current_context (sptxt, "tags")) {
+	fputs (" (", sptxt->stream);
+	sptxt->first_tag = TRUE;
+    }
+}
+
+static void
+text_search_end (struct sprinter *sp)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    if (current_context (sptxt, "tags")) {
+	fputc (')', sptxt->stream);
+	sptxt->current_name = NULL;
+    }
+}
+
+static void
+text_search_string (struct sprinter *sp, const char *val)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    if (sptxt->current_name != NULL) {
+	if (current_context (sptxt, "thread"))
+	    fprintf ( sptxt->stream, "thread:%s ", val);
+	else if (current_context (sptxt, "date_relative"))
+	    fprintf ( sptxt->stream, "%12s ", val);
+	else if (current_context (sptxt, "authors")) {
+	    print_sanitized_string (sptxt->stream, val);
+	    fputs ("; ", sptxt->stream);
+	} else if (current_context (sptxt, "subject"))
+	    print_sanitized_string (sptxt->stream, val);
+	else if (current_context (sptxt, "tags")) {
+	    if (! sptxt->first_tag)
+		fputc (' ', sptxt->stream);
+	    else
+		sptxt->first_tag = FALSE;
+
+	    fputs (val, sptxt->stream);
+	} else {
+	    fputs (sptxt->current_name, sptxt->stream);
+	    fputc (':', sptxt->stream);
+	    fputs (val, sptxt->stream);
+	}
+    } else {
+	fputs (val, sptxt->stream);
+    }
+}
+
+static void
+text_search_integer (struct sprinter *sp, int val)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    if (sptxt->current_name != NULL) {
+	if (current_context (sptxt, "matched"))
+	    fprintf ( sptxt->stream, "[%d/", val);
+	else if (current_context (sptxt, "total"))
+	    fprintf ( sptxt->stream, "%d] ", val);
+    } else
+	fprintf (sptxt->stream, "%d", val);
+}
+
+static void
+text_search_boolean (struct sprinter *sp, notmuch_bool_t val)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    fputs (val ? "true" : "false", sptxt->stream);
+}
+
+static void
+text_search_null (struct sprinter *sp)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    fputs ("null", sptxt->stream);
+}
+
+static void
+text_search_map_key (struct sprinter *sp, const char *key)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    sptxt->current_name = key;
+}
+
+static void
+text_search_separator (struct sprinter *sp)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    fputc ('\n', sptxt->stream);
+}
+
+static void
+text_search_set_prefix (struct sprinter *sp, const char *name)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    sptxt->current_name = name;
+}
+
+struct sprinter *
+sprinter_text_search_create (const void *ctx, FILE *stream)
+{
+    static const struct sprinter_text_search template = {
+	.vtable = {
+	    .begin_map = text_search_begin_map,
+	    .begin_list = text_search_begin_list,
+	    .end = text_search_end,
+	    .string = text_search_string,
+	    .integer = text_search_integer,
+	    .boolean = text_search_boolean,
+	    .null = text_search_null,
+	    .map_key = text_search_map_key,
+	    .separator = text_search_separator,
+	    .set_prefix = text_search_set_prefix,
+	}
+    };
+    struct sprinter_text_search *res;
+
+    res = talloc (ctx, struct sprinter_text_search);
+    if (! res)
+	return NULL;
+
+    *res = template;
+    res->stream = stream;
+    return &res->vtable;
+}
diff --git a/sprinter.h b/sprinter.h
index c9cd6a6..4241d65 100644
--- a/sprinter.h
+++ b/sprinter.h
@@ -47,4 +47,12 @@ typedef struct sprinter {
     void (*separator)(struct sprinter *);
 } sprinter_t;
 
+/* Create a new unstructured printer that emits the default Text format for search. */
+struct sprinter *
+sprinter_text_search_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
-- 
1.7.11.1

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH v5 3/3] Use the structured formatters in notmuch-search.c.
  2012-07-13  8:11         ` [PATCH v5 0/3] notmuch-reply: Structured Formatters Peter Feigl
  2012-07-13  8:11           ` [PATCH v5 1/3] Add support for structured output formatters Peter Feigl
  2012-07-13  8:11           ` [PATCH v5 2/3] Add structured output formatter for JSON and text Peter Feigl
@ 2012-07-13  8:11           ` Peter Feigl
  2012-07-14  2:09             ` Austin Clements
  2012-07-13  8:17           ` Proof of concept: S-Expression format craven
  3 siblings, 1 reply; 39+ messages in thread
From: Peter Feigl @ 2012-07-13  8:11 UTC (permalink / raw)
  To: notmuch

From: <craven@gmx.net>

This patch switches from the current ad-hoc printer to the structured
output formatter in sprinter.h, sprinter-text-search.c and
sprinter-json.c.

The JSON tests are changed slightly in order to make them PASS for the
new structured output formatter.

The Text tests pass without adaptation.
---
 notmuch-search.c | 292 +++++++++++--------------------------------------------
 test/json        |  18 +---
 2 files changed, 58 insertions(+), 252 deletions(-)

diff --git a/notmuch-search.c b/notmuch-search.c
index 3be296d..99fddac 100644
--- a/notmuch-search.c
+++ b/notmuch-search.c
@@ -19,6 +19,7 @@
  */
 
 #include "notmuch-client.h"
+#include "sprinter.h"
 
 typedef enum {
     OUTPUT_SUMMARY,
@@ -28,175 +29,8 @@ typedef enum {
     OUTPUT_TAGS
 } output_t;
 
-typedef struct search_format {
-    const char *results_start;
-    const char *item_start;
-    void (*item_id) (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-    void (*thread_summary) (const void *ctx,
-			    const char *thread_id,
-			    const time_t date,
-			    const int matched,
-			    const int total,
-			    const char *authors,
-			    const char *subject);
-    const char *tag_start;
-    const char *tag;
-    const char *tag_sep;
-    const char *tag_end;
-    const char *item_sep;
-    const char *item_end;
-    const char *results_end;
-    const char *results_null;
-} search_format_t;
-
-static void
-format_item_id_text (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-
-static void
-format_thread_text (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject);
-static const search_format_t format_text = {
-    "",
-	"",
-	    format_item_id_text,
-	    format_thread_text,
-	    " (",
-		"%s", " ",
-	    ")", "\n",
-	"",
-    "\n",
-    "",
-};
-
-static void
-format_item_id_json (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject);
-
-/* Any changes to the JSON format should be reflected in the file
- * devel/schemata. */
-static const search_format_t format_json = {
-    "[",
-	"{",
-	    format_item_id_json,
-	    format_thread_json,
-	    "\"tags\": [",
-		"\"%s\"", ", ",
-	    "]", ",\n",
-	"}",
-    "]\n",
-    "]\n",
-};
-
-static void
-format_item_id_text (unused (const void *ctx),
-		     const char *item_type,
-		     const char *item_id)
-{
-    printf ("%s%s", item_type, item_id);
-}
-
-static char *
-sanitize_string (const void *ctx, const char *str)
-{
-    char *out, *loop;
-
-    if (NULL == str)
-	return NULL;
-
-    loop = out = talloc_strdup (ctx, str);
-
-    for (; *loop; loop++) {
-	if ((unsigned char)(*loop) < 32)
-	    *loop = '?';
-    }
-    return out;
-}
-
-static void
-format_thread_text (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("thread:%s %12s [%d/%d] %s; %s",
-	    thread_id,
-	    notmuch_time_relative_date (ctx, date),
-	    matched,
-	    total,
-	    sanitize_string (ctx_quote, authors),
-	    sanitize_string (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
-static void
-format_item_id_json (const void *ctx,
-		     unused (const char *item_type),
-		     const char *item_id)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("%s", json_quote_str (ctx_quote, item_id));
-
-    talloc_free (ctx_quote);
-    
-}
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("\"thread\": %s,\n"
-	    "\"timestamp\": %ld,\n"
-	    "\"date_relative\": \"%s\",\n"
-	    "\"matched\": %d,\n"
-	    "\"total\": %d,\n"
-	    "\"authors\": %s,\n"
-	    "\"subject\": %s,\n",
-	    json_quote_str (ctx_quote, thread_id),
-	    date,
-	    notmuch_time_relative_date (ctx, date),
-	    matched,
-	    total,
-	    json_quote_str (ctx_quote, authors),
-	    json_quote_str (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
 static int
-do_search_threads (const search_format_t *format,
+do_search_threads (sprinter_t *format,
 		   notmuch_query_t *query,
 		   notmuch_sort_t sort,
 		   output_t output,
@@ -207,7 +41,6 @@ do_search_threads (const search_format_t *format,
     notmuch_threads_t *threads;
     notmuch_tags_t *tags;
     time_t date;
-    int first_thread = 1;
     int i;
 
     if (offset < 0) {
@@ -220,14 +53,12 @@ do_search_threads (const search_format_t *format,
     if (threads == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (i = 0;
 	 notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
 	 notmuch_threads_move_to_next (threads), i++)
     {
-	int first_tag = 1;
-
 	thread = notmuch_threads_get (threads);
 
 	if (i < offset) {
@@ -235,60 +66,65 @@ do_search_threads (const search_format_t *format,
 	    continue;
 	}
 
-	if (! first_thread)
-	    fputs (format->item_sep, stdout);
-
 	if (output == OUTPUT_THREADS) {
-	    format->item_id (thread, "thread:",
-			     notmuch_thread_get_thread_id (thread));
+	    format->set_prefix (format, "thread");
+	    format->string (format,
+			    notmuch_thread_get_thread_id (thread));
+	    format->separator (format);
 	} else { /* output == OUTPUT_SUMMARY */
-	    fputs (format->item_start, stdout);
+	    void *ctx_quote = talloc_new (thread);
+
+	    format->begin_map (format);
 
 	    if (sort == NOTMUCH_SORT_OLDEST_FIRST)
 		date = notmuch_thread_get_oldest_date (thread);
 	    else
 		date = notmuch_thread_get_newest_date (thread);
 
-	    format->thread_summary (thread,
-				    notmuch_thread_get_thread_id (thread),
-				    date,
-				    notmuch_thread_get_matched_messages (thread),
-				    notmuch_thread_get_total_messages (thread),
-				    notmuch_thread_get_authors (thread),
-				    notmuch_thread_get_subject (thread));
-
-	    fputs (format->tag_start, stdout);
+	    format->map_key (format, "thread");
+	    format->string (format, notmuch_thread_get_thread_id (thread));
+	    format->map_key (format, "timestamp");
+	    format->integer (format, date);
+	    format->map_key (format, "date_relative");
+	    format->string (format, notmuch_time_relative_date (ctx_quote, date));
+	    format->map_key (format, "matched");
+	    format->integer (format, notmuch_thread_get_matched_messages (thread));
+	    format->map_key (format, "total");
+	    format->integer (format, notmuch_thread_get_total_messages (thread));
+	    format->map_key (format, "authors");
+	    format->string (format, notmuch_thread_get_authors (thread));
+	    format->map_key (format, "subject");
+	    format->string (format, notmuch_thread_get_subject (thread));
+
+	    talloc_free (ctx_quote);
+
+	    format->map_key (format, "tags");
+	    format->begin_list (format);
 
 	    for (tags = notmuch_thread_get_tags (thread);
 		 notmuch_tags_valid (tags);
 		 notmuch_tags_move_to_next (tags))
 	    {
-		if (! first_tag)
-		    fputs (format->tag_sep, stdout);
-		printf (format->tag, notmuch_tags_get (tags));
-		first_tag = 0;
-	    }
+		const char *tag = notmuch_tags_get (tags);
 
-	    fputs (format->tag_end, stdout);
+		format->string (format, tag);
+	    }
 
-	    fputs (format->item_end, stdout);
+	    format->end (format);
+	    format->end (format);
+	    format->separator (format);
 	}
 
-	first_thread = 0;
-
 	notmuch_thread_destroy (thread);
     }
 
-    if (first_thread)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
 
 static int
-do_search_messages (const search_format_t *format,
+do_search_messages (sprinter_t *format,
 		    notmuch_query_t *query,
 		    output_t output,
 		    int offset,
@@ -297,7 +133,6 @@ do_search_messages (const search_format_t *format,
     notmuch_message_t *message;
     notmuch_messages_t *messages;
     notmuch_filenames_t *filenames;
-    int first_message = 1;
     int i;
 
     if (offset < 0) {
@@ -310,7 +145,7 @@ do_search_messages (const search_format_t *format,
     if (messages == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (i = 0;
 	 notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
@@ -328,24 +163,17 @@ do_search_messages (const search_format_t *format,
 		 notmuch_filenames_valid (filenames);
 		 notmuch_filenames_move_to_next (filenames))
 	    {
-		if (! first_message)
-		    fputs (format->item_sep, stdout);
-
-		format->item_id (message, "",
-				 notmuch_filenames_get (filenames));
-
-		first_message = 0;
+		format->string (format, notmuch_filenames_get (filenames));
+		format->separator (format);
 	    }
-	    
+
 	    notmuch_filenames_destroy( filenames );
 
 	} else { /* output == OUTPUT_MESSAGES */
-	    if (! first_message)
-		fputs (format->item_sep, stdout);
-
-	    format->item_id (message, "id:",
-			     notmuch_message_get_message_id (message));
-	    first_message = 0;
+	    format->set_prefix (format, "id");
+	    format->string (format,
+			    notmuch_message_get_message_id (message));
+	    format->separator (format);
 	}
 
 	notmuch_message_destroy (message);
@@ -353,23 +181,19 @@ do_search_messages (const search_format_t *format,
 
     notmuch_messages_destroy (messages);
 
-    if (first_message)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
 
 static int
 do_search_tags (notmuch_database_t *notmuch,
-		const search_format_t *format,
+		sprinter_t *format,
 		notmuch_query_t *query)
 {
     notmuch_messages_t *messages = NULL;
     notmuch_tags_t *tags;
     const char *tag;
-    int first_tag = 1;
 
     /* should the following only special case if no excluded terms
      * specified? */
@@ -387,7 +211,7 @@ do_search_tags (notmuch_database_t *notmuch,
     if (tags == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (;
 	 notmuch_tags_valid (tags);
@@ -395,12 +219,9 @@ do_search_tags (notmuch_database_t *notmuch,
     {
 	tag = notmuch_tags_get (tags);
 
-	if (! first_tag)
-	    fputs (format->item_sep, stdout);
+	format->string (format, tag);
+	format->separator (format);
 
-	format->item_id (tags, "", tag);
-
-	first_tag = 0;
     }
 
     notmuch_tags_destroy (tags);
@@ -408,10 +229,7 @@ do_search_tags (notmuch_database_t *notmuch,
     if (messages)
 	notmuch_messages_destroy (messages);
 
-    if (first_tag)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
@@ -430,7 +248,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_t *query;
     char *query_str;
     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
-    const search_format_t *format = &format_text;
+    sprinter_t *format = NULL;
     int opt_index, ret;
     output_t output = OUTPUT_SUMMARY;
     int offset = 0;
@@ -475,10 +293,10 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 
     switch (format_sel) {
     case NOTMUCH_FORMAT_TEXT:
-	format = &format_text;
+	format = sprinter_text_search_create (ctx, stdout);
 	break;
     case NOTMUCH_FORMAT_JSON:
-	format = &format_json;
+	format = sprinter_json_create (ctx, stdout);
 	break;
     }
 
@@ -546,5 +364,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_destroy (query);
     notmuch_database_destroy (notmuch);
 
+    talloc_free (format);
+
     return ret;
 }
diff --git a/test/json b/test/json
index 6439788..f0ebf08 100755
--- a/test/json
+++ b/test/json
@@ -10,14 +10,7 @@ test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"e
 test_begin_subtest "Search message: json"
 add_message "[subject]=\"json-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-search-message\""
 output=$(notmuch search --format=json "json-search-message" | notmuch_search_sanitize)
-test_expect_equal "$output" "[{\"thread\": \"XXX\",
-\"timestamp\": 946728000,
-\"date_relative\": \"2000-01-01\",
-\"matched\": 1,
-\"total\": 1,
-\"authors\": \"Notmuch Test Suite\",
-\"subject\": \"json-search-subject\",
-\"tags\": [\"inbox\", \"unread\"]}]"
+test_expect_equal "$output" "[{\"thread\": \"XXX\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"matched\": 1, \"total\": 1, \"authors\": \"Notmuch Test Suite\", \"subject\": \"json-search-subject\", \"tags\": [\"inbox\", \"unread\"]}]"
 
 test_begin_subtest "Show message: json, utf-8"
 add_message "[subject]=\"json-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
@@ -40,13 +33,6 @@ test_expect_equal "$output" "[[[{\"id\": \"$id\", \"match\": true, \"excluded\":
 test_begin_subtest "Search message: json, utf-8"
 add_message "[subject]=\"json-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
 output=$(notmuch search --format=json "jsön-search-méssage" | notmuch_search_sanitize)
-test_expect_equal "$output" "[{\"thread\": \"XXX\",
-\"timestamp\": 946728000,
-\"date_relative\": \"2000-01-01\",
-\"matched\": 1,
-\"total\": 1,
-\"authors\": \"Notmuch Test Suite\",
-\"subject\": \"json-search-utf8-body-sübjéct\",
-\"tags\": [\"inbox\", \"unread\"]}]"
+test_expect_equal "$output" "[{\"thread\": \"XXX\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"matched\": 1, \"total\": 1, \"authors\": \"Notmuch Test Suite\", \"subject\": \"json-search-utf8-body-sübjéct\", \"tags\": [\"inbox\", \"unread\"]}]"
 
 test_done
-- 
1.7.11.1

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* Re: Proof of concept: S-Expression format
  2012-07-13  8:11         ` [PATCH v5 0/3] notmuch-reply: Structured Formatters Peter Feigl
                             ` (2 preceding siblings ...)
  2012-07-13  8:11           ` [PATCH v5 3/3] Use the structured formatters in notmuch-search.c Peter Feigl
@ 2012-07-13  8:17           ` craven
  3 siblings, 0 replies; 39+ messages in thread
From: craven @ 2012-07-13  8:17 UTC (permalink / raw)
  To: notmuch

This patch shows how to add a new output format to notmuch-search.c.

As an example, it adds S-Expressions. The concrete formatting can
easily be changed, this is meant as a proof of concept that the
changes to core notmuch code are very few and all formatting state is
kept inside sprinter-sexp.c.
---
 Makefile.local   |   1 +
 notmuch-search.c |   6 +-
 sprinter-sexp.c  | 185 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 sprinter.h       |   4 ++
 4 files changed, 195 insertions(+), 1 deletion(-)
 create mode 100644 sprinter-sexp.c

diff --git a/Makefile.local b/Makefile.local
index b6c7e0c..cc1d58a 100644
--- a/Makefile.local
+++ b/Makefile.local
@@ -292,6 +292,7 @@ notmuch_client_srcs =		\
 	notmuch-time.c		\
 	sprinter-json.c		\
 	sprinter-text-search.c	\
+	sprinter-sexp.c		\
 	query-string.c		\
 	mime-node.c		\
 	crypto.c		\
diff --git a/notmuch-search.c b/notmuch-search.c
index 99fddac..2db58a5 100644
--- a/notmuch-search.c
+++ b/notmuch-search.c
@@ -256,7 +256,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     int exclude = EXCLUDE_TRUE;
     unsigned int i;
 
-    enum { NOTMUCH_FORMAT_JSON, NOTMUCH_FORMAT_TEXT }
+    enum { NOTMUCH_FORMAT_JSON, NOTMUCH_FORMAT_TEXT, NOTMUCH_FORMAT_SEXP }
 	format_sel = NOTMUCH_FORMAT_TEXT;
 
     notmuch_opt_desc_t options[] = {
@@ -267,6 +267,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 	{ NOTMUCH_OPT_KEYWORD, &format_sel, "format", 'f',
 	  (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
 				  { "text", NOTMUCH_FORMAT_TEXT },
+				  { "sexp", NOTMUCH_FORMAT_SEXP },
 				  { 0, 0 } } },
 	{ NOTMUCH_OPT_KEYWORD, &output, "output", 'o',
 	  (notmuch_keyword_t []){ { "summary", OUTPUT_SUMMARY },
@@ -298,6 +299,9 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     case NOTMUCH_FORMAT_JSON:
 	format = sprinter_json_create (ctx, stdout);
 	break;
+    case NOTMUCH_FORMAT_SEXP:
+	format = sprinter_sexp_create (ctx, stdout);
+	break;
     }
 
     config = notmuch_config_open (ctx, NULL, NULL);
diff --git a/sprinter-sexp.c b/sprinter-sexp.c
new file mode 100644
index 0000000..68a5db5
--- /dev/null
+++ b/sprinter-sexp.c
@@ -0,0 +1,185 @@
+#include <stdbool.h>
+#include <stdio.h>
+#include <talloc.h>
+#include "sprinter.h"
+
+typedef enum { MAP, LIST } aggregate_t;
+
+struct sprinter_sexp {
+    struct sprinter vtable;
+    FILE *stream;
+    /* Top of the state stack, or NULL if the printer is not currently
+     * inside any aggregate types. */
+    struct sexp_state *state;
+};
+
+struct sexp_state {
+    struct sexp_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. */
+    aggregate_t type;
+};
+
+static struct sprinter_sexp *
+sexp_begin_value (struct sprinter *sp)
+{
+    struct sprinter_sexp *spsx = (struct sprinter_sexp *) sp;
+
+    if (spsx->state) {
+	if (! spsx->state->first)
+	    fputc (' ', spsx->stream);
+	else
+	    spsx->state->first = FALSE;
+    }
+    return spsx;
+}
+
+static void
+sexp_begin_aggregate (struct sprinter *sp, aggregate_t type)
+{
+    struct sprinter_sexp *spsx = (struct sprinter_sexp *) sp;
+    struct sexp_state *state = talloc (spsx, struct sexp_state);
+
+    fputc ('(', spsx->stream);
+    state->parent = spsx->state;
+    state->first = TRUE;
+    state->type = type;
+
+    spsx->state = state;
+}
+
+static void
+sexp_begin_map (struct sprinter *sp)
+{
+    sexp_begin_aggregate (sp, MAP);
+}
+
+static void
+sexp_begin_list (struct sprinter *sp)
+{
+    sexp_begin_aggregate (sp, LIST);
+}
+
+static void
+sexp_end (struct sprinter *sp)
+{
+    struct sprinter_sexp *spsx = (struct sprinter_sexp *) sp;
+    struct sexp_state *state = spsx->state;
+
+    fputc (')', spsx->stream);
+    spsx->state = state->parent;
+    talloc_free (state);
+    if (spsx->state == NULL)
+	fputc ('\n', spsx->stream);
+    else
+	if (spsx->state->type == MAP)
+	    fputc (')', spsx->stream);
+}
+
+static void
+sexp_string (struct sprinter *sp, const char *val)
+{
+    static const char *const escapes[] = {
+	['\"'] = "\\\"", ['\\'] = "\\\\", ['\b'] = "\\b",
+	['\f'] = "\\f",  ['\n'] = "\\n",  ['\t'] = "\\t"
+    };
+    struct sprinter_sexp *spsx = sexp_begin_value (sp);
+
+    fputc ('"', spsx->stream);
+    for (; *val; ++val) {
+	unsigned char ch = *val;
+	if (ch < ARRAY_SIZE (escapes) && escapes[ch])
+	    fputs (escapes[ch], spsx->stream);
+	else if (ch >= 32)
+	    fputc (ch, spsx->stream);
+	else
+	    fprintf (spsx->stream, "\\u%04x", ch);
+    }
+    fputc ('"', spsx->stream);
+    if (spsx->state != NULL &&  spsx->state->type == MAP)
+	fputc (')', spsx->stream);
+    spsx->state->first = FALSE;
+}
+
+static void
+sexp_integer (struct sprinter *sp, int val)
+{
+    struct sprinter_sexp *spsx = sexp_begin_value (sp);
+
+    fprintf (spsx->stream, "%d", val);
+    if (spsx->state != NULL &&  spsx->state->type == MAP)
+	fputc (')', spsx->stream);
+}
+
+static void
+sexp_boolean (struct sprinter *sp, notmuch_bool_t val)
+{
+    struct sprinter_sexp *spsx = sexp_begin_value (sp);
+
+    fputs (val ? "#t" : "#f", spsx->stream);
+    if (spsx->state != NULL &&  spsx->state->type == MAP)
+	fputc (')', spsx->stream);
+}
+
+static void
+sexp_null (struct sprinter *sp)
+{
+    struct sprinter_sexp *spsx = sexp_begin_value (sp);
+
+    fputs ("'()", spsx->stream);
+    spsx->state->first = FALSE;
+}
+
+static void
+sexp_map_key (struct sprinter *sp, const char *key)
+{
+    struct sprinter_sexp *spsx = sexp_begin_value (sp);
+
+    fputc ('(', spsx->stream);
+    fputs (key, spsx->stream);
+    fputs (" . ", spsx->stream);
+    spsx->state->first = TRUE;
+}
+
+static void
+sexp_set_prefix (unused (struct sprinter *sp), unused (const char *name))
+{
+}
+
+static void
+sexp_separator (struct sprinter *sp)
+{
+    struct sprinter_sexp *spsx = (struct sprinter_sexp *) sp;
+
+    fputc ('\n', spsx->stream);
+}
+
+struct sprinter *
+sprinter_sexp_create (const void *ctx, FILE *stream)
+{
+    static const struct sprinter_sexp template = {
+	.vtable = {
+	    .begin_map = sexp_begin_map,
+	    .begin_list = sexp_begin_list,
+	    .end = sexp_end,
+	    .string = sexp_string,
+	    .integer = sexp_integer,
+	    .boolean = sexp_boolean,
+	    .null = sexp_null,
+	    .map_key = sexp_map_key,
+	    .separator = sexp_separator,
+	    .set_prefix = sexp_set_prefix,
+	}
+    };
+    struct sprinter_sexp *res;
+
+    res = talloc (ctx, struct sprinter_sexp);
+    if (! res)
+	return NULL;
+
+    *res = template;
+    res->stream = stream;
+    return &res->vtable;
+}
diff --git a/sprinter.h b/sprinter.h
index 4241d65..c0146f6 100644
--- a/sprinter.h
+++ b/sprinter.h
@@ -55,4 +55,8 @@ sprinter_text_search_create (const void *ctx, FILE *stream);
 struct sprinter *
 sprinter_json_create (const void *ctx, FILE *stream);
 
+/* Create a new structure printer that emits S-Expressions. */
+struct sprinter *
+sprinter_sexp_create (const void *ctx, FILE *stream);
+
 #endif // NOTMUCH_SPRINTER_H
-- 
1.7.11.1

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* Re: [PATCH v5 1/3] Add support for structured output formatters.
  2012-07-13  8:11           ` [PATCH v5 1/3] Add support for structured output formatters Peter Feigl
@ 2012-07-14  1:46             ` Austin Clements
  0 siblings, 0 replies; 39+ messages in thread
From: Austin Clements @ 2012-07-14  1:46 UTC (permalink / raw)
  To: Peter Feigl; +Cc: notmuch

Quoth Peter Feigl on Jul 13 at 10:11 am:
> From: <craven@gmx.net>
> 
> This patch adds a new type sprinter_t, which is used for structured
> formatting, e.g. JSON or S-Expressions. The structure printer is the
> code from Austin Clements (id:87d34hsdx8.fsf@awakening.csail.mit.edu)
> with minor modifications.
> 
> The structure printer contains the following function pointers:
> 
> /* 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 *);
> 
> /* Set the prefix of the next component. This is purely for
>  * debugging purposes and for the unstructured text formatter.
>  */
> void (*set_prefix)(struct sprinter *, const char *);
> 
> /* 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 *);
> 
> 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 | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 50 insertions(+)
>  create mode 100644 sprinter.h
> 
> diff --git a/sprinter.h b/sprinter.h
> new file mode 100644
> index 0000000..c9cd6a6
> --- /dev/null
> +++ b/sprinter.h
> @@ -0,0 +1,50 @@
> +#ifndef NOTMUCH_SPRINTER_H
> +#define NOTMUCH_SPRINTER_H
> +
> +/* Necessary for notmuch_bool_t */
> +#include "notmuch-client.h"
> +
> +/* Structure printer interface */
> +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 *);

It looks like you lost the spaces before the argument lists, which
your previous version had.  Was this from uncrustify?  The few other
places that notmuch had function pointers do put a space here.

> +
> +    /* Start a new list/array structure.
> +     */
> +    void (*begin_list)(struct sprinter *);
> +
> +    /* End the last opened list or map structure.
> +     */
> +    void (*end)(struct sprinter *);
> +
> +    /* Set the prefix of the next component. This is purely for
> +     * debugging purposes and for the unstructured text formatter.

This should probably be more specific about what this operation does
and under what circumstances.  It also makes it sound like it only
applies to the next component, which I think is not the case.  Maybe

/* 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 *);
> +
> +    /* 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 *);
> +} sprinter_t;
> +
> +#endif // NOTMUCH_SPRINTER_H

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v5 2/3] Add structured output formatter for JSON and text.
  2012-07-13  8:11           ` [PATCH v5 2/3] Add structured output formatter for JSON and text Peter Feigl
@ 2012-07-14  2:02             ` Austin Clements
  0 siblings, 0 replies; 39+ messages in thread
From: Austin Clements @ 2012-07-14  2:02 UTC (permalink / raw)
  To: Peter Feigl; +Cc: notmuch

Quoth Peter Feigl on Jul 13 at 10:11 am:
> From: <craven@gmx.net>
> 
> 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 that prints the current plain
> text format. This passes all tests, but the exact formatting is probably
> specific to notmuch-search and cannot easily (if at all) be adapted to
> be used across all of notmuch-{search,reply,show,...}.
> ---
>  Makefile.local         |   2 +
>  sprinter-json.c        | 184 +++++++++++++++++++++++++++++++++++++++++++++++
>  sprinter-text-search.c | 190 +++++++++++++++++++++++++++++++++++++++++++++++++
>  sprinter.h             |   8 +++
>  4 files changed, 384 insertions(+)
>  create mode 100644 sprinter-json.c
>  create mode 100644 sprinter-text-search.c
> 
> diff --git a/Makefile.local b/Makefile.local
> index a890df2..b6c7e0c 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-search.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..215151d
> --- /dev/null
> +++ b/sprinter-json.c
> @@ -0,0 +1,184 @@
> +#include <stdbool.h>
> +#include <stdio.h>
> +#include <talloc.h>
> +#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,
> +	}
> +    };
> +    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-search.c b/sprinter-text-search.c
> new file mode 100644
> index 0000000..95ed9cb
> --- /dev/null
> +++ b/sprinter-text-search.c
> @@ -0,0 +1,190 @@
> +#include <stdbool.h>
> +#include <stdio.h>
> +#include <talloc.h>
> +#include "sprinter.h"
> +
> +/* "Structured printer" interface for unstructured text printing.
> + * This is at best a misuse of the interface, but it simplifies the code
> + * in notmuch-search.c considerably.
> + */
> +
> +struct sprinter_text_search {
> +    struct sprinter vtable;
> +    FILE *stream;
> +
> +    /* The current name or prefix to be printed with string/integer/boolean
> +     * data.
> +     */
> +    const char *current_name;

"current_prefix"?  Or maybe just "prefix"?  This is the only place
where you use the term "name" for this.

> +
> +    /* A flag to indicate if this is the first tag. Used in list of tags
> +     * for summary.
> +     */
> +    notmuch_bool_t first_tag;
> +};
> +
> +/* struct text_search_state { */
> +/*     struct text_search_state *parent; */
> +/* }; */

Left over scratch code?

> +
> +static notmuch_bool_t
> +current_context (struct sprinter_text_search *sptxt, const char *marker)
> +{
> +    return (sptxt->current_name != NULL
> +	    && ! strncmp (marker, sptxt->current_name, strlen (marker)));
> +}

All of this context stuff seems way more complicated than having a few
special cases in notmuch-search.c.  This is, in effect, just moving
this special casing from there to here, but since the text format is
highly irregular, attempting to generalize it is only going to
obfuscate it.

I think the simplest and most readable thing to do is to make all of
these functions no-ops (literally empty function bodies) except
text_search_separator and text_search_set_prefix, which should be like
you have them, and text_search_string:

static void
text_search_string (struct sprinter *sp, const char *val)
{
    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;

    if (sptxt->current_name)
	fprintf (sptxt->stream, "%s:", sptxt->current_name);
    print_sanitized_string (sptxt->stream, val);
}

For the summary output, you'll have to format the summary line in
notmuch-search.c, much like you did in v4, which was a much simpler
and more readable way to put together the text format lines.

> +
> +static void
> +print_sanitized_string (FILE *stream, const char *str)
> +{
> +    if (NULL == str)
> +	return;
> +
> +    for (; *str; str++) {
> +	if ((unsigned char) (*str) < 32)
> +	    fputc ('?', stream);
> +	else
> +	    fputc (*str, stream);
> +    }
> +}
> +
> +static void
> +text_search_begin_map (unused (struct sprinter *sp))
> +{
> +}
> +
> +static void
> +text_search_begin_list (struct sprinter *sp)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    if (current_context (sptxt, "tags")) {
> +	fputs (" (", sptxt->stream);
> +	sptxt->first_tag = TRUE;
> +    }
> +}
> +
> +static void
> +text_search_end (struct sprinter *sp)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    if (current_context (sptxt, "tags")) {
> +	fputc (')', sptxt->stream);
> +	sptxt->current_name = NULL;
> +    }
> +}
> +
> +static void
> +text_search_string (struct sprinter *sp, const char *val)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    if (sptxt->current_name != NULL) {
> +	if (current_context (sptxt, "thread"))
> +	    fprintf ( sptxt->stream, "thread:%s ", val);
> +	else if (current_context (sptxt, "date_relative"))
> +	    fprintf ( sptxt->stream, "%12s ", val);
> +	else if (current_context (sptxt, "authors")) {
> +	    print_sanitized_string (sptxt->stream, val);
> +	    fputs ("; ", sptxt->stream);
> +	} else if (current_context (sptxt, "subject"))
> +	    print_sanitized_string (sptxt->stream, val);
> +	else if (current_context (sptxt, "tags")) {
> +	    if (! sptxt->first_tag)
> +		fputc (' ', sptxt->stream);
> +	    else
> +		sptxt->first_tag = FALSE;
> +
> +	    fputs (val, sptxt->stream);
> +	} else {
> +	    fputs (sptxt->current_name, sptxt->stream);
> +	    fputc (':', sptxt->stream);
> +	    fputs (val, sptxt->stream);
> +	}
> +    } else {
> +	fputs (val, sptxt->stream);
> +    }
> +}
> +
> +static void
> +text_search_integer (struct sprinter *sp, int val)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    if (sptxt->current_name != NULL) {
> +	if (current_context (sptxt, "matched"))
> +	    fprintf ( sptxt->stream, "[%d/", val);
> +	else if (current_context (sptxt, "total"))
> +	    fprintf ( sptxt->stream, "%d] ", val);
> +    } else
> +	fprintf (sptxt->stream, "%d", val);
> +}
> +
> +static void
> +text_search_boolean (struct sprinter *sp, notmuch_bool_t val)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    fputs (val ? "true" : "false", sptxt->stream);
> +}
> +
> +static void
> +text_search_null (struct sprinter *sp)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    fputs ("null", sptxt->stream);
> +}
> +
> +static void
> +text_search_map_key (struct sprinter *sp, const char *key)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    sptxt->current_name = key;
> +}
> +
> +static void
> +text_search_separator (struct sprinter *sp)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    fputc ('\n', sptxt->stream);
> +}
> +
> +static void
> +text_search_set_prefix (struct sprinter *sp, const char *name)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    sptxt->current_name = name;
> +}
> +
> +struct sprinter *
> +sprinter_text_search_create (const void *ctx, FILE *stream)
> +{
> +    static const struct sprinter_text_search template = {
> +	.vtable = {
> +	    .begin_map = text_search_begin_map,
> +	    .begin_list = text_search_begin_list,
> +	    .end = text_search_end,
> +	    .string = text_search_string,
> +	    .integer = text_search_integer,
> +	    .boolean = text_search_boolean,
> +	    .null = text_search_null,
> +	    .map_key = text_search_map_key,
> +	    .separator = text_search_separator,
> +	    .set_prefix = text_search_set_prefix,
> +	}
> +    };
> +    struct sprinter_text_search *res;
> +
> +    res = talloc (ctx, struct sprinter_text_search);
> +    if (! res)
> +	return NULL;
> +
> +    *res = template;
> +    res->stream = stream;
> +    return &res->vtable;
> +}
> diff --git a/sprinter.h b/sprinter.h
> index c9cd6a6..4241d65 100644
> --- a/sprinter.h
> +++ b/sprinter.h
> @@ -47,4 +47,12 @@ typedef struct sprinter {
>      void (*separator)(struct sprinter *);
>  } sprinter_t;
>  
> +/* Create a new unstructured printer that emits the default Text format for search. */

Wrap at 70 columns.

> +struct sprinter *
> +sprinter_text_search_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

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v5 3/3] Use the structured formatters in notmuch-search.c.
  2012-07-13  8:11           ` [PATCH v5 3/3] Use the structured formatters in notmuch-search.c Peter Feigl
@ 2012-07-14  2:09             ` Austin Clements
  2012-07-16  8:34               ` [PATCH v6 0/3] notmuch-reply: Structured Formatters craven
  0 siblings, 1 reply; 39+ messages in thread
From: Austin Clements @ 2012-07-14  2:09 UTC (permalink / raw)
  To: Peter Feigl; +Cc: notmuch

Just one comment below.  Otherwise this patch LGTM (though some of my
comments on the previous patch will affect this one).  It's nice to
see all of that old newline logic disappear.

Quoth Peter Feigl on Jul 13 at 10:11 am:
> From: <craven@gmx.net>
> 
> This patch switches from the current ad-hoc printer to the structured
> output formatter in sprinter.h, sprinter-text-search.c and
> sprinter-json.c.
> 
> The JSON tests are changed slightly in order to make them PASS for the
> new structured output formatter.
> 
> The Text tests pass without adaptation.
> ---
>  notmuch-search.c | 292 +++++++++++--------------------------------------------
>  test/json        |  18 +---
>  2 files changed, 58 insertions(+), 252 deletions(-)
> 
> diff --git a/notmuch-search.c b/notmuch-search.c
> index 3be296d..99fddac 100644
> --- a/notmuch-search.c
> +++ b/notmuch-search.c
> @@ -19,6 +19,7 @@
>   */
>  
>  #include "notmuch-client.h"
> +#include "sprinter.h"
>  
>  typedef enum {
>      OUTPUT_SUMMARY,
> @@ -28,175 +29,8 @@ typedef enum {
>      OUTPUT_TAGS
>  } output_t;
>  
> -typedef struct search_format {
> -    const char *results_start;
> -    const char *item_start;
> -    void (*item_id) (const void *ctx,
> -		     const char *item_type,
> -		     const char *item_id);
> -    void (*thread_summary) (const void *ctx,
> -			    const char *thread_id,
> -			    const time_t date,
> -			    const int matched,
> -			    const int total,
> -			    const char *authors,
> -			    const char *subject);
> -    const char *tag_start;
> -    const char *tag;
> -    const char *tag_sep;
> -    const char *tag_end;
> -    const char *item_sep;
> -    const char *item_end;
> -    const char *results_end;
> -    const char *results_null;
> -} search_format_t;
> -
> -static void
> -format_item_id_text (const void *ctx,
> -		     const char *item_type,
> -		     const char *item_id);
> -
> -static void
> -format_thread_text (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject);
> -static const search_format_t format_text = {
> -    "",
> -	"",
> -	    format_item_id_text,
> -	    format_thread_text,
> -	    " (",
> -		"%s", " ",
> -	    ")", "\n",
> -	"",
> -    "\n",
> -    "",
> -};
> -
> -static void
> -format_item_id_json (const void *ctx,
> -		     const char *item_type,
> -		     const char *item_id);
> -
> -static void
> -format_thread_json (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject);
> -
> -/* Any changes to the JSON format should be reflected in the file
> - * devel/schemata. */
> -static const search_format_t format_json = {
> -    "[",
> -	"{",
> -	    format_item_id_json,
> -	    format_thread_json,
> -	    "\"tags\": [",
> -		"\"%s\"", ", ",
> -	    "]", ",\n",
> -	"}",
> -    "]\n",
> -    "]\n",
> -};
> -
> -static void
> -format_item_id_text (unused (const void *ctx),
> -		     const char *item_type,
> -		     const char *item_id)
> -{
> -    printf ("%s%s", item_type, item_id);
> -}
> -
> -static char *
> -sanitize_string (const void *ctx, const char *str)
> -{
> -    char *out, *loop;
> -
> -    if (NULL == str)
> -	return NULL;
> -
> -    loop = out = talloc_strdup (ctx, str);
> -
> -    for (; *loop; loop++) {
> -	if ((unsigned char)(*loop) < 32)
> -	    *loop = '?';
> -    }
> -    return out;
> -}
> -
> -static void
> -format_thread_text (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject)
> -{
> -    void *ctx_quote = talloc_new (ctx);
> -
> -    printf ("thread:%s %12s [%d/%d] %s; %s",
> -	    thread_id,
> -	    notmuch_time_relative_date (ctx, date),
> -	    matched,
> -	    total,
> -	    sanitize_string (ctx_quote, authors),
> -	    sanitize_string (ctx_quote, subject));
> -
> -    talloc_free (ctx_quote);
> -}
> -
> -static void
> -format_item_id_json (const void *ctx,
> -		     unused (const char *item_type),
> -		     const char *item_id)
> -{
> -    void *ctx_quote = talloc_new (ctx);
> -
> -    printf ("%s", json_quote_str (ctx_quote, item_id));
> -
> -    talloc_free (ctx_quote);
> -    
> -}
> -
> -static void
> -format_thread_json (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject)
> -{
> -    void *ctx_quote = talloc_new (ctx);
> -
> -    printf ("\"thread\": %s,\n"
> -	    "\"timestamp\": %ld,\n"
> -	    "\"date_relative\": \"%s\",\n"
> -	    "\"matched\": %d,\n"
> -	    "\"total\": %d,\n"
> -	    "\"authors\": %s,\n"
> -	    "\"subject\": %s,\n",
> -	    json_quote_str (ctx_quote, thread_id),
> -	    date,
> -	    notmuch_time_relative_date (ctx, date),
> -	    matched,
> -	    total,
> -	    json_quote_str (ctx_quote, authors),
> -	    json_quote_str (ctx_quote, subject));
> -
> -    talloc_free (ctx_quote);
> -}
> -
>  static int
> -do_search_threads (const search_format_t *format,
> +do_search_threads (sprinter_t *format,
>  		   notmuch_query_t *query,
>  		   notmuch_sort_t sort,
>  		   output_t output,
> @@ -207,7 +41,6 @@ do_search_threads (const search_format_t *format,
>      notmuch_threads_t *threads;
>      notmuch_tags_t *tags;
>      time_t date;
> -    int first_thread = 1;
>      int i;
>  
>      if (offset < 0) {
> @@ -220,14 +53,12 @@ do_search_threads (const search_format_t *format,
>      if (threads == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    format->begin_list (format);
>  
>      for (i = 0;
>  	 notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
>  	 notmuch_threads_move_to_next (threads), i++)
>      {
> -	int first_tag = 1;
> -
>  	thread = notmuch_threads_get (threads);
>  
>  	if (i < offset) {
> @@ -235,60 +66,65 @@ do_search_threads (const search_format_t *format,
>  	    continue;
>  	}
>  
> -	if (! first_thread)
> -	    fputs (format->item_sep, stdout);
> -
>  	if (output == OUTPUT_THREADS) {
> -	    format->item_id (thread, "thread:",
> -			     notmuch_thread_get_thread_id (thread));
> +	    format->set_prefix (format, "thread");
> +	    format->string (format,
> +			    notmuch_thread_get_thread_id (thread));
> +	    format->separator (format);
>  	} else { /* output == OUTPUT_SUMMARY */
> -	    fputs (format->item_start, stdout);
> +	    void *ctx_quote = talloc_new (thread);
> +
> +	    format->begin_map (format);
>  
>  	    if (sort == NOTMUCH_SORT_OLDEST_FIRST)
>  		date = notmuch_thread_get_oldest_date (thread);
>  	    else
>  		date = notmuch_thread_get_newest_date (thread);
>  
> -	    format->thread_summary (thread,
> -				    notmuch_thread_get_thread_id (thread),
> -				    date,
> -				    notmuch_thread_get_matched_messages (thread),
> -				    notmuch_thread_get_total_messages (thread),
> -				    notmuch_thread_get_authors (thread),
> -				    notmuch_thread_get_subject (thread));
> -
> -	    fputs (format->tag_start, stdout);
> +	    format->map_key (format, "thread");
> +	    format->string (format, notmuch_thread_get_thread_id (thread));
> +	    format->map_key (format, "timestamp");
> +	    format->integer (format, date);
> +	    format->map_key (format, "date_relative");
> +	    format->string (format, notmuch_time_relative_date (ctx_quote, date));
> +	    format->map_key (format, "matched");
> +	    format->integer (format, notmuch_thread_get_matched_messages (thread));
> +	    format->map_key (format, "total");
> +	    format->integer (format, notmuch_thread_get_total_messages (thread));
> +	    format->map_key (format, "authors");
> +	    format->string (format, notmuch_thread_get_authors (thread));
> +	    format->map_key (format, "subject");
> +	    format->string (format, notmuch_thread_get_subject (thread));
> +
> +	    talloc_free (ctx_quote);
> +
> +	    format->map_key (format, "tags");
> +	    format->begin_list (format);
>  
>  	    for (tags = notmuch_thread_get_tags (thread);
>  		 notmuch_tags_valid (tags);
>  		 notmuch_tags_move_to_next (tags))
>  	    {
> -		if (! first_tag)
> -		    fputs (format->tag_sep, stdout);
> -		printf (format->tag, notmuch_tags_get (tags));
> -		first_tag = 0;
> -	    }
> +		const char *tag = notmuch_tags_get (tags);
>  
> -	    fputs (format->tag_end, stdout);
> +		format->string (format, tag);
> +	    }
>  
> -	    fputs (format->item_end, stdout);
> +	    format->end (format);
> +	    format->end (format);
> +	    format->separator (format);
>  	}
>  
> -	first_thread = 0;
> -
>  	notmuch_thread_destroy (thread);
>      }
>  
> -    if (first_thread)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    format->end (format);
>  
>      return 0;
>  }
>  
>  static int
> -do_search_messages (const search_format_t *format,
> +do_search_messages (sprinter_t *format,
>  		    notmuch_query_t *query,
>  		    output_t output,
>  		    int offset,
> @@ -297,7 +133,6 @@ do_search_messages (const search_format_t *format,
>      notmuch_message_t *message;
>      notmuch_messages_t *messages;
>      notmuch_filenames_t *filenames;
> -    int first_message = 1;
>      int i;
>  
>      if (offset < 0) {
> @@ -310,7 +145,7 @@ do_search_messages (const search_format_t *format,
>      if (messages == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    format->begin_list (format);
>  
>      for (i = 0;
>  	 notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
> @@ -328,24 +163,17 @@ do_search_messages (const search_format_t *format,
>  		 notmuch_filenames_valid (filenames);
>  		 notmuch_filenames_move_to_next (filenames))
>  	    {
> -		if (! first_message)
> -		    fputs (format->item_sep, stdout);
> -
> -		format->item_id (message, "",
> -				 notmuch_filenames_get (filenames));
> -
> -		first_message = 0;
> +		format->string (format, notmuch_filenames_get (filenames));
> +		format->separator (format);
>  	    }
> -	    
> +
>  	    notmuch_filenames_destroy( filenames );
>  
>  	} else { /* output == OUTPUT_MESSAGES */
> -	    if (! first_message)
> -		fputs (format->item_sep, stdout);
> -
> -	    format->item_id (message, "id:",
> -			     notmuch_message_get_message_id (message));
> -	    first_message = 0;
> +	    format->set_prefix (format, "id");
> +	    format->string (format,
> +			    notmuch_message_get_message_id (message));
> +	    format->separator (format);
>  	}
>  
>  	notmuch_message_destroy (message);
> @@ -353,23 +181,19 @@ do_search_messages (const search_format_t *format,
>  
>      notmuch_messages_destroy (messages);
>  
> -    if (first_message)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    format->end (format);
>  
>      return 0;
>  }
>  
>  static int
>  do_search_tags (notmuch_database_t *notmuch,
> -		const search_format_t *format,
> +		sprinter_t *format,
>  		notmuch_query_t *query)
>  {
>      notmuch_messages_t *messages = NULL;
>      notmuch_tags_t *tags;
>      const char *tag;
> -    int first_tag = 1;
>  
>      /* should the following only special case if no excluded terms
>       * specified? */
> @@ -387,7 +211,7 @@ do_search_tags (notmuch_database_t *notmuch,
>      if (tags == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    format->begin_list (format);
>  
>      for (;
>  	 notmuch_tags_valid (tags);
> @@ -395,12 +219,9 @@ do_search_tags (notmuch_database_t *notmuch,
>      {
>  	tag = notmuch_tags_get (tags);
>  
> -	if (! first_tag)
> -	    fputs (format->item_sep, stdout);
> +	format->string (format, tag);
> +	format->separator (format);
>  
> -	format->item_id (tags, "", tag);
> -
> -	first_tag = 0;
>      }
>  
>      notmuch_tags_destroy (tags);
> @@ -408,10 +229,7 @@ do_search_tags (notmuch_database_t *notmuch,
>      if (messages)
>  	notmuch_messages_destroy (messages);
>  
> -    if (first_tag)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    format->end (format);
>  
>      return 0;
>  }
> @@ -430,7 +248,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>      notmuch_query_t *query;
>      char *query_str;
>      notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
> -    const search_format_t *format = &format_text;
> +    sprinter_t *format = NULL;
>      int opt_index, ret;
>      output_t output = OUTPUT_SUMMARY;
>      int offset = 0;
> @@ -475,10 +293,10 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>  
>      switch (format_sel) {
>      case NOTMUCH_FORMAT_TEXT:
> -	format = &format_text;
> +	format = sprinter_text_search_create (ctx, stdout);
>  	break;
>      case NOTMUCH_FORMAT_JSON:
> -	format = &format_json;
> +	format = sprinter_json_create (ctx, stdout);
>  	break;
>      }
>  
> @@ -546,5 +364,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>      notmuch_query_destroy (query);
>      notmuch_database_destroy (notmuch);
>  
> +    talloc_free (format);
> +
>      return ret;
>  }
> diff --git a/test/json b/test/json
> index 6439788..f0ebf08 100755
> --- a/test/json
> +++ b/test/json
> @@ -10,14 +10,7 @@ test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"e
>  test_begin_subtest "Search message: json"
>  add_message "[subject]=\"json-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-search-message\""
>  output=$(notmuch search --format=json "json-search-message" | notmuch_search_sanitize)

If you pipe this through notmuch_json_show_sanitize, it'll make the
test output much cleaner and closer to the original output (probably
not exactly the same, unfortunately).

> -test_expect_equal "$output" "[{\"thread\": \"XXX\",
> -\"timestamp\": 946728000,
> -\"date_relative\": \"2000-01-01\",
> -\"matched\": 1,
> -\"total\": 1,
> -\"authors\": \"Notmuch Test Suite\",
> -\"subject\": \"json-search-subject\",
> -\"tags\": [\"inbox\", \"unread\"]}]"
> +test_expect_equal "$output" "[{\"thread\": \"XXX\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"matched\": 1, \"total\": 1, \"authors\": \"Notmuch Test Suite\", \"subject\": \"json-search-subject\", \"tags\": [\"inbox\", \"unread\"]}]"
>  
>  test_begin_subtest "Show message: json, utf-8"
>  add_message "[subject]=\"json-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
> @@ -40,13 +33,6 @@ test_expect_equal "$output" "[[[{\"id\": \"$id\", \"match\": true, \"excluded\":
>  test_begin_subtest "Search message: json, utf-8"
>  add_message "[subject]=\"json-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
>  output=$(notmuch search --format=json "jsön-search-méssage" | notmuch_search_sanitize)

Same here.

> -test_expect_equal "$output" "[{\"thread\": \"XXX\",
> -\"timestamp\": 946728000,
> -\"date_relative\": \"2000-01-01\",
> -\"matched\": 1,
> -\"total\": 1,
> -\"authors\": \"Notmuch Test Suite\",
> -\"subject\": \"json-search-utf8-body-sübjéct\",
> -\"tags\": [\"inbox\", \"unread\"]}]"
> +test_expect_equal "$output" "[{\"thread\": \"XXX\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"matched\": 1, \"total\": 1, \"authors\": \"Notmuch Test Suite\", \"subject\": \"json-search-utf8-body-sübjéct\", \"tags\": [\"inbox\", \"unread\"]}]"
>  
>  test_done

^ permalink raw reply	[flat|nested] 39+ messages in thread

* [PATCH v6 0/3] notmuch-reply: Structured Formatters
  2012-07-14  2:09             ` Austin Clements
@ 2012-07-16  8:34               ` craven
  2012-07-16  8:35                 ` [PATCH v6 1/3] Add support for structured output formatters craven
                                   ` (2 more replies)
  0 siblings, 3 replies; 39+ messages in thread
From: craven @ 2012-07-16  8:34 UTC (permalink / raw)
  To: notmuch

Currently there is no easy way to add support for different structured
formatters (like JSON). For example, adding support for S-Expressions
would result in code duplication.

This patch series amends the situation by introducing structured
formatters, which allow different implementations of structures like
lists, maps, strings and numbers.

The new code in sprinter.h and sprinter-json.c can be used instead of
the current ad-hoc output in all parts of notmuch, a patch for
notmuch-search.c is included.

In a later patch, all other parts of notmuch should be adapted to the
structured formatters, and the creation of formatters should be
centralised (to make adding new formatters easier).

A "structured" formatter is provided for notmuch-search that prints the
current text format. This removes almost all the special-casing from
notmuch-search.c.

^ permalink raw reply	[flat|nested] 39+ messages in thread

* [PATCH v6 1/3] Add support for structured output formatters.
  2012-07-16  8:34               ` [PATCH v6 0/3] notmuch-reply: Structured Formatters craven
@ 2012-07-16  8:35                 ` craven
  2012-07-16  8:35                 ` [PATCH v6 2/3] Add structured output formatter for JSON and plain text craven
  2012-07-16  8:35                 ` [PATCH v6 3/3] Use the structured formatters in notmuch-search.c craven
  2 siblings, 0 replies; 39+ messages in thread
From: craven @ 2012-07-16  8:35 UTC (permalink / raw)
  To: notmuch

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 *);

To support the plain text format properly, the following two additional
functions 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 *);

    /* Return TRUE if this is a text printer. Some special casing
     * applies to the pure plain text printer. This should always
     * return FALSE in custom structured output printers.
     */
    notmuch_bool_t (*is_text_printer) (struct sprinter *);

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 | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 60 insertions(+)
 create mode 100644 sprinter.h

diff --git a/sprinter.h b/sprinter.h
new file mode 100644
index 0000000..dc09a15
--- /dev/null
+++ b/sprinter.h
@@ -0,0 +1,60 @@
+#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 *);
+
+    /* Return TRUE if this is a text printer. Some special casing
+     * applies to the pure plain text printer. This should always
+     * return FALSE in custom structured output printers.
+     */ 
+    notmuch_bool_t (*is_text_printer) (struct sprinter *);
+} sprinter_t;
+
+#endif // NOTMUCH_SPRINTER_H
-- 
1.7.11.1

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH v6 2/3] Add structured output formatter for JSON and plain text.
  2012-07-16  8:34               ` [PATCH v6 0/3] notmuch-reply: Structured Formatters craven
  2012-07-16  8:35                 ` [PATCH v6 1/3] Add support for structured output formatters craven
@ 2012-07-16  8:35                 ` craven
  2012-07-18 19:48                   ` Austin Clements
  2012-07-16  8:35                 ` [PATCH v6 3/3] Use the structured formatters in notmuch-search.c craven
  2 siblings, 1 reply; 39+ messages in thread
From: craven @ 2012-07-16  8:35 UTC (permalink / raw)
  To: notmuch

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        | 191 +++++++++++++++++++++++++++++++++++++++++++++++++
 sprinter-text-search.c | 146 +++++++++++++++++++++++++++++++++++++
 sprinter.h             |   9 +++
 4 files changed, 348 insertions(+)
 create mode 100644 sprinter-json.c
 create mode 100644 sprinter-text-search.c

diff --git a/Makefile.local b/Makefile.local
index a890df2..4f534f1 100644
--- a/Makefile.local
+++ b/Makefile.local
@@ -290,6 +290,8 @@ notmuch_client_srcs =		\
 	notmuch-show.c		\
 	notmuch-tag.c		\
 	notmuch-time.c		\
+	sprinter-text-search.c  \
+	sprinter-json.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..a93a390
--- /dev/null
+++ b/sprinter-json.c
@@ -0,0 +1,191 @@
+#include <stdbool.h>
+#include <stdio.h>
+#include <talloc.h>
+#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;
+}
+
+static notmuch_bool_t
+json_is_text_printer (unused (struct sprinter *sp))
+{
+    return FALSE;
+}
+
+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 = json_is_text_printer,
+	}
+    };
+    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-search.c b/sprinter-text-search.c
new file mode 100644
index 0000000..b115722
--- /dev/null
+++ b/sprinter-text-search.c
@@ -0,0 +1,146 @@
+#include <stdbool.h>
+#include <stdio.h>
+#include <talloc.h>
+#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_search {
+    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
+print_sanitized_string (FILE *stream, const char *str)
+{
+    if (NULL == str)
+	return;
+
+    for (; *str; str++) {
+	if ((unsigned char) (*str) < 32)
+	    fputc ('?', stream);
+	else
+	    fputc (*str, stream);
+    }
+}
+
+static void
+text_search_string (struct sprinter *sp, const char *val)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    if (sptxt->current_prefix != NULL)
+	fprintf (sptxt->stream, "%s:", sptxt->current_prefix);
+
+    print_sanitized_string (sptxt->stream, val);
+}
+
+static void
+text_search_integer (struct sprinter *sp, int val)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    fprintf (sptxt->stream, "%d", val);
+}
+
+static void
+text_search_boolean (struct sprinter *sp, notmuch_bool_t val)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    fputs (val ? "true" : "false", sptxt->stream);
+}
+
+static void
+text_search_separator (struct sprinter *sp)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    fputc ('\n', sptxt->stream);
+}
+
+static void
+text_search_set_prefix (struct sprinter *sp, const char *prefix)
+{
+    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
+
+    sptxt->current_prefix = prefix;
+}
+
+static notmuch_bool_t
+text_search_is_text_printer (unused (struct sprinter *sp))
+{
+    return TRUE;
+}
+
+/* The structure functions begin_map, begin_list, end and map_key
+ * don't do anything in the text formatter.
+ */
+
+static void
+text_search_begin_map (unused (struct sprinter *sp))
+{
+}
+
+static void
+text_search_begin_list (unused (struct sprinter *sp))
+{
+}
+
+static void
+text_search_end (unused (struct sprinter *sp))
+{
+}
+
+static void
+text_search_null (unused (struct sprinter *sp))
+{
+}
+
+static void
+text_search_map_key (unused (struct sprinter *sp), unused (const char *key))
+{
+}
+
+struct sprinter *
+sprinter_text_search_create (const void *ctx, FILE *stream)
+{
+    static const struct sprinter_text_search template = {
+	.vtable = {
+	    .begin_map = text_search_begin_map,
+	    .begin_list = text_search_begin_list,
+	    .end = text_search_end,
+	    .string = text_search_string,
+	    .integer = text_search_integer,
+	    .boolean = text_search_boolean,
+	    .null = text_search_null,
+	    .map_key = text_search_map_key,
+	    .separator = text_search_separator,
+	    .set_prefix = text_search_set_prefix,
+	    .is_text_printer = text_search_is_text_printer,
+	}
+    };
+    struct sprinter_text_search *res;
+
+    res = talloc (ctx, struct sprinter_text_search);
+    if (! res)
+	return NULL;
+
+    *res = template;
+    res->stream = stream;
+    return &res->vtable;
+}
diff --git a/sprinter.h b/sprinter.h
index dc09a15..7ec6344 100644
--- a/sprinter.h
+++ b/sprinter.h
@@ -57,4 +57,13 @@ typedef struct sprinter {
     notmuch_bool_t (*is_text_printer) (struct sprinter *);
 } sprinter_t;
 
+/* Create a new unstructured printer that emits the default text format
+ * for "notmuch search". */
+struct sprinter *
+sprinter_text_search_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
-- 
1.7.11.1

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH v6 3/3] Use the structured formatters in notmuch-search.c.
  2012-07-16  8:34               ` [PATCH v6 0/3] notmuch-reply: Structured Formatters craven
  2012-07-16  8:35                 ` [PATCH v6 1/3] Add support for structured output formatters craven
  2012-07-16  8:35                 ` [PATCH v6 2/3] Add structured output formatter for JSON and plain text craven
@ 2012-07-16  8:35                 ` craven
  2012-07-18 19:56                   ` Austin Clements
  2 siblings, 1 reply; 39+ messages in thread
From: craven @ 2012-07-16  8:35 UTC (permalink / raw)
  To: notmuch

This patch switches from the current ad-hoc printer to the structured
formatters in sprinter.h, sprinter-text-search.c and sprinter-json.c.

The JSON tests are changed slightly in order to make them PASS for the
new structured output formatter.

The text tests pass without adaptation.
---
 notmuch-search.c | 300 ++++++++++++++++---------------------------------------
 test/json        |  18 +---
 2 files changed, 86 insertions(+), 232 deletions(-)

diff --git a/notmuch-search.c b/notmuch-search.c
index 3be296d..cf927e6 100644
--- a/notmuch-search.c
+++ b/notmuch-search.c
@@ -19,6 +19,7 @@
  */
 
 #include "notmuch-client.h"
+#include "sprinter.h"
 
 typedef enum {
     OUTPUT_SUMMARY,
@@ -28,92 +29,6 @@ typedef enum {
     OUTPUT_TAGS
 } output_t;
 
-typedef struct search_format {
-    const char *results_start;
-    const char *item_start;
-    void (*item_id) (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-    void (*thread_summary) (const void *ctx,
-			    const char *thread_id,
-			    const time_t date,
-			    const int matched,
-			    const int total,
-			    const char *authors,
-			    const char *subject);
-    const char *tag_start;
-    const char *tag;
-    const char *tag_sep;
-    const char *tag_end;
-    const char *item_sep;
-    const char *item_end;
-    const char *results_end;
-    const char *results_null;
-} search_format_t;
-
-static void
-format_item_id_text (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-
-static void
-format_thread_text (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject);
-static const search_format_t format_text = {
-    "",
-	"",
-	    format_item_id_text,
-	    format_thread_text,
-	    " (",
-		"%s", " ",
-	    ")", "\n",
-	"",
-    "\n",
-    "",
-};
-
-static void
-format_item_id_json (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject);
-
-/* Any changes to the JSON format should be reflected in the file
- * devel/schemata. */
-static const search_format_t format_json = {
-    "[",
-	"{",
-	    format_item_id_json,
-	    format_thread_json,
-	    "\"tags\": [",
-		"\"%s\"", ", ",
-	    "]", ",\n",
-	"}",
-    "]\n",
-    "]\n",
-};
-
-static void
-format_item_id_text (unused (const void *ctx),
-		     const char *item_type,
-		     const char *item_id)
-{
-    printf ("%s%s", item_type, item_id);
-}
-
 static char *
 sanitize_string (const void *ctx, const char *str)
 {
@@ -131,72 +46,8 @@ sanitize_string (const void *ctx, const char *str)
     return out;
 }
 
-static void
-format_thread_text (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("thread:%s %12s [%d/%d] %s; %s",
-	    thread_id,
-	    notmuch_time_relative_date (ctx, date),
-	    matched,
-	    total,
-	    sanitize_string (ctx_quote, authors),
-	    sanitize_string (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
-static void
-format_item_id_json (const void *ctx,
-		     unused (const char *item_type),
-		     const char *item_id)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("%s", json_quote_str (ctx_quote, item_id));
-
-    talloc_free (ctx_quote);
-    
-}
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("\"thread\": %s,\n"
-	    "\"timestamp\": %ld,\n"
-	    "\"date_relative\": \"%s\",\n"
-	    "\"matched\": %d,\n"
-	    "\"total\": %d,\n"
-	    "\"authors\": %s,\n"
-	    "\"subject\": %s,\n",
-	    json_quote_str (ctx_quote, thread_id),
-	    date,
-	    notmuch_time_relative_date (ctx, date),
-	    matched,
-	    total,
-	    json_quote_str (ctx_quote, authors),
-	    json_quote_str (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
 static int
-do_search_threads (const search_format_t *format,
+do_search_threads (sprinter_t *format,
 		   notmuch_query_t *query,
 		   notmuch_sort_t sort,
 		   output_t output,
@@ -207,7 +58,6 @@ do_search_threads (const search_format_t *format,
     notmuch_threads_t *threads;
     notmuch_tags_t *tags;
     time_t date;
-    int first_thread = 1;
     int i;
 
     if (offset < 0) {
@@ -220,14 +70,12 @@ do_search_threads (const search_format_t *format,
     if (threads == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (i = 0;
 	 notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
 	 notmuch_threads_move_to_next (threads), i++)
     {
-	int first_tag = 1;
-
 	thread = notmuch_threads_get (threads);
 
 	if (i < offset) {
@@ -235,60 +83,96 @@ do_search_threads (const search_format_t *format,
 	    continue;
 	}
 
-	if (! first_thread)
-	    fputs (format->item_sep, stdout);
-
 	if (output == OUTPUT_THREADS) {
-	    format->item_id (thread, "thread:",
-			     notmuch_thread_get_thread_id (thread));
+	    format->set_prefix (format, "thread");
+	    format->string (format,
+			    notmuch_thread_get_thread_id (thread));
+	    format->separator (format);
 	} else { /* output == OUTPUT_SUMMARY */
-	    fputs (format->item_start, stdout);
+	    void *ctx_quote = talloc_new (thread);
+	    const char *authors = notmuch_thread_get_authors (thread);
+	    const char *subject = notmuch_thread_get_subject (thread);
+	    const char *thread_id = notmuch_thread_get_thread_id (thread);
+	    int matched = notmuch_thread_get_matched_messages (thread);
+	    int total = notmuch_thread_get_total_messages (thread);
+	    const char *relative_date = NULL;
+	    notmuch_bool_t first_tag = TRUE;
+
+	    format->begin_map (format);
 
 	    if (sort == NOTMUCH_SORT_OLDEST_FIRST)
 		date = notmuch_thread_get_oldest_date (thread);
 	    else
 		date = notmuch_thread_get_newest_date (thread);
 
-	    format->thread_summary (thread,
-				    notmuch_thread_get_thread_id (thread),
-				    date,
-				    notmuch_thread_get_matched_messages (thread),
-				    notmuch_thread_get_total_messages (thread),
-				    notmuch_thread_get_authors (thread),
-				    notmuch_thread_get_subject (thread));
+	    relative_date = notmuch_time_relative_date (ctx_quote, date);
+
+	    if (format->is_text_printer (format)) {
+                /* Special case for the text formatter */
+		printf ("thread:%s %12s [%d/%d] %s; %s (",
+			thread_id,
+			relative_date,
+			matched,
+			total,
+			sanitize_string (ctx_quote, authors),
+			sanitize_string (ctx_quote, subject));
+	    } else { /* Structured Output */
+		format->map_key (format, "thread");
+		format->string (format, thread_id);
+		format->map_key (format, "timestamp");
+		format->integer (format, date);
+		format->map_key (format, "date_relative");
+		format->string (format, relative_date);
+		format->map_key (format, "matched");
+		format->integer (format, matched);
+		format->map_key (format, "total");
+		format->integer (format, total);
+		format->map_key (format, "authors");
+		format->string (format, authors);
+		format->map_key (format, "subject");
+		format->string (format, subject);
+	    }
+
+	    talloc_free (ctx_quote);
 
-	    fputs (format->tag_start, stdout);
+	    format->map_key (format, "tags");
+	    format->begin_list (format);
 
 	    for (tags = notmuch_thread_get_tags (thread);
 		 notmuch_tags_valid (tags);
 		 notmuch_tags_move_to_next (tags))
 	    {
-		if (! first_tag)
-		    fputs (format->tag_sep, stdout);
-		printf (format->tag, notmuch_tags_get (tags));
-		first_tag = 0;
+		const char *tag = notmuch_tags_get (tags);
+
+		if (format->is_text_printer (format)) {
+                  /* Special case for the text formatter */
+		    if (first_tag)
+			first_tag = FALSE;
+		    else
+			fputc (' ', stdout);
+		    fputs (tag, stdout);
+		} else /* Structured Output */
+		    format->string (format, tag);
 	    }
 
-	    fputs (format->tag_end, stdout);
+	    if (format->is_text_printer (format))
+		printf (")");
 
-	    fputs (format->item_end, stdout);
+	    format->end (format);
+	    format->end (format);
+	    format->separator (format);
 	}
 
-	first_thread = 0;
-
 	notmuch_thread_destroy (thread);
     }
 
-    if (first_thread)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
 
 static int
-do_search_messages (const search_format_t *format,
+do_search_messages (sprinter_t *format,
 		    notmuch_query_t *query,
 		    output_t output,
 		    int offset,
@@ -297,7 +181,6 @@ do_search_messages (const search_format_t *format,
     notmuch_message_t *message;
     notmuch_messages_t *messages;
     notmuch_filenames_t *filenames;
-    int first_message = 1;
     int i;
 
     if (offset < 0) {
@@ -310,7 +193,7 @@ do_search_messages (const search_format_t *format,
     if (messages == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (i = 0;
 	 notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
@@ -328,24 +211,17 @@ do_search_messages (const search_format_t *format,
 		 notmuch_filenames_valid (filenames);
 		 notmuch_filenames_move_to_next (filenames))
 	    {
-		if (! first_message)
-		    fputs (format->item_sep, stdout);
-
-		format->item_id (message, "",
-				 notmuch_filenames_get (filenames));
-
-		first_message = 0;
+		format->string (format, notmuch_filenames_get (filenames));
+		format->separator (format);
 	    }
 	    
 	    notmuch_filenames_destroy( filenames );
 
 	} else { /* output == OUTPUT_MESSAGES */
-	    if (! first_message)
-		fputs (format->item_sep, stdout);
-
-	    format->item_id (message, "id:",
-			     notmuch_message_get_message_id (message));
-	    first_message = 0;
+	    format->set_prefix (format, "id");
+	    format->string (format,
+			    notmuch_message_get_message_id (message));
+	    format->separator (format);
 	}
 
 	notmuch_message_destroy (message);
@@ -353,23 +229,19 @@ do_search_messages (const search_format_t *format,
 
     notmuch_messages_destroy (messages);
 
-    if (first_message)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
 
 static int
 do_search_tags (notmuch_database_t *notmuch,
-		const search_format_t *format,
+		sprinter_t *format,
 		notmuch_query_t *query)
 {
     notmuch_messages_t *messages = NULL;
     notmuch_tags_t *tags;
     const char *tag;
-    int first_tag = 1;
 
     /* should the following only special case if no excluded terms
      * specified? */
@@ -387,7 +259,7 @@ do_search_tags (notmuch_database_t *notmuch,
     if (tags == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (;
 	 notmuch_tags_valid (tags);
@@ -395,12 +267,9 @@ do_search_tags (notmuch_database_t *notmuch,
     {
 	tag = notmuch_tags_get (tags);
 
-	if (! first_tag)
-	    fputs (format->item_sep, stdout);
+	format->string (format, tag);
+	format->separator (format);
 
-	format->item_id (tags, "", tag);
-
-	first_tag = 0;
     }
 
     notmuch_tags_destroy (tags);
@@ -408,10 +277,7 @@ do_search_tags (notmuch_database_t *notmuch,
     if (messages)
 	notmuch_messages_destroy (messages);
 
-    if (first_tag)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
@@ -430,7 +296,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_t *query;
     char *query_str;
     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
-    const search_format_t *format = &format_text;
+    sprinter_t *format = NULL;
     int opt_index, ret;
     output_t output = OUTPUT_SUMMARY;
     int offset = 0;
@@ -475,10 +341,10 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 
     switch (format_sel) {
     case NOTMUCH_FORMAT_TEXT:
-	format = &format_text;
+	format = sprinter_text_search_create (ctx, stdout);
 	break;
     case NOTMUCH_FORMAT_JSON:
-	format = &format_json;
+	format = sprinter_json_create (ctx, stdout);
 	break;
     }
 
@@ -546,5 +412,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_destroy (query);
     notmuch_database_destroy (notmuch);
 
+    talloc_free (format);
+
     return ret;
 }
diff --git a/test/json b/test/json
index 6439788..f0ebf08 100755
--- a/test/json
+++ b/test/json
@@ -10,14 +10,7 @@ test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"e
 test_begin_subtest "Search message: json"
 add_message "[subject]=\"json-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-search-message\""
 output=$(notmuch search --format=json "json-search-message" | notmuch_search_sanitize)
-test_expect_equal "$output" "[{\"thread\": \"XXX\",
-\"timestamp\": 946728000,
-\"date_relative\": \"2000-01-01\",
-\"matched\": 1,
-\"total\": 1,
-\"authors\": \"Notmuch Test Suite\",
-\"subject\": \"json-search-subject\",
-\"tags\": [\"inbox\", \"unread\"]}]"
+test_expect_equal "$output" "[{\"thread\": \"XXX\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"matched\": 1, \"total\": 1, \"authors\": \"Notmuch Test Suite\", \"subject\": \"json-search-subject\", \"tags\": [\"inbox\", \"unread\"]}]"
 
 test_begin_subtest "Show message: json, utf-8"
 add_message "[subject]=\"json-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
@@ -40,13 +33,6 @@ test_expect_equal "$output" "[[[{\"id\": \"$id\", \"match\": true, \"excluded\":
 test_begin_subtest "Search message: json, utf-8"
 add_message "[subject]=\"json-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
 output=$(notmuch search --format=json "jsön-search-méssage" | notmuch_search_sanitize)
-test_expect_equal "$output" "[{\"thread\": \"XXX\",
-\"timestamp\": 946728000,
-\"date_relative\": \"2000-01-01\",
-\"matched\": 1,
-\"total\": 1,
-\"authors\": \"Notmuch Test Suite\",
-\"subject\": \"json-search-utf8-body-sübjéct\",
-\"tags\": [\"inbox\", \"unread\"]}]"
+test_expect_equal "$output" "[{\"thread\": \"XXX\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"matched\": 1, \"total\": 1, \"authors\": \"Notmuch Test Suite\", \"subject\": \"json-search-utf8-body-sübjéct\", \"tags\": [\"inbox\", \"unread\"]}]"
 
 test_done
-- 
1.7.11.1

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 2/3] Add structured output formatter for JSON and plain text.
  2012-07-16  8:35                 ` [PATCH v6 2/3] Add structured output formatter for JSON and plain text craven
@ 2012-07-18 19:48                   ` Austin Clements
  2012-07-20  6:36                     ` notmuch-reply: Structured Formatters craven
  0 siblings, 1 reply; 39+ messages in thread
From: Austin Clements @ 2012-07-18 19:48 UTC (permalink / raw)
  To: craven; +Cc: notmuch

Quoth craven@gmx.net on Jul 16 at 10:35 am:
> 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        | 191 +++++++++++++++++++++++++++++++++++++++++++++++++
>  sprinter-text-search.c | 146 +++++++++++++++++++++++++++++++++++++
>  sprinter.h             |   9 +++
>  4 files changed, 348 insertions(+)
>  create mode 100644 sprinter-json.c
>  create mode 100644 sprinter-text-search.c
> 
> diff --git a/Makefile.local b/Makefile.local
> index a890df2..4f534f1 100644
> --- a/Makefile.local
> +++ b/Makefile.local
> @@ -290,6 +290,8 @@ notmuch_client_srcs =		\
>  	notmuch-show.c		\
>  	notmuch-tag.c		\
>  	notmuch-time.c		\
> +	sprinter-text-search.c  \
> +	sprinter-json.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..a93a390
> --- /dev/null
> +++ b/sprinter-json.c
> @@ -0,0 +1,191 @@
> +#include <stdbool.h>
> +#include <stdio.h>
> +#include <talloc.h>
> +#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;
> +}
> +
> +static notmuch_bool_t
> +json_is_text_printer (unused (struct sprinter *sp))
> +{
> +    return FALSE;
> +}

Seems like overkill to have a method for this.  Why not just use some
flag in notmuch-search.c?  Or, if you really want it to be part of the
sprinter abstraction, why not just put a flag in struct sprinter?
This isn't going to change dynamically.

> +
> +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 = json_is_text_printer,
> +	}
> +    };
> +    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-search.c b/sprinter-text-search.c
> new file mode 100644
> index 0000000..b115722
> --- /dev/null
> +++ b/sprinter-text-search.c
> @@ -0,0 +1,146 @@
> +#include <stdbool.h>
> +#include <stdio.h>
> +#include <talloc.h>
> +#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_search {

Why is this sprinter_text_search rather than just 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
> +print_sanitized_string (FILE *stream, const char *str)
> +{
> +    if (NULL == str)
> +	return;
> +
> +    for (; *str; str++) {
> +	if ((unsigned char) (*str) < 32)
> +	    fputc ('?', stream);
> +	else
> +	    fputc (*str, stream);
> +    }
> +}

Either the text sprinter should be responsible for sanitization or the
caller should be.  Currently you have a text sanitizer in both.  I
think you should leave sanitization to the caller and output the
string directly in text_search_string.  For example, search
--output=files should output file names untouched, but doing
sanitization here means unusual (but legal) characters in file names
will get sanitized.

> +
> +static void
> +text_search_string (struct sprinter *sp, const char *val)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    if (sptxt->current_prefix != NULL)
> +	fprintf (sptxt->stream, "%s:", sptxt->current_prefix);
> +
> +    print_sanitized_string (sptxt->stream, val);
> +}
> +
> +static void
> +text_search_integer (struct sprinter *sp, int val)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    fprintf (sptxt->stream, "%d", val);
> +}
> +
> +static void
> +text_search_boolean (struct sprinter *sp, notmuch_bool_t val)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    fputs (val ? "true" : "false", sptxt->stream);
> +}
> +
> +static void
> +text_search_separator (struct sprinter *sp)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    fputc ('\n', sptxt->stream);
> +}
> +
> +static void
> +text_search_set_prefix (struct sprinter *sp, const char *prefix)
> +{
> +    struct sprinter_text_search *sptxt = (struct sprinter_text_search *) sp;
> +
> +    sptxt->current_prefix = prefix;
> +}
> +
> +static notmuch_bool_t
> +text_search_is_text_printer (unused (struct sprinter *sp))
> +{
> +    return TRUE;
> +}
> +
> +/* The structure functions begin_map, begin_list, end and map_key
> + * don't do anything in the text formatter.
> + */
> +
> +static void
> +text_search_begin_map (unused (struct sprinter *sp))
> +{
> +}
> +
> +static void
> +text_search_begin_list (unused (struct sprinter *sp))
> +{
> +}
> +
> +static void
> +text_search_end (unused (struct sprinter *sp))
> +{
> +}
> +
> +static void
> +text_search_null (unused (struct sprinter *sp))
> +{
> +}
> +
> +static void
> +text_search_map_key (unused (struct sprinter *sp), unused (const char *key))
> +{
> +}
> +
> +struct sprinter *
> +sprinter_text_search_create (const void *ctx, FILE *stream)
> +{
> +    static const struct sprinter_text_search template = {
> +	.vtable = {
> +	    .begin_map = text_search_begin_map,
> +	    .begin_list = text_search_begin_list,
> +	    .end = text_search_end,
> +	    .string = text_search_string,
> +	    .integer = text_search_integer,
> +	    .boolean = text_search_boolean,
> +	    .null = text_search_null,
> +	    .map_key = text_search_map_key,
> +	    .separator = text_search_separator,
> +	    .set_prefix = text_search_set_prefix,
> +	    .is_text_printer = text_search_is_text_printer,
> +	}
> +    };
> +    struct sprinter_text_search *res;
> +
> +    res = talloc (ctx, struct sprinter_text_search);
> +    if (! res)
> +	return NULL;
> +
> +    *res = template;
> +    res->stream = stream;
> +    return &res->vtable;
> +}
> diff --git a/sprinter.h b/sprinter.h
> index dc09a15..7ec6344 100644
> --- a/sprinter.h
> +++ b/sprinter.h
> @@ -57,4 +57,13 @@ typedef struct sprinter {
>      notmuch_bool_t (*is_text_printer) (struct sprinter *);
>  } sprinter_t;
>  
> +/* Create a new unstructured printer that emits the default text format
> + * for "notmuch search". */
> +struct sprinter *
> +sprinter_text_search_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

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 3/3] Use the structured formatters in notmuch-search.c.
  2012-07-16  8:35                 ` [PATCH v6 3/3] Use the structured formatters in notmuch-search.c craven
@ 2012-07-18 19:56                   ` Austin Clements
  0 siblings, 0 replies; 39+ messages in thread
From: Austin Clements @ 2012-07-18 19:56 UTC (permalink / raw)
  To: craven; +Cc: notmuch

Just a few comments (don't forget to scroll all the way down).
Overall this is looking pretty good.

Quoth craven@gmx.net on Jul 16 at 10:35 am:
> This patch switches from the current ad-hoc printer to the structured
> formatters in sprinter.h, sprinter-text-search.c and sprinter-json.c.
> 
> The JSON tests are changed slightly in order to make them PASS for the
> new structured output formatter.
> 
> The text tests pass without adaptation.
> ---
>  notmuch-search.c | 300 ++++++++++++++++---------------------------------------
>  test/json        |  18 +---
>  2 files changed, 86 insertions(+), 232 deletions(-)

That's a fantastic diffstat.

> 
> diff --git a/notmuch-search.c b/notmuch-search.c
> index 3be296d..cf927e6 100644
> --- a/notmuch-search.c
> +++ b/notmuch-search.c
> @@ -19,6 +19,7 @@
>   */
>  
>  #include "notmuch-client.h"
> +#include "sprinter.h"
>  
>  typedef enum {
>      OUTPUT_SUMMARY,
> @@ -28,92 +29,6 @@ typedef enum {
>      OUTPUT_TAGS
>  } output_t;
>  
> -typedef struct search_format {
> -    const char *results_start;
> -    const char *item_start;
> -    void (*item_id) (const void *ctx,
> -		     const char *item_type,
> -		     const char *item_id);
> -    void (*thread_summary) (const void *ctx,
> -			    const char *thread_id,
> -			    const time_t date,
> -			    const int matched,
> -			    const int total,
> -			    const char *authors,
> -			    const char *subject);
> -    const char *tag_start;
> -    const char *tag;
> -    const char *tag_sep;
> -    const char *tag_end;
> -    const char *item_sep;
> -    const char *item_end;
> -    const char *results_end;
> -    const char *results_null;
> -} search_format_t;
> -
> -static void
> -format_item_id_text (const void *ctx,
> -		     const char *item_type,
> -		     const char *item_id);
> -
> -static void
> -format_thread_text (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject);
> -static const search_format_t format_text = {
> -    "",
> -	"",
> -	    format_item_id_text,
> -	    format_thread_text,
> -	    " (",
> -		"%s", " ",
> -	    ")", "\n",
> -	"",
> -    "\n",
> -    "",
> -};
> -
> -static void
> -format_item_id_json (const void *ctx,
> -		     const char *item_type,
> -		     const char *item_id);
> -
> -static void
> -format_thread_json (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject);
> -
> -/* Any changes to the JSON format should be reflected in the file
> - * devel/schemata. */
> -static const search_format_t format_json = {
> -    "[",
> -	"{",
> -	    format_item_id_json,
> -	    format_thread_json,
> -	    "\"tags\": [",
> -		"\"%s\"", ", ",
> -	    "]", ",\n",
> -	"}",
> -    "]\n",
> -    "]\n",
> -};
> -
> -static void
> -format_item_id_text (unused (const void *ctx),
> -		     const char *item_type,
> -		     const char *item_id)
> -{
> -    printf ("%s%s", item_type, item_id);
> -}
> -
>  static char *
>  sanitize_string (const void *ctx, const char *str)
>  {
> @@ -131,72 +46,8 @@ sanitize_string (const void *ctx, const char *str)
>      return out;
>  }
>  
> -static void
> -format_thread_text (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject)
> -{
> -    void *ctx_quote = talloc_new (ctx);
> -
> -    printf ("thread:%s %12s [%d/%d] %s; %s",
> -	    thread_id,
> -	    notmuch_time_relative_date (ctx, date),
> -	    matched,
> -	    total,
> -	    sanitize_string (ctx_quote, authors),
> -	    sanitize_string (ctx_quote, subject));
> -
> -    talloc_free (ctx_quote);
> -}
> -
> -static void
> -format_item_id_json (const void *ctx,
> -		     unused (const char *item_type),
> -		     const char *item_id)
> -{
> -    void *ctx_quote = talloc_new (ctx);
> -
> -    printf ("%s", json_quote_str (ctx_quote, item_id));
> -
> -    talloc_free (ctx_quote);
> -    
> -}
> -
> -static void
> -format_thread_json (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject)
> -{
> -    void *ctx_quote = talloc_new (ctx);
> -
> -    printf ("\"thread\": %s,\n"
> -	    "\"timestamp\": %ld,\n"
> -	    "\"date_relative\": \"%s\",\n"
> -	    "\"matched\": %d,\n"
> -	    "\"total\": %d,\n"
> -	    "\"authors\": %s,\n"
> -	    "\"subject\": %s,\n",
> -	    json_quote_str (ctx_quote, thread_id),
> -	    date,
> -	    notmuch_time_relative_date (ctx, date),
> -	    matched,
> -	    total,
> -	    json_quote_str (ctx_quote, authors),
> -	    json_quote_str (ctx_quote, subject));
> -
> -    talloc_free (ctx_quote);
> -}
> -
>  static int
> -do_search_threads (const search_format_t *format,
> +do_search_threads (sprinter_t *format,
>  		   notmuch_query_t *query,
>  		   notmuch_sort_t sort,
>  		   output_t output,
> @@ -207,7 +58,6 @@ do_search_threads (const search_format_t *format,
>      notmuch_threads_t *threads;
>      notmuch_tags_t *tags;
>      time_t date;
> -    int first_thread = 1;
>      int i;
>  
>      if (offset < 0) {
> @@ -220,14 +70,12 @@ do_search_threads (const search_format_t *format,
>      if (threads == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    format->begin_list (format);
>  
>      for (i = 0;
>  	 notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
>  	 notmuch_threads_move_to_next (threads), i++)
>      {
> -	int first_tag = 1;
> -
>  	thread = notmuch_threads_get (threads);
>  
>  	if (i < offset) {
> @@ -235,60 +83,96 @@ do_search_threads (const search_format_t *format,
>  	    continue;
>  	}
>  
> -	if (! first_thread)
> -	    fputs (format->item_sep, stdout);
> -
>  	if (output == OUTPUT_THREADS) {
> -	    format->item_id (thread, "thread:",
> -			     notmuch_thread_get_thread_id (thread));
> +	    format->set_prefix (format, "thread");
> +	    format->string (format,
> +			    notmuch_thread_get_thread_id (thread));
> +	    format->separator (format);
>  	} else { /* output == OUTPUT_SUMMARY */
> -	    fputs (format->item_start, stdout);
> +	    void *ctx_quote = talloc_new (thread);
> +	    const char *authors = notmuch_thread_get_authors (thread);
> +	    const char *subject = notmuch_thread_get_subject (thread);
> +	    const char *thread_id = notmuch_thread_get_thread_id (thread);
> +	    int matched = notmuch_thread_get_matched_messages (thread);
> +	    int total = notmuch_thread_get_total_messages (thread);
> +	    const char *relative_date = NULL;
> +	    notmuch_bool_t first_tag = TRUE;
> +
> +	    format->begin_map (format);
>  
>  	    if (sort == NOTMUCH_SORT_OLDEST_FIRST)
>  		date = notmuch_thread_get_oldest_date (thread);
>  	    else
>  		date = notmuch_thread_get_newest_date (thread);
>  
> -	    format->thread_summary (thread,
> -				    notmuch_thread_get_thread_id (thread),
> -				    date,
> -				    notmuch_thread_get_matched_messages (thread),
> -				    notmuch_thread_get_total_messages (thread),
> -				    notmuch_thread_get_authors (thread),
> -				    notmuch_thread_get_subject (thread));
> +	    relative_date = notmuch_time_relative_date (ctx_quote, date);
> +
> +	    if (format->is_text_printer (format)) {
> +                /* Special case for the text formatter */
> +		printf ("thread:%s %12s [%d/%d] %s; %s (",
> +			thread_id,
> +			relative_date,
> +			matched,
> +			total,
> +			sanitize_string (ctx_quote, authors),
> +			sanitize_string (ctx_quote, subject));

Great.  This seems much simpler than trying to track the context in
the text printer.

> +	    } else { /* Structured Output */
> +		format->map_key (format, "thread");
> +		format->string (format, thread_id);
> +		format->map_key (format, "timestamp");
> +		format->integer (format, date);
> +		format->map_key (format, "date_relative");
> +		format->string (format, relative_date);
> +		format->map_key (format, "matched");
> +		format->integer (format, matched);
> +		format->map_key (format, "total");
> +		format->integer (format, total);
> +		format->map_key (format, "authors");
> +		format->string (format, authors);
> +		format->map_key (format, "subject");
> +		format->string (format, subject);
> +	    }
> +
> +	    talloc_free (ctx_quote);
>  
> -	    fputs (format->tag_start, stdout);
> +	    format->map_key (format, "tags");
> +	    format->begin_list (format);
>  
>  	    for (tags = notmuch_thread_get_tags (thread);
>  		 notmuch_tags_valid (tags);
>  		 notmuch_tags_move_to_next (tags))
>  	    {
> -		if (! first_tag)
> -		    fputs (format->tag_sep, stdout);
> -		printf (format->tag, notmuch_tags_get (tags));
> -		first_tag = 0;
> +		const char *tag = notmuch_tags_get (tags);
> +
> +		if (format->is_text_printer (format)) {
> +                  /* Special case for the text formatter */
> +		    if (first_tag)
> +			first_tag = FALSE;
> +		    else
> +			fputc (' ', stdout);
> +		    fputs (tag, stdout);
> +		} else /* Structured Output */

Please put braces around the else part as well (since you need braces
around the if part).

> +		    format->string (format, tag);
>  	    }
>  
> -	    fputs (format->tag_end, stdout);
> +	    if (format->is_text_printer (format))
> +		printf (")");
>  
> -	    fputs (format->item_end, stdout);
> +	    format->end (format);
> +	    format->end (format);
> +	    format->separator (format);
>  	}
>  
> -	first_thread = 0;
> -
>  	notmuch_thread_destroy (thread);
>      }
>  
> -    if (first_thread)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    format->end (format);
>  
>      return 0;
>  }
>  
>  static int
> -do_search_messages (const search_format_t *format,
> +do_search_messages (sprinter_t *format,
>  		    notmuch_query_t *query,
>  		    output_t output,
>  		    int offset,
> @@ -297,7 +181,6 @@ do_search_messages (const search_format_t *format,
>      notmuch_message_t *message;
>      notmuch_messages_t *messages;
>      notmuch_filenames_t *filenames;
> -    int first_message = 1;
>      int i;
>  
>      if (offset < 0) {
> @@ -310,7 +193,7 @@ do_search_messages (const search_format_t *format,
>      if (messages == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    format->begin_list (format);
>  
>      for (i = 0;
>  	 notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
> @@ -328,24 +211,17 @@ do_search_messages (const search_format_t *format,
>  		 notmuch_filenames_valid (filenames);
>  		 notmuch_filenames_move_to_next (filenames))
>  	    {
> -		if (! first_message)
> -		    fputs (format->item_sep, stdout);
> -
> -		format->item_id (message, "",
> -				 notmuch_filenames_get (filenames));
> -
> -		first_message = 0;
> +		format->string (format, notmuch_filenames_get (filenames));
> +		format->separator (format);
>  	    }
>  	    
>  	    notmuch_filenames_destroy( filenames );
>  
>  	} else { /* output == OUTPUT_MESSAGES */
> -	    if (! first_message)
> -		fputs (format->item_sep, stdout);
> -
> -	    format->item_id (message, "id:",
> -			     notmuch_message_get_message_id (message));
> -	    first_message = 0;
> +	    format->set_prefix (format, "id");
> +	    format->string (format,
> +			    notmuch_message_get_message_id (message));
> +	    format->separator (format);
>  	}
>  
>  	notmuch_message_destroy (message);
> @@ -353,23 +229,19 @@ do_search_messages (const search_format_t *format,
>  
>      notmuch_messages_destroy (messages);
>  
> -    if (first_message)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    format->end (format);
>  
>      return 0;
>  }
>  
>  static int
>  do_search_tags (notmuch_database_t *notmuch,
> -		const search_format_t *format,
> +		sprinter_t *format,
>  		notmuch_query_t *query)
>  {
>      notmuch_messages_t *messages = NULL;
>      notmuch_tags_t *tags;
>      const char *tag;
> -    int first_tag = 1;
>  
>      /* should the following only special case if no excluded terms
>       * specified? */
> @@ -387,7 +259,7 @@ do_search_tags (notmuch_database_t *notmuch,
>      if (tags == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    format->begin_list (format);
>  
>      for (;
>  	 notmuch_tags_valid (tags);
> @@ -395,12 +267,9 @@ do_search_tags (notmuch_database_t *notmuch,
>      {
>  	tag = notmuch_tags_get (tags);
>  
> -	if (! first_tag)
> -	    fputs (format->item_sep, stdout);
> +	format->string (format, tag);
> +	format->separator (format);
>  
> -	format->item_id (tags, "", tag);
> -
> -	first_tag = 0;
>      }
>  
>      notmuch_tags_destroy (tags);
> @@ -408,10 +277,7 @@ do_search_tags (notmuch_database_t *notmuch,
>      if (messages)
>  	notmuch_messages_destroy (messages);
>  
> -    if (first_tag)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    format->end (format);
>  
>      return 0;
>  }
> @@ -430,7 +296,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>      notmuch_query_t *query;
>      char *query_str;
>      notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
> -    const search_format_t *format = &format_text;
> +    sprinter_t *format = NULL;
>      int opt_index, ret;
>      output_t output = OUTPUT_SUMMARY;
>      int offset = 0;
> @@ -475,10 +341,10 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>  
>      switch (format_sel) {
>      case NOTMUCH_FORMAT_TEXT:
> -	format = &format_text;
> +	format = sprinter_text_search_create (ctx, stdout);
>  	break;
>      case NOTMUCH_FORMAT_JSON:
> -	format = &format_json;
> +	format = sprinter_json_create (ctx, stdout);
>  	break;
>      }
>  
> @@ -546,5 +412,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>      notmuch_query_destroy (query);
>      notmuch_database_destroy (notmuch);
>  
> +    talloc_free (format);
> +
>      return ret;
>  }
> diff --git a/test/json b/test/json
> index 6439788..f0ebf08 100755
> --- a/test/json
> +++ b/test/json
> @@ -10,14 +10,7 @@ test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"e
>  test_begin_subtest "Search message: json"
>  add_message "[subject]=\"json-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-search-message\""
>  output=$(notmuch search --format=json "json-search-message" | notmuch_search_sanitize)

What's your reason for not piping this through
notmuch_json_show_sanitize?

> -test_expect_equal "$output" "[{\"thread\": \"XXX\",
> -\"timestamp\": 946728000,
> -\"date_relative\": \"2000-01-01\",
> -\"matched\": 1,
> -\"total\": 1,
> -\"authors\": \"Notmuch Test Suite\",
> -\"subject\": \"json-search-subject\",
> -\"tags\": [\"inbox\", \"unread\"]}]"
> +test_expect_equal "$output" "[{\"thread\": \"XXX\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"matched\": 1, \"total\": 1, \"authors\": \"Notmuch Test Suite\", \"subject\": \"json-search-subject\", \"tags\": [\"inbox\", \"unread\"]}]"
>  
>  test_begin_subtest "Show message: json, utf-8"
>  add_message "[subject]=\"json-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
> @@ -40,13 +33,6 @@ test_expect_equal "$output" "[[[{\"id\": \"$id\", \"match\": true, \"excluded\":
>  test_begin_subtest "Search message: json, utf-8"
>  add_message "[subject]=\"json-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
>  output=$(notmuch search --format=json "jsön-search-méssage" | notmuch_search_sanitize)
> -test_expect_equal "$output" "[{\"thread\": \"XXX\",
> -\"timestamp\": 946728000,
> -\"date_relative\": \"2000-01-01\",
> -\"matched\": 1,
> -\"total\": 1,
> -\"authors\": \"Notmuch Test Suite\",
> -\"subject\": \"json-search-utf8-body-sübjéct\",
> -\"tags\": [\"inbox\", \"unread\"]}]"
> +test_expect_equal "$output" "[{\"thread\": \"XXX\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"matched\": 1, \"total\": 1, \"authors\": \"Notmuch Test Suite\", \"subject\": \"json-search-utf8-body-sübjéct\", \"tags\": [\"inbox\", \"unread\"]}]"
>  
>  test_done

^ permalink raw reply	[flat|nested] 39+ messages in thread

* notmuch-reply: Structured Formatters
  2012-07-18 19:48                   ` Austin Clements
@ 2012-07-20  6:36                     ` craven
  2012-07-20  6:36                       ` [PATCH v7 1/3] Add support for structured output formatters craven
                                         ` (3 more replies)
  0 siblings, 4 replies; 39+ messages in thread
From: craven @ 2012-07-20  6:36 UTC (permalink / raw)
  To: notmuch

Currently there is no easy way to add support for different structured
formatters (like JSON). For example, adding support for S-Expressions
would result in code duplication.

This patch series amends the situation by introducing structured
formatters, which allow different implementations of structures like
lists, maps, strings and numbers.

The new code in sprinter.h and sprinter-json.c can be used instead of
the current ad-hoc output in all parts of notmuch, a patch for
notmuch-search.c is included.

In a later patch, all other parts of notmuch should be adapted to the
structured formatters, and the creation of formatters should be
centralised (to make adding new formatters easier).

A "structured" formatter is provided for notmuch-search that prints the
current text format. This removes almost all the special-casing from
notmuch-search.c.

Overall diff --stat:

 Makefile.local   |   2 +
 notmuch-search.c | 301 +++++++++++++----------------------------------
 sprinter-json.c  | 185 +++++++++++++++++++++++++++++
 sprinter-text.c  | 126 ++++++++++++++++++++
 sprinter.h       |  68 +++++++++++
 test/json        |  34 +++---
 6 files changed, 484 insertions(+), 232 deletions(-)

Changes versus v6 of this patch:
- is_text_printer is now a field, not a function.
- minor formatting
- sprinter_text_search has been renamed to sprinter_text (as it contains
  no search-specific code).
- string sanitization removed from sprinter_text, the caller should
  sanitize the strings.

^ permalink raw reply	[flat|nested] 39+ messages in thread

* [PATCH v7 1/3] Add support for structured output formatters.
  2012-07-20  6:36                     ` notmuch-reply: Structured Formatters craven
@ 2012-07-20  6:36                       ` craven
  2012-07-20  9:09                         ` Tomi Ollila
  2012-07-20  6:36                       ` [PATCH v7 2/3] Add structured output formatter for JSON and plain text (but don't use them yet) craven
                                         ` (2 subsequent siblings)
  3 siblings, 1 reply; 39+ messages in thread
From: craven @ 2012-07-20  6:36 UTC (permalink / raw)
  To: notmuch

From: <craven@gmx.net>

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

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
-- 
1.7.11.2

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH v7 2/3] Add structured output formatter for JSON and plain text (but don't use them yet).
  2012-07-20  6:36                     ` notmuch-reply: Structured Formatters craven
  2012-07-20  6:36                       ` [PATCH v7 1/3] Add support for structured output formatters craven
@ 2012-07-20  6:36                       ` craven
  2012-07-22 16:08                         ` Austin Clements
  2012-07-20  6:36                       ` =?\x10\x10?q?=5BPATCH=20v7=203/3=5D=20Use=20the=20structured=20formatters=20in=20notmuch-search=2Ec=2E?= craven
  2012-07-22 16:13                       ` notmuch-reply: Structured Formatters Austin Clements
  3 siblings, 1 reply; 39+ messages in thread
From: craven @ 2012-07-20  6:36 UTC (permalink / raw)
  To: notmuch

From: <craven@gmx.net>

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 | 185 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 sprinter-text.c | 126 ++++++++++++++++++++++++++++++++++++++
 sprinter.h      |  10 +++
 4 files changed, 323 insertions(+)
 create mode 100644 sprinter-json.c
 create mode 100644 sprinter-text.c

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..32daa5a
--- /dev/null
+++ b/sprinter-json.c
@@ -0,0 +1,185 @@
+#include <stdbool.h>
+#include <stdio.h>
+#include <talloc.h>
+#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 <stdbool.h>
+#include <stdio.h>
+#include <talloc.h>
+#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
-- 
1.7.11.2

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* =?\x10\x10?q?=5BPATCH=20v7=203/3=5D=20Use=20the=20structured=20formatters=20in=20notmuch-search=2Ec=2E?=
  2012-07-20  6:36                     ` notmuch-reply: Structured Formatters craven
  2012-07-20  6:36                       ` [PATCH v7 1/3] Add support for structured output formatters craven
  2012-07-20  6:36                       ` [PATCH v7 2/3] Add structured output formatter for JSON and plain text (but don't use them yet) craven
@ 2012-07-20  6:36                       ` craven
  2012-07-20  8:23                         ` [PATCH v7 3/3] Use the structured formatters in notmuch-search.c craven
  2012-07-20  9:14                         ` Tomi Ollila
  2012-07-22 16:13                       ` notmuch-reply: Structured Formatters Austin Clements
  3 siblings, 2 replies; 39+ messages in thread
From: craven @ 2012-07-20  6:36 UTC (permalink / raw)
  To: notmuch

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #1: Type: text/plain; charset=\x10\x10, Size: 15139 bytes --]

From: <craven@gmx.net>

This patch switches from the current ad-hoc printer to the structured
formatters in sprinter.h, sprinter-text.c and sprinter-json.c.

The JSON tests are changed slightly in order to make them PASS for the
new structured output formatter.

The text tests pass without adaptation.
---
 notmuch-search.c | 301 ++++++++++++++++---------------------------------------
 test/json        |  34 ++++---
 2 files changed, 103 insertions(+), 232 deletions(-)

diff --git a/notmuch-search.c b/notmuch-search.c
index 3be296d..07211e8 100644
--- a/notmuch-search.c
+++ b/notmuch-search.c
@@ -19,6 +19,7 @@
  */
 
 #include "notmuch-client.h"
+#include "sprinter.h"
 
 typedef enum {
     OUTPUT_SUMMARY,
@@ -28,92 +29,6 @@ typedef enum {
     OUTPUT_TAGS
 } output_t;
 
-typedef struct search_format {
-    const char *results_start;
-    const char *item_start;
-    void (*item_id) (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-    void (*thread_summary) (const void *ctx,
-			    const char *thread_id,
-			    const time_t date,
-			    const int matched,
-			    const int total,
-			    const char *authors,
-			    const char *subject);
-    const char *tag_start;
-    const char *tag;
-    const char *tag_sep;
-    const char *tag_end;
-    const char *item_sep;
-    const char *item_end;
-    const char *results_end;
-    const char *results_null;
-} search_format_t;
-
-static void
-format_item_id_text (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-
-static void
-format_thread_text (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject);
-static const search_format_t format_text = {
-    "",
-	"",
-	    format_item_id_text,
-	    format_thread_text,
-	    " (",
-		"%s", " ",
-	    ")", "\n",
-	"",
-    "\n",
-    "",
-};
-
-static void
-format_item_id_json (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject);
-
-/* Any changes to the JSON format should be reflected in the file
- * devel/schemata. */
-static const search_format_t format_json = {
-    "[",
-	"{",
-	    format_item_id_json,
-	    format_thread_json,
-	    "\"tags\": [",
-		"\"%s\"", ", ",
-	    "]", ",\n",
-	"}",
-    "]\n",
-    "]\n",
-};
-
-static void
-format_item_id_text (unused (const void *ctx),
-		     const char *item_type,
-		     const char *item_id)
-{
-    printf ("%s%s", item_type, item_id);
-}
-
 static char *
 sanitize_string (const void *ctx, const char *str)
 {
@@ -131,72 +46,8 @@ sanitize_string (const void *ctx, const char *str)
     return out;
 }
 
-static void
-format_thread_text (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("thread:%s %12s [%d/%d] %s; %s",
-	    thread_id,
-	    notmuch_time_relative_date (ctx, date),
-	    matched,
-	    total,
-	    sanitize_string (ctx_quote, authors),
-	    sanitize_string (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
-static void
-format_item_id_json (const void *ctx,
-		     unused (const char *item_type),
-		     const char *item_id)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("%s", json_quote_str (ctx_quote, item_id));
-
-    talloc_free (ctx_quote);
-    
-}
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("\"thread\": %s,\n"
-	    "\"timestamp\": %ld,\n"
-	    "\"date_relative\": \"%s\",\n"
-	    "\"matched\": %d,\n"
-	    "\"total\": %d,\n"
-	    "\"authors\": %s,\n"
-	    "\"subject\": %s,\n",
-	    json_quote_str (ctx_quote, thread_id),
-	    date,
-	    notmuch_time_relative_date (ctx, date),
-	    matched,
-	    total,
-	    json_quote_str (ctx_quote, authors),
-	    json_quote_str (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
 static int
-do_search_threads (const search_format_t *format,
+do_search_threads (sprinter_t *format,
 		   notmuch_query_t *query,
 		   notmuch_sort_t sort,
 		   output_t output,
@@ -207,7 +58,6 @@ do_search_threads (const search_format_t *format,
     notmuch_threads_t *threads;
     notmuch_tags_t *tags;
     time_t date;
-    int first_thread = 1;
     int i;
 
     if (offset < 0) {
@@ -220,14 +70,12 @@ do_search_threads (const search_format_t *format,
     if (threads == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (i = 0;
 	 notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
 	 notmuch_threads_move_to_next (threads), i++)
     {
-	int first_tag = 1;
-
 	thread = notmuch_threads_get (threads);
 
 	if (i < offset) {
@@ -235,60 +83,97 @@ do_search_threads (const search_format_t *format,
 	    continue;
 	}
 
-	if (! first_thread)
-	    fputs (format->item_sep, stdout);
-
 	if (output == OUTPUT_THREADS) {
-	    format->item_id (thread, "thread:",
-			     notmuch_thread_get_thread_id (thread));
+	    format->set_prefix (format, "thread");
+	    format->string (format,
+			    notmuch_thread_get_thread_id (thread));
+	    format->separator (format);
 	} else { /* output == OUTPUT_SUMMARY */
-	    fputs (format->item_start, stdout);
+	    void *ctx_quote = talloc_new (thread);
+	    const char *authors = notmuch_thread_get_authors (thread);
+	    const char *subject = notmuch_thread_get_subject (thread);
+	    const char *thread_id = notmuch_thread_get_thread_id (thread);
+	    int matched = notmuch_thread_get_matched_messages (thread);
+	    int total = notmuch_thread_get_total_messages (thread);
+	    const char *relative_date = NULL;
+	    notmuch_bool_t first_tag = TRUE;
+
+	    format->begin_map (format);
 
 	    if (sort == NOTMUCH_SORT_OLDEST_FIRST)
 		date = notmuch_thread_get_oldest_date (thread);
 	    else
 		date = notmuch_thread_get_newest_date (thread);
 
-	    format->thread_summary (thread,
-				    notmuch_thread_get_thread_id (thread),
-				    date,
-				    notmuch_thread_get_matched_messages (thread),
-				    notmuch_thread_get_total_messages (thread),
-				    notmuch_thread_get_authors (thread),
-				    notmuch_thread_get_subject (thread));
+	    relative_date = notmuch_time_relative_date (ctx_quote, date);
+
+	    if (format->is_text_printer) {
+                /* Special case for the text formatter */
+		printf ("thread:%s %12s [%d/%d] %s; %s (",
+			thread_id,
+			relative_date,
+			matched,
+			total,
+			sanitize_string (ctx_quote, authors),
+			sanitize_string (ctx_quote, subject));
+	    } else { /* Structured Output */
+		format->map_key (format, "thread");
+		format->string (format, thread_id);
+		format->map_key (format, "timestamp");
+		format->integer (format, date);
+		format->map_key (format, "date_relative");
+		format->string (format, relative_date);
+		format->map_key (format, "matched");
+		format->integer (format, matched);
+		format->map_key (format, "total");
+		format->integer (format, total);
+		format->map_key (format, "authors");
+		format->string (format, authors);
+		format->map_key (format, "subject");
+		format->string (format, subject);
+	    }
+
+	    talloc_free (ctx_quote);
 
-	    fputs (format->tag_start, stdout);
+	    format->map_key (format, "tags");
+	    format->begin_list (format);
 
 	    for (tags = notmuch_thread_get_tags (thread);
 		 notmuch_tags_valid (tags);
 		 notmuch_tags_move_to_next (tags))
 	    {
-		if (! first_tag)
-		    fputs (format->tag_sep, stdout);
-		printf (format->tag, notmuch_tags_get (tags));
-		first_tag = 0;
+		const char *tag = notmuch_tags_get (tags);
+
+		if (format->is_text_printer) {
+                  /* Special case for the text formatter */
+		    if (first_tag)
+			first_tag = FALSE;
+		    else
+			fputc (' ', stdout);
+		    fputs (tag, stdout);
+		} else { /* Structured Output */
+		    format->string (format, tag);
+		}
 	    }
 
-	    fputs (format->tag_end, stdout);
+	    if (format->is_text_printer)
+		printf (")");
 
-	    fputs (format->item_end, stdout);
+	    format->end (format);
+	    format->end (format);
+	    format->separator (format);
 	}
 
-	first_thread = 0;
-
 	notmuch_thread_destroy (thread);
     }
 
-    if (first_thread)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
 
 static int
-do_search_messages (const search_format_t *format,
+do_search_messages (sprinter_t *format,
 		    notmuch_query_t *query,
 		    output_t output,
 		    int offset,
@@ -297,7 +182,6 @@ do_search_messages (const search_format_t *format,
     notmuch_message_t *message;
     notmuch_messages_t *messages;
     notmuch_filenames_t *filenames;
-    int first_message = 1;
     int i;
 
     if (offset < 0) {
@@ -310,7 +194,7 @@ do_search_messages (const search_format_t *format,
     if (messages == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (i = 0;
 	 notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
@@ -328,24 +212,17 @@ do_search_messages (const search_format_t *format,
 		 notmuch_filenames_valid (filenames);
 		 notmuch_filenames_move_to_next (filenames))
 	    {
-		if (! first_message)
-		    fputs (format->item_sep, stdout);
-
-		format->item_id (message, "",
-				 notmuch_filenames_get (filenames));
-
-		first_message = 0;
+		format->string (format, notmuch_filenames_get (filenames));
+		format->separator (format);
 	    }
 	    
 	    notmuch_filenames_destroy( filenames );
 
 	} else { /* output == OUTPUT_MESSAGES */
-	    if (! first_message)
-		fputs (format->item_sep, stdout);
-
-	    format->item_id (message, "id:",
-			     notmuch_message_get_message_id (message));
-	    first_message = 0;
+	    format->set_prefix (format, "id");
+	    format->string (format,
+			    notmuch_message_get_message_id (message));
+	    format->separator (format);
 	}
 
 	notmuch_message_destroy (message);
@@ -353,23 +230,19 @@ do_search_messages (const search_format_t *format,
 
     notmuch_messages_destroy (messages);
 
-    if (first_message)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
 
 static int
 do_search_tags (notmuch_database_t *notmuch,
-		const search_format_t *format,
+		sprinter_t *format,
 		notmuch_query_t *query)
 {
     notmuch_messages_t *messages = NULL;
     notmuch_tags_t *tags;
     const char *tag;
-    int first_tag = 1;
 
     /* should the following only special case if no excluded terms
      * specified? */
@@ -387,7 +260,7 @@ do_search_tags (notmuch_database_t *notmuch,
     if (tags == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (;
 	 notmuch_tags_valid (tags);
@@ -395,12 +268,9 @@ do_search_tags (notmuch_database_t *notmuch,
     {
 	tag = notmuch_tags_get (tags);
 
-	if (! first_tag)
-	    fputs (format->item_sep, stdout);
+	format->string (format, tag);
+	format->separator (format);
 
-	format->item_id (tags, "", tag);
-
-	first_tag = 0;
     }
 
     notmuch_tags_destroy (tags);
@@ -408,10 +278,7 @@ do_search_tags (notmuch_database_t *notmuch,
     if (messages)
 	notmuch_messages_destroy (messages);
 
-    if (first_tag)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
@@ -430,7 +297,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_t *query;
     char *query_str;
     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
-    const search_format_t *format = &format_text;
+    sprinter_t *format = NULL;
     int opt_index, ret;
     output_t output = OUTPUT_SUMMARY;
     int offset = 0;
@@ -475,10 +342,10 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 
     switch (format_sel) {
     case NOTMUCH_FORMAT_TEXT:
-	format = &format_text;
+	format = sprinter_text_create (ctx, stdout);
 	break;
     case NOTMUCH_FORMAT_JSON:
-	format = &format_json;
+	format = sprinter_json_create (ctx, stdout);
 	break;
     }
 
@@ -546,5 +413,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_destroy (query);
     notmuch_database_destroy (notmuch);
 
+    talloc_free (format);
+
     return ret;
 }
diff --git a/test/json b/test/json
index 6439788..337b3f5 100755
--- a/test/json
+++ b/test/json
@@ -9,15 +9,16 @@ test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"e
 
 test_begin_subtest "Search message: json"
 add_message "[subject]=\"json-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-search-message\""
-output=$(notmuch search --format=json "json-search-message" | notmuch_search_sanitize)
+output=$(notmuch search --format=json "json-search-message" | notmuch_json_show_sanitize | notmuch_search_sanitize)
 test_expect_equal "$output" "[{\"thread\": \"XXX\",
-\"timestamp\": 946728000,
-\"date_relative\": \"2000-01-01\",
-\"matched\": 1,
-\"total\": 1,
-\"authors\": \"Notmuch Test Suite\",
-\"subject\": \"json-search-subject\",
-\"tags\": [\"inbox\", \"unread\"]}]"
+ \"timestamp\": 946728000,
+ \"date_relative\": \"2000-01-01\",
+ \"matched\": 1,
+ \"total\": 1,
+ \"authors\": \"Notmuch Test Suite\",
+ \"subject\": \"json-search-subject\",
+ \"tags\": [\"inbox\",
+ \"unread\"]}]"
 
 test_begin_subtest "Show message: json, utf-8"
 add_message "[subject]=\"json-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
@@ -39,14 +40,15 @@ test_expect_equal "$output" "[[[{\"id\": \"$id\", \"match\": true, \"excluded\":
 
 test_begin_subtest "Search message: json, utf-8"
 add_message "[subject]=\"json-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
-output=$(notmuch search --format=json "jsön-search-méssage" | notmuch_search_sanitize)
+output=$(notmuch search --format=json "jsön-search-méssage" | notmuch_json_show_sanitize | notmuch_search_sanitize)
 test_expect_equal "$output" "[{\"thread\": \"XXX\",
-\"timestamp\": 946728000,
-\"date_relative\": \"2000-01-01\",
-\"matched\": 1,
-\"total\": 1,
-\"authors\": \"Notmuch Test Suite\",
-\"subject\": \"json-search-utf8-body-sübjéct\",
-\"tags\": [\"inbox\", \"unread\"]}]"
+ \"timestamp\": 946728000,
+ \"date_relative\": \"2000-01-01\",
+ \"matched\": 1,
+ \"total\": 1,
+ \"authors\": \"Notmuch Test Suite\",
+ \"subject\": \"json-search-utf8-body-sübjéct\",
+ \"tags\": [\"inbox\",
+ \"unread\"]}]"
 
 test_done
-- 
1.7.11.2

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH v7 3/3] Use the structured formatters in notmuch-search.c.
  2012-07-20  6:36                       ` =?\x10\x10?q?=5BPATCH=20v7=203/3=5D=20Use=20the=20structured=20formatters=20in=20notmuch-search=2Ec=2E?= craven
@ 2012-07-20  8:23                         ` craven
  2012-07-20  9:14                         ` Tomi Ollila
  1 sibling, 0 replies; 39+ messages in thread
From: craven @ 2012-07-20  8:23 UTC (permalink / raw)
  To: notmuch

From: <craven@gmx.net>

This patch switches from the current ad-hoc printer to the structured
formatters in sprinter.h, sprinter-text.c and sprinter-json.c.

The JSON tests are changed slightly in order to make them PASS for the
new structured output formatter.

The text tests pass without adaptation.
---
 notmuch-search.c | 301 ++++++++++++++++---------------------------------------
 test/json        |  34 ++++---
 2 files changed, 103 insertions(+), 232 deletions(-)

diff --git a/notmuch-search.c b/notmuch-search.c
index 3be296d..07211e8 100644
--- a/notmuch-search.c
+++ b/notmuch-search.c
@@ -19,6 +19,7 @@
  */
 
 #include "notmuch-client.h"
+#include "sprinter.h"
 
 typedef enum {
     OUTPUT_SUMMARY,
@@ -28,92 +29,6 @@ typedef enum {
     OUTPUT_TAGS
 } output_t;
 
-typedef struct search_format {
-    const char *results_start;
-    const char *item_start;
-    void (*item_id) (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-    void (*thread_summary) (const void *ctx,
-			    const char *thread_id,
-			    const time_t date,
-			    const int matched,
-			    const int total,
-			    const char *authors,
-			    const char *subject);
-    const char *tag_start;
-    const char *tag;
-    const char *tag_sep;
-    const char *tag_end;
-    const char *item_sep;
-    const char *item_end;
-    const char *results_end;
-    const char *results_null;
-} search_format_t;
-
-static void
-format_item_id_text (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-
-static void
-format_thread_text (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject);
-static const search_format_t format_text = {
-    "",
-	"",
-	    format_item_id_text,
-	    format_thread_text,
-	    " (",
-		"%s", " ",
-	    ")", "\n",
-	"",
-    "\n",
-    "",
-};
-
-static void
-format_item_id_json (const void *ctx,
-		     const char *item_type,
-		     const char *item_id);
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject);
-
-/* Any changes to the JSON format should be reflected in the file
- * devel/schemata. */
-static const search_format_t format_json = {
-    "[",
-	"{",
-	    format_item_id_json,
-	    format_thread_json,
-	    "\"tags\": [",
-		"\"%s\"", ", ",
-	    "]", ",\n",
-	"}",
-    "]\n",
-    "]\n",
-};
-
-static void
-format_item_id_text (unused (const void *ctx),
-		     const char *item_type,
-		     const char *item_id)
-{
-    printf ("%s%s", item_type, item_id);
-}
-
 static char *
 sanitize_string (const void *ctx, const char *str)
 {
@@ -131,72 +46,8 @@ sanitize_string (const void *ctx, const char *str)
     return out;
 }
 
-static void
-format_thread_text (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("thread:%s %12s [%d/%d] %s; %s",
-	    thread_id,
-	    notmuch_time_relative_date (ctx, date),
-	    matched,
-	    total,
-	    sanitize_string (ctx_quote, authors),
-	    sanitize_string (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
-static void
-format_item_id_json (const void *ctx,
-		     unused (const char *item_type),
-		     const char *item_id)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("%s", json_quote_str (ctx_quote, item_id));
-
-    talloc_free (ctx_quote);
-    
-}
-
-static void
-format_thread_json (const void *ctx,
-		    const char *thread_id,
-		    const time_t date,
-		    const int matched,
-		    const int total,
-		    const char *authors,
-		    const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("\"thread\": %s,\n"
-	    "\"timestamp\": %ld,\n"
-	    "\"date_relative\": \"%s\",\n"
-	    "\"matched\": %d,\n"
-	    "\"total\": %d,\n"
-	    "\"authors\": %s,\n"
-	    "\"subject\": %s,\n",
-	    json_quote_str (ctx_quote, thread_id),
-	    date,
-	    notmuch_time_relative_date (ctx, date),
-	    matched,
-	    total,
-	    json_quote_str (ctx_quote, authors),
-	    json_quote_str (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
 static int
-do_search_threads (const search_format_t *format,
+do_search_threads (sprinter_t *format,
 		   notmuch_query_t *query,
 		   notmuch_sort_t sort,
 		   output_t output,
@@ -207,7 +58,6 @@ do_search_threads (const search_format_t *format,
     notmuch_threads_t *threads;
     notmuch_tags_t *tags;
     time_t date;
-    int first_thread = 1;
     int i;
 
     if (offset < 0) {
@@ -220,14 +70,12 @@ do_search_threads (const search_format_t *format,
     if (threads == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (i = 0;
 	 notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
 	 notmuch_threads_move_to_next (threads), i++)
     {
-	int first_tag = 1;
-
 	thread = notmuch_threads_get (threads);
 
 	if (i < offset) {
@@ -235,60 +83,97 @@ do_search_threads (const search_format_t *format,
 	    continue;
 	}
 
-	if (! first_thread)
-	    fputs (format->item_sep, stdout);
-
 	if (output == OUTPUT_THREADS) {
-	    format->item_id (thread, "thread:",
-			     notmuch_thread_get_thread_id (thread));
+	    format->set_prefix (format, "thread");
+	    format->string (format,
+			    notmuch_thread_get_thread_id (thread));
+	    format->separator (format);
 	} else { /* output == OUTPUT_SUMMARY */
-	    fputs (format->item_start, stdout);
+	    void *ctx_quote = talloc_new (thread);
+	    const char *authors = notmuch_thread_get_authors (thread);
+	    const char *subject = notmuch_thread_get_subject (thread);
+	    const char *thread_id = notmuch_thread_get_thread_id (thread);
+	    int matched = notmuch_thread_get_matched_messages (thread);
+	    int total = notmuch_thread_get_total_messages (thread);
+	    const char *relative_date = NULL;
+	    notmuch_bool_t first_tag = TRUE;
+
+	    format->begin_map (format);
 
 	    if (sort == NOTMUCH_SORT_OLDEST_FIRST)
 		date = notmuch_thread_get_oldest_date (thread);
 	    else
 		date = notmuch_thread_get_newest_date (thread);
 
-	    format->thread_summary (thread,
-				    notmuch_thread_get_thread_id (thread),
-				    date,
-				    notmuch_thread_get_matched_messages (thread),
-				    notmuch_thread_get_total_messages (thread),
-				    notmuch_thread_get_authors (thread),
-				    notmuch_thread_get_subject (thread));
+	    relative_date = notmuch_time_relative_date (ctx_quote, date);
+
+	    if (format->is_text_printer) {
+                /* Special case for the text formatter */
+		printf ("thread:%s %12s [%d/%d] %s; %s (",
+			thread_id,
+			relative_date,
+			matched,
+			total,
+			sanitize_string (ctx_quote, authors),
+			sanitize_string (ctx_quote, subject));
+	    } else { /* Structured Output */
+		format->map_key (format, "thread");
+		format->string (format, thread_id);
+		format->map_key (format, "timestamp");
+		format->integer (format, date);
+		format->map_key (format, "date_relative");
+		format->string (format, relative_date);
+		format->map_key (format, "matched");
+		format->integer (format, matched);
+		format->map_key (format, "total");
+		format->integer (format, total);
+		format->map_key (format, "authors");
+		format->string (format, authors);
+		format->map_key (format, "subject");
+		format->string (format, subject);
+	    }
+
+	    talloc_free (ctx_quote);
 
-	    fputs (format->tag_start, stdout);
+	    format->map_key (format, "tags");
+	    format->begin_list (format);
 
 	    for (tags = notmuch_thread_get_tags (thread);
 		 notmuch_tags_valid (tags);
 		 notmuch_tags_move_to_next (tags))
 	    {
-		if (! first_tag)
-		    fputs (format->tag_sep, stdout);
-		printf (format->tag, notmuch_tags_get (tags));
-		first_tag = 0;
+		const char *tag = notmuch_tags_get (tags);
+
+		if (format->is_text_printer) {
+                  /* Special case for the text formatter */
+		    if (first_tag)
+			first_tag = FALSE;
+		    else
+			fputc (' ', stdout);
+		    fputs (tag, stdout);
+		} else { /* Structured Output */
+		    format->string (format, tag);
+		}
 	    }
 
-	    fputs (format->tag_end, stdout);
+	    if (format->is_text_printer)
+		printf (")");
 
-	    fputs (format->item_end, stdout);
+	    format->end (format);
+	    format->end (format);
+	    format->separator (format);
 	}
 
-	first_thread = 0;
-
 	notmuch_thread_destroy (thread);
     }
 
-    if (first_thread)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
 
 static int
-do_search_messages (const search_format_t *format,
+do_search_messages (sprinter_t *format,
 		    notmuch_query_t *query,
 		    output_t output,
 		    int offset,
@@ -297,7 +182,6 @@ do_search_messages (const search_format_t *format,
     notmuch_message_t *message;
     notmuch_messages_t *messages;
     notmuch_filenames_t *filenames;
-    int first_message = 1;
     int i;
 
     if (offset < 0) {
@@ -310,7 +194,7 @@ do_search_messages (const search_format_t *format,
     if (messages == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (i = 0;
 	 notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
@@ -328,24 +212,17 @@ do_search_messages (const search_format_t *format,
 		 notmuch_filenames_valid (filenames);
 		 notmuch_filenames_move_to_next (filenames))
 	    {
-		if (! first_message)
-		    fputs (format->item_sep, stdout);
-
-		format->item_id (message, "",
-				 notmuch_filenames_get (filenames));
-
-		first_message = 0;
+		format->string (format, notmuch_filenames_get (filenames));
+		format->separator (format);
 	    }
 	    
 	    notmuch_filenames_destroy( filenames );
 
 	} else { /* output == OUTPUT_MESSAGES */
-	    if (! first_message)
-		fputs (format->item_sep, stdout);
-
-	    format->item_id (message, "id:",
-			     notmuch_message_get_message_id (message));
-	    first_message = 0;
+	    format->set_prefix (format, "id");
+	    format->string (format,
+			    notmuch_message_get_message_id (message));
+	    format->separator (format);
 	}
 
 	notmuch_message_destroy (message);
@@ -353,23 +230,19 @@ do_search_messages (const search_format_t *format,
 
     notmuch_messages_destroy (messages);
 
-    if (first_message)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
 
 static int
 do_search_tags (notmuch_database_t *notmuch,
-		const search_format_t *format,
+		sprinter_t *format,
 		notmuch_query_t *query)
 {
     notmuch_messages_t *messages = NULL;
     notmuch_tags_t *tags;
     const char *tag;
-    int first_tag = 1;
 
     /* should the following only special case if no excluded terms
      * specified? */
@@ -387,7 +260,7 @@ do_search_tags (notmuch_database_t *notmuch,
     if (tags == NULL)
 	return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (;
 	 notmuch_tags_valid (tags);
@@ -395,12 +268,9 @@ do_search_tags (notmuch_database_t *notmuch,
     {
 	tag = notmuch_tags_get (tags);
 
-	if (! first_tag)
-	    fputs (format->item_sep, stdout);
+	format->string (format, tag);
+	format->separator (format);
 
-	format->item_id (tags, "", tag);
-
-	first_tag = 0;
     }
 
     notmuch_tags_destroy (tags);
@@ -408,10 +278,7 @@ do_search_tags (notmuch_database_t *notmuch,
     if (messages)
 	notmuch_messages_destroy (messages);
 
-    if (first_tag)
-	fputs (format->results_null, stdout);
-    else
-	fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
@@ -430,7 +297,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_t *query;
     char *query_str;
     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
-    const search_format_t *format = &format_text;
+    sprinter_t *format = NULL;
     int opt_index, ret;
     output_t output = OUTPUT_SUMMARY;
     int offset = 0;
@@ -475,10 +342,10 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 
     switch (format_sel) {
     case NOTMUCH_FORMAT_TEXT:
-	format = &format_text;
+	format = sprinter_text_create (ctx, stdout);
 	break;
     case NOTMUCH_FORMAT_JSON:
-	format = &format_json;
+	format = sprinter_json_create (ctx, stdout);
 	break;
     }
 
@@ -546,5 +413,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_destroy (query);
     notmuch_database_destroy (notmuch);
 
+    talloc_free (format);
+
     return ret;
 }
diff --git a/test/json b/test/json
index 6439788..337b3f5 100755
--- a/test/json
+++ b/test/json
@@ -9,15 +9,16 @@ test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"e
 
 test_begin_subtest "Search message: json"
 add_message "[subject]=\"json-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-search-message\""
-output=$(notmuch search --format=json "json-search-message" | notmuch_search_sanitize)
+output=$(notmuch search --format=json "json-search-message" | notmuch_json_show_sanitize | notmuch_search_sanitize)
 test_expect_equal "$output" "[{\"thread\": \"XXX\",
-\"timestamp\": 946728000,
-\"date_relative\": \"2000-01-01\",
-\"matched\": 1,
-\"total\": 1,
-\"authors\": \"Notmuch Test Suite\",
-\"subject\": \"json-search-subject\",
-\"tags\": [\"inbox\", \"unread\"]}]"
+ \"timestamp\": 946728000,
+ \"date_relative\": \"2000-01-01\",
+ \"matched\": 1,
+ \"total\": 1,
+ \"authors\": \"Notmuch Test Suite\",
+ \"subject\": \"json-search-subject\",
+ \"tags\": [\"inbox\",
+ \"unread\"]}]"
 
 test_begin_subtest "Show message: json, utf-8"
 add_message "[subject]=\"json-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
@@ -39,14 +40,15 @@ test_expect_equal "$output" "[[[{\"id\": \"$id\", \"match\": true, \"excluded\":
 
 test_begin_subtest "Search message: json, utf-8"
 add_message "[subject]=\"json-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
-output=$(notmuch search --format=json "jsön-search-méssage" | notmuch_search_sanitize)
+output=$(notmuch search --format=json "jsön-search-méssage" | notmuch_json_show_sanitize | notmuch_search_sanitize)
 test_expect_equal "$output" "[{\"thread\": \"XXX\",
-\"timestamp\": 946728000,
-\"date_relative\": \"2000-01-01\",
-\"matched\": 1,
-\"total\": 1,
-\"authors\": \"Notmuch Test Suite\",
-\"subject\": \"json-search-utf8-body-sübjéct\",
-\"tags\": [\"inbox\", \"unread\"]}]"
+ \"timestamp\": 946728000,
+ \"date_relative\": \"2000-01-01\",
+ \"matched\": 1,
+ \"total\": 1,
+ \"authors\": \"Notmuch Test Suite\",
+ \"subject\": \"json-search-utf8-body-sübjéct\",
+ \"tags\": [\"inbox\",
+ \"unread\"]}]"
 
 test_done
-- 
1.7.11.2

^ permalink raw reply related	[flat|nested] 39+ messages in thread

* Re: [PATCH v7 1/3] Add support for structured output formatters.
  2012-07-20  6:36                       ` [PATCH v7 1/3] Add support for structured output formatters craven
@ 2012-07-20  9:09                         ` Tomi Ollila
  2012-07-20  9:13                           ` craven
  0 siblings, 1 reply; 39+ messages in thread
From: Tomi Ollila @ 2012-07-20  9:09 UTC (permalink / raw)
  To: craven, notmuch

On Fri, Jul 20 2012, craven@gmx.net wrote:

> From: <craven@gmx.net>
>
> 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 *);

The above block duplicated below. Otherwise this LGTM.

I presume the patch 3/3 emails

id:"1342766173-1344-4-git-send-email-craven@gmx.net"
id:"1342772624-23329-1-git-send-email-craven@gmx.net"

have identical content ?

> 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
>
> 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
> -- 
> 1.7.11.2
>
> _______________________________________________
> notmuch mailing list
> notmuch@notmuchmail.org
> http://notmuchmail.org/mailman/listinfo/notmuch

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v7 1/3] Add support for structured output formatters.
  2012-07-20  9:09                         ` Tomi Ollila
@ 2012-07-20  9:13                           ` craven
  0 siblings, 0 replies; 39+ messages in thread
From: craven @ 2012-07-20  9:13 UTC (permalink / raw)
  To: Tomi Ollila, notmuch

> I presume the patch 3/3 emails
> 
> id:"1342766173-1344-4-git-send-email-craven@gmx.net"
> id:"1342772624-23329-1-git-send-email-craven@gmx.net"
> 
> have identical content ?

Yes, they do, there was a problem with the Content-Type header having
^P^P, so I re-sent correctly, but the actual e-mail content is exactly
the same.

Greetings,
Peter

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v7 3/3] Use the structured formatters in notmuch-search.c.
  2012-07-20  6:36                       ` =?\x10\x10?q?=5BPATCH=20v7=203/3=5D=20Use=20the=20structured=20formatters=20in=20notmuch-search=2Ec=2E?= craven
  2012-07-20  8:23                         ` [PATCH v7 3/3] Use the structured formatters in notmuch-search.c craven
@ 2012-07-20  9:14                         ` Tomi Ollila
  1 sibling, 0 replies; 39+ messages in thread
From: Tomi Ollila @ 2012-07-20  9:14 UTC (permalink / raw)
  To: craven, notmuch

On Fri, Jul 20 2012, craven@gmx.net wrote:

> From: <craven@gmx.net>
>
> This patch switches from the current ad-hoc printer to the structured
> formatters in sprinter.h, sprinter-text.c and sprinter-json.c.
>
> The JSON tests are changed slightly in order to make them PASS for the
> new structured output formatter.
>
> The text tests pass without adaptation.
> ---
>  notmuch-search.c | 301 ++++++++++++++++---------------------------------------
>  test/json        |  34 ++++---
>  2 files changed, 103 insertions(+), 232 deletions(-)
>
> diff --git a/notmuch-search.c b/notmuch-search.c
> index 3be296d..07211e8 100644
> --- a/notmuch-search.c
> +++ b/notmuch-search.c
> @@ -19,6 +19,7 @@
>   */
>  
>  #include "notmuch-client.h"
> +#include "sprinter.h"
>  
>  typedef enum {
>      OUTPUT_SUMMARY,
> @@ -28,92 +29,6 @@ typedef enum {
>      OUTPUT_TAGS
>  } output_t;
>  
> -typedef struct search_format {
> -    const char *results_start;
> -    const char *item_start;
> -    void (*item_id) (const void *ctx,
> -		     const char *item_type,
> -		     const char *item_id);
> -    void (*thread_summary) (const void *ctx,
> -			    const char *thread_id,
> -			    const time_t date,
> -			    const int matched,
> -			    const int total,
> -			    const char *authors,
> -			    const char *subject);
> -    const char *tag_start;
> -    const char *tag;
> -    const char *tag_sep;
> -    const char *tag_end;
> -    const char *item_sep;
> -    const char *item_end;
> -    const char *results_end;
> -    const char *results_null;
> -} search_format_t;
> -
> -static void
> -format_item_id_text (const void *ctx,
> -		     const char *item_type,
> -		     const char *item_id);
> -
> -static void
> -format_thread_text (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject);
> -static const search_format_t format_text = {
> -    "",
> -	"",
> -	    format_item_id_text,
> -	    format_thread_text,
> -	    " (",
> -		"%s", " ",
> -	    ")", "\n",
> -	"",
> -    "\n",
> -    "",
> -};
> -
> -static void
> -format_item_id_json (const void *ctx,
> -		     const char *item_type,
> -		     const char *item_id);
> -
> -static void
> -format_thread_json (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject);
> -
> -/* Any changes to the JSON format should be reflected in the file
> - * devel/schemata. */
> -static const search_format_t format_json = {
> -    "[",
> -	"{",
> -	    format_item_id_json,
> -	    format_thread_json,
> -	    "\"tags\": [",
> -		"\"%s\"", ", ",
> -	    "]", ",\n",
> -	"}",
> -    "]\n",
> -    "]\n",
> -};
> -
> -static void
> -format_item_id_text (unused (const void *ctx),
> -		     const char *item_type,
> -		     const char *item_id)
> -{
> -    printf ("%s%s", item_type, item_id);
> -}
> -
>  static char *
>  sanitize_string (const void *ctx, const char *str)
>  {
> @@ -131,72 +46,8 @@ sanitize_string (const void *ctx, const char *str)
>      return out;
>  }
>  
> -static void
> -format_thread_text (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject)
> -{
> -    void *ctx_quote = talloc_new (ctx);
> -
> -    printf ("thread:%s %12s [%d/%d] %s; %s",
> -	    thread_id,
> -	    notmuch_time_relative_date (ctx, date),
> -	    matched,
> -	    total,
> -	    sanitize_string (ctx_quote, authors),
> -	    sanitize_string (ctx_quote, subject));
> -
> -    talloc_free (ctx_quote);
> -}
> -
> -static void
> -format_item_id_json (const void *ctx,
> -		     unused (const char *item_type),
> -		     const char *item_id)
> -{
> -    void *ctx_quote = talloc_new (ctx);
> -
> -    printf ("%s", json_quote_str (ctx_quote, item_id));
> -
> -    talloc_free (ctx_quote);
> -    
> -}
> -
> -static void
> -format_thread_json (const void *ctx,
> -		    const char *thread_id,
> -		    const time_t date,
> -		    const int matched,
> -		    const int total,
> -		    const char *authors,
> -		    const char *subject)
> -{
> -    void *ctx_quote = talloc_new (ctx);
> -
> -    printf ("\"thread\": %s,\n"
> -	    "\"timestamp\": %ld,\n"
> -	    "\"date_relative\": \"%s\",\n"
> -	    "\"matched\": %d,\n"
> -	    "\"total\": %d,\n"
> -	    "\"authors\": %s,\n"
> -	    "\"subject\": %s,\n",
> -	    json_quote_str (ctx_quote, thread_id),
> -	    date,
> -	    notmuch_time_relative_date (ctx, date),
> -	    matched,
> -	    total,
> -	    json_quote_str (ctx_quote, authors),
> -	    json_quote_str (ctx_quote, subject));
> -
> -    talloc_free (ctx_quote);
> -}
> -
>  static int
> -do_search_threads (const search_format_t *format,
> +do_search_threads (sprinter_t *format,
>  		   notmuch_query_t *query,
>  		   notmuch_sort_t sort,
>  		   output_t output,
> @@ -207,7 +58,6 @@ do_search_threads (const search_format_t *format,
>      notmuch_threads_t *threads;
>      notmuch_tags_t *tags;
>      time_t date;
> -    int first_thread = 1;
>      int i;
>  
>      if (offset < 0) {
> @@ -220,14 +70,12 @@ do_search_threads (const search_format_t *format,
>      if (threads == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    format->begin_list (format);
>  
>      for (i = 0;
>  	 notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit);
>  	 notmuch_threads_move_to_next (threads), i++)
>      {
> -	int first_tag = 1;
> -
>  	thread = notmuch_threads_get (threads);
>  
>  	if (i < offset) {
> @@ -235,60 +83,97 @@ do_search_threads (const search_format_t *format,
>  	    continue;
>  	}
>  
> -	if (! first_thread)
> -	    fputs (format->item_sep, stdout);
> -
>  	if (output == OUTPUT_THREADS) {
> -	    format->item_id (thread, "thread:",
> -			     notmuch_thread_get_thread_id (thread));
> +	    format->set_prefix (format, "thread");
> +	    format->string (format,
> +			    notmuch_thread_get_thread_id (thread));
> +	    format->separator (format);
>  	} else { /* output == OUTPUT_SUMMARY */
> -	    fputs (format->item_start, stdout);
> +	    void *ctx_quote = talloc_new (thread);
> +	    const char *authors = notmuch_thread_get_authors (thread);
> +	    const char *subject = notmuch_thread_get_subject (thread);
> +	    const char *thread_id = notmuch_thread_get_thread_id (thread);
> +	    int matched = notmuch_thread_get_matched_messages (thread);
> +	    int total = notmuch_thread_get_total_messages (thread);
> +	    const char *relative_date = NULL;
> +	    notmuch_bool_t first_tag = TRUE;
> +
> +	    format->begin_map (format);
>  
>  	    if (sort == NOTMUCH_SORT_OLDEST_FIRST)
>  		date = notmuch_thread_get_oldest_date (thread);
>  	    else
>  		date = notmuch_thread_get_newest_date (thread);
>  
> -	    format->thread_summary (thread,
> -				    notmuch_thread_get_thread_id (thread),
> -				    date,
> -				    notmuch_thread_get_matched_messages (thread),
> -				    notmuch_thread_get_total_messages (thread),
> -				    notmuch_thread_get_authors (thread),
> -				    notmuch_thread_get_subject (thread));
> +	    relative_date = notmuch_time_relative_date (ctx_quote, date);
> +
> +	    if (format->is_text_printer) {
> +                /* Special case for the text formatter */
> +		printf ("thread:%s %12s [%d/%d] %s; %s (",
> +			thread_id,
> +			relative_date,
> +			matched,
> +			total,
> +			sanitize_string (ctx_quote, authors),
> +			sanitize_string (ctx_quote, subject));
> +	    } else { /* Structured Output */
> +		format->map_key (format, "thread");
> +		format->string (format, thread_id);
> +		format->map_key (format, "timestamp");
> +		format->integer (format, date);
> +		format->map_key (format, "date_relative");
> +		format->string (format, relative_date);
> +		format->map_key (format, "matched");
> +		format->integer (format, matched);
> +		format->map_key (format, "total");
> +		format->integer (format, total);
> +		format->map_key (format, "authors");
> +		format->string (format, authors);
> +		format->map_key (format, "subject");
> +		format->string (format, subject);
> +	    }
> +
> +	    talloc_free (ctx_quote);
>  
> -	    fputs (format->tag_start, stdout);
> +	    format->map_key (format, "tags");
> +	    format->begin_list (format);
>  
>  	    for (tags = notmuch_thread_get_tags (thread);
>  		 notmuch_tags_valid (tags);
>  		 notmuch_tags_move_to_next (tags))
>  	    {
> -		if (! first_tag)
> -		    fputs (format->tag_sep, stdout);
> -		printf (format->tag, notmuch_tags_get (tags));
> -		first_tag = 0;
> +		const char *tag = notmuch_tags_get (tags);
> +
> +		if (format->is_text_printer) {
> +                  /* Special case for the text formatter */
> +		    if (first_tag)
> +			first_tag = FALSE;
> +		    else
> +			fputc (' ', stdout);
> +		    fputs (tag, stdout);
> +		} else { /* Structured Output */
> +		    format->string (format, tag);
> +		}
>  	    }
>  
> -	    fputs (format->tag_end, stdout);
> +	    if (format->is_text_printer)
> +		printf (")");
>  
> -	    fputs (format->item_end, stdout);
> +	    format->end (format);
> +	    format->end (format);
> +	    format->separator (format);
>  	}
>  
> -	first_thread = 0;
> -
>  	notmuch_thread_destroy (thread);
>      }
>  
> -    if (first_thread)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    format->end (format);
>  
>      return 0;
>  }
>  
>  static int
> -do_search_messages (const search_format_t *format,
> +do_search_messages (sprinter_t *format,
>  		    notmuch_query_t *query,
>  		    output_t output,
>  		    int offset,
> @@ -297,7 +182,6 @@ do_search_messages (const search_format_t *format,
>      notmuch_message_t *message;
>      notmuch_messages_t *messages;
>      notmuch_filenames_t *filenames;
> -    int first_message = 1;
>      int i;
>  
>      if (offset < 0) {
> @@ -310,7 +194,7 @@ do_search_messages (const search_format_t *format,
>      if (messages == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    format->begin_list (format);
>  
>      for (i = 0;
>  	 notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
> @@ -328,24 +212,17 @@ do_search_messages (const search_format_t *format,
>  		 notmuch_filenames_valid (filenames);
>  		 notmuch_filenames_move_to_next (filenames))
>  	    {
> -		if (! first_message)
> -		    fputs (format->item_sep, stdout);
> -
> -		format->item_id (message, "",
> -				 notmuch_filenames_get (filenames));
> -
> -		first_message = 0;
> +		format->string (format, notmuch_filenames_get (filenames));
> +		format->separator (format);
>  	    }
>  	    
>  	    notmuch_filenames_destroy( filenames );
>  
>  	} else { /* output == OUTPUT_MESSAGES */
> -	    if (! first_message)
> -		fputs (format->item_sep, stdout);
> -
> -	    format->item_id (message, "id:",
> -			     notmuch_message_get_message_id (message));
> -	    first_message = 0;
> +	    format->set_prefix (format, "id");
> +	    format->string (format,
> +			    notmuch_message_get_message_id (message));
> +	    format->separator (format);
>  	}
>  
>  	notmuch_message_destroy (message);
> @@ -353,23 +230,19 @@ do_search_messages (const search_format_t *format,
>  
>      notmuch_messages_destroy (messages);
>  
> -    if (first_message)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    format->end (format);
>  
>      return 0;
>  }
>  
>  static int
>  do_search_tags (notmuch_database_t *notmuch,
> -		const search_format_t *format,
> +		sprinter_t *format,
>  		notmuch_query_t *query)
>  {
>      notmuch_messages_t *messages = NULL;
>      notmuch_tags_t *tags;
>      const char *tag;
> -    int first_tag = 1;
>  
>      /* should the following only special case if no excluded terms
>       * specified? */
> @@ -387,7 +260,7 @@ do_search_tags (notmuch_database_t *notmuch,
>      if (tags == NULL)
>  	return 1;
>  
> -    fputs (format->results_start, stdout);
> +    format->begin_list (format);
>  
>      for (;
>  	 notmuch_tags_valid (tags);
> @@ -395,12 +268,9 @@ do_search_tags (notmuch_database_t *notmuch,
>      {
>  	tag = notmuch_tags_get (tags);
>  
> -	if (! first_tag)
> -	    fputs (format->item_sep, stdout);
> +	format->string (format, tag);
> +	format->separator (format);
>  
> -	format->item_id (tags, "", tag);
> -
> -	first_tag = 0;
>      }
>  
>      notmuch_tags_destroy (tags);
> @@ -408,10 +278,7 @@ do_search_tags (notmuch_database_t *notmuch,
>      if (messages)
>  	notmuch_messages_destroy (messages);
>  
> -    if (first_tag)
> -	fputs (format->results_null, stdout);
> -    else
> -	fputs (format->results_end, stdout);
> +    format->end (format);
>  
>      return 0;
>  }
> @@ -430,7 +297,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>      notmuch_query_t *query;
>      char *query_str;
>      notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
> -    const search_format_t *format = &format_text;
> +    sprinter_t *format = NULL;

due to this change -- dropping "fallback" setting due to ...

>      int opt_index, ret;
>      output_t output = OUTPUT_SUMMARY;
>      int offset = 0;
> @@ -475,10 +342,10 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>  
>      switch (format_sel) {
>      case NOTMUCH_FORMAT_TEXT:
> -	format = &format_text;
> +	format = sprinter_text_create (ctx, stdout);
>  	break;
>      case NOTMUCH_FORMAT_JSON:
> -	format = &format_json;
> +	format = sprinter_json_create (ctx, stdout);
>  	break;
>      }

... the dynamically created structure above, should we have 'default:'
case which executes INTERNAL_ERROR() (or something) instead of allowing
NULL derefencing (and crash) if one ever encounters such a situation ?

Tomi


> @@ -546,5 +413,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
>      notmuch_query_destroy (query);
>      notmuch_database_destroy (notmuch);
>  
> +    talloc_free (format);
> +
>      return ret;
>  }
> diff --git a/test/json b/test/json
> index 6439788..337b3f5 100755
> --- a/test/json
> +++ b/test/json
> @@ -9,15 +9,16 @@ test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"e
>  
>  test_begin_subtest "Search message: json"
>  add_message "[subject]=\"json-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-search-message\""
> -output=$(notmuch search --format=json "json-search-message" | notmuch_search_sanitize)
> +output=$(notmuch search --format=json "json-search-message" | notmuch_json_show_sanitize | notmuch_search_sanitize)
>  test_expect_equal "$output" "[{\"thread\": \"XXX\",
> -\"timestamp\": 946728000,
> -\"date_relative\": \"2000-01-01\",
> -\"matched\": 1,
> -\"total\": 1,
> -\"authors\": \"Notmuch Test Suite\",
> -\"subject\": \"json-search-subject\",
> -\"tags\": [\"inbox\", \"unread\"]}]"
> + \"timestamp\": 946728000,
> + \"date_relative\": \"2000-01-01\",
> + \"matched\": 1,
> + \"total\": 1,
> + \"authors\": \"Notmuch Test Suite\",
> + \"subject\": \"json-search-subject\",
> + \"tags\": [\"inbox\",
> + \"unread\"]}]"
>  
>  test_begin_subtest "Show message: json, utf-8"
>  add_message "[subject]=\"json-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
> @@ -39,14 +40,15 @@ test_expect_equal "$output" "[[[{\"id\": \"$id\", \"match\": true, \"excluded\":
>  
>  test_begin_subtest "Search message: json, utf-8"
>  add_message "[subject]=\"json-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
> -output=$(notmuch search --format=json "jsön-search-méssage" | notmuch_search_sanitize)
> +output=$(notmuch search --format=json "jsön-search-méssage" | notmuch_json_show_sanitize | notmuch_search_sanitize)
>  test_expect_equal "$output" "[{\"thread\": \"XXX\",
> -\"timestamp\": 946728000,
> -\"date_relative\": \"2000-01-01\",
> -\"matched\": 1,
> -\"total\": 1,
> -\"authors\": \"Notmuch Test Suite\",
> -\"subject\": \"json-search-utf8-body-sübjéct\",
> -\"tags\": [\"inbox\", \"unread\"]}]"
> + \"timestamp\": 946728000,
> + \"date_relative\": \"2000-01-01\",
> + \"matched\": 1,
> + \"total\": 1,
> + \"authors\": \"Notmuch Test Suite\",
> + \"subject\": \"json-search-utf8-body-sübjéct\",
> + \"tags\": [\"inbox\",
> + \"unread\"]}]"
>  
>  test_done
> -- 
> 1.7.11.2
>
> _______________________________________________
> notmuch mailing list
> notmuch@notmuchmail.org
> http://notmuchmail.org/mailman/listinfo/notmuch

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v7 2/3] Add structured output formatter for JSON and plain text (but don't use them yet).
  2012-07-20  6:36                       ` [PATCH v7 2/3] Add structured output formatter for JSON and plain text (but don't use them yet) craven
@ 2012-07-22 16:08                         ` Austin Clements
  0 siblings, 0 replies; 39+ messages in thread
From: Austin Clements @ 2012-07-22 16:08 UTC (permalink / raw)
  To: craven; +Cc: notmuch

Quoth craven@gmx.net on Jul 20 at  8:36 am:
> From: <craven@gmx.net>
> 
> 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 | 185 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
>  sprinter-text.c | 126 ++++++++++++++++++++++++++++++++++++++
>  sprinter.h      |  10 +++
>  4 files changed, 323 insertions(+)
>  create mode 100644 sprinter-json.c
>  create mode 100644 sprinter-text.c
> 
> 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..32daa5a
> --- /dev/null
> +++ b/sprinter-json.c
> @@ -0,0 +1,185 @@
> +#include <stdbool.h>
> +#include <stdio.h>
> +#include <talloc.h>
> +#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;

Just a nit: if you roll a v8, it would be nice to have braces around
both of these else parts to match the braces on the then parts.
Obviously this doesn't warrant a v8 on its own, though.

> +    }
> +    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 <stdbool.h>
> +#include <stdio.h>
> +#include <talloc.h>
> +#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

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: notmuch-reply: Structured Formatters
  2012-07-20  6:36                     ` notmuch-reply: Structured Formatters craven
                                         ` (2 preceding siblings ...)
  2012-07-20  6:36                       ` =?\x10\x10?q?=5BPATCH=20v7=203/3=5D=20Use=20the=20structured=20formatters=20in=20notmuch-search=2Ec=2E?= craven
@ 2012-07-22 16:13                       ` Austin Clements
  3 siblings, 0 replies; 39+ messages in thread
From: Austin Clements @ 2012-07-22 16:13 UTC (permalink / raw)
  To: craven; +Cc: notmuch

LGTM!  This is a great cleanup and I'm looking forward to S-expression
support.

Quoth craven@gmx.net on Jul 20 at  8:36 am:
> Currently there is no easy way to add support for different structured
> formatters (like JSON). For example, adding support for S-Expressions
> would result in code duplication.
> 
> This patch series amends the situation by introducing structured
> formatters, which allow different implementations of structures like
> lists, maps, strings and numbers.
> 
> The new code in sprinter.h and sprinter-json.c can be used instead of
> the current ad-hoc output in all parts of notmuch, a patch for
> notmuch-search.c is included.
> 
> In a later patch, all other parts of notmuch should be adapted to the
> structured formatters, and the creation of formatters should be
> centralised (to make adding new formatters easier).
> 
> A "structured" formatter is provided for notmuch-search that prints the
> current text format. This removes almost all the special-casing from
> notmuch-search.c.
> 
> Overall diff --stat:
> 
>  Makefile.local   |   2 +
>  notmuch-search.c | 301 +++++++++++++----------------------------------
>  sprinter-json.c  | 185 +++++++++++++++++++++++++++++
>  sprinter-text.c  | 126 ++++++++++++++++++++
>  sprinter.h       |  68 +++++++++++
>  test/json        |  34 +++---
>  6 files changed, 484 insertions(+), 232 deletions(-)
> 
> Changes versus v6 of this patch:
> - is_text_printer is now a field, not a function.
> - minor formatting
> - sprinter_text_search has been renamed to sprinter_text (as it contains
>   no search-specific code).
> - string sanitization removed from sprinter_text, the caller should
>   sanitize the strings.

^ permalink raw reply	[flat|nested] 39+ messages in thread

end of thread, other threads:[~2012-07-22 16:13 UTC | newest]

Thread overview: 39+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2012-07-10 10:02 [PATCH] Added better support for multiple structured output formats craven
2012-07-10 10:48 ` [PATCH] FIXED: " craven
2012-07-10 12:45   ` Mark Walters
2012-07-10 13:30     ` [PATCH] v2: " craven
2012-07-10 17:34       ` Mark Walters
2012-07-10 19:13       ` Austin Clements
2012-07-11  8:26         ` [PATCH v3 0/3] Structured Formatters craven
2012-07-11  8:26           ` [PATCH v3 1/3] Add support for structured output formatters craven
2012-07-11  8:26           ` [PATCH v3 2/3] Adding a structured formatter for JSON craven
2012-07-11  8:26           ` [PATCH v3 3/3] Use the JSON structure printer in notmuch-search craven
2012-07-13  8:11         ` [PATCH v5 0/3] notmuch-reply: Structured Formatters Peter Feigl
2012-07-13  8:11           ` [PATCH v5 1/3] Add support for structured output formatters Peter Feigl
2012-07-14  1:46             ` Austin Clements
2012-07-13  8:11           ` [PATCH v5 2/3] Add structured output formatter for JSON and text Peter Feigl
2012-07-14  2:02             ` Austin Clements
2012-07-13  8:11           ` [PATCH v5 3/3] Use the structured formatters in notmuch-search.c Peter Feigl
2012-07-14  2:09             ` Austin Clements
2012-07-16  8:34               ` [PATCH v6 0/3] notmuch-reply: Structured Formatters craven
2012-07-16  8:35                 ` [PATCH v6 1/3] Add support for structured output formatters craven
2012-07-16  8:35                 ` [PATCH v6 2/3] Add structured output formatter for JSON and plain text craven
2012-07-18 19:48                   ` Austin Clements
2012-07-20  6:36                     ` notmuch-reply: Structured Formatters craven
2012-07-20  6:36                       ` [PATCH v7 1/3] Add support for structured output formatters craven
2012-07-20  9:09                         ` Tomi Ollila
2012-07-20  9:13                           ` craven
2012-07-20  6:36                       ` [PATCH v7 2/3] Add structured output formatter for JSON and plain text (but don't use them yet) craven
2012-07-22 16:08                         ` Austin Clements
2012-07-20  6:36                       ` =?\x10\x10?q?=5BPATCH=20v7=203/3=5D=20Use=20the=20structured=20formatters=20in=20notmuch-search=2Ec=2E?= craven
2012-07-20  8:23                         ` [PATCH v7 3/3] Use the structured formatters in notmuch-search.c craven
2012-07-20  9:14                         ` Tomi Ollila
2012-07-22 16:13                       ` notmuch-reply: Structured Formatters Austin Clements
2012-07-16  8:35                 ` [PATCH v6 3/3] Use the structured formatters in notmuch-search.c craven
2012-07-18 19:56                   ` Austin Clements
2012-07-13  8:17           ` Proof of concept: S-Expression format craven
2012-07-10 17:04     ` [PATCH] FIXED: Added better support for multiple structured output formats Jameson Graef Rollins
2012-07-10 17:28       ` Austin Clements
2012-07-10 17:40         ` Jameson Graef Rollins
2012-07-10 22:45       ` deprecating legacy text output David Bremner
2012-07-10 16:58   ` [PATCH] FIXED: Added better support for multiple structured output formats Jameson Graef Rollins

Code repositories for project(s) associated with this public inbox

	https://yhetil.org/notmuch.git/

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).