Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add read-values and write-values #53

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 128 additions & 2 deletions src/clj/jsonista/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@
RatioSerializer FunctionalKeywordSerializer)
(com.fasterxml.jackson.core JsonGenerator$Feature JsonFactory)
(com.fasterxml.jackson.databind
JsonSerializer ObjectMapper module.SimpleModule
JsonSerializer ObjectMapper SequenceWriter
SerializationFeature DeserializationFeature Module)
(com.fasterxml.jackson.databind.module SimpleModule)
(java.io InputStream Writer File OutputStream DataOutput Reader)
(java.net URL)
(com.fasterxml.jackson.datatype.jsr310 JavaTimeModule)
(java.util List Map Date)
(java.util List Map Date Iterator)
(clojure.lang Keyword Ratio Symbol)))

(defn- ^Module clojure-module
Expand Down Expand Up @@ -194,6 +194,38 @@
(-read-value [this ^ObjectMapper mapper]
(.readValue mapper this ^Class Object)))

(defprotocol ReadValues
(-read-values [this mapper]))

(extend-protocol ReadValues

(Class/forName "[B")
(-read-values [this ^ObjectMapper mapper]
(.readValues (.readerFor mapper ^Class Object) ^bytes this))

nil
(-read-values [_ _])

File
(-read-values [this ^ObjectMapper mapper]
(.readValues (.readerFor mapper ^Class Object) this))

URL
(-read-values [this ^ObjectMapper mapper]
(.readValues (.readerFor mapper ^Class Object) this))

String
(-read-values [this ^ObjectMapper mapper]
(.readValues (.readerFor mapper ^Class Object) this))

Reader
(-read-values [this ^ObjectMapper mapper]
(.readValues (.readerFor mapper ^Class Object) this))

InputStream
(-read-values [this ^ObjectMapper mapper]
(.readValues (.readerFor mapper ^Class Object) this)))

(defprotocol WriteValue
(-write-value [this value mapper]))

Expand All @@ -214,6 +246,50 @@
(-write-value [this value ^ObjectMapper mapper]
(.writeValue mapper this value)))

(defprotocol WriteAll
(-write-all [this ^SequenceWriter writer]))

(extend-protocol WriteAll

(Class/forName "[Ljava.lang.Object;")
(-write-all [this ^SequenceWriter w]
(.writeAll w ^"[Ljava.lang.Object;" this))

Iterable
(-write-all [this ^SequenceWriter w]
(.writeAll w this)))

(defprotocol WriteValues
(-write-values [this values mapper]))

(defmacro ^:private -write-values*
[this value mapper]
`(doto ^SequenceWriter
(-write-all
~value
(-> ~mapper
(.writerFor Object)
(.without SerializationFeature/FLUSH_AFTER_WRITE_VALUE)
(.writeValuesAsArray ~this)))
(.close)))

(extend-protocol WriteValues
File
(-write-values [this value ^ObjectMapper mapper]
(-write-values* this value mapper))

OutputStream
(-write-values [this value ^ObjectMapper mapper]
(-write-values* this value mapper))

DataOutput
(-write-values [this value ^ObjectMapper mapper]
(-write-values* this value mapper))

Writer
(-write-values [this value ^ObjectMapper mapper]
(-write-values* this value mapper)))

;;
;; public api
;;
Expand Down Expand Up @@ -259,3 +335,53 @@
(-write-value to object default-object-mapper))
([to object ^ObjectMapper mapper]
(-write-value to object mapper)))

(defn- wrap-values
[^Iterator iterator]
(when iterator
(reify
Iterable
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I'm unsure of is implementing Iterable here. Is it a good idea to return an object which is both an Iterable and Iterator?

(iterator [this] iterator)
Iterator
(hasNext [this] (.hasNext iterator))
(next [this] (.next iterator))
(remove [this] (.remove iterator))
clojure.lang.IReduceInit
(reduce [_ f val]
(loop [ret val]
(if (.hasNext iterator)
(let [ret (f ret (.next iterator))]
(if (reduced? ret)
@ret
(recur ret)))
ret)))
clojure.lang.Sequential)))

(defn read-values
"Decodes a sequence of values from a JSON as an iterator
from anything that satisfies [[ReadValue]] protocol.
By default, File, URL, String, Reader and InputStream are supported.

The returned object is an Iterable, Iterator and IReduceInit.
It can be reduced on via [[reduce]] and turned into a lazy sequence
via [[iterator-seq]].

To configure, pass in an ObjectMapper created with [[object-mapper]],
see [[object-mapper]] docstring for the available options."
([object]
(wrap-values (-read-values object default-object-mapper)))
([object ^ObjectMapper mapper]
(wrap-values (-read-values object mapper))))

(defn write-values
"Encode values as JSON and write using the provided [[WriteValue]] instance.
By default, File, OutputStream, DataOutput and Writer are supported.

By default, values can be an array or an Iterable.

To configure, pass in an ObjectMapper created with [[object-mapper]],
see [[object-mapper]] docstring for the available options."
([to object]
(-write-values to object default-object-mapper))
([to object ^ObjectMapper mapper]
(-write-values to object mapper)))
76 changes: 76 additions & 0 deletions test/jsonista/core_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,33 @@
(testing "Reader"
(is (= original (j/read-value (InputStreamReader. (str->input-stream input-string))))))))

(deftest read-values-types
(let [original [{"ok" 1}]
input-string (j/write-value-as-string original)
file (tmp-file)]
(spit file input-string)

(testing "nil"
(is (= nil (j/read-values nil))))

(testing "byte-array"
(is (= original (j/read-values (j/write-value-as-bytes original)))))

(testing "File"
(is (= original (j/read-values file))))

(testing "URL"
(is (= original (j/read-values (.toURL file)))))

(testing "String"
(is (= original (j/read-values input-string))))

(testing "InputStream"
(is (= original (j/read-values (str->input-stream input-string)))))

(testing "Reader"
(is (= original (j/read-values (InputStreamReader. (str->input-stream input-string))))))))

(deftest write-value-types
(let [original {"ok" 1}
expected (j/write-value-as-string original)
Expand All @@ -268,3 +295,52 @@
(is (= expected (slurp file)))
(.delete file))))

(deftest write-values-types
(let [original [{"ok" 1}]
expected (j/write-value-as-string original)
file (tmp-file)]

(testing "File"
(j/write-values file original)
(is (= expected (slurp file)))
(.delete file))

(testing "OutputStream"
(j/write-values (FileOutputStream. file) original)
(is (= expected (slurp file)))
(.delete file))

(testing "DataOutput"
(j/write-values (RandomAccessFile. file "rw") original)
(is (= expected (slurp file)))
(.delete file))

(testing "Writer"
(j/write-values (FileWriter. file) original)
(is (= expected (slurp file)))
(.delete file))))

(deftest read-values-iteration
(let [original [{"ok" 1}]
^java.util.Iterator it (j/read-values (j/write-value-as-bytes original))]
(is (instance? java.util.Iterator it))
(is (.hasNext it))
(is (= (first original) (.next it)))
(is (false? (.hasNext it)))))

(deftest read-values-reduction
(let [original [{"ok" 1}]
^java.util.Iterator it (j/read-values (j/write-value-as-bytes original))
xf (map #(update % "ok" inc))]
(is (= (into [] xf original) (into [] xf it)))))

(deftest write-values-iterable
(let [original [{"ok" 1}]
xf (map #(update % "ok" inc))
expected (j/write-value-as-string (into [] xf original))
file (tmp-file)
eduction (->Eduction xf original)]

(j/write-values file eduction)
(is (= expected (slurp file)))
(.delete file)))