Compare commits

...

103 commits

Author SHA1 Message Date
Joel Kaasinen
7c1544c3ce
doc: document match-by-name :url-encode? option
Some checks failed
testsuite / Clojure 11 (Java 11) (push) Has been cancelled
testsuite / Clojure 11 (Java 17) (push) Has been cancelled
testsuite / Clojure 11 (Java 21) (push) Has been cancelled
testsuite / Clojure 11 (Java 25) (push) Has been cancelled
testsuite / Clojure 12 (Java 11) (push) Has been cancelled
testsuite / Clojure 12 (Java 17) (push) Has been cancelled
testsuite / Clojure 12 (Java 21) (push) Has been cancelled
testsuite / Clojure 12 (Java 25) (push) Has been cancelled
testsuite / ClojureScript (push) Has been cancelled
testsuite / Lint cljdoc.edn (push) Has been cancelled
testsuite / Check cljdoc analysis (push) Has been cancelled
for #778 #519
2026-02-06 13:25:33 +02:00
Joel Kaasinen
0724c0c5a0
doc: update CHANGELOG.md 2026-02-06 13:20:51 +02:00
Joel Kaasinen
7051c99e99
Merge pull request #778 from lucacervello/master
feat: add url-encode? options to path-params
2026-02-06 13:13:49 +02:00
Luca Cervello
4c8a69c616 refactor: remove repetitions 2026-02-06 12:01:44 +01:00
Joel Kaasinen
e3306e1876
Merge pull request #774 from metosin/feat/753-doc-parameter-coercion
Some checks failed
testsuite / Check cljdoc analysis (push) Has been cancelled
testsuite / Clojure 11 (Java 11) (push) Has been cancelled
testsuite / Clojure 11 (Java 17) (push) Has been cancelled
testsuite / Clojure 11 (Java 21) (push) Has been cancelled
testsuite / Clojure 11 (Java 25) (push) Has been cancelled
testsuite / Clojure 12 (Java 11) (push) Has been cancelled
testsuite / Clojure 12 (Java 17) (push) Has been cancelled
testsuite / Clojure 12 (Java 21) (push) Has been cancelled
testsuite / Clojure 12 (Java 25) (push) Has been cancelled
testsuite / ClojureScript (push) Has been cancelled
testsuite / Lint cljdoc.edn (push) Has been cancelled
doc: document nuances of reitit.coercion/default-parameter-coercion
2026-01-23 14:38:39 +02:00
Joel Kaasinen
e6e1bfd5c4
doc: clarify malli body coercion doc 2026-01-23 14:25:19 +02:00
Joel Kaasinen
8391fafbe2
Merge pull request #776 from metosin/feat/337-fix-external-redirect
fix: redirect-trailing-slash-handler won't make external redirects
2026-01-23 14:16:11 +02:00
Luca Cervello
97bfafa907 feat: add url-encode? options to path-params 2026-01-23 11:20:38 +01:00
Joel Kaasinen
71a777b4fa
fix: redirect-trailing-slash-handler won't make external redirects
A redirect header like

Location: //malicious.com/foo

Would result in a redirect to https://malicious.com/foo . We never
want Locations like that out of redirect-trailing-slash-handler.

Fixes #337
2026-01-23 09:54:26 +02:00
Joel Kaasinen
a6b68cc3d6
doc: document nuances of reitit.coercion/default-parameter-coercion
for #753
2026-01-23 09:00:47 +02:00
Joel Kaasinen
248200aad3
Merge pull request #770 from lucacervello/master
Some checks failed
testsuite / Clojure 11 (Java 11) (push) Has been cancelled
testsuite / Clojure 11 (Java 17) (push) Has been cancelled
testsuite / Clojure 11 (Java 21) (push) Has been cancelled
testsuite / Clojure 11 (Java 25) (push) Has been cancelled
testsuite / Clojure 12 (Java 11) (push) Has been cancelled
testsuite / Clojure 12 (Java 17) (push) Has been cancelled
testsuite / Clojure 12 (Java 21) (push) Has been cancelled
testsuite / Clojure 12 (Java 25) (push) Has been cancelled
testsuite / ClojureScript (push) Has been cancelled
testsuite / Lint cljdoc.edn (push) Has been cancelled
testsuite / Check cljdoc analysis (push) Has been cancelled
Allow colons in bracket parameter syntax
2026-01-09 13:00:33 +02:00
Joel Kaasinen
bf18586d75
examples/openapi: fix /complex example 2026-01-09 11:28:04 +02:00
Joel Kaasinen
373ea9bb62
Release 0.10.0
Some checks are pending
testsuite / Clojure 11 (Java 11) (push) Waiting to run
testsuite / Clojure 11 (Java 17) (push) Waiting to run
testsuite / Clojure 11 (Java 21) (push) Waiting to run
testsuite / Clojure 11 (Java 25) (push) Waiting to run
testsuite / Clojure 12 (Java 11) (push) Waiting to run
testsuite / Clojure 12 (Java 17) (push) Waiting to run
testsuite / Clojure 12 (Java 21) (push) Waiting to run
testsuite / Clojure 12 (Java 25) (push) Waiting to run
testsuite / ClojureScript (push) Waiting to run
testsuite / Lint cljdoc.edn (push) Waiting to run
testsuite / Check cljdoc analysis (push) Waiting to run
2026-01-09 10:07:15 +02:00
Joel Kaasinen
334a42e03d
Merge pull request #773 from metosin/bump-deps
chore: bump deps
2026-01-09 10:06:04 +02:00
Joel Kaasinen
5ac4e65284
doc: mention Java 25 2026-01-09 10:01:28 +02:00
Joel Kaasinen
dbac18546b
chore: don't run CI twice on pull requests
both the push and pull_request triggers matched
2026-01-09 09:58:35 +02:00
Joel Kaasinen
69b23c49b9
chore: upgrade clojure; add clj11, clj12 and java 25 to ci matrix 2026-01-09 09:55:05 +02:00
Joel Kaasinen
e1d5789f40
chore: bump deps 2026-01-09 09:40:53 +02:00
Joel Kaasinen
d27454efdc
doc: update CHANGELOG.md 2026-01-09 09:36:54 +02:00
Joel Kaasinen
1d4473e1f4
Merge pull request #772 from metosin/fix/768-ancestors
fix create-exception-middleware for hierarchical keywords
2026-01-09 09:35:14 +02:00
Joel Kaasinen
75faf709e2
fix: create-exception-middleware for deep hierarchies
The code was not finding the closest ancestor to the error type,
because `ancestors` is not ordered. Now the code does a DFS to find a
nearest ancestor. If the nearest ancestor is non-unique, an arbitrary
one is picked.
2026-01-09 09:26:21 +02:00
Joel Kaasinen
2c87d90bda
fix: create-exception-middleware for hierarchical keywords
Previously, the code was searching among the descendants, not the
ancestors, of the error type for an error handler. The test also got
this wrong, perhaps due to a mistake in the parameter order of derive.
2026-01-09 08:37:49 +02:00
Joel Kaasinen
8907dfc5f5
Merge pull request #771 from mthl/lint
Fix linting issues and run Clj-kondo in CI
2026-01-09 08:12:44 +02:00
Mathieu Lirzin
63429a2d1e
chore: Run Linter on CI 2026-01-01 18:40:21 +01:00
Mathieu Lirzin
8721c7ae37
refactor: Implement all Executor protocol method 2026-01-01 18:40:21 +01:00
Mathieu Lirzin
e3180e4d6a
refactor: Reify protocol instead of interface 2026-01-01 18:40:21 +01:00
Mathieu Lirzin
2597d14125
refactor: Ignore :missing-protocol-method linter 2026-01-01 18:40:21 +01:00
Mathieu Lirzin
54a040f136
test: Use atom in reloading-ring-handler-test
This removes usage of inline defs.
2026-01-01 18:40:21 +01:00
Mathieu Lirzin
e4c53a64e2
test: Add missing protocol method implementation 2026-01-01 18:40:21 +01:00
Mathieu Lirzin
218f05972e
test: Remove unused def 2026-01-01 18:40:21 +01:00
Mathieu Lirzin
e342ac5401
test: Comment unused values 2026-01-01 18:40:21 +01:00
Mathieu Lirzin
e6137cb47a
refactor: Remove redundant let 2026-01-01 18:40:21 +01:00
Mathieu Lirzin
3191d9ee59
refactor: Remove unneeded and 2026-01-01 18:40:21 +01:00
Mathieu Lirzin
579eb28a50
refactor: Remove redundant str calls 2026-01-01 18:40:21 +01:00
Mathieu Lirzin
20735730c9
chore: Lint defspec as deftest 2026-01-01 18:40:20 +01:00
Mathieu Lirzin
aa6c1ac460
test: Use .clj extension instead of .cljc
Those tests are only working on Clojure.
2026-01-01 18:40:20 +01:00
Mathieu Lirzin
c113bded4e
refactor: Remove unused required namespaces 2026-01-01 18:40:17 +01:00
Luca Cervello
ed6397cd05 feat: allow colons in bracket parameter syntax
closes #748
2025-12-30 10:05:20 +01:00
Joel Kaasinen
c3a152a44e
doc: update CHANGELOG.md
Some checks failed
testsuite / Clojure (Java 11) (push) Has been cancelled
testsuite / Clojure (Java 17) (push) Has been cancelled
testsuite / Clojure (Java 21) (push) Has been cancelled
testsuite / ClojureScript (push) Has been cancelled
testsuite / Lint cljdoc.edn (push) Has been cancelled
testsuite / Check cljdoc analysis (push) Has been cancelled
2025-11-17 11:01:37 +02:00
Joel Kaasinen
c0bc789863
Merge pull request #767 from metosin/humanize-opts
feat: support humanize options
2025-11-17 10:59:23 +02:00
Joel Kaasinen
78aba57d2d
doc: document configuring malli registry
Some checks failed
testsuite / Clojure (Java 11) (push) Has been cancelled
testsuite / Clojure (Java 17) (push) Has been cancelled
testsuite / Clojure (Java 21) (push) Has been cancelled
testsuite / ClojureScript (push) Has been cancelled
testsuite / Lint cljdoc.edn (push) Has been cancelled
testsuite / Check cljdoc analysis (push) Has been cancelled
2025-11-14 14:58:52 +02:00
Joel Kaasinen
451b286f1d
doc: add brief docs for configuring humanized error messages 2025-11-14 14:16:48 +02:00
Joel Kaasinen
eb06404f1e
feat: fold malli :humanize-opts into :options 2025-11-14 14:06:43 +02:00
Joel Kaasinen
af7313bd9b
test: add test for overriding malli registry 2025-11-14 14:05:36 +02:00
Joel Kaasinen
ea58100fec
test: add test for malli coercion :humanize-opts 2025-11-14 13:51:23 +02:00
ertugrulcetin
a4576cc622
feat: support humanize options 2025-11-14 13:15:50 +02:00
Joel Kaasinen
9d88d92241
Merge pull request #766 from metosin/spec-and-or
Some checks failed
testsuite / Clojure (Java 11) (push) Has been cancelled
testsuite / Clojure (Java 17) (push) Has been cancelled
testsuite / Clojure (Java 21) (push) Has been cancelled
testsuite / ClojureScript (push) Has been cancelled
testsuite / Lint cljdoc.edn (push) Has been cancelled
testsuite / Check cljdoc analysis (push) Has been cancelled
Bump spec-tools, test openapi + s/keys + or
2025-11-14 11:49:34 +02:00
Joel Kaasinen
d16aac673e
test: test openapi + s/keys + or 2025-11-14 11:30:51 +02:00
Joel Kaasinen
dede2db213
chore: bump spec-tools 2025-11-14 11:22:03 +02:00
Joel Kaasinen
1dc961f661
Merge pull request #764 from metosin/483-doc-allow-symlinks
Some checks failed
testsuite / Clojure (Java 11) (push) Has been cancelled
testsuite / Clojure (Java 17) (push) Has been cancelled
testsuite / Clojure (Java 21) (push) Has been cancelled
testsuite / ClojureScript (push) Has been cancelled
testsuite / Lint cljdoc.edn (push) Has been cancelled
testsuite / Check cljdoc analysis (push) Has been cancelled
doc: document allow-symlinks? option
2025-10-31 13:41:31 +02:00
Joel Kaasinen
2ce9850de6
doc: document allow-symlinks? option
... for create-resource-handler and create-file-handler

fixes #483
2025-10-31 11:43:25 +02:00
Joel Kaasinen
0bc30e9361
doc: update CHANGELOG.md 2025-10-31 10:45:36 +02:00
Joel Kaasinen
9b26d5c0fd
Merge pull request #763 from metosin/remove-dead-code
refactor: remove unused reitit.dependency ns
2025-10-31 10:44:38 +02:00
Joel Kaasinen
e671f78741
Merge pull request #762 from metosin/745-coerce-response-content-type
Some checks are pending
testsuite / Clojure (Java 11) (push) Waiting to run
testsuite / Clojure (Java 17) (push) Waiting to run
testsuite / Clojure (Java 21) (push) Waiting to run
testsuite / ClojureScript (push) Waiting to run
testsuite / Lint cljdoc.edn (push) Waiting to run
testsuite / Check cljdoc analysis (push) Waiting to run
improve & document response coercion content-type selection
2025-10-31 09:48:00 +02:00
Joel Kaasinen
55f8d98bde
test: improve per-content-type coercion tests
- The :headers "Content-Type" case in per-content-type-test was
  unrealistic. Ring would've thrown an exception at the non-string
  :body.
- Test response Content-Type in muuntaja-per-content-type-coercion-test
2025-10-31 09:38:56 +02:00
Joel Kaasinen
342bae3ffe
refactor: remove unused reitit.dependency ns
leftover from #33 #210
2025-10-30 15:32:27 +02:00
Joel Kaasinen
ae52000b29
doc: update CHANGELOG.md 2025-10-29 10:57:59 +02:00
Joel Kaasinen
39c5ae86a4
doc: return random content-type from openapi example /pizza 2025-10-29 10:54:16 +02:00
Joel Kaasinen
7fb9c27e46
feat: use request Content-Type or :muuntaja/content-type to coerce
Previously, `extract-response-format-default` was only looking at
(-> request :muuntaja/response :format). This led to wrong behaviour
when there were separate schemas for separate response content types
and an explicitly picked content-type for the response.
2025-10-29 10:54:10 +02:00
Joel Kaasinen
f4da07c222
Release 0.9.2
Some checks failed
testsuite / Clojure (Java 11) (push) Has been cancelled
testsuite / Clojure (Java 17) (push) Has been cancelled
testsuite / Clojure (Java 21) (push) Has been cancelled
testsuite / ClojureScript (push) Has been cancelled
testsuite / Lint cljdoc.edn (push) Has been cancelled
testsuite / Check cljdoc analysis (push) Has been cancelled
2025-10-28 14:57:54 +02:00
Joel Kaasinen
d25dca19f6 chore: disable release signing harder
Some checks failed
testsuite / Clojure (Java 11) (push) Has been cancelled
testsuite / Clojure (Java 17) (push) Has been cancelled
testsuite / Clojure (Java 21) (push) Has been cancelled
testsuite / ClojureScript (push) Has been cancelled
testsuite / Lint cljdoc.edn (push) Has been cancelled
testsuite / Check cljdoc analysis (push) Has been cancelled
2025-10-24 16:14:11 +03:00
Joel Kaasinen
1b02662c78 chore: disable release signing
now that we have automated releases, there's no point
2025-10-24 16:08:19 +03:00
Joel Kaasinen
36af88b65e doc: update release instructions 2025-10-24 16:08:08 +03:00
Joel Kaasinen
152c598858 Release 0.9.2-rc1 2025-10-24 15:52:47 +03:00
Joel Kaasinen
6d9632e85e chore: automated release pipeline 2025-10-24 15:45:42 +03:00
Joel Kaasinen
5ff8ba2e3e
Merge pull request #761 from metosin/bump-deps
bump deps
2025-10-24 15:45:27 +03:00
Joel Kaasinen
c684c83c99 chore: update openapi-schema-validator, fix test
validator now disallows having both "example" and "examples"
2025-10-24 15:38:29 +03:00
Joel Kaasinen
defebb0f1f chore: upgrade dependencies
... and pin pedestal versions since we don't work with 0.7/0.8
2025-10-24 15:31:39 +03:00
Joel Kaasinen
54c0935078 doc: updating deps 2025-10-24 15:26:20 +03:00
Joel Kaasinen
cde050f964 doc: remove old TODO 2025-10-24 14:24:38 +03:00
Joel Kaasinen
560c6d7969 doc: update CHANGELOG.md 2025-10-24 14:16:59 +03:00
Joel Kaasinen
abe95bfc17
Merge pull request #760 from metosin/fix/758-match-by-name-bang-nil
fix: match-by-name! should throw when match-by-name is PartialMatch
2025-10-24 14:15:37 +03:00
Joel Kaasinen
d2f44b8015 fix: match-by-name! should throw when match-by-name is PartialMatch
If a path param was nil, match-by-name (via impl/path-for) was
treating the parameter as missing, but match-by-name!
(via impl/throw-on-missing-path-params) was treating it as present.

That is:

(reitit/match-by-name router :page {:id "1"}) ;; => Match
(reitit/match-by-name router :page) ;; => PartialMatch
(reitit/match-by-name router :page {:id nil}) ;; => PartialMatch

(reitit/match-by-name! router :page {:id "1"}) ;; => Match
(reitit/match-by-name! router :page) ;; => ExceptionInfo: missing path-params for route /pages/:id -> #{:id}
(reitit/match-by-name! router :page {:id nil}) ;; => nil  !!!

fixes #758
2025-10-24 09:58:03 +03:00
Joel Kaasinen
ef9dd495be doc: update CHANGELOG.md
Some checks failed
testsuite / Clojure (Java 11) (push) Has been cancelled
testsuite / Clojure (Java 17) (push) Has been cancelled
testsuite / Clojure (Java 21) (push) Has been cancelled
testsuite / ClojureScript (push) Has been cancelled
testsuite / Lint cljdoc.edn (push) Has been cancelled
testsuite / Check cljdoc analysis (push) Has been cancelled
2025-10-13 15:41:31 +03:00
Joel Kaasinen
9509e40dae
Merge pull request #756 from metosin/feat/defaults-for-optional-keys
setting default values for optional keys in malli coercion
2025-10-13 15:38:45 +03:00
Joel Kaasinen
67918a3f9c feat: reuse :default-values config key instead of adding a new one 2025-10-13 15:18:29 +03:00
Joel Kaasinen
45951aa82e doc: update CHANGELOG.md
Some checks are pending
testsuite / Clojure (Java 11) (push) Waiting to run
testsuite / Clojure (Java 17) (push) Waiting to run
testsuite / Clojure (Java 21) (push) Waiting to run
testsuite / ClojureScript (push) Waiting to run
testsuite / Lint cljdoc.edn (push) Waiting to run
testsuite / Check cljdoc analysis (push) Waiting to run
2025-10-13 09:17:33 +03:00
Joel Kaasinen
1cdca2e3d5
Merge pull request #739 from Ramblurr/fix/top-level-mw-registry
Apply router options to top-level middleware chain
2025-10-13 09:14:36 +03:00
Joel Kaasinen
2f22838820 doc: using middleware from registry at ring-handler level 2025-10-13 09:09:11 +03:00
Joel Kaasinen
d809291553 test: ring-handler middleware from registry inside router 2025-10-13 09:01:21 +03:00
Joel Kaasinen
4e572e86d6 Merge remote-tracking branch 'origin/master' into fix/top-level-mw-registry 2025-10-13 08:50:38 +03:00
Joel Kaasinen
10700e0ca2
Merge pull request #757 from metosin/doc/multipart-middleware-order
Some checks failed
testsuite / Clojure (Java 11) (push) Has been cancelled
testsuite / Clojure (Java 17) (push) Has been cancelled
testsuite / Clojure (Java 21) (push) Has been cancelled
testsuite / ClojureScript (push) Has been cancelled
testsuite / Lint cljdoc.edn (push) Has been cancelled
testsuite / Check cljdoc analysis (push) Has been cancelled
doc: multipart-middleware should be after coerce-request-middleware
2025-10-10 13:04:57 +03:00
Joel Kaasinen
3e0f1d3188 doc: multipart-middleware should be after coerce-request-middleware 2025-10-10 12:50:24 +03:00
Joel Kaasinen
f26dc1ab19 feat: :default-values-for-optional-keys for malli coercion 2025-10-10 08:51:05 +03:00
Joel Kaasinen
01766ee211 doc: update CHANGELOG.md
Some checks are pending
testsuite / Clojure (Java 11) (push) Waiting to run
testsuite / Clojure (Java 17) (push) Waiting to run
testsuite / Clojure (Java 21) (push) Waiting to run
testsuite / ClojureScript (push) Waiting to run
testsuite / Lint cljdoc.edn (push) Waiting to run
testsuite / Check cljdoc analysis (push) Waiting to run
2025-10-09 15:30:55 +03:00
Joel Kaasinen
79627aea7b
Merge pull request #755 from metosin/feat/multimethod-as-handler
feat: allow multimethods as :handlers in validation
2025-10-09 15:26:37 +03:00
Joel Kaasinen
4d284385ec
Merge pull request #754 from metosin/openapi-error-reporting
feat: better error reporting while building openapi docs
2025-10-09 15:26:27 +03:00
Joel Kaasinen
31fa0376cf feat: better error reporting while building openapi docs
Include the path and method when openapi generation goes wrong.
2025-10-09 15:19:56 +03:00
Joel Kaasinen
05bc331397 feat: allow multimethods as :handlers in validation
fixes #749
2025-10-07 15:50:51 +03:00
Juho Teperi
7520d20f12 No need to use specific Java version for releases 2025-05-27 14:35:50 +03:00
Juho Teperi
210f20e714 Release 0.9.1 2025-05-27 14:04:31 +03:00
Juho Teperi
aad054ef39
Merge pull request #743 from metosin/fix-responses
fix: response coercion for unlisted http statuses, when no default
2025-05-27 14:03:20 +03:00
Joel Kaasinen
653db7efa3 doc: update CHANGELOG.md 2025-05-27 13:08:59 +03:00
Joel Kaasinen
5025ca3a75 fix: response coercion for unlisted http statuses, when no default
fixes #742
2025-05-27 12:09:38 +03:00
Joel Kaasinen
55c30af979
Merge pull request #741 from metosin/readme-example
doc: improve middleware docs & examples in README.md & elsewhere
2025-05-26 12:56:11 +03:00
Joel Kaasinen
9ee8e364f3 doc: add a reason to use top-level mw 2025-05-26 10:27:00 +03:00
Joel Kaasinen
ad9cd31168 doc: mention one more reason to use top-level route data for mw 2025-05-26 10:21:40 +03:00
Joel Kaasinen
99267d2943 doc: mention that coercion uses top-level-route-data-middleware 2025-05-26 10:14:14 +03:00
Joel Kaasinen
ecf22d5c4a doc: elaborate on different ways of setting middleware 2025-05-26 10:06:36 +03:00
Joel Kaasinen
1803643f1f doc: link to ring.md#middleware from data_driven_middleware.md
Data Driven Middleware is the first doc title with "Middleware" in it,
so people will land in it while trying to figure out the basics of
middleware.
2025-05-26 08:52:05 +03:00
Joel Kaasinen
f247751409 doc: remove misleading link to open issue
the factoring hasn't happened, so no reason to talk about issue #134
in the docs until it does
2025-05-26 08:45:48 +03:00
Joel Kaasinen
48fdb692ef doc: improve ring example in README.md
- format-response-middleware wasn't doing anything, use
  format-middleware instead as that's what the users usually want
