docs: use "empty string" instead of "null string"
[jimtcl.git] / jim-json.c
blob1b3869cb6e81fdc8f22a534812bd5888974fbc77
1 /*
2 * Copyright (c) 2015 - 2016 Svyatoslav Mishyn <juef@openmailbox.org>
3 * Copyright (c) 2019 Steve Bennett <steveb@workware.net.au>
5 * Permission to use, copy, modify, and/or distribute this software for
6 * any purpose with or without fee is hereby granted, provided that the
7 * above copyright notice and this permission notice appear in all
8 * copies.
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
11 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
12 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
13 * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
14 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
15 * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
16 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
17 * PERFORMANCE OF THIS SOFTWARE.
20 #include <assert.h>
21 #include <stddef.h>
22 #include <stdlib.h>
23 #include <string.h>
25 #include <jim.h>
27 #include "jsmn/jsmn.h"
29 /* These are all the schema types we support */
30 typedef enum {
31 JSON_BOOL,
32 JSON_OBJ,
33 JSON_LIST,
34 JSON_MIXED,
35 JSON_STR,
36 JSON_NUM,
37 JSON_MAX_TYPE,
38 } json_schema_t;
40 struct json_state {
41 Jim_Obj *nullObj;
42 const char *json;
43 jsmntok_t *tok;
44 int need_subst;
45 /* The following are used for -schema */
46 int enable_schema;
47 int enable_index;
48 Jim_Obj *schemaObj;
49 Jim_Obj *schemaTypeObj[JSON_MAX_TYPE];
52 static void json_decode_dump_value(Jim_Interp *interp, struct json_state *state, Jim_Obj *list);
54 /**
55 * Start a new subschema. Returns the previous schemaObj.
56 * Does nothing and returns NULL if -schema is not enabled.
58 static Jim_Obj *json_decode_schema_push(Jim_Interp *interp, struct json_state *state)
60 Jim_Obj *prevSchemaObj = NULL;
61 if (state->enable_schema) {
62 prevSchemaObj = state->schemaObj;
63 state->schemaObj = Jim_NewListObj(interp, NULL, 0);
64 Jim_IncrRefCount(state->schemaObj);
66 return prevSchemaObj;
69 /**
70 * Combines the current schema with the previous schema, prevSchemaObj
71 * returned by json_decode_schema_push().
72 * Does nothing if -schema is not enabled.
74 static void json_decode_schema_pop(Jim_Interp *interp, struct json_state *state, Jim_Obj *prevSchemaObj)
76 if (state->enable_schema) {
77 Jim_ListAppendElement(interp, prevSchemaObj, state->schemaObj);
78 Jim_DecrRefCount(interp, state->schemaObj);
79 state->schemaObj = prevSchemaObj;
83 /**
84 * Appends the schema type to state->schemaObj based on 'type'
86 static void json_decode_add_schema_type(Jim_Interp *interp, struct json_state *state, json_schema_t type)
88 static const char * const schema_names[] = {
89 "bool",
90 "obj",
91 "list",
92 "mixed",
93 "str",
94 "num",
96 assert(type >= 0 && type < JSON_MAX_TYPE);
97 /* Share multiple instances of the same type */
98 if (state->schemaTypeObj[type] == NULL) {
99 state->schemaTypeObj[type] = Jim_NewStringObj(interp, schema_names[type], -1);
101 Jim_ListAppendElement(interp, state->schemaObj, state->schemaTypeObj[type]);
105 * Returns the schema type for the given token.
106 * There is a one-to-one correspondence except for JSMN_PRIMITIVE
107 * which will return JSON_BOOL for true, false and JSON_NUM otherise.
109 static json_schema_t json_decode_get_type(const jsmntok_t *tok, const char *json)
111 switch (tok->type) {
112 case JSMN_PRIMITIVE:
113 assert(json);
114 if (json[tok->start] == 't' || json[tok->start] == 'f') {
115 return JSON_BOOL;
117 return JSON_NUM;
118 case JSMN_OBJECT:
119 return JSON_OBJ;
120 case JSMN_ARRAY:
121 /* Return mixed by default - need other checks to select list instead */
122 return JSON_MIXED;
123 case JSMN_STRING:
124 default:
125 return JSON_STR;
130 * Returns the current object (state->tok) as a Tcl list.
132 * state->tok is incremented to just past the object that was dumped.
134 static Jim_Obj *
135 json_decode_dump_container(Jim_Interp *interp, struct json_state *state)
137 int i;
138 Jim_Obj *list = Jim_NewListObj(interp, NULL, 0);
139 int size = state->tok->size;
140 int type = state->tok->type;
141 json_schema_t container_type = JSON_OBJ; /* JSON_LIST, JSON_MIXED or JSON_OBJ */
143 if (state->schemaObj) {
144 json_schema_t list_type;
145 /* Figure out the type to use for the container */
146 if (type == JSMN_ARRAY) {
147 /* If every element of the array is of the same primitive schema type (str, bool or num),
148 * we can use "list", otherwise need to use "mixed"
150 container_type = JSON_LIST;
151 if (size) {
152 list_type = json_decode_get_type(&state->tok[1], state->json);
154 if (list_type == JSON_BOOL || list_type == JSON_STR || list_type == JSON_NUM) {
155 for (i = 2; i <= size; i++) {
156 if (json_decode_get_type(state->tok + i, state->json) != list_type) {
157 /* Can't use list */
158 container_type = JSON_MIXED;
159 break;
163 else {
164 container_type = JSON_MIXED;
168 json_decode_add_schema_type(interp, state, container_type);
169 if (container_type == JSON_LIST && size) {
170 json_decode_add_schema_type(interp, state, list_type);
174 state->tok++;
176 for (i = 0; i < size; i++) {
177 if (type == JSMN_OBJECT) {
178 /* Dump the object key */
179 if (state->enable_schema) {
180 const char *p = state->json + state->tok->start;
181 int len = state->tok->end - state->tok->start;
182 Jim_ListAppendElement(interp, state->schemaObj, Jim_NewStringObj(interp, p, len));
184 json_decode_dump_value(interp, state, list);
187 if (state->enable_index && type == JSMN_ARRAY) {
188 Jim_ListAppendElement(interp, list, Jim_NewIntObj(interp, i));
191 if (state->schemaObj && container_type != JSON_LIST) {
192 if (state->tok->type == JSMN_STRING || state->tok->type == JSMN_PRIMITIVE) {
193 json_decode_add_schema_type(interp, state, json_decode_get_type(state->tok, state->json));
197 /* Dump the array or object value */
198 json_decode_dump_value(interp, state, list);
201 return list;
205 * Appends the value at state->tok to 'list' and increments state->tok to just
206 * past that token.
208 * Also appends to the schema if state->enable_schema is set.
210 static void
211 json_decode_dump_value(Jim_Interp *interp, struct json_state *state, Jim_Obj *list)
213 const jsmntok_t *t = state->tok;
215 if (t->type == JSMN_STRING || t->type == JSMN_PRIMITIVE) {
216 Jim_Obj *elem;
217 int len = t->end - t->start;
218 const char *p = state->json + t->start;
219 if (t->type == JSMN_STRING) {
220 /* Do we need to process backslash escapes? */
221 if (state->need_subst == 0 && memchr(p, '\\', len) != NULL) {
222 state->need_subst = 1;
224 elem = Jim_NewStringObj(interp, p, len);
225 } else if (p[0] == 'n') { /* null */
226 elem = state->nullObj;
227 } else if (p[0] == 'I') {
228 elem = Jim_NewStringObj(interp, "Inf", -1);
229 } else if (p[0] == '-' && p[1] == 'I') {
230 elem = Jim_NewStringObj(interp, "-Inf", -1);
231 } else { /* number, true or false */
232 elem = Jim_NewStringObj(interp, p, len);
235 Jim_ListAppendElement(interp, list, elem);
236 state->tok++;
238 else {
239 Jim_Obj *prevSchemaObj = json_decode_schema_push(interp, state);
240 Jim_Obj *newList = json_decode_dump_container(interp, state);
241 Jim_ListAppendElement(interp, list, newList);
242 json_decode_schema_pop(interp, state, prevSchemaObj);
246 /* Parses the options ?-null string? ?-schema? *state.
247 * Any options not present are not set.
249 * Returns JIM_OK or JIM_ERR and sets an error result.
251 static int parse_json_decode_options(Jim_Interp *interp, int argc, Jim_Obj *const argv[], struct json_state *state)
253 static const char * const options[] = { "-index", "-null", "-schema", NULL };
254 enum { OPT_INDEX, OPT_NULL, OPT_SCHEMA, };
255 int i;
257 for (i = 1; i < argc - 1; i++) {
258 int option;
259 if (Jim_GetEnum(interp, argv[i], options, &option, NULL, JIM_ERRMSG | JIM_ENUM_ABBREV) != JIM_OK) {
260 return JIM_ERR;
262 switch (option) {
263 case OPT_INDEX:
264 state->enable_index = 1;
265 break;
267 case OPT_NULL:
268 i++;
269 Jim_IncrRefCount(argv[i]);
270 Jim_DecrRefCount(interp, state->nullObj);
271 state->nullObj = argv[i];
272 break;
274 case OPT_SCHEMA:
275 state->enable_schema = 1;
276 break;
280 if (i != argc - 1) {
281 Jim_WrongNumArgs(interp, 1, argv,
282 "?-index? ?-null nullvalue? ?-schema? json");
283 return JIM_ERR;
286 return JIM_OK;
290 * Use jsmn to tokenise the JSON string 'json' of length 'len'
292 * Returns an allocated array of tokens or NULL on error (and sets an error result)
294 static jsmntok_t *
295 json_decode_tokenize(Jim_Interp *interp, const char *json, size_t len)
297 jsmntok_t *t;
298 jsmn_parser parser;
299 int n;
301 /* Parse once just to find the number of tokens */
302 jsmn_init(&parser);
303 n = jsmn_parse(&parser, json, len, NULL, 0);
305 error:
306 switch (n) {
307 case JSMN_ERROR_INVAL:
308 Jim_SetResultString(interp, "invalid JSON string", -1);
309 return NULL;
311 case JSMN_ERROR_PART:
312 Jim_SetResultString(interp, "truncated JSON string", -1);
313 return NULL;
315 case 0:
316 Jim_SetResultString(interp, "root element must be an object or an array", -1);
317 return NULL;
319 default:
320 break;
323 if (n < 0) {
324 return NULL;
327 t = Jim_Alloc(n * sizeof(*t));
329 jsmn_init(&parser);
330 n = jsmn_parse(&parser, json, len, t, n);
331 if (t->type != JSMN_OBJECT && t->type != JSMN_ARRAY) {
332 n = 0;
334 if (n <= 0) {
335 Jim_Free(t);
336 goto error;
339 return t;
343 * json::decode returns the decoded data structure.
345 * If -schema is specified, returns a list of {data schema}
347 static int
348 json_decode(Jim_Interp *interp, int argc, Jim_Obj *const argv[])
350 Jim_Obj *list;
351 jsmntok_t *tokens;
352 int len;
353 int ret = JIM_ERR;
354 struct json_state state;
356 memset(&state, 0, sizeof(state));
358 state.nullObj = Jim_NewStringObj(interp, "null", -1);
359 Jim_IncrRefCount(state.nullObj);
361 if (parse_json_decode_options(interp, argc, argv, &state) != JIM_OK) {
362 goto done;
365 state.json = Jim_GetString(argv[argc - 1], &len);
367 if (!len) {
368 Jim_SetResultString(interp, "empty JSON string", -1);
369 goto done;
371 if ((tokens = json_decode_tokenize(interp, state.json, len)) == NULL) {
372 goto done;
374 state.tok = tokens;
375 json_decode_schema_push(interp, &state);
377 list = json_decode_dump_container(interp, &state);
378 Jim_Free(tokens);
379 ret = JIM_OK;
381 /* Make sure the refcount doesn't go to 0 during Jim_SubstObj() */
382 Jim_IncrRefCount(list);
384 if (state.need_subst) {
385 /* Subsitute backslashes in the returned dictionary.
386 * Need to be careful of refcounts.
387 * Note that Jim_SubstObj() supports a few more escapes than
388 * JSON requires, but should give the same result for all legal escapes.
390 Jim_Obj *newList;
391 Jim_SubstObj(interp, list, &newList, JIM_SUBST_FLAG | JIM_SUBST_NOCMD | JIM_SUBST_NOVAR);
392 Jim_IncrRefCount(newList);
393 Jim_DecrRefCount(interp, list);
394 list = newList;
397 if (state.schemaObj) {
398 Jim_Obj *resultObj = Jim_NewListObj(interp, NULL, 0);
399 Jim_ListAppendElement(interp, resultObj, list);
400 Jim_ListAppendElement(interp, resultObj, state.schemaObj);
401 Jim_SetResult(interp, resultObj);
402 Jim_DecrRefCount(interp, state.schemaObj);
404 else {
405 Jim_SetResult(interp, list);
407 Jim_DecrRefCount(interp, list);
409 done:
410 Jim_DecrRefCount(interp, state.nullObj);
412 return ret;
416 Jim_jsonInit(Jim_Interp *interp)
418 if (Jim_PackageProvide(interp, "json", "1.0", JIM_ERRMSG) != JIM_OK) {
419 return JIM_ERR;
422 Jim_CreateCommand(interp, "json::decode", json_decode, NULL, NULL);
423 /* Load the Tcl implementation of the json encoder if possible */
424 Jim_PackageRequire(interp, "jsonencode", 0);
425 return JIM_OK;