diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..4abd42ad --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Tests +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Prepare java + uses: actions/setup-java@v5 + with: + distribution: 'zulu' + java-version: '11' + + - name: Install clojure tools + uses: DeLaGuardo/setup-clojure@13.4 + with: + lein: 2.12.0 + + - name: Cache clojure dependencies + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: cljdeps-${{ hashFiles('project.clj') }} + restore-keys: cljdeps- + + - name: Run tests + run: lein test-all diff --git a/.gitignore b/.gitignore index d4074226..7d58bdd6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ pom.xml.asc *.class /.lein-* /.nrepl-port +/.clj-kondo \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f0bfe7e7..00000000 --- a/.travis.yml +++ /dev/null @@ -1,2 +0,0 @@ -language: clojure -script: lein test-all diff --git a/HISTORY.md b/CHANGELOG.md similarity index 76% rename from HISTORY.md rename to CHANGELOG.md index c2a7f0f7..e1765751 100644 --- a/HISTORY.md +++ b/CHANGELOG.md @@ -1,3 +1,58 @@ +## 1.7.2 (2025-09-15) + +* Updated Medley dependency to 1.9.0 (see #222) +* Updated Ring dependency to 1.15.1 +* Updated Ring-Codec dependency to 1.3.0 +* Updated tools.macro dependency to 0.2.1 + +## 1.7.1 (2024-01-31) + +* Updated Ring dependency to 1.11.0 +* Updated minimum Clojure version to 1.9.0 + +## 1.7.0 (2022-05-21) + +* Added `:compojure/route-context` key to requests (see #212) +* Updated Ring dependency to 1.9.3 +* Updated Ring-Codec dependency to 1.2.0 + +## 1.6.3 (2022-05-12) + +* Updated Medley dependency to 1.4.0 (see #206) + +## 1.6.2 (2020-08-04) + +* Fixed exception when coercion in context fails (see #184) +* Updated Ring dependency to 1.8.1 +* Updated Ring-Codec dependency to 1.1.2 +* Updated Medley dependency to 1.3.0 + +## 1.6.1 (2018-04-15) + +* Updated Ring dependency to 1.6.3 +* Updated Ring-Codec dependency to 1.1.0 +* Updated Clout dependency to 2.2.1 + +## 1.6.0 (2017-05-03) + +* Update Ring dependency to 1.6.0 +* Fixed middleware ordering on `wrap-routes` (see #157) +* Fixed `Renderable` protocol to use `IPersistentMap` (see #167) + +## 1.6.0-beta3 (2017-01-10) + +* Updated Ring dependency to 1.6.0-beta7 to fix path traversal vulnerability + +## 1.6.0-beta2 (2016-12-03) + +* Fixed bug when route called asynchronously with non-matching method +* Added asynchronous support to compojure.route/not-found + +## 1.6.0-beta1 (2016-07-22) + +* Updated Ring dependency to 1.6.0-beta4 +* Added support for asynchronous handlers + ## 1.5.2 (2017-01-10) * Updated Ring dependency to 1.5.1 to fix path traversal vulnerability @@ -5,7 +60,7 @@ ## 1.5.1 (2016-06-19) * Fixed coercions for functions that return false (see #150) -* Fixed contexts with a route of "/" (seee #125) +* Fixed contexts with a route of "/" (see #125) * Updated Ring dependency to 1.5.0 * Updated Medley dependency to 0.8.2 @@ -26,7 +81,6 @@ * Updated Ring dependency to 1.4.0 ## 1.3.4 (2015-05-08) - * Updated Clout dependency to 2.1.2 to fix Clojure 1.7.0-beta2 issue (see clout#24) ## 1.3.3 (2015-01-01) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..bf2cf212 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing Guidelines + +**Do** follow [the seven rules of a great Git commit message][1]. + +**Do** follow [the Clojure Style Guide][2]. + +**Do** include tests for your change when appropriate. + +**Do** ensure that the CI checks pass. + +**Do** squash the commits in your PR to remove corrections +irrelevant to the code history, once the PR has been reviewed. + +**Do** feel free to pester the project maintainers about the PR if it +hasn't been responded to. Sometimes notifications can be missed. + +**Don't** include more than one feature or fix in a single PR. + +**Don't** include changes unrelated to the purpose of the PR. This +includes changing the project version number, adding lines to the +`.gitignore` file, or changing the indentation or formatting. + +**Don't** open a new PR if changes are requested. Just push to the +same branch and the PR will be updated. + +**Don't** overuse vertical whitespace; avoid multiple sequential blank +lines. + +**Don't** docstring private vars or functions. + +[1]: https://chris.beams.io/posts/git-commit/#seven-rules +[2]: https://github.com/bbatsov/clojure-style-guide diff --git a/LICENSE.html b/LICENSE.html deleted file mode 100644 index 813c07d8..00000000 --- a/LICENSE.html +++ /dev/null @@ -1,261 +0,0 @@ - - - - -
- -Eclipse Public License - v 1.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 -code and documentation 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 additions to the -Program which: (i) are separate modules of software distributed in -conjunction with the Program under their own license agreement, and (ii) -are not derivative works of the Program.
- -"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, including all Contributors.
- -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, in -source code and object code form.
- -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 and object code 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.
- -3. REQUIREMENTS
- -A Contributor may choose to distribute the Program in object code -form under its own license agreement, provided that:
- -a) it complies with the terms and conditions of this -Agreement; and
- -b) its license agreement:
- -i) effectively disclaims on behalf of all 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 Contributors -all liability for damages, including direct, indirect, special, -incidental and consequential damages, such as lost profits;
- -iii) states that any provisions which differ from this -Agreement are offered by that Contributor alone and not by any other -party; and
- -iv) states that source code for the Program is available -from such Contributor, and informs licensees how to obtain it in a -reasonable manner on or through a medium customarily used for software -exchange.
- -When the Program is made available in source code form:
- -a) it must be made available under this Agreement; and
- -b) a copy of this Agreement must be included with each -copy of the Program.
- -Contributors may not remove or alter any copyright notices contained -within the Program.
- -Each Contributor must identify itself as the originator of its -Contribution, if any, in a manner that reasonably allows subsequent -Recipients to identify the originator of the Contribution.
- -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, 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, 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.
- -This Agreement is governed by the laws of the State of New York and -the intellectual property laws of the United States of America. No party -to this Agreement will bring a legal action under this Agreement more -than one year after the cause of action arose. Each party waives its -rights to a jury trial in any resulting litigation.
- - - - diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..8f48f685 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,204 @@ +Eclipse Public License - v 1.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 code and + documentation 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 +additions to the Program which: (i) are separate modules of software +distributed in conjunction with the Program under their own license agreement, +and (ii) are not derivative works of the Program. +"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, +including all Contributors. + +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, in source code and object code form. + + 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 and + object code 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. + +3. REQUIREMENTS +A Contributor may choose to distribute the Program in object code form under +its own license agreement, provided that: + + a) it complies with the terms and conditions of this Agreement; and + + b) its license agreement: + i) effectively disclaims on behalf of all 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 Contributors all liability + for damages, including direct, indirect, special, incidental and + consequential damages, such as lost profits; + iii) states that any provisions which differ from this Agreement are + offered by that Contributor alone and not by any other party; and + iv) states that source code for the Program is available from such + Contributor, and informs licensees how to obtain it in a reasonable + manner on or through a medium customarily used for software + exchange. + +When the Program is made available in source code form: + + a) it must be made available under this Agreement; and + + b) a copy of this Agreement must be included with each copy of the + Program. +Contributors may not remove or alter any copyright notices contained within +the Program. + +Each Contributor must identify itself as the originator of its Contribution, +if any, in a manner that reasonably allows subsequent Recipients to identify +the originator of the Contribution. + +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, 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, 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. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to this +Agreement will bring a legal action under this Agreement more than one year +after the cause of action arose. Each party waives its rights to a jury trial +in any resulting litigation. diff --git a/README.md b/README.md index 73de64d8..0dc6e047 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,22 @@ -# Compojure - -[](https://travis-ci.org/weavejester/compojure) +# Compojure [](https://github.com/weavejester/compojure/actions/workflows/test.yml) Compojure is a small routing library for [Ring][1] that allows web applications to be composed of small, independent parts. ## Installation -Add the following dependency to your `project.clj` file: +Add the following dependency to your deps.edn file: + + compojure/compojure {:mvn/version "1.7.2"} + +Or to your Leiningen project file: - [compojure "1.5.2"] + [compojure "1.7.2"] ## Documentation * [Wiki](https://github.com/weavejester/compojure/wiki) -* [API Docs](http://weavejester.github.com/compojure) +* [API Docs](http://weavejester.github.io/compojure) ## Community @@ -43,6 +45,6 @@ Also refer to the [Getting Started][2] page on the wiki. ## License -Copyright © 2016 James Reeves +Copyright © 2025 James Reeves Distributed under the Eclipse Public License, the same as Clojure. diff --git a/project.clj b/project.clj index 44bdd766..16c286a4 100644 --- a/project.clj +++ b/project.clj @@ -1,24 +1,26 @@ -(defproject compojure "1.5.2" +(defproject compojure "1.7.2" :description "A concise routing library for Ring" :url "https://github.com/weavejester/compojure" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} - :dependencies [[org.clojure/clojure "1.7.0"] - [org.clojure/tools.macro "0.1.5"] - [clout "2.1.2"] - [medley "0.8.2"] - [ring/ring-core "1.5.1"] - [ring/ring-codec "1.0.1"]] - :plugins [[lein-codox "0.9.5"]] + :dependencies [[org.clojure/clojure "1.9.0"] + [org.clojure/tools.macro "0.2.1"] + [clout "2.2.1"] + [dev.weavejester/medley "1.9.0"] + [ring/ring-core "1.15.1"] + [ring/ring-codec "1.3.0"]] + :plugins [[lein-codox "0.10.8"]] :codox {:output-path "codox" :metadata {:doc/format :markdown} :source-uri "http://github.com/weavejester/compojure/blob/{version}/{filepath}#L{line}"} :aliases - {"test-all" ["with-profile" "default:+1.8" "test"]} + {"test-all" ["with-profile" "default:+1.10:+1.11:+1.12" "test"]} :profiles {:dev {:jvm-opts ^:replace [] - :dependencies [[ring/ring-mock "0.3.0"] - [criterium "0.4.3"] + :dependencies [[ring/ring-mock "0.6.2"] + [criterium "0.4.6"] [javax.servlet/servlet-api "2.5"]]} - :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}}) + :1.10 {:dependencies [[org.clojure/clojure "1.10.0"]]} + :1.11 {:dependencies [[org.clojure/clojure "1.11.4"]]} + :1.12 {:dependencies [[org.clojure/clojure "1.12.2"]]}}) diff --git a/src/compojure/core.clj b/src/compojure/core.clj index 93fd7d1f..4216cbea 100644 --- a/src/compojure/core.clj +++ b/src/compojure/core.clj @@ -9,25 +9,31 @@ (:require [compojure.response :as response] [clojure.tools.macro :as macro] [clout.core :as clout] + [ring.middleware.head :refer [wrap-head]] [ring.util.codec :as codec] [medley.core :refer [map-vals]])) -(defn- method-matches? [method request] - (let [request-method (request :request-method) - form-method (or (get-in request [:form-params "_method"]) - (get-in request [:multipart-params "_method"]))] - (if (and form-method (= request-method :post)) - (.equalsIgnoreCase (name method) form-method) - (= method request-method)))) - -(defn- if-method [method handler] - (fn [request] - (cond - (or (nil? method) (method-matches? method request)) - (handler request) - (and (= :get method) (= :head (:request-method request))) - (if-let [response (handler request)] - (assoc response :body nil))))) +(defn- form-method [request] + (or (get-in request [:form-params "_method"]) + (get-in request [:multipart-params "_method"]))) + +(defn- method-matches? [request method] + (or (nil? method) + (let [request-method (:request-method request)] + (case request-method + :head + (or (= method :head) (= method :get)) + :post + (if-let [fm (form-method request)] + (.equalsIgnoreCase (name method) fm) + (= method :post)) + ;else + (= request-method method))))) + +(defn- head-response [response request method] + (if (and response (= :get method) (= :head (:request-method request))) + (assoc response :body nil) + response)) (defn- decode-route-params [params] (map-vals codec/url-decode params)) @@ -39,10 +45,9 @@ (let [path (:compojure/path request)] (clout/route-matches route (cond-> request path (assoc :path-info path))))) -(defn- if-route [route handler] - (fn [request] - (if-let [params (route-matches route request)] - (handler (assoc-route-params request (decode-route-params params)))))) +(defn- route-request [request route] + (if-let [params (route-matches route request)] + (assoc-route-params request (decode-route-params params)))) (defn- literal? [x] (if (coll? x) @@ -116,32 +121,61 @@ `(let [~bindings ~request] ~@body))) (defn- wrap-route-middleware [handler] - (fn [request] - (if-let [mw (:route-middleware request)] - ((mw handler) request) - (handler request)))) + (fn + ([request] + (if-let [mw (:route-middleware request)] + ((mw handler) request) + (handler request))) + ([request respond raise] + (if-let [mw (:route-middleware request)] + ((mw handler) request respond raise) + (handler request respond raise))))) (defn- wrap-route-info [handler route-info] - (fn [request] - (handler (assoc request :compojure/route route-info)))) + (fn + ([request] + (handler (assoc request :compojure/route route-info))) + ([request respond raise] + (handler (assoc request :compojure/route route-info) respond raise)))) + +(defn- wrap-route-matches [handler method path] + (fn + ([request] + (if (method-matches? request method) + (if-let [request (route-request request path)] + (-> (handler request) + (head-response request method))))) + ([request respond raise] + (if (method-matches? request method) + (if-let [request (route-request request path)] + (handler request #(respond (head-response % request method)) raise) + (respond nil)) + (respond nil))))) + +(defn- wrap-response [handler] + (fn + ([request] + (response/render (handler request) request)) + ([request respond raise] + (response/send (handler request) request respond raise)))) (defn make-route "Returns a function that will only call the handler if the method and path match the request." [method path handler] - (let [route-info [(or method :any) (str path)]] - (if-method method - (if-route path - (-> (fn [request] (response/render (handler request) request)) - (wrap-route-middleware) - (wrap-route-info route-info)))))) + (-> handler + (wrap-response) + (wrap-route-middleware) + (wrap-route-info [(or method :any) (str path)]) + (wrap-route-matches method path))) (defn compile-route "Compile a route in the form `(method path bindings & body)` into a function. Used to create custom route macros." [method path bindings body] `(make-route - ~method ~(prepare-route path) + ~method + ~(prepare-route path) (fn [request#] (let-request [~bindings request#] ~@body)))) @@ -153,7 +187,17 @@ (defn routes "Create a Ring handler by combining several handlers into one." [& handlers] - #(apply routing % handlers)) + (fn + ([request] + (apply routing request handlers)) + ([request respond raise] + (letfn [(f [handlers] + (if (seq handlers) + (let [handler (first handlers) + respond' #(if % (respond %) (f (rest handlers)))] + (handler request respond' raise)) + (respond nil)))] + (f handlers))))) (defmacro defroutes "Define a Ring handler function from a sequence of routes. The name may @@ -194,34 +238,31 @@ [path args & body] (compile-route nil path args body)) +(defn ^:no-doc make-rfn [handler] + (-> handler + wrap-response + wrap-route-middleware + wrap-head)) + (defmacro rfn "Generate a route that matches any method and path." [args & body] - `(#'wrap-route-middleware - (fn [request#] - (let [result# (let-request [~args request#] ~@body) - response# (response/render result# request#)] - (if (and response# (= :head (:request-method request#))) - (assoc response# :body nil) - response#))))) + `(make-rfn (fn [request#] (let-request [~args request#] ~@body)))) (defn- remove-suffix [path suffix] (subs path 0 (- (count path) (count suffix)))) -(defn- if-context [route handler] - (if (#{":__path-info" "/:__path-info"} (:source route)) - handler - (fn [request] - (if-let [params (clout/route-matches route request)] - (let [uri (:uri request) - path (:path-info request uri) - context (or (:context request) "") - subpath (:__path-info params) - params (dissoc params :__path-info)] - (handler - (-> request - (assoc-route-params (decode-route-params params)) - (assoc :path-info (if (= subpath "") "/" subpath) - :context (remove-suffix uri subpath))))))))) +(defn- context-request [request route route-context] + (if-let [params (clout/route-matches route request)] + (let [uri (:uri request) + path (:path-info request uri) + context (or (:context request) "") + subpath (:__path-info params) + params (dissoc params :__path-info)] + (-> request + (assoc-route-params (decode-route-params params)) + (assoc :path-info (if (= subpath "") "/" subpath) + :context (remove-suffix uri subpath)) + (update :compojure/route-context str route-context))))) (defn- context-route [route] (let [re-context {:__path-info #"|/.*"}] @@ -239,6 +280,26 @@ :else `(clout/route-compile (str ~route ":__path-info") ~re-context)))) +(defn ^:no-doc make-context [route path make-handler] + (letfn [(handler + ([request] + (when-let [context-handler (make-handler request)] + (context-handler request))) + ([request respond raise] + (if-let [context-handler (make-handler request)] + (context-handler request respond raise) + (respond nil))))] + (if (#{":__path-info" "/:__path-info"} (:source route)) + handler + (fn + ([request] + (if-let [request (context-request request route path)] + (handler request))) + ([request respond raise] + (if-let [request (context-request request route path)] + (handler request respond raise) + (respond nil))))))) + (defmacro context "Give all routes in the form a common path prefix and set of bindings. @@ -249,11 +310,12 @@ (GET \"/profile\" [] ...) (GET \"/settings\" [] ...))" [path args & routes] - `(#'if-context + `(make-context ~(context-route path) + ~path (fn [request#] (let-request [~args request#] - (routing request# ~@routes))))) + (routes ~@routes))))) (defmacro let-routes "Takes a vector of bindings and a body of routes. @@ -265,17 +327,31 @@ `(let ~bindings (routes ~@body))) (defn- pre-init [middleware] - (let [proxy (middleware (fn [req] ((:route-handler req) req)))] + (let [proxy (middleware + (fn + ([request] + ((:route-handler request) request)) + ([request respond raise] + ((:route-handler request) request respond raise))))] (fn [handler] - (fn [request] - (proxy (assoc request :route-handler handler)))))) + (let [prep-request #(assoc % :route-handler handler)] + (fn + ([request] + (proxy (prep-request request))) + ([request respond raise] + (proxy (prep-request request) respond raise))))))) (defn wrap-routes "Apply a middleware function to routes after they have been matched." ([handler middleware] - (let [middleware (pre-init middleware)] - (fn [request] - (let [mw (:route-middleware request identity)] - (handler (assoc request :route-middleware (comp middleware mw))))))) + (let [middleware (pre-init middleware) + prep-request (fn [request] + (let [mw (:route-middleware request identity)] + (assoc request :route-middleware (comp mw middleware))))] + (fn + ([request] + (handler (prep-request request))) + ([request respond raise] + (handler (prep-request request) respond raise))))) ([handler middleware & args] (wrap-routes handler #(apply middleware % args)))) diff --git a/src/compojure/middleware.clj b/src/compojure/middleware.clj index adb5e142..bbb2cb52 100644 --- a/src/compojure/middleware.clj +++ b/src/compojure/middleware.clj @@ -10,8 +10,14 @@ (.substring uri 0 (dec (.length uri))) uri)) -(defn- redirect-to-canonical [request] - (resp/redirect (:compojure/path request) 301)) +(defn- redirect-to-canonical + ([request] + (resp/redirect (:compojure/path request) 301)) + ([request respond raise] + (respond (redirect-to-canonical request)))) + +(defn- assoc-path [request path] + (assoc request :compojure/path path)) (defn wrap-canonical-redirect "Middleware that permanently redirects any non-canonical route to its @@ -22,8 +28,14 @@ (wrap-canonical-redirect handler remove-trailing-slash)) ([handler make-canonical] (let [redirect-handler (wrap-routes handler (constantly redirect-to-canonical))] - (fn [{uri :uri :as request}] - (let [canonical-uri (make-canonical uri)] - (if (= uri canonical-uri) - (handler request) - (redirect-handler (assoc request :compojure/path canonical-uri)))))))) + (fn + ([{uri :uri :as request}] + (let [canonical-uri (make-canonical uri)] + (if (= uri canonical-uri) + (handler request) + (redirect-handler (assoc-path request canonical-uri))))) + ([{uri :uri :as request} respond raise] + (let [canonical-uri (make-canonical uri)] + (if (= uri canonical-uri) + (handler request respond raise) + (redirect-handler (assoc-path request canonical-uri) respond raise)))))))) diff --git a/src/compojure/response.clj b/src/compojure/response.clj index 59ea90fc..0c9bdb67 100644 --- a/src/compojure/response.clj +++ b/src/compojure/response.clj @@ -1,5 +1,6 @@ (ns compojure.response "A protocol for generating Ring response maps" + (:refer-clojure :exclude [send]) (:require [ring.util.mime-type :as mime] [ring.util.response :as response])) @@ -10,8 +11,21 @@ This protocol supports rendering strings, maps, functions, refs, files, seqs, input streams and URLs by default, and may be extended to cover many custom types." - (render [this request] - "Render the object into a form suitable for the given request map.")) + (render [x request] + "Render `x` into a form suitable for the given request map.")) + +(defprotocol Sendable + "A protocol that tells Compojure how to handle the return value of + asynchronous routes, should they require special attention." + (send* [x request respond raise])) + +(defn send + "Send `x` as a Ring response. Checks to see if `x` satisfies [[Sendable]], + and if not, falls back to [[Renderable]]." + [x request respond raise] + (if (satisfies? Sendable x) + (send* x request respond raise) + (respond (render x request)))) (defn- guess-content-type [response name] (if-let [mime-type (mime/ext-mime-type (str name))] @@ -25,7 +39,7 @@ (render [body _] (-> (response/response body) (response/content-type "text/html; charset=utf-8"))) - clojure.lang.APersistentMap + clojure.lang.IPersistentMap (render [resp-map _] (merge (with-meta (response/response "") (meta resp-map)) resp-map)) @@ -49,3 +63,11 @@ (render [url _] (-> (response/url-response url) (guess-content-type url)))) + +(extend-protocol Sendable + clojure.lang.Fn + (send* [func request respond raise] + (func request #(send % request respond raise) raise)) + clojure.lang.MultiFn + (send* [func request respond raise] + (func request #(send % request respond raise) raise))) diff --git a/src/compojure/route.clj b/src/compojure/route.clj index 23419496..526d59cb 100644 --- a/src/compojure/route.clj +++ b/src/compojure/route.clj @@ -56,7 +56,10 @@ supplied response body. The response body may be anything accepted by the [[response/render]] function." [body] - (fn [request] - (-> (response/render body request) - (status 404) - (cond-> (= (:request-method request) :head) (assoc :body nil))))) + (fn handler + ([request] + (-> (response/render body request) + (status 404) + (cond-> (= (:request-method request) :head) (assoc :body nil)))) + ([request respond raise] + (respond (handler request))))) diff --git a/test/compojure/core_test.clj b/test/compojure/core_test.clj index 8790532a..7576ad73 100644 --- a/test/compojure/core_test.clj +++ b/test/compojure/core_test.clj @@ -61,6 +61,16 @@ (is (nil? ((GET "/foo/:x" [x :<< coercions/as-int] (str x)) (mock/request :get "/foo/bar"))))) + (testing "nil coercions in contexts" + (is (not (nil? ((context "/foo/:x" [x] (GET "/" [] (str x))) + (mock/request :get "/foo/bar"))))) + (is (not (nil? ((context "/foo/:x" [x :<< coercions/as-int] (GET "/" [] (str x))) + (mock/request :get "/foo/100"))))) + (is (not (nil? ((context "/foo/:x" [x :<< #(Boolean/valueOf %)] (GET "/" [] (str x))) + (mock/request :get "/foo/false"))))) + (is (nil? ((context "/foo/:x" [x :<< coercions/as-int] (GET "/" [] (str x))) + (mock/request :get "/foo/bar"))))) + (testing "map arguments" ((GET "/foo" {params :params} (is (= params {:x "a", :y "b"})) @@ -186,6 +196,23 @@ "/foo/10/b%20r" "/b%20r" "/bar/10" nil))) + (testing ":compojure/route-context key" + (let [handler (context "/foo/:id" [_] :compojure/route-context)] + (are [url route-context] (= (handler (mock/request :get url)) route-context) + "/foo/10" "/foo/:id" + "/foo/10/bar" "/foo/:id" + "/foo/10/b%20r" "/foo/:id" + "/bar/10" nil))) + + (testing ":compojure/route-context key in nested context" + (let [handler (context "/foo/:foo-id" [_] + (context "/bar/:bar-id" [_] :compojure/route-context))] + (are [url route-context] (= (handler (mock/request :get url)) route-context) + "/foo/1/bar/2" "/foo/:foo-id/bar/:bar-id" + "/foo/1/bar/2/baz" "/foo/:foo-id/bar/:bar-id" + "/foo/1/bar/2/b%20r" "/foo/:foo-id/bar/:bar-id" + "/bar/10" nil))) + (testing "routes" (let [handler (context "/foo/:id" [id] (GET "/" [] "root") @@ -286,7 +313,21 @@ middleware (fn [h] (fn [r] (reset! matched (:compojure/route r)) (h r))) handler (wrap-routes route middleware) response (handler (mock/request :get "/foo"))] - (is (= @matched [:get "/foo"]))))) + (is (= @matched [:get "/foo"])))) + + (testing "nested route-middlewares are applied in order" + (let [mw (fn [handler value] + (fn [req] + (let [resp (handler (update req :stack str value))] + (update resp :body str value)))) + handler (wrap-routes + (routes + (wrap-routes (GET "/foo" req (:stack req)) mw "a") + (wrap-routes (GET "/bar" req (:stack req)) mw "b")) + mw + "1")] + (is (= "1aa1" (:body (handler (mock/request :get "/foo"))))) + (is (= "1bb1" (:body (handler (mock/request :get "/bar")))))))) (deftest route-information-test (let [route (GET "/foo/:id" req req) @@ -300,3 +341,142 @@ (testing "ANY request has matched route information" (is (= (request :compojure/route) [:any "/foo/:id"]))))) + +(deftest route-async-test + (testing "single route" + (let [route (GET "/hello/:name" [name] (str "hello " name))] + (testing "matching request" + (let [request (mock/request :get "/hello/world") + response (promise) + exception (promise)] + (route request response exception) + (is (not (realized? exception))) + (is (= @response + {:status 200 + :headers {"Content-Type" "text/html; charset=utf-8"} + :body "hello world"})))) + + (testing "not-matching request" + (let [request (mock/request :get "/goodbye/world") + response (promise) + exception (promise)] + (route request response exception) + (is (not (realized? exception))) + (is (nil? @response)))))) + + (testing "multiple routes" + (let [route (routes + (GET "/foo" [] "foo") + (GET "/bar" [] "bar") + (GET "/baz" [] "baz"))] + (testing "matching request" + (let [request (mock/request :get "/bar") + response (promise) + exception (promise)] + (route request response exception) + (is (not (realized? exception))) + (is (= (:body @response) "bar")))) + + (testing "not-matching URI" + (let [request (mock/request :get "/quz") + response (promise) + exception (promise)] + (route request response exception) + (is (not (realized? exception))) + (is (nil? @response)))) + + (testing "not-matching method" + (let [request (mock/request :post "/bar") + response (promise) + exception (promise)] + (route request response exception) + (is (not (realized? exception))) + (is (nil? @response)))))) + + (testing "rfn" + (let [route (rfn [name] (str "hello " name)) + request (-> (mock/request :get "/") + (assoc :params {:name "world"})) + response (promise) + exception (promise)] + (route request response exception) + (is (not (realized? exception))) + (is (= @response + {:status 200 + :headers {"Content-Type" "text/html; charset=utf-8"} + :body "hello world"})))) + + (testing "context" + (let [route (context "/:name" [name] + (GET "/bar" [] (str name "bar")) + (GET "/baz" [] (str name "baz")))] + (testing "matching request" + (let [request (mock/request :get "/foo/baz") + response (promise) + exception (promise)] + (route request response exception) + (is (not (realized? exception))) + (is (= (:body @response) "foobaz")))) + + (testing "not-matching request" + (let [request (mock/request :get "/foo/quz") + response (promise) + exception (promise)] + (route request response exception) + (is (not (realized? exception))) + (is (nil? @response))))) + + (testing "with coercion" + (let [route (context "/:id" [id :<< coercions/as-int] + (GET "/" [] (str id)))] + (testing "matching request" + (let [request (mock/request :get "/123") + response (promise) + exception (promise)] + (route request response exception) + (is (not (realized? exception))) + (is (= (:body @response) "123")))) + (testing "not-matching request" + (let [request (mock/request :get "/foo") + response (promise) + exception (promise)] + (route request response exception) + (is (not (realized? exception))) + (is (nil? @response))))))) + + (testing "wrap-routes" + (let [route (wrap-routes + (GET "/foo" [] "foo") + (fn [handler] + (fn [request respond raise] + (handler request + #(respond (assoc % ::r (:compojure/route request))) + raise))))] + (testing "matching request" + (let [request (mock/request :get "/foo") + response (promise) + exception (promise)] + (route request response exception) + (is (not (realized? exception))) + (is (= (:body @response) "foo")) + (is (= (::r @response) [:get "/foo"])))) + + (testing "not-matching request" + (let [request (mock/request :get "/bar") + response (promise) + exception (promise)] + (route request response exception) + (is (not (realized? exception))) + (is (nil? @response)))))) + + (testing "async response" + (let [route (GET "/" [] (fn [_ respond _] (respond "foobar"))) + request (mock/request :get "/") + response (promise) + exception (promise)] + (route request response exception) + (is (not (realized? exception))) + (is (= @response + {:status 200 + :headers {"Content-Type" "text/html; charset=utf-8"} + :body "foobar"}))))) diff --git a/test/compojure/middleware_test.clj b/test/compojure/middleware_test.clj index 0dcccb6c..7d8f188b 100644 --- a/test/compojure/middleware_test.clj +++ b/test/compojure/middleware_test.clj @@ -6,7 +6,7 @@ [ring.mock.request :as mock] [ring.util.response :as resp])) -(deftest test-canonical-redirect +(deftest canonical-redirect-test (let [handler (wrap-canonical-redirect (routes (GET "/foo" [] "foo") @@ -28,3 +28,25 @@ {:status 404 :headers {"Content-Type" "text/html; charset=utf-8"} :body "not found"})))) + +(deftest canonical-redirect-async-test + (let [handler (wrap-canonical-redirect (GET "/foo" [] "foo"))] + (testing "matching route" + (let [response (promise) + exception (promise)] + (handler (mock/request :get "/foo") response exception) + (is (not (realized? exception))) + (is (= @response + {:status 200 + :headers {"Content-Type" "text/html; charset=utf-8"} + :body "foo"})))) + + (testing "redirect" + (let [response (promise) + exception (promise)] + (handler (mock/request :get "/foo/") response exception) + (is (not (realized? exception))) + (is (= @response + {:status 301 + :headers {"Location" "/foo"} + :body ""})))))) diff --git a/test/compojure/response_test.clj b/test/compojure/response_test.clj index 99ac7fd8..68f14a0a 100644 --- a/test/compojure/response_test.clj +++ b/test/compojure/response_test.clj @@ -14,7 +14,7 @@ (defmethod handler-multi :default [request] expected-response) -(deftest response-test +(deftest render-test (testing "with nil" (is (nil? (response/render nil {})))) @@ -74,3 +74,24 @@ (is (thrown-with-msg? IllegalArgumentException #"No implementation of method: :render of protocol" (response/render [] {}))))) + +(deftest send-test + (testing "render fallback" + (let [response (promise) + exception (promise)] + (response/send "foo" {} response exception) + (is (not (realized? exception))) + (is (= @response + {:status 200 + :headers {"Content-Type" "text/html; charset=utf-8"} + :body "foo"})))) + + (testing "function return value" + (let [response (promise) + exception (promise)] + (response/send (fn [_ respond _] (respond "bar")) {} response exception) + (is (not (realized? exception))) + (is (= @response + {:status 200 + :headers {"Content-Type" "text/html; charset=utf-8"} + :body "bar"}))))) diff --git a/test/compojure/route_test.clj b/test/compojure/route_test.clj index 67f31077..027156f1 100644 --- a/test/compojure/route_test.clj +++ b/test/compojure/route_test.clj @@ -12,7 +12,15 @@ (let [response ((route/not-found {:status 200 :body "bar"}) (mock/request :get "/"))] (is (= (:status response) 404)) - (is (= (:body response) "bar"))))) + (is (= (:body response) "bar")))) + (testing "async arity" + (let [handler (route/not-found "baz") + response (promise) + exception (promise)] + (handler (mock/request :get "/") response exception) + (is (not (realized? exception))) + (is (= (:status @response) 404)) + (is (= (:body @response) "baz"))))) (deftest resources-route (let [route (route/resources "/foo" {:root "test_files"})