- add exception handling
- document middleware stack a bit
2025-05-26 08:44:53 +03:00
Casey Link
0454e8914f Apply router options to top-level middleware chain
Middleware supplied to the `ring-handler` function could not be resolved
from the middleware registry, because the router options (which contain
the registry) were not being propagated.

Fixes #738
2025-04-29 14:52:09 +02:00
97 changed files with 1787 additions and 1993 deletions

View file

@ -1,5 +1,6 @@
{;;:skip-comments true {;;:skip-comments true
:lint-as {potemkin/def-derived-map clojure.core/defrecord} :lint-as {potemkin/def-derived-map clojure.core/defrecord
clojure.test.check.clojure-test/defspec clojure.test/deftest}
:linters {:missing-else-branch {:level :off} :linters {:missing-else-branch {:level :off}
:unused-binding {:level :off} :unused-binding {:level :off}
:unused-referred-var {:exclude {clojure.test [deftest testing is are] :unused-referred-var {:exclude {clojure.test [deftest testing is are]

26
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,26 @@
name: Release
on:
release:
types:
- published # reacts to releases and prereleases, but not their drafts
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: "Setup Java"
uses: actions/setup-java@v5
with:
distribution: zulu
java-version: 11
- name: "Setup Clojure"
uses: DeLaGuardo/setup-clojure@master
with:
lein: 2.9.5
- name: Deploy to Clojars
run: ./scripts/lein-modules do clean, deploy clojars
env:
CLOJARS_USERNAME: metosinci
CLOJARS_PASSWORD: "${{ secrets.CLOJARS_DEPLOY_TOKEN }}"

View file

@ -2,16 +2,19 @@ name: testsuite
on: on:
push: push:
branches: [master]
pull_request: pull_request:
branches: [master]
jobs: jobs:
build-clj: build-clj:
strategy: strategy:
matrix: matrix:
# Supported Java versions: LTS releases and latest # Supported Java versions: LTS releases and latest
jdk: [11, 17, 21] jdk: [11, 17, 21, 25]
clojure: [11, 12]
name: Clojure (Java ${{ matrix.jdk }}) name: Clojure ${{ matrix.clojure }} (Java ${{ matrix.jdk }})
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -36,14 +39,18 @@ jobs:
uses: DeLaGuardo/setup-clojure@13.1 uses: DeLaGuardo/setup-clojure@13.1
with: with:
lein: 2.9.5 lein: 2.9.5
clj-kondo: 2025.12.23
# Install openapi-schema-validator for openapi-tests # Install openapi-schema-validator for openapi-tests
# Uses node version from the ubuntu-latest # Uses node version from the ubuntu-latest
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Run Linter
run: ./lint.sh
- name: Run tests - name: Run tests
run: ./scripts/test.sh clj run: ./scripts/test.sh clj${{ matrix.clojure }}
build-cljs: build-cljs:
name: ClojureScript name: ClojureScript

View file

@ -12,6 +12,51 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md [breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
## UNRELEASED
* **FIX** redirect-trailing-slash-handler won't make external redirects. [#776](https://github.com/metosin/reitit/pull/776)
* Allow colons in bracket parameter syntax. [#770](https://github.com/metosin/reitit/pull/770)
* Add `url-encode?` option to `match-by-name`. [#778](https://github.com/metosin/reitit/pull/778)
## 0.10.0 (2026-01-09)
* Improve & document how response schemas get picked in per-content-type coercion. See [docs](./doc/ring/coercion.md#per-content-type-coercion). [#745](https://github.com/metosin/reitit/issues/745).
* **BREAKING** Remove unused `reitit.dependency` ns. [#763](https://github.com/metosin/reitit/pull/763)
* Support passing options to malli humanize. See [docs](./doc/coercion/malli_coercion.md). [#467](https://github.com/metosin/reitit/issues/467)
* **FIX** Handling of ex-type keyword hierarchies in create-exception-middleware. [#768](https://github.com/metosin/reitit/issues/768)
* Updated dependencies:
```
[metosin/malli "0.20.0"] is available but we use "0.19.2"
[com.fasterxml.jackson.core/jackson-core "2.20.1"] is available but we use "2.20.0"
[com.fasterxml.jackson.core/jackson-databind "2.20.1"] is available but we use "2.20.0"
[org.clojure/core.rrb-vector "0.2.1"] is available but we use "0.2.0"
```
## 0.9.2 (2025-10-28)
* Allow multimethods as handlers when validating [#755](https://github.com/metosin/reitit/pull/755)
* Improve error reporting when generating OpenAPI fails [#754](https://github.com/metosin/reitit/pull/754)
* Allow middleware registry to be used when defining middleware in `ring-handler`. See [docs](./doc/ring/middleware_registry.md). [#739](https://github.com/metosin/reitit/pull/739)
* Allow passing options (eg. `:malli.transform/add-optional-keys`) to malli's `default-value-transformer`. See [docs](./doc/coercion/malli_coercion.md). [#756](https://github.com/metosin/reitit/pull/756)
* **FIX**: `match-by-name!` returning `nil` instead of throwing an exception for some partial matches [#758](https://github.com/metosin/reitit/issues/758)
* Updated dependencies:
```
[com.fasterxml.jackson.core/jackson-core "2.20.0"] is available but we use "2.18.2"
[com.fasterxml.jackson.core/jackson-databind "2.20.0"] is available but we use "2.18.2"
[compojure "1.7.2"] is available but we use "1.7.1"
[fipp "0.6.29"] is available but we use "0.6.27"
[metosin/malli "0.19.2"] is available but we use "0.18.0"
[ring "1.15.3"] is available but we use "1.14.1"
[ring/ring-core "1.15.3"] is available but we use "1.14.1"
[ring/ring-defaults "0.7.0"] is available but we use "0.6.0"
```
## 0.9.1 (2025-05-27)
* **FIX**: response coercion threw an exception for unlisted HTTP status codes if there was no `:default`. Broken in 0.9.0. [#742](https://github.com/metosin/reitit/issues/742)
## 0.9.0 (2025-05-23) ## 0.9.0 (2025-05-23)
* Improvements to mime type handling in `create-file-handler` and `create-resource-handler` [#733](https://github.com/metosin/reitit/pull/733) * Improvements to mime type handling in `create-file-handler` and `create-resource-handler` [#733](https://github.com/metosin/reitit/pull/733)

View file

@ -66,14 +66,14 @@ modules will continue to be released under `metosin` for compatibility purposes.
All main modules bundled: All main modules bundled:
```clj ```clj
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
``` ```
Optionally, the parts can be required separately. Optionally, the parts can be required separately.
Reitit requires Clojure 1.11 and Java 11. Reitit requires Clojure 1.11 and Java 11.
Reitit is tested with the LTS releases Java 11, 17 and 21. Reitit is tested with the LTS releases Java 11, 17, 21 and 25
## Quick start ## Quick start
@ -109,6 +109,7 @@ A Ring routing app with input & output coercion using [data-specs](https://githu
(require '[reitit.ring :as ring]) (require '[reitit.ring :as ring])
(require '[reitit.coercion.spec]) (require '[reitit.coercion.spec])
(require '[reitit.ring.coercion :as rrc]) (require '[reitit.ring.coercion :as rrc])
(require '[reitit.ring.middleware.exception :as exception])
(require '[reitit.ring.middleware.muuntaja :as muuntaja]) (require '[reitit.ring.middleware.muuntaja :as muuntaja])
(require '[reitit.ring.middleware.parameters :as parameters]) (require '[reitit.ring.middleware.parameters :as parameters])
@ -124,39 +125,45 @@ A Ring routing app with input & output coercion using [data-specs](https://githu
;; router data affecting all routes ;; router data affecting all routes
{:data {:coercion reitit.coercion.spec/coercion {:data {:coercion reitit.coercion.spec/coercion
:muuntaja m/instance :muuntaja m/instance
:middleware [parameters/parameters-middleware :middleware [parameters/parameters-middleware ; decoding query & form params
muuntaja/format-middleware ; content negotiation
exception/exception-middleware ; converting exceptions to HTTP responses
rrc/coerce-request-middleware rrc/coerce-request-middleware
muuntaja/format-response-middleware
rrc/coerce-response-middleware]}}))) rrc/coerce-response-middleware]}})))
``` ```
Valid request: Valid request:
```clj ```clj
(app {:request-method :get (-> (app {:request-method :get
:uri "/api/math" :uri "/api/math"
:query-params {:x "1", :y "2"}}) :query-params {:x "1", :y "2"}})
(update :body slurp))
; {:status 200 ; {:status 200
; :body {:total 3}} ; :body "{\"total\":3}"
; :headers {"Content-Type" "application/json; charset=utf-8"}}
``` ```
Invalid request: Invalid request:
```clj ```clj
(app {:request-method :get (-> (app {:request-method :get
:uri "/api/math" :uri "/api/math"
:query-params {:x "1", :y "a"}}) :query-params {:x "1", :y "a"}})
;{:status 400, (update :body jsonista.core/read-value))
; :body {:type :reitit.coercion/request-coercion, ; {:status 400
; :coercion :spec, ; :headers {"Content-Type" "application/json; charset=utf-8"}
; :spec "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:$spec20745/x :$spec20745/y]), :type :map, :keys #{:y :x}, :keys/req #{:y :x}})", ; :body {"spec" "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:spec$8974/x :spec$8974/y]), :type :map, :leaf? false})"
; :problems [{:path [:y], ; "value" {"x" "1"
; :pred "clojure.core/int?", ; "y" "a"}
; :val "a", ; "problems" [{"via" ["spec$8974/y"]
; :via [:$spec20745/y], ; "path" ["y"]
; :in [:y]}], ; "pred" "clojure.core/int?"
; :value {:x "1", :y "a"}, ; "in" ["y"]
; :in [:request :query-params]}} ; "val" "a"}]
; "type" "reitit.coercion/request-coercion"
; "coercion" "spec"
; "in" ["request" "query-params"]}}
``` ```
## More examples ## More examples

View file

@ -41,7 +41,7 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
All bundled: All bundled:
```clj ```clj
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
``` ```
Optionally, the parts can be required separately. Optionally, the parts can be required separately.

View file

@ -22,7 +22,7 @@ The default exception formatting uses `reitit.exception/exception`. It produces
## Pretty Errors ## Pretty Errors
```clj ```clj
[metosin/reitit-dev "0.9.0"] [metosin/reitit-dev "0.10.0"]
``` ```
For human-readable and developer-friendly exception messages, there is `reitit.dev.pretty/exception` (in the `reitit-dev` module). It is inspired by the lovely errors messages of [ELM](https://elm-lang.org/blog/compiler-errors-for-humans) and [ETA](https://twitter.com/jyothsnasrin/status/1037703436043603968) and uses [fipp](https://github.com/brandonbloom/fipp), [expound](https://github.com/bhb/expound) and [spell-spec](https://github.com/bhauman/spell-spec) for most of heavy lifting. For human-readable and developer-friendly exception messages, there is `reitit.dev.pretty/exception` (in the `reitit-dev` module). It is inspired by the lovely errors messages of [ELM](https://elm-lang.org/blog/compiler-errors-for-humans) and [ETA](https://twitter.com/jyothsnasrin/status/1037703436043603968) and uses [fipp](https://github.com/brandonbloom/fipp), [expound](https://github.com/bhb/expound) and [spell-spec](https://github.com/bhauman/spell-spec) for most of heavy lifting.

View file

@ -75,6 +75,17 @@ Path-parameters are automatically coerced into strings, with the help of (curren
; :path-params {:id "1"}} ; :path-params {:id "1"}}
``` ```
In case you want to do something like generate a template path for documentation, you can disable url-encoding:
```clj
(r/match-by-name router ::user {:id "<id goes here>"} {:url-encode? false})
; #reitit.core.Match{:template "/api/user/:id"
; :data {:name :user/user}
; :path "/api/user/<id goes here>"
; :result nil
; :path-params {:id "<id goes here>"}}
```
There is also an exception throwing version: There is also an exception throwing version:
```clj ```clj

View file

@ -73,9 +73,10 @@ Using `create` with options to create the coercion instead of `coercion`:
{:transformers {:body {:default reitit.coercion.malli/default-transformer-provider {:transformers {:body {:default reitit.coercion.malli/default-transformer-provider
:formats {"application/json" reitit.coercion.malli/json-transformer-provider}} :formats {"application/json" reitit.coercion.malli/json-transformer-provider}}
:string {:default reitit.coercion.malli/string-transformer-provider} :string {:default reitit.coercion.malli/string-transformer-provider}
:response {:default reitit.coercion.malli/default-transformer-provider}} :response {:default reitit.coercion.malli/default-transformer-provider
:formats {"application/json" reitit.coercion.malli/json-transformer-provider}}}
;; set of keys to include in error messages ;; set of keys to include in error messages
:error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed} :error-keys #{:type :coercion :in #_:schema :value #_:errors :humanized #_:transformed}
;; support lite syntax? ;; support lite syntax?
:lite true :lite true
;; schema identity function (default: close all map schemas) ;; schema identity function (default: close all map schemas)
@ -87,7 +88,37 @@ Using `create` with options to create the coercion instead of `coercion`:
;; strip-extra-keys (affects only predefined transformers) ;; strip-extra-keys (affects only predefined transformers)
:strip-extra-keys true :strip-extra-keys true
;; add/set default values ;; add/set default values
;; Can be false, true or a map of options to pass to malli.transform/default-value-transformer,
;; for example {:malli.transform/add-optional-keys true}
:default-values true :default-values true
;; encode-error
:encode-error nil
;; malli options ;; malli options
:options nil}) :options nil})
``` ```
## Configuring humanize error messages
Malli humanized error messages can be configured using `:options :errors`:
```clj
(reitit.coercion.malli/create
{:options
{:errors (assoc malli.error/default-errors
:malli.core/missing-key {:error/message {:en "MISSING"}})}})
```
See the malli docs for more info.
## Custom registry
Malli registry can be configured conveniently via `:options :registry`:
```clj
(require '[malli.core :as m])
(reitit.coercion.malli/create
{:options
{:registry {:registry (merge (m/default-schemas)
{:my-type :string})}}})
```

View file

@ -25,6 +25,14 @@ clojure-lsp clean-ns
The documentation lives under `doc` and it is hosted on [cljdoc](https://cljdoc.org). See their The documentation lives under `doc` and it is hosted on [cljdoc](https://cljdoc.org). See their
documentation for [library authors](https://github.com/cljdoc/cljdoc/blob/master/doc/userguide/for-library-authors.adoc) documentation for [library authors](https://github.com/cljdoc/cljdoc/blob/master/doc/userguide/for-library-authors.adoc)
## Updating deps
* `lein ancient upgrade`
* Mention non-dev non-test dep upgrades in CHANGELOG.md
* `npm update --save`
* Make a PR, run CI
## Making a release ## Making a release
We use [Break Versioning][breakver]. Remember our promise: patch-level bumps never include breaking changes! We use [Break Versioning][breakver]. Remember our promise: patch-level bumps never include breaking changes!
@ -32,29 +40,20 @@ We use [Break Versioning][breakver]. Remember our promise: patch-level bumps nev
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md [breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
```bash ```bash
# Check that you're using Java 8! Making the release with a newer Java version # create a release commit
# means that it is broken when used with Java 8.
java -version
# new version
./scripts/set-version "1.0.0" ./scripts/set-version "1.0.0"
# create a release commit and a tag # !!! update the changelog
git add -u git add -u
git commit -m "Release 1.0.0" git commit -m "Release 1.0.0"
git tag 1.0.0
# works # push the commit
./scripts/lein-modules install
lein test
# deploy to clojars
CLOJARS_USERNAME=*** CLOJARS_PASSWORD=*** ./scripts/lein-modules do clean, deploy clojars
# push the commit and the tag
git push git push
git push --tags
# !!! check that tests pass on CI
``` ```
* Remembor to update the changelog! * Create a new release on github at <https://github.com/metosin/reitit/releases>
* This will trigger the automated release workflow <https://github.com/metosin/reitit/actions/workflows/release.yml>
* Announce the release at least on #reitit in Clojurians. * Announce the release at least on #reitit in Clojurians.

View file

@ -1,7 +1,7 @@
# Default Interceptors # Default Interceptors
```clj ```clj
[metosin/reitit-interceptors "0.9.0"] [metosin/reitit-interceptors "0.10.0"]
``` ```
Just like the [ring default middleware](../ring/default_middleware.md), but for interceptors. Just like the [ring default middleware](../ring/default_middleware.md), but for interceptors.

View file

@ -5,7 +5,7 @@ Reitit has also support for [interceptors](http://pedestal.io/reference/intercep
## Reitit-http ## Reitit-http
```clj ```clj
[metosin/reitit-http "0.9.0"] [metosin/reitit-http "0.10.0"]
``` ```
A module for http-routing using interceptors instead of middleware. Builds on top of the [`reitit-ring`](../ring/ring.md) module having all the same features. A module for http-routing using interceptors instead of middleware. Builds on top of the [`reitit-ring`](../ring/ring.md) module having all the same features.

View file

@ -3,7 +3,7 @@
[Pedestal](http://pedestal.io/) is a backend web framework for Clojure. `reitit-pedestal` provides an alternative routing engine for Pedestal. [Pedestal](http://pedestal.io/) is a backend web framework for Clojure. `reitit-pedestal` provides an alternative routing engine for Pedestal.
```clj ```clj
[metosin/reitit-pedestal "0.9.0"] [metosin/reitit-pedestal "0.10.0"]
``` ```
Why should one use reitit instead of the Pedestal [default routing](http://pedestal.io/reference/routing-quick-reference)? Why should one use reitit instead of the Pedestal [default routing](http://pedestal.io/reference/routing-quick-reference)?
@ -26,8 +26,8 @@ A minimalistic example on how to to swap the default-router with a reitit router
```clj ```clj
; [io.pedestal/pedestal.service "0.5.5"] ; [io.pedestal/pedestal.service "0.5.5"]
; [io.pedestal/pedestal.jetty "0.5.5"] ; [io.pedestal/pedestal.jetty "0.5.5"]
; [metosin/reitit-pedestal "0.9.0"] ; [metosin/reitit-pedestal "0.10.0"]
; [metosin/reitit "0.9.0"] ; [metosin/reitit "0.10.0"]
(require '[io.pedestal.http :as server]) (require '[io.pedestal.http :as server])
(require '[reitit.pedestal :as pedestal]) (require '[reitit.pedestal :as pedestal])

View file

@ -1,7 +1,7 @@
# Sieppari # Sieppari
```clj ```clj
[metosin/reitit-sieppari "0.9.0"] [metosin/reitit-sieppari "0.10.0"]
``` ```
[Sieppari](https://github.com/metosin/sieppari) is a new and fast interceptor implementation for Clojure, with pluggable async supporting [core.async](https://github.com/clojure/core.async), [Manifold](https://github.com/ztellman/manifold) and [Promesa](http://funcool.github.io/promesa/latest). [Sieppari](https://github.com/metosin/sieppari) is a new and fast interceptor implementation for Clojure, with pluggable async supporting [core.async](https://github.com/clojure/core.async), [Manifold](https://github.com/ztellman/manifold) and [Promesa](http://funcool.github.io/promesa/latest).

View file

@ -65,7 +65,7 @@ There is an extra option in http-router (actually, in the underlying interceptor
### Printing Context Diffs ### Printing Context Diffs
```clj ```clj
[metosin/reitit-interceptors "0.9.0"] [metosin/reitit-interceptors "0.10.0"]
``` ```
Using `reitit.http.interceptors.dev/print-context-diffs` transformation, the context diffs between each interceptor are printed out to the console. To use it, add the following router option: Using `reitit.http.interceptors.dev/print-context-diffs` transformation, the context diffs between each interceptor are printed out to the console. To use it, add the following router option:

View file

@ -63,8 +63,6 @@ Handlers can access the coerced parameters via the `:parameters` key in the requ
{:status 200 {:status 200
:body {:total total}}))}) :body {:total total}}))})
``` ```
### Nested parameter definitions ### Nested parameter definitions
Parameters are accumulated recursively along the route tree, just like Parameters are accumulated recursively along the route tree, just like
@ -94,6 +92,26 @@ handling for merging eg. malli `:map` schemas.
; [:task-id :int]]} ; [:task-id :int]]}
``` ```
### Differences in behaviour for different parameters
All parameter coercions *except* `:body`:
1. Allow keys outside the schema (by opening up the schema using eg. `malli.util/open-schema`)
2. Keywordize the keys (ie. header & query parameter names) of the input before coercing
In contrast, the `:body` coercion:
1. Uses the specified schema
* depending on the coercion, it can be configured as open or closed, see specific coercion docs for details
2. Does not keywordize the keys of the input before coercion
* however, coercions like malli might do the keywordization when coercing json bodies, depending on configuration
This admittedly confusing behaviour is retained currently due to
backwards compatibility reasons. It can be configured by passing
option `:reitit.coercion/parameter-coercion` to `reitit.ring/router`
or `reitit.coercion/compile-request-coercers`. See also:
`reitit.coercion/default-parameter-coercion`.
## Coercion Middleware ## Coercion Middleware
Defining a coercion for a route data doesn't do anything, as it's just data. We have to attach some code to apply the actual coercion. We can use the middleware from `reitit.ring.coercion`: Defining a coercion for a route data doesn't do anything, as it's just data. We have to attach some code to apply the actual coercion. We can use the middleware from `reitit.ring.coercion`:
@ -202,9 +220,11 @@ is:
"application/edn" {:schema {:x s/Int}} "application/edn" {:schema {:x s/Int}}
:default {:schema {:ww s/Int}}}}} :default {:schema {:ww s/Int}}}}}
:handler ...}}]] :handler ...}}]]
{:data {:middleware [rrc/coerce-exceptions-middleware {:data {:muuntaja muuntaja.core/instance
rrc/coerce-request-middleware :middleware [reitit.ring.middleware.muuntaja/format-middleware
rrc/coerce-response-middleware]}}))) reitit.ring.coercion/coerce-exceptions-middleware
reitit.ring.coercion/coerce-request-middleware
reitit.ring.coercion/coerce-response-middleware]}})))
``` ```
The resolution logic for response coercers is: The resolution logic for response coercers is:
@ -215,6 +235,17 @@ The resolution logic for response coercers is:
3. `:body` 3. `:body`
3. If nothing was found, do not coerce 3. If nothing was found, do not coerce
To select the response content-type, you can either:
1. Let muuntaja pick the content-type based on things like the request Accept header
- This is what most users want
2. Set `:muuntaja/content-type` in the response to pick an explicit content type
3. Set the `"Content-Type"` header in the response
- This disables muuntaja, so you need to encode your response body in some other way!
- This is not compatible with response schema checking, since coercion won't know what to do with the already-encoded response body.
4. Use the `:extract-response-format` option to inject your own logic. See `reitit.coercion/extract-response-format-default` for the default.
See also the [muuntaja content negotiation](./content_negotiation.md) docs.
## Pretty printing spec errors ## Pretty printing spec errors
Spec problems are exposed as is in request & response coercion errors. Pretty-printers like [expound](https://github.com/bhb/expound) can be enabled like this: Spec problems are exposed as is in request & response coercion errors. Pretty-printers like [expound](https://github.com/bhb/expound) can be enabled like this:

View file

@ -2,6 +2,8 @@
Ring [defines middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware) as a function of type `handler & args => request => response`. It is relatively easy to understand and allows for good performance. A downside is that the middleware chain is just a opaque function, making things like debugging and composition hard. It is too easy to apply the middlewares in wrong order. Ring [defines middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware) as a function of type `handler & args => request => response`. It is relatively easy to understand and allows for good performance. A downside is that the middleware chain is just a opaque function, making things like debugging and composition hard. It is too easy to apply the middlewares in wrong order.
For the basics of reitit middleware, [read this first](ring.md#middleware).
Reitit defines middleware as data: Reitit defines middleware as data:
1. A middleware can be defined as first-class data entries 1. A middleware can be defined as first-class data entries

View file

@ -1,7 +1,7 @@
# Default Middleware # Default Middleware
```clj ```clj
[metosin/reitit-middleware "0.9.0"] [metosin/reitit-middleware "0.10.0"]
``` ```
Any Ring middleware can be used with `reitit-ring`, but using data-driven middleware is preferred as they are easier to manage and in many cases yield better performance. `reitit-middleware` contains a set of common ring middleware, lifted into data-driven middleware. Any Ring middleware can be used with `reitit-ring`, but using data-driven middleware is preferred as they are easier to manage and in many cases yield better performance. `reitit-middleware` contains a set of common ring middleware, lifted into data-driven middleware.
@ -17,8 +17,6 @@ Any Ring middleware can be used with `reitit-ring`, but using data-driven middle
`reitit.ring.middleware.parameters/parameters-middleware` to capture query- and form-params. Wraps `reitit.ring.middleware.parameters/parameters-middleware` to capture query- and form-params. Wraps
`ring.middleware.params/wrap-params`. `ring.middleware.params/wrap-params`.
**NOTE**: This middleware will be factored into two parts: a query-parameters middleware and a Muuntaja format responsible for the the `application/x-www-form-urlencoded` body format. cf. https://github.com/metosin/reitit/issues/134
## Exception Handling ## Exception Handling
See [Exception Handling with Ring](exceptions.md). See [Exception Handling with Ring](exceptions.md).

View file

@ -1,7 +1,7 @@
# Exception Handling with Ring # Exception Handling with Ring
```clj ```clj
[metosin/reitit-middleware "0.9.0"] [metosin/reitit-middleware "0.10.0"]
``` ```
Exceptions thrown in router creation can be [handled with custom exception handler](../basics/error_messages.md). By default, exceptions thrown at runtime from a handler or a middleware are not caught by the `reitit.ring/ring-handler`. A good practice is to have a top-level exception handler to log and format errors for clients. Exceptions thrown in router creation can be [handled with custom exception handler](../basics/error_messages.md). By default, exceptions thrown at runtime from a handler or a middleware are not caught by the `reitit.ring/ring-handler`. A good practice is to have a top-level exception handler to log and format errors for clients.

View file

@ -52,6 +52,20 @@ Router creation fails fast if the registry doesn't contain the middleware:
;| :bonus | reitit.ring_test$wrap_bonus@59fddabb | ;| :bonus | reitit.ring_test$wrap_bonus@59fddabb |
``` ```
Middleware defined in the registry can also be used on the `ring-handler` level:
```clj
(def app
(ring/ring-handler
(ring/router
["/api"
["/bonus" {:get (fn [{:keys [bonus]}]
{:status 200, :body {:bonus bonus}})}]]
{::middleware/registry {:bonus wrap-bonus}})
nil
{:middleware [[:bonus 15]]}))
```
## When to use the registry? ## When to use the registry?
Middleware as Keywords helps to keep the routes (all but handlers) as literal data (i.e. data that evaluates to itself), enabling the routes to be persisted in external formats like EDN-files and databases. Duct is a good example, where the [middleware can be referenced from EDN-files](https://github.com/duct-framework/duct/wiki/Configuration). It should be easy to make Duct configuration a Middleware Registry in `reitit-ring`. Middleware as Keywords helps to keep the routes (all but handlers) as literal data (i.e. data that evaluates to itself), enabling the routes to be persisted in external formats like EDN-files and databases. Duct is a good example, where the [middleware can be referenced from EDN-files](https://github.com/duct-framework/duct/wiki/Configuration). It should be easy to make Duct configuration a Middleware Registry in `reitit-ring`.

View file

@ -5,7 +5,7 @@
Read more about the [Ring Concepts](https://github.com/ring-clojure/ring/wiki/Concepts). Read more about the [Ring Concepts](https://github.com/ring-clojure/ring/wiki/Concepts).
```clj ```clj
[metosin/reitit-ring "0.9.0"] [metosin/reitit-ring "0.10.0"]
``` ```
## `reitit.ring/router` ## `reitit.ring/router`
@ -141,7 +141,7 @@ Name-based reverse routing:
# Middleware # Middleware
Middleware can be mounted using a `:middleware` key - either to top-level or under request method submap. Its value should be a vector of `reitit.middleware/IntoMiddleware` values. These include: Middleware can be mounted using a `:middleware` key in [Route Data](../basics/route_data.md) - either to top-level or under request method submap. Its value should be a vector of `reitit.middleware/IntoMiddleware` values. These include:
1. normal ring middleware function `handler -> request -> response` 1. normal ring middleware function `handler -> request -> response`
2. vector of middleware function `[handler args*] -> request -> response` and it's arguments 2. vector of middleware function `[handler args*] -> request -> response` and it's arguments
@ -194,11 +194,56 @@ Top-level middleware, applied before any routing is done:
(def app (def app
(ring/ring-handler (ring/ring-handler
(ring/router (ring/router
["/api" {:middleware [[mw :api]]} ["/api" {:middleware [[wrap :api]]}
["/get" {:get handler}]]) ["/get" {:get handler}]])
nil nil
{:middleware [[mw :top]]})) {:middleware [[wrap :top]]}))
(app {:request-method :get, :uri "/api/get"}) (app {:request-method :get, :uri "/api/get"})
; {:status 200, :body [:top :api :ok]} ; {:status 200, :body [:top :api :ok]}
``` ```
Same middleware for all routes, using [top-level route data](route_data.md#top-level-route-data):
```clj
(def app
(ring/ring-handler
(ring/router
["/api"
["/get" {:get handler
:middleware [[wrap :specific]]}]]
{:data {:middleware [[wrap :generic]]}})))
(app {:request-method :get, :uri "/api/get"})
; {:status 200, :body [:generic :specific :handler]}
```
## Execution order
Here's a full example that shows the execution order of the middleware
using all of the above techniques:
```clj
(def app
(ring/ring-handler
(ring/router
["/api" {:middleware [[wrap :3-parent]]}
["/get" {:get handler
:middleware [[wrap :4-route]]}]]
{:data {:middleware [[wrap :2-top-level-route-data]]}})
nil
{:middleware [[wrap :1-top]]}))
(app {:request-method :get, :uri "/api/get"})
; {:status 200, :body [:1-top :2-top-level-route-data :3-parent :4-route :handler]}
```
## Which method should I use for defining middleware?
- If you have middleware that you want to apply to the default handler (second argument of `ring/ring-handler`), use _top-level middleware_
- If you have a generic middleware, that doesn't depend on the route, use _top-level middleware_ or _top-level route data_
- If you are using top-level route data anyway for some other reasons, it might be clearest to have all the middleware there. This is what most of the reitit examples do.
- If you want to apply a middleware to only a couple of routes, use _nested middleware_ (ie. _route data_)
- If you want a middleware to apply to all routes, but use route-specific data, you need _top-level route data_ combined with [Compiling Middleware](compiling_middleware.md)
- This is what many reitit features like [Ring Coercion](coercion.md) do. Check the examples & docs for the reitit features you want to use!

View file

@ -1,7 +1,7 @@
# Swagger Support # Swagger Support
``` ```
[metosin/reitit-swagger "0.9.0"] [metosin/reitit-swagger "0.10.0"]
``` ```
Reitit supports [Swagger2](https://swagger.io/) documentation, thanks to [schema-tools](https://github.com/metosin/schema-tools) and [spec-tools](https://github.com/metosin/spec-tools). Documentation is extracted from route definitions, coercion `:parameters` and `:responses` and from a set of new documentation keys. Reitit supports [Swagger2](https://swagger.io/) documentation, thanks to [schema-tools](https://github.com/metosin/schema-tools) and [spec-tools](https://github.com/metosin/spec-tools). Documentation is extracted from route definitions, coercion `:parameters` and `:responses` and from a set of new documentation keys.
@ -47,7 +47,7 @@ If you need to post-process the generated spec, just wrap the handler with a cus
[Swagger-ui](https://github.com/swagger-api/swagger-ui) is a user interface to visualize and interact with the Swagger specification. To make things easy, there is a pre-integrated version of the swagger-ui as a separate module. [Swagger-ui](https://github.com/swagger-api/swagger-ui) is a user interface to visualize and interact with the Swagger specification. To make things easy, there is a pre-integrated version of the swagger-ui as a separate module.
``` ```
[metosin/reitit-swagger-ui "0.9.0"] [metosin/reitit-swagger-ui "0.10.0"]
``` ```
`reitit.swagger-ui/create-swagger-ui-handler` can be used to create a ring-handler to serve the swagger-ui. It accepts the following options: `reitit.swagger-ui/create-swagger-ui-handler` can be used to create a ring-handler to serve the swagger-ui. It accepts the following options:

View file

@ -59,7 +59,7 @@ There is an extra option in the Ring router (actually, in the underlying middlew
### Printing Request Diffs ### Printing Request Diffs
```clj ```clj
[metosin/reitit-middleware "0.9.0"] [metosin/reitit-middleware "0.10.0"]
``` ```
Using `reitit.ring.middleware.dev/print-request-diffs` transformation, the request diffs between each middleware are printed out to the console. To use it, add the following router option: Using `reitit.ring.middleware.dev/print-request-diffs` transformation, the request diffs between each middleware are printed out to the console. To use it, add the following router option:

View file

@ -2,6 +2,6 @@
:description "Reitit Buddy Auth App" :description "Reitit Buddy Auth App"
:dependencies [[org.clojure/clojure "1.11.2"] :dependencies [[org.clojure/clojure "1.11.2"]
[ring/ring-jetty-adapter "1.12.1"] [ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
[buddy "2.0.0"]] [buddy "2.0.0"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -10,9 +10,9 @@
[ring "1.12.1"] [ring "1.12.1"]
[hiccup "1.0.5"] [hiccup "1.0.5"]
[org.clojure/clojurescript "1.11.132"] [org.clojure/clojurescript "1.11.132"]
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
[metosin/reitit-schema "0.9.0"] [metosin/reitit-schema "0.10.0"]
[metosin/reitit-frontend "0.9.0"] [metosin/reitit-frontend "0.10.0"]
[cljsjs/react "17.0.2-0"] [cljsjs/react "17.0.2-0"]
[cljsjs/react-dom "17.0.2-0"] [cljsjs/react-dom "17.0.2-0"]
;; Just for pretty printting the match ;; Just for pretty printting the match

View file

@ -10,9 +10,9 @@
[ring "1.12.1"] [ring "1.12.1"]
[hiccup "1.0.5"] [hiccup "1.0.5"]
[org.clojure/clojurescript "1.11.132"] [org.clojure/clojurescript "1.11.132"]
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
[metosin/reitit-schema "0.9.0"] [metosin/reitit-schema "0.10.0"]
[metosin/reitit-frontend "0.9.0"] [metosin/reitit-frontend "0.10.0"]
[cljsjs/react "17.0.2-0"] [cljsjs/react "17.0.2-0"]
[cljsjs/react-dom "17.0.2-0"] [cljsjs/react-dom "17.0.2-0"]
;; Just for pretty printting the match ;; Just for pretty printting the match

View file

@ -10,9 +10,9 @@
[ring "1.12.1"] [ring "1.12.1"]
[hiccup "1.0.5"] [hiccup "1.0.5"]
[org.clojure/clojurescript "1.10.520"] [org.clojure/clojurescript "1.10.520"]
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
[metosin/reitit-spec "0.9.0"] [metosin/reitit-spec "0.10.0"]
[metosin/reitit-frontend "0.9.0"] [metosin/reitit-frontend "0.10.0"]
[cljsjs/react "17.0.2-0"] [cljsjs/react "17.0.2-0"]
[cljsjs/react-dom "17.0.2-0"] [cljsjs/react-dom "17.0.2-0"]
;; Just for pretty printting the match ;; Just for pretty printting the match

View file

@ -10,9 +10,9 @@
[ring "1.12.1"] [ring "1.12.1"]
[hiccup "1.0.5"] [hiccup "1.0.5"]
[org.clojure/clojurescript "1.11.132"] [org.clojure/clojurescript "1.11.132"]
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
[metosin/reitit-malli "0.9.0"] [metosin/reitit-malli "0.10.0"]
[metosin/reitit-frontend "0.9.0"] [metosin/reitit-frontend "0.10.0"]
[cljsjs/react "17.0.2-0"] [cljsjs/react "17.0.2-0"]
[cljsjs/react-dom "17.0.2-0"] [cljsjs/react-dom "17.0.2-0"]
;; Just for pretty printting the match ;; Just for pretty printting the match

View file

@ -10,9 +10,9 @@
[ring "1.12.1"] [ring "1.12.1"]
[hiccup "1.0.5"] [hiccup "1.0.5"]
[org.clojure/clojurescript "1.11.132"] [org.clojure/clojurescript "1.11.132"]
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
[metosin/reitit-spec "0.9.0"] [metosin/reitit-spec "0.10.0"]
[metosin/reitit-frontend "0.9.0"] [metosin/reitit-frontend "0.10.0"]
[cljsjs/react "17.0.2-0"] [cljsjs/react "17.0.2-0"]
[cljsjs/react-dom "17.0.2-0"] [cljsjs/react-dom "17.0.2-0"]
;; Just for pretty printting the match ;; Just for pretty printting the match

View file

@ -1,7 +1,7 @@
(defproject frontend-re-frame "0.1.0-SNAPSHOT" (defproject frontend-re-frame "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.11.2"] :dependencies [[org.clojure/clojure "1.11.2"]
[org.clojure/clojurescript "1.11.132"] [org.clojure/clojurescript "1.11.132"]
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
[reagent "1.2.0"] [reagent "1.2.0"]
[re-frame "0.10.6"] [re-frame "0.10.6"]
[cljsjs/react "17.0.2-0"] [cljsjs/react "17.0.2-0"]

View file

@ -1,6 +1,5 @@
(ns frontend-re-frame.core (ns frontend-re-frame.core
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as re-frame]
[reagent.core :as reagent]
[reagent.dom :as rd] [reagent.dom :as rd]
[reitit.core :as r] [reitit.core :as r]
[reitit.coercion.spec :as rss] [reitit.coercion.spec :as rss]

View file

@ -10,9 +10,9 @@
[ring "1.12.1"] [ring "1.12.1"]
[hiccup "1.0.5"] [hiccup "1.0.5"]
[org.clojure/clojurescript "1.11.132"] [org.clojure/clojurescript "1.11.132"]
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
[metosin/reitit-spec "0.9.0"] [metosin/reitit-spec "0.10.0"]
[metosin/reitit-frontend "0.9.0"] [metosin/reitit-frontend "0.10.0"]
[cljsjs/react "17.0.2-0"] [cljsjs/react "17.0.2-0"]
[cljsjs/react-dom "17.0.2-0"] [cljsjs/react-dom "17.0.2-0"]
;; Just for pretty printting the match ;; Just for pretty printting the match

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.11.2"] :dependencies [[org.clojure/clojure "1.11.2"]
[ring/ring-jetty-adapter "1.12.1"] [ring/ring-jetty-adapter "1.12.1"]
[aleph "0.7.1"] [aleph "0.7.1"]
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
[metosin/ring-swagger-ui "5.9.0"]] [metosin/ring-swagger-ui "5.9.0"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -5,5 +5,5 @@
[funcool/promesa "11.0.678"] [funcool/promesa "11.0.678"]
[manifold "0.4.2"] [manifold "0.4.2"]
[ring/ring-jetty-adapter "1.12.1"] [ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.9.0"]] [metosin/reitit "0.10.0"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -2,4 +2,4 @@
:description "Reitit coercion with vanilla ring" :description "Reitit coercion with vanilla ring"
:dependencies [[org.clojure/clojure "1.11.2"] :dependencies [[org.clojure/clojure "1.11.2"]
[ring/ring-jetty-adapter "1.12.1"] [ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.9.0"]]) [metosin/reitit "0.10.0"]])

View file

@ -3,7 +3,8 @@
:dependencies [[org.clojure/clojure "1.11.2"] :dependencies [[org.clojure/clojure "1.11.2"]
[metosin/jsonista "0.3.8"] [metosin/jsonista "0.3.8"]
[ring/ring-jetty-adapter "1.12.1"] [ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
[metosin/ring-swagger-ui "5.9.0"]] [metosin/ring-swagger-ui "5.9.0"]
[org.slf4j/slf4j-simple "2.0.9"]]
:repl-options {:init-ns example.server} :repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}}) :profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}})

View file

@ -12,7 +12,6 @@
[reitit.ring.middleware.multipart :as multipart] [reitit.ring.middleware.multipart :as multipart]
[reitit.ring.middleware.parameters :as parameters] [reitit.ring.middleware.parameters :as parameters]
[ring.adapter.jetty :as jetty] [ring.adapter.jetty :as jetty]
[malli.core :as malli]
[muuntaja.core :as m])) [muuntaja.core :as m]))
(def Transaction (def Transaction
@ -52,23 +51,34 @@
{:get {:summary "Fetch a pizza | Multiple content-types, multiple examples" {:get {:summary "Fetch a pizza | Multiple content-types, multiple examples"
:responses {200 {:description "Fetch a pizza as json or EDN" :responses {200 {:description "Fetch a pizza as json or EDN"
:content {"application/json" {:schema [:map :content {"application/json" {:schema [:map
[:format [:enum :json]]
[:color :keyword] [:color :keyword]
[:pineapple :boolean]] [:pineapple :boolean]]
:examples {:white {:description "White pizza with pineapple" :examples {:white {:description "White pizza with pineapple"
:value {:color :white :value {:format :json
:color :white
:pineapple true}} :pineapple true}}
:red {:description "Red pizza" :red {:description "Red pizza"
:value {:color :red :value {:format :json
:color :red
:pineapple false}}}} :pineapple false}}}}
"application/edn" {:schema [:map "application/edn" {:schema [:map
[:format [:enum :edn]]
[:color :keyword] [:color :keyword]
[:pineapple :boolean]] [:pineapple :boolean]]
:examples {:red {:description "Red pizza with pineapple" :examples {:red {:description "Red pizza with pineapple"
:value (pr-str {:color :red :pineapple true})}}}}}} :value (pr-str {:format :edn :color :red :pineapple true})}}}}}}
:handler (fn [_request] :handler (fn [_request]
{:status 200 (rand-nth [{:status 200
:body {:color :red :muuntaja/content-type "application/json"
:pineapple true}})} :body {:format :json
:color :red
:pineapple true}}
{:status 200
:muuntaja/content-type "application/edn"
:body {:format :edn
:color :red
:pineapple true}}]))}
:post {:summary "Create a pizza | Multiple content-types, multiple examples | Default response schema" :post {:summary "Create a pizza | Multiple content-types, multiple examples | Default response schema"
:request {:description "Create a pizza using json or EDN" :request {:description "Create a pizza using json or EDN"
:content {"application/json" {:schema [:map :content {"application/json" {:schema [:map
@ -146,22 +156,22 @@
[:regex [:re "[0-9]+"]] [:regex [:re "[0-9]+"]]
[:enum [:enum 1 3 5 42]] [:enum [:enum 1 3 5 42]]
[:multi [:multi {:dispatch :type} [:multi [:multi {:dispatch :type}
[:literal [:map ["literal" [:map
[:type [:= :literal]] [:type [:= "literal"]]
[:value [:or :int :string]]]] [:value [:or :int :string]]]]
[:reference [:map ["reference" [:map
[:type [:= :reference]] [:type [:= "reference"]]
[:description :string] [:description :string]
[:ref :uuid]]]]]] [:ref :uuid]]]]]]
:example {:vector-of-tuples [["a" 1] ["b" 2]] :example {:vector-of-tuples [["a" 1] ["b" 2]]
:regex "01234" :regex "01234"
:enum 5 :enum 5
:multi {:type :literal :multi {:type "literal"
:value "x"}}}}} :value "x"}}}}}
:responses {200 {:content {:default {:schema [:map]}}}} :responses {200 {:content {:default {:schema [:map-of :keyword :any]}}}}
:handler (fn [request] :handler (fn [request]
{:status 200 {:status 200
:body (:body request)})}}] :body (get-in request [:parameters :request])})}}]
["/secure" ["/secure"
{:tags #{"secure"} {:tags #{"secure"}

View file

@ -3,7 +3,7 @@
:dependencies [[org.clojure/clojure "1.11.2"] :dependencies [[org.clojure/clojure "1.11.2"]
[io.pedestal/pedestal.service "0.6.3"] [io.pedestal/pedestal.service "0.6.3"]
[io.pedestal/pedestal.jetty "0.6.3"] [io.pedestal/pedestal.jetty "0.6.3"]
[metosin/reitit-malli "0.9.0"] [metosin/reitit-malli "0.10.0"]
[metosin/reitit-pedestal "0.9.0"] [metosin/reitit-pedestal "0.10.0"]
[metosin/reitit "0.9.0"]] [metosin/reitit "0.10.0"]]
:repl-options {:init-ns server}) :repl-options {:init-ns server})

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.11.2"] :dependencies [[org.clojure/clojure "1.11.2"]
[io.pedestal/pedestal.service "0.6.3"] [io.pedestal/pedestal.service "0.6.3"]
[io.pedestal/pedestal.jetty "0.6.3"] [io.pedestal/pedestal.jetty "0.6.3"]
[metosin/reitit-pedestal "0.9.0"] [metosin/reitit-pedestal "0.10.0"]
[metosin/reitit "0.9.0"]] [metosin/reitit "0.10.0"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.11.2"] :dependencies [[org.clojure/clojure "1.11.2"]
[io.pedestal/pedestal.service "0.6.3"] [io.pedestal/pedestal.service "0.6.3"]
[io.pedestal/pedestal.jetty "0.6.3"] [io.pedestal/pedestal.jetty "0.6.3"]
[metosin/reitit-pedestal "0.9.0"] [metosin/reitit-pedestal "0.10.0"]
[metosin/reitit "0.9.0"]] [metosin/reitit "0.10.0"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -2,5 +2,5 @@
:description "Reitit Ring App" :description "Reitit Ring App"
:dependencies [[org.clojure/clojure "1.11.2"] :dependencies [[org.clojure/clojure "1.11.2"]
[ring/ring-jetty-adapter "1.12.1"] [ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.9.0"]] [metosin/reitit "0.10.0"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -2,7 +2,7 @@
:description "Reitit Ring App with Integrant" :description "Reitit Ring App with Integrant"
:dependencies [[org.clojure/clojure "1.11.2"] :dependencies [[org.clojure/clojure "1.11.2"]
[ring/ring-jetty-adapter "1.12.1"] [ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
[integrant "0.8.1"]] [integrant "0.8.1"]]
:main example.server :main example.server
:repl-options {:init-ns user} :repl-options {:init-ns user}

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.11.2"] :dependencies [[org.clojure/clojure "1.11.2"]
[metosin/jsonista "0.3.8"] [metosin/jsonista "0.3.8"]
[ring/ring-jetty-adapter "1.12.1"] [ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.9.0"]] [metosin/reitit "0.10.0"]]
:repl-options {:init-ns example.server} :repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}}) :profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}})

View file

@ -3,7 +3,7 @@
:dependencies [[org.clojure/clojure "1.11.2"] :dependencies [[org.clojure/clojure "1.11.2"]
[metosin/jsonista "0.3.8"] [metosin/jsonista "0.3.8"]
[ring/ring-jetty-adapter "1.12.1"] [ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
[metosin/ring-swagger-ui "5.9.0"]] [metosin/ring-swagger-ui "5.9.0"]]
:repl-options {:init-ns example.server} :repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}}) :profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}})

View file

@ -2,7 +2,7 @@
:description "Reitit Ring App with Swagger" :description "Reitit Ring App with Swagger"
:dependencies [[org.clojure/clojure "1.11.2"] :dependencies [[org.clojure/clojure "1.11.2"]
[ring/ring-jetty-adapter "1.12.1"] [ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.9.0"] [metosin/reitit "0.10.0"]
[metosin/ring-swagger-ui "5.9.0"]] [metosin/ring-swagger-ui "5.9.0"]]
:repl-options {:init-ns example.server} :repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}}) :profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}})

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-core "0.9.0" (defproject metosin/reitit-core "0.10.0"
:description "Snappy data-driven router for Clojure(Script)" :description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -152,8 +152,10 @@
rcs (request-coercers coercion parameters (cond-> opts route-request (assoc ::skip #{:body})))] rcs (request-coercers coercion parameters (cond-> opts route-request (assoc ::skip #{:body})))]
(if (and crc rcs) (into crc (vec rcs)) (or crc rcs))))) (if (and crc rcs) (into crc (vec rcs)) (or crc rcs)))))
(defn extract-response-format-default [request _] (defn extract-response-format-default [request response]
(-> request :muuntaja/response :format)) (or (get-in response [:headers "Content-Type"])
(:muuntaja/content-type response)
(-> request :muuntaja/response :format)))
(defn -format->coercer [coercion {:keys [content body]} _opts] (defn -format->coercer [coercion {:keys [content body]} _opts]
(->> (concat (when body (->> (concat (when body
@ -178,8 +180,9 @@
(let [format->coercer (or (status->format->coercer (:status response)) (let [format->coercer (or (status->format->coercer (:status response))
(status->format->coercer :default)) (status->format->coercer :default))
format (extract-response-format request response) format (extract-response-format request response)
coercer (or (format->coercer format) coercer (when format->coercer
(format->coercer :default))] (or (format->coercer format)
(format->coercer :default)))]
(if-not coercer (if-not coercer
response response
(let [value (:body response) (let [value (:body response)
@ -241,7 +244,7 @@
(if coercer (if coercer
(let [result (coercer query-params :default)] (let [result (coercer query-params :default)]
(if (error? result) (if (error? result)
(throw (ex-info (str "Query parameters coercion failed") (throw (ex-info "Query parameters coercion failed"
result)) result))
result)) result))
query-params)))) query-params))))

View file

@ -46,7 +46,7 @@
(options [this]) (options [this])
(route-names [this]) (route-names [this])
(match-by-path [this path]) (match-by-path [this path])
(match-by-name [this name] [this name path-params])) (match-by-name [this name] [this name path-params] [this name path-params opts]))
(defn router? [x] (defn router? [x]
(satisfies? Router x)) (satisfies? Router x))
@ -122,9 +122,11 @@
(match-by-name [_ name] (match-by-name [_ name]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
(match nil))) (match nil)))
(match-by-name [_ name path-params] (match-by-name [r name path-params]
(match-by-name r name path-params nil))
(match-by-name [_ name path-params opts]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
(match (impl/path-params path-params)))))))) (match (impl/path-params path-params opts))))))))
(defn lookup-router (defn lookup-router
"Creates a lookup-router from resolved routes and optional "Creates a lookup-router from resolved routes and optional
@ -161,9 +163,11 @@
(match-by-name [_ name] (match-by-name [_ name]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
(match nil))) (match nil)))
(match-by-name [_ name path-params] (match-by-name [r name path-params]
(match-by-name r name path-params nil))
(match-by-name [_ name path-params opts]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
(match (impl/path-params path-params)))))))) (match (impl/path-params path-params opts))))))))
(defn trie-router (defn trie-router
"Creates a special prefix-tree router from resolved routes and optional "Creates a special prefix-tree router from resolved routes and optional
@ -208,9 +212,11 @@
(match-by-name [_ name] (match-by-name [_ name]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
(match nil))) (match nil)))
(match-by-name [_ name path-params] (match-by-name [r name path-params]
(match-by-name r name path-params nil))
(match-by-name [_ name path-params opts]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
(match (impl/path-params path-params)))))))) (match (impl/path-params path-params opts))))))))
(defn single-static-path-router (defn single-static-path-router
"Creates a fast router of 1 static route(s) and optional "Creates a fast router of 1 static route(s) and optional
@ -238,8 +244,10 @@
(if (#?(:clj .equals :cljs =) p path) match)) (if (#?(:clj .equals :cljs =) p path) match))
(match-by-name [_ name] (match-by-name [_ name]
(if (= n name) match)) (if (= n name) match))
(match-by-name [_ name path-params] (match-by-name [r name path-params]
(if (= n name) (impl/fast-assoc match :path-params (impl/path-params path-params)))))))) (match-by-name r name path-params nil))
(match-by-name [_ name path-params opts]
(if (= n name) (impl/fast-assoc match :path-params (impl/path-params path-params opts))))))))
(defn mixed-router (defn mixed-router
"Creates two routers: [[lookup-router]] or [[single-static-path-router]] for "Creates two routers: [[lookup-router]] or [[single-static-path-router]] for
@ -268,9 +276,11 @@
(match-by-name [_ name] (match-by-name [_ name]
(or (match-by-name static-router name) (or (match-by-name static-router name)
(match-by-name wildcard-router name))) (match-by-name wildcard-router name)))
(match-by-name [_ name path-params] (match-by-name [r name path-params]
(or (match-by-name static-router name path-params) (match-by-name r name path-params nil))
(match-by-name wildcard-router name path-params))))))) (match-by-name [_ name path-params opts]
(or (match-by-name static-router name path-params opts)
(match-by-name wildcard-router name path-params opts)))))))
(defn quarantine-router (defn quarantine-router
"Creates two routers: [[mixed-router]] for non-conflicting routes "Creates two routers: [[mixed-router]] for non-conflicting routes
@ -299,9 +309,11 @@
(match-by-name [_ name] (match-by-name [_ name]
(or (match-by-name mixed-router name) (or (match-by-name mixed-router name)
(match-by-name linear-router name))) (match-by-name linear-router name)))
(match-by-name [_ name path-params] (match-by-name [r name path-params]
(or (match-by-name mixed-router name path-params) (match-by-name r name path-params nil))
(match-by-name linear-router name path-params))))))) (match-by-name [_ name path-params opts]
(or (match-by-name mixed-router name path-params opts)
(match-by-name linear-router name path-params opts)))))))
;; ;;
;; Creating Routers ;; Creating Routers

