From ecdc14f5352618910b43a9a041600a92af69fb5c Mon Sep 17 00:00:00 2001 From: Eric Haszlakiewicz Date: Sat, 29 Oct 2016 15:01:20 -0400 Subject: [PATCH] Add a json_c_visit() function to provide a way to iterate over a tree of json-c objects. --- .gitignore | 9 +-- Makefile.am | 2 + json_visit.c | 133 ++++++++++++++++++++++++++++++++++++++ json_visit.h | 91 ++++++++++++++++++++++++++ tests/Makefile.am | 1 + tests/test_visit.c | 111 +++++++++++++++++++++++++++++++ tests/test_visit.expected | 55 ++++++++++++++++ tests/test_visit.test | 12 ++++ 8 files changed, 410 insertions(+), 4 deletions(-) create mode 100644 json_visit.c create mode 100644 json_visit.h create mode 100644 tests/test_visit.c create mode 100644 tests/test_visit.expected create mode 100755 tests/test_visit.test diff --git a/.gitignore b/.gitignore index 68b6fcf602..b7098da21b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,18 +22,19 @@ /tests/test4 /tests/testReplaceExisting /tests/testSubDir -/tests/test_parse_int64 -/tests/test_parse /tests/test_cast /tests/test_charcase +/tests/test_compare /tests/test_double_serializer /tests/test_locale /tests/test_null +/tests/test_parse +/tests/test_parse_int64 /tests/test_printbuf /tests/test_set_serializer -/tests/test_compare -/tests/test_util_file /tests/test_set_value +/tests/test_util_file +/tests/test_visit /tests/*.vg.out /tests/*.log /tests/*.trs diff --git a/Makefile.am b/Makefile.am index c407576045..a5d126b0d2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -28,6 +28,7 @@ libjson_cinclude_HEADERS = \ json_object_private.h \ json_tokener.h \ json_util.h \ + json_visit.h \ linkhash.h \ math_compat.h \ printbuf.h \ @@ -43,6 +44,7 @@ libjson_c_la_SOURCES = \ json_object_iterator.c \ json_tokener.c \ json_util.c \ + json_visit.c \ linkhash.c \ printbuf.c \ random_seed.c diff --git a/json_visit.c b/json_visit.c new file mode 100644 index 0000000000..837ffd2b90 --- /dev/null +++ b/json_visit.c @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2016 Eric Haszlakiewicz + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See COPYING for details. + */ + +#include + +#include "config.h" +#include "json_inttypes.h" +#include "json_object.h" +#include "json_visit.h" +#include "linkhash.h" + +static int _json_c_visit(json_object *jso, json_object *parent_jso, + const char *jso_key, size_t *jso_index, + json_c_visit_userfunc userfunc, void *userarg); + +int json_c_visit(json_object *jso, int future_flags, + json_c_visit_userfunc userfunc, void *userarg) +{ + int ret = _json_c_visit(jso, NULL, NULL, NULL, userfunc, userarg); + switch(ret) + { + case JSON_C_VISIT_RETURN_CONTINUE: + case JSON_C_VISIT_RETURN_SKIP: + case JSON_C_VISIT_RETURN_POP: + case JSON_C_VISIT_RETURN_STOP: + return 0; + default: + return JSON_C_VISIT_RETURN_ERROR; + } +} +static int _json_c_visit(json_object *jso, json_object *parent_jso, + const char *jso_key, size_t *jso_index, + json_c_visit_userfunc userfunc, void *userarg) +{ + int userret = userfunc(jso, 0, parent_jso, jso_key, jso_index, userarg); + switch(userret) + { + case JSON_C_VISIT_RETURN_CONTINUE: + break; + case JSON_C_VISIT_RETURN_SKIP: + case JSON_C_VISIT_RETURN_POP: + case JSON_C_VISIT_RETURN_STOP: + case JSON_C_VISIT_RETURN_ERROR: + return userret; + default: + fprintf(stderr, "ERROR: invalid return value from json_c_visit userfunc: %d\n", userret); + return JSON_C_VISIT_RETURN_ERROR; + } + + switch(json_object_get_type(jso)) + { + case json_type_null: + case json_type_boolean: + case json_type_double: + case json_type_int: + case json_type_string: + // we already called userfunc above, move on to the next object + return JSON_C_VISIT_RETURN_CONTINUE; + + case json_type_object: + { + json_object_object_foreach(jso, key, child) + { + userret = _json_c_visit(child, jso, key, NULL, userfunc, userarg); + if (userret == JSON_C_VISIT_RETURN_POP) + break; + if (userret == JSON_C_VISIT_RETURN_STOP || + userret == JSON_C_VISIT_RETURN_ERROR) + return userret; + if (userret != JSON_C_VISIT_RETURN_CONTINUE && + userret != JSON_C_VISIT_RETURN_SKIP) + { + fprintf(stderr, "INTERNAL ERROR: _json_c_visit returned %d\n", userret); + return JSON_C_VISIT_RETURN_ERROR; + } + } + break; + } + case json_type_array: + { + size_t array_len = json_object_array_length(jso); + size_t ii; + for (ii = 0; ii < array_len; ii++) + { + json_object *child = json_object_array_get_idx(jso, ii); + userret = _json_c_visit(child, jso, NULL, &ii, userfunc, userarg); + if (userret == JSON_C_VISIT_RETURN_POP) + break; + if (userret == JSON_C_VISIT_RETURN_STOP || + userret == JSON_C_VISIT_RETURN_ERROR) + return userret; + if (userret != JSON_C_VISIT_RETURN_CONTINUE && + userret != JSON_C_VISIT_RETURN_SKIP) + { + fprintf(stderr, "INTERNAL ERROR: _json_c_visit returned %d\n", userret); + return JSON_C_VISIT_RETURN_ERROR; + } + } + break; + } + default: + fprintf(stderr, "INTERNAL ERROR: _json_c_visit found object of unknown type: %d\n", json_object_get_type(jso)); + return JSON_C_VISIT_RETURN_ERROR; + } + + // Call userfunc for the second type on container types, after all + // members of the container have been visited. + // Non-container types will have already returned before this point. + + userret = userfunc(jso, JSON_C_VISIT_SECOND, parent_jso, jso_key, jso_index, userarg); + switch(userret) + { + case JSON_C_VISIT_RETURN_SKIP: + case JSON_C_VISIT_RETURN_POP: + // These are not really sensible during JSON_C_VISIT_SECOND, + // but map them to JSON_C_VISIT_CONTINUE anyway. + // FALLTHROUGH + case JSON_C_VISIT_RETURN_CONTINUE: + return JSON_C_VISIT_RETURN_CONTINUE; + case JSON_C_VISIT_RETURN_STOP: + case JSON_C_VISIT_RETURN_ERROR: + return userret; + default: + fprintf(stderr, "ERROR: invalid return value from json_c_visit userfunc: %d\n", userret); + return JSON_C_VISIT_RETURN_ERROR; + } + // NOTREACHED +} + diff --git a/json_visit.h b/json_visit.h new file mode 100644 index 0000000000..1d6c68a214 --- /dev/null +++ b/json_visit.h @@ -0,0 +1,91 @@ + +#ifndef _json_c_json_visit_h_ +#define _json_c_json_visit_h_ + +#include "json_object.h" + +typedef int (json_c_visit_userfunc)(json_object *jso, int flags, + json_object *parent_jso, const char *jso_key, + size_t *jso_index, void *userarg); + +/** + * Visit each object in the JSON hierarchy starting at jso. + * For each object, userfunc is called, passing the object and userarg. + * If the object has a parent (i.e. anything other than jso itself) + * its parent will be passed as parent_jso, and either jso_key or jso_index + * will be set, depending on whether the parent is an object or an array. + * + * Nodes will be visited depth first, but containers (arrays and objects) + * will be visited twice, the second time with JSON_C_VISIT_SECOND set in + * flags. + * + * userfunc must return one of the defined return values, to indicate + * whether and how to continue visiting nodes, or one of various ways to stop. + * + * Returns 0 if nodes were visited successfully, even if some were + * intentionally skipped due to what userfunc returned. + * Returns <0 if an error occurred during iteration, including if + * userfunc returned JSON_C_VISIT_RETURN_ERROR. + */ +int json_c_visit(json_object *jso, int future_flags, + json_c_visit_userfunc userfunc, void *userarg); + +/** + * Passed to json_c_visit_userfunc as one of the flags values to indicate + * that this is the second time a container (array or object) is being + * called, after all of it's members have been iterated over. + */ +#define JSON_C_VISIT_SECOND 0x02 + +/** + * This json_c_visit_userfunc return value indicates that iteration + * should proceed normally. + */ +#define JSON_C_VISIT_RETURN_CONTINUE 0 + + +/** + * This json_c_visit_userfunc return value indicates that iteration + * over the members of the current object should be skipped. + * If the current object isn't a container (array or object), this + * is no different than JSON_C_VISIT_RETURN_CONTINUE. + */ +#define JSON_C_VISIT_RETURN_SKIP 7547 + +/** + * This json_c_visit_userfunc return value indicates that iteration + * of the fields/elements of the containing object should stop + * and continue "popped up" a level of the object hierarchy. + * For example, returning this when handling arg will result in + * arg3 and any other fields being skipped. The next call to userfunc + * will be the JSON_C_VISIT_SECOND call on "foo", followed by a userfunc + * call on "bar". + *
+ * {
+ *   "foo": {
+ *     "arg1": 1,
+ *     "arg2": 2,
+ *     "arg3": 3,
+ *     ...
+ *   },
+ *   "bar": {
+ *     ...
+ *   }
+ * }
+ * 
+ */ +#define JSON_C_VISIT_RETURN_POP 767 + +/** + * This json_c_visit_userfunc return value indicates that iteration + * should stop immediately, and cause json_c_visit to return success. + */ +#define JSON_C_VISIT_RETURN_STOP 7867 + +/** + * This json_c_visit_userfunc return value indicates that iteration + * should stop immediately, and cause json_c_visit to return an error. + */ +#define JSON_C_VISIT_RETURN_ERROR -1 + +#endif /* _json_c_json_visit_h_ */ diff --git a/tests/Makefile.am b/tests/Makefile.am index f6e4b2d37c..1ee06c4c80 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -22,6 +22,7 @@ TESTS+= test_printbuf.test TESTS+= test_set_serializer.test TESTS+= test_compare.test TESTS+= test_set_value.test +TESTS+= test_visit.test check_PROGRAMS= check_PROGRAMS += $(TESTS:.test=) diff --git a/tests/test_visit.c b/tests/test_visit.c new file mode 100644 index 0000000000..f2ffe8c098 --- /dev/null +++ b/tests/test_visit.c @@ -0,0 +1,111 @@ +#include +#include +#include +#include +#include + +#include "json.h" +#include "json_tokener.h" +#include "json_visit.h" + +static json_c_visit_userfunc emit_object; +static json_c_visit_userfunc skip_arrays; +static json_c_visit_userfunc pop_and_stop; +static json_c_visit_userfunc err_on_subobj2; + +int main(void) +{ + MC_SET_DEBUG(1); + + const char *input = "{\ + \"obj1\": 123,\ + \"obj2\": {\ + \"subobj1\": \"aaa\",\ + \"subobj2\": \"bbb\",\ + \"subobj3\": [ \"elem1\", \"elem2\", true ],\ + },\ + \"obj3\": 1.234,\ + \"obj4\": [ true, false, null ]\ + }"; + + json_object *jso = json_tokener_parse(input); + printf("jso.to_string()=%s\n", json_object_to_json_string(jso)); + + int rv; + rv = json_c_visit(jso, 0, emit_object, NULL); + printf("json_c_visit(emit_object)=%d\n", rv); + printf("================================\n\n"); + + rv = json_c_visit(jso, 0, skip_arrays, NULL); + printf("json_c_visit(skip_arrays)=%d\n", rv); + printf("================================\n\n"); + + rv = json_c_visit(jso, 0, pop_and_stop, NULL); + printf("json_c_visit(pop_and_stop)=%d\n", rv); + printf("================================\n\n"); + + rv = json_c_visit(jso, 0, err_on_subobj2, NULL); + printf("json_c_visit(err_on_subobj2)=%d\n", rv); + printf("================================\n\n"); + + json_object_put(jso); +} + + +static int emit_object(json_object *jso, int flags, + json_object *parent_jso, + const char *jso_key, + size_t *jso_index, void *userarg) +{ + printf("flags: 0x%x, key: %s, index: %ld, value: %s\n", + flags, + (jso_key ? jso_key : "(null)"), + (jso_index ? (long)*jso_index : -1L), + json_object_to_json_string(jso)); + return JSON_C_VISIT_RETURN_CONTINUE; +} + +static int skip_arrays(json_object *jso, int flags, + json_object *parent_jso, + const char *jso_key, + size_t *jso_index, void *userarg) +{ + (void)emit_object(jso, flags, parent_jso, jso_key, jso_index, userarg); + if (json_object_get_type(jso) == json_type_array) + return JSON_C_VISIT_RETURN_SKIP; + return JSON_C_VISIT_RETURN_CONTINUE; +} + +static int pop_and_stop(json_object *jso, int flags, + json_object *parent_jso, + const char *jso_key, + size_t *jso_index, void *userarg) +{ + (void)emit_object(jso, flags, parent_jso, jso_key, jso_index, userarg); + if (jso_key != NULL && strcmp(jso_key, "subobj1") == 0) + { + printf("POP after handling subobj1\n"); + return JSON_C_VISIT_RETURN_POP; + } + if (jso_key != NULL && strcmp(jso_key, "obj3") == 0) + { + printf("STOP after handling obj3\n"); + return JSON_C_VISIT_RETURN_STOP; + } + return JSON_C_VISIT_RETURN_CONTINUE; +} + +static int err_on_subobj2(json_object *jso, int flags, + json_object *parent_jso, + const char *jso_key, + size_t *jso_index, void *userarg) +{ + (void)emit_object(jso, flags, parent_jso, jso_key, jso_index, userarg); + if (jso_key != NULL && strcmp(jso_key, "subobj2") == 0) + { + printf("ERROR after handling subobj1\n"); + return JSON_C_VISIT_RETURN_ERROR; + } + return JSON_C_VISIT_RETURN_CONTINUE; +} + diff --git a/tests/test_visit.expected b/tests/test_visit.expected new file mode 100644 index 0000000000..2f05622b9c --- /dev/null +++ b/tests/test_visit.expected @@ -0,0 +1,55 @@ +jso.to_string()={ "obj1": 123, "obj2": { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] }, "obj3": 1.234, "obj4": [ true, false, null ] } +flags: 0x0, key: (null), index: -1, value: { "obj1": 123, "obj2": { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] }, "obj3": 1.234, "obj4": [ true, false, null ] } +flags: 0x0, key: obj1, index: -1, value: 123 +flags: 0x0, key: obj2, index: -1, value: { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] } +flags: 0x0, key: subobj1, index: -1, value: "aaa" +flags: 0x0, key: subobj2, index: -1, value: "bbb" +flags: 0x0, key: subobj3, index: -1, value: [ "elem1", "elem2", true ] +flags: 0x0, key: (null), index: 0, value: "elem1" +flags: 0x0, key: (null), index: 1, value: "elem2" +flags: 0x0, key: (null), index: 2, value: true +flags: 0x2, key: subobj3, index: -1, value: [ "elem1", "elem2", true ] +flags: 0x2, key: obj2, index: -1, value: { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] } +flags: 0x0, key: obj3, index: -1, value: 1.234 +flags: 0x0, key: obj4, index: -1, value: [ true, false, null ] +flags: 0x0, key: (null), index: 0, value: true +flags: 0x0, key: (null), index: 1, value: false +flags: 0x0, key: (null), index: 2, value: null +flags: 0x2, key: obj4, index: -1, value: [ true, false, null ] +flags: 0x2, key: (null), index: -1, value: { "obj1": 123, "obj2": { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] }, "obj3": 1.234, "obj4": [ true, false, null ] } +json_c_visit(emit_object)=0 +================================ + +flags: 0x0, key: (null), index: -1, value: { "obj1": 123, "obj2": { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] }, "obj3": 1.234, "obj4": [ true, false, null ] } +flags: 0x0, key: obj1, index: -1, value: 123 +flags: 0x0, key: obj2, index: -1, value: { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] } +flags: 0x0, key: subobj1, index: -1, value: "aaa" +flags: 0x0, key: subobj2, index: -1, value: "bbb" +flags: 0x0, key: subobj3, index: -1, value: [ "elem1", "elem2", true ] +flags: 0x2, key: obj2, index: -1, value: { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] } +flags: 0x0, key: obj3, index: -1, value: 1.234 +flags: 0x0, key: obj4, index: -1, value: [ true, false, null ] +flags: 0x2, key: (null), index: -1, value: { "obj1": 123, "obj2": { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] }, "obj3": 1.234, "obj4": [ true, false, null ] } +json_c_visit(skip_arrays)=0 +================================ + +flags: 0x0, key: (null), index: -1, value: { "obj1": 123, "obj2": { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] }, "obj3": 1.234, "obj4": [ true, false, null ] } +flags: 0x0, key: obj1, index: -1, value: 123 +flags: 0x0, key: obj2, index: -1, value: { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] } +flags: 0x0, key: subobj1, index: -1, value: "aaa" +POP after handling subobj1 +flags: 0x2, key: obj2, index: -1, value: { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] } +flags: 0x0, key: obj3, index: -1, value: 1.234 +STOP after handling obj3 +json_c_visit(pop_and_stop)=0 +================================ + +flags: 0x0, key: (null), index: -1, value: { "obj1": 123, "obj2": { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] }, "obj3": 1.234, "obj4": [ true, false, null ] } +flags: 0x0, key: obj1, index: -1, value: 123 +flags: 0x0, key: obj2, index: -1, value: { "subobj1": "aaa", "subobj2": "bbb", "subobj3": [ "elem1", "elem2", true ] } +flags: 0x0, key: subobj1, index: -1, value: "aaa" +flags: 0x0, key: subobj2, index: -1, value: "bbb" +ERROR after handling subobj1 +json_c_visit(err_on_subobj2)=-1 +================================ + diff --git a/tests/test_visit.test b/tests/test_visit.test new file mode 100755 index 0000000000..87e25616db --- /dev/null +++ b/tests/test_visit.test @@ -0,0 +1,12 @@ +#!/bin/sh + +# Common definitions +if test -z "$srcdir"; then + srcdir="${0%/*}" + test "$srcdir" = "$0" && srcdir=. + test -z "$srcdir" && srcdir=. +fi +. "$srcdir/test-defs.sh" + +run_output_test test_visit +exit $?