Repository: metosin/sieppari
Branch: develop
Commit: 47847a09af9a
Files: 38
Total size: 113.3 KB
Directory structure:
gitextract_i1aehsnv/
├── .github/
│ └── workflows/
│ └── main.yml
├── .gitignore
├── CHANGELOG.md
├── HACKING.md
├── LICENSE
├── README.md
├── deps.edn
├── dev/
│ ├── lumo_runner.cljs
│ └── user.clj
├── examples/
│ └── example/
│ ├── perf_testing.clj
│ └── simple.cljc
├── package.json
├── project.clj
├── scripts/
│ ├── lein-modules
│ └── test-self-host
├── src/
│ └── sieppari/
│ ├── async/
│ │ ├── core_async.cljc
│ │ └── manifold.clj
│ ├── async.cljc
│ ├── context.cljc
│ ├── core.cljc
│ ├── interceptor.cljc
│ ├── queue.cljc
│ └── util.cljc
├── test/
│ ├── clj/
│ │ └── sieppari/
│ │ ├── async/
│ │ │ └── manifold_test.clj
│ │ ├── core_async_test.clj
│ │ ├── core_execute_test.clj
│ │ ├── core_test.clj
│ │ ├── manifold_test.clj
│ │ └── promesa_test.clj
│ ├── cljc/
│ │ └── sieppari/
│ │ ├── async/
│ │ │ ├── core_async_test.cljc
│ │ │ └── promesa_test.cljc
│ │ ├── context_test.cljc
│ │ ├── interceptor_test.cljc
│ │ └── queue_test.cljc
│ └── cljs/
│ └── sieppari/
│ ├── core_async_test.cljs
│ ├── core_execute_test.cljs
│ └── native_promise_test.cljs
└── tests.edn
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/main.yml
================================================
name: Clojure CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Install npm dependencies
run: npm install
- name: Install Clojure dependencies
run: lein deps
- name: Run tests
run: lein kaocha
================================================
FILE: .gitignore
================================================
/target
/out
/node_modules
pom.xml
pom.xml.asc
/.lein-*
/.nrepl-port
/.cpcache
/.cljs
/node_modules
.cljs_node_repl
/.lumo_cache/
.idea/
*.iml
================================================
FILE: CHANGELOG.md
================================================
# 0.0.0-alpha13 (2020-05-18)
# 0.0.0-alpha12 (2020-05-18)
# 0.0.0-alpha11 (2020-05-18)
* rebuild without dev-dependencies.
# 0.0.0-alpha10 (2020-05-15)
* Fail nicely (throw or return error) for unsupported Contexts.
* e.g. returning a `core.async` channel without enabling it's support by requiring `sieppari.async.core-async`
# 0.0.0-alpha9 (2020-05-04)
* Fix performance regressions from previous Alphas.
* Tested against Promesa 5.*.
# 0.0.0-alpha8 (2019-11-06)
* Support `java.util.concurrent.CompletionStage` by default on the JVM and
`js/Promise` for ClojureScript.
* `sieppari.async.promesa` is not needed anymore and has been removed.
* Catch Async Exceptions
* Support Promesa 4.x (thanks to by [Andrea Richiardi](https://github.com/arichiardi) and [Andrey Antukh](https://github.com/niwinz))
* Remove automatic support for 3rd party async libs
* Reduced `sieppari.core` load time by about 4 seconds.
* `sieppari.async.*` namespaces now need to be `require`:d explicitly, you probably only need one of them.
* Run tests with Kaocha on JVM and node, then consolidate build tools back to just Leiningen.
# 0.0.0-alpha7 (2018-12-29)
* Initial support for ClojureScript by [Andrea Richiardi](https://github.com/arichiardi)
* `core.async` & `promesa` (requires `1.10.0-SNAPSHOT`)
================================================
FILE: HACKING.md
================================================
# Testing
## CLI
```shell script
lein kaocha # run clj and cljs tests
```
`kaocha-cljs` [requires](https://github.com/lambdaisland/kaocha-cljs#quickstart) `ws` from npm.
A `package(-lock).json` is provided so you can just `npm install` those.
## REPL
```clojure
(kaocha/run :unit) # `user` ns has alias `kaocha` -> `kaocha.repl`
```
================================================
FILE: LICENSE
================================================
Eclipse Public License - v 2.0
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial content
Distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from
and are Distributed by that particular Contributor. A Contribution
"originates" from a Contributor if it was added to the Program by
such Contributor itself or anyone acting on such Contributor's behalf.
Contributions do not include changes or additions to the Program that
are not Modified Works.
"Contributor" means any person or entity that Distributes the Program.
"Licensed Patents" mean patent claims licensable by a Contributor which
are necessarily infringed by the use or sale of its Contribution alone
or when combined with the Program.
"Program" means the Contributions Distributed in accordance with this
Agreement.
"Recipient" means anyone who receives the Program under this Agreement
or any Secondary License (as applicable), including Contributors.
"Derivative Works" shall mean any work, whether in Source Code or other
form, that is based on (or derived from) the Program and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship.
"Modified Works" shall mean any work in Source Code or other form that
results from an addition to, deletion from, or modification of the
contents of the Program, including, for purposes of clarity any new file
in Source Code form that contains any contents of the Program. Modified
Works shall not include works that contain only declarations,
interfaces, types, classes, structures, or files of the Program solely
in each case in order to link to, bind by name, or subclass the Program
or Modified Works thereof.
"Distribute" means the acts of a) distributing or b) making available
in any manner that enables the transfer of a copy.
"Source Code" means the form of a Program preferred for making
modifications, including but not limited to software source code,
documentation source, and configuration files.
"Secondary License" means either the GNU General Public License,
Version 2.0, or any later versions of that license, including any
exceptions or additional permissions as identified by the initial
Contributor.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby
grants Recipient a non-exclusive, worldwide, royalty-free copyright
license to reproduce, prepare Derivative Works of, publicly display,
publicly perform, Distribute and sublicense the Contribution of such
Contributor, if any, and such Derivative Works.
b) Subject to the terms of this Agreement, each Contributor hereby
grants Recipient a non-exclusive, worldwide, royalty-free patent
license under Licensed Patents to make, use, sell, offer to sell,
import and otherwise transfer the Contribution of such Contributor,
if any, in Source Code or other form. This patent license shall
apply to the combination of the Contribution and the Program if, at
the time the Contribution is added by the Contributor, such addition
of the Contribution causes such combination to be covered by the
Licensed Patents. The patent license shall not apply to any other
combinations which include the Contribution. No hardware per se is
licensed hereunder.
c) Recipient understands that although each Contributor grants the
licenses to its Contributions set forth herein, no assurances are
provided by any Contributor that the Program does not infringe the
patent or other intellectual property rights of any other entity.
Each Contributor disclaims any liability to Recipient for claims
brought by any other entity based on infringement of intellectual
property rights or otherwise. As a condition to exercising the
rights and licenses granted hereunder, each Recipient hereby
assumes sole responsibility to secure any other intellectual
property rights needed, if any. For example, if a third party
patent license is required to allow Recipient to Distribute the
Program, it is Recipient's responsibility to acquire that license
before distributing the Program.
d) Each Contributor represents that to its knowledge it has
sufficient copyright rights in its Contribution, if any, to grant
the copyright license set forth in this Agreement.
e) Notwithstanding the terms of any Secondary License, no
Contributor makes additional grants to any Recipient (other than
those set forth in this Agreement) as a result of such Recipient's
receipt of the Program under the terms of a Secondary License
(if permitted under the terms of Section 3).
3. REQUIREMENTS
3.1 If a Contributor Distributes the Program in any form, then:
a) the Program must also be made available as Source Code, in
accordance with section 3.2, and the Contributor must accompany
the Program with a statement that the Source Code for the Program
is available under this Agreement, and informs Recipients how to
obtain it in a reasonable manner on or through a medium customarily
used for software exchange; and
b) the Contributor may Distribute the Program under a license
different than this Agreement, provided that such license:
i) effectively disclaims on behalf of all other Contributors all
warranties and conditions, express and implied, including
warranties or conditions of title and non-infringement, and
implied warranties or conditions of merchantability and fitness
for a particular purpose;
ii) effectively excludes on behalf of all other Contributors all
liability for damages, including direct, indirect, special,
incidental and consequential damages, such as lost profits;
iii) does not attempt to limit or alter the recipients' rights
in the Source Code under section 3.2; and
iv) requires any subsequent distribution of the Program by any
party to be under a license that satisfies the requirements
of this section 3.
3.2 When the Program is Distributed as Source Code:
a) it must be made available under this Agreement, or if the
Program (i) is combined with other material in a separate file or
files made available under a Secondary License, and (ii) the initial
Contributor attached to the Source Code the notice described in
Exhibit A of this Agreement, then the Program may be made available
under the terms of such Secondary Licenses, and
b) a copy of this Agreement must be included with each copy of
the Program.
3.3 Contributors may not remove or alter any copyright, patent,
trademark, attribution notices, disclaimers of warranty, or limitations
of liability ("notices") contained within the Program from any copy of
the Program which they Distribute, provided that Contributors may add
their own appropriate notices.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities
with respect to end users, business partners and the like. While this
license is intended to facilitate the commercial use of the Program,
the Contributor who includes the Program in a commercial product
offering should do so in a manner which does not create potential
liability for other Contributors. Therefore, if a Contributor includes
the Program in a commercial product offering, such Contributor
("Commercial Contributor") hereby agrees to defend and indemnify every
other Contributor ("Indemnified Contributor") against any losses,
damages and costs (collectively "Losses") arising from claims, lawsuits
and other legal actions brought by a third party against the Indemnified
Contributor to the extent caused by the acts or omissions of such
Commercial Contributor in connection with its distribution of the Program
in a commercial product offering. The obligations in this section do not
apply to any claims or Losses relating to any actual or alleged
intellectual property infringement. In order to qualify, an Indemnified
Contributor must: a) promptly notify the Commercial Contributor in
writing of such claim, and b) allow the Commercial Contributor to control,
and cooperate with the Commercial Contributor in, the defense and any
related settlement negotiations. The Indemnified Contributor may
participate in any such claim at its own expense.
For example, a Contributor might include the Program in a commercial
product offering, Product X. That Contributor is then a Commercial
Contributor. If that Commercial Contributor then makes performance
claims, or offers warranties related to Product X, those performance
claims and warranties are such Commercial Contributor's responsibility
alone. Under this section, the Commercial Contributor would have to
defend claims against the other Contributors related to those performance
claims and warranties, and if a court requires any other Contributor to
pay any damages as a result, the Commercial Contributor must pay
those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS"
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF
TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR
PURPOSE. Each Recipient is solely responsible for determining the
appropriateness of using and distributing the Program and assumes all
risks associated with its exercise of rights under this Agreement,
including but not limited to the risks and costs of program errors,
compliance with applicable laws, damage to or loss of data, programs
or equipment, and unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS
SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this Agreement, and without further
action by the parties hereto, such provision shall be reformed to the
minimum extent necessary to make such provision valid and enforceable.
If Recipient institutes patent litigation against any entity
(including a cross-claim or counterclaim in a lawsuit) alleging that the
Program itself (excluding combinations of the Program with other software
or hardware) infringes such Recipient's patent(s), then such Recipient's
rights granted under Section 2(b) shall terminate as of the date such
litigation is filed.
All Recipient's rights under this Agreement shall terminate if it
fails to comply with any of the material terms or conditions of this
Agreement and does not cure such failure in a reasonable period of
time after becoming aware of such noncompliance. If all Recipient's
rights under this Agreement terminate, Recipient agrees to cease use
and distribution of the Program as soon as reasonably practicable.
However, Recipient's obligations under this Agreement and any licenses
granted by Recipient relating to the Program shall continue and survive.
Everyone is permitted to copy and distribute copies of this Agreement,
but in order to avoid inconsistency the Agreement is copyrighted and
may only be modified in the following manner. The Agreement Steward
reserves the right to publish new versions (including revisions) of
this Agreement from time to time. No one other than the Agreement
Steward has the right to modify this Agreement. The Eclipse Foundation
is the initial Agreement Steward. The Eclipse Foundation may assign the
responsibility to serve as the Agreement Steward to a suitable separate
entity. Each new version of the Agreement will be given a distinguishing
version number. The Program (including Contributions) may always be
Distributed subject to the version of the Agreement under which it was
received. In addition, after a new version of the Agreement is published,
Contributor may elect to Distribute the Program (including its
Contributions) under the new version.
Except as expressly stated in Sections 2(a) and 2(b) above, Recipient
receives no rights or licenses to the intellectual property of any
Contributor under this Agreement, whether expressly, by implication,
estoppel or otherwise. All rights in the Program not expressly granted
under this Agreement are reserved. Nothing in this Agreement is intended
to be enforceable by any entity that is not a Contributor or Recipient.
No third-party beneficiary rights are created under this Agreement.
Exhibit A - Form of Secondary Licenses Notice
"This Source Code may also be made available under the following
Secondary Licenses when the conditions for such availability set forth
in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),
version(s), and exceptions or additional permissions here}."
Simply including a copy of this Agreement, including this Exhibit A
is not sufficient to license the Source Code under Secondary Licenses.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to
look for such a notice.
You may add additional accurate notices of copyright ownership.
================================================
FILE: README.md
================================================
# sieppari [](https://cljdoc.org/d/metosin/sieppari/CURRENT)
Small, fast, and complete interceptor library for Clojure/Script with built-in support
for common async libraries.
> Noun
> **Siepata (Intercept)**
>
> sieppari, _someone or something that intercepts_
## What it does
Interceptors, like in [Pedestal](http://pedestal.io/reference/interceptors), but
with minimal implementation and optimal performance.
The core _Sieppari_ depends on Clojure and nothing else.
If you are new to interceptors, check the
[Pedestal Interceptors documentation](http://pedestal.io/pedestal/0.7/reference/interceptors.html).
Sieppari's `sieppari.core/execute` follows a `:request` / `:response` pattern. For
Pedestal-like behavior, use `sieppari.core/execute-context`.
## First example
```clj
(ns example.simple
(:require [sieppari.core :as s]))
;; interceptor, in enter update value in `[:request :x]` with `inc`
(def inc-x-interceptor
{:enter (fn [ctx] (update-in ctx [:request :x] inc))})
;; handler, take `:x` from request, apply `inc`, and return an map with `:y`
(defn handler [request]
{:y (inc (:x request))})
(s/execute
[inc-x-interceptor handler]
{:x 40})
;=> {:y 42}
```
## Async
Any step in the execution pipeline (`:enter`, `:leave`, `:error`) can return either a context map (synchronous execution) or an instance of [`AsyncContext`](https://github.com/metosin/sieppari/blob/develop/src/sieppari/async.cljc) - indicating asynchronous execution.
By default, clojure deferrables, `java.util.concurrent.CompletionStage` and `js/promise` satisfy the `AsyncContext` protocol.
Using `s/execute` with async steps will block:
```clj
;; async interceptor, in enter double value of `[:response :y]`:
(def multiply-y-interceptor
{:leave (fn [ctx]
(future
(Thread/sleep 1000)
(update-in ctx [:response :y] * 2)))})
(s/execute
[inc-x-interceptor multiply-y-interceptor handler]
{:x 40})
; ... 1 second later:
;=> {:y 84}
```
Using non-blocking version of `s/execute`:
```clj
(s/execute
[inc-x-interceptor multiply-y-interceptor handler]
{:x 40}
(partial println "SUCCESS:")
(partial println "FAILURE:"))
; => nil
; prints "SUCCESS: {:y 84}" 1sec later
```
Blocking on async computation:
```clj
(let [respond (promise)
raise (promise)]
(s/execute
[inc-x-interceptor multiply-y-interceptor handler]
{:x 40}
respond
raise) ; returns nil immediately
(deref respond 2000 :timeout))
; ... 1 second later:
;=> {:y 84}
```
Any step can return a `java.util.concurrent.CompletionStage` or `js/promise`, Sieppari works oob with libraries like [Promesa](http://funcool.github.io/promesa/latest):
```clj
;; [funcool/promesa "5.1.0"]`
(require '[promesa.core :as p])
(def chain
[{:enter #(update-in % [:request :x] inc)} ;; 1
{:leave #(p/promise (update-in % [:response :x] / 10))} ;; 4
{:enter #(p/delay 1000 %)} ;; 2
identity]) ;; 3
;; blocking
(s/execute chain {:x 40})
; => {:x 41/10} after after 1sec
;; non-blocking
(s/execute
chain
{:x 40}
(partial println "SUCCESS:")
(partial println "FAILURE:"))
; => nil
;; prints "SUCCESS: {:x 41/10}" after 1sec
```
## External Async Libraries
To add a support for one of the supported external async libraries, just add a dependency to them and `require` the
respective Sieppari namespace. Currently supported async libraries are:
* [core.async](https://github.com/clojure/core.async) - `sieppari.async.core-async`, clj & cljs
* [Manifold](https://github.com/ztellman/manifold) - `sieppari.async.manifold` clj
To extend Sieppari async support to other libraries, just extend the `AsyncContext` protocol.
## core.async
Requires dependency to `[org.clojure/core.async "0.4.474"]` or higher.
```clj
(require '[clojure.core.async :as a])
(defn multiply-x-interceptor [n]
{:enter (fn [ctx]
(a/go (update-in ctx [:request :x] * n)))})
(s/execute
[inc-x-interceptor (multiply-x-interceptor 10) handler]
{:x 40})
;=> {:y 411}
```
## manifold
Requires dependency to `[manifold "0.1.8"]` or higher.
```clj
(require '[manifold.deferred :as d])
(defn minus-x-interceptor [n]
{:enter (fn [ctx]
(d/success-deferred (update-in ctx [:request :x] - n)))})
(s/execute
[inc-x-interceptor (minus-x-interceptor 10) handler]
{:x 40})
;=> {:y 31}
```
# Performance
_Sieppari_ aims for minimal functionality and can therefore be
quite fast. Complete example to test performance is
[included](https://github.com/metosin/sieppari/blob/develop/examples/example/perf_testing.clj).
## Silly numbers
Executing a chain of 10 interceptors, which have `:enter` of `clojure.core/identity`.
* **sync**: all steps return the ctx
* **promesa**: all steps return the ctx in an `promesa.core/promise`
* **core.async**: all step return the ctx in a `core.async` channel
* **manifold**: all step return the ctx in a `manifold.deferred.Deferred`
All numbers are execution time lower quantile (not testing the goodness of the async libraries
, just the execution overhead sippari interceptors adds)
| Executor | sync | promesa | core.async | manifold |
| ----------------- | -------|---------|------------|----------|
| Pedestal | 8.2µs | - | 92µs | - |
| Sieppari | 1.2µs | 4.0µs | 70µs | 110µs |
| Middleware (comp) | 0.1µs | - | - | - |
* MacBook Pro (Retina, 15-inch, Mid 2015), 2.5 GHz Intel Core i7, 16 MB RAM
* Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
* Clojure 1.9.0
**NOTE**: running async flows without interceptors is still much faster,
e.g. synchronous `manifold` chain is much faster than via interceptors.
**NOTE**: Goal is to have a Java-backed and optimized chain compiler into Sieppari,
initial tests show it will be near the perf of middleware chain / `comp`.
# Differences to Pedestal
## Execution
* `io.pedestal.interceptor.chain/execute` executes _Contexts_
* `sieppari.core/execute` executes _Requests_ (which are internally wrapped inside a _Context_ for interceptors)
## Errors
* In _Pedestal_ the `error` handler takes two arguments, the `ctx` and the exception.
* In _Sieppari_ the `error` handlers takes just one argument, the `ctx`, and the exception is in the `ctx` under the key `:error`.
* In _Pedestal_ the `error` handler resolves the exception by returning the `ctx`, and continues the **error** stage by re-throwing the exception.
* In _Sieppari_ the `error` handler resolves the exception by returning the `ctx` with the `:error` removed. To continue in the **error** stage, just return the `ctx` with the exception still at `:error`.
* In _Pedestal_ the exception are wrapped in other exceptions.
* In _Sieppari_ exceptions are not wrapped.
* _Pedestal_ interception execution catches `java.lang.Throwable` for error processing. _Sieppari_ catches `java.lang.Exception`. This means that things like out of memory or class loader failures are not captured by _Sieppari_.
## Async
* _Pedestal_ transfers thread local bindings from call-site into async interceptors.
* _Sieppari_ does not support this.
# Thanks
* Original idea from [Pedestal Interceptors](https://github.com/pedestal/pedestal/tree/master/interceptor).
## License
Copyright © 2018-2020 [Metosin Oy](https://www.metosin.fi/)
Distributed under the Eclipse Public License 2.0.
================================================
FILE: deps.edn
================================================
;; Do *not* add deps, aliases etc. here. Use the existing project.clj instead
;; this is for the sole purpose of exposing sieppari's git coords for tools.deps projects
{:paths ["src"]}
================================================
FILE: dev/lumo_runner.cljs
================================================
(ns sieppari.lumo-runner
(:require [cljs.test :as test :refer-macros [run-tests]]
[lumo.core :as lumo]
sieppari.context-test
sieppari.queue-test
sieppari.interceptor-test
sieppari.core-execute-test
;; sieppari.promesa-test ;; not self-host compatible yet
;; sieppari.async.promesa-test
sieppari.native-promise-test
sieppari.core-async-test
sieppari.async.core-async-test))
(enable-console-print!)
(defmethod test/report [:cljs.test/default :end-run-tests] [m]
(when-not (test/successful? m)
(lumo/exit 1)))
(run-tests
'sieppari.context-test
'sieppari.queue-test
'sieppari.interceptor-test
'sieppari.core-execute-test
'sieppari.native-promise-test
'sieppari.core-async-test
'sieppari.async.core-async-test)
================================================
FILE: dev/user.clj
================================================
(ns user
(:require [clojure.tools.namespace.repl :as repl]
[kaocha.repl :as kaocha]))
(def reset repl/refresh)
(def start (constantly :ok))
(def stop (constantly :ok))
================================================
FILE: examples/example/perf_testing.clj
================================================
(ns example.perf-testing
(:require [criterium.core :as criterium]
[sieppari.core :as s]
[sieppari.queue :as sq]
[sieppari.async.core-async]
[io.pedestal.interceptor :as pi]
[io.pedestal.interceptor.chain :as pc]
[manifold.deferred :as d]
[promesa.core :as p]
[clojure.core.async :as a]))
(set! *warn-on-reflection* true)
(defn raw-title [color s]
(println (str color (apply str (repeat (count s) "#")) "\u001B[0m"))
(println (str color s "\u001B[0m"))
(println (str color (apply str (repeat (count s) "#")) "\u001B[0m")))
(def title (partial raw-title "\u001B[35m"))
(def suite (partial raw-title "\u001B[32m"))
(defmacro bench! [name & body]
`(do
(title ~name)
(assert (= ~@body {}))
(let [{[lower#] :lower-q :as res#} (criterium/quick-benchmark (do ~@body) nil)]
(println "\u001B[32m\n" (format "%.2fµs" (* 1000000 lower#)) "\u001B[0m")
(println)
(criterium/report-result res#))
(println)))
(defn make-capture-result-interceptor [p]
(pi/interceptor
{:leave (fn [ctx]
(deliver p (:response ctx))
ctx)}))
(comment
; 2.7µs
; 0.5µs (chain')
(bench!
"manifold: identity"
@(d/chain'
{}
identity
identity
identity
identity
identity
identity
identity
identity
identity
identity))
; 73µs
; 82µs (chain')
(bench!
"manifold: future"
@(d/chain'
{}
d/future
d/future
d/future
d/future
d/future
d/future
d/future
d/future
d/future
d/future))
; 3.5µs
; 1.5µs (chain')
(bench!
"manifold: success-deferred"
@(d/chain'
{}
d/success-deferred
d/success-deferred
d/success-deferred
d/success-deferred
d/success-deferred
d/success-deferred
d/success-deferred
d/success-deferred
d/success-deferred
success-deferred)))
(def sync-interceptor {:enter identity})
(def async-interceptor {:enter #(a/go %)})
(def deferred-interceptor {:enter d/success-deferred})
(def promesa-interceptor {:enter p/promise})
(def future-interceptor {:enter #(future %)})
(def delay-interceptor {:enter #(delay %)})
(defn create-s-chain [n i]
(sq/into-queue (concat (repeat n i) [identity])))
(defn create-s-mixed-chain [n i]
(sq/into-queue (concat (repeat (dec n) sync-interceptor) [i identity])))
(defn run-simple-perf-test [n]
(let [sync-interceptors (concat (repeat n sync-interceptor) [identity])
async-interceptors (concat (repeat n async-interceptor) [identity])
p-context {:request {}}
p-sync-chain (mapv pi/interceptor sync-interceptors)
p-async-chain (map pi/interceptor async-interceptors)
s-sync-chain (create-s-chain n sync-interceptor)
s-async-chain (create-s-chain n async-interceptor)
s-deferred-chain (create-s-chain n deferred-interceptor)
s-future-chain (create-s-chain n future-interceptor)
s-delay-chain (create-s-chain n delay-interceptor)
s-promesa-chain (create-s-chain n promesa-interceptor)]
(suite (str "queue of " n))
;; 8.2µs
;; 8.2µs (context-api)
(bench!
"pedestal: sync"
(->> p-sync-chain
(pc/enqueue p-context)
(pc/execute)
:response))
;; 99µs
;; 92µs (context-api)
(bench!
"pedestal: core.async"
(let [p (promise)]
(->> (cons (make-capture-result-interceptor p) p-async-chain)
(pc/enqueue p-context)
(pc/execute))
@p))
;; 1.3µs
;; 2.1µs
;; 1.2µs (context-api)
(bench!
"sieppari: sync (sync)"
(s/execute s-sync-chain {}))
;; 1.3µs
;; 2,5µs
;; 1.2µs (context-api)
(bench!
"sieppari: sync (async)"
(let [p (promise)]
(s/execute s-sync-chain {} p identity)
@p))
;; 61µs
;; 69µs
;; 66µs (context-api)
(bench!
"sieppari: core.async (sync)"
(s/execute s-async-chain {}))
;; 60µs
;; 65µs
;; 70µs (context-api)
(bench!
"sieppari: core.async (async)"
(let [p (promise)]
(s/execute s-async-chain {} p identity)
@p))
;; 140µs
;; 140µs (context-api)
(bench!
"sieppari: future (async)"
(let [p (promise)]
(s/execute s-future-chain {} p identity)
@p))
;; 84µs
;; 100µs
;; 110µs (context-api)
(bench!
"sieppari: delay (async)"
(let [p (promise)]
(s/execute s-delay-chain {} p identity)
@p))
;; 84µs
;; 62µs (chain'-)
;; 89µs
;; 90µs (context-api)
(bench!
"sieppari: deferred (sync)"
(s/execute s-deferred-chain {}))
;; 84µs
;; 84µs (chain'-)
;; 150µs
;; 110µs (context-api)
(bench!
"sieppari: deferred (async)"
(let [p (promise)]
(s/execute s-deferred-chain {} p identity)
@p))
;; 36µs
;; 3.8µs
;; 5.4µs
;; 3.8µs (context-api)
(bench!
"sieppari: promesa (sync)"
(s/execute s-promesa-chain {}))
;; 38µs
;; 4.0µs
;; 5.3µs
;; 4.0µs (context-api)
(bench!
"sieppari: promesa (async)"
(let [p (promise)]
(s/execute s-promesa-chain {} p identity)
@p))))
(defn one-async-in-sync-pipeline-test [n]
(doseq [[name chain] [[(str "homogeneous queue of " n) create-s-chain]
[(str "queue of " (dec n) " sync + 1 async step") create-s-mixed-chain]]
:let [_ (suite name)]
[name interceptor] [["identity" sync-interceptor]
#_["deferred" deferred-interceptor]
#_["core.async" async-interceptor]
["promesa" promesa-interceptor]]]
(let [interceptors (chain n interceptor)]
(bench!
name
(let [p (promise)]
(s/execute interceptors {} p identity)
@p)))
;; 1.8µs => 4.6µs => 1.8µs => 1.3µs
;; 1.7µs => 4.6µs => 1.8µs => 1.4µs
"identity"
;; 93µs => 100µs
;; 20µs => 18µs
"deferred"
;; 54µs => 73µs
;; 20µs => 17µs
"core.async"
;; 40µs => 4.0µs => 4.4µs => 3.9µs => 3.3µs
;; 19µs => 2.5µs => 5.0µs => 2.0µs => 1.8µs
"promesa"))
(defn middleware-comp [n]
(let [chain (apply comp (repeat n identity))]
;; 105ns
(bench!
"comp"
(chain {}))))
(defn pedestal-one-async [n]
(let [ctx {:request {}}
chain (conj (conj (vec (repeat (dec n) (pi/interceptor {:enter identity}))) (pi/interceptor {:enter #(a/go %)})) (pi/interceptor identity))]
;; 50µs
(bench!
"pedestal 9 sync + 1 async"
(let [p (promise)]
(->> (cons (make-capture-result-interceptor p) chain)
(pc/enqueue ctx)
(pc/execute))
@p))))
(defn -main [& _]
(run-simple-perf-test 10)
(one-async-in-sync-pipeline-test 10)
(middleware-comp 10)
(pedestal-one-async 10))
(comment
(run-simple-perf-test 10)
(one-async-in-sync-pipeline-test 10)
(middleware-comp 10)
(pedestal-one-async 10)
(do
(require '[clj-async-profiler.core :as prof])
(prof/serve-files 8080))
(time
(prof/profile
(let [interceptors (create-s-chain 10 identity)]
(dotimes [_ 3000000]
(let [p (promise)]
(s/execute interceptors {} p identity)
@p))))))
================================================
FILE: examples/example/simple.cljc
================================================
(ns example.simple
(:require [sieppari.core :as s]))
(def times-2-interceptor
{:enter (fn [ctx] (update-in ctx [:request :x] * 2))})
(def inc-interceptor
{:enter (fn [ctx] (update-in ctx [:request :x] inc))})
;; Simple handler, take `:x` from request, apply `inc`, and
;; return an map with `:y`.
(defn handler [request]
{:y (inc (:x request))})
(def chain [times-2-interceptor
inc-interceptor
handler])
#?(:clj (s/execute chain {:x 20})
:cljs (s/execute chain {:x 20} prn prn))
;=> {:y 42}
================================================
FILE: package.json
================================================
{
"name": "sieppari",
"description": "Small, fast, and complete interceptor library.",
"devDependencies": {
"ws": "^7.5.10"
},
"repository": {
"type": "git",
"url": "git+https://github.com/metosin/sieppari.git"
},
"license": "EPL-2.0",
"bugs": {
"url": "https://github.com/metosin/sieppari/issues"
},
"homepage": "https://github.com/metosin/sieppari#readme"
}
================================================
FILE: project.clj
================================================
(defproject metosin/sieppari "0.0.0-alpha13"
:description "Small, fast, and complete interceptor library."
:url "https://github.com/metosin/sieppari"
:license {:name "Eclipse Public License", :url "https://www.eclipse.org/legal/epl-2.0/"}
:deploy-repositories [["releases" :clojars]]
:lein-release {:deploy-via :clojars}
:dependencies []
:test-paths ["test/clj" "test/cljs" "test/cljc"]
:profiles {:dev {:source-paths ["dev"]
:dependencies [[org.clojure/clojure "1.10.1" :scope "provided"]
[org.clojure/clojurescript "1.10.758"]
;; Add-ons:
[org.clojure/core.async "1.2.603"]
[manifold "0.1.8"]
[funcool/promesa "5.1.0"]
;; Testing:
[metosin/testit "0.4.0"]
[lambdaisland/kaocha "1.0.632"]
[lambdaisland/kaocha-cljs "0.0-71"]
;; Dev:
[org.clojure/tools.namespace "1.0.0"]
;; Perf testing:
[criterium "0.4.5"]
[com.clojure-goes-fast/clj-async-profiler "0.5.0-SNAPSHOT"]
[io.pedestal/pedestal.interceptor "0.5.7"]
[org.slf4j/slf4j-nop "1.7.30"]]}
;; needed because of https://github.com/lambdaisland/kaocha-cljs#known-issues
:test-cljs {:source-paths ["test/cljc" "test/cljs"]}
:examples {:source-paths ["examples"]}
:perf {:jvm-opts ^:replace ["-server" "-Xms4096m" "-Xmx4096m" "-Dclojure.compiler.direct-linking=true"]}}
:aliases {"kaocha" ["with-profile" "+dev-deps,+test-cljs" "run" "-m" "kaocha.runner" "--reporter" "kaocha.report/documentation"]
"perf" ["with-profile" "default,dev,examples,perf"]
"perf-test" ["perf" "run" "-m" "example.perf-testing"]})
================================================
FILE: scripts/lein-modules
================================================
#!/bin/bash
set -e
for ext in sieppari.core \
sieppari.async.core-async \
sieppari.async.deref \
sieppari;
do
( cd modules/$ext && lein "$@"; )
done
================================================
FILE: scripts/test-self-host
================================================
#!/bin/bash
set -euo pipefail
cp="src:test/cljs:test/cljc:$(clojure -Srepro -A:self-host -Spath)"
lumo -K -c "$cp" dev/lumo_runner.cljs
================================================
FILE: src/sieppari/async/core_async.cljc
================================================
(ns sieppari.async.core-async
(:require [sieppari.async :as sa]
[sieppari.util :refer [exception?]]
[clojure.core.async :as cca
#?@(:clj [:refer [go <! <!!]]
:cljs [:refer-macros [go]])]))
(extend-protocol sa/AsyncContext
#?(:clj clojure.core.async.impl.protocols.Channel
:cljs cljs.core.async.impl.channels/ManyToManyChannel)
(async? [_] true)
(continue [c f] (go (f (cca/<! c))))
(catch [c f] (go (let [c (cca/<! c)]
(if (exception? c) (f c) c))))
#?(:clj (await [c] (<!! c))))
================================================
FILE: src/sieppari/async/manifold.clj
================================================
(ns sieppari.async.manifold
(:require [sieppari.async :as sa]
[manifold.deferred :as d]))
(extend-protocol sa/AsyncContext
manifold.deferred.Deferred
(async? [_] true)
(continue [d f] (d/chain'- nil d f))
(catch [d f] (d/catch' d f))
(await [d] (deref d))
manifold.deferred.ErrorDeferred
(async? [_] true)
(continue [d f] (d/chain'- nil d f))
(catch [d f] (d/catch' d f))
(await [d] (deref d)))
================================================
FILE: src/sieppari/async.cljc
================================================
(ns sieppari.async
#?(:clj (:refer-clojure :exclude [await]))
(:require [sieppari.util :refer [exception?]])
#?(:clj (:import java.util.concurrent.CompletionStage
java.util.concurrent.CompletionException
java.util.function.Function)))
(defprotocol AsyncContext
(async? [t])
(continue [t f])
(catch [c f])
#?(:clj (await [t])))
#?(:clj
(deftype FunctionWrapper [f]
Function
(apply [_ v]
(f v))))
#?(:clj
(extend-protocol AsyncContext
Object
(async? [_] false)
(continue [t f] (f t))
(await [t] t)))
#?(:cljs
(extend-protocol AsyncContext
default
(async? [_] false)
(continue [t f] (f t))))
#?(:clj
(extend-protocol AsyncContext
clojure.lang.IDeref
(async? [_] true)
(continue [c f] (future (f @c)))
(catch [c f] (future (let [c @c]
(if (exception? c) (f c) c))))
(await [c] @c)))
#?(:clj
(extend-protocol AsyncContext
CompletionStage
(async? [_] true)
(continue [this f]
(.thenApply ^CompletionStage this
^Function (->FunctionWrapper f)))
(catch [this f]
(letfn [(handler [e]
(if (instance? CompletionException e)
(f (.getCause ^Exception e))
(f e)))]
(.exceptionally ^CompletionStage this
^Function (->FunctionWrapper handler))))
(await [this]
(deref this))))
#?(:cljs
(extend-protocol AsyncContext
js/Promise
(async? [_] true)
(continue [t f] (.then t f))
(catch [t f] (.catch t f))))
================================================
FILE: src/sieppari/context.cljc
================================================
(ns sieppari.context
(:require [sieppari.queue :as q]))
(defprotocol Context
(context? [this]))
(extend-protocol Context
#?(:clj clojure.lang.PersistentHashMap
:cljs cljs.core.PersistentHashMap)
(context? [_] true)
#?(:clj clojure.lang.PersistentArrayMap
:cljs cljs.core.PersistentArrayMap)
(context? [_] true))
#?(:clj
(extend-protocol Context
Object
(context? [_] false)))
#?(:cljs
(extend-protocol Context
default
(context? [_] false)))
(defn terminate
"Removes all remaining interceptors from context's execution queue.
This effectively short-circuits execution of Interceptors' :enter
functions and begins executing the :leave functions.
Two arity version allows setting the response at the same call."
([ctx]
(assoc ctx :queue q/empty-queue))
([ctx response]
(-> ctx
(assoc :queue q/empty-queue)
(assoc :response response))))
(defn inject
"Adds interceptor or seq of interceptors to the head of context's execution queue. Creates
the queue if necessary. Returns updated context."
[ctx interceptor-or-interceptors]
(let [interceptors (if (sequential? interceptor-or-interceptors)
interceptor-or-interceptors
(cons interceptor-or-interceptors nil))]
(assoc ctx :queue (q/into-queue (concat interceptors (:queue ctx))))))
; TODO: figure out how enqueue should work? Should enqueue add interceptors just
#_(defn enqueue
"Adds interceptor or seq of interceptors to the end of context's execution queue. Creates
the queue if necessary. Returns updated context."
[ctx interceptor-or-interceptors]
(let [interceptors (into-interceptors (if (sequential? interceptor-or-interceptors)
interceptor-or-interceptors
[interceptor-or-interceptors]))]
(update ctx :queue (fnil into PersistentQueue/EMPTY) interceptors)))
================================================
FILE: src/sieppari/core.cljc
================================================
(ns sieppari.core
#?(:cljs (:refer-clojure :exclude [iter]))
(:require [sieppari.queue :as q]
[sieppari.async :as a]
[sieppari.context :as c]
#?(:cljs [goog.iter :as iter]))
#?(:clj (:import (java.util Iterator))))
(defrecord Context [error queue stack]
c/Context
(context? [_] true))
(defrecord RequestResponseContext [request response error queue stack]
c/Context
(context? [_] true))
(defn- -try [ctx f]
(if f
(try
(let [ctx* (f ctx)]
(if (a/async? ctx*)
(a/catch ctx* (fn [e] (assoc ctx :error e)))
ctx*))
(catch #?(:clj Exception :cljs :default) e
(assoc ctx :error e)))
ctx))
(defn- -iter [v]
#?(:clj (clojure.lang.RT/iter v)
:cljs (cljs.core/iter v)))
(defn- -invalid-context-type! [ctx stage]
(throw
(ex-info
(str "Unsupported Context on " stage" - " ctx)
{:ctx ctx})))
(defn- leave [ctx]
(cond
(a/async? ctx) (a/continue ctx leave)
(c/context? ctx) (let [^Iterator it (:stack ctx)]
(if (.hasNext it)
(let [stage (if (:error ctx) :error :leave)
f (-> it .next stage)]
(recur (-try ctx f)))
ctx))
:else (-invalid-context-type! ctx :leave)))
(defn- enter [ctx]
(cond
(a/async? ctx) (a/continue ctx enter)
(c/context? ctx) (let [queue (:queue ctx)
stack (:stack ctx)
interceptor (peek queue)]
(if (or (not interceptor) (:error ctx))
(assoc ctx :stack (-iter stack))
(recur (-> ctx
(assoc :queue (pop queue))
(assoc :stack #?(:clj (conj stack interceptor)
:cljs (doto (or stack (array))
(.unshift interceptor))))
(-try (:enter interceptor))))))
:else (-invalid-context-type! ctx :enter)))
#?(:clj
(defn- await-result [ctx get-result]
(if (a/async? ctx)
(recur (a/await ctx) get-result)
(if-let [error (:error ctx)]
(throw error)
(get-result ctx)))))
(defn- deliver-result [ctx get-result on-complete on-error]
(if (a/async? ctx)
(a/continue ctx #(deliver-result % get-result on-complete on-error))
(let [error (:error ctx)
result (or error (get-result ctx))
callback (if error on-error on-complete)]
(callback result))))
(defn- remove-context-keys [ctx]
(dissoc ctx :error :queue :stack))
;;
;; Public API:
;;
(defn execute-context
{:arglists
'([interceptors ctx]
[interceptors ctx on-complete on-error])}
([interceptors ctx on-complete on-error]
(execute-context interceptors ctx on-complete on-error remove-context-keys))
([interceptors ctx on-complete on-error get-result]
(if-let [queue (q/into-queue interceptors)]
(try
(-> (assoc ctx :queue queue)
(map->Context)
(enter)
(leave)
(deliver-result get-result on-complete on-error))
(catch #?(:clj Exception :cljs js/Error) e (on-error e)))
(on-complete nil))
nil)
#?(:clj
([interceptors ctx]
(execute-context interceptors ctx remove-context-keys)))
#?(:clj
([interceptors ctx get-result]
(when-let [queue (q/into-queue interceptors)]
(-> (assoc ctx :queue queue)
(map->Context)
(enter)
(leave)
(await-result get-result))))))
(defn execute
{:arglists
'([interceptors request]
[interceptors request on-complete on-error])}
([interceptors request on-complete on-error]
(if-let [queue (q/into-queue interceptors)]
(try
(-> (new RequestResponseContext request nil nil queue nil)
(enter)
(leave)
(deliver-result :response on-complete on-error))
(catch #?(:clj Exception :cljs js/Error) e (on-error e)))
(on-complete nil))
nil)
#?(:clj
([interceptors request]
(when-let [queue (q/into-queue interceptors)]
(-> (new RequestResponseContext request nil nil queue nil)
(enter)
(leave)
(await-result :response))))))
================================================
FILE: src/sieppari/interceptor.cljc
================================================
(ns sieppari.interceptor
(:require [sieppari.async :as a]
[sieppari.util :refer [exception?]]))
(defrecord Interceptor [name enter leave error])
(defprotocol IntoInterceptor
(into-interceptor [t] "Given a value, produce an Interceptor Record."))
(defn- set-result [ctx response]
(if (and (some? response) (a/async? response))
(a/continue response (partial set-result ctx))
(assoc ctx
(if (exception? response) :error :response)
response)))
(extend-protocol IntoInterceptor
;; Map -> Interceptor:
#?(:clj clojure.lang.IPersistentMap
:cljs cljs.core.PersistentHashMap)
(into-interceptor [interceptor-map]
(map->Interceptor interceptor-map))
#?(:cljs cljs.core.PersistentArrayMap)
(into-interceptor [interceptor-map]
(map->Interceptor interceptor-map))
;; Function -> Handler interceptor:
#?(:clj clojure.lang.Fn
:cljs function)
(into-interceptor [handler]
(into-interceptor {:enter (fn [ctx]
(set-result ctx (handler (:request ctx))))}))
;; Vector -> Interceptor, first element is a function to create
;; the interceptor, rest are arguments for it:
#?(:clj clojure.lang.IPersistentVector
:cljs cljs.core.PersistentVector)
(into-interceptor [t]
(into-interceptor (apply (first t) (rest t))))
;; Interceptor -> Interceptor, nop:
Interceptor
(into-interceptor [t]
t)
;; nil -> nil, nop:
nil
(into-interceptor [_]
nil))
================================================
FILE: src/sieppari/queue.cljc
================================================
(ns sieppari.queue
(:require [sieppari.interceptor :as i]))
(defprotocol IntoQueue
(into-queue [t]))
(def empty-queue
#?(:clj clojure.lang.PersistentQueue/EMPTY
:cljs #queue []))
(defn- into-queue*
[t]
(when (seq t)
(into empty-queue
(keep i/into-interceptor)
t)))
#?(:clj
(extend-protocol IntoQueue
clojure.lang.PersistentQueue
(into-queue [t]
t)
clojure.lang.ISeq
(into-queue [t]
(into-queue* t))
clojure.lang.Seqable
(into-queue [t]
(into-queue* (seq t)))
nil
(into-queue [_])))
#?(:cljs
(extend-protocol IntoQueue
cljs.core.PersistentQueue
(into-queue [t]
t)
cljs.core.List
(into-queue [t]
(into-queue* t))
cljs.core.LazySeq
(into-queue [t]
(into-queue* t))
cljs.core.PersistentVector
(into-queue [t]
(into-queue* t))
cljs.core.RSeq
(into-queue [t]
(into-queue* t))
cljs.core.EmptyList
(into-queue [_])
cljs.core.Cons
(into-queue [t]
(conj empty-queue t))
array
(into-queue [t]
(into-queue* t))
nil
(into-queue [_])))
================================================
FILE: src/sieppari/util.cljc
================================================
(ns sieppari.util)
(defn exception? [e]
(instance? #?(:clj Exception :cljs js/Error) e))
================================================
FILE: test/clj/sieppari/async/manifold_test.clj
================================================
(ns sieppari.async.manifold-test
(:require [clojure.test :refer :all]
[testit.core :refer :all]
[sieppari.async :as as]
[sieppari.async.manifold]
[manifold.deferred :as d]))
(deftest async?-test
(fact
(as/async? (d/deferred)) => true))
(deftest continue-test
(let [p (promise)
d (d/deferred)]
(future
(d/success! d "foo"))
(as/continue d (partial deliver p))
(fact
@p =eventually=> "foo")))
(deftest catch-test
(let [p (promise)
d (d/deferred)]
(future
(d/success! d "foo"))
(as/catch (as/continue d (partial deliver p))
(fn [_] (deliver p "barf")))
(fact
@p =eventually=> "foo"))
(let [p (promise)
d (d/deferred)]
(future
(d/error! d (Exception. "fubar")))
(as/catch d (fn [_] (deliver p "foo")))
(fact
@p =eventually=> "foo")))
(deftest await-test
(fact
(as/await (let [d (d/deferred)]
(future
(d/success! d "foo"))
d))
=> "foo"))
================================================
FILE: test/clj/sieppari/core_async_test.clj
================================================
(ns sieppari.core-async-test
(:require [clojure.test :refer :all]
[testit.core :refer :all]
[sieppari.core :as sc]
[clojure.core.async :refer [go <! <!!]]))
(defn make-logging-interceptor [log name]
{:name name
:enter (fn [ctx] (swap! log conj [:enter name]) ctx)
:leave (fn [ctx] (swap! log conj [:leave name]) ctx)
:error (fn [ctx] (swap! log conj [:error name]) ctx)})
(defn make-async-logging-interceptor [log name]
{:name name
:enter (fn [ctx] (swap! log conj [:enter name]) (go ctx))
:leave (fn [ctx] (swap! log conj [:leave name]) (go ctx))
:error (fn [ctx] (swap! log conj [:error name]) (go ctx))})
(defn make-logging-handler [log]
(fn [request]
(swap! log conj [:handler])
request))
(defn make-async-logging-handler [log]
(fn [request]
(swap! log conj [:handler])
(go request)))
(def request {:foo "bar"})
(def error (ex-info "oh no" {}))
(defn fail! [& _]
(throw (ex-info "Should never be called" {})))
(deftest setup-sync-test-test
(let [log (atom [])
response (-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-logging-handler log)]
(sc/execute request))]
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])
(fact
response => request)))
(deftest setup-async-test-test
(let [log (atom [])
response (-> [(make-async-logging-interceptor log :a)
(make-async-logging-interceptor log :b)
(make-async-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request))]
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])
(fact
response => request)))
(deftest execute-context-setup-async-test-test
(let [log (atom [])
response (-> [(make-async-logging-interceptor log :a)
(make-async-logging-interceptor log :b)
(make-async-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute-context {:request request}))]
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])
(fact
(:response response) => request)))
(deftest async-b-sync-execute-test
(let [log (atom [])
response (-> [(make-logging-interceptor log :a)
(make-async-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-logging-handler log)]
(sc/execute request))]
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])
(fact
response => request)))
(deftest async-handler-test
(let [log (atom [])
response (-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request))]
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])
(fact
response => request)))
(deftest async-stack-sync-execute-test
(let [log (atom [])
response (-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request))]
(fact
response => request)
(fact
@log =eventually=> [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])))
(deftest async-stack-async-execute-test
(let [log (atom [])
response-p (promise)]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request (partial deliver response-p) fail!))
(fact
@response-p =eventually=> request)
(fact
@log =eventually=> [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])))
(deftest async-execute-with-error-test
(let [log (atom [])
response-p (promise)]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(throw error))]
(sc/execute request fail! (partial deliver response-p)))
(fact
@response-p =eventually=> error)
(fact
@log =eventually=> [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]])))
(deftest async-execute-rejection-test
(let [log (atom [])
response-p (promise)]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(go error))]
(sc/execute request fail! (partial deliver response-p)))
(fact
@response-p =eventually=> error)
(fact
@log =eventually=> [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]])))
(deftest async-failing-handler-test
(let [log (atom [])]
(fact
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(go error))]
(sc/execute request))
=throws=> error)
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]])))
(deftest async-failing-handler-b-fixes-test
(let [log (atom [])]
(fact
(-> [(make-logging-interceptor log :a)
(assoc (make-async-logging-interceptor log :b)
:error (fn [ctx]
(swap! log conj [:error :b])
(go
(-> ctx
(assoc :error nil)
(assoc :response :fixed-by-b)))))
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(go error))]
(sc/execute request))
=> :fixed-by-b)
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:leave :a]])))
================================================
FILE: test/clj/sieppari/core_execute_test.clj
================================================
(ns sieppari.core-execute-test
(:require [clojure.test :refer :all]
[testit.core :refer :all]
[sieppari.core :as s]
[sieppari.context :as sc]
[clojure.string :as str]))
;;
;; Following tests use a test-chain that has some interceptors
;; that fail on each stage function (enter, leave, error). The
;; idea is that the tests override the expected stage functions
;; with test specific function. This ensures that no unexpected
;; stage functions are called.
;;
; Make an interceptor with given name and set all stage functions
; to report unexpected invocation. Tests should override expected
; stages.
(defn unexpected [name stage]
(fn [ctx]
(throw (ex-info "unexpected invocation"
{:name name
:stage stage
:ctx ctx}))))
(defn make-test-interceptor [name]
{:name name
:enter (unexpected name :enter)
:leave (unexpected name :leave)
:error (unexpected name :error)})
; Test stack with three interceptors and a handler:
(def test-chain [(make-test-interceptor :a)
(make-test-interceptor :b)
(make-test-interceptor :c)
(unexpected :handler nil)])
(def a-index 0)
(def b-index 1)
(def c-index 2)
(def h-index 3)
;; Helper: always throws an exception with specific marker
;; in data part:
(def error (ex-info "oh no" {::error-marker true}))
(defn always-throw [ctx]
(throw error))
;; Helper: return error handler function that ensures
;; that `ctx` contains an exception caused by `always-throw`,
;; clears the exception and sets response to given response:
(defn handle-error [overwrite-context]
(fn [ctx]
(assert (-> ctx :error ex-data (= {::error-marker true})))
(-> ctx
(dissoc :error)
(merge overwrite-context))))
(defn handle-error-response [response]
(fn [ctx]
(assert (not (some? (:response ctx))))
(assert (-> ctx :error ex-data (= {::error-marker true})))
(-> ctx
(dissoc :error)
(assoc :response response))))
;;
;; Tests:
;;
;;`execute-context` Tests
(deftest execute-context-test
(fact "enable all enter and leave stages, add `inc` interceptor"
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] identity)
(assoc-in [c-index :enter] identity)
(assoc-in [h-index] {:enter (fn [ctx] (update ctx :data inc))})
(assoc-in [c-index :leave] identity)
(assoc-in [b-index :leave] identity)
(assoc-in [a-index :leave] identity)
(s/execute-context {:data 41}))
=> {:data 42}))
(deftest execute-context-enter-b-causes-exception-test
(fact ":b causes an exception"
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] always-throw)
(assoc-in [b-index :error] identity)
(assoc-in [a-index :error] identity)
(s/execute-context {:data 41}))
=throws=> error))
(deftest execute-context-enter-c-causes-exception-a-handles-test
(fact ":c enter causes an exception, :b sees error, :a handles"
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] identity)
(assoc-in [c-index :enter] always-throw)
(assoc-in [c-index :error] identity)
(assoc-in [b-index :error] identity)
(assoc-in [a-index :error] (handle-error {:data :fixed-by-a}))
(s/execute-context {:data 41}))
=> {:data :fixed-by-a}))
(deftest execute-context-enter-c-causes-exception-b-handles-test
(fact ":c enter causes an exception, :b handles"
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] identity)
(assoc-in [c-index :enter] always-throw)
(assoc-in [c-index :error] identity)
(assoc-in [b-index :error] (handle-error {:data :fixed-by-b}))
(assoc-in [a-index :leave] identity)
(s/execute-context {:data 41}))
=> {:data :fixed-by-b}))
(deftest execute-context-enter-b-sets-response-test
(fact ":b sets the response, no invocation of :c nor :handler"
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] (fn [ctx] (sc/terminate
(assoc ctx :data :response-by-b))))
(assoc-in [b-index :leave] identity)
(assoc-in [a-index :leave] identity)
(s/execute-context {:data 41}))
=> {:data :response-by-b}))
(deftest execute-context-empty-interceptors-test
(facts "interceptor chain can be empty"
(s/execute-context [] {}) => nil))
(defn make-logging-interceptor [name]
{:name name
:enter (fn [ctx]
(update ctx :request conj [:enter name]))
:leave (fn [ctx]
(update ctx :response conj [:leave name]))})
(defn logging-handler [request]
(conj request [:handler]))
(deftest execute-context-inject-interceptor-test
(fact ":b injects interceptor :x to chain, ensure the order is correct"
(-> [(make-logging-interceptor :a)
{:enter (fn [ctx] (sc/inject ctx (make-logging-interceptor :x)))}
(make-logging-interceptor :c)
logging-handler]
(s/execute-context {:request []})
:response)
=> [[:enter :a]
[:enter :x]
[:enter :c]
[:handler]
[:leave :c]
[:leave :x]
[:leave :a]]))
;;`execute` Tests
(deftest happy-case-test
(fact "enable all enter and leave stages, use `inc` as handler"
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] identity)
(assoc-in [c-index :enter] identity)
(assoc-in [h-index] inc)
(assoc-in [c-index :leave] identity)
(assoc-in [b-index :leave] identity)
(assoc-in [a-index :leave] identity)
(s/execute 41))
=> 42))
(deftest enter-b-causes-exception-test
(fact ":b causes an exception"
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] always-throw)
(assoc-in [b-index :error] identity)
(assoc-in [a-index :error] identity)
(s/execute 41))
=throws=> error))
(deftest enter-c-causes-exception-a-handles-test
(fact ":c enter causes an exception, :b sees error, :a handles"
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] identity)
(assoc-in [c-index :enter] always-throw)
(assoc-in [c-index :error] identity)
(assoc-in [b-index :error] identity)
(assoc-in [a-index :error] (handle-error-response :fixed-by-a))
(s/execute 41))
=> :fixed-by-a))
(deftest enter-c-causes-exception-b-handles-test
(fact ":c enter causes an exception, :b handles"
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] identity)
(assoc-in [c-index :enter] always-throw)
(assoc-in [c-index :error] identity)
(assoc-in [b-index :error] (handle-error-response :fixed-by-b))
(assoc-in [a-index :leave] identity)
(s/execute 41))
=> :fixed-by-b))
(deftest handler-causes-exception-b-handles-test
(fact
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] identity)
(assoc-in [c-index :enter] identity)
(assoc-in [h-index] always-throw)
(assoc-in [c-index :error] identity)
(assoc-in [b-index :error] (handle-error-response :fixed-by-b))
(assoc-in [a-index :leave] identity)
(s/execute 41))
=> :fixed-by-b))
(deftest enter-b-sets-response-test
(fact ":b sets the response, no invocation of :c nor :handler"
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] (fn [ctx] (sc/terminate ctx :response-by-b)))
(assoc-in [b-index :leave] identity)
(assoc-in [a-index :leave] identity)
(s/execute 41))
=> :response-by-b))
(deftest nil-response-test
(fact "nil response is allowed"
(s/execute [(constantly nil)] {})
=> nil))
(deftest empty-interceptors-test
(facts "interceptor chain can be empty"
(s/execute [] {}) => nil
(s/execute nil {}) => nil))
(defn make-logging-interceptor [name]
{:name name
:enter (fn [ctx] (update ctx :request conj [:enter name]))
:leave (fn [ctx] (update ctx :response conj [:leave name]))})
(defn logging-handler [request]
(conj request [:handler]))
(deftest inject-interceptor-test
(fact ":b injects interceptor :x to chain, ensure the order is correct"
(-> [(make-logging-interceptor :a)
{:enter (fn [ctx] (sc/inject ctx (make-logging-interceptor :x)))}
(make-logging-interceptor :c)
logging-handler]
(s/execute []))
=> [[:enter :a]
[:enter :x]
[:enter :c]
[:handler]
[:leave :c]
[:leave :x]
[:leave :a]]))
; TODO: figure out how enqueue should work? Should enqueue add interceptors just
; before the handler?
#_(deftest enqueue-interceptor-test
(fact ":b enqueues interceptor :x to chain, ensure the order is correct"
(-> [(make-logging-interceptor :a)
{:enter (fn [ctx] (sc/enqueue ctx (make-logging-interceptor :x)))}
(make-logging-interceptor :c)
logging-handler]
(sc/into-interceptors)
(s/execute []))
=> [[:enter :a]
[:enter :c]
[:enter :x]
[:handler]
[:leave :x]
[:leave :c]
[:leave :a]]))
(defrecord UnsupportedContext [])
(defn invalid-context-class-exception? [e]
(-> e .getMessage (str/starts-with? "Unsupported Context on :enter")))
(deftest invalid-context-test
(fact "fails on sync"
(s/execute [{:enter map->UnsupportedContext}] {:x 40})
=throws=> invalid-context-class-exception?)
(testing "async"
(let [on-error (promise)]
(s/execute [{:enter map->UnsupportedContext}] {:x 40} ::irrelevant on-error)
(fact "responds failure"
@on-error => invalid-context-class-exception?))))
================================================
FILE: test/clj/sieppari/core_test.clj
================================================
(ns sieppari.core-test
(:require [clojure.test :refer :all]
[testit.core :refer :all]
[sieppari.core :as s]
[sieppari.async.core-async]
[clojure.core.async :as a]))
(def try-f #'s/-try)
(deftest try-f-test
(fact
(try-f {} nil)
=> {})
(fact
(try-f {} (fn [ctx] (assoc ctx :foo "bar")))
=> {:foo "bar"})
(fact
(try-f {} (fn [_] (throw (ex-info "oh no" {}))))
=in=> {:error (ex-info? "oh no" {})})
(fact
@(try-f {} (fn [_] (future (ex-info "oh no" {}))))
=eventually-in=> {:error (ex-info? "oh no" {})}))
(def await-result #'s/await-result)
(def error (RuntimeException. "kosh"))
(deftest wait-result-core-async-test
(facts "response"
(await-result {:response :ctx} :response) => :ctx
(await-result (a/go {:response :ctx}) :response) => :ctx
(await-result (a/go (a/go {:response :ctx})) :response) => :ctx)
(facts "error"
(await-result {:error error} :response) =throws=> error
(await-result (a/go {:error error}) :response) =throws=> error
(await-result (a/go (a/go {:error error})) :response) =throws=> error))
(deftest wait-result-deref-test
(facts "response"
(await-result {:response :ctx} :response) => :ctx
(await-result (future {:response :ctx}) :response) => :ctx
(await-result (future (future {:response :ctx})) :response) => :ctx)
(facts "exception"
(await-result {:error error} :response) =throws=> error
(await-result (future {:error error}) :response) =throws=> error
(await-result (future (future {:error error})) :response) =throws=> error))
(def deliver-result #'s/deliver-result)
(defn fail! [_] (throw (ex-info "should never get here" {})))
(let [p (promise)]
(deliver-result {:response :r} :response p fail!)
(fact
@p =eventually=> :r))
(deftest deliver-result-test
(let [p (promise)]
(deliver-result {:response :r} :response p fail!)
(fact
@p =eventually=> :r))(let [p (promise)]
(deliver-result {:response :r} :response p fail!)
(fact @p =eventually=> :r))
(let [p (promise)]
(deliver-result {:response :r} :response p fail!)
(fact @p =eventually=> :r))
(let [p (promise)]
(deliver-result {:error (ex-info "oh no" {})} :response fail! p)
(fact @p =eventually=> (ex-info? "oh no" {})))
(let [p (promise)]
(deliver-result (a/go {:response :r}) :response p fail!)
(fact @p =eventually=> :r))
(let [p (promise)]
(deliver-result (a/go {:error (ex-info "oh no" {})}) :response fail! p)
(fact @p =eventually=> (ex-info? "oh no" {})))
(let [p (promise)]
(deliver-result (future (a/go {:response :r})) :response p fail!)
(fact @p =eventually=> :r)))
================================================
FILE: test/clj/sieppari/manifold_test.clj
================================================
(ns sieppari.manifold-test
(:require [clojure.test :refer :all]
[testit.core :refer :all]
[sieppari.core :as sc]
[manifold.deferred :as d]))
(defn make-logging-interceptor [log name]
{:name name
:enter (fn [ctx] (swap! log conj [:enter name]) ctx)
:leave (fn [ctx] (swap! log conj [:leave name]) ctx)
:error (fn [ctx] (swap! log conj [:error name]) ctx)})
(defn make-async-logging-interceptor [log name]
{:name name
:enter (fn [ctx] (swap! log conj [:enter name]) (d/success-deferred ctx))
:leave (fn [ctx] (swap! log conj [:leave name]) (d/success-deferred ctx))
:error (fn [ctx] (swap! log conj [:error name]) (d/success-deferred ctx))})
(defn make-logging-handler [log]
(fn [request]
(swap! log conj [:handler])
request))
(defn make-async-logging-handler [log]
(fn [request]
(swap! log conj [:handler])
(d/success-deferred request)))
(def request {:foo "bar"})
(def error (ex-info "oh no" {}))
(defn fail! [& _]
(throw (ex-info "Should never be called" {})))
(deftest setup-sync-test-test
(let [log (atom [])
response (-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-logging-handler log)]
(sc/execute request))]
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])
(fact
response => request)))
(deftest setup-async-test-test
(let [log (atom [])
response (-> [(make-async-logging-interceptor log :a)
(make-async-logging-interceptor log :b)
(make-async-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request))]
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])
(fact
response => request)))
(deftest async-b-sync-execute-test
(let [log (atom [])
response (-> [(make-logging-interceptor log :a)
(make-async-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-logging-handler log)]
(sc/execute request))]
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])
(fact
response => request)))
(deftest async-handler-test
(let [log (atom [])
response (-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request))]
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])
(fact
response => request)))
(deftest async-stack-sync-execute-test
(let [log (atom [])
response (-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request))]
(fact
response => request)
(fact
@log =eventually=> [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])))
(deftest async-stack-async-execute-test
(let [log (atom [])
response-p (promise)]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request (partial deliver response-p) fail!))
(fact
@response-p =eventually=> request)
(fact
@log =eventually=> [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])))
(deftest async-execute-with-error-test
(let [log (atom [])
response-p (promise)]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(throw error))]
(sc/execute request fail! (partial deliver response-p)))
(fact
@response-p =eventually=> error)
(fact
@log =eventually=> [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]])))
(deftest async-execute-rejection-test
(let [log (atom [])
response-p (promise)]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(d/error-deferred error))]
(sc/execute request fail! (partial deliver response-p)))
(fact
@response-p =eventually=> error)
(fact
@log =eventually=> [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]])))
(deftest async-failing-handler-test
(let [log (atom [])]
(fact
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(d/success-deferred error))]
(sc/execute request))
=throws=> error)
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]])))
(deftest async-failing-handler-b-fixes-test
(let [log (atom [])]
(fact
(-> [(make-logging-interceptor log :a)
(assoc (make-async-logging-interceptor log :b)
:error (fn [ctx]
(swap! log conj [:error :b])
(d/success-deferred
(-> ctx
(assoc :error nil)
(assoc :response :fixed-by-b)))))
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(d/success-deferred error))]
(sc/execute request))
=> :fixed-by-b)
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:leave :a]])))
================================================
FILE: test/clj/sieppari/promesa_test.clj
================================================
(ns sieppari.promesa-test
(:require [clojure.test :refer :all]
[testit.core :refer :all]
[sieppari.core :as sc]
[promesa.core :as p]))
(defn make-logging-interceptor [log name]
{:name name
:enter (fn [ctx] (swap! log conj [:enter name]) ctx)
:leave (fn [ctx] (swap! log conj [:leave name]) ctx)
:error (fn [ctx] (swap! log conj [:error name]) ctx)})
(defn make-async-logging-interceptor [log name]
{:name name
:enter (fn [ctx] (swap! log conj [:enter name]) (p/promise ctx))
:leave (fn [ctx] (swap! log conj [:leave name]) (p/promise ctx))
:error (fn [ctx] (swap! log conj [:error name]) (p/promise ctx))})
(defn make-logging-handler [log]
(fn [request]
(swap! log conj [:handler])
request))
(defn make-async-logging-handler [log]
(fn [request]
(swap! log conj [:handler])
(p/promise request)))
(def request {:foo "bar"})
(def error (ex-info "oh no" {}))
(defn fail! [& _]
(throw (ex-info "Should never be called" {})))
(deftest setup-sync-test-test
(let [log (atom [])
response (-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-logging-handler log)]
(sc/execute request))]
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])
(fact
response => request)))
(deftest setup-async-test-test
(let [log (atom [])
response (-> [(make-async-logging-interceptor log :a)
(make-async-logging-interceptor log :b)
(make-async-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request))]
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])
(fact
response => request)))
(deftest async-b-sync-execute-test
(let [log (atom [])
response (-> [(make-logging-interceptor log :a)
(make-async-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-logging-handler log)]
(sc/execute request))]
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])
(fact
response => request)))
(deftest async-handler-test
(let [log (atom [])
response (-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request))]
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])
(fact
response => request)))
(deftest async-stack-sync-execute-test
(let [log (atom [])
response (-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request))]
(fact
response => request)
(fact
@log =eventually=> [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])))
(deftest async-stack-async-execute-test
(let [log (atom [])
response-p (promise)]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request (partial deliver response-p) fail!))
(fact
@response-p =eventually=> request)
(fact
@log =eventually=> [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]])))
(deftest async-execute-with-error-test
(let [log (atom [])
response-p (promise)]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(throw error))]
(sc/execute request fail! (partial deliver response-p)))
(fact
@response-p =eventually=> error)
(fact
@log =eventually=> [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]])))
(deftest async-execute-rejection-test
(let [log (atom [])
response-p (promise)]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(p/rejected error))]
(sc/execute request fail! (partial deliver response-p)))
(fact
@response-p =eventually=> error)
(fact
@log =eventually=> [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]])))
(deftest async-failing-handler-test
(let [log (atom [])]
(fact
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(p/resolved error))]
(sc/execute request))
=throws=> error)
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]])))
(deftest async-failing-handler-b-fixes-test
(let [log (atom [])]
(fact
(-> [(make-logging-interceptor log :a)
(assoc (make-async-logging-interceptor log :b)
:error (fn [ctx]
(swap! log conj [:error :b])
(p/promise
(-> ctx
(assoc :error nil)
(assoc :response :fixed-by-b)))))
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(p/resolved error))]
(sc/execute request))
=> :fixed-by-b)
(fact
@log => [[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:leave :a]])))
================================================
FILE: test/cljc/sieppari/async/core_async_test.cljc
================================================
(ns sieppari.async.core-async-test
(:require [clojure.test :refer [deftest is #?(:cljs async)]]
[sieppari.async :as as]
[sieppari.async.core-async]
[clojure.core.async :as a]))
(deftest async?-test
(is (as/async? (a/go "foo"))))
#?(:clj
(deftest core-async-continue-clj-promise-test
(let [respond (promise)]
(as/continue (a/go "foo") (partial deliver respond))
(is (= "foo" @respond))))
:cljs
(deftest core-async-continue-cljs-callback-test
(async done
(is (as/continue (a/go "foo")
(fn [response]
(is (= "foo" response))
(done)))))))
#?(:clj
(deftest core-async-catch-clj-promise-test
(let [respond (promise)]
(as/catch (a/go (Exception. "fubar")) (fn [_] (deliver respond "foo")))
(is (= "foo" @respond))))
:cljs
(deftest core-async-catch-cljs-callback-test
(async done
(is (as/continue (as/catch (a/go (js/Error. "fubar"))
(fn [_] "foo"))
(fn [response]
(is (= "foo" response))
(done)))))))
#?(:clj
(deftest await-test
(is (= "foo" (as/await (a/go "foo"))))))
================================================
FILE: test/cljc/sieppari/async/promesa_test.cljc
================================================
(ns sieppari.async.promesa-test
(:require [clojure.test :refer [deftest is #?(:cljs async)]]
[sieppari.async :as as]
[promesa.core :as p]
[promesa.exec :as px]))
(deftest async?-test
(is (as/async? (p/promise 1))))
#?(:clj
(deftest core-async-continue-cljs-callback-test
(let [respond (promise)
p (p/create
(fn [resolve _]
(px/schedule! 10 #(resolve "foo"))))]
(as/continue p respond)
(is (= @respond "foo"))))
:cljs
(deftest core-async-continue-cljs-callback-test
(let [p (p/create
(fn [resolve _]
(px/schedule! 10 #(resolve "foo"))))]
(async done
(is (as/continue p (fn [response]
(is (= "foo" response))
(done))))))))
#?(:clj
(deftest core-async-catch-cljs-callback-test
(let [respond (promise)
p (p/create
(fn [_ reject]
(px/schedule! 10 #(reject (Exception. "fubar")))))]
(as/catch p (fn [_] (respond "foo")))
(is (= @respond "foo"))))
:cljs
(deftest core-async-catch-cljs-callback-test
(let [p (p/create
(fn [_ reject]
(px/schedule! 10 #(reject (js/Error. "fubar")))))]
(async done
(is (as/continue (as/catch p (fn [_] "foo"))
(fn [response]
(is (= "foo" response))
(done))))))))
#?(:clj
(deftest await-test
(is (= "foo"
(as/await (p/create
(fn [resolve _]
(px/schedule! 10 #(resolve "foo")))))))))
================================================
FILE: test/cljc/sieppari/context_test.cljc
================================================
(ns sieppari.context-test
(:require [clojure.test :refer [deftest is testing]]
[sieppari.context :as sc]
[sieppari.interceptor :as si]
[sieppari.queue :as sq]))
(deftest terminate-test
(let [queue (sq/into-queue [{:name :a} {:name :b}])]
(is (= {:queue sq/empty-queue}
(sc/terminate {:queue queue})))
(is (= (sc/terminate {:queue queue} :the-response)
{:queue sq/empty-queue
:response :the-response}))))
(deftest inject-test
(let [queue (sq/into-queue [{:name :a} {:name :b}])]
(is (= {:queue (conj sq/empty-queue
(si/map->Interceptor {:name :x :enter nil :leave nil :error nil})
(si/map->Interceptor {:name :a :enter nil :leave nil :error nil})
(si/map->Interceptor {:name :b :enter nil :leave nil :error nil}))}
(sc/inject {:queue queue} {:name :x}))
"it should add the x interceptor at the head of the queue")
(is (instance? #?(:clj clojure.lang.PersistentQueue
:cljs cljs.core/PersistentQueue)
(:queue (sc/inject {:queue queue} {:name :x})))
"it should return a queue of interceptors")))
================================================
FILE: test/cljc/sieppari/interceptor_test.cljc
================================================
(ns sieppari.interceptor-test
(:require [clojure.test :refer [deftest is testing]]
[sieppari.interceptor :as si])
#?(:clj (:import (sieppari.interceptor Interceptor))))
(defn make-interceptor [value]
{:enter (fn [ctx]
(update ctx :foo + value))})
(defn make-handler [value]
(fn [request]
(+ request value)))
(deftest into-interceptor-test
(is (instance? #?(:clj Interceptor :cljs si/Interceptor) (si/into-interceptor {})) "result is record")
(is (fn? (:enter (si/into-interceptor str))) "functions can be made to interceptors")
(is (= {:request 41 :response 42}
(-> inc si/into-interceptor :enter (apply [{:request 41}]))) "functions are treated as request handlers")
(is (= {:foo 42}
(-> [make-interceptor 10]
si/into-interceptor
:enter
(apply [{:foo 32}]))) "vectors are evaluated, interceptor factory case")
(is (= {:request 32 :response 42}
(-> [make-handler 10]
si/into-interceptor
:enter
(apply [{:request 32}]))) "vectors are evaluated, handler factory case")
(let [i (si/into-interceptor {:name "test"})]
(is (identical? i (si/into-interceptor i)) "interceptors are already interceptors"))
(is (nil? (si/into-interceptor nil)) "nil punning"))
================================================
FILE: test/cljc/sieppari/queue_test.cljc
================================================
(ns sieppari.queue-test
(:require [clojure.test :refer [deftest is testing]]
[sieppari.queue :as sq]
[sieppari.interceptor :as si])
#?(:clj (:import (clojure.lang PersistentQueue)
(sieppari.interceptor Interceptor))))
(deftest into-queue-test
(is (instance? #?(:clj PersistentQueue :cljs cljs.core.PersistentQueue)
(sq/into-queue [{}])))
(is (instance? #?(:clj PersistentQueue :cljs cljs.core.PersistentQueue)
(sq/into-queue '({}))))
(is (instance? #?(:clj PersistentQueue :cljs cljs.core.PersistentQueue)
(sq/into-queue (cons {} nil))))
(let [the-queue (sq/into-queue [{}])]
(is (identical? the-queue (sq/into-queue the-queue))))
(is (every? (partial instance? #?(:clj Interceptor :cljs si/Interceptor))
(sq/into-queue [{}])))
(is (nil? (sq/into-queue [])))
(is (nil? (sq/into-queue '())))
(is (nil? (sq/into-queue nil))))
================================================
FILE: test/cljs/sieppari/core_async_test.cljs
================================================
(ns sieppari.core-async-test
(:require [clojure.test :refer [deftest is async]]
[clojure.core.async :refer [go]]
[sieppari.core :as sc]
[sieppari.async.core-async]))
(defn make-logging-interceptor [log name]
{:name name
:enter (fn [ctx] (swap! log conj [:enter name]) ctx)
:leave (fn [ctx] (swap! log conj [:leave name]) ctx)
:error (fn [ctx] (swap! log conj [:error name]) ctx)})
(defn make-async-logging-interceptor [log name]
{:name name
:enter (fn [ctx] (swap! log conj [:enter name]) (go ctx))
:leave (fn [ctx] (swap! log conj [:leave name]) (go ctx))
:error (fn [ctx] (swap! log conj [:error name]) (go ctx))})
(defn make-logging-handler [log]
(fn [request]
(swap! log conj [:handler])
request))
(defn make-async-logging-handler [log]
(fn [request]
(swap! log conj [:handler])
(go request)))
(def request {:foo "bar"})
(def error (ex-info "oh no" {}))
(defn fail! [& _]
(throw (ex-info "Should never be called" {})))
(deftest chan-happy-sync-test
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-logging-handler log)]
(sc/execute request
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]]))
(is (= response request))
(done))
fail!)))))
(deftest chan-happy-async-test
(async done
(let [log (atom [])]
(-> [(make-async-logging-interceptor log :a)
(make-async-logging-interceptor log :b)
(make-async-logging-interceptor log :c)
(make-logging-handler log)]
(sc/execute request
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]]))
(is (= response request))
(done))
fail!)))))
(deftest chan-async-b-sync-execute-test
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-async-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-logging-handler log)]
(sc/execute request
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]]))
(is (= response request))
(done))
fail!)))))
(deftest chan-async-handler-test
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]]))
(is (= response request))
(done))
fail!)))))
(deftest chan-async-execute-with-handler-throws-test
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(throw error))]
(sc/execute request
fail!
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]]))
(is (= response error))
(done)))))))
(deftest chan-async-execute-rejection-test
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(go error))]
(sc/execute request
fail!
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]]))
(is (= response error))
(done)))))))
(deftest chan-async-failing-handler-test
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(go error))]
(sc/execute request
fail!
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]]))
(is (= response error))
(done)))))))
(deftest chan-async-failing-handler-b-fixes-test
(let [make-fixing-error-b (fn [log]
(-> (make-async-logging-interceptor log :b)
(assoc :error (fn [ctx]
(swap! log conj [:error :b])
(go
(-> ctx
(assoc :error nil)
(assoc :response :fixed-by-b)))))))]
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-fixing-error-b log)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(go error))]
(sc/execute request
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:leave :a]]))
(is (= response :fixed-by-b))
(done))
fail!))))))
================================================
FILE: test/cljs/sieppari/core_execute_test.cljs
================================================
(ns sieppari.core-execute-test
(:require [clojure.test :refer-macros [deftest is testing async]]
[sieppari.core :as sc]
[sieppari.context :as sctx]))
;; Following tests use a test-chain that has some interceptors
;; that fail on each stage function (enter, leave, error). The
;; idea is that the tests override the expected stage functions
;; with test specific function. This ensures that no unexpected
;; stage functions are called.
;;
;; Make an interceptor with given name and set all stage functions
;; to report unexpected invocation. Tests should override expected
;; stages.
(defn unexpected [name stage]
(fn [ctx]
(throw (ex-info "unexpected invocation"
{:name name
:stage stage
:ctx ctx}))))
(defn make-test-interceptor [name]
{:name name
:enter (unexpected name :enter)
:leave (unexpected name :leave)
:error (unexpected name :error)})
; Test stack with three interceptors and a handler:
(def test-chain [(make-test-interceptor :a)
(make-test-interceptor :b)
(make-test-interceptor :c)
(unexpected :handler nil)])
(def a-index 0)
(def b-index 1)
(def c-index 2)
(def h-index 3)
;; Helper: always throws an exception with specific marker
;; in data part:
(defn fail! [& _]
(throw (ex-info "Should never be called" {})))
(def error (ex-info "oh no" {::error-marker true}))
(defn always-throw [ctx]
(throw error))
;; Helper: return error handler function that ensures
;; that `ctx` contains an exception caused by `always-throw`,
;; clears the exception and sets response to given response:
(defn error-handler-interceptor [response]
(fn [ctx]
(assert (not (some? (:response ctx))))
(assert (-> ctx :error ex-data (= {::error-marker true})))
(-> ctx
(dissoc :error)
(assoc :response response))))
;;
;; Tests:
;;
(deftest happy-case-test
(async done
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] identity)
(assoc-in [c-index :enter] identity)
(assoc-in [h-index] inc)
(assoc-in [c-index :leave] identity)
(assoc-in [b-index :leave] identity)
(assoc-in [a-index :leave] identity)
(sc/execute 41
(fn [response]
(is (= 42 response) "enable all enter and leave stages, use `inc` as handler")
(done))
fail!))))
(deftest enter-b-causes-exception-test
(async done
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] always-throw)
(assoc-in [b-index :error] identity)
(assoc-in [a-index :error] identity)
(sc/execute 41
fail!
(fn [err]
(is (= error err) ":b causes an exception")
(done))))))
(deftest enter-c-causes-exception-a-handles-test
(async done
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] identity)
(assoc-in [c-index :enter] always-throw)
(assoc-in [c-index :error] identity)
(assoc-in [b-index :error] identity)
(assoc-in [a-index :error] (error-handler-interceptor :fixed-by-a))
(sc/execute 41
(fn [response]
(is (= :fixed-by-a response) ":c enter causes an exception, :b sees error, :a handles")
(done))
fail!))))
(deftest enter-c-causes-exception-b-handles-test
(async done
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] identity)
(assoc-in [c-index :enter] always-throw)
(assoc-in [c-index :error] identity)
(assoc-in [b-index :error] (error-handler-interceptor :fixed-by-b))
(assoc-in [a-index :leave] identity)
(sc/execute 41
(fn [response]
(is (= :fixed-by-b response) ":c enter causes an exception, :b handles")
(done))
fail!))))
(deftest handler-causes-exception-b-handles-test
(async done
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] identity)
(assoc-in [c-index :enter] identity)
(assoc-in [h-index] always-throw)
(assoc-in [c-index :error] identity)
(assoc-in [b-index :error] (error-handler-interceptor :fixed-by-b))
(assoc-in [a-index :leave] identity)
(sc/execute 41
(fn [response]
(is (= :fixed-by-b response) ":c enter causes an exception, :b handles")
(done))
fail!))))
(deftest enter-b-sets-response-test
(async done
(-> test-chain
(assoc-in [a-index :enter] identity)
(assoc-in [b-index :enter] (fn [ctx] (sctx/terminate ctx :response-by-b)))
(assoc-in [b-index :leave] identity)
(assoc-in [a-index :leave] identity)
(sc/execute 41
(fn [response]
(is (= :response-by-b response) ":b sets the response, no invocation of :c nor :handler")
(done))
fail!))))
(deftest nil-response-test
(async done
(sc/execute [(constantly nil)] {}
(fn [response]
(is (= nil response) "nil response is allowed")
(done))
fail!)))
(deftest nil-interceptors-test
(async done
(sc/execute nil {}
(fn [response]
(is (= nil response) "interceptor chain can be nil")
(done))
fail!)))
(deftest empty-interceptors-test
(async done
(sc/execute [] {}
(fn [response]
(is (= nil response) "interceptor chain can be empty")
(done))
fail!)))
(defn make-logging-interceptor [name]
{:name name
:enter (fn [ctx]
(update ctx :request conj [:enter name]))
:leave (fn [ctx]
(update ctx :response conj [:leave name]))})
(defn logging-handler [request]
(conj request [:handler]))
(deftest inject-interceptor-test
(async done
(-> [(make-logging-interceptor :a)
{:enter (fn [ctx] (sctx/inject ctx (make-logging-interceptor :x)))}
(make-logging-interceptor :c)
logging-handler]
(sc/execute []
(fn [response]
(is (= [[:enter :a]
[:enter :x]
[:enter :c]
[:handler]
[:leave :c]
[:leave :x]
[:leave :a]]
response) ":b injects interceptor :x to chain, ensure the order is correct")
(done))
fail!))))
;; TODO: figure out how enqueue should work? Should enqueue add interceptors just
;; before the handler?
#_
(deftest enqueue-interceptor-test
(fact ":b enqueues interceptor :x to chain, ensure the order is correct"
(-> [(make-logging-interceptor :a)
{:enter (fn [ctx] (sc/enqueue ctx (make-logging-interceptor :x)))}
(make-logging-interceptor :c)
logging-handler]
(sctx/into-interceptors)
(sc/execute []))
=> [[:enter :a]
[:enter :c]
[:enter :x]
[:handler]
[:leave :x]
[:leave :c]
[:leave :a]]))
================================================
FILE: test/cljs/sieppari/native_promise_test.cljs
================================================
(ns sieppari.native-promise-test
(:require [clojure.test :as test :refer-macros [deftest is testing async]]
[sieppari.core :as sc]
[sieppari.async]))
(defn make-logging-interceptor [log name]
{:name name
:enter (fn [ctx] (swap! log conj [:enter name]) ctx)
:leave (fn [ctx] (swap! log conj [:leave name]) ctx)
:error (fn [ctx] (swap! log conj [:error name]) ctx)})
(defn make-async-logging-interceptor [log name]
{:name name
:enter (fn [ctx] (js/Promise. #(do (swap! log conj [:enter name]) (% ctx))))
:leave (fn [ctx] (js/Promise. #(do (swap! log conj [:leave name]) (% ctx))))
:error (fn [ctx] (js/Promise. #(do (swap! log conj [:error name]) (% ctx))))})
(defn make-logging-handler [log]
(fn [request]
(swap! log conj [:handler])
request))
(defn make-async-logging-handler [log]
(fn [request]
(js/Promise. #(do (swap! log conj [:handler]) (% request)))))
(def request {:foo "bar"})
(def error (ex-info "oh no" {}))
(defn fail! [& _]
(throw (ex-info "Should never be called" {})))
(deftest native-promise-happy-sync-test
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-logging-handler log)]
(sc/execute request
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]]))
(is (= response request))
(done))
fail!)))))
(deftest native-promise-happy-async-test
(async done
(let [log (atom [])]
(-> [(make-async-logging-interceptor log :a)
(make-async-logging-interceptor log :b)
(make-async-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]]))
(is (= response request))
(done))
fail!)))))
(deftest native-promise-async-b-sync-execute-test
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-async-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-logging-handler log)]
(sc/execute request
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]]))
(is (= response request))
(done))
fail!)))))
(deftest native-promise-async-handler-test
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]]))
(is (= response request))
(done))
fail!)))))
(deftest native-promise-async-stack-async-execute-test
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(make-async-logging-handler log)]
(sc/execute request
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:leave :c]
[:leave :b]
[:leave :a]]))
(is (= response request))
(done))
fail!)))))
(deftest native-promise-async-execute-handler-throws-test
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(throw error))]
(sc/execute request
fail!
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]]))
(is (= response error))
(done)))))))
(deftest native-promise-async-failing-handler-test
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(js/Promise.resolve error))]
(sc/execute request
fail!
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]]))
(is (= response error))
(done)))))))
(deftest native-promise-async-rejection-test
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-logging-interceptor log :b)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(js/Promise.reject error))]
(sc/execute request
fail!
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:error :a]]))
(is (= response error))
(done)))))))
(deftest native-promise-async-failing-handler-b-fixes-test
(let [make-fixing-error-b (fn [log]
(-> (make-async-logging-interceptor log :b)
(assoc :error (fn [ctx]
(swap! log conj [:error :b])
(js/Promise.resolve
(-> ctx
(assoc :error nil)
(assoc :response :fixed-by-b)))))))]
(async done
(let [log (atom [])]
(-> [(make-logging-interceptor log :a)
(make-fixing-error-b log)
(make-logging-interceptor log :c)
(fn [_]
(swap! log conj [:handler])
(js/Promise.resolve error))]
(sc/execute request
(fn [response]
(is (= @log
[[:enter :a]
[:enter :b]
[:enter :c]
[:handler]
[:error :c]
[:error :b]
[:leave :a]]))
(is (= response :fixed-by-b))
(done))
fail!))))))
================================================
FILE: tests.edn
================================================
#kaocha/v1
{:tests [{:id :unit
:test-paths ["test/cljc" "test/clj"]}
{:id :unit-cljs
:type :kaocha.type/cljs
:test-paths ["test/cljc" "test/cljs"]}]}
gitextract_i1aehsnv/ ├── .github/ │ └── workflows/ │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── HACKING.md ├── LICENSE ├── README.md ├── deps.edn ├── dev/ │ ├── lumo_runner.cljs │ └── user.clj ├── examples/ │ └── example/ │ ├── perf_testing.clj │ └── simple.cljc ├── package.json ├── project.clj ├── scripts/ │ ├── lein-modules │ └── test-self-host ├── src/ │ └── sieppari/ │ ├── async/ │ │ ├── core_async.cljc │ │ └── manifold.clj │ ├── async.cljc │ ├── context.cljc │ ├── core.cljc │ ├── interceptor.cljc │ ├── queue.cljc │ └── util.cljc ├── test/ │ ├── clj/ │ │ └── sieppari/ │ │ ├── async/ │ │ │ └── manifold_test.clj │ │ ├── core_async_test.clj │ │ ├── core_execute_test.clj │ │ ├── core_test.clj │ │ ├── manifold_test.clj │ │ └── promesa_test.clj │ ├── cljc/ │ │ └── sieppari/ │ │ ├── async/ │ │ │ ├── core_async_test.cljc │ │ │ └── promesa_test.cljc │ │ ├── context_test.cljc │ │ ├── interceptor_test.cljc │ │ └── queue_test.cljc │ └── cljs/ │ └── sieppari/ │ ├── core_async_test.cljs │ ├── core_execute_test.cljs │ └── native_promise_test.cljs └── tests.edn
Condensed preview — 38 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (122K chars).
[
{
"path": ".github/workflows/main.yml",
"chars": 302,
"preview": "name: Clojure CI\n\non: [push, pull_request]\n\njobs:\n build:\n\n runs-on: ubuntu-latest\n \n steps:\n - uses: actio"
},
{
"path": ".gitignore",
"chars": 143,
"preview": "/target\n/out\n/node_modules\npom.xml\npom.xml.asc\n/.lein-*\n/.nrepl-port\n/.cpcache\n/.cljs\n/node_modules\n.cljs_node_repl\n/.lu"
},
{
"path": "CHANGELOG.md",
"chars": 1307,
"preview": "# 0.0.0-alpha13 (2020-05-18)\n# 0.0.0-alpha12 (2020-05-18)\n# 0.0.0-alpha11 (2020-05-18)\n\n* rebuild without dev-dependenci"
},
{
"path": "HACKING.md",
"chars": 339,
"preview": "# Testing\n\n## CLI\n\n```shell script\nlein kaocha # run clj and cljs tests\n```\n\n`kaocha-cljs` [requires](https://github.com"
},
{
"path": "LICENSE",
"chars": 14198,
"preview": "Eclipse Public License - v 2.0\n\n THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE\n PUBLIC LICE"
},
{
"path": "README.md",
"chars": 7515,
"preview": "# sieppari [](https://cljdoc.org/d/metosin/sieppari/CURRENT)\n\n"
},
{
"path": "deps.edn",
"chars": 186,
"preview": ";; Do *not* add deps, aliases etc. here. Use the existing project.clj instead\n;; this is for the sole purpose of exposin"
},
{
"path": "dev/lumo_runner.cljs",
"chars": 850,
"preview": "(ns sieppari.lumo-runner\n (:require [cljs.test :as test :refer-macros [run-tests]]\n [lumo.core :as lumo]\n "
},
{
"path": "dev/user.clj",
"chars": 183,
"preview": "(ns user\n (:require [clojure.tools.namespace.repl :as repl]\n [kaocha.repl :as kaocha]))\n\n(def reset repl/ref"
},
{
"path": "examples/example/perf_testing.clj",
"chars": 7512,
"preview": "(ns example.perf-testing\n (:require [criterium.core :as criterium]\n [sieppari.core :as s]\n [siepp"
},
{
"path": "examples/example/simple.cljc",
"chars": 532,
"preview": "(ns example.simple\n (:require [sieppari.core :as s]))\n\n(def times-2-interceptor\n {:enter (fn [ctx] (update-in ctx [:re"
},
{
"path": "package.json",
"chars": 396,
"preview": "{\n \"name\": \"sieppari\",\n \"description\": \"Small, fast, and complete interceptor library.\",\n \"devDependencies\": {\n \"w"
},
{
"path": "project.clj",
"chars": 2120,
"preview": "(defproject metosin/sieppari \"0.0.0-alpha13\"\n :description \"Small, fast, and complete interceptor library.\"\n :url \"htt"
},
{
"path": "scripts/lein-modules",
"chars": 186,
"preview": "#!/bin/bash\n\nset -e\n\nfor ext in sieppari.core \\\n sieppari.async.core-async \\\n sieppari.async.deref \\"
},
{
"path": "scripts/test-self-host",
"chars": 139,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\ncp=\"src:test/cljs:test/cljc:$(clojure -Srepro -A:self-host -Spath)\"\n\nlumo -K -c \"$cp\" de"
},
{
"path": "src/sieppari/async/core_async.cljc",
"chars": 575,
"preview": "(ns sieppari.async.core-async\n (:require [sieppari.async :as sa]\n [sieppari.util :refer [exception?]]\n "
},
{
"path": "src/sieppari/async/manifold.clj",
"chars": 431,
"preview": "(ns sieppari.async.manifold\n (:require [sieppari.async :as sa]\n [manifold.deferred :as d]))\n\n(extend-protoco"
},
{
"path": "src/sieppari/async.cljc",
"chars": 1645,
"preview": "(ns sieppari.async\n #?(:clj (:refer-clojure :exclude [await]))\n (:require [sieppari.util :refer [exception?]])\n #?(:c"
},
{
"path": "src/sieppari/context.cljc",
"chars": 1968,
"preview": "(ns sieppari.context\n (:require [sieppari.queue :as q]))\n\n(defprotocol Context\n (context? [this]))\n\n(extend-protocol C"
},
{
"path": "src/sieppari/core.cljc",
"chars": 4400,
"preview": "(ns sieppari.core\n #?(:cljs (:refer-clojure :exclude [iter]))\n (:require [sieppari.queue :as q]\n [sieppari."
},
{
"path": "src/sieppari/interceptor.cljc",
"chars": 1471,
"preview": "(ns sieppari.interceptor\n (:require [sieppari.async :as a]\n [sieppari.util :refer [exception?]]))\n\n(defrecor"
},
{
"path": "src/sieppari/queue.cljc",
"chars": 1184,
"preview": "(ns sieppari.queue\n (:require [sieppari.interceptor :as i]))\n\n(defprotocol IntoQueue\n (into-queue [t]))\n\n(def empty-qu"
},
{
"path": "src/sieppari/util.cljc",
"chars": 92,
"preview": "(ns sieppari.util)\n\n(defn exception? [e]\n (instance? #?(:clj Exception :cljs js/Error) e))\n"
},
{
"path": "test/clj/sieppari/async/manifold_test.clj",
"chars": 1070,
"preview": "(ns sieppari.async.manifold-test\n (:require [clojure.test :refer :all]\n [testit.core :refer :all]\n "
},
{
"path": "test/clj/sieppari/core_async_test.clj",
"chars": 7944,
"preview": "(ns sieppari.core-async-test\n (:require [clojure.test :refer :all]\n [testit.core :refer :all]\n [s"
},
{
"path": "test/clj/sieppari/core_execute_test.clj",
"chars": 10085,
"preview": "(ns sieppari.core-execute-test\n (:require [clojure.test :refer :all]\n [testit.core :refer :all]\n "
},
{
"path": "test/clj/sieppari/core_test.clj",
"chars": 2760,
"preview": "(ns sieppari.core-test\n (:require [clojure.test :refer :all]\n [testit.core :refer :all]\n [sieppar"
},
{
"path": "test/clj/sieppari/manifold_test.clj",
"chars": 7428,
"preview": "(ns sieppari.manifold-test\n (:require [clojure.test :refer :all]\n [testit.core :refer :all]\n [sie"
},
{
"path": "test/clj/sieppari/promesa_test.clj",
"chars": 7355,
"preview": "(ns sieppari.promesa-test\n (:require [clojure.test :refer :all]\n [testit.core :refer :all]\n [siep"
},
{
"path": "test/cljc/sieppari/async/core_async_test.cljc",
"chars": 1281,
"preview": "(ns sieppari.async.core-async-test\n (:require [clojure.test :refer [deftest is #?(:cljs async)]]\n [sieppari."
},
{
"path": "test/cljc/sieppari/async/promesa_test.cljc",
"chars": 1709,
"preview": "(ns sieppari.async.promesa-test\n (:require [clojure.test :refer [deftest is #?(:cljs async)]]\n [sieppari.asy"
},
{
"path": "test/cljc/sieppari/context_test.cljc",
"chars": 1232,
"preview": "(ns sieppari.context-test\n (:require [clojure.test :refer [deftest is testing]]\n [sieppari.context :as sc]\n "
},
{
"path": "test/cljc/sieppari/interceptor_test.cljc",
"chars": 1320,
"preview": "(ns sieppari.interceptor-test\n (:require [clojure.test :refer [deftest is testing]]\n [sieppari.interceptor :"
},
{
"path": "test/cljc/sieppari/queue_test.cljc",
"chars": 964,
"preview": "(ns sieppari.queue-test\n (:require [clojure.test :refer [deftest is testing]]\n [sieppari.queue :as sq]\n "
},
{
"path": "test/cljs/sieppari/core_async_test.cljs",
"chars": 8054,
"preview": "(ns sieppari.core-async-test\n (:require [clojure.test :refer [deftest is async]]\n [clojure.core.async :refer"
},
{
"path": "test/cljs/sieppari/core_execute_test.cljs",
"chars": 7602,
"preview": "(ns sieppari.core-execute-test\n (:require [clojure.test :refer-macros [deftest is testing async]]\n [sieppari"
},
{
"path": "test/cljs/sieppari/native_promise_test.cljs",
"chars": 9360,
"preview": "(ns sieppari.native-promise-test\n (:require [clojure.test :as test :refer-macros [deftest is testing async]]\n "
},
{
"path": "tests.edn",
"chars": 211,
"preview": "#kaocha/v1\n{:tests [{:id :unit\n :test-paths [\"test/cljc\" \"test/clj\"]}\n {:id :unit-cljs\n"
}
]
About this extraction
This page contains the full source code of the metosin/sieppari GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 38 files (113.3 KB), approximately 30.0k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.