View file

@ -1,52 +0,0 @@
(ns reitit.dependency
"Dependency resolution for middleware/interceptors."
(:require [reitit.exception :as exception]))
(defn- providers
"Map from provision key to provider. `get-provides` should return the provision keys of a dependent."
[get-provides nodes]
(reduce (fn [acc dependent]
(into acc
(map (fn [provide]
(when (contains? acc provide)
(exception/fail!
(str "multiple providers for: " provide)
{::multiple-providers provide}))
[provide dependent]))
(get-provides dependent)))
{} nodes))
(defn- get-provider
"Get the provider for `k`, throw if no provider can be found for it."
[providers k]
(if (contains? providers k)
(get providers k)
(exception/fail!
(str "provider missing for dependency: " k)
{::missing-provider k})))
(defn post-order
"Put `nodes` in post-order. Can also be described as a reverse topological sort.
`get-provides` and `get-requires` are callbacks that you can provide to compute the provide and depend
key sets of nodes, the defaults are `:provides` and `:requires`."
([nodes] (post-order :provides :requires nodes))
([get-provides get-requires nodes]
(let [providers-by-key (providers get-provides nodes)]
(letfn [(toposort [node path colors]
(case (get colors node)
:white (let [requires (get-requires node)
[nodes* colors] (toposort-seq (map (partial get-provider providers-by-key) requires)
(conj path node)
(assoc colors node :grey))]
[(conj nodes* node)
(assoc colors node :black)])
:grey (exception/fail! "circular dependency" {:cycle (drop-while #(not= % node) (conj path node))})
:black [() colors]))
(toposort-seq [nodes path colors]
(reduce (fn [[nodes* colors] node]
(let [[nodes** colors] (toposort node path colors)]
[(into nodes* nodes**) colors]))
[[] colors] nodes))]
(first (toposort-seq nodes [] (zipmap nodes (repeat :white))))))))

View file

@ -7,6 +7,10 @@
([type data] ([type data]
(throw (ex-info (str type) {:type type, :data data})))) (throw (ex-info (str type) {:type type, :data data}))))
(defn unsupported-protocol-method!
[method]
(fail! :unsupported-protocol-method {:method method}))
(defn get-message [e] (defn get-message [e]
#?(:clj (.getMessage ^Exception e) :cljs (ex-message e))) #?(:clj (.getMessage ^Exception e) :cljs (ex-message e)))

View file

@ -1,7 +1,6 @@
(ns ^:no-doc reitit.impl (ns ^:no-doc reitit.impl
#?(:cljs (:require-macros [reitit.impl])) #?(:cljs (:require-macros [reitit.impl]))
(:require [clojure.set :as set] (:require [clojure.string :as str]
[clojure.string :as str]
[meta-merge.core :as mm] [meta-merge.core :as mm]
[reitit.exception :as ex] [reitit.exception :as ex]
[reitit.trie :as trie]) [reitit.trie :as trie])
@ -198,9 +197,8 @@
(:path route))) (:path route)))
(defn throw-on-missing-path-params [template required path-params] (defn throw-on-missing-path-params [template required path-params]
(when-not (every? #(contains? path-params %) required) (let [missing (set (remove #(get path-params %) required))]
(let [defined (-> path-params keys set) (when-not (empty? missing)
missing (set/difference required defined)]
(ex/fail! (ex/fail!
(str "missing path-params for route " template " -> " missing) (str "missing path-params for route " template " -> " missing)
{:path-params path-params, :required required})))) {:path-params path-params, :required required}))))
@ -299,8 +297,11 @@
(defn path-params (defn path-params
"Convert parameters' values into URL-encoded strings, suitable for URL paths" "Convert parameters' values into URL-encoded strings, suitable for URL paths"
[params] ([params] (path-params params nil))
(maybe-map-values #(url-encode (into-string %)) params)) ([params {:keys [url-encode?] :or {url-encode? true}}]
(if url-encode?
(maybe-map-values #(url-encode (into-string %)) params)
(maybe-map-values #(into-string %) params))))
(defn- query-parameter [k v] (defn- query-parameter [k v]
(str (form-encode (into-string k)) (str (form-encode (into-string k))

View file

@ -37,8 +37,11 @@
;; Default data ;; Default data
;; ;;
(defn -multi? [x]
(instance? #?(:clj clojure.lang.MultiFn :cljs cljs.core.MultiFn) x))
(s/def ::name keyword?) (s/def ::name keyword?)
(s/def ::handler (s/or :fn fn? :var var?)) (s/def ::handler (s/or :fn fn? :var var? :multi -multi?))
(s/def ::no-doc boolean?) (s/def ::no-doc boolean?)
(s/def ::conflicting boolean?) (s/def ::conflicting boolean?)
(s/def ::default-data (s/def ::default-data

View file

@ -71,11 +71,17 @@
(and bracket? (= \{ c)) (and bracket? (= \{ c))
(let [^long to' (or (str/index-of s "}" to) (ex/fail! ::unclosed-brackets {:path s}))] (let [^long to' (or (str/index-of s "}" to) (ex/fail! ::unclosed-brackets {:path s}))]
(if (= \* (get s (inc to))) (cond
(= \* (get s (inc to)))
(recur (concat ss (-static from to) (-catch-all (inc to) to')) (long (inc to')) (long (inc to'))) (recur (concat ss (-static from to) (-catch-all (inc to) to')) (long (inc to')) (long (inc to')))
(= \: (get s (inc to)))
(recur (concat ss (-static from to) (-wild (inc to) to')) (long (inc to')) (long (inc to')))
:else
(recur (concat ss (-static from to) (-wild to to')) (long (inc to')) (long (inc to'))))) (recur (concat ss (-static from to) (-wild to to')) (long (inc to')) (long (inc to')))))
(and colon? (= \: c)) (and colon? (= \: c) (not= \{ (get s (dec to))))
(let [^long to' (or (str/index-of s "/" to) (count s))] (let [^long to' (or (str/index-of s "/" to) (count s))]
(if (= 1 (- to' to)) (if (= 1 (- to' to))
(recur ss from (inc to)) (recur ss from (inc to))

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-dev "0.9.0" (defproject metosin/reitit-dev "0.10.0"
:description "Snappy data-driven router for Clojure(Script)" :description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -174,7 +174,7 @@
;; TODO: this is hack, but seems to work and is safe. ;; TODO: this is hack, but seems to work and is safe.
(defn source-str [[target _ file line]] (defn source-str [[target _ file line]]
(try (try
(if (and (not= 1 line)) (if (not= 1 line)
(let [file-name (str/replace file #"(.*?)\.\S[^\.]+" "$1") (let [file-name (str/replace file #"(.*?)\.\S[^\.]+" "$1")
target-name (name target) target-name (name target)
ns (str (subs target-name 0 (or (str/index-of target-name (str file-name "$")) 0)) file-name)] ns (str (subs target-name 0 (or (str/index-of target-name (str file-name "$")) 0)) file-name)]
@ -190,7 +190,7 @@
(color :title message " ") (color :title message " ")
(color :title-dark (repeat-str "-" between) " ") (color :title-dark (repeat-str "-" between) " ")
(color :title source) " " (color :title source) " "
(color :title-dark (str "--"))])) (color :title-dark "--")]))
(defn footer [{:keys [width]}] (defn footer [{:keys [width]}]
(color :title-dark (repeat-str "-" width))) (color :title-dark (repeat-str "-" width)))

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-frontend "0.9.0" (defproject metosin/reitit-frontend "0.10.0"
:description "Reitit: Clojurescript frontend routing core" :description "Reitit: Clojurescript frontend routing core"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-http "0.9.0" (defproject metosin/reitit-http "0.10.0"
:description "Reitit: HTTP routing with interceptors" :description "Reitit: HTTP routing with interceptors"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -22,12 +22,12 @@
compile (fn [[path data] opts scope] compile (fn [[path data] opts scope]
(interceptor/compile-result [path data] opts scope)) (interceptor/compile-result [path data] opts scope))
->endpoint (fn [p d m s] ->endpoint (fn [p d m s]
(let [d (ring/-compile-coercion d)] (let [d (ring/-compile-coercion d)
(let [compiled (compile [p d] opts s)] compiled (compile [p d] opts s)]
(-> compiled (-> compiled
(map->Endpoint) (map->Endpoint)
(assoc :path p) (assoc :path p)
(assoc :method m))))) (assoc :method m))))
->methods (fn [any? data] ->methods (fn [any? data]
(reduce (reduce
(fn [acc method] (fn [acc method]

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-interceptors "0.9.0" (defproject metosin/reitit-interceptors "0.10.0"
:description "Reitit, common interceptors bundled" :description "Reitit, common interceptors bundled"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-malli "0.9.0" (defproject metosin/reitit-malli "0.10.0"
:description "Reitit: Malli coercion" :description "Reitit: Malli coercion"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -9,8 +9,7 @@
[malli.swagger :as swagger] [malli.swagger :as swagger]
[malli.transform :as mt] [malli.transform :as mt]
[malli.util :as mu] [malli.util :as mu]
[reitit.coercion :as coercion] [reitit.coercion :as coercion]))
[clojure.string :as string]))
;; ;;
;; coercion ;; coercion
@ -31,7 +30,7 @@
(mt/transformer (mt/transformer
(if strip-extra-keys (mt/strip-extra-keys-transformer)) (if strip-extra-keys (mt/strip-extra-keys-transformer))
transformer transformer
(if default-values (mt/default-value-transformer)))))) (if default-values (mt/default-value-transformer (if (map? default-values) default-values {})))))))
(def string-transformer-provider (-provider (mt/string-transformer))) (def string-transformer-provider (-provider (mt/string-transformer)))
(def json-transformer-provider (-provider (mt/json-transformer))) (def json-transformer-provider (-provider (mt/json-transformer)))
@ -116,7 +115,9 @@
:enabled true :enabled true
;; strip-extra-keys (affects only predefined transformers) ;; strip-extra-keys (affects only predefined transformers)
:strip-extra-keys true :strip-extra-keys true
;; add/set default values ;; add/set default values.
;; Can be false, true or a map of options to pass to malli.transform/default-value-transformer,
;; for example {:malli.transform/add-optional-keys true}
:default-values true :default-values true
;; encode-error ;; encode-error
:encode-error nil :encode-error nil
@ -187,7 +188,8 @@
(-open-model [_ schema] schema) (-open-model [_ schema] schema)
(-encode-error [_ error] (-encode-error [_ error]
(cond-> error (cond-> error
(show? :humanized) (assoc :humanized (me/humanize error {:wrap :message})) (show? :humanized) (assoc :humanized (me/humanize error (cond-> {:wrap :message}
options (merge options))))
(show? :schema) (update :schema edn/write-string opts) (show? :schema) (update :schema edn/write-string opts)
(show? :errors) (-> (me/with-error-messages opts) (show? :errors) (-> (me/with-error-messages opts)
(update :errors (partial map #(update % :schema edn/write-string opts)))) (update :errors (partial map #(update % :schema edn/write-string opts))))

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-middleware "0.9.0" (defproject metosin/reitit-middleware "0.10.0"
:description "Reitit, common middleware bundled" :description "Reitit, common middleware bundled"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -19,17 +19,17 @@
(recur (.getSuperclass sk) (conj ks sk)) (recur (.getSuperclass sk) (conj ks sk))
ks))) ks)))
(defn- descendants-safe [type] (defn- find-closest-ancestor [val m]
(when-not (class? type) (descendants type))) (or (get m val)
(some #(find-closest-ancestor % m) (parents val))))
(defn- call-error-handler [handlers error request] (defn- call-error-handler [handlers error request]
(let [type (:type (ex-data error)) (let [type (:type (ex-data error))
ex-class (class error) ex-class (class error)
error-handler (or (get handlers type) error-handler (or (get handlers type)
(get handlers ex-class) (get handlers ex-class)
(some (when-not (class? type)
(partial get handlers) (find-closest-ancestor type handlers))
(descendants-safe type))
(some (some
(partial get handlers) (partial get handlers)
(super-classes ex-class)) (super-classes ex-class))
@ -143,6 +143,9 @@
4) Super Classes of exception 4) Super Classes of exception
5) The ::default handler 5) The ::default handler
Note! If the closest ancestor for `:type` is not unique, an
arbitrary one is picked.
Example: Example:
(require '[reitit.ring.middleware.exception :as exception]) (require '[reitit.ring.middleware.exception :as exception])

View file

@ -58,7 +58,10 @@
"Creates a Middleware to handle the multipart params, based on "Creates a Middleware to handle the multipart params, based on
ring.middleware.multipart-params, taking same options. Mounts only ring.middleware.multipart-params, taking same options. Mounts only
if endpoint has `[:parameters :multipart]` defined. Publishes coerced if endpoint has `[:parameters :multipart]` defined. Publishes coerced
parameters into `[:parameters :multipart]` under request." parameters into `[:parameters :multipart]` under request.
Note! You want to have multipart-middleware after coerce-request-middleware,
because coerce-request-middleware overwrites `:parameters`."
([] ([]
(create-multipart-middleware nil)) (create-multipart-middleware nil))
([options] ([options]
@ -69,5 +72,8 @@
"Middleware to handle the multipart params, based on "Middleware to handle the multipart params, based on
ring.middleware.multipart-params, taking same options. Mounts only ring.middleware.multipart-params, taking same options. Mounts only
if endpoint has `[:parameters :multipart]` defined. Publishes coerced if endpoint has `[:parameters :multipart]` defined. Publishes coerced
parameters into `[:parameters :multipart]` under request." parameters into `[:parameters :multipart]` under request.
Note! You want to have multipart-middleware after coerce-request-middleware,
because coerce-request-middleware overwrites `:parameters`."
(create-multipart-middleware)) (create-multipart-middleware))

View file

@ -1,4 +1,4 @@
(defproject fi.metosin/reitit-openapi "0.9.0" (defproject fi.metosin/reitit-openapi "0.10.0"
:description "Reitit: OpenAPI-support" :description "Reitit: OpenAPI-support"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -206,20 +206,23 @@
accept-route (fn [route] accept-route (fn [route]
(-> route second :openapi :id (or ::default) (trie/into-set) (set/intersection ids) seq)) (-> route second :openapi :id (or ::default) (trie/into-set) (set/intersection ids) seq))
definitions (volatile! {}) definitions (volatile! {})
transform-endpoint (fn [[method {{:keys [coercion no-doc openapi] :as data} :data transform-endpoint (fn [path [method {{:keys [coercion no-doc openapi] :as data} :data
middleware :middleware middleware :middleware
interceptors :interceptors}]] interceptors :interceptors}]]
(if (and data (not no-doc)) (try
[method (if (and data (not no-doc))
(meta-merge [method
(apply meta-merge (keep (comp :openapi :data) middleware)) (meta-merge
(apply meta-merge (keep (comp :openapi :data) interceptors)) (apply meta-merge (keep (comp :openapi :data) middleware))
(if coercion (apply meta-merge (keep (comp :openapi :data) interceptors))
(-get-apidocs-openapi coercion data definitions)) (if coercion
(select-keys data [:tags :summary :description]) (-get-apidocs-openapi coercion data definitions))
(strip-top-level-keys openapi))])) (select-keys data [:tags :summary :description])
(strip-top-level-keys openapi))])
(catch Throwable t
(throw (ex-info "While building openapi docs" {:path path :method method} t)))))
transform-path (fn [[p _ c]] transform-path (fn [[p _ c]]
(if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))] (if-let [endpoint (some->> c (keep (partial transform-endpoint p)) (seq) (into {}))]
[(openapi-path p (r/options router)) endpoint])) [(openapi-path p (r/options router)) endpoint]))
map-in-order #(->> % (apply concat) (apply array-map)) map-in-order #(->> % (apply concat) (apply array-map))
paths (->> router (r/compiled-routes) (filter accept-route) (map transform-path) map-in-order)] paths (->> router (r/compiled-routes) (filter accept-route) (map transform-path) map-in-order)]

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-pedestal "0.9.0" (defproject metosin/reitit-pedestal "0.10.0"
:description "Reitit + Pedestal Integration" :description "Reitit + Pedestal Integration"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -2,10 +2,10 @@
(:require [io.pedestal.http :as http] (:require [io.pedestal.http :as http]
[io.pedestal.interceptor :as interceptor] [io.pedestal.interceptor :as interceptor]
[io.pedestal.interceptor.chain :as chain] [io.pedestal.interceptor.chain :as chain]
[reitit.exception :as ex]
[reitit.http] [reitit.http]
[reitit.interceptor]) [reitit.interceptor])
(:import (java.lang.reflect Method) (:import (java.lang.reflect Method)))
(reitit.interceptor Executor)))
;; TODO: variadic ;; TODO: variadic
(defn- arities [f] (defn- arities [f]
@ -46,12 +46,16 @@
(def pedestal-executor (def pedestal-executor
(reify (reify
Executor reitit.interceptor/Executor
(queue [_ interceptors] (queue [_ interceptors]
(->> interceptors (->> interceptors
(map (fn [{::interceptor/keys [handler] :as interceptor}] (map (fn [{::interceptor/keys [handler] :as interceptor}]
(or handler interceptor))) (or handler interceptor)))
(keep ->interceptor))) (keep ->interceptor)))
(execute [_ _ _]
(ex/unsupported-protocol-method! 'reitit.interceptor/execute))
(execute [_ _ _ _ _]
(ex/unsupported-protocol-method! 'reitit.interceptor/execute))
(enqueue [_ context interceptors] (enqueue [_ context interceptors]
(chain/enqueue context interceptors)))) (chain/enqueue context interceptors))))

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-ring "0.9.0" (defproject metosin/reitit-ring "0.10.0"
:description "Reitit: Ring routing" :description "Reitit: Ring routing"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -173,7 +173,8 @@
(letfn [(maybe-redirect [{:keys [query-string] :as request} path] (letfn [(maybe-redirect [{:keys [query-string] :as request} path]
(if (and (seq path) (r/match-by-path (::r/router request) path)) (if (and (seq path) (r/match-by-path (::r/router request) path))
{:status (if (= (:request-method request) :get) 301 308) {:status (if (= (:request-method request) :get) 301 308)
:headers {"Location" (if query-string (str path "?" query-string) path)} :headers {"Location" (let [path (str/replace-first path #"^/+" "/")] ; Locations starting with // redirect to another hostname. Avoid these due to security implications.
(if query-string (str path "?" query-string) path))}
:body ""})) :body ""}))
(redirect-handler [request] (redirect-handler [request]
(let [uri (:uri request)] (let [uri (:uri request)]
@ -298,7 +299,8 @@
| :index-redirect? | optional boolean: if true (default false), redirect to index file, if false serve it directly | :index-redirect? | optional boolean: if true (default false), redirect to index file, if false serve it directly
| :canonicalize-uris? | optional boolean: if true (default), try to serve index files for non directory paths (paths that end with slash) | :canonicalize-uris? | optional boolean: if true (default), try to serve index files for non directory paths (paths that end with slash)
| :not-found-handler | optional handler function to use if the requested resource is missing (404 Not Found) | :not-found-handler | optional handler function to use if the requested resource is missing (404 Not Found)
| :mime-types | optional map of filename extensions to mime-types that will be used to guess the content type in addition to the ones defined in ring.util.mime-type/default-mime-types" | :mime-types | optional map of filename extensions to mime-types that will be used to guess the content type in addition to the ones defined in ring.util.mime-type/default-mime-types
| :allow-symlinks? | allow symlinks that lead to paths outside the root classpath directories, defaults to false"
([] ([]
(create-resource-handler nil)) (create-resource-handler nil))
([opts] ([opts]
@ -318,7 +320,8 @@
| :index-redirect? | optional boolean: if true (default false), redirect to index file, if false serve it directly | :index-redirect? | optional boolean: if true (default false), redirect to index file, if false serve it directly
| :canonicalize-uris? | optional boolean: if true (default), try to serve index files for non directory paths (paths that end with slash) | :canonicalize-uris? | optional boolean: if true (default), try to serve index files for non directory paths (paths that end with slash)
| :not-found-handler | optional handler function to use if the requested resource is missing (404 Not Found) | :not-found-handler | optional handler function to use if the requested resource is missing (404 Not Found)
| :mime-types | optional map of filename extensions to mime-types that will be used to guess the content type in addition to the ones defined in ring.util.mime-type/default-mime-types" | :mime-types | optional map of filename extensions to mime-types that will be used to guess the content type in addition to the ones defined in ring.util.mime-type/default-mime-types
| :allow-symlinks? | allow symlinks that lead to paths outside the root classpath directories, defaults to false"
([] ([]
(create-file-handler nil)) (create-file-handler nil))
([opts] ([opts]
@ -370,7 +373,7 @@
([router default-handler {:keys [middleware inject-match? inject-router?] ([router default-handler {:keys [middleware inject-match? inject-router?]
:or {inject-match? true, inject-router? true}}] :or {inject-match? true, inject-router? true}}]
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil)))) (let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
wrap (if middleware (partial middleware/chain middleware) identity) wrap (if middleware #(middleware/chain middleware % nil (r/options router)) identity)
enrich-request (create-enrich-request inject-match? inject-router?) enrich-request (create-enrich-request inject-match? inject-router?)
enrich-default-request (create-enrich-default-request inject-router?)] enrich-default-request (create-enrich-default-request inject-router?)]
(with-meta (with-meta

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-schema "0.9.0" (defproject metosin/reitit-schema "0.10.0"
:description "Reitit: Plumatic Schema coercion" :description "Reitit: Plumatic Schema coercion"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-sieppari "0.9.0" (defproject metosin/reitit-sieppari "0.10.0"
:description "Reitit: Sieppari Interceptors" :description "Reitit: Sieppari Interceptors"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -1,5 +1,6 @@
(ns reitit.interceptor.sieppari (ns reitit.interceptor.sieppari
(:require [reitit.interceptor :as interceptor] (:require [reitit.exception :as ex]
[reitit.interceptor :as interceptor]
[sieppari.core :as sieppari] [sieppari.core :as sieppari]
[sieppari.queue :as queue])) [sieppari.queue :as queue]))
@ -15,4 +16,6 @@
(execute [_ interceptors request] (execute [_ interceptors request]
(sieppari/execute interceptors request)) (sieppari/execute interceptors request))
(execute [_ interceptors request respond raise] (execute [_ interceptors request respond raise]
(sieppari/execute interceptors request respond raise)))) (sieppari/execute interceptors request respond raise))
(enqueue [_ _ _]
(ex/unsupported-protocol-method! 'reitit.interceptor/enqueue))))

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-spec "0.9.0" (defproject metosin/reitit-spec "0.10.0"
:description "Reitit: clojure.spec coercion" :description "Reitit: clojure.spec coercion"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-swagger-ui "0.9.0" (defproject metosin/reitit-swagger-ui "0.10.0"
:description "Reitit: Swagger-ui support" :description "Reitit: Swagger-ui support"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-swagger "0.9.0" (defproject metosin/reitit-swagger "0.10.0"
:description "Reitit: Swagger-support" :description "Reitit: Swagger-support"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit "0.9.0" (defproject metosin/reitit "0.10.0"
:description "Snappy data-driven router for Clojure(Script)" :description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

2145
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,13 +2,13 @@
"name": "reitit", "name": "reitit",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@seriousme/openapi-schema-validator": "^2.4.0", "@seriousme/openapi-schema-validator": "^2.7.0",
"karma": "^6.4.4", "karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0", "karma-chrome-launcher": "^3.2.0",
"karma-cli": "^2.0.0", "karma-cli": "^2.0.0",
"karma-cljs-test": "^0.1.0" "karma-cljs-test": "^0.1.0"
}, },
"dependencies": { "dependencies": {
"shadow-cljs": "^2.28.22" "shadow-cljs": "^3.2.1"
} }
} }

View file

@ -80,12 +80,15 @@
(defrecord NoOpCoercion [] (defrecord NoOpCoercion []
coercion/Coercion coercion/Coercion
(-get-name [_] :no-op) (-get-name [_] :no-op)
(-get-options [_])
(-get-apidocs [_ _ {:keys [parameters responses] :as info}]) (-get-apidocs [_ _ {:keys [parameters responses] :as info}])
(-get-model-apidocs [_ _ _ _])
(-compile-model [_ model _] model) (-compile-model [_ model _] model)
(-open-model [_ spec] spec) (-open-model [_ spec] spec)
(-encode-error [_ error] error) (-encode-error [_ error] error)
(-request-coercer [_ type spec] (fn [value format] value)) (-request-coercer [_ type spec] (fn [value format] value))
(-response-coercer [this spec] (coercion/request-coercer this :response spec {}))) (-response-coercer [this spec] (coercion/request-coercer this :response spec {}))
(-query-string-coercer [_ _]))
(comment (comment
(doseq [coercion [nil (->NoOpCoercion) spec/coercion]] (doseq [coercion [nil (->NoOpCoercion) spec/coercion]]

View file

@ -53,27 +53,27 @@
request (json-request data) request (json-request data)
request! (request-stream request)]] request! (request-stream request)]]
"10b" ;; # 10b
;; 38µs (c-api 1.x) ;; 38µs (c-api 1.x)
;; 14µs (c-api 2.0.0-alpha21) ;; 14µs (c-api 2.0.0-alpha21)
;; 6µs ;; 6µs
"100b" ;; # 100b
;; 74µs (c-api 1.x) ;; 74µs (c-api 1.x)
;; 16µs (c-api 2.0.0-alpha21) ;; 16µs (c-api 2.0.0-alpha21)
;; 8µs ;; 8µs
"1k" ;; # 1k
;; 322µs (c-api 1.x) ;; 322µs (c-api 1.x)
;; 24µs (c-api 2.0.0-alpha21) ;; 24µs (c-api 2.0.0-alpha21)
;; 16µs ;; 16µs
"10k" ;; # 10k
;; 3300µs (c-api 1.x) ;; 3300µs (c-api 1.x)
;; 120µs (c-api 2.0.0-alpha21) ;; 120µs (c-api 2.0.0-alpha21)
;; 110µs ;; 110µs
"100k" ;; # 100k
;; 10600µs (c-api 1.x) ;; 10600µs (c-api 1.x)
;; 1100µs (c-api 2.0.0-alpha21) ;; 1100µs (c-api 2.0.0-alpha21)
;; 1100µs ;; 1100µs

View file

@ -37,40 +37,40 @@
(defn routing-test [] (defn routing-test []
;; 21385 / 14337 ;; 21385 / 14337
"barista" ;; "barista"
;; 26259 / 25571 ;; 26259 / 25571
"choreographer" ;; "choreographer"
;; 24277 / 19174 ;; 24277 / 19174
"clutch" ;; "clutch"
;; 26158 / 25584 ;; 26158 / 25584
"connect" ;; "connect"
;; 24614 / 25413 ;; 24614 / 25413
"escort" ;; "escort"
;; 21979 / 18595 ;; 21979 / 18595
"express" ;; "express"
;; 23123 / 25405 ;; 23123 / 25405
"find-my-way" ;; "find-my-way"
;; 24798 / 25286 ;; 24798 / 25286
"http-hash" ;; "http-hash"
;; 24215 / 23670 ;; 24215 / 23670
"i40" ;; "i40"
;; 23561 / 26278 ;; 23561 / 26278
"light-router" ;; "light-router"
;; 28362 / 30056 ;; 28362 / 30056
"http-raw" ;; "http-raw"
;; 25310 / 25126 ;; 25310 / 25126
"regex" ;; "regex"
;; 112719 / 113959 ;; 112719 / 113959
(title "reitit") (title "reitit")

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-parent "0.9.0" (defproject metosin/reitit-parent "0.10.0"
:description "Snappy data-driven router for Clojure(Script)" :description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"
@ -6,7 +6,8 @@
:test-paths ["test/clj" "test/cljc"] :test-paths ["test/clj" "test/cljc"]
:deploy-repositories [["clojars" {:url "https://repo.clojars.org" :deploy-repositories [["clojars" {:url "https://repo.clojars.org"
:username :env/clojars_username :username :env/clojars_username
:password :env/clojars_password}]] :password :env/clojars_password
:sign-releases false}]]
:repositories [["clojars" {:url "https://repo.clojars.org" :repositories [["clojars" {:url "https://repo.clojars.org"
:username :env/clojars_username :username :env/clojars_username
:password :env/clojars_password}]] :password :env/clojars_password}]]
@ -15,48 +16,47 @@
:metadata {:doc/format :markdown}} :metadata {:doc/format :markdown}}
:scm {:name "git" :scm {:name "git"
:url "https://github.com/metosin/reitit"} :url "https://github.com/metosin/reitit"}
;; TODO: need to verify that the code actually worked with Java1.8, see #242
;; Ring 1.13.1 drops support for Java 1.8 so lets target 11 ;; Ring 1.13.1 drops support for Java 1.8 so lets target 11
:javac-options ["-Xlint:unchecked" "-target" "11" "-source" "11"] :javac-options ["-Xlint:unchecked" "-target" "11" "-source" "11"]
:managed-dependencies [[metosin/reitit "0.9.0"] :managed-dependencies [[metosin/reitit "0.10.0"]
[metosin/reitit-core "0.9.0"] [metosin/reitit-core "0.10.0"]
[metosin/reitit-dev "0.9.0"] [metosin/reitit-dev "0.10.0"]
[metosin/reitit-spec "0.9.0"] [metosin/reitit-spec "0.10.0"]
[metosin/reitit-malli "0.9.0"] [metosin/reitit-malli "0.10.0"]
[metosin/reitit-schema "0.9.0"] [metosin/reitit-schema "0.10.0"]
[metosin/reitit-ring "0.9.0"] [metosin/reitit-ring "0.10.0"]
[metosin/reitit-middleware "0.9.0"] [metosin/reitit-middleware "0.10.0"]
[metosin/reitit-http "0.9.0"] [metosin/reitit-http "0.10.0"]
[metosin/reitit-interceptors "0.9.0"] [metosin/reitit-interceptors "0.10.0"]
[metosin/reitit-swagger "0.9.0"] [metosin/reitit-swagger "0.10.0"]
[fi.metosin/reitit-openapi "0.9.0"] [fi.metosin/reitit-openapi "0.10.0"]
[metosin/reitit-swagger-ui "0.9.0"] [metosin/reitit-swagger-ui "0.10.0"]
[metosin/reitit-frontend "0.9.0"] [metosin/reitit-frontend "0.10.0"]
[metosin/reitit-sieppari "0.9.0"] [metosin/reitit-sieppari "0.10.0"]
[metosin/reitit-pedestal "0.9.0"] [metosin/reitit-pedestal "0.10.0"]
[metosin/ring-swagger-ui "5.20.0"] [metosin/ring-swagger-ui "5.20.0"]
[metosin/spec-tools "0.10.7"] [metosin/spec-tools "0.10.8"]
[metosin/schema-tools "0.13.1"] [metosin/schema-tools "0.13.1"]
[metosin/muuntaja "0.6.11"] [metosin/muuntaja "0.6.11"]
[metosin/jsonista "0.3.13"] [metosin/jsonista "0.3.13"]
[metosin/sieppari "0.0.0-alpha13"] [metosin/sieppari "0.0.0-alpha13"]
[metosin/malli "0.18.0"] [metosin/malli "0.20.0"]
;; https://clojureverse.org/t/depending-on-the-right-versions-of-jackson-libraries/5111 ;; https://clojureverse.org/t/depending-on-the-right-versions-of-jackson-libraries/5111
[com.fasterxml.jackson.core/jackson-core "2.18.2"] [com.fasterxml.jackson.core/jackson-core "2.20.1"]
[com.fasterxml.jackson.core/jackson-databind "2.18.2"] [com.fasterxml.jackson.core/jackson-databind "2.20.1"]
[meta-merge "1.0.0"] [meta-merge "1.0.0"]
[fipp "0.6.27" :exclusions [org.clojure/core.rrb-vector]] [fipp "0.6.29" :exclusions [org.clojure/core.rrb-vector]]
;; Deep-diff uses this version, override olders versiom from fipp. ;; Deep-diff uses this version, override olders versiom from fipp.
[org.clojure/core.rrb-vector "0.2.0"] [org.clojure/core.rrb-vector "0.2.1"]
[expound "0.9.0"] [expound "0.9.0"]
[lambdaisland/deep-diff "0.0-47"] [lambdaisland/deep-diff "0.0-47"]
[com.bhauman/spell-spec "0.1.2"] [com.bhauman/spell-spec "0.1.2"]
[mvxcvi/arrangement "2.1.0"] [mvxcvi/arrangement "2.1.0"]
[ring/ring-core "1.14.1"] [ring/ring-core "1.15.3"]
[io.pedestal/pedestal.service "0.6.4"]] [io.pedestal/pedestal.service "0.6.4" :upgrade false]]
:plugins [[jonase/eastwood "1.4.3"] :plugins [[jonase/eastwood "1.4.3"]
;[lein-virgil "0.1.7"] ;[lein-virgil "0.1.7"]
@ -67,7 +67,8 @@
[lein-codox "0.10.8"] [lein-codox "0.10.8"]
[metosin/bat-test "0.4.4"]] [metosin/bat-test "0.4.4"]]
:profiles {:dev {:jvm-opts ^:replace ["-server"] :profiles {:clj11 {:dependencies [[org.clojure/clojure "1.11.4"]]}
:dev {:jvm-opts ^:replace ["-server"]
;; all module sources for development ;; all module sources for development
:source-paths ["modules/reitit/src" :source-paths ["modules/reitit/src"
@ -89,44 +90,45 @@
:java-source-paths ["modules/reitit-core/java-src"] :java-source-paths ["modules/reitit-core/java-src"]
:dependencies [[org.clojure/clojure "1.11.4"] :dependencies [[org.clojure/clojure "1.12.4"]
[thheller/shadow-cljs "2.28.22"] [thheller/shadow-cljs "3.3.4"]
[org.clojure/clojurescript "1.11.132"] [org.clojure/clojurescript "1.12.134"]
;; modules dependencies ;; modules dependencies
[metosin/schema-tools "0.13.1"] [metosin/schema-tools "0.13.1"]
[metosin/spec-tools "0.10.7"] [metosin/spec-tools "0.10.8"]
[metosin/muuntaja "0.6.11"] [metosin/muuntaja "0.6.11"]
[metosin/sieppari "0.0.0-alpha13"] [metosin/sieppari "0.0.0-alpha13"]
[metosin/jsonista "0.3.13"] [metosin/jsonista "0.3.13"]
[metosin/malli "0.18.0"] [metosin/malli "0.20.0"]
[lambdaisland/deep-diff "0.0-47"] [lambdaisland/deep-diff "0.0-47"]
[meta-merge "1.0.0"] [meta-merge "1.0.0"]
[com.bhauman/spell-spec "0.1.2"] [com.bhauman/spell-spec "0.1.2"]
[expound "0.9.0"] [expound "0.9.0"]
[fipp "0.6.27"] [fipp "0.6.29"]
[orchestra "2021.01.01-1"] [orchestra "2021.01.01-1"]
[ring "1.14.1"] [ring "1.15.3"]
[ikitommi/immutant-web "3.0.0-alpha1"] [ikitommi/immutant-web "3.0.0-alpha1"]
[metosin/ring-http-response "0.9.5"] [metosin/ring-http-response "0.9.5"]
[metosin/ring-swagger-ui "5.20.0"] [metosin/ring-swagger-ui "5.20.0"]
[org.clojure/tools.analyzer "1.2.0"] [org.clojure/tools.analyzer "1.2.1"]
[criterium "0.4.6"] [criterium "0.4.6"]
[org.clojure/test.check "1.1.1"] [org.clojure/test.check "1.1.3"]
[org.clojure/tools.namespace "1.5.0"] [org.clojure/tools.namespace "1.5.1"]
[com.gfredericks/test.chuck "0.2.14"] [com.gfredericks/test.chuck "0.2.15"]
[nubank/matcher-combinators "3.9.1"] [nubank/matcher-combinators "3.9.2"]
[io.pedestal/pedestal.service "0.6.4"] ;; TODO: adapt to breaking changes in pedestal 0.7 and 0.8
[io.pedestal/pedestal.service "0.6.4" :upgrade false]
[org.clojure/core.async "1.7.701"] [org.clojure/core.async "1.8.741"]
[manifold "0.4.3"] [manifold "0.5.0"]
[funcool/promesa "11.0.678"] [funcool/promesa "11.0.678"]
[com.clojure-goes-fast/clj-async-profiler "1.6.1"] [com.clojure-goes-fast/clj-async-profiler "1.7.0"]
[ring-cors "0.1.13"] [ring-cors "0.1.13"]
[com.bhauman/rebel-readline "0.1.5"]]} [com.bhauman/rebel-readline "0.1.5"]]}
@ -135,18 +137,18 @@
"-Xmx4096m" "-Xmx4096m"
"-Dclojure.compiler.direct-linking=true"] "-Dclojure.compiler.direct-linking=true"]
:test-paths ["perf-test/clj"] :test-paths ["perf-test/clj"]
:dependencies [[compojure "1.7.1"] :dependencies [[compojure "1.7.2"]
[ring/ring-defaults "0.6.0"] [ring/ring-defaults "0.7.0"]
[ikitommi/immutant-web "3.0.0-alpha1"] [ikitommi/immutant-web "3.0.0-alpha1"]
[io.pedestal/pedestal.service "0.6.4"] [io.pedestal/pedestal.service "0.6.4" :upgrade false]
[io.pedestal/pedestal.jetty "0.6.4"] [io.pedestal/pedestal.jetty "0.6.4" :upgrade false]
[calfpath "0.8.1"] [calfpath "0.8.1"]
[org.clojure/core.async "1.7.701"] [org.clojure/core.async "1.8.741"]
[manifold "0.4.3"] [manifold "0.5.0"]
[funcool/promesa "11.0.678"] [funcool/promesa "11.0.678"]
[metosin/sieppari] [metosin/sieppari]
[yada "1.2.16"] [yada "1.2.16"]
[aleph "0.8.3"] [aleph "0.9.3"]
[ataraxy "0.4.3"] [ataraxy "0.4.3"]
[bidi "2.1.6"] [bidi "2.1.6"]
[janus "1.3.2"]]} [janus "1.3.2"]]}
@ -158,6 +160,7 @@
:aliases {"all" ["with-profile" "dev,default"] :aliases {"all" ["with-profile" "dev,default"]
"perf" ["with-profile" "default,dev,perf"] "perf" ["with-profile" "default,dev,perf"]
"test-clj" ["all" "do" ["bat-test"] ["check"]] "test-clj" ["all" "do" ["bat-test"] ["check"]]
"test-clj11" ["with-profile" "dev,default,clj11" "do" ["bat-test"] ["check"]]
;; NOTE: These are deprecated, kept around for ensuring shadow-cljs works ;; NOTE: These are deprecated, kept around for ensuring shadow-cljs works
;; the same way. ;; the same way.
"test-browser" ["doo" "chrome-headless" "test"] "test-browser" ["doo" "chrome-headless" "test"]

View file

@ -13,11 +13,14 @@ case $1 in
npx shadow-cljs release karma npx shadow-cljs release karma
npx karma start --single-run npx karma start --single-run
;; ;;
clj) clj11)
lein test-clj11
;;
clj12)
lein test-clj lein test-clj
;; ;;
*) *)
echo "Please select [clj|cljs]" echo "Please select [clj11|clj12|cljs]"
exit 1 exit 1
;; ;;
esac esac

View file

@ -308,10 +308,10 @@
["/ping" {:get {:interceptors [{:enter #(a/go %)}] ["/ping" {:get {:interceptors [{:enter #(a/go %)}]
:handler (fn [_] (a/go response))}}]) :handler (fn [_] (a/go response))}}])
(ring/create-default-handler) (ring/create-default-handler)
{:executor sieppari/executor})] {:executor sieppari/executor})
(let [respond (promise)] respond (promise)]
(app {:request-method :get, :uri "/ping"} respond ::irrelevant) (app {:request-method :get, :uri "/ping"} respond ::irrelevant)
(is (= response (deref respond 100 ::timeout))))))) (is (= response (deref respond 100 ::timeout))))))
(defrecord MyAsyncContext []) (defrecord MyAsyncContext [])

View file

@ -11,7 +11,7 @@
(:import (clojure.lang ExceptionInfo) (:import (clojure.lang ExceptionInfo)
(java.sql SQLException SQLWarning))) (java.sql SQLException SQLWarning)))
(derive ::kikka ::kukka) (derive ::kukka ::kikka)
(deftest exception-test (deftest exception-test
(letfn [(create (letfn [(create
@ -147,6 +147,55 @@
(is (= status 500)) (is (= status 500))
(is (= body "too many tries"))))))) (is (= body "too many tries")))))))
(derive ::table ::object)
(derive ::living ::object)
(derive ::plant ::living)
(derive ::animal ::living)
(derive ::dog ::animal)
(derive ::cat ::animal)
(derive ::garfield ::cat)
(deftest exception-hierarchy-test
(letfn [(create [f]
(ring/ring-handler
(ring/router
[["/defaults"
{:handler f}]]
{:data {:middleware [(exception/create-exception-middleware
(merge
exception/default-handlers
{::object (constantly (http-response/bad-request "object"))
::living (constantly (http-response/bad-request "living"))
::animal (constantly (http-response/bad-request "animal"))
::cat (constantly (http-response/bad-request "cat"))}))]}})))
(call [ex-typ]
(let [app (create (fn [_] (throw (ex-info "fail" {:type ex-typ}))))]
(app {:request-method :get, :uri "/defaults"})))]
(let [{:keys [status body]} (call ::object)]
(is (= status 400))
(is (= body "object")))
(let [{:keys [status body]} (call ::table)]
(is (= status 400))
(is (= body "object")))
(let [{:keys [status body]} (call ::living)]
(is (= status 400))
(is (= body "living")))
(let [{:keys [status body]} (call ::plant)]
(is (= status 400))
(is (= body "living")))
(let [{:keys [status body]} (call ::animal)]
(is (= status 400))
(is (= body "animal")))
(let [{:keys [status body]} (call ::dog)]
(is (= status 400))
(is (= body "animal")))
(let [{:keys [status body]} (call ::cat)]
(is (= status 400))
(is (= body "cat")))
(let [{:keys [status body]} (call ::garfield)]
(is (= status 400))
(is (= body "cat")))))
(deftest spec-coercion-exception-test (deftest spec-coercion-exception-test
(let [app (ring/ring-handler (let [app (ring/ring-handler
(ring/router (ring/router

View file

@ -2,7 +2,6 @@
(:require [clojure.spec.alpha :as cs] (:require [clojure.spec.alpha :as cs]
[clojure.string :as str] [clojure.string :as str]
[clojure.test :refer [deftest is testing]] [clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.experimental.lite :as l] [malli.experimental.lite :as l]
[reitit.coercion :as coercion] [reitit.coercion :as coercion]
[reitit.coercion.malli] [reitit.coercion.malli]
@ -10,8 +9,7 @@
[reitit.coercion.spec] [reitit.coercion.spec]
[reitit.core :as r] [reitit.core :as r]
[schema.core :as s] [schema.core :as s]
[spec-tools.data-spec :as ds] [spec-tools.data-spec :as ds])
[malli.transform :as mt])
#?(:clj #?(:clj
(:import (clojure.lang ExceptionInfo)))) (:import (clojure.lang ExceptionInfo))))
@ -110,7 +108,6 @@
(testing "spec-coercion (shallow)" (testing "spec-coercion (shallow)"
(testing "succeeds" (testing "succeeds"
(let [m (r/match-by-path r "/spec-shallow/1/abba")] (let [m (r/match-by-path r "/spec-shallow/1/abba")]
(def MATCH m)
(is (= {:path {:keyword :abba, :number 1}, :query nil} (is (= {:path {:keyword :abba, :number 1}, :query nil}
(coercion/coerce! m)))) (coercion/coerce! m))))
(let [m (r/match-by-path r "/spec-shallow/1/abba")] (let [m (r/match-by-path r "/spec-shallow/1/abba")]
@ -140,6 +137,36 @@
(let [m (r/match-by-path r "/none/kikka/abba")] (let [m (r/match-by-path r "/none/kikka/abba")]
(is (= nil (coercion/coerce! m)))))))) (is (= nil (coercion/coerce! m))))))))
(deftest malli-query-parameter-coercion-test
(let [router (fn [coercion]
(r/router ["/test"
{:coercion coercion
:parameters {:query [:map
[:a [:string {:default "a"}]]
[:x {:optional true} [:keyword {:default :a}]]]}}]
{:compile coercion/compile-request-coercers}))]
(testing "default values for :optional query keys do not get added"
(is (= {:query {:a "a"}}
(-> (r/match-by-path (router reitit.coercion.malli/coercion) "/test")
(assoc :query-params {})
(coercion/coerce!)))))
(testing "default values for :optional query keys get added when :malli.transform/add-optional-keys is set"
(is (= {:query {:a "a" :x :a}}
(-> (r/match-by-path (router (reitit.coercion.malli/create
(assoc reitit.coercion.malli/default-options
:default-values {:malli.transform/add-optional-keys true}))) "/test")
(assoc :query-params {})
(coercion/coerce!)))))
(testing "default values can be disabled"
(is (thrown-with-msg?
ExceptionInfo
#"Request coercion failed"
(-> (r/match-by-path (router (reitit.coercion.malli/create
(assoc reitit.coercion.malli/default-options
:default-values false))) "/test")
(assoc :query-params {})
(coercion/coerce!)))))))
(defn match-by-path-and-coerce! [router path] (defn match-by-path-and-coerce! [router path]
(if-let [match (r/match-by-path router path)] (if-let [match (r/match-by-path router path)]
(assoc match :parameters (coercion/coerce! match)))) (assoc match :parameters (coercion/coerce! match))))

View file

@ -1,10 +1,9 @@
(ns reitit.core-test (ns reitit.core-test
(:require [clojure.test :refer [are deftest is testing]] (:require [clojure.test :refer [are deftest is testing]]
[reitit.core :as r #?@(:cljs [:refer [Router]])] [reitit.core :as r]
[reitit.impl :as impl]) [reitit.impl :as impl])
#?(:clj #?(:clj
(:import (clojure.lang ExceptionInfo) (:import (clojure.lang ExceptionInfo))))
(reitit.core Router))))
(defn- var-handler [& _] (defn- var-handler [& _]
"var-handler") "var-handler")
@ -13,8 +12,7 @@
(testing "routers handling wildcard paths" (testing "routers handling wildcard paths"
(are [r name] (are [r name]
(testing "wild" (testing (str name)
(testing "simple" (testing "simple"
(let [router (r/router ["/api" ["/ipa" ["/:size" ::beer]]] {:router r})] (let [router (r/router ["/api" ["/ipa" ["/:size" ::beer]]] {:router r})]
(is (= name (r/router-name router))) (is (= name (r/router-name router)))
@ -35,6 +33,12 @@
:path "/api/ipa/large" :path "/api/ipa/large"
:path-params {:size "large"}}) :path-params {:size "large"}})
(r/match-by-name router ::beer {:size "large"}))) (r/match-by-name router ::beer {:size "large"})))
(is (= (r/map->Match
{:template "/api/ipa/:size"
:data {:name ::beer}
:path "/api/ipa/:large"
:path-params {:size ":large"}})
(r/match-by-name router ::beer {:size ":large"} {:url-encode? false})))
(is (= (r/map->Match (is (= (r/map->Match
{:template "/api/ipa/:size" {:template "/api/ipa/:size"
:data {:name ::beer} :data {:name ::beer}
@ -52,10 +56,17 @@
:path-params nil}) :path-params nil})
(r/match-by-name router ::beer))) (r/match-by-name router ::beer)))
(is (r/partial-match? (r/match-by-name router ::beer))) (is (r/partial-match? (r/match-by-name router ::beer)))
(is (r/partial-match? (r/match-by-name router ::beer {:size nil}))
"nil counts as missing")
(is (thrown-with-msg? (is (thrown-with-msg?
ExceptionInfo ExceptionInfo
#"^missing path-params for route /api/ipa/:size -> \#\{:size\}$" #"^missing path-params for route /api/ipa/:size -> \#\{:size\}$"
(r/match-by-name! router ::beer)))))) (r/match-by-name! router ::beer)))
(is (thrown-with-msg?
ExceptionInfo
#"^missing path-params for route /api/ipa/:size -> \#\{:size\}$"
(r/match-by-name! router ::beer {:size nil}))
"nil counts as missing"))))
(testing "decode %-encoded path params" (testing "decode %-encoded path params"
(let [router (r/router [["/one-param-path/:param1" ::one] (let [router (r/router [["/one-param-path/:param1" ::one]
@ -255,10 +266,12 @@
(is (= #'var-handler result)))))) (is (= #'var-handler result))))))
(testing "custom router" (testing "custom router"
(let [router (r/router ["/ping"] {:router (fn [_ _] (let [router (r/router
(reify Router ["/ping"]
(r/router-name [_] {:router (fn [_ _]
::custom)))})] #_{:clj-kondo/ignore [:missing-protocol-method]}
(reify r/Router
(router-name [_] ::custom)))})]
(is (= ::custom (r/router-name router))))) (is (= ::custom (r/router-name router)))))
(testing "bide sample" (testing "bide sample"

View file

@ -1,33 +0,0 @@
(ns reitit.dependency-test
(:require [clojure.test :refer [are deftest is testing]]
[reitit.dependency :as rc])
#?(:clj (:import [clojure.lang ExceptionInfo])))
(deftest post-order-test
(let [base-middlewares [{:name ::bar, :provides #{:bar}, :requires #{:foo}, :wrap identity}
{:name ::baz, :provides #{:baz}, :requires #{:bar :foo}, :wrap identity}
{:name ::foo, :provides #{:foo}, :requires #{}, :wrap identity}]]
(testing "happy cases"
(testing "default ordering works"
(is (= (rc/post-order base-middlewares)
(into (vec (drop 2 base-middlewares)) (take 2 base-middlewares)))))
(testing "custom provides and requires work"
(is (= (rc/post-order (comp hash-set :name)
(fn [node] (into #{} (map (fn [k] (keyword "reitit.dependency-test" (name k))))
(:requires node)))
base-middlewares)
(into (vec (drop 2 base-middlewares)) (take 2 base-middlewares))))))
(testing "errors"
(testing "missing dependency detection"
(is (thrown-with-msg? ExceptionInfo #"missing"
(rc/post-order (drop 1 base-middlewares)))))
(testing "ambiguous dependency detection"
(is (thrown-with-msg? ExceptionInfo #"multiple providers"
(rc/post-order (update-in base-middlewares [0 :provides] conj :foo)))))
(testing "circular dependency detection"
(is (thrown-with-msg? ExceptionInfo #"circular"
(rc/post-order (assoc-in base-middlewares [2 :requires] #{:baz}))))))))

View file

@ -41,7 +41,33 @@
:u #uuid "c2541900-17a7-4353-9024-db8ac258ba4e" :u #uuid "c2541900-17a7-4353-9024-db8ac258ba4e"
:k :kikka :k :kikka
:qk ::kikka :qk ::kikka
:nil nil})))) :nil nil})))
(is (= {:n "1"
:n1 "-1"
:n2 "1"
:n3 "1"
:n4 "1"
:n5 "1"
:d "2.2"
:b "true"
:s "kikka"
:u "c2541900-17a7-4353-9024-db8ac258ba4e"
:k "kikka"
:qk "reitit.impl-test/kikka"
:nil nil}
(impl/path-params {:n 1
:n1 -1
:n2 (long 1)
:n3 (int 1)
:n4 (short 1)
:n5 (byte 1)
:d 2.2
:b true
:s "kikka"
:u #uuid "c2541900-17a7-4353-9024-db8ac258ba4e"
:k :kikka
:qk ::kikka
:nil nil} {:url-encode? false}))))
(deftest query-params-test (deftest query-params-test
(are [x y] (are [x y]

View file

@ -4,7 +4,6 @@
[jsonista.core :as j] [jsonista.core :as j]
[malli.core :as mc] [malli.core :as mc]
[matcher-combinators.test :refer [match?]] [matcher-combinators.test :refer [match?]]
[matcher-combinators.matchers :as matchers]
[muuntaja.core :as m] [muuntaja.core :as m]
[reitit.coercion.malli :as malli] [reitit.coercion.malli :as malli]
[reitit.coercion.schema :as schema] [reitit.coercion.schema :as schema]
@ -18,6 +17,7 @@
[reitit.swagger-ui :as swagger-ui] [reitit.swagger-ui :as swagger-ui]
[schema.core :as s] [schema.core :as s]
[schema-tools.core] [schema-tools.core]
[clojure.spec.alpha :as sp]
[spec-tools.core :as st] [spec-tools.core :as st]
[spec-tools.data-spec :as ds])) [spec-tools.data-spec :as ds]))
@ -901,13 +901,11 @@
:request {:description "body description" :request {:description "body description"
:content {"application/json" {:schema {:x int?, :y int?} :content {"application/json" {:schema {:x int?, :y int?}
:examples {"1+1" {:value {:x 1, :y 1}} :examples {"1+1" {:value {:x 1, :y 1}}
"1+2" {:value {:x 1, :y 2}}} "1+2" {:value {:x 1, :y 2}}}}}}
:openapi {:example {:x 2, :y 2}}}}}
:responses {200 {:description "success" :responses {200 {:description "success"
:content {"application/json" {:schema {:total int?} :content {"application/json" {:schema {:total int?}
:examples {"2" {:value {:total 2}} :examples {"2" {:value {:total 2}}
"3" {:value {:total 3}}} "3" {:value {:total 3}}}}}}}
:openapi {:example {:total 4}}}}}}
:handler (fn [request] :handler (fn [request]
(let [{:keys [x y]} (-> request :parameters :body)] (let [{:keys [x y]} (-> request :parameters :body)]
{:status 200, :body {:total (+ x y)}}))}}]]] {:status 200, :body {:total (+ x y)}}))}}]]]
@ -925,16 +923,14 @@
:required [:x :y], :required [:x :y],
:additionalProperties false}, :additionalProperties false},
:examples {"1+1" {:value {:x 1, :y 1}} :examples {"1+1" {:value {:x 1, :y 1}}
"1+2" {:value {:x 1, :y 2}}}, "1+2" {:value {:x 1, :y 2}}}}}},
:example {:x 2, :y 2}}}},
:responses {200 {:description "success", :responses {200 {:description "success",
:content {"application/json" {:schema {:type "object", :content {"application/json" {:schema {:type "object",
:properties {:total {:type "integer"}}, :properties {:total {:type "integer"}},
:required [:total], :required [:total],
:additionalProperties false}, :additionalProperties false},
:examples {"2" {:value {:total 2}}, :examples {"2" {:value {:total 2}},
"3" {:value {:total 3}}}, "3" {:value {:total 3}}}}}}},
:example {:total 4}}}}},
:summary "plus with body"}}} :summary "plus with body"}}}
(:paths spec))) (:paths spec)))
(is (nil? (validate spec)))) (is (nil? (validate spec))))
@ -1031,3 +1027,36 @@
"reitit.openapi-test.Y" {:type "integer"}}}} "reitit.openapi-test.Y" {:type "integer"}}}}
spec)) spec))
(is (nil? (validate spec)))))) (is (nil? (validate spec))))))
(sp/def ::address string?)
(sp/def ::zip int?)
(sp/def ::city string?)
(sp/def ::street string?)
(sp/def ::or-and-schema (sp/keys :req-un [(or (and ::address ::zip) (and ::city ::street))]))
(deftest openapi-spec-tests
(testing "s/keys + or maps to :anyOf"
(let [app (ring/ring-handler
(ring/router
[["/openapi.json"
{:get {:no-doc true
:openapi {:info {:title "" :version "0.0.1"}}
:handler (openapi/create-openapi-handler)}}]
["/spec" {:coercion spec/coercion
:post {:summary "or-and-schema"
:request {:content {"application/json" {:schema ::or-and-schema}}}
:handler identity}}]]
{:validate reitit.ring.spec/validate
:data {:middleware [openapi/openapi-feature]}}))
spec (:body (app {:request-method :get :uri "/openapi.json"}))]
(is (nil? (validate spec)))
(is (= {:title "reitit.openapi-test/or-and-schema"
:type "object"
:properties {"address" {:type "string"}
"zip" {:type "integer" :format "int64"}
"city" {:type "string"}
"street" {:type "string"}}
:anyOf [{:required ["address" "zip"]}
{:required ["city" "street"]}]}
(get-in spec [:paths "/spec" :post :requestBody :content "application/json" :schema]))))))

View file

@ -1,19 +1,21 @@
(ns reitit.ring-coercion-test (ns reitit.ring-coercion-test
(:require [clojure.test :refer [deftest is testing]] (:require [clojure.test :refer [deftest is testing]]
[malli.experimental.lite :as l] [malli.experimental.lite :as l]
#?@(:clj [[muuntaja.middleware] #?@(:clj [[muuntaja.core]
[jsonista.core :as j]]) [muuntaja.middleware]
[jsonista.core :as j]
[reitit.coercion.schema :as schema]
[reitit.ring.middleware.muuntaja]
[schema.core :as s]])
[malli.core :as m] [malli.core :as m]
[malli.util :as mu] [malli.util :as mu]
[meta-merge.core :refer [meta-merge]] [meta-merge.core :refer [meta-merge]]
[reitit.coercion.malli :as malli] [reitit.coercion.malli :as malli]
[reitit.coercion.schema :as schema]
[reitit.coercion.spec :as spec] [reitit.coercion.spec :as spec]
[reitit.core :as r] [reitit.core :as r]
[reitit.ring :as ring] [reitit.ring :as ring]
[reitit.ring.spec] [reitit.ring.spec]
[reitit.ring.coercion :as rrc] [reitit.ring.coercion :as rrc]
[schema.core :as s]
[clojure.spec.alpha] [clojure.spec.alpha]
[spec-tools.data-spec :as ds]) [spec-tools.data-spec :as ds])
#?(:clj #?(:clj
@ -581,103 +583,168 @@
:request-method :get}))] :request-method :get}))]
(is (= {:status 200, :body {:total "FOO: this, BAR: that"}} (call m/schema custom-meta-merge-checking-schema))) (is (= {:status 200, :body {:total "FOO: this, BAR: that"}} (call m/schema custom-meta-merge-checking-schema)))
(is (= {:status 200, :body {:total "FOO: this, BAR: that"}} (call identity custom-meta-merge-checking-parameters))))))) (is (= {:status 200, :body {:total "FOO: this, BAR: that"}} (call identity custom-meta-merge-checking-parameters)))))
(testing "malli options"
(let [->app (fn [options]
(ring/ring-handler
(ring/router
["/api" {:get {:parameters {:body [:map
[:i :int]
[:x :string]]}
:handler (fn [{{:keys [body]} :parameters}]
{:status 200 :body body})}}]
{:data {:middleware [rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]
:coercion (malli/create options)}})))
request {:uri "/api"
:request-method :get
:muuntaja/request {:format "application/json"}}]
(testing "humanize options"
(is (= {:i ["should be an integer"] :x ["missing required key"]}
(-> ((->app nil) (assoc request :body-params {:i "x"}))
:body
:humanized)))
(is (= {:i ["SHOULD INT"] :x ["MISSING"]}
(-> ((->app {:options {:errors {:int {:error/message {:en "SHOULD INT"}}
:malli.core/missing-key {:error/message {:en "MISSING"}}}}})
(assoc request :body-params {:i "x"}))
:body
:humanized))))
(testing "overriding registry"
(is (= {:body {:i "x" :x "x"} :status 200}
(-> ((->app {:options {:registry (merge (m/default-schemas)
{:int :string})}})
(assoc request :body-params {:i "x" :x "x"}))))))))))
#?(:clj #?(:clj
(deftest per-content-type-test (deftest per-content-type-test
(doseq [[coercion json-request edn-request default-request json-response edn-response default-response] (let [normalize-json (fn [resp]
[[malli/coercion (update resp :body #(-> % j/write-value-as-string (j/read-value j/keyword-keys-object-mapper))))]
[:map [:request [:enum :json]] [:response any?]] (doseq [[coercion json-request edn-request default-request json-response edn-response default-response]
[:map [:request [:enum :edn]] [:response any?]] [[malli/coercion
[:map [:request [:enum :default]] [:response any?]] [:map [:request [:enum :json]] [:response any?]]
[:map [:request any?] [:response [:enum :json]]] [:map [:request [:enum :edn]] [:response any?]]
[:map [:request any?] [:response [:enum :edn]]] [:map [:request [:enum :default]] [:response any?]]
[:map [:request any?] [:response [:enum :default]]]] [:map [:request any?] [:response [:enum :json]]]
[schema/coercion [:map [:request any?] [:response [:enum :edn]]]
{:request (s/eq :json) :response s/Any} [:map [:request any?] [:response [:enum :default]]]]
{:request (s/eq :edn) :response s/Any} [schema/coercion
{:request (s/eq :default) :response s/Any} {:request (s/eq :json) :response s/Any}
{:request s/Any :response (s/eq :json)} {:request (s/eq :edn) :response s/Any}
{:request s/Any :response (s/eq :edn)} {:request (s/eq :default) :response s/Any}
{:request s/Any :response (s/eq :default)}] {:request s/Any :response (s/eq :json)}
[spec/coercion {:request s/Any :response (s/eq :edn)}
{:request (clojure.spec.alpha/spec #{:json}) :response any?} {:request s/Any :response (s/eq :default)}]
{:request (clojure.spec.alpha/spec #{:edn}) :response any?} [spec/coercion
{:request (clojure.spec.alpha/spec #{:default}) :response any?} {:request (clojure.spec.alpha/spec #{:json}) :response any?}
{:request any? :response (clojure.spec.alpha/spec #{:json})} {:request (clojure.spec.alpha/spec #{:edn}) :response any?}
{:request any? :response (clojure.spec.alpha/spec #{:end})} {:request (clojure.spec.alpha/spec #{:default}) :response any?}
{:request any? :response (clojure.spec.alpha/spec #{:default})}]]] {:request any? :response (clojure.spec.alpha/spec #{:json})}
(testing (str coercion) {:request any? :response (clojure.spec.alpha/spec #{:end})}
(doseq [{:keys [name app]} {:request any? :response (clojure.spec.alpha/spec #{:default})}]]]
[{:name "using top-level :body" (testing (str coercion)
:app (ring/ring-handler (doseq [{:keys [name app]}
(ring/router [{:name "using top-level :body"
["/foo" {:post {:request {:content {"application/json" {:schema json-request} :app (ring/ring-handler
"application/edn" {:schema edn-request}} (ring/router
:body default-request} ["/foo" {:post {:request {:content {"application/json" {:schema json-request}
:responses {200 {:content {"application/json" {:schema json-response} "application/edn" {:schema edn-request}}
"application/edn" {:schema edn-response}} :body default-request}
:body default-response}} :responses {200 {:content {"application/json" {:schema json-response}
:handler (fn [req] "application/edn" {:schema edn-response}}
{:status 200 :body default-response}}
:body (-> req :parameters :request)})}}] :handler (fn [req]
{:validate reitit.ring.spec/validate {:status 200
:data {:middleware [rrc/coerce-request-middleware :body (-> req :parameters :request)})}}]
rrc/coerce-response-middleware] {:validate reitit.ring.spec/validate
:coercion coercion}}))} :data {:middleware [rrc/coerce-request-middleware
{:name "using :default content" rrc/coerce-response-middleware]
:app (ring/ring-handler :coercion coercion}}))}
(ring/router {:name "using :default content"
["/foo" {:post {:request {:content {"application/json" {:schema json-request} :app (ring/ring-handler
"application/edn" {:schema edn-request} (ring/router
:default {:schema default-request}} ["/foo" {:post {:request {:content {"application/json" {:schema json-request}
:body json-request} ;; not applied as :default exists "application/edn" {:schema edn-request}
:responses {200 {:content {"application/json" {:schema json-response} :default {:schema default-request}}
"application/edn" {:schema edn-response} :body json-request} ;; not applied as :default exists
:default {:schema default-response}} :responses {200 {:content {"application/json" {:schema json-response}
:body json-response}} ;; not applied as :default exists "application/edn" {:schema edn-response}
:handler (fn [req] :default {:schema default-response}}
{:status 200 :body json-response}} ;; not applied as :default exists
:body (-> req :parameters :request)})}}] :handler (fn [req]
{:validate reitit.ring.spec/validate {:status 200
:data {:middleware [rrc/coerce-request-middleware :body (-> req :parameters :request)})}}]
rrc/coerce-response-middleware] {:validate reitit.ring.spec/validate
:coercion coercion}}))}]] :data {:middleware [rrc/coerce-request-middleware
(testing name rrc/coerce-response-middleware]
(let [call (fn [request] :coercion coercion}}))}]]
(testing name
(let [call (fn [request]
(try
(app request)
(catch ExceptionInfo e
(select-keys (ex-data e) [:type :in]))))
request (fn [request-format response-format body]
{:request-method :post
:uri "/foo"
:muuntaja/request {:format request-format}
:muuntaja/response {:format response-format}
:body-params body})]
(testing "succesful call"
(is (= {:status 200 :body {:request "json", :response "json"}}
(normalize-json (call (request "application/json" "application/json" {:request :json :response :json})))))
(is (= {:status 200 :body {:request "edn", :response "json"}}
(normalize-json (call (request "application/edn" "application/json" {:request :edn :response :json})))))
(is (= {:status 200 :body {:request :default, :response :default}}
(call (request "application/transit" "application/transit" {:request :default :response :default})))))
(testing "request validation fails"
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
(call (request "application/edn" "application/json" {:request :json :response :json}))))
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
(call (request "application/json" "application/json" {:request :edn :response :json}))))
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
(call (request "application/transit" "application/json" {:request :edn :response :json})))))
(testing "response validation fails"
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
(call (request "application/json" "application/json" {:request :json :response :edn}))))
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
(call (request "application/json" "application/edn" {:request :json :response :json}))))
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
(call (request "application/json" "application/transit" {:request :json :response :json}))))))))
(testing "explicit response content type"
(let [response (atom nil)
app (ring/ring-handler
(ring/router
["/foo" {:post {:responses {200 {:content {"application/json" {:schema json-response}
"application/edn" {:schema edn-response}
:default {:schema default-response}}}}
:handler (fn [req]
@response)}}]
{:validate reitit.ring.spec/validate
:data {:middleware [rrc/coerce-request-middleware
rrc/coerce-response-middleware]
:coercion coercion}}))
call (fn [request]
(try (try
(app request) (app request)
(catch ExceptionInfo e (catch ExceptionInfo e
#_(ex-data e)
(select-keys (ex-data e) [:type :in])))) (select-keys (ex-data e) [:type :in]))))
request (fn [request-format response-format body] request (fn [request-format body resp]
(reset! response resp)
{:request-method :post {:request-method :post
:uri "/foo" :uri "/foo"
:muuntaja/request {:format request-format} :muuntaja/request {:format request-format}
:muuntaja/response {:format response-format} :body-params body})]
:body-params body}) (testing "via :muuntaja/content-type"
normalize-json (fn[body] (is (= {:status 200 :body {:request "json" :response "json"} :muuntaja/content-type "application/json"}
(-> body j/write-value-as-string (j/read-value j/keyword-keys-object-mapper)))] (normalize-json (call (request "application/json" {:request :json :response :json} {:status 200 :body {:request :json :response :json} :muuntaja/content-type "application/json"}))))
(testing "succesful call" "valid reponse")
(is (= {:status 200 :body {:request "json", :response "json"}} (is (= {:in [:response :body] :type :reitit.coercion/response-coercion}
(normalize-json (call (request "application/json" "application/json" {:request :json :response :json}))))) (call (request "application/json" {:request :json :response :json} {:status 200 :body {:request :json :response :invalid} :muuntaja/content-type "application/json"})))
(is (= {:status 200 :body {:request "edn", :response "json"}} "invalid reponse")))))))))
(normalize-json (call (request "application/edn" "application/json" {:request :edn :response :json})))))
(is (= {:status 200 :body {:request :default, :response :default}}
(call (request "application/transit" "application/transit" {:request :default :response :default})))))
(testing "request validation fails"
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
(call (request "application/edn" "application/json" {:request :json :response :json}))))
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
(call (request "application/json" "application/json" {:request :edn :response :json}))))
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
(call (request "application/transit" "application/json" {:request :edn :response :json})))))
(testing "response validation fails"
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
(call (request "application/json" "application/json" {:request :json :response :edn}))))
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
(call (request "application/json" "application/edn" {:request :json :response :json}))))
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
(call (request "application/json" "application/transit" {:request :json :response :json}))))))))))))
#?(:clj #?(:clj
@ -695,15 +762,22 @@
(testing (str coercion) (testing (str coercion)
(let [app (ring/ring-handler (let [app (ring/ring-handler
(ring/router (ring/router
["/foo" {:post {:responses {200 {:content {:default {:schema schema-200}}} [["/foo" {:post {:responses {200 {:content {:default {:schema schema-200}}}
201 {:content {"application/edn" {:schema schema-200}}} 201 {:content {"application/edn" {:schema schema-200}}}
202 {:description "status code and content-type explicitly mentioned, but no :schema" 202 {:description "status code and content-type explicitly mentioned, but no :schema"
:content {"application/edn" {} :content {"application/edn" {}
"application/json" {}}} "application/json" {}}}
:default {:content {"application/json" {:schema schema-default}}}} :default {:content {"application/json" {:schema schema-default}}}}
:handler (fn [req] :handler (fn [req]
{:status (-> req :body-params :status) {:status (-> req :body-params :status)
:body (-> req :body-params :response)})}}] :body (-> req :body-params :response)})}}]
["/bar" {:post {:responses {200 {:content {:default {:schema schema-200}}}}
:handler (fn [req]
{:status (-> req :body-params :status)
:body (-> req :body-params :response)})}}]
["/quux" {:post {:handler (fn [req]
{:status (-> req :body-params :status)
:body (-> req :body-params :response)})}}]]
{:validate reitit.ring.spec/validate {:validate reitit.ring.spec/validate
:data {:middleware [rrc/coerce-request-middleware :data {:middleware [rrc/coerce-request-middleware
rrc/coerce-response-middleware] rrc/coerce-response-middleware]
@ -713,40 +787,52 @@
(app request) (app request)
(catch ExceptionInfo e (catch ExceptionInfo e
(select-keys (ex-data e) [:type :in])))) (select-keys (ex-data e) [:type :in]))))
request (fn [body] request (fn [uri body]
{:request-method :post {:request-method :post
:uri "/foo" :uri uri
:muuntaja/request {:format "application/json"} :muuntaja/request {:format "application/json"}
:muuntaja/response {:format (:format body "application/json")} :muuntaja/response {:format (:format body "application/json")}
:body-params body})] :body-params body})]
(testing "explicit response schema" (testing "explicit response schema"
(is (= {:status 200 :body {:a 1}} (is (= {:status 200 :body {:a 1}}
(call (request {:status 200 :response {:a 1}}))) (call (request "/foo" {:status 200 :response {:a 1}})))
"valid response") "valid response")
(is (= {:type :reitit.coercion/response-coercion, :in [:response :body]} (is (= {:type :reitit.coercion/response-coercion, :in [:response :body]}
(call (request {:status 200 :response {:b 1}}))) (call (request "/foo" {:status 200 :response {:b 1}})))
"invalid response") "invalid response")
(is (= {:type :reitit.coercion/response-coercion, :in [:response :body]} (is (= {:type :reitit.coercion/response-coercion, :in [:response :body]}
(call (request {:status 200 :response {:b 1} :format "application/edn"}))) (call (request "/foo" {:status 200 :response {:b 1} :format "application/edn"})))
"invalid response, different content-type")) "invalid response, different content-type"))
(testing "explicit response schema, but for the wrong content-type" (testing "explicit response schema, but for the wrong content-type"
(is (= {:status 201 :body "anything goes!"} (is (= {:status 201 :body "anything goes!"}
(call (request {:status 201 :response "anything goes!"}))) (call (request "/foo" {:status 201 :response "anything goes!"})))
"no coercion applied")) "no coercion applied"))
(testing "response config without :schema" (testing "response config without :schema"
(is (= {:status 202 :body "anything goes!"} (is (= {:status 202 :body "anything goes!"}
(call (request {:status 202 :response "anything goes!"}))) (call (request "/foo" {:status 202 :response "anything goes!"})))
"no coercion applied")) "no coercion applied"))
(testing "default response schema" (testing "default response schema"
(is (= {:status 300 :body {:b 2}} (is (= {:status 300 :body {:b 2}}
(call (request {:status 300 :response {:b 2}}))) (call (request "/foo" {:status 300 :response {:b 2}})))
"valid response") "valid response")
(is (= {:type :reitit.coercion/response-coercion, :in [:response :body]} (is (= {:type :reitit.coercion/response-coercion, :in [:response :body]}
(call (request {:status 300 :response {:a 2}}))) (call (request "/foo" {:status 300 :response {:a 2}})))
"invalid response") "invalid response")
(is (= {:status 300 :body "anything goes!"} (is (= {:status 300 :body "anything goes!"}
(call (request {:status 300 :response "anything goes!" :format "application/edn"}))) (call (request "/foo" {:status 300 :response "anything goes!" :format "application/edn"})))
"no coercion applied due to content-type"))))))) "no coercion applied due to content-type"))
(testing "no default"
(is (= {:status 200 :body {:a 1}}
(call (request "/bar" {:status 200 :response {:a 1}})))
"valid response")
(testing "unlisted response code"
(is (= {:status 202 :body "anything goes!"}
(call (request "/bar" {:status 202 :response "anything goes!"})))
"no coercion applied")))
(testing "no response coercion"
(is (= {:status 200 :body "anything goes!"}
(call (request "/quux" {:status 200 :response "anything goes!"})))
"no coercion applied")))))))
#?(:clj #?(:clj
(deftest muuntaja-test (deftest muuntaja-test
@ -782,3 +868,100 @@
(app) :body slurp (read-string))] (app) :body slurp (read-string))]
(is (= data-edn (e2e (assoc data-edn :EXTRA "VALUE")))) (is (= data-edn (e2e (assoc data-edn :EXTRA "VALUE"))))
(is (thrown? ExceptionInfo (e2e data-json)))))))) (is (thrown? ExceptionInfo (e2e data-json))))))))
#?(:clj
(deftest muuntaja-per-content-type-coercion-test
;; Test integration between per-content-type coercion and muuntaja.
;; Malli-only for now.
(let [response (atom nil)
app (ring/ring-handler
(ring/router
["/foo" {:post {:request {:content {"application/json" {:schema [:map [:request [:enum :json]]]}
"application/edn" {:schema [:map [:request [:enum :edn]]]}
:default {:schema [:map [:request [:enum :default]]]}}}
:responses {200 {:content {"application/json" {:schema [:map [:response [:enum :json]]]}
"application/edn" {:schema [:map [:response [:enum :edn]]]}
:default {}}}}
:handler (fn [req] @response)}}]
{:data {:middleware [reitit.ring.middleware.muuntaja/format-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]
:muuntaja muuntaja.core/instance
:coercion malli/coercion}}))
maybe-slurp #(if (instance? java.io.InputStream %)
(slurp %)
%)
call (fn [request resp]
(reset! response resp)
(try
(-> (merge {:request-method :post :uri "/foo"} request)
(update :body #(ByteArrayInputStream. (.getBytes % "UTF-8")))
(app))
(catch ExceptionInfo e
#_(ex-data e)
(select-keys (ex-data e) [:in :type]))))
read-json #(j/read-value % (j/object-mapper {:decode-key-fn true}))
json-response? (fn [resp]
(and (.startsWith (get-in resp [:headers "Content-Type"]) "application/json") ;; ignore the ;charset=utf-8 part
(= {:response "json"} (read-json (maybe-slurp (:body resp))))))
edn-response? (fn [resp]
(and (.startsWith (get-in resp [:headers "Content-Type"]) "application/edn") ;; ignore the ;charset=utf-8 part
(= {:response :edn} (read-string (maybe-slurp (:body resp))))))
custom-response? (fn [resp]
(and (= (get-in resp [:headers "Content-Type"]) "application/custom")
(= "custom data" (maybe-slurp (:body resp)))))]
(testing "response content-type defaults to json"
(is (json-response?
(call {:headers {"content-type" "application/json"}
:body (j/write-value-as-string {:request :json})}
{:status 200
:body {:response :json}})))
(is (json-response?
(call {:headers {"content-type" "application/edn"}
:body (pr-str {:request :edn})}
{:status 200
:body {:response :json}})))
(is (= {:in [:response :body] :type :reitit.coercion/response-coercion}
(call {:headers {"content-type" "application/json"}
:body (j/write-value-as-string {:request :json})}
{:status 200
:body {:response :invalid}}))
"invalid response"))
(testing "response content-type negotiated via accept header"
(is (json-response?
(call {:headers {"content-type" "application/json" "accept" "application/json"}
:body (j/write-value-as-string {:request :json})}
{:status 200
:body {:response :json}})))
(is (edn-response?
(call {:headers {"content-type" "application/json" "accept" "application/edn"}
:body (j/write-value-as-string {:request :json})}
{:status 200
:body {:response :edn}})))
(is (= {:in [:response :body] :type :reitit.coercion/response-coercion}
(call {:headers {"content-type" "application/json" "accept" "application/edn"}
:body (j/write-value-as-string {:request :json})}
{:status 200
:body {:response :invalid}}))
"invalid response"))
(testing "response content-type set via :muuntaja/content-type"
(is (edn-response?
(call {:headers {"content-type" "application/json" "accept" "application/json"}
:body (j/write-value-as-string {:request :json})}
{:status 200
:muuntaja/content-type "application/edn"
:body {:response :edn}})))
(is (= {:in [:response :body] :type :reitit.coercion/response-coercion}
(call {:headers {"content-type" "application/json" "accept" "application/json"}
:body (j/write-value-as-string {:request :json})}
{:status 200
:muuntaja/content-type "application/edn"
:body {:response :invalid}}))
"invalid response"))
(testing "response content-type set via Content-Type header. muuntaja disabled for response."
(is (custom-response?
(call {:headers {"content-type" "application/json" "accept" "application/json"}
:body (j/write-value-as-string {:request :json})}
{:status 200
:headers {"Content-Type" "application/custom"}
:body "custom data"})))))))

