Complete rewrite

This commit is contained in:
Daniel Ziltener 2024-09-13 23:59:25 +02:00
parent c3f2662c72
commit cddc76849c
Signed by: zilti
GPG key ID: B38976E82C9DAE42
13 changed files with 1719 additions and 2540 deletions

5
.dir-locals.el Normal file
View file

@ -0,0 +1,5 @@
((nil . ((geiser-default-implementation . chicken)
(geiser-scheme-implementation . chicken)
(geiser-active-implementations . (chicken))
(org-confirm-babel-evaluate . nil)))
(org . ((org-confirm-babel-evaluate . nil))))

1
.envrc Normal file
View file

@ -0,0 +1 @@
use nix -p chicken chickenPackages_5.chickenEggs.apropos chickenPackages_5.chickenEggs.chicken-doc chickenPackages_5.chickenEggs.srfi-1 chickenPackages_5.chickenEggs.srfi-18 chickenPackages_5.chickenEggs.srfi-34 chickenPackages_5.chickenEggs.srfi-35 chickenPackages_5.chickenEggs.srfi-158 chicken chickenPackages_5.chickenEggs.apropos chickenPackages_5.chickenEggs.chicken-doc chickenPackages_5.chickenEggs.srfi-1 chickenPackages_5.chickenEggs.srfi-18 chickenPackages_5.chickenEggs.test

25
LICENSE Normal file
View file

@ -0,0 +1,25 @@
Copyright (C) 2022 Daniel Ziltener
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

216
README.org Normal file
View file

