Skip to content

Commit

Permalink
Callbacks wrapper feature and bug fixes (#4)
Browse files Browse the repository at this point in the history
* Callbacks wrapper feature and bug fixes

- :callbacks-wrapper-fn can be passed in the configuration with the value as a function with signature [val ctx step-res] that will be called if a callback is a value instead of a function.
- fixed the bug that threw exception when the injector step returned nil
- fixed a bug that was calling the step callback functions with unresolved promises.

* Documenting that the global callbacks can also be values if `callbacks-wrapper-fn` is provided on the configuration

Co-authored-by: EPChache <60799426+EPChache@users.noreply.github.com>
  • Loading branch information
ElChache and EPChache committed Feb 22, 2020
1 parent d0e7f79 commit d3984d5
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 68 deletions.
39 changes: 32 additions & 7 deletions README.md
Expand Up @@ -3,9 +3,22 @@ An async pipeline approach to functional core - imperative shell from by Gary Be

## Fork differences

### Step callbacks
Each step admits now the following keywords: `on-start`, `on-complete`, `on-success` and `on-error`.
The values should be functions with the signature `(fn [ctx step-res]`. If the value is not a function, and a `callbacks-wrapper-fn` function is given
on the configuration, and that function will be called with that value.

### Global callbacks
Global callbacks can now be a value that will be passed to the `callbacks-wrapper-fn`.

### callbacks-wrapper-fn
As described above, if a `callbacks-wrapper-fn` function is provided on the configuration, the steps and global callbacks can be values instead of functions,
and those values will be passed to the callback wrapper function. It should have the signature `(fn [callback-value ctx step-res])`.
Can be used to dispatch events instead of calling functions. And because the values of the step callbacks are now data, it can be tested.

### Steps and callback functions signature changed to receive the result from the previous step
The first steps's function signature remains the same, but consequent steps signature has been changed to `(fn [prev-step-res ctx])`.
The first argument is the result of the previous step, and the second the context, same as before.
The first steps's function signature remains the same, but consequent steps receive one more argument `(fn [ctx prev-step-res])`.
The first argument remains the same - the context - and the second argument is the result of the previous step.

### Path is now optional
If a step doesn't define a `:path`, the step will not augment the context, and the result of the step can only be used by the next step
Expand Down Expand Up @@ -97,6 +110,10 @@ The following section describes the parameters `fonda/execute` accepts.
|---|---|---|
| `:tap` | No | A function that gets the context but doesn't augment it. If it succeeds the result is ignored. If asynchronous it will still block the pipeline and interrupt the execution whenever either an anomaly or an exception happen. |
| `:name` | Yes | The name of the step as string or keyword |
| `:on-start` | Yes | A function with the signature `(fn [ctx])`, of a value if `callbacks-wrapper-fn` is provided on the configuration. It is called before the step is executed |
| `:on-complete` | Yes | A function with the signature `(fn [ctx step-res-or-exception-or-anomaly])`, of a value if `callbacks-wrapper-fn` is provided on the configuration. Called after the step is executed |
| `:on-success` | Yes | A function with the signature `(fn [ctx step-res])`, of a value if `callbacks-wrapper-fn` is provided on the configuration. Called after the step sucessfully executed |
| `:on-error` | Yes | A function with the signature `(fn [ctx step-exception-or-anomaly])`, of a value if `callbacks-wrapper-fn` is provided on the configuration. Called when the step returns an exception or an anomaly. |

- processor

Expand All @@ -105,7 +122,11 @@ The following section describes the parameters `fonda/execute` accepts.
| `:processor` or `:fn`| No | A function that gets the context and returns data. The data is [assoced-in](https://clojuredocs.org/clojure.core/assoc-in) at the given path Can be asynchronous. If asynchronous it will still block the pipeline and interrupt the execution whenever either an anomaly or an exception happen. |
| `:path` | Yes | Path where to assoc the result of the processor. If not given, the step will not augment the context. |
| `:name` | Yes | The name of the step as string or keyword. |

| `:on-start` | Yes | Same as in tap |
| `:on-complete` | Yes | Same as in tap |
| `:on-success` | Yes | Same as in tap |
| `:on-error` | Yes | Same as in tap |

- injector

| Key | Optional? | Notes |
Expand All @@ -114,9 +135,9 @@ The following section describes the parameters `fonda/execute` accepts.
| `:name` | Yes | The name of the injector step as string or keyword |


- **on-exception** Function called with an exception when any of the steps throws one.
- **on-success** Function called with the context if all steps succeed.
- [Optional] **on-anomaly** Function called in case of anomaly with the anomaly data itself.
- **on-exception** Function with the signature `(fn [ctx exception])` called with the context and an exception when any of the steps throws one.
- **on-success** Function with the signature `(fn [ctx last-step-result])` called with the context if all steps succeed, and the last step result.
- [Optional] **on-anomaly** Function with the signature `(fn [ctx exception])` called in case of anomaly with the context and the anomaly data itself.


## Full Example
Expand Down Expand Up @@ -150,7 +171,11 @@ The following section describes the parameters `fonda/execute` accepts.
;; Doesn't run this function, instead it runs the provided mock
[{:processor :example.full/get-remote-thing
:name "get-remote-thing"
:path [:remote-thing-response]}
:path [:remote-thing-response]
:on-start (fn [ctx] (println "Going to fetch the remote thing")
:on-success (fn [ctx res] (println "got the remote thing with response:" res))
:on-error (fn [ctx err-or-anomaly] (println "error fetching the remote thing:" err-or-anomaly))
:on-complete (fn [ctx step-res-or-exception-or-anomaly] (println "Done fetching the remote thing, the result (or error) is:" step-res-or-exception-or-anomaly)))}

{:tap :example.full/print-remote-thing}

Expand Down
4 changes: 2 additions & 2 deletions project.clj
@@ -1,4 +1,4 @@
(defproject elchache/fonda "0.0.2-SNAPSHOT"
(defproject elchache/fonda "0.0.3"
:url "https://github.com/arichiardi/fonda"
:description "An async pipeline approach to functional core - imperative shell."
:license {:name "Apache License"
Expand All @@ -12,4 +12,4 @@
:lein-tools-deps/config {:config-files [:project]}
:profiles {:dev {:test-paths ["test"]
:lein-tools-deps/config {:config-files [:install :user :project]
:aliases [:dev :test]}}})
:aliases [:dev :test]}}})
18 changes: 10 additions & 8 deletions src/fonda/core.cljs
Expand Up @@ -12,14 +12,16 @@
Each step function - tap or processor - can be synchronous or asynchronous.
- config: A map with:
- [opt] anomaly? A function that gets a map and determines if it is an anomaly.
- [opt] ctx The data that initializes the context. Must be a map.
- [opt] mock-fns A map of functions that will replace the function on the step, matching the map
key with the step name
- [opt] anomaly-handlers A map of functions indexed by step name that get called with a map
`{:ctx <ctx> :anomaly <anomaly>}` when the step returns an anomaly.
- [opt] exception-handlers A map of functions indexed by step name that get called with a map
`{:ctx <ctx> :exception <exception>}` when the step triggers an exception.
- [opt] anomaly? A function that gets a map and determines if it is an anomaly.
- [opt] ctx The data that initializes the context. Must be a map.
- [opt] mock-fns A map of functions that will replace the function on the step, matching the map
key with the step name
- [opt] anomaly-handlers A map of functions indexed by step name that get called with a map
`{:ctx <ctx> :anomaly <anomaly>}` when the step returns an anomaly.
- [opt] exception-handlers A map of functions indexed by step name that get called with a map
`{:ctx <ctx> :exception <exception>}` when the step triggers an exception.
- [opt] callbacks-wrapper-fn A function that gets called with the value of the on-* and the result of the step
`(fn [on-callback-val ctx step-res] ...)`
- steps: Each item on the `steps` collection must be either a Tap, a Processor, or an Injector
Expand Down
10 changes: 6 additions & 4 deletions src/fonda/core/specs.cljc
Expand Up @@ -15,10 +15,11 @@
(s/def ::ctx map?)
(s/def ::anomaly-handlers ::handlers-map)
(s/def ::exception-handlers ::handlers-map)
(s/def ::callbacks-wrapper-fn (s/nilable fn?))

