unofficial mirror of emacs-devel@gnu.org 
 help / color / mirror / code / Atom feed
From: "João Távora" <joaotavora@gmail.com>
To: emacs-devel@gnu.org
Subject: [PATCH] Accept plists when serializing and parsing JSON
Date: Tue, 29 May 2018 15:59:06 +0100	[thread overview]
Message-ID: <87sh6awls5.fsf@gmail.com> (raw)

[-- Attachment #1: Type: text/plain, Size: 741 bytes --]

Hi,

So I found this other thread

  https://lists.gnu.org/archive/html/emacs-devel/2017-12/msg00667.html

where plist support for json.c is briefly requested and discussed, but
there didn't seem to be an overwhelming argument against it, so I had
another look at the file and it didn't seem very hard or problematic to
implement.

Anyway, I had a go.  It's small, so have a look.  Patch at the end of
this file, or find it in the scratch/support-plists-in-jsonc branch.

I used a global to control json-serialize's interpretation of lists, but
it could be an argument as well.  Added some tests and some doc, too.

João

PS: Take this opportunity to thank Eli and everyone else very much for
the Emacs 26.1 release!


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Support-plists-in-json.c.patch --]
[-- Type: text/x-diff, Size: 12594 bytes --]

From 224f8ea95f00cc60ee77aeaad6585bb2ef845f70 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= <joaotavora@gmail.com>
Date: Tue, 29 May 2018 15:41:30 +0100
Subject: [PATCH] Support plists in json.c

* doc/lispref/text.texi (Parsing JSON): Mention plists and
json-serialize-use-plists.

* src/json.c (lisp_to_json_toplevel_1): Decide with
Vjson_serialize_use_plists.
(Fjson_serialize): Update docstring.
(enum json_object_type): Add json_object_plist.
(json_to_lisp): Can build plists.
(json_parse_object_type): Accept plists.
(Fjson_parse_string): Update docstring.
(json-serialize-use-plist): New DEFVAR_LISP.
(Qplist): New sym_of_json

* test/src/json-tests.el (json-serialize/object): Do some tests
with json-serialize-use-plists to t.
(json-parse-string/object): Parse something as a plist.
---
 doc/lispref/text.texi  | 27 ++++++++----
 src/json.c             | 97 ++++++++++++++++++++++++++++++++----------
 test/src/json-tests.el | 18 +++++++-
 3 files changed, 110 insertions(+), 32 deletions(-)

diff --git a/doc/lispref/text.texi b/doc/lispref/text.texi
index da09b4ae1c..3995102237 100644
--- a/doc/lispref/text.texi
+++ b/doc/lispref/text.texi
@@ -5025,16 +5025,18 @@ Parsing JSON
 
 @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} is both a valid alist and a valid plist and
+represents the empty JSON object, @code{@{@}}, 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}.
@@ -5057,12 +5059,21 @@ Parsing JSON
   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.
+string keys (the default), @code{alist} to parse them as alists or
+@code{plist} to parse them as plists.
+
+@vindex json-serialize-use-plists
+@cindex serializing plists as json
+  For the serialization function, the variable
+@var{json-serialize-use-plists} controls the converse process,
+resolving the ambiguity when a list is found in the Lisp object to
+serialize. If @code{nil}, its default, the list is interpreted as an
+alist, otherwise it is interpreted as a plist.
 
 @defun json-serialize object
 This function returns a new Lisp string which contains the JSON
