From 3c6b3cc617491dea2bcce0e07971e7c4d34c1963 Mon Sep 17 00:00:00 2001 From: Dave Gamble Date: Tue, 10 Feb 2015 14:17:59 +0000 Subject: [PATCH] start of JSON Patch implementation. cJSON gained a cJSON_InsertItemToArray which pushes elements up by one. This is needed for JSON Patch. Everything but Test is implemented for ApplyPatches. git-svn-id: svn://svn.code.sf.net/p/cjson/code@65 e3330c51-1366-4df0-8b21-3ccf24e3d50e --- cJSON.c | 2 + cJSON.h | 1 + cJSON_Utils.c | 107 +++++++++++++++++++++++++++++++++++++++++++++++++- cJSON_Utils.h | 14 +++++++ test_utils.c | 35 ++++++++++++++++- 5 files changed, 157 insertions(+), 2 deletions(-) diff --git a/cJSON.c b/cJSON.c index 6917aec..d8d1cca 100644 --- a/cJSON.c +++ b/cJSON.c @@ -691,6 +691,8 @@ cJSON *cJSON_DetachItemFromObject(cJSON *object,const char *string) {int i=0;cJS void cJSON_DeleteItemFromObject(cJSON *object,const char *string) {cJSON_Delete(cJSON_DetachItemFromObject(object,string));} /* Replace array/object items with new ones. */ +void cJSON_InsertItemInArray(cJSON *array,int which,cJSON *newitem) {cJSON *c=array->child;while (c && which>0) c=c->next,which--;if (!c) {cJSON_AddItemToArray(array,newitem);return;} + newitem->next=c;newitem->prev=c->prev;c->prev=newitem;if (c==array->child) array->child=newitem; else newitem->prev->next=newitem;} void cJSON_ReplaceItemInArray(cJSON *array,int which,cJSON *newitem) {cJSON *c=array->child;while (c && which>0) c=c->next,which--;if (!c) return; newitem->next=c->next;newitem->prev=c->prev;if (newitem->next) newitem->next->prev=newitem; if (c==array->child) array->child=newitem; else newitem->prev->next=newitem;c->next=c->prev=0;cJSON_Delete(c);} diff --git a/cJSON.h b/cJSON.h index 55b85ea..9b04cb1 100644 --- a/cJSON.h +++ b/cJSON.h @@ -115,6 +115,7 @@ extern cJSON *cJSON_DetachItemFromObject(cJSON *object,const char *string); extern void cJSON_DeleteItemFromObject(cJSON *object,const char *string); /* Update array items. */ +extern void cJSON_InsertItemInArray(cJSON *array,int which,cJSON *newitem); // Shifts pre-existing items to the right. extern void cJSON_ReplaceItemInArray(cJSON *array,int which,cJSON *newitem); extern void cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem); diff --git a/cJSON_Utils.c b/cJSON_Utils.c index 26bc225..cfc1619 100644 --- a/cJSON_Utils.c +++ b/cJSON_Utils.c @@ -1,6 +1,9 @@ #include +#include +#include #include "cJSON_Utils.h" +// JSON Pointer implementation: static int cJSONUtils_Pstrcasecmp(const char *a,const char *e) { if (!a || !e) return (a==e)?0:1; @@ -12,7 +15,6 @@ static int cJSONUtils_Pstrcasecmp(const char *a,const char *e) return 0; } - cJSON *cJSONUtils_GetPointer(cJSON *object,const char *pointer) { cJSON *target=object;int which=0;const char *element=0; @@ -36,4 +38,107 @@ cJSON *cJSONUtils_GetPointer(cJSON *object,const char *pointer) return object; } +// JSON Patch implementation. +static void cJSONUtils_InplaceDecodePointerString(char *string) +{ + char *s2=string; + for (;*string;s2++,string++) + *s2=(*string!='~')?(*string):((*(++string)=='0')?'~':'/'); + *s2=0; +} + +static cJSON *cJSONUtils_PatchDetach(cJSON *object,const char *path) +{ + char *parentptr=0,*childptr=0;cJSON *parent=0; + + parentptr=strdup(path); childptr=strrchr(parentptr,'/'); if (childptr) *childptr++=0; + parent=cJSONUtils_GetPointer(object,parentptr); + cJSONUtils_InplaceDecodePointerString(childptr); + + cJSON *ret=0; + if (!parent) ret=0; // Couldn't find object to remove child from. + else if (parent->type==cJSON_Array) ret=cJSON_DetachItemFromArray(parent,atoi(childptr)); + else if (parent->type==cJSON_Object) ret=cJSON_DetachItemFromObject(parent,childptr); + free(parentptr); + return ret; +} + +static int cJSONUtils_ApplyPatch(cJSON *object,cJSON *patch) +{ + cJSON *op=0,*path=0,*value=0;int opcode=0; + + op=cJSON_GetObjectItem(patch,"op"); + path=cJSON_GetObjectItem(patch,"path"); + if (!op || !path) return 2; // malformed patch. + + if (!strcmp(op->valuestring,"add")) opcode=0; + else if (!strcmp(op->valuestring,"remove")) opcode=1; + else if (!strcmp(op->valuestring,"replace"))opcode=2; + else if (!strcmp(op->valuestring,"move")) opcode=3; + else if (!strcmp(op->valuestring,"copy")) opcode=4; + else if (!strcmp(op->valuestring,"test")) opcode=5; + else return 3; // unknown opcode. + + if (opcode==5) return 10; // TEST IS CURRENTLY UNIMPLEMENTED. + + if (opcode==1 || opcode==2) // Remove/Replace + { + cJSON_Delete(cJSONUtils_PatchDetach(object,path->valuestring)); // Get rid of old. + if (opcode==1) return 0; // For Remove, this is job done. + } + + if (opcode==3 || opcode==4) // Copy/Move uses "from". + { + cJSON *from=cJSON_GetObjectItem(patch,"from"); if (!from) return 4; // missing "from" for copy/move. + + if (opcode==3) value=cJSONUtils_PatchDetach(object,from->valuestring); + if (opcode==4) value=cJSONUtils_GetPointer(object,from->valuestring); + if (!value) return 5; // missing "from" for copy/move. + if (opcode==4) value=cJSON_Duplicate(value,1); + if (!value) return 6; // out of memory for copy/move. + } + else // Add/Replace uses "value". + { + value=cJSON_GetObjectItem(patch,"value"); + if (!value) return 7; // missing "value" for add/replace. + value=cJSON_Duplicate(value,1); + if (!value) return 8; // out of memory for add/replace. + } + + // Now, just add "value" to "path". + char *parentptr=0,*childptr=0;cJSON *parent=0; + + parentptr=strdup(path->valuestring); childptr=strrchr(parentptr,'/'); if (childptr) *childptr++=0; + parent=cJSONUtils_GetPointer(object,parentptr); + cJSONUtils_InplaceDecodePointerString(childptr); + + // add, remove, replace, move, copy, test. + if (!parent) {free(parentptr); return 9;} // Couldn't find object to add to. + else if (parent->type==cJSON_Array) + { + if (!strcmp(childptr,"-")) cJSON_AddItemToArray(parent,value); + else cJSON_InsertItemInArray(parent,atoi(childptr),value); + } + else if (parent->type==cJSON_Object) + { + cJSON_DeleteItemFromObject(parent,childptr); + cJSON_AddItemToObject(parent,childptr,value); + } + free(parentptr); + return 0; +} + + +int cJSONUtils_ApplyPatches(cJSON *object,cJSON *patches) +{ + int err; + if (!patches->type==cJSON_Array) return 1; // malformed patches. + if (patches) patches=patches->child; + while (patches) + { + if ((err=cJSONUtils_ApplyPatch(object,patches))) return err; + patches=patches->next; + } + return 0; +} diff --git a/cJSON_Utils.h b/cJSON_Utils.h index 3f7b0ce..91ee08c 100644 --- a/cJSON_Utils.h +++ b/cJSON_Utils.h @@ -3,3 +3,17 @@ // Implement RFC6901 (https://tools.ietf.org/html/rfc6901) JSON Pointer spec. cJSON *cJSONUtils_GetPointer(cJSON *object,const char *pointer); +// Implement RFC6902 (https://tools.ietf.org/html/rfc6902) JSON Patch spec. +//cJSON* cJSONUtils_GeneratePatches(cJSON *from,cJSON *to); // Not yet implemented. +int cJSONUtils_ApplyPatches(cJSON *object,cJSON *patches); // Returns 0 for success. + +// Note that ApplyPatches is NOT atomic on failure. To implement an atomic ApplyPatches, use: +//int cJSONUtils_AtomicApplyPatches(cJSON **object, cJSON *patches) +//{ +// cJSON *modme=cJSON_Duplicate(*object,1); +// int error=cJSONUtils_ApplyPatches(modme,patches); +// if (!error) {cJSON_Delete(*object);*object=modme;} +// else cJSON_Delete(modme); +// return error; +//} +// Code not added to library since this strategy is a LOT slower. diff --git a/test_utils.c b/test_utils.c index 74d4a33..fc222de 100644 --- a/test_utils.c +++ b/test_utils.c @@ -4,6 +4,7 @@ int main() { + // JSON Pointer tests: const char *json="{" "\"foo\": [\"bar\", \"baz\"]," "\"\": 0," @@ -23,8 +24,40 @@ int main() cJSON *root=cJSON_Parse(json); for (int i=0;i<12;i++) { - printf("Test %d:\n%s\n\n",i+1,cJSON_Print(cJSONUtils_GetPointer(root,tests[i]))); + char *output=cJSON_Print(cJSONUtils_GetPointer(root,tests[i])); + printf("Test %d:\n%s\n\n",i+1,output); + free(output); } + cJSON_Delete(root); + // JSON Patch tests: + const char *patches[15][2]={ + {"{ \"foo\": \"bar\"}", "[{ \"op\": \"add\", \"path\": \"/baz\", \"value\": \"qux\" }]"}, + {"{ \"foo\": [ \"bar\", \"baz\" ] }", "[{ \"op\": \"add\", \"path\": \"/foo/1\", \"value\": \"qux\" }]"}, + {"{\"baz\": \"qux\",\"foo\": \"bar\"}"," [{ \"op\": \"remove\", \"path\": \"/baz\" }]"}, + {"{ \"foo\": [ \"bar\", \"qux\", \"baz\" ] }","[{ \"op\": \"remove\", \"path\": \"/foo/1\" }]"}, + {"{ \"baz\": \"qux\",\"foo\": \"bar\"}","[{ \"op\": \"replace\", \"path\": \"/baz\", \"value\": \"boo\" }]"}, + {"{\"foo\": {\"bar\": \"baz\",\"waldo\": \"fred\"},\"qux\": {\"corge\": \"grault\"}}","[{ \"op\": \"move\", \"from\": \"/foo/waldo\", \"path\": \"/qux/thud\" }]"}, + {"{ \"foo\": [ \"all\", \"grass\", \"cows\", \"eat\" ] }","[ { \"op\": \"move\", \"from\": \"/foo/1\", \"path\": \"/foo/3\" }]"}, + {"{\"baz\": \"qux\",\"foo\": [ \"a\", 2, \"c\" ]}","[{ \"op\": \"test\", \"path\": \"/baz\", \"value\": \"qux\" },{ \"op\": \"test\", \"path\": \"/foo/1\", \"value\": 2 }]"}, + {"{ \"baz\": \"qux\" }","[ { \"op\": \"test\", \"path\": \"/baz\", \"value\": \"bar\" }]"}, + {"{ \"foo\": \"bar\" }","[{ \"op\": \"add\", \"path\": \"/child\", \"value\": { \"grandchild\": { } } }]"}, + {"{ \"foo\": \"bar\" }","[{ \"op\": \"add\", \"path\": \"/baz\", \"value\": \"qux\", \"xyz\": 123 }]"}, + {"{ \"foo\": \"bar\" }","[{ \"op\": \"add\", \"path\": \"/baz/bat\", \"value\": \"qux\" }]"}, + {"{\"/\": 9,\"~1\": 10}","[{\"op\": \"test\", \"path\": \"/~01\", \"value\": 10}]"}, + {"{\"/\": 9,\"~1\": 10}","[{\"op\": \"test\", \"path\": \"/~01\", \"value\": \"10\"}]"}, + {"{ \"foo\": [\"bar\"] }","[ { \"op\": \"add\", \"path\": \"/foo/-\", \"value\": [\"abc\", \"def\"] }]"}}; + printf("JSON Patch Tests\n"); + for (int i=0;i<15;i++) + { + cJSON *object=cJSON_Parse(patches[i][0]); + cJSON *patch=cJSON_Parse(patches[i][1]); + int err=cJSONUtils_ApplyPatches(object,patch); + char *output=cJSON_Print(object); + printf("Test %d (err %d):\n%s\n\n",i+1,err,output); + free(output); + } + + }