emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/compat 7e678b3fa1 1/3: Add json-parse-string, json-pars


From: ELPA Syncer
Subject: [elpa] externals/compat 7e678b3fa1 1/3: Add json-parse-string, json-parse-buffer, json-serialize and json-insert
Date: Tue, 24 Jan 2023 18:57:27 -0500 (EST)

branch: externals/compat
commit 7e678b3fa102a86553921d6c24056bbbe5365c7e
Author: Daniel Mendler <mail@daniel-mendler.de>
Commit: Daniel Mendler <mail@daniel-mendler.de>

    Add json-parse-string, json-parse-buffer, json-serialize and json-insert
---
 NEWS.org        |   2 +
 compat-tests.el | 157 ++++++++++++++++++++++++++++++++++++++
 compat.el       | 233 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 compat.texi     |  69 ++++++++++++++++-
 4 files changed, 460 insertions(+), 1 deletion(-)

diff --git a/NEWS.org b/NEWS.org
index 84a9331abc..0973994b52 100644
--- a/NEWS.org
+++ b/NEWS.org
@@ -3,6 +3,8 @@
 * Development
 
 - compat-27: Add ~file-name-unquote~.
+- compat-28: Add libjansson compatibility functions ~json-parse-string~,
+  ~json-parse-buffer~, ~json-serialize~ and ~json-insert~.
 - compat-29: Replace ~string-lines~ with version from Emacs 29, support 
optional
   KEEP-NEWLINES argument.
 
