pax_global_header00006660000000000000000000000064125372054110014512gustar00rootroot0000000000000052 comment=b357add4f96586ab5c81f2bf2fe3a25e9cb01124 yason-0.7.5/000077500000000000000000000000001253720541100126545ustar00rootroot00000000000000yason-0.7.5/.gitignore000066400000000000000000000000101253720541100146330ustar00rootroot00000000000000*.*f*sl yason-0.7.5/.pre-release.sh000066400000000000000000000000211253720541100154630ustar00rootroot00000000000000sh render-doc.sh yason-0.7.5/CHANGELOG000066400000000000000000000011701253720541100140650ustar00rootroot00000000000000Version 0.7.5 2015-06-14 Fixed release script (Hans Huebner) Version 0.7.4 2015-06-14 Remove post-release.sh that updated c-l.net (Hans Huebner) Version 0.7.3 2015-06-14 Fix #28 Add ENCODE-OBJECT-SLOTS (Thayne McCombs) Update html documentation (Hans Huebner) update documentation link (Hans Huebner) Documentation work (Hans Huebner) Version 0.7.2 2014-12-05 Avoid using e notation because Lisp, JS and JSON numeral syntaxes are incompatible. (Grim Schjetne) readd encode-object/encode-slots API with proper documentation (Philipp Matthias Schaefer) Version 0.7.1 2014-11-04 Remove unused ENCODE-SLOTS and ENCODE-OBJECT stubs yason-0.7.5/LICENSE000066400000000000000000000027501253720541100136650ustar00rootroot00000000000000Copyright (c) 2008-2014 Hans Huebner and contributors 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 BKNR 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 THE COPYRIGHT OWNER OR CONTRIBUTORS 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. yason-0.7.5/README.md000066400000000000000000000011161253720541100141320ustar00rootroot00000000000000YASON ===== > YASON is a Common Lisp library for encoding and decoding data in the > [JSON](https://raw.github.com/hanshuebner/clixdoc/master/clixdoc.xsl) > interchange format. JSON is used as a lightweight alternative to > XML. YASON has the sole purpose of encoding and decoding data and > does not impose any object model on the Common Lisp application that > uses it. It does, however, include an optional feature which enables > mapping between JSON and either plists or alists with a 1:1 > correspondance. Please proceed to the [Documentation](http://hanshuebner.github.io/yason) yason-0.7.5/clixdoc.xsl000066400000000000000000000333771253720541100150460ustar00rootroot00000000000000 <xsl:value-of select="clix:title"/>

[Generic function] [Method] [Macro] [Function]
=>

[Generic reader] [Specialized reader] [Reader]
=>

[Generic writer] [Specialized writer] [Writer]
(setf ( ) new-value) =>

[Generic accessor] [Specialized accessor] [Accessor]
=>
(setf ( ) new-value)

[Special variable]

[Standard class]

[Condition type]

[Symbol]

[Constant]


[Constants]

[Logical Pathname Host]

& # http://www.lispworks.com/documentation/HyperSpec/Body/

Abstract

Contents

  1. #
    1. #
yason-0.7.5/doc.xml000066400000000000000000000744171253720541100141600ustar00rootroot00000000000000 YASON - A JSON encoder/decoder for Common Lisp YASON is a JSON encoding and decoding library for Common Lisp. It provides for functions to read JSON strings into Lisp data structures and for serializing Lisp data structures as JSON strings. YASON is a Common Lisp library for encoding and decoding data in the JSON interchange format. JSON is used as a lightweight alternative to XML. YASON has the sole purpose of encoding and decoding data and does not impose any object model on the Common Lisp application that uses it.

JSON is an established alternative to XML as a data interchange format for web applications. YASON implements reading and writing of JSON formatted data in Common Lisp. It does not attempt to provide a mapping between CLOS objects and YASON, but can be used to implement such mappings. It does include an optional feature which enables mapping between JSON and either plists or alists with a 1:1 correspondance.

CL-JSON is another Common Lisp package that can be used to work with JSON encoded data. It takes a more integrated approach, providing for library internal mappings between JSON objects and CLOS objects. YASON was created as a lightweight, documented alternative with a minimalistic approach and extensibilty.

YASON has its permanent home at GitHub. It can be obtained by downloading the release tarball. The current release is .

You may also check out the current development version from its git repository. If you have suggestions regarding YASON, please email me at hans.huebner@gmail.com.

YASON is written in ANSI Common Lisp. It depends on UNIT-TEST, TRIVIAL-GRAY-STREAMS and ALEXANDRIA open source libraries. The recommended way to install YASON and its dependencies is through the excellent Quicklisp library management system.

YASON lives in the :yason package and creates a package nickname :json. Applications will not normally :use this package, but rather use qualified names to access YASON's symbols. For that reason, YASON's symbols do not contain the string "JSON" themselves. See below for usage samples.

Versions of YASON preceding the v0.6.0 release provided a package nickname "JSON" for the "YASON" package. This made it impossible to load both YASON and CL-JSON into the same image, because CL-JSON uses the "JSON" package name as well.

As CL-JSON's use of "JSON" as package name has a much longer history and loading of both CL-JSON and YASON into the same image has become more common, the "JSON" nickname was removed from the YASON package with the v0.6.0 release. Users will need to change their applications so that the "JSON" nickname is no longer used to refer to the "YASON" package. It is understood that this is a disruptive change, but as there is no all-encompassing workaround, this step was felt to be the right one to make

By default, YASON performs the following mappings between JSON and CL datatypes:
JSON
datatype
CL
datatype
Notes
object hash-table
:test #'equal
Keys are strings by default (see *parse-object-key-fn*). Set *parse-object-as* to :alist in order to have YASON parse objects as alists or to :plist to parse them as plists. When using plists, you probably want to also set *parse-object-key-fn* to a function that interns the object's keys to symbols.
array list Can be changed to read to vectors (see *parse-json-arrays-as-vectors*).
string string JSON escape characters are recognized upon reading. Upon writing, known escape characters are used, but non-ASCII Unicode characters are written as is.
number number Parsed with READ, printed with PRINC. This is not a faithful implementation of the specification.
true t Can be changed to read as TRUE (see *parse-json-booleans-as-symbols*).
false nil Can be changed to read as FALSE (see *parse-json-booleans-as-symbols*).
null nil

JSON data is always completely parsed into an equivalent in-memory representation. Upon reading, some translations are performed by default to make it easier for the Common Lisp program to work with the data; see mapping for details. If desired, the parser can be configured to preserve the full semantics of the JSON data read.

For example
CL-USER> (defvar *json-string* "[{\"foo\":1,\"bar\":[7,8,9]},2,3,4,[5,6,7],true,null]")
*JSON-STRING*
CL-USER> (let* ((result (yason:parse *json-string*)))
           (print result)
           (alexandria:hash-table-plist (first result)))