View file

@ -12,6 +12,9 @@
(s/def ::role #{:admin :user}) (s/def ::role #{:admin :user})
(s/def ::roles (s/and (s/coll-of ::role :into #{}) set?)) (s/def ::roles (s/and (s/coll-of ::role :into #{}) set?))
(defmulti my-multi (constantly :default))
(defmethod my-multi :default [x] x)
(deftest route-data-validation-test (deftest route-data-validation-test
(testing "validation is turned off by default" (testing "validation is turned off by default"
(is (r/router? (is (r/router?
@ -85,6 +88,12 @@
(ring/router (ring/router
["/api" {:handler identity ["/api" {:handler identity
:middleware '()}] :middleware '()}]
{:validate rrs/validate}))))
(testing "handler can be a multimethod"
(is (r/router?
(ring/router
["/api" {:get {:handler my-multi}}]
{:validate rrs/validate}))))) {:validate rrs/validate})))))
(deftest coercion-spec-test (deftest coercion-spec-test

View file

@ -4,7 +4,8 @@
[reitit.core :as r] [reitit.core :as r]
[reitit.middleware :as middleware] [reitit.middleware :as middleware]
[reitit.ring :as ring] [reitit.ring :as ring]
[reitit.trie :as trie]) #?(:clj [reitit.trie :as trie]
:cljs [reitit.trie :as-alias trie]))
#?(:clj #?(:clj
(:import (clojure.lang ExceptionInfo)))) (:import (clojure.lang ExceptionInfo))))
@ -114,6 +115,25 @@
(is (= {:status 200, :body [:top :api :ok]} (is (= {:status 200, :body [:top :api :ok]}
(app {:uri "/api/get" :request-method :get})))))) (app {:uri "/api/get" :request-method :get}))))))
(testing "middleware from registry"
(let [router (ring/router
["/api" {:middleware [:mw-foo]}
["/get" {:middleware [[:mw :inner]]
:get handler}]]
{::middleware/registry {:mw mw
:mw-foo #(mw % :foo)}})
app (ring/ring-handler router nil {:middleware [[:mw :top]]})]
(testing "router can be extracted"
(is (= router (ring/get-router app))))
(testing "not found"
(is (= nil (app {:uri "/favicon.ico"}))))
(testing "on match"
(is (= {:status 200, :body [:top :foo :inner :ok]}
(app {:uri "/api/get" :request-method :get}))))))
(testing "named routes" (testing "named routes"
(let [router (ring/router (let [router (ring/router
[["/api" [["/api"
@ -396,7 +416,31 @@
:get "/slash-less//" "/slash-less?kikka=kukka" :get "/slash-less//" "/slash-less?kikka=kukka"
:post "/with-slash" "/with-slash/?kikka=kukka" :post "/with-slash" "/with-slash/?kikka=kukka"
:post "/slash-less/" "/slash-less?kikka=kukka" :post "/slash-less/" "/slash-less?kikka=kukka"
:post "/slash-less//" "/slash-less?kikka=kukka")))))) :post "/slash-less//" "/slash-less?kikka=kukka"))))
;; See issue #337
(testing "Avoid external redirects"
(let [app (ring/ring-handler
(ring/router [["*" {:get (constantly nil)}]])
(ring/redirect-trailing-slash-handler))
resp (fn [uri & [query-string]]
(let [r (app {:request-method :get :uri uri :query-string query-string})]
{:status (:status r)
:Location (get-in r [:headers "Location"])}))]
(testing "without query params"
(is (= {:status 301 :Location "/malicious.com/foo/"} (resp "//malicious.com/foo")))
(is (= {:status 301 :Location "/malicious.com/foo"} (resp "//malicious.com/foo/")))
(is (= {:status 301 :Location "/malicious.com/foo"} (resp "//malicious.com/foo//")))
(is (= {:status 301 :Location "/malicious.com/foo/"} (resp "///malicious.com/foo")))
(is (= {:status 301 :Location "/malicious.com/foo"} (resp "///malicious.com/foo/")))
(is (= {:status 301 :Location "/malicious.com/foo"} (resp "///malicious.com/foo//"))))
(testing "with query params"
(is (= {:status 301 :Location "/malicious.com/foo/?bar=quux"} (resp "//malicious.com/foo" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo?bar=quux"} (resp "//malicious.com/foo/" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo?bar=quux"} (resp "//malicious.com/foo//" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo/?bar=quux"} (resp "///malicious.com/foo" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo?bar=quux"} (resp "///malicious.com/foo/" "bar=quux")))
(is (= {:status 301 :Location "/malicious.com/foo?bar=quux"} (resp "///malicious.com/foo//" "bar=quux"))))))))
(deftest async-ring-test (deftest async-ring-test
(let [promise #(let [value (atom ::nil)] (let [promise #(let [value (atom ::nil)]
@ -828,23 +872,22 @@
(let [body (:body (app {:request-method :get, :uri (str "/" n)}))] (let [body (:body (app {:request-method :get, :uri (str "/" n)}))]
(is (= body (str n)))))))))))) (is (= body (str n))))))))))))
(declare routes) (def routes (atom nil))
(deftest reloading-ring-handler-test (deftest reloading-ring-handler-test
(let [r (fn [body] {:status 200, :body body})] (let [r (fn [body] {:status 200, :body body})]
(def routes ["/" (constantly (r "1"))]) ;; initial value (reset! routes ["/" (constantly (r "1"))]) ;; initial value
(let [create-handler (fn [] (ring/ring-handler (ring/router @routes)))]
(let [create-handler (fn [] (ring/ring-handler (ring/router routes)))]
(testing "static ring handler does not see underlying route changes" (testing "static ring handler does not see underlying route changes"
(let [app (create-handler)] (let [app (create-handler)]
(is (= (r "1") (app {:uri "/", :request-method :get}))) (is (= (r "1") (app {:uri "/", :request-method :get})))
(def routes ["/" (constantly (r "2"))]) ;; redefine (reset! routes ["/" (constantly (r "2"))]) ;; redefine
(is (= (r "1") (app {:uri "/", :request-method :get}))))) (is (= (r "1") (app {:uri "/", :request-method :get})))))
(testing "reloading ring handler sees underlying route changes" (testing "reloading ring handler sees underlying route changes"
(let [app (ring/reloading-ring-handler create-handler)] (let [app (ring/reloading-ring-handler create-handler)]
(is (= (r "2") (app {:uri "/", :request-method :get}))) (is (= (r "2") (app {:uri "/", :request-method :get})))
(def routes ["/" (constantly (r "3"))]) ;; redefine again (reset! routes ["/" (constantly (r "3"))]) ;; redefine again
(is (= (r "3") (app {:uri "/", :request-method :get})))))))) (is (= (r "3") (app {:uri "/", :request-method :get}))))))))
(defrecord FooTest [a b]) (defrecord FooTest [a b])