diff --git a/compat-tests.el b/compat-tests.el
index 9724314f5f..cf7af9b947 100644
--- a/compat-tests.el
+++ b/compat-tests.el
@@ -2839,5 +2839,162 @@
     (should sentence-end-double-space)
     (should-equal major-mode #'text-mode)))
 
+(ert-deftest json-parse-string ()
+  ;; Errors
+  (should-error (compat-call json-parse-string ""))
+  (should-error (compat-call json-parse-string " "))
+  (should-error (compat-call json-parse-string "11 22 33"))
+  (should-error (compat-call json-parse-string "[1][2]"))
+  (should-error (compat-call json-parse-string "[1"))
+  (should-error (compat-call json-parse-string " \"foo bar\"\"baz\" "))
+  ;; True, Null, False
+  (should-equal [t :false :null] (compat-call json-parse-string " 
[true,false,null] "))
+  (should-equal [t nil nil] (compat-call json-parse-string " [true,false,null] 
" :false-object nil :null-object nil))
+  (should-equal [t "false" nil] (compat-call json-parse-string " 
[true,false,null] " :null-object nil :false-object "false"))
+  ;; RFC 4627
+  (should-equal [1 2 3] (compat-call json-parse-string " [1,2,3] "))
+  (should-equal [1 2 3] (compat-call json-parse-string "[1,2,3]"))
+  (should-equal ["a" 2 3] (compat-call json-parse-string "[\"a\",2,3]"))
+  (should-equal [["a" 2] 3] (compat-call json-parse-string "[[\"a\",2],3]"))
+  (should-equal [["a" 2] 3] (compat-call json-parse-string "[[\"a\",2],3]" 
:array-type 'array))
+  (should-equal '(("a" 2) 3) (compat-call json-parse-string "[[\"a\",2],3]" 
:array-type 'list))
+  (should-equal ["false" t] (compat-call json-parse-string "[false, true]" 
:false-object "false"))
+  (let ((input "{\"key\":[\"abc\", 2], \"yek\": null}"))
+    (let ((obj (compat-call json-parse-string input :object-type 'alist)))
+      (should-equal (cdr (assq 'key obj)) ["abc" 2])
+      (should-equal (cdr (assq 'yek obj)) :null))
+    (let ((obj (compat-call json-parse-string input :object-type 'plist)))
+      (should-equal (plist-get obj :key) ["abc" 2])
+      (should-equal (plist-get obj :yek) :null))
+    (let ((obj (compat-call json-parse-string input :object-type 'hash-table)))
+      (should-equal (gethash "key" obj) ["abc" 2])
+      (should-equal (gethash "yek" obj) :null))
+    (let ((obj (compat-call json-parse-string input)))
+      (should-equal (gethash "key" obj) ["abc" 2])
+      (should-equal (gethash "yek" obj) :null)))
+  ;; RFC 8259
+  (should-equal "foo bar" (compat-call json-parse-string " \"foo bar\" "))
+  (should-equal 0 (compat-call json-parse-string " 0 "))
+  (should-equal 0 (compat-call json-parse-string " 0"))
+  (should-equal 0 (compat-call json-parse-string "0"))
+  (should-equal 1 (compat-call json-parse-string "1"))
+  (should-equal 0.5 (compat-call json-parse-string "0.5"))
+  (should-equal 'foo (compat-call json-parse-string "null" :null-object 'foo)))
+
+(ert-deftest json-parse-buffer ()
+  ;; Errors
+  (with-temp-buffer
+    (should-error (compat-call json-parse-buffer))
+    (insert " ")
+    (goto-char (point-min))
+    (should-error (compat-call json-parse-buffer)))
+  (with-temp-buffer
+    (insert "[1")
+    (goto-char (point-min))
+    (should-error (compat-call json-parse-buffer)))
+  ;; RFC 4627
+  (with-temp-buffer
+    (insert "[1,2] [4,5]")
+    (goto-char (point-min))
+    (should-equal [1 2] (compat-call json-parse-buffer)))
+  (with-temp-buffer
+    (insert "[1,2,3]")
+    (goto-char (point-min))
+    (should-equal [1 2 3] (compat-call json-parse-buffer)))
+  (with-temp-buffer
+    (insert " [1,2,3] ")
+    (goto-char (point-min))
+    (should-equal '(1 2 3) (compat-call json-parse-buffer :array-type 'list)))
+  ;; RFC 8259
+  (with-temp-buffer
+    (insert " 11 22 33 ")
+    (goto-char (point-min))
+    (should-equal 11 (compat-call json-parse-buffer)))
+  (with-temp-buffer
+    (insert "11 22 33")
+    (goto-char (point-min))
+    (should-equal 11 (compat-call json-parse-buffer)))
+  (with-temp-buffer
+    (insert " \"foo\" ")
+    (goto-char (point-min))
+    (should-equal "foo" (compat-call json-parse-buffer)))
+  (with-temp-buffer
+    (insert " [1,2,3][4,5,6]123{\"a\":1,\"b\":2}\"str\"")
+    (goto-char (point-min))
+    (should-equal [1 2 3] (compat-call json-parse-buffer))
+    (should-equal [4 5 6] (compat-call json-parse-buffer))
+    (should-equal 123 (compat-call json-parse-buffer))
+    (should (member
+             (compat-call json-parse-buffer :object-type 'plist)
+             '((:b 2 :a 1) (:a 1 :b 2))))
+    (should-equal "str" (compat-call json-parse-buffer)))
+  ;; TODO Our compatibility functions don't support RFC 4627 toplevel strings
+  ;; with spaces.
+  ;; (with-temp-buffer ;; TODO
+  ;;   (insert " \"foo bar\"\"baz\" ")
+  ;;   (goto-char (point-min))
+  ;;   (should-equal "foo bar" (compat-call json-parse-buffer))
+  ;;   (should-equal "baz" (compat-call json-parse-buffer)))
+  ;; (with-temp-buffer ;; TODO
+  ;;   (insert " \"foo bar\" \"baz\" ")
+  ;;   (goto-char (point-min))
+  ;;   (should-equal "foo bar" (compat-call json-parse-buffer)))
+  (with-temp-buffer
+    (insert "0")
+    (goto-char (point-min))
+    (should-equal 0 (compat-call json-parse-buffer)))
+  (with-temp-buffer
+    (insert " 1 ")
+    (goto-char (point-min))
+    (should-equal 1 (compat-call json-parse-buffer))))
+
+(ert-deftest json-insert ()
+  (with-temp-buffer
+    (should-error (compat-call json-insert '(("a" . 1)))))
+  (with-temp-buffer
+    (compat-call json-insert nil)
+    (compat-call json-insert 1)
+    (compat-call json-insert [2 3 4])
+    (should-equal "{}1[2,3,4]" (buffer-string))))
+
+(ert-deftest json-serialize ()
+  (should-error (compat-call json-serialize '(("a" . 1))))
+  (should-error (compat-call json-serialize '("a" 1)))
+  (should-error (compat-call json-serialize '("a" 1 2)))
+  (should-error (compat-call json-serialize '(:a 1 2)))
+  (should-error (compat-call json-serialize '(1 . 2)))
+  (should-error (compat-call json-serialize '(1 2 3)))
+  (should-error (compat-call json-serialize '(:a 1 :b)))
+  (should-error (compat-call json-serialize '((one 1) (two 2))))
+  (should-error (compat-call json-serialize 'invalid))
+  (should-error (compat-call json-serialize :invalid))
+  (should-error (compat-call json-serialize
+                             (let ((ht (make-hash-table)))
+                               (puthash 'a 1 ht)
+                               ht)))
+  (should-equal "[{}]" (compat-call json-serialize [nil]))
+  (should-equal "{}" (compat-call json-serialize nil))
+  (should-equal "{\"a\":{},\"b\":{}}" (compat-call json-serialize '(:a nil :b 
nil)))
+  (should-equal "{\"a\":{},\"b\":{}}" (compat-call json-serialize '((a) (b))))
+  (should-equal "{\"a\":1,\"b\":2}" (compat-call json-serialize '(:a 1 :b 2)))
+  (should-equal "{\"one\":1,\"two\":2,\"three\":3}" (compat-call 
json-serialize '(:one 1 two 2 three 3)))
+  (should-equal "{\"one\":1,\"two\":2}" (compat-call json-serialize '((one . 
1) (two . 2))))
+  (should-equal "[true,false,null]" (compat-call json-serialize [t :false 
:null]))
+  (should-equal "[true,false,null]" (compat-call json-serialize [t f n] 
:null-object 'n :false-object 'f))
+  (should-equal "[true,false,null]" (compat-call json-serialize [t f nil] 
:null-object nil :false-object 'f))
+  (should-equal "[true,false,null]" (compat-call json-serialize [t nil n] 
:null-object 'n :false-object nil))
+  (should-equal "1" (compat-call json-serialize 1))
+  (should-equal "\"foo\"" (compat-call json-serialize "foo"))
+  (should-equal "[1,2,3]" (compat-call json-serialize [1 2 3]))
+  (should-equal "{\"key\":[\"abc\",2],\"yek\":true}"
+                (compat-call json-serialize '(:key ["abc" 2] yek t)))
+  (should-equal "{\":key\":[\"abc\",2],\"yek\":true}"
+                (compat-call json-serialize '((:key . ["abc" 2]) (yek . t))))
+  (should-equal "{\"key\":[\"abc\",2],\"yek\":true}"
+                (compat-call json-serialize (let ((ht (make-hash-table)))
+                                              (puthash "key" ["abc" 2] ht)
+                                              (puthash "yek" t ht)
+                                              ht))))
+
 (provide 'compat-tests)
 ;;; compat-tests.el ends here
diff --git a/compat.el b/compat.el
index 62ca74a09c..1e38672e45 100644
--- a/compat.el
+++ b/compat.el
@@ -80,5 +80,238 @@ See also `compat-function' to lookup compatibility 
functions."
   (let ((compat (intern (format "compat--%s" fun))))
     `(,(if (fboundp compat) compat fun) ,@args)))
 
+;;;; Backported libjansson API
+
+(unless (eval-when-compile (ignore-errors (eval '(json-parse-string "0") t)))
+  (defvar json-null)
+  (defvar json-false)
+  (defvar json-array-type)
+  (defvar json-object-type)
+  (defvar json-key-type)
+  (declare-function json-read nil)
+
+  (declare-function compat--json--print nil)
+  (unless (eval-when-compile (ignore-errors (eval '(json-parse-string "[]") 
t)))
+    (defun compat--json--print (obj)
+      (cond
+       ((numberp obj) (prin1 obj))
+       ((eq obj t) (insert "true"))
+       ((eq obj json-null) (insert "null"))
+       ((eq obj json-false) (insert "false"))
+       ((not obj) (insert "{}"))
+       ((stringp obj)
+        (insert ?\")
+        (goto-char (prog1 (point) (princ obj)))
+        (while (re-search-forward "[\"\\[:cntrl:]]" nil 'move)
+          (let ((char (preceding-char)))
+            (delete-char -1)
+            (insert ?\\ (or (car (rassq char
+                                        '((?\" . ?\")
+                                          (?\\ . ?\\)
+                                          (?b . ?\b)
+                                          (?f . ?\f)
+                                          (?n . ?\n)
+                                          (?r . ?\r)
+                                          (?t . ?\t))))
+                            (format "u%04x" char)))))
+        (insert ?\"))
+       ((hash-table-p obj)
+        (insert ?\{)
+        (let ((first t))
+          (maphash
+           (lambda (key val)
+             (unless (stringp key)
+               (signal 'wrong-type-argument `(stringp ,key)))
+             (if first (setq first nil) (insert ?,))
+             (compat--json--print key)
+             (insert ?:)
+             (compat--json--print val))
+           obj))
+        (insert ?\}))
+       ((and (car-safe obj) (symbolp (car obj))) ;; plist
+        (insert ?\{)
+        (let ((head obj))
+          (while obj
+            (unless (and (car obj) (symbolp (car obj)))
+              (signal 'wrong-type-argument `(symbolp ,obj)))
+            (unless (cdr obj)
+              (signal 'wrong-type-argument `(consp ,(cdr obj))))
+            (unless (eq obj head) (insert ?,))
+            (compat--json--print
+             (if (keywordp (car obj))
+                 (substring (symbol-name (car obj)) 1)
+               (symbol-name (car obj))))
+            (insert ?:)
+            (compat--json--print (cadr obj))
+            (setq obj (cddr obj))))
+        (insert ?\}))
+       ((consp (car-safe obj)) ;; alist
+        (insert ?\{)
+        (let ((head obj))
+          (while obj
+            (unless (and (caar obj) (symbolp (caar obj)))
+              (signal 'wrong-type-argument `(symbolp ,(caar obj))))
+            (unless (eq obj head) (insert ?,))
+            (compat--json--print (symbol-name (caar obj)))
+            (insert ?:)
+            (compat--json--print (cdar obj))
+            (pop obj)))
+        (insert ?\}))
+       ((vectorp obj)
+        (insert ?\[)
+        (dotimes (i (length obj))
+          (when (> i 0) (insert ?,))
+          (compat--json--print (aref obj i)))
+        (insert ?\]))
+       (t (signal 'wrong-type-argument `(vectorp ,obj))))))
+
+  (defun compat--json-serialize (object &rest args) ;; 
<compat-tests:json-serialize>
+    "Return the JSON representation of OBJECT as a string.
+
+OBJECT must be t, a number, string, vector, hashtable, alist, plist,
+or the Lisp equivalents to the JSON null and false values, and its
+elements must recursively consist of the same kinds of values.  t will
+be converted to the JSON true value.  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.
+
+The Lisp equivalents to the JSON null and false values are
+configurable in the arguments ARGS, a list of keyword/argument pairs:
+
+The keyword argument `:null-object' specifies which object to use
+to represent a JSON null value.  It defaults to `:null'.
+
+The keyword argument `:false-object' specifies which object to use to
+represent a JSON false value.  It defaults to `:false'.
+
+In you specify the same value for `:null-object' and `:false-object',
+a potentially ambiguous situation, the JSON output will not contain
+any JSON false values."
+    (if (eval-when-compile (ignore-errors (eval '(json-parse-string "[]") t)))
+        (if (or (listp object) (vectorp object))
+            (apply 'json-serialize object args)
+          (substring (apply 'json-serialize (vector object) args) 1 -1))
+      (let ((json-false (if (plist-member args :false-object)
+                            (plist-get args :false-object)
+                          :false))
+            (json-null (if (plist-member args :null-object)
+                            (plist-get args :null-object)
+                         :null)))
+        (with-output-to-string
+          (with-current-buffer standard-output
+            (compat--json--print object))))))
+
+  (defun compat--json-insert (object &rest args) ;; <compat-tests:json-insert>
+    "Insert the JSON representation of OBJECT before point.
+This is the same as (insert (json-serialize OBJECT)), but potentially
+faster.  See the function `json-serialize' for allowed values of
+OBJECT."
+    (if (eval-when-compile (ignore-errors (eval '(json-parse-string "[]") t)))
+        (if (or (listp object) (vectorp object))
+            (apply 'json-insert object args)
+          (insert (substring (apply 'json-serialize (vector object) args) 1 
-1)))
+      (let ((json-false (if (plist-member args :false-object)
+                            (plist-get args :false-object)
+                          :false))
+            (json-null (if (plist-member args :null-object)
+                            (plist-get args :null-object)
+                         :null))
+            (standard-output (current-buffer)))
+        (compat--json--print object))))
+
+  (defun compat--json-parse-buffer (&rest args) ;; 
<compat-tests:json-parse-buffer>
+    "Read JSON object from current buffer starting at point.
+Move point after the end of the object if parsing was successful.
+On error, don't move point.
+
+The returned object will be a vector, list, hashtable, alist, or
+plist.  Its elements will be the JSON null value, the JSON false
+value, t, numbers, strings, or further vectors, lists, hashtables,
+alists, or plists.  If there are duplicate keys in an object, all
+but the last one are ignored.
+
+If the current buffer doesn't contain a valid JSON object, the
+function signals an error of type `json-parse-error'.
+
+The arguments ARGS are a list of keyword/argument pairs:
+
+The keyword argument `:object-type' specifies which Lisp type is used
+to represent objects; it can be `hash-table', `alist' or `plist'.  It
+defaults to `hash-table'.
+
+The keyword argument `:array-type' specifies which Lisp type is used
+to represent arrays; it can be `array' (the default) or `list'.
+
+The keyword argument `:null-object' specifies which object to use
+to represent a JSON null value.  It defaults to `:null'.
+
+The keyword argument `:false-object' specifies which object to use to
+represent a JSON false value.  It defaults to `:false'."
+    (if (eval-when-compile (ignore-errors (eval '(json-parse-string "[]") t)))
+        (save-match-data
+          (if (looking-at "\\s-*\\([^[{[:space:]]+\\)")
+              (let ((str (match-string 1)))
+                (goto-char (match-end 0))
+                (apply 'compat--json-parse-string str args))
+            (apply 'json-parse-buffer args)))
+      (unless (fboundp 'json-read)
+        (require 'json))
+      (let ((json-key-type nil)
+            (json-object-type (or (plist-get args :object-type) 'hash-table))
+            (json-array-type (or (plist-get args :array-type) 'array))
+            (json-false (if (plist-member args :false-object)
+                            (plist-get args :false-object)
+                          :false))
+            (json-null (if (plist-member args :null-object)
+                            (plist-get args :null-object)
+                          :null)))
+        (when (eq json-array-type 'array)
+          (setq json-array-type 'vector))
+      (json-read))))
+
+  (defun compat--json-parse-string (string &rest args) ;; 
<compat-tests:json-parse-string>
+    "Parse the JSON STRING into a Lisp object.
+This is essentially the reverse operation of `json-serialize', which
+see.  The returned object will be the JSON null value, the JSON false
+value, t, a number, a string, a vector, a list, a hashtable, an alist,
+or a plist.  Its elements will be further objects of these types.  If
+there are duplicate keys in an object, all but the last one are
+ignored.  If STRING doesn't contain a valid JSON object, this function
+signals an error of type `json-parse-error'.
+
+The arguments ARGS are a list of keyword/argument pairs:
+
+The keyword argument `:object-type' specifies which Lisp type is used
+to represent objects; it can be `hash-table', `alist' or `plist'.  It
+defaults to `hash-table'.
+
+The keyword argument `:array-type' specifies which Lisp type is used
+to represent arrays; it can be `array' (the default) or `list'.
+
+The keyword argument `:null-object' specifies which object to use
+to represent a JSON null value.  It defaults to `:null'.
+
+The keyword argument `:false-object' specifies which object to use to
+represent a JSON false value.  It defaults to `:false'."
+    (if (eval-when-compile (ignore-errors (eval '(json-parse-string "[]") t)))
+        (if (string-match-p "\\`\\s-*[[{]" string)
+            (apply 'json-parse-string string args)
+          ;; Add array wrapper and extract first element, in order to
+          ;; support RFC 8259. The older RFC 4627 implemented by
+          ;; `json-parse-string' did not support parsing toplevel atoms.
+          (elt (apply 'json-parse-string (concat "[" string "]") args) 0))
+      (with-temp-buffer
+        (insert string)
+        (goto-char (point-min))
+        ;; Do not use `json-read-from-string' here, since it also creates a
+        ;; temporary buffer.
+        (prog1 (apply 'compat--json-parse-buffer args)
+          (skip-chars-forward "[:space:]")
+          (unless (eobp)
+            (signal 'json-error "Trailing content after JSON stream")))))))
+
 (provide 'compat)
 ;;; compat.el ends here
diff --git a/compat.texi b/compat.texi
index e70c63d0ee..798c663177 100644
--- a/compat.texi
+++ b/compat.texi
@@ -2028,6 +2028,73 @@ If native compilation is not available, this function 
always returns
 These functions must be called explicitly via @code{compat-call},
 since their calling convention or behavior was extended in Emacs 28.1:
 
+@c copied from lispref/text.texi
+@defun compat-call@ json-serialize object &rest args
+This function returns a new Lisp string which contains the JSON
+representation of @var{object}.  The argument @var{args} is a list of
+keyword/argument pairs.  The following keywords are accepted:
+
+@table @code
+@item :null-object
+The value decides which Lisp object to use to represent the JSON
+keyword @code{null}.  It defaults to the symbol @code{:null}.
+
+@item :false-object
+The value decides which Lisp object to use to represent the JSON
+keyword @code{false}.  It defaults to the symbol @code{:false}.
+@end table
+
+@end defun
+
+@c copied from lispref/text.texi
+@defun compat-call@ json-insert object &rest args
+This function inserts the JSON representation of @var{object} into the
+current buffer before point.  The argument @var{args} are interpreted
+as in @code{json-parse-string}.
+@end defun
+
+@c copied from lispref/text.texi
+@defun compat-call@ json-parse-string string &rest args
+This function parses the JSON value in @var{string}, which must be a
+Lisp string.  If @var{string} doesn't contain a valid JSON object,
+this function signals the @code{json-parse-error} error.
+
+The argument @var{args} is a list of keyword/argument pairs.  The
+following keywords are accepted:
+
+@table @code
+@item :object-type
+The 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.
+
+@item :array-type
+The value decides which Lisp object to use for representing a JSON
+array.  It can be either @code{array}, the default, to use Lisp
+arrays; or @code{list} to use lists.
+
+@item :null-object
+The value decides which Lisp object to use to represent the JSON
+keyword @code{null}.  It defaults to the symbol @code{:null}.
+
+@item :false-object
+The value decides which Lisp object to use to represent the JSON
+keyword @code{false}.  It defaults to the symbol @code{:false}.
+@end table
+
+@end defun
+
+@c copied from lispref/text.texi
+@defun compat-call@ json-parse-buffer &rest args
+This function reads the next JSON value from the current buffer,
+starting at point.  It moves point to the position immediately after
+the value if contains a valid JSON object; otherwise it signals the
+@code{json-parse-error} error and doesn't move point.  The arguments
+@var{args} are interpreted as in @code{json-parse-string}.
+@end defun
+
 @defun compat-call@ string-width string &optional from to
 This function returns the width in columns of the string @var{string},
 if it were displayed in the current buffer and the selected window.
@@ -2090,7 +2157,7 @@ Support for the @code{natnum} defcustom type.
 @item
 Additional Edebug keywords.
 @item
-The libjansson JSON APIs, e.g., @code{json-parse-string}.
+The function @code{json-available-p}.
 @item
 The macro @code{pcase-setq}.
 @item



reply via email to

[Prev in Thread] Current Thread [Next in Thread]