#lang scheme/base
(define-struct (exn:fail:invalid-json exn:fail)
(location)
#:transparent)
(define (make-invalid-json-exc
sym
#:text text
#:continuation-marks continuation-marks
#:location (location #f))
(let ((location (if (input-port? location)
(call-with-values
(lambda () (port-next-location location))
list)
location)))
(make-exn:fail:invalid-json (format "~A: invalid JSON: ~A~A"
sym
text
(if location
(format ", location ~S" location)
""))
continuation-marks
location)))
(define-syntax raise-invalid-json-error
(syntax-rules ()
((_ SYM ARGn ...)
(raise (make-invalid-json-exc
SYM
#:continuation-marks (current-continuation-marks)
ARGn ...)))))
(define-syntax %json-parsing:syntax-error
(syntax-rules ()
((_) #f)))
(define-syntax %json-parsing:case-read-token
(syntax-rules ()
((_ (IN ...) SEED C0 Cn ...)
(let ((in (IN ...)))
(%json-parsing:case-read-token in SEED C0 Cn ...)))
((_ IN (SEED ...) C0 Cn ...)
(let ((seed (SEED ...)))
(%json-parsing:case-read-token IN seed C0 Cn ...)))
((_ IN SEED C0 Cn ...)
(%json-parsing:case-read-token:2 (C0 Cn ...) () IN SEED (C0 Cn ...)))))
(define-syntax %json-parsing:case-read-token:2
(syntax-rules (else else-error)
((_ () As IN SEED SCs)
(%json-parsing:case-read-token:3 SCs () IN SEED As))
((_ ((else En ...) Cn ...) As IN SEED SCs)
(%json-parsing:case-read-token:2 (Cn ...) As IN SEED SCs))
((_ ((else-error En ...) Cn ...) As IN SEED SCs)
(%json-parsing:case-read-token:2 (Cn ...) As IN SEED SCs))
((_ ((T En ...) Cn ...) (An ...) IN SEED SCs)
(%json-parsing:case-read-token:2 (Cn ...) (An ... T) IN SEED SCs))
((_ (C0 Cn ...) (An ...) IN SEED SCs)
(%json-parsing:syntax-error "invalid case-read-token clause" C0))))
(define-syntax %json-parsing:case-read-token:3
(syntax-rules (=>
open-curly
close-curly
comma
colon
open-square
close-square
string
number
true
false
null
else)
((_ () (OCn ...) IN SEED As)
(let loop ()
(let ((c (peek-char IN)))
(case c
((#\space #\tab #\return #\newline #\page) (read-char IN) (loop))
OCn ...))))
((_ ((open-curly E0 En ...) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:case-read-token:3 (ICn ...)
(OCn ... ((#\{) (read-char IN) E0 En ...))
IN SEED As))
((_ ((close-curly E0 En ...) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:case-read-token:3 (ICn ...)
(OCn ... ((#\}) (read-char IN) E0 En ...))
IN SEED As))
((_ ((comma E0 En ...) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:case-read-token:3 (ICn ...)
(OCn ... ((#\,) (read-char IN) E0 En ...))
IN SEED As))
((_ ((colon E0 En ...) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:case-read-token:3 (ICn ...)
(OCn ... ((#\:) (read-char IN) E0 En ...))
IN SEED As))
((_ ((open-square E0 En ...) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:case-read-token:3 (ICn ...)
(OCn ... ((#\[) (read-char IN) E0 En ...))
IN SEED As))
((_ ((close-square E0 En ...) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:case-read-token:3 (ICn ...)
(OCn ... ((#\]) (read-char IN) E0 En ...))
IN SEED As))
((_ ((string => P) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:case-read-token:3
(ICn ...)
(OCn ...
((#\")
(read-char IN)
(P (%json-parsing:read-string IN) SEED)))
IN SEED As))
((_ ((number => P) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:case-read-token:3
(ICn ...)
(OCn ... ((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9 #\-)
(P (%json-parsing:read-number IN) SEED)))
IN SEED As))
((_ ((true => P) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:case-read-token:3
(ICn ...)
(OCn ... ((#\t)
(%json-parsing:parse-keyword
IN (#\r #\u #\e) "true" ((P 'true SEED)))))
IN SEED As))
((_ ((false => P) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:case-read-token:3
(ICn ...)
(OCn ... ((#\f)
(%json-parsing:parse-keyword
IN (#\a #\l #\s #\e) "false" ((P 'false SEED)))))
IN SEED As))
((_ ((null => P) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:case-read-token:3
(ICn ...)
(OCn ... ((#\n)
(%json-parsing:parse-keyword
IN (#\u #\l #\l) "null" ((P 'null SEED)))))
IN SEED As))
((_ ((else E0 En ...) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:case-read-token:3 (ICn ...)
(OCn ... (else E0 En ...))
IN SEED As))
((_ ((else X ...) IC0 ICn ...) (OCn ...) IN SEED As)
(%json-parsing:syntax-error "stuff after else clause" (else X ...)))
((_ ((else-error CONTEXT) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:case-read-token:3
(ICn ...)
(OCn ... (else (raise-invalid-json-error
'%<json-parsing:case-read-token>
#:text (format "character ~S in context ~S"
(peek-char IN)
CONTEXT)
#:location IN)))
IN SEED As))
((_ ((else-error X ...) IC0 ICn ...) (OCn ...) IN SEED As)
(%json-parsing:syntax-error "stuff after else-error clause"
(else-error X ...)))
((_ ((T E0 En ...) ICn ...) (OCn ...) IN SEED As)
(%json-parsing:syntax-error
"invalid token name" T "in clause" (T E0 En ...)))
((_ (IC0 ICn ...) (OCn ...) IN SEED As)
(%json-parsing:syntax-error "invalid clause" IC0))))
(define-syntax %json-parsing:parse-keyword
(syntax-rules ()
((_ IN (Cn ...) KW (E0 En ...))
(begin (read-char IN)
(if (and (eqv? (read-char IN) Cn) ...
(let ((c (peek-char IN)))
(or (eof-object? c)
(let ((n (char->integer c)))
(not (or (<= 97 n 122)
(<= 65 n 90)
(<= 48 n 57)))))))
(begin E0 En ...)
(raise-invalid-json-error
'%json-parsing:parse-keyword
#:text (format "invalid keyword that started to be ~S" KW)
#:location IN))))))
(define (%json-parsing:read-string in)
(let loop ((result '()))
(let ((c (read-char in)))
(case c
((#\") (apply string (reverse result)))
((#\\)
(let ((c (read-char in)))
(case c
((#\" #\\ #\/) (loop (cons c result)))
((#\b) (loop (cons #\backspace result)))
((#\f) (loop (cons #\page result)))
((#\n) (loop (cons #\newline result)))
((#\r) (loop (cons #\return result)))
((#\t) (loop (cons #\tab result)))
((#\u)
(let loop-u ((mults '(4096 256 16 1))
(num 0))
(if (null? mults)
(loop (cons (integer->char num) result))
(loop-u (cdr mults)
(+ num
(* (car mults)
(let ((c (read-char in)))
(case c
((#\0) 0)
((#\1) 1)
((#\2) 2)
((#\3) 3)
((#\4) 4)
((#\5) 5)
((#\6) 6)
((#\7) 7)
((#\8) 8)
((#\9) 9)
((#\a #\A) 10)
((#\b #\B) 11)
((#\c #\C) 12)
((#\d #\D) 13)
((#\e #\E) 14)
((#\f #\F) 15)
(else
(raise-invalid-json-error
'%json-parsing:read-string
#:text
(format
"invalid character ~S in \\u in string"
c)
#:location in))))))))))
(else (raise-invalid-json-error
'%json-parsing:read-string
#:text (format "invalid escape sequence \"\\~A\" in string"
c)
#:location in)))))
(else (if (eof-object? c)
(raise-invalid-json-error '%json-parsing:read-string
#:text "EOF in string"
#:location in)
(loop (cons c result))))))))
(define %json-parsing:read-number
(letrec ((read-digits
(lambda (in chars required?)
(let loop ((chars chars)
(required? required?))
(let ((c (peek-char in)))
(case c
((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9)
(read-char in)
(loop (cons c chars) #f))
(else (if required?
(raise-invalid-json-error
'%json-parsing:read-number
#:text "missing digits in number"
#:location in)
chars))))))))
(lambda (in)
(let* ((chars (let ((c (read-char in)))
(case c
((#\-) (read-digits in '(#\-) #t))
(else (read-digits in `(,c) #f)))))
(chars (let ((c (peek-char in)))
(case c
((#\.)
(read-char in)
(read-digits in `(#\. ,@chars) #t))
(else chars))))
(chars (case (peek-char in)
((#\e #\E)
(read-char in)
(case (peek-char in)
((#\-)
(read-char in)
(read-digits in `(#\- #\e ,@chars) #t))
((#\+)
(read-char in)
(read-digits in `(#\+ #\e ,@chars) #t))
(else
(read-digits in `( #\e ,@chars) #t))))
(else chars))))
(let ((c (peek-char in)))
(if (or (eof-object? c)
(not (or (eqv? #\- c)
(eqv? #\+ c)
(char-alphabetic? c))))
(string->number (apply string (reverse chars)))
(raise-invalid-json-error
'%json-parsing:read-number
#:text (format "invalid character ~S after number" c)
#:location in)))))))
(define-syntax json-fold-lambda
(syntax-rules ()
((_ #:error-name EN
#:visit-object-start VOS
#:visit-object-end VOE
#:visit-member-start VMS
#:visit-member-end VME
#:visit-array-start VAS
#:visit-array-end VAE
#:visit-string VS
#:visit-number VN
#:visit-constant VC)
(letrec
(
(error-name EN)
(visit-object-start VOS)
(visit-object-end VOE)
(visit-member-start VMS)
(visit-member-end VME)
(visit-array-start VAS)
(visit-array-end VAE)
(visit-string VS)
(visit-number VN)
(visit-constant VC)
(do-value
(lambda (in seed)
(%json-parsing:case-read-token
in
seed
(open-curly (do-object in seed))
(open-square (do-array in seed))
(string => visit-string)
(number => visit-number)
(true => visit-constant)
(false => visit-constant)
(null => visit-constant)
(else-error "value"))))
(do-object
(lambda (in seed)
(do-object-members in (visit-object-start seed) seed)))
(do-object-members
(lambda (in object-seed object-parent-seed)
(%json-parsing:case-read-token
in
'dummy-seed
(close-curly (visit-object-end object-seed object-parent-seed))
(string
=> (lambda (name dummy-seed)
(let ((value-seed (visit-member-start name object-seed)))
(%json-parsing:case-read-token
in
value-seed
(colon
(let ((value-seed (do-value in value-seed)))
(let ((object-seed (visit-member-end
name
value-seed object-seed)))
(%json-parsing:case-read-token
in
object-seed
(close-curly (visit-object-end
object-seed
object-parent-seed))
(comma (do-object-members
in
object-seed
object-parent-seed))
(else-error "object comma or end")))))
(else-error "object member colon")))))
(else-error "object member name or end"))))
(do-array
(lambda (in seed)
(do-array-members in (visit-array-start seed) seed)))
(do-array-members
(lambda (in seed parent-seed)
(%json-parsing:case-read-token
in
seed
(close-square (visit-array-end seed parent-seed))
(else (let ((seed (do-value in seed)))
(%json-parsing:case-read-token
in
seed
(close-square (visit-array-end seed
parent-seed))
(comma (do-array-members in
seed
parent-seed))
(else-error "array comma or end"))))))))
(lambda (in seed exhaust?)
(let ((in (if (string? in)
(open-input-string in)
in)))
(%json-parsing:case-read-token
in
#f
(else
(let ((c (peek-char in)))
(if (eof-object? c)
c
(begin0 (do-value in seed)
(and exhaust?
(%json-parsing:case-read-token
in
#f
(else
(or (eof-object? (peek-char in))
(raise-invalid-json-error
error-name
#:text
(format "input not exhausted; character ~S"
(peek-char in))
#:location in))))))))))))))))
(define (make-json-fold
#:error-name (error-name '<make-json-fold>)
#:visit-object-start visit-object-start
#:visit-object-end visit-object-end
#:visit-member-start visit-member-start
#:visit-member-end visit-member-end
#:visit-array-start visit-array-start
#:visit-array-end visit-array-end
#:visit-string visit-string
#:visit-number visit-number
#:visit-constant visit-constant)
(json-fold-lambda
#:error-name error-name
#:visit-object-start visit-object-start
#:visit-object-end visit-object-end
#:visit-member-start visit-member-start
#:visit-member-end visit-member-end
#:visit-array-start visit-array-start
#:visit-array-end visit-array-end
#:visit-string visit-string
#:visit-number visit-number
#:visit-constant visit-constant))
(define %json-parsing:json->sjson:fold
(json-fold-lambda
#:error-name 'json->sjson
#:visit-object-start (lambda (seed)
(make-hasheq))
#:visit-object-end (lambda (seed parent-seed)
`(,seed ,@parent-seed))
#:visit-member-start (lambda (name seed)
'())
#:visit-member-end (lambda (name seed parent-seed)
(hash-set! parent-seed
(string->symbol name)
(car seed))
parent-seed)
#:visit-array-start (lambda (seed)
'())
#:visit-array-end (lambda (seed parent-seed)
`(,(reverse seed) ,@parent-seed))
#:visit-string (lambda (str seed)
`(,str ,@seed))
#:visit-number (lambda (num seed)
`(,num ,@seed))
#:visit-constant (lambda (name seed)
`(,(case name
((true) #t)
((false) #f)
((null) #\null)
(else (error 'json->sjson
"invalid constant ~S"
name)))
,@seed))))
(define (json->sjson in #:exhaust? (exhaust? #t))
(let ((result (%json-parsing:json->sjson:fold in '() exhaust?)))
(if (eof-object? result)
result
(car result))))
(define %json-parsing:json->sxml:fold
(json-fold-lambda
#:error-name 'json->sxml
#:visit-object-start (lambda (seed)
'())
#:visit-object-end (lambda (seed parent-seed)
`((object ,@(reverse seed)) ,@parent-seed))
#:visit-member-start (lambda (name seed)
'())
#:visit-member-end (lambda (name seed parent-seed)
`((member (@ (name ,name)) ,@seed) ,@parent-seed))
#:visit-array-start (lambda (seed)
'())
#:visit-array-end (lambda (seed parent-seed)
`((array ,@(reverse seed)) ,@parent-seed))
#:visit-string (lambda (str seed)
`((string ,str) ,@seed))
#:visit-number (lambda (num seed)
`((number ,(number->string num)) ,@seed))
#:visit-constant (lambda (name seed)
`((,name) ,@seed))))
(define (json->sxml in #:exhaust? (exhaust? #t))
(let ((result (%json-parsing:json->sxml:fold in '() exhaust?)))
(if (eof-object? result)
result
(cons '*TOP* result))))
(define %json-parsing:write-json-as-xml:fold
(lambda (in seed exhaust? out)
((json-fold-lambda
#:error-name 'write-json-as-xml
#:visit-object-start (lambda (seed)
(display "<object>" out)
#t)
#:visit-object-end (lambda (seed parent-seed)
(display "</object>" out)
#t)
#:visit-member-start (lambda (name seed)
(fprintf out "<member name=\"~A\">" name)
#t)
#:visit-member-end (lambda (name seed parent-seed)
(display "</member>" out)
#t)
#:visit-array-start (lambda (seed)
(display "<array>" out)
#t)
#:visit-array-end (lambda (seed parent-seed)
(display "</array>" out)
#t)
#:visit-string (lambda (str seed)
(fprintf out "<string>~A</string>" str)
#t)
#:visit-number (lambda (num seed)
(fprintf out
"<number>~A</number>"
(number->string num))
#t)
#:visit-constant (lambda (name seed)
(fprintf out "<~A/>" name)))
in seed exhaust?)))
(define (write-json-as-xml in
#:exhaust? (exhaust? #t)
#:out (out (current-output-port)))
(or (%json-parsing:write-json-as-xml:fold in #f exhaust? out)
(error 'write-json-as-xml
"no JSON to read"))
(void))
(define (json->xml in #:exhaust? (exhaust? #t))
(let ((out (open-output-string)))
(write-json-as-xml in #:exhaust? exhaust? #:out out)
(get-output-string out)))
(provide
exn:fail:invalid-json-location
exn:fail:invalid-json?
json->sjson
json->sxml
json->xml
make-json-fold
write-json-as-xml)