View file

@ -41,7 +41,9 @@
"/olipa/:kerran/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"] "/olipa/:kerran/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/{kerran}/avaruus"] "/olipa/{kerran}/avaruus", ["/olipa/{kerran}/avaruus"]
"/olipa/{:kerran}/avaruus", ["/olipa/{:kerran}/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/{a.b/c}/avaruus"] "/olipa/{a.b/c}/avaruus", ["/olipa/{a.b/c}/avaruus"]
"/olipa/{:a.b/c}/avaruus", ["/olipa/{:a.b/c}/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/" (trie/->CatchAll :avaruus)] "/olipa/kerran/*avaruus", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/{" (trie/->CatchAll (keyword "avaruus}"))] "/olipa/kerran/{*avaruus}", ["/olipa/kerran/{" (trie/->CatchAll (keyword "avaruus}"))]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/{" (trie/->CatchAll (keyword "valtavan.suuri/avaruus}"))]))) "/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/{" (trie/->CatchAll (keyword "valtavan.suuri/avaruus}"))])))
@ -53,7 +55,9 @@
"/olipa/:kerran/avaruus", ["/olipa/:kerran/avaruus"] "/olipa/:kerran/avaruus", ["/olipa/:kerran/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"] "/olipa/{kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{:kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"] "/olipa/{a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/{:a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/*avaruus"] "/olipa/kerran/*avaruus", ["/olipa/kerran/*avaruus"]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/" (trie/->CatchAll :avaruus)] "/olipa/kerran/{*avaruus}", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/" (trie/->CatchAll :valtavan.suuri/avaruus)]))) "/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/" (trie/->CatchAll :valtavan.suuri/avaruus)])))
@ -65,7 +69,9 @@
"/olipa/:kerran/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"] "/olipa/:kerran/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"] "/olipa/{kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{:kerran}/avaruus", ["/olipa/" (trie/->Wild :kerran) "/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"] "/olipa/{a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/{:a.b/c}/avaruus", ["/olipa/" (trie/->Wild :a.b/c) "/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/" (trie/->CatchAll :avaruus)] "/olipa/kerran/*avaruus", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/" (trie/->CatchAll :avaruus)] "/olipa/kerran/{*avaruus}", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/" (trie/->CatchAll :valtavan.suuri/avaruus)]))) "/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/" (trie/->CatchAll :valtavan.suuri/avaruus)])))
@ -77,7 +83,9 @@
"/olipa/:kerran/avaruus", ["/olipa/:kerran/avaruus"] "/olipa/:kerran/avaruus", ["/olipa/:kerran/avaruus"]
"/olipa/{kerran}/avaruus", ["/olipa/{kerran}/avaruus"] "/olipa/{kerran}/avaruus", ["/olipa/{kerran}/avaruus"]
"/olipa/{:kerran}/avaruus", ["/olipa/{:kerran}/avaruus"]
"/olipa/{a.b/c}/avaruus", ["/olipa/{a.b/c}/avaruus"] "/olipa/{a.b/c}/avaruus", ["/olipa/{a.b/c}/avaruus"]
"/olipa/{:a.b/c}/avaruus", ["/olipa/{:a.b/c}/avaruus"]
"/olipa/kerran/*avaruus", ["/olipa/kerran/*avaruus"] "/olipa/kerran/*avaruus", ["/olipa/kerran/*avaruus"]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/{*avaruus}"] "/olipa/kerran/{*avaruus}", ["/olipa/kerran/{*avaruus}"]
"/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/{*valtavan.suuri/avaruus}"])))) "/olipa/kerran/{*valtavan.suuri/avaruus}", ["/olipa/kerran/{*valtavan.suuri/avaruus}"]))))