diff --git a/CHANGELOG.md b/CHANGELOG.md index 080abbc0..6e3378ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ A preview of the next release can be installed from [Babashka](https://github.com/babashka/babashka): Native, fast starting Clojure interpreter for scripting +## Unreleased + +- [#1806](https://github.com/babashka/babashka/issues/1806): Add `cheshire.factory` namespace ([@lread](https://github.com/lread)) + ## 1.12.198 (2024-04-17) - Bump GraalVM to `24` diff --git a/doc/libraries.csv b/doc/libraries.csv index 398b1760..1277283a 100644 --- a/doc/libraries.csv +++ b/doc/libraries.csv @@ -9,6 +9,7 @@ borkdude/missing.test.assertions,https://github.com/borkdude/missing.test.assert borkdude/rewrite-edn,https://github.com/borkdude/rewrite-edn camel-snake-kebab/camel-snake-kebab,https://github.com/clj-commons/camel-snake-kebab cc.qbits/auspex,https://github.com/mpenet/auspex +cheshire/cheshire,https://github.com/dakrone/cheshire circleci/bond,https://github.com/circleci/bond cli-matic/cli-matic,https://github.com/l3nz/cli-matic.git clj-commons/clj-yaml,https://github.com/clj-commons/clj-yaml diff --git a/src/babashka/impl/cheshire.clj b/src/babashka/impl/cheshire.clj index 4e89afcb..832c09dd 100644 --- a/src/babashka/impl/cheshire.clj +++ b/src/babashka/impl/cheshire.clj @@ -1,25 +1,120 @@ (ns babashka.impl.cheshire {:no-doc true} (:require [cheshire.core :as json] + [cheshire.factory :as fact] [sci.core :as sci :refer [copy-var]])) (def tns (sci/create-ns 'cheshire.core nil)) +(def fns (sci/create-ns 'cheshire.factory nil)) + +(def json-factory (sci/new-dynamic-var '*json-factory* nil {:ns fns})) + +;; wrap cheshire fns to support `*json-factory*` dynamic var + +(defn generate-string + ([obj] + (binding [fact/*json-factory* @json-factory] + (json/generate-string obj))) + ([obj opt-map] + (binding [fact/*json-factory* @json-factory] + (json/generate-string obj opt-map)))) + +(defn generate-stream + ([obj writer] + (binding [fact/*json-factory* @json-factory] + (json/generate-stream obj writer))) + ([obj writer opt-map] + (binding [fact/*json-factory* @json-factory] + (json/generate-stream obj writer opt-map)))) + +(defn parse-string + ([string] + (when string + (binding [fact/*json-factory* @json-factory] + (json/parse-string string)))) + ([string key-fn] + (when string + (binding [fact/*json-factory* @json-factory] + (json/parse-string string key-fn)))) + ([^String string key-fn array-coerce-fn] + (when string + (binding [fact/*json-factory* @json-factory] + (json/parse-string string key-fn array-coerce-fn))))) + +(defn parse-string-strict + ([string] + (when string + (binding [fact/*json-factory* @json-factory] + (json/parse-string-strict string)))) + ([string key-fn] + (when string + (binding [fact/*json-factory* @json-factory] + (json/parse-string-strict string key-fn)))) + ([^String string key-fn array-coerce-fn] + (when string + (binding [fact/*json-factory* @json-factory] + (json/parse-string-strict string key-fn array-coerce-fn))))) + +(defn parse-stream + ([rdr] + (when rdr + (binding [fact/*json-factory* @json-factory] + (json/parse-stream rdr)))) + ([rdr key-fn] + (when rdr + (binding [fact/*json-factory* @json-factory] + (json/parse-stream rdr key-fn)))) + ([rdr key-fn array-coerce-fn] + (when rdr + (binding [fact/*json-factory* @json-factory] + (json/parse-stream rdr key-fn array-coerce-fn))))) + +(defn parse-stream-strict + ([rdr] + (when rdr + (binding [fact/*json-factory* @json-factory] + (json/parse-stream-strict rdr)))) + ([rdr key-fn] + (when rdr + (binding [fact/*json-factory* @json-factory] + (json/parse-stream-strict rdr key-fn)))) + ([rdr key-fn array-coerce-fn] + (when rdr + (binding [fact/*json-factory* @json-factory] + (json/parse-stream-strict rdr key-fn array-coerce-fn))))) + +(defn parsed-seq + ([reader] + (binding [fact/*json-factory* @json-factory] + (json/parsed-seq reader))) + ([reader key-fn] + (binding [fact/*json-factory* @json-factory] + (json/parsed-seq reader key-fn))) + ([reader key-fn array-coerce-fn] + (binding [fact/*json-factory* @json-factory] + (json/parsed-seq reader key-fn array-coerce-fn)))) (def cheshire-core-namespace - {'encode (copy-var json/encode tns) - 'generate-string (copy-var json/generate-string tns) - 'encode-stream (copy-var json/encode-stream tns) - 'generate-stream (copy-var json/generate-stream tns) + {'encode (copy-var generate-string tns) + 'generate-string (copy-var generate-string tns) + 'encode-stream (copy-var generate-stream tns) + 'generate-stream (copy-var generate-stream tns) ;;'encode-smile (copy-var json/encode-smile tns) ;;'generate-smile (copy-var json/generate-smile tns) - 'decode (copy-var json/decode tns) - 'parse-string (copy-var json/parse-string tns) - 'parse-string-strict (copy-var json/parse-string-strict tns) + 'decode (copy-var parse-string tns) + 'parse-string (copy-var parse-string tns) + 'parse-string-strict (copy-var parse-string-strict tns) ;;'parse-smile (copy-var json/parse-smile tns) - 'parse-stream (copy-var json/parse-stream tns) - 'parse-stream-strict (copy-var json/parse-stream-strict tns) - 'parsed-seq (copy-var json/parsed-seq tns) + 'parse-stream (copy-var parse-stream tns) + 'parse-stream-strict (copy-var parse-stream-strict tns) + 'parsed-seq (copy-var parsed-seq tns) ;;'parsed-smile-seq (copy-var json/parsed-smile-seq tns) ;;'decode-smile (copy-var json/decode-smile tns) 'default-pretty-print-options (copy-var json/default-pretty-print-options tns) 'create-pretty-printer (copy-var json/create-pretty-printer tns)}) + +(def cheshire-factory-namespace + {'*json-factory* json-factory + 'default-factory-options (copy-var fact/default-factory-options fns) + 'json-factory (copy-var fact/json-factory fns) + 'make-json-factory (copy-var fact/make-json-factory fns)}) diff --git a/src/babashka/main.clj b/src/babashka/main.clj index b2affbc6..773cc7fb 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -6,7 +6,7 @@ [babashka.deps :as bdeps] [babashka.fs :as fs] [babashka.impl.bencode :refer [bencode-namespace]] - [babashka.impl.cheshire :refer [cheshire-core-namespace]] + [babashka.impl.cheshire :refer [cheshire-core-namespace cheshire-factory-namespace]] [babashka.impl.classes :as classes :refer [classes-namespace]] [babashka.impl.classpath :as cp :refer [classpath-namespace]] [babashka.impl.cli :as cli] @@ -379,6 +379,7 @@ Use bb run --help to show this help output. 'babashka.signal signal-ns 'clojure.java.io io-namespace 'cheshire.core cheshire-core-namespace + 'cheshire.factory cheshire-factory-namespace 'clojure.data data/data-namespace 'clojure.instant instant/instant-namespace 'clojure.stacktrace stacktrace-namespace diff --git a/test-resources/lib_tests/bb-tested-libs.edn b/test-resources/lib_tests/bb-tested-libs.edn index 00a68e87..1b32339f 100644 --- a/test-resources/lib_tests/bb-tested-libs.edn +++ b/test-resources/lib_tests/bb-tested-libs.edn @@ -200,4 +200,5 @@ hickory.test.hiccup-utils hickory.test.render hickory.test.select - hickory.test.zip]}} + hickory.test.zip]} + cheshire/cheshire {:git-url "https://github.com/dakrone/cheshire", :test-namespaces [cheshire.test.core], :manually-added true}} diff --git a/test-resources/lib_tests/cheshire/test/core.clj b/test-resources/lib_tests/cheshire/test/core.clj new file mode 100644 index 00000000..0722eab2 --- /dev/null +++ b/test-resources/lib_tests/cheshire/test/core.clj @@ -0,0 +1,793 @@ +(ns cheshire.test.core + (:require [clojure.test :refer [deftest testing is are]] + [clojure.java.io :as io] + [clojure.string :as str] + [cheshire.core :as json] + ;; BB-TEST-PATCH: bb does not include cheshire.exact + #_[cheshire.exact :as json-exact] + ;; BB-TEST-PATCH: bb does not include cheshire.gen + #_[cheshire.generate :as gen] + [cheshire.factory :as fact] + ;; BB-TEST-PATCH: bb does not include cheshire.parse + #_[cheshire.parse :as parse]) + (:import ;; BB-TEST-PATCH: tests adjusted to check for general Exception instead of specific jackson exceptions + #_(com.fasterxml.jackson.core JsonGenerationException + JsonParseException) + #_(com.fasterxml.jackson.core.exc StreamConstraintsException) + (java.io StringReader StringWriter + BufferedReader BufferedWriter + IOException) + ;; BB-TEST-PATCH: bb does not support creating java.sql.Timestamps + #_(java.sql Timestamp) + (java.util Date UUID))) + +(defn- str-of-len + ([len] + (str-of-len len "x")) + ([len val] + (apply str (repeat len val)))) + +(defn- nested-map [depth] + (reduce (fn [acc n] {(str n) acc}) + {"0" "foo"} + (range 1 depth))) + +(defn- encode-stream->str [obj opts] + (let [sw (StringWriter.) + bw (BufferedWriter. sw)] + (json/generate-stream obj bw opts) + (.toString sw))) + +(def test-obj {"int" 3 "long" (long -2147483647) "boolean" true + "LongObj" (Long/parseLong "2147483647") "double" 1.23 + "nil" nil "string" "string" "vec" [1 2 3] "map" {"a" "b"} + "list" (list "a" "b") "short" (short 21) "byte" (byte 3)}) + +(deftest t-ratio + (let [n 1/2] + (is (= (double n) (:num (json/decode (json/encode {:num n}) true)))))) + +(deftest t-long-wrap-around + (is (= 2147483648 (json/decode (json/encode 2147483648))))) + +(deftest t-bigint + (let [n 9223372036854775808] + (is (= n (:num (json/decode (json/encode {:num n}) true)))))) + +(deftest t-biginteger + (let [n (BigInteger. "42")] + (is (= n (:num (json/decode (json/encode {:num n}) true)))))) + +(deftest t-bigdecimal + (let [n (BigDecimal. "42.5")] + (is (= (.doubleValue n) (:num (json/decode (json/encode {:num n}) true)))) + ;; BB-TEST-PATCH: + #_(binding [parse/*use-bigdecimals?* true] + (is (= n (:num (json/decode (json/encode {:num n}) true))))))) + +(deftest test-string-round-trip + (is (= test-obj (json/decode (json/encode test-obj))))) + +(deftest test-generate-accepts-float + (is (= "3.14" (json/encode 3.14)))) + +(deftest test-keyword-encode + (is (= {"key" "val"} + (json/decode (json/encode {:key "val"}))))) + +(deftest test-generate-set + (is (= {"set" ["a" "b"]} + (json/decode (json/encode {"set" #{"a" "b"}}))))) + +(deftest test-generate-empty-set + (is (= {"set" []} + (json/decode (json/encode {"set" #{}}))))) + +(deftest test-generate-empty-array + (is (= {"array" []} + (json/decode (json/encode {"array" []}))))) + +(deftest test-key-coercion + (is (= {"foo" "bar" "1" "bat" "2" "bang" "3" "biz"} + (json/decode + (json/encode + {:foo "bar" 1 "bat" (long 2) "bang" (bigint 3) "biz"}))))) + +(deftest test-keywords + (is (= {:foo "bar" :bat 1} + (json/decode (json/encode {:foo "bar" :bat 1}) true)))) + +(deftest test-symbols + (is (= {"foo" "clojure.core/map"} + (json/decode (json/encode {"foo" 'clojure.core/map}))))) + +(deftest test-accepts-java-map + (is (= {"foo" 1} + (json/decode + (json/encode (doto (java.util.HashMap.) (.put "foo" 1))))))) + +(deftest test-accepts-java-list + (is (= [1 2 3] + (json/decode (json/encode (doto (java.util.ArrayList. 3) + (.add 1) + (.add 2) + (.add 3))))))) + +(deftest test-accepts-java-set + (is (= {"set" [1 2 3]} + (json/decode (json/encode {"set" (doto (java.util.HashSet. 3) + (.add 1) + (.add 2) + (.add 3))}))))) + +(deftest test-accepts-empty-java-set + (is (= {"set" []} + (json/decode (json/encode {"set" (java.util.HashSet. 3)}))))) + +(deftest test-nil + (is (nil? (json/decode nil true)))) + +(deftest test-parsed-seq + (let [br (BufferedReader. (StringReader. "1\n2\n3\n"))] + (is (= (list 1 2 3) (json/parsed-seq br))))) + +;; BB-TEST-PATCH: bb does not support smile +#_(deftest test-smile-round-trip + (is (= test-obj (json/parse-smile (json/generate-smile test-obj))))) + +(def bin-obj {"byte-array" (byte-array (map byte [1 2 3]))}) + +;; BB-TEST-PATCH: bb does not support smile/cbor +#_(deftest test-round-trip-binary + (doseq [[p g] {json/parse-string json/generate-string + json/parse-smile json/generate-smile + json/parse-cbor json/generate-cbor}] + (is (let [roundtripped (p (g bin-obj))] + ;; test value equality + (is (= (->> bin-obj (get "byte-array") seq) + (->> roundtripped (get "byte-array") seq))))))) + +;; BB-TEST-PATCH: bb does not support smile +#_(deftest test-smile-factory + (binding [fact/*smile-factory* (fact/make-smile-factory {})] + (is (= {"a" 1} (-> {:a 1} + json/generate-smile + json/parse-smile))))) + +;; BB-TEST-PATCH: bb does not support smile/cbor +#_(deftest test-smile-duplicate-detection + (let [smile-data (byte-array [0x3a 0x29 0x0a 0x01 ;; smile header + 0xFa ;; object start + 0x80 0x61 ;; key a + 0xC2 ;; value 1 + 0x80 0x61 ;; key a (again) + 0xC4 ;; value 2 + 0xFB ;; object end + ])] + (binding [fact/*smile-factory* (fact/make-smile-factory {:strict-duplicate-detection false})] + (is (= {"a" 2} (json/parse-smile smile-data)))) + (binding [fact/*smile-factory* (fact/make-smile-factory {:strict-duplicate-detection true})] + (is (thrown? JsonParseException (json/parse-smile smile-data)))))) + +;; BB-TEST-PATCH: bb does not support cbor +#_(deftest test-cbor-factory + (binding [fact/*cbor-factory* (fact/make-cbor-factory {})] + (is (= {"a" 1} (-> {:a 1} + json/generate-cbor + json/parse-cbor))))) + +;; BB-TEST-PATCH: bb does not support cbor +#_(deftest test-cbor-duplicate-detection + (let [cbor-data (byte-array [0xbf ;; object begin + 0x61 0x61 ;; key a + 0x01 ;; value 1 + 0x61 0x61 ;; key a (again) + 0x02 ;; value 2 + 0xff ;; object end + ])] + (binding [fact/*cbor-factory* (fact/make-cbor-factory {:strict-duplicate-detection false})] + (is (= {"a" 2} (json/parse-cbor cbor-data)))) + (binding [fact/*cbor-factory* (fact/make-cbor-factory {:strict-duplicate-detection true})] + (is (thrown? JsonParseException (json/parse-cbor cbor-data)))))) + +(deftest test-aliases + (is (= {"foo" "bar" "1" "bat" "2" "bang" "3" "biz"} + (json/decode + (json/encode + {:foo "bar" 1 "bat" (long 2) "bang" (bigint 3) "biz"}))))) + +(deftest test-date + (is (= {"foo" "1970-01-01T00:00:00Z"} + (json/decode (json/encode {:foo (Date. (long 0))})))) + (is (= {"foo" "1970-01-01"} + (json/decode (json/encode {:foo (Date. (long 0))} + {:date-format "yyyy-MM-dd"}))) + "encode with given date format")) + +;; BB-TEST-PATCH: bb does not support creating java.sql.Timestamps +#_(deftest test-sql-timestamp + (is (= {"foo" "1970-01-01T00:00:00Z"} + (json/decode (json/encode {:foo (Timestamp. (long 0))})))) + (is (= {"foo" "1970-01-01"} + (json/decode (json/encode {:foo (Timestamp. (long 0))} + {:date-format "yyyy-MM-dd"}))) + "encode with given date format")) + +(deftest test-uuid + (let [id (UUID/randomUUID) + id-str (str id)] + (is (= {"foo" id-str} (json/decode (json/encode {:foo id})))))) + +(deftest test-char-literal + (is (= "{\"foo\":\"a\"}" (json/encode {:foo \a})))) + +(deftest test-streams + (testing "parse-stream" + (are [parsed parse parsee] (= parsed + (parse (BufferedReader. (StringReader. parsee)))) + {"foo" "bar"} json/parse-stream "{\"foo\":\"bar\"}\n" + {"foo" "bar"} json/parse-stream-strict "{\"foo\":\"bar\"}\n") + + (are [parsed parse parsee] (= parsed + (with-open [rdr (StringReader. parsee)] + (parse rdr true))) + {(keyword "foo baz") "bar"} json/parse-stream "{\"foo baz\":\"bar\"}\n" + {(keyword "foo baz") "bar"} json/parse-stream-strict "{\"foo baz\":\"bar\"}\n")) + + (testing "generate-stream" + (let [sw (StringWriter.) + bw (BufferedWriter. sw)] + (json/generate-stream {"foo" "bar"} bw) + (is (= "{\"foo\":\"bar\"}" (.toString sw)))))) + +;; BB-TEST-PATCH: bb does not include with-writer +#_(deftest serial-writing + (is (= "[\"foo\",\"bar\"]" + (.toString + (json/with-writer [(StringWriter.) nil] + (json/write [] :start) + (json/write "foo") + (json/write "bar") + (json/write [] :end))))) + (is (= "[1,[2,3],4]" + (.toString + (json/with-writer [(StringWriter.) nil] + (json/write [1 [2]] :start-inner) + (json/write 3) + (json/write [] :end) + (json/write 4) + (json/write [] :end))))) + (is (= "{\"a\":1,\"b\":2,\"c\":3}" + (.toString + (json/with-writer [(StringWriter.) nil] + (json/write {:a 1} :start) + (json/write {:b 2} :bare) + (json/write {:c 3} :end))))) + (is (= (str "[\"start\",\"continue\",[\"implicitly-nested\"]," + "[\"explicitly-nested\"],\"flatten\",\"end\"]") + (.toString + (json/with-writer [(StringWriter.) nil] + (json/write ["start"] :start) + (json/write "continue") + (json/write ["implicitly-nested"]) + (json/write ["explicitly-nested"] :all) + (json/write ["flatten"] :bare) + (json/write ["end"] :end))))) + (is (= "{\"head\":\"head info\",\"data\":[1,2,3],\"tail\":\"tail info\"}" + (.toString + (json/with-writer [(StringWriter.) nil] + (json/write {:head "head info" :data []} :start-inner) + (json/write 1) + (json/write 2) + (json/write 3) + (json/write [] :end) + (json/write {:tail "tail info"} :end)))))) + +;; BB-TEST-PATCH: modified so that json files could be found +(deftest test-multiple-objs-in-file + (is (= {"one" 1, "foo" "bar"} + (first (json/parsed-seq (io/reader (io/resource "cheshire/test/multi.json")))))) + (is (= {"two" 2, "foo" "bar"} + (second (json/parsed-seq (io/reader (io/resource "cheshire/test/multi.json")))))) + (with-open [r (io/reader (io/resource "cheshire/test/multi.json"))] + (is (= [{"one" 1, "foo" "bar"} {"two" 2, "foo" "bar"}] + (json/parsed-seq r))))) + +(deftest test-jsondotorg-pass1 + (let [;; BB-TEST-PATCH: modified so that json files could be found + string (slurp (io/resource "cheshire/test/pass1.json")) + decoded-json (json/decode string) + encoded-json (json/encode decoded-json) + re-decoded-json (json/decode encoded-json)] + (is (= decoded-json re-decoded-json)))) + +(deftest test-namespaced-keywords + (is (= "{\"foo\":\"user/bar\"}" + (json/encode {:foo :user/bar}))) + (is (= {:foo/bar "baz/eggplant"} + (json/decode (json/encode {:foo/bar :baz/eggplant}) true)))) + +(deftest test-array-coerce-fn + (is (= {"set" #{"a" "b"} "array" ["a" "b"] "map" {"a" 1}} + (json/decode + (json/encode {"set" #{"a" "b"} "array" ["a" "b"] "map" {"a" 1}}) false + (fn [field-name] (if (= "set" field-name) #{} [])))))) + +(deftest t-symbol-encoding-for-non-resolvable-symbols + (is (= "{\"bar\":\"clojure.core/pam\",\"foo\":\"clojure.core/map\"}" + (json/encode (sorted-map :foo 'clojure.core/map :bar 'clojure.core/pam)))) + (is (= "{\"bar\":\"clojure.core/pam\",\"foo\":\"foo.bar/baz\"}" + (json/encode (sorted-map :foo 'foo.bar/baz :bar 'clojure.core/pam))))) + +(deftest t-bindable-factories-auto-close-source + (binding [fact/*json-factory* (fact/make-json-factory + {:auto-close-source false})] + (let [br (BufferedReader. (StringReader. "123"))] + (is (= 123 (json/parse-stream br))) + (is (= -1 (.read br))))) + (binding [fact/*json-factory* (fact/make-json-factory + {:auto-close-source true})] + (let [br (BufferedReader. (StringReader. "123"))] + (is (= 123 (json/parse-stream br))) + (is (thrown? IOException (.read br)))))) + +(deftest t-bindable-factories-allow-comments + (let [s "{\"a\": /* comment */ 1, // comment\n \"b\": 2}"] + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-comments true})] + (is (= {"a" 1 "b" 2} (json/decode s)))) + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-comments false})] + ;; BB-TEST-PATCH: Generalized exception check + (is (thrown? #_JsonParseException Exception (json/decode s)))))) + +(deftest t-bindable-factories-allow-unquoted-field-names + (let [s "{a: 1, b: 2}"] + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-unquoted-field-names true})] + (is (= {"a" 1 "b" 2} (json/decode s)))) + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-unquoted-field-names false})] + ;; BB-TEST-PATCH: Generalized exception check + (is (thrown? #_JsonParseException Exception (json/decode s)))))) + +(deftest t-bindable-factories-allow-single-quotes + (doseq [s ["{'a': \"one\", 'b': \"two\"}" + "{\"a\": 'one', \"b\": 'two'}" + "{'a': 'one', 'b': 'two'}"]] + (testing s + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-single-quotes true})] + (is (= {"a" "one" "b" "two"} (json/decode s)))) + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-single-quotes false})] + ;; BB-TEST-PATCH: Generalized exception check + (is (thrown? #_JsonParseException Exception (json/decode s))))))) + +(deftest t-bindable-factories-allow-unquoted-control-chars + (let [s "{\"a\": \"one\ntwo\"}"] + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-unquoted-control-chars true})] + (is (= {"a" "one\ntwo"} (json/decode s)))) + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-unquoted-control-chars false})] + ;; BB-TEST-PATCH: Generalized exception check + (is (thrown? #_JsonParseException Exception (json/decode s)))))) + +(deftest t-bindable-factories-allow-backslash-escaping-any-char + (let [s "{\"a\": 00000000001}"] + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-numeric-leading-zeros true})] + (is (= {"a" 1} (json/decode s)))) + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-numeric-leading-zeros false})] + ;; BB-TEST-PATCH: Generalized exception check + (is (thrown? #_JsonParseException Exception (json/decode s)))))) + +(deftest t-bindable-factories-allow-numeric-leading-zeros + (let [s "{\"a\": \"\\o\\n\\e\"}"] + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-backslash-escaping true})] + (is (= {"a" "o\ne"} (json/decode s)))) + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-backslash-escaping false})] + ;; BB-TEST-PATCH: Generalized exception check + (is (thrown? #_JsonParseException Exception (json/decode s)))))) + +(deftest t-bindable-factories-non-numeric-numbers + (let [s "{\"foo\":NaN}"] + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-non-numeric-numbers true})] + (is (= (type Double/NaN) + (type (:foo (json/decode s true)))))) + (binding [fact/*json-factory* (fact/make-json-factory + {:allow-non-numeric-numbers false})] + ;; BB-TEST-PATCH: Generalized exception check + (is (thrown? #_JsonParseException Exception (json/decode s true)))))) + +(deftest t-bindable-factories-optimization-opts + (let [s "{\"a\": \"foo\"}"] + (doseq [opts [{:intern-field-names true} + {:intern-field-names false} + {:canonicalize-field-names true} + {:canonicalize-field-names false}]] + (binding [fact/*json-factory* (fact/make-json-factory opts)] + (is (= {"a" "foo"} (json/decode s))))))) + +(deftest t-bindable-factories-escape-non-ascii + ;; includes testing legacy fn opt of same name can override factory + (let [edn {:foo "It costs £100"} + expected-esc "{\"foo\":\"It costs \\u00A3100\"}" + expected-no-esc "{\"foo\":\"It costs £100\"}" + opt-esc {:escape-non-ascii true} + opt-no-esc {:escape-non-ascii false}] + (testing "default factory" + (doseq [[fn-opts expected] + [[{} expected-no-esc] + [opt-esc expected-esc] + [opt-no-esc expected-no-esc]]] + (testing fn-opts + (is (= expected (json/encode edn fn-opts) (encode-stream->str edn fn-opts)))))) + (testing (str "factory: " opt-esc) + (binding [fact/*json-factory* (fact/make-json-factory opt-esc)] + (doseq [[fn-opts expected] + [[{} expected-esc] + [opt-esc expected-esc] + [opt-no-esc expected-no-esc]]] + (testing (str "fn: " fn-opts) + (is (= expected (json/encode edn fn-opts) (encode-stream->str edn fn-opts))))))) + (testing (str "factory: " opt-no-esc) + (binding [fact/*json-factory* (fact/make-json-factory opt-no-esc)] + (doseq [[fn-opts expected] + [[{} expected-no-esc] + [opt-esc expected-esc] + [opt-no-esc expected-no-esc]]] + (testing (str "fn: " fn-opts) + (is (= expected (json/encode edn fn-opts) (encode-stream->str edn fn-opts))))))))) + +(deftest t-bindable-factories-quoteless + (binding [fact/*json-factory* (fact/make-json-factory + {:quote-field-names true})] + (is (= "{\"a\":\"foo\"}" (json/encode {:a "foo"})))) + (binding [fact/*json-factory* (fact/make-json-factory + {:quote-field-names false})] + (is (= "{a:\"foo\"}" (json/encode {:a "foo"}))))) + +(deftest t-bindable-factories-strict-duplicate-detection + (binding [fact/*json-factory* (fact/make-json-factory + {:strict-duplicate-detection true})] + ;; BB-TEST-PATCH: Generalized exception check + (is (thrown? #_ JsonParseException Exception + (json/decode "{\"a\": 1, \"b\": 2, \"a\": 3}")))) + + (binding [fact/*json-factory* (fact/make-json-factory + {:strict-duplicate-detection false})] + (is (= {"a" 3 "b" 2} + (json/decode "{\"a\": 1, \"b\": 2, \"a\": 3}"))))) + +(deftest t-bindable-factories-max-input-document-length + (let [edn {"a" (apply str (repeat 10000 "x"))} + sample-data (json/encode edn)] + (binding [fact/*json-factory* (fact/make-json-factory + {:max-input-document-length (count sample-data)})] + (is (= edn (json/decode sample-data)))) + (binding [fact/*json-factory* (fact/make-json-factory + ;; as per Jackson docs, limit is inexact, so dividing input length by 2 should do the trick + {:max-input-document-length (/ (count sample-data) 2)})] + (is (thrown-with-msg? + ;; BB-TEST-PATCH: Generalized exception check + #_StreamConstraintsException Exception #"(?i)document length .* exceeds" + (json/decode sample-data)))))) + +(deftest t-bindable-factories-max-input-token-count + ;; A token is a single unit of input, such as a number, a string, an object start or end, or an array start or end. + (let [edn {"1" 2 "3" 4} + sample-data (json/encode edn)] + (binding [fact/*json-factory* (fact/make-json-factory + {:max-input-token-count 6})] + (is (= edn (json/decode sample-data)))) + (binding [fact/*json-factory* (fact/make-json-factory + {:max-input-token-count 5})] + (is (thrown-with-msg? + ;; BB-TEST-PATCH: Generalized exception check + #_StreamConstraintsException Exception #"(?i)token count .* exceeds" + (json/decode sample-data)))))) + +(deftest t-bindable-factories-max-input-name-length + (let [k "somekey" + edn {k 1} + sample-data (json/encode edn)] + (binding [fact/*json-factory* (fact/make-json-factory + {:max-input-name-length (count k)})] + (is (= edn (json/decode sample-data)))) + (binding [fact/*json-factory* (fact/make-json-factory + {:max-input-name-length (dec (count k))})] + (is (thrown-with-msg? + ;; BB-TEST-PATCH: Generalized exception check + #_StreamConstraintsException Exception #"(?i)name .* exceeds" + (json/decode sample-data))))) + (let [default-limit (:max-input-name-length fact/default-factory-options)] + (let [k (str-of-len default-limit) + edn {k 1} + sample-data (json/encode edn)] + (is (= edn (json/decode sample-data)))) + (let [k (str-of-len (inc default-limit)) + sample-data (json/encode {k 1})] + (is (thrown-with-msg? + ;; BB-TEST-PATCH: Generalized exception check + #_StreamConstraintsException Exception #"(?i)name .* exceeds" + (json/decode sample-data)))))) + +(deftest t-bindable-factories-input-nesting-depth + (let [edn (nested-map 100) + sample-data (json/encode edn)] + (binding [fact/*json-factory* (fact/make-json-factory + {:max-input-nesting-depth 100})] + (is (= edn (json/decode sample-data)))) + (binding [fact/*json-factory* (fact/make-json-factory + {:max-input-nesting-depth 99})] + (is (thrown-with-msg? + ;; BB-TEST-PATCH: Generalized exception check + #_StreamConstraintsException Exception #"(?i)nesting depth .* exceeds" + (json/decode sample-data)))))) + +(deftest t-bindable-factories-max-input-number-length + (let [num 123456789 + edn {"foo" num} + sample-data (json/encode edn)] + (binding [fact/*json-factory* (fact/make-json-factory + {:max-input-number-length (-> num str count)})] + (is (= edn (json/decode sample-data)))) + (binding [fact/*json-factory* (fact/make-json-factory + {:max-input-number-length (-> num str count dec)})] + (is (thrown-with-msg? + ;; BB-TEST-PATCH: Generalized exception check + #_StreamConstraintsException Exception #"(?i)number value length .* exceeds" + (json/decode sample-data))))) + (let [default-limit (:max-input-number-length fact/default-factory-options)] + (let [num (bigint (str-of-len default-limit 2)) + edn {"foo" num} + sample-data (json/encode edn)] + (is (= edn (json/decode sample-data)))) + (let [num (bigint (str-of-len (inc default-limit) 2)) + sample-data (json/encode {"foo" num})] + (is (thrown-with-msg? + ;; BB-TEST-PATCH: Generalized exception check + #_StreamConstraintsException Exception #"(?i)number value length .* exceeds" + (json/decode sample-data)))))) + +(deftest t-bindable-factories-max-input-string-length + (let [big-string (str-of-len 40000000) + edn {"big-string" big-string} + sample-data (json/encode edn)] + (binding [fact/*json-factory* (fact/make-json-factory + {:max-input-string-length (count big-string)})] + (is (= edn (json/decode sample-data)))) + (binding [fact/*json-factory* (fact/make-json-factory + {:max-input-string-length (dec (count big-string))})] + (is (thrown-with-msg? + ;; BB-TEST-PATCH: Generalized exception check + #_StreamConstraintsException Exception #"(?i)string value length .* exceeds" + (json/decode sample-data))))) + (let [default-limit (:max-input-string-length fact/default-factory-options)] + (let [big-string (str-of-len default-limit) + edn {"big-string" big-string} + sample-data (json/encode edn)] + (is (= edn (json/decode sample-data)))) + (let [big-string (str-of-len (inc default-limit)) + sample-data (json/encode {"big-string" big-string})] + (is (thrown-with-msg? + ;; BB-TEST-PATCH: Generalized exception check + #_StreamConstraintsException Exception #"(?i)string value length .* exceeds" + (json/decode sample-data)))))) + +(deftest t-bindable-factories-max-output-nesting-depth + (let [edn (nested-map 100)] + (binding [fact/*json-factory* (fact/make-json-factory + {:max-output-nesting-depth 100})] + (is (.contains (json/encode edn) "\"99\""))) + (binding [fact/*json-factory* (fact/make-json-factory + {:max-output-nesting-depth 99})] + (is (thrown-with-msg? + ;; BB-TEST-PATCH: Generalized exception check + #_StreamConstraintsException Exception #"(?i)nesting depth .* exceeds" + (json/encode edn)))))) + +(deftest t-persistent-queue + (let [q (conj clojure.lang.PersistentQueue/EMPTY 1 2 3)] + (is (= q (json/decode (json/encode q)))))) + +(deftest t-pretty-print + (is (= (str/join (System/lineSeparator) + ["{" + " \"bar\" : [ {" + " \"baz\" : 2" + " }, \"quux\", [ 1, 2, 3 ] ]," + " \"foo\" : 1" + "}"]) + (json/encode (sorted-map :foo 1 :bar [{:baz 2} :quux [1 2 3]]) + {:pretty true})))) + +(deftest t-pretty-print-custom-linebreak + (is (= (str/join "foo" + ["{" + " \"bar\" : [ {" + " \"baz\" : 2" + " }, \"quux\", [ 1, 2, 3 ] ]," + " \"foo\" : 1" + "}"]) + (json/encode (sorted-map :foo 1 :bar [{:baz 2} :quux [1 2 3]]) + {:pretty {:line-break "foo"}})))) + +(deftest t-pretty-print-illegal-argument + ; just expecting this not to throw + (json/encode {:foo "bar"} + {:pretty []}) + (json/encode {:foo "bar"} + {:pretty nil})) + +(deftest t-custom-pretty-print-with-defaults + (let [test-obj (sorted-map :foo 1 :bar {:baz [{:ulu "mulu"} {:moot "foo"} 3]} :quux :blub) + pretty-str-default (json/encode test-obj {:pretty true}) + pretty-str-custom (json/encode test-obj {:pretty {}})] + (is (= pretty-str-default pretty-str-custom)) + (when-not (= pretty-str-default pretty-str-custom) + ; print for easy comparison + (println "; default pretty print") + (println pretty-str-default) + (println "; custom pretty print with default options") + (println pretty-str-custom)))) + +(deftest t-custom-pretty-print-with-non-defaults + (let [test-obj (sorted-map :foo 1 :bar {:baz [{:ulu "mulu"} {:moot "foo"} 3]} :quux :blub) + test-opts {:pretty {:indentation 4 + :indent-arrays? false + :before-array-values "" + :after-array-values "" + :object-field-value-separator ": "}} + expected (str/join (System/lineSeparator) + ["{" + " \"bar\": {" + " \"baz\": [{" + " \"ulu\": \"mulu\"" + " }, {" + " \"moot\": \"foo\"" + " }, 3]" + " }," + " \"foo\": 1," + " \"quux\": \"blub\"" + "}"]) + pretty-str (json/encode test-obj test-opts)] + + ; just to be easy on the eyes in case of error + (when-not (= expected pretty-str) + (println "; pretty print with options - actual") + (println pretty-str) + (println "; pretty print with options - expected") + (println expected)) + (is (= expected pretty-str)))) + +(deftest t-custom-pretty-print-with-noident-objects + (let [test-obj [{:foo 1 :bar 2} {:foo 3 :bar 4}] + test-opts {:pretty {:indent-objects? false}} + expected (str "[ { \"foo\" : 1, \"bar\" : 2 }, " + "{ \"foo\" : 3, \"bar\" : 4 } ]") + pretty-str (json/encode test-obj test-opts)] + ; just to be easy on the eyes in case of error + (when-not (= expected pretty-str) + (println "; pretty print with options - actual") + (println pretty-str) + (println "; pretty print with options - expected") + (println expected)) + (is (= expected pretty-str)))) + +(deftest t-custom-keyword-fn + (is (= {:FOO "bar"} (json/decode "{\"foo\": \"bar\"}" + (fn [k] (keyword (.toUpperCase k)))))) + (is (= {"foo" "bar"} (json/decode "{\"foo\": \"bar\"}" nil))) + (is (= {"foo" "bar"} (json/decode "{\"foo\": \"bar\"}" false))) + (is (= {:foo "bar"} (json/decode "{\"foo\": \"bar\"}" true)))) + +(deftest t-custom-encode-key-fn + (is (= "{\"FOO\":\"bar\"}" + (json/encode {:foo :bar} + {:key-fn (fn [k] (.toUpperCase (name k)))})))) + +;; BB-TEST-PATCH: bb does nto include cheshire.generate ns +#_(deftest test-add-remove-encoder + (gen/remove-encoder java.net.URL) + (gen/add-encoder java.net.URL gen/encode-str) + (is (= "\"http://foo.com\"" + (json/encode (java.net.URL. "http://foo.com")))) + (gen/remove-encoder java.net.URL) + (is (thrown? JsonGenerationException + (json/encode (java.net.URL. "http://foo.com"))))) + +#_(defprotocol TestP + (foo [this] "foo method")) + +#_(defrecord TestR [state]) + +#_(extend TestR + TestP + {:foo (constantly "bar")}) + +#_(deftest t-custom-protocol-encoder + (let [rec (TestR. :quux)] + (is (= {:state "quux"} (json/decode (json/encode rec) true))) + (gen/add-encoder cheshire.test.core.TestR + (fn [obj jg] + (.writeString jg (foo obj)))) + (is (= "bar" (json/decode (json/encode rec)))) + (gen/remove-encoder cheshire.test.core.TestR) + (is (= {:state "quux"} (json/decode (json/encode rec) true))))) + +#_(defprotocol CTestP + (thing [this] "thing method")) +#_(defrecord CTestR [state]) +#_(extend CTestR + CTestP + {:thing (constantly "thing")}) + +#_(deftest t-custom-helpers + (let [thing (CTestR. :state) + remove #(gen/remove-encoder CTestR)] + (gen/add-encoder CTestR (fn [_obj jg] (gen/encode-nil nil jg))) + (is (= nil (json/decode (json/encode thing) true))) + (remove) + (gen/add-encoder CTestR (fn [_obj jg] (gen/encode-str "foo" jg))) + (is (= "foo" (json/decode (json/encode thing) true))) + (remove) + (gen/add-encoder CTestR (fn [_obj jg] (gen/encode-number 5 jg))) + (is (= 5 (json/decode (json/encode thing) true))) + (remove) + (gen/add-encoder CTestR (fn [_obj jg] (gen/encode-long 4 jg))) + (is (= 4 (json/decode (json/encode thing) true))) + (remove) + (gen/add-encoder CTestR (fn [_obj jg] (gen/encode-int 3 jg))) + (is (= 3 (json/decode (json/encode thing) true))) + (remove) + (gen/add-encoder CTestR (fn [_obj jg] (gen/encode-ratio 1/2 jg))) + (is (= 0.5 (json/decode (json/encode thing) true))) + (remove) + (gen/add-encoder CTestR (fn [_obj jg] (gen/encode-seq [:foo :bar] jg))) + (is (= ["foo" "bar"] (json/decode (json/encode thing) true))) + (remove) + (gen/add-encoder CTestR (fn [_obj jg] (gen/encode-date (Date. (long 0)) jg))) + (binding [gen/*date-format* "yyyy-MM-dd'T'HH:mm:ss'Z'"] + (is (= "1970-01-01T00:00:00Z" (json/decode (json/encode thing) true)))) + (remove) + (gen/add-encoder CTestR (fn [_obj jg] (gen/encode-bool true jg))) + (is (= true (json/decode (json/encode thing) true))) + (remove) + (gen/add-encoder CTestR (fn [_obj jg] (gen/encode-named :foo jg))) + (is (= "foo" (json/decode (json/encode thing) true))) + (remove) + (gen/add-encoder CTestR (fn [_obj jg] (gen/encode-map {:foo "bar"} jg))) + (is (= {:foo "bar"} (json/decode (json/encode thing) true))) + (remove) + (gen/add-encoder CTestR (fn [_obj jg] (gen/encode-symbol 'foo jg))) + (is (= "foo" (json/decode (json/encode thing) true))) + (remove))) + +(deftest t-float-encoding + (is (= "{\"foo\":0.01}" (json/encode {:foo (float 0.01)})))) + +(deftest t-non-const-bools + (is (= {:a 1} (json/decode "{\"a\": 1}" (Boolean. true))))) + +;; BB-TEST-PATCH: bb does not include cheshire.exact ns +#_(deftest t-invalid-json + (let [invalid-json-message "Invalid JSON, expected exactly one parseable object but multiple objects were found"] + (are [x y] (= x (try + y + (catch Exception e + (.getMessage e)))) + invalid-json-message (json-exact/decode "{\"foo\": 1}asdf") + invalid-json-message (json-exact/decode "{\"foo\": 123}null") + invalid-json-message (json-exact/decode "\"hello\" : 123}") + {"foo" 1} (json/decode "{\"foo\": 1}") + invalid-json-message (json-exact/decode-strict "{\"foo\": 1}asdf") + invalid-json-message (json-exact/decode-strict "{\"foo\": 123}null") + invalid-json-message (json-exact/decode-strict "\"hello\" : 123}") + {"foo" 1} (json/decode-strict "{\"foo\": 1}")))) diff --git a/test-resources/lib_tests/cheshire/test/multi.json b/test-resources/lib_tests/cheshire/test/multi.json new file mode 100644 index 00000000..c888db2a --- /dev/null +++ b/test-resources/lib_tests/cheshire/test/multi.json @@ -0,0 +1,2 @@ +{"one":1,"foo":"bar"} +{"two":2,"foo":"bar"} diff --git a/test-resources/lib_tests/cheshire/test/pass1.json b/test-resources/lib_tests/cheshire/test/pass1.json new file mode 100644 index 00000000..70e26854 --- /dev/null +++ b/test-resources/lib_tests/cheshire/test/pass1.json @@ -0,0 +1,58 @@ +[ + "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": "" \u0022 %22 0x22 034 "", + "\/\\\"\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"] \ No newline at end of file