aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/clojure/clojure/contrib/json.clj305
-rw-r--r--src/test/clojure/clojure/contrib/test_contrib/test_json.clj172
2 files changed, 477 insertions, 0 deletions
diff --git a/src/main/clojure/clojure/contrib/json.clj b/src/main/clojure/clojure/contrib/json.clj
new file mode 100644
index 00000000..0af05913
--- /dev/null
+++ b/src/main/clojure/clojure/contrib/json.clj
@@ -0,0 +1,305 @@
+;;; json.clj: JavaScript Object Notation (JSON) parser/writer
+
+;; by Stuart Sierra, http://stuartsierra.com/
+;; January 30, 2010
+
+;; Copyright (c) Stuart Sierra, 2010. All rights reserved. The use
+;; and distribution terms for this software are covered by the Eclipse
+;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
+;; which can be found in the file epl-v10.html at the root of this
+;; distribution. By using this software in any fashion, you are
+;; agreeing to be bound by the terms of this license. You must not
+;; remove this notice, or any other, from this software.
+
+
+(ns #^{:author "Stuart Sierra"
+ :doc "JavaScript Object Notation (JSON) parser/writer.
+ See http://www.json.org/
+ To write JSON, use json-str, write-json, or print-json.
+ To read JSON, use read-json."}
+ clojure.contrib.json
+ (:require [clojure.contrib.java-utils :as j])
+ (:import (java.io PushbackReader StringReader Reader EOFException)))
+
+(declare read-json-reader)
+
+(defn- read-json-array [#^PushbackReader stream keywordize?]
+ ;; Expects to be called with the head of the stream AFTER the
+ ;; opening bracket.
+ (loop [i (.read stream), result (transient [])]
+ (let [c (char i)]
+ (cond
+ (= i -1) (throw (EOFException. "JSON error (end-of-file inside array)"))
+ (Character/isWhitespace c) (recur (.read stream) result)
+ (= c \,) (recur (.read stream) result)
+ (= c \]) (persistent! result)
+ :else (do (.unread stream (int c))
+ (let [element (read-json-reader stream keywordize? true nil)]
+ (recur (.read stream) (conj! result element))))))))
+
+(defn- read-json-object [#^PushbackReader stream keywordize?]
+ ;; Expects to be called with the head of the stream AFTER the
+ ;; opening bracket.
+ (loop [i (.read stream), key nil, result (transient {})]
+ (let [c (char i)]
+ (cond
+ (= i -1) (throw (EOFException. "JSON error (end-of-file inside object)"))
+
+ (Character/isWhitespace c) (recur (.read stream) key result)
+
+ (= c \,) (recur (.read stream) nil result)
+
+ (= c \:) (recur (.read stream) key result)
+
+ (= c \}) (if (nil? key)
+ (persistent! result)
+ (throw (Exception. "JSON error (key missing value in object)")))
+
+ :else (do (.unread stream i)
+ (let [element (read-json-reader stream keywordize? true nil)]
+ (if (nil? key)
+ (if (string? element)
+ (recur (.read stream) element result)
+ (throw (Exception. "JSON error (non-string key in object)")))
+ (recur (.read stream) nil
+ (assoc! result (if keywordize? (keyword key) key)
+ element)))))))))
+
+(defn- read-json-hex-character [#^PushbackReader stream]
+ ;; Expects to be called with the head of the stream AFTER the
+ ;; initial "\u". Reads the next four characters from the stream.
+ (let [digits [(.read stream)
+ (.read stream)
+ (.read stream)
+ (.read stream)]]
+ (when (some neg? digits)
+ (throw (EOFException. "JSON error (end-of-file inside Unicode character escape)")))
+ (let [chars (map char digits)]
+ (when-not (every? #{\0 \1 \2 \3 \4 \5 \6 \7 \8 \9 \a \b \c \d \e \f \A \B \C \D \E \F}
+ chars)
+ (throw (Exception. "JSON error (invalid hex character in Unicode character escape)")))
+ (char (Integer/parseInt (apply str chars) 16)))))
+
+(defn- read-json-escaped-character [#^PushbackReader stream]
+ ;; Expects to be called with the head of the stream AFTER the
+ ;; initial backslash.
+ (let [c (char (.read stream))]
+ (cond
+ (#{\" \\ \/} c) c
+ (= c \b) \backspace
+ (= c \f) \formfeed
+ (= c \n) \newline
+ (= c \r) \return
+ (= c \t) \tab
+ (= c \u) (read-json-hex-character stream))))
+
+(defn- read-json-quoted-string [#^PushbackReader stream]
+ ;; Expects to be called with the head of the stream AFTER the
+ ;; opening quotation mark.
+ (let [buffer (StringBuilder.)]
+ (loop [i (.read stream)]
+ (let [c (char i)]
+ (cond
+ (= i -1) (throw (EOFException. "JSON error (end-of-file inside string)"))
+ (= c \") (str buffer)
+ (= c \\) (do (.append buffer (read-json-escaped-character stream))
+ (recur (.read stream)))
+ :else (do (.append buffer c)
+ (recur (.read stream))))))))
+
+(defn read-json-reader
+ ([#^PushbackReader stream keywordize? eof-error? eof-value]
+ (loop [i (.read stream)]
+ (let [c (char i)]
+ (cond
+ ;; Handle end-of-stream
+ (= i -1) (if eof-error?
+ (throw (EOFException. "JSON error (end-of-file)"))
+ eof-value)
+
+ ;; Ignore whitespace
+ (Character/isWhitespace c) (recur (.read stream))
+
+ ;; Read numbers, true, and false with Clojure reader
+ (#{\- \0 \1 \2 \3 \4 \5 \6 \7 \8 \9} c)
+ (do (.unread stream i)
+ (read stream true nil))
+
+ ;; Read strings
+ (= c \") (read-json-quoted-string stream)
+
+ ;; Read null as nil
+ (= c \n) (let [ull [(char (.read stream))
+ (char (.read stream))
+ (char (.read stream))]]
+ (if (= ull [\u \l \l])
+ nil
+ (throw (Exception. (str "JSON error (expected null): " c ull)))))
+
+ ;; Read true
+ (= c \t) (let [rue [(char (.read stream))
+ (char (.read stream))
+ (char (.read stream))]]
+ (if (= rue [\r \u \e])
+ true
+ (throw (Exception. (str "JSON error (expected true): " c rue)))))
+
+ ;; Read false
+ (= c \f) (let [alse [(char (.read stream))
+ (char (.read stream))
+ (char (.read stream))
+ (char (.read stream))]]
+ (if (= alse [\a \l \s \e])
+ false
+ (throw (Exception. (str "JSON error (expected false): " c alse)))))
+
+ ;; Read JSON objects
+ (= c \{) (read-json-object stream keywordize?)
+
+ ;; Read JSON arrays
+ (= c \[) (read-json-array stream keywordize?)
+
+ :else (throw (Exception. (str "JSON error (unexpected character): " c))))))))
+
+(defprotocol Read-JSON-From
+ (read-json-from [input keywordize? eof-error? eof-value]
+ "Reads one JSON value from input String or Reader.
+ If keywordize? is true, object keys will be converted to keywords.
+ If eof-error? is true, empty input will throw an EOFException; if
+ false EOF will return eof-value. "))
+
+(extend-protocol
+ Read-JSON-From
+ String
+ (read-json-from [input keywordize? eof-error? eof-value]
+ (read-json-reader (PushbackReader. (StringReader. input))
+ keywordize? eof-error? eof-value))
+ PushbackReader
+ (read-json-from [input keywordize? eof-error? eof-value]
+ (read-json-reader (PushbackReader. (StringReader. input))
+ keywordize? eof-error? eof-value))
+ Reader
+ (read-json-from [input keywordize? eof-error? eof-value]
+ (read-json-reader (PushbackReader. input)
+ keywordize? eof-error? eof-value)))
+
+(defn read-json
+ "Reads one JSON value from input String or Reader.
+ If keywordize? is true (default), object keys will be converted to
+ keywords. If eof-error? is true (default), empty input will throw
+ an EOFException; if false EOF will return eof-value. "
+ ([input]
+ (read-json-from input true true nil))
+ ([input keywordize?]
+ (read-json-from input keywordize? true nil))
+ ([input keywordize? eof-error? eof-value]
+ (read-json-from input keywordize? eof-error? eof-value)))
+
+(defprotocol Print-JSON
+ (print-json [object]
+ "Print object to *out* as JSON"))
+
+(extend-protocol
+ Print-JSON
+
+ nil
+ (print-json [x] (print "null"))
+
+ clojure.lang.Named
+ (print-json [x] (print-json (name x)))
+
+ java.lang.Boolean
+ (print-json [x] (pr x))
+
+ java.lang.Number
+ (print-json [x] (pr x))
+
+ java.math.BigInteger
+ (print-json [x] (print (str x)))
+
+ java.math.BigDecimal
+ (print-json [x] (print (str x)))
+
+ java.lang.CharSequence
+ (print-json [s]
+ (let [sb (StringBuilder. (count s))]
+ (.append sb \")
+ (dotimes [i (count s)]
+ (let [cp (Character/codePointAt s i)]
+ (cond
+ ;; Handle printable JSON escapes before ASCII
+ (= cp 34) (.append sb "\\\"")
+ (= cp 92) (.append sb "\\\\")
+ (= cp 47) (.append sb "\\/")
+ ;; Print simple ASCII characters
+ (< 31 cp 127) (.append sb (.charAt s i))
+ ;; Handle non-printable JSON escapes
+ (= cp 8) (.append sb "\\b")
+ (= cp 12) (.append sb "\\f")
+ (= cp 10) (.append sb "\\n")
+ (= cp 13) (.append sb "\\r")
+ (= cp 9) (.append sb "\\t")
+ ;; Any other character is Hexadecimal-escaped
+ :else (.append sb (format "\\u%04x" cp)))))
+ (.append sb \")
+ (print (str sb))))
+
+ java.util.Map
+ (print-json [m]
+ (print \{)
+ (loop [x m]
+ (when (seq m)
+ (let [[k v] (first x)]
+ (when (nil? k)
+ (throw (Exception. "JSON object keys cannot be nil/null")))
+ (print-json (j/as-str k))
+ (print \:)
+ (print-json v))
+ (let [nxt (next x)]
+ (when (seq nxt)
+ (print \,)
+ (recur nxt)))))
+ (print \}))
+
+ java.util.Collection
+ (print-json [s]
+ (print \[)
+ (loop [x s]
+ (when (seq x)
+ (let [fst (first x)
+ nxt (next x)]
+ (print-json fst)
+ (when (seq nxt)
+ (print \,)
+ (recur nxt)))))
+ (print \]))
+
+ clojure.lang.ISeq
+ (print-json [s]
+ (print \[)
+ (loop [x s]
+ (when (seq x)
+ (let [fst (first x)
+ nxt (next x)]
+ (print-json fst)
+ (when (seq nxt)
+ (print \,)
+ (recur nxt)))))
+ (print \]))
+
+ java.lang.Object
+ (print-json [x]
+ (if (.isArray (class x))
+ (print-json (seq x))
+ (throw (Exception. "Don't know how to print JSON of " (class x))))))
+
+(defn json-str
+ "Converts x to a JSON-formatted string."
+ [x]
+ (with-out-str (print-json x)))
+
+(defn write-json
+ "Writes JSON-formatted text to out."
+ [x out]
+ (binding [*out* out]
+ (print-json x)))
diff --git a/src/test/clojure/clojure/contrib/test_contrib/test_json.clj b/src/test/clojure/clojure/contrib/test_contrib/test_json.clj
new file mode 100644
index 00000000..36237ad5
--- /dev/null
+++ b/src/test/clojure/clojure/contrib/test_contrib/test_json.clj
@@ -0,0 +1,172 @@
+(ns clojure.contrib.test-contrib.test-json
+ (:use clojure.test clojure.contrib.json))
+
+(deftest can-read-numbers
+ (is (= 42 (read-json "42")))
+ (is (= -3 (read-json "-3")))
+ (is (= 3.14159 (read-json "3.14159")))
+ (is (= 6.022e23 (read-json "6.022e23"))))
+
+(deftest can-read-null
+ (is (= nil (read-json "null"))))
+
+(deftest can-read-strings
+ (is (= "Hello, World!" (read-json "\"Hello, World!\""))))
+
+(deftest handles-escaped-slashes-in-strings
+ (is (= "/foo/bar" (read-json "\"\\/foo\\/bar\""))))
+
+(deftest handles-unicode-escapes
+ (is (= " \u0beb " (read-json "\" \\u0bEb \""))))
+
+(deftest handles-escaped-whitespace
+ (is (= "foo\nbar" (read-json "\"foo\\nbar\"")))
+ (is (= "foo\rbar" (read-json "\"foo\\rbar\"")))
+ (is (= "foo\tbar" (read-json "\"foo\\tbar\""))))
+
+(deftest can-read-booleans
+ (is (= true (read-json "true")))
+ (is (= false (read-json "false"))))
+
+(deftest can-ignore-whitespace
+ (is (= nil (read-json "\r\n null"))))
+
+(deftest can-read-arrays
+ (is (= [1 2 3] (read-json "[1,2,3]")))
+ (is (= ["Ole" "Lena"] (read-json "[\"Ole\", \r\n \"Lena\"]"))))
+
+(deftest can-read-objects
+ (is (= {:a 1, :b 2} (read-json "{\"a\": 1, \"b\": 2}"))))
+
+(deftest can-read-nested-structures
+ (is (= {:a [1 2 {:b [3 "four"]} 5.5]}
+ (read-json "{\"a\":[1,2,{\"b\":[3,\"four\"]},5.5]}"))))
+
+(deftest disallows-non-string-keys
+ (is (thrown? Exception (read-json "{26:\"z\""))))
+
+(deftest disallows-barewords
+ (is (thrown? Exception (read-json " foo "))))
+
+(deftest disallows-unclosed-arrays
+ (is (thrown? Exception (read-json "[1, 2, "))))
+
+(deftest disallows-unclosed-objects
+ (is (thrown? Exception (read-json "{\"a\":1, "))))
+
+(deftest can-get-string-keys
+ (is (= {"a" [1 2 {"b" [3 "four"]} 5.5]}
+ (read-json "{\"a\":[1,2,{\"b\":[3,\"four\"]},5.5]}" false true nil))))
+
+(declare *pass1-string*)
+
+(deftest pass1-test
+ (let [input (read-json *pass1-string* false true nil)]
+ (is (= "JSON Test Pattern pass1" (first input)))
+ (is (= "array with 1 element" (get-in input [1 "object with 1 member" 0])))
+ (is (= 1234567890 (get-in input [8 "integer"])))
+ (is (= "rosebud" (last input)))))
+
+; from http://www.json.org/JSON_checker/test/pass1.json
+(def *pass1-string*
+ "[
+ \"JSON Test Pattern pass1\",
+ {\"object with 1 member\":[\"array with 1 element\"]},
+ {},
+ [],
+ -42,
+ true,
+ false,
+ null,
+ {
+ \"integer\": 1234567890,
+ \"real\": -9876.543210,
+ \"e\": 0.123456789e-12,
+ \"E\": 1.234567890E+34,
+ \"\": 23456789012E66,
+ \"zero\": 0,
+ \"one\": 1,
+ \"space\": \" \",
+ \"quote\": \"\\\"\",
+ \"backslash\": \"\\\\\",
+ \"controls\": \"\\b\\f\\n\\r\\t\",
+ \"slash\": \"/ & \\/\",
+ \"alpha\": \"abcdefghijklmnopqrstuvwyz\",
+ \"ALPHA\": \"ABCDEFGHIJKLMNOPQRSTUVWYZ\",
+ \"digit\": \"0123456789\",
+ \"0123456789\": \"digit\",
+ \"special\": \"`1~!@#$%^&*()_+-={':[,]}|;.</>?\",
+ \"hex\": \"\\u0123\\u4567\\u89AB\\uCDEF\\uabcd\\uef4A\",
+ \"true\": true,
+ \"false\": false,
+ \"null\": null,
+ \"array\":[ ],
+ \"object\":{ },
+ \"address\": \"50 St. James Street\",
+ \"url\": \"http://www.JSON.org/\",
+ \"comment\": \"// /* <!-- --\",
+ \"# -- --> */\": \" \",
+ \" s p a c e d \" :[1,2 , 3
+
+,
+
+4 , 5 , 6 ,7 ],\"compact\":[1,2,3,4,5,6,7],
+ \"jsontext\": \"{\\\"object with 1 member\\\":[\\\"array with 1 element\\\"]}\",
+ \"quotes\": \"&#34; \\u0022 %22 0x22 034 &#x22;\",
+ \"\\/\\\\\\\"\\uCAFE\\uBABE\\uAB98\\uFCDE\\ubcda\\uef4A\\b\\f\\n\\r\\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?\"
+: \"A key can be any string\"
+ },
+ 0.5 ,98.6
+,
+99.44
+,
+
+1066,
+1e1,
+0.1e1,
+1e-1,
+1e00,2e+00,2e-00
+,\"rosebud\"]")
+
+
+(deftest can-print-json-strings
+ (is (= "\"Hello, World!\"" (json-str "Hello, World!")))
+ (is (= "\"\\\"Embedded\\\" Quotes\"" (json-str "\"Embedded\" Quotes"))))
+
+(deftest can-print-unicode
+ (is (= "\"\\u1234\\u4567\"" (json-str "\u1234\u4567"))))
+
+(deftest can-print-json-null
+ (is (= "null" (json-str nil))))
+
+(deftest can-print-json-arrays
+ (is (= "[1,2,3]" (json-str [1 2 3])))
+ (is (= "[1,2,3]" (json-str (list 1 2 3))))
+ (is (= "[1,2,3]" (json-str (sorted-set 1 2 3))))
+ (is (= "[1,2,3]" (json-str (seq [1 2 3])))))
+
+(deftest can-print-java-arrays
+ (is (= "[1,2,3]" (json-str (into-array [1 2 3])))))
+
+(deftest can-print-empty-arrays
+ (is (= "[]" (json-str [])))
+ (is (= "[]" (json-str (list))))
+ (is (= "[]" (json-str #{}))))
+
+(deftest can-print-json-objects
+ (is (= "{\"a\":1,\"b\":2}" (json-str (sorted-map :a 1 :b 2)))))
+
+(deftest object-keys-must-be-strings
+ (is (= "{\"1\":1,\"2\":2") (json-str (sorted-map 1 1 2 2))))
+
+(deftest can-print-empty-objects
+ (is (= "{}" (json-str {}))))
+
+(deftest accept-sequence-of-nils
+ (is (= "[null,null,null]" (json-str [nil nil nil]))))
+
+(deftest error-on-nil-keys
+ (is (thrown? Exception (json-str {nil 1}))))
+
+(deftest characters-in-symbols-are-escaped
+ (is (= "\"foo\\u1b1b\"" (json-str (symbol "foo\u1b1b")))))