From 9be8ed7732e5a21b81830c04657910aa8acaa3ac Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Sun, 23 Feb 2020 14:30:33 -0500 Subject: [PATCH 01/21] Add JSON encoder and decoder --- CHANGELOG.md | 1 + src/basilisp/json.lpy | 81 ++++++++++++++++++++++++++ src/basilisp/lang/compiler/analyzer.py | 4 ++ 3 files changed, 86 insertions(+) create mode 100644 src/basilisp/json.lpy diff --git a/CHANGELOG.md b/CHANGELOG.md index dd374b5c4..c32e99007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `letfn` special form (#473) * Added `defn-`, `declare`, and `defonce` macros (#480) * Added EDN reader in the `basilisp.edn` namespace (#477) + * Add JSON encoder and decoder in `basilisp.json` namespace (#???) ### Changed * Change the default user namespace to `basilisp.user` (#466) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy new file mode 100644 index 000000000..c588d3f9a --- /dev/null +++ b/src/basilisp/json.lpy @@ -0,0 +1,81 @@ +(ns basilisp.json + (:import json)) + +;;;;;;;;;;;;;; +;; Encoders ;; +;;;;;;;;;;;;;; + +(declare ^:private encode-json) + +(defn ^:private encode-kw-or-sym + [o] + (if-let [ns-str (namespace o)] + (str ns-str "/" (name o)) + (name o))) + +(defn ^:private encode-map-key + [k] + (if (keyword? k) + (name k) + (str k))) + +(defn ^:private encode-map + [o {:keys [key-fn value-fn] :as opts}] + (let [encode-value (or value-fn + (fn [_ v] + (encode-json v opts)))] + (->> o + (map (fn [[k v]] + [(key-fn k) (encode-value k v)])) + (python/dict)))) + +(defn ^:private encode-seq + [o opts] + (->> o + (map #(encode-json % opts)) + (python/list))) + +(defmulti ^:private encode-json + (fn [o _] + (println (type o)) + (type o))) + +(defmethod encode-json basilisp.lang.list/List + [o opts] + (encode-seq o opts)) + +(defmethod encode-json basilisp.lang.map/Map + [o opts] + (encode-map o opts)) + +(defmethod encode-json basilisp.lang.set/Set + [o opts] + (encode-seq o opts)) + +(defmethod encode-json basilisp.lang.vector/Vector + [o opts] + (encode-seq o opts)) + +(defmethod encode-json basilisp.lang.keyword/Keyword + [o _] + (encode-kw-or-sym o)) + +(defmethod encode-json basilisp.lang.symbol/Symbol + [o _] + (encode-kw-or-sym o)) + +(defmethod encode-json :default + [o opts] + (cond + (map? o) (encode-map o opts) + (seq o) (encode-seq o opts) + (symbol? o) (encode-kw-or-sym o) + (keyword? o) (encode-kw-or-sym o) + :else (throw + (python/TypeError "Object is not JSON serializable")))) + +(defn write-str + [o & {:keys [key-fn value-fn]}] + (apply-kw json/dumps o {:default (fn [o] + (encode-json o {:key-fn (or key-fn encode-map-key) + :value-fn value-fn}))})) diff --git a/src/basilisp/lang/compiler/analyzer.py b/src/basilisp/lang/compiler/analyzer.py index 4b98c1e28..8acb61602 100644 --- a/src/basilisp/lang/compiler/analyzer.py +++ b/src/basilisp/lang/compiler/analyzer.py @@ -2442,6 +2442,10 @@ def __resolve_namespaced_symbol_in_ns( # pylint: disable=too-many-branches ns = which_ns.import_aliases[ns_sym] assert ns is not None ns_name = ns.name + elif ns_sym in which_ns.aliases: + ns = which_ns.aliases[ns_sym] + assert ns is not None + ns_name = ns.name else: ns_name = ns_sym.name From e25e2b0155d75e3d609213594b7a7285b281fc8a Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Fri, 15 May 2020 21:46:06 -0400 Subject: [PATCH 02/21] remove it --- src/basilisp/lang/compiler/analyzer.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/basilisp/lang/compiler/analyzer.py b/src/basilisp/lang/compiler/analyzer.py index 2d1ddcc19..76fa98410 100644 --- a/src/basilisp/lang/compiler/analyzer.py +++ b/src/basilisp/lang/compiler/analyzer.py @@ -2759,10 +2759,6 @@ def __resolve_namespaced_symbol_in_ns( # pylint: disable=too-many-branches ns = which_ns.import_aliases[ns_sym] assert ns is not None ns_name = ns.name - elif ns_sym in which_ns.aliases: - ns = which_ns.aliases[ns_sym] - assert ns is not None - ns_name = ns.name else: ns_name = ns_sym.name From b0d1539518aa9f893d4e4845263831ffd451a7fe Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Sun, 31 May 2020 17:45:46 -0400 Subject: [PATCH 03/21] Protocol-based JSON encoding --- src/basilisp/json.lpy | 101 ++++++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 43 deletions(-) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index c588d3f9a..6ce2a2538 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -5,14 +5,51 @@ ;; Encoders ;; ;;;;;;;;;;;;;; -(declare ^:private encode-json) +(defprotocol JSONEncodeable + (encode-to-json* [this opts] + "Return an object which can be JSON encoded by Python's default JSONEncoder. + + `opts` is a map of options: + - `:key-fn`")) + +(defn ^:private encode-scalar + [o _] + o) + +(extend python/str + JSONEncodeable + {:encode-to-json* encode-scalar}) + +(extend python/int + JSONEncodeable + {:encode-to-json* encode-scalar}) + +(extend python/float + JSONEncodeable + {:encode-to-json* encode-scalar}) + +(extend python/bool + JSONEncodeable + {:encode-to-json* encode-scalar}) + +(extend nil + JSONEncodeable + {:encode-to-json* encode-scalar}) (defn ^:private encode-kw-or-sym - [o] + [o _] (if-let [ns-str (namespace o)] (str ns-str "/" (name o)) (name o))) +(extend basilisp.lang.keyword/Keyword + JSONEncodeable + {:encode-to-json* encode-kw-or-sym}) + +(extend basilisp.lang.symbol/Symbol + JSONEncodeable + {:encode-to-json* encode-kw-or-sym}) + (defn ^:private encode-map-key [k] (if (keyword? k) @@ -23,59 +60,37 @@ [o {:keys [key-fn value-fn] :as opts}] (let [encode-value (or value-fn (fn [_ v] - (encode-json v opts)))] + (encode-to-json* v opts)))] (->> o (map (fn [[k v]] [(key-fn k) (encode-value k v)])) (python/dict)))) +(extend basilisp.lang.interfaces/IPersistentMap + JSONEncodeable + {:encode-to-json* encode-map}) + (defn ^:private encode-seq [o opts] (->> o - (map #(encode-json % opts)) + (map #(encode-to-json* % opts)) (python/list))) -(defmulti ^:private encode-json - (fn [o _] - (println (type o)) - (type o))) +(extend basilisp.lang.interfaces/IPersistentList + JSONEncodeable + {:encode-to-json* encode-seq}) -(defmethod encode-json basilisp.lang.list/List - [o opts] - (encode-seq o opts)) - -(defmethod encode-json basilisp.lang.map/Map - [o opts] - (encode-map o opts)) - -(defmethod encode-json basilisp.lang.set/Set - [o opts] - (encode-seq o opts)) - -(defmethod encode-json basilisp.lang.vector/Vector - [o opts] - (encode-seq o opts)) - -(defmethod encode-json basilisp.lang.keyword/Keyword - [o _] - (encode-kw-or-sym o)) +(extend basilisp.lang.interfaces/IPersistentSet + JSONEncodeable + {:encode-to-json* encode-seq}) -(defmethod encode-json basilisp.lang.symbol/Symbol - [o _] - (encode-kw-or-sym o)) - -(defmethod encode-json :default - [o opts] - (cond - (map? o) (encode-map o opts) - (seq o) (encode-seq o opts) - (symbol? o) (encode-kw-or-sym o) - (keyword? o) (encode-kw-or-sym o) - :else (throw - (python/TypeError "Object is not JSON serializable")))) +(extend basilisp.lang.interfaces/IPersistentVector + JSONEncodeable + {:encode-to-json* encode-seq}) (defn write-str [o & {:keys [key-fn value-fn]}] - (apply-kw json/dumps o {:default (fn [o] - (encode-json o {:key-fn (or key-fn encode-map-key) - :value-fn value-fn}))})) + (json/dumps o ** + :default (fn [o] + (encode-to-json* o {:key-fn (or key-fn encode-map-key) + :value-fn value-fn})))) From 21b72eac060c1a059fd45da5c6b22d3acccd62e9 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Sun, 31 May 2020 17:49:09 -0400 Subject: [PATCH 04/21] Documentation --- src/basilisp/json.lpy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index 6ce2a2538..7f43a56a2 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -10,7 +10,8 @@ "Return an object which can be JSON encoded by Python's default JSONEncoder. `opts` is a map of options: - - `:key-fn`")) + - `:key-fn` is a function which will be called for each key in a map. + - `:value-fn` is a function called for each value in a map.")) (defn ^:private encode-scalar [o _] From 369e9c1138e0a0f2af130685bbbc9a1f16f6b9c5 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Sun, 31 May 2020 22:24:01 -0400 Subject: [PATCH 05/21] Decoder --- src/basilisp/json.lpy | 58 ++++++++++++++++++++++++++++++++++++ src/basilisp/lang/runtime.py | 8 ++--- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index 7f43a56a2..e488b9ce7 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -95,3 +95,61 @@ :default (fn [o] (encode-to-json* o {:key-fn (or key-fn encode-map-key) :value-fn value-fn})))) + +;;;;;;;;;;;;;; +;; Decoders ;; +;;;;;;;;;;;;;; + +(defprotocol JSONDecodeable + (decode-from-json* [this opts] + "Return an object which can be JSON encoded by Python's default JSONEncoder. + + `opts` is a map of options: + - `:key-fn` is a function which will be called for each key in a map. + - `:value-fn` is a function called for each value in a map.")) + +(extend-protocol JSONDecodeable + python/dict + (decode-from-json* [this {:keys [key-fn value-fn] :as opts}] + (->> (.items this) + (mapcat (fn [[k v]] + (let [new-k (key-fn k)] + [new-k (value-fn new-k v)]))) + (apply hash-map))) + + python/list + (decode-from-json* [this opts] + (->> this (map #(decode-from-json* % opts)) (vec)))) + +(defn ^:private decode-scalar + [o _] + o) + +(extend python/int + JSONDecodeable + {:decode-from-json* decode-scalar}) + +(extend python/float + JSONDecodeable + {:decode-from-json* decode-scalar}) + +(extend python/str + JSONDecodeable + {:decode-from-json* decode-scalar}) + +(extend python/bool + JSONDecodeable + {:decode-from-json* decode-scalar}) + +(extend nil + JSONDecodeable + {:decode-from-json* decode-scalar}) + +(defn read-str + [s & {:keys [key-fn value-fn] :as opts}] + (let [opts {:key-fn (or key-fn identity) + :value-fn (or value-fn + (fn [_ v] + (decode-from-json* v opts)))}] + (-> (json/loads s) + (decode-from-json* opts)))) diff --git a/src/basilisp/lang/runtime.py b/src/basilisp/lang/runtime.py index 86fdbf447..597ab4333 100644 --- a/src/basilisp/lang/runtime.py +++ b/src/basilisp/lang/runtime.py @@ -1246,9 +1246,9 @@ def _to_py_kw(o: kw.Keyword, keyword_fn: Callable[[kw.Keyword], Any] = _kw_name) return keyword_fn(o) -@to_py.register(llist.List) +@to_py.register(IPersistentList) @to_py.register(ISeq) -@to_py.register(vec.Vector) +@to_py.register(IPersistentVector) def _to_py_list( o: Union[IPersistentList, ISeq, IPersistentVector], keyword_fn: Callable[[kw.Keyword], Any] = _kw_name, @@ -1256,7 +1256,7 @@ def _to_py_list( return list(map(functools.partial(to_py, keyword_fn=keyword_fn), o)) -@to_py.register(lmap.Map) +@to_py.register(IPersistentMap) def _to_py_map( o: IPersistentMap, keyword_fn: Callable[[kw.Keyword], Any] = _kw_name ) -> dict: @@ -1266,7 +1266,7 @@ def _to_py_map( } -@to_py.register(lset.Set) +@to_py.register(IPersistentSet) def _to_py_set( o: IPersistentSet, keyword_fn: Callable[[kw.Keyword], Any] = _kw_name ) -> set: From 7183c3d50a720d552baa96a4e7331a2827ef3d42 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Mon, 1 Jun 2020 08:21:11 -0400 Subject: [PATCH 06/21] REPL stuff --- src/basilisp/repl.lpy | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/basilisp/repl.lpy b/src/basilisp/repl.lpy index 1e807f058..aabbf0d6a 100644 --- a/src/basilisp/repl.lpy +++ b/src/basilisp/repl.lpy @@ -17,15 +17,20 @@ (defn pydoc "Print the Python docstring for a function." [o] - (print (inspect/getdoc o))) + (println (inspect/getdoc o))) (defn print-doc "Print the docstring from an interned var." [v] (let [var-meta (meta v)] - (if var-meta - (print (:doc var-meta)) - nil))) + (println "------------------------") + (println (cond->> (name v) + (namespace v) (str (namespace v) "/"))) + (when var-meta + (when-let [arglists (:arglists var-meta)] + (println arglists)) + (when-let [docstring (:doc var-meta)] + (println " " docstring))))) (defmacro doc "Print the docstring from an interned Var if found." From a4092ad8a17e9098c5249a31f80af484afa5fc36 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Mon, 1 Jun 2020 09:18:15 -0400 Subject: [PATCH 07/21] j s o n --- src/basilisp/core.lpy | 2 +- src/basilisp/json.lpy | 145 +++++++++++++++++++----------------------- 2 files changed, 67 insertions(+), 80 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index c9955329d..4472219b0 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -4831,7 +4831,7 @@ (->> (group-by first methods) (reduce (fn [m [method-name arities]] (->> (map rest arities) - (apply list `fn method-name) + (apply list `fn) (assoc m (keyword (name method-name))))) {}))) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index e488b9ce7..a741085ae 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -17,40 +17,12 @@ [o _] o) -(extend python/str - JSONEncodeable - {:encode-to-json* encode-scalar}) - -(extend python/int - JSONEncodeable - {:encode-to-json* encode-scalar}) - -(extend python/float - JSONEncodeable - {:encode-to-json* encode-scalar}) - -(extend python/bool - JSONEncodeable - {:encode-to-json* encode-scalar}) - -(extend nil - JSONEncodeable - {:encode-to-json* encode-scalar}) - (defn ^:private encode-kw-or-sym [o _] (if-let [ns-str (namespace o)] (str ns-str "/" (name o)) (name o))) -(extend basilisp.lang.keyword/Keyword - JSONEncodeable - {:encode-to-json* encode-kw-or-sym}) - -(extend basilisp.lang.symbol/Symbol - JSONEncodeable - {:encode-to-json* encode-kw-or-sym}) - (defn ^:private encode-map-key [k] (if (keyword? k) @@ -67,34 +39,41 @@ [(key-fn k) (encode-value k v)])) (python/dict)))) -(extend basilisp.lang.interfaces/IPersistentMap - JSONEncodeable - {:encode-to-json* encode-map}) - (defn ^:private encode-seq [o opts] (->> o (map #(encode-to-json* % opts)) (python/list))) -(extend basilisp.lang.interfaces/IPersistentList - JSONEncodeable - {:encode-to-json* encode-seq}) +(extend python/str JSONEncodeable {:encode-to-json* encode-scalar}) +(extend python/int JSONEncodeable {:encode-to-json* encode-scalar}) +(extend python/float JSONEncodeable {:encode-to-json* encode-scalar}) +(extend python/bool JSONEncodeable {:encode-to-json* encode-scalar}) +(extend nil JSONEncodeable {:encode-to-json* encode-scalar}) + +(extend basilisp.lang.keyword/Keyword JSONEncodeable {:encode-to-json* encode-kw-or-sym}) +(extend basilisp.lang.symbol/Symbol JSONEncodeable {:encode-to-json* encode-kw-or-sym}) + +(extend basilisp.lang.interfaces/IPersistentMap JSONEncodeable {:encode-to-json* encode-map}) -(extend basilisp.lang.interfaces/IPersistentSet - JSONEncodeable - {:encode-to-json* encode-seq}) +(extend basilisp.lang.interfaces/IPersistentList JSONEncodeable {:encode-to-json* encode-seq}) +(extend basilisp.lang.interfaces/IPersistentSet JSONEncodeable {:encode-to-json* encode-seq}) +(extend basilisp.lang.interfaces/IPersistentVector JSONEncodeable {:encode-to-json* encode-seq}) -(extend basilisp.lang.interfaces/IPersistentVector - JSONEncodeable - {:encode-to-json* encode-seq}) +(defn ^:private write-opts + [{:keys [key-fn value-fn]}] + {:key-fn (or key-fn encode-map-key) + :value-fn value-fn}) + +(defn write + [writer & opts] + (let [opts (write-opts opts)] + (json/dump writer ** :default #(encode-to-json* % opts)))) (defn write-str - [o & {:keys [key-fn value-fn]}] - (json/dumps o ** - :default (fn [o] - (encode-to-json* o {:key-fn (or key-fn encode-map-key) - :value-fn value-fn})))) + [o & opts] + (let [opts (write-opts opts)] + (json/dumps o ** :default #(encode-to-json* % opts)))) ;;;;;;;;;;;;;; ;; Decoders ;; @@ -102,20 +81,27 @@ (defprotocol JSONDecodeable (decode-from-json* [this opts] - "Return an object which can be JSON encoded by Python's default JSONEncoder. + "Return a Basilisp object in place of a Python object returrned by Python's + default JSONDecoder. `opts` is a map of options: - - `:key-fn` is a function which will be called for each key in a map. - - `:value-fn` is a function called for each value in a map.")) + - `:key-fn` is a function which will be called for each key in a map; + default is `identity` + - `:value-fn` is a function of two arguments called for each value in a + map; the first argument is the key transformed by `:key-fn` and the + second is the value from the object.")) (extend-protocol JSONDecodeable python/dict (decode-from-json* [this {:keys [key-fn value-fn] :as opts}] - (->> (.items this) - (mapcat (fn [[k v]] - (let [new-k (key-fn k)] - [new-k (value-fn new-k v)]))) - (apply hash-map))) + (let [decode-value (or value-fn + (fn [_ v] + (decode-from-json* v opts)))] + (->> (.items this) + (mapcat (fn [[k v]] + (let [new-k (key-fn k)] + [new-k (decode-value new-k v)]))) + (apply hash-map)))) python/list (decode-from-json* [this opts] @@ -125,31 +111,32 @@ [o _] o) -(extend python/int - JSONDecodeable - {:decode-from-json* decode-scalar}) - -(extend python/float - JSONDecodeable - {:decode-from-json* decode-scalar}) - -(extend python/str - JSONDecodeable - {:decode-from-json* decode-scalar}) - -(extend python/bool - JSONDecodeable - {:decode-from-json* decode-scalar}) - -(extend nil - JSONDecodeable - {:decode-from-json* decode-scalar}) +(extend python/int JSONDecodeable {:decode-from-json* decode-scalar}) +(extend python/float JSONDecodeable {:decode-from-json* decode-scalar}) +(extend python/str JSONDecodeable {:decode-from-json* decode-scalar}) +(extend python/bool JSONDecodeable {:decode-from-json* decode-scalar}) +(extend nil JSONDecodeable {:decode-from-json* decode-scalar}) + +(defn ^:private read-opts + [{:keys [key-fn value-fn strict?]}] + {:key-fn (or key-fn identity) + :value-fn value-fn + :strict (if (or (nil? strict?) (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 + [reader & opts] + (let [{:keys [strict?] :as opts} (read-opts opts)] + (-> (json/load reader ** :strict strict?) + (decode-from-json* opts)))) (defn read-str - [s & {:keys [key-fn value-fn] :as opts}] - (let [opts {:key-fn (or key-fn identity) - :value-fn (or value-fn - (fn [_ v] - (decode-from-json* v opts)))}] - (-> (json/loads s) + [s & opts] + (let [{:keys [strict?] :as opts} (read-opts opts)] + (-> (json/loads s ** :strict strict?) (decode-from-json* opts)))) From 779294d5cf68aca735a3e3e0ac42545a11ccbaa2 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Mon, 1 Jun 2020 22:03:24 -0400 Subject: [PATCH 08/21] Lots of stuff --- src/basilisp/core.lpy | 11 +++++ src/basilisp/json.lpy | 93 ++++++++++++++++++++++++---------- src/basilisp/lang/obj.py | 104 ++++++++++----------------------------- 3 files changed, 104 insertions(+), 104 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 4472219b0..9ab580b3b 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -3019,6 +3019,17 @@ ~@(rest method-calls))) x)) +(defmacro memfn + "Expands into a function that calls the method `name` on the first argument + of the resulting function. If `args` are provided, the resulting function will + have arguments of these names. + + This is a convenient way of producing a first-class function for a Python + method." + [name & args] + `(fn [t# ~@args] + (. t# ~name ~@args))) + (defmacro new "Create a new instance of class with args. diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index a741085ae..026e1677f 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -1,79 +1,120 @@ (ns basilisp.json - (:import json)) + (:refer-basilisp :exclude [read]) + (:import + datetime + decimal + fractions + json + uuid)) ;;;;;;;;;;;;;; ;; Encoders ;; ;;;;;;;;;;;;;; (defprotocol JSONEncodeable - (encode-to-json* [this opts] + (to-json-encodeable* [this opts] "Return an object which can be JSON encoded by Python's default JSONEncoder. `opts` is a map of options: - - `:key-fn` is a function which will be called for each key in a map. - - `:value-fn` is a function called for each value in a map.")) -(defn ^:private encode-scalar + `:key-fn` - is a function which will be called for each key in a map. + `:value-fn` - is a function called for each value in a map.")) + +(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 encode-kw-or-sym +(defn ^:private stringify-scalar + [o _] + (python/str o)) + +(defn ^:private kw-or-sym-to-encodeable [o _] (if-let [ns-str (namespace o)] (str ns-str "/" (name o)) (name o))) -(defn ^:private encode-map-key +(defn ^:private map-key-to-encodeable [k] (if (keyword? k) (name k) (str k))) -(defn ^:private encode-map +(defn ^:private map-to-encodeable [o {:keys [key-fn value-fn] :as opts}] (let [encode-value (or value-fn (fn [_ v] - (encode-to-json* v opts)))] + (to-json-encodeable* v opts)))] (->> o (map (fn [[k v]] [(key-fn k) (encode-value k v)])) (python/dict)))) -(defn ^:private encode-seq +(defn ^:private seq-to-encodeable [o opts] (->> o - (map #(encode-to-json* % opts)) + (map #(to-json-encodeable* % opts)) (python/list))) -(extend python/str JSONEncodeable {:encode-to-json* encode-scalar}) -(extend python/int JSONEncodeable {:encode-to-json* encode-scalar}) -(extend python/float JSONEncodeable {:encode-to-json* encode-scalar}) -(extend python/bool JSONEncodeable {:encode-to-json* encode-scalar}) -(extend nil JSONEncodeable {:encode-to-json* encode-scalar}) +(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.keyword/Keyword JSONEncodeable {:encode-to-json* encode-kw-or-sym}) -(extend basilisp.lang.symbol/Symbol JSONEncodeable {:encode-to-json* encode-kw-or-sym}) +(extend basilisp.lang.interfaces/IPersistentMap JSONEncodeable {:to-json-encodeable* map-to-encodeable}) -(extend basilisp.lang.interfaces/IPersistentMap JSONEncodeable {:encode-to-json* encode-map}) +(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}) -(extend basilisp.lang.interfaces/IPersistentList JSONEncodeable {:encode-to-json* encode-seq}) -(extend basilisp.lang.interfaces/IPersistentSet JSONEncodeable {:encode-to-json* encode-seq}) -(extend basilisp.lang.interfaces/IPersistentVector JSONEncodeable {:encode-to-json* encode-seq}) +;; Support extended reader types. +(extend datetime/datetime JSONEncodeable {:to-json-encodeable* (fn [o _] (.isoformat o))}) +(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 [key-fn value-fn]}] - {:key-fn (or key-fn encode-map-key) + {:key-fn (or key-fn map-key-to-encodeable) :value-fn value-fn}) (defn write - [writer & opts] + "Serialize the object `o` as JSON to the writer object `writer` (which must be + any file-like object supporting `.write()` method). + + Several options may be specified as key/value pairs as `opts`: + + :key-fn - + :value-fn - " + [o writer & opts] (let [opts (write-opts opts)] - (json/dump writer ** :default #(encode-to-json* % opts)))) + (json/dump o writer ** :default #(to-json-encodeable* % opts)))) (defn write-str + "Serialize the object `o` as JSON and return the serialized object as a string. + + The options for `write-str` are the same as for those of `write`." [o & opts] (let [opts (write-opts opts)] - (json/dumps o ** :default #(encode-to-json* % opts)))) + (json/dumps o ** :default #(to-json-encodeable* % opts)))) ;;;;;;;;;;;;;; ;; Decoders ;; diff --git a/src/basilisp/lang/obj.py b/src/basilisp/lang/obj.py index 2cc31a90e..4c0cd895f 100644 --- a/src/basilisp/lang/obj.py +++ b/src/basilisp/lang/obj.py @@ -140,7 +140,8 @@ def seq_lrepr( return f"{start}{seq_lrepr}{end}" -def lrepr( # pylint: disable=too-many-arguments +@singledispatch +def lrepr( # pylint: disable=too-many-arguments,unused-arguments o: Any, human_readable: bool = False, print_dup: bool = PRINT_DUP, @@ -170,29 +171,11 @@ def lrepr( # pylint: disable=too-many-arguments runtime to the basilisp.core dynamic variables which correspond to each of the keyword arguments to this function. To use a version of lrepr which does capture those values, call basilisp.lang.runtime.lrepr directly.""" - if isinstance(o, LispObject): - return o._lrepr( - human_readable=human_readable, - print_dup=print_dup, - print_length=print_length, - print_level=print_level, - print_meta=print_meta, - print_readably=print_readably, - ) - else: # pragma: no cover - return _lrepr_fallback( - o, - human_readable=human_readable, - print_dup=print_dup, - print_length=print_length, - print_level=print_level, - print_meta=print_meta, - print_readably=print_readably, - ) + return repr(o) -@singledispatch -def _lrepr_fallback( # pylint: disable=too-many-arguments +@lrepr.register(LispObject) +def _lrepr_lisp_obj( # pylint: disable=too-many-arguments o: Any, human_readable: bool = False, print_dup: bool = PRINT_DUP, @@ -201,62 +184,27 @@ def _lrepr_fallback( # pylint: disable=too-many-arguments print_meta: bool = PRINT_META, print_readably: bool = PRINT_READABLY, ) -> str: # pragma: no cover - """Fallback function for lrepr for subclasses of standard types. - - The singledispatch used for standard lrepr dispatches using an exact - type match on the first argument, so we will only hit this function - for subclasses of common Python types like strings or lists.""" - kwargs = { - "human_readable": human_readable, - "print_dup": print_dup, - "print_length": print_length, - "print_level": print_level, - "print_meta": print_meta, - "print_readably": print_readably, - } - if isinstance(o, bool): - return _lrepr_bool(o) - elif o is None: - return _lrepr_nil(o) - elif isinstance(o, str): - return _lrepr_str( - o, human_readable=human_readable, print_readably=print_readably - ) - elif isinstance(o, dict): - return _lrepr_py_dict(o, **kwargs) - elif isinstance(o, list): - return _lrepr_py_list(o, **kwargs) - elif isinstance(o, set): - return _lrepr_py_set(o, **kwargs) - elif isinstance(o, tuple): - return _lrepr_py_tuple(o, **kwargs) - elif isinstance(o, complex): - return _lrepr_complex(o) - elif isinstance(o, datetime.datetime): - return _lrepr_datetime(o) - elif isinstance(o, Decimal): - return _lrepr_decimal(o, print_dup=print_dup) - elif isinstance(o, Fraction): - return _lrepr_fraction(o) - elif isinstance(o, Pattern): - return _lrepr_pattern(o) - elif isinstance(o, uuid.UUID): - return _lrepr_uuid(o) - else: - return repr(o) + return o._lrepr( + human_readable=human_readable, + print_dup=print_dup, + print_length=print_length, + print_level=print_level, + print_meta=print_meta, + print_readably=print_readably, + ) -@_lrepr_fallback.register(bool) +@lrepr.register(bool) def _lrepr_bool(o: bool, **_) -> str: return repr(o).lower() -@_lrepr_fallback.register(type(None)) +@lrepr.register(type(None)) def _lrepr_nil(_: None, **__) -> str: return "nil" -@_lrepr_fallback.register(str) +@lrepr.register(str) def _lrepr_str( o: str, human_readable: bool = False, print_readably: bool = PRINT_READABLY, **_ ) -> str: @@ -267,53 +215,53 @@ def _lrepr_str( return f'"{o.encode("unicode_escape").decode("utf-8")}"' -@_lrepr_fallback.register(list) +@lrepr.register(list) def _lrepr_py_list(o: list, **kwargs) -> str: return f"#py {seq_lrepr(o, '[', ']', **kwargs)}" -@_lrepr_fallback.register(dict) +@lrepr.register(dict) def _lrepr_py_dict(o: dict, **kwargs) -> str: return f"#py {map_lrepr(o.items, '{', '}', **kwargs)}" -@_lrepr_fallback.register(set) +@lrepr.register(set) def _lrepr_py_set(o: set, **kwargs) -> str: return f"#py {seq_lrepr(o, '#{', '}', **kwargs)}" -@_lrepr_fallback.register(tuple) +@lrepr.register(tuple) def _lrepr_py_tuple(o: tuple, **kwargs) -> str: return f"#py {seq_lrepr(o, '(', ')', **kwargs)}" -@_lrepr_fallback.register(complex) +@lrepr.register(complex) def _lrepr_complex(o: complex, **_) -> str: return repr(o).upper() -@_lrepr_fallback.register(datetime.datetime) +@lrepr.register(datetime.datetime) def _lrepr_datetime(o: datetime.datetime, **_) -> str: return f'#inst "{o.isoformat()}"' -@_lrepr_fallback.register(Decimal) +@lrepr.register(Decimal) def _lrepr_decimal(o: Decimal, print_dup: bool = PRINT_DUP, **_) -> str: if print_dup: return f"{str(o)}M" return str(o) -@_lrepr_fallback.register(Fraction) +@lrepr.register(Fraction) def _lrepr_fraction(o: Fraction, **_) -> str: return f"{o.numerator}/{o.denominator}" -@_lrepr_fallback.register(type(re.compile(""))) +@lrepr.register(type(re.compile(""))) def _lrepr_pattern(o: Pattern, **_) -> str: return f'#"{o.pattern}"' -@_lrepr_fallback.register(uuid.UUID) +@lrepr.register(uuid.UUID) def _lrepr_uuid(o: uuid.UUID, **_) -> str: return f'#uuid "{str(o)}"' From 434a43bea2a36ffc3070abbcb5fac3b1eb12e677 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Mon, 1 Jun 2020 22:10:03 -0400 Subject: [PATCH 09/21] Change the name --- src/basilisp/json.lpy | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index 026e1677f..fcfc9fc30 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -18,7 +18,9 @@ `opts` is a map of options: `:key-fn` - is a function which will be called for each key in a map. - `:value-fn` - is a function called for each value in a map.")) + `:value-fn` - is a function of two arguments called for each value in a + map; the first argument is the key before being transformed + by `:key-fn` and the second is the value from the object")) (extend-protocol JSONEncodeable python/object @@ -121,23 +123,24 @@ ;;;;;;;;;;;;;; (defprotocol JSONDecodeable - (decode-from-json* [this opts] + (from-decoded-json* [this opts] "Return a Basilisp object in place of a Python object returrned by Python's default JSONDecoder. `opts` is a map of options: - - `:key-fn` is a function which will be called for each key in a map; - default is `identity` - - `:value-fn` is a function of two arguments called for each value in a - map; the first argument is the key transformed by `:key-fn` and the - second is the value from the object.")) + + `:key-fn` - is a function which will be called for each key in a map; + default is `identity` + `:value-fn` - is a function of two arguments called for each value in a + map; the first argument is the key transformed by `:key-fn` + and the second is the value from the object")) (extend-protocol JSONDecodeable python/dict - (decode-from-json* [this {:keys [key-fn value-fn] :as opts}] + (from-decoded-json* [this {:keys [key-fn value-fn] :as opts}] (let [decode-value (or value-fn (fn [_ v] - (decode-from-json* v opts)))] + (from-decoded-json* v opts)))] (->> (.items this) (mapcat (fn [[k v]] (let [new-k (key-fn k)] @@ -145,18 +148,18 @@ (apply hash-map)))) python/list - (decode-from-json* [this opts] - (->> this (map #(decode-from-json* % opts)) (vec)))) + (from-decoded-json* [this opts] + (->> this (map #(from-decoded-json* % opts)) (vec)))) (defn ^:private decode-scalar [o _] o) -(extend python/int JSONDecodeable {:decode-from-json* decode-scalar}) -(extend python/float JSONDecodeable {:decode-from-json* decode-scalar}) -(extend python/str JSONDecodeable {:decode-from-json* decode-scalar}) -(extend python/bool JSONDecodeable {:decode-from-json* decode-scalar}) -(extend nil JSONDecodeable {:decode-from-json* decode-scalar}) +(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 value-fn strict?]}] @@ -174,10 +177,10 @@ [reader & opts] (let [{:keys [strict?] :as opts} (read-opts opts)] (-> (json/load reader ** :strict strict?) - (decode-from-json* opts)))) + (from-decoded-json* opts)))) (defn read-str [s & opts] (let [{:keys [strict?] :as opts} (read-opts opts)] (-> (json/loads s ** :strict strict?) - (decode-from-json* opts)))) + (from-decoded-json* opts)))) From ec3c00d82a8016f28c00111528803ab0fa591aa0 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Tue, 2 Jun 2020 08:21:16 -0400 Subject: [PATCH 10/21] Clean up single dispatch --- src/basilisp/lang/reader.py | 8 +++---- src/basilisp/lang/runtime.py | 44 +++++------------------------------- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/src/basilisp/lang/reader.py b/src/basilisp/lang/reader.py index a55f0c354..949102049 100644 --- a/src/basilisp/lang/reader.py +++ b/src/basilisp/lang/reader.py @@ -231,22 +231,22 @@ def _py_from_lisp( raise SyntaxError(f"Unrecognized Python type: {type(form)}") -@_py_from_lisp.register(llist.List) +@_py_from_lisp.register(IPersistentList) def _py_tuple_from_list(form: llist.List) -> tuple: return tuple(form) -@_py_from_lisp.register(lmap.Map) +@_py_from_lisp.register(IPersistentMap) def _py_dict_from_map(form: lmap.Map) -> dict: return dict(form) -@_py_from_lisp.register(lset.Set) +@_py_from_lisp.register(IPersistentSet) def _py_set_from_set(form: lset.Set) -> set: return set(form) -@_py_from_lisp.register(vector.Vector) +@_py_from_lisp.register(IPersistentVector) def _py_list_from_vec(form: vector.Vector) -> list: return list(form) diff --git a/src/basilisp/lang/runtime.py b/src/basilisp/lang/runtime.py index 597ab4333..66ceae887 100644 --- a/src/basilisp/lang/runtime.py +++ b/src/basilisp/lang/runtime.py @@ -1165,23 +1165,9 @@ def is_special_form(s: sym.Symbol) -> bool: @functools.singledispatch -def to_lisp(o, keywordize_keys: bool = True): +def to_lisp(o, keywordize_keys: bool = True): # pylint: disable=unused-argument """Recursively convert Python collections into Lisp collections.""" - if not isinstance(o, (dict, frozenset, list, set, tuple)): - return o - else: # pragma: no cover - return _to_lisp_backup(o, keywordize_keys=keywordize_keys) - - -def _to_lisp_backup(o, keywordize_keys: bool = True): # pragma: no cover - if isinstance(o, Mapping): - return _to_lisp_map(o, keywordize_keys=keywordize_keys) - elif isinstance(o, AbstractSet): - return _to_lisp_set(o, keywordize_keys=keywordize_keys) - elif isinstance(o, Iterable): - return _to_lisp_vec(o, keywordize_keys=keywordize_keys) - else: - return o + return o @to_lisp.register(list) @@ -1216,29 +1202,11 @@ def _kw_name(kw: kw.Keyword) -> str: @functools.singledispatch -def to_py(o, keyword_fn: Callable[[kw.Keyword], Any] = _kw_name): - """Recursively convert Lisp collections into Python collections.""" - if isinstance(o, ISeq): - return _to_py_list(o, keyword_fn=keyword_fn) - elif not isinstance( - o, (IPersistentList, IPersistentMap, IPersistentSet, IPersistentVector) - ): - return o - else: # pragma: no cover - return _to_py_backup(o, keyword_fn=keyword_fn) - - -def _to_py_backup( +def to_py( o, keyword_fn: Callable[[kw.Keyword], Any] = _kw_name -): # pragma: no cover - if isinstance(o, (IPersistentList, IPersistentVector)): - return _to_py_list(o, keyword_fn=keyword_fn) - elif isinstance(o, IPersistentMap): - return _to_py_map(o, keyword_fn=keyword_fn) - elif isinstance(o, IPersistentSet): - return _to_py_set(o, keyword_fn=keyword_fn) - else: - return o +): # pylint: disable=unused-argument + """Recursively convert Lisp collections into Python collections.""" + return o @to_py.register(kw.Keyword) From 84090ce4c92a81e1a5fb562ee584a944c79f03f4 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Wed, 3 Jun 2020 09:26:24 -0400 Subject: [PATCH 11/21] Test baseline --- src/basilisp/json.lpy | 4 +-- tests/basilisp/test_json.lpy | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 tests/basilisp/test_json.lpy diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index fcfc9fc30..ca16d7265 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -124,7 +124,7 @@ (defprotocol JSONDecodeable (from-decoded-json* [this opts] - "Return a Basilisp object in place of a Python object returrned by Python's + "Return a Basilisp object in place of a Python object returned by Python's default JSONDecoder. `opts` is a map of options: @@ -165,7 +165,7 @@ [{:keys [key-fn value-fn strict?]}] {:key-fn (or key-fn identity) :value-fn value-fn - :strict (if (or (nil? strict?) (boolean? strict?)) strict? true)}) + :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 diff --git a/tests/basilisp/test_json.lpy b/tests/basilisp/test_json.lpy new file mode 100644 index 000000000..2ee7b3958 --- /dev/null +++ b/tests/basilisp/test_json.lpy @@ -0,0 +1,66 @@ +(ns tests.basilisp.test-json + (:require + [basilisp.json :as json] + [basilisp.test :refer [deftest is are testing]])) + +(deftest read-str-test + (testing "primitive-values" + (are [x y] (= y (json/read-str x)) + "null" nil + "true" true + "false" false + "0" 0 + "1" 1 + "-1" -1 + "0.0" 0.0 + "1.0" 1.0 + "-1.0" -1.0 + "\"\"" "" + "\"string\"" "string" + "[]" [] + "{}" {})) + + (testing "arrays" + (are [x y] (= y (json/read-str x)) + "[null, true, false, 0, 1, -1, 0.0, 1.0, -1.0, \"\", \"string\", [], {}]" + [nil true false 0 1 -1 0.0 1.0 -1.0 "" "string" [] {}] + + "[[null, true, false], [0, 1, -1], [0.0, 1.0, -1.0], [\"\", [\"string\"]], [[]], [{}]]" + [[nil true false] [0 1 -1] [0.0 1.0 -1.0] ["" ["string"]] [[]] [{}]] + + "[ + {\"id\": 35, \"name\": \"Chris\", \"is_admin\": false, \"roles\": [\"user\"]}, + {\"id\": 42, \"name\": \"Carl\", \"is_admin\": true, \"roles\": [\"admin\"]} + ]" + [{"id" 35 "name" "Chris" "is_admin" false "roles" ["user"]} + {"id" 42 "name" "Carl" "is_admin" true "roles" ["admin"]}]))) + +(deftest write-str-test + (testing "primitive-values" + (are [x y] (= y (json/write-str x)) + nil "null" + true "true" + false "false" + 0 "0" + 1 "1" + -1 "-1" + 0.0 "0.0" + 1.0 "1.0" + -1.0 "-1.0" + "" "\"\"" + "string" "\"string\"" + :kw "\"kw\"" + :ns/kw "\"ns/kw\"" + :long.ns/kw "\"long.ns/kw\"" + 'sym "\"sym\"" + 'ns/sym "\"ns/sym\"" + 'long.ns/sym "\"long.ns/sym\"" + #{} "[]" + '() "[]" + [] "[]" + {} "{}" + ;; (eval '#py #{}) "[]" + #py () "[]" + ;; #py [] "[]" + ;; #py {} "{}" + ))) From 3e290fc6e97373c318bed3047f9daf8d996c9585 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Thu, 4 Jun 2020 08:46:41 -0400 Subject: [PATCH 12/21] Reader tests --- src/basilisp/json.lpy | 52 ++++++++++++++++++---------- tests/basilisp/test_json.lpy | 66 ++++++++++++++++++++++++++++++++---- 2 files changed, 95 insertions(+), 23 deletions(-) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index ca16d7265..11239b683 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -106,7 +106,7 @@ :key-fn - :value-fn - " - [o writer & opts] + [o writer & {:as opts}] (let [opts (write-opts opts)] (json/dump o writer ** :default #(to-json-encodeable* % opts)))) @@ -114,7 +114,7 @@ "Serialize the object `o` as JSON and return the serialized object as a string. The options for `write-str` are the same as for those of `write`." - [o & opts] + [o & {:as opts}] (let [opts (write-opts opts)] (json/dumps o ** :default #(to-json-encodeable* % opts)))) @@ -130,22 +130,14 @@ `opts` is a map of options: `:key-fn` - is a function which will be called for each key in a map; - default is `identity` - `:value-fn` - is a function of two arguments called for each value in a - map; the first argument is the key transformed by `:key-fn` - and the second is the value from the object")) + default is `identity`")) (extend-protocol JSONDecodeable python/dict (from-decoded-json* [this {:keys [key-fn value-fn] :as opts}] - (let [decode-value (or value-fn - (fn [_ v] - (from-decoded-json* v opts)))] - (->> (.items this) - (mapcat (fn [[k v]] - (let [new-k (key-fn k)] - [new-k (decode-value new-k v)]))) - (apply hash-map)))) + (->> (.items this) + (mapcat (fn [[k v]] [(key-fn k) v])) + (apply hash-map))) python/list (from-decoded-json* [this opts] @@ -164,7 +156,6 @@ (defn ^:private read-opts [{:keys [key-fn value-fn strict?]}] {:key-fn (or key-fn identity) - :value-fn value-fn :strict (if (boolean? strict?) strict? true)}) ;; Python's builtin `json.load` currently only includes an Object hook; it has @@ -174,13 +165,40 @@ ;; Python, but it has gotten no traction: https://bugs.python.org/issue36738 (defn read - [reader & opts] + "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 - [s & opts] + "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 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`" + [s & {:as opts}] (let [{:keys [strict?] :as opts} (read-opts opts)] (-> (json/loads s ** :strict strict?) (from-decoded-json* opts)))) diff --git a/tests/basilisp/test_json.lpy b/tests/basilisp/test_json.lpy index 2ee7b3958..c9c725ee0 100644 --- a/tests/basilisp/test_json.lpy +++ b/tests/basilisp/test_json.lpy @@ -33,7 +33,59 @@ {\"id\": 42, \"name\": \"Carl\", \"is_admin\": true, \"roles\": [\"admin\"]} ]" [{"id" 35 "name" "Chris" "is_admin" false "roles" ["user"]} - {"id" 42 "name" "Carl" "is_admin" true "roles" ["admin"]}]))) + {"id" 42 "name" "Carl" "is_admin" true "roles" ["admin"]}])) + + (testing "objects" + (is (= {"id" 35 + "name" "Chris" + "title" nil + "phone" {"type" "home" + "number" "+15558675309"} + "addresses" [{"street_address" "330 W 86th St" + "city" "New York" + "state" "NY" + "zip" "10024"}] + "balance" 3800.60 + "is_admin" false + "roles" ["user"]} + (json/read-str + (str + "{" + "\"id\": 35," + "\"name\": \"Chris\"," + "\"title\": null," + "\"phone\": {\"type\": \"home\", \"number\":\"+15558675309\"}," + "\"addresses\": [{\"street_address\": \"330 W 86th St\", \"city\": \"New York\", \"state\": \"NY\", \"zip\":\"10024\"}]," + "\"balance\": 3800.60," + "\"is_admin\": false," + "\"roles\": [\"user\"]" + "}")))) + + (is (= {:id 35 + :name "Chris" + :title nil + :phone {:type "home" + :number "+15558675309"} + :addresses [{:street_address "330 W 86th St" + :city "New York" + :state "NY" + :zip "10024"}] + :balance 3800.60 + :is_admin false + :roles ["user"]} + (json/read-str + (str + "{" + "\"id\": 35," + "\"name\": \"Chris\"," + "\"title\": null," + "\"phone\": {\"type\": \"home\", \"number\":\"+15558675309\"}," + "\"addresses\": [{\"street_address\": \"330 W 86th St\", \"city\": \"New York\", \"state\": \"NY\", \"zip\":\"10024\"}]," + "\"balance\": 3800.60," + "\"is_admin\": false," + "\"roles\": [\"user\"]" + "}") + :key-fn keyword))))) (deftest write-str-test (testing "primitive-values" @@ -59,8 +111,10 @@ '() "[]" [] "[]" {} "{}" - ;; (eval '#py #{}) "[]" - #py () "[]" - ;; #py [] "[]" - ;; #py {} "{}" - ))) + #py () "[]") + + ;; Mutable Python data types cause issues with the `do-template` used by + ;; the `are` macro, so these have to be written as `is` cases. + (is (= "[]" (json/write-str #py #{}))) + (is (= "[]" (json/write-str #py []))) + (is (= "{}" (json/write-str #py {}))))) From d6a09ab80e802c71c45520c3b84ceb983af5cdf7 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Thu, 4 Jun 2020 09:12:58 -0400 Subject: [PATCH 13/21] Lots of cleaning --- src/basilisp/json.lpy | 85 ++++++++++++++++++----------------- tests/basilisp/prompt_test.py | 1 + 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index 11239b683..7a106bc82 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -15,12 +15,10 @@ (to-json-encodeable* [this opts] "Return an object which can be JSON encoded by Python's default JSONEncoder. - `opts` is a map of options: + `opts` is a map with the following options: - `:key-fn` - is a function which will be called for each key in a map. - `:value-fn` - is a function of two arguments called for each value in a - map; the first argument is the key before being transformed - by `:key-fn` and the second is the value from the object")) + `:key-fn` - is a function which will be called for each key in a map; + default is `name`")) (extend-protocol JSONEncodeable python/object @@ -43,21 +41,11 @@ (str ns-str "/" (name o)) (name o))) -(defn ^:private map-key-to-encodeable - [k] - (if (keyword? k) - (name k) - (str k))) - (defn ^:private map-to-encodeable - [o {:keys [key-fn value-fn] :as opts}] - (let [encode-value (or value-fn - (fn [_ v] - (to-json-encodeable* v opts)))] - (->> o - (map (fn [[k v]] - [(key-fn k) (encode-value k v)])) - (python/dict)))) + [o {:keys [key-fn] :as opts}] + (->> o + (map (fn [[k v]] [(key-fn k) v])) + (python/dict))) (defn ^:private seq-to-encodeable [o opts] @@ -94,29 +82,50 @@ (extend python/frozenset JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) (defn ^:private write-opts - [{:keys [key-fn value-fn]}] - {:key-fn (or key-fn map-key-to-encodeable) - :value-fn value-fn}) + [{: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). - Several options may be specified as key/value pairs as `opts`: - - :key-fn - - :value-fn - " + 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 [opts (write-opts opts)] - (json/dump o writer ** :default #(to-json-encodeable* % 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 + :separator separator))) (defn write-str "Serialize the object `o` as JSON and return the serialized object as a string. The options for `write-str` are the same as for those of `write`." [o & {:as opts}] - (let [opts (write-opts opts)] - (json/dumps o ** :default #(to-json-encodeable* % 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 + :separator separator))) ;;;;;;;;;;;;;; ;; Decoders ;; @@ -127,10 +136,10 @@ "Return a Basilisp object in place of a Python object returned by Python's default JSONDecoder. - `opts` is a map of options: + `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`")) + `:key-fn` - is a function which will be called for each key in a map; + default is `identity`")) (extend-protocol JSONDecodeable python/dict @@ -154,7 +163,7 @@ (extend nil JSONDecodeable {:from-decoded-json* decode-scalar}) (defn ^:private read-opts - [{:keys [key-fn value-fn strict?]}] + [{:keys [key-fn strict?]}] {:key-fn (or key-fn identity) :strict (if (boolean? strict?) strict? true)}) @@ -191,13 +200,7 @@ 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`" + 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?) diff --git a/tests/basilisp/prompt_test.py b/tests/basilisp/prompt_test.py index bec5a4a5a..3da4aaaf9 100644 --- a/tests/basilisp/prompt_test.py +++ b/tests/basilisp/prompt_test.py @@ -68,6 +68,7 @@ def patch_completions(self, completions: Iterable[str]): "mapv", "max", "max-key", + "memfn", "merge", "meta", "methods", From e80c3621d8cbfb58532a889d6b3387fa11473238 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Thu, 4 Jun 2020 09:18:59 -0400 Subject: [PATCH 14/21] Fixes --- src/basilisp/json.lpy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index 7a106bc82..e056ab0ef 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -113,7 +113,7 @@ :default #(to-json-encodeable* % opts) :ensure-ascii escape-non-ascii :indent indent - :separator separator))) + :separators separator))) (defn write-str "Serialize the object `o` as JSON and return the serialized object as a string. @@ -125,7 +125,7 @@ :default #(to-json-encodeable* % opts) :ensure-ascii escape-non-ascii :indent indent - :separator separator))) + :separators separator))) ;;;;;;;;;;;;;; ;; Decoders ;; @@ -145,7 +145,7 @@ python/dict (from-decoded-json* [this {:keys [key-fn value-fn] :as opts}] (->> (.items this) - (mapcat (fn [[k v]] [(key-fn k) v])) + (mapcat (fn [[k v]] [(key-fn k) (from-decoded-json* v opts)])) (apply hash-map))) python/list From 253a265749b26db9e6f1907486ba6b448da6154f Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Thu, 4 Jun 2020 09:25:30 -0400 Subject: [PATCH 15/21] Doing a talking --- src/basilisp/json.lpy | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index e056ab0ef..c7ce331ef 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -92,6 +92,19 @@ "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) are serialized as ISO 8601 date strings. Decimals are + serialized as stringified floats. Fractions are serialized as 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; @@ -118,6 +131,19 @@ (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) are serialized as ISO 8601 date strings. Decimals are + serialized as stringified floats. Fractions are serialized as 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)] From 16b3b7a0c6f7b63af3ab161182b5b865a0541030 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Thu, 4 Jun 2020 22:41:01 -0400 Subject: [PATCH 16/21] Include date and time --- src/basilisp/json.lpy | 8 +++++- tests/basilisp/test_json.lpy | 55 +++++++++++++++++++++--------------- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index c7ce331ef..19d59df41 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -35,6 +35,10 @@ [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)] @@ -69,7 +73,9 @@ (extend basilisp.lang.interfaces/IPersistentVector JSONEncodeable {:to-json-encodeable* seq-to-encodeable}) ;; Support extended reader types. -(extend datetime/datetime JSONEncodeable {:to-json-encodeable* (fn [o _] (.isoformat o))}) +(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}) diff --git a/tests/basilisp/test_json.lpy b/tests/basilisp/test_json.lpy index c9c725ee0..adeff5759 100644 --- a/tests/basilisp/test_json.lpy +++ b/tests/basilisp/test_json.lpy @@ -1,4 +1,5 @@ (ns tests.basilisp.test-json + (:import datetime) (:require [basilisp.json :as json] [basilisp.test :refer [deftest is are testing]])) @@ -90,28 +91,38 @@ (deftest write-str-test (testing "primitive-values" (are [x y] (= y (json/write-str x)) - nil "null" - true "true" - false "false" - 0 "0" - 1 "1" - -1 "-1" - 0.0 "0.0" - 1.0 "1.0" - -1.0 "-1.0" - "" "\"\"" - "string" "\"string\"" - :kw "\"kw\"" - :ns/kw "\"ns/kw\"" - :long.ns/kw "\"long.ns/kw\"" - 'sym "\"sym\"" - 'ns/sym "\"ns/sym\"" - 'long.ns/sym "\"long.ns/sym\"" - #{} "[]" - '() "[]" - [] "[]" - {} "{}" - #py () "[]") + nil "null" + true "true" + false "false" + 0 "0" + 1 "1" + -1 "-1" + 0.0 "0.0" + 1.0 "1.0" + -1.0 "-1.0" + "" "\"\"" + "string" "\"string\"" + + :kw "\"kw\"" + :ns/kw "\"ns/kw\"" + :long.ns/kw "\"long.ns/kw\"" + 'sym "\"sym\"" + 'ns/sym "\"ns/sym\"" + 'long.ns/sym "\"long.ns/sym\"" + + #{} "[]" + '() "[]" + [] "[]" + {} "{}" + + #inst "2020-06-04T22:32:29.871744" "\"2020-06-04T22:32:29.871744\"" + (datetime/date 2020 6 4) "\"2020-06-04\"" + (datetime/time 22 35 38) "\"22:35:38\"" + 3.1415926535M "\"3.1415926535\"" + 22/7 "\"22/7\"" + #uuid "632ac3d8-fcfd-4d36-a05b-a54277a345bc" "\"632ac3d8-fcfd-4d36-a05b-a54277a345bc\"" + + #py () "[]") ;; Mutable Python data types cause issues with the `do-template` used by ;; the `are` macro, so these have to be written as `is` cases. From 40d524e6153159c3ae302f0b297829c4c769b430 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Thu, 4 Jun 2020 22:43:11 -0400 Subject: [PATCH 17/21] doc the string --- src/basilisp/json.lpy | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index 19d59df41..1eec7d826 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -103,9 +103,10 @@ 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) are serialized as ISO 8601 date strings. Decimals are - serialized as stringified floats. Fractions are serialized as ratios (numerator - and denominator). UUIDs are serialized as their canonical hex string format. + (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 @@ -142,9 +143,10 @@ 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) are serialized as ISO 8601 date strings. Decimals are - serialized as stringified floats. Fractions are serialized as ratios (numerator - and denominator). UUIDs are serialized as their canonical hex string format. + (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 From 3fa458527e6009642fdcccb58ea74491abb0d049 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Fri, 5 Jun 2020 08:50:57 -0400 Subject: [PATCH 18/21] Moar tests --- src/basilisp/json.lpy | 8 +++++ tests/basilisp/test_json.lpy | 64 ++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index 1eec7d826..377844a92 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -1,4 +1,12 @@ (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 diff --git a/tests/basilisp/test_json.lpy b/tests/basilisp/test_json.lpy index adeff5759..10182e541 100644 --- a/tests/basilisp/test_json.lpy +++ b/tests/basilisp/test_json.lpy @@ -5,7 +5,7 @@ [basilisp.test :refer [deftest is are testing]])) (deftest read-str-test - (testing "primitive-values" + (testing "primitive values" (are [x y] (= y (json/read-str x)) "null" nil "true" true @@ -89,7 +89,7 @@ :key-fn keyword))))) (deftest write-str-test - (testing "primitive-values" + (testing "primitive values" (are [x y] (= y (json/write-str x)) nil "null" true "true" @@ -128,4 +128,62 @@ ;; the `are` macro, so these have to be written as `is` cases. (is (= "[]" (json/write-str #py #{}))) (is (= "[]" (json/write-str #py []))) - (is (= "{}" (json/write-str #py {}))))) + (is (= "{}" (json/write-str #py {})))) + + (testing "to JSON array" + (testing "vectors" + (are [x y] (= y (json/write-str x)) + [nil true false 0 1 -1 0.0 1.0 -1.0 "" "string"] + "[null, true, false, 0, 1, -1, 0.0, 1.0, -1.0, \"\", \"string\"]" + + [:kw :ns/kw :long.ns/kw 'sym 'ns/sym 'long.ns/sym] + "[\"kw\", \"ns/kw\", \"long.ns/kw\", \"sym\", \"ns/sym\", \"long.ns/sym\"]" + + [#inst "2020-06-04T22:32:29.871744" + (datetime/date 2020 6 4) + (datetime/time 22 35 38) + 3.1415926535M + 22/7 + #uuid "632ac3d8-fcfd-4d36-a05b-a54277a345bc"] + "[\"2020-06-04T22:32:29.871744\", \"2020-06-04\", \"22:35:38\", \"3.1415926535\", \"22/7\", \"632ac3d8-fcfd-4d36-a05b-a54277a345bc\"]" + + ;; output key order is not guaranteed, so testing objects is annoying + [{:id 35} {:name "Arbuckle"} {:title :title/Administrator} {:roles [:user :system/admin]}] + "[{\"id\": 35}, {\"name\": \"Arbuckle\"}, {\"title\": \"title/Administrator\"}, {\"roles\": [\"user\", \"system/admin\"]}]")) + + (testing "list" + (are [x y] (= y (json/write-str x)) + '(nil true false 0 1 -1 0.0 1.0 -1.0 "" "string") + "[null, true, false, 0, 1, -1, 0.0, 1.0, -1.0, \"\", \"string\"]" + + '(:kw :ns/kw :long.ns/kw sym ns/sym long.ns/sym) + "[\"kw\", \"ns/kw\", \"long.ns/kw\", \"sym\", \"ns/sym\", \"long.ns/sym\"]" + + (list + #inst "2020-06-04T22:32:29.871744" + (datetime/date 2020 6 4) + (datetime/time 22 35 38) + 3.1415926535M + 22/7 + #uuid "632ac3d8-fcfd-4d36-a05b-a54277a345bc") + "[\"2020-06-04T22:32:29.871744\", \"2020-06-04\", \"22:35:38\", \"3.1415926535\", \"22/7\", \"632ac3d8-fcfd-4d36-a05b-a54277a345bc\"]" + + ;; output key order is not guaranteed, so testing objects is annoying + '({:id 35} {:name "Arbuckle"} {:title :title/Administrator} {:roles [:user :system/admin]}) + "[{\"id\": 35}, {\"name\": \"Arbuckle\"}, {\"title\": \"title/Administrator\"}, {\"roles\": [\"user\", \"system/admin\"]}]")) + + (testing "sets" + (are [x] (= (set x) (set (json/read-str (json/write-str x)))) + [nil true false 0 1 -1 0.0 1.0 -1.0 "" "string"]) + + (are [x y] (= y (set (json/read-str (json/write-str x)))) + [:kw :ns/kw :long.ns/kw 'sym 'ns/sym 'long.ns/sym] + #{"kw" "ns/kw" "long.ns/kw" "sym" "ns/sym" "long.ns/sym"} + + #{#inst "2020-06-04T22:32:29.871744" + (datetime/date 2020 6 4) + (datetime/time 22 35 38) + 3.1415926535M + 22/7 + #uuid "632ac3d8-fcfd-4d36-a05b-a54277a345bc"} + #{"2020-06-04T22:32:29.871744", "2020-06-04", "22:35:38", "3.1415926535", "22/7", "632ac3d8-fcfd-4d36-a05b-a54277a345bc"})))) From 80ef39659d9d6d843b16bb3f0c9e7bf060bed1b4 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Fri, 5 Jun 2020 09:13:57 -0400 Subject: [PATCH 19/21] Object tests --- tests/basilisp/test_json.lpy | 51 +++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/tests/basilisp/test_json.lpy b/tests/basilisp/test_json.lpy index 10182e541..22c48deb8 100644 --- a/tests/basilisp/test_json.lpy +++ b/tests/basilisp/test_json.lpy @@ -145,11 +145,13 @@ 3.1415926535M 22/7 #uuid "632ac3d8-fcfd-4d36-a05b-a54277a345bc"] - "[\"2020-06-04T22:32:29.871744\", \"2020-06-04\", \"22:35:38\", \"3.1415926535\", \"22/7\", \"632ac3d8-fcfd-4d36-a05b-a54277a345bc\"]" + "[\"2020-06-04T22:32:29.871744\", \"2020-06-04\", \"22:35:38\", \"3.1415926535\", \"22/7\", \"632ac3d8-fcfd-4d36-a05b-a54277a345bc\"]") - ;; output key order is not guaranteed, so testing objects is annoying - [{:id 35} {:name "Arbuckle"} {:title :title/Administrator} {:roles [:user :system/admin]}] - "[{\"id\": 35}, {\"name\": \"Arbuckle\"}, {\"title\": \"title/Administrator\"}, {\"roles\": [\"user\", \"system/admin\"]}]")) + (are [x y] (= y (json/read-str (json/write-str x))) + [{:id 35 :name "Chris" :title nil :roles #{"user"}} + {:id 41 :name "Arbuckle" :title :title/Administrator :roles #py ("user", :system/admin)}] + [{"id" 35 "name" "Chris" "title" nil "roles" ["user"]} + {"id" 41 "name" "Arbuckle" "title" "title/Administrator" "roles" ["user" "system/admin"]}])) (testing "list" (are [x y] (= y (json/write-str x)) @@ -166,11 +168,13 @@ 3.1415926535M 22/7 #uuid "632ac3d8-fcfd-4d36-a05b-a54277a345bc") - "[\"2020-06-04T22:32:29.871744\", \"2020-06-04\", \"22:35:38\", \"3.1415926535\", \"22/7\", \"632ac3d8-fcfd-4d36-a05b-a54277a345bc\"]" + "[\"2020-06-04T22:32:29.871744\", \"2020-06-04\", \"22:35:38\", \"3.1415926535\", \"22/7\", \"632ac3d8-fcfd-4d36-a05b-a54277a345bc\"]") - ;; output key order is not guaranteed, so testing objects is annoying - '({:id 35} {:name "Arbuckle"} {:title :title/Administrator} {:roles [:user :system/admin]}) - "[{\"id\": 35}, {\"name\": \"Arbuckle\"}, {\"title\": \"title/Administrator\"}, {\"roles\": [\"user\", \"system/admin\"]}]")) + (are [x y] (= y (json/read-str (json/write-str x))) + '({:id 35 :name "Chris" :title nil :roles #{"user"}} + {:id 41 :name "Arbuckle" :title :title/Administrator :roles #py ("user", :system/admin)}) + [{"id" 35 "name" "Chris" "title" nil "roles" ["user"]} + {"id" 41 "name" "Arbuckle" "title" "title/Administrator" "roles" ["user" "system/admin"]}])) (testing "sets" (are [x] (= (set x) (set (json/read-str (json/write-str x)))) @@ -186,4 +190,33 @@ 3.1415926535M 22/7 #uuid "632ac3d8-fcfd-4d36-a05b-a54277a345bc"} - #{"2020-06-04T22:32:29.871744", "2020-06-04", "22:35:38", "3.1415926535", "22/7", "632ac3d8-fcfd-4d36-a05b-a54277a345bc"})))) + #{"2020-06-04T22:32:29.871744", "2020-06-04", "22:35:38", "3.1415926535", "22/7", "632ac3d8-fcfd-4d36-a05b-a54277a345bc"})) + + (testing "objects" + (are [x y] (= y (json/read-str (json/write-str x))) + {:id #uuid "632ac3d8-fcfd-4d36-a05b-a54277a345bc" + :name "Chris" + :title nil + :phone {:type "home" + :number "+15558675309"} + :addresses [{:street-address "330 W 86th St" + :city "New York" + :state "NY" + :zip "10024"}] + :balance 3800.60 + :is-admin false + :roles [:user] + :last-login #inst "2020-06-04T22:32:29.871744"} + {"id" "632ac3d8-fcfd-4d36-a05b-a54277a345bc" + "name" "Chris" + "title" nil + "phone" {"type" "home" + "number" "+15558675309"} + "addresses" [{"street-address" "330 W 86th St" + "city" "New York" + "state" "NY" + "zip" "10024"}] + "balance" 3800.60 + "is-admin" false + "roles" ["user"] + "last-login" "2020-06-04T22:32:29.871744"})))) From 799e9fcc6bc23dfe52e9219f49e48b0e7de1cbc4 Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Fri, 5 Jun 2020 09:17:55 -0400 Subject: [PATCH 20/21] Unused symbol --- src/basilisp/json.lpy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/basilisp/json.lpy b/src/basilisp/json.lpy index 377844a92..e6f557184 100644 --- a/src/basilisp/json.lpy +++ b/src/basilisp/json.lpy @@ -185,7 +185,7 @@ (extend-protocol JSONDecodeable python/dict - (from-decoded-json* [this {:keys [key-fn value-fn] :as opts}] + (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))) From 7bdcd391c800df68199685616574a21afe81131b Mon Sep 17 00:00:00 2001 From: Christopher Rink Date: Fri, 5 Jun 2020 09:26:41 -0400 Subject: [PATCH 21/21] Apply namespace docstring to namespace meta --- src/basilisp/core.lpy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 9ab580b3b..e6e730168 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -3953,6 +3953,8 @@ (:import opts)))] `(do (in-ns (quote ~name)) + ~(when doc + `(alter-meta! (the-ns (quote ~name)) assoc :doc ~doc)) (refer-basilisp ~@refer-filters) ~requires ~uses