Initial import of Dave Herman's json library.

This commit has the unmodified original.
This commit is contained in:
Eli Barzilay 2012-03-12 03:44:42 -04:00
parent 0eb5f09e23
commit b35d1b01b2
3 changed files with 318 additions and 0 deletions

3
collects/json/info.rkt Normal file
View File

@ -0,0 +1,3 @@
#lang setup/infotab
(define scribblings '(("json.scrbl" () (parsing-library))))

89
collects/json/json.scrbl Normal file
View File

@ -0,0 +1,89 @@
#lang scribble/doc
@require[scribble/manual
scribble/base
scribble/eval
scribble/bnf
scheme/runtime-path]
@require[(for-syntax scheme/base)]
@require[(for-label scheme/base json)]
@define-runtime-path[home (build-path 'same)]
@define[the-eval
(let ([the-eval (make-base-eval)])
(parameterize ([current-directory home])
(the-eval `(require (file ,(path->string (build-path home "main.ss"))))))
the-eval)]
@title[#:tag "top"]{@bold{JSON}}
by Dave Herman (@tt{dherman at ccs dot neu dot edu})
This library provides utilities for marshalling and unmarshalling data in the JSON data exchange format.
See the @link["http://www.json.org"]{JSON web site} and the @link["http://www.ietf.org/rfc/rfc4627.txt?number=4627"]{JSON RFC}
for more information about JSON.
@defmodule[json]
@section[#:tag "jsexprs"]{JS-Expressions}
This library defines a subset of Scheme values that can be represented as JSON strings.
A @deftech{JS-Expression}, or @deftech{jsexpr}, is one of:
@itemlist[
@item{@schemevalfont{#\null}}
@item{@scheme[boolean?]}
@item{@scheme[string?]}
@item{@scheme[(or integer? inexact-real?)]}
@item{@scheme[(listof jsexpr?)]}
@item{@scheme[(hasheqof symbol? jsexpr?)]}
]
@defproc[(jsexpr? [x any]) boolean?]{
Performs a deep check to determine whether @scheme[x] is a @tech{jsexpr}.}
@defproc[(read-json [in input-port? (current-input-port)]) jsexpr?]{
Reads an immutable @tech{jsexpr} from a JSON-encoded input port @scheme[in].}
@defproc[(write-json [x jsexpr?] [out output-port? (current-output-port)]) any]{
Writes the @tech{jsexpr} @scheme[x], encoded as JSON, to output port @scheme[out].}
@defproc[(jsexpr->json [x jsexpr?]) string?]{
Generates a JSON source string for the @tech{jsexpr} @scheme[x].}
@defproc[(json->jsexpr [s string?]) jsexpr?]{
Parses the JSON string @scheme[s] as an immutable @tech{jsexpr}.}
@;examples[#:eval the-eval 42]
@section[#:tag "rationale"]{A word about design}
Because JSON distinguishes syntactically between @tt{null}, array literals, and object literals,
this library uses non-overlapping datatypes for the three corresponding variants of @tech{jsexpr}.
Since the Scheme null value @scheme['()] overlaps with lists, there is no natural choice for the
@tech{jsexpr} represented as @tt{null}. We prefer @schemevalfont{#\null} as the least objectionable
option from Scheme's host of singleton datatypes (note that the @void-const and @undefined-const
constants do not have @scheme[read]able and @scheme[write]able representations, which makes them
less convenient choices).
The @link["http://www.ietf.org/rfc/rfc4627.txt?number=4627"]{JSON RFC} only states that object
literal expressions "SHOULD" contain unique keys, but does not proscribe them entirely. Looking at
existing practice, it appears that popular JSON libraries parse object literals with duplicate keys
by simply picking one of the key-value pairs and discarding the others with the same key. This
behavior is naturally paralleled by PLT Scheme hash tables, making them a natural analog.
Finally, the @link["http://www.ietf.org/rfc/rfc4627.txt?number=4627"]{JSON RFC} is almost completely
silent about the order of key-value pairs. While the RFC only specifies the syntax of JSON, which of
course always must represent object literals as an ordered collection, the introduction states:
@nested[#:style 'inset]{An object is an unordered collection of zero or more name/value
pairs, where a name is a string and a value is a string, number,
boolean, null, object, or array.}
In practice, JSON libraries discard the order of object literals in parsed JSON text and make no
guarantees about the order of generated object literals. This again makes hash tables a good choice
for representing as JSON object literals.

226
collects/json/main.rkt Normal file
View File

@ -0,0 +1,226 @@
#lang racket/base
#| Based on the PLaneT package by Dave Herman,
Originally released under MIT license.
|#
(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)])
(skip-whitespace 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)
(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) #\-) (list (read-char port)) '())]
[digits (read/digits port)]
[frac (if (eq? (peek-char port) #\.)
(list* (read-char port) (read/digits port))
'())]
[exp (if (memq (peek-char port) '(#\e #\E))
(list* (read-char port) (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)))