Files
dat-thang/scheme/dat-json.scm
T
divorce b735900f2e 0.0.002
ddat interop with dat js
cable interop with cabal js
ddat's frontend = dat-thang
cable fe = cable-thang
dillo support based on cel's dillo-dat
bootstrap node
xplatform support

Signed-off-by: divo <418@cryspace.space>
2026-10-19 00:17:02 -04:00

245 lines
11 KiB
Scheme

;;; Strict JSON reader shared by the S7-backed Dat application layer.
;;; Objects become (json:object ("key" . value) ...), arrays become
;;; (json:array value ...), and JSON null is the symbol json:null.
(define (json:codepoint->utf8 code)
(if (<= code #x7f)
(list (integer->char code))
(if (<= code #x7ff)
(list
(integer->char (+ #xc0 (quotient code 64)))
(integer->char (+ #x80 (modulo code 64))))
(if (<= code #xffff)
(list
(integer->char (+ #xe0 (quotient code 4096)))
(integer->char (+ #x80 (modulo (quotient code 64) 64)))
(integer->char (+ #x80 (modulo code 64))))
(list
(integer->char (+ #xf0 (quotient code 262144)))
(integer->char (+ #x80 (modulo (quotient code 4096) 64)))
(integer->char (+ #x80 (modulo (quotient code 64) 64)))
(integer->char (+ #x80 (modulo code 64))))))))
(define (dat:json-parse source)
(if (not (string? source))
(error 'wrong-type-arg "dat:json-parse expects a string" source))
(let ((port (open-input-string source)))
(define (fail message)
(error 'json-parse-error message))
(define (peek) (peek-char port))
(define (take) (read-char port))
(define (ws?)
(let ((c (peek)))
(and (char? c)
(or (char=? c #\space) (char=? c #\tab)
(char=? c #\newline) (char=? c #\return)))))
(define (skip-ws)
(do () ((not (ws?))) (take)))
(define (expect character)
(if (or (not (char? (peek))) (not (char=? (take) character)))
(fail (string-append "expected " (string character)))))
(define (hex-value c)
(cond ((and (char>=? c #\0) (char<=? c #\9))
(- (char->integer c) (char->integer #\0)))
((and (char>=? c #\a) (char<=? c #\f))
(+ 10 (- (char->integer c) (char->integer #\a))))
((and (char>=? c #\A) (char<=? c #\F))
(+ 10 (- (char->integer c) (char->integer #\A))))
(else (fail "invalid hexadecimal escape"))))
(define (hex4)
(let loop ((left 4) (value 0))
(if (= left 0) value
(let ((c (take)))
(if (not (char? c)) (fail "incomplete unicode escape"))
(loop (- left 1) (+ (* value 16) (hex-value c)))))))
(define (parse-string)
(expect #\")
(let loop ((reversed '()))
(let ((c (take)))
(cond ((not (char? c)) (fail "unterminated string"))
((char=? c #\") (list->string (reverse reversed)))
((char=? c #\\)
(let ((escape (take)))
(cond ((not (char? escape)) (fail "incomplete escape"))
((char=? escape #\") (loop (cons #\" reversed)))
((char=? escape #\\) (loop (cons #\\ reversed)))
((char=? escape #\/) (loop (cons #\/ reversed)))
((char=? escape #\b) (loop (cons (integer->char 8) reversed)))
((char=? escape #\f) (loop (cons (integer->char 12) reversed)))
((char=? escape #\n) (loop (cons #\newline reversed)))
((char=? escape #\r) (loop (cons #\return reversed)))
((char=? escape #\t) (loop (cons #\tab reversed)))
((char=? escape #\u)
(let* ((first (hex4))
(code
(cond ((and (>= first #xd800) (<= first #xdbff))
(expect #\\) (expect #\u)
(let ((second (hex4)))
(if (or (< second #xdc00) (> second #xdfff))
(fail "invalid unicode surrogate pair"))
(+ #x10000 (* (- first #xd800) #x400)
(- second #xdc00))))
((and (>= first #xdc00) (<= first #xdfff))
(fail "unpaired unicode low surrogate"))
(else first))))
(if (< code 32) (fail "invalid control character escape"))
(loop (append (reverse (json:codepoint->utf8 code))
reversed))))
(else (fail "invalid string escape")))))
((< (char->integer c) 32) (fail "unescaped control character"))
(else (loop (cons c reversed)))))))
(define (digit? c)
(and (char? c) (char>=? c #\0) (char<=? c #\9)))
(define (parse-number)
(let loop ((chars '()))
(let ((c (peek)))
(if (and (char? c)
(or (digit? c) (memv c '(#\- #\+ #\. #\e #\E))))
(begin (take) (loop (cons c chars)))
(let* ((text (list->string (reverse chars)))
(number (string->number text)))
(if (or (string=? text "") (not number)
(not (json:number-token? text)))
(fail "invalid JSON number"))
number)))))
(define (parse-array)
(expect #\[)
(skip-ws)
(if (and (char? (peek)) (char=? (peek) #\]))
(begin (take) '(json:array))
(let loop ((values '()))
(let ((value (parse-value)))
(skip-ws)
(cond ((and (char? (peek)) (char=? (peek) #\]))
(take) (cons 'json:array (reverse (cons value values))))
((and (char? (peek)) (char=? (peek) #\,))
(take) (skip-ws) (loop (cons value values)))
(else (fail "expected comma or closing bracket")))))))
(define (parse-object)
(expect #\{)
(skip-ws)
(if (and (char? (peek)) (char=? (peek) #\}))
(begin (take) '(json:object))
(let loop ((fields '()))
(if (or (not (char? (peek))) (not (char=? (peek) #\")))
(fail "object key must be a string"))
(let ((key (parse-string)))
(skip-ws) (expect #\:) (skip-ws)
(let ((field (cons key (parse-value))))
(skip-ws)
(cond ((and (char? (peek)) (char=? (peek) #\}))
(take) (cons 'json:object (reverse (cons field fields))))
((and (char? (peek)) (char=? (peek) #\,))
(take) (skip-ws) (loop (cons field fields)))
(else (fail "expected comma or closing brace"))))))))
(define (literal token value)
(for-each (lambda (expected)
(if (or (not (char? (peek)))
(not (char=? (take) expected)))
(fail "invalid JSON literal")))
(string->list token))
value)
(define (parse-value)
(skip-ws)
(let ((c (peek)))
(cond ((not (char? c)) (fail "expected a JSON value"))
((char=? c #\") (parse-string))
((char=? c #\{) (parse-object))
((char=? c #\[) (parse-array))
((char=? c #\t) (literal "true" #t))
((char=? c #\f) (literal "false" #f))
((char=? c #\n) (literal "null" 'json:null))
((or (digit? c) (char=? c #\-)) (parse-number))
(else (fail "unexpected character in JSON")))))
(let ((value (parse-value)))
(skip-ws)
(if (not (eof-object? (peek))) (fail "trailing data after JSON value"))
value)))
;;; Validate the RFC 8259 number grammar before string->number, which accepts
;;; Scheme-specific forms that are not valid JSON.
(define (json:number-token? text)
(let* ((n (string-length text)) (i 0))
(define (at c) (and (< i n) (char=? (string-ref text i) c)))
(define (digits)
(let ((start i))
(do () ((or (>= i n)
(not (char<=? #\0 (string-ref text i) #\9))))
(set! i (+ i 1)))
(> i start)))
(if (at #\-) (set! i (+ i 1)))
(let ((integer-ok
(cond ((at #\0) (set! i (+ i 1)) (not (and (< i n) (char<=? #\0 (string-ref text i) #\9))))
(else (and (< i n) (char<=? #\1 (string-ref text i) #\9)
(begin (set! i (+ i 1)) (digits) #t))))))
(and integer-ok
(or (not (at #\.))
(begin (set! i (+ i 1)) (digits)))
(or (not (or (at #\e) (at #\E)))
(begin (set! i (+ i 1))
(if (or (at #\+) (at #\-)) (set! i (+ i 1)))
(digits)))
(= i n)))))
(define (dat:json-object-ref object key . fallback)
(if (or (not (pair? object)) (not (eq? (car object) 'json:object)))
(error 'wrong-type-arg "expected a parsed JSON object" object)
(let ((field (assoc key (cdr object))))
(if field (cdr field) (if (pair? fallback) (car fallback) #f)))))
;;; Encode the same tagged values returned by dat:json-parse. Scheme records
;;; remain the native representation; this is the narrow adapter used when a
;;; compatible peer requires JSON bytes on the wire.
(define (dat:json-stringify value)
(define (write-unicode-escape n port)
(let ((digits "0123456789abcdef"))
(display "\\u" port)
(for-each
(lambda (shift)
(write-char (string-ref digits (modulo (quotient n shift) 16)) port))
'(4096 256 16 1))))
(define (string-encode text)
(let ((port (open-output-string)))
(write-char #\" port)
(for-each
(lambda (c)
(let ((n (char->integer c)))
(cond ((char=? c #\") (display "\\\"" port))
((char=? c #\\) (display "\\\\" port))
((char=? c #\backspace) (display "\\b" port))
((char=? c #\newline) (display "\\n" port))
((char=? c #\return) (display "\\r" port))
((char=? c #\tab) (display "\\t" port))
((< n 32) (write-unicode-escape n port))
(else (write-char c port)))))
(string->list text))
(write-char #\" port)
(get-output-string port)))
(define (encode item)
(cond ((string? item) (string-encode item))
((eq? item 'json:null) "null")
((eq? item #t) "true")
((eq? item #f) "false")
((number? item)
(let ((text (number->string item)))
(if (json:number-token? text) text
(error 'json-encode-error "number is not representable in JSON" item))))
((and (pair? item) (eq? (car item) 'json:array))
(string-append "[" (join "," (map encode (cdr item))) "]"))
((and (pair? item) (eq? (car item) 'json:object))
(string-append "{" (join ","
(map (lambda (field)
(if (or (not (pair? field)) (not (string? (car field))))
(error 'json-encode-error "object keys must be strings" field))
(string-append (string-encode (car field)) ":" (encode (cdr field))))
(cdr item))) "}"))
(else (error 'json-encode-error "unsupported JSON value" item))))
(define (join separator strings)
(if (null? strings) ""
(let loop ((rest (cdr strings)) (out (car strings)))
(if (null? rest) out
(loop (cdr rest) (string-append out separator (car rest)))))))
(encode value))
(provide 'dat:json)