dependencies
| (this space intentionally left almost blank) | ||||||||||||
Transducer Error Handling | |||||||||||||
(ns danger-mouse.catch-errors) | |||||||||||||
(defn process-error [error input] {:error-msg (ex-message error) :error error :input input}) | |||||||||||||
Transducer to catch errors, capture additional info, and cache them as DM errors in a side channel that will be returned at the end of the reduction. | (def catch-errors (fn [rf] (let [errors (volatile! [])] (fn ([] (try (rf) (catch Exception e {:result nil :errors (conj @errors (process-error e nil))}))) ([result] (try {:result (rf result) :errors @errors} (catch Exception e {:result result :errors (conj @errors (process-error e result))}))) ([result input] (try (rf result input) (catch Exception e (vswap! errors conj (process-error e input)) result))))))) | ||||||||||||
(defn errors-coll? [coll] (and (map? coll) (= (set (keys coll)) #{:result :errors}))) | |||||||||||||
Helper function to separate out results from errors after applying a transducer. Collection is first. | (defn transduce-> [coll xform initial & args] (let [start (if (errors-coll? coll) (:result coll) coll) {new-result :result new-errors :errors} (transduce (apply comp catch-errors args) xform initial start)] {:result new-result :errors (into (or (:errors coll) []) new-errors)})) | ||||||||||||
Helper function to separate out results from errors after applying a transducer. Collection is last. | (defn transduce->> [xform initial & args-and-coll] (let [coll (last args-and-coll) args (drop-last args-and-coll)] (apply transduce-> coll xform initial args))) | ||||||||||||
Helper function to separate out results from errors in a collection. Collection is first. | (defn catch-errors-> [coll & args] (apply transduce-> coll conj [] args)) | ||||||||||||
Helper function to separate out results from errors in a collection. Collection is last. | (defn catch-errors->> [& args-and-coll] (let [coll (last args-and-coll) args (drop-last args-and-coll)] (apply catch-errors-> coll args))) | ||||||||||||
Error Handling Transducers | |||||||||||||
(ns danger-mouse.transducers (:require [danger-mouse.utils :as utils] [clojure.string :as str] [schema.core :as s] [danger-mouse.schema :as dm-schema] [clojure.string :as str])) | |||||||||||||
Transducers | |||||||||||||
Like | (def contain-errors-xf (fn [rf] (fn ([] (rf)) ([result] (try (rf result) (catch Exception e (dm-schema/as-error {:error-msg (.getMessage e) :error e :input result})))) ([result input] (try (rf result input) (catch Exception e (rf result (dm-schema/as-error {:error-msg (.getMessage e) :error e :input input})))))))) | ||||||||||||
Handle errors as part of the transduction process via | (defn handle-errors-xf [handler] (fn [rf] (fn ([] (rf)) ([result] (rf result)) ([result input] (utils/resolve (fn [error] (handler error) result) (fn [success] (rf result success)) input))))) | ||||||||||||
Tranducer transformer that takes an existing transducer | (defn handle-and-continue-xf [handler xf] (fn [rf] (fn ([] (rf)) ([result] (rf result)) ([result input] (utils/resolve (fn [error] (handler error) result) (fn [success] ((xf rf) result success)) input))))) | ||||||||||||
Propogates errors as errors, and otherwise applies the marked transducer | (defn carry-errors-xf [xf] (fn [rf] (let [transformed (xf rf)] (fn ([] (transformed)) ([result] (transformed result)) ([result input] (utils/resolve (fn [error] (rf result (dm-schema/as-error error))) (fn [success] (transformed result success)) input)))))) | ||||||||||||
Transducer Helper Functions | |||||||||||||
Takes a splat of transducers | (defn chain [& xfs] (apply comp (map carry-errors-xf xfs))) | ||||||||||||
Takes a splat of transducers | (defn collect [& xfs] (fn [coll] (let [errors (transient []) handler (handle-errors-xf (fn [e] (conj! errors e))) result (into [] (apply comp (interleave (cons handler xfs) (repeat handler))) coll)] {:errors (persistent! errors) :result result}))) | ||||||||||||
Schemas & Functions for them | |||||||||||||
(ns danger-mouse.schema (:require [schema.core :as s])) | |||||||||||||
Schemas | |||||||||||||
(s/defschema ErrorResult {::error s/Any}) | |||||||||||||
(s/defschema GroupedResults {:errors [s/Any] :result [s/Any]}) | |||||||||||||
(s/defschema ProcessedError {:error-msg s/Str :error Throwable :input s/Any}) | |||||||||||||
Used to show that a result of | (defn WithErrors [schema] {:errors [ProcessedError] :result schema}) | ||||||||||||
Schema Utility Functions | |||||||||||||
Format any value as an error. Preferable to using the keyword manually, but the tools in utils are preferred over explicitly creating errors. | (s/defn as-error :- ErrorResult [x :- s/Any] {::error x}) | ||||||||||||
Check whether a value is an error. Preferable to checking the keyword manually. | (s/defn is-error? :- s/Bool [{::keys [error]}] (not (not error))) | ||||||||||||
Retrieve error value, which can be an Any (not necessarily an exception). Preferable to destructuring manually. | (s/defn get-error [{::keys [error]}] error) | ||||||||||||
Utility Functions | |||||||||||||
(ns danger-mouse.utils (:require [danger-mouse.schema :as dm-schema] [schema.core :as s])) | |||||||||||||
Collection Helper | |||||||||||||
(s/defn collect-results-map :- dm-schema/GroupedResults "Separate out errors and result from a vector argument and return them in a map." [xs :- [s/Any]] (loop [[y & ys :as all] xs errors (transient []) result (transient [])] (cond (empty? all) {:errors (persistent! errors) :result (persistent! result)} (dm-schema/is-error? y) (recur ys (conj! errors (dm-schema/get-error y)) result) :else (recur ys errors (conj! result y))))) | |||||||||||||
Mapping functions | |||||||||||||
Only apply | (defn on-success [success-fn result] (if (dm-schema/is-error? result) result (success-fn result))) | ||||||||||||
Only apply | (defn on-error [error-fn result] (if (dm-schema/is-error? result) (dm-schema/as-error (error-fn (dm-schema/get-error result))) result)) | ||||||||||||
Apply | (defn on-error-and-success [error-fn success-fn result] (if (dm-schema/is-error? result) (dm-schema/as-error (error-fn (dm-schema/get-error result))) (success-fn result))) | ||||||||||||
Apply | (defn resolve [error-fn success-fn result] (if (dm-schema/is-error? result) (error-fn (dm-schema/get-error result)) (success-fn result))) | ||||||||||||
Error handling functions | |||||||||||||
(s/defn handle-errors :- [s/Any] "After using `collect-results-map` to group values, this function will handle the `errors` portion using `handler` (which only produces side effects) and returns only the `result`." [handler :- (s/=> (s/named (s/eq nil) 'Unit) [s/Any]) {:keys [errors result]} :- dm-schema/GroupedResults] (handler errors) result) | |||||||||||||
Function version of a try-catch block. The body must be provided as a thunk to delay processing.
Macro version in | (s/defn try-catch* [thunk :- (s/=> s/Any)] (try (thunk) (catch Exception e {::dm-schema/error e}))) | ||||||||||||
(ns danger-mouse.threading) | |||||||||||||
Update the | (defmacro update-errors->> [& body-and-coll] (let [coll (last body-and-coll) body (drop-last body-and-coll)] `(update ~coll :errors #(->> % ~@body)))) | ||||||||||||
Update the | (defmacro update-result->> [& body-and-coll] (let [coll (last body-and-coll) body (drop-last body-and-coll)] `(update ~coll :result #(->> % ~@body)))) | ||||||||||||
TODO: This has the result in the first position, but still assumes that the functions passed in to update need the argument in last position. This has been my most common use case so far, but at least the naming needs to be cleared up. | |||||||||||||
Update the | (defmacro update-errors-> [coll & body] `(update ~coll :errors #(->> % ~@body))) | ||||||||||||
Update the | (defmacro update-result-> [coll & body] `(update ~coll :result #(->> % ~@body))) | ||||||||||||
Macros | |||||||||||||
(ns danger-mouse.macros (:require [danger-mouse.utils :as utils])) | |||||||||||||
A try-catch block in function form. Uses a macro to delay resolution of the | (defmacro try-catch [& body] `(utils/try-catch* (fn [] ~@body))) | ||||||||||||
(ns danger-mouse.async (:require [clojure.core.async :as async] [danger-mouse.transducers :as dm-transducers])) | |||||||||||||
(defn safe-channel [buf-or-n & xforms] (async/chan buf-or-n (apply dm-transducers/chain dm-transducers/contain-errors-xf xforms))) | |||||||||||||