Compare commits

...

30 commits

Author SHA1 Message Date
Daniel Compton
daa23a6c87
Merge 7a707f042b into 248200aad3 2026-01-16 17:06:05 +13: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
Daniel Compton
7a707f042b Add regex based router
Reitit is very fast, but cannot support all routing structures that
other routers like Bidi support, particular regex-based routes.
2025-02-25 22:50:06 +13:00
80 changed files with 682 additions and 186 deletions

View file

@ -1,5 +1,6 @@
{;;: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}
:unused-binding {:level :off}
:unused-referred-var {:exclude {clojure.test [deftest testing is are]

View file

@ -2,16 +2,19 @@ name: testsuite
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build-clj:
strategy:
matrix:
# 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
@ -36,14 +39,18 @@ jobs:
uses: DeLaGuardo/setup-clojure@13.1
with:
lein: 2.9.5
clj-kondo: 2025.12.23
# Install openapi-schema-validator for openapi-tests
# Uses node version from the ubuntu-latest
- name: Install dependencies
run: npm ci
- name: Run Linter
run: ./lint.sh
- name: Run tests
run: ./scripts/test.sh clj
run: ./scripts/test.sh clj${{ matrix.clojure }}
build-cljs:
name: ClojureScript

View file

@ -12,11 +12,20 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
## UNRELEASED
## 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)

View file

@ -66,14 +66,14 @@ modules will continue to be released under `metosin` for compatibility purposes.
All main modules bundled:
```clj
[metosin/reitit "0.9.2"]
[metosin/reitit "0.10.0"]
```
Optionally, the parts can be required separately.
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

View file

@ -41,7 +41,7 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
All bundled:
```clj
[metosin/reitit "0.9.2"]
[metosin/reitit "0.10.0"]
```
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
```clj
[metosin/reitit-dev "0.9.2"]
[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.

View file

@ -1,7 +1,7 @@
# Default Interceptors
```clj
[metosin/reitit-interceptors "0.9.2"]
[metosin/reitit-interceptors "0.10.0"]
```
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
```clj
[metosin/reitit-http "0.9.2"]
[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.

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.
```clj
[metosin/reitit-pedestal "0.9.2"]
[metosin/reitit-pedestal "0.10.0"]
```
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
; [io.pedestal/pedestal.service "0.5.5"]
; [io.pedestal/pedestal.jetty "0.5.5"]
; [metosin/reitit-pedestal "0.9.2"]
; [metosin/reitit "0.9.2"]
; [metosin/reitit-pedestal "0.10.0"]
; [metosin/reitit "0.10.0"]
(require '[io.pedestal.http :as server])
(require '[reitit.pedestal :as pedestal])

View file

@ -1,7 +1,7 @@
# Sieppari
```clj
[metosin/reitit-sieppari "0.9.2"]
[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).

View file

@ -65,7 +65,7 @@ There is an extra option in http-router (actually, in the underlying interceptor
### Printing Context Diffs
```clj
[metosin/reitit-interceptors "0.9.2"]
[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:

View file

@ -1,7 +1,7 @@
# Default Middleware
```clj
[metosin/reitit-middleware "0.9.2"]
[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.

View file

@ -1,7 +1,7 @@
# Exception Handling with Ring
```clj
[metosin/reitit-middleware "0.9.2"]
[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.

View file

@ -5,7 +5,7 @@
Read more about the [Ring Concepts](https://github.com/ring-clojure/ring/wiki/Concepts).
```clj
[metosin/reitit-ring "0.9.2"]
[metosin/reitit-ring "0.10.0"]
```
## `reitit.ring/router`

View file

@ -1,7 +1,7 @@
# Swagger Support
```
[metosin/reitit-swagger "0.9.2"]
[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.
@ -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.
```
[metosin/reitit-swagger-ui "0.9.2"]
[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:

View file

@ -59,7 +59,7 @@ There is an extra option in the Ring router (actually, in the underlying middlew
### Printing Request Diffs
```clj
[metosin/reitit-middleware "0.9.2"]
[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:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,6 @@
[reitit.ring.middleware.multipart :as multipart]
[reitit.ring.middleware.parameters :as parameters]
[ring.adapter.jetty :as jetty]
[malli.core :as malli]
[muuntaja.core :as m]))
(def Transaction
@ -157,22 +156,22 @@
[:regex [:re "[0-9]+"]]
[:enum [:enum 1 3 5 42]]
[:multi [:multi {:dispatch :type}
[:literal [:map
[:type [:= :literal]]
["literal" [:map
[:type [:= "literal"]]
[:value [:or :int :string]]]]
[:reference [:map
[:type [:= :reference]]
["reference" [:map
[:type [:= "reference"]]
[:description :string]
[:ref :uuid]]]]]]
:example {:vector-of-tuples [["a" 1] ["b" 2]]
:regex "01234"
:enum 5
:multi {:type :literal
:multi {:type "literal"
:value "x"}}}}}
:responses {200 {:content {:default {:schema [:map]}}}}
:responses {200 {:content {:default {:schema [:map-of :keyword :any]}}}}
:handler (fn [request]
{:status 200
:body (:body request)})}}]
:body (get-in request [:parameters :request])})}}]
["/secure"
{:tags #{"secure"}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -244,7 +244,7 @@
(if coercer
(let [result (coercer query-params :default)]
(if (error? result)
(throw (ex-info (str "Query parameters coercion failed")
(throw (ex-info "Query parameters coercion failed"
result))
result))
query-params))))

View file

@ -7,6 +7,10 @@
([type 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]
#?(:clj (.getMessage ^Exception e) :cljs (ex-message e)))

View file

@ -1,7 +1,6 @@
(ns ^:no-doc reitit.impl
#?(:cljs (:require-macros [reitit.impl]))
(:require [clojure.set :as set]
[clojure.string :as str]
(:require [clojure.string :as str]
[meta-merge.core :as mm]
[reitit.exception :as ex]
[reitit.trie :as trie])

View file

@ -0,0 +1,121 @@
(ns reitit.regex
(:require [clojure.set :as set]
[clojure.string :as str]
[reitit.core :as r]))
(defn compile-regex-route
"Given a route vector [path route-data], returns a map with:
- :pattern: a compiled regex pattern built from the path segments,
- :group-keys: vector of parameter keys in order,
- :route-data: the provided route data,
- :original-segments: original path segments for path generation,
- :template: the original path template for Match objects."
[[path route-data]]
(let [;; Normalize route-data to ensure it's a map with :name
route-data (if (keyword? route-data)
{:name route-data}
route-data)
;; Store the original path template for Match objects
template (if (str/starts-with? path "/")
path
(str "/" path))
;; Handle paths with or without leading slashes
normalized-path (cond-> path
(str/starts-with? path "/") (subs 1))
;; Split into segments, handling empty paths
segments (if (empty? normalized-path)
[]
(str/split normalized-path #"/"))
;; Store original segments for path generation
original-segments segments
compiled-segments
(map (fn [seg]
(if (str/starts-with? seg ":")
(let [param-key (keyword (subs seg 1))
param-regex (get-in route-data [:parameters :path param-key])]
(if (and param-regex (instance? java.util.regex.Pattern param-regex))
(str "(" (.pattern ^java.util.regex.Pattern param-regex) ")")
;; Fallback: match any non-slash characters.
"([^/]+)"))
(java.util.regex.Pattern/quote seg)))
segments)
;; Create the pattern string, handling special case for root path
pattern-str (if (empty? segments)
"^/?$" ;; Match root path with optional trailing slash
(str "^/" (str/join "/" compiled-segments) "$"))
group-keys (->> segments
(filter #(str/starts-with? % ":"))
(map #(keyword (subs % 1)))
(vec))]
{:pattern (re-pattern pattern-str)
:group-keys group-keys
:route-data route-data
:original-segments original-segments
:template template}))
(defn- generate-path
"Generate a path from a route and path parameters."
[route path-params]
(if (empty? (:original-segments route))
"/"
(str "/" (str/join "/"
(map (fn [segment]
(if (str/starts-with? segment ":")
(let [param-key (keyword (subs segment 1))]
(get path-params param-key ""))
segment))
(:original-segments route))))))
(defrecord RegexRouter [compiled-routes]
r/Router
(router-name [_] :regex-router)
(routes [_]
(mapv (fn [{:keys [route-data original-segments]}]
[(str "/" (str/join "/" original-segments)) route-data])
compiled-routes))
(compiled-routes [_] compiled-routes)
(options [_] {})
(route-names [_]
(keep (comp :name :route-data) compiled-routes))
(match-by-path [_ path]
(some (fn [{:keys [pattern group-keys route-data template]}]
(when-let [matches (re-matches pattern path)]
(let [params (zipmap group-keys (rest matches))]
(r/->Match template route-data nil params path))))
compiled-routes))
(match-by-name [this name]
(r/match-by-name this name {}))
(match-by-name [router name path-params]
(when-let [{:keys [group-keys route-data template] :as route}
(first (filter #(= name (get-in % [:route-data :name])) (r/compiled-routes router)))]
;; Check if all required params are provided
(let [required-params (set group-keys)
provided-params (set (keys path-params))]
(if (every? #(contains? provided-params %) required-params)
;; All required params provided, return a Match
(let [path (generate-path route path-params)]
(r/->Match template route-data nil path-params path))
;; Some required params missing, return a PartialMatch
(let [missing (set/difference required-params provided-params)]
(r/->PartialMatch template route-data nil path-params missing)))))))
(defn create-regex-router
"Create a RegexRouter from a vector of routes.
Each route should be a vector [path route-data]."
[routes]
(->RegexRouter (mapv compile-regex-route routes)))

View file

@ -71,11 +71,17 @@
(and bracket? (= \{ c))
(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')))
(= \: (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')))))
(and colon? (= \: c))
(and colon? (= \: c) (not= \{ (get s (dec to))))
(let [^long to' (or (str/index-of s "/" to) (count s))]
(if (= 1 (- to' to))
(recur ss from (inc to))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
(ns reitit.interceptor.sieppari
(:require [reitit.interceptor :as interceptor]
(:require [reitit.exception :as ex]
[reitit.interceptor :as interceptor]
[sieppari.core :as sieppari]
[sieppari.queue :as queue]))
@ -15,4 +16,6 @@
(execute [_ interceptors request]
(sieppari/execute interceptors request))
(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.2"
(defproject metosin/reitit-spec "0.10.0"
:description "Reitit: clojure.spec coercion"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

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

View file

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

View file

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

View file

@ -80,12 +80,15 @@
(defrecord NoOpCoercion []
coercion/Coercion
(-get-name [_] :no-op)
(-get-options [_])
(-get-apidocs [_ _ {:keys [parameters responses] :as info}])
(-get-model-apidocs [_ _ _ _])
(-compile-model [_ model _] model)
(-open-model [_ spec] spec)
(-encode-error [_ error] error)
(-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
(doseq [coercion [nil (->NoOpCoercion) spec/coercion]]

View file

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

View file

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

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-parent "0.9.2"
(defproject metosin/reitit-parent "0.10.0"
:description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"
@ -18,38 +18,38 @@
:url "https://github.com/metosin/reitit"}
;; Ring 1.13.1 drops support for Java 1.8 so lets target 11
:javac-options ["-Xlint:unchecked" "-target" "11" "-source" "11"]
:managed-dependencies [[metosin/reitit "0.9.2"]
[metosin/reitit-core "0.9.2"]
[metosin/reitit-dev "0.9.2"]
[metosin/reitit-spec "0.9.2"]
[metosin/reitit-malli "0.9.2"]
[metosin/reitit-schema "0.9.2"]
[metosin/reitit-ring "0.9.2"]
[metosin/reitit-middleware "0.9.2"]
[metosin/reitit-http "0.9.2"]
[metosin/reitit-interceptors "0.9.2"]
[metosin/reitit-swagger "0.9.2"]
[fi.metosin/reitit-openapi "0.9.2"]
[metosin/reitit-swagger-ui "0.9.2"]
[metosin/reitit-frontend "0.9.2"]
[metosin/reitit-sieppari "0.9.2"]
[metosin/reitit-pedestal "0.9.2"]
:managed-dependencies [[metosin/reitit "0.10.0"]
[metosin/reitit-core "0.10.0"]
[metosin/reitit-dev "0.10.0"]
[metosin/reitit-spec "0.10.0"]
[metosin/reitit-malli "0.10.0"]
[metosin/reitit-schema "0.10.0"]
[metosin/reitit-ring "0.10.0"]
[metosin/reitit-middleware "0.10.0"]
[metosin/reitit-http "0.10.0"]
[metosin/reitit-interceptors "0.10.0"]
[metosin/reitit-swagger "0.10.0"]
[fi.metosin/reitit-openapi "0.10.0"]
[metosin/reitit-swagger-ui "0.10.0"]
[metosin/reitit-frontend "0.10.0"]
[metosin/reitit-sieppari "0.10.0"]
[metosin/reitit-pedestal "0.10.0"]
[metosin/ring-swagger-ui "5.20.0"]
[metosin/spec-tools "0.10.8"]
[metosin/schema-tools "0.13.1"]
[metosin/muuntaja "0.6.11"]
[metosin/jsonista "0.3.13"]
[metosin/sieppari "0.0.0-alpha13"]
[metosin/malli "0.19.2"]
[metosin/malli "0.20.0"]
;; https://clojureverse.org/t/depending-on-the-right-versions-of-jackson-libraries/5111
[com.fasterxml.jackson.core/jackson-core "2.20.0"]
[com.fasterxml.jackson.core/jackson-databind "2.20.0"]
[com.fasterxml.jackson.core/jackson-core "2.20.1"]
[com.fasterxml.jackson.core/jackson-databind "2.20.1"]
[meta-merge "1.0.0"]
[fipp "0.6.29" :exclusions [org.clojure/core.rrb-vector]]
;; 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"]
[lambdaisland/deep-diff "0.0-47"]
[com.bhauman/spell-spec "0.1.2"]
@ -67,7 +67,8 @@
[lein-codox "0.10.8"]
[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
:source-paths ["modules/reitit/src"
@ -89,9 +90,9 @@
:java-source-paths ["modules/reitit-core/java-src"]
:dependencies [[org.clojure/clojure "1.11.4"]
[thheller/shadow-cljs "3.2.1"]
[org.clojure/clojurescript "1.12.42"]
:dependencies [[org.clojure/clojure "1.12.4"]
[thheller/shadow-cljs "3.3.4"]
[org.clojure/clojurescript "1.12.134"]
;; modules dependencies
[metosin/schema-tools "0.13.1"]
@ -99,7 +100,7 @@
[metosin/muuntaja "0.6.11"]
[metosin/sieppari "0.0.0-alpha13"]
[metosin/jsonista "0.3.13"]
[metosin/malli "0.19.2"]
[metosin/malli "0.20.0"]
[lambdaisland/deep-diff "0.0-47"]
[meta-merge "1.0.0"]
[com.bhauman/spell-spec "0.1.2"]
@ -112,11 +113,11 @@
[ikitommi/immutant-web "3.0.0-alpha1"]
[metosin/ring-http-response "0.9.5"]
[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"]
[org.clojure/test.check "1.1.1"]
[org.clojure/tools.namespace "1.5.0"]
[org.clojure/test.check "1.1.3"]
[org.clojure/tools.namespace "1.5.1"]
[com.gfredericks/test.chuck "0.2.15"]
[nubank/matcher-combinators "3.9.2"]
@ -124,10 +125,10 @@
[io.pedestal/pedestal.service "0.6.4" :upgrade false]
[org.clojure/core.async "1.8.741"]
[manifold "0.4.3"]
[manifold "0.5.0"]
[funcool/promesa "11.0.678"]
[com.clojure-goes-fast/clj-async-profiler "1.6.2"]
[com.clojure-goes-fast/clj-async-profiler "1.7.0"]
[ring-cors "0.1.13"]
[com.bhauman/rebel-readline "0.1.5"]]}
@ -143,7 +144,7 @@
[io.pedestal/pedestal.jetty "0.6.4" :upgrade false]
[calfpath "0.8.1"]
[org.clojure/core.async "1.8.741"]
[manifold "0.4.3"]
[manifold "0.5.0"]
[funcool/promesa "11.0.678"]
[metosin/sieppari]
[yada "1.2.16"]
@ -159,6 +160,7 @@
:aliases {"all" ["with-profile" "dev,default"]
"perf" ["with-profile" "default,dev,perf"]
"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
;; the same way.
"test-browser" ["doo" "chrome-headless" "test"]

View file

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

View file

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

View file

@ -11,7 +11,7 @@
(:import (clojure.lang ExceptionInfo)
(java.sql SQLException SQLWarning)))
(derive ::kikka ::kukka)
(derive ::kukka ::kikka)
(deftest exception-test
(letfn [(create
@ -147,6 +147,55 @@
(is (= status 500))
(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
(let [app (ring/ring-handler
(ring/router

View file

@ -2,7 +2,6 @@
(:require [clojure.spec.alpha :as cs]
[clojure.string :as str]
[clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.experimental.lite :as l]
[reitit.coercion :as coercion]
[reitit.coercion.malli]
@ -10,8 +9,7 @@
[reitit.coercion.spec]
[reitit.core :as r]
[schema.core :as s]
[spec-tools.data-spec :as ds]
[malli.transform :as mt])
[spec-tools.data-spec :as ds])
#?(:clj
(:import (clojure.lang ExceptionInfo))))
@ -110,7 +108,6 @@
(testing "spec-coercion (shallow)"
(testing "succeeds"
(let [m (r/match-by-path r "/spec-shallow/1/abba")]
(def MATCH m)
(is (= {:path {:keyword :abba, :number 1}, :query nil}
(coercion/coerce! m))))
(let [m (r/match-by-path r "/spec-shallow/1/abba")]

View file

@ -1,10 +1,9 @@
(ns reitit.core-test
(:require [clojure.test :refer [are deftest is testing]]
[reitit.core :as r #?@(:cljs [:refer [Router]])]
[reitit.core :as r]
[reitit.impl :as impl])
#?(:clj
(:import (clojure.lang ExceptionInfo)
(reitit.core Router))))
(:import (clojure.lang ExceptionInfo))))
(defn- var-handler [& _]
"var-handler")
@ -261,10 +260,12 @@
(is (= #'var-handler result))))))
(testing "custom router"
(let [router (r/router ["/ping"] {:router (fn [_ _]
(reify Router
(r/router-name [_]
::custom)))})]
(let [router (r/router
["/ping"]
{:router (fn [_ _]
#_{:clj-kondo/ignore [:missing-protocol-method]}
(reify r/Router
(router-name [_] ::custom)))})]
(is (= ::custom (r/router-name router)))))
(testing "bide sample"

View file

@ -4,7 +4,6 @@
[jsonista.core :as j]
[malli.core :as mc]
[matcher-combinators.test :refer [match?]]
[matcher-combinators.matchers :as matchers]
[muuntaja.core :as m]
[reitit.coercion.malli :as malli]
[reitit.coercion.schema :as schema]

View file

@ -0,0 +1,278 @@
(ns reitit.regex-test
(:require [clojure.test :refer [deftest is testing]]
[reitit.core :as r]
[reitit.regex :as rt.regex]))
(defn re-=
"A custom equality function that handles regex patterns specially.
Returns true if a and b are equal, with special handling for regex patterns.
Also handles comparing records by their map representation."
[a b]
(cond
;; Handle record comparison by using their map representation
(and (instance? clojure.lang.IRecord a)
(instance? clojure.lang.IRecord b))
(re-= (into {} a) (into {} b))
;; If both are regex patterns, compare their string representations
(and (instance? java.util.regex.Pattern a)
(instance? java.util.regex.Pattern b))
(= (str a) (str b))
;; If one is a regex and the other isn't, they're not equal
(or (instance? java.util.regex.Pattern a)
(instance? java.util.regex.Pattern b))
false
;; For maps, compare each key-value pair using regex-aware-equals
(and (map? a) (map? b))
(and (= (set (keys a)) (set (keys b)))
(every? #(re-= (get a %) (get b %)) (keys a)))
;; For sequences, compare each element using regex-aware-equals
(and (sequential? a) (sequential? b))
(and (= (count a) (count b))
(every? identity (map re-= a b)))
;; For sets, convert to sequences and compare
(and (set? a) (set? b))
(re-= (seq a) (seq b))
;; For everything else, use regular equality
:else
(= a b)))
(def routes
(rt.regex/create-regex-router
[["" ::home]
[":item-id" {:name ::item
:parameters {:path {:item-id #"[a-z]{16,20}"}}}]
["inbox" ::inbox]
["teams" ::teams]
["teams/:team-id-b58/members" {:name ::->members
:parameters {:path {:team-id-b58 #"[a-z]"}}}]]))
(deftest regex-match-by-path-test
(testing "Basic path matching"
(is (= (r/map->Match {:path "/"
:path-params {}
:data {:name ::home}
:template "/"
:result nil})
(r/match-by-path routes "/")))
(is (= (r/map->Match {:path "/inbox"
:path-params {}
:data {:name ::inbox}
:template "/inbox"
:result nil})
(r/match-by-path routes "/inbox")))
(is (= (r/map->Match {:path "/teams"
:path-params {}
:data {:name ::teams}
:template "/teams"
:result nil})
(r/match-by-path routes "/teams"))))
(testing "Path with regex parameter"
(let [valid-id "abcdefghijklmnopq"] ; 17 lowercase letters
(is (re-= (r/map->Match {:path (str "/" valid-id)
:path-params {:item-id valid-id}
:data {:name ::item
:parameters {:path {:item-id #"[a-z]{16,20}"}}},
:template "/:item-id"
:result nil})
(r/match-by-path routes (str "/" valid-id)))))
;; Invalid parameter cases
(is (nil? (r/match-by-path routes "/abcdefg")) "Too short")
(is (nil? (r/match-by-path routes "/abcdefghijklmnopqRST")) "Contains uppercase")
(is (nil? (r/match-by-path routes "/abcdefghijklmn1234")) "Contains digits"))
(testing "Nested path with parameter"
(is (re-= (r/map->Match {:path "/teams/a/members"
:path-params {:team-id-b58 "a"}
:data {:name ::->members
:parameters {:path {:team-id-b58 #"[a-z]"}}}
:template "/teams/:team-id-b58/members"
:result nil})
(r/match-by-path routes "/teams/a/members")))
(is (nil? (r/match-by-path routes "/teams/abc/members")) "Multiple characters")
(is (nil? (r/match-by-path routes "/teams/1/members")) "Digit instead of letter"))
(testing "Non-matching paths"
(is (nil? (r/match-by-path routes "/unknown")))
(is (nil? (r/match-by-path routes "/team"))) ; 'team' not 'teams'
(is (nil? (r/match-by-path routes "/teams/extra/segments/here")))))
(deftest regex-match-by-name-test
(testing "Basic match-by-name functionality"
;; Root path
(is (re-= (r/map->Match {:path "/"
:path-params {}
:data {:name ::home}
:template "/"
:result nil})
(r/match-by-name routes ::home)))
;; Static paths
(is (re-= (r/map->Match {:path "/inbox"
:path-params {}
:data {:name ::inbox}
:template "/inbox"
:result nil})
(r/match-by-name routes ::inbox)))
(is (re-= (r/map->Match {:path "/teams"
:path-params {}
:data {:name ::teams}
:template "/teams"
:result nil})
(r/match-by-name routes ::teams)))
;; Path with parameter
(let [valid-id "abcdefghijklmnopq"]
(is (re-= (r/map->Match {:path (str "/" valid-id)
:path-params {:item-id valid-id}
:data {:name ::item
:parameters {:path {:item-id #"[a-z]{16,20}"}}}
:template "/:item-id"
:result nil})
(r/match-by-name routes ::item {:item-id valid-id}))))
;; Nested path with parameter
(is (re-= (r/map->Match {:path "/teams/a/members"
:path-params {:team-id-b58 "a"}
:data {:name ::->members
:parameters {:path {:team-id-b58 #"[a-z]"}}}
:template "/teams/:team-id-b58/members"
:result nil})
(r/match-by-name routes ::->members {:team-id-b58 "a"}))))
(testing "Path round-trip matching"
;; Test that paths generated by match-by-name can be successfully matched by match-by-path
(let [valid-id "abcdefghijklmnopq"
match (r/match-by-name routes ::item {:item-id valid-id})
path (:path match)]
(is (some? path) "Should generate a valid path")
(is (re-= match (r/match-by-path routes path))
"match-by-path should find the same route that generated the path"))
(let [match (r/match-by-name routes ::->members {:team-id-b58 "a"})
path (:path match)]
(is (some? path) "Should generate a valid path")
(is (re-= match (r/match-by-path routes path))
"match-by-path should find the same route that generated the path")))
(testing "Partial match with missing parameters"
;; Test that routes with missing parameters return PartialMatch
(let [partial-match (r/match-by-name routes ::item {})]
(is (instance? reitit.core.PartialMatch partial-match)
"Should return a PartialMatch when params are missing")
(is (= #{:item-id} (:required partial-match))
"PartialMatch should indicate the required parameters")
(is (re-= (r/map->PartialMatch {:template "/:item-id"
:data {:name ::item
:parameters {:path {:item-id #"[a-z]{16,20}"}}}
:path-params {}
:required #{:item-id}
:result nil})
partial-match)))
;; Test for a nested path with missing parameters
(let [partial-match (r/match-by-name routes ::->members {})]
(is (instance? reitit.core.PartialMatch partial-match)
"Should return a PartialMatch for nested paths too")
(is (= #{:team-id-b58} (:required partial-match))
"PartialMatch should indicate the required parameters")))
(testing "Match with invalid parameters"
;; Invalid parameters (that don't match the regex) still produce a Match
(let [match (r/match-by-name routes ::item {:item-id "too-short"})
path (:path match)]
(is (instance? reitit.core.Match match)
"Should produce a Match even with invalid parameters")
(is (= "/too-short" path)
"Path should contain the provided parameter value")
(is (nil? (r/match-by-path routes path))
"Path with invalid parameter shouldn't be matchable by match-by-path")))
(testing "Non-existent routes"
(is (nil? (r/match-by-name routes ::non-existent))
"Should return nil for non-existent routes")))
(deftest regex-router-edge-cases-test
(testing "Empty router"
(let [empty-router (rt.regex/create-regex-router [])]
(is (nil? (r/match-by-path empty-router "/any/path")))))
(testing "Handling trailing slashes"
(is (nil? (r/match-by-path routes "/inbox/")))
(let [router-with-trailing-slash (rt.regex/create-regex-router [["inbox/" ::inbox-with-slash]])]
(is (nil? (r/match-by-path router-with-trailing-slash "/inbox/")))
(is (some? (r/match-by-path router-with-trailing-slash "/inbox")))))
(testing "Complex path patterns"
(let [complex-router (rt.regex/create-regex-router
[["articles/:year/:month/:slug"
{:name ::article
:parameters {:path {:year #"\d{4}"
:month #"\d{2}"
:slug #"[a-z0-9\-]+"}}}]
["files/:path*"
{:name ::file-path}]])]
;; Test article route with valid params
(let [match (r/match-by-name complex-router ::article
{:year "2023" :month "02" :slug "test-article"})]
(is (instance? reitit.core.Match match)
"Should return a Match for complex routes with valid params")
(is (= "/articles/2023/02/test-article" (:path match))
"Path should be constructed correctly"))
;; Test match-by-path with the generated path
(let [match (r/match-by-path complex-router "/articles/2023/02/test-article")]
(is (some? match)
"Should match a valid article path")
(is (= {:year "2023", :month "02", :slug "test-article"}
(:path-params match))
"Should extract all parameters correctly"))
;; Test invalid path
(is (nil? (r/match-by-path complex-router "/articles/202/02/test-article"))
"Should not match an invalid year (3 digits)")
;; Test partial params
(let [partial-match (r/match-by-name complex-router ::article {:year "2023"})]
(is (instance? reitit.core.PartialMatch partial-match)
"Should return PartialMatch when some params are missing")
(is (= #{:month :slug} (:required partial-match))
"Should indicate which params are missing")))))
(deftest custom-router-features-test
(testing "Router information access"
;; Test that router information methods work properly
(is (= :regex-router (r/router-name routes))
"Should return the correct router name")
(is (seq (r/routes routes))
"Should return the list of routes")
(is (= (set [::home ::item ::inbox ::teams ::->members])
(set (r/route-names routes)))
"Should return all route names"))
(testing "Compiled routes access"
(let [compiled (r/compiled-routes routes)]
(is (seq compiled)
"Should return compiled routes")
(is (every? :pattern compiled)
"Every compiled route should have a pattern")
(is (every? :route-data compiled)
"Every compiled route should have route data"))))

View file

@ -4,18 +4,18 @@
#?@(:clj [[muuntaja.core]
[muuntaja.middleware]
[jsonista.core :as j]
[reitit.ring.middleware.muuntaja]])
[reitit.coercion.schema :as schema]
[reitit.ring.middleware.muuntaja]
[schema.core :as s]])
[malli.core :as m]
[malli.util :as mu]
[meta-merge.core :refer [meta-merge]]
[reitit.coercion.malli :as malli]
[reitit.coercion.schema :as schema]
[reitit.coercion.spec :as spec]
[reitit.core :as r]
[reitit.ring :as ring]
[reitit.ring.spec]
[reitit.ring.coercion :as rrc]
[schema.core :as s]
[clojure.spec.alpha]
[spec-tools.data-spec :as ds])
#?(:clj

View file

@ -4,7 +4,8 @@
[reitit.core :as r]
[reitit.middleware :as middleware]
[reitit.ring :as ring]
[reitit.trie :as trie])
#?(:clj [reitit.trie :as trie]
:cljs [reitit.trie :as-alias trie]))
#?(:clj
(:import (clojure.lang ExceptionInfo))))
@ -847,23 +848,22 @@
(let [body (:body (app {:request-method :get, :uri (str "/" n)}))]
(is (= body (str n))))))))))))
(declare routes)
(def routes (atom nil))
(deftest reloading-ring-handler-test
(let [r (fn [body] {:status 200, :body body})]
(def routes ["/" (constantly (r "1"))]) ;; initial value
(let [create-handler (fn [] (ring/ring-handler (ring/router routes)))]
(reset! routes ["/" (constantly (r "1"))]) ;; initial value
(let [create-handler (fn [] (ring/ring-handler (ring/router @routes)))]
(testing "static ring handler does not see underlying route changes"
(let [app (create-handler)]
(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})))))
(testing "reloading ring handler sees underlying route changes"
(let [app (ring/reloading-ring-handler create-handler)]
(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}))))))))
(defrecord FooTest [a b])

View file

@ -41,7 +41,9 @@
"/olipa/:kerran/avaruus", ["/olipa/" (trie/->Wild :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/kerran/*avaruus", ["/olipa/kerran/" (trie/->CatchAll :avaruus)]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/{" (trie/->CatchAll (keyword "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/" (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/kerran/*avaruus", ["/olipa/kerran/*avaruus"]
"/olipa/kerran/{*avaruus}", ["/olipa/kerran/" (trie/->CatchAll :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/{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/{*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/{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/{*valtavan.suuri/avaruus}", ["/olipa/kerran/{*valtavan.suuri/avaruus}"]))))