Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge remote-tracking branch 'origin/combining-core-custom'

  • Loading branch information...
commit 535295b577b2138fc1b5ebad9d46c113d0a1d364 2 parents dc6f43c + f7f10b4
@dakrone authored
View
32 README.md
@@ -38,13 +38,13 @@ encoders.
## Usage
```clojure
-[cheshire "4.0.4"]
+[cheshire "5.0.0"]
-;; Cheshire v4.0.4 uses Jackson 2.1.0
+;; Cheshire v5.0.0 uses Jackson 2.1.1
;; In your ns statement:
-(ns myns
- (:use [cheshire.core]))
+(ns my.ns
+ (:require [cheshire.core :refer :all]))
```
### Encoding
@@ -130,14 +130,17 @@ The type must be "transient-able", so use either #{} or []
### Custom Encoders
Custom encoding is supported from 2.0.0 and up, if you encounter a
-bug, please open a github issue.
+bug, please open a github issue. From 5.0.0 onwards, custom encoding
+has been moved to be part of the core namespace (not requiring a
+namespace change)
```clojure
;; Custom encoders allow you to swap out the api for the fast
;; encoder with one that is slightly slower, but allows custom
;; things to be encoded:
(ns myns
- (:use [cheshire.custom]))
+ (:require [cheshire.core :refer :all]
+ [cheshire.generate :refer [add-encoder remove-encoder]]))
;; First, add a custom encoder for a class:
(add-encoder java.awt.Color
@@ -147,7 +150,7 @@ bug, please open a github issue.
;; There are also helpers for common encoding actions:
(add-encoder java.net.URL encode-str)
-;; List of common encoders that can be used: (see custom.clj)
+;; List of common encoders that can be used: (see generate.clj)
;; encode-nil
;; encode-number
;; encode-seq
@@ -168,6 +171,9 @@ bug, please open a github issue.
;; Decoding remains the same, you are responsible for doing custom decoding.
```
+<h3>NOTE: `cheshire.custom` has been deprecated in version 5.0.0</h3>
+
+<del>
In version 3.0.0 and above, custom encoding first attempts to encode
the object using the core encoding (because it would be faster). If
the encoding fails, it uses the custom encoding mechanics to encode
@@ -180,6 +186,11 @@ to encode custom classes. The API methods for cheshire.core and
cheshire.custom are exactly the same (except for add-encoder,
remove-encoder and the methods ending with '*' in the custom
namespace).
+</del>
+
+Custom and Core encoding have been combined in Cheshire 5.0.0, so
+there is no longer any need to require a different namespace depending
+on what you would like to use.
### Aliases
@@ -215,6 +226,9 @@ Cheshire encoding supports:
- Date
- UUID
- java.sql.Timestamp
+- any java.util.Set
+- any java.util.Map
+- any java.util.List
### Custom class encoding while still being fast
@@ -287,9 +301,9 @@ know.
## Advanced customization for factories
See
-[this](http://fasterxml.github.com/jackson-core/javadoc/2.0.0/com/fasterxml/jackson/core/JsonFactory.Feature.html)
+[this](http://fasterxml.github.com/jackson-core/javadoc/2.1.1/com/fasterxml/jackson/core/JsonFactory.Feature.html)
and
-[this](http://fasterxml.github.com/jackson-core/javadoc/2.0.0/com/fasterxml/jackson/core/JsonParser.Feature.html)
+[this](http://fasterxml.github.com/jackson-core/javadoc/2.1.1/com/fasterxml/jackson/core/JsonParser.Feature.html)
for a list of features that can be customized if desired. A custom
factory can be used like so:
View
4 project.clj
@@ -2,8 +2,8 @@
:description "JSON and JSON SMILE encoding, fast."
:url "https://github.com/dakrone/cheshire"
:warn-on-reflection false
- :dependencies [[com.fasterxml.jackson.core/jackson-core "2.1.0"]
- [com.fasterxml.jackson.dataformat/jackson-dataformat-smile "2.1.0"]]
+ :dependencies [[com.fasterxml.jackson.core/jackson-core "2.1.1"]
+ [com.fasterxml.jackson.dataformat/jackson-dataformat-smile "2.1.1"]]
:profiles {:dev {:dependencies [[org.clojure/clojure "1.4.0"]
[criterium "0.3.0"]
[org.clojure/test.generative "0.1.4"]
View
51 src/cheshire/core.clj
@@ -1,7 +1,8 @@
(ns cheshire.core
- (:use [cheshire.factory]
- [cheshire.generate :only [generate]]
- [cheshire.parse :only [parse]])
+ "Main encoding and decoding namespace."
+ (:require [cheshire.factory :as factory]
+ [cheshire.generate :as gen]
+ [cheshire.parse :as parse])
(:import (com.fasterxml.jackson.core JsonParser JsonFactory
JsonGenerator$Feature)
(com.fasterxml.jackson.dataformat.smile SmileFactory)
@@ -19,14 +20,15 @@
([obj opt-map]
(let [sw (StringWriter.)
generator (.createJsonGenerator
- ^JsonFactory (or *json-factory* json-factory) sw)]
+ ^JsonFactory (or factory/*json-factory*
+ factory/json-factory) sw)]
(when (:pretty opt-map)
(.useDefaultPrettyPrinter generator))
(when (:escape-non-ascii opt-map)
(.enable generator JsonGenerator$Feature/ESCAPE_NON_ASCII))
- (generate generator obj
- (or (:date-format opt-map) default-date-format)
- (:ex opt-map))
+ (gen/generate generator obj
+ (or (:date-format opt-map) factory/default-date-format)
+ (:ex opt-map))
(.flush generator)
(.toString sw))))
@@ -40,12 +42,14 @@
(generate-stream obj writer nil))
([obj ^BufferedWriter writer opt-map]
(let [generator (.createJsonGenerator
- ^JsonFactory (or *json-factory* json-factory) writer)]
+ ^JsonFactory (or factory/*json-factory*
+ factory/json-factory) writer)]
(when (:pretty opt-map)
(.useDefaultPrettyPrinter generator))
(when (:escape-non-ascii opt-map)
(.enable generator JsonGenerator$Feature/ESCAPE_NON_ASCII))
- (generate generator obj (or (:date-format opt-map) default-date-format)
+ (gen/generate generator obj (or (:date-format opt-map)
+ factory/default-date-format)
(:ex opt-map))
(.flush generator)
writer)))
@@ -60,9 +64,11 @@
([obj opt-map]
(let [baos (ByteArrayOutputStream.)
generator (.createJsonGenerator ^SmileFactory
- (or *smile-factory* smile-factory)
+ (or factory/*smile-factory*
+ factory/smile-factory)
baos)]
- (generate generator obj (or (:date-format opt-map) default-date-format)
+ (gen/generate generator obj (or (:date-format opt-map)
+ factory/default-date-format)
(:ex opt-map))
(.flush generator)
(.toByteArray baos))))
@@ -77,8 +83,9 @@
and returning the collection to be used for array values."
[^String string & [key-fn array-coerce-fn]]
(when string
- (parse
- (.createJsonParser ^JsonFactory (or *json-factory* json-factory)
+ (parse/parse
+ (.createJsonParser ^JsonFactory (or factory/*json-factory*
+ factory/json-factory)
(StringReader. string))
key-fn nil array-coerce-fn)))
@@ -93,8 +100,9 @@
If laziness is needed, see parsed-seq."
[^BufferedReader rdr & [key-fn array-coerce-fn]]
(when rdr
- (parse
- (.createJsonParser ^JsonFactory (or *json-factory* json-factory) rdr)
+ (parse/parse
+ (.createJsonParser ^JsonFactory (or factory/*json-factory*
+ factory/json-factory) rdr)
key-fn nil array-coerce-fn)))
(defn parse-smile
@@ -106,8 +114,9 @@
and returning the collection to be used for array values."
[^bytes bytes & [key-fn array-coerce-fn]]
(when bytes
- (parse
- (.createJsonParser ^SmileFactory (or *smile-factory* smile-factory) bytes)
+ (parse/parse
+ (.createJsonParser ^SmileFactory (or factory/*smile-factory*
+ factory/smile-factory) bytes)
key-fn nil array-coerce-fn)))
(def ^{:doc "Object used to determine end of lazy parsing attempt."}
@@ -118,7 +127,7 @@
"Internal lazy-seq parser"
[^JsonParser parser key-fn array-coerce-fn]
(lazy-seq
- (let [elem (parse parser key-fn eof array-coerce-fn)]
+ (let [elem (parse/parse parser key-fn eof array-coerce-fn)]
(when-not (identical? elem eof)
(cons elem (parsed-seq* parser key-fn array-coerce-fn))))))
@@ -132,7 +141,8 @@
[^BufferedReader reader & [key-fn array-coerce-fn]]
(when reader
(parsed-seq* (.createJsonParser ^JsonFactory
- (or *json-factory* json-factory) reader)
+ (or factory/*json-factory*
+ factory/json-factory) reader)
key-fn array-coerce-fn)))
(defn parsed-smile-seq
@@ -144,7 +154,8 @@
[^BufferedReader reader & [key-fn array-coerce-fn]]
(when reader
(parsed-seq* (.createJsonParser ^SmileFactory
- (or *smile-factory* smile-factory) reader)
+ (or factory/*smile-factory*
+ factory/smile-factory) reader)
key-fn array-coerce-fn)))
;; aliases for clojure-json users
View
55 src/cheshire/custom.clj
@@ -1,5 +1,7 @@
(ns cheshire.custom
- "Methods used for extending JSON generation to different Java classes.
+ "DEPRECATED
+
+ Methods used for extending JSON generation to different Java classes.
Has the same public API as core.clj so they can be swapped in and out."
(:use [cheshire.factory])
(:require [cheshire.core :as core])
@@ -12,6 +14,11 @@
JsonGenerator$Feature
JsonGenerationException JsonParser)))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;; DEPRECATED, DO NOT USE ;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+
;; date format rebound for custom encoding
(def ^{:dynamic true :private true} *date-format*)
@@ -40,14 +47,7 @@
(.flush generator)
(.toString sw)))))
-(defn ^String encode
- ([obj]
- (encode obj nil))
- ([obj opt-map]
- (try
- (core/encode obj (merge opt-map {:ex core-failure}))
- (catch JsonGenerationException _
- (encode* obj opt-map)))))
+(def ^String encode encode*)
(defn ^String encode-stream*
([obj ^BufferedWriter w]
@@ -64,14 +64,7 @@
(.flush generator)
w))))
-(defn ^String encode-stream
- ([obj ^BufferedWriter w]
- (encode-stream obj w nil))
- ([obj ^BufferedWriter w opt-map]
- (try
- (core/encode-stream obj w (merge opt-map {:ex core-failure}))
- (catch JsonGenerationException _
- (encode-stream* obj w opt-map)))))
+(def ^String encode-stream encode-stream*)
(defn encode-smile*
([obj]
@@ -86,14 +79,7 @@
(.flush generator)
(.toByteArray baos)))))
-(defn encode-smile
- ([obj]
- (encode-smile* obj nil))
- ([obj opt-map]
- (try
- (core/encode-smile obj (merge opt-map {:ex core-failure}))
- (catch JsonGenerationException _
- (encode-smile* obj opt-map)))))
+(def encode-smile encode-smile*)
;; there are no differences in parsing, but these are here to make
;; this a self-contained namespace if desired
@@ -107,13 +93,14 @@
(def decode-smile parse-smile)
;; aliases for encoding
-(def generate-string encode)
+(def generate-string encode*)
(def generate-string* encode*)
-(def generate-stream encode-stream)
+(def generate-stream encode-stream*)
(def generate-stream* encode-stream*)
-(def generate-smile encode-smile)
+(def generate-smile encode-smile*)
(def generate-smile* encode-smile*)
+
;; Generic encoders, these can be used by someone writing a custom
;; encoder if so desired, after transforming an arbitrary data
;; structure into a clojure one, these can just be called.
@@ -278,6 +265,18 @@
(extend clojure.lang.Associative
JSONable
{:to-json encode-map})
+
+(extend java.util.Map
+ JSONable
+ {:to-json encode-map})
+
+(extend java.util.Set
+ JSONable
+ {:to-json encode-seq})
+
+(extend java.util.List
+ JSONable
+ {:to-json encode-seq})
;; Utility methods to add and remove encoders
(defn add-encoder
"Provide an encoder for a type not handled by Cheshire.
View
209 src/cheshire/generate.clj
@@ -1,4 +1,5 @@
(ns cheshire.generate
+ "Namespace used to generate JSON from Clojure data structures."
(:import (com.fasterxml.jackson.core JsonGenerator JsonGenerationException)
(java.util Date Map List Set SimpleTimeZone UUID)
(java.sql Timestamp)
@@ -6,13 +7,22 @@
(java.math BigInteger)
(clojure.lang IPersistentCollection Keyword Ratio Symbol)))
+;; date format rebound for custom encoding
+(def ^{:dynamic true :private true} *date-format*)
+
+(defprotocol JSONable
+ (to-json [t jg]))
+
(definline write-string [^JsonGenerator jg ^String str]
`(.writeString ~jg ~str))
-(definline fail [obj ^Exception e]
- `(throw (or ~e (JsonGenerationException.
- (str "Cannot JSON encode object of class: "
- (class ~obj) ": " ~obj)))))
+(defmacro fail [obj jg ^Exception e]
+ `(try
+ (to-json ~obj ~jg)
+ (catch IllegalArgumentException _#
+ (throw (or ~e (JsonGenerationException.
+ (str "Cannot JSON encode object of class: "
+ (class ~obj) ": " ~obj)))))))
(defmacro number-dispatch [^JsonGenerator jg obj ^Exception e]
(if (< 2 (:minor *clojure-version*))
@@ -28,7 +38,7 @@
Byte (.writeNumber ~jg (int ~obj))
clojure.lang.BigInt (.writeNumber ~jg ^clojure.lang.BigInt
(.toBigInteger (bigint ~obj)))
- (fail ~obj ~e))
+ (fail ~obj ~jg ~e))
`(condp instance? ~obj
Integer (.writeNumber ~jg (int ~obj))
Long (.writeNumber ~jg (long ~obj))
@@ -39,7 +49,7 @@
Ratio (.writeNumber ~jg (double ~obj))
Short (.writeNumber ~jg (int ~obj))
Byte (.writeNumber ~jg (int ~obj))
- (fail ~obj ~e))))
+ (fail ~obj ~jg ~e))))
(declare generate)
@@ -102,4 +112,189 @@
;; it must be a primative then
(try
(.writeNumber ^JsonGenerator jg obj)
- (catch Exception e (fail obj ex))))))
+ (catch Exception e (fail obj jg ex))))))
+
+;; Generic encoders, these can be used by someone writing a custom
+;; encoder if so desired, after transforming an arbitrary data
+;; structure into a clojure one, these can just be called.
+(defn encode-nil
+ "Encode null to the json generator."
+ [_ ^JsonGenerator jg]
+ (.writeNull jg))
+
+(defn encode-str
+ "Encode a string to the json generator."
+ [^String s ^JsonGenerator jg]
+ (.writeString jg (str s)))
+
+(defn encode-number
+ "Encode anything implementing java.lang.Number to the json generator."
+ [^java.lang.Number n ^JsonGenerator jg]
+ (.writeNumber jg n))
+
+(defn encode-long
+ "Encode anything implementing java.lang.Number to the json generator."
+ [^Long n ^JsonGenerator jg]
+ (.writeNumber jg (long n)))
+
+(defn encode-int
+ "Encode anything implementing java.lang.Number to the json generator."
+ [n ^JsonGenerator jg]
+ (.writeNumber jg (long n)))
+
+(defn encode-ratio
+ "Encode a clojure.lang.Ratio to the json generator."
+ [^clojure.lang.Ratio n ^JsonGenerator jg]
+ (.writeNumber jg (double n)))
+
+(defn encode-seq
+ "Encode a seq to the json generator."
+ [s ^JsonGenerator jg]
+ (.writeStartArray jg)
+ (doseq [i s]
+ (to-json i jg))
+ (.writeEndArray jg))
+
+(defn encode-date
+ "Encode a date object to the json generator."
+ [^Date d ^JsonGenerator jg]
+ (let [sdf (SimpleDateFormat. *date-format*)]
+ (.setTimeZone sdf (SimpleTimeZone. 0 "UTC"))
+ (.writeString jg (.format sdf d))))
+
+(defn encode-bool
+ "Encode a Boolean object to the json generator."
+ [^Boolean b ^JsonGenerator jg]
+ (.writeBoolean jg b))
+
+(defn encode-named
+ "Encode a keyword to the json generator."
+ [^clojure.lang.Keyword k ^JsonGenerator jg]
+ (.writeString jg (if-let [ns (namespace k)]
+ (str ns "/" (name k))
+ (name k))))
+
+(defn encode-map
+ "Encode a clojure map to the json generator."
+ [^clojure.lang.IPersistentMap m ^JsonGenerator jg]
+ (.writeStartObject jg)
+ (doseq [[k v] m]
+ (.writeFieldName jg (if (instance? clojure.lang.Keyword k)
+ (if-let [ns (namespace k)]
+ (str ns "/" (name k))
+ (name k))
+ (str k)))
+ (to-json v jg))
+ (.writeEndObject jg))
+
+(defn encode-symbol
+ "Encode a clojure symbol to the json generator."
+ [^clojure.lang.Symbol s ^JsonGenerator jg]
+ (.writeString jg (str s)))
+
+;; extended implementations for clojure datastructures
+(extend nil
+ JSONable
+ {:to-json encode-nil})
+
+(extend java.lang.String
+ JSONable
+ {:to-json encode-str})
+
+;; This is lame, thanks for changing all the BigIntegers to BigInts
+;; in 1.3 clojure/core :-/
+(when (not= {:major 1 :minor 2} (select-keys *clojure-version* [:major :minor]))
+ ;; Use Class/forName so it only resolves if it's running on clojure 1.3
+ (extend (Class/forName "clojure.lang.BigInt")
+ JSONable
+ {:to-json (fn encode-bigint
+ [^java.lang.Number n ^JsonGenerator jg]
+ (.writeNumber jg ^java.math.BigInteger (.toBigInteger n)))}))
+
+(extend clojure.lang.Ratio
+ JSONable
+ {:to-json encode-ratio})
+
+(extend Long
+ JSONable
+ {:to-json encode-long})
+
+(extend Short
+ JSONable
+ {:to-json encode-int})
+
+(extend Byte
+ JSONable
+ {:to-json encode-int})
+
+(extend java.lang.Number
+ JSONable
+ {:to-json encode-number})
+
+(extend clojure.lang.ISeq
+ JSONable
+ {:to-json encode-seq})
+
+(extend clojure.lang.IPersistentVector
+ JSONable
+ {:to-json encode-seq})
+
+(extend clojure.lang.IPersistentSet
+ JSONable
+ {:to-json encode-seq})
+
+(extend clojure.lang.IPersistentList
+ JSONable
+ {:to-json encode-seq})
+
+(extend java.util.Date
+ JSONable
+ {:to-json encode-date})
+
+(extend java.sql.Timestamp
+ JSONable
+ {:to-json #(encode-date (Date. (.getTime ^java.sql.Timestamp %1)) %2)})
+
+(extend java.util.UUID
+ JSONable
+ {:to-json encode-str})
+
+(extend java.lang.Boolean
+ JSONable
+ {:to-json encode-bool})
+
+(extend clojure.lang.Keyword
+ JSONable
+ {:to-json encode-named})
+
+(extend clojure.lang.IPersistentMap
+ JSONable
+ {:to-json encode-map})
+
+(extend clojure.lang.Symbol
+ JSONable
+ {:to-json encode-symbol})
+
+(extend clojure.lang.Associative
+ JSONable
+ {:to-json encode-map})
+
+;; Utility methods to add and remove encoders
+(defn add-encoder
+ "Provide an encoder for a type not handled by Cheshire.
+
+ ex. (add-encoder java.net.URL encode-string)
+
+ See encode-str, encode-map, etc, in the cheshire.custom
+ namespace for encoder examples."
+ [cls encoder]
+ (extend cls
+ JSONable
+ {:to-json encoder}))
+
+(defn remove-encoder [cls]
+ "Remove encoder for a given type.
+
+ ex. (remove-encoder java.net.URL)"
+ (alter-var-root #'JSONable #(assoc % :impls (dissoc (:impls %) cls)))
+ (clojure.core/-reset-methods JSONable))
View
14 test/cheshire/test/benchmark.clj
@@ -1,7 +1,7 @@
(ns cheshire.test.benchmark
(:use [clojure.test])
(:require [cheshire.core :as core]
- [cheshire.custom :as custom]
+ [cheshire.generate :as custom]
[clojure.data.json :as cj]
[clj-json.core :as clj-json]
[criterium.core :as bench]))
@@ -42,20 +42,12 @@
(deftest ^{:benchmark true} t-bench-custom
(println "--------- Custom Benchmarks ---------")
- (println "[+] Custom, no custom fields:")
- (bench/with-progress-reporting
- (bench/quick-bench (custom/decode (custom/encode test-obj)) :verbose))
- (println "- - - - - - - - - - - - - - - - - - -")
(custom/add-encoder java.net.URL custom/encode-str)
- (is (= "\"http://foo.com\"" (custom/encode (java.net.URL. "http://foo.com"))))
+ (is (= "\"http://foo.com\"" (core/encode (java.net.URL. "http://foo.com"))))
(let [custom-obj (assoc test-obj "url" (java.net.URL. "http://foo.com"))]
(println "[+] Custom, all custom fields:")
(bench/with-progress-reporting
- (bench/quick-bench (custom/decode (custom/encode custom-obj)) :verbose))
- (println "- - - - - - - - - - - - - - - - - - -")
- (println "[+] Custom, bypass core with custom fields:")
- (bench/with-progress-reporting
- (bench/quick-bench (custom/decode (custom/encode* custom-obj)) :verbose)))
+ (bench/quick-bench (core/decode (core/encode custom-obj)) :verbose)))
(println "-------------------------------------"))
(deftest ^{:benchmark true} t-bench-custom-kw-coercion
View
14 test/cheshire/test/core.clj
@@ -2,9 +2,11 @@
(:use [clojure.test]
[clojure.java.io :only [file reader]])
(:require [cheshire.core :as json]
+ [cheshire.generate :as gen]
[cheshire.factory :as fact]
[cheshire.parse :as parse])
- (:import (java.io FileInputStream StringReader StringWriter
+ (:import (com.fasterxml.jackson.core JsonGenerationException)
+ (java.io FileInputStream StringReader StringWriter
BufferedReader BufferedWriter)
(java.sql Timestamp)
(java.util Date UUID)))
@@ -204,3 +206,13 @@
(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 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")))))
+
View
6 test/cheshire/test/custom.clj
@@ -1,4 +1,5 @@
(ns cheshire.test.custom
+ "DEPRECATED, kept here to ensure backward compatibility."
(:use [clojure.test]
[clojure.java.io :only [reader]])
(:require [cheshire.custom :as json] :reload
@@ -9,6 +10,11 @@
(java.sql Timestamp)
(java.util Date UUID)))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;; DEPRECATED, DO NOT USE ;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+
;; Generally, all tests in here should use json/encode*, unless you
;; know what you're doing and what you're trying to test.
Please sign in to comment.
Something went wrong with that request. Please try again.