Browse Source

Implement json_setf

PUBLISHED_FROM=b5f1bd6e292f3d0f65729b5e557544a4eefc1a6f
Бобби 8 years ago
parent
commit
629211cd96
4 changed files with 303 additions and 17 deletions
  1. 32 2
      README.md
  2. 138 14
      frozen.c
  3. 19 0
      frozen.h
  4. 114 1
      unit_test.c

+ 32 - 2
README.md

@@ -6,9 +6,16 @@ JSON parser and emitter for C/C++
 - ISO C and ISO C++ compliant portable code
 - Very small footprint
 - No dependencies
-- Print and scan directly to/from C/C++ variables
+- `json_scanf()` scans a string directly into C/C++ variables
+- `json_printf()` prints C/C++ variables directly into an output stream
+- `json_setf()` modifies an existing JSON string
+- `json_fread()` reads JSON from a file
+- `json_fprintf()` writes JSON to a file
+- Built-in base64 encoder and decoder for binary data
 - Parser provides low-level callback API and high-level scanf-like API
 - 100% test coverage
+- Used in [Mongoose OS](https://mongoose-os.com), an operating system
+  for connected commercial products on low-power microcontrollers
 
 # API reference
 
@@ -194,7 +201,7 @@ A helper `%M` callback that prints contiguous C arrays.
 Consumes `void *array_ptr, size_t array_size, size_t elem_size, char *fmt`
 Returns number of bytes printed.
 
-## `json_walk()`
+## `json_walk()` - low level parsing API
 
 
 ```c
@@ -297,6 +304,29 @@ int json_vfprintf(const char *file_name, const char *fmt, va_list ap);
 char *json_fread(const char *file_name);
 ```
 
+## `json_setf()`, `json_vsetf()`
+
+```c
+/*
+ * Update given JSON string `s,len` by changing the value at given `json_path`.
+ * The result is saved to `out`. If `json_fmt` == NULL, that deletes the key.
+ * If path is not present, missing keys are added. Array path without an
+ * index pushes a value to the end of an array.
+ * Return 1 if the string was changed, 0 otherwise.
+ *
+ * Example:  s is a JSON string { "a": 1, "b": [ 2 ] }
+ *   json_setf(s, len, out, ".a", "7");     // { "a": 7, "b": [ 2 ] }
+ *   json_setf(s, len, out, ".b", "7");     // { "a": 1, "b": 7 }
+ *   json_setf(s, len, out, ".b[]", "7");   // { "a": 1, "b": [ 2,7 ] }
+ *   json_setf(s, len, out, ".b", NULL);    // { "a": 1 }
+ */
+int json_setf(const char *s, int len, struct json_out *out,
+              const char *json_path, const char *json_fmt, ...);
+
+int json_vsetf(const char *s, int len, struct json_out *out,
+               const char *json_path, const char *json_fmt, va_list ap);
+```
+
 # Contributions
 
 To submit contributions, sign

+ 138 - 14
frozen.c

@@ -289,9 +289,9 @@ static int parse_number(struct frozen *f) {
 static int parse_array(struct frozen *f) {
   int i = 0, current_path_len;
   char buf[20];
+  CALL_BACK(f, JSON_TYPE_ARRAY_START, NULL, 0);
   TRY(test_and_skip(f, '['));
   {
-    CALL_BACK(f, JSON_TYPE_ARRAY_START, NULL, 0);
     {
       SET_STATE(f, f->cur - 1, "", 0);
       while (cur(f) != ']') {
@@ -375,9 +375,6 @@ static int parse_value(struct frozen *f) {
 /* key = identifier | string */
 static int parse_key(struct frozen *f) {
   int ch = cur(f);
-#if 0
-  printf("%s [%.*s]\n", __func__, (int) (f->end - f->cur), f->cur);
-#endif
   if (is_alpha(ch)) {
     TRY(parse_identifier(f));
   } else if (ch == '"') {
@@ -408,19 +405,17 @@ static int parse_pair(struct frozen *f) {
 
 /* object = '{' pair { ',' pair } '}' */
 static int parse_object(struct frozen *f) {
+  CALL_BACK(f, JSON_TYPE_OBJECT_START, NULL, 0);
   TRY(test_and_skip(f, '{'));
   {
-    CALL_BACK(f, JSON_TYPE_OBJECT_START, NULL, 0);
-    {
-      SET_STATE(f, f->cur - 1, ".", 1);
-      while (cur(f) != '}') {
-        TRY(parse_pair(f));
-        if (cur(f) == ',') f->cur++;
-      }
-      TRY(test_and_skip(f, '}'));
-      truncate_path(f, fstate.path_len);
-      CALL_BACK(f, JSON_TYPE_OBJECT_END, fstate.ptr, f->cur - fstate.ptr);
+    SET_STATE(f, f->cur - 1, ".", 1);
+    while (cur(f) != '}') {
+      TRY(parse_pair(f));
+      if (cur(f) == ',') f->cur++;
     }
+    TRY(test_and_skip(f, '}'));
+    truncate_path(f, fstate.path_len);
+    CALL_BACK(f, JSON_TYPE_OBJECT_END, fstate.ptr, f->cur - fstate.ptr);
   }
   return 0;
 }
@@ -1062,3 +1057,132 @@ char *json_fread(const char *path) {
   }
   return data;
 }
+
+struct json_setf_data {
+  const char *json_path;
+  const char *base; /* Pointer to the source JSON string */
+  int matched;      /* Matched part of json_path */
+  int pos;          /* Offset of the mutated value begin */
+  int end;          /* Offset of the mutated value end */
+  int prev;         /* Offset of the previous token end */
+};
+
+static int get_matched_prefix_len(const char *s1, const char *s2) {
+  int i = 0;
+  while (s1[i] && s2[i] && s1[i] == s2[i]) i++;
+  return i;
+}
+
+static void json_vsetf_cb(void *userdata, const char *name, size_t name_len,
+                          const char *path, const struct json_token *t) {
+  struct json_setf_data *data = (struct json_setf_data *) userdata;
+  int off, len = get_matched_prefix_len(path, data->json_path);
+  if (t->ptr == NULL) return;
+  off = t->ptr - data->base;
+  // printf("--%d %s %d\n", t->type, path, off);
+  if (len > data->matched) data->matched = len;
+
+  /*
+   * If there is no exact path match, set the mutation position to tbe end
+   * of the object or array
+   */
+  if (len < data->matched && data->pos == 0 &&
+      (t->type == JSON_TYPE_OBJECT_END || t->type == JSON_TYPE_ARRAY_END)) {
+    data->pos = data->end = data->prev;
+  }
+
+  /* Exact path match. Set mutation position to the value of this token */
+  if (strcmp(path, data->json_path) == 0 && t->type != JSON_TYPE_OBJECT_START &&
+      t->type != JSON_TYPE_ARRAY_START) {
+    data->pos = off;
+    data->end = off + t->len;
+  }
+
+  /*
+   * For deletion, we need to know where the previous value ends, because
+   * we don't know where matched value key starts.
+   * When the mutation position is not yet set, remember each value end.
+   * When the mutation position is already set, but it is at the beginning
+   * of the object/array, we catch the end of the object/array and see
+   * whether the object/array start is closer then previously stored prev.
+   */
+  if (data->pos == 0) {
+    data->prev = off + t->len; /* pos is not yet set */
+  } else if ((t->ptr[0] == '[' || t->ptr[0] == '{') && off + 1 < data->pos &&
+             off + 1 > data->prev) {
+    data->prev = off + 1;
+  }
+  (void) name;
+  (void) name_len;
+}
+
+int json_vsetf(const char *s, int len, struct json_out *out,
+               const char *json_path, const char *json_fmt, va_list ap) WEAK;
+int json_vsetf(const char *s, int len, struct json_out *out,
+               const char *json_path, const char *json_fmt, va_list ap) {
+  struct json_setf_data data;
+  memset(&data, 0, sizeof(data));
+  data.json_path = json_path;
+  data.base = s;
+  data.end = len;
+  // printf("S:[%.*s] %s %p\n", len, s, json_path, json_fmt);
+  json_walk(s, len, json_vsetf_cb, &data);
+  // printf("-> %d %d %d\n", data.prev, data.pos, data.end);
+  if (json_fmt == NULL) {
+    /* Deletion codepath */
+    json_printf(out, "%.*s", data.prev, s);
+    /* Trim comma after the value that begins at object/array start */
+    if (s[data.prev - 1] == '{' || s[data.prev - 1] == '[') {
+      int i = data.end;
+      while (i < len && is_space(s[i])) i++;
+      if (s[i] == ',') data.end = i + 1; /* Point after comma */
+    }
+    json_printf(out, "%.*s", len - data.end, s + data.end);
+  } else {
+    /* Modification codepath */
+    int n, off = data.matched, depth = 0;
+
+    /* Print the unchanged beginning */
+    json_printf(out, "%.*s", data.pos, s);
+
+    /* Add missing keys */
+    while ((n = strcspn(&json_path[off], ".[")) > 0) {
+      if (s[data.prev - 1] != '{' && s[data.prev - 1] != '[' && depth == 0) {
+        json_printf(out, ",");
+      }
+      if (off > 0 && json_path[off - 1] != '.') break;
+      json_printf(out, "%.*Q:", 1, json_path + off);
+      off += n;
+      if (json_path[off] != '\0') {
+        json_printf(out, "%c", json_path[off] == '.' ? '{' : '[');
+        depth++;
+        off++;
+      }
+    }
+    /* Print the new value */
+    json_vprintf(out, json_fmt, ap);
+
+    /* Close brackets/braces of the added missing keys */
+    for (; off > data.matched; off--) {
+      int ch = json_path[off];
+      const char *p = ch == '.' ? "}" : ch == '[' ? "]" : "";
+      json_printf(out, "%s", p);
+    }
+
+    /* Print the rest of the unchanged string */
+    json_printf(out, "%.*s", len - data.end, s + data.end);
+  }
+  return data.end > data.pos ? 1 : 0;
+}
+
+int json_setf(const char *s, int len, struct json_out *out,
+              const char *json_path, const char *json_fmt, ...) WEAK;
+int json_setf(const char *s, int len, struct json_out *out,
+              const char *json_path, const char *json_fmt, ...) {
+  int result;
+  va_list ap;
+  va_start(ap, json_fmt);
+  result = json_vsetf(s, len, out, json_path, json_fmt, ap);
+  va_end(ap);
+  return result;
+}

+ 19 - 0
frozen.h

@@ -233,6 +233,25 @@ int json_escape(struct json_out *out, const char *str, size_t str_len);
  */
 char *json_fread(const char *file_name);
 
+/*
+ * Update given JSON string `s,len` by changing the value at given `json_path`.
+ * The result is saved to `out`. If `json_fmt` == NULL, that deletes the key.
+ * If path is not present, missing keys are added. Array path without an
+ * index pushes a value to the end of an array.
+ * Return 1 if the string was changed, 0 otherwise.
+ *
+ * Example:  s is a JSON string { "a": 1, "b": [ 2 ] }
+ *   json_setf(s, len, out, ".a", "7");     // { "a": 7, "b": [ 2 ] }
+ *   json_setf(s, len, out, ".b", "7");     // { "a": 1, "b": 7 }
+ *   json_setf(s, len, out, ".b[]", "7");   // { "a": 1, "b": [ 2,7 ] }
+ *   json_setf(s, len, out, ".b", NULL);    // { "a": 1 }
+ */
+int json_setf(const char *s, int len, struct json_out *out,
+              const char *json_path, const char *json_fmt, ...);
+
+int json_vsetf(const char *s, int len, struct json_out *out,
+               const char *json_path, const char *json_fmt, va_list ap);
+
 #ifdef __cplusplus
 }
 #endif /* __cplusplus */

+ 114 - 1
unit_test.c

@@ -604,7 +604,8 @@ static const char *test_scanf(void) {
     double c = 5.67;
     float fa = 0.0, fb = 0.0;
     double fc = 0.0;
-    ASSERT(json_scanf(str, strlen(str), "{fa: %f, fb: %f, fc: %lf}", &fa, &fb, &fc) == 3);
+    ASSERT(json_scanf(str, strlen(str), "{fa: %f, fb: %f, fc: %lf}", &fa, &fb,
+                      &fc) == 3);
     ASSERT(fa == a);
     ASSERT(fb == b);
     ASSERT(fc == c);
@@ -672,6 +673,117 @@ static const char *test_fprintf(void) {
   return NULL;
 }
 
+static const char *test_json_setf(void) {
+  char buf[200];
+  const char *s1 = "{ \"a\": 123, \"b\": [ 1 ], \"c\": true }";
+
+  {
+    struct json_out out = JSON_OUT_BUF(buf, sizeof(buf));
+    const char *s2 = "{ \"a\": 7, \"b\": [ 1 ], \"c\": true }";
+    int res = json_setf(s1, strlen(s1), &out, ".a", "%d", 7);
+    ASSERT(res == 1);
+    ASSERT(strcmp(buf, s2) == 0);
+  }
+
+  {
+    struct json_out out = JSON_OUT_BUF(buf, sizeof(buf));
+    const char *s2 = "{ \"a\": 123, \"b\": false, \"c\": true }";
+    int res = json_setf(s1, strlen(s1), &out, ".b", "%B", 0);
+    ASSERT(res == 1);
+    ASSERT(strcmp(buf, s2) == 0);
+  }
+
+  {
+    struct json_out out = JSON_OUT_BUF(buf, sizeof(buf));
+    const char *s2 = "{ \"a\": 123, \"b\": [ 2 ], \"c\": true }";
+    int res = json_setf(s1, strlen(s1), &out, ".b[0]", "%d", 2);
+    ASSERT(res == 1);
+    ASSERT(strcmp(buf, s2) == 0);
+  }
+
+  {
+    struct json_out out = JSON_OUT_BUF(buf, sizeof(buf));
+    const char *s2 = "{ \"b\": [ 1 ], \"c\": true }";
+    int res = json_setf(s1, strlen(s1), &out, ".a", NULL);
+    ASSERT(res == 1);
+    ASSERT(strcmp(buf, s2) == 0);
+  }
+
+  {
+    struct json_out out = JSON_OUT_BUF(buf, sizeof(buf));
+    const char *s2 = "{ \"a\": 123, \"b\": [ 1 ] }";
+    int res = json_setf(s1, strlen(s1), &out, ".c", NULL);
+    ASSERT(res == 1);
+    ASSERT(strcmp(buf, s2) == 0);
+  }
+
+  {
+    /* Delete non-existent key */
+    struct json_out out = JSON_OUT_BUF(buf, sizeof(buf));
+    const char *s1 = "{\"a\":1}";
+    int res = json_setf(s1, strlen(s1), &out, ".d", NULL);
+    ASSERT(res == 0);
+    ASSERT(strcmp(buf, s1) == 0);
+  }
+
+  {
+    /* Delete non-existent key, spaces in obj */
+    struct json_out out = JSON_OUT_BUF(buf, sizeof(buf));
+    int res = json_setf(s1, strlen(s1), &out, ".d", NULL);
+    ASSERT(res == 0);
+    ASSERT(strcmp(buf, s1) == 0);
+  }
+
+  {
+    /* Change the whole JSON object */
+    struct json_out out = JSON_OUT_BUF(buf, sizeof(buf));
+    const char *s2 = "123";
+    int res = json_setf(s1, strlen(s1), &out, "", "%d", 123);
+    ASSERT(res == 1);
+    ASSERT(strcmp(buf, s2) == 0);
+  }
+
+  {
+    /* Add missing keys */
+    struct json_out out = JSON_OUT_BUF(buf, sizeof(buf));
+    const char *s2 =
+        "{ \"a\": 123, \"b\": [ 1 ], \"c\": true,\"d\":{\"e\":8} }";
+    int res = json_setf(s1, strlen(s1), &out, ".d.e", "%d", 8);
+    ASSERT(res == 0);
+    ASSERT(strcmp(buf, s2) == 0);
+  }
+
+  {
+    /* Append to arrays */
+    struct json_out out = JSON_OUT_BUF(buf, sizeof(buf));
+    const char *s2 = "{ \"a\": 123, \"b\": [ 1,2 ], \"c\": true }";
+    int res = json_setf(s1, strlen(s1), &out, ".b[]", "%d", 2);
+    ASSERT(res == 0);
+    ASSERT(strcmp(buf, s2) == 0);
+  }
+
+  {
+    /* Delete from array */
+    struct json_out out = JSON_OUT_BUF(buf, sizeof(buf));
+    const char *s2 = "{ \"a\": 123, \"b\": [ ], \"c\": true }";
+    int res = json_setf(s1, strlen(s1), &out, ".b[0]", NULL);
+    ASSERT(res == 1);
+    ASSERT(strcmp(buf, s2) == 0);
+  }
+
+  {
+    /* Create array and push value  */
+    struct json_out out = JSON_OUT_BUF(buf, sizeof(buf));
+    const char *s2 = "{ \"a\": 123, \"b\": [ 1 ], \"c\": true,\"d\":[3] }";
+    int res = json_setf(s1, strlen(s1), &out, ".d[]", "%d", 3);
+    // printf("[%s]\n[%s]\n", buf, s2);
+    ASSERT(res == 0);
+    ASSERT(strcmp(buf, s2) == 0);
+  }
+
+  return NULL;
+}
+
 static const char *run_all_tests(void) {
   RUN_TEST(test_eos);
   RUN_TEST(test_scanf);
@@ -683,6 +795,7 @@ static const char *run_all_tests(void) {
   RUN_TEST(test_json_unescape);
   RUN_TEST(test_parse_string);
   RUN_TEST(test_fprintf);
+  RUN_TEST(test_json_setf);
   return NULL;
 }