Commit 3509aaae authored by João Távora's avatar João Távora
Browse files

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.
parent 2e2f61ef
......@@ -5026,16 +5026,18 @@ represented using Lisp vectors.
@item
JSON has only one map type, the object. JSON objects are represented
using Lisp hashtables or alists. When an alist contains several
elements with the same key, Emacs uses only the first element for
serialization, in accordance with the behavior of @code{assq}.
using Lisp hashtables, alists or plists. When an alist or plist
contains several elements with the same key, Emacs uses only the first
element for serialization, in accordance with the behavior of
@code{assq}.
@end itemize
@noindent
Note that @code{nil} is a valid alist and represents the empty JSON
object, @code{@{@}}, not @code{null}, @code{false}, or an empty array,
all of which are different JSON values.
Note that @code{nil}, being both a valid alist and a valid plist,
represents @code{@{@}}, the empty JSON object; not @code{null},
@code{false}, or an empty array, all of which are different JSON
values.
If some Lisp object can't be represented in JSON, the serialization
functions will signal an error of type @code{wrong-type-argument}.
......@@ -5058,12 +5060,15 @@ The parsing functions will signal the following errors:
Only top-level values (arrays and objects) can be serialized to
JSON. The subobjects within these top-level values can be of any
type. Likewise, the parsing functions will only return vectors,
hashtables, and alists.
hashtables, alists, and plists.
The parsing functions accept keyword arguments. Currently only one
keyword argument, @code{:object-type}, is recognized; its value can be
either @code{hash-table} to parse JSON objects as hashtables with
string keys (the default) or @code{alist} to parse them as alists.
keyword argument, @code{:object-type}, is recognized; its value
decides which Lisp object to use for representing the key-value
mappings of a JSON object. It can be either @code{hash-table}, the
default, to make hashtables with strings as keys, @code{alist} to use
alists with symbols as keys or @code{plist} to use plists with keyword
symbols as keys.
@defun json-serialize object
This function returns a new Lisp string which contains the JSON
......
......@@ -393,18 +393,39 @@ lisp_to_json_toplevel_1 (Lisp_Object lisp, json_t **json)
*json = json_check (json_object ());
ptrdiff_t count = SPECPDL_INDEX ();
record_unwind_protect_ptr (json_release_object, *json);
bool is_plist = !CONSP (XCAR (tail));
FOR_EACH_TAIL (tail)
{
Lisp_Object pair = XCAR (tail);
CHECK_CONS (pair);
Lisp_Object key_symbol = XCAR (pair);
Lisp_Object value = XCDR (pair);
const char *key_str;
Lisp_Object value;
Lisp_Object key_symbol;
if (is_plist)
{
key_symbol = XCAR (tail);
tail = XCDR (tail);
CHECK_CONS (tail);
value = XCAR (tail);
if (EQ (tail, li.tortoise)) circular_list (lisp);
}
else
{
Lisp_Object pair = XCAR (tail);
CHECK_CONS (pair);
key_symbol = XCAR (pair);
value = XCDR (pair);
}
CHECK_SYMBOL (key_symbol);
Lisp_Object key = SYMBOL_NAME (key_symbol);
/* We can't specify the length, so the string must be
null-terminated. */
check_string_without_embedded_nulls (key);
const char *key_str = SSDATA (key);
key_str = SSDATA (key);
/* In plists, ensure leading ":" in keys is stripped. It
will be reconstructed later in `json_to_lisp'.*/
if (is_plist && ':' == key_str[0] && key_str[1])
{
key_str = &key_str[1];
}
/* Only add element if key is not already present. */
if (json_object_get (*json, key_str) == NULL)
{
......@@ -423,7 +444,7 @@ lisp_to_json_toplevel_1 (Lisp_Object lisp, json_t **json)
/* Convert LISP to a toplevel JSON object (array or object). Signal
an error of type `wrong-type-argument' if LISP is not a vector,
hashtable, or alist. */
hashtable, alist, or plist. */
static json_t *
lisp_to_json_toplevel (Lisp_Object lisp)
......@@ -470,20 +491,21 @@ lisp_to_json (Lisp_Object lisp)
return json;
}
/* LISP now must be a vector, hashtable, or alist. */
/* LISP now must be a vector, hashtable, alist, or plist. */
return lisp_to_json_toplevel (lisp);
}
DEFUN ("json-serialize", Fjson_serialize, Sjson_serialize, 1, 1, NULL,
doc: /* Return the JSON representation of OBJECT as a string.
OBJECT must be a vector, hashtable, or alist, and its elements can
recursively contain `:null', `:false', t, numbers, strings, or other
vectors hashtables, and alist. `:null', `:false', and t will be
converted to JSON null, false, and true values, respectively. Vectors
will be converted to JSON arrays, and hashtables and alists to JSON
objects. Hashtable keys must be strings without embedded null
characters and must be unique within each object. Alist keys must be
symbols; if a key is duplicate, the first instance is used. */)
OBJECT must be a vector, hashtable, alist, or plist and its elements
can recursively contain `:null', `:false', t, numbers, strings, or
other vectors hashtables, alists or plists. `:null', `:false', and t
will be converted to JSON null, false, and true values, respectively.
Vectors will be converted to JSON arrays, whereas hashtables, alists
and plists are converted to JSON objects. Hashtable keys must be
strings without embedded null characters and must be unique within
each object. Alist and plist keys must be symbols; if a key is
duplicate, the first instance is used. */)
(Lisp_Object object)
{
ptrdiff_t count = SPECPDL_INDEX ();
......@@ -605,6 +627,7 @@ OBJECT. */)
enum json_object_type {
json_object_hashtable,
json_object_alist,
json_object_plist
};
/* Convert a JSON object to a Lisp object. */
......@@ -692,6 +715,28 @@ json_to_lisp (json_t *json, enum json_object_type object_type)
result = Fnreverse (result);
break;
}
case json_object_plist:
{
result = Qnil;
const char *key_str;
json_t *value;
json_object_foreach (json, key_str, value)
{
USE_SAFE_ALLOCA;
ptrdiff_t key_str_len = strlen (key_str);
char *keyword_key_str = SAFE_ALLOCA (1 + key_str_len + 1);
keyword_key_str[0] = ':';
strcpy (&keyword_key_str[1], key_str);
Lisp_Object key = intern_1 (keyword_key_str, key_str_len + 1);
/* Build the plist as value-key since we're going to
reverse it in the end.*/
result = Fcons (key, result);
result = Fcons (json_to_lisp (value, object_type), result);
SAFE_FREE ();
}
result = Fnreverse (result);
break;
}
default:
/* Can't get here. */
emacs_abort ();
......@@ -721,8 +766,10 @@ json_parse_object_type (ptrdiff_t nargs, Lisp_Object *args)
return json_object_hashtable;
else if (EQ (value, Qalist))
return json_object_alist;
else if (EQ (value, Qplist))
return json_object_plist;
else
wrong_choice (list2 (Qhash_table, Qalist), value);
wrong_choice (list3 (Qhash_table, Qalist, Qplist), value);
}
default:
wrong_type_argument (Qplistp, Flist (nargs, args));
......@@ -733,14 +780,15 @@ DEFUN ("json-parse-string", Fjson_parse_string, Sjson_parse_string, 1, MANY,
NULL,
doc: /* Parse the JSON STRING into a Lisp object.
This is essentially the reverse operation of `json-serialize', which
see. The returned object will be a vector, hashtable, or alist. Its
elements will be `:null', `:false', t, numbers, strings, or further
vectors, hashtables, and alists. If there are duplicate keys in an
object, all but the last one are ignored. If STRING doesn't contain a
valid JSON object, an error of type `json-parse-error' is signaled.
The keyword argument `:object-type' specifies which Lisp type is used
to represent objects; it can be `hash-table' or `alist'.
usage: (json-parse-string STRING &key (OBJECT-TYPE \\='hash-table)) */)
see. The returned object will be a vector, hashtable, alist, or
plist. Its elements will be `:null', `:false', t, numbers, strings,
or further vectors, hashtables, alists, or plists. If there are
duplicate keys in an object, all but the last one are ignored. If
STRING doesn't contain a valid JSON object, an error of type
`json-parse-error' is signaled. The keyword argument `:object-type'
specifies which Lisp type is used to represent objects; it can be
`hash-table', `alist' or `plist'.
usage: (json-parse-string STRING &key (OBJECT-TYPE \\='hash-table)) */)
(ptrdiff_t nargs, Lisp_Object *args)
{
ptrdiff_t count = SPECPDL_INDEX ();
......@@ -912,6 +960,7 @@ syms_of_json (void)
DEFSYM (QCobject_type, ":object-type");
DEFSYM (Qalist, "alist");
DEFSYM (Qplist, "plist");
defsubr (&Sjson_serialize);
defsubr (&Sjson_insert);
......
......@@ -69,7 +69,31 @@
(should-error (json-serialize '((1 . 2))) :type 'wrong-type-argument)
(should-error (json-serialize '((a . 1) . b)) :type 'wrong-type-argument)
(should-error (json-serialize '#1=((a . 1) . #1#)) :type 'circular-list)
(should-error (json-serialize '(#1=(a #1#)))))
(should-error (json-serialize '(#1=(a #1#))))
(should (equal (json-serialize '(:abc [1 2 t] :def :null))
"{\"abc\":[1,2,true],\"def\":null}"))
(should (equal (json-serialize '(abc [1 2 t] :def :null))
"{\"abc\":[1,2,true],\"def\":null}"))
(should-error (json-serialize '#1=(:a 1 . #1#)) :type 'circular-list)
(should-error (json-serialize '#1=(:a 1 :b . #1#)) :type 'circular-list)
(should-error (json-serialize '(:foo "bar" (unexpected-alist-key . 1)))
:type 'wrong-type-argument)
(should-error (json-serialize '((abc . "abc") :unexpected-plist-key "key"))
:type 'wrong-type-argument)
(should-error (json-serialize '(:foo bar :odd-numbered))
:type 'wrong-type-argument)
(should (equal
(json-serialize
(list :detect-hash-table #s(hash-table test equal data ("bla" "ble"))
:detect-alist `((bla . "ble"))
:detect-plist `(:bla "ble")))
"\
{\
\"detect-hash-table\":{\"bla\":\"ble\"},\
\"detect-alist\":{\"bla\":\"ble\"},\
\"detect-plist\":{\"bla\":\"ble\"}\
}")))
(ert-deftest json-serialize/object-with-duplicate-keys ()
(skip-unless (fboundp 'json-serialize))
......@@ -89,7 +113,9 @@
(should (equal (cl-sort (map-pairs actual) #'string< :key #'car)
'(("abc" . [9 :false]) ("def" . :null)))))
(should (equal (json-parse-string input :object-type 'alist)
'((abc . [9 :false]) (def . :null))))))
'((abc . [9 :false]) (def . :null))))
(should (equal (json-parse-string input :object-type 'plist)
'(:abc [9 :false] :def :null)))))
(ert-deftest json-parse-string/string ()
(skip-unless (fboundp 'json-parse-string))
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment