help-gnu-emacs
[Top][All Lists]
Advanced

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

Re: Parse a field in JSON given a path to the field


From: Yuri Khan
Subject: Re: Parse a field in JSON given a path to the field
Date: Sat, 13 Nov 2021 21:08:28 +0700

On Fri, 12 Nov 2021 at 01:59, Husain Alshehhi <husain@alshehhi.io> wrote:
>
> Does emacs provide a function that can return a path from a JSON object? I 
> find something like this useful if I want a field from a nested, large JSON.

I happen to have a limited implementation of JSON Pointer (RFC 6901).
The limitation is that it assumes a JSON representation produced by:

    (let ((json-object-type 'alist)
          (json-array-type 'vector)
          (json-key-type 'string)
          (json-null :json-null))
      (json-read-from-string "…"))

because at the time of writing I found that the most unambiguous
representation offered:

* distinguishes arrays from objects
* distinguishes null, false, empty array, and empty object
* preserves object property order


(If someone wants to turn this into a proper package, please be my
guest. A useful addition would be for ‘jsonpointer-eval’ to support
hash representation for JSON objects; this has better performance at
the expense of losing property order and value readability.)

```
(require 'ert)
(require 'seq)
(eval-when-compile
  (require 'pcase)
  (require 'rx)
  (require 'subr-x))

(defun jsonpointer--unescape-tilde (token)
  "Unescape ‘~0’ to ‘~’ and ‘~1’ to ‘/’ in TOKEN."
  (thread-last token
    (replace-regexp-in-string "~1" "/")
    (replace-regexp-in-string "~0" "~")))

(ert-deftest jsonpointer--unescape-tilde/test-slash ()
  (should (equal (jsonpointer--unescape-tilde "foo~10") "foo/0")))
(ert-deftest jsonpointer--unescape-tilde/test-tilde ()
  (should (equal (jsonpointer--unescape-tilde "foo~01") "foo~1")))
(ert-deftest jsonpointer--unescape-tilde/test-multiple ()
  (should (equal (jsonpointer--unescape-tilde "foo~01~10~00~11")
"foo~1/0~0/1")))

(defun jsonpointer-parse (string)
  "Parse a JSON Pointer from string representation STRING.
Return a list of reference tokens with ‘~0’ and ‘~1’ sequences unescaped.
See RFC 6901 §§ 3, 5."
  (when (not (string-empty-p string))
    (assert (string-prefix-p "/" string))
    (seq-map #'jsonpointer--unescape-tilde
             (cdr (split-string string "/")))))

(ert-deftest jsonpointer-parse/test-empty-pointer ()
  (should (equal (jsonpointer-parse "") '())))
(ert-deftest jsonpointer-parse/test-empty-token ()
  (should (equal (jsonpointer-parse "/") '(""))))
(ert-deftest jsonpointer-parse/test-unescaped ()
  (should (equal (jsonpointer-parse "/unescaped") '("unescaped"))))
(ert-deftest jsonpointer-parse/test-tilde ()
  (should (equal (jsonpointer-parse "/escaped~01") '("escaped~1"))))
(ert-deftest jsonpointer-parse/test-slash ()
  (should (equal (jsonpointer-parse "/escaped~10") '("escaped/0"))))
(ert-deftest jsonpointer-parse/test-multiple ()
  (should (equal (jsonpointer-parse "/mix~01/match~10/unescaped")
                 '("mix~1" "match/0" "unescaped"))))

(defun jsonpointer-eval (pointer json)
  "Evaluate a JSON Pointer against a JSON document.
POINTER should be a list of reference tokens
as parsed by ‘jsonpointer-parse’.
See RFC 6901 § 4.
If at any point the token cannot be followed,
signal an error."
  (seq-reduce
   (lambda (json token)
     (pcase `(,json ,token)
       (`(,(and (pred listp) (app (assoc token) `(,_ . ,item)))
          ,_)
        item)
       (`(,(pred vectorp)
          ,(and (rx bos (or "0" (seq (any "1-9") (* (any "0-9")))) eos)
                (app string-to-number
                     (and (pred (<= 0)) (pred (> (length json))) index))))
        (aref json index))
       (_ (error "Cannot follow JSON pointer: %s  token: %s  json: %s"
                 pointer token json))))
   pointer json))

(defconst jsonpointer--eval-test-json
  '(("foo" . ["bar" "baz"])
    ("" . 0)
    ("a/b" . 1)
    ("c%d" . 2)
    ("e^f" . 3)
    ("g|h" . 4)
    ("i\\j" . 5)
    ("k\"l" . 6)
    (" " . 7)
    ("m~n" . 8)))

(ert-deftest jsonpointer-eval/test-empty-pointer ()
  (should (equal (jsonpointer-eval '() jsonpointer--eval-test-json)
                 jsonpointer--eval-test-json)))
(ert-deftest jsonpointer-eval/test-unescaped ()
  (should (equal (jsonpointer-eval '("foo") jsonpointer--eval-test-json)
                 ["bar" "baz"])))
(ert-deftest jsonpointer-eval/test-multi-token ()
  (should (equal (jsonpointer-eval '("foo" "0")
                                   jsonpointer--eval-test-json)
                 "bar")))
(ert-deftest jsonpointer-eval/test-empty-token ()
  (should (equal (jsonpointer-eval '("") jsonpointer--eval-test-json)
                 0)))
(ert-deftest jsonpointer-eval/test-slash ()
  (should (equal (jsonpointer-eval '("a/b") jsonpointer--eval-test-json)
                 1)))
(ert-deftest jsonpointer-eval/test-percent ()
  (should (equal (jsonpointer-eval '("c%d") jsonpointer--eval-test-json)
                 2)))
(ert-deftest jsonpointer-eval/test-caret ()
  (should (equal (jsonpointer-eval '("e^f") jsonpointer--eval-test-json)
                 3)))
(ert-deftest jsonpointer-eval/test-pipe ()
  (should (equal (jsonpointer-eval '("g|h") jsonpointer--eval-test-json)
                 4)))
(ert-deftest jsonpointer-eval/test-backslash ()
  (should (equal (jsonpointer-eval '("i\\j") jsonpointer--eval-test-json)
                 5)))
(ert-deftest jsonpointer-eval/test-quote ()
  (should (equal (jsonpointer-eval '("k\"l") jsonpointer--eval-test-json)
                 6)))
(ert-deftest jsonpointer-eval/test-space ()
  (should (equal (jsonpointer-eval '(" ") jsonpointer--eval-test-json)
                 7)))
(ert-deftest jsonpointer-eval/test-tilde ()
  (should (equal (jsonpointer-eval '("m~n") jsonpointer--eval-test-json)
                 8)))

(provide 'jsonpointer)
```



reply via email to

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