Hi David,
"Thompson, David" <[email protected]> writes:
> Attached is a patch that adds a new (web json) module. Some may
> remember that I submitted a patch back in 2015 (time flies, eh?) for
> an (ice-9 json) module that never made it in. Well, 10 years is a long
> time and Guile still doesn't have a built-in JSON module. Third party
> libraries like guile-json and guile-sjson are available, the latter
> being an adaptation of my original patch and the former remaining the
> go-to library used by larger Guile projects like Guix. There's also
> SRFI-180 (which sounds like a cool surfing trick!) which was published
> in 2020 but the API is, in my opinion, overly complicated due to
> generators and other things. Anyway, JSON continues to be *the* data
> interchange format of the web and Guile really ought to have a simple
> API that can read/write JSON to/from a port using only Scheme data
> types that have read syntax (i.e. no hash tables like guile-json).
> This minimal, practical API is what my patch provides. I've tried my
> best to make it as efficient as possible.
>
> I've settled on the following JSON<->Scheme data type mapping which is
> nearly identical to SRFI-180 with the exception of object keys:
>
> - true and false are #t and #f
> - null is the symbol 'null
> - numbers are either exact integers (fixnums and bignums) or inexact
> reals (flonums, NaNs and infinities excluded)
> - strings are strings
> - arrays are vectors
> - objects are association lists with string keys (SRFI-180 chose
> symbols but JSON uses strings so strings feel the most honest)
>
> Thanks in advance for the review,
First of all, let me say thank you for working on that! I agree that
this would be most welcome in core Guile, for the reasons you mention.
[...]
> +@example
> +@verbatim
> +{
> + "name": "Eva Luator",
> + "age": 24,
> + "schemer": true,
> + "hobbies": [
> + "hacking",
> + "cycling",
> + "surfing"
> + ]
> +}
> +@end verbatim
> +@end example
> +
> +can be represented with the following Scheme expression:
> +
> +@example
> +@verbatim
> +'(("name" . "Eva Luator")
> + ("age" . 24)
> + ("schemer" . #t)
> + ("hobbies" . #("hacking" "cycling" "surfing")))
> +@end verbatim
> +@end example
Is there particular reason for using vectors instead of plain list to
represent JSON arrays? The later would be more idiomatic unless there
are technical reasons (perhaps performance?).
> +Strings, exact integers, inexact reals (excluding NaNs and infinities),
> +@code{#t}, @code{#f}, the symbol @code{null}, vectors, and association
> +lists may be serialized as JSON. Association lists serialize as JSON
> +objects and vectors serialize as JSON arrays. The keys of association
> +lists @emph{must} be strings.
> +
> +@deffn {Scheme Procedure} read-json [port]
> +
> +Parse a JSON-encoded value from @var{port} and return its Scheme
> +representation. If @var{port} is unspecified, the current input port is
> +used.
> +
> +@example
> +@verbatim
> +(call-with-input-string "[true,false,null,42,\"foo\"]" read-json)
> +;; => #(#t #f null 42 "foo")
> +
> +(call-with-input-string "{\"foo\":1,\"bar\":2}" read-json)
> +;; => (("foo" . 1) ("bar" . 2))
> +@end verbatim
> +@end example
> +
> +@end deffn
> +
> +@deftp {Exception Type} &json-read-error
> +An exception type denoting JSON read errors.
> +@end deftp
>
> +@deffn {Scheme Procedure} write-json exp [port]
> +
> +Serialize the expression @var{exp} as JSON-encoded text to @var{port}.
> +If @var{port} is unspecified, the current output port is used.
> +
> +@example
> +@verbatim
> +(with-output-to-string (lambda () (write-json #(#t #f null 42 "foo"))))
> +;; => "[true,false,null,42,\"foo\"]"
> +
> +(with-output-to-string (lambda () (write-json '(("foo" . 1) ("bar" . 2)))))
> +;; => "{\"foo\":1,\"bar\":2}"
> +@end verbatim
> +@end example
> +
> +@end deffn
> +
> +@deftp {Exception Type} &json-write-error
> +An exception type denoting JSON write errors.
> +@end deftp
I think it could be a bit nicer if the deffn of read-json and write-json
explicitly mentioned that upon error an exception of type X is raised.
> +
> @node Web Client
> @subsection Web Client
>
> diff --git a/module/web/json.scm b/module/web/json.scm
> new file mode 100644
> index 000000000..41aac0e90
> --- /dev/null
> +++ b/module/web/json.scm
> @@ -0,0 +1,308 @@
> +;;;; json.scm --- JSON reader/writer (ECMA-404)
> +;;;; Copyright (C) 2025 Free Software Foundation, Inc.
> +;;;;
> +;;;; This library is free software; you can redistribute it and/or
> +;;;; modify it under the terms of the GNU Lesser General Public
> +;;;; License as published by the Free Software Foundation; either
> +;;;; version 3 of the License, or (at your option) any later version.
> +;;;;
> +;;;; This library is distributed in the hope that it will be useful,
> +;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
> +;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> +;;;; Lesser General Public License for more details.
> +;;;;
> +;;;; You should have received a copy of the GNU Lesser General Public
> +;;;; License along with this library; if not, write to the Free Software
> +;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> 02110-1301 USA
The FSF has gone office-less, so the above address is now incorrect [0].
The up-to-date template for the copyright notice (header) reads [1]:
--8<---------------cut here---------------start------------->8---
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see
<https://www.gnu.org/licenses/>.
--8<---------------cut here---------------end--------------->8---
[0] https://www.fsf.org/blogs/community/fsf-office-closing-party
[1] https://www.gnu.org/licenses/gpl-howto.html
> +
> +(define-module (web json)
> + #:use-module (ice-9 exceptions)
> + #:use-module (ice-9 match)
> + #:use-module (ice-9 textual-ports)
> + #:export (&json-read-error
> + read-json
> +
> + &json-write-error
> + write-json))
> +
> +(define-exception-type &json-read-error &error
> + make-json-read-error
> + json-read-error?)
> +
> +(define* (read-json #:optional (port (current-input-port)))
> + "Parse a JSON-encoded value from @var{port} and return its Scheme
> +representation. If @var{port} is unspecified, the current input port is
> +used."
> + (define (fail message)
> + (raise-exception
> + (make-exception (make-json-read-error)
> + (make-exception-with-origin 'read-json)
> + (make-exception-with-message message)
> + (make-exception-with-irritants (list port)))))
Hm, I wonder what (list port) looks like in the irritants when the
exception is reported; is it useful? Shouldn't it show instead the
problematic value?
> + (define (consume-whitespace)
> + (case (peek-char port)
> + ((#\space #\tab #\return #\newline)
Should a match + ((? char-whitespace?)) predicate pattern be used here
instead, or similar? Or perhaps the above is faster and more
self-contained, which can be a good thing.
> + (read-char port)
> + (consume-whitespace))
> + (else (values))))
> + (define-syntax-rule (define-keyword-reader name str val)
> + (define (name)
> + (if (string=? (get-string-n port (string-length str)) str)
> + val
> + (fail "invalid keyword"))))
> + (define-keyword-reader read-true "true" #t)
> + (define-keyword-reader read-false "false" #f)
> + (define-keyword-reader read-null "null" 'null)
> + (define (read-hex-digit)
> + (case (peek-char port)
> + ((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9)
> + (- (char->integer (read-char port)) (char->integer #\0)))
> + ((#\a #\b #\c #\d #\e #\f)
> + (+ 10 (- (char->integer (read-char port)) (char->integer #\a))))
> + ((#\A #\B #\C #\D #\E #\F)
> + (+ 10 (- (char->integer (read-char port)) (char->integer #\A))))
> + (else (fail "invalid hex digit"))))
> + (define (read-utf16-character)
> + (let* ((a (read-hex-digit))
> + (b (read-hex-digit))
> + (c (read-hex-digit))
> + (d (read-hex-digit)))
> + (integer->char (+ (* a (expt 16 3)) (* b (expt 16 2)) (* c 16) d))))
> + (define (read-escape-character)
> + (case (read-char port)
> + ((#\") #\")
> + ((#\\) #\\)
> + ((#\/) #\/)
> + ((#\b) #\backspace)
> + ((#\f) #\page)
> + ((#\n) #\newline)
> + ((#\r) #\return)
> + ((#\t) #\tab)
> + ((#\u) (read-utf16-character))
> + (else (fail "invalid escape character"))))
> + (define (read-string)
> + (read-char port)
> + (list->string
> + (let lp ()
> + (match (read-char port)
> + ((? eof-object?) (fail "EOF while reading string"))
> + (#\" '())
> + (#\\ (cons (read-escape-character) (lp)))
> + (char (cons char (lp)))))))
> + (define (read-digit-maybe)
> + (case (peek-char port)
> + ((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9)
> + (- (char->integer (read-char port))
> + (char->integer #\0)))
> + (else #f)))
> + (define (read-integer)
> + (let ((x (read-digit-maybe)))
> + (and x
> + (let lp ((x x))
> + (match (read-digit-maybe)
> + (#f x)
> + (y (lp (+ (* x 10) y))))))))
Perhaps the above should be named read-integer-maybe, since it may
return #f?
> + (define (read-fraction)
> + (case (peek-char port)
> + ((#\.)
> + (read-char port)
> + (let lp ((mag 10))
> + (let ((n (read-digit-maybe)))
> + (if n (+ (/ n mag) (lp (* mag 10))) 0))))
> + (else 0)))
Should the above be named 'read-decimal' ? Does a decimal number in
JSON always start with '.' and not with 0. ? I was a bit puzzled on
what 'mag' may mean here, I guess 'magnitude' although there doesn't
appear to have a clear terminology for it.
> + (define (read-exponent)
> + (case (peek-char port)
> + ((#\e #\E)
> + (read-char port)
> + (case (peek-char port)
> + ((#\-)
> + (read-char port)
> + (expt 10 (- (read-integer))))
> + ((#\+)
> + (read-char port)
> + (expt 10 (read-integer)))
> + (else
> + (expt 10 (read-integer)))))
> + (else 1)))
> + (define (read-positive-number)
> + (let ((n (read-integer)))
> + (and n
> + (let* ((f (read-fraction))
> + (e (read-exponent))
> + (x (* (+ n f) e)))
> + (if (exact-integer? x) x (exact->inexact x))))))
This may return #f. Should it fail instead, or be named
read-positive-number-maybe ?
> + (define (read-negative-number)
> + (read-char port)
> + (let ((x (read-positive-number)))
> + (if x (- x) (fail "invalid number"))))
Not symmetrical with the above: this one would fail if an integer
couldn't be read in read-positive-number.
> + (define (read-leading-zero-number)
> + (read-char port)
> + (case (peek-char port)
> + ;; Extraneous zeroes are not allowed. A single leading zero
> + ;; can only be followed by a decimal point.
> + ((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9 #\e #\E)
> + (fail "extraneous leading zero"))
Why not check for (not #\.) explicitly? That'd be clearer and would
cover all cases (even crazy unexpected ones).
> + ;; Fractional number.
> + ((#\.)
> + (let* ((d (read-fraction))
> + (e (read-exponent)))
> + (exact->inexact (* d e))))
> + ;; Just plain zero.
> + (else 0)))
> + (define (read-key+value-pair)
> + (let ((key (read-string)))
> + (consume-whitespace)
> + (case (read-char port)
> + ((#\:)
> + (consume-whitespace)
> + (cons key (read-value)))
> + (else (fail "invalid key/value pair delimiter")))))
> + (define (read-object)
> + (read-char port)
> + (consume-whitespace)
> + (case (peek-char port)
> + ;; Empty object.
> + ((#\})
> + (read-char port)
> + '())
> + (else
> + ;; Read first key/value pair, then all subsequent pairs delimited
> + ;; by commas.
> + (cons (read-key+value-pair)
> + (let lp ()
> + (consume-whitespace)
> + (case (peek-char port)
> + ((#\,)
> + (read-char port)
> + (consume-whitespace)
> + (cons (read-key+value-pair) (lp)))
> + ;; End of object.
> + ((#\})
> + (read-char port)
> + '())
> + (else (fail "invalid object delimiter"))))))))
> + (define (read-array)
> + (read-char port)
> + (consume-whitespace)
> + (case (peek-char port)
> + ;; Empty array.
> + ((#\])
> + (read-char port)
> + #())
> + (else
> + (list->vector
As mentioned above, just a plain list would be more Schemey, no? What
does the vector type buys us? A user wanting a vector could always call
list->vector themselves, and otherwise we save some computation.
> + ;; Read the first element, then all subsequent elements
> + ;; delimited by commas.
> + (cons (read-value)
> + (let lp ()
> + (consume-whitespace)
> + (case (peek-char port)
> + ;; Elements are comma delimited.
> + ((#\,)
> + (read-char port)
> + (consume-whitespace)
> + (cons (read-value) (lp)))
> + ;; End of array.
> + ((#\])
> + (read-char port)
> + '())
> + (else (fail "invalid array delimiter")))))))))
> + (define (read-value)
> + (consume-whitespace)
> + (case (peek-char port)
> + ((#\") (read-string))
> + ((#\{) (read-object))
> + ((#\[) (read-array))
> + ((#\t) (read-true))
> + ((#\f) (read-false))
> + ((#\n) (read-null))
> + ((#\-) (read-negative-number))
> + ((#\0) (read-leading-zero-number))
> + ((#\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9) (read-positive-number))
> + (else (fail "invalid value"))))
> + (read-value))
> +
> +(define-exception-type &json-write-error &error
> + make-json-write-error
> + json-write-error?)
> +
> +(define* (write-json exp #:optional (port (current-output-port)))
> + "Serialize the expression @var{exp} as JSON-encoded text to @var{port}.
> +If @var{port} is unspecified, the current output port is used."
> + (define (fail message x)
> + (raise-exception
> + (make-exception (make-json-write-error)
> + (make-exception-with-origin 'write-json)
> + (make-exception-with-message message)
> + (make-exception-with-irritants (list x)))))
> + (define (write-char/escape char)
> + (match char
> + (#\" (put-string port "\\\""))
> + (#\\ (put-string port "\\\\"))
> + (#\/ (put-string port "\\/"))
> + (#\backspace (put-string port "\\b"))
> + (#\page (put-string port "\\f"))
> + (#\newline (put-string port "\\n"))
> + (#\return (put-string port "\\r"))
> + (#\tab (put-string port "\\t"))
> + (_ (put-char port char))))
> + (define (write-string str)
> + (let ((in (open-input-string str)))
Looks like the above 'in' binding is not used.
> + (put-char port #\")
> + (string-for-each write-char/escape str)
> + (put-char port #\")))
> + (define (write-pair x)
> + (match x
> + (((? string? key) . value)
> + (write-string key)
> + (put-char port #\:)
> + (write-value value))
> + (_ (fail "invalid key/value pair" x))))
> + (define (write-object obj)
> + (put-char port #\{)
> + (match obj
> + ((head . rest)
> + (write-pair head)
> + (let lp ((obj rest))
> + (match obj
> + (() (values))
Any reason to return (values) instead of some dummy #t to denote 'no-op'
?.
> + ((head . rest)
> + (put-char port #\,)
> + (write-pair head)
> + (lp rest))
> + (_ (fail "invalid object" obj))))))
> + (put-char port #\}))
> + (define (write-array v)
> + (put-char port #\[)
> + (match (vector-length v)
> + (0 (values))
> + (n
> + (write-value (vector-ref v 0))
> + (do ((i 1 (1+ i)))
> + ((= i n))
> + (put-char port #\,)
> + (write-value (vector-ref v i)))))
I suppose the above is more efficient than a for-each loop? I'd be
curious to see it profiled, if you still have data. At least now I see
than for > 100k, vector-ref is faster than list-ref, which probably
explains why you went with vectors (could still be an implementation
detail with the list->vector call left in the writer though, in my
opinion).
> + (put-char port #\]))
> + (define (write-number x)
> + (if (or (exact-integer? x)
> + (and (real? x)
> + (inexact? x)
> + ;; NaNs and infinities are not allowed.
> + (not (or (nan? x) (inf? x)))))
> + ;; Scheme's string representations of exact integers and floats
> + ;; are compatible with JSON.
> + (put-string port (number->string x))
> + (fail "invalid number" x)))
> + (define (write-value x)
> + (match x
> + (#t (put-string port "true"))
> + (#f (put-string port "false"))
> + ('null (put-string port "null"))
> + (() (put-string port "{}"))
> + ((? pair?) (write-object x))
> + ((? vector?) (write-array x))
> + ((? string?) (write-string x))
> + ((? number?) (write-number x))
> + (_ (fail "invalid value" x))))
> + (write-value exp))
Phew. That's a pretty low-level parser! I hope it's fast, otherwise it
seems it'd be more concise/fun/maintainable to devise a PEG-based one,
which appears to be doable for JSON, from what I've read. Perhaps
sprinkle with a few performance-related comments where such concerns
impacted the design choices, so that we can remember and retest/reverify
these in the future when Guile evolves.
> diff --git a/test-suite/Makefile.am b/test-suite/Makefile.am
> index 6014b1f1f..00afea142 100644
> --- a/test-suite/Makefile.am
> +++ b/test-suite/Makefile.am
> @@ -73,6 +73,7 @@ SCM_TESTS = tests/00-initial-env.test \
> tests/iconv.test \
> tests/import.test \
> tests/interp.test \
> + tests/json.test \
> tests/keywords.test \
> tests/list.test \
> tests/load.test \
> diff --git a/test-suite/tests/json.test b/test-suite/tests/json.test
> new file mode 100644
> index 000000000..f92eeccec
> --- /dev/null
> +++ b/test-suite/tests/json.test
> @@ -0,0 +1,154 @@
> +;;;; json.test --- test JSON reader/writer -*- scheme -*-
> +;;;;
> +;;;; Copyright (C) 2015 Free Software Foundation, Inc.
> +;;;;
> +;;;; This library is free software; you can redistribute it and/or
> +;;;; modify it under the terms of the GNU Lesser General Public
> +;;;; License as published by the Free Software Foundation; either
> +;;;; version 3 of the License, or (at your option) any later version.
> +;;;;
> +;;;; This library is distributed in the hope that it will be useful,
> +;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
> +;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> +;;;; Lesser General Public License for more details.
> +;;;;
> +;;;; You should have received a copy of the GNU Lesser General Public
> +;;;; License along with this library; if not, write to the Free Software
> +;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> 02110-1301 USA
> +
> +(define-module (test-suite test-json)
> + #:use-module (test-suite lib)
> + #:use-module (web json))
> +
> +;;;
> +;;; Reader
> +;;;
> +
> +(define (read-json-string str)
> + (call-with-input-string str read-json))
> +
> +(define (json-read=? str x)
> + (= x (read-json-string str)))
> +
> +(define (json-read-eq? str x)
> + (eq? x (read-json-string str)))
> +
> +(define (json-read-equal? str x)
> + (equal? x (read-json-string str)))
> +
> +(define (json-read-string=? str x)
> + (string=? x (read-json-string str)))
> +
> +(with-test-prefix "read-json"
> + ;; Keywords
> + (pass-if (json-read-eq? "true" #t))
> + (pass-if (json-read-eq? "false" #f))
> + (pass-if (json-read-eq? "null" 'null))
> + ;; Numbers
> + (pass-if (json-read=? "0" 0))
> + (pass-if (json-read=? "-0" 0))
> + (pass-if (json-read=? "0.0" 0.0))
> + (pass-if (json-read=? "-0.0" -0.0))
> + (pass-if (json-read=? "0.1" 0.1))
> + (pass-if (json-read=? "1.234" 1.234))
> + (pass-if (json-read=? "1" 1))
> + (pass-if (json-read=? "-1" -1))
> + (pass-if (json-read=? "1.1" 1.1))
> + (pass-if (json-read=? "1e2" 1e2))
> + (pass-if (json-read=? "1.1e2" 1.1e2))
> + (pass-if (json-read=? "1.1e-2" 1.1e-2))
> + (pass-if (json-read=? "1.1e+2" 1.1e2))
> + ;; Extraneous zeroes in fraction
> + (pass-if (json-read=? "1.000" 1))
> + (pass-if (json-read=? "1.5000" 1.5))
> + ;; Extraneous zeroes in exponent
> + (pass-if (json-read=? "1.1e000" 1.1))
> + (pass-if (json-read=? "1.1e-02" 1.1e-2))
> + (pass-if (json-read=? "1.1e+02" 1.1e2))
> + ;; Strings
> + (pass-if (json-read-string=? "\"foo\"" "foo"))
> + ;; Escape codes
> + (pass-if (json-read-string=? "\"\\\"\"" "\""))
> + (pass-if (json-read-string=? "\"\\\\\"" "\\"))
> + (pass-if (json-read-string=? "\"\\/\"" "/"))
> + (pass-if (json-read-string=? "\"\\b\"" "\b"))
> + (pass-if (json-read-string=? "\"\\f\"" "\f"))
> + (pass-if (json-read-string=? "\"\\n\"" "\n"))
> + (pass-if (json-read-string=? "\"\\r\"" "\r"))
> + (pass-if (json-read-string=? "\"\\t\"" "\t"))
> + ;; Unicode in hexadecimal format
> + (pass-if (json-read-string=? "\"\\u12ab\"" "\u12ab"))
> + ;; Objects
> + (pass-if (json-read-equal? "{}" '()))
> + (pass-if (json-read-equal? "{ \"foo\": \"bar\", \"baz\": \"frob\"}"
> + '(("foo" . "bar") ("baz" . "frob"))))
> + ;; Nested objects
> + (pass-if (json-read-equal? "{\"foo\":{\"bar\":\"baz\"}}"
> + '(("foo" . (("bar" . "baz"))))))
> + ;; Arrays
> + (pass-if (json-read-equal? "[]" #()))
> + (pass-if (json-read-equal? "[1, 2, \"foo\"]"
> + #(1 2 "foo")))
> + ;; Nested arrays
> + (pass-if (json-read-equal? "[1, 2, [\"foo\", \"bar\"]]"
> + #(1 2 #("foo" "bar"))))
> + ;; Arrays and objects nested within each other
> + (pass-if (json-read-equal? "{\"foo\":[{\"bar\":true},{\"baz\":[1,2,3]}]}"
> + '(("foo" . #((("bar" . #t))
> + (("baz" . #(1 2 3))))))))
> + ;; Leading whitespace
> + (pass-if (json-read-eq? "\t\r\n true" #t)))
> +;;;
> +;;; Writer
> +;;;
> +
> +(define (write-json-string exp)
> + (call-with-output-string
> + (lambda (port)
> + (write-json exp port))))
> +
> +(define (json-write-string=? exp str)
> + (string=? str (write-json-string exp)))
> +
> +(with-test-prefix "write-json"
> + ;; Keywords
> + (pass-if (json-write-string=? #t "true"))
> + (pass-if (json-write-string=? #f "false"))
> + (pass-if (json-write-string=? 'null "null"))
> + ;; Numbers
> + (pass-if (json-write-string=? 0 "0"))
> + (pass-if (json-write-string=? 0.0 "0.0"))
> + (pass-if (json-write-string=? 0.1 "0.1"))
> + (pass-if (json-write-string=? 1 "1"))
> + (pass-if (json-write-string=? -1 "-1"))
> + (pass-if (json-write-string=? 1.1 "1.1"))
> + ;; Strings
> + (pass-if (json-write-string=? "foo" "\"foo\""))
> + ;; Escape codes
> + (pass-if (json-write-string=? "\"" "\"\\\"\""))
> + (pass-if (json-write-string=? "\\" "\"\\\\\""))
> + (pass-if (json-write-string=? "/" "\"\\/\""))
> + (pass-if (json-write-string=? "\b" "\"\\b\""))
> + (pass-if (json-write-string=? "\f" "\"\\f\""))
> + (pass-if (json-write-string=? "\n" "\"\\n\""))
> + (pass-if (json-write-string=? "\r" "\"\\r\""))
> + (pass-if (json-write-string=? "\t" "\"\\t\""))
> + ;; Objects
> + (pass-if (json-write-string=? '() "{}"))
> + (pass-if (json-write-string=? '(("foo" . "bar") ("baz" . "frob"))
> + "{\"foo\":\"bar\",\"baz\":\"frob\"}"))
> + ;; Nested objects
> + (pass-if (json-write-string=? '(("foo" . (("bar" . "baz"))))
> + "{\"foo\":{\"bar\":\"baz\"}}"))
> + ;; Arrays
> + (pass-if (json-write-string=? #() "[]"))
> + (pass-if (json-write-string=? #(1 2 "foo")
> + "[1,2,\"foo\"]"))
> + ;; Nested arrays
> + (pass-if (json-write-string=? #(1 2 #("foo" "bar"))
> + "[1,2,[\"foo\",\"bar\"]]"))
> + ;; Arrays and objects nested in each other
> + (pass-if (json-write-string=? '(("foo" . #((("bar" . #t))
> + (("baz" . #(1 2))))))
> +
> "{\"foo\":[{\"bar\":true},{\"baz\":[1,2]}]}")))
Neat. Nitpick: perhaps add a trailing '.' after each stand-alone
comments, to follow existing conventions.
I hope my armchair commentary is of some use :-).
Thanks again for working on a JSON parser/writer for Guile.
--
Thanks,
Maxim