-
-
Notifications
You must be signed in to change notification settings - Fork 19
Add JSON encoder and decoder #485
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
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
9be8ed7
Add JSON encoder and decoder
chrisrink10 a6184b2
Merge branch 'master' into feature/json-encoder
chrisrink10 e25e2b0
remove it
chrisrink10 f3946fa
Merge branch 'master' into feature/json-encoder
chrisrink10 ff78875
Merge branch 'master' into feature/json-encoder
chrisrink10 b0d1539
Protocol-based JSON encoding
chrisrink10 21b72ea
Documentation
chrisrink10 369e9c1
Decoder
chrisrink10 7183c3d
REPL stuff
chrisrink10 a4092ad
j s o n
chrisrink10 779294d
Lots of stuff
chrisrink10 434a43b
Change the name
chrisrink10 ec3c00d
Clean up single dispatch
chrisrink10 fe3e7b9
Merge branch 'master' into feature/json-encoder
chrisrink10 84090ce
Test baseline
chrisrink10 3e290fc
Reader tests
chrisrink10 d6a09ab
Lots of cleaning
chrisrink10 e80c362
Fixes
chrisrink10 253a265
Doing a talking
chrisrink10 16b3b7a
Include date and time
chrisrink10 40d524e
doc the string
chrisrink10 3fa4585
Moar tests
chrisrink10 80ef396
Object tests
chrisrink10 799e9fc
Unused symbol
chrisrink10 7bdcd39
Apply namespace docstring to namespace meta
chrisrink10 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,249 @@ | ||
| (ns basilisp.json | ||
| "JSON Encoder and Decoders | ||
|
|
||
| This namespace includes functions for performing basic JSON encoding from | ||
| and decoding to Basilisp builtin data structures. It is built on top of Python's | ||
| builtin `json` module. The builtin `json` module is not intended to be extended | ||
| in the way that is done here. As such, it is not the fastest JSON decoder or | ||
| encoder available, but it is builtin so it is readily available for quick | ||
| encoding and decoding needs." | ||
| (:refer-basilisp :exclude [read]) | ||
| (:import | ||
| datetime | ||
| decimal | ||
| fractions | ||
| json | ||
| uuid)) | ||
|
|
||
| ;;;;;;;;;;;;;; | ||
| ;; Encoders ;; | ||
| ;;;;;;;;;;;;;; | ||
|
|
||
| (defprotocol JSONEncodeable | ||
| (to-json-encodeable* [this opts] | ||
| "Return an object which can be JSON encoded by Python's default JSONEncoder. | ||
|
|
||
| `opts` is a map with the following options: | ||
|
|
||
| `:key-fn` - is a function which will be called for each key in a map; | ||
| default is `name`")) | ||
|
|
||
| (extend-protocol JSONEncodeable | ||
| python/object | ||
| (to-json-encodeable* [this _] | ||
| (throw | ||
| (python/TypeError | ||
| (str "Cannot JSON encode objects of type " (python/type this)))))) | ||
|
|
||
| (defn ^:private encodeable-scalar | ||
| [o _] | ||
| o) | ||
|
|
||
| (defn ^:private stringify-scalar | ||
| [o _] | ||
| (python/str o)) | ||
|
|
||
| (defn ^:private encodeable-date-type | ||
| [o _] | ||
| (.isoformat o)) | ||
|
|
||
| (defn ^:private kw-or-sym-to-encodeable | ||
| [o _] | ||
| (if-let [ns-str (namespace o)] | ||
| (str ns-str "/" (name o)) | ||
| (name o))) | ||
|
|
||
| (defn ^:private map-to-encodeable | ||
| [o {:keys [key-fn] :as opts}] | ||
| (->> o | ||
| (map (fn [[k v]] [(key-fn k) v])) | ||
| (python/dict))) | ||
|
|
||
| (defn ^:private seq-to-encodeable | ||
| [o opts] | ||
| (->> o | ||
| (map #(to-json-encodeable* % opts)) | ||
| (python/list))) | ||
|
|
||
| (extend python/str JSONEncodeable {:to-json-encodeable* encodeable-scalar}) | ||
| (extend python/int JSONEncodeable {:to-json-encodeable* encodeable-scalar}) | ||
| (extend python/float JSONEncodeable {:to-json-encodeable* encodeable-scalar}) | ||
| (extend python/bool JSONEncodeable {:to-json-encodeable* encodeable-scalar}) | ||
| (extend nil JSONEncodeable {:to-json-encodeable* encodeable-scalar}) | ||
|
|
||
| (extend basilisp.lang.keyword/Keyword JSONEncodeable {:to-json-encodeable* kw-or-sym-to-encodeable}) | ||
| (extend basilisp.lang.symbol/Symbol JSONEncodeable {:to-json-encodeable* kw-or-sym-to-encodeable}) | ||
|
|
||
| (extend basilisp.lang.interfaces/IPersistentMap JSONEncodeable {:to-json-encodeable* map-to-encodeable}) | ||
|
|
||
| (extend basilisp.lang.interfaces/IPersistentList JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) | ||
| (extend basilisp.lang.interfaces/IPersistentSet JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) | ||
| (extend basilisp.lang.interfaces/IPersistentVector JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) | ||
|
|
||
| ;; Support extended reader types. | ||
| (extend datetime/datetime JSONEncodeable {:to-json-encodeable* encodeable-date-type}) | ||
| (extend datetime/date JSONEncodeable {:to-json-encodeable* encodeable-date-type}) | ||
| (extend datetime/time JSONEncodeable {:to-json-encodeable* encodeable-date-type}) | ||
| (extend decimal/Decimal JSONEncodeable {:to-json-encodeable* stringify-scalar}) | ||
| (extend fractions/Fraction JSONEncodeable {:to-json-encodeable* stringify-scalar}) | ||
| (extend uuid/UUID JSONEncodeable {:to-json-encodeable* stringify-scalar}) | ||
|
|
||
| ;; Support Python types in case they are embedded in other Basilisp collections. | ||
| (extend python/dict JSONEncodeable {:to-json-encodeable* (fn [d opts] (map-to-encodeable (.items d) opts))}) | ||
| (extend python/list JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) | ||
| (extend python/tuple JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) | ||
| (extend python/set JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) | ||
| (extend python/frozenset JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) | ||
|
|
||
| (defn ^:private write-opts | ||
| [{:keys [escape-non-ascii indent item-sep key-fn key-sep]}] | ||
| {:escape-non-ascii (if (boolean? escape-non-ascii) escape-non-ascii true) | ||
| :indent indent | ||
| :key-fn (or key-fn name) | ||
| :separator #py ((or item-sep ", ") (or key-sep ": "))}) | ||
|
|
||
| (defn write | ||
| "Serialize the object `o` as JSON to the writer object `writer` (which must be | ||
| any file-like object supporting `.write()` method). | ||
|
|
||
| All data structures supported by the Basilisp reader are serialized to JSON | ||
| by default. Maps are serialized as JSON Objects. Lists, sets, and vectors are | ||
| serialized as JSON arrays. Keywords and symbols are serialized as strings with | ||
| their namespace (if they have one). Python scalar types are serialized as their | ||
| corresponding JSON types (string, integer, float, boolean, and `nil`). Instants | ||
| (Python `datetime`s) and the related Python `date` and `time` types are | ||
| serialized as ISO 8601 date strings. Decimals are serialized as stringified | ||
| floats. Fractions are serialized as stringified ratios (numerator and | ||
| denominator). UUIDs are serialized as their canonical hex string format. | ||
|
|
||
| Support for other data structures can be added by extending the `JSONEncodeable` | ||
| Protocol. That protocol includes one method which must return a Python data type | ||
| which can be understood by Python's builtin `json` module. | ||
|
|
||
| The encoder supports a few options which may be specified as key/value pairs: | ||
|
|
||
| `:key-fn` - is a function which will be called for each key in a map; | ||
| default is `name` | ||
| `:escape-non-ascii` - if `true`, escape non-ASCII characters in the output; | ||
| default is `true` | ||
| `:indent` - if `nil`, use a compact representation; if a positive | ||
| integer, each indent level will be that many spaces; if | ||
| zero, a negative integer, or the empty string, newlines | ||
| will be inserted without indenting; if a string, that | ||
| string value will be used as the indent | ||
| `:item-sep` - a string separator between object and array items; | ||
| default is ', ' | ||
| `:key-sep` - a string separator between object key/value pairs; | ||
| default is ': '" | ||
| [o writer & {:as opts}] | ||
| (let [{:keys [escape-non-ascii indent separator] :as opts} (write-opts opts)] | ||
| (json/dump o writer ** | ||
| :default #(to-json-encodeable* % opts) | ||
| :ensure-ascii escape-non-ascii | ||
| :indent indent | ||
| :separators separator))) | ||
|
|
||
| (defn write-str | ||
| "Serialize the object `o` as JSON and return the serialized object as a string. | ||
|
|
||
| All data structures supported by the Basilisp reader are serialized to JSON | ||
| by default. Maps are serialized as JSON Objects. Lists, sets, and vectors are | ||
| serialized as JSON arrays. Keywords and symbols are serialized as strings with | ||
| their namespace (if they have one). Python scalar types are serialized as their | ||
| corresponding JSON types (string, integer, float, boolean, and `nil`). Instants | ||
| (Python `datetime`s) and the related Python `date` and `time` types are | ||
| serialized as ISO 8601 date strings. Decimals are serialized as stringified | ||
| floats. Fractions are serialized as stringified ratios (numerator and | ||
| denominator). UUIDs are serialized as their canonical hex string format. | ||
|
|
||
| Support for other data structures can be added by extending the `JSONEncodeable` | ||
| Protocol. That protocol includes one method which must return a Python data type | ||
| which can be understood by Python's builtin `json` module. | ||
|
|
||
| The options for `write-str` are the same as for those of `write`." | ||
| [o & {:as opts}] | ||
| (let [{:keys [escape-non-ascii indent separator] :as opts} (write-opts opts)] | ||
| (json/dumps o ** | ||
| :default #(to-json-encodeable* % opts) | ||
| :ensure-ascii escape-non-ascii | ||
| :indent indent | ||
| :separators separator))) | ||
|
|
||
| ;;;;;;;;;;;;;; | ||
| ;; Decoders ;; | ||
| ;;;;;;;;;;;;;; | ||
|
|
||
| (defprotocol JSONDecodeable | ||
| (from-decoded-json* [this opts] | ||
| "Return a Basilisp object in place of a Python object returned by Python's | ||
| default JSONDecoder. | ||
|
|
||
| `opts` is a map with the following options: | ||
|
|
||
| `:key-fn` - is a function which will be called for each key in a map; | ||
| default is `identity`")) | ||
|
|
||
| (extend-protocol JSONDecodeable | ||
| python/dict | ||
| (from-decoded-json* [this {:keys [key-fn] :as opts}] | ||
| (->> (.items this) | ||
| (mapcat (fn [[k v]] [(key-fn k) (from-decoded-json* v opts)])) | ||
| (apply hash-map))) | ||
|
|
||
| python/list | ||
| (from-decoded-json* [this opts] | ||
| (->> this (map #(from-decoded-json* % opts)) (vec)))) | ||
|
|
||
| (defn ^:private decode-scalar | ||
| [o _] | ||
| o) | ||
|
|
||
| (extend python/int JSONDecodeable {:from-decoded-json* decode-scalar}) | ||
| (extend python/float JSONDecodeable {:from-decoded-json* decode-scalar}) | ||
| (extend python/str JSONDecodeable {:from-decoded-json* decode-scalar}) | ||
| (extend python/bool JSONDecodeable {:from-decoded-json* decode-scalar}) | ||
| (extend nil JSONDecodeable {:from-decoded-json* decode-scalar}) | ||
|
|
||
| (defn ^:private read-opts | ||
| [{:keys [key-fn strict?]}] | ||
| {:key-fn (or key-fn identity) | ||
| :strict (if (boolean? strict?) strict? true)}) | ||
|
|
||
| ;; Python's builtin `json.load` currently only includes an Object hook; it has | ||
| ;; no hook for Array types. Due to this limitation, we have to iteratively | ||
| ;; transform the entire parsed object into Basilisp data structures rather than | ||
| ;; building the final object iteratively. There is an open bug report with | ||
| ;; Python, but it has gotten no traction: https://bugs.python.org/issue36738 | ||
|
|
||
| (defn read | ||
| "Decode the JSON-encoded stream from `reader` (which can be any Python file-like | ||
| object) into Basilisp data structures. | ||
|
|
||
| JSON Objects will be decoded as Basilisp maps. JSON Arrays will be decoded as | ||
| as Basilisp vectors. All other JSON data types will be decoded as the | ||
| corresponding Python types (strings, booleans, integers, floats, and `nil`). | ||
|
|
||
| The decoder supports a few options which may be specified as key/value pairs: | ||
|
|
||
| `:key-fn` - is a function which will be called for each key in a map; | ||
| default is `identity` | ||
| `:strict?` - boolean value; if `true`, control characters (characters in | ||
| ASCII 0-31 range) will be prohibited inside JSON strings; | ||
| default is `true`" | ||
| [reader & {:as opts}] | ||
| (let [{:keys [strict?] :as opts} (read-opts opts)] | ||
| (-> (json/load reader ** :strict strict?) | ||
| (from-decoded-json* opts)))) | ||
|
|
||
| (defn read-str | ||
| "Decode the JSON-encoded string `s` into Basilisp data structures. | ||
|
|
||
| JSON Objects will be decoded as Basilisp maps. JSON Arrays will be decoded as | ||
| as Basilisp vectors. All other JSON data types will be decoded as the | ||
| corresponding Python types (strings, booleans, integers, floats, and `nil`). | ||
|
|
||
| The options for `read-str` are the same as for those of `read`." | ||
| [s & {:as opts}] | ||
| (let [{:keys [strict?] :as opts} (read-opts opts)] | ||
| (-> (json/loads s ** :strict strict?) | ||
| (from-decoded-json* opts)))) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the extended method function uses the dispatch method name, then the compiler will add the local function name to the symbol table for that name and users will be unable to call the generic Protocol dispatch method.