MEDIUM: stats: Add show json schema

This may be used to output the JSON schema which describes the output of
show info json and show stats json.

The JSON output is without any extra whitespace in order to reduce the
volume of output. For human consumption passing the output through a
pretty printer may be helpful.

e.g.:
$ echo "show schema json" | socat /var/run/haproxy.stat stdio | \
     python -m json.tool

The implementation does not generate the schema. Some consideration could
be given to integrating the output of the schema with the output of
typed and json info and stats. In particular the types (u32, s64, etc...)
and tags.

A sample verification of show info json and show stats json using
the schema is as follows. It uses the jsonschema python module:

cat > jschema.py <<  __EOF__
import json

from jsonschema import validate
from jsonschema.validators import Draft3Validator

with open('schema.txt', 'r') as f:
    schema = json.load(f)
    Draft3Validator.check_schema(schema)

    with open('instance.txt', 'r') as f:
        instance = json.load(f)
	validate(instance, schema, Draft3Validator)
__EOF__

$ echo "show schema json" | socat /var/run/haproxy.stat stdio > schema.txt
$ echo "show info json" | socat /var/run/haproxy.stat stdio > instance.txt
python ./jschema.py
$ echo "show stats json" | socat /var/run/haproxy.stat stdio > instance.txt
python ./jschema.py

Signed-off-by: Simon Horman <horms@verge.net.au>
diff --git a/doc/management.txt b/doc/management.txt
index c0e32b9..791d297 100644
--- a/doc/management.txt
+++ b/doc/management.txt
@@ -1858,7 +1858,7 @@
       (...)
 
   The format of JSON output is described in a schema which may be output
-  using "show schema json" (to be implemented).
+  using "show schema json".
 
   The JSON output contains no extra whitespace in order to reduce the
   volume of output. For human consumption passing the output through a
@@ -1867,6 +1867,13 @@
   $ echo "show info json" | socat /var/run/haproxy.sock stdio | \
     python -m json.tool
 
+  The JSON output contains no extra whitespace in order to reduce the
+  volume of output. For human consumption passing the output through a
+  pretty printer may be helpful. Example :
+
+  $ echo "show info json" | socat /var/run/haproxy.sock stdio | \
+    python -m json.tool
+
 show map [<map>]
   Dump info about map converters. Without argument, the list of all available
   maps is returned. If a <map> is specified, its contents are dumped. <map> is
@@ -2137,7 +2144,14 @@
         (...)
 
   The format of JSON output is described in a schema which may be output
-  using "show schema json" (to be implemented).
+  using "show schema json".
+
+  The JSON output contains no extra whitespace in order to reduce the
+  volume of output. For human consumption passing the output through a
+  pretty printer may be helpful. Example :
+
+  $ echo "show stat json" | socat /var/run/haproxy.sock stdio | \
+    python -m json.tool
 
   The JSON output contains no extra whitespace in order to reduce the
   volume of output. For human consumption passing the output through a
@@ -2246,6 +2260,21 @@
   specified as parameter, it will dump the tickets, using * it will dump every
   keys from every references.
 
+show schema json
+  Dump the schema used for the output of "show info json" and "show stat json".
+
+  The contains no extra whitespace in order to reduce the volume of output.
+  For human consumption passing the output through a pretty printer may be
+  helpful. Example :
+
+  $ echo "show schema json" | socat /var/run/haproxy.sock stdio | \
+    python -m json.tool
+
+  The schema follows "JSON Schema" (json-schema.org) and accordingly
+  verifiers may be used to verify the output of "show info json" and "show
+  stat json" against the schema.
+
+
 shutdown frontend <frontend>
   Completely delete the specified frontend. All the ports it was bound to will
   be released. It will not be possible to enable the frontend anymore after
diff --git a/include/types/stats.h b/include/types/stats.h
index aad694c..7022468 100644
--- a/include/types/stats.h
+++ b/include/types/stats.h
@@ -215,8 +215,9 @@
 	FS_MASK     = 0xFF000000,
 };
 
-/* Please consider updating stats_dump_fields_*() and
- * stats_dump_.*_info_fields() when modifying struct field or related enums.
+/* Please consider updating stats_dump_fields_*(),
+ * stats_dump_.*_info_fields() and stats_*_schema()
+ * when modifying struct field or related enums.
  */
 struct field {
 	uint32_t type;
diff --git a/src/stats.c b/src/stats.c
index 66ab8d8..736852b 100644
--- a/src/stats.c
+++ b/src/stats.c
@@ -3299,6 +3299,234 @@
 	return 1;
 }
 
