From e0f45138ff4080c068818a5bca83e76ea65d6f97 Mon Sep 17 00:00:00 2001 From: david Date: Sat, 12 Oct 2019 15:37:34 -0700 Subject: [PATCH 1/9] WIP - Add previous result in step functions, mocked functions WIP --- README.md | 143 ++++++--- deps.edn | 7 +- package.json | 2 +- pom.xml | 63 ---- scripts/deploy-jar | 43 --- scripts/git-revision | 58 ---- scripts/install-jar | 43 --- scripts/repl | 54 ---- scripts/version | 78 ----- src/fonda/core.cljs | 22 +- src/fonda/core/specs.cljc | 21 +- src/fonda/execute.cljs | 159 ++++++++-- src/fonda/execute/specs.cljc | 2 +- src/fonda/step.cljs | 36 ++- src/fonda/step/specs.cljc | 21 +- test/fonda/core_test.cljs | 547 ++++++++++++++++++++++++++++------- test/fonda/step_test.cljs | 4 +- 17 files changed, 751 insertions(+), 552 deletions(-) delete mode 100644 pom.xml delete mode 100755 scripts/deploy-jar delete mode 100755 scripts/git-revision delete mode 100755 scripts/install-jar delete mode 100755 scripts/repl delete mode 100755 scripts/version diff --git a/README.md b/README.md index 1f1d3b8..0513956 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,38 @@ # Fonda -An async pipeline approach to functional core - imperative shell from by Gary Bernhardt's [Boundaries talk.](https://www.destroyallsoftware.com/talks/boundaries) +An async pipeline approach to functional core - imperative shell from by Gary Bernhardt's [Boundaries talk.](https://www.destroyallsoftware.com/talks/boundaries). + +--- +## WIP + +### 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 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 + +### Mocked functions +Now is possible to mock the functions on the steps by passing a map of mocked functions to the fonda configuration. + +### `:fn` or `:processor` +We realized the `:processor` keyword is too long and verbose so now it's also possible to define processor steps with the `:fn` keyword + +--- + ## Minimal Viable Example @@ -19,13 +51,13 @@ This example illustrates `fonda`'s basic mechanics: {:basic-auth (select-keys ctx [:username :password])})) (fonda/execute - {:initial-ctx {:username js/process.USER - :password js/process.PASSWORD}} + {:ctx {:username js/process.USER + :password js/process.PASSWORD}} [{:processor :example.simple/fetch-user ;; can be either a function or a keyword :path [:github-response]} - {:processor :example.simple/github-response->things ;; pure function - ctx in -> ctx out + {:processor :example.simple/github-response->things ;; pure function [prev-step-res ctx] -> step-res :path [:github-things]}] ;; on-exception @@ -33,15 +65,15 @@ This example illustrates `fonda`'s basic mechanics: (handle-exception exception)) ;; on-success - (fn [ctx] - (handle-success (:github-things ctx)))) + (fn [res ctx] + (handle-success (:github-things res)))) ``` *HINT*: The parameter order makes it easy to partially apply `execute` for leaner call sites. --- -Fonda sequentially executes a series of [steps](#trivia), one after the other, augmenting a context map. The steps can be synchronous or asyncronous. After the steps are run, the termination callbacks will be executed. +Fonda sequentially executes a series of [steps](#trivia), one after the other, possibly augmenting a context map, and passing the result to the next one. The steps can be synchronous or asyncronous. After the steps are run, the termination callbacks will be executed. If a `js/Error`, an exception in `fonda` parlance, is thrown it will be automatically caught and the chain short circuits and the `on-exception` function is called with the `js/Error`. @@ -67,41 +99,50 @@ The following section describes the parameters `fonda/execute` accepts. - **config** - static configuration map - | Key | Optional? | Notes | - |---|---|---| - | `:anomaly?` | Yes | A boolean or a function that gets a map and determines if it is an anomaly. | - | `:initial-ctx` | Yes | The data that initializes the context. Must be a map, `{}` by default. | - | `:anomaly-handlers` | Yes | A map from step name keyword to function that gets called with a map `{:ctx :anomaly }` when the step returns an anomaly. | - | `:exception-handlers` | Yes | A map from step name keyword to function that gets called with a map `{:ctx :exception }` when the step triggers an exception. | + | Key | Optional? | Notes | + |-----------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------| + | `:anomaly?` | Yes | A boolean or a function that gets a map and determines if it is an anomaly. | + | `:ctx` | Yes | The data that initializes the context. Must be a map, `{}` by default. | + | `:mocked-fns` | Yes | A map of functions that will replace the functions on the step, indexed by step name. | + | `:anomaly-handlers` | Yes | A map from step name keyword to function that gets called with a map `{:ctx :anomaly }` when the step returns an anomaly. | + | `:exception-handlers` | Yes | A map from step name keyword to function that gets called with a map `{:ctx :exception }` when the step triggers an exception. | - **steps** - each item must be either a `Tap` or a `Processor`, or a `Injector` - tap - | Key | Optional? | Notes | - |---|---|---| - | `: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 | + | Key | Optional? | Notes | + |----------------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + | `: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 result-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 result])`, 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 - | Key | Optional? | Notes | - |---|---|---| - | `:processor` | 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` | No | Path where to assoc the result of the processor | - | `:name` | Yes | The name of the step as string or keyword | + | Key | Optional? | Notes | + |-----------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + | `: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 | - |---|---|---| - | `:injector` | No | A function that gets the context and returns either a step or a collection of steps. The step(s) returned will be executed right after the injector step and just before the next steps. Can be asynchronous. - | `:name` | Yes | The name of the injector step as string or keyword | + | Key | Optional? | Notes | + |-----------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + | `:inject` | No | A function that gets the context and returns either a step or a collection of steps. The step(s) returned will be executed right after the injector step and just before the next steps. Can be asynchronous. | + | `: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 @@ -110,38 +151,49 @@ The following section describes the parameters `fonda/execute` accepts. (ns example.full ...) -(defn print-remote-thing - [{:keys [remote-thing-response]}] - (println "the remote thing response was:" remote-thing-response)) - (defn get-remote-thing [ctx] (ajax/GET "http://remote-thing-url.com" {:params (:remote-thing-params ctx)})) +(defn print-remote-thing + [remote-thing-response ctx] + (println "the remote thing response was:" remote-thing-response)) + +(defn mocked-remote-thing (constantly :mocked-result)) + (fonda/execute - {:initial-ctx {:env-var-xyz "value", - :remote-thing-params {:p1 "p1" :p2 "p2"} - :other-remote-thing-responses []} + {:ctx {:env-var-xyz "value", + :remote-thing-params {:p1 "p1" :p2 "p2"} + :other-remote-thing-responses []} + + ;; Being replaced by the mock + :mocked-fns {:get-remote-thing mocked-remote-thing} + :anomaly-handlers {:get-remote-thing (fn [{:keys [anomaly]}] (post-error-to-log-server anomaly))} :exception-handlers {:get-remote-thing (fn [{:keys [exception]}] (js/console.log "An exception retrieving the remote thing occurred:" exception))}} + ;; 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 result-or-exception-or-anomaly] (println "Done fetching the remote thing, the result (or error) is:" result-or-exception-or-anomaly)))} {:tap :example.full/print-remote-thing} - {:processor :other.namespace/process-remote-thing-response + {:processor :example.other/parse-remote-thing-response :path [:remote-thing]} ;; Injector returns a collection of steps to be added right after the injector step - {:inject (fn [{:keys [remote-thing]}] - (->> (:side-effect-post-urls remote-thing) - (map (fn [side-effect-post-url] - {:tap (fn [{:keys [remote-thing-params]}] - (ajax/POST side-effect-post-url remote-thing-params))}))))}] + {:inject (fn [{:keys [remote-thing remote-thing-response]}] + (->> (:more-urls remote-thing) + (map (fn [url] + {:tap (fn [{:keys [remote-thing-params]}] + (ajax/POST url remote-thing-params))}))))}] ;; on-exception (fn [exception] @@ -154,7 +206,6 @@ The following section describes the parameters `fonda/execute` accepts. ;; on-anomaly (fn [anomaly] (handle-anomaly anomaly))) - ``` ## Thanks @@ -164,6 +215,10 @@ development work at [Elastic Path Software Inc.](https://www.elasticpath.com). A heart-felt thank you goes especially to [Matt Bishop](https://github.com/mattbishop) for supporting open source. +This package is has been basically co-maintained by [David Cerezo +Iñigo](https://github.com/ElChache), which has brought this library to the next +level, experimenting with it. + ## Trivia The name got inspired by Jane Fonda's step very successful fitness programs. diff --git a/deps.edn b/deps.edn index 484acd8..42fe626 100644 --- a/deps.edn +++ b/deps.edn @@ -4,8 +4,5 @@ org.clojure/clojurescript {:mvn/version "1.10.520"} expound {:mvn/version "0.7.2"} org.clojure/test.check {:mvn/version "0.10.0-alpha3" :exclusions [org.clojure/clojure]} - orchestra {:mvn/version "2018.12.06-2"}}} - :pack {:extra-deps {pack/pack.alpha {:git/url "https://github.com/juxt/pack.alpha.git" - :sha "90a84a01c365fdac224bf4eef6e9c8e1d018a29e"}}} - :deploy {:extra-deps {deps-deploy {:git/url "https://github.com/slipset/deps-deploy.git" - :sha "388481b808fc7b1a27951ad2725cac0ba10adb6b"}}}}} + orchestra {:mvn/version "2018.12.06-2"} + thheller/shadow-cljs {:mvn/version "2.8.59"}}}}} \ No newline at end of file diff --git a/package.json b/package.json index 791f9c5..414f989 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "An async pipeline approach to functional core - imperative shell.", "license": "Unlicense", "contributors": [ - "David Cerezo Iñigo ", "Sebastian Schulz" ], diff --git a/pom.xml b/pom.xml deleted file mode 100644 index b75ef30..0000000 --- a/pom.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - 4.0.0 - - fonda - fonda - fonda - 1.0.0-SNAPSHOT - An async pipeline approach to functional core - imperative shell. - - https://github.com/arichiardi/fonda - 2018 - - https://github.com/arichiardi/fonda - - - - - Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 - repo - - - - - - Andrea Richiardi - a.richiardi.work@gmail.com - - - David Cerezo Iñigo - davidcerezoinigo@gmail.com - - - Sebastian Schulz - - - - - src - - - - - clojars - https://repo.clojars.org/ - - - - - clojars - Clojars repository - https://clojars.org/repo - - - - - org.clojure - clojure - 1.10.0 - - - diff --git a/scripts/deploy-jar b/scripts/deploy-jar deleted file mode 100755 index 204c35c..0000000 --- a/scripts/deploy-jar +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -do_usage() { - echo -n - echo - echo "Deploy the jar to Clojars." - echo - echo "It requires mvn." - echo - echo "Usage: $0 -j " - echo - echo "Options:" - echo " -h --help Show this screen." - echo " -j --jar-path= The path to the jar file." - exit 1 -} - -jar_path= - -set +e -TEMP=$(getopt -o h:j: --long help,jar-path: -n "$(basename $0)" -- "$@") -set -e -eval set -- "$TEMP" - -# extract options and their arguments into variables. -while true ; do - case "$1" in - -j|--jar-path) jar_path="$2" ; shift 2 ;; - -h|--help) shift ; do_usage ; break ;; - --) shift ; break ;; - *) do_usage ; exit 1 ;; - esac -done - -# sanity checks -# set defaults from env var if present (note that arguments win) -set +u -[ -z "$jar_path" ] && do_usage -set -u - -clojure -A:deploy -m deps-deploy.deps-deploy deploy "$jar_path" diff --git a/scripts/git-revision b/scripts/git-revision deleted file mode 100755 index bb1ff80..0000000 --- a/scripts/git-revision +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -do_usage() { - echo -n - echo - echo "Compute the revision number from the latest git tag." - echo - echo "The revision is none other than the number of commits from the most - recent tag, but it is important because it is reproducible." - echo - echo "Usage: $0 --regex " - echo - echo "Options:" - echo " -h --help Show this screen." - echo " -r --regex= [OPTIONAL] The regex to be used in git describe -match." - exit 1 -} - -tag_regex= - -set +e -TEMP=$(getopt -o hr: --long help,regex: -n "$(basename "$0")" -- "$@") -set -e -eval set -- "$TEMP" - -# extract options and their arguments into variables. -while true ; do - case "$1" in - -r|--regex) tag_regex="$2" ; shift 2 ;; - -h|--help) shift ; do_usage ; break ;; - --) shift ; break ;; - *) do_usage ; exit 1 ;; - esac -done - -[ -n "$tag_regex" ] || do_usage - -set +e -revision=$(git --no-replace-objects describe --match "$tag_regex" 2> /dev/null) -set -e - -if [ -n "$revision" ]; then - # trim everything before @ - revision=${revision##*@} - - # Extract the commit count from the revision - revision_regex="[0-9]*\.[0-9]*.*-([0-9]*)-.*" - if [[ $revision =~ $revision_regex ]]; then - revision="${BASH_REMATCH[1]}" - fi -else - echo "Cannot find a tag for the \"$tag_regex\" regex." - exit 1 -fi - -echo "$revision" diff --git a/scripts/install-jar b/scripts/install-jar deleted file mode 100755 index 74f47cd..0000000 --- a/scripts/install-jar +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -do_usage() { - echo -n - echo - echo "Install the jar named .jar in the local .m2/repository." - echo - echo "It requires and uses Maven." - echo - echo "Usage: $0 -p " - echo - echo "Options:" - echo " -h --help Show this screen." - echo " -j --jar-path= The path to the jar file." - exit 1 -} - -jar_path= - -set +e -TEMP=$(getopt -o h:j: --long help,jar-path: -n "$(basename $0)" -- "$@") -set -e -eval set -- "$TEMP" - -# extract options and their arguments into variables. -while true ; do - case "$1" in - -j|--jar-path) jar_path="$2" ; shift 2 ;; - -h|--help) shift ; do_usage ; break ;; - --) shift ; break ;; - *) do_usage ; exit 1 ;; - esac -done - -# sanity checks -# set defaults from env var if present (note that arguments win) -set +u -[ -z "$jar_path" ] && do_usage -set -u - -mvn install:install-file -Dfile="$jar_path" -DpomFile=pom.xml diff --git a/scripts/repl b/scripts/repl deleted file mode 100755 index 224620e..0000000 --- a/scripts/repl +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -# -# Call yarn shadow-cljs watch and then launch node fonda.js. -# -# Note: the $@ parameters are forwarded to yarn shadow-cljs not to the node -# command. -# -set -euo pipefail - -output_dir=dist -output_file=$output_dir/fonda.js - -shadow_dir=.shadow-cljs -shadow_pid_file=$shadow_dir/server.pid - -shadow_build="lib" -shadow_bin="yarn shadow-cljs" -shadow_subcmd="watch" - -node_bin=$(which node) -node_opts="--inspect $output_file" - -function do_cleanup { - pkill $(cat "$shadow_pid_file"); -} - -set +e -rm -v "$output_file" -set -e - -shadow_cmd="$shadow_bin $shadow_subcmd $shadow_build $@" -shadow_cmd=$(echo "$shadow_cmd" | sed -e 's/[[:space:]]*$//') - -echo "[repl] \"$shadow_cmd\"" -$shadow_cmd & - -# active loop waiting for the js file to appear -i=1 -spinner="/-\|" -echo -n ' ' -while [ ! -f "$output_file" ]; do - for c in / - \\ \|; do - printf '\b%s' "$c"; - sleep .1 - done -done - -node_cmd="$node_bin $node_opts" -echo "[repl] Shadow-cljs is up" -echo "[repl] Execute \"$node_cmd\"" - -$node_cmd - -trap "set +e; do_cleanup" ERR SIGTERM SIGINT EXIT diff --git a/scripts/version b/scripts/version deleted file mode 100755 index 97cb890..0000000 --- a/scripts/version +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash -# -# Compute the next release version. -# -# The revision number is calculated reproducibly: this number will always be -# the number of commits fro the latest tag. This is useful for reproducible -# version numbers that do not rely on semantic versioning. - -set -euo pipefail - -do_usage() { - echo -n - echo - echo "Compute the next release version." - echo - echo "It merges the revision number with the content of the VERSION file." - echo "The VERSION file should contain a COMPUTED_REVISION string." - echo - echo "Usage: $0 -n -p " - echo - echo "Options:" - echo " -h --help Show this screen." - echo " -s --snapshot Append -SNAPSHOT to the version." - echo " -p --tag-prefix= The git tag prefix. Default is \"v\"." - exit 1 -} - -project_name= - -set +e -TEMP=$(getopt -o h:sp: --long help,snapshot,tag-prefix: -n "$(basename $0)" -- "$@") -set -e -eval set -- "$TEMP" - -version_snapshot= - -# extract options and their arguments into variables. -while true ; do - case "$1" in - -s|--snapshot) version_snapshot=-SNAPSHOT ; shift ;; - -p|--tag-prefix) tag_prefix="$2" ; shift 2 ;; - -h|--help) shift ; do_usage ; break ;; - --) shift ; break ;; - *) do_usage ; exit 1 ;; - esac -done - -tag_prefix="${tag_prefix:-v}" - -# The command `git describe --match v0.0` will return a string like -# -# v0.0-856-g329708b -# -# where 856 is the number of commits since the v0.0 tag. We use that as -# revision. -tag_regex="$tag_prefix*" - -set +e -revision=$(git --no-replace-objects describe --match "$tag_regex" 2> /dev/null) -set -e - -if [ -n "$revision" ]; then - # trim everything before @ - revision=${revision##*@} - - # Extract the commit count from the revision - revision_regex="[0-9]*\.[0-9]*.*-([0-9]*)-.*" - if [[ $revision =~ $revision_regex ]]; then - revision="${BASH_REMATCH[1]}" - fi -else - revision=$(git rev-list --count HEAD) -fi - -version_template=$(cat VERSION) -version=${version_template/COMPUTED_REVISION/$revision}$version_snapshot - -echo "$version" diff --git a/src/fonda/core.cljs b/src/fonda/core.cljs index f769fd2..c64e51e 100644 --- a/src/fonda/core.cljs +++ b/src/fonda/core.cljs @@ -12,12 +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] initial-ctx The data that initializes the context. Must be a map. - - [opt] anomaly-handlers A map of functions indexed by step name that get called with a map - `{:ctx :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 :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 :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 :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 @@ -26,9 +30,9 @@ - name: The name of the step Processor: - - processor: A function that gets the context and assocs the result into it on the given path - - path: Path where to assoc the result of the processor - - name: The name of the step + - [processor] or [fn]: A function that gets the context and assocs the result into it on the given path + - path: Path where to assoc the result of the processor + - name: The name of the step Injector: - inject: A function that gets the context and returns either a step or a collection of them. diff --git a/src/fonda/core/specs.cljc b/src/fonda/core/specs.cljc index cb5e7db..bf0f23a 100644 --- a/src/fonda/core/specs.cljc +++ b/src/fonda/core/specs.cljc @@ -2,21 +2,24 @@ (:require [clojure.spec.alpha :as s] [fonda.step.specs :as step])) -(s/def ::name (s/nilable (s/or :string string? - :keyword keyword?))) +(def name-s (s/or :string string? + :keyword keyword?)) +(s/def ::name (s/nilable name-s)) ;; handler-maps keys in fonda.core can be either strings or keywords (s/def ::handlers-map (s/map-of ::name fn?)) ;; Config (s/def ::anomaly? (s/or :boolean boolean? :predicate fn?)) -(s/def ::initial-ctx map?) +(s/def ::mock-fns (s/map-of name-s fn?)) +(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])) @@ -30,9 +33,11 @@ (s/def ::config (s/keys :opt-un [::anomaly? - ::initial-ctx + ::mock-fns + ::ctx ::anomaly-handlers - ::exception-handlers])) + ::exception-handlers + ::callbacks-wrapper-fn])) (s/fdef fonda.core/execute :args (s/cat :config ::config diff --git a/src/fonda/execute.cljs b/src/fonda/execute.cljs index f261842..147732e 100644 --- a/src/fonda/execute.cljs +++ b/src/fonda/execute.cljs @@ -3,57 +3,136 @@ [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]} res] - (if (and anomaly-fn (anomaly-fn res)) - (assoc fonda-ctx :anomaly res) - fonda-ctx)) + [{:as fonda-ctx :keys [anomaly-fn :processor-results-stack]} res] + (let [anomaly? (and anomaly-fn (anomaly-fn res)) + + ;; taps only contribute anomalies to the context + new-fonda-ctx (if anomaly? (assoc fonda-ctx :anomaly res) fonda-ctx)] + + (cond-> new-fonda-ctx + + ;; Only if the tap result is an anomaly, it adds the result to the results stack + anomaly? (update :processor-results-stack conj res)))) (defn assoc-processor-result - [{:as fonda-ctx :keys [anomaly-fn]} path res] - (if (and anomaly-fn (anomaly-fn res)) - (assoc fonda-ctx :anomaly res) - (assoc-in fonda-ctx (concat [:ctx] path) res))) + [{:as fonda-ctx :keys [anomaly-fn]} + {:keys [path is-anomaly-error?]} + res] + (let [new-fonda-ctx (cond + + ;; If it is an anomaly, associates the anomaly on the context, and execution will stop here + (and anomaly-fn (anomaly-fn res) (is-anomaly-error? res)) (assoc fonda-ctx :anomaly res) + + ;; If there is no path on the step, it doesn't contribute to the context + (nil? path) fonda-ctx + + ;; Otherwise associates the result into the context + :else (assoc-in fonda-ctx (concat [:ctx] path) res))] + + + ;; It always adds the result ot the results stack + (update new-fonda-ctx :processor-results-stack conj res))) (defn assoc-injector-result [{:as fonda-ctx :keys [queue]} res] (let [steps (if (sequential? res) res [res])] (assoc fonda-ctx :queue (into #queue [] st/xf (concat steps queue))))) +(defn assoc-exception-result [fonda-ctx e] + (-> fonda-ctx + (assoc :exception e) + (update :processor-results-stack conj e))) + +(defn handle-exception + [{:as fonda-ctx :keys [ctx]} {:keys [on-error] :as step} e] + (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 + ((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 + ((get-callback-fn fonda-ctx on-error) aug-ctx step-res)) + + ;; Otherwise calls on-success + (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). If an exception gets triggerd, an exception is added on the context. If an anomaly is returned, an anomaly is added to the context" - [{:as fonda-ctx :keys [ctx]} - {:as step :keys [processor tap inject]}] + [{:as fonda-ctx :keys [ctx mock-fns processor-results-stack]} + {:as step :keys [processor tap inject name on-start]}] + + ;; Calls the on-start callback with the context + (when on-start ((get-callback-fn fonda-ctx on-start) ctx)) (try - (let [res (cond - processor (processor ctx) - tap (tap ctx) - inject (inject ctx)) - assoc-result-fn (cond + (let [last-res (last processor-results-stack) + + ;; First step only gets the ctx, next ones receive last-result,ctx + args (if (empty? processor-results-stack) [ctx] [ctx last-res]) + + ; fn is an alias for processor + processor (or processor (:fn step)) + + ;; If there is a mocked-fn with the same name, it will used the mocked-fn instead + mocked-fn (when name (get mock-fns (keyword name))) + f (or mocked-fn processor tap inject) + res (apply f args) + assoc-result-fn* (cond tap (partial assoc-tap-result fonda-ctx) - processor (partial assoc-processor-result fonda-ctx (:path step)) - inject (partial assoc-injector-result fonda-ctx))] + processor (partial assoc-processor-result fonda-ctx step) + 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 #(assoc fonda-ctx :exception %)) + (a/continue res assoc-result-fn #(handle-exception fonda-ctx step %)) (assoc-result-fn res))) (catch :default e - (assoc fonda-ctx :exception e)))) + (handle-exception fonda-ctx step e)))) (defn- deliver-result "Calls a callback depending on what is on the context. If there is an exception on the context, calls on-exception. If there is an anomaly on the context, calls on-anomaly. Otherwise calls on-success." - [{:as fonda-ctx :keys [ctx exception anomaly]}] + [{:as fonda-ctx :keys [ctx exception anomaly processor-results-stack]}] (if (a/async? fonda-ctx) (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)))) + :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. @@ -109,12 +188,21 @@ ;; 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 ;; Callback function that gets called with an exception that a step triggered on-exception + ;;A map of functions that will replace the function on the step, matching the map key with the step name + mock-fns + ;; Callback function taht gets called with an anomaly that a step returned on-anomaly @@ -122,7 +210,11 @@ queue ;; The steps that have already been processed - stack]) + stack + + ;; The results of the steps that have already been processed + processor-results-stack + ]) (defn cognitect-anomaly? [m] @@ -150,20 +242,23 @@ This function does config validation." [config] - (let [{:keys [anomaly? 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} + (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 (:initial-ctx config) {}) - :queue (into #queue [] st/xf steps) - :stack []})))) \ No newline at end of file + {:ctx (or ctx {}) + :queue (into #queue [] st/xf steps) + :stack [] + :processor-results-stack []})))) \ No newline at end of file diff --git a/src/fonda/execute/specs.cljc b/src/fonda/execute/specs.cljc index 61d4362..ee58937 100644 --- a/src/fonda/execute/specs.cljc +++ b/src/fonda/execute/specs.cljc @@ -65,7 +65,7 @@ (s/fdef fonda.execute/assoc-processor-result :args (s/cat :fonda-ctx ::fonda-context - :path ::step/path + :step ::step/processor-step :res any?)) (s/fdef fonda.execute/assoc-tap-result diff --git a/src/fonda/step.cljs b/src/fonda/step.cljs index 3dbb014..2541057 100644 --- a/src/fonda/step.cljs +++ b/src/fonda/step.cljs @@ -7,17 +7,37 @@ tap ;; The name for the step - name]) + name + + on-start + + on-success + + on-error + + on-complete + ]) (defrecord Processor [;; A function that gets the context, the result is attached to the context on the given path - processor + fn ;; Name for the step name ;; Path were to attach the processor result on the context - path]) + path + + on-start + + on-success + + on-error + + on-complete + + is-anomaly-error? + ]) (defrecord Injector [;; Function that returns step(s) to be injected right after this step on the queue @@ -33,14 +53,14 @@ fn-or-keyword)) (defn step->record - [{:keys [tap processor inject] :as step}] + [{:keys [tap inject fn processor] :as step}] (let [step (cond tap (map->Tap (update step :tap resolve-function)) - processor (map->Processor (update step :processor resolve-function)) + (or processor fn) (map->Processor (-> step + (update :fn resolve-function) + (assoc :is-anomaly-error? (or (:is-anomaly-error? step) (constantly true))))) inject (map->Injector (update step :inject resolve-function)))] (update step :name keyword))) -(def ^{:doc "Step transducer."} -xf - (map step->record)) \ No newline at end of file +(def ^{:doc "Step transducer."} xf (map step->record)) diff --git a/src/fonda/step/specs.cljc b/src/fonda/step/specs.cljc index 662b901..563f081 100644 --- a/src/fonda/step/specs.cljc +++ b/src/fonda/step/specs.cljc @@ -1,16 +1,29 @@ (ns fonda.step.specs (:require [clojure.spec.alpha :as s])) +(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 (s/def ::tap (s/or :function fn? :qualified-keyword qualified-keyword?)) (s/def ::tap-step - (s/keys :req-un [::tap])) + (s/keys :req-un [::tap] + :opt-un [::on-start ::on-success ::on-error ::on-complete])) ;; Processor step -(s/def ::path vector?) -(s/def ::processor (s/or :function fn? :qualified-keyword qualified-keyword?)) +(s/def ::path (s/nilable vector?)) +(s/def ::fn (s/or :function fn? :qualified-keyword qualified-keyword?)) (s/def ::processor-step - (s/keys :req-un [::processor ::path])) + (s/keys :req-un [::fn] + :opt-un [::path + ::on-start + ::on-success + ::on-error + ::on-complete + ::is-anomaly-error?])) ;; Injector step (s/def ::inject (s/or :function fn? :qualified-keyword qualified-keyword?)) diff --git a/test/fonda/core_test.cljs b/test/fonda/core_test.cljs index bc63e73..01050a8 100644 --- a/test/fonda/core_test.cljs +++ b/test/fonda/core_test.cljs @@ -1,5 +1,5 @@ (ns fonda.core-test - (:require [cljs.test :refer-macros [deftest is testing async]] + (:require [cljs.test :refer-macros [deftest is testing async use-fixtures]] [fonda.core :as fonda] [fonda.core.specs] [fonda.execute.specs] @@ -7,13 +7,25 @@ (orchestra/instrument) -(defn success-cb-throw [res] - (throw (js/Error (str "unexpected success callback called with res:" res)))) +(def events (atom [])) +(defn cb-with-result [event] + (fn [ctx result] + (swap! events conj [event result ctx]))) -(defn exception-cb-throw [err] +(defn cb-no-result [event] + (fn [ctx] + (swap! events conj [event ctx]))) + + +(use-fixtures :each {:before (fn [] (reset! events []))}) + +(defn success-cb-throw [ctx res] + (throw (js/Error (str "unexpected success callback called with res:" res " and ctx:" ctx)))) + +(defn exception-cb-throw [ctx err] (throw err)) -(defn anomaly-cb-throw [anomaly] +(defn anomaly-cb-throw [ctx anomaly] (throw (js/Error (str "unexpected anomaly callback called with anomaly:" anomaly)))) (defn anomaly @@ -29,19 +41,20 @@ (fonda/execute {} [] exception-cb-throw - (fn [res] - (is (= {} res)) + (fn [ctx res] + (is (= @events [])) + (is (= {} ctx)) (done)))))) (deftest execute-empty-chain-test-2 (testing "Passing a context on the configuration with empty steps should call on-success with that context." (let [initial {:foo :bar}] (async done - (fonda/execute {:initial-ctx initial} + (fonda/execute {:ctx initial} [] exception-cb-throw - (fn [res] - (is (= initial res)) + (fn [ctx res] + (is (= initial ctx)) (done))))))) (deftest one-successful-sync-processor-test @@ -49,13 +62,50 @@ (async done (let [processor-res 42 processor-path [:processor-path] - processor {:path processor-path - :processor (constantly processor-res)}] + processor {:on-success (cb-with-result ::auth-success) + :path processor-path + :fn (constantly processor-res)}] (fonda/execute {} [processor] exception-cb-throw - (fn [res] - (is (= processor-res (get-in res processor-path))) + (fn [ctx res] + (is (= processor-res (get-in ctx processor-path) res)) + (is (= @events [[::auth-success processor-res {:processor-path processor-res}]])) + (done))))))) + +(deftest one-successful-sync-processor-with-no-path-doesnt-contribute-to-ctx-test + (testing "Passing one synchronous processor with no path should call on-success with the context untouched" + (async done + (let [initial-ctx {:initial "value"} + processor-res 42 + processor {:fn (constantly processor-res) + :on-complete (cb-with-result ::complete)}] + (fonda/execute {:ctx initial-ctx} + [processor] + exception-cb-throw + (fn [ctx res] + (is (= res processor-res)) + (is (= ctx initial-ctx)) + (is (= @events [[::complete processor-res initial-ctx]])) + (done))))))) + +(deftest one-successful-sync-mocked-processor-test + (testing "The mocked function should replace the function on the step" + (async done + (let [processor-res 42 + mocked-processor-res 43 + processor-path [:processor-path] + processor {:path processor-path + :fn (constantly processor-res) + :name ::step-1 + :on-start (cb-no-result ::start)}] + (fonda/execute {:mock-fns {::step-1 (constantly mocked-processor-res)}} + [processor] + exception-cb-throw + (fn [ctx res] + ;; The on-start callback was called before the step, with the initial ctx + (is (= @events [[::start {}]])) + (is (= mocked-processor-res (get-in ctx processor-path))) (done))))))) (deftest one-successful-sync-tap-doesnt-augment-context-test @@ -64,11 +114,11 @@ (let [initial {:foo :bar} tap {:name "tap1" :tap (constantly :whatever-value)}] - (fonda/execute {:initial-ctx initial} + (fonda/execute {:ctx initial} [tap] exception-cb-throw - (fn [res] - (is (= initial res)) + (fn [ctx res] + (is (= initial ctx)) (done))))))) (deftest one-successful-sync-tap-is-passed-the-context @@ -78,7 +128,36 @@ tap {:tap (fn [ctx] (is (= initial ctx)) (done))}] - (fonda/execute {:initial-ctx initial} + (fonda/execute {:ctx initial} + [tap] + exception-cb-throw + (fn [_])))))) + +(deftest one-successful-sync-tap-as-second-step-prev-res-is-passed + (testing "Passing one synchronous tap should call on-success with the initial context" + (async done + (let [initial {:foo :bar} + prev-step-res 42 + prev-step {:fn (constantly prev-step-res)} + tap {:tap (fn [ctx step-res] + (is (= step-res prev-step-res)) + (is (= ctx initial)) + (done))}] + (fonda/execute {:ctx initial} + [prev-step tap] + exception-cb-throw + (fn [_])))))) + +(deftest one-successful-mocked-sync-tap-is-passed-the-context + (testing "Passing one synchronous tap should call on-success with the initial context" + (async done + (let [initial {:foo :bar} + tap {:tap (fn [_]) + :name ::tap-step}] + (fonda/execute {:ctx initial + :mock-fns {::tap-step (fn [ctx] + (is (= initial ctx)) + (done))}} [tap] exception-cb-throw (fn [_])))))) @@ -89,12 +168,28 @@ (let [processor-res 42 processor-path [:processor-path] processor {:path processor-path - :processor (constantly (js/Promise.resolve processor-res))}] + :fn (constantly (js/Promise.resolve processor-res))}] (fonda/execute {} [processor] exception-cb-throw - (fn [res] - (is (= processor-res (get-in res processor-path))) + (fn [ctx res] + (is (= processor-res (get-in ctx processor-path))) + (done))))))) + +(deftest one-successful-async-mocked-processor-test + (testing "The mocked function should replace the function on the step" + (async done + (let [mocked-processor-res 43 + processor-res 42 + processor-path [:processor-path] + processor {:path processor-path + :fn (constantly (js/Promise.resolve processor-res)) + :name ::step-1}] + (fonda/execute {:mock-fns {::step-1 (constantly (js/Promise.resolve mocked-processor-res))}} + [processor] + exception-cb-throw + (fn [ctx res] + (is (= mocked-processor-res (get-in ctx processor-path))) (done))))))) (deftest one-unsuccessful-sync-processor-test @@ -103,17 +198,55 @@ (let [processor-res (anomaly :cognitect.anomalies/incorrect) processor {:path [:processor-path] :name "step1" - :processor (constantly processor-res)} + :fn (constantly processor-res) + :on-error (cb-with-result ::error)} + anomaly-handler-arg (atom nil)] + (fonda/execute {:anomaly? true + :anomaly-handlers {"step1" #(reset! anomaly-handler-arg (:anomaly %))}} + [processor] + exception-cb-throw + (fn [_]) + (fn [ctx anomaly] + (is (= @anomaly-handler-arg processor-res)) + (is (= processor-res anomaly)) + (is (= @events [[::error anomaly {:processor-path anomaly}]])) + (done))))))) + +(deftest one-unsuccessful-sync-processor-with-no-path-test + (testing "Passing one synchronous unsuccessful processor should call on-anomaly with the anomaly after calling the anomaly-handler" + (async done + (let [processor-res (anomaly :cognitect.anomalies/incorrect) + processor {:name "step1" + :fn (constantly processor-res)} anomaly-handler-arg (atom nil)] (fonda/execute {:anomaly? true :anomaly-handlers {"step1" #(reset! anomaly-handler-arg (:anomaly %))}} [processor] exception-cb-throw (fn [_]) - (fn [anomaly] + (fn [ctx anomaly] (is (= @anomaly-handler-arg processor-res)) (is (= processor-res anomaly)) (done))))))) +(deftest one-unsuccessful-mocked-sync-processor-test + (testing "Passing one synchronous unsuccessful mocked processor should call on-anomaly with the anomaly after calling the anomaly-handler" + (async done + (let [mocked-processor-res (anomaly :cognitect.anomalies/incorrect) + processor-res (anomaly ::another-anomaly) + processor {:path [:processor-path] + :name "step1" + :fn (constantly processor-res)} + anomaly-handler-arg (atom nil)] + (fonda/execute {:anomaly? true + :anomaly-handlers {"step1" #(reset! anomaly-handler-arg (:anomaly %))} + :mock-fns {:step1 (constantly mocked-processor-res)}} + [processor] + exception-cb-throw + (fn [_]) + (fn [ctx anomaly] + (is (= @anomaly-handler-arg mocked-processor-res)) + (is (= mocked-processor-res anomaly)) (done))))))) + (deftest one-unsuccessful-sync-tap-test (testing "Passing one synchronous unsuccessful tap should call on-anomaly with the anomaly after calling the anomaly handler, and passing step keyword" @@ -128,7 +261,7 @@ [tap] exception-cb-throw success-cb-throw - (fn [anomaly] + (fn [ctx anomaly] (is (= @anomaly-handler-arg tap-res)) (is (= tap-res anomaly)) (done))))))) @@ -140,14 +273,14 @@ (let [processor-res (anomaly :cognitect.anomalies/incorrect) processor {:path [:processor-path] :name "step1" - :processor (constantly (js/Promise.resolve processor-res))} + :fn (constantly (js/Promise.resolve processor-res))} anomaly-handler-arg (atom nil)] (fonda/execute {:anomaly? true :anomaly-handlers {:step1 #(reset! anomaly-handler-arg (:anomaly %))}} [processor] exception-cb-throw success-cb-throw - (fn [anomaly] + (fn [ctx anomaly] (is (= @anomaly-handler-arg processor-res)) (is (= processor-res anomaly)) (done))))))) @@ -159,11 +292,11 @@ (let [processor-res (js/Error "Bad exception") processor {:path [:processor-path] :name "step1" - :processor (fn [_] (throw processor-res))} + :fn (fn [_] (throw processor-res))} exception-handler-arg (atom nil)] (fonda/execute {:exception-handlers {:step1 #(reset! exception-handler-arg (:exception %))}} [processor] - (fn [err] + (fn [ctx err] (is (= @exception-handler-arg processor-res)) (is (= processor-res err)) (done)) @@ -179,7 +312,7 @@ exception-handler-arg (atom nil)] (fonda/execute {:exception-handlers {:step1 #(reset! exception-handler-arg (:exception %))}} [tap] - (fn [err] + (fn [ctx err] (is (= @exception-handler-arg tap-res)) (is (= tap-res err)) (done)) @@ -192,11 +325,11 @@ (let [processor-res (js/Error "Bad exception") processor {:path [:processor-path] :name "step1" - :processor (constantly (js/Promise.reject processor-res))} + :fn (constantly (js/Promise.reject processor-res))} exception-handler-arg (atom nil)] (fonda/execute {:exception-handlers {"step1" #(reset! exception-handler-arg (:exception %))}} [processor] - (fn [err] + (fn [ctx err] (is (= @exception-handler-arg processor-res)) (is (= processor-res err)) (done)) @@ -208,11 +341,11 @@ (async done (let [processor-res (js/Error "Bad exception") processor {:path [:processor-path] - :processor (constantly (js/Promise.reject processor-res))} + :fn (constantly (js/Promise.reject processor-res))} exception-handler-arg (atom nil)] (fonda/execute {:exception-handlers {"step1" #(reset! exception-handler-arg (:exception %))}} [processor] - (fn [err] + (fn [ctx err] (is (nil? @exception-handler-arg)) (is (= processor-res err)) (done)) @@ -224,14 +357,41 @@ (let [step1-val 1 step2-fn inc] (fonda/execute {} - [{:path [:step1] :processor (constantly step1-val)} - {:path [:step2] :processor (fn [{:keys [step1]}] (step2-fn step1))}] + [{:path [:step1] :fn (constantly step1-val)} + {:path [:step2] :fn (fn [{:keys [step1]} _] (step2-fn step1))}] exception-cb-throw - (fn [res] - (is (= res {:step1 step1-val + (fn [ctx res] + (is (= ctx {:step1 step1-val :step2 (step2-fn step1-val)})) (done))))))) +(deftest multiple-successful-synchronous-steps-using-prev-step-result-test + (testing "Passing multiple successful synchronous steps should call the on-success callback with the result of the last step" + (async done + (let [step1-val 1 + step2-fn inc] + (fonda/execute {} + [{:path [:step1] :fn (constantly step1-val)} + {:path [:step2] :fn (fn [_ step1] (step2-fn step1))}] + exception-cb-throw + (fn [_ step2] + (is (= step2 (step2-fn step1-val))) + (done))))))) + +(deftest taps-dont-contribute-result-to-next-step + (testing "Passing multiple successful synchronous steps should call the on-success callback with the result of the last step" + (async done + (let [step1-val 1 + step2-fn inc] + (fonda/execute {} + [{:path [:step1] :fn (constantly step1-val)} + {:tap (constantly "tap-res")} + {:path [:step2] :fn (fn [_ step1] (step2-fn step1))}] + exception-cb-throw + (fn [_ step2] + (is (= step2 (step2-fn step1-val))) + (done))))))) + (deftest multiple-successful-asynchronous-steps-augmented-context-on-success-test (testing "Passing multiple successful asynchronous steps should call the on-success callback with the augmented context" (async done @@ -239,13 +399,31 @@ step2-fn inc] (fonda/execute {} [{:path [:step1] - :processor (constantly (js/Promise.resolve step1-val))} + :fn (constantly (js/Promise.resolve step1-val))} {:path [:step2] - :processor (fn [{:keys [step1]}] + :fn (fn [{:keys [step1]} _] (js/Promise.resolve (step2-fn step1)))}] exception-cb-throw - (fn [res] - (is (= res {:step1 step1-val + (fn [ctx res] + (is (= ctx {:step1 step1-val + :step2 (step2-fn step1-val)})) + (done))))))) + +(deftest multiple-successful-asynchronous-steps-using-prev-step-res-augmented-context-on-success-test + (testing "Passing multiple successful asynchronous steps should call the on-success callback with the augmented context" + (async done + (let [step1-val 1 + step2-fn inc] + (fonda/execute {} + [{:path [:step1] + :fn (constantly (js/Promise.resolve step1-val))} + {:path [:step2] + :fn (fn [_ step1] + (js/Promise.resolve (step2-fn step1)))}] + exception-cb-throw + (fn [ctx res] + (is (= res (step2-fn step1-val))) + (is (= ctx {:step1 step1-val :step2 (step2-fn step1-val)})) (done))))))) @@ -258,18 +436,20 @@ step3-fn str] (fonda/execute {} [{:path [:step1] - :processor (constantly (js/Promise.resolve step1-val))} + :fn (constantly (js/Promise.resolve step1-val))} {:path [:step2] - :processor (fn [{:keys [step1]}] + :fn (fn [_ step1] (step2-fn step1))} {:path [:step3] - :processor (fn [{:keys [step2]}] + :fn (fn [_ step2] (step3-fn step2))}] exception-cb-throw - (fn [res] - (is (= res {:step1 step1-val - :step2 (step2-fn step1-val) - :step3 (-> step1-val (step2-fn) (step3-fn))})) + (fn [ctx step3] + (let [step3-res (-> step1-val (step2-fn) (step3-fn))] + (is (= step3 step3-res)) + (is (= ctx {:step1 step1-val + :step2 (step2-fn step1-val) + :step3 step3-res}))) (done))))))) @@ -281,22 +461,21 @@ (fonda/execute {:anomaly? true :anomaly-handlers {"step2" #(reset! anomaly-handler-arg (:anomaly %))}} [{:path [:step1] - :processor (constantly (js/Promise.resolve 1))} + :fn (constantly (js/Promise.resolve 1))} {:path [:step2] :name "step2" - :processor (constantly unsuccessful-anomaly)} + :fn (constantly unsuccessful-anomaly)} {:path [:step3] - :processor (constantly 3)}] + :fn (constantly 3)}] exception-cb-throw success-cb-throw - (fn [anomaly] + (fn [ctx anomaly] (is (= @anomaly-handler-arg anomaly)) (is (= unsuccessful-anomaly anomaly) "it should call the on-anomaly callback with the anomaly data") (done))))))) - (deftest multiple-steps-on-anomaly-do-not-short-circuit-test (testing "When :anomaly is false and an anomaly occurs it should not call on-anomaly callback neither the anomaly handlers" @@ -306,16 +485,16 @@ (fonda/execute {:anomaly? false :anomaly-handlers {"step2" #(reset! anomaly-handler-arg (:anomaly %))}} [{:path [:step1] - :processor (constantly (js/Promise.resolve 1))} + :fn (constantly (js/Promise.resolve 1))} {:path [:step2] :name "step2" - :processor (constantly successful-anomaly)} + :fn (constantly successful-anomaly)} {:path [:step3] - :processor (constantly 3)}] + :fn (constantly 3)}] exception-cb-throw - (fn [ctx] + (fn [ctx res] (is (nil? @anomaly-handler-arg)) (is (= {:step1 1 :step2 successful-anomaly @@ -325,7 +504,6 @@ (done)) anomaly-cb-throw))))) - (deftest multiple-steps-one-unsuccessful-short-circuits-test (testing "When :anomaly is true and an anomaly occurs" (async done @@ -336,24 +514,58 @@ (fonda/execute {:anomaly? true :anomaly-handlers {"step2" #(reset! anomaly-handler-arg (:anomaly %))}} [{:path [:step1] - :processor (fn [_] + :fn (fn [_] (js/Promise.resolve (swap! step1-counter inc)))} {:path [:step2] :name "step2" - :processor (fn [_] unsuccessful-res)} + :fn (fn [_] unsuccessful-res)} {:path [:step3] - :processor (fn [_] (swap! step3-counter inc))}] + :fn (fn [_] (swap! step3-counter inc))}] exception-cb-throw success-cb-throw - (fn [anomaly] + (fn [ctx anomaly] (is (= @anomaly-handler-arg anomaly)) (is (and (= 1 @step1-counter) (= 0 @step3-counter)) "it should not call the previous but not the subsequent steps") (done))))))) +(deftest multiple-steps-one-not-breaking-anomaly + (testing "When :anomaly is true and an anomaly occurs, and the anomaly is not breaking, it shouldn't short-circuit" + (async done + (let [unsuccessful-res (anomaly :cognitect.anomalies/incorrect) + step1-counter (atom 0) + step3-atom (atom nil) + anomaly-handler-arg (atom nil)] + (fonda/execute {:anomaly? true + :anomaly-handlers {"step2" #(reset! anomaly-handler-arg (:anomaly %))}} + [{:path [:step1] + :fn (fn [_] + (js/Promise.resolve (swap! step1-counter inc)))} + + {:path [:step2] + :name "step2" + :fn (fn [_] unsuccessful-res) + :is-anomaly-error? (fn [a] false #_(not= a unsuccessful-res))} + + {:path [:step3] + :fn (fn [_ a] + (reset! step3-atom a) + "not anomaly result")}] + + exception-cb-throw + (fn [ctx res] + (is (nil? @anomaly-handler-arg) + "The on-anomaly callback should not be called") + (is (= 1 @step1-counter) + "it should have called the previous step") + (is (= unsuccessful-res @step3-atom) + "it should also call the step after the not-error anomaly with the anomaly") + (done)) + anomaly-cb-throw))))) + (deftest multiple-steps-one-exceptional-calls-on-exception-test (testing "When an exception occurs" (async done @@ -361,16 +573,16 @@ exception-handler-arg (atom nil)] (fonda/execute {:exception-handlers {"step2" #(reset! exception-handler-arg (:exception %))}} [{:path [:step1] - :processor (fn [_] + :fn (fn [_] (js/Promise.resolve 1))} {:path [:step2] :name "step2" - :processor (fn [_] (throw exception))} + :fn (fn [_] (throw exception))} {:path [:step3] - :processor (fn [_] 1)}] - (fn [err] + :fn (fn [_] 1)}] + (fn [ctx err] (is (= @exception-handler-arg exception)) (is (= exception err) "it should call on-exception passing the js/Error") (done)) @@ -384,15 +596,15 @@ step3-counter (atom 0)] (fonda/execute {} [{:path [:step1] - :processor (fn [_] + :fn (fn [_] (js/Promise.resolve (swap! step1-counter inc)))} {:path [:step2] - :processor (fn [_] (js/Promise.reject exception))} + :fn (fn [_] (js/Promise.reject exception))} {:path [:step3] - :processor (fn [_] (swap! step3-counter inc))}] - (fn [err] + :fn (fn [_] (swap! step3-counter inc))}] + (fn [ctx err] (is (and (= 1 @step1-counter) (= 0 @step3-counter)) "it should not call the previous but not the subsequent steps") @@ -407,14 +619,14 @@ step3-counter (atom 0)] (fonda/execute {} [{:path [:step1] - :processor (fn [_] + :fn (fn [_] (js/Promise.resolve (swap! step1-counter inc)))} {:tap (fn [_] (js/Promise.reject exception))} {:path [:step3] - :processor (fn [_] (swap! step3-counter inc))}] - (fn [err] + :fn (fn [_] (swap! step3-counter inc))}] + (fn [ctx err] (is (and (= 1 @step1-counter) (= 0 @step3-counter)) "it should not call the previous but not the subsequent steps") @@ -424,79 +636,216 @@ (deftest injected-step-should-run-after-injector (testing "Injecting one step should add the step after the injector" (async done - (fonda/execute {:initial-ctx {:steps []}} + (fonda/execute {:ctx {:steps []}} [{:path [:steps] :name "processor1" - :processor (fn [{:keys [steps]}] + :fn (fn [{:keys [steps]}] (conj steps :step1))} {:inject (fn [_] {:path [:steps] :name "injected-step" - :processor (fn [{:keys [steps]}] + :fn (fn [{:keys [steps]} _] (conj steps :injected-step))}) :name "injector1"} {:path [:steps] :name "processor2" - :processor (fn [{:keys [steps]}] + :fn (fn [{:keys [steps]} _] (conj steps :step2))}] exception-cb-throw - (fn [res] (is (= res {:steps [:step1 :injected-step :step2]})) (done)) + (fn [ctx res] (is (= ctx {:steps [:step1 :injected-step :step2]})) (done)) + anomaly-cb-throw)))) + +(deftest injector-should-receive-prev-step-res-and-ctx + (testing "Injecting one step should add the step after the injector" + (async done + (fonda/execute {:ctx {:steps []}} + [{:path [:steps] + :name "processor1" + :fn (fn [{:keys [steps]}] + (conj steps :step1))} + {:inject (fn [ctx step1] + (is (= step1 [:step1])) + (is (= ctx {:steps [:step1]})) + {:path [:steps] + :name "injected-step" + :fn (fn [{:keys [steps]} _] + (conj steps :injected-step))}) + :name "injector1"} + {:path [:steps] + :name "processor2" + :fn (fn [{:keys [steps]} _] + (conj steps :step2))}] + exception-cb-throw + (fn [ctx res] (is (= ctx {:steps [:step1 :injected-step :step2]})) (done)) anomaly-cb-throw)))) (deftest injected-steps-should-run-after-injector (testing "Injecting multiple steps should add the steps after the injector" (async done - (fonda/execute {:initial-ctx {:steps []}} + (fonda/execute {:ctx {:steps []}} [{:path [:steps] :name "processor1" - :processor (fn [{:keys [steps]}] + :fn (fn [{:keys [steps]}] (conj steps :step1))} {:inject (fn [_] [{:path [:steps] :name "injected-step1" - :processor (fn [{:keys [steps]}] + :fn (fn [{:keys [steps]} _] (conj steps :injected-step1))} {:path [:steps] :name "injected-step2" - :processor (fn [{:keys [steps]}] + :fn (fn [{:keys [steps]} _] (conj steps :injected-step2))}]) :name "injector1"} {:path [:steps] :name "processor2" - :processor (fn [{:keys [steps]}] + :fn (fn [{:keys [steps]} _] (conj steps :step2))}] exception-cb-throw - (fn [res] (is (= res {:steps [:step1 :injected-step1 :injected-step2 :step2]})) (done)) + (fn [ctx res] (is (= ctx {:steps [:step1 :injected-step1 :injected-step2 :step2]})) (done)) + anomaly-cb-throw)))) + +(deftest injected-steps-using-prev-step-res-should-run-after-injector + (testing "Injecting multiple steps should add the steps after the injector" + (async done + (fonda/execute {:ctx {:steps []}} + [{:name "processor1" + :fn (fn [{:keys [steps]}] + (conj steps :step1))} + {:inject (fn [_] + [{:name "injected-step1" + :fn (fn [_ steps] + (conj steps :injected-step1))} + {:name "injected-step2" + :fn (fn [_ steps] + (conj steps :injected-step2))}]) + :name "injector1"} + {:name "processor2" + :fn (fn [_ steps] + (conj steps :step2))}] + exception-cb-throw + (fn [_ steps] + (is (= steps [:step1 :injected-step1 :injected-step2 :step2])) (done)) + anomaly-cb-throw)))) + +(deftest injected-mocked-steps-should-run-after-injector + (testing "Injected steps are also mocked when injecting multiple steps" + (async done + (fonda/execute {:ctx {:steps []} + :mock-fns {:injected-step1 (fn [{:keys [steps]} _] (conj steps :mocked-injected-step1)) + :injected-step2 (fn [{:keys [steps]} _] (conj steps :mocked-injected-step2))}} + [{:path [:steps] + :name "processor1" + :fn (fn [{:keys [steps]}] + (conj steps :step1))} + {:inject (fn [_] + [{:path [:steps] + :name "injected-step1" + :fn (fn [{:keys [steps]} _] + (conj steps :injected-step1))} + {:path [:steps] + :name "injected-step2" + :fn (fn [{:keys [steps]} _] + (conj steps :injected-step2))}]) + :name "injector1"} + {:path [:steps] + :name "processor2" + :fn (fn [{:keys [steps]} _] + (conj steps :step2))}] + exception-cb-throw + (fn [ctx res] (is (= ctx {:steps [:step1 :mocked-injected-step1 :mocked-injected-step2 :step2]})) (done)) anomaly-cb-throw)))) (deftest lonely-injector-with-one-step (testing "Only one injector on the steps" (async done - (fonda/execute {:initial-ctx {:steps []}} + (fonda/execute {:ctx {:steps []}} [{:inject (fn [_] {:path [:steps] :name "injected-step" - :processor (fn [{:keys [steps]}] - (conj steps :injected-step))}) + :fn (fn [{:keys [steps]}] + (conj steps :injected-step))}) :name "injector1"}] exception-cb-throw - (fn [res] (is (= res {:steps [:injected-step]})) (done)) + (fn [ctx res] (is (= ctx {:steps [:injected-step]})) (done)) anomaly-cb-throw)))) (deftest lonely-injector-with-multiple-steps (testing "Only one injector on the steps" (async done - (fonda/execute {:initial-ctx {:steps []}} + (fonda/execute {:ctx {:steps []}} [{:inject (fn [_] - [{:path [:steps] - :name "injected-step1" - :processor (fn [{:keys [steps]}] - (conj steps :injected-step1))} - {:path [:steps] - :name "injected-step2" - :processor (fn [{:keys [steps]}] - (conj steps :injected-step2))}]) + [{:path [:steps] + :name "injected-step1" + :fn (fn [{:keys [steps]}] + (conj steps :injected-step1))} + {:path [:steps] + :name "injected-step2" + :fn (fn [{:keys [steps]} _] + (conj steps :injected-step2))}]) :name "injector1"}] exception-cb-throw - (fn [res] (is (= res {:steps [:injected-step1 :injected-step2]})) (done)) - anomaly-cb-throw)))) \ No newline at end of file + (fn [ctx res] + (is (= ctx {:steps [:injected-step1 :injected-step2]})) + (done)) + anomaly-cb-throw)))) + +(deftest callbacks-wrapper-fn-global-on-success + (testing "If a callback wrapper is passed, it should be called with the value of the on-callbacks and the arguments + that normally would be passsed to the on-callback-fns." + (async done + (let [processor-res 42 + on-success-event ::on-success-event + processor-path [:success-processor-path] + processor {:on-success (cb-with-result ::auth-success) + :path processor-path + :fn (constantly (js/Promise.resolve processor-res))} + ] + (fonda/execute {:callbacks-wrapper-fn (fn [cb-val ctx step-res] + (is (= cb-val on-success-event)) + (is (= processor-res (get-in ctx processor-path) step-res)) + (is (= @events [[::auth-success processor-res {(first processor-path) processor-res}]])) + (done))} + [processor] + exception-cb-throw + on-success-event))))) + +(deftest callbacks-wrapper-fn-global-on-anomaly + (testing "If a callback wrapper is passed, it should be called with the value of the on-callbacks and the arguments + that normally would be passsed to the on-callback-fns." + (async done + (let [processor-anomaly (anomaly :cognitect.anomalies/incorrect) + on-anomaly-event ::on-anomaly-event + processor-path [:anomaly-processor-path] + processor {:on-error (cb-with-result ::anomaly-event) + :path processor-path + :fn (constantly processor-anomaly)}] + (fonda/execute {:callbacks-wrapper-fn (fn [cb-val ctx anomaly-res] + (is (= cb-val on-anomaly-event)) + (is (= anomaly-res processor-anomaly)) + (is (= @events [[::anomaly-event anomaly-res {(first processor-path) anomaly-res}]])) + (done)) + :anomaly? true} + [processor] + exception-cb-throw + success-cb-throw + on-anomaly-event))))) + +(deftest callbacks-wrapper-fn-global-on-exception + (testing "If a callback wrapper is passed, it should be called with the value of the on-callbacks and the arguments + that normally would be passsed to the on-callback-fns." + (async done + (let [processor-exception (js/Error "Bad exception") + on-exception-event ::on-exception-event + processor-path [:exception-processor-path] + processor {:on-error (cb-with-result ::exception-event) + :path processor-path + :fn (fn [_] (throw processor-exception))}] + (fonda/execute {:callbacks-wrapper-fn (fn [cb-val ctx exception-res] + (is (= cb-val on-exception-event)) + (is (= exception-res processor-exception)) + (is (= @events [[::exception-event exception-res {}]])) + (done))} + [processor] + on-exception-event + success-cb-throw))))) \ No newline at end of file diff --git a/test/fonda/step_test.cljs b/test/fonda/step_test.cljs index 26852d9..3f97ac1 100644 --- a/test/fonda/step_test.cljs +++ b/test/fonda/step_test.cljs @@ -7,10 +7,10 @@ (orchestra/instrument) (deftest processor-step-test - (let [step (step/step->record {:processor :cljs.core/inc + (let [step (step/step->record {:fn :cljs.core/inc :path [:test]})] (is (record? step) "the step should be a record") - (is (fn? (:processor step)) "the :processor key should become a function"))) + (is (fn? (:fn step)) "the :fn key should become a function"))) (deftest tap-step-test (let [step (step/step->record {:tap :cljs.core/println From d34d6a34050be3b8592678aa4c10c9f17dbbd96d Mon Sep 17 00:00:00 2001 From: David Cerezo Inigo Date: Fri, 10 Apr 2020 12:46:36 -0700 Subject: [PATCH 2/9] Steps don't receive previous step result anymore - Tests fixed accordingly --- README.md | 21 ++++++------- src/fonda/execute.cljs | 6 ++-- test/fonda/core_test.cljs | 64 ++++++++------------------------------- 3 files changed, 25 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 0513956..f087972 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ An async pipeline approach to functional core - imperative shell from by Gary Be ### 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 +The values should be functions with the signature `(fn [ctx]`. 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 @@ -18,9 +18,8 @@ As described above, if a `callbacks-wrapper-fn` function is provided on the conf 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 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. +### Success callback functions signature changed to receive the result from the last step +The first argument remains the same - the context - and the second argument is the result of the last step executed. ### 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 @@ -61,12 +60,12 @@ This example illustrates `fonda`'s basic mechanics: :path [:github-things]}] ;; on-exception - (fn [exception] + (fn [ctx exception] (handle-exception exception)) ;; on-success - (fn [res ctx] - (handle-success (:github-things res)))) + (fn [ctx last-step-res] + (handle-success (:github-things last-step-res)))) ``` *HINT*: The parameter order makes it easy to partially apply `execute` for leaner call sites. @@ -142,7 +141,7 @@ The following section describes the parameters `fonda/execute` accepts. - **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. +- [Optional] **on-anomaly** Function with the signature `(fn [ctx anomaly])` called in case of anomaly with the context and the anomaly data itself. ## Full Example @@ -196,15 +195,15 @@ The following section describes the parameters `fonda/execute` accepts. (ajax/POST url remote-thing-params))}))))}] ;; on-exception - (fn [exception] + (fn [ctx exception] (handle-exception exception)) ;; on-success - (fn [{:keys [remote-thing-processed]}] + (fn [{:keys [remote-thing-processed]} _] (handle-success remote-thing-processed)) ;; on-anomaly - (fn [anomaly] + (fn [ctx anomaly] (handle-anomaly anomaly))) ``` diff --git a/src/fonda/execute.cljs b/src/fonda/execute.cljs index 147732e..7f734e7 100644 --- a/src/fonda/execute.cljs +++ b/src/fonda/execute.cljs @@ -95,10 +95,8 @@ ;; Calls the on-start callback with the context (when on-start ((get-callback-fn fonda-ctx on-start) ctx)) (try - (let [last-res (last processor-results-stack) - + (let [ ;; First step only gets the ctx, next ones receive last-result,ctx - args (if (empty? processor-results-stack) [ctx] [ctx last-res]) ; fn is an alias for processor processor (or processor (:fn step)) @@ -106,7 +104,7 @@ ;; If there is a mocked-fn with the same name, it will used the mocked-fn instead mocked-fn (when name (get mock-fns (keyword name))) f (or mocked-fn processor tap inject) - res (apply f args) + res (f ctx) assoc-result-fn* (cond tap (partial assoc-tap-result fonda-ctx) processor (partial assoc-processor-result fonda-ctx step) diff --git a/test/fonda/core_test.cljs b/test/fonda/core_test.cljs index 01050a8..3320d54 100644 --- a/test/fonda/core_test.cljs +++ b/test/fonda/core_test.cljs @@ -133,21 +133,6 @@ exception-cb-throw (fn [_])))))) -(deftest one-successful-sync-tap-as-second-step-prev-res-is-passed - (testing "Passing one synchronous tap should call on-success with the initial context" - (async done - (let [initial {:foo :bar} - prev-step-res 42 - prev-step {:fn (constantly prev-step-res)} - tap {:tap (fn [ctx step-res] - (is (= step-res prev-step-res)) - (is (= ctx initial)) - (done))}] - (fonda/execute {:ctx initial} - [prev-step tap] - exception-cb-throw - (fn [_])))))) - (deftest one-successful-mocked-sync-tap-is-passed-the-context (testing "Passing one synchronous tap should call on-success with the initial context" (async done @@ -365,16 +350,17 @@ :step2 (step2-fn step1-val)})) (done))))))) -(deftest multiple-successful-synchronous-steps-using-prev-step-result-test +(deftest multiple-successful-synchronous-steps-success-receives-last-step-result-test (testing "Passing multiple successful synchronous steps should call the on-success callback with the result of the last step" (async done (let [step1-val 1 step2-fn inc] (fonda/execute {} [{:path [:step1] :fn (constantly step1-val)} - {:path [:step2] :fn (fn [_ step1] (step2-fn step1))}] + {:path [:step2] :fn (fn [{:keys [step1]}] (step2-fn step1))}] exception-cb-throw (fn [_ step2] + (println "step2:" step2) (is (= step2 (step2-fn step1-val))) (done))))))) @@ -386,7 +372,7 @@ (fonda/execute {} [{:path [:step1] :fn (constantly step1-val)} {:tap (constantly "tap-res")} - {:path [:step2] :fn (fn [_ step1] (step2-fn step1))}] + {:path [:step2] :fn (fn [{:keys [step1]}] (step2-fn step1))}] exception-cb-throw (fn [_ step2] (is (= step2 (step2-fn step1-val))) @@ -418,7 +404,7 @@ [{:path [:step1] :fn (constantly (js/Promise.resolve step1-val))} {:path [:step2] - :fn (fn [_ step1] + :fn (fn [{:keys [step1]}] (js/Promise.resolve (step2-fn step1)))}] exception-cb-throw (fn [ctx res] @@ -438,10 +424,10 @@ [{:path [:step1] :fn (constantly (js/Promise.resolve step1-val))} {:path [:step2] - :fn (fn [_ step1] + :fn (fn [{:keys [step1]}] (step2-fn step1))} {:path [:step3] - :fn (fn [_ step2] + :fn (fn [{:keys [step2]}] (step3-fn step2))}] exception-cb-throw (fn [ctx step3] @@ -548,11 +534,11 @@ {:path [:step2] :name "step2" :fn (fn [_] unsuccessful-res) - :is-anomaly-error? (fn [a] false #_(not= a unsuccessful-res))} + :is-anomaly-error? (fn [a] false)} {:path [:step3] - :fn (fn [_ a] - (reset! step3-atom a) + :fn (fn [{:keys [step2]}] + (reset! step3-atom step2) "not anomaly result")}] exception-cb-throw @@ -655,7 +641,7 @@ (fn [ctx res] (is (= ctx {:steps [:step1 :injected-step :step2]})) (done)) anomaly-cb-throw)))) -(deftest injector-should-receive-prev-step-res-and-ctx +(deftest injector-should-receive-ctx (testing "Injecting one step should add the step after the injector" (async done (fonda/execute {:ctx {:steps []}} @@ -663,8 +649,7 @@ :name "processor1" :fn (fn [{:keys [steps]}] (conj steps :step1))} - {:inject (fn [ctx step1] - (is (= step1 [:step1])) + {:inject (fn [ctx] (is (= ctx {:steps [:step1]})) {:path [:steps] :name "injected-step" @@ -676,7 +661,7 @@ :fn (fn [{:keys [steps]} _] (conj steps :step2))}] exception-cb-throw - (fn [ctx res] (is (= ctx {:steps [:step1 :injected-step :step2]})) (done)) + (fn [ctx] (is (= ctx {:steps [:step1 :injected-step :step2]})) (done)) anomaly-cb-throw)))) (deftest injected-steps-should-run-after-injector @@ -705,29 +690,6 @@ (fn [ctx res] (is (= ctx {:steps [:step1 :injected-step1 :injected-step2 :step2]})) (done)) anomaly-cb-throw)))) -(deftest injected-steps-using-prev-step-res-should-run-after-injector - (testing "Injecting multiple steps should add the steps after the injector" - (async done - (fonda/execute {:ctx {:steps []}} - [{:name "processor1" - :fn (fn [{:keys [steps]}] - (conj steps :step1))} - {:inject (fn [_] - [{:name "injected-step1" - :fn (fn [_ steps] - (conj steps :injected-step1))} - {:name "injected-step2" - :fn (fn [_ steps] - (conj steps :injected-step2))}]) - :name "injector1"} - {:name "processor2" - :fn (fn [_ steps] - (conj steps :step2))}] - exception-cb-throw - (fn [_ steps] - (is (= steps [:step1 :injected-step1 :injected-step2 :step2])) (done)) - anomaly-cb-throw)))) - (deftest injected-mocked-steps-should-run-after-injector (testing "Injected steps are also mocked when injecting multiple steps" (async done From 0a2c1a6774b5dd04899c4c0512c0a5dcc4af45d8 Mon Sep 17 00:00:00 2001 From: David Cerezo Inigo Date: Fri, 10 Apr 2020 12:47:14 -0700 Subject: [PATCH 3/9] Fixed assoc-injector-result signature spec to receive a nilable sequence of steps --- src/fonda/execute/specs.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fonda/execute/specs.cljc b/src/fonda/execute/specs.cljc index ee58937..6b01466 100644 --- a/src/fonda/execute/specs.cljc +++ b/src/fonda/execute/specs.cljc @@ -74,4 +74,4 @@ (s/fdef fonda.execute/assoc-injector-result :args (s/cat :fonda-ctx ::fonda-context - :res (s/or :step ::core/step :steps ::core/steps))) + :res (s/nilable (s/or :step ::core/step :steps ::core/steps)))) From 42c025c9cb97e4b31a627ddae8e9acd056668749 Mon Sep 17 00:00:00 2001 From: Andrea Richiardi Date: Sat, 11 Apr 2020 18:18:09 -0700 Subject: [PATCH 4/9] Handle corner case when :inject step returns nil --- src/fonda/execute.cljs | 5 ++++- test/fonda/core_test.cljs | 12 +++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/fonda/execute.cljs b/src/fonda/execute.cljs index 7f734e7..78883b9 100644 --- a/src/fonda/execute.cljs +++ b/src/fonda/execute.cljs @@ -50,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] diff --git a/test/fonda/core_test.cljs b/test/fonda/core_test.cljs index 3320d54..9b0d74c 100644 --- a/test/fonda/core_test.cljs +++ b/test/fonda/core_test.cljs @@ -726,12 +726,22 @@ {:path [:steps] :name "injected-step" :fn (fn [{:keys [steps]}] - (conj steps :injected-step))}) + (conj steps :injected-step))}) :name "injector1"}] exception-cb-throw (fn [ctx res] (is (= ctx {:steps [:injected-step]})) (done)) anomaly-cb-throw)))) +(deftest lonely-injector-returning-nil + (testing "If the injector doesn't return any value, it should not inject anything" + (async done + (fonda/execute {:ctx {:steps []}} + [{:inject (fn [_]) + :name "injector1"}] + exception-cb-throw + (fn [ctx res] (is (= ctx {:steps []})) (done)) + anomaly-cb-throw)))) + (deftest lonely-injector-with-multiple-steps (testing "Only one injector on the steps" (async done From 2d226435a20b648a1dbb3538f4d122fc6190466e Mon Sep 17 00:00:00 2001 From: Andrea Richiardi Date: Sat, 11 Apr 2020 18:21:11 -0700 Subject: [PATCH 5/9] Remove a couple of spurious println --- src/fonda/execute.cljs | 1 - test/fonda/core_test.cljs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/fonda/execute.cljs b/src/fonda/execute.cljs index 78883b9..41c1c27 100644 --- a/src/fonda/execute.cljs +++ b/src/fonda/execute.cljs @@ -65,7 +65,6 @@ [{:as fonda-ctx :keys [ctx]} {:keys [on-error] :as step} e] (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 diff --git a/test/fonda/core_test.cljs b/test/fonda/core_test.cljs index 9b0d74c..bbf963f 100644 --- a/test/fonda/core_test.cljs +++ b/test/fonda/core_test.cljs @@ -360,7 +360,6 @@ {:path [:step2] :fn (fn [{:keys [step1]}] (step2-fn step1))}] exception-cb-throw (fn [_ step2] - (println "step2:" step2) (is (= step2 (step2-fn step1-val))) (done))))))) From 04fc613804c00e64c25ef1df44797aaf5454dc33 Mon Sep 17 00:00:00 2001 From: Andrea Richiardi Date: Sun, 12 Apr 2020 19:31:21 -0700 Subject: [PATCH 6/9] Extract processing functions out of the try/catch in try-step --- src/fonda/execute.cljs | 45 ++++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/src/fonda/execute.cljs b/src/fonda/execute.cljs index 41c1c27..6b4c40f 100644 --- a/src/fonda/execute.cljs +++ b/src/fonda/execute.cljs @@ -92,34 +92,27 @@ If an exception gets triggerd, an exception is added on the context. If an anomaly is returned, an anomaly is added to the context" [{:as fonda-ctx :keys [ctx mock-fns processor-results-stack]} - {:as step :keys [processor tap inject name on-start]}] + {:as step :keys [processor fn tap inject name on-start]}] ;; Calls the on-start callback with the context - (when on-start ((get-callback-fn fonda-ctx on-start) ctx)) - (try - (let [ - ;; First step only gets the ctx, next ones receive last-result,ctx - - ; fn is an alias for processor - processor (or processor (:fn step)) - - ;; If there is a mocked-fn with the same name, it will used the mocked-fn instead - mocked-fn (when name (get mock-fns (keyword name))) - f (or mocked-fn processor tap inject) - res (f ctx) - 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)) - 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))) - - (catch :default e - (handle-exception fonda-ctx step e)))) + (when on-start + ((get-callback-fn fonda-ctx on-start) ctx)) + + (let [mocked-fn (when name (get mock-fns (keyword name))) ;; we use a mocked-fn with the same name if there + f (or mocked-fn processor fn tap inject) + assoc-result-fn #(do + (invoke-post-callback-fns fonda-ctx step %) + (cond + tap (assoc-tap-result fonda-ctx %) + (or processor fn) (assoc-processor-result fonda-ctx step %) + inject (assoc-injector-result fonda-ctx %)))] + (try + (let [res (f ctx)] + (if (a/async? res) + (a/continue res assoc-result-fn #(handle-exception fonda-ctx step %)) + (assoc-result-fn res))) + (catch :default e + (handle-exception fonda-ctx step e))))) (defn- deliver-result "Calls a callback depending on what is on the context. From 6c2580a5a571ba3ea2b0095dfaf568954db24ed1 Mon Sep 17 00:00:00 2001 From: Andrea Richiardi Date: Sun, 12 Apr 2020 19:31:57 -0700 Subject: [PATCH 7/9] Make the simple example actually work It can now be executed with yarn shadow-cljs compile simple-example and node dist/simple-example.js. --- deps.edn | 3 +- examples/fonda/simple.cljs | 62 ++++++++++++++++++++------------------ shadow-cljs.edn | 9 ++++-- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/deps.edn b/deps.edn index 42fe626..99882dd 100644 --- a/deps.edn +++ b/deps.edn @@ -5,4 +5,5 @@ expound {:mvn/version "0.7.2"} org.clojure/test.check {:mvn/version "0.10.0-alpha3" :exclusions [org.clojure/clojure]} orchestra {:mvn/version "2018.12.06-2"} - thheller/shadow-cljs {:mvn/version "2.8.59"}}}}} \ No newline at end of file + thheller/shadow-cljs {:mvn/version "2.8.59"}}} + :example {:extra-paths ["examples"]}}} diff --git a/examples/fonda/simple.cljs b/examples/fonda/simple.cljs index 8a35b88..8057b91 100644 --- a/examples/fonda/simple.cljs +++ b/examples/fonda/simple.cljs @@ -1,7 +1,12 @@ -(ns fonda.example.simple +(ns fonda.simple (:require [clojure.string :as str] [fonda.core :as fonda] - [goog.object :as gobj])) + [fonda.core.specs] + [goog.object :as gobj] + [orchestra-cljs.spec.test :as orchestra] + [process])) + +(orchestra/instrument) (defn response->loads-of-data [ctx] @@ -15,31 +20,28 @@ (str/split dd "-") (str/join " " dd))) -(fonda/execute - {} - - [{:processor (fn [ctx] - {:status 200 - :headers {"Content-Type" "application/json"} - :body {:data :loads-of}}) - :name "get-loads-of-data" - :path [:http-response]} - - {:processor response->loads-of-data ;; Pure function - ctx in - ctx out - :name "response-processor" - :path [:data]}] - - {:data :very-little} - - ;; on-success - (fn [ctx] - (println "Before we had" (-> ctx :data :before to-str) "data," - "but now we have" (-> ctx :data :after to-str) "it.")) - - ;; on-anomaly - (fn [anomaly] - (println "Anomaly detected in the matrix")) - - ;; on-exception - (fn [exception] - (println (gobj/get exception "message")))) +(defn main [& cli-args] + (fonda/execute + {:ctx {:data :very-little}} + + [{:processor (fn [ctx] + {:status 200 + :headers {"Content-Type" "application/json"} + :body {:data :loads-of}}) + :name "get-loads-of-data" + :path [:http-response]} + + {:processor response->loads-of-data ;; Pure function - ctx in - ctx out + :name "response-processor" + :path [:data]}] + + ;; on-exception + (fn [ctx exception] + (println (gobj/get exception "message")) + (process/exit 1)) + + ;; on-success + (fn [ctx last-step-result] + (println "Before we had" (-> ctx :data :before to-str) "data," + "but now we have" (-> ctx :data :after to-str) "it.") + (process/exit 0)))) diff --git a/shadow-cljs.edn b/shadow-cljs.edn index c81871c..c050fc0 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -1,4 +1,4 @@ -{:deps {:aliases [:dev :test]} +{:deps {:aliases [:dev :test :example]} :builds {:lib {:target :node-library :output-to "dist/fonda.js" :exports {:execute fonda.core/execute} @@ -18,4 +18,9 @@ :pretty-print true :source-map true :elide-asserts false} - :autorun false}}}} + :autorun false}} + + :simple-example {:target :node-script + :main fonda.simple/main + :output-to "dist/simple-example.js" + :devtools {:preloads [fonda.test-preload]}}}} From f479a19846689551211ed38dae46349702c39fba Mon Sep 17 00:00:00 2001 From: Andrea Richiardi Date: Sun, 12 Apr 2020 19:35:45 -0700 Subject: [PATCH 8/9] Drop keyword only constraint for :name in execute specs and refactor A couple of changes here. First of all the name specs in execute and core are now the same and both allow keyword and string for :name keys. Secondly, we refactor the specs so that including fonda.core.specs actually adds everything to the registry. Finally we fix the tests and issues that were detected with the change. --- src/fonda/core/specs.cljc | 31 ++++++----------------- src/fonda/execute/specs.cljc | 48 +++++++++++++++++++++++++----------- src/fonda/step/specs.cljc | 14 ++++++++--- test/fonda/core_test.cljs | 31 +++++++++++------------ 4 files changed, 64 insertions(+), 60 deletions(-) diff --git a/src/fonda/core/specs.cljc b/src/fonda/core/specs.cljc index bf0f23a..ff143c3 100644 --- a/src/fonda/core/specs.cljc +++ b/src/fonda/core/specs.cljc @@ -1,36 +1,19 @@ (ns fonda.core.specs (:require [clojure.spec.alpha :as s] + [fonda.execute.specs :as execute] [fonda.step.specs :as step])) -(def name-s (s/or :string string? - :keyword keyword?)) -(s/def ::name (s/nilable name-s)) - ;; handler-maps keys in fonda.core can be either strings or keywords -(s/def ::handlers-map (s/map-of ::name fn?)) +(s/def ::handlers-map (s/map-of ::execute/name fn?)) ;; Config (s/def ::anomaly? (s/or :boolean boolean? :predicate fn?)) -(s/def ::mock-fns (s/map-of name-s fn?)) +(s/def ::mock-fns (s/map-of ::execute/str-or-kw fn?)) (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 some?) -(s/def ::on-exception some?) -(s/def ::on-anomaly (s/nilable any?)) - -(s/def ::step-name-map - (s/keys :opt-un [::name])) - -(s/def ::step - (s/or :tap (s/merge ::step/tap-step ::step-name-map) - :processor (s/merge ::step/processor-step ::step-name-map) - :injector (s/merge ::step/injector-step ::step-name-map))) - -(s/def ::steps (s/coll-of ::step)) - (s/def ::config (s/keys :opt-un [::anomaly? ::mock-fns @@ -41,7 +24,7 @@ (s/fdef fonda.core/execute :args (s/cat :config ::config - :steps ::steps - :on-exception ::on-exception - :on-success ::on-success - :on-anomaly (s/? ::on-anomaly))) + :steps (s/spec (s/* ::execute/step)) + :on-exception ::execute/on-exception + :on-success ::execute/on-success + :on-anomaly (s/? ::execute/on-anomaly))) diff --git a/src/fonda/execute/specs.cljc b/src/fonda/execute/specs.cljc index 6b01466..0069d81 100644 --- a/src/fonda/execute/specs.cljc +++ b/src/fonda/execute/specs.cljc @@ -1,13 +1,11 @@ (ns fonda.execute.specs (:require [clojure.spec.alpha :as s] - [fonda.step.specs :as step] - [fonda.core.specs :as core])) + [fonda.step.specs :as step])) (s/def ::js-error #(instance? js/Error %)) -;; this namespace has a different take on the step name, we duplicate waiting -;; for spec2 to save us -(s/def ::name (s/nilable keyword?)) +(s/def ::str-or-kw (s/or :string string? :keyword keyword?)) +(s/def ::name (s/nilable ::str-or-kw)) (s/def ::step-name-map (s/keys :opt-un [::name])) @@ -17,32 +15,34 @@ :processor (s/merge ::step/processor-step ::step-name-map) :injector (s/merge ::step/injector-step ::step-name-map))) -(s/def ::steps (s/coll-of ::step)) - ;; handler-maps keys in fonda.execute can only be keywords -(s/def ::handlers-map (s/nilable (s/map-of ::step/name fn?))) +(s/def ::handlers-map (s/nilable (s/map-of keyword? fn?))) (s/def ::anomaly-handlers ::handlers-map) (s/def ::exception-handlers ::handlers-map) -(s/def ::queue ::steps) -(s/def ::stack ::steps) +(s/def ::queue (s/* ::step)) +(s/def ::stack (s/* ::step)) ;; the following are all required but nilable we use a record as FondaContext. (s/def ::anomaly-fn (s/nilable fn?)) (s/def ::exception (s/nilable ::js-error)) (s/def ::anomaly (s/nilable any?)) +(s/def ::on-success some?) +(s/def ::on-exception some?) +(s/def ::on-anomaly (s/nilable any?)) + (s/def ::fonda-context-async some?) (s/def ::fonda-context-sync - (s/keys :req-un [::core/ctx - ::core/on-success - ::core/on-exception + (s/keys :req-un [::ctx + ::on-success + ::on-exception ::queue ::stack] :opt-un [::anomaly-handlers ::exception-handlers - ::core/on-anomaly + ::on-anomaly ::anomaly-fn ::exception ::anomaly])) @@ -50,6 +50,21 @@ (s/def ::fonda-context (s/or :async ::fonda-context-async :sync ::fonda-context-sync)) +(s/def ::config + (s/keys :req-un [::step/steps + ::on-success + ::on-exception + ::on-anomaly] + :opt-un [::anomaly? + ::mock-fns + ::ctx + ::anomaly-handlers + ::exception-handlers + ::callbacks-wrapper-fn])) + +(s/fdef fonda.execute/fonda-context + :args (s/cat :config ::config)) + (s/fdef fonda.execute/execute-steps :args (s/cat :fonda-ctx ::fonda-context)) @@ -72,6 +87,9 @@ :args (s/cat :fonda-ctx ::fonda-context :res any?)) +(s/def ::injected-steps + (s/or :step ::step ::step-seq (s/* ::step))) + (s/fdef fonda.execute/assoc-injector-result :args (s/cat :fonda-ctx ::fonda-context - :res (s/nilable (s/or :step ::core/step :steps ::core/steps)))) + :res ::injected-steps)) diff --git a/src/fonda/step/specs.cljc b/src/fonda/step/specs.cljc index 563f081..9b0dd8b 100644 --- a/src/fonda/step/specs.cljc +++ b/src/fonda/step/specs.cljc @@ -11,19 +11,25 @@ (s/def ::tap (s/or :function fn? :qualified-keyword qualified-keyword?)) (s/def ::tap-step (s/keys :req-un [::tap] - :opt-un [::on-start ::on-success ::on-error ::on-complete])) + :opt-un [::on-start + ::on-success + ::on-error + ::on-complete])) ;; Processor step (s/def ::path (s/nilable vector?)) (s/def ::fn (s/or :function fn? :qualified-keyword qualified-keyword?)) -(s/def ::processor-step - (s/keys :req-un [::fn] - :opt-un [::path +(s/def ::processor (s/or :function fn? :qualified-keyword qualified-keyword?)) +(s/def ::processor-common + (s/keys :opt-un [::path ::on-start ::on-success ::on-error ::on-complete ::is-anomaly-error?])) +(s/def ::processor-step + (s/or :processor-legacy (s/merge ::processor-common (s/keys :req-un [::processor])) + :processor-fn (s/merge ::processor-common (s/keys :req-un [::fn])))) ;; Injector step (s/def ::inject (s/or :function fn? :qualified-keyword qualified-keyword?)) diff --git a/test/fonda/core_test.cljs b/test/fonda/core_test.cljs index bbf963f..571b359 100644 --- a/test/fonda/core_test.cljs +++ b/test/fonda/core_test.cljs @@ -2,7 +2,6 @@ (:require [cljs.test :refer-macros [deftest is testing async use-fixtures]] [fonda.core :as fonda] [fonda.core.specs] - [fonda.execute.specs] [orchestra-cljs.spec.test :as orchestra])) (orchestra/instrument) @@ -187,7 +186,7 @@ :on-error (cb-with-result ::error)} anomaly-handler-arg (atom nil)] (fonda/execute {:anomaly? true - :anomaly-handlers {"step1" #(reset! anomaly-handler-arg (:anomaly %))}} + :anomaly-handlers {:step1 #(reset! anomaly-handler-arg (:anomaly %))}} [processor] exception-cb-throw (fn [_]) @@ -205,7 +204,7 @@ :fn (constantly processor-res)} anomaly-handler-arg (atom nil)] (fonda/execute {:anomaly? true - :anomaly-handlers {"step1" #(reset! anomaly-handler-arg (:anomaly %))}} + :anomaly-handlers {:step1 #(reset! anomaly-handler-arg (:anomaly %))}} [processor] exception-cb-throw (fn [_]) @@ -223,7 +222,7 @@ :fn (constantly processor-res)} anomaly-handler-arg (atom nil)] (fonda/execute {:anomaly? true - :anomaly-handlers {"step1" #(reset! anomaly-handler-arg (:anomaly %))} + :anomaly-handlers {:step1 #(reset! anomaly-handler-arg (:anomaly %))} :mock-fns {:step1 (constantly mocked-processor-res)}} [processor] exception-cb-throw @@ -242,7 +241,7 @@ :tap (constantly tap-res)} anomaly-handler-arg (atom nil)] (fonda/execute {:anomaly? true - :anomaly-handlers {"step1" #(reset! anomaly-handler-arg (:anomaly %))}} + :anomaly-handlers {:step1 #(reset! anomaly-handler-arg (:anomaly %))}} [tap] exception-cb-throw success-cb-throw @@ -312,7 +311,7 @@ :name "step1" :fn (constantly (js/Promise.reject processor-res))} exception-handler-arg (atom nil)] - (fonda/execute {:exception-handlers {"step1" #(reset! exception-handler-arg (:exception %))}} + (fonda/execute {:exception-handlers {:step1 #(reset! exception-handler-arg (:exception %))}} [processor] (fn [ctx err] (is (= @exception-handler-arg processor-res)) @@ -328,7 +327,7 @@ processor {:path [:processor-path] :fn (constantly (js/Promise.reject processor-res))} exception-handler-arg (atom nil)] - (fonda/execute {:exception-handlers {"step1" #(reset! exception-handler-arg (:exception %))}} + (fonda/execute {:exception-handlers {:step1 #(reset! exception-handler-arg (:exception %))}} [processor] (fn [ctx err] (is (nil? @exception-handler-arg)) @@ -444,7 +443,7 @@ (let [unsuccessful-anomaly (anomaly :cognitect.anomalies/incorrect) anomaly-handler-arg (atom nil)] (fonda/execute {:anomaly? true - :anomaly-handlers {"step2" #(reset! anomaly-handler-arg (:anomaly %))}} + :anomaly-handlers {:step2 #(reset! anomaly-handler-arg (:anomaly %))}} [{:path [:step1] :fn (constantly (js/Promise.resolve 1))} @@ -468,7 +467,7 @@ (let [successful-anomaly (anomaly :cognitect.anomalies/incorrect) anomaly-handler-arg (atom nil)] (fonda/execute {:anomaly? false - :anomaly-handlers {"step2" #(reset! anomaly-handler-arg (:anomaly %))}} + :anomaly-handlers {:step2 #(reset! anomaly-handler-arg (:anomaly %))}} [{:path [:step1] :fn (constantly (js/Promise.resolve 1))} @@ -497,10 +496,9 @@ step3-counter (atom 0) anomaly-handler-arg (atom nil)] (fonda/execute {:anomaly? true - :anomaly-handlers {"step2" #(reset! anomaly-handler-arg (:anomaly %))}} + :anomaly-handlers {:step2 #(reset! anomaly-handler-arg (:anomaly %))}} [{:path [:step1] - :fn (fn [_] - (js/Promise.resolve (swap! step1-counter inc)))} + :fn (fn [_] (js/Promise.resolve (swap! step1-counter inc)))} {:path [:step2] :name "step2" @@ -525,7 +523,7 @@ step3-atom (atom nil) anomaly-handler-arg (atom nil)] (fonda/execute {:anomaly? true - :anomaly-handlers {"step2" #(reset! anomaly-handler-arg (:anomaly %))}} + :anomaly-handlers {:step2 #(reset! anomaly-handler-arg (:anomaly %))}} [{:path [:step1] :fn (fn [_] (js/Promise.resolve (swap! step1-counter inc)))} @@ -556,10 +554,9 @@ (async done (let [exception (js/Error "Bad exception") exception-handler-arg (atom nil)] - (fonda/execute {:exception-handlers {"step2" #(reset! exception-handler-arg (:exception %))}} + (fonda/execute {:exception-handlers {:step2 #(reset! exception-handler-arg (:exception %))}} [{:path [:step1] - :fn (fn [_] - (js/Promise.resolve 1))} + :fn (fn [_] (js/Promise.resolve 1))} {:path [:step2] :name "step2" @@ -819,4 +816,4 @@ (done))} [processor] on-exception-event - success-cb-throw))))) \ No newline at end of file + success-cb-throw))))) From f6afbf4ae75dc06646cfda9e8be4aa233dd6cd55 Mon Sep 17 00:00:00 2001 From: David Cerezo Inigo Date: Tue, 14 Apr 2020 18:19:25 -0700 Subject: [PATCH 9/9] Bugfix on is-anomaly-error --- src/fonda/execute.cljs | 2 +- src/fonda/step.cljs | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/fonda/execute.cljs b/src/fonda/execute.cljs index 6b4c40f..a522477 100644 --- a/src/fonda/execute.cljs +++ b/src/fonda/execute.cljs @@ -77,7 +77,7 @@ (when on-complete ((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 (and anomaly-fn (anomaly-fn step-res) (is-anomaly-error? step-res)) ;; If anomaly, calls on-error (when on-error diff --git a/src/fonda/step.cljs b/src/fonda/step.cljs index 2541057..6eaec5a 100644 --- a/src/fonda/step.cljs +++ b/src/fonda/step.cljs @@ -54,13 +54,12 @@ (defn step->record [{:keys [tap inject fn processor] :as step}] - (let [step - (cond - tap (map->Tap (update step :tap resolve-function)) - (or processor fn) (map->Processor (-> step - (update :fn resolve-function) - (assoc :is-anomaly-error? (or (:is-anomaly-error? step) (constantly true))))) - inject (map->Injector (update step :inject resolve-function)))] - (update step :name keyword))) + (let [step (cond + tap (map->Tap (update step :tap resolve-function)) + (or processor fn) (map->Processor (update step :fn resolve-function)) + inject (map->Injector (update step :inject resolve-function)))] + (-> step + (update :name keyword) + (assoc :is-anomaly-error? (constantly true))))) (def ^{:doc "Step transducer."} xf (map step->record))