(s/def ::on-success fn?)
(s/def ::on-exception fn?)
(s/def ::on-anomaly (s/nilable fn?))
(s/def ::on-success some?)
(s/def ::on-exception some?)
(s/def ::on-anomaly (s/nilable any?))

(s/def ::step-name-map
(s/keys :opt-un [::name]))
Expand All @@ -35,7 +36,8 @@
::mock-fns
::ctx
::anomaly-handlers
::exception-handlers]))
::exception-handlers
::callbacks-wrapper-fn]))

(s/fdef fonda.core/execute
:args (s/cat :config ::config
Expand Down
69 changes: 47 additions & 22 deletions src/fonda/execute.cljs
Expand Up @@ -3,6 +3,20 @@
[fonda.async :as a]
[fonda.step :as st]))

(defn get-callback-fn
[{:as fonda-ctx :keys [callbacks-wrapper-fn]} callback-val-or-fn]

(cond

;; If the given callback is a function, calls it. It won't be wrapped
(fn? callback-val-or-fn) callback-val-or-fn

;; If the given callback is a value, and there is a wrapper function, wraps it
(fn? callbacks-wrapper-fn) (partial callbacks-wrapper-fn callback-val-or-fn)

:else
callback-val-or-fn))

(defn assoc-tap-result
[{:as fonda-ctx :keys [anomaly-fn :processor-results-stack]} res]
(let [anomaly? (and anomaly-fn (anomaly-fn res))
Expand Down Expand Up @@ -36,7 +50,10 @@

(defn assoc-injector-result
[{:as fonda-ctx :keys [queue]} res]
(let [steps (if (sequential? res) res [res])]
(let [steps (cond
(nil? res) []
(sequential? res) res
:else [res])]
(assoc fonda-ctx :queue (into #queue [] st/xf (concat steps queue)))))

(defn assoc-exception-result [fonda-ctx e]
Expand All @@ -46,28 +63,30 @@

(defn handle-exception
[{:as fonda-ctx :keys [ctx]} {:keys [on-error] :as step} e]
(when on-error (on-error e ctx))
(when on-error
((get-callback-fn fonda-ctx on-error) ctx e))
(when (:name step) (println "Exception on step " (:name step)))
(assoc-exception-result fonda-ctx e))

(defn invoke-post-callback-fns
[{:as fonda-ctx :keys [anomaly-fn ctx]}
{:keys [on-complete on-success on-error path is-anomaly-error?] :as step}
step-res]

(let [aug-ctx (if path (assoc-in ctx path step-res) ctx)]

;; Always calls on-complete
(when on-complete
(on-complete step-res aug-ctx))
((get-callback-fn fonda-ctx on-complete) aug-ctx step-res))

(if (and anomaly-fn (anomaly-fn step-res) #_(is-anomaly-error? step-res))

;; If anomaly, calls on-error
(when on-error (on-error step-res aug-ctx))
(when on-error
((get-callback-fn fonda-ctx on-error) aug-ctx step-res))

;; Otherwise calls on-success
(when on-success (on-success step-res aug-ctx)))))
(when on-success
((get-callback-fn fonda-ctx on-success) aug-ctx step-res)))))

(defn- try-step
"Tries running the given step (a tap step, or a processor step).
Expand All @@ -77,7 +96,7 @@
{:as step :keys [processor tap inject name on-start]}]

;; Calls the on-start callback with the context
(when on-start (on-start ctx))
(when on-start ((get-callback-fn fonda-ctx on-start) ctx))
(try
(let [last-res (last processor-results-stack)

Expand All @@ -91,14 +110,13 @@
mocked-fn (when name (get mock-fns (keyword name)))
f (or mocked-fn processor tap inject)
res (apply f args)
assoc-result-fn (cond
assoc-result-fn* (cond
tap (partial assoc-tap-result fonda-ctx)
processor (partial assoc-processor-result fonda-ctx step)
inject (partial assoc-injector-result fonda-ctx))]

;; Invokes the callback functions
(invoke-post-callback-fns fonda-ctx step res)

inject (partial assoc-injector-result fonda-ctx))
assoc-result-fn (fn [res]
(invoke-post-callback-fns fonda-ctx step res)
(assoc-result-fn* res))]
(if (a/async? res)
(a/continue res assoc-result-fn #(handle-exception fonda-ctx step %))
(assoc-result-fn res)))
Expand All @@ -116,8 +134,8 @@
(a/continue fonda-ctx deliver-result (:on-exception fonda-ctx))
(let [[cb result] (cond exception [(:on-exception fonda-ctx) exception]
anomaly [(:on-anomaly fonda-ctx) anomaly]
:else [(:on-success fonda-ctx) ctx])]
(cb result (last processor-results-stack)))))
:else [(:on-success fonda-ctx) (last processor-results-stack)])]
((get-callback-fn fonda-ctx cb) ctx result))))