diff --git a/src/json.c b/src/json.c
index b046d34f66..ccd58c047e 100644
--- a/src/json.c
+++ b/src/json.c
@@ -395,16 +395,31 @@ lisp_to_json_toplevel_1 (Lisp_Object lisp, json_t **json)
       record_unwind_protect_ptr (json_release_object, *json);
       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 ( EQ (Vjson_serialize_use_plists, Qt) ) {
+            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);
+          /* If using plists, maybe strip the ":" from symbol-name */
+          if (EQ (Vjson_serialize_use_plists, Qt) &&
+              ':' == 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)
             {
@@ -476,13 +491,17 @@ lisp_to_json (Lisp_Object 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
+
+OBJECT must be a vector of values or a key-value map.  Hashtables,
+alists and plists are accepted as maps, the variable
+`json-serialize-use-plists' controlling which one of the latter two to
+use.  In any of these cases, values can be `:null', `:false', t,
+numbers, strings, or, recursively, 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, and hashtables, alists and plists to JSON objects.
+Hashtable keys must be strings without embedded null characters and
+must be unique within each object.  Alist or plist keys must be
 symbols; if a key is duplicate, the first instance is used.  */)
   (Lisp_Object object)
 {
@@ -605,6 +624,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 +712,30 @@ 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)
+                {
+                  /* No idea if using AUTO_STRING and Fconcat for
+                     making keywords is idiomatic, but seems to work
+                     nicely */
+                  AUTO_STRING (colon, ":");
+                  Lisp_Object key =
+                    Fintern (CALLN (Fconcat, colon, json_build_string (key_str))
+                             , Qnil);
+                  result = Fcons (key, result); /* build the plist as
+                                                   value-key since
+                                                   we're going to
+                                                   reverse it in the
+                                                   end.*/
+                  result = Fcons (json_to_lisp (value, object_type), result);
+                }
+              result = Fnreverse (result);
+              break;
+            }
           default:
             /* Can't get here.  */
             emacs_abort ();
@@ -721,8 +765,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,15 +779,16 @@ 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))  */)
-  (ptrdiff_t nargs, Lisp_Object *args)
+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 ();
 
@@ -903,6 +950,11 @@ syms_of_json (void)
   DEFSYM (Qpure, "pure");
   DEFSYM (Qside_effect_free, "side-effect-free");
 
+  DEFVAR_LISP ("json-serialize-use-plists", Vjson_serialize_use_plists,
+               doc:
+               /* If non-nil use plists instead of alists in json-serialize.*/);
+  Vjson_serialize_use_plists = Qnil;
+
   DEFSYM (Qjson_serialize, "json-serialize");
   DEFSYM (Qjson_parse_string, "json-parse-string");
   Fput (Qjson_serialize, Qpure, Qt);
@@ -912,6 +964,7 @@ syms_of_json (void)
 
   DEFSYM (QCobject_type, ":object-type");
   DEFSYM (Qalist, "alist");
+  DEFSYM (Qplist, "plist");
 
   defsubr (&Sjson_serialize);
   defsubr (&Sjson_insert);
diff --git a/test/src/json-tests.el b/test/src/json-tests.el
index 09067bad8c..5c9be20e95 100644
--- a/test/src/json-tests.el
+++ b/test/src/json-tests.el
@@ -69,7 +69,19 @@ 'json-tests--error
   (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#))))
+
+  (let ((json-serialize-use-plists t))
+    (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 '((abc . 1))) :type 'wrong-type-argument)
+    (should-error (json-serialize '(:foo bar (abc . 1)))
+                  :type 'wrong-type-argument)
+    (should-error (json-serialize '(:foo bar :odd-numbered))
+                  :type 'wrong-type-argument)))
 
 (ert-deftest json-serialize/object-with-duplicate-keys ()
   (skip-unless (fboundp 'json-serialize))
@@ -89,7 +101,9 @@ 'json-tests--error
       (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))
-- 
2.17.0


             reply	other threads:[~2018-05-29 14:59 UTC|newest]

Thread overview: 20+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2018-05-29 14:59 João Távora [this message]
2018-05-29 21:20 ` [PATCH] Accept plists when serializing and parsing JSON Philipp Stephani
2018-05-29 22:03   ` João Távora
2018-05-30  6:37     ` Yuri Khan
2018-05-30  8:58       ` João Távora
2018-06-02  8:04         ` Philipp Stephani
2018-06-03  0:34           ` João Távora
2018-06-03  4:05             ` Stefan Monnier
2018-06-03 13:43               ` João Távora
2018-06-02  7:45       ` Philipp Stephani
2018-06-02  7:39     ` Philipp Stephani
2018-06-01  9:39 ` Eli Zaretskii
2018-06-01 23:29   ` João Távora
2018-06-02  6:55     ` Eli Zaretskii
2018-06-02  8:24     ` Philipp Stephani
2018-06-02  9:00       ` Eli Zaretskii
2018-06-02 16:46         ` Philipp Stephani
2018-06-02 19:18           ` Eli Zaretskii
2018-06-08 14:45     ` Eli Zaretskii
2018-06-02  8:30 ` Philipp Stephani

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.gnu.org/software/emacs/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=87sh6awls5.fsf@gmail.com \
    --to=joaotavora@gmail.com \
    --cc=emacs-devel@gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).