main.ss
#lang scheme/base
(require (only-in scheme/base [read scheme:read] [write scheme:write]))
(provide read-json write-json jsexpr->json json->jsexpr jsexpr?)

(define (write-json json [port (current-output-port)])
  (cond
    [(hash? json)
     (display "{" port)
     (for ([(key value) json]
           [i (in-naturals)])
       (when (> i 0)
         (display ", " port))
       (fprintf port "\"~a\"" key)
       (display ": " port)
       (write-json value port))
     (display "}" port)]
    [(list? json)
     (display "[" port)
     (for ([(value i) (in-indexed json)])
       (when (> i 0)
         (display ", " port))
       (write-json value port))
     (display "]" port)]
    [(or (string? json) (and (number? json) (or (integer? json) (inexact? json))))
     (scheme:write json port)]
    [(boolean? json) (scheme:write (if json 'true 'false) port)]
    [(null-jsexpr? json) (scheme:write 'null port)]
    [else (error 'json "bad json value: ~v" json)]))

(define (read-json [port (current-input-port)])
  (case (peek-char port)
    [(#\{) (read/hash port)]
    [(#\[) (read/list port)]
    [(#\") (read/string port)]
    [(#\t) (read/true port)]
    [(#\f) (read/false port)]
    [(#\n) (read/null port)]
    [else (read/number port)]))

(define (expect ch . expected)
  (unless (memq ch expected)
    (error 'read "expected: ~v, got: ~a" expected ch))
  ch)

(define (expect-string port expected)
  (list->string (for/list ([ch expected])
                  (expect (read-char port) ch))))

(define (skip-whitespace port)
  (let ([ch (peek-char port)])
    (when (char-whitespace? ch)
      (read-char port)
      (skip-whitespace port))))

(define (in-port-until port reader done?)
  (make-do-sequence (lambda ()
                      (values reader
                              (lambda (port) port)
                              port
                              (lambda (port)
                                (not (done? port)))
                              (lambda values #t)
                              (lambda (port . values) #t)))))

(define (read/hash port)
  (expect (read-char port) #\{)
  (skip-whitespace port)
  (begin0 (for/hasheq ([(key value)
                        (in-port-until port
                                       (lambda (port)
                                         (let ([key (read/string port)])
                                           (unless (string? key)
                                             (error 'read "expected: string, got: ~v" key))
                                           (skip-whitespace port)
                                           (expect (read-char port) #\:)
                                           (skip-whitespace port)
                                           (let ([value (read-json port)])
                                             (skip-whitespace port)
                                             (expect (peek-char port) #\, #\})
                                             (values (string->symbol key) value))))
                                       (lambda (port)
                                         (eq? (peek-char port) #\})))])
            (when (eq? (peek-char port) #\,)
              (read-char port))
            (skip-whitespace port)
            (values key value))
          (expect (read-char port) #\})))

(define (read/list port)
  (expect (read-char port) #\[)
  (begin0 (for/list ([value
                      (in-port-until port
                                     (lambda (port)
                                       (skip-whitespace port)
                                       (begin0 (read-json port)
                                               (skip-whitespace port)
                                               (expect (peek-char port) #\, #\])))
                                     (lambda (port)
                                       (eq? (peek-char port) #\])))])
            (when (eq? (peek-char port) #\,)
              (read-char port))
            value)
          (expect (read-char port) #\])))

(define (read/string port)
  (expect (read-char port) #\")
  (begin0 (list->string
           (for/list ([ch (in-port-until port
                                         (lambda (port)
                                           (let ([ch (read-char port)])
                                             (when (eof-object? ch)
                                               (error 'read "unexpected EOF"))
                                             (if (eq? ch #\\)
                                                 (let ([esc (read-char port)])
                                                   (when (eof-object? ch)
                                                     (error 'read "unexpected EOF"))
                                                   (case esc
                                                     [(#\b) #\backspace]
                                                     [(#\n) #\newline]
                                                     [(#\r) #\return]
                                                     [(#\f) #\page]
                                                     [(#\t) #\tab]
                                                     [(#\\) #\\]
                                                     [(#\") #\"]
                                                     [(#\/) #\/]
                                                     [(#\u) (unescape (read-string 4 port))]
                                                     [else esc]))
                                                 ch)))
                                         (lambda (port)
                                           (eq? (peek-char port) #\")))])
             ch))
          (expect (read-char port) #\")))

(define (unescape str)
  (unless (regexp-match #px"[a-fA-F0-9]{4}" str)
    (error 'read "bad unicode escape sequence: \"\\u~a\"" str))
  (integer->char (string->number str 16)))

(define (read/true port)
  (expect-string port "true")
  #t)

(define (read/false port)
  (expect-string port "false")
  #f)

(define (read/null port)
  (expect-string port "null")
  null-jsexpr)

(define (read/digits port)
  (let ([digits (for/list ([digit (in-port-until port
                                                 read-char
                                                 (lambda (port)
                                                   (let ([ch (peek-char port)])
                                                     (or (eof-object? ch)
                                                         (not (char-numeric? ch))))))])
                  digit)])
    (when (and (null? digits) (eof-object? (peek-char port)))
      (error 'read "unexpected EOF"))
    (when (null? digits)
      (error 'read "expected: digits, got: ~a" (peek-char port)))
    digits))

(define (read/exponent port)
  (expect (read-char port) #\e #\E)
  (let ([sign (case (peek-char port)
                [(#\- #\+) (list (read-char port))]
                [else '()])])
    (append sign (read/digits port))))

(define (read/number port)
  (let* ([sign (if (eq? (peek-char port) #\-) '(#\-) '())]
         [digits (read/digits port)]
         [frac (if (eq? (peek-char port) #\.) (read/digits port) '())]
         [exp (if (memq (peek-char port) '(#\e #\E)) (read/exponent port) '())])
    (string->number
     (list->string
      (append sign digits frac exp)))))

(define (jsexpr? x)
  (or (integer? x)
      (and (number? x) (inexact? x))
      (null-jsexpr? x)
      (boolean? x)
      (string? x)
      (null? x)
      (array-jsexpr? x)
      (object-jsexpr? x)))

(define (array-jsexpr? x)
  (or (null? x)
      (and (pair? x)
           (jsexpr? (car x))
           (array-jsexpr? (cdr x)))))

(define (object-jsexpr? x)
  (let/ec return
    (and (hash? x)
         (for ([(key value) x])
           (unless (and (symbol? key) (jsexpr? value))
             (return #f)))
         #t)))

(define (null-jsexpr? x)
  (eqv? x #\null))

(define null-jsexpr #\null)

(define (jsexpr->json x)
  (let ([out (open-output-string)])
    (write-json x out)
    (get-output-string out)))

(define (json->jsexpr s)
  (let ([in (open-input-string s)])
    (read-json in)))