(defn execute-steps
"Sequentially runs each of the steps.
Expand Down Expand Up @@ -173,6 +191,12 @@
;; step triggers an exception.
exception-handlers

;; A function with the signature (fn [on-callback-val step-res]) that, if provided, will be called on every
;; callback (steps and globals). When provided, the callbacks can have any value - a function is not required anymore-
;; and that value will be passed to this function.
;; Useful if you want to dispatch events instead of calling functions.
callbacks-wrapper-fn

;; Callback function that gets called with the context after all the steps succeeded
on-success

Expand Down Expand Up @@ -221,19 +245,20 @@
This function does config validation."
[config]
(let [{:keys [anomaly? mock-fns ctx anomaly-handlers exception-handlers on-exception on-anomaly on-success steps]} config
(let [{:keys [anomaly? mock-fns ctx anomaly-handlers exception-handlers callbacks-wrapper-fn on-exception on-anomaly on-success steps]} config
anomaly-fn (anomaly-fn anomaly?)]
(assert (or (not anomaly-fn) (and anomaly-fn on-anomaly)) "When :anomaly? is truthy the on-anomaly callback is required.")
(assert on-success "The on-success callback is required.")
(assert on-exception "The on-exception callback is required.")

(map->FondaContext
(merge {:anomaly-handlers (clojure.walk/keywordize-keys anomaly-handlers)
:exception-handlers (clojure.walk/keywordize-keys exception-handlers)
:on-anomaly on-anomaly
:on-exception on-exception
:on-success on-success
:mock-fns (clojure.walk/keywordize-keys (or mock-fns {}))}
(merge {:anomaly-handlers (clojure.walk/keywordize-keys anomaly-handlers)
:exception-handlers (clojure.walk/keywordize-keys exception-handlers)
:callbacks-wrapper-fn callbacks-wrapper-fn
:on-anomaly on-anomaly
:on-exception on-exception
:on-success on-success
:mock-fns (clojure.walk/keywordize-keys (or mock-fns {}))}
(when anomaly-fn
{:anomaly-fn anomaly-fn})
{:ctx (or ctx {})
Expand Down
8 changes: 4 additions & 4 deletions src/fonda/step/specs.cljc
@@ -1,10 +1,10 @@
(ns fonda.step.specs
(:require [clojure.spec.alpha :as s]))

(s/def ::on-start (s/nilable fn?))
(s/def ::on-success (s/nilable fn?))
(s/def ::on-error (s/nilable fn?))
(s/def ::on-complete (s/nilable fn?))
(s/def ::on-start (s/nilable any?))
(s/def ::on-success (s/nilable any?))
(s/def ::on-error (s/nilable any?))
(s/def ::on-complete (s/nilable any?))
(s/def ::is-anomaly-error? (s/nilable fn?))

;; Tap step
Expand Down

0 comments on commit d3984d5

Please sign in to comment.