+/* This function dumps the schema onto the stream interface's read buffer.
+ * It returns 0 as long as it does not complete, non-zero upon completion.
+ * No state is used.
+ *
+ * Integer values bouned to the range [-(2**53)+1, (2**53)-1] as
+ * per the recommendation for interoperable integers in section 6 of RFC 7159.
+ */
+static void stats_dump_json_schema(struct chunk *out)
+{
+
+	int old_len = out->len;
+
+	chunk_strcat(out,
+		     "{"
+		      "\"$schema\":\"http://json-schema.org/draft-04/schema#\","
+		      "\"oneOf\":["
+		       "{"
+			"\"title\":\"Info\","
+			"\"type\":\"array\","
+			"\"items\":{"
+			 "\"properties\":{"
+			  "\"title\":\"InfoItem\","
+			  "\"type\":\"object\","
+			  "\"field\":{\"$ref\":\"#/definitions/field\"},"
+			  "\"processNum\":{\"$ref\":\"#/definitions/processNum\"},"
+			  "\"tags\":{\"$ref\":\"#/definitions/tags\"},"
+			  "\"value\":{\"$ref\":\"#/definitions/typedValue\"}"
+			 "},"
+			 "\"required\":[\"field\",\"processNum\",\"tags\","
+				       "\"value\"]"
+			"}"
+		       "},"
+		       "{"
+			"\"title\":\"Stat\","
+			"\"type\":\"array\","
+			"\"items\":{"
+			 "\"title\":\"InfoItem\","
+			 "\"type\":\"object\","
+			 "\"properties\":{"
+			  "\"objType\":{"
+			   "\"enum\":[\"Frontend\",\"Backend\",\"Listener\","
+				     "\"Server\",\"Unknown\"]"
+			  "},"
+			  "\"proxyId\":{"
+			   "\"type\":\"integer\","
+			   "\"minimum\":0"
+			  "},"
+			  "\"id\":{"
+			   "\"type\":\"integer\","
+			   "\"minimum\":0"
+			  "},"
+			  "\"field\":{\"$ref\":\"#/definitions/field\"},"
+			  "\"processNum\":{\"$ref\":\"#/definitions/processNum\"},"
+			  "\"tags\":{\"$ref\":\"#/definitions/tags\"},"
+			  "\"typedValue\":{\"$ref\":\"#/definitions/typedValue\"}"
+			 "},"
+			 "\"required\":[\"objType\",\"proxyId\",\"id\","
+				       "\"field\",\"processNum\",\"tags\","
+				       "\"value\"]"
+			"}"
+		       "},"
+		       "{"
+			"\"title\":\"Error\","
+			"\"type\":\"object\","
+			"\"properties\":{"
+			 "\"errorStr\":{"
+			  "\"type\":\"string\""
+			 "},"
+			 "\"required\":[\"errorStr\"]"
+			"}"
+		       "}"
+		      "],"
+		      "\"definitions\":{"
+		       "\"field\":{"
+			"\"type\":\"object\","
+			"\"pos\":{"
+			 "\"type\":\"integer\","
+			 "\"minimum\":0"
+			"},"
+			"\"name\":{"
+			 "\"type\":\"string\""
+			"},"
+			"\"required\":[\"pos\",\"name\"]"
+		       "},"
+		       "\"processNum\":{"
+			"\"type\":\"integer\","
+			"\"minimum\":1"
+		       "},"
+		       "\"tags\":{"
+			"\"type\":\"object\","
+			"\"origin\":{"
+			 "\"type\":\"string\","
+			 "\"enum\":[\"Metric\",\"Status\",\"Key\","
+				   "\"Config\",\"Product\",\"Unknown\"]"
+			"},"
+			"\"nature\":{"
+			 "\"type\":\"string\","
+			 "\"enum\":[\"Gauge\",\"Limit\",\"Min\",\"Max\","
+				   "\"Rate\",\"Counter\",\"Duration\","
+				   "\"Age\",\"Time\",\"Name\",\"Output\","
+				   "\"Avg\", \"Unknown\"]"
+			"},"
+			"\"scope\":{"
+			 "\"type\":\"string\","
+			 "\"enum\":[\"Cluster\",\"Process\",\"Service\","
+				   "\"System\",\"Unknown\"]"
+			"},"
+			"\"required\":[\"origin\",\"nature\",\"scope\"]"
+		       "},"
+		       "\"typedValue\":{"
+			"\"type\":\"object\","
+			"\"oneOf\":["
+			 "{\"$ref\":\"#/definitions/typedValue/definitions/s32Value\"},"
+			 "{\"$ref\":\"#/definitions/typedValue/definitions/s64Value\"},"
+			 "{\"$ref\":\"#/definitions/typedValue/definitions/u32Value\"},"
+			 "{\"$ref\":\"#/definitions/typedValue/definitions/u64Value\"},"
+			 "{\"$ref\":\"#/definitions/typedValue/definitions/strValue\"}"
+			"],"
+			"\"definitions\":{"
+			 "\"s32Value\":{"
+			  "\"properties\":{"
+			   "\"type\":{"
+			    "\"type\":\"string\","
+			    "\"enum\":[\"s32\"]"
+			   "},"
+			   "\"value\":{"
+			    "\"type\":\"integer\","
+			    "\"minimum\":-2147483648,"
+			    "\"maximum\":2147483647"
+			   "}"
+			  "},"
+			  "\"required\":[\"type\",\"value\"]"
+			 "},"
+			 "\"s64Value\":{"
+			  "\"properties\":{"
+			   "\"type\":{"
+			    "\"type\":\"string\","
+			    "\"enum\":[\"s64\"]"
+			   "},"
+			   "\"value\":{"
+			    "\"type\":\"integer\","
+			    "\"minimum\":-9007199254740991,"
+			    "\"maximum\":9007199254740991"
+			   "}"
+			  "},"
+			  "\"required\":[\"type\",\"value\"]"
+			 "},"
+			 "\"u32Value\":{"
+			  "\"properties\":{"
+			   "\"type\":{"
+			    "\"type\":\"string\","
+			    "\"enum\":[\"u32\"]"
+			   "},"
+			   "\"value\":{"
+			    "\"type\":\"integer\","
+			    "\"minimum\":0,"
+			    "\"maximum\":4294967295"
+			   "}"
+			  "},"
+			  "\"required\":[\"type\",\"value\"]"
+			 "},"
+			 "\"u64Value\":{"
+			  "\"properties\":{"
+			   "\"type\":{"
+			    "\"type\":\"string\","
+			    "\"enum\":[\"u64\"]"
+			   "},"
+			   "\"value\":{"
+			    "\"type\":\"integer\","
+			    "\"minimum\":0,"
+			    "\"maximum\":9007199254740991"
+			   "}"
+			  "},"
+			  "\"required\":[\"type\",\"value\"]"
+			 "},"
+			 "\"strValue\":{"
+			  "\"properties\":{"
+			   "\"type\":{"
+			    "\"type\":\"string\","
+			    "\"enum\":[\"str\"]"
+			   "},"
+			   "\"value\":{\"type\":\"string\"}"
+			  "},"
+			  "\"required\":[\"type\",\"value\"]"
+			 "},"
+			 "\"unknownValue\":{"
+			  "\"properties\":{"
+			   "\"type\":{"
+			    "\"type\":\"integer\","
+			    "\"minimum\":0"
+			   "},"
+			   "\"value\":{"
+			    "\"type\":\"string\","
+			    "\"enum\":[\"unknown\"]"
+			   "}"
+			  "},"
+			  "\"required\":[\"type\",\"value\"]"
+			 "}"
+			"}"
+		       "}"
+		      "}"
+		     "}");
+
+	if (old_len == out->len) {
+		chunk_reset(out);
+		chunk_appendf(out,
+			      "{\"errorStr\":\"output buffer too short\"}");
+	}
+}
+
+/* This function dumps the schema onto the stream interface's read buffer.
+ * It returns 0 as long as it does not complete, non-zero upon completion.
+ * No state is used.
+ */
+static int stats_dump_json_schema_to_buffer(struct stream_interface *si)
+{
+	chunk_reset(&trash);
+
+	stats_dump_json_schema(&trash);
+
+	if (bi_putchk(si_ic(si), &trash) == -1) {
+		si_applet_cant_put(si);
+		return 0;
+	}
+
+	return 1;
+}
+
 static int cli_parse_clear_counters(char **args, struct appctx *appctx, void *private)
 {
 	struct proxy *px;
@@ -3420,11 +3648,17 @@
 	return stats_dump_stat_to_buffer(appctx->owner, NULL);
 }
 
+static int cli_io_handler_dump_json_schema(struct appctx *appctx)
+{
+	return stats_dump_json_schema_to_buffer(appctx->owner);
+}
+
 /* register cli keywords */
 static struct cli_kw_list cli_kws = {{ },{
 	{ { "clear", "counters",  NULL }, "clear counters : clear max statistics counters (add 'all' for all counters)", cli_parse_clear_counters, NULL, NULL },
 	{ { "show", "info",  NULL }, "show info      : report information about the running process", cli_parse_show_info, cli_io_handler_dump_info, NULL },
 	{ { "show", "stat",  NULL }, "show stat      : report counters for each proxy and server", cli_parse_show_stat, cli_io_handler_dump_stat, NULL },
+	{ { "show", "schema",  "json", NULL }, "show schema json : report schema used for stats", NULL, cli_io_handler_dump_json_schema, NULL },
 	{{},}
 }};