;;; json/read.clj: JavaScript Object Notation (JSON) parser

;; by Stuart Sierra, http://stuartsierra.com/
;; February 13, 2009

;; Copyright (c) Stuart Sierra, 2009. 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.


;; Change Log
;;
;; February 13, 2009: added custom handler for quoted strings, to
;; allow escaped forward backslash characters ("\/") in strings.
;;
;; January 26, 2009: initial version


;; For more information on JSON, see http://www.json.org/
;;
;; This library parses data in JSON format.  This is a fairly strict
;; implementation of JSON as described at json.org, not a full-fledged
;; JavaScript parser.  JavaScript functions and object constructors
;; are not supported.  Object field names must be quoted strings; they
;; may not be bare symbols.



(ns 
  #^{:author "Stuart Sierra",
     :doc "JavaScript Object Notation (JSON) parser

           For more information on JSON, see http://www.json.org/

           This library parses data in JSON format.  This is a fairly strict
           implementation of JSON as described at json.org, not a full-fledged
           JavaScript parser.  JavaScript functions and object constructors
           are not supported.  Object field names must be quoted strings; they
           may not be bare symbols.

           If you want to convert map keys from strings to keywords, use
           clojure.contrib.walk/keywordize-keys
",
     :see-also [["http://www.json.org", "JSON Home Page"]]}
  clojure.contrib.json.read
  (:import (java.io PushbackReader StringReader EOFException))
  (:use [clojure.test :only (deftest- is)]))

(declare read-json)

(def #^{:doc "If true, JSON object keys will be converted to keywords
  instead of strings.  Defaults to false.  There are no checks that
  the strings form valid keywords."} *json-keyword-keys* false)

(defn- read-json-array [#^PushbackReader stream]
  ;; Expects to be called with the head of the stream AFTER the
  ;; opening bracket.
  (loop [i (.read stream), result []]
    (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 \]) result
       :else (do (.unread stream (int c))
                 (let [element (read-json stream)]
                   (recur (.read stream) (conj result element))))))))

(defn- read-json-object [#^PushbackReader stream]
  ;; Expects to be called with the head of the stream AFTER the
  ;; opening bracket.
  (loop [i (.read stream), key nil, result {}]
    (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)
                  result
                  (throw (Exception. "JSON error (key missing value in object)")))

       :else (do (.unread stream i)
                 (let [element (read-json stream)]
                   (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 *json-keyword-keys* (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
  "Read one JSON record from s, which may be a String or a
  java.io.PushbackReader."
  ([] (read-json *in* true nil))
  ([s] (if (string? s)
         (read-json (PushbackReader. (StringReader. s)) true nil)
         (read-json s true nil)))
  ([#^PushbackReader stream 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)

          ;; Read JSON arrays
          (= c \[) (read-json-array stream)

          :else (throw (Exception. (str "JSON error (unexpected character): " c))))))))


(defn read-json-string [string]
  (read-json (PushbackReader. (StringReader. string))))


;;; TESTS

(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-keyword-keys
  (is (= {:a [1 2 {:b [3 "four"]} 5.5]}
         (binding [*json-keyword-keys* true]
           (read-json "{\"a\":[1,2,{\"b\":[3,\"four\"]},5.5]}")))))

(declare *pass1-string*)

(deftest- pass1-test
  (let [input (read-json *pass1-string*)]
    (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\"]")