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)
```
next prev 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).