diff --git a/.github/workflows/doc-build.yml b/.github/workflows/doc-build.yml new file mode 100644 index 0000000..07265e0 --- /dev/null +++ b/.github/workflows/doc-build.yml @@ -0,0 +1,13 @@ +name: Build API Docs + +permissions: + contents: write + +on: + workflow_dispatch: + +jobs: + call-doc-build-workflow: + uses: clojure/build.ci/.github/workflows/doc-build.yml@master + with: + project: clojure/data.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..286cf95 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,22 @@ +name: Release on demand + +permissions: + contents: write + +on: + workflow_dispatch: + inputs: + releaseVersion: + description: "Version to release" + required: true + snapshotVersion: + description: "Snapshot version after release" + required: true + +jobs: + call-release: + uses: clojure/build.ci/.github/workflows/release.yml@master + with: + releaseVersion: ${{ github.event.inputs.releaseVersion }} + snapshotVersion: ${{ github.event.inputs.snapshotVersion }} + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml new file mode 100644 index 0000000..9fdad8c --- /dev/null +++ b/.github/workflows/snapshot.yml @@ -0,0 +1,11 @@ +name: Snapshot on demand + +permissions: + contents: read + +on: [workflow_dispatch] + +jobs: + call-snapshot: + uses: clojure/build.ci/.github/workflows/snapshot.yml@master + secrets: inherit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2cc441a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,10 @@ +name: Test + +permissions: + contents: read + +on: [push] + +jobs: + call-test: + uses: clojure/build.ci/.github/workflows/test.yml@master diff --git a/.gitignore b/.gitignore index 29c10dc..d6672d7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ target .idea/ *.iml .lein* +.nrepl* diff --git a/README.md b/README.md index e8eaf5e..653f3e2 100644 --- a/README.md +++ b/README.md @@ -13,23 +13,23 @@ Releases and Dependency Information This project follows the version scheme MAJOR.MINOR.PATCH where each component provides some relative indication of the size of the change, but does not follow semantic versioning. In general, all changes endeavor to be non-breaking (by moving to new names rather than by breaking existing names). -Latest stable release is [2.3.0] +Latest stable release is [2.5.2] [CLI/`deps.edn`](https://clojure.org/reference/deps_and_cli) dependency information: ```clojure -org.clojure/data.json {:mvn/version "2.3.0"} +org.clojure/data.json {:mvn/version "2.5.2"} ``` [Leiningen] dependency information: - [org.clojure/data.json "2.3.0"] + [org.clojure/data.json "2.5.2"] [Maven] dependency information: org.clojure data.json - 2.3.0 + 2.5.2 [Leiningen]: https://leiningen.org/ @@ -39,9 +39,7 @@ org.clojure/data.json {:mvn/version "2.3.0"} Other versions: * [All Released Versions](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.clojure%22%20AND%20a%3A%22data.json%22) - * [Development Snapshots](https://oss.sonatype.org/index.html#nexus-search;gav~org.clojure~data.json~~~) - * [Development Snapshot Repositories](https://clojure.org/releases/downloads#_using_clojure_snapshot_releases) @@ -145,14 +143,33 @@ Developer Information * [GitHub project](https://github.com/clojure/data.json) * [How to contribute](https://clojure.org/community/contributing) * [Bug Tracker](https://clojure.atlassian.net/browse/DJSON) -* [Continuous Integration](https://build.clojure.org/job/data.json/) -* [Compatibility Test Matrix](https://build.clojure.org/job/data.json-test-matrix/) +* [Continuous Integration](https://github.com/clojure/data.json/actions/workflows/test.yml) Change Log ---------------------------------------- +* Release [2.5.2] on 2026-Jan-02 + * Update to latest parent pom and Clojure 1.11.4 + * Fix: [DJSON-56] During `read`, better error messages for chars < 32 +* Release [2.5.1] on 2024-Nov-26 + * Fix: `read` of JSON number followed by EOF can break subsequent read on supplier PBR from seeing EOF + * Fix: `read` docstring updated to specify minimum buffer size when PushbackReader supplied (64) +* Release [2.5.0] on 2023-Dec-21 + * Fix [DJSON-50]: `read` can take a PushbackReader for repeated read use case + * Fix `write` docstring to add `:indent` option added in [DJSON-18] + * Fix [DJSON-57]: Throw better exception when EOF encountered while reading array or object + * Add [DJSON-46]: In `read`, add `:extra-data-fn` that can be provided to cause an eof check after value is read + * Add [DJSON-54]: In `write`, add custom fallback fn for writing unknown types + * Perf [DJSON-61]: Faster string writing when string is "simple" + * Perf: Faster string writing when string is not simple + * Perf: Faster `read-str` +* Release [2.4.0] on 2021-Jul-12 + * Fix [DJSON-52]: Remove Classloader workaround to support Clojure 1.2.x and below + * Fix [DJSON-53]: Move deprecated API functions from compat ns into main ns +* Release [2.3.1] on 2021-May-19 + * Fix [DJSON-51]: Fix possible read of x00's in quoted strings on partial stream read * Release [2.3.0] on 2021-May-14 * Fix [DJSON-48]: Make array parsing match spec * Fix [DJSON-18]: Make pprint-json much faster @@ -183,6 +200,7 @@ Change Log * Perf [DJSON-35]: Replace PrintWriter with more generic Appendable, reduce wrapping * Perf [DJSON-34]: More efficient writing for common path * Perf [DJSON-32]: Use option map instead of dynamic variables (affects read+write) + * NOTE: Includes a breaking change in the internal JSONWriter protocol method signature * Perf [DJSON-33]: Improve speed of reading JSON strings * Fix [DJSON-30]: Fix bad test * Release [1.1.0] on 2021-Mar-5 @@ -235,8 +253,17 @@ Change Log * Initial release. * Source-compatible with clojure.contrib.json, except for the name change. +[DJSON-61]: https://clojure.atlassian.net/browse/DJSON-61 +[DJSON-57]: https://clojure.atlassian.net/browse/DJSON-57 +[DJSON-56]: https://clojure.atlassian.net/browse/DJSON-56 +[DJSON-54]: https://clojure.atlassian.net/browse/DJSON-54 +[DJSON-53]: https://clojure.atlassian.net/browse/DJSON-53 +[DJSON-52]: https://clojure.atlassian.net/browse/DJSON-52 +[DJSON-51]: https://clojure.atlassian.net/browse/DJSON-51 +[DJSON-50]: https://clojure.atlassian.net/browse/DJSON-50 [DJSON-48]: https://clojure.atlassian.net/browse/DJSON-48 [DJSON-47]: https://clojure.atlassian.net/browse/DJSON-47 +[DJSON-46]: https://clojure.atlassian.net/browse/DJSON-46 [DJSON-45]: https://clojure.atlassian.net/browse/DJSON-45 [DJSON-43]: https://clojure.atlassian.net/browse/DJSON-43 [DJSON-41]: https://clojure.atlassian.net/browse/DJSON-41 @@ -263,12 +290,17 @@ Change Log [DJSON-7]: https://clojure.atlassian.net/browse/DJSON-7 [DJSON-1]: https://clojure.atlassian.net/browse/DJSON-1 -[2.3.0]: https://github.com/clojure/data.json/tree/data.json-2.3.0 -[2.2.3]: https://github.com/clojure/data.json/tree/data.json-2.2.3 -[2.2.2]: https://github.com/clojure/data.json/tree/data.json-2.2.2 -[2.2.1]: https://github.com/clojure/data.json/tree/data.json-2.2.1 -[2.2.0]: https://github.com/clojure/data.json/tree/data.json-2.2.0 -[2.1.1]: https://github.com/clojure/data.json/tree/data.json-2.1.1 +[2.5.2]: https://github.com/clojure/data.json/tree/v2.5.2 +[2.5.1]: https://github.com/clojure/data.json/tree/v2.5.1 +[2.5.0]: https://github.com/clojure/data.json/tree/v2.5.0 +[2.4.0]: https://github.com/clojure/data.json/tree/v2.4.0 +[2.3.1]: https://github.com/clojure/data.json/tree/v2.3.1 +[2.3.0]: https://github.com/clojure/data.json/tree/v2.3.0 +[2.2.3]: https://github.com/clojure/data.json/tree/v2.2.3 +[2.2.2]: https://github.com/clojure/data.json/tree/v2.2.2 +[2.2.1]: https://github.com/clojure/data.json/tree/v2.2.1 +[2.2.0]: https://github.com/clojure/data.json/tree/v2.2.0 +[2.1.1]: https://github.com/clojure/data.json/tree/v2.1.1 [2.1.0]: https://github.com/clojure/data.json/tree/data.json-2.1.0 [2.0.2]: https://github.com/clojure/data.json/tree/data.json-2.0.2 [2.0.1]: https://github.com/clojure/data.json/tree/data.json-2.0.1 @@ -293,10 +325,10 @@ Change Log Copyright and License ---------------------------------------- -Copyright (c) Stuart Sierra, Rich Hickey, and contriburos 2012-2020. +Copyright (c) Stuart Sierra, Rich Hickey, and contributors. All rights reserved. The use and distribution terms for this software are covered by the Eclipse Public -License 1.0 (https://opensource.org/licenses/eclipse-1.0.php) which can +License 1.0 (https://opensource.org/license/epl-1-0/) witch 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 diff --git a/deps.edn b/deps.edn index eff4f54..1fafc86 100644 --- a/deps.edn +++ b/deps.edn @@ -1,2 +1,2 @@ {:paths ["src/main/clojure"] - :deps {org.clojure/clojure {:mvn/version "1.9.0"}}} + :deps {org.clojure/clojure {:mvn/version "1.11.4"}}} diff --git a/pom.xml b/pom.xml index 6fcfde4..7af80aa 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 data.json - 2.3.1-SNAPSHOT + 2.5.3-SNAPSHOT data.json Generating/parsing JSON from/to Clojure data structures https://github.com/clojure/data.json @@ -19,7 +19,7 @@ org.clojure pom.contrib - 1.1.0 + 1.4.0 @@ -30,14 +30,14 @@ - 1.9.0 + 1.11.4 org.clojure test.check - 1.1.0 + 1.1.3 test @@ -71,7 +71,7 @@ maven-jar-plugin - 3.0.2 + 3.5.0 default-jar @@ -85,11 +85,24 @@ + + javadoc-jar + package + + jar + + + javadoc + + **/*.html + + + maven-assembly-plugin - 3.0.0 + 3.8.0 aot-jar diff --git a/project.clj b/project.clj index b367048..17f63e4 100644 --- a/project.clj +++ b/project.clj @@ -1,20 +1,19 @@ ;; NOTE: Used only for perf testing - this project is built with Maven (see pom.xml) (defproject clojure.data.json "1.1.1-SNAPSHOT" - :dependencies [[org.clojure/clojure "1.10.3"]] + :dependencies [[org.clojure/clojure "1.12.4"]] :source-paths ["src/main/clojure"] :java-source-paths ["src/main/java"] :java-test-paths ["src/test/java"] :test-paths ["src/test/clojure" "src/test/clojure-perf"] - :profiles {:dev {:dependencies [[com.clojure-goes-fast/clj-async-profiler "0.5.0"] - [com.clojure-goes-fast/clj-java-decompiler "0.3.0"] - [org.clojure/test.check "1.1.0"] + :profiles {:dev {:dependencies [[com.clojure-goes-fast/clj-async-profiler "1.6.2"] + [com.clojure-goes-fast/clj-java-decompiler "0.3.7"] + [org.clojure/test.check "1.1.3"] [criterium/criterium "0.4.6"] - [metosin/jsonista "0.3.1"] - [cheshire/cheshire "5.10.0"] - [org.openjdk.jmh/jmh-core "1.28"] - [jmh-clojure "0.4.0"] + [metosin/jsonista "0.3.13"] + [cheshire/cheshire "6.1.0"] + [org.openjdk.jmh/jmh-core "1.37"] + [jmh-clojure "0.4.1"] [com.jsoniter/jsoniter "0.9.23"]] :resource-paths ["dev-resources"] :global-vars {*warn-on-reflection* true}}} - ;;:plugins [[lein-nodisassemble "0.1.3"]] :jvm-opts ["-Djdk.attach.allowAttachSelf=true"]) diff --git a/src/main/clojure/clojure/data/json.clj b/src/main/clojure/clojure/data/json.clj index 747a1ab..8afcd14 100644 --- a/src/main/clojure/clojure/data/json.clj +++ b/src/main/clojure/clojure/data/json.clj @@ -15,6 +15,72 @@ (:import (java.io PrintWriter PushbackReader StringWriter Writer StringReader EOFException))) +;; CUSTOM PUSHBACK READER + +(set! *warn-on-reflection* true) + +(definterface InternalPBR + (^int readChar []) + (^long readChars [^chars buffer ^long start ^long bufflen]) + (^void unreadChar [^int c]) + (^void unreadChars [^chars buffer ^int off ^int bufflen]) + (^java.io.Reader toReader [])) + +(deftype ReaderPBR [^PushbackReader rdr] + InternalPBR + (readChar [_] + (.read rdr)) + (readChars [_ buffer start bufflen] + (.read rdr ^chars buffer start bufflen)) + (unreadChar [_ c] + ;; ASSERT: c should never be -1 (EOF) + (.unread rdr c)) + (unreadChars [_ buffer start bufflen] + (.unread rdr buffer start bufflen)) + (toReader [_] + rdr)) + +(comment + (compile 'clojure.data.json) + ) + +(deftype StringPBR [^String s ^:unsynchronized-mutable ^long pos ^long len] + InternalPBR + (readChar [_] + (if (< pos len) + (let [p pos] + (set! pos (unchecked-inc pos)) + (let [c (int (.charAt s p))] + c)) + (let [i (int -1)] + i))) + (readChars [_ buffer start bufflen] + (let [remaining (- len pos) + n (Math/min remaining bufflen)] + (when (pos? n) + (let [p pos + end (+ p n)] + (set! pos end) + (.getChars ^String s p end ^chars buffer start))) + (if (pos? n) n -1))) + (unreadChar [_ _c] + ;; ASSERT: c should never be -1 (EOF) + (set! pos (unchecked-dec pos)) + nil) + (unreadChars [_ _buffer _start bufflen] + (set! pos (unchecked-subtract pos bufflen)) + nil) + (toReader [_] + (StringReader. (.subSequence s pos len)))) + +(defn- pushback-pbr + [^PushbackReader r] + (->ReaderPBR r)) + +(defn- string-pbr + [^String s] + (->StringPBR s 0 (.length s))) + ;;; JSON READER (set! *warn-on-reflection* true) @@ -50,23 +116,23 @@ ~@(when (odd? (count clauses)) [(last clauses)]))) -(defn- read-hex-char [^PushbackReader stream] +(defn- read-hex-char [^InternalPBR stream] ;; Expects to be called with the head of the stream AFTER the ;; initial "\u". Reads the next four characters from the stream. - (let [a (.read stream) - b (.read stream) - c (.read stream) - d (.read stream)] + (let [a (.readChar stream) + b (.readChar stream) + c (.readChar stream) + d (.readChar stream)] (when (or (neg? a) (neg? b) (neg? c) (neg? d)) (throw (EOFException. "JSON error (end-of-file inside Unicode character escape)"))) (let [s (str (char a) (char b) (char c) (char d))] (char (Integer/parseInt s 16))))) -(defn- read-escaped-char [^PushbackReader stream] +(defn- read-escaped-char [^InternalPBR stream] ;; Expects to be called with the head of the stream AFTER the ;; initial backslash. - (let [c (.read stream)] + (let [c (.readChar stream)] (when (neg? c) (throw (EOFException. "JSON error (end-of-file inside escaped char)"))) (codepoint-case c @@ -78,10 +144,10 @@ \t \tab \u (read-hex-char stream)))) -(defn- slow-read-string [^PushbackReader stream ^String already-read] +(defn- slow-read-string [^InternalPBR stream ^String already-read] (let [buffer (StringBuilder. already-read)] (loop [] - (let [c (.read stream)] + (let [c (.readChar stream)] (when (neg? c) (throw (EOFException. "JSON error (end-of-file inside string)"))) (codepoint-case c @@ -91,11 +157,12 @@ (do (.append buffer (char c)) (recur))))))) -(defn- read-quoted-string [^PushbackReader stream] +(defn- read-quoted-string [^InternalPBR stream] ;; Expects to be called with the head of the stream AFTER the ;; opening quotation mark. (let [buffer ^chars (char-array 64) - read (.read stream buffer 0 64)] + read (.readChars stream buffer 0 64) + end-index (unchecked-dec-int read)] (when (neg? read) (throw (EOFException. "JSON error (end-of-file inside string)"))) (loop [i (int 0)] @@ -103,14 +170,14 @@ (codepoint-case c \" (let [off (unchecked-inc-int i) len (unchecked-subtract-int read off)] - (.unread stream buffer off len) + (.unreadChars stream buffer off len) (String. buffer 0 i)) \\ (let [off i len (unchecked-subtract-int read off)] - (.unread stream buffer off len) + (.unreadChars stream buffer off len) (slow-read-string stream (String. buffer 0 i))) - (if (= i (dec read)) - (do (.unread stream c) + (if (= i end-index) + (do (.unreadChar stream c) (slow-read-string stream (String. buffer 0 i))) (recur (unchecked-inc-int i)))))))) @@ -126,10 +193,10 @@ (bigdec string) (Double/valueOf string))) -(defn- read-number [^PushbackReader stream bigdec?] +(defn- read-number [^InternalPBR stream bigdec?] (let [buffer (StringBuilder.) decimal? (loop [stage :minus] - (let [c (.read stream)] + (let [c (.readChar stream)] (case stage :minus (codepoint-case c @@ -167,11 +234,13 @@ (recur :exp-symbol)) ;; early exit :whitespace - (do (.unread stream c) + (do (.unreadChar stream c) false) - (\, \] \} -1) - (do (.unread stream c) + (\, \] \}) + (do (.unreadChar stream c) false) + -1 + false (throw (Exception. "JSON error (invalid number literal)"))) ;; previous character is a "0" :frac-point @@ -184,11 +253,13 @@ (recur :exp-symbol)) ;; early exit :whitespace - (do (.unread stream c) + (do (.unreadChar stream c) false) - (\, \] \} -1) - (do (.unread stream c) + (\, \] \}) + (do (.unreadChar stream c) false) + -1 + false ;; Disallow zero-padded numbers or invalid characters (throw (Exception. "JSON error (invalid number literal)"))) ;; previous character is a "." @@ -209,11 +280,13 @@ (recur :exp-symbol)) ;; early exit :whitespace - (do (.unread stream c) + (do (.unreadChar stream c) true) - (\, \] \} -1) - (do (.unread stream c) + (\, \] \}) + (do (.unreadChar stream c) true) + -1 + true (throw (Exception. "JSON error (invalid number literal)"))) ;; previous character is a "e" or "E" :exp-symbol @@ -239,37 +312,43 @@ (do (.append buffer (char c)) (recur :exp-digit)) :whitespace - (do (.unread stream c) + (do (.unreadChar stream c) true) - (\, \] \} -1) - (do (.unread stream c) + (\, \] \}) + (do (.unreadChar stream c) true) + -1 + true (throw (Exception. "JSON error (invalid number literal)"))))))] (if decimal? (read-decimal (str buffer) bigdec?) (read-integer (str buffer))))) -(defn- next-token [^PushbackReader stream] - (loop [c (.read stream)] +(defn- next-token [^InternalPBR stream] + (loop [c (.readChar stream)] (if (< 32 c) (int c) (codepoint-case (int c) - :whitespace (recur (.read stream)) - -1 -1)))) + :whitespace (recur (.readChar stream)) + c)))) (defn invalid-array-exception [] (Exception. "JSON error (invalid array)")) -(defn- read-array* [^PushbackReader stream options] +(defn- eof-array-exception [] + (EOFException. "JSON error (EOF in array)")) + +(defn- read-array* [^InternalPBR stream options] ;; Handles all array values after the first. (loop [result (transient [])] (let [r (conj! result (-read stream true nil options))] (codepoint-case (int (next-token stream)) \] (persistent! r) \, (recur r) + -1 (throw (eof-array-exception)) (throw (invalid-array-exception)))))) -(defn- read-array [^PushbackReader stream options] +(defn- read-array [^InternalPBR stream options] ;; Expects to be called with the head of the stream AFTER the ;; opening bracket. ;; Only handles array value. @@ -277,21 +356,37 @@ (codepoint-case c \] [] \, (throw (invalid-array-exception)) - (do (.unread stream c) + -1 (throw (eof-array-exception)) + (do (.unreadChar stream c) (read-array* stream options))))) -(defn- read-key [^PushbackReader stream] +(defn- object-colon-exception [] + (Exception. "JSON error (missing `:` in object)")) + +(defn- eof-object-exception [] + (EOFException. "JSON error (EOF in object)")) + +(defn- invalid-key-exception [c] + (if (= c -1) + (throw (eof-object-exception)) + (throw (Exception. (str "JSON error (non-string key in object), found `" (char c) "`, expected `\"`"))))) + +(comment + (compile 'clojure.data.json) + ) + +(defn- read-key [^InternalPBR stream] (let [c (int (next-token stream))] (if (= c (codepoint \")) (let [key (read-quoted-string stream)] (if (= (codepoint \:) (int (next-token stream))) key - (throw (Exception. "JSON error (missing `:` in object)")))) + (throw (object-colon-exception)))) (if (= c (codepoint \})) nil - (throw (Exception. (str "JSON error (non-string key in object), found `" (char c) "`, expected `\"`"))))))) + (invalid-key-exception c))))) -(defn- read-object [^PushbackReader stream options] +(defn- read-object [^InternalPBR stream options] ;; Expects to be called with the head of the stream AFTER the ;; opening bracket. (let [key-fn (get options :key-fn) @@ -309,6 +404,7 @@ (codepoint-case (int (next-token stream)) \, (recur r) \} (persistent! r) + -1 (throw (eof-object-exception)) (throw (Exception. "JSON error (missing entry in object)")))) (let [r (persistent! result)] (if (empty? r) @@ -316,36 +412,36 @@ (throw (Exception. "JSON error empty entry in object is not allowed")))))))) (defn- -read - [^PushbackReader stream eof-error? eof-value options] + [^InternalPBR stream eof-error? eof-value options] (let [c (int (next-token stream))] (codepoint-case c ;; Read numbers (\- \0 \1 \2 \3 \4 \5 \6 \7 \8 \9) - (do (.unread stream c) + (do (.unreadChar stream c) (read-number stream (:bigdec options))) ;; Read strings \" (read-quoted-string stream) ;; Read null as nil - \n (if (and (= (codepoint \u) (.read stream)) - (= (codepoint \l) (.read stream)) - (= (codepoint \l) (.read stream))) + \n (if (and (= (codepoint \u) (.readChar stream)) + (= (codepoint \l) (.readChar stream)) + (= (codepoint \l) (.readChar stream))) nil (throw (Exception. "JSON error (expected null)"))) ;; Read true - \t (if (and (= (codepoint \r) (.read stream)) - (= (codepoint \u) (.read stream)) - (= (codepoint \e) (.read stream))) + \t (if (and (= (codepoint \r) (.readChar stream)) + (= (codepoint \u) (.readChar stream)) + (= (codepoint \e) (.readChar stream))) true (throw (Exception. "JSON error (expected true)"))) ;; Read false - \f (if (and (= (codepoint \a) (.read stream)) - (= (codepoint \l) (.read stream)) - (= (codepoint \s) (.read stream)) - (= (codepoint \e) (.read stream))) + \f (if (and (= (codepoint \a) (.readChar stream)) + (= (codepoint \l) (.readChar stream)) + (= (codepoint \s) (.readChar stream)) + (= (codepoint \e) (.readChar stream))) false (throw (Exception. "JSON error (expected false)"))) @@ -362,12 +458,45 @@ (throw (Exception. (str "JSON error (unexpected character): " (char c)))))))) +(defn- -read1 + [^InternalPBR stream eof-error? eof-value options] + (let [val (-read stream eof-error? eof-value options)] + (if-let [extra-data-fn (:extra-data-fn options)] + (if (or eof-error? (not (identical? eof-value val))) + (let [c (.readChar stream)] + (if (neg? c) + val + (do + (.unreadChar stream c) + (extra-data-fn val (.toReader stream))))) + val) + val))) + +(defn on-extra-throw + "Pass as :extra-data-fn to `read` or `read-str` to throw if data is found + after the first object." + [val rdr] + (throw (ex-info "Found extra data after json object" {:val val}))) + +(defn on-extra-throw-remaining + "Pass as :extra-data-fn to `read` or `read-str` to throw if data is found + after the first object and return the remaining data in ex-data :remaining." + [val rdr] + (let [remaining (slurp rdr)] + (throw (ex-info (str "Found extra data after json object: " remaining) + {:val val, :remaining remaining})))) + (def default-read-options {:bigdec false :key-fn nil :value-fn nil}) (defn read - "Reads a single item of JSON data from a java.io.Reader. Options are - key-value pairs, valid options are: + "Reads a single item of JSON data from a java.io.Reader. + + If you wish to repeatedly read items from the same reader, you must + supply a PushbackReader with buffer size >= 64, and reuse it on + subsequent calls. + + Options are key-value pairs, valid options are: :eof-error? boolean @@ -399,13 +528,24 @@ in the output. If value-fn returns itself, the property will be omitted from the output. The default value-fn returns the value unchanged. This option does not apply to non-map - collections." + collections. + + :extra-data-fn function + + If :extra-data-fn is not nil, then the reader will be checked + for extra data after the read. If found, the extra-data-fn will + be invoked with the read value and the reader. The result of + the extra-data-fn will be returned." [reader & {:as options}] (let [{:keys [eof-error? eof-value] - :or {eof-error? true}} options] + :or {eof-error? true}} options + pbr (pushback-pbr + (if (instance? PushbackReader reader) + reader + (PushbackReader. reader 64)))] (->> options (merge default-read-options) - (-read (PushbackReader. reader 64) eof-error? eof-value)))) + (-read1 pbr eof-error? eof-value)))) (defn read-str "Reads one JSON value from input String. Options are the same as for @@ -415,7 +555,7 @@ :or {eof-error? true}} options] (->> options (merge default-read-options) - (-read (PushbackReader. (StringReader. string) 64) eof-error? eof-value)))) + (-read1 (string-pbr string) eof-error? eof-value)))) ;;; JSON WRITER @@ -453,16 +593,18 @@ (aset shorts i (short 0))))) shorts)) -(defn- write-string [^CharSequence s ^Appendable out options] - (let [decoder codepoint-decoder] - (.append out \") +(defn- slow-write-string [^CharSequence s ^Appendable out options] + (let [decoder codepoint-decoder + slash (get options :escape-slash) + escape-js-separators (get options :escape-js-separators) + escape-unicode (get options :escape-unicode)] (dotimes [i (.length s)] (let [cp (int (.charAt s i))] (if (< cp 128) (case (aget decoder cp) 0 (.append out (char cp)) 1 (do (.append out (char (codepoint \\))) (.append out (char cp))) - 2 (.append out (if (get options :escape-slash) "\\/" "/")) + 2 (.append out (if slash "\\/" "/")) 3 (.append out "\\b") 4 (.append out "\\f") 5 (.append out "\\n") @@ -470,12 +612,27 @@ 7 (.append out "\\t") 8 (->hex-string out cp)) (codepoint-case cp - :js-separators (if (get options :escape-js-separators) + :js-separators (if escape-js-separators (->hex-string out cp) (.append out (char cp))) - (if (get options :escape-unicode) + (if escape-unicode (->hex-string out cp) ; Hexadecimal-escaped - (.append out (char cp))))))) + (.append out (char cp))))))))) + +(defn- write-string [^CharSequence s ^Appendable out options] + (let [decoder codepoint-decoder + l (.length s)] + (.append out \") + (loop [i 0] + (if (= i l) + (.append out s) + (let [cp (int (.charAt s i))] + (if (and (< cp 128) + (zero? (aget decoder cp))) + (recur (unchecked-inc i)) + (do + (.append out s 0 i) + (slow-write-string (.subSequence s i l) out options)))))) (.append out \"))) (defn- write-indent [^Appendable out options] @@ -595,7 +752,7 @@ (defn- write-generic [x out options] (if (.isArray (class x)) (-write (seq x) out options) - (throw (Exception. (str "Don't know how to write JSON of " (class x)))))) + ((:default-write-fn options) x out options))) (defn- write-ratio [x out options] (-write (double x) out options)) @@ -620,12 +777,7 @@ (extend java.time.Instant JSONWriter {:-write write-instant}) (extend java.util.Date JSONWriter {:-write write-date}) (extend java.sql.Date JSONWriter {:-write write-sql-date}) -;; Attempt to support Clojure 1.2.x: -(when-let [class (try (.. Thread currentThread getContextClassLoader - (loadClass "clojure.lang.BigInt")) - (catch ClassNotFoundException _ false))] - (extend class JSONWriter {:-write write-bignum})) - +(extend clojure.lang.BigInt JSONWriter {:-write write-bignum}) ;; Symbols, Keywords, and Strings (extend clojure.lang.Named JSONWriter {:-write write-named}) @@ -638,6 +790,9 @@ ;; Maybe a Java array, otherwise fail (extend java.lang.Object JSONWriter {:-write write-generic}) +(defn- default-write-fn [x out options] + (throw (Exception. (str "Don't know how to write JSON of " (class x))))) + (def default-write-options {:escape-unicode true :escape-js-separators true :escape-slash true @@ -645,8 +800,10 @@ :date-formatter java.time.format.DateTimeFormatter/ISO_INSTANT :key-fn default-write-key-fn :value-fn default-value-fn + :default-write-fn default-write-fn :indent false - :indent-depth 0}) + :indent-depth 0 ;; internal, to track nesting depth + }) (defn write "Write JSON-formatted output to a java.io.Writer. Options are key-value pairs, valid options are: @@ -700,7 +857,18 @@ the return value is a map, it will be processed recursively, calling value-fn again on its key-value pairs. If value-fn returns itself, the key-value pair will be omitted from the - output. This option does not apply to non-map collections." + output. This option does not apply to non-map collections. + + :default-write-fn function + + Function to handle types which are unknown to data.json. Defaults + to a function which throws an exception. Expects to be called with + three args, the value to be serialized, the output stream, and the + options map. + + :indent boolean + + If true, indent json while writing (default = false)." [x ^Writer writer & {:as options}] (-write x writer (merge default-write-options options))) @@ -738,16 +906,76 @@ :else (pprint-generic x options))) (defn pprint - "Pretty-prints JSON representation of x to *out*. Options are the - same as for write except :value-fn, which is not supported." + "Pretty-prints JSON representation of x to *out*. Options are the same + as for write except :value-fn and :indent, which are not supported." [x & {:as options}] (let [opts (merge default-write-options options)] (pprint/with-pprint-dispatch #(pprint-dispatch % opts) (pprint/pprint x)))) -(load "json_compat_0_1") - -;; Local Variables: -;; mode: clojure -;; eval: (define-clojure-indent (codepoint-case (quote defun))) -;; End: +;; DEPRECATED APIs from 0.1.x + +(defn read-json + "DEPRECATED; replaced by read-str. + + 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 input true true nil)) + ([input keywordize?] + (read-json input keywordize? true nil)) + ([input keywordize? eof-error? eof-value] + (let [key-fn (if keywordize? keyword identity)] + (condp instance? input + String + (read-str input + :key-fn key-fn + :eof-error? eof-error? + :eof-value eof-value) + java.io.Reader + (read input + :key-fn key-fn + :eof-error? eof-error? + :eof-value eof-value))))) + +(defn write-json + "DEPRECATED; replaced by 'write'. + + Print object to PrintWriter out as JSON" + [x out escape-unicode?] + (write x out :escape-unicode escape-unicode?)) + +(defn json-str + "DEPRECATED; replaced by 'write-str'. + + Converts x to a JSON-formatted string. + + Valid options are: + :escape-unicode false + to turn of \\uXXXX escapes of Unicode characters." + [x & options] + (apply write-str x options)) + +(defn print-json + "DEPRECATED; replaced by 'write' to *out*. + + Write JSON-formatted output to *out*. + + Valid options are: + :escape-unicode false + to turn off \\uXXXX escapes of Unicode characters." + [x & options] + (apply write x *out* options)) + +(defn pprint-json + "DEPRECATED; replaced by 'pprint'. + + Pretty-prints JSON representation of x to *out*. + + Valid options are: + :escape-unicode false + to turn off \\uXXXX escapes of Unicode characters." + [x & options] + (apply pprint x options)) diff --git a/src/main/clojure/clojure/data/json_compat_0_1.clj b/src/main/clojure/clojure/data/json_compat_0_1.clj deleted file mode 100644 index 1455623..0000000 --- a/src/main/clojure/clojure/data/json_compat_0_1.clj +++ /dev/null @@ -1,73 +0,0 @@ -;; Copyright (c) Stuart Sierra, 2012. 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) -;; 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. - -(in-ns 'clojure.data.json) - -(defn read-json - "DEPRECATED; replaced by read-str. - - 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 input true true nil)) - ([input keywordize?] - (read-json input keywordize? true nil)) - ([input keywordize? eof-error? eof-value] - (let [key-fn (if keywordize? keyword identity)] - (condp instance? input - String - (read-str input - :key-fn key-fn - :eof-error? eof-error? - :eof-value eof-value) - java.io.Reader - (read input - :key-fn key-fn - :eof-error? eof-error? - :eof-value eof-value))))) - -(defn write-json - "DEPRECATED; replaced by 'write'. - - Print object to PrintWriter out as JSON" - [x out escape-unicode?] - (write x out :escape-unicode escape-unicode?)) - -(defn json-str - "DEPRECATED; replaced by 'write-str'. - - Converts x to a JSON-formatted string. - - Valid options are: - :escape-unicode false - to turn of \\uXXXX escapes of Unicode characters." - [x & options] - (apply write-str x options)) - -(defn print-json - "DEPRECATED; replaced by 'write' to *out*. - - Write JSON-formatted output to *out*. - - Valid options are: - :escape-unicode false - to turn off \\uXXXX escapes of Unicode characters." - [x & options] - (apply write x *out* options)) - -(defn pprint-json - "DEPRECATED; replaced by 'pprint'. - - Pretty-prints JSON representation of x to *out*. - - Valid options are: - :escape-unicode false - to turn off \\uXXXX escapes of Unicode characters." - [x & options] - (apply pprint x options)) diff --git a/src/test/clojure/clojure/data/json_test.clj b/src/test/clojure/clojure/data/json_test.clj index 70ea218..aa6d79c 100644 --- a/src/test/clojure/clojure/data/json_test.clj +++ b/src/test/clojure/clojure/data/json_test.clj @@ -3,9 +3,47 @@ [clojure.test :refer :all] [clojure.string :as str])) +(defn pbr + ([s] + (pbr s 64)) + ([s size] + (if (< size 64) + (throw (RuntimeException. "Size must be >= 64")) + (java.io.PushbackReader. (java.io.StringReader. s) size)))) + (deftest read-from-pushback-reader - (let [s (java.io.PushbackReader. (java.io.StringReader. "42"))] - (is (= 42 (json/read s))))) + (is (= 42 (json/read (pbr "42")))) + (is (= ["abc" "def"] (json/read (pbr "[\"abc\", \"def\"]"))))) + +;; DJSON-50 - pass PBR to safely do repeated read +(deftest read-multiple + (let [st "{\"foo\":\"some string\"}{\"foo\":\"another string\"}" + pbr (pbr st)] + (is (= {"foo" "some string"} (json/read pbr))) + (is (= {"foo" "another string"} (json/read pbr)))) + + (let [st "{\"foo\":\"some string\"}{\"foo\":\"another long ......................................................... string\"}" + pbr (pbr st)] + (is (= {"foo" "some string"} (json/read pbr))) + (is (= {"foo" "another long ......................................................... string"} (json/read pbr))))) + +(defn read-then-eof [s] + (let [r (pbr s) + val (json/read r :eof-error? false :eof-value :EOF)] + (is (= :EOF (json/read r :eof-error? false :eof-value :EOF))) + val)) + +(deftest read-multiple-eof + (are [expected s] (= expected (read-then-eof s)) + 1.2 "1.2" + 0 "0" + 1 "1" + 1.0 "1.0" + "abc" "\"abc\"" + "\u2202" "\"\u2202\"" + [] "[]" + [1 2] "[1, 2]") + ) (deftest read-from-reader (let [s (java.io.StringReader. "42")] @@ -21,6 +59,35 @@ (is (= 123456789012345678901234567890N (json/read-str "123456789012345678901234567890")))) +(deftest lenient-on-extra-data + (is (= [42] (json/read-str "[42],abc"))) + (is (= [42] (json/read (java.io.StringReader. "[42],abc"))))) + +(deftest strict-on-extra-data + ;; on-extra-throw + (is (thrown? clojure.lang.ExceptionInfo + (json/read-str "[42],abc" :extra-data-fn json/on-extra-throw))) + (is (thrown? clojure.lang.ExceptionInfo + (json/read (java.io.StringReader. "[42],abc") :extra-data-fn json/on-extra-throw))) + + ;; on-extra-throw-remaining + (try + (json/read-str "[42],abc" :extra-data-fn json/on-extra-throw-remaining) + (is false "expected exception to be thrown") + (catch clojure.lang.ExceptionInfo e + (is (= ",abc" (:remaining (ex-data e)))))) + (try + (json/read-str "[1], 1]" :extra-data-fn json/on-extra-throw-remaining) + (is false "expected exception to be thrown") + (catch clojure.lang.ExceptionInfo e + (is (= ", 1]" (:remaining (ex-data e)))))) + + ;; check that empty input behavior not modified when :extra-data-fn specified + (is (= :hi (json/read-str "" + :eof-error? false, :eof-value :hi, :extra-data-fn json/on-extra-throw))) + (is (= :hi (json/read (java.io.StringReader. "") + :eof-error? false, :eof-value :hi, :extra-data-fn json/on-extra-throw)))) + (deftest write-bigint (is (= "123456789012345678901234567890" (json/write-str 123456789012345678901234567890N)))) @@ -122,7 +189,7 @@ :key-fn keyword :value-fn (fn [k v] (if (= :date k) - (java.sql.Date/valueOf v) + (java.sql.Date/valueOf ^String v) v)))))) (deftest omit-values @@ -364,6 +431,18 @@ (is (thrown? java.io.EOFException (json/read-str "\"\\")))) +(deftest throws-eof-in-arrays + (is (thrown? java.io.EOFException + (json/read-str "[1,"))) + (is (thrown? java.io.EOFException + (json/read-str "[1,2,")))) + +(deftest throws-eof-in-objects + (is (thrown? java.io.EOFException + (json/read-str "{"))) + (is (thrown? java.io.EOFException + (json/read-str "{\"\":1,")))) + (deftest accept-eof (is (= ::eof (json/read-str "" :eof-error? false :eof-value ::eof)))) @@ -423,3 +502,13 @@ (dotimes [_ 1000] (assert (= (json/read-str pass1-string) (json/read-str (json/write-str (json/read-str pass1-string))))))))) + +(defn djson-54-default-write-fn [x out options] + (#'json/write-string (str x) out options)) + +(deftest DJSON-54-test + (is (thrown? Exception (json/write-str {:foo (java.net.URI. "http://clojure.org")}))) + (try (json/write-str {:foo (java.net.URI. "http://clojure.org")}) + (catch Exception e + (is (= "Don't know how to write JSON of class java.net.URI" (.getMessage e))))) + (is (= "{\"foo\":\"http:\\/\\/clojure.org\"}" (json/write-str {:foo (java.net.URI. "http://clojure.org")} :default-write-fn djson-54-default-write-fn))))