@ -0,0 +1,216 @@
# Created 2024-09-14 Sat 00:15
#+title: SRFI-180
#+author: Daniel Ziltener
#+export_file_name: README.org
#+property: header-args:scheme :session *chicken* :comments none
#+property: header-args:fundamental :eval no
* Dependencies
Main dependencies:
#+name: dependencies
| Egg | Description |
|----------+--------------------|
| srfi-34 | Exception Handling |
| srfi-35 | Exception Types |
| srfi-158 | Generators |
|----------+--------------------|
Test dependencies:
#+name: test-dependencies
| Egg | Description |
|------+--------------------------------|
| test | The de-facto standard test egg |
* API
** Exceptions
This library defines an SRFI-35 exception type ~&json-error~ that gets raised when invalid tokens are encountered. The exception type has a field ~json-invalid-token~ that contains the offending token.
#+begin_src scheme
(define-condition-type &json-error &error
json-error?
(json-error-reason json-error-reason)
(json-invalid-token json-invalid-token))
#+end_src
** Parameters
This library offers the following configuration parameters:
#+name: parameters
| Parameter | Default | Description |
|--------------------------------+---------+-----------------------------------------------------|
| json-nesting-depth-limit | +inf.0 | the maximum nesting depth of JSON that can be read. |
| json-number-of-character-limit | +inf.0 | the maximum length of JSON input that can be read. |
** Predicates
For some reason, this SRFI includes a predicate to check for JSON null values:
#+begin_src scheme
(define (json-null? obj) (eq? obj 'null))
#+end_src
** Reading JSON
*** json-generator
~(json-generator [port-or-generator]) → generator~
Streaming event-based JSON reader. =PORT-OR-GENERATOR= default value is the value returned by =current-input-port=. It must be a textual input port or a generator of characters. =json-generator= returns a generator of Scheme objects, each of which must be one of:
- ~'array-start~ symbol denoting that an array should be constructed.
- ~'array-end~ symbol denoting that the construction of the array for which the last ~'array-start~ was generated and not closed is finished.
- ~'object-start~ symbol denoting that an object should be constructed. The object's key-value pairs are emitted in sequence like those in a property list (plist) where keys are strings. That is, the generation of a key is always followed by the generation of a value. Otherwise, the JSON would be invalid and =json-generator= would raise an error.
- ~'object-end~ symbol denoting that the construction of the object for which the last ~'object-start~ was generated and not closed is finished.
- the symbol ~'null~
- boolean
- number
- string
In the case where nesting of arrays or objects reaches the value returned by the parameter =json-nesting-depth-limit=, the generator must raise an object that satisfies the predicate =json-error?=.
In cases where the JSON is invalid, the generator returned by =json-generator= should raise an object that satisfies the predicate =json-error?=.
Otherwise, if =PORT-OR-GENERATOR= contains valid JSON text, the generator returned by =json-generator= must yield an end-of-file object in two situations:
- The first time the generator returned by =json-generator= is called, it returns an object that is a boolean, a number, a string or the symbol ='null=.
- The first time the generator returned by =json-generator= is called, it returns a symbol that is not the symbol ='null=. When the underlying JSON text is valid, it should be the symbol starting a structure: ='object-start= or ='array-start=. The end-of-file object is generated when that structure is finished.
In other words, the generator returned by =json-generator= will parse at most one JSON value or one top-level structure. If =PORT= is not finished, as in the case of JSON lines, the user should call =json-generator= again with the same =PORT-OR-GENERATOR=.
**** Examples
#+begin_src scheme
(call-with-input-string "42 101 1337" (lambda (port) (generator->list (json-generator port))))
#+end_src
#+results:
#+begin_src scheme
(42)
#+end_src
#+begin_src scheme
(call-with-input-string "[42] 101 1337" (lambda (port) (generator->list (json-generator port))))
#+end_src
#+results:
#+begin_src scheme
(array-start 42 array-end)
#+end_src
*** json-fold
~(json-fold proc array-start array-end object-start object-end seed [port-or-generator])~
Fundamental JSON iterator.
=json-fold= will read the JSON text from =PORT-OR-GENERATOR=, which has ~(current-input-port)~ as its default value. =json-fold= will call the procedures passed as argument:
- ~(PROC obj seed)~ is called when a JSON value is generated or a complete JSON structure is read. =PROC= should return the new seed that will be used to iterate over the rest of the generator. Termination is described below.
- ~(OBJECT-START seed)~ is called with a seed and should return a seed that will be used as the seed of the iteration over the key and values of that object.
- ~(OBJECT-END seed)~ is called with a seed and should return a new seed that is the result of the iteration over a JSON object.
=ARRAY-START= and =ARRAY-END= take the same arguments, and have similar behavior, but are called for iterating on JSON arrays.
=json-fold= must return the seed when:
- =PORT-OR-GENERATOR= yields an object that satisfies the predicate =eof-object?=
- All structures, array or object, that were started have ended. The returned object is ~(PROC obj SEED)~ where obj is the object returned by =ARRAY-END= or =OBJECT-END=
*** json-read
~(json-read [port-or-generator]) → object~
JSON reader procedure. =PORT-OR-GENERATOR= must be a textual input port or a generator of characters. The default value of =PORT-OR-GENERATOR= is the value returned by the procedure =current-input-port=. The returned value is a Scheme object. =json-read= must return only the first toplevel JSON value or structure. When there are multiple toplevel values or structures in =PORT-OR-GENERATOR=, the user should call =json-read= several times to read all of it.
The mapping between JSON types and Scheme objects is the following:
- =null= → the symbol ='null=
- =true==#t=
- =false==#f=
- =number= → number
- =string= → string
- =array= → vector
- =object= → association list with keys that are symbols
In the case where nesting of arrays or objects reaches the value returned by the parameter =json-nesting-depth-limit=, =json-read= must raise an object that satisfies the predicate =json-error?=
*** json-lines-read
~(json-lines-read [port-or-generator]) → generator~
JSON reader of jsonlines or ndjson. As its first and only argument, it takes a generator of characters or a textual input port whose default value is the value returned by =current-input-port=. It will return a generator of Scheme objects as specified in =json-read=.
*** json-sequence-read
~(json-sequence-read [port-or-generator]) → generator~
JSON reader of JSON Text Sequences (RFC 7464). As its first and only argument, it takes a generator of characters or a textual input port whose default value is the value returned by =current-input-port=. It will return a generator of Scheme objects as specified in =json-read=.
*** json-accumulator
~(json-accumulator port-or-accumulator) → procedure~
Streaming event-based JSON writer. =PORT-OR-ACCUMULATOR= must be a textual output port or an accumulator that accepts characters and strings. It returns an accumulator procedure that accepts Scheme objects as its first and only argument and that follows the same protocol as described in =json-generator=. Any deviation from the protocol must raise an error that satisfies =json-error?=. In particular, objects and arrays must be properly nested.
Mind the fact that most JSON parsers have a nesting limit that is not documented by the standard. Even if you can produce arbitrarily nested JSON with this library, you might not be able to read it with another library.
*** json-write
~(json-write obj [port-or-accumulator]) → unspecified~
JSON writer procedure. =PORT-OR-ACCUMULATOR= must be a textual output port, or an accumulator that accepts characters and strings. The default value of =PORT-OR-ACCUMULATOR= is the value returned by the procedure =current-output-port=. The value returned by =json-write= is unspecified.
=json-write= will validate that =OBJ= can be serialized into JSON before writing to =PORT=. An error that satisfies =json-error?= is raised in the case where =OBJ= is not an object or a composition of the following types:
- symbol ='null=
- boolean
- number. Must be integers or inexact rationals. (That is, they must not be complex, infinite, NaN, or exact rationals that are not integers.)
- string
- vector
- association list with keys as symbols
* About this egg
** Source
The source is available at [[https://gitea.lyrion.ch/Chicken/srfi-180]].
** Author
Daniel Ziltener
** Version History
#+name: version-history
| 1.5 | Reimplementation |
| 1.0 | Reference Implementation |
* License
#+begin_src fundamental
Copyright (C) 2022 Daniel Ziltener
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
,* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
,* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
,* Neither the name of the <organization> nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#+end_src

View file

@ -1,19 +1,11 @@
;;; -*- scheme -*-
((author "Amirouche Boubekki")
(synopsis "This library describes a JavaScript Object Notation (JSON) parser and printer. It supports JSON that may be bigger than memory.")
;; -*- Scheme -*-
((author "Daniel Ziltener")
(synopsis "A JSON parser and printer that supports JSON bigger than memory.")
(category parsing)
(license "MIT")
(version "1.0.0")
(dependencies r7rs srfi-60 srfi-145)
(build-dependencies srfi-121)
(license "BSD")
(version "1.5.0")
(dependencies srfi-34 srfi-35 srfi-158)
(test-dependencies test)
(components
(extension srfi.180.helpers
(csc-options "-X" "r7rs" "-R" "r7rs"))
(extension srfi-180
(component-dependencies srfi.180.helpers)
(csc-options "-X" "r7rs" "-R" "r7rs"))
(extension srfi.180.checks
(component-dependencies srfi-180)
(csc-options "-X" "r7rs" "-R" "r7rs"))
))
(extension srfi-180
(csc-options "-sJ"))))

297
srfi-180.impl.scm Normal file
View file

@ -0,0 +1,297 @@
(import
(scheme)
(chicken format)
(chicken port)
(chicken string)
(srfi-34) ;;Exception Handling
(srfi-35) ;;Exception Types
(srfi-158) ;;Generators
)
(define-condition-type &json-error &error
json-error?
(json-error-reason json-error-reason)
(json-invalid-token json-invalid-token))
(define json-nesting-depth-limit (make-parameter +inf.0)) ;; the maximum nesting depth of JSON that can be read.
(define json-number-of-character-limit (make-parameter +inf.0)) ;; the maximum length of JSON input that can be read.
(define (json-null? obj) (eq? obj 'null))
(define (is-array-start? c)
(char=? #\[ c))
(define (is-array-end? c)
(char=? #\] c))
(define (is-object-start? c)
(char=? #\{ c))
(define (is-object-end? c)
(char=? #\} c))
(define (is-number-start? c)
(or (char-numeric? c)
(char=? #\+ c)
(char=? #\- c)))
(define (is-string-start? c)
(char=? #\" c))
(define (is-null-start? c)
(char=? #\n c))
(define (is-bool-start? c)
(or (char=? #\t c)
(char=? #\f c)))
(define (is-whitespace? c)
(or (char-whitespace? c)
(char=? #\, c)
(char=? #\: c)))
(define (is-delimiter? x)
(or (eof-object? x)
(is-whitespace? x)
(is-array-start? x)
(is-array-end? x)
(is-object-start? x)
(is-object-end? x)))
(define (determine-reader-proc peek-char)
(cond
((is-array-start? peek-char) read-array-start)
((is-array-end? peek-char) read-array-end)
((is-object-start? peek-char) read-object-start)
((is-object-end? peek-char) read-object-end)
((is-null-start? peek-char) read-null-sym)
((is-bool-start? peek-char) read-boolean)
((is-number-start? peek-char) read-number)
((is-string-start? peek-char) read-string)
((is-whitespace? peek-char) read-whitespace)
(else (raise (make-condition &json-error 'json-error-reason "Invalid token" 'json-invalid-token peek-char)))))
(define (json-generator #!optional (port-or-generator (current-input-port)))
(let* ((input-generator (if (procedure? port-or-generator)
port-or-generator
(lambda () (read-char port-or-generator))))
(nesting-limit (json-nesting-depth-limit))
(character-limit (json-number-of-character-limit)))
(make-coroutine-generator
(lambda (yield)
(let loop ((next-char (input-generator))
(json-nesting-depth #f)
(json-number-of-characters 0))
(cond
((> (or json-nesting-depth 0) nesting-limit)
(raise (make-condition &json-error
'json-error-reason "Nesting depth exceeded"
'json-invalid-token next-char)))
((> json-number-of-characters character-limit)
(raise (make-condition &json-error
'json-error-reason "Character limit exceeded"
'json-invalid-token next-char)))
((and (eof-object? next-char)
(< 0 json-nesting-depth))
(raise (make-condition &json-error
'json-error-reason "Unfinished JSON expression"
'json-invalid-token next-char)))
((or (eof-object? next-char)
(eq? 0 json-nesting-depth))
#!eof)
(else
(let-values (((token next-char* new-charcount nesting-delta)
((determine-reader-proc next-char)
json-number-of-characters next-char input-generator)))
(if (not (eq? '() token))
(yield token))
(loop next-char* (+ (or json-nesting-depth 0) nesting-delta) new-charcount)))))))))
(define (read-whitespace charcount next-char input-proc)
(values '() (input-proc) (+ charcount 1) 0))
(define (read-array-start charcount next-char input-proc)
(values 'array-start (input-proc) (+ charcount 1) +1))
(define (read-array-end charcount next-char input-proc)
(values 'array-end (input-proc) (+ charcount 1) -1))
(define (read-object-start charcount next-char input-proc)
(values 'object-start (input-proc) (+ charcount 1) +1))
(define (read-object-end charcount next-char input-proc)
(values 'object-end (input-proc) (+ charcount 1) -1))
(define (read-null-sym charcount next-char input-proc)
(if (not (is-delimiter? next-char))
(read-null-sym (+ charcount 1) (input-proc) input-proc)
(values 'null next-char charcount 0)))
(define (read-boolean charcount next-char input-proc #!optional (accu '()))
(set! accu (cons next-char accu))
(let ((accu-str (reverse-list->string accu)))
(cond
((string=? "true" accu-str) (values #t (input-proc) (+ charcount 1) 0))
((string=? "false" accu-str) (values #f (input-proc) (+ charcount 1) 0))
(else (let ((next-char* (input-proc)))
(if (is-delimiter? next-char*)
(values accu next-char* charcount 0) ;; TODO: Throw error instead
(read-boolean (+ charcount 1) next-char* input-proc accu)))))))
(define (read-number charcount next-char input-proc #!optional (accu '()))
(set! accu (cons next-char accu))
(let ((next-char* (input-proc)))
(if (is-delimiter? next-char*)
(values (string->number (reverse-list->string accu))
next-char* (+ charcount 1) 0)
(read-number (+ charcount 1) next-char* input-proc accu))))
(define (read-string charcount next-char input-proc #!optional (beginning? #t) (accu '()) (esc? #f))
(cond
(beginning?
(read-string (+ charcount 1)
(input-proc)
input-proc
#f '() #f))
((and (not esc?) (char=? next-char #\"))
(values (reverse-list->string accu)
(input-proc) (+ charcount 1) 0))
(else (read-string (+ charcount 1)
(input-proc) input-proc
#f (cons next-char accu)
(and (not esc?) (char=? next-char #\\))))))
(define-record json-foldstate mode cache accumulator)
(define (json-proc obj foldstate)
(if (json-foldstate? foldstate)
(case (json-foldstate-mode foldstate)
((%array) (begin
(json-foldstate-accumulator-set!
foldstate
(cons obj (json-foldstate-accumulator foldstate)))
foldstate))
((%object) (begin
(if (equal? '() (json-foldstate-cache foldstate))
(begin
(json-foldstate-cache-set! foldstate obj))
(begin
(json-foldstate-accumulator-set!
foldstate
(cons (cons (json-foldstate-cache foldstate) obj)
(json-foldstate-accumulator foldstate)))
(json-foldstate-cache-set! foldstate '())))
foldstate)))
obj))
(define (object-start seed)
(make-json-foldstate '%object '() '()))
(define (object-end seed)
(reverse (json-foldstate-accumulator seed)))
(define (array-start seed)
(make-json-foldstate '%array '() '()))
(define (array-end seed)
(list->vector (reverse (json-foldstate-accumulator seed))))
(define (json-fold proc array-start array-end object-start object-end seed #!optional (port-or-generator (current-input-port)))
(let ((generator (json-generator port-or-generator)))
(let recurse ((seed seed)
(jump #f))
(generator-fold
(lambda (token seed)
(case token
((array-start) (proc
(call-with-current-continuation
(lambda (jump)
(recurse (array-start seed) jump)))
seed))
((array-end) (if jump
(jump (array-end seed))
(array-end seed)))
((object-start) (proc
(call-with-current-continuation
(lambda (jump)
(recurse (object-start seed) jump)))
seed))
((object-end) (if jump
(jump (object-end seed))
(object-end seed)))
(else (proc token seed))))
seed generator))))
(define (json-read #!optional (port-or-generator (current-input-port)))
(json-fold json-proc array-start array-end object-start object-end '() port-or-generator))
(define json-lines-read json-read)
(define json-sequence-read json-read)
(define (accumulate-boolean accumulator bool)
(if bool (accumulator 'true) (accumulator 'false)))
(define (accumulate-null accumulator)
(accumulator 'null))
(define (accumulate-number accumulator num)
(accumulator num))
(define (accumulate-string accumulator str)
(accumulator str))
(define (accumulate-vector accumulator vec)
(accumulator #\[)
(let ((max-index (- (vector-length vec) 1)))
(let loop ((index 0))
(accumulate-dispatch accumulator
(vector-ref vec index))
(if (< index max-index)
(begin (accumulator #\,) (accumulator #\space)
(loop (+ index 1))))))
(accumulator #\]))
(define (accumulate-alist accumulator alist)
(accumulator #\{)
(let loop ((alist alist))
(let ((kv-pair (car alist)))
(if (not (pair? kv-pair))
(raise (make-condition &json-error
'json-error-reason "Unbalanced alist"
'json-invalid-token kv-pair)))
(accumulate-dispatch accumulator
(symbol->string (car kv-pair)))
(accumulator #\:) (accumulator #\space)
(accumulate-dispatch accumulator (cdr kv-pair))
(if (not (eq? '() (cdr alist)))
(begin
(accumulator #\,) (accumulator #\space)
(loop (cdr alist))))))
(accumulator #\}))
(define (accumulate-dispatch accumulator obj)
(cond
((number? obj) (accumulate-number accumulator obj))
((string? obj) (accumulate-string accumulator obj))
((boolean? obj) (accumulate-boolean accumulator obj))
((eq? 'null obj) (accumulate-null accumulator))
((vector? obj) (accumulate-vector accumulator obj))
((list? obj) (accumulate-alist accumulator obj))))
(define (json-accumulator #!optional (port-or-accumulator (current-output-port)))
(let ((accumulator (if (procedure? port-or-accumulator)
port-or-accumulator
(lambda (txt)
(if (char? txt)
(display txt port-or-accumulator)
(write txt port-or-accumulator)))))
(leading-space? #f))
(lambda (obj)
(if leading-space? (accumulator #\space) (set! leading-space? #t))
(accumulate-dispatch accumulator obj))))
(define (json-write obj #!optional (port-or-accumulator (current-output-port)))
(let ((black-hole (make-output-port (lambda (poor-soul) #t) (lambda () #t))))
((json-accumulator black-hole) obj))
((json-accumulator port-or-accumulator) obj))

1015
srfi-180.org Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
(repo git "https://gitea.lyrion.ch/zilti/srfi-180.git")
(uri targz "https://gitea.lyrion.ch/zilti/srfi-180/archive/{egg-release}.tar.gz")
(release "1.0.0")
;; -*- Scheme -*-
(repo git "https://gitea.lyrion.ch/Chicken/srfi-180.git")
(uri targz "https://gitea.lyrion.ch/Chicken/srfi-180/archive/{egg-release}.tar.gz")
(release "1.0.0") ;; Reference Implementation

View file

@ -1,24 +1,17 @@
(import (r7rs))
(define-library (srfi 180)
(import (scheme base)
(scheme inexact)
(scheme case-lambda)
(scheme char)
(scheme write)
(srfi 180 helpers)
(srfi 145)
(only (srfi 60) arithmetic-shift bitwise-ior))
(export json-number-of-character-limit
json-nesting-depth-limit
json-null?
json-error?
json-error-reason
json-fold
json-generator
json-read
json-lines-read
json-sequence-read
json-accumulator
json-write)
(include "srfi.180-impl.scm"))
(module (srfi 180)
(&json-error
json-error?
json-error-reason
json-invalid-token
json-nesting-depth-limit
json-number-of-character-limit
json-generator
json-null?
json-fold
json-read
json-lines-read
json-sequence-read
json-accumulator
json-write)
(import (chicken base))
(include-relative "srfi-180.impl.scm"))

View file

@ -1,750 +0,0 @@
(define (pk . args)
(write args)
(newline)
(car (reverse args)))
(define json-number-of-character-limit (make-parameter +inf.0))
(define json-nesting-depth-limit (make-parameter +inf.0))
(define (json-null? obj)
(eq? obj 'null))
(define-record-type <json-error>
(make-json-error reason)
json-error?
(reason json-error-reason))
(define (json-whitespace? char)
(case char
((#\x20 ; Space
#\x09 ; Horizontal tab
#\x0A ; Line feed or New line
#\x0D
#\x1E ;; Record Separator
)
#t)
(else #f)))
(define (expect value other)
(when (eof-object? value)
(raise (make-json-error "Unexpected end-of-file.")))
(assume (and (char? value) (char? other)) "invalid argument" '%json-tokens expect value other)
(unless (char=? value other)
(raise (make-json-error "Unexpected character."))))
(define (port->generator port)
(let ((count 0)
(limit (json-number-of-character-limit)))
(lambda ()
(let ((out (guard (ex ((read-error? ex) (raise (make-json-error "Read error!"))))
(read-char port))))
(if (= count limit)
(raise (make-json-error "Maximum number of character reached."))
(begin
(set! count (+ count 1))
out))))))
(define (gcons head generator)
;; returns a generator that will yield, HEAD the first time, and
;; after than, it will yield items from GENERATOR.
(let ((head? #t))
(lambda ()
(if head?
(begin (set! head? #f) head)
(generator)))))
(define (%json-tokens generator)
(define (maybe-ignore-whitespace generator)
(let loop ((char (generator)))
(if (json-whitespace? char)
(loop (generator))
char)))
(define (expect-null generator)
(expect (generator) #\u)
(expect (generator) #\l)
(expect (generator) #\l))
(define (expect-true generator)
(expect (generator) #\r)
(expect (generator) #\u)
(expect (generator) #\e))
(define (expect-false generator)
(expect (generator) #\a)
(expect (generator) #\l)
(expect (generator) #\s)
(expect (generator) #\e))
(define (maybe-char generator)
(let ((char (generator)))
(when (eof-object? char)
(raise (make-json-error "Unexpected end-of-file.")))
(when (char=? char #\")
(raise (make-json-error "Unexpected end of string.")))
char))
(define (read-unicode-escape generator)
(let* ((one (maybe-char generator))
(two (maybe-char generator))
(three (maybe-char generator))
(four (maybe-char generator)))
(let ((out (string->number (list->string (list one two three four)) 16)))
(if out
out
(raise (make-json-error "Invalid code point."))))))
(define ash arithmetic-shift)
(define (read-json-string generator)
(let loop ((char (generator))
(out '()))
(when (eof-object? char)
(raise (make-json-error "Unexpected end of file.")))
(when (or (char=? char #\x00)
(char=? char #\newline)
(char=? char #\tab))
(raise (make-json-error "Unescaped control char.")))
;; XXX: Here be dragons.
(if (char=? char #\\)
(begin
(let loop-unescape ((char (generator))
(chars-unescaped '()))
(case char
((#\" #\\ #\/) (loop (generator)
(cons char (append chars-unescaped
out))))
((#\b) (loop (generator) (cons #\backspace
(append chars-unescaped
out))))
((#\f) (loop (generator) (cons #\x0C
(append chars-unescaped
out))))
((#\n) (loop (generator) (cons #\newline
(append chars-unescaped
out))))
((#\r) (loop (generator) (cons #\x0D
(append chars-unescaped
out))))
((#\t) (loop (generator) (cons #\tab
(append chars-unescaped
out))))
((#\u) (let loop-unicode ((code1 (read-unicode-escape generator))
(chars chars-unescaped))
(let ((next-char (generator)))
(if (and (<= #xd800 code1 #xdbff)
(char=? next-char #\\))
(if (char=? (generator) #\u)
(let ((code2 (read-unicode-escape generator)))
(if (<= #xdc00 code2 #xdfff)
(let ((integer
(+ #x10000 (bitwise-ior
(ash (- code1 #xd800) 10)
(- code2 #xdc00)))))
;; full escape of unicode is parsed...
(loop (generator)
(cons (integer->char integer)
(append chars
out))))
;; This is another unicode char
(loop-unicode (read-unicode-escape generator)
(cons (integer->char code1) chars))))
;; The escaped unicode char is
;; parsed, need to parse another
;; escape that is not a unicode
;; escape sequence
(loop-unescape char (cons (integer->char code1)
chars)))
;; This is not a big-ish unicode char and
;; the next thing is some other char.
(loop next-char
(cons (integer->char code1) (append chars out)))))))
(else (raise (make-json-error "Unexpected escaped sequence."))))))
(cond
((char=? char #\")
(list->string (reverse out)))
(else
(loop (generator) (cons char out)))))))
(define (maybe-read-number generator)
;; accumulate chars until a control char or whitespace is reached,
;; validate that it is JSON number, then intrepret it as Scheme
;; number using string->number
(let loop ((char (generator))
(out '()))
(if (or (eof-object? char)
(json-whitespace? char)
(char=? char #\,)
(char=? char #\])
(char=? char #\}))
(let ((string (list->string (reverse out))))
(if (valid-number? string)
(let ((number (string->number string)))
(if number
(values number char)
(raise (make-json-error "Invalid number."))))
(raise (make-json-error "Invalid number."))))
(loop (generator) (cons char out)))))
;; gist
(assume (procedure? generator) "invalid argument" generator)
(let ((char (generator)))
(if (eof-object? char)
eof-object ;; return an empty generator
(begin
(unless (char=? char #\xFEFF)
;; if it is not a UTF-8 BOM, put back the char in front of
;; the generator
(set! generator (gcons char generator)))
(lambda ()
(define char (maybe-ignore-whitespace generator))
(if (eof-object? char)
char ;; return that eof-object
(case char
((#\n) (expect-null generator) 'null)
((#\t) (expect-true generator) #t)
((#\f) (expect-false generator) #f)
((#\:) 'colon)
((#\,) 'comma)
((#\[) 'array-start)
((#\]) 'array-end)
((#\{) 'object-start)
((#\}) 'object-end)
((#\") (read-json-string generator))
(else
(call-with-values (lambda () (maybe-read-number (gcons char generator)))
(lambda (number next)
(set! generator (gcons next generator))
number))))))))))
(define json-tokens
(case-lambda
(() (json-tokens (current-input-port)))
((port-or-generator)
(cond
((procedure? port-or-generator)
(%json-tokens port-or-generator))
((and (textual-port? port-or-generator) (input-port? port-or-generator))
(%json-generator (port->generator port-or-generator)))
(else (error 'json "json-tokens error, argument is not valid" port-or-generator))))))
(define (%json-generator tokens)
(define limit (json-nesting-depth-limit))
(define count 0)
(define (handle-limit!)
(if (= count limit)
(raise (make-json-error "Maximum JSON nesting depth reached"))
(set! count (+ count 1))))
(define (array-maybe-continue tokens k)
(lambda ()
(let ((token (tokens)))
(case token
((comma) (start tokens (array-maybe-continue tokens k)))
((array-end) (values 'array-end k))
(else (raise (make-json-error "Invalid array, expected comma or array close.")))))))
(define (array-start tokens k)
(lambda ()
(handle-limit!)
(let ((token (tokens)))
(if (eq? token 'array-end)
(values 'array-end k)
(start (gcons token tokens) (array-maybe-continue tokens k))))))
(define (object-maybe-continue tokens k)
(lambda ()
(let ((token (tokens)))
(case token
((object-end) (values 'object-end k))
((comma) (let ((token (tokens)))
(unless (string? token)
(raise (make-json-error "Invalid object, expected an object key")))
(values token
(object-colon tokens k))))
(else (raise (make-json-error "Invalid object, expected comma or object close.")))))))
(define (object-colon tokens k)
(lambda ()
(let ((token (tokens)))
(if (eq? token 'colon)
(let ((token (tokens)))
(if (eof-object? token)
(raise (make-json-error "Invalid object, expected object value."))
(start (gcons token tokens) (object-maybe-continue tokens k))))
(raise (make-json-error "Invalid object, expected colon."))))))
(define (object-start tokens k)
(lambda ()
(handle-limit!)
(let ((token (tokens)))
(cond
((eq? token 'object-end) (values 'object-end k))
((string? token)
(values token
(object-colon tokens k)))
(else (raise (make-json-error "Invalid object, expected object key or object close.")))))))
(define (start tokens k)
(let ((token (tokens)))
(if (eof-object? token)
(values token k)
(cond
((or (json-null? token)
(number? token)
(string? token)
(boolean? token))
(values token k))
((eq? token 'array-start)
(values 'array-start (array-start tokens k)))
((eq? token 'object-start)
(values 'object-start (object-start tokens k)))
(else (raise (make-json-error "Is it JSON text?!")))))))
(define (end-of-top-level-value)
;; json-generator returns a generator that reads one top-level
;; json. If there is more than one top-level json value in the
;; generator separated with space as it is the case of json-lines,
;; you need to call json-generator with the same port or
;; generator.
(values (eof-object) #f))
(define (make-trampoline-generator tokens)
(let ((continuation (lambda () (start tokens end-of-top-level-value))))
(lambda ()
(when continuation
(call-with-values continuation
(lambda (event new-continuation)
(set! continuation new-continuation)
event))))))
;; gist
(assume (procedure? tokens) "invalid argument" %json-generator tokens)
(make-trampoline-generator tokens))
(define json-generator-error
"Argument does not look like a generator and is not a textual input port.")
(define json-generator
(case-lambda
(() (json-generator (current-input-port)))
((port)
(%json-generator (json-tokens (port->generator port))))))
;; XXX: procedure foldts is not used as-is. It was copied here for
;; documentation purpose (public domain, by Oleg Kiselyov).
(define (foldts fdown fup fhere seed tree)
;; - fhere is applied to the leafs of the tree
;;
;; - fdown is invoked when a non-leaf node is entered before any of
;; the node's children are visited. fdown action has to generate a
;; seed to be passed to the first visited child of the node.
;;
;; - fup is invoked after all the children of a node have been
;; seen. The first argument is the local state at the moment the
;; traversal process enters the branch rooted at the current node. The
;; second argument is the result of visiting all child branches. The
;; action of fup isto produce a seed that is taken to be the state of
;; the traversal after the process leave the currents the current
;; branch.
(cond
((null? tree) seed)
((not (pair? tree)) ; An atom
(fhere seed tree))
(else
(let loop ((kid-seed (fdown seed tree))
(kids (cdr tree)))
(if (null? kids)
(fup seed kid-seed tree)
(loop (foldts fdown fup fhere kid-seed (car kids))
(cdr kids)))))))
(define (%json-fold proc array-start array-end object-start object-end seed port-or-generator)
;; json-fold is inspired from the above foldts definition, unlike
;; the above definition, it is continuation-passing-style. fhere is
;; renamed PROC. Unlike foldts, json-fold will call (proc obj seed)
;; everytime a JSON value or complete structure is read from the
;; EVENTS generator, where OBJ will be: a) In the case of
;; structures, the the result of the recursive call or b) a JSON
;; value.
;; json-fold will terminates in three cases:
;;
;; - eof-object was generated, return the seed.
;;
;; - event-type 'array-end is generated, if EVENTS is returned by
;; json-generator, it means a complete array was read.
;;
;; - event-type 'object-end is generated, similarly, if EVENTS is
;; returned by json-generator, it means complete array was
;; read.
;;
;; IF EVENTS does not follow the json-generator protocol, the
;; behavior is unspecified.
(define events (json-generator port-or-generator))
(define (ruse seed k)
(lambda ()
(let loop ((seed seed))
(let ((event (events)))
(if (eof-object? event)
(begin (k seed) #f)
(case event
;; termination cases
((array-end) (k seed))
((object-end) (k seed))
;; recursion
((array-start) (ruse (array-start seed)
(lambda (out) (loop (proc (array-end out) seed)))))
((object-start) (ruse (object-start seed)
(lambda (out) (loop (proc (object-end out) seed)))))
(else (loop (proc event seed)))))))))
(define (make-trampoline-fold k)
(let ((thunk (ruse seed k)))
(let loop ((thunk thunk))
(when thunk
(loop (thunk))))))
(define %unset '(unset))
(let ((out %unset))
(define (escape out*)
(set! out out*)
#f)
(make-trampoline-fold escape)
(if (eq? out %unset)
(error 'json "Is this JSON text")
out)))
(define json-fold
(case-lambda
((proc array-start array-end object-start object-end seed)
(json-fold proc array-start array-end object-start object-end seed (current-input-port)))
((proc array-start array-end object-start object-end seed port-or-generator)
(%json-fold proc array-start array-end object-start object-end seed port-or-generator))))
(define (%json-read port-or-generator)
(define %root '(root))
(define (array-start seed)
;; array will be read as a list, then converted into a vector in
;; array-end.
'())
(define (array-end items)
(list->vector (reverse items)))
(define (object-start seed)
;; object will be read as a property list, then converted into an
;; alist in object-end.
'())
(define (plist->alist plist)
;; PLIST is a list of even items, otherwise json-generator
;; would have raised a json-error.
(let loop ((plist plist)
(out '()))
(if (null? plist)
out
(loop (cddr plist) (cons (cons (string->symbol (cadr plist)) (car plist)) out)))))
(define object-end plist->alist)
(define (proc obj seed)
;; proc is called when a JSON value or structure was completly
;; read. The parse result is passed as OBJ. In the case where
;; what is parsed is a JSON simple json value then OBJ is simply
;; the token that is read that can be 'null, a number or a string.
;; In the case where what is parsed is a JSON structure, OBJ is
;; what is returned by OBJECT-END or ARRAY-END.
(if (eq? seed %root)
;; It is toplevel, a complete JSON value or structure was read,
;; return it.
obj
;; This is not toplevel, hence json-fold is called recursivly,
;; to parse an array or object. Both ARRAY-START and
;; OBJECT-START return an empty list as a seed to serve as an
;; accumulator. Both OBJECT-END and ARRAY-END expect a list
;; as argument.
(cons obj seed)))
(let ((out (json-fold proc
array-start
array-end
object-start
object-end
%root
port-or-generator)))
;; if out is the root object, then the port or generator is empty.
(if (eq? out %root)
(eof-object)
out)))
(define json-read
(case-lambda
(() (json-read (current-input-port)))
((port-or-generator) (%json-read port-or-generator))))
;; json-lines-read
(define json-lines-read
(case-lambda
(() (json-lines-read (current-input-port)))
((port-or-generator)
(lambda ()
(json-read port-or-generator)))))
;; json-sequence-read
(define json-sequence-read
(case-lambda
(() (json-sequence-read (current-input-port)))
((port-or-generator)
(lambda ()
(let loop ()
(guard (ex ((json-error? ex) (loop)))
(json-read port-or-generator)))))))
;; write procedures
(define (json-accumulator accumulator)
(define (write-json-char char accumulator)
(case char
((#\x00) (accumulator "\\u0000"))
((#\") (accumulator "\\\""))
((#\\) (accumulator "\\\\"))
((#\/) (accumulator "\\/"))
((#\return) (accumulator "\\r"))
((#\newline) (accumulator "\\n"))
((#\tab) (accumulator "\\t"))
((#\backspace) (accumulator "\\b"))
((#\x0c) (accumulator "\\f"))
(else (accumulator char))))
(define (write-json-string string accumulator)
(accumulator #\")
(string-for-each
(lambda (char) (write-json-char char accumulator))
string)
(accumulator #\"))
(define (write-json-value obj accumulator)
(cond
((eq? obj 'null) (accumulator "null"))
((boolean? obj) (if obj
(accumulator "true")
(accumulator "false")))
((string? obj) (write-json-string obj accumulator))
((number? obj) (accumulator (number->string obj)))
(else (raise (make-json-error "Invalid json value.")))))
(define (raise-invalid-event event)
(raise event))
;;(raise (make-json-error "json-accumulator: invalid event.")))
(define (object-start k)
(lambda (accumulator event)
(accumulator #\{)
(case (car event)
((json-value)
(let ((key (cdr event)))
(unless (symbol? key) (raise-invalid-event event))
(write-json-string (symbol->string key) accumulator)
(object-value k)))
((json-structure)
(case (cdr event)
((object-end)
(accumulator #\})
k)
(else (raise-invalid-event event))))
(else (raise-invalid-event event)))))
(define (object-value k)
(lambda (accumulator event)
(accumulator #\:)
(case (car event)
((json-value)
(write-json-value (cdr event) accumulator)
(object-maybe-continue k))
((json-structure)
(case (cdr event)
((array-start)
(array-start (object-maybe-continue k)))
((object-start)
(object-start (object-maybe-continue k)))
(else (raise-invalid-event event))))
(else (raise-invalid-event event)))))
(define (object-maybe-continue k)
(lambda (accumulator event)
(case (car event)
((json-value)
(accumulator #\,)
(let ((key (cdr event)))
(unless (symbol? key) (raise-invalid-event event))
(write-json-value (symbol->string key) accumulator)
(object-value k)))
((json-structure)
(case (cdr event)
((object-end)
(accumulator #\})
k)
(else (raise-invalid-event event))))
(else (raise-invalid-event event)))))
(define (array-start k)
(lambda (accumulator event)
(accumulator #\[)
(case (car event)
((json-value)
(write-json-value (cdr event) accumulator)
(array-maybe-continue k))
((json-structure)
(case (cdr event)
((array-end)
(accumulator #\])
k)
((array-start) (array-start (array-maybe-continue k)))
((object-start) (object-start (array-maybe-continue k)))
(else (raise-invalid-event event))))
(else (raise-invalid-event event)))))
(define (array-maybe-continue k)
(lambda (accumulator event)
(case (car event)
((json-value)
(accumulator #\,)
(write-json-value (cdr event) accumulator)
(array-maybe-continue k))
((json-structure)
(case (cdr event)
((array-end)
(accumulator #\])
k)
((array-start)
(accumulator #\,)
(array-start (array-maybe-continue k)))
((object-start)
(accumulator #\,)
(object-start (array-maybe-continue k)))
(else (raise-invalid-event event))))
(else (raise-invalid-event event)))))
(define (start accumulator event)
(case (car event)
((json-value)
(write-json-value (cdr event) accumulator)
raise-invalid-event)
((json-structure)
(case (cdr event)
((array-start)
(array-start raise-invalid-event))
((object-start)
(object-start raise-invalid-event))
(else (raise-invalid-event event))))
(else (raise-invalid-event event))))
(assume (procedure? accumulator)
"ACCUMULATOR does look like a valid accumulator.")
(let ((k start))
(lambda (event)
(set! k (k accumulator event)))))
(define (%json-write obj accumulator)
(define (void)
(if #f #f))
(define (raise-unless-valid? obj)
(cond
((null? obj) (void))
((eq? obj 'null) (void))
((boolean? obj) (void))
((string? obj) (void))
((and (number? obj)
(not (infinite? obj))
(not (nan? obj))
(real? obj)
(or (and (exact? obj) (= (denominator obj) 1))
(inexact? obj)))
(void))
((vector? obj)
(vector-for-each (lambda (obj) (raise-unless-valid? obj)) obj))
;; XXX: use pair? then recursively check the tail.
((pair? obj)
(for-each (lambda (obj)
(unless (pair? obj)
(raise (make-json-error "Unexpected object, not a pair.")))
(unless (symbol? (car obj))
(raise (make-json-error "Unexpected object, not a symbol key.")))
(raise-unless-valid? (cdr obj)))
obj))
(else (raise (make-json-error "Unexpected object")))))
(define (write obj accumulator)
(cond
((or (eq? obj 'null)
(boolean? obj)
(string? obj)
(symbol? obj)
(number? obj))
(accumulator (cons 'json-value obj)))
((vector? obj)
(accumulator '(json-structure . array-start))
(vector-for-each (lambda (obj) (write obj accumulator)) obj)
(accumulator '(json-structure . array-end)))
((null? obj)
(accumulator '(json-structure . object-start))
(accumulator '(json-structure . object-end)))
((pair? obj)
(accumulator '(json-structure . object-start))
(for-each (lambda (pair)
(write (car pair) accumulator)
(write (cdr pair) accumulator))
obj)
(accumulator '(json-structure . object-end)))
(else (error "Unexpected error!"))))
(assume (procedure? accumulator)
"ACCUMULATOR does look like a valid accumulator.")
(raise-unless-valid? obj)
(write obj (json-accumulator accumulator)))
(define (port->accumulator port)
(lambda (char-or-string)
(cond
((char? char-or-string) (write-char char-or-string port))
((string? char-or-string) (write-string char-or-string port))
(else (raise (make-json-error "Not a char or string"))))))
(define json-write
(case-lambda
((obj) (json-write obj (current-output-port)))
((obj port-or-accumulator)
(assume (or (procedure? port-or-accumulator)
(and (textual-port? port-or-accumulator)
(output-port? port-or-accumulator)))
"ACCUMULATOR does look like a valid accumulator.")
(if (procedure? port-or-accumulator)
(%json-write obj port-or-accumulator)
(%json-write obj (port->accumulator port-or-accumulator))))))

File diff suppressed because it is too large Load diff

View file

@ -1,22 +0,0 @@
(import (r7rs))
(define-library (srfi 180 helpers)
(export valid-number?)
(import (scheme base)
(chicken irregex))
(begin
(define (valid-number? string)
(irregex-match?
`(seq
(? #\-)
(or #\0 (seq (- numeric #\0)
(* numeric)))
(? (seq #\. (+ numeric)))
(? (seq (or #\e #\E)
(? (or #\- #\+))
(+ numeric))))
string))))

View file

@ -1,90 +1,142 @@
(import (scheme base))
(import (scheme cxr))
(import (scheme eval))
(import (scheme write))
(import (scheme read))
(import (scheme file))
(import (scheme process-context))
(import (chicken string))
(import test
(chicken base)
(chicken format)
(chicken port)
(chicken string)
(chicken io)
(srfi-34) ;;Exception Handling
(srfi-35) ;;Exception Types
(srfi-158) ;;Generators
)
(define (pk . args) ;; peek stuff, debug helper.
(write args (current-error-port))
(display #\newline (current-error-port))
(flush-output-port (current-error-port))
(car (reverse args)))
(include-relative "../srfi-180.impl.scm")
(define filename "../srfi.180.checks.scm")
(test-group "Whitespace predicate"
(test "#\\space"
#t (is-whitespace? #\space)))
(define-syntax define-syntax-rule
(syntax-rules ()
((define-syntax-rule (keyword args ...) body)
(define-syntax keyword
(syntax-rules ()
((keyword args ...) body))))))
(test-group "JSON Generator"
(test "Basic test"
'(array-start 1 2 3 "Hello" object-start "a" 1 object-end array-end)
(with-input-from-string "[1, 2, 3, \"Hello\", {\"a\", 1}] true [5 4 3 2]"
(lambda ()
(let ((generator (json-generator)))
(let loop ((accu '()))
(let ((token (generator)))
(if (not (eof-object? token))
(loop (cons token accu))
(reverse accu)))))))))
(define-syntax-rule (check expected actual)
(lambda ()
(let ((expected* expected))
(guard (ex (else (vector #f 'exception-raised expected* ex)))
(let ((actual* actual))
(if (equal? expected* actual*)
(vector #t)
(vector #f 'unexpected-result expected* actual*)))))))
(test-group "Array delimiter reading"
(test "Start delimiter"
'(array-start " " 1)
(let-values (((val input charcount nesting-delta) (read-array-start 0 "[" (lambda () " "))))
(list val input charcount)))
(test "End delimiter"
'(array-end " " 9)
(let-values (((val input charcount nesting-delta) (read-array-end 8 "]" (lambda () " "))))
(list val input charcount))))
(define-syntax-rule (check-raise predicate? actual)
(lambda ()
(let ((predicate?* predicate?))
(guard (ex ((predicate?* ex) (vector #t))
(else (vector #f 'unexpected-exception predicate?* ex)))
(let ((actual* actual))
(vector #f 'no-exception predicate?* actual*))))))
(test-group "Object delimiter reading"
(test "Start delimiter"
'(object-start " " 1)
(let-values (((val input charcount nesting-delta) (read-object-start 0 "{" (lambda () " "))))
(list val input charcount)))
(test "End delimiter"
'(object-end " " 5)
(let-values (((val input charcount nesting-delta) (read-object-end 4 "}" (lambda () " "))))
(list val input charcount))))
(define-syntax-rule (skip test expected actual)
(lambda ()
(vector #t)))
(test-group "Null reading"
(let ((input '(#\u #\l #\l #\space)))
(test "Null reading"
'(null #\space 4)
(let-values (((val input charcount nesting-delta)
(read-null-sym 0 #\n (lambda () (let ((next (car input)))
(set! input (cdr input))
next)))))
(list val input charcount)))))
(define (success? v)
(vector-ref v 0))
(test-group "Boolean reading"
(let ((input '(#\r #\u #\e #\space)))
(test "True values"
'(#t #\space 4)
(let-values (((val input charcount nesting-delta)
(read-boolean 0 #\t (lambda () (let ((next (car input)))
(set! input (cdr input))
next)))))
(list val input charcount)))))
(define (failure? v)
(not (success? v)))
(test-group "Number reading"
(let ((input '(#\2 #\3 #\4 #\space)))
(test "Integer"
'(1234 #\space 4)
(let-values (((val input charcount nesting-delta)
(read-number 0 #\1 (lambda () (let ((next (car input)))
(set! input (cdr input))
next)))))
(list val input charcount)))))
(define (failure-expected v)
(vector-ref v 1))
(test-group "String reading"
(let ((input '(#\T #\e #\s #\t #\space #\T #\e #\\ #\s #\\ #\" #\t #\" #\space)))
(test "String"
'("Test Te\\s\\\"t" #\space 14)
(let-values (((val input charcount nesting-delta)
(read-string 0 #\" (lambda () (let ((next (car input)))
(set! input (cdr input))
next)))))
(list val input charcount)))))
(define (failure-actual v)
(vector-ref v 2))
(test-group "JSON folding"
(test "Single value"
42
(with-input-from-string "42 25"
(lambda ()
(json-fold json-proc array-start array-end object-start object-end '()))))
(test "Simple array"
#(24 42 43)
(with-input-from-string "[24 42 43]"
(lambda ()
(json-fold json-proc array-start array-end object-start object-end '()))))
(test "Nested array"
#(24 #(42 24) 42)
(with-input-from-string "[24 [42 24] 42]"
(lambda ()
(json-fold json-proc array-start array-end object-start object-end '()))))
(test "Nested object"
'(("a" . 1) ("b" . 2) ("c" . (("d" . 4))))
(with-input-from-string "{\"a\": 1, \"b\": 2, \"c\": {\"d\": 4}}"
(lambda ()
(json-fold json-proc array-start array-end object-start object-end '())))))
(define (filename->library-name filename)
;; TODO: try to guess ;)
'(srfi 180 checks))
(test-group "JSON Accumulator"
(test "Accumulate a number"
"1234"
(with-output-to-string
(lambda ()
((json-accumulator) 1234))))
(test "Accumulate a string"
"\"Accumulator\""
(with-output-to-string
(lambda ()
((json-accumulator) "Accumulator"))))
(test "Accumulate a boolean"
"true"
(with-output-to-string
(lambda ()
((json-accumulator) #t))))
(test "Accumulate an array"
"[1, 2, 3, true, null, \"Test\"]"
(with-output-to-string
(lambda ()
((json-accumulator)
#(1 2 3 #t null "Test")))))
(test "Accumulate an alist"
"{\"a\": 1, \"b\": 2}"
(with-output-to-string
(lambda ()
((json-accumulator)
'((a . 1) (b . 2)))))))
(define (filename->library-exports filename)
(define library (call-with-input-file filename read))
(let loop ((forms (cddr library))
(out '()))
(if (null? forms)
out
(if (and (pair? (car forms))
(eq? (caar forms) 'export))
(loop (cdr forms) (append out (cdar forms)))
(loop (cdr forms) out)))))
(define library-name (filename->library-name filename))
(define (check-one? library-name symbol)
(pk library-name symbol)
(let* ((proc (eval `,symbol (environment library-name)))
(out (proc)))
(if (failure? out)
(begin (pk out) #f)
#t)))
(if (null? (cddr (command-line)))
(let loop ((symbols (filename->library-exports filename))
(errors? #f))
(if (null? symbols)
(exit (if errors? 1 0))
(if (check-one? library-name (car symbols))
(begin (loop (cdr symbols) #f))
(loop (cdr symbols) #t))))
(check-one? library-name (string->symbol (caddr (command-line)))))
(test-exit)