unofficial mirror of help-gnu-emacs@gnu.org
 help / color / mirror / Atom feed
From: Yuri Khan <yuri.v.khan@gmail.com>
To: Husain Alshehhi <husain@alshehhi.io>
Cc: help-gnu-emacs <help-gnu-emacs@gnu.org>
Subject: Re: Parse a field in JSON given a path to the field
Date: Sat, 13 Nov 2021 21:08:28 +0700	[thread overview]
Message-ID: <CAP_d_8WVXnL6BeRTwGKseAb9NknCKbL9V80qWvf-8N9wD5NdtA@mail.gmail.com> (raw)
In-Reply-To: <87r1bmpm8n.fsf@alshehhi.io>

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)
```



  parent reply	other threads:[~2021-11-13 14:08 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-11-11 18:42 Parse a field in JSON given a path to the field Husain Alshehhi
2021-11-13 13:15 ` Andreas Röhler
2021-11-13 14:08 ` Yuri Khan [this message]
2022-01-26 22:05 ` Tim Landscheidt
2022-01-26 22:19   ` 2QdxY4RzWzUUiLuE

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=CAP_d_8WVXnL6BeRTwGKseAb9NknCKbL9V80qWvf-8N9wD5NdtA@mail.gmail.com \
    --to=yuri.v.khan@gmail.com \
    --cc=help-gnu-emacs@gnu.org \
    --cc=husain@alshehhi.io \
    /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.
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).