diff options
| author | João Távora | 2018-06-02 00:23:38 +0100 |
|---|---|---|
| committer | João Távora | 2018-06-07 12:30:39 +0100 |
| commit | 3509aaaefe1996ea46b038850629b6d2f7a726fe (patch) | |
| tree | 651fec98d47d8c77863f6f0b08c9b3c9c6ac2362 | |
| parent | 2e2f61efa66b69fbd12c83bbd5370a4be2374f66 (diff) | |
| download | emacs-3509aaaefe1996ea46b038850629b6d2f7a726fe.tar.gz emacs-3509aaaefe1996ea46b038850629b6d2f7a726fe.zip | |
Accept plists when serializing and parsing JSON
* doc/lispref/text.texi (Parsing JSON): Mention plist support.
* src/json.c (lisp_to_json_toplevel_1): Serialize plists to json.
(Fjson_serialize): Mention plists in docstring.
(enum json_object_type): Add json_object_plist.
(json_to_lisp): Parse JSON into plists.
(json_parse_object_type): Consider plists.
(Fjson_parse_string): Mention plists in docstring.
(syms_of_json): New Qplist sym_of_json.
(lisp_to_json): Update comment.
* test/src/json-tests.el (json-serialize/object)
(json-parse-string/object): New plist tests.
| -rw-r--r-- | doc/lispref/text.texi | 25 | ||||
| -rw-r--r-- | src/json.c | 97 | ||||
| -rw-r--r-- | test/src/json-tests.el | 30 |
3 files changed, 116 insertions, 36 deletions
diff --git a/doc/lispref/text.texi b/doc/lispref/text.texi index 2afcd59a706..2c5b5a1b42e 100644 --- a/doc/lispref/text.texi +++ b/doc/lispref/text.texi | |||
| @@ -5026,16 +5026,18 @@ represented using Lisp vectors. | |||
| 5026 | 5026 | ||
| 5027 | @item | 5027 | @item |
| 5028 | JSON has only one map type, the object. JSON objects are represented | 5028 | JSON has only one map type, the object. JSON objects are represented |
| 5029 | using Lisp hashtables or alists. When an alist contains several | 5029 | using Lisp hashtables, alists or plists. When an alist or plist |
| 5030 | elements with the same key, Emacs uses only the first element for | 5030 | contains several elements with the same key, Emacs uses only the first |
| 5031 | serialization, in accordance with the behavior of @code{assq}. | 5031 | element for serialization, in accordance with the behavior of |
| 5032 | @code{assq}. | ||
| 5032 | 5033 | ||
| 5033 | @end itemize | 5034 | @end itemize |
| 5034 | 5035 | ||
| 5035 | @noindent | 5036 | @noindent |
| 5036 | Note that @code{nil} is a valid alist and represents the empty JSON | 5037 | Note that @code{nil}, being both a valid alist and a valid plist, |
| 5037 | object, @code{@{@}}, not @code{null}, @code{false}, or an empty array, | 5038 | represents @code{@{@}}, the empty JSON object; not @code{null}, |
| 5038 | all of which are different JSON values. | 5039 | @code{false}, or an empty array, all of which are different JSON |
| 5040 | values. | ||
| 5039 | 5041 | ||
| 5040 | If some Lisp object can't be represented in JSON, the serialization | 5042 | If some Lisp object can't be represented in JSON, the serialization |
| 5041 | functions will signal an error of type @code{wrong-type-argument}. | 5043 | functions will signal an error of type @code{wrong-type-argument}. |
| @@ -5058,12 +5060,15 @@ The parsing functions will signal the following errors: | |||
| 5058 | Only top-level values (arrays and objects) can be serialized to | 5060 | Only top-level values (arrays and objects) can be serialized to |
| 5059 | JSON. The subobjects within these top-level values can be of any | 5061 | JSON. The subobjects within these top-level values can be of any |
| 5060 | type. Likewise, the parsing functions will only return vectors, | 5062 | type. Likewise, the parsing functions will only return vectors, |
| 5061 | hashtables, and alists. | 5063 | hashtables, alists, and plists. |
| 5062 | 5064 | ||
| 5063 | The parsing functions accept keyword arguments. Currently only one | 5065 | The parsing functions accept keyword arguments. Currently only one |
| 5064 | keyword argument, @code{:object-type}, is recognized; its value can be | 5066 | keyword argument, @code{:object-type}, is recognized; its value |
| 5065 | either @code{hash-table} to parse JSON objects as hashtables with | 5067 | decides which Lisp object to use for representing the key-value |
| 5066 | string keys (the default) or @code{alist} to parse them as alists. | 5068 | mappings of a JSON object. It can be either @code{hash-table}, the |
| 5069 | default, to make hashtables with strings as keys, @code{alist} to use | ||
| 5070 | alists with symbols as keys or @code{plist} to use plists with keyword | ||
| 5071 | symbols as keys. | ||
| 5067 | 5072 | ||
| 5068 | @defun json-serialize object | 5073 | @defun json-serialize object |
| 5069 | This function returns a new Lisp string which contains the JSON | 5074 | This function returns a new Lisp string which contains the JSON |
diff --git a/src/json.c b/src/json.c index b046d34f667..afb81587a47 100644 --- a/src/json.c +++ b/src/json.c | |||
| @@ -393,18 +393,39 @@ lisp_to_json_toplevel_1 (Lisp_Object lisp, json_t **json) | |||
| 393 | *json = json_check (json_object ()); | 393 | *json = json_check (json_object ()); |
| 394 | ptrdiff_t count = SPECPDL_INDEX (); | 394 | ptrdiff_t count = SPECPDL_INDEX (); |
| 395 | record_unwind_protect_ptr (json_release_object, *json); | 395 | record_unwind_protect_ptr (json_release_object, *json); |
| 396 | bool is_plist = !CONSP (XCAR (tail)); | ||
| 396 | FOR_EACH_TAIL (tail) | 397 | FOR_EACH_TAIL (tail) |
| 397 | { | 398 | { |
| 398 | Lisp_Object pair = XCAR (tail); | 399 | const char *key_str; |
| 399 | CHECK_CONS (pair); | 400 | Lisp_Object value; |
| 400 | Lisp_Object key_symbol = XCAR (pair); | 401 | Lisp_Object key_symbol; |
| 401 | Lisp_Object value = XCDR (pair); | 402 | if (is_plist) |
| 403 | { | ||
| 404 | key_symbol = XCAR (tail); | ||
| 405 | tail = XCDR (tail); | ||
| 406 | CHECK_CONS (tail); | ||
| 407 | value = XCAR (tail); | ||
| 408 | if (EQ (tail, li.tortoise)) circular_list (lisp); | ||
| 409 | } | ||
| 410 | else | ||
| 411 | { | ||
| 412 | Lisp_Object pair = XCAR (tail); | ||
| 413 | CHECK_CONS (pair); | ||
| 414 | key_symbol = XCAR (pair); | ||
| 415 | value = XCDR (pair); | ||
| 416 | } | ||
| 402 | CHECK_SYMBOL (key_symbol); | 417 | CHECK_SYMBOL (key_symbol); |
| 403 | Lisp_Object key = SYMBOL_NAME (key_symbol); | 418 | Lisp_Object key = SYMBOL_NAME (key_symbol); |
| 404 | /* We can't specify the length, so the string must be | 419 | /* We can't specify the length, so the string must be |
| 405 | null-terminated. */ | 420 | null-terminated. */ |
| 406 | check_string_without_embedded_nulls (key); | 421 | check_string_without_embedded_nulls (key); |
| 407 | const char *key_str = SSDATA (key); | 422 | key_str = SSDATA (key); |
| 423 | /* In plists, ensure leading ":" in keys is stripped. It | ||
| 424 | will be reconstructed later in `json_to_lisp'.*/ | ||
| 425 | if (is_plist && ':' == key_str[0] && key_str[1]) | ||
| 426 | { | ||
| 427 | key_str = &key_str[1]; | ||
| 428 | } | ||
| 408 | /* Only add element if key is not already present. */ | 429 | /* Only add element if key is not already present. */ |
| 409 | if (json_object_get (*json, key_str) == NULL) | 430 | if (json_object_get (*json, key_str) == NULL) |
| 410 | { | 431 | { |
| @@ -423,7 +444,7 @@ lisp_to_json_toplevel_1 (Lisp_Object lisp, json_t **json) | |||
| 423 | 444 | ||
| 424 | /* Convert LISP to a toplevel JSON object (array or object). Signal | 445 | /* Convert LISP to a toplevel JSON object (array or object). Signal |
| 425 | an error of type `wrong-type-argument' if LISP is not a vector, | 446 | an error of type `wrong-type-argument' if LISP is not a vector, |
| 426 | hashtable, or alist. */ | 447 | hashtable, alist, or plist. */ |
| 427 | 448 | ||
| 428 | static json_t * | 449 | static json_t * |
| 429 | lisp_to_json_toplevel (Lisp_Object lisp) | 450 | lisp_to_json_toplevel (Lisp_Object lisp) |
| @@ -470,20 +491,21 @@ lisp_to_json (Lisp_Object lisp) | |||
| 470 | return json; | 491 | return json; |
| 471 | } | 492 | } |
| 472 | 493 | ||
| 473 | /* LISP now must be a vector, hashtable, or alist. */ | 494 | /* LISP now must be a vector, hashtable, alist, or plist. */ |
| 474 | return lisp_to_json_toplevel (lisp); | 495 | return lisp_to_json_toplevel (lisp); |
| 475 | } | 496 | } |
| 476 | 497 | ||
| 477 | DEFUN ("json-serialize", Fjson_serialize, Sjson_serialize, 1, 1, NULL, | 498 | DEFUN ("json-serialize", Fjson_serialize, Sjson_serialize, 1, 1, NULL, |
| 478 | doc: /* Return the JSON representation of OBJECT as a string. | 499 | doc: /* Return the JSON representation of OBJECT as a string. |
| 479 | OBJECT must be a vector, hashtable, or alist, and its elements can | 500 | OBJECT must be a vector, hashtable, alist, or plist and its elements |
| 480 | recursively contain `:null', `:false', t, numbers, strings, or other | 501 | can recursively contain `:null', `:false', t, numbers, strings, or |
| 481 | vectors hashtables, and alist. `:null', `:false', and t will be | 502 | other vectors hashtables, alists or plists. `:null', `:false', and t |
| 482 | converted to JSON null, false, and true values, respectively. Vectors | 503 | will be converted to JSON null, false, and true values, respectively. |
| 483 | will be converted to JSON arrays, and hashtables and alists to JSON | 504 | Vectors will be converted to JSON arrays, whereas hashtables, alists |
| 484 | objects. Hashtable keys must be strings without embedded null | 505 | and plists are converted to JSON objects. Hashtable keys must be |
| 485 | characters and must be unique within each object. Alist keys must be | 506 | strings without embedded null characters and must be unique within |
| 486 | symbols; if a key is duplicate, the first instance is used. */) | 507 | each object. Alist and plist keys must be symbols; if a key is |
| 508 | duplicate, the first instance is used. */) | ||
| 487 | (Lisp_Object object) | 509 | (Lisp_Object object) |
| 488 | { | 510 | { |
| 489 | ptrdiff_t count = SPECPDL_INDEX (); | 511 | ptrdiff_t count = SPECPDL_INDEX (); |
| @@ -605,6 +627,7 @@ OBJECT. */) | |||
| 605 | enum json_object_type { | 627 | enum json_object_type { |
| 606 | json_object_hashtable, | 628 | json_object_hashtable, |
| 607 | json_object_alist, | 629 | json_object_alist, |
| 630 | json_object_plist | ||
| 608 | }; | 631 | }; |
| 609 | 632 | ||
| 610 | /* Convert a JSON object to a Lisp object. */ | 633 | /* Convert a JSON object to a Lisp object. */ |
| @@ -692,6 +715,28 @@ json_to_lisp (json_t *json, enum json_object_type object_type) | |||
| 692 | result = Fnreverse (result); | 715 | result = Fnreverse (result); |
| 693 | break; | 716 | break; |
| 694 | } | 717 | } |
| 718 | case json_object_plist: | ||
| 719 | { | ||
| 720 | result = Qnil; | ||
| 721 | const char *key_str; | ||
| 722 | json_t *value; | ||
| 723 | json_object_foreach (json, key_str, value) | ||
| 724 | { | ||
| 725 | USE_SAFE_ALLOCA; | ||
| 726 | ptrdiff_t key_str_len = strlen (key_str); | ||
| 727 | char *keyword_key_str = SAFE_ALLOCA (1 + key_str_len + 1); | ||
| 728 | keyword_key_str[0] = ':'; | ||
| 729 | strcpy (&keyword_key_str[1], key_str); | ||
| 730 | Lisp_Object key = intern_1 (keyword_key_str, key_str_len + 1); | ||
| 731 | /* Build the plist as value-key since we're going to | ||
| 732 | reverse it in the end.*/ | ||
| 733 | result = Fcons (key, result); | ||
| 734 | result = Fcons (json_to_lisp (value, object_type), result); | ||
| 735 | SAFE_FREE (); | ||
| 736 | } | ||
| 737 | result = Fnreverse (result); | ||
| 738 | break; | ||
| 739 | } | ||
| 695 | default: | 740 | default: |
| 696 | /* Can't get here. */ | 741 | /* Can't get here. */ |
| 697 | emacs_abort (); | 742 | emacs_abort (); |
| @@ -721,8 +766,10 @@ json_parse_object_type (ptrdiff_t nargs, Lisp_Object *args) | |||
| 721 | return json_object_hashtable; | 766 | return json_object_hashtable; |
| 722 | else if (EQ (value, Qalist)) | 767 | else if (EQ (value, Qalist)) |
| 723 | return json_object_alist; | 768 | return json_object_alist; |
| 769 | else if (EQ (value, Qplist)) | ||
| 770 | return json_object_plist; | ||
| 724 | else | 771 | else |
| 725 | wrong_choice (list2 (Qhash_table, Qalist), value); | 772 | wrong_choice (list3 (Qhash_table, Qalist, Qplist), value); |
| 726 | } | 773 | } |
| 727 | default: | 774 | default: |
| 728 | wrong_type_argument (Qplistp, Flist (nargs, args)); | 775 | wrong_type_argument (Qplistp, Flist (nargs, args)); |
| @@ -733,14 +780,15 @@ DEFUN ("json-parse-string", Fjson_parse_string, Sjson_parse_string, 1, MANY, | |||
| 733 | NULL, | 780 | NULL, |
| 734 | doc: /* Parse the JSON STRING into a Lisp object. | 781 | doc: /* Parse the JSON STRING into a Lisp object. |
| 735 | This is essentially the reverse operation of `json-serialize', which | 782 | This is essentially the reverse operation of `json-serialize', which |
| 736 | see. The returned object will be a vector, hashtable, or alist. Its | 783 | see. The returned object will be a vector, hashtable, alist, or |
| 737 | elements will be `:null', `:false', t, numbers, strings, or further | 784 | plist. Its elements will be `:null', `:false', t, numbers, strings, |
| 738 | vectors, hashtables, and alists. If there are duplicate keys in an | 785 | or further vectors, hashtables, alists, or plists. If there are |
| 739 | object, all but the last one are ignored. If STRING doesn't contain a | 786 | duplicate keys in an object, all but the last one are ignored. If |
| 740 | valid JSON object, an error of type `json-parse-error' is signaled. | 787 | STRING doesn't contain a valid JSON object, an error of type |
| 741 | The keyword argument `:object-type' specifies which Lisp type is used | 788 | `json-parse-error' is signaled. The keyword argument `:object-type' |
| 742 | to represent objects; it can be `hash-table' or `alist'. | 789 | specifies which Lisp type is used to represent objects; it can be |
| 743 | usage: (json-parse-string STRING &key (OBJECT-TYPE \\='hash-table)) */) | 790 | `hash-table', `alist' or `plist'. |
| 791 | usage: (json-parse-string STRING &key (OBJECT-TYPE \\='hash-table)) */) | ||
| 744 | (ptrdiff_t nargs, Lisp_Object *args) | 792 | (ptrdiff_t nargs, Lisp_Object *args) |
| 745 | { | 793 | { |
| 746 | ptrdiff_t count = SPECPDL_INDEX (); | 794 | ptrdiff_t count = SPECPDL_INDEX (); |
| @@ -912,6 +960,7 @@ syms_of_json (void) | |||
| 912 | 960 | ||
| 913 | DEFSYM (QCobject_type, ":object-type"); | 961 | DEFSYM (QCobject_type, ":object-type"); |
| 914 | DEFSYM (Qalist, "alist"); | 962 | DEFSYM (Qalist, "alist"); |
| 963 | DEFSYM (Qplist, "plist"); | ||
| 915 | 964 | ||
| 916 | defsubr (&Sjson_serialize); | 965 | defsubr (&Sjson_serialize); |
| 917 | defsubr (&Sjson_insert); | 966 | defsubr (&Sjson_insert); |
diff --git a/test/src/json-tests.el b/test/src/json-tests.el index 09067bad8c8..7a193545b1a 100644 --- a/test/src/json-tests.el +++ b/test/src/json-tests.el | |||
| @@ -69,7 +69,31 @@ | |||
| 69 | (should-error (json-serialize '((1 . 2))) :type 'wrong-type-argument) | 69 | (should-error (json-serialize '((1 . 2))) :type 'wrong-type-argument) |
| 70 | (should-error (json-serialize '((a . 1) . b)) :type 'wrong-type-argument) | 70 | (should-error (json-serialize '((a . 1) . b)) :type 'wrong-type-argument) |
| 71 | (should-error (json-serialize '#1=((a . 1) . #1#)) :type 'circular-list) | 71 | (should-error (json-serialize '#1=((a . 1) . #1#)) :type 'circular-list) |
| 72 | (should-error (json-serialize '(#1=(a #1#))))) | 72 | (should-error (json-serialize '(#1=(a #1#)))) |
| 73 | |||
| 74 | (should (equal (json-serialize '(:abc [1 2 t] :def :null)) | ||
| 75 | "{\"abc\":[1,2,true],\"def\":null}")) | ||
| 76 | (should (equal (json-serialize '(abc [1 2 t] :def :null)) | ||
| 77 | "{\"abc\":[1,2,true],\"def\":null}")) | ||
| 78 | (should-error (json-serialize '#1=(:a 1 . #1#)) :type 'circular-list) | ||
| 79 | (should-error (json-serialize '#1=(:a 1 :b . #1#)) :type 'circular-list) | ||
| 80 | (should-error (json-serialize '(:foo "bar" (unexpected-alist-key . 1))) | ||
| 81 | :type 'wrong-type-argument) | ||
| 82 | (should-error (json-serialize '((abc . "abc") :unexpected-plist-key "key")) | ||
| 83 | :type 'wrong-type-argument) | ||
| 84 | (should-error (json-serialize '(:foo bar :odd-numbered)) | ||
| 85 | :type 'wrong-type-argument) | ||
| 86 | (should (equal | ||
| 87 | (json-serialize | ||
| 88 | (list :detect-hash-table #s(hash-table test equal data ("bla" "ble")) | ||
| 89 | :detect-alist `((bla . "ble")) | ||
| 90 | :detect-plist `(:bla "ble"))) | ||
| 91 | "\ | ||
| 92 | {\ | ||
| 93 | \"detect-hash-table\":{\"bla\":\"ble\"},\ | ||
| 94 | \"detect-alist\":{\"bla\":\"ble\"},\ | ||
| 95 | \"detect-plist\":{\"bla\":\"ble\"}\ | ||
| 96 | }"))) | ||
| 73 | 97 | ||
| 74 | (ert-deftest json-serialize/object-with-duplicate-keys () | 98 | (ert-deftest json-serialize/object-with-duplicate-keys () |
| 75 | (skip-unless (fboundp 'json-serialize)) | 99 | (skip-unless (fboundp 'json-serialize)) |
| @@ -89,7 +113,9 @@ | |||
| 89 | (should (equal (cl-sort (map-pairs actual) #'string< :key #'car) | 113 | (should (equal (cl-sort (map-pairs actual) #'string< :key #'car) |
| 90 | '(("abc" . [9 :false]) ("def" . :null))))) | 114 | '(("abc" . [9 :false]) ("def" . :null))))) |
| 91 | (should (equal (json-parse-string input :object-type 'alist) | 115 | (should (equal (json-parse-string input :object-type 'alist) |
| 92 | '((abc . [9 :false]) (def . :null)))))) | 116 | '((abc . [9 :false]) (def . :null)))) |
| 117 | (should (equal (json-parse-string input :object-type 'plist) | ||
| 118 | '(:abc [9 :false] :def :null))))) | ||
| 93 | 119 | ||
| 94 | (ert-deftest json-parse-string/string () | 120 | (ert-deftest json-parse-string/string () |
| 95 | (skip-unless (fboundp 'json-parse-string)) | 121 | (skip-unless (fboundp 'json-parse-string)) |