(#<HASH-TABLE :TEST EQUAL :COUNT 2 {5A4420F1}> 2 3 4 (5 6 7) T NIL)
("bar" (7 8 9) "foo" 1)
CL-USER> (defun maybe-convert-to-keyword (js-name)
           (or (find-symbol (string-upcase js-name) :keyword)
               js-name))
MAYBE-CONVERT-TO-KEYWORD
CL-USER> :FOO ; intern the :FOO keyword
:FOO
CL-USER> (let* ((yason:*parse-json-arrays-as-vectors* t)
                (yason:*parse-json-booleans-as-symbols* t)
                (yason:*parse-object-key-fn* #'maybe-convert-to-keyword)
                (result (yason:parse *json-string*)))
           (print result)
           (alexandria:hash-table-plist (aref result 0)))

#(#<HASH-TABLE :TEST EQUAL :COUNT 2 {59B4EAD1}> 2 3 4 #(5 6 7) YASON:TRUE NIL)
("bar" #(7 8 9) :FOO 1)

The second example modifies the parser's behaviour so that JSON arrays are read as CL vectors, JSON booleans will be read as the symbols TRUE and FALSE and JSON object keys will be looked up in the :keyword package. Interning strings coming from an external source is not recommended practice.

input &key (object-key-fn *parse-object-as-key-fn*) (object-as *parse-object-as*) (json-arrays-as-vectors *parse-json-arrays-as-vectors*) (json-booleans-as-symbols *parse-json-booleans-as-symbols*) (json-nulls-as-keyword *parse-json-null-as-keyword*) object Parse input, which must be a string or a stream, as JSON. Returns the Lisp representation of the JSON structure parsed.

The keyword arguments object-key-fn, object-as, json-arrays-as-vectors, json-booleans-as-symbols, and json-null-as-keyword may be used to specify different values for the parsing parameters from the current bindings of the respective special variables.

If set to a true value, JSON arrays will be parsed as vectors, not as lists. NIL is the default. Can be set to :hash-table to parse objects as hash tables, :alist to parse them as alists or :plist to parse them as plists. :hash-table is the default. If set to a true value, JSON booleans will be read as the symbols TRUE and FALSE instead of T and NIL, respectively. NIL is the default. If set to a true value, JSON null will be read as the keyword :NULL, instead of NIL. NIL is the default. Function to call to convert a key string in a JSON array to a key in the CL hash produced. IDENTITY is the default.
YASON provides two distinct modes to encode JSON data: applications can either create an in-memory representation of the data to be serialized, then have YASON convert it to JSON in one go, or they can use a set of macros to serialze the JSON data element-by-element, allowing fine-grained control over the layout of the generated data.

Optionally, the JSON that is produced can be indented. Indentation requires the use of a JSON-OUTPUT-STREAM as serialization target. With the stream serializer, such a stream is automatically used. If indentation is desired with the DOM serializer, such a stream can be obtained by calling the MAKE-JSON-OUTPUT-STREAM function with the target output string as argument. Please be aware that indented output not requires more space, but is also slower and should not be enabled in performance critical applications.

In this mode, an in-memory structure is encoded in JSON format. The structure must consist of objects that are serializable using the ENCODE function. YASON defines a number of encoders for standard data types (see MAPPING), but the application can define additional methods (e.g. for encoding CLOS objects).

For example:
CL-USER> (yason:encode
          (list (alexandria:plist-hash-table
                 '("foo" 1 "bar" (7 8 9))
                 :test #'equal)
                2 3 4
                '(5 6 7)
                t nil)
          *standard-output*)
[{"foo":1,"bar":[7,8,9]},2,3,4,[5,6,7],true,null]
(#<HASH-TABLE :TEST EQUAL :COUNT 2 {59942D21}> 2 3 4 (5 6 7) T NIL)
object &optional stream object Encode object in JSON format and write to stream. May be specialized by applications to perform specific rendering. Stream defaults to *STANDARD-OUTPUT*. object &optional (stream *standard-output*) object Encodes object in JSON format and write to stream. If alists are encountered whilst traversing the list structure of object, they will be encoded as JSON objects. object &optional (stream *standard-output*) object Encodes object in JSON format and write to stream. If plists are encountered whilst traversing the list structure of object, they will be encoded as JSON objects. stream &key (indent t) stream Creates a json-output-stream instance that wraps the supplied stream and optionally performs indentation of the generated JSON data. The indent argument is described in WITH-OUTPUT. Note that if the indent argument is NIL, the original stream is returned in order to avoid the performance penalty of the indentation algorithm.

In this mode, the JSON structure is generated in a stream. The application makes explicit calls to the encoding library in order to generate the JSON structure. It provides for more control over the generated output, and can be used to generate arbitary JSON without requiring that there exists a directly matching Lisp data structure. The streaming API uses the encode function, so it is possible to intermix the two (see app-encoders for an example).

For example:
CL-USER> (yason:with-output (*standard-output*)
           (yason:with-array ()
             (dotimes (i 3)
               (yason:encode-array-element i))))
[0,1,2]
NIL
CL-USER> (yason:with-output (*standard-output*)
           (yason:with-object ()
             (yason:encode-object-element "hello" "hu hu")
             (yason:with-object-element ("harr")
               (yason:with-array ()
                 (dotimes (i 3)
                   (yason:encode-array-element i))))))
{"hello":"hu hu","harr":[0,1,2]}
NIL
(stream &key indent) &body body result* Set up a JSON streaming encoder context on stream, then evaluate body. indent can be set to T to enable indentation with a default indentation width or to an integer specifying the desired indentation width. By default, indentation is switched off. (&key indent) &body body result* Set up a JSON streaming encoder context, then evaluate body. Return a string with the generated JSON output. See WITH-OUTPUT for the description of the indent keyword argument. This condition is signalled when one of the stream encoding functions is used outside the dynamic context of a WITH-OUTPUT or WITH-OUTPUT-TO-STRING* body. () &body body result* Open a JSON array, then run body. Inside the body, ENCODE-ARRAY-ELEMENT must be called to encode elements to the opened array. Must be called within an existing JSON encoder context (see WITH-OUTPUT and WITH-OUTPUT-TO-STRING*). object object Encode object as next array element to the last JSON array opened with WITH-ARRAY in the dynamic context. object is encoded using the ENCODE generic function, so it must be of a type for which an ENCODE method is defined. &rest objects result* Encode objects, a series of JSON encodable objects, as the next array elements in a JSON array opened with WITH-ARRAY. ENCODE-ARRAY-ELEMENTS uses ENCODE-ARRAY-ELEMENT, which must be applicable to each object in the list (i.e. ENCODE must be defined for each object type). Additionally, this must be called within a valid stream context. () &body body result* Open a JSON object, then run body. Inside the body, ENCODE-OBJECT-ELEMENT or WITH-OBJECT-ELEMENT must be called to encode elements to the object. Must be called within an existing JSON encoder WITH-OUTPUT and WITH-OUTPUT-TO-STRING*. (key) &body body result* Open a new encoding context to encode a JSON object element. key is the key of the element. The value will be whatever body serializes to the current JSON output context using one of the stream encoding functions. This can be used to stream out nested object structures. key value value Encode key and value as object element to the last JSON object opened with WITH-OBJECT in the dynamic context. key and value are encoded using the ENCODE generic function, so they both must be of a type for which an ENCODE method is defined. &rest elements result* Encodes the parameters into JSON in the last object opened with WITH-OBJECT using ENCODE-OBJECT-ELEMENT. The parameters should consist of alternating key/value pairs, and this must be called within a valid stream context. object slots result* Encodes each slot in SLOTS for OBJECT in the last object opened with WITH-OBJECT using ENCODE-OBJECT-ELEMENT. The key is the slot name, and the value is the slot value for the slot on OBJECT. It is equivalent to
(loop for slot in slots
    do (encode-object-element (string slot)
                              (slot-value object slot)))
			
object result* Generic function to encode object slots. There is no default implementation. It should be called in an object encoding context. It uses PROGN combinatation with MOST-SPECIFIC-LAST order, so that base class slots are encoded before derived class slots. object result* Generic function to encode an object. The default implementation opens a new object encoding context and calls ENCODE-SLOTS on the argument. Instances of this class are used to wrap an output stream that is used as a serialization target in the stream encoder and optionally in the DOM encoder if indentation is desired. The class name is not exported, use make-json-output-stream to create a wrapper stream if required.
Suppose your application uses structs to represent its data and you want to encode these structs using JSON in order to send them to a client application. Suppose further that your structs also include internal information that you do not want to send. Here is some code that illustrates how one could implement a serialization function:
CL-USER> (defstruct user name age password)
USER
CL-USER> (defmethod yason:encode ((user user) &optional (stream *standard-output*))
           (yason:with-output (stream)
             (yason:with-object ()
               (yason:encode-object-element "name" (user-name user))
               (yason:encode-object-element "age" (user-age user)))))
#<STANDARD-METHOD YASON:ENCODE (USER) {5B40A591}>
CL-USER> (yason:encode (list (make-user :name "horst" :age 27 :password "puppy")
                            (make-user :name "uschi" :age 28 :password "kitten")))
[{"name":"horst","age":27},{"name":"uschi","age":28}]
(#S(USER :NAME "horst" :AGE 27 :PASSWORD "puppy")
 #S(USER :NAME "uschi" :AGE 28 :PASSWORD "kitten"))
As you can see, the streaming API and the DOM encoder can be used together. ENCODE invokes itself recursively, so any application defined method will be called while encoding in-memory objects as appropriate.

For an example of the interplay between ENCODE-OBJECT and ENCODE-SLOTS, suppose you have the following CLOS class heirarchy:

(defclass shape ()
  ((color :reader color)))

(defclass square (shape)
  ((side-length :reader side-length)))

(defclass circle (shape)
  ((radius :reader radius)))
In order to implement encoding of circles and squares without duplicating code you can specialize ENCODE-SLOTS for all three classes
(defmethod yason:encode-slots progn ((shape shape))
  (yason:encode-object-element "color" (color shape)))

(defmethod yason:encode-slots progn ((square square))
  (yason:encode-object-element "side-length" (side-length square)))

(defmethod yason:encode-slots progn ((circle circle))
  (yason:encode-object-element "radius" (radius circle)))
and then use ENCODE-OBJECT:
CL-USER> (yason:with-output-to-string* ()
           (yason:encode-object (make-instance 'square :color "red" :side-length 3)))
"{\"color\":\"red\",\"side-length\":3}"
CL-USER> (yason:with-output-to-string* ()
           (yason:encode-object (make-instance 'circle :color "blue" :side-length 5)))
"{\"color\":\"blue\",\"radius\":5}"

Alternatively, you can use the shortcut ENCODE-OBJECT-SLOTS if you want the keys to be the slot names. For example:

(defclass person ()
  ((name :reader name :initarg :name)
   (address :reader address :initarg :address)
   (phone-number :reader phone-number :initarg :phone)
   (favorite-color :reader favorite-color :initarg :color)))

(defmethod yason:encode-slots progn ((person person))
  (yason:encode-object-slots person '(name address phone-number favorite-color)))
and then:
CL-USER> (yason:with-output-to-string* ()
       (yason:encode-object (make-instance 'person :name "John Doe"
                                                   :address "123 Main St."
                                                   :phone "(123)-456-7890"
                                                   :color "blue")))
"{\"NAME\":\"John Doe\",\"ADDRESS\":\"123 Main St.\",\"PHONE-NUMBER\":\"(123)-456-7890\",
\"FAVORITE-COLOR\":\"blue\"}"

Copyright (c) 2008-2014 Hans Hübner and contributors
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 BKNR 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 THE COPYRIGHT
OWNER OR CONTRIBUTORS 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.
Thanks go to Edi Weitz for being a great inspiration. This documentation as been generated with a hacked-up version of his DOCUMENTATION-TEMPLATE software. Thanks to David Lichteblau for coining YASON's name.
yason-0.7.5/encode.lisp000066400000000000000000000323201253720541100150020ustar00rootroot00000000000000;; This file is part of yason, a Common Lisp JSON parser/encoder ;; ;; Copyright (c) 2008-2014 Hans Huebner and contributors ;; All rights reserved. ;; ;; Please see the file LICENSE in the distribution. (in-package :yason) (defvar *json-output*) (defparameter *default-indent* nil "Set to T or an numeric indentation width in order to have YASON indent its output by default.") (defparameter *default-indent-width* 2 "Default indentation width for output if indentation is selected with no indentation width specified.") (defgeneric encode (object &optional stream) (:documentation "Encode OBJECT to STREAM in JSON format. May be specialized by applications to perform specific rendering. STREAM defaults to *STANDARD-OUTPUT*.")) (defparameter *char-replacements* (alexandria:plist-hash-table '(#\\ "\\\\" #\" "\\\"" #\Backspace "\\b" #\Page "\\f" #\Newline "\\n" #\Return "\\r" #\Tab "\\t"))) (defmethod encode ((string string) &optional (stream *standard-output*)) (write-char #\" stream) (dotimes (i (length string)) (let* ((char (aref string i)) (replacement (gethash char *char-replacements*))) (if replacement (write-string replacement stream) (write-char char stream)))) (write-char #\" stream) string) (defmethod encode ((object ratio) &optional (stream *standard-output*)) (encode (coerce object 'double-float) stream) object) (defmethod encode ((object float) &optional (stream *standard-output*)) (let ((*read-default-float-format* 'double-float)) (format stream "~F" (coerce object 'double-float))) object) (defmethod encode ((object integer) &optional (stream *standard-output*)) (princ object stream)) (defmacro with-aggregate/object ((stream opening-char closing-char) &body body) "Set up serialization context for aggregate serialization with the object encoder." (alexandria:with-gensyms (printed) `(progn (write-delimiter ,opening-char ,stream) (change-indentation ,stream #'+) (prog1 (let (,printed) (macrolet ((with-element-output (() &body body) `(progn (cond (,',printed (write-delimiter #\, ,',stream)) (t (setf ,',printed t))) (write-indentation ,',stream) ,@body))) ,@body)) (change-indentation ,stream #'-) (write-indentation ,stream) (write-delimiter ,closing-char ,stream))))) (defun encode-key/value (key value stream) (encode key stream) (write-char #\: stream) (encode value stream)) (defmethod encode ((object hash-table) &optional (stream *standard-output*)) (with-aggregate/object (stream #\{ #\}) (maphash (lambda (key value) (with-element-output () (encode-key/value key value stream))) object) object)) (defmethod encode ((object vector) &optional (stream *standard-output*)) (with-aggregate/object (stream #\[ #\]) (loop for value across object do (with-element-output () (encode value stream))) object)) (defun encode-list% (object &optional (stream *standard-output*)) (with-aggregate/object (stream #\[ #\]) (dolist (value object) (with-element-output () (encode value stream))) object)) (defun encode-assoc-key/value (key value stream) (let ((string (string key))) (encode-key/value string value stream))) (defun encode-alist% (object &optional (stream *standard-output*)) (with-aggregate/object (stream #\{ #\}) (loop for (key . value) in object do (with-element-output () (if (consp value) (progn (format stream "\"~a\":" key) (encode value stream)) (progn (encode-assoc-key/value key value stream))))) object)) (defun encode-plist% (object &optional (stream *standard-output*)) (with-aggregate/object (stream #\{ #\}) (loop for (key value) on object by #'cddr do (with-element-output () (if (consp value) (progn (format stream "\"~a\":" key) (encode value stream)) (encode-assoc-key/value key value stream)))) object)) (defvar *list->object-convention* :keyword-string) (defun can-map-to-json-object (my-list) (case *list->object-convention* (:plist (keywordp (car my-list))) (:alist (if (consp (car my-list)) (keywordp (caar my-list)))))) (defmethod encode ((object list) &optional (stream *standard-output*)) (if (can-map-to-json-object object) (case *list->object-convention* (:plist (encode-plist% object stream)) (:alist (encode-alist% object stream))) (encode-list% object stream))) (defun encode-alist (object &optional (stream *standard-output*)) (let ((*list->object-convention* :alist)) (encode object stream))) (defun encode-plist (object &optional (stream *standard-output*)) (let ((*list->object-convention* :plist)) (encode object stream))) (defmethod encode ((object (eql 'true)) &optional (stream *standard-output*)) (write-string "true" stream) object) (defmethod encode ((object (eql 'false)) &optional (stream *standard-output*)) (write-string "false" stream) object) (defmethod encode ((object (eql 'null)) &optional (stream *standard-output*)) (write-string "null" stream) object) (defmethod encode ((object (eql t)) &optional (stream *standard-output*)) (write-string "true" stream) object) (defmethod encode ((object (eql nil)) &optional (stream *standard-output*)) (write-string "null" stream) object) (defclass json-output-stream (trivial-gray-streams:fundamental-character-output-stream) ((output-stream :reader output-stream :initarg :output-stream) (stack :accessor stack :initform nil) (indent :initarg :indent :reader indent :accessor indent%) (indent-string :initform "" :accessor indent-string)) (:default-initargs :indent *default-indent*) (:documentation "Objects of this class capture the state of a JSON stream encoder.")) (defmethod initialize-instance :after ((stream json-output-stream) &key indent) (when (eq indent t) (setf (indent% stream) *default-indent-width*))) (defgeneric make-json-output-stream (stream &key indent)) (defmethod make-json-output-stream (stream &key (indent t)) "Create a JSON output stream with indentation enabled." (if indent (make-instance 'json-output-stream :output-stream stream :indent indent) stream)) (defmethod trivial-gray-streams:stream-write-char ((stream json-output-stream) char) (write-char char (output-stream stream))) (defgeneric write-indentation (stream) (:method ((stream t)) nil) (:method ((stream json-output-stream)) (when (indent stream) (fresh-line (output-stream stream)) (write-string (indent-string stream) (output-stream stream))))) (defgeneric write-delimiter (char stream) (:method (char stream) (write-char char stream)) (:method (char (stream json-output-stream)) (write-char char (output-stream stream)))) (defgeneric change-indentation (stream operator) (:method ((stream t) (operator t)) nil) (:method ((stream json-output-stream) operator) (when (indent stream) (setf (indent-string stream) (make-string (funcall operator (length (indent-string stream)) (indent stream)) :initial-element #\Space))))) (defun next-aggregate-element () (if (car (stack *json-output*)) (write-char (car (stack *json-output*)) (output-stream *json-output*)) (setf (car (stack *json-output*)) #\,))) (defmacro with-output ((stream &rest args &key indent) &body body) (declare (ignore indent)) "Set up a JSON streaming encoder context on STREAM, then evaluate BODY." `(let ((*json-output* (make-instance 'json-output-stream :output-stream ,stream ,@args))) ,@body)) (defmacro with-output-to-string* ((&rest args &key indent) &body body) "Set up a JSON streaming encoder context, then evaluate BODY. Return a string with the generated JSON output." (declare (ignore indent)) `(with-output-to-string (s) (with-output (s ,@args) ,@body))) (define-condition no-json-output-context (error) () (:report "No JSON output context is active") (:documentation "This condition is signalled when one of the stream encoding function is used outside the dynamic context of a WITH-OUTPUT or WITH-OUTPUT-TO-STRING* body.")) (defmacro with-aggregate/stream ((begin-char end-char) &body body) "Set up context for aggregate serialization for the stream encoder." `(progn (unless (boundp '*json-output*) (error 'no-json-output-context)) (when (stack *json-output*) (next-aggregate-element)) (write-indentation *json-output*) (write-delimiter ,begin-char *json-output*) (change-indentation *json-output* #'+) (push nil (stack *json-output*)) (prog1 (progn ,@body) (pop (stack *json-output*)) (change-indentation *json-output* #'-) (write-indentation *json-output*) (write-delimiter ,end-char *json-output*)))) (defmacro with-array (() &body body) "Open a JSON array, then run BODY. Inside the body, ENCODE-ARRAY-ELEMENT must be called to encode elements to the opened array. Must be called within an existing JSON encoder context, see WITH-OUTPUT and WITH-OUTPUT-TO-STRING*." `(with-aggregate/stream (#\[ #\]) ,@body)) (defmacro with-object (() &body body) "Open a JSON object, then run BODY. Inside the body, ENCODE-OBJECT-ELEMENT or WITH-OBJECT-ELEMENT must be called to encode elements to the object. Must be called within an existing JSON encoder context, see WITH-OUTPUT and WITH-OUTPUT-TO-STRING*." `(with-aggregate/stream (#\{ #\}) ,@body)) (defun encode-array-element (object) "Encode OBJECT as next array element to the last JSON array opened with WITH-ARRAY in the dynamic context. OBJECT is encoded using the ENCODE generic function, so it must be of a type for which an ENCODE method is defined." (next-aggregate-element) (write-indentation *json-output*) (encode object (output-stream *json-output*))) (defun encode-array-elements (&rest objects) "Encode OBJECTS, a list of JSON encodable objects, as array elements." (dolist (object objects) (encode-array-element object))) (defun encode-object-element (key value) "Encode KEY and VALUE as object element to the last JSON object opened with WITH-OBJECT in the dynamic context. KEY and VALUE are encoded using the ENCODE generic function, so they both must be of a type for which an ENCODE method is defined." (next-aggregate-element) (write-indentation *json-output*) (encode-key/value key value (output-stream *json-output*)) value) (defun encode-object-elements (&rest elements) "Encode plist ELEMENTS as object elements." (loop for (key value) on elements by #'cddr do (encode-object-element key value))) (defun encode-object-slots (object slots) "For each slot in SLOTS, encode that slot on OBJECT as an object element. Equivalent to calling ENCODE-OBJECT-ELEMENT for each slot where the key is the slot name, and the value is the (SLOT-VALUE OBJECT slot)" (loop for slot in slots do (encode-object-element (string slot) (slot-value object slot)))) (define-compiler-macro encode-object-slots (&whole form &environment env object raw-slots) "Compiler macro to allow open-coding with encode-object-slots when slots are literal list." (let ((slots (macroexpand raw-slots env))) (cond ((null slots) nil) ((eq (car slots) 'quote) (setf slots (cadr slots)) ; Get the quoted list `(with-slots ,slots ,object ,@(loop for slot in slots collect `(encode-object-element ,(string slot) ,slot)))) (t form)))) (defmacro with-object-element ((key) &body body) "Open a new encoding context to encode a JSON object element. KEY is the key of the element. The value will be whatever BODY serializes to the current JSON output context using one of the stream encoding functions. This can be used to stream out nested object structures." `(progn (next-aggregate-element) (write-indentation *json-output*) (encode ,key (output-stream *json-output*)) (setf (car (stack *json-output*)) #\:) (unwind-protect (progn ,@body) (setf (car (stack *json-output*)) #\,)))) (defgeneric encode-slots (object) (:documentation "Generic function to encode object slots. It should be called in an object encoding context. It uses PROGN combinatation with MOST-SPECIFIC-LAST order, so that base class slots are encoded before derived class slots.") (:method-combination progn :most-specific-last)) (defgeneric encode-object (object) (:documentation "Generic function to encode an object. The default implementation opens a new object encoding context and calls ENCODE-SLOTS on the argument.") (:method (object) (with-object () (yason:encode-slots object)))) yason-0.7.5/index.html000066400000000000000000001135641253720541100146630ustar00rootroot00000000000000 YASON - A JSON encoder/decoder for Common Lisp

YASON - A JSON encoder/decoder for Common Lisp

Abstract

YASON is a Common Lisp library for encoding and decoding data in the JSON interchange format. JSON is used as a lightweight alternative to XML. YASON has the sole purpose of encoding and decoding data and does not impose any object model on the Common Lisp application that uses it.

Contents

  1. Introduction
  2. Download and Installation
  3. Using JSON as package name
  4. Mapping between JSON and CL datatypes
  5. Parsing JSON data
    1. Parser dictionary
  6. Encoding JSON data
    1. Encoding a JSON DOM
    2. Encoding JSON in streaming mode
    3. Application specific encoders
  7. Symbol index
  8. License
  9. Acknowledgements

Introduction

JSON is an established alternative to XML as a data interchange format for web applications. YASON implements reading and writing of JSON formatted data in Common Lisp. It does not attempt to provide a mapping between CLOS objects and YASON, but can be used to implement such mappings. It does include an optional feature which enables mapping between JSON and either plists or alists with a 1:1 correspondance.

CL-JSON is another Common Lisp package that can be used to work with JSON encoded data. It takes a more integrated approach, providing for library internal mappings between JSON objects and CLOS objects. YASON was created as a lightweight, documented alternative with a minimalistic approach and extensibilty.

Download and Installation

YASON has its permanent home at GitHub. It can be obtained by downloading the release tarball. The current release is 0.7.5.

You may also check out the current development version from its git repository. If you have suggestions regarding YASON, please email me at hans.huebner@gmail.com.

YASON is written in ANSI Common Lisp. It depends on UNIT-TEST, TRIVIAL-GRAY-STREAMS and ALEXANDRIA open source libraries. The recommended way to install YASON and its dependencies is through the excellent Quicklisp library management system.

YASON lives in the :yason package and creates a package nickname :json. Applications will not normally :use this package, but rather use qualified names to access YASON's symbols. For that reason, YASON's symbols do not contain the string "JSON" themselves. See below for usage samples.

Using JSON as package name

Versions of YASON preceding the v0.6.0 release provided a package nickname "JSON" for the "YASON" package. This made it impossible to load both YASON and CL-JSON into the same image, because CL-JSON uses the "JSON" package name as well.

As CL-JSON's use of "JSON" as package name has a much longer history and loading of both CL-JSON and YASON into the same image has become more common, the "JSON" nickname was removed from the YASON package with the v0.6.0 release. Users will need to change their applications so that the "JSON" nickname is no longer used to refer to the "YASON" package. It is understood that this is a disruptive change, but as there is no all-encompassing workaround, this step was felt to be the right one to make

Mapping between JSON and CL datatypes

By default, YASON performs the following mappings between JSON and CL datatypes:
JSON

datatype
CL

datatype
Notes
object hash-table

:test #'equal
Keys are strings by default (see *parse-object-key-fn*). Set *parse-object-as* to :alist in order to have YASON parse objects as alists or to :plist to parse them as plists. When using plists, you probably want to also set *parse-object-key-fn* to a function that interns the object's keys to symbols.
array list Can be changed to read to vectors (see *parse-json-arrays-as-vectors*).
string string JSON escape characters are recognized upon reading. Upon writing, known escape characters are used, but non-ASCII Unicode characters are written as is.
number number Parsed with READ, printed with PRINC. This is not a faithful implementation of the specification.
true t Can be changed to read as TRUE (see *parse-json-booleans-as-symbols*).
false nil Can be changed to read as FALSE (see *parse-json-booleans-as-symbols*).
null nil

Parsing JSON data

JSON data is always completely parsed into an equivalent in-memory representation. Upon reading, some translations are performed by default to make it easier for the Common Lisp program to work with the data; see mapping for details. If desired, the parser can be configured to preserve the full semantics of the JSON data read.

For example
CL-USER> (defvar *json-string* "[{\"foo\":1,\"bar\":[7,8,9]},2,3,4,[5,6,7],true,null]")
*JSON-STRING*
CL-USER> (let* ((result (yason:parse *json-string*)))
           (print result)
           (alexandria:hash-table-plist (first result)))

(#<HASH-TABLE :TEST EQUAL :COUNT 2 {5A4420F1}> 2 3 4 (5 6 7) T NIL)
("bar" (7 8 9) "foo" 1)
CL-USER> (defun maybe-convert-to-keyword (js-name)
           (or (find-symbol (string-upcase js-name) :keyword)
               js-name))
MAYBE-CONVERT-TO-KEYWORD
CL-USER> :FOO ; intern the :FOO keyword
:FOO
CL-USER> (let* ((yason:*parse-json-arrays-as-vectors* t)
                (yason:*parse-json-booleans-as-symbols* t)
                (yason:*parse-object-key-fn* #'maybe-convert-to-keyword)
                (result (yason:parse *json-string*)))
           (print result)
           (alexandria:hash-table-plist (aref result 0)))

#(#<HASH-TABLE :TEST EQUAL :COUNT 2 {59B4EAD1}> 2 3 4 #(5 6 7) YASON:TRUE NIL)
("bar" #(7 8 9) :FOO 1)

The second example modifies the parser's behaviour so that JSON arrays are read as CL vectors, JSON booleans will be read as the symbols TRUE and FALSE and JSON object keys will be looked up in the :keyword package. Interning strings coming from an external source is not recommended practice.

Parser dictionary

[Function]
parse input &key (object-key-fn *parse-object-as-key-fn*) (object-as *parse-object-as*) (json-arrays-as-vectors *parse-json-arrays-as-vectors*) (json-booleans-as-symbols *parse-json-booleans-as-symbols*) (json-nulls-as-keyword *parse-json-null-as-keyword*) => object

Parse input, which must be a string or a stream, as JSON. Returns the Lisp representation of the JSON structure parsed.

The keyword arguments object-key-fn, object-as, json-arrays-as-vectors, json-booleans-as-symbols, and json-null-as-keyword may be used to specify different values for the parsing parameters from the current bindings of the respective special variables.

[Special variable]
*parse-json-arrays-as-vectors*

If set to a true value, JSON arrays will be parsed as vectors, not as lists. NIL is the default.

[Special variable]
*parse-object-as*

Can be set to :hash-table to parse objects as hash tables, :alist to parse them as alists or :plist to parse them as plists. :hash-table is the default.

[Special variable]
*parse-json-booleans-as-symbols*

If set to a true value, JSON booleans will be read as the symbols TRUE and FALSE instead of T and NIL, respectively. NIL is the default.

[Special variable]
*parse-json-null-as-keyword*

If set to a true value, JSON null will be read as the keyword :NULL, instead of NIL. NIL is the default.

[Special variable]
*parse-object-key-fn*

Function to call to convert a key string in a JSON array to a key in the CL hash produced. IDENTITY is the default.

Encoding JSON data

YASON provides two distinct modes to encode JSON data: applications can either create an in-memory representation of the data to be serialized, then have YASON convert it to JSON in one go, or they can use a set of macros to serialze the JSON data element-by-element, allowing fine-grained control over the layout of the generated data.

Optionally, the JSON that is produced can be indented. Indentation requires the use of a JSON-OUTPUT-STREAM as serialization target. With the stream serializer, such a stream is automatically used. If indentation is desired with the DOM serializer, such a stream can be obtained by calling the MAKE-JSON-OUTPUT-STREAM function with the target output string as argument. Please be aware that indented output not requires more space, but is also slower and should not be enabled in performance critical applications.

Encoding a JSON DOM

In this mode, an in-memory structure is encoded in JSON format. The structure must consist of objects that are serializable using the ENCODE function. YASON defines a number of encoders for standard data types (see MAPPING), but the application can define additional methods (e.g. for encoding CLOS objects).

For example:
CL-USER> (yason:encode
          (list (alexandria:plist-hash-table
                 '("foo" 1 "bar" (7 8 9))
                 :test #'equal)
                2 3 4
                '(5 6 7)
                t nil)
          *standard-output*)
[{"foo":1,"bar":[7,8,9]},2,3,4,[5,6,7],true,null]
(#<HASH-TABLE :TEST EQUAL :COUNT 2 {59942D21}> 2 3 4 (5 6 7) T NIL)

DOM encoder dictionary

[Generic function]
encode object &optional stream => object

Encode object in JSON format and write to stream. May be specialized by applications to perform specific rendering. Stream defaults to *STANDARD-OUTPUT*.

[Function]
encode-alist object &optional (stream *standard-output*) => object

Encodes object in JSON format and write to stream. If alists are encountered whilst traversing the list structure of object, they will be encoded as JSON objects.

[Function]
encode-plist object &optional (stream *standard-output*) => object

Encodes object in JSON format and write to stream. If plists are encountered whilst traversing the list structure of object, they will be encoded as JSON objects.

[Function]
make-json-output-stream stream &key (indent t) => stream

Creates a json-output-stream instance that wraps the supplied stream and optionally performs indentation of the generated JSON data. The indent argument is described in WITH-OUTPUT. Note that if the indent argument is NIL, the original stream is returned in order to avoid the performance penalty of the indentation algorithm.

Encoding JSON in streaming mode

In this mode, the JSON structure is generated in a stream. The application makes explicit calls to the encoding library in order to generate the JSON structure. It provides for more control over the generated output, and can be used to generate arbitary JSON without requiring that there exists a directly matching Lisp data structure. The streaming API uses the encode function, so it is possible to intermix the two (see app-encoders for an example).

For example:
CL-USER> (yason:with-output (*standard-output*)
           (yason:with-array ()
             (dotimes (i 3)
               (yason:encode-array-element i))))
[0,1,2]
NIL
CL-USER> (yason:with-output (*standard-output*)
           (yason:with-object ()
             (yason:encode-object-element "hello" "hu hu")
             (yason:with-object-element ("harr")
               (yason:with-array ()
                 (dotimes (i 3)
                   (yason:encode-array-element i))))))
{"hello":"hu hu","harr":[0,1,2]}
NIL

Streaming encoder dictionary

[Macro]
with-output (stream &key indent) &body body => result*

Set up a JSON streaming encoder context on stream, then evaluate body. indent can be set to T to enable indentation with a default indentation width or to an integer specifying the desired indentation width. By default, indentation is switched off.

[Macro]
with-output-to-string* (&key indent) &body body => result*

Set up a JSON streaming encoder context, then evaluate body. Return a string with the generated JSON output. See WITH-OUTPUT for the description of the indent keyword argument.

[Condition type]
no-json-output-context

This condition is signalled when one of the stream encoding functions is used outside the dynamic context of a WITH-OUTPUT or WITH-OUTPUT-TO-STRING* body.

[Macro]
with-array () &body body => result*

Open a JSON array, then run body. Inside the body, ENCODE-ARRAY-ELEMENT must be called to encode elements to the opened array. Must be called within an existing JSON encoder context (see WITH-OUTPUT and WITH-OUTPUT-TO-STRING*).

[Function]
encode-array-element object => object

Encode object as next array element to the last JSON array opened with WITH-ARRAY in the dynamic context. object is encoded using the ENCODE generic function, so it must be of a type for which an ENCODE method is defined.

[Function]
encode-array-elements &rest objects => result*

Encode objects, a series of JSON encodable objects, as the next array elements in a JSON array opened with WITH-ARRAY. ENCODE-ARRAY-ELEMENTS uses ENCODE-ARRAY-ELEMENT, which must be applicable to each object in the list (i.e. ENCODE must be defined for each object type). Additionally, this must be called within a valid stream context.

[Macro]
with-object () &body body => result*

Open a JSON object, then run body. Inside the body, ENCODE-OBJECT-ELEMENT or WITH-OBJECT-ELEMENT must be called to encode elements to the object. Must be called within an existing JSON encoder WITH-OUTPUT and WITH-OUTPUT-TO-STRING*.

[Macro]
with-object-element (key) &body body => result*

Open a new encoding context to encode a JSON object element. key is the key of the element. The value will be whatever body serializes to the current JSON output context using one of the stream encoding functions. This can be used to stream out nested object structures.

[Function]
encode-object-element key value => value

Encode key and value as object element to the last JSON object opened with WITH-OBJECT in the dynamic context. key and value are encoded using the ENCODE generic function, so they both must be of a type for which an ENCODE method is defined.

[Function]
encode-object-elements &rest elements => result*

Encodes the parameters into JSON in the last object opened with WITH-OBJECT using ENCODE-OBJECT-ELEMENT. The parameters should consist of alternating key/value pairs, and this must be called within a valid stream context.

[Function]
encode-object-slots object slots => result*

Encodes each slot in SLOTS for OBJECT in the last object opened with WITH-OBJECT using ENCODE-OBJECT-ELEMENT. The key is the slot name, and the value is the slot value for the slot on OBJECT. It is equivalent to
(loop for slot in slots
    do (encode-object-element (string slot)
                              (slot-value object slot)))
			

[Function]
encode-slots object => result*

Generic function to encode object slots. There is no default implementation. It should be called in an object encoding context. It uses PROGN combinatation with MOST-SPECIFIC-LAST order, so that base class slots are encoded before derived class slots.

[Function]
encode-object object => result*

Generic function to encode an object. The default implementation opens a new object encoding context and calls ENCODE-SLOTS on the argument.

[Standard class]
json-output-stream

Instances of this class are used to wrap an output stream that is used as a serialization target in the stream encoder and optionally in the DOM encoder if indentation is desired. The class name is not exported, use make-json-output-stream to create a wrapper stream if required.

Application specific encoders

Suppose your application uses structs to represent its data and you want to encode these structs using JSON in order to send them to a client application. Suppose further that your structs also include internal information that you do not want to send. Here is some code that illustrates how one could implement a serialization function:
CL-USER> (defstruct user name age password)
USER
CL-USER> (defmethod yason:encode ((user user) &optional (stream *standard-output*))
           (yason:with-output (stream)
             (yason:with-object ()
               (yason:encode-object-element "name" (user-name user))
               (yason:encode-object-element "age" (user-age user)))))
#<STANDARD-METHOD YASON:ENCODE (USER) {5B40A591}>
CL-USER> (yason:encode (list (make-user :name "horst" :age 27 :password "puppy")
                            (make-user :name "uschi" :age 28 :password "kitten")))
[{"name":"horst","age":27},{"name":"uschi","age":28}]
(#S(USER :NAME "horst" :AGE 27 :PASSWORD "puppy")
 #S(USER :NAME "uschi" :AGE 28 :PASSWORD "kitten"))
As you can see, the streaming API and the DOM encoder can be used together. ENCODE invokes itself recursively, so any application defined method will be called while encoding in-memory objects as appropriate.

For an example of the interplay between ENCODE-OBJECT and ENCODE-SLOTS, suppose you have the following CLOS class heirarchy:

(defclass shape ()
  ((color :reader color)))

(defclass square (shape)
  ((side-length :reader side-length)))

(defclass circle (shape)
  ((radius :reader radius)))
In order to implement encoding of circles and squares without duplicating code you can specialize ENCODE-SLOTS for all three classes
(defmethod yason:encode-slots progn ((shape shape))
  (yason:encode-object-element "color" (color shape)))

(defmethod yason:encode-slots progn ((square square))
  (yason:encode-object-element "side-length" (side-length square)))

(defmethod yason:encode-slots progn ((circle circle))
  (yason:encode-object-element "radius" (radius circle)))
and then use ENCODE-OBJECT:
CL-USER> (yason:with-output-to-string* ()
           (yason:encode-object (make-instance 'square :color "red" :side-length 3)))
"{\"color\":\"red\",\"side-length\":3}"
CL-USER> (yason:with-output-to-string* ()
           (yason:encode-object (make-instance 'circle :color "blue" :side-length 5)))
"{\"color\":\"blue\",\"radius\":5}"

Alternatively, you can use the shortcut ENCODE-OBJECT-SLOTS if you want the keys to be the slot names. For example:

(defclass person ()
  ((name :reader name :initarg :name)
   (address :reader address :initarg :address)
   (phone-number :reader phone-number :initarg :phone)
   (favorite-color :reader favorite-color :initarg :color)))

(defmethod yason:encode-slots progn ((person person))
  (yason:encode-object-slots person '(name address phone-number favorite-color)))
and then:
CL-USER> (yason:with-output-to-string* ()
       (yason:encode-object (make-instance 'person :name "John Doe"
                                                   :address "123 Main St."
                                                   :phone "(123)-456-7890"
                                                   :color "blue")))
"{\"NAME\":\"John Doe\",\"ADDRESS\":\"123 Main St.\",\"PHONE-NUMBER\":\"(123)-456-7890\",
\"FAVORITE-COLOR\":\"blue\"}"

Symbol index

License

Copyright (c) 2008-2014 Hans Hübner and contributors
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 BKNR 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 THE COPYRIGHT
OWNER OR CONTRIBUTORS 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.

Acknowledgements

Thanks go to Edi Weitz for being a great inspiration. This documentation as been generated with a hacked-up version of his DOCUMENTATION-TEMPLATE software. Thanks to David Lichteblau for coining YASON's name. yason-0.7.5/package.lisp000066400000000000000000000016711253720541100151450ustar00rootroot00000000000000;; This file is part of yason, a Common Lisp JSON parser/encoder ;; ;; Copyright (c) 2008-2014 Hans Huebner and contributors ;; All rights reserved. ;; ;; Please see the file LICENSE in the distribution. (defpackage :yason (:use :cl) (:export ;; Parser #:parse #:*parse-object-key-fn* #:*parse-object-as* #:*parse-object-as-alist* ; deprecated #:*parse-json-arrays-as-vectors* #:*parse-json-booleans-as-symbols* #:*parse-json-null-as-keyword* #:true #:false #:null ;; Basic encoder interface #:encode #:encode-slots #:encode-object #:encode-plist #:encode-alist #:make-json-output-stream ;; Streaming encoder interface #:with-output #:with-output-to-string* #:no-json-output-context #:with-array #:encode-array-element #:encode-array-elements #:with-object #:encode-object-element #:encode-object-elements #:encode-object-slots #:with-object-element)) yason-0.7.5/parse.lisp000066400000000000000000000227661253720541100146740ustar00rootroot00000000000000;; This file is part of yason, a Common Lisp JSON parser/encoder ;; ;; Copyright (c) 2008-2014 Hans Huebner and contributors ;; All rights reserved. ;; ;; Please see the file LICENSE in the distribution. (in-package :yason) (defconstant +default-string-length+ 20 "Default length of strings that are created while reading json input.") (defvar *parse-object-key-fn* #'identity "Function to call to convert a key string in a JSON array to a key in the CL hash produced.") (defvar *parse-json-arrays-as-vectors* nil "If set to a true value, JSON arrays will be parsed as vectors, not as lists.") (defvar *parse-json-booleans-as-symbols* nil "If set to a true value, JSON booleans will be read as the symbols TRUE and FALSE, not as T and NIL, respectively.") (defvar *parse-json-null-as-keyword* nil "If set to a true value, JSON nulls will be read as the keyword :NULL, not as NIL.") (defvar *parse-object-as* :hash-table "Set to either :hash-table, :plist or :alist to determine the data structure that objects are parsed to.") (defvar *parse-object-as-alist* nil "DEPRECATED, provided for backward compatibility") (defun make-adjustable-string () "Return an adjustable empty string, usable as a buffer for parsing strings and numbers." (make-array +default-string-length+ :adjustable t :fill-pointer 0 :element-type 'character)) (defun parse-number (input) ;; would be ;; (cl-ppcre:scan-to-strings "^-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+|)(?:[eE][-+]?[0-9]+|)" buffer) ;; but we want to operate on streams (let ((buffer (make-adjustable-string))) (loop while (position (peek-char nil input nil) ".0123456789+-Ee") do (vector-push-extend (read-char input) buffer)) (values (read-from-string buffer)))) (defun parse-unicode-escape (input) (let ((char-code (let ((buffer (make-string 4))) (read-sequence buffer input) (parse-integer buffer :radix 16)))) (if (and (>= char-code #xd800) (<= char-code #xdbff)) (let ((buffer (make-string 6))) (read-sequence buffer input) (when (not (string= buffer "\\u" :end1 2)) (error "Lead Surrogate without Tail Surrogate")) (let ((tail-code (parse-integer buffer :radix 16 :start 2))) (when (not (and (>= tail-code #xdc00) (<= tail-code #xdfff))) (error "Lead Surrogate without Tail Surrogate")) (code-char (+ #x010000 (ash (- char-code #xd800) 10) (- tail-code #xdc00))))) (code-char char-code)))) (defun parse-string (input) (let ((output (make-adjustable-string))) (labels ((outc (c) (vector-push-extend c output)) (next () (read-char input)) (peek () (peek-char nil input))) (let* ((starting-symbol (next)) (string-quoted (equal starting-symbol #\"))) (unless string-quoted (outc starting-symbol)) (loop (cond ((eql (peek) #\") (next) (return-from parse-string output)) ((eql (peek) #\\) (next) (ecase (next) (#\" (outc #\")) (#\\ (outc #\\)) (#\/ (outc #\/)) (#\b (outc #\Backspace)) (#\f (outc #\Page)) (#\n (outc #\Newline)) (#\r (outc #\Return)) (#\t (outc #\Tab)) (#\u (outc (parse-unicode-escape input))))) ((and (or (whitespace-p (peek)) (eql (peek) #\:)) (not string-quoted)) (return-from parse-string output)) (t (outc (next))))))))) (defun whitespace-p (char) (member char '(#\Space #\Newline #\Tab #\Linefeed #\Return))) (defun skip-whitespace (input) (loop while (and (listen input) (whitespace-p (peek-char nil input))) do (read-char input))) (defun peek-char-skipping-whitespace (input &optional (eof-error-p t)) (skip-whitespace input) (peek-char nil input eof-error-p)) (defun parse-constant (input) (destructuring-bind (expected-string return-value) (find (peek-char nil input nil) `(("true" ,(if *parse-json-booleans-as-symbols* 'true t)) ("false" ,(if *parse-json-booleans-as-symbols* 'false nil)) ("null" ,(if *parse-json-null-as-keyword* :null nil))) :key (lambda (entry) (aref (car entry) 0)) :test #'eql) (loop for char across expected-string unless (eql (read-char input nil) char) do (error "invalid constant")) return-value)) (define-condition cannot-convert-key (error) ((key-string :initarg :key-string :reader key-string)) (:report (lambda (c stream) (format stream "cannot convert key ~S used in JSON object to hash table key" (key-string c))))) (defun create-container () (ecase *parse-object-as* ((:plist :alist) nil) (:hash-table (make-hash-table :test #'equal)))) (defun add-attribute (to key value) (ecase *parse-object-as* (:plist (append to (list key value))) (:alist (append to (list (cons key value)))) (:hash-table (setf (gethash key to) value) to))) (define-condition expected-colon (error) ((key-string :initarg :key-string :reader key-string)) (:report (lambda (c stream) (format stream "expected colon to follow key ~S used in JSON object" (key-string c))))) (defun parse-object (input) (let ((return-value (create-container))) (read-char input) (loop (when (eql (peek-char-skipping-whitespace input) #\}) (return)) (skip-whitespace input) (setf return-value (add-attribute return-value (let ((key-string (parse-string input))) (prog1 (or (funcall *parse-object-key-fn* key-string) (error 'cannot-convert-key :key-string key-string)) (skip-whitespace input) (unless (eql #\: (read-char input)) (error 'expected-colon :key-string key-string)) (skip-whitespace input))) (parse input))) (ecase (peek-char-skipping-whitespace input) (#\, (read-char input)) (#\} nil))) (read-char input) return-value)) (defconstant +initial-array-size+ 20 "Initial size of JSON arrays read, they will grow as needed.") (defun %parse-array (input add-element-function) "Parse JSON array from input, calling ADD-ELEMENT-FUNCTION for each array element parsed." (read-char input) (loop (when (eql (peek-char-skipping-whitespace input) #\]) (return)) (funcall add-element-function (parse input)) (ecase (peek-char-skipping-whitespace input) (#\, (read-char input)) (#\] nil))) (read-char input)) (defun parse-array (input) (if *parse-json-arrays-as-vectors* (let ((return-value (make-array +initial-array-size+ :adjustable t :fill-pointer 0))) (%parse-array input (lambda (element) (vector-push-extend element return-value))) return-value) (let (return-value) (%parse-array input (lambda (element) (push element return-value))) (nreverse return-value)))) (defgeneric parse% (input) (:method ((input stream)) ;; backward compatibility code (assert (or (not *parse-object-as-alist*) (eq *parse-object-as* :hash-table)) () "unexpected combination of *parse-object-as* and *parse-object-as-alist*, please use *parse-object-as* exclusively") (let ((*parse-object-as* (if *parse-object-as-alist* :alist *parse-object-as*))) ;; end of backward compatibility code (check-type *parse-object-as* (member :hash-table :alist :plist)) (ecase (peek-char-skipping-whitespace input) (#\" (parse-string input)) ((#\- #\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9) (parse-number input)) (#\{ (parse-object input)) (#\[ (parse-array input)) ((#\t #\f #\n) (parse-constant input))))) (:method ((input pathname)) (with-open-file (stream input) (parse stream))) (:method ((input string)) (parse (make-string-input-stream input)))) (defun parse (input &key (object-key-fn *parse-object-key-fn*) (object-as *parse-object-as*) (json-arrays-as-vectors *parse-json-arrays-as-vectors*) (json-booleans-as-symbols *parse-json-booleans-as-symbols*) (json-nulls-as-keyword *parse-json-null-as-keyword*)) "Parse INPUT, which needs to be a string or a stream, as JSON. Returns the lisp representation of the JSON structure parsed. The keyword arguments can be used to override the parser settings as defined by the respective special variables." (let ((*parse-object-key-fn* object-key-fn) (*parse-object-as* object-as) (*parse-json-arrays-as-vectors* json-arrays-as-vectors) (*parse-json-booleans-as-symbols* json-booleans-as-symbols) (*parse-json-null-as-keyword* json-nulls-as-keyword)) (parse% input))) yason-0.7.5/render-doc.sh000077500000000000000000000004141253720541100152340ustar00rootroot00000000000000#!/bin/sh xmllint --noout doc.xml [ -f clixdoc.xsl ] || wget -q https://raw.github.com/hanshuebner/clixdoc/master/clixdoc.xsl xsltproc --stringparam current-release `perl -ne 'if (/^ *:version +"(.*)"/) { print "$1\n" }' yason.asd` -o index.html clixdoc.xsl doc.xml yason-0.7.5/test.lisp000066400000000000000000000127551253720541100145360ustar00rootroot00000000000000(defpackage :yason-test (:use :cl :unit-test)) (in-package :yason-test) (defparameter *basic-test-json-string* "[{\"foo\":1,\"bar\":[7,8,9]},2,3,4,[5,6,7],true,null]") (defparameter *basic-test-json-string-indented* " [ {\"foo\":1, \"bar\":[7,8,9] }, 2, 3, 4, [5, 6, 7], true, null ]") (defparameter *basic-test-json-dom* (list (alexandria:plist-hash-table '("foo" 1 "bar" (7 8 9)) :test #'equal) 2 3 4 '(5 6 7) t nil)) (deftest :yason "parser.basic" (let ((result (yason:parse *basic-test-json-string*))) (test-equal (first *basic-test-json-dom*) (first result) :test #'equalp) (test-equal (rest *basic-test-json-dom*) (rest result)))) (deftest :yason "parser.basic-with-whitespace" (let ((result (yason:parse *basic-test-json-string-indented*))) (test-equal (first *basic-test-json-dom*) (first result) :test #'equalp) (test-equal (rest *basic-test-json-dom*) (rest result)))) (deftest :yason "dom-encoder.basic" (let ((result (yason:parse (with-output-to-string (s) (yason:encode *basic-test-json-dom* s))))) (test-equal (first *basic-test-json-dom*) (first result) :test #'equalp) (test-equal (rest *basic-test-json-dom*) (rest result)))) (defun whitespace-char-p (char) (member char '(#\space #\tab #\return #\newline #\linefeed))) (deftest :yason "dom-encoder.indentation" (test-equal "[ 1, 2, 3 ]" (with-output-to-string (s) (yason:encode '(1 2 3) (yason:make-json-output-stream s :indent 10)))) (dolist (indentation-arg '(nil t 2 20)) (test-equal "[1,2,3]" (remove-if #'whitespace-char-p (with-output-to-string (s) (yason:encode '(1 2 3) (yason:make-json-output-stream s :indent indentation-arg))))))) (deftest :yason "stream-encoder.basic-array" (test-equal "[0,1,2]" (with-output-to-string (s) (yason:with-output (s) (yason:with-array () (dotimes (i 3) (yason:encode-array-element i))))))) (deftest :yason "stream-encoder.basic-object" (test-equal "{\"hello\":\"hu hu\",\"harr\":[0,1,2]}" (with-output-to-string (s) (yason:with-output (s) (yason:with-object () (yason:encode-object-element "hello" "hu hu") (yason:with-object-element ("harr") (yason:with-array () (dotimes (i 3) (yason:encode-array-element i))))))))) (defstruct user name age password) (defmethod yason:encode ((user user) &optional (stream *standard-output*)) (yason:with-output (stream) (yason:with-object () (yason:encode-object-element "name" (user-name user)) (yason:encode-object-element "age" (user-age user))))) (deftest :yason "stream-encoder.application-struct" (test-equal "[{\"name\":\"horst\",\"age\":27},{\"name\":\"uschi\",\"age\":28}]" (with-output-to-string (s) (yason:encode (list (make-user :name "horst" :age 27 :password "puppy") (make-user :name "uschi" :age 28 :password "kitten")) s)))) (defun compare-tree (tree1 tree2 &optional (test #'equal)) (if (atom tree1) (funcall test tree1 tree2) (and (compare-tree (car tree1) (car tree2)) (compare-tree (cdr tree1) (cdr tree2))))) (defun roundtrip (sexp convention) (case convention (:plist (yason:parse (with-output-to-string (stream) (yason:encode-plist sexp stream)) :object-as :plist :object-key-fn (lambda (str) (intern str :keyword)))) (:alist (yason:parse (with-output-to-string (stream) (yason:encode-alist sexp stream)) :object-as :alist :object-key-fn (lambda (str) (intern str :keyword)))) (otherwise (yason:parse (with-output-to-string (stream) (yason:encode sexp stream)))))) (defparameter *serialisable-plists* '((:a "a" :one 1) (:a-b-c "a-b-c") (:a "a" :b-and-c (:b "b" :c "c")) (:a "a" :b "b" :c "c" :1-2-3 (1 2 3)) (:a "a" :b-and-c-and-d ("b" (:c "c" :d "d"))))) (deftest :yason "plist-roundtrip" (dolist (plist *serialisable-plists*) (test-assert (compare-tree plist (roundtrip plist :plist))))) (defparameter *serialisable-alists* '(((:a . "a") (:one . 1)) ((:a-b-c . "a-b-c")) ((:a . "a") (:b-and-c . ((:b . "b") (:c . "c")))) ((:a . "a") (:b . "b") (:c . "c") (:1-2-3 . (1 2 3))) ((:a . "a") (:b-and-c-and-d . ("b" ((:c . "c") (:d . "d"))))))) (deftest :yason "alist-roundtrip" (dolist (alist *serialisable-alists*) (test-assert (compare-tree alist (roundtrip alist :alist))))) (deftest :yason "no-keywords-in-alist/plist-content" (test-condition (yason:encode-plist '(:a :b0rk)) 'condition) (test-condition (yason:encode-plist '((:a . :b0rk))) 'condition)) yason-0.7.5/yason.asd000066400000000000000000000020111253720541100144700ustar00rootroot00000000000000;;;; -*- Mode: Lisp -*- ;; This file is part of yason, a Common Lisp JSON parser/encoder ;; ;; Copyright (c) 2008-2014 Hans Huebner and contributors ;; All rights reserved. ;; ;; Please see the file LICENSE in the distribution. (in-package :cl-user) (defpackage :yason.system (:use :cl :asdf)) (in-package :yason.system) (defsystem :yason :name "YASON" :author "Hans Huebner " :version "0.7.5" :maintainer "Hans Huebner " :licence "BSD" :description "JSON parser/encoder" :long-description "YASON is a Common Lisp library for encoding and decoding data in the JSON interchange format. JSON is used as a lightweight alternative to XML. YASON has the sole purpose of encoding and decoding data and does not impose any object model on the Common Lisp application that uses it." :depends-on (:alexandria :trivial-gray-streams) :components ((:file "package") (:file "encode" :depends-on ("package")) (:file "parse" :depends-on ("package"))))