|
| 1 | +(ns basilisp.json |
| 2 | + "JSON Encoder and Decoders |
| 3 | + |
| 4 | + This namespace includes functions for performing basic JSON encoding from |
| 5 | + and decoding to Basilisp builtin data structures. It is built on top of Python's |
| 6 | + builtin `json` module. The builtin `json` module is not intended to be extended |
| 7 | + in the way that is done here. As such, it is not the fastest JSON decoder or |
| 8 | + encoder available, but it is builtin so it is readily available for quick |
| 9 | + encoding and decoding needs." |
| 10 | + (:refer-basilisp :exclude [read]) |
| 11 | + (:import |
| 12 | + datetime |
| 13 | + decimal |
| 14 | + fractions |
| 15 | + json |
| 16 | + uuid)) |
| 17 | + |
| 18 | +;;;;;;;;;;;;;; |
| 19 | +;; Encoders ;; |
| 20 | +;;;;;;;;;;;;;; |
| 21 | + |
| 22 | +(defprotocol JSONEncodeable |
| 23 | + (to-json-encodeable* [this opts] |
| 24 | + "Return an object which can be JSON encoded by Python's default JSONEncoder. |
| 25 | + |
| 26 | + `opts` is a map with the following options: |
| 27 | + |
| 28 | + `:key-fn` - is a function which will be called for each key in a map; |
| 29 | + default is `name`")) |
| 30 | + |
| 31 | +(extend-protocol JSONEncodeable |
| 32 | + python/object |
| 33 | + (to-json-encodeable* [this _] |
| 34 | + (throw |
| 35 | + (python/TypeError |
| 36 | + (str "Cannot JSON encode objects of type " (python/type this)))))) |
| 37 | + |
| 38 | +(defn ^:private encodeable-scalar |
| 39 | + [o _] |
| 40 | + o) |
| 41 | + |
| 42 | +(defn ^:private stringify-scalar |
| 43 | + [o _] |
| 44 | + (python/str o)) |
| 45 | + |
| 46 | +(defn ^:private encodeable-date-type |
| 47 | + [o _] |
| 48 | + (.isoformat o)) |
| 49 | + |
| 50 | +(defn ^:private kw-or-sym-to-encodeable |
| 51 | + [o _] |
| 52 | + (if-let [ns-str (namespace o)] |
| 53 | + (str ns-str "/" (name o)) |
| 54 | + (name o))) |
| 55 | + |
| 56 | +(defn ^:private map-to-encodeable |
| 57 | + [o {:keys [key-fn] :as opts}] |
| 58 | + (->> o |
| 59 | + (map (fn [[k v]] [(key-fn k) v])) |
| 60 | + (python/dict))) |
| 61 | + |
| 62 | +(defn ^:private seq-to-encodeable |
| 63 | + [o opts] |
| 64 | + (->> o |
| 65 | + (map #(to-json-encodeable* % opts)) |
| 66 | + (python/list))) |
| 67 | + |
| 68 | +(extend python/str JSONEncodeable {:to-json-encodeable* encodeable-scalar}) |
| 69 | +(extend python/int JSONEncodeable {:to-json-encodeable* encodeable-scalar}) |
| 70 | +(extend python/float JSONEncodeable {:to-json-encodeable* encodeable-scalar}) |
| 71 | +(extend python/bool JSONEncodeable {:to-json-encodeable* encodeable-scalar}) |
| 72 | +(extend nil JSONEncodeable {:to-json-encodeable* encodeable-scalar}) |
| 73 | + |
| 74 | +(extend basilisp.lang.keyword/Keyword JSONEncodeable {:to-json-encodeable* kw-or-sym-to-encodeable}) |
| 75 | +(extend basilisp.lang.symbol/Symbol JSONEncodeable {:to-json-encodeable* kw-or-sym-to-encodeable}) |
| 76 | + |
| 77 | +(extend basilisp.lang.interfaces/IPersistentMap JSONEncodeable {:to-json-encodeable* map-to-encodeable}) |
| 78 | + |
| 79 | +(extend basilisp.lang.interfaces/IPersistentList JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) |
| 80 | +(extend basilisp.lang.interfaces/IPersistentSet JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) |
| 81 | +(extend basilisp.lang.interfaces/IPersistentVector JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) |
| 82 | + |
| 83 | +;; Support extended reader types. |
| 84 | +(extend datetime/datetime JSONEncodeable {:to-json-encodeable* encodeable-date-type}) |
| 85 | +(extend datetime/date JSONEncodeable {:to-json-encodeable* encodeable-date-type}) |
| 86 | +(extend datetime/time JSONEncodeable {:to-json-encodeable* encodeable-date-type}) |
| 87 | +(extend decimal/Decimal JSONEncodeable {:to-json-encodeable* stringify-scalar}) |
| 88 | +(extend fractions/Fraction JSONEncodeable {:to-json-encodeable* stringify-scalar}) |
| 89 | +(extend uuid/UUID JSONEncodeable {:to-json-encodeable* stringify-scalar}) |
| 90 | + |
| 91 | +;; Support Python types in case they are embedded in other Basilisp collections. |
| 92 | +(extend python/dict JSONEncodeable {:to-json-encodeable* (fn [d opts] (map-to-encodeable (.items d) opts))}) |
| 93 | +(extend python/list JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) |
| 94 | +(extend python/tuple JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) |
| 95 | +(extend python/set JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) |
| 96 | +(extend python/frozenset JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) |
| 97 | + |
| 98 | +(defn ^:private write-opts |
| 99 | + [{:keys [escape-non-ascii indent item-sep key-fn key-sep]}] |
| 100 | + {:escape-non-ascii (if (boolean? escape-non-ascii) escape-non-ascii true) |
| 101 | + :indent indent |
| 102 | + :key-fn (or key-fn name) |
| 103 | + :separator #py ((or item-sep ", ") (or key-sep ": "))}) |
| 104 | + |
| 105 | +(defn write |
| 106 | + "Serialize the object `o` as JSON to the writer object `writer` (which must be |
| 107 | + any file-like object supporting `.write()` method). |
| 108 | + |
| 109 | + All data structures supported by the Basilisp reader are serialized to JSON |
| 110 | + by default. Maps are serialized as JSON Objects. Lists, sets, and vectors are |
| 111 | + serialized as JSON arrays. Keywords and symbols are serialized as strings with |
| 112 | + their namespace (if they have one). Python scalar types are serialized as their |
| 113 | + corresponding JSON types (string, integer, float, boolean, and `nil`). Instants |
| 114 | + (Python `datetime`s) and the related Python `date` and `time` types are |
| 115 | + serialized as ISO 8601 date strings. Decimals are serialized as stringified |
| 116 | + floats. Fractions are serialized as stringified ratios (numerator and |
| 117 | + denominator). UUIDs are serialized as their canonical hex string format. |
| 118 | + |
| 119 | + Support for other data structures can be added by extending the `JSONEncodeable` |
| 120 | + Protocol. That protocol includes one method which must return a Python data type |
| 121 | + which can be understood by Python's builtin `json` module. |
| 122 | + |
| 123 | + The encoder supports a few options which may be specified as key/value pairs: |
| 124 | + |
| 125 | + `:key-fn` - is a function which will be called for each key in a map; |
| 126 | + default is `name` |
| 127 | + `:escape-non-ascii` - if `true`, escape non-ASCII characters in the output; |
| 128 | + default is `true` |
| 129 | + `:indent` - if `nil`, use a compact representation; if a positive |
| 130 | + integer, each indent level will be that many spaces; if |
| 131 | + zero, a negative integer, or the empty string, newlines |
| 132 | + will be inserted without indenting; if a string, that |
| 133 | + string value will be used as the indent |
| 134 | + `:item-sep` - a string separator between object and array items; |
| 135 | + default is ', ' |
| 136 | + `:key-sep` - a string separator between object key/value pairs; |
| 137 | + default is ': '" |
| 138 | + [o writer & {:as opts}] |
| 139 | + (let [{:keys [escape-non-ascii indent separator] :as opts} (write-opts opts)] |
| 140 | + (json/dump o writer ** |
| 141 | + :default #(to-json-encodeable* % opts) |
| 142 | + :ensure-ascii escape-non-ascii |
| 143 | + :indent indent |
| 144 | + :separators separator))) |
| 145 | + |
| 146 | +(defn write-str |
| 147 | + "Serialize the object `o` as JSON and return the serialized object as a string. |
| 148 | + |
| 149 | + All data structures supported by the Basilisp reader are serialized to JSON |
| 150 | + by default. Maps are serialized as JSON Objects. Lists, sets, and vectors are |
| 151 | + serialized as JSON arrays. Keywords and symbols are serialized as strings with |
| 152 | + their namespace (if they have one). Python scalar types are serialized as their |
| 153 | + corresponding JSON types (string, integer, float, boolean, and `nil`). Instants |
| 154 | + (Python `datetime`s) and the related Python `date` and `time` types are |
| 155 | + serialized as ISO 8601 date strings. Decimals are serialized as stringified |
| 156 | + floats. Fractions are serialized as stringified ratios (numerator and |
| 157 | + denominator). UUIDs are serialized as their canonical hex string format. |
| 158 | + |
| 159 | + Support for other data structures can be added by extending the `JSONEncodeable` |
| 160 | + Protocol. That protocol includes one method which must return a Python data type |
| 161 | + which can be understood by Python's builtin `json` module. |
| 162 | + |
| 163 | + The options for `write-str` are the same as for those of `write`." |
| 164 | + [o & {:as opts}] |
| 165 | + (let [{:keys [escape-non-ascii indent separator] :as opts} (write-opts opts)] |
| 166 | + (json/dumps o ** |
| 167 | + :default #(to-json-encodeable* % opts) |
| 168 | + :ensure-ascii escape-non-ascii |
| 169 | + :indent indent |
| 170 | + :separators separator))) |
| 171 | + |
| 172 | +;;;;;;;;;;;;;; |
| 173 | +;; Decoders ;; |
| 174 | +;;;;;;;;;;;;;; |
| 175 | + |
| 176 | +(defprotocol JSONDecodeable |
| 177 | + (from-decoded-json* [this opts] |
| 178 | + "Return a Basilisp object in place of a Python object returned by Python's |
| 179 | + default JSONDecoder. |
| 180 | + |
| 181 | + `opts` is a map with the following options: |
| 182 | + |
| 183 | + `:key-fn` - is a function which will be called for each key in a map; |
| 184 | + default is `identity`")) |
| 185 | + |
| 186 | +(extend-protocol JSONDecodeable |
| 187 | + python/dict |
| 188 | + (from-decoded-json* [this {:keys [key-fn] :as opts}] |
| 189 | + (->> (.items this) |
| 190 | + (mapcat (fn [[k v]] [(key-fn k) (from-decoded-json* v opts)])) |
| 191 | + (apply hash-map))) |
| 192 | + |
| 193 | + python/list |
| 194 | + (from-decoded-json* [this opts] |
| 195 | + (->> this (map #(from-decoded-json* % opts)) (vec)))) |
| 196 | + |
| 197 | +(defn ^:private decode-scalar |
| 198 | + [o _] |
| 199 | + o) |
| 200 | + |
| 201 | +(extend python/int JSONDecodeable {:from-decoded-json* decode-scalar}) |
| 202 | +(extend python/float JSONDecodeable {:from-decoded-json* decode-scalar}) |
| 203 | +(extend python/str JSONDecodeable {:from-decoded-json* decode-scalar}) |
| 204 | +(extend python/bool JSONDecodeable {:from-decoded-json* decode-scalar}) |
| 205 | +(extend nil JSONDecodeable {:from-decoded-json* decode-scalar}) |
| 206 | + |
| 207 | +(defn ^:private read-opts |
| 208 | + [{:keys [key-fn strict?]}] |
| 209 | + {:key-fn (or key-fn identity) |
| 210 | + :strict (if (boolean? strict?) strict? true)}) |
| 211 | + |
| 212 | +;; Python's builtin `json.load` currently only includes an Object hook; it has |
| 213 | +;; no hook for Array types. Due to this limitation, we have to iteratively |
| 214 | +;; transform the entire parsed object into Basilisp data structures rather than |
| 215 | +;; building the final object iteratively. There is an open bug report with |
| 216 | +;; Python, but it has gotten no traction: https://bugs.python.org/issue36738 |
| 217 | + |
| 218 | +(defn read |
| 219 | + "Decode the JSON-encoded stream from `reader` (which can be any Python file-like |
| 220 | + object) into Basilisp data structures. |
| 221 | + |
| 222 | + JSON Objects will be decoded as Basilisp maps. JSON Arrays will be decoded as |
| 223 | + as Basilisp vectors. All other JSON data types will be decoded as the |
| 224 | + corresponding Python types (strings, booleans, integers, floats, and `nil`). |
| 225 | + |
| 226 | + The decoder supports a few options which may be specified as key/value pairs: |
| 227 | + |
| 228 | + `:key-fn` - is a function which will be called for each key in a map; |
| 229 | + default is `identity` |
| 230 | + `:strict?` - boolean value; if `true`, control characters (characters in |
| 231 | + ASCII 0-31 range) will be prohibited inside JSON strings; |
| 232 | + default is `true`" |
| 233 | + [reader & {:as opts}] |
| 234 | + (let [{:keys [strict?] :as opts} (read-opts opts)] |
| 235 | + (-> (json/load reader ** :strict strict?) |
| 236 | + (from-decoded-json* opts)))) |
| 237 | + |
| 238 | +(defn read-str |
| 239 | + "Decode the JSON-encoded string `s` into Basilisp data structures. |
| 240 | + |
| 241 | + JSON Objects will be decoded as Basilisp maps. JSON Arrays will be decoded as |
| 242 | + as Basilisp vectors. All other JSON data types will be decoded as the |
| 243 | + corresponding Python types (strings, booleans, integers, floats, and `nil`). |
| 244 | + |
| 245 | + The options for `read-str` are the same as for those of `read`." |
| 246 | + [s & {:as opts}] |
| 247 | + (let [{:keys [strict?] :as opts} (read-opts opts)] |
| 248 | + (-> (json/loads s ** :strict strict?) |
| 249 | + (from-decoded-json* opts)))) |
0 commit comments