Merge branch 'master' into support-operationid

This commit is contained in:
Tommi Reiman 2023-01-09 17:27:00 +02:00 committed by GitHub
commit 42e988e518
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
160 changed files with 3544 additions and 2555 deletions

View file

@ -1,86 +0,0 @@
version: 2.1
jobs:
test-clj:
working_directory: ~/test
parameters:
image-name:
type: string
docker:
- image: << parameters.image-name >>
steps:
- checkout
- restore_cache:
keys:
- 'v1-clj-{{ checksum "project.clj" }}'
- 'v1-clj-'
- 'v1-test-'
- run:
name: Install modules
command: ./scripts/lein-modules install
- run:
name: Run tests
command: ./scripts/test.sh clj
- run:
name: Install curl if missing
command: apt update && apt install -y curl
- run:
name: Verify cljdoc.edn
command: curl -fsSL https://raw.githubusercontent.com/cljdoc/cljdoc/master/script/verify-cljdoc-edn | bash -s doc/cljdoc.edn
- store_test_results:
# path must be a directory under which there a subdirectories that
# contain the JUnit XML files.
path: ~/test/target/results
# - run:
# name: Run coverage
# command: ./scripts/submit-to-coveralls.sh clj
- save_cache:
key: 'v1-clj-{{ checksum "project.clj" }}'
paths:
- ~/.m2
- ~/.cljs/.aot_cache
test-cljs:
working_directory: ~/test
docker:
- image: circleci/clojure:lein-2.8.1-node-browsers
steps:
- checkout
- restore_cache:
keys:
- 'v1-cljs-{{ checksum "project.clj" }}-{{ checksum "package.json" }}'
- 'v1-cljs-'
- run:
name: Install npm dependencies
command: npm install
- run:
name: Install modules
command: ./scripts/lein-modules install
- run:
name: Run tests
command: ./scripts/test.sh cljs
- store_test_results:
path: ~/test/target/results
- save_cache:
key: 'v1-cljs-{{ checksum "project.clj" }}-{{ checksum "package.json" }}'
paths:
- ~/.m2
- ~/test/node_modules
workflows:
version: 2
test-and-build-docs:
jobs:
- test-clj:
name: jdk8
image-name: clojure:openjdk-8-lein-2.9.1
- test-clj:
name: jdk11
image-name: clojure:openjdk-11-lein-2.9.1
- test-clj:
name: jdk13
image-name: clojure:openjdk-13-lein-2.9.1
- test-clj:
name: jdk14
image-name: clojure:openjdk-14-lein-2.9.1
- test-cljs

View file

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

View file

@ -0,0 +1 @@
{:config-paths ["../../../.clj-kondo"]}

78
.github/workflows/testsuite.yml vendored Normal file
View file

@ -0,0 +1,78 @@
name: testsuite
on:
push:
pull_request:
jobs:
build-clj:
strategy:
matrix:
# Supported Java versions: LTS releases 8 and 11 and the latest release
jdk: [8, 11, 15]
name: Clojure (Java ${{ matrix.jdk }})
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-clj-${{ hashFiles('**/project.clj') }}
restore-keys: |
${{ runner.os }}-clj-
- name: Setup Java ${{ matrix.jdk }}
uses: actions/setup-java@v1.4.3
with:
java-version: ${{ matrix.jdk }}
- name: Setup Clojure
uses: DeLaGuardo/setup-clojure@3.1
with:
lein: 2.9.5
- name: Run tests
run: ./scripts/test.sh clj
build-cljs:
name: ClojureScript
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Cache dependencies
uses: actions/cache@v2
with:
path: |
~/.m2/repository
**/node_modules
key: ${{ runner.os }}-cljs-${{ hashFiles('**/project.clj', '**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-cljs-
- name: Setup Java 11
uses: actions/setup-java@v1.4.3
with:
java-version: 11
- name: Setup Clojure
uses: DeLaGuardo/setup-clojure@3.1
with:
lein: 2.9.5
- name: Setup Node.js
uses: actions/setup-node@v2.1.2
with:
node-version: 12
- name: Install dependencies
run: |
npm ci
- name: Install modules
run: ./scripts/lein-modules install
- name: Run tests
run: ./scripts/test.sh cljs
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Verify cljdoc.edn
run: curl -fsSL https://raw.githubusercontent.com/cljdoc/cljdoc/master/script/verify-cljdoc-edn | bash -s doc/cljdoc.edn

3
.lsp/config.edn Normal file
View file

@ -0,0 +1,3 @@
{:cljfmt {:indents {for-all [[:inner 0]]
are [[:inner 0]]}}
:clean {:ns-inner-blocks-indentation :same-line}}

View file

@ -12,8 +12,138 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
## 0.5.18 (2022-04-05)
* FIX [#334](https://github.com/metosin/reitit/pull/334) - Frontend: there is no way to catch the exception if coercion fails (via [#549](https://github.com/metosin/reitit/pull/549))
* Save three seq constructions [#537](https://github.com/metosin/reitit/pull/537)
* update jackson-databind for CVE-2020-36518 [#544](https://github.com/metosin/reitit/pull/544)
* Balance parenthesis in docs [#547](https://github.com/metosin/reitit/pull/547)
## 0.5.17 (2022-03-10)
* FIX match-by-path is broken if there are no non-conflicting wildcard routes [#538](https://github.com/metosin/reitit/issues/538)
## 0.5.16 (2022-02-15)
**[compare](https://github.com/metosin/reitit/compare/0.5.15...0.5.16)**
* Support for [Malli Lite Syntax](https://github.com/metosin/malli#lite) in coercion (enabled by default):
```clj
["/add/:id" {:post {:parameters {:path {:id int?}
:query {:a (l/optional int?)}
:body {:id int?
:data {:id (l/maybe int?)
:orders (l/map-of uuid? {:name string?})}}}
:responses {200 {:body {:total pos-int?}}
500 {:description "fail"}}}}]
```
* Improved Reitit-frontend function docstrings
* Updated deps:
```clj
[metosin/ring-swagger-ui "4.3.0"] is available but we use "3.46.0"
[metosin/jsonista "0.3.5"] is available but we use "0.3.3"
[metosin/malli "0.8.2"] is available but we use "0.5.1"
[com.fasterxml.jackson.core/jackson-core "2.13.1"] is available but we use "2.12.4"
[com.fasterxml.jackson.core/jackson-databind "2.13.1"] is available but we use "2.12.4"
[fipp "0.6.25"] is available but we use "0.6.24"
[expound "0.9.0"] is available but we use "0.8.9"
[ring/ring-core "1.9.5"] is available but we use "1.9.4"
```
## 0.5.15 (2021-08-05)
**[compare](https://github.com/metosin/reitit/compare/0.5.14...0.5.15)**
* recompiled with Java8
## 0.5.14 (2021-08-03)
**[compare](https://github.com/metosin/reitit/compare/0.5.13...0.5.14)**
* updated deps:
```clj
[metosin/ring-swagger-ui "3.46.0"] is available but we use "3.36.0"
[metosin/jsonista "0.3.3"] is available but we use "0.3.1"
[metosin/malli "0.5.1"] is available but we use "0.3.0"
[com.fasterxml.jackson.core/jackson-core "2.12.4"] is available but we use "2.12.1"
[com.fasterxml.jackson.core/jackson-databind "2.12.4"] is available but we use "2.12.1"
[fipp "0.6.24"] is available but we use "0.6.23"
[ring/ring-core "1.9.4"] is available but we use "1.9.1"
[io.pedestal/pedestal.service "0.5.9"] is available but we use "0.5.8"
```
### `reitit-ring`
* Fixes `reitit.ring/create-resource-handler` and `reitit.ring/create-file-handler` to support URL-escaped characters. [#484](https://github.com/metosin/reitit/issues/484). PR [#489](https://github.com/metosin/reitit/pull/489).
### `reitit-malli`
* FIX: Malli response coercision seems to do nothing at all [#498](https://github.com/metosin/reitit/pull/501)
### `reitit-pedestal`
* Enrich request for pedestal/routing-interceptor default-queue [#495](https://github.com/metosin/reitit/pull/495)
## 0.5.13 (2021-04-23)
**[compare](https://github.com/metosin/reitit/compare/0.5.12...0.5.13)**
* updated deps:
```clj
[metosin/malli "0.3.0"] is available but we use "0.2.1"
[metosin/schema-tools "0.12.3"] is available but we use "0.12.2"
[ring/ring-core "1.9.1"] is available but we use "1.9.0"
[metosin/schema-tools "0.12.3"] is available but we use "0.12.2"
[expound "0.8.9"] is available but we use "0.8.7"
[ring "1.9.1"] is available but we use "1.9.0"
```
### `reitit-ring`
* Make reitit.ring/create-resource-handler's `:not-found-handler` work when used outside of a router. [#464](https://github.com/metosin/reitit/issues/464). PR [#471](https://github.com/metosin/reitit/pull/471) by Kari Marttila and Metosin Maintenance Mob.
## 0.5.12 (2021-02-01)
**[compare](https://github.com/metosin/reitit/compare/0.5.11...0.5.12)**
* updated deps:
```
[metosin/spec-tools "0.10.5"] is available but we use "0.10.4"
[metosin/jsonista "0.3.1"] is available but we use "0.3.0"
[metosin/muuntaja "0.6.8"] is available but we use "0.6.7"
[ring/ring-core "1.9.0"] is available but we use "1.8.2"
[metosin/muuntaja "0.6.8"] is available but we use "0.6.7"
[com.fasterxml.jackson.core/jackson-core "2.12.1"] is available but we use "2.12.0"
[com.fasterxml.jackson.core/jackson-databind "2.12.1"] is available but we use "2.12.0"
```
* Allow whitespace as separator in route paths. [#411](https://github.com/metosin/reitit/issues/411). PR [#466](https://github.com/metosin/reitit/pull/466) by Kimmo Koskinen and Metosin Maintenance Mob.
## 0.5.11 (2020-12-27)
**[compare](https://github.com/metosin/reitit/compare/0.5.10...0.5.11)**
* updated deps:
```clj
[metosin/ring-swagger-ui "3.36.0"] is available but we use "3.25.3"
[metosin/jsonista "0.3.0"] is available but we use "0.2.7"
[com.fasterxml.jackson.core/jackson-core "2.12.0"] is available but we use "2.11.2"
[com.fasterxml.jackson.core/jackson-databind "2.12.0"] is available but we use "2.11.2"
```
## 0.5.10 (2020-10-22)
**[compare](https://github.com/metosin/reitit/compare/0.5.9...0.5.10)**
* updated deps:
```clj
@ -26,6 +156,8 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
## 0.5.9 (2020-10-19)
**[compare](https://github.com/metosin/reitit/compare/0.5.8...0.5.9)**
### `reitit-frontend`
- `reitit.frontend.easy/start!` now correctly removes old event listeners
@ -34,10 +166,14 @@ when called repeatedly (e.g. with hot code reload workflow)
## 0.5.8 (2020-10-19)
**[compare](https://github.com/metosin/reitit/compare/0.5.7...0.5.8)**
* Add `:conflicting` to route data spec [#444](https://github.com/metosin/reitit/pull/444).
## 0.5.7 (2020-10-18)
**[compare](https://github.com/metosin/reitit/compare/0.5.6...0.5.7)**
* updated deps:
```clj
@ -55,6 +191,8 @@ when called repeatedly (e.g. with hot code reload workflow)
## 0.5.6 (2020-09-26)
**[compare](https://github.com/metosin/reitit/compare/0.5.5...0.5.6)**
* updated deps:
```clj
@ -79,6 +217,8 @@ when called repeatedly (e.g. with hot code reload workflow)
## 0.5.5 (2020-07-15)
**[compare](https://github.com/metosin/reitit/compare/0.5.4...0.5.5)**
* recompile with Java8
```clj
@ -87,18 +227,24 @@ when called repeatedly (e.g. with hot code reload workflow)
## 0.5.4 (2020-07-13)
**[compare](https://github.com/metosin/reitit/compare/0.5.3...0.5.4)**
```clj
[metosin/malli "0.0.1-20200713.080243-20"] is available but we use "0.0.1-20200709.163702-18"
```
## 0.5.3 (2020-07-09)
**[compare](https://github.com/metosin/reitit/compare/0.5.2...0.5.3)**
```clj
[metosin/malli "0.0.1-20200709.163702-18"] is available but we use "0.0.1-20200525.162645-15"
```
## 0.5.2 (2020-05-27)
**[compare](https://github.com/metosin/reitit/compare/0.5.1...0.5.2)**
```clj
[metosin/malli "0.0.1-20200525.162645-15"] is available but we use "0.0.1-20200404.091302-14"
```
@ -124,6 +270,8 @@ when called repeatedly (e.g. with hot code reload workflow)
## 0.5.1 (2020-05-18)
**[compare](https://github.com/metosin/reitit/compare/0.5.0...0.5.1)**
```clj
[metosin/sieppari "0.0.0-alpha13"] is available but we use "0.0.0-alpha10"
```

View file

@ -1,4 +1,4 @@
# reitit [![Build Status](https://img.shields.io/circleci/project/github/metosin/reitit.svg)](https://circleci.com/gh/metosin/reitit) [![cljdoc badge](https://cljdoc.org/badge/metosin/reitit)](https://cljdoc.org/jump/release/metosin/reitit) [![Slack](https://img.shields.io/badge/clojurians-reitit-blue.svg?logo=slack)](https://clojurians.slack.com/messages/reitit/)
# reitit [![Build Status](https://github.com/metosin/reitit/workflows/testsuite/badge.svg)](https://github.com/metosin/reitit/actions?query=workflow%3Atestsuite) [![cljdoc badge](https://cljdoc.org/badge/metosin/reitit)](https://cljdoc.org/jump/release/metosin/reitit) [![Slack](https://img.shields.io/badge/clojurians-reitit-blue.svg?logo=slack)](https://clojurians.slack.com/messages/reitit/)
A fast data-driven router for Clojure(Script).
@ -20,6 +20,8 @@ Presentations:
* [Data-Driven Ring with Reitit](https://www.metosin.fi/blog/reitit-ring/)
* [Reitit, Data-Driven Routing with Clojure(Script)](https://www.metosin.fi/blog/reitit/)
**Status:** [stable](https://github.com/metosin/open-source#project-lifecycle-model)
## [Full Documentation](https://cljdoc.org/d/metosin/reitit/CURRENT)
There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians Slack](http://clojurians.net/) for discussion & help.
@ -50,7 +52,7 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
All main modules bundled:
```clj
[metosin/reitit "0.5.10"]
[metosin/reitit "0.5.18"]
```
Optionally, the parts can be required separately.
@ -85,23 +87,28 @@ Optionally, the parts can be required separately.
A Ring routing app with input & output coercion using [data-specs](https://github.com/metosin/spec-tools/blob/master/README.md#data-specs).
```clj
(require '[muuntaja.core :as m])
(require '[reitit.ring :as ring])
(require '[reitit.coercion.spec])
(require '[reitit.ring.coercion :as rrc])
(require '[reitit.ring.middleware.muuntaja :as muuntaja])
(require '[reitit.ring.middleware.parameters :as parameters])
(def app
(ring/ring-handler
(ring/router
["/api"
["/math" {:get {:parameters {:query {:x int?, :y int?}}
:responses {200 {:body {:total pos-int?}}}
:handler (fn [{{{:keys [x y]} :query} :parameters}]
{:status 200
:body {:total (+ x y)}})}}]]
;; router data effecting all routes
{:data {:coercion reitit.coercion.spec/coercion
:middleware [rrc/coerce-exceptions-middleware
:responses {200 {:body {:total int?}}}
:handler (fn [{{{:keys [x y]} :query} :parameters}]
{:status 200
:body {:total (+ x y)}})}}]]
;; router data affecting all routes
{:data {:coercion reitit.coercion.spec/coercion
:muuntaja m/instance
:middleware [parameters/parameters-middleware
rrc/coerce-request-middleware
muuntaja/format-response-middleware
rrc/coerce-response-middleware]}})))
```
@ -146,12 +153,17 @@ All examples are in https://github.com/metosin/reitit/tree/master/examples
## External resources
* Simple web application using Ring/Reitit and Integrant: https://github.com/PrestanceDesign/usermanager-reitit-integrant-example
* A simple [ClojureScript](https://clojurescript.org/) frontend and Clojure backend using Reitit, [JUXT Clip](https://github.com/juxt/clip), [next.jdbc](https://github.com/seancorfield/next-jdbc) and other bits and bobs...
* [startrek](https://git.sr.ht/~dharrigan/startrek)
* [startrek-ui](https://git.sr.ht/~dharrigan/startrek-ui)
* A simple Clojure backend using Reitit to serve up a RESTful API: [startrek](https://github.com/dharrigan/startrek). Technologies include:
* [Donut System](https://github.com/donut-party/system)
* [next-jdbc](https://github.com/seancorfield/next-jdbc)
* [JUXT Clip](https://github.com/juxt/clip)
* [Flyway](https://github.com/flyway/flyway)
* [HoneySQL](https://github.com/seancorfield/honeysql)
* [Babashka](https://babashka.org)
* https://www.learnreitit.com/
* Lipas, liikuntapalvelut: https://github.com/lipas-liikuntapaikat/lipas
* Implementation of the Todo-Backend API spec, using Clojure, Ring/Reitit and next-jdbc: https://github.com/PrestanceDesign/todo-backend-clojure-reitit
* Ping CRM, a single page app written in Clojure Ring, Reitit, Integrant and next.jdbc: https://github.com/prestancedesign/clojure-inertia-pingcrm-demo
## More info
@ -172,6 +184,6 @@ Roadmap is mostly written in [issues](https://github.com/metosin/reitit/issues).
## License
Copyright © 2017-2020 [Metosin Oy](http://www.metosin.fi)
Copyright © 2017-2021 [Metosin Oy](http://www.metosin.fi)
Distributed under the Eclipse Public License, the same as Clojure.

View file

@ -0,0 +1 @@
hello

View file

@ -6,7 +6,7 @@
* Route [conflict resolution](./basics/route_conflicts.md)
* First-class [route data](./basics/route_data.md)
* Bi-directional routing
* [Pluggable coercion](./coercion/coercion.md) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec))
* [Pluggable coercion](./coercion/coercion.md) ([schema](https://github.com/plumatic/schema), [clojure.spec](https://clojure.org/about/spec), [malli](https://github.com/metosin/malli))
* Helpers for [ring](./ring/ring.md), [http](./http/interceptors.md), [pedestal](./http/pedestal.md) & [frontend](./frontend/basics.md)
* Friendly [Error Messages](./basics/error_messages.md)
* Extendable
@ -40,7 +40,7 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
All bundled:
```clj
[metosin/reitit "0.5.10"]
[metosin/reitit "0.5.18"]
```
Optionally, the parts can be required separately.
@ -139,7 +139,7 @@ Routing:
```clj
(app {:request-method :get, :uri "/api/admin/users"})
; {:status 200, :body "ok", :wrap (:api :admin}
; {:status 200, :body "ok", :wrap (:api :admin)}
(app {:request-method :put, :uri "/api/admin/users"})
; nil

View file

@ -143,10 +143,10 @@ As the `Match` contains all the route data, we can create a new matching functio
(require '[clojure.string :as str])
(defn recursive-match-by-path [router path]
(if-let [match (r/match-by-path router path)]
(when-let [match (r/match-by-path router path)]
(if-let [subrouter (-> match :data :router)]
(let [subpath (subs path (str/last-index-of (:template match) "/"))]
(if-let [submatch (recursive-match-by-path subrouter subpath)]
(when-let [submatch (recursive-match-by-path subrouter subpath)]
(cons match submatch)))
(list match))))
```
@ -206,10 +206,10 @@ First, we need to modify our matching function to support router references:
(deref x) x))
(defn recursive-match-by-path [router path]
(if-let [match (r/match-by-path (<< router) path)]
(when-let [match (r/match-by-path (<< router) path)]
(if-let [subrouter (-> match :data :router <<)]
(let [subpath (subs path (str/last-index-of (:template match) "/"))]
(if-let [submatch (recursive-match-by-path subrouter subpath)]
(when-let [submatch (recursive-match-by-path subrouter subpath)]
(cons match submatch)))
(list match))))
```

View file

@ -4,15 +4,16 @@ Routers can be configured via options. The following options are available for t
| key | description
|--------------|-------------
| `:path` | Base-path for routes
| `:routes` | Initial resolved routes (default `[]`)
| `:data` | Initial route data (default `{}`)
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this
| `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon})
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
| `:compile` | Function of `route opts => result` to compile a route handler
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes
| `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`)
| `:router` | Function of `routes opts => router` to override the actual router implementation
| `:path` | Base-path for routes
| `:routes` | Initial resolved routes (default `[]`)
| `:data` | Initial route data (default `{}`)
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this
| `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon})
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
| `:meta-merge-fn` | Function which follows the signature of `meta-merge.core/meta-merge`, useful for when you want to have more control over the meta merging
| `:compile` | Function of `route opts => result` to compile a route handler
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes
| `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`)
| `:router` | Function of `routes opts => router` to override the actual router implementation

View file

@ -1,6 +1,6 @@
# Different Routers
Reitit ships with several different implementations for the `Router` protocol, originally based on the [Pedestal](https://github.com/pedestal/pedestal/tree/master/route) implementation. `router` function selects the most suitable implementation by inspecting the expanded routes. The implementation can be set manually using `:router` option, see [configuring routers](advanced/configuring_routers.md).
Reitit ships with several different implementations for the `Router` protocol, originally based on the [Pedestal](https://github.com/pedestal/pedestal/tree/master/route) implementation. `router` function selects the most suitable implementation by inspecting the expanded routes. The implementation can be set manually using `:router` option, see [configuring routers](configuring_routers.md).
| router | description |
| ------------------------------|-------------|

View file

@ -22,7 +22,7 @@ The default exception formatting uses `reitit.exception/exception`. It produces
## Pretty Errors
```clj
[metosin/reitit-dev "0.5.10"]
[metosin/reitit-dev "0.5.18"]
```
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.
@ -51,4 +51,4 @@ See the [validating route data](route_data_validation.md) page.
## Runtime Exception
See [Exception Handling with Ring](exceptions.md).
See [Exception Handling with Ring](../ring/exceptions.md).

View file

@ -79,7 +79,7 @@ Resolved route tree:
; :name :user/ping}]
; ["/api/admin/users" {:interceptors [::api]
; :roles #{:admin}
; :name ::users} nil]
; :name ::users}]
; ["/api/admin/db" {:interceptors [::api ::db]
; :roles #{:db-admin}}]]
```

View file

@ -132,7 +132,7 @@ Routes are just data, so it's easy to create them programmatically:
### Explicit path-parameter syntax
Router options `:syntax` allows the path-parameter syntax to be explicitely defined. It takes a keyword or set of keywords as a value. Valid values are `:colon` and `:bracket`. Default value is `#{:colon :bracket}`.
Router options `:syntax` allows the path-parameter syntax to be explicitly defined. It takes a keyword or set of keywords as a value. Valid values are `:colon` and `:bracket`. Default value is `#{:colon :bracket}`.
With defaults:

View file

@ -63,14 +63,6 @@ Route names:
; [:user/ping :user/user]
```
The compiled route tree:
```clj
(r/routes router)
; [["/api/ping" {:name :user/ping} nil]
; ["/api/user/:id" {:name :user/user} nil]]
```
### Composing
As routes are defined as plain data, it's easy to merge multiple route trees into a single router
@ -85,9 +77,10 @@ As routes are defined as plain data, it's easy to merge multiple route trees int
["/ping" ::ping]
["/db" ::db]])
(r/router
[admin-routes
user-routes])
(def router
(r/router
[admin-routes
user-routes]))
```
Merged route tree:
@ -109,6 +102,6 @@ When router is created, the following steps are done:
* route arguments are expanded (via `:expand` option)
* routes are coerced (via `:coerce` options)
* route tree is compiled (via `:compile` options)
* [route conflicts](advanced/route_conflicts.md) are resolved (via `:conflicts` options)
* [route conflicts](route_conflicts.md) are resolved (via `:conflicts` options)
* optionally, route data is validated (via `:validate` options)
* [router implementation](../advanced/different_routers.md) is automatically selected (or forced via `:router` options) and created

View file

@ -2,6 +2,10 @@
[Malli](https://github.com/metosin/malli) is data-driven Schema library for Clojure/Script.
## Default Syntax
By default, [Vector Syntax](https://github.com/metosin/malli#vector-syntax) is used:
```clj
(require '[reitit.coercion.malli])
(require '[reitit.coercion :as coercion])
@ -13,7 +17,7 @@
:coercion reitit.coercion.malli/coercion
:parameters {:path [:map
[:company string?]
[:user-id int?]]}]
[:user-id int?]]}}]
{:compile coercion/compile-request-coercers}))
(defn match-by-path-and-coerce! [path]
@ -44,18 +48,36 @@ Failing coercion:
; => ExceptionInfo Request coercion failed...
```
## Lite Syntax
Same using [Lite Syntax](https://github.com/metosin/malli#lite):
```clj
(def router
(r/router
["/:company/users/:user-id" {:name ::user-view
:coercion reitit.coercion.malli/coercion
:parameters {:path {:company string?
:user-id int?}}}]
{:compile coercion/compile-request-coercers}))
```
## Configuring coercion
Using `create` with options to create the coercion instead of `coercion`:
```clj
(require '[malli.util :as mu])
(reitit.coercion.malli/create
{:transformers {:body {:default default-transformer-provider
:formats {"application/json" json-transformer-provider}}
:string {:default string-transformer-provider}
:response {:default default-transformer-provider}}
{:transformers {:body {:default reitit.coercion.malli/default-transformer-provider
:formats {"application/json" reitit.coercion.malli/json-transformer-provider}}
:string {:default reitit.coercion.malli/string-transformer-provider}
:response {:default reitit.coercion.malli/default-transformer-provider}}
;; set of keys to include in error messages
:error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed}
;; support lite syntax?
:lite true
;; schema identity function (default: close all map schemas)
:compile mu/closed-schema
;; validate request & response

View file

@ -13,25 +13,48 @@
./scripts/test.sh cljs
```
## Formatting
```bash
clojure-lsp format
clojure-lsp clean-ns
```
## Documentation
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)
## To bump up version:
## Making a release
We use [Break Versioning][breakver]. Remember our promise: patch-level bumps never include breaking changes!
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
```bash
# Check that you're using Java 8! Making the release with a newer Java version
# means that it is broken when used with Java 8.
java -version
# new version
./scripts/set-version "1.0.0"
./scripts/lein-modules install
# create a release commit and a tag
git add -u
git commit -m "Release 1.0.0"
git tag 1.0.0
# works
./scripts/lein-modules install
lein test
# deploy to clojars
./scripts/lein-modules do clean, deploy clojars
# push the commit and the tag
git push
git push --tags
```
* Remembor to update the changelog!
* Announce the release at least on #reitit in Clojurians.

View file

@ -1,7 +1,7 @@
# Default Interceptors
```clj
[metosin/reitit-interceptors "0.5.10"]
[metosin/reitit-interceptors "0.5.18"]
```
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.5.10"]
[metosin/reitit-http "0.5.18"]
```
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.5.10"]
[metosin/reitit-pedestal "0.5.18"]
```
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.5.10"]
; [metosin/reitit "0.5.10"]
; [metosin/reitit-pedestal "0.5.18"]
; [metosin/reitit "0.5.18"]
(require '[io.pedestal.http :as server])
(require '[reitit.pedestal :as pedestal])

View file

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

@ -76,7 +76,7 @@ The routing sample taken from [bide](https://github.com/funcool/bide) README:
(r/match-by-path routes "/workspace/1/1")))
```
Based on the [perf tests](https://github.com/metosin/reitit/tree/master/perf-test/clj/reitit/perf/bide_perf_test.clj), the first (static path) lookup is 300-500x faster and the second (wildcard path) lookup is 18-110x faster that the other tested routing libs (Ataraxy, Bidi, Compojure and Pedestal).
Based on the [perf tests](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/bide_perf_test.clj), the first (static path) lookup is 300-500x faster and the second (wildcard path) lookup is 18-110x faster that the other tested routing libs (Ataraxy, Bidi, Compojure and Pedestal).
But, the example is too simple for any real benchmark. Also, some of the libraries always match on the `:request-method` too and by doing so, do more work than just match by path. Compojure does most work also by invoking the handler.

View file

@ -8,9 +8,10 @@ We can do this with middleware in reitit like this:
```clj
(defn- hidden-method
[request]
(keyword
(or (get-in request [:form-params "_method"]) ;; look for "_method" field in :form-params
(get-in request [:multipart-params "_method"])))) ;; or in :multipart-params
(some-> (or (get-in request [:form-params "_method"]) ;; look for "_method" field in :form-params
(get-in request [:multipart-params "_method"])) ;; or in :multipart-params
clojure.string/lower-case
keyword))
(def wrap-hidden-method
{:name ::wrap-hidden-method

View file

@ -39,7 +39,7 @@ Responses are defined in route data under `:responses` key. It's value should be
Below is an example with [Plumatic Schema](https://github.com/plumatic/schema). It defines schemas for `:query`, `:body` and `:path` parameters and for http 200 response `:body`.
Handler can access the coerced parameters can be read under `:parameters` key in the request.
Handlers can access the coerced parameters via the `:parameters` key in the request.
```clj
(require '[reitit.coercion.schema])
@ -71,7 +71,7 @@ Defining a coercion for a route data doesn't do anything, as it's just data. We
### Full example
Here's an full example for applying coercion with Reitit, Ring and Schema:
Here is a full example for applying coercion with Reitit, Ring and Schema:
```clj
(require '[reitit.ring.coercion :as rrc])
@ -150,7 +150,7 @@ Invalid response:
## Pretty printing spec errors
Spec problems are exposed as-is into request & response coercion errors, enabling pretty-printers like [expound](https://github.com/bhb/expound) to be used:
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:
```clj
(require '[reitit.ring :as ring])
@ -216,7 +216,7 @@ Spec problems are exposed as-is into request & response coercion errors, enablin
### Optimizations
The coercion middleware are [compiled against a route](compiling_middleware.md). In the middleware compilation step the actual coercer implementations are constructed for the defined models. Also, the middleware doesn't mount itself if a route doesn't have `:coercion` and `:parameters` or `:responses` defined.
The coercion middlewares are [compiled against a route](compiling_middleware.md). In the middleware compilation step the actual coercer implementations are constructed for the defined models. Also, the middleware doesn't mount itself if a route doesn't have `:coercion` and `:parameters` or `:responses` defined.
We can query the compiled middleware chain for the routes:

View file

@ -1,12 +1,12 @@
# Compiling Middleware
The [dynamic extensions](dynamic_extensions.md) is a easy way to extend the system. To enable fast lookups into route data, we can compile them into any shape (records, functions etc.) we want, enabling fast access at request-time.
The [dynamic extensions](dynamic_extensions.md) are an easy way to extend the system. To enable fast lookup of route data, we can compile them into any shape (records, functions etc.), enabling fast access at request-time.
But, we can do much better. As we know the exact route that middleware/interceptor is linked to, we can pass the (compiled) route information into the middleware at creation-time. It can do local reasoning: extract and transform relevant data just for it and pass the optimized data into the actual request-handler via a closure - yielding much faster runtime processing. Middleware can also decide not to mount itself by returning `nil`. Why mount a `wrap-enforce-roles` middleware for a route if there are no roles required for it?
But, we can do much better. As we know the exact route that a middleware/interceptor is linked to, we can pass the (compiled) route information into the middleware at creation-time. It can do local reasoning: Extract and transform relevant data just for it and pass the optimized data into the actual request-handler via a closure - yielding much faster runtime processing. A middleware can also decide not to mount itself by returning `nil`. (E.g. Why mount a `wrap-enforce-roles` middleware for a route if there are no roles required for it?)
To enable this we use [middleware records](data_driven_middleware.md) `:compile` key instead of the normal `:wrap`. `:compile` expects a function of `route-data router-opts => ?IntoMiddleware`.
To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with `:compile`.
To demonstrate the two approaches, below is the response coercion middleware written as normal ring middleware function and as middleware record with `:compile`.
## Normal Middleware

View file

@ -1,8 +1,8 @@
# Content Negotiation
Wrapper for [Muuntaja](https://github.com/metosin/muuntaja) middleware for content-negotiation, request decoding and response encoding. Takes explicit configuration via `:muuntaja` key in route data. Emit's [swagger](swagger.md) `:produces` and `:consumes` definitions automatically based on the Muuntaja configuration.
Wrapper for [Muuntaja](https://github.com/metosin/muuntaja) middleware for content negotiation, request decoding and response encoding. Takes explicit configuration via `:muuntaja` key in route data. Emits [swagger](swagger.md) `:produces` and `:consumes` definitions automatically based on the Muuntaja configuration.
Negotiates a request body based on `Content-Type` header and response body based on `Accept`, `Accept-Charset` headers. Publishes the negotiation results as `:muuntaja/request` and `:muuntaja/response` keys into the request.
Negotiates a request body based on `Content-Type` header and response body based on `Accept` and `Accept-Charset` headers. Publishes the negotiation results as `:muuntaja/request` and `:muuntaja/response` keys into the request.
Decodes the request body into `:body-params` using the `:muuntaja/request` key in request if the `:body-params` doesn't already exist.
@ -87,7 +87,7 @@ Server: Jetty(9.2.21.v20170120)
## Changing default parameters
The current JSON formatter used by `reitit` already have the option to parse keys as `keyword` which is a sane default in Clojure. However, if you would like to parse all the `double` as `bigdecimal` you'd need to change an option of the [JSON formatter](https://github.com/metosin/jsonista)
The current JSON formatter used by `reitit` already has the option to parse keys as `keyword` which is a sane default in Clojure. However, if you would like to parse all the `double` as `bigdecimal` you'd need to change an option of the [JSON formatter](https://github.com/metosin/jsonista)
```clj
@ -102,7 +102,7 @@ The current JSON formatter used by `reitit` already have the option to parse key
Now you should change the `m/instance` installed in the router with the `new-muuntaja-instance`.
You can find more options for [JSON](https://cljdoc.org/d/metosin/jsonista/0.2.5/api/jsonista.core#object-mapper) and [EDN].
Here you can find more options for [JSON](https://cljdoc.org/d/metosin/jsonista/0.2.5/api/jsonista.core#object-mapper) and EDN.
## Adding custom encoder
@ -125,14 +125,14 @@ The example below is from `muuntaja` explaining how to add a custom encoder to p
```
## Adding all together
## Putting it all together
If you inspect `m/default-options` it's only a map, therefore you can compose your new muuntaja instance with as many options as you need it.
If you inspect `m/default-options` you'll find it's only a map. This means you can compose your new muuntaja instance with as many options as you need.
```clj
(def new-muuntaja
(m/create
(-> m/default-options
(assoc-in [:formats "application/json" :decoder-opts :bigdecimals] true)
(assoc-in [:formats "application/json" :encoder-opts :data-format] "yyyy-MM-dd"))))
(assoc-in [:formats "application/json" :encoder-opts :date-format] "yyyy-MM-dd"))))
```

View file

@ -1,19 +1,19 @@
# Data-driven Middleware
Ring [defines middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware) as a function of type `handler & args => request => response`. It's relatively easy to understand and enables good performance. Downside is that the middleware-chain is just a opaque function, making things like debugging and composition hard. It's too easy to apply the middleware 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.
Reitit defines middleware as data:
1. Middleware can be defined as first-class data entries
2. Middleware can be mounted as a [duct-style](https://github.com/duct-framework/duct/wiki/Configuration) vector (of middleware)
4. Middleware can be optimized & [compiled](compiling_middleware.md) against an endpoint
3. Middleware chain can be transformed by the router
1. A middleware can be defined as first-class data entries
2. A middleware can be mounted as a [duct-style](https://github.com/duct-framework/duct/wiki/Configuration) vector (of middlewares)
4. A middleware can be optimized & [compiled](compiling_middleware.md) against an endpoint
3. A middleware chain can be transformed by the router
## Middleware as data
All values in the `:middleware` vector in the route data are expanded into `reitit.middleware/Middleware` Records with using the `reitit.middleware/IntoMiddleware` Protocol. By default, functions, maps and `Middleware` records are allowed.
All values in the `:middleware` vector of route data are expanded into `reitit.middleware/Middleware` Records by using the `reitit.middleware/IntoMiddleware` Protocol. By default, functions, maps and `Middleware` records are allowed.
Records can have arbitrary keys, but the following keys have a special purpose:
Records can have arbitrary keys, but the following keys have special purpose:
| key | description |
| ---------------|-------------|
@ -22,13 +22,13 @@ Records can have arbitrary keys, but the following keys have a special purpose:
| `:wrap` | The actual middleware function of `handler & args => request => response`
| `:compile` | Middleware compilation function, see [compiling middleware](compiling_middleware.md).
Middleware Records are accessible in their raw form in the compiled route results, thus available for inventories, creating api-docs etc.
Middleware Records are accessible in their raw form in the compiled route results, and thus are available for inventories, creating api-docs, etc.
For the actual request processing, the Records are unwrapped into normal functions and composed into a middleware function chain, yielding zero runtime penalty.
### Creating Middleware
The following produce identical middleware runtime function.
The following examples produce identical middleware runtime functions.
### Function
@ -77,7 +77,7 @@ The following produce identical middleware runtime function.
:handler handler}}]])))
```
All the middleware are applied correctly:
All the middlewares are applied correctly:
```clj
(app {:request-method :get, :uri "/api/ping"})
@ -86,7 +86,7 @@ All the middleware are applied correctly:
## Compiling middleware
Middleware can be optimized against an endpoint using [middleware compilation](compiling_middleware.md).
Middlewares can be optimized against an endpoint using [middleware compilation](compiling_middleware.md).
## Ideas for the future

View file

@ -1,6 +1,6 @@
# Default handler
By default, if no routes match, `nil` is returned, which is not valid response in Ring:
By default, if no routes match, `nil` is returned, which is not a valid response in Ring:
```clj
(require '[reitit.ring :as ring])

View file

@ -1,10 +1,10 @@
# Default Middleware
```clj
[metosin/reitit-middleware "0.5.10"]
[metosin/reitit-middleware "0.5.18"]
```
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.
* [Parameter Handling](#parameters-handling)
* [Exception Handling](#exception-handling)
@ -17,7 +17,7 @@ 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
`ring.middleware.params/wrap-params`.
**NOTE**: 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.
**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

View file

@ -1,8 +1,8 @@
# Dynamic Extensions
`ring-handler` injects the `Match` into a request and it can be extracted at runtime with `reitit.ring/get-match`. This can be used to build ad-hoc extensions to the system.
`ring-handler` injects the `Match` into a request and it can be extracted at runtime with `reitit.ring/get-match`. This can be used to build ad hoc extensions to the system.
Example middleware to guard routes based on user roles:
This example shows a middleware to guard routes based on user roles:
```clj
(require '[reitit.ring :as ring])

View file

@ -1,10 +1,10 @@
# Exception Handling with Ring
```clj
[metosin/reitit-middleware "0.5.10"]
[metosin/reitit-middleware "0.5.18"]
```
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 practise is a have an top-level exception handler to log and format the 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.
```clj
(require '[reitit.ring.middleware.exception :as exception])
@ -36,7 +36,7 @@ A preconfigured middleware using `exception/default-handlers`. Catches:
### `exception/create-exception-middleware`
Creates the exception-middleware with custom options. Takes a map of `identifier => exception request => response` that is used to select the exception handler for the thrown/raised exception identifier. Exception identifier is either a `Keyword` or a Exception Class.
Creates the exception-middleware with custom options. Takes a map of `identifier => exception request => response` that is used to select the exception handler for the thrown/raised exception identifier. Exception identifier is either a `Keyword` or an Exception Class.
The following handlers are available by default:
@ -55,7 +55,7 @@ The handler is selected from the options map by exception identifier in the foll
2) Class of exception
3) `:type` ancestors of exception ex-data
4) Super Classes of exception
5) The ::default handler
5) The `::default` handler
```clj
;; type hierarchy
@ -94,7 +94,7 @@ The handler is selected from the options map by exception identifier in the foll
(def app
(ring/ring-handler
(ring/router
["/fail" (fn [_] (throw (ex-info "fail" {:type ::failue})))]
["/fail" (fn [_] (throw (ex-info "fail" {:type ::failure})))]
{:data {:middleware [exception-middleware]}})))
(app {:request-method :get, :uri "/fail"})
@ -102,6 +102,6 @@ The handler is selected from the options map by exception identifier in the foll
; => {:status 500,
; :body {:message "default"
; :exception clojure.lang.ExceptionInfo
; :data {:type :user/failue}
; :data {:type :user/failure}
; :uri "/fail"}}
```

View file

@ -5,14 +5,14 @@
Read more about the [Ring Concepts](https://github.com/ring-clojure/ring/wiki/Concepts).
```clj
[metosin/reitit-ring "0.5.10"]
[metosin/reitit-ring "0.5.18"]
```
## `reitit.ring/ring-router`
`ring-router` is a higher order router, which adds support for `:request-method` based routing, [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers) and [middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware).
It accepts the following options:
It accepts the following options:
| key | description |
| ----------------------------------------|-------------|
@ -53,7 +53,7 @@ Given a `ring-router`, optional default-handler & options, `ring-handler` functi
| key | description |
| ------------------|-------------|
| `:middleware` | Optional sequence of middleware that wrap the ring-handler"
| `:middleware` | Optional sequence of middlewares that wrap the ring-handler
| `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true)
| `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)

View file

@ -1,8 +1,12 @@
# Static Resources (Clojure Only)
Static resources can be served using `reitit.ring/create-resource-handler`. It takes optionally an options map and returns a ring handler to serve files from Classpath.
Static resources can be served by using the following two functions:
There are two options to serve the files.
* `reitit.ring/create-resource-handler`, which returns a Ring handler that serves files from classpath, and
* `reitit.ring/create-file-handler`, which returns a Ring handler that servers files from file system
There are two ways to mount the handlers.
The examples below use `reitit.ring/create-resource-handler`, but `reitit.ring/create-file-handler` works the same way.
## Internal routes
@ -33,7 +37,9 @@ To serve static files with conflicting routes, e.g. `"/*"`, one needs to disable
## External routes
A better way to serve files from conflicting paths, e.g. `"/*"`, is to serve them from the default-handler. One can compose multiple default locations using `ring-handler`. This way, they are only served if none of the actual routes have matched.
A better way to serve files from conflicting paths, e.g. `"/*"`, is to serve them from the default-handler.
One can compose multiple default locations using `reitit.ring/ring-handler`.
This way, they are only served if none of the actual routes have matched.
```clj
(ring/ring-handler
@ -46,21 +52,19 @@ A better way to serve files from conflicting paths, e.g. `"/*"`, is to serve the
## Configuration
`reitit.ring/create-resource-handler` takes optionally an options map to configure how the files are being served.
`reitit.ring/create-file-handler` and `reitit.ring/create-resource-handler` take optionally an options map to configure how the files are being served.
| key | description |
| -----------------|-------------|
| key | description |
| -------------------|-------------|
| :parameter | optional name of the wildcard parameter, defaults to unnamed keyword `:`
| :root | optional resource root, defaults to `\"public\"`
| :path | optional path to mount the handler to. Works only if mounted outside of a router.
| :path | path to mount the handler to. Required when mounted outside of a router, does not work inside a router.
| :loader | optional class loader to resolve the resources
| :index-files | optional vector of index-files to look in a resource directory, defaults to `[\"index.html\"]`
| :not-found-handler | optional handler function to use if the requested resource is missing (404 Not Found)
### TODO
* support for things like `:cache`, `:etag`, `:last-modified?`, and `:gzip`
* support for ClojureScript
* serve from file-system

View file

@ -1,7 +1,7 @@
# Swagger Support
```
[metosin/reitit-swagger "0.5.10"]
[metosin/reitit-swagger "0.5.18"]
```
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.
@ -45,10 +45,10 @@ 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.5.10"]
[metosin/reitit-swagger-ui "0.5.18"]
```
`reitit.swagger-ui/create-swagger-ui-hander` 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:
| key | description |
| -----------------|-------------|

View file

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

50
examples/README.md Normal file
View file

@ -0,0 +1,50 @@
# Examples
## buddy-auth
## frontend-auth
## frontend-controllers
## frontend-links
## frontend-prompt
## frontend-re-frame
## frontend
## http-swagger
Coercion with Spec and Swagger generation.
Same as ring-spec-swagger?
Async examples as extra.
## http
Async example.
## just-coercion-with-ring
Bad name.
Coercion example for spec, data-spec, Schema.
No Swagger generation or Malli!
Same as ring-example?
## pedestal-swagger
## pedestal
## ring-example
Coercion example for spec, data-spec, Schema.
No Swagger generation or Malli!
## ring-integrant
## ring-malli-swagger
Coercion with Malli and Swagger generation.
## ring-spec-swagger
Coercion with Spec and Swagger generation.
## ring-swagger
Coercion with Spec and Swagger generation. Same as previous!

View file

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

View file

@ -10,9 +10,9 @@
[ring "1.7.1"]
[hiccup "1.0.5"]
[org.clojure/clojurescript "1.10.439"]
[metosin/reitit "0.5.10"]
[metosin/reitit-schema "0.5.10"]
[metosin/reitit-frontend "0.5.10"]
[metosin/reitit "0.5.18"]
[metosin/reitit-schema "0.5.18"]
[metosin/reitit-frontend "0.5.18"]
;; Just for pretty printting the match
[fipp "0.6.14"]]

View file

@ -10,9 +10,9 @@
[ring "1.7.1"]
[hiccup "1.0.5"]
[org.clojure/clojurescript "1.10.439"]
[metosin/reitit "0.5.10"]
[metosin/reitit-schema "0.5.10"]
[metosin/reitit-frontend "0.5.10"]
[metosin/reitit "0.5.18"]
[metosin/reitit-schema "0.5.18"]
[metosin/reitit-frontend "0.5.18"]
;; Just for pretty printting the match
[fipp "0.6.14"]]

View file

@ -10,9 +10,9 @@
[ring "1.7.1"]
[hiccup "1.0.5"]
[org.clojure/clojurescript "1.10.520"]
[metosin/reitit "0.5.10"]
[metosin/reitit-spec "0.5.10"]
[metosin/reitit-frontend "0.5.10"]
[metosin/reitit "0.5.18"]
[metosin/reitit-spec "0.5.18"]
[metosin/reitit-frontend "0.5.18"]
;; Just for pretty printting the match
[fipp "0.6.14"]]

View file

@ -10,9 +10,9 @@
[ring "1.7.1"]
[hiccup "1.0.5"]
[org.clojure/clojurescript "1.10.520"]
[metosin/reitit "0.5.10"]
[metosin/reitit-spec "0.5.10"]
[metosin/reitit-frontend "0.5.10"]
[metosin/reitit "0.5.18"]
[metosin/reitit-spec "0.5.18"]
[metosin/reitit-frontend "0.5.18"]
;; Just for pretty printting the match
[fipp "0.6.14"]]

View file

@ -1,7 +1,7 @@
(defproject frontend-re-frame "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.10.0"]
[org.clojure/clojurescript "1.10.520"]
[metosin/reitit "0.5.10"]
[metosin/reitit "0.5.18"]
[reagent "0.8.1"]
[re-frame "0.10.6"]]

View file

@ -7,6 +7,14 @@
[reitit.frontend.controllers :as rfc]
[reitit.frontend.easy :as rfe]))
;;; Effects ;;;
;; Triggering navigation from events.
(re-frame/reg-fx :push-state
(fn [route]
(apply rfe/push-state route)))
;;; Events ;;;
(re-frame/reg-event-db ::initialize-db
@ -16,7 +24,7 @@
{:current-route nil})))
(re-frame/reg-event-fx ::push-state
(fn [db [_ & route]]
(fn [_ [_ & route]]
{:push-state route}))
(re-frame/reg-event-db ::navigated
@ -49,14 +57,6 @@
[:div
[:h1 "This is sub-page 2"]])
;;; Effects ;;;
;; Triggering navigation from events.
(re-frame/reg-fx :push-state
(fn [route]
(apply rfe/push-state route)))
;;; Routes ;;;
(defn href

View file

@ -10,9 +10,9 @@
[ring "1.8.1"]
[hiccup "1.0.5"]
[org.clojure/clojurescript "1.10.773"]
[metosin/reitit "0.5.10"]
[metosin/reitit-spec "0.5.10"]
[metosin/reitit-frontend "0.5.10"]
[metosin/reitit "0.5.18"]
[metosin/reitit-spec "0.5.18"]
[metosin/reitit-frontend "0.5.18"]
;; Just for pretty printting the match
[fipp "0.6.23"]]

View file

@ -3,5 +3,5 @@
:dependencies [[org.clojure/clojure "1.10.0"]
[ring/ring-jetty-adapter "1.7.1"]
[aleph "0.4.7-alpha5"]
[metosin/reitit "0.5.10"]]
[metosin/reitit "0.5.18"]]
:repl-options {:init-ns example.server})

View file

@ -5,5 +5,5 @@
[funcool/promesa "1.9.0"]
[manifold "0.1.8"]
[ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.5.10"]]
[metosin/reitit "0.5.18"]]
:repl-options {:init-ns example.server})

View file

@ -2,4 +2,4 @@
:description "Reitit coercion with vanilla ring"
:dependencies [[org.clojure/clojure "1.10.0"]
[ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.5.10"]])
[metosin/reitit "0.5.18"]])

View file

@ -0,0 +1,11 @@
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.hgignore
.hg/

View file

@ -0,0 +1,9 @@
(defproject pedestal-malli-swagger-example "0.1.0-SNAPSHOT"
:description "Reitit-http with pedestal"
:dependencies [[org.clojure/clojure "1.10.0"]
[io.pedestal/pedestal.service "0.5.5"]
[io.pedestal/pedestal.jetty "0.5.5"]
[metosin/reitit-malli "0.5.18"]
[metosin/reitit-pedestal "0.5.18"]
[metosin/reitit "0.5.18"]]
:repl-options {:init-ns server})

View file

@ -0,0 +1,164 @@
(ns example.server
(:require [clojure.java.io :as io]
[io.pedestal.http.route]
[reitit.interceptor]
[reitit.dev.pretty :as pretty]
[reitit.coercion.malli]
[io.pedestal.http]
[reitit.ring]
[reitit.ring.malli]
[reitit.http]
[reitit.pedestal]
[reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui]
[reitit.http.coercion :as coercion]
[reitit.http.interceptors.parameters :as parameters]
[reitit.http.interceptors.muuntaja :as muuntaja]
[reitit.http.interceptors.multipart :as multipart]
[muuntaja.core]
[malli.util :as mu]))
(defn reitit-routes
[_config]
[["/swagger.json" {:get {:no-doc true
:swagger {:info {:title "my-api"
:description "with [malli](https://github.com/metosin/malli) and reitit-ring"}
:tags [{:name "files",
:description "file api"}
{:name "math",
:description "math api"}]}
:handler (swagger/create-swagger-handler)}}]
["/files" {:swagger {:tags ["files"]}}
["/upload"
{:post {:summary "upload a file"
:parameters {:multipart [:map [:file reitit.ring.malli/temp-file-part]]}
:responses {200 {:body [:map
[:name string?]
[:size int?]]}}
:handler (fn [{{{{:keys [filename
size]} :file}
:multipart}
:parameters}]
{:status 200
:body {:name filename
:size size}})}}]
["/download" {:get {:summary "downloads a file"
:swagger {:produces ["image/png"]}
:handler (fn [_]
{:status 200
:headers {"Content-Type" "image/png"}
:body (-> "reitit.png"
(io/resource)
(io/input-stream))})}}]]
["/math" {:swagger {:tags ["math"]}}
["/plus"
{:get {:summary "plus with malli query parameters"
:parameters {:query [:map
[:x
{:title "X parameter"
:description "Description for X parameter"
:json-schema/default 42}
int?]
[:y int?]]}
:responses {200 {:body [:map [:total int?]]}}
:handler (fn [{{{:keys [x
y]}
:query}
:parameters}]
{:status 200
:body {:total (+ x y)}})}
:post {:summary "plus with malli body parameters"
:parameters {:body [:map
[:x
{:title "X parameter"
:description "Description for X parameter"
:json-schema/default 42}
int?]
[:y int?]]}
:responses {200 {:body [:map [:total int?]]}}
:handler (fn [{{{:keys [x
y]}
:body}
:parameters}]
{:status 200
:body {:total (+ x y)}})}}]]])
(defn reitit-ring-routes
[_config]
[(swagger-ui/create-swagger-ui-handler
{:path "/"
:config {:validatorUrl nil
:operationsSorter "alpha"}})
(reitit.ring/create-resource-handler)
(reitit.ring/create-default-handler)])
(defn reitit-router-config
[_config]
{:exception pretty/exception
:data {:coercion (reitit.coercion.malli/create
{:error-keys #{:coercion
:in
:schema
:value
:errors
:humanized}
:compile mu/closed-schema
:strip-extra-keys true
:default-values true
:options nil})
:muuntaja muuntaja.core/instance
:interceptors [swagger/swagger-feature
(parameters/parameters-interceptor)
(muuntaja/format-negotiate-interceptor)
(muuntaja/format-response-interceptor)
(muuntaja/format-request-interceptor)
(coercion/coerce-response-interceptor)
(coercion/coerce-request-interceptor)
(multipart/multipart-interceptor)]}})
(def config
{:env :dev
:io.pedestal.http/routes []
:io.pedestal.http/type :jetty
:io.pedestal.http/port 3000
:io.pedestal.http/join? false
:io.pedestal.http/secure-headers {:content-security-policy-settings
{:default-src "'self'"
:style-src "'self' 'unsafe-inline'"
:script-src "'self' 'unsafe-inline'"}}
::reitit-routes reitit-routes
::reitit-ring-routes reitit-ring-routes
::reitit-router-config reitit-router-config})
(defn reitit-http-router
[{::keys [reitit-routes
reitit-ring-routes
reitit-router-config]
:as config}]
(reitit.pedestal/routing-interceptor
(reitit.http/router
(reitit-routes config)
(reitit-router-config config))
(->> config
reitit-ring-routes
(apply reitit.ring/routes))))
(defonce server (atom nil))
(defn start
[server
config]
(when @server
(io.pedestal.http/stop @server)
(println "server stopped"))
(-> config
io.pedestal.http/default-interceptors
(reitit.pedestal/replace-last-interceptor (reitit-http-router config))
io.pedestal.http/dev-interceptors
io.pedestal.http/create-server
io.pedestal.http/start
(->> (reset! server)))
(println "server running in port 3000"))
#_(start server config)

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.10.0"]
[io.pedestal/pedestal.service "0.5.5"]
[io.pedestal/pedestal.jetty "0.5.5"]
[metosin/reitit-pedestal "0.5.10"]
[metosin/reitit "0.5.10"]]
[metosin/reitit-pedestal "0.5.18"]
[metosin/reitit "0.5.18"]]
:repl-options {:init-ns example.server})

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.10.0"]
[io.pedestal/pedestal.service "0.5.5"]
[io.pedestal/pedestal.jetty "0.5.5"]
[metosin/reitit-pedestal "0.5.10"]
[metosin/reitit "0.5.10"]]
[metosin/reitit-pedestal "0.5.18"]
[metosin/reitit "0.5.18"]]
:repl-options {:init-ns example.server})

View file

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

View file

@ -2,7 +2,7 @@
:description "Reitit Ring App with Integrant"
:dependencies [[org.clojure/clojure "1.10.1"]
[ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.5.10"]
[metosin/reitit "0.5.18"]
[integrant "0.7.0"]]
:main example.server
:repl-options {:init-ns user}

View file

@ -0,0 +1,11 @@
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.hgignore
.hg/

View file

@ -0,0 +1,23 @@
# reitit-ring, malli, swagger
## Usage
```clj
> lein repl
(start)
```
To test the endpoints using [httpie](https://httpie.org/):
```bash
http GET :3000/math/plus x==1 y==20
http POST :3000/math/plus x:=1 y:=20
http GET :3000/swagger.json
```
<img src="https://raw.githubusercontent.com/metosin/reitit/master/examples/ring-spec-swagger/swagger.png" />
## License
Copyright © 2017-2019 Metosin Oy

View file

@ -0,0 +1,8 @@
(defproject ring-example "0.1.0-SNAPSHOT"
:description "Reitit Ring App with Swagger"
:dependencies [[org.clojure/clojure "1.10.0"]
[metosin/jsonista "0.2.6"]
[ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.5.18"]]
:repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]]}})

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

View file

@ -0,0 +1,123 @@
(ns example.server
(:require [reitit.ring :as ring]
[reitit.coercion.malli]
[reitit.ring.malli]
[reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui]
[reitit.ring.coercion :as coercion]
[reitit.dev.pretty :as pretty]
[reitit.ring.middleware.muuntaja :as muuntaja]
[reitit.ring.middleware.exception :as exception]
[reitit.ring.middleware.multipart :as multipart]
[reitit.ring.middleware.parameters :as parameters]
; [reitit.ring.middleware.dev :as dev]
; [reitit.ring.spec :as spec]
; [spec-tools.spell :as spell]
[ring.adapter.jetty :as jetty]
[muuntaja.core :as m]
[clojure.java.io :as io]
[malli.util :as mu]))
(def app
(ring/ring-handler
(ring/router
[["/swagger.json"
{:get {:no-doc true
:swagger {:info {:title "my-api"
:description "with [malli](https://github.com/metosin/malli) and reitit-ring"}
:tags [{:name "files", :description "file api"}
{:name "math", :description "math api"}]}
:handler (swagger/create-swagger-handler)}}]
["/files"
{:swagger {:tags ["files"]}}
["/upload"
{:post {:summary "upload a file"
:parameters {:multipart {:file reitit.ring.malli/temp-file-part}}
:responses {200 {:body {:name :string, :size :int}}}
:handler (fn [{{{:keys [file]} :multipart} :parameters}]
{:status 200
:body {:name (:filename file)
:size (:size file)}})}}]
["/download"
{:get {:summary "downloads a file"
:swagger {:produces ["image/png"]}
:handler (fn [_]
{:status 200
:headers {"Content-Type" "image/png"}
:body (-> "reitit.png"
(io/resource)
(io/input-stream))})}}]]
["/math"
{:swagger {:tags ["math"]}}
["/plus"
{:get {:summary "plus with malli query parameters"
:parameters {:query {:x [:int {:title "X parameter"
:description "Description for X parameter"
:json-schema/default 42}]
:y :int}}
:responses {200 {:body {:total :int}}}
:handler (fn [{{{:keys [x y]} :query} :parameters}]
{:status 200
:body {:total (+ x y)}})}
:post {:summary "plus with malli body parameters"
:parameters {:body {:x [:int {:title "X parameter"
:description "Description for X parameter"
:json-schema/default 42}]
:y :int}}
:responses {200 {:body {:total :int}}}
:handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200
:body {:total (+ x y)}})}}]]]
{;;:reitit.middleware/transform dev/print-request-diffs ;; pretty diffs
;;:validate spec/validate ;; enable spec validation for route data
;;:reitit.spec/wrap spell/closed ;; strict top-level validation
:exception pretty/exception
:data {:coercion (reitit.coercion.malli/create
{;; set of keys to include in error messages
:error-keys #{#_:type :coercion :in :schema :value :errors :humanized #_:transformed}
;; schema identity function (default: close all map schemas)
:compile mu/closed-schema
;; strip-extra-keys (effects only predefined transformers)
:strip-extra-keys true
;; add/set default values
:default-values true
;; malli options
:options nil})
:muuntaja m/instance
:middleware [;; swagger feature
swagger/swagger-feature
;; query-params & form-params
parameters/parameters-middleware
;; content-negotiation
muuntaja/format-negotiate-middleware
;; encoding response body
muuntaja/format-response-middleware
;; exception handling
exception/exception-middleware
;; decoding request body
muuntaja/format-request-middleware
;; coercing response bodys
coercion/coerce-response-middleware
;; coercing request parameters
coercion/coerce-request-middleware
;; multipart
multipart/multipart-middleware]}})
(ring/routes
(swagger-ui/create-swagger-ui-handler
{:path "/"
:config {:validatorUrl nil
:operationsSorter "alpha"}})
(ring/create-default-handler))))
(defn start []
(jetty/run-jetty #'app {:port 3000, :join? false})
(println "server running in port 3000"))
(comment
(start))

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

View file

@ -0,0 +1,38 @@
(ns example.server-test
(:require [clojure.test :refer [deftest testing is]]
[example.server :refer [app]]
[ring.mock.request :refer [request json-body]]))
(deftest example-server
(testing "GET"
(is (= (-> (request :get "/math/plus?x=20&y=3")
app :body slurp)
(-> {:request-method :get :uri "/math/plus" :query-string "x=20&y=3"}
app :body slurp)
(-> {:request-method :get :uri "/math/plus" :query-params {:x 20 :y 3}}
app :body slurp)
"{\"total\":23}")))
(testing "POST"
(is (= (-> (request :post "/math/plus") (json-body {:x 40 :y 2})
app :body slurp)
(-> {:request-method :post :uri "/math/plus" :body-params {:x 40 :y 2}}
app :body slurp)
"{\"total\":42}")))
(testing "Download"
(is (= (-> {:request-method :get :uri "/files/download"}
app :body (#(slurp % :encoding "ascii")) count) ;; binary
(.length (clojure.java.io/file "resources/reitit.png"))
506325)))
(testing "Upload"
(let [file (clojure.java.io/file "resources/reitit.png")
multipart-temp-file-part {:tempfile file
:size (.length file)
:filename (.getName file)
:content-type "image/png;"}]
(is (= (-> {:request-method :post :uri "/files/upload" :multipart-params {:file multipart-temp-file-part}}
app :body slurp)
"{\"name\":\"reitit.png\",\"size\":506325}")))))

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.10.0"]
[metosin/jsonista "0.2.6"]
[ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.5.10"]]
[metosin/reitit "0.5.18"]]
:repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]]}})

View file

@ -56,13 +56,25 @@
["/plus"
{:get {:summary "plus with malli query parameters"
:parameters {:query [:map [:x int?] [:y int?]]}
:parameters {:query [:map
[:x
{:title "X parameter"
:description "Description for X parameter"
:json-schema/default 42}
int?]
[:y int?]]}
:responses {200 {:body [:map [:total int?]]}}
:handler (fn [{{{:keys [x y]} :query} :parameters}]
{:status 200
:body {:total (+ x y)}})}
:post {:summary "plus with malli body parameters"
:parameters {:body [:map [:x int?] [:y int?]]}
:parameters {:body [:map
[:x
{:title "X parameter"
:description "Description for X parameter"
:json-schema/default 42}
int?]
[:y int?]]}
:responses {200 {:body [:map [:total int?]]}}
:handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200

View file

@ -2,6 +2,6 @@
:description "Reitit Ring App with Swagger"
:dependencies [[org.clojure/clojure "1.10.0"]
[ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.5.10"]]
[metosin/reitit "0.5.18"]]
:repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]]}})

View file

@ -13,6 +13,7 @@
; [reitit.ring.middleware.dev :as dev]
; [reitit.ring.spec :as spec]
; [spec-tools.spell :as spell]
[spec-tools.core :as st]
[ring.adapter.jetty :as jetty]
[muuntaja.core :as m]
[clojure.spec.alpha :as s]
@ -25,7 +26,12 @@
(s/def ::size int?)
(s/def ::file-response (s/keys :req-un [::name ::size]))
(s/def ::x int?)
;; Use data-specs to provide extra JSON-Schema properties:
;; https://github.com/metosin/spec-tools/blob/master/docs/04_json_schema.md#annotated-specs
(s/def ::x (st/spec {:spec int?
:name "X parameter"
:description "Description for X parameter"
:json-schema/default 42}))
(s/def ::y int?)
(s/def ::total int?)
(s/def ::math-request (s/keys :req-un [::x ::y]))

View file

@ -2,5 +2,5 @@
:description "Reitit Ring App with Swagger"
:dependencies [[org.clojure/clojure "1.10.0"]
[ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.5.10"]]
[metosin/reitit "0.5.18"]]
:repl-options {:init-ns example.server})

View file

@ -11,8 +11,6 @@
[reitit.ring.middleware.parameters :as parameters]
;; Uncomment to use
; [reitit.ring.middleware.dev :as dev]
; [reitit.ring.spec :as spec]
; [spec-tools.spell :as spell]
[ring.adapter.jetty :as jetty]
[muuntaja.core :as m]
[clojure.java.io :as io]))
@ -53,13 +51,15 @@
["/plus"
{:get {:summary "plus with spec query parameters"
:parameters {:query {:x int?, :y int?}}
:parameters {:query {:x int?
:y int?}}
:responses {200 {:body {:total int?}}}
:handler (fn [{{{:keys [x y]} :query} :parameters}]
{:status 200
:body {:total (+ x y)}})}
:post {:summary "plus with spec body parameters"
:parameters {:body {:x int?, :y int?}}
:parameters {:body {:x int?
:y int?}}
:responses {200 {:body {:total int?}}}
:handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200
@ -80,7 +80,8 @@
;; encoding response body
muuntaja/format-response-middleware
;; exception handling
exception/exception-middleware
(exception/create-exception-middleware
{::exception/default (partial exception/wrap-log-to-console exception/default-handler)})
;; decoding request body
muuntaja/format-request-middleware
;; coercing response bodys

View file

@ -0,0 +1 @@
../../../.clj-kondo/module_config.edn

View file

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

View file

@ -41,36 +41,44 @@
:header (->ParameterCoercion :headers :string true true)
:path (->ParameterCoercion :path-params :string true true)})
(defn ^:no-doc request-coercion-failed! [result coercion value in request]
(defn ^:no-doc request-coercion-failed! [result coercion value in request serialize-failed-result]
(throw
(ex-info
(ex-info
(if serialize-failed-result
(str "Request coercion failed: " (pr-str result))
(merge
(into {} result)
{:type ::request-coercion
:coercion coercion
:value value
:in [:request in]
:request request}))))
"Request coercion failed")
(-> {}
transient
(as-> $ (reduce conj! $ result))
(assoc! :type ::request-coercion)
(assoc! :coercion coercion)
(assoc! :value value)
(assoc! :in [:request in])
(assoc! :request request)
persistent!))))
(defn ^:no-doc response-coercion-failed! [result coercion value request response]
(defn ^:no-doc response-coercion-failed! [result coercion value request response serialize-failed-result]
(throw
(ex-info
(ex-info
(if serialize-failed-result
(str "Response coercion failed: " (pr-str result))
(merge
(into {} result)
{:type ::response-coercion
:coercion coercion
:value value
:in [:response :body]
:request request
:response response}))))
"Response coercion failed")
(-> {}
transient
(as-> $ (reduce conj! $ result))
(assoc! :type ::response-coercion)
(assoc! :coercion coercion)
(assoc! :value value)
(assoc! :in [:response :body])
(assoc! :request request)
(assoc! :response response)
persistent!))))
(defn extract-request-format-default [request]
(-> request :muuntaja/request :format))
;; TODO: support faster key walking, walk/keywordize-keys is quite slow...
(defn request-coercer [coercion type model {::keys [extract-request-format parameter-coercion]
(defn request-coercer [coercion type model {::keys [extract-request-format parameter-coercion serialize-failed-result]
:or {extract-request-format extract-request-format-default
parameter-coercion default-parameter-coercion}}]
(if coercion
@ -83,13 +91,13 @@
format (extract-request-format request)
result (coercer value format)]
(if (error? result)
(request-coercion-failed! result coercion value in request)
(request-coercion-failed! result coercion value in request serialize-failed-result)
result))))))))
(defn extract-response-format-default [request _]
(-> request :muuntaja/response :format))
(defn response-coercer [coercion body {:keys [extract-response-format]
(defn response-coercer [coercion body {:keys [extract-response-format serialize-failed-result]
:or {extract-response-format extract-response-format-default}}]
(if coercion
(if-let [coercer (-response-coercer coercion body)]
@ -98,7 +106,7 @@
value (:body response)
result (coercer value format)]
(if (error? result)
(response-coercion-failed! result coercion value request response)
(response-coercion-failed! result coercion value request response serialize-failed-result)
result))))))
(defn encode-error [data]
@ -109,9 +117,9 @@
(defn coerce-request [coercers request]
(reduce-kv
(fn [acc k coercer]
(impl/fast-assoc acc k (coercer request)))
{} coercers))
(fn [acc k coercer]
(impl/fast-assoc acc k (coercer request)))
{} coercers))
(defn coerce-response [coercers request response]
(if response
@ -147,13 +155,13 @@
:multipart :formData}]
(case specification
:swagger (->> (update
data
:parameters
(fn [parameters]
(->> parameters
(map (fn [[k v]] [(swagger-parameter k) v]))
(filter first)
(into {}))))
data
:parameters
(fn [parameters]
(->> parameters
(map (fn [[k v]] [(swagger-parameter k) v]))
(filter first)
(into {}))))
(-get-apidocs coercion specification)))))
;;

View file

@ -1,6 +1,6 @@
(ns reitit.core
(:require [reitit.impl :as impl]
[reitit.exception :as exception]
(:require [reitit.exception :as exception]
[reitit.impl :as impl]
[reitit.trie :as trie]))
;;
@ -61,7 +61,7 @@
(if-not (partial-match? match)
match
(impl/throw-on-missing-path-params
(:template match) (:required match) path-params)))))
(:template match) (:required match) path-params)))))
(defn match->path
([match]
@ -87,15 +87,15 @@
(let [compiler (::trie/trie-compiler opts (trie/compiler))
names (impl/find-names compiled-routes opts)
[pl nl] (reduce
(fn [[pl nl] [p {:keys [name] :as data} result]]
(let [{:keys [path-params] :as route} (impl/parse p opts)
f #(if-let [path (impl/path-for route %)]
(->Match p data result (impl/url-decode-coll %) path)
(->PartialMatch p data result (impl/url-decode-coll %) path-params))]
[(conj pl (-> (trie/insert nil p (->Match p data result nil nil) opts) (trie/compile)))
(if name (assoc nl name f) nl)]))
[[] {}]
compiled-routes)
(fn [[pl nl] [p {:keys [name] :as data} result]]
(let [{:keys [path-params] :as route} (impl/parse p opts)
f #(if-let [path (impl/path-for route %)]
(->Match p data result (impl/url-decode-coll %) path)
(->PartialMatch p data result (impl/url-decode-coll %) path-params))]
[(conj pl (-> (trie/insert nil p (->Match p data result nil nil) opts) (trie/compile)))
(if name (assoc nl name f) nl)]))
[[] {}]
compiled-routes)
lookup (impl/fast-map nl)
matcher (trie/linear-matcher compiler pl true)
match-by-path (trie/path-matcher matcher compiler)
@ -103,16 +103,11 @@
^{:type ::router}
(reify
Router
(router-name [_]
:linear-router)
(routes [_]
routes)
(compiled-routes [_]
compiled-routes)
(options [_]
opts)
(route-names [_]
names)
(router-name [_] :linear-router)
(routes [_] routes)
(compiled-routes [_] compiled-routes)
(options [_] opts)
(route-names [_] names)
(match-by-path [_ path]
(if-let [match (match-by-path path)]
(-> (:data match)
@ -133,33 +128,28 @@
([compiled-routes opts]
(when-let [wilds (seq (filter (impl/->wild-route? opts) compiled-routes))]
(exception/fail!
(str "can't create :lookup-router with wildcard routes: " wilds)
{:wilds wilds
:routes compiled-routes}))
(str "can't create :lookup-router with wildcard routes: " wilds)
{:wilds wilds
:routes compiled-routes}))
(let [names (impl/find-names compiled-routes opts)
[pl nl] (reduce
(fn [[pl nl] [p {:keys [name] :as data} result]]
[(assoc pl p (->Match p data result {} p))
(if name
(assoc nl name #(->Match p data result % p))
nl)])
[{} {}]
compiled-routes)
(fn [[pl nl] [p {:keys [name] :as data} result]]
[(assoc pl p (->Match p data result {} p))
(if name
(assoc nl name #(->Match p data result % p))
nl)])
[{} {}]
compiled-routes)
data (impl/fast-map pl)
lookup (impl/fast-map nl)
routes (impl/uncompile-routes compiled-routes)]
^{:type ::router}
(reify Router
(router-name [_]
:lookup-router)
(routes [_]
routes)
(compiled-routes [_]
compiled-routes)
(options [_]
opts)
(route-names [_]
names)
(router-name [_] :lookup-router)
(routes [_] routes)
(compiled-routes [_] compiled-routes)
(options [_] opts)
(route-names [_] names)
(match-by-path [_ path]
(impl/fast-get data path))
(match-by-name [_ name]
@ -183,34 +173,29 @@
(let [compiler (::trie/trie-compiler opts (trie/compiler))
names (impl/find-names compiled-routes opts)
[pl nl] (reduce
(fn [[pl nl] [p {:keys [name] :as data} result]]
(let [{:keys [path-params] :as route} (impl/parse p opts)
f #(if-let [path (impl/path-for route %)]
(->Match p data result (impl/url-decode-coll %) path)
(->PartialMatch p data result (impl/url-decode-coll %) path-params))]
[(trie/insert pl p (->Match p data result nil nil) opts)
(if name (assoc nl name f) nl)]))
[nil {}]
compiled-routes)
(fn [[pl nl] [p {:keys [name] :as data} result]]
(let [{:keys [path-params] :as route} (impl/parse p opts)
f #(if-let [path (impl/path-for route %)]
(->Match p data result (impl/url-decode-coll %) path)
(->PartialMatch p data result (impl/url-decode-coll %) path-params))]
[(trie/insert pl p (->Match p data result nil nil) opts)
(if name (assoc nl name f) nl)]))
[nil {}]
compiled-routes)
matcher (trie/compile pl compiler)
match-by-path (trie/path-matcher matcher compiler)
match-by-path (if matcher (trie/path-matcher matcher compiler))
lookup (impl/fast-map nl)
routes (impl/uncompile-routes compiled-routes)]
^{:type ::router}
(reify
Router
(router-name [_]
:trie-router)
(routes [_]
routes)
(compiled-routes [_]
compiled-routes)
(options [_]
opts)
(route-names [_]
names)
(router-name [_] :trie-router)
(routes [_] routes)
(compiled-routes [_] compiled-routes)
(options [_] opts)
(route-names [_] names)
(match-by-path [_ path]
(if-let [match (match-by-path path)]
(if-let [match (and match-by-path (match-by-path path))]
(-> (:data match)
(assoc :path-params (:params match))
(assoc :path path))))
@ -229,8 +214,8 @@
([compiled-routes opts]
(when (or (not= (count compiled-routes) 1) (some (impl/->wild-route? opts) compiled-routes))
(exception/fail!
(str ":single-static-path-router requires exactly 1 static route: " compiled-routes)
{:routes compiled-routes}))
(str ":single-static-path-router requires exactly 1 static route: " compiled-routes)
{:routes compiled-routes}))
(let [[n :as names] (impl/find-names compiled-routes opts)
[[p data result]] compiled-routes
p #?(:clj (.intern ^String p) :cljs p)
@ -238,25 +223,17 @@
routes (impl/uncompile-routes compiled-routes)]
^{:type ::router}
(reify Router
(router-name [_]
:single-static-path-router)
(routes [_]
routes)
(compiled-routes [_]
compiled-routes)
(options [_]
opts)
(route-names [_]
names)
(router-name [_] :single-static-path-router)
(routes [_] routes)
(compiled-routes [_] compiled-routes)
(options [_] opts)
(route-names [_] names)
(match-by-path [_ path]
(if (#?(:clj .equals :cljs =) p path)
match))
(if (#?(:clj .equals :cljs =) p path) match))
(match-by-name [_ name]
(if (= n name)
match))
(if (= n name) match))
(match-by-name [_ name path-params]
(if (= n name)
(impl/fast-assoc match :path-params (impl/path-params path-params))))))))
(if (= n name) (impl/fast-assoc match :path-params (impl/path-params path-params))))))))
(defn mixed-router
"Creates two routers: [[lookup-router]] or [[single-static-path-router]] for
@ -274,16 +251,11 @@
routes (impl/uncompile-routes compiled-routes)]
^{:type ::router}
(reify Router
(router-name [_]
:mixed-router)
(routes [_]
routes)
(compiled-routes [_]
compiled-routes)
(options [_]
opts)
(route-names [_]
names)
(router-name [_] :mixed-router)
(routes [_] routes)
(compiled-routes [_] compiled-routes)
(options [_] opts)
(route-names [_] names)
(match-by-path [_ path]
(or (match-by-path static-router path)
(match-by-path wildcard-router path)))
@ -310,16 +282,11 @@
routes (impl/uncompile-routes compiled-routes)]
^{:type ::router}
(reify Router
(router-name [_]
:quarantine-router)
(routes [_]
routes)
(compiled-routes [_]
compiled-routes)
(options [_]
opts)
(route-names [_]
names)
(router-name [_] :quarantine-router)
(routes [_] routes)
(compiled-routes [_] compiled-routes)
(options [_] opts)
(route-names [_] names)
(match-by-path [_ path]
(or (match-by-path mixed-router path)
(match-by-path linear-router path)))

View file

@ -10,8 +10,8 @@
(map (fn [provide]
(when (contains? acc provide)
(exception/fail!
(str "multiple providers for: " provide)
{::multiple-providers provide}))
(str "multiple providers for: " provide)
{::multiple-providers provide}))
[provide dependent]))
(get-provides dependent)))
{} nodes))
@ -22,8 +22,8 @@
(if (contains? providers k)
(get providers k)
(exception/fail!
(str "provider missing for dependency: " k)
{::missing-provider k})))
(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.

View file

@ -31,20 +31,20 @@
path " " (not-empty (select-keys route-data [:conflicting]))))]
(apply str "Router contains conflicting route paths:\n\n"
(mapv
(fn [[[path route-data] vals]]
(str (resolve-str path route-data)
"\n"
(str/join "\n" (mapv (fn [[path route-data]]
(resolve-str path route-data)) vals))
"\n\n"))
conflicts))))
(fn [[[path route-data] vals]]
(str (resolve-str path route-data)
"\n"
(str/join "\n" (mapv (fn [[path route-data]]
(resolve-str path route-data)) vals))
"\n\n"))
conflicts))))
(defmethod format-exception :name-conflicts [_ _ conflicts]
(apply str "Router contains conflicting route names:\n\n"
(mapv
(fn [[name vals]]
(str name "\n-> " (str/join "\n-> " (mapv first vals)) "\n"))
conflicts)))
(fn [[name vals]]
(str name "\n-> " (str/join "\n-> " (mapv first vals)) "\n"))
conflicts)))
(defmethod format-exception :reitit.impl/merge-data [_ _ data]
(str "Error merging route-data\n\n" (pr-str data)))

View file

@ -1,13 +1,13 @@
(ns ^:no-doc reitit.impl
#?(:cljs (:require-macros [reitit.impl]))
(:require [clojure.string :as str]
[clojure.set :as set]
(:require [clojure.set :as set]
[clojure.string :as str]
[meta-merge.core :as mm]
[reitit.trie :as trie]
[reitit.exception :as ex])
[reitit.exception :as ex]
[reitit.trie :as trie])
#?(:clj
(:import (java.util HashMap Map)
(java.net URLEncoder URLDecoder))))
(:import (java.net URLEncoder URLDecoder)
(java.util HashMap Map))))
(defn parse [path opts]
(let [path #?(:clj (.intern ^String (trie/normalize path opts)) :cljs (trie/normalize path opts))
@ -28,59 +28,59 @@
Also works on vectors. Maintains key for maps, order for vectors."
[f coll]
(reduce-kv
(fn [coll k v]
(if-some [v' (f v)]
(assoc coll k v')
coll))
coll
coll))
(fn [coll k v]
(if-some [v' (f v)]
(assoc coll k v')
coll))
coll
coll))
(defn walk [raw-routes {:keys [path data routes expand]
:or {data [], routes []}
:as opts}]
(letfn
[(walk-many [p m r]
(reduce #(into %1 (walk-one p m %2)) [] r))
(walk-one [pacc macc routes]
(if (vector? (first routes))
(walk-many pacc macc routes)
(when (string? (first routes))
(let [[path & [maybe-arg :as args]] routes
[data childs] (if (or (vector? maybe-arg)
(and (sequential? maybe-arg)
(sequential? (first maybe-arg)))
(nil? maybe-arg))
[{} args]
[maybe-arg (rest args)])
macc (into macc (expand data opts))
child-routes (walk-many (str pacc path) macc (keep identity childs))]
(if (seq childs) (seq child-routes) [[(str pacc path) macc]])))))]
[(walk-many [p m r]
(reduce #(into %1 (walk-one p m %2)) [] r))
(walk-one [pacc macc routes]
(if (vector? (first routes))
(walk-many pacc macc routes)
(when (string? (first routes))
(let [[path & [maybe-arg :as args]] routes
[data childs] (if (or (vector? maybe-arg)
(and (sequential? maybe-arg)
(sequential? (first maybe-arg)))
(nil? maybe-arg))
[{} args]
[maybe-arg (rest args)])
macc (into macc (expand data opts))
child-routes (walk-many (str pacc path) macc (keep identity childs))]
(if (seq childs) (seq child-routes) [[(str pacc path) macc]])))))]
(walk-one path (mapv identity data) raw-routes)))
(defn map-data [f routes]
(mapv (fn [[p ds]] [p (f p ds)]) routes))
(defn merge-data [p x]
(defn merge-data [{:keys [meta-merge-fn] :as g} p x]
(reduce
(fn [acc [k v]]
(try
(mm/meta-merge acc {k v})
(catch #?(:clj Exception, :cljs js/Error) e
(ex/fail! ::merge-data {:path p, :left acc, :right {k v}, :exception e}))))
{} x))
(fn [acc [k v]]
(try
((or meta-merge-fn mm/meta-merge) acc {k v})
(catch #?(:clj Exception, :cljs js/Error) e
(ex/fail! ::merge-data {:path p, :left acc, :right {k v}, :exception e}))))
{} x))
(defn resolve-routes [raw-routes {:keys [coerce] :as opts}]
(cond->> (->> (walk raw-routes opts) (map-data merge-data))
coerce (into [] (keep #(coerce % opts)))))
(cond->> (->> (walk raw-routes opts) (map-data #(merge-data opts %1 %2)))
coerce (into [] (keep #(coerce % opts)))))
(defn path-conflicting-routes [routes opts]
(let [parts-and-routes (mapv (fn [[s :as r]] [(trie/split-path s opts) r]) routes)]
(-> (into {} (comp (map-indexed (fn [index [p r]]
[r (reduce
(fn [acc [p' r']]
(if (trie/conflicting-parts? p p')
(conj acc r') acc))
#{} (subvec parts-and-routes (inc index)))]))
(fn [acc [p' r']]
(if (trie/conflicting-parts? p p')
(conj acc r') acc))
#{} (subvec parts-and-routes (inc index)))]))
(filter (comp seq second))) parts-and-routes)
(not-empty))))
@ -123,13 +123,13 @@
(defn path-for [route path-params]
(if (:path-params route)
(if-let [parts (reduce
(fn [acc part]
(if (string? part)
(conj acc part)
(if-let [p (get path-params (:value part))]
(conj acc p)
(reduced nil))))
[] (:path-parts route))]
(fn [acc part]
(if (string? part)
(conj acc part)
(if-let [p (get path-params (:value part))]
(conj acc p)
(reduced nil))))
[] (:path-parts route))]
(apply str parts))
(:path route)))
@ -138,8 +138,8 @@
(let [defined (-> path-params keys set)
missing (set/difference required defined)]
(ex/fail!
(str "missing path-params for route " template " -> " missing)
{:path-params path-params, :required required}))))
(str "missing path-params for route " template " -> " missing)
{:path-params path-params, :required required}))))
(defn fast-assoc
#?@(:clj [[^clojure.lang.Associative a k v] (.assoc a k v)]
@ -178,10 +178,10 @@
(if s
#?(:clj (if (.contains ^String s "%")
(URLDecoder/decode
(if (.contains ^String s "+")
(.replace ^String s "+" "%2B")
^String s)
"UTF-8"))
(if (.contains ^String s "+")
(.replace ^String s "+" "%2B")
^String s)
"UTF-8"))
:cljs (js/decodeURIComponent s))))
(defn url-decode [s]
@ -249,18 +249,10 @@
(->> params
(map (fn [[k v]]
(if (or (sequential? v) (set? v))
(str/join "&" (map query-parameter (repeat k) v))
(if (seq v)
(str/join "&" (map query-parameter (repeat k) v))
;; Empty seq results in single & character in the query string.
;; Handle as empty string to behave similarly as when the value is nil.
(query-parameter k ""))
(query-parameter k v))))
(str/join "&")))
(defmacro goog-extend [type base-type ctor & methods]
`(do
(def ~type (fn ~@ctor))
(goog/inherits ~type ~base-type)
~@(map
(fn [method]
`(set! (.. ~type -prototype ~(symbol (str "-" (first method))))
(fn ~@(rest method))))
methods)))

View file

@ -1,9 +1,9 @@
(ns reitit.interceptor
(:require [meta-merge.core :refer [meta-merge]]
[clojure.pprint :as pprint]
(:require [clojure.pprint :as pprint]
[meta-merge.core :refer [meta-merge]]
[reitit.core :as r]
[reitit.impl :as impl]
[reitit.exception :as exception]))
[reitit.exception :as exception]
[reitit.impl :as impl]))
(defprotocol IntoInterceptor
(into-interceptor [this data opts]))
@ -37,36 +37,36 @@
(if-let [interceptor (if registry (registry this))]
(into-interceptor interceptor data opts)
(throw
(ex-info
(str
"Interceptor " this " not found in registry.\n\n"
(if (seq registry)
(str
"Available interceptors in registry:\n"
(with-out-str
(pprint/print-table [:id :description] (for [[k v] registry] {:id k :description v}))))
"see [reitit.interceptor/router] on how to add interceptor to the registry.\n") "\n")
{:id this
:registry registry}))))
(ex-info
(str
"Interceptor " this " not found in registry.\n\n"
(if (seq registry)
(str
"Available interceptors in registry:\n"
(with-out-str
(pprint/print-table [:id :description] (for [[k v] registry] {:id k :description v}))))
"see [reitit.interceptor/router] on how to add interceptor to the registry.\n") "\n")
{:id this
:registry registry}))))
#?(:clj clojure.lang.APersistentVector
:cljs cljs.core.PersistentVector)
(into-interceptor [[f & args :as form] data opts]
(when (and (seq args) (not (fn? f)))
(exception/fail!
(str "Invalid Interceptor form: " form "")
{:form form}))
(str "Invalid Interceptor form: " form "")
{:form form}))
(into-interceptor (apply f args) data opts))
#?(:clj clojure.lang.Fn
:cljs function)
(into-interceptor [this data opts]
(into-interceptor
{:name ::handler
::handler this
:enter (fn [ctx]
(assoc ctx :response (this (:request ctx))))}
data opts))
{:name ::handler
::handler this
:enter (fn [ctx]
(assoc ctx :response (this (:request ctx))))}
data opts))
#?(:clj clojure.lang.PersistentArrayMap
:cljs cljs.core.PersistentArrayMap)
@ -86,13 +86,13 @@
opts (assoc opts ::compiled (inc ^long compiled))]
(when (>= ^long compiled ^long *max-compile-depth*)
(exception/fail!
(str "Too deep Interceptor compilation - " compiled)
{:this this, :data data, :opts opts}))
(str "Too deep Interceptor compilation - " compiled)
{:this this, :data data, :opts opts}))
(if-let [interceptor (into-interceptor (compile data opts) data opts)]
(map->Interceptor
(merge
(dissoc this :compile)
(impl/strip-nils interceptor)))))))
(merge
(dissoc this :compile)
(impl/strip-nils interceptor)))))))
nil
(into-interceptor [_ _ _]))
@ -122,9 +122,9 @@
([[_ {:keys [interceptors handler] :as data}] {::keys [queue] :as opts} _]
(let [chain (chain (into (vec interceptors) [handler]) data opts)]
(map->Endpoint
{:interceptors chain
:queue ((or queue identity) chain)
:data data}))))
{:interceptors chain
:queue ((or queue identity) chain)
:data data}))))
(defn transform-butlast
"Returns a function to that takes a interceptor transformation function and
@ -132,8 +132,8 @@
[f]
(fn [interceptors]
(concat
(f (butlast interceptors))
[(last interceptors)])))
(f (butlast interceptors))
[(last interceptors)])))
(defn router
"Creates a [[reitit.core/Router]] from raw route data and optionally an options map with
@ -155,8 +155,8 @@
:handler get-user}]])"
([data]
(router data nil))
([data opts]
(let [opts (meta-merge {:compile compile-result} opts)]
([data {:keys [meta-merge-fn] :as opts}]
(let [opts ((or meta-merge-fn meta-merge) {:compile compile-result} opts)]
(r/router data opts))))
(defn interceptor-handler [router]

View file

@ -1,9 +1,9 @@
(ns reitit.middleware
(:require [meta-merge.core :refer [meta-merge]]
[clojure.pprint :as pprint]
(:require [clojure.pprint :as pprint]
[meta-merge.core :refer [meta-merge]]
[reitit.core :as r]
[reitit.impl :as impl]
[reitit.exception :as exception]))
[reitit.exception :as exception]
[reitit.impl :as impl]))
(defprotocol IntoMiddleware
(into-middleware [this data opts]))
@ -21,17 +21,17 @@
(if-let [middleware (if registry (registry this))]
(into-middleware middleware data opts)
(throw
(ex-info
(str
"Middleware " this " not found in registry.\n\n"
(if (seq registry)
(str
"Available middleware in registry:\n"
(with-out-str
(pprint/print-table [:id :description] (for [[k v] registry] {:id k :description v}))))
"see [reitit.middleware/router] on how to add middleware to the registry.\n") "\n")
{:id this
:registry registry}))))
(ex-info
(str
"Middleware " this " not found in registry.\n\n"
(if (seq registry)
(str
"Available middleware in registry:\n"
(with-out-str
(pprint/print-table [:id :description] (for [[k v] registry] {:id k :description v}))))
"see [reitit.middleware/router] on how to add middleware to the registry.\n") "\n")
{:id this
:registry registry}))))
#?(:clj clojure.lang.APersistentVector
:cljs cljs.core.PersistentVector)
@ -43,7 +43,7 @@
:cljs function)
(into-middleware [this _ _]
(map->Middleware
{:wrap this}))
{:wrap this}))
#?(:clj clojure.lang.PersistentArrayMap
:cljs cljs.core.PersistentArrayMap)
@ -63,13 +63,13 @@
opts (assoc opts ::compiled (inc ^long compiled))]
(when (>= ^long compiled ^long *max-compile-depth*)
(exception/fail!
(str "Too deep Middleware compilation - " compiled)
{:this this, :data data, :opts opts}))
(str "Too deep Middleware compilation - " compiled)
{:this this, :data data, :opts opts}))
(if-let [middeware (into-middleware (compile data opts) data opts)]
(map->Middleware
(merge
(dissoc this :compile)
(impl/strip-nils middeware)))))))
(merge
(dissoc this :compile)
(impl/strip-nils middeware)))))))
nil
(into-middleware [_ _ _]))
@ -77,10 +77,10 @@
(defn- ensure-handler! [path data scope]
(when-not (:handler data)
(exception/fail!
(str "path \"" path "\" doesn't have a :handler defined"
(if scope (str " for " scope)))
(merge {:path path, :data data}
(if scope {:scope scope})))))
(str "path \"" path "\" doesn't have a :handler defined"
(if scope (str " for " scope)))
(merge {:path path, :data data}
(if scope {:scope scope})))))
(defn- expand-and-transform
[middleware data {::keys [transform] :or {transform identity} :as opts}]
@ -116,9 +116,9 @@
(ensure-handler! path data scope)
(let [middleware (expand-and-transform middleware data opts)]
(map->Endpoint
{:handler (compile-handler middleware handler)
:middleware middleware
:data data}))))
{:handler (compile-handler middleware handler)
:middleware middleware
:data data}))))
(defn router
"Creates a [[reitit.core/Router]] from raw route data and optionally an options map with
@ -138,8 +138,8 @@
:handler get-user}]])"
([data]
(router data nil))
([data opts]
(let [opts (meta-merge {:compile compile-result} opts)]
([data {:keys [meta-merge-fn] :as opts}]
(let [opts ((or meta-merge-fn meta-merge) {:compile compile-result} opts)]
(r/router data opts))))
(defn middleware-handler [router]

View file

@ -1,8 +1,8 @@
(ns reitit.spec
(:require [clojure.spec.alpha :as s]
[clojure.spec.gen.alpha :as gen]
[reitit.exception :as exception]
[reitit.core :as r]))
[reitit.core :as r]
[reitit.exception :as exception]))
;;
;; routes
@ -16,9 +16,9 @@
(s/def ::raw-route
(s/nilable
(s/cat :path ::path
:arg (s/? ::arg)
:childs (s/* (s/and (s/nilable ::raw-routes))))))
(s/cat :path ::path
:arg (s/? ::arg)
:childs (s/* (s/and (s/nilable ::raw-routes))))))
(s/def ::raw-routes
(s/or :route ::raw-route
@ -60,19 +60,19 @@
(s/def ::opts
(s/nilable
(s/keys :opt-un [:reitit.router/path
:reitit.router/routes
:reitit.router/data
:reitit.router/expand
:reitit.router/coerce
:reitit.router/compile
:reitit.router/conflicts
:reitit.router/router])))
(s/keys :opt-un [:reitit.router/path
:reitit.router/routes
:reitit.router/data
:reitit.router/expand
:reitit.router/coerce
:reitit.router/compile
:reitit.router/conflicts
:reitit.router/router])))
(s/fdef r/router
:args (s/or :1arity (s/cat :data (s/spec ::raw-routes))
:2arity (s/cat :data (s/spec ::raw-routes), :opts ::opts))
:ret ::router)
:args (s/or :1arity (s/cat :data (s/spec ::raw-routes))
:2arity (s/cat :data (s/spec ::raw-routes), :opts ::opts))
:ret ::router)
;;
;; coercion
@ -119,24 +119,25 @@
(defrecord Problem [path scope data spec problems])
(defn validate-route-data [routes wrap spec]
(let [spec (wrap spec)]
(some->> (for [[p d _] routes]
(when-let [problems (and spec (s/explain-data spec d))]
(->Problem p nil d spec problems)))
(keep identity) (seq) (vec))))
(let [spec (wrap spec)
spec-explain (fn [[p d _]]
(when-let [problems (and spec (s/explain-data spec d))]
(->Problem p nil d spec problems)))
errors (into [] (keep spec-explain) routes)]
(when (pos? (count errors)) errors)))
(defn validate [routes {:keys [spec] ::keys [wrap] :or {spec ::default-data, wrap identity}}]
(when-let [problems (validate-route-data routes wrap spec)]
(exception/fail!
::invalid-route-data
{:problems problems})))
::invalid-route-data
{:problems problems})))
(defmethod exception/format-exception :reitit.spec/invalid-route-data [_ _ {:keys [problems]}]
(apply str "Invalid route data:\n\n"
(mapv
(fn [{:keys [path scope data spec]}]
(str "-- On route -----------------------\n\n"
(pr-str path) (if scope (str " " (pr-str scope))) "\n\n"
(pr-str data) "\n\n"
(s/explain-str spec data) "\n"))
problems)))
(fn [{:keys [path scope data spec]}]
(str "-- On route -----------------------\n\n"
(pr-str path) (if scope (str " " (pr-str scope))) "\n\n"
(pr-str data) "\n\n"
(s/explain-str spec data) "\n"))
problems)))

View file

@ -2,8 +2,8 @@
(:refer-clojure :exclude [compile])
(:require [clojure.string :as str]
[reitit.exception :as ex])
#?(:clj (:import [reitit Trie Trie$Match Trie$Matcher]
(java.net URLDecoder))))
#?(:clj (:import (java.net URLDecoder)
[reitit Trie Trie$Match Trie$Matcher])))
(defn ^:no-doc into-set [x]
(cond
@ -90,12 +90,12 @@
(defn join-path [xs]
(reduce
(fn [s x]
(str s (cond
(string? x) x
(instance? Wild x) (str "{" (-> x :value str (subs 1)) "}")
(instance? CatchAll x) (str "{*" (-> x :value str (subs 1)) "}"))))
"" xs))
(fn [s x]
(str s (cond
(string? x) x
(instance? Wild x) (str "{" (-> x :value str (subs 1)) "}")
(instance? CatchAll x) (str "{*" (-> x :value str (subs 1)) "}"))))
"" xs))
(defn normalize [s opts]
(-> s (split-path opts) (join-path)))
@ -167,30 +167,30 @@
(instance? CatchAll path)
(assoc-in node [:catch-all path] (-node {:params params, :data data}))
(str/blank? path)
(empty? path)
(-insert node ps fp params data)
:else
(or
(reduce
(fn [_ [p n]]
(if-let [cp (common-prefix p path)]
(if (= cp p)
(reduce
(fn [_ [p n]]
(if-let [cp (common-prefix p path)]
(if (= cp p)
;; insert into child node
(let [n' (-insert n (conj ps (subs path (count p))) fp params data)]
(reduced (assoc-in node [:children p] n')))
(let [n' (-insert n (conj ps (subs path (count p))) fp params data)]
(reduced (assoc-in node [:children p] n')))
;; split child node
(let [rp (subs p (count cp))
rp' (subs path (count cp))
n' (-insert (-node {}) ps fp params data)
n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil nil nil)]
(reduced (update node :children (fn [children]
(-> children
(dissoc p)
(assoc cp n'')))))))))
nil (:children node))
(let [rp (subs p (count cp))
rp' (subs path (count cp))
n' (-insert (-node {}) ps fp params data)
n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil nil nil)]
(reduced (update node :children (fn [children]
(-> children
(dissoc p)
(assoc cp n'')))))))))
nil (:children node))
;; new child node
(assoc-in node [:children path] (-insert (-node {}) ps fp params data))))]
(assoc-in node [:children path] (-insert (-node {}) ps fp params data))))]
(if-let [child (get-in node' [:children ""])]
;; optimize by removing empty paths
(-> (merge-with merge (dissoc node' :data) child)
@ -202,10 +202,10 @@
(if percent?
#?(:cljs (js/decodeURIComponent param)
:clj (URLDecoder/decode
(if (.contains ^String param "+")
(.replace ^String param "+" "%2B")
param)
"UTF-8"))
(if (.contains ^String param "+")
(.replace ^String param "+" "%2B")
param)
"UTF-8"))
param)))
;;
@ -313,13 +313,13 @@
(def record-parameters
"Memoized function to transform parameters into runtime generated Record."
(memoize
(fn [keys]
(if (some qualified-keyword? keys)
(map-parameters keys)
(let [sym (gensym "PathParams")
ctor (symbol (str "map->" sym))]
(binding [*ns* (find-ns 'user)]
(eval `(do (defrecord ~sym ~(mapv (comp symbol name) keys)) (~ctor {}))))))))))
(fn [keys]
(if (some qualified-keyword? keys)
(map-parameters keys)
(let [sym (gensym "PathParams")
ctor (symbol (str "map->" sym))]
(binding [*ns* (find-ns 'user)]
(eval `(do (defrecord ~sym ~(mapv (comp symbol name) keys)) (~ctor {}))))))))))
(defn insert
"Returns a trie with routes added to it."
@ -327,9 +327,9 @@
(insert nil routes))
([node routes]
(reduce
(fn [acc [p d]]
(insert acc p d))
node routes))
(fn [acc [p d]]
(insert acc p d))
node routes))
([node path data]
(insert node path data nil))
([node path data {::keys [parameters] :or {parameters map-parameters} :as opts}]
@ -355,17 +355,16 @@
(cond-> data (conj (data-matcher compiler params data)))
(into (for [[p c] children] (static-matcher compiler p (compile c compiler (conj cp p)))))
(into
(for [[p c] wilds]
(let [pv (:value p)
ends (ends c)]
(if (next ends)
(ex/fail! ::multiple-terminators {:terminators ends, :path (join-path (conj cp p))})
(wild-matcher compiler pv (ffirst ends) (compile c compiler (conj cp pv)))))))
(for [[p c] wilds]
(let [pv (:value p)
ends (ends c)]
(if (next ends)
(ex/fail! ::multiple-terminators {:terminators ends, :path (join-path (conj cp p))})
(wild-matcher compiler pv (ffirst ends) (compile c compiler (conj cp pv)))))))
(into (for [[p c] catch-all] (catch-all-matcher compiler (:value p) params (:data c)))))]
(cond
(> (count matchers) 1) (linear-matcher compiler matchers false)
(= (count matchers) 1) (first matchers)
:else (data-matcher compiler {} nil)))))
(= (count matchers) 1) (first matchers)))))
(defn pretty
"Returns a simplified EDN structure of a compiled trie for printing purposes."
@ -387,59 +386,61 @@
(comment
(->
[["/v2/whoami" 1]
["/v2/users/:user-id/datasets" 2]
["/v2/public/projects/:project-id/datasets" 3]
["/v1/public/topics/:topic" 4]
["/v1/users/:user-id/orgs/:org-id" 5]
["/v1/search/topics/:term" 6]
["/v1/users/:user-id/invitations" 7]
["/v1/users/:user-id/topics" 9]
["/v1/users/:user-id/bookmarks/followers" 10]
["/v2/datasets/:dataset-id" 11]
["/v1/orgs/:org-id/usage-stats" 12]
["/v1/orgs/:org-id/devices/:client-id" 13]
["/v1/messages/user/:user-id" 14]
["/v1/users/:user-id/devices" 15]
["/v1/public/users/:user-id" 16]
["/v1/orgs/:org-id/errors" 17]
["/v1/public/orgs/:org-id" 18]
["/v1/orgs/:org-id/invitations" 19]
["/v1/users/:user-id/device-errors" 22]
["/v2/login" 23]
["/v1/users/:user-id/usage-stats" 24]
["/v2/users/:user-id/devices" 25]
["/v1/users/:user-id/claim-device/:client-id" 26]
["/v2/public/projects/:project-id" 27]
["/v2/public/datasets/:dataset-id" 28]
["/v2/users/:user-id/topics/bulk" 29]
["/v1/messages/device/:client-id" 30]
["/v1/users/:user-id/owned-orgs" 31]
["/v1/topics/:topic" 32]
["/v1/users/:user-id/bookmark/:topic" 33]
["/v1/orgs/:org-id/members/:user-id" 34]
["/v1/users/:user-id/devices/:client-id" 35]
["/v1/users/:user-id" 36]
["/v1/orgs/:org-id/devices" 37]
["/v1/orgs/:org-id/members" 38]
["/v2/orgs/:org-id/topics" 40]
["/v1/whoami" 41]
["/v1/orgs/:org-id" 42]
["/v1/users/:user-id/api-key" 43]
["/v2/schemas" 44]
["/v2/users/:user-id/topics" 45]
["/v1/orgs/:org-id/confirm-membership/:token" 46]
["/v2/topics/:topic" 47]
["/v1/messages/topic/:topic" 48]
["/v1/users/:user-id/devices/:client-id/reset-password" 49]
["/v2/topics" 50]
["/v1/login" 51]
["/v1/users/:user-id/orgs" 52]
["/v2/public/messages/dataset/:dataset-id" 53]
["/v1/topics" 54]
["/v1/orgs" 55]
["/v1/users/:user-id/bookmarks" 56]
["/v1/orgs/:org-id/topics" 57]]
(insert)
(compile)
(pretty)))
[["/v2/whoami" 1]
["/v2/users/:user-id/datasets" 2]
["/v2/public/projects/:project-id/datasets" 3]
["/v1/public/topics/:topic" 4]
["/v1/users/:user-id/orgs/:org-id" 5]
["/v1/search/topics/:term" 6]
["/v1/users/:user-id/invitations" 7]
["/v1/users/:user-id/topics" 9]
["/v1/users/:user-id/bookmarks/followers" 10]
["/v2/datasets/:dataset-id" 11]
["/v1/orgs/:org-id/usage-stats" 12]
["/v1/orgs/:org-id/devices/:client-id" 13]
["/v1/messages/user/:user-id" 14]
["/v1/users/:user-id/devices" 15]
["/v1/public/users/:user-id" 16]
["/v1/orgs/:org-id/errors" 17]
["/v1/public/orgs/:org-id" 18]
["/v1/orgs/:org-id/invitations" 19]
["/v1/users/:user-id/device-errors" 22]
["/v2/login" 23]
["/v1/users/:user-id/usage-stats" 24]
["/v2/users/:user-id/devices" 25]
["/v1/users/:user-id/claim-device/:client-id" 26]
["/v2/public/projects/:project-id" 27]
["/v2/public/datasets/:dataset-id" 28]
["/v2/users/:user-id/topics/bulk" 29]
["/v1/messages/device/:client-id" 30]
["/v1/users/:user-id/owned-orgs" 31]
["/v1/topics/:topic" 32]
["/v1/users/:user-id/bookmark/:topic" 33]
["/v1/orgs/:org-id/members/:user-id" 34]
["/v1/users/:user-id/devices/:client-id" 35]
["/v1/users/:user-id" 36]
["/v1/orgs/:org-id/devices" 37]
["/v1/orgs/:org-id/members" 38]
["/v2/orgs/:org-id/topics" 40]
["/v1/whoami" 41]
["/v1/orgs/:org-id" 42]
["/v1/users/:user-id/api-key" 43]
["/v2/schemas" 44]
["/v2/users/:user-id/topics" 45]
["/v1/orgs/:org-id/confirm-membership/:token" 46]
["/v2/topics/:topic" 47]
["/v1/messages/topic/:topic" 48]
["/v1/users/:user-id/devices/:client-id/reset-password" 49]
["/v2/topics" 50]
["/v1/login" 51]
["/v1/users/:user-id/orgs" 52]
["/v2/public/messages/dataset/:dataset-id" 53]
["/v1/topics" 54]
["/v1/orgs" 55]
["/v1/users/:user-id/bookmarks" 56]
["/v1/orgs/:org-id/topics" 57]
["/command1 {arg1} {arg2}" ::cmd1]
["/command2 {arg1} {arg2} {arg3}" ::cmd2]]
(insert)
(compile)
(pretty)))

View file

@ -0,0 +1 @@
../../../.clj-kondo/module_config.edn

View file

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

View file

@ -1,18 +1,16 @@
(ns reitit.dev.pretty
(:require [clojure.string :as str]
(:require [arrangement.core] ;; spell-spec
[clojure.spec.alpha :as s]
[reitit.exception :as exception]
[arrangement.core]
;; spell-spec
[spell-spec.expound]
;; expound
[clojure.string :as str]
[expound.alpha] ;; fipp
[expound.ansi]
[expound.alpha]
;; fipp
[fipp.visit]
[fipp.edn]
[fipp.ednize]
[fipp.engine]))
[fipp.engine]
[fipp.visit]
[reitit.exception :as exception]
[spell-spec.expound] ;; expound
))
;;
;; colors
@ -152,13 +150,13 @@
(printer nil))
([options]
(map->EdnPrinter
(merge
{:width 80
:symbols {}
:print-length *print-length*
:print-level *print-level*
:print-meta *print-meta*}
options))))
(merge
{:width 80
:symbols {}
:print-length *print-length*
:print-level *print-level*
:print-meta *print-meta*}
options))))
(defn pprint
([x] (pprint x {}))
@ -209,13 +207,13 @@
(defn exception-str [message source printer]
(with-out-str
(print-doc
[:group
(title "Router creation failed" source printer)
[:break] [:break]
message
[:break]
(footer printer)]
printer)))
[:group
(title "Router creation failed" source printer)
[:break] [:break]
message
[:break]
(footer printer)]
printer)))
(defmulti format-exception (fn [type _ _] type))
@ -231,11 +229,11 @@
(defn de-expound-colors [^String s mappings]
(let [s' (reduce
(fn [s [from to]]
(.replace ^String s
^String (expound.ansi/esc [from])
^String (-start (colors to))))
s mappings)]
(fn [s [from to]]
(.replace ^String s
^String (expound.ansi/esc [from])
^String (-start (colors to))))
s mappings)]
(.replace ^String s'
^String (expound.ansi/esc [:none])
(str (expound.ansi/esc [:none]) (-start (colors :text))))))
@ -254,9 +252,9 @@
(def expound-printer
(expound.alpha/custom-printer
{:theme :figwheel-theme
:show-valid-values? false
:print-specs? false}))
{:theme :figwheel-theme
:show-valid-values? false
:print-specs? false}))
;;
;; Formatters
@ -276,18 +274,18 @@
" ")
(edn (not-empty (select-keys route-data [:conflicting])))])]
(into
[:group]
(mapv
(fn [[[path route-data] vals]]
[:group
(path-report path route-data)
(into
[:group]
(map
(fn [[path route-data]] (path-report path route-data))
vals))
[:break]])
conflicts)))
[:group]
(mapv
(fn [[[path route-data] vals]]
[:group
(path-report path route-data)
(into
[:group]
(map
(fn [[path route-data]] (path-report path route-data))
vals))
[:break]])
conflicts)))
[:span (text "Either fix the conflicting paths or disable the conflict resolution")
[:break] (text "by setting route data for conflicting route: ") [:break] [:break]
(edn {:conflicting true} {:margin 3})
@ -302,19 +300,19 @@
(text "Router contains conflicting route names:")
[:break] [:break]
(into
[:group]
(mapv
(fn [[name vals]]
[:group
[:span (text name)]
[:break]
(into
[:group]
(map
(fn [p] [:span (color :grey "-> " p) [:break]])
(mapv first vals)))
[:break]])
conflicts))
[:group]
(mapv
(fn [[name vals]]
[:group
[:span (text name)]
[:break]
(into
[:group]
(map
(fn [p] [:span (color :grey "-> " p) [:break]])
(mapv first vals)))
[:break]])
conflicts))
(color :white "https://cljdoc.org/d/metosin/reitit/CURRENT/doc/basics/route-conflicts")
[:break]])
@ -323,22 +321,22 @@
(text "Invalid route data:")
[:break] [:break]
(into
[:group]
(map
(fn [{:keys [data path spec scope]}]
[:group
[:span (color :grey "-- On route -----------------------")]
[:break]
[:break]
(text path) (if scope [:span " " (text scope)])
[:break]
[:break]
(-> (s/explain-data spec data)
(expound-printer)
(with-out-str)
(fippify))
[:break]])
problems))
[:group]
(map
(fn [{:keys [data path spec scope]}]
[:group
[:span (color :grey "-- On route -----------------------")]
[:break]
[:break]
(text path) (if scope [:span " " (text scope)])
[:break]
[:break]
(-> (s/explain-data spec data)
(expound-printer)
(with-out-str)
(fippify))
[:break]])
problems))
(color :white "https://cljdoc.org/d/metosin/reitit/CURRENT/doc/basics/route-data-validation")
[:break]])

View file

@ -0,0 +1 @@
../../../.clj-kondo/module_config.edn

View file

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

View file

@ -22,18 +22,28 @@
(defn match-by-path
"Given routing tree and current path, return match with possibly
coerced parameters. Return nil if no match found."
[router path]
(let [uri (.parse Uri path)]
(if-let [match (r/match-by-path router (.getPath uri))]
(let [q (query-params uri)
match (assoc match :query-params q)
;; Return uncoerced values if coercion is not enabled - so
;; that tha parameters are always accessible from same property.
parameters (or (coercion/coerce! match)
{:path (:path-params match)
:query q})]
(assoc match :parameters parameters)))))
coerced parameters. Return nil if no match found.
:on-coercion-error - a sideeffecting fn of `match exception -> nil`"
([router path] (match-by-path router path nil))
([router path {:keys [on-coercion-error]}]
(let [uri (.parse Uri path)
coerce! (if on-coercion-error
(fn [match]
(try (coercion/coerce! match)
(catch js/Error e
(on-coercion-error match e)
(throw e))))
coercion/coerce!)]
(if-let [match (r/match-by-path router (.getPath uri))]
(let [q (query-params uri)
match (assoc match :query-params q)
;; Return uncoerced values if coercion is not enabled - so
;; that tha parameters are always accessible from same property.
parameters (or (coerce! match)
{:path (:path-params match)
:query q})]
(assoc match :parameters parameters))))))
(defn match-by-name
"Given a router, route name and optionally path-parameters,
@ -64,11 +74,11 @@
(let [defined (-> path-params keys set)
missing (set/difference (:required match) defined)]
(js/console.warn
"missing path-params for route" name
{:template (:template match)
:missing missing
:path-params path-params
:required (:required match)})
"missing path-params for route" name
{:template (:template match)
:missing missing
:path-params path-params
:required (:required match)})
nil))
match)
(do (js/console.warn "missing route" name)

View file

@ -10,7 +10,8 @@
;; Differences:
;; This one automatically removes previous event listeners.
(defn start!
(defn ^{:see-also ["reitit.frontend.history/start!"]}
start!
"This registers event listeners on HTML5 history and hashchange events.
Automatically removes previous event listeners so it is safe to call this repeatedly, for example when using
@ -37,30 +38,66 @@
(when (nil? @history)
(reset! history this))
(on-navigate m this))
opts))
opts))
(defn href
([k]
(rfh/href @history k nil nil))
([k params]
(rfh/href @history k params nil))
([k params query]
(rfh/href @history k params query)))
(defn
^{:see-also ["reitit.frontend.history/href"]}
href
"Generate a URL for a route defined by name, with given path-params and query-params.
(defn push-state
"Sets the new route, leaving previous route in history."
([k]
(rfh/push-state @history k nil nil))
([k params]
(rfh/push-state @history k params nil))
([k params query]
(rfh/push-state @history k params query)))
The URL is formatted using Reitit frontend history handler, so using it with
anchor element href will correctly trigger route change event.
(defn replace-state
"Replaces current route. I.e. current route is not left on history."
([k]
(rfh/replace-state @history k nil nil))
([k params]
(rfh/replace-state @history k params nil))
([k params query]
(rfh/replace-state @history k params query)))
Note: currently collections in query-parameters are encoded as field-value
pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them
differently, convert the collections to strings first."
([name]
(rfh/href @history name nil nil))
([name path-params]
(rfh/href @history name path-params nil))
([name path-params query-params]
(rfh/href @history name path-params query-params)))
(defn
^{:see-also ["reitit.frontend.history/push-state"]}
push-state
"Updates the browser location and pushes new entry to the history stack using
URL built from a route defined by name, with given path-params and
query-params.
Will also trigger on-navigate callback on Reitit frontend History handler.
Note: currently collections in query parameters are encoded as field-value
pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them
differently, convert the collections to strings first.
See also:
https://developer.mozilla.org/en-US/docs/Web/API/History/pushState"
([name]
(rfh/push-state @history name nil nil))
([name path-params]
(rfh/push-state @history name path-params nil))
([name path-params query-params]
(rfh/push-state @history name path-params query-params)))
(defn
^{:see-also ["reitit.frontend.history/replace-state"]}
replace-state
"Updates the browser location and replaces latest entry in the history stack
using URL built from a route defined by name, with given path-params and
query-params.
Will also trigger on-navigate callback on Reitit frontend History handler.
Note: currently collections in query-parameters are encoded as field-value
pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them
differently, convert the collections to strings first.
See also:
https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState"
([name]
(rfh/replace-state @history name nil nil))
([name path-params]
(rfh/replace-state @history name path-params nil))
([name path-params query-params]
(rfh/replace-state @history name path-params query-params)))

View file

@ -1,9 +1,9 @@
(ns reitit.frontend.history
"Provides integration to hash-change or HTML5 History
events."
(:require [reitit.core :as reitit]
[reitit.frontend :as rf]
[goog.events :as gevents])
(:require [goog.events :as gevents]
[reitit.core :as reitit]
[reitit.frontend :as rf])
(:import goog.Uri))
(defprotocol History
@ -40,7 +40,7 @@
nil)
(-on-navigate [this path]
(reset! last-fragment path)
(on-navigate (rf/match-by-path router path) this))
(on-navigate (rf/match-by-path router path this) this))
(-get-path [this]
;; Remove #
;; "" or "#" should be same as "#/"
@ -125,7 +125,7 @@
(-on-navigate this (-get-path this))
this))
(-on-navigate [this path]
(on-navigate (rf/match-by-path router path) this))
(on-navigate (rf/match-by-path router path this) this))
(-stop [this]
(gevents/unlistenByKey listen-key)
(gevents/unlistenByKey click-listen-key)
@ -171,40 +171,73 @@
(map->FragmentHistory opts)
(map->Html5History opts))))))
(defn stop! [history]
(defn stop!
"Stops the given history handler, removing the event handlers."
[history]
(if history
(-stop history)))
(defn href
([history k]
(href history k nil))
([history k params]
(href history k params nil))
([history k params query]
(let [match (rf/match-by-name! (:router history) k params)]
(-href history (reitit/match->path match query)))))
"Generate a URL for a route defined by name, with given path-params and query-params.
(defn push-state
"Sets the new route, leaving previous route in history."
([history k]
(push-state history k nil nil))
([history k params]
(push-state history k params nil))
([history k params query]
(let [match (rf/match-by-name! (:router history) k params)
path (reitit/match->path match query)]
The URL is formatted using Reitit frontend history handler, so using it with
anchor element href will correctly trigger route change event.
Note: currently collections in query parameters are encoded as field-value
pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them
differently, convert the collections to strings first."
([history name]
(href history name nil))
([history name path-params]
(href history name path-params nil))
([history name path-params query-params]
(let [match (rf/match-by-name! (:router history) name path-params)]
(-href history (reitit/match->path match query-params)))))
(defn
^{:see-also ["reitit.core/match->path"]}
push-state
"Updates the browser URL and pushes new entry to the history stack using
a route defined by name, with given path-params and query-params.
Will also trigger on-navigate callback on Reitit frontend History handler.
Note: currently collections in query-parameters are encoded as field-value
pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them
differently, convert the collections to strings first.
See also:
https://developer.mozilla.org/en-US/docs/Web/API/History/pushState"
([history name]
(push-state history name nil nil))
([history name path-params]
(push-state history name path-params nil))
([history name path-params query-params]
(let [match (rf/match-by-name! (:router history) name path-params)
path (reitit/match->path match query-params)]
;; pushState and replaceState don't trigger popstate event so call on-navigate manually
(.pushState js/window.history nil "" (-href history path))
(-on-navigate history path))))
(defn replace-state
"Replaces current route. I.e. current route is not left on history."
([history k]
(replace-state history k nil nil))
([history k params]
(replace-state history k params nil))
([history k params query]
(let [match (rf/match-by-name! (:router history) k params)
path (reitit/match->path match query)]
"Updates the browser location and replaces latest entry in the history stack
using URL built from a route defined by name, with given path-params and
query-params.
Will also trigger on-navigate callback on Reitit frontend History handler.
Note: currently collections in query-parameters are encoded as field-value
pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them
differently, convert the collections to strings first.
See also:
https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState"
([history name]
(replace-state history name nil nil))
([history name path-params]
(replace-state history name path-params nil))
([history name path-params query-params]
(let [match (rf/match-by-name! (:router history) name path-params)
path (reitit/match->path match query-params)]
(.replaceState js/window.history nil "" (-href history path))
(-on-navigate history path))))

View file

@ -0,0 +1 @@
../../../.clj-kondo/module_config.edn

View file

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

View file

@ -1,24 +1,24 @@
(ns reitit.http
(:require [meta-merge.core :refer [meta-merge]]
[reitit.interceptor :as interceptor]
[reitit.core :as r]
[reitit.exception :as ex]
[reitit.ring :as ring]
[reitit.core :as r]))
[reitit.interceptor :as interceptor]
[reitit.ring :as ring]))
(defrecord Endpoint [data interceptors queue handler path method])
(defn coerce-handler [[path data] {:keys [expand] :as opts}]
[path (reduce
(fn [acc method]
(if (contains? acc method)
(update acc method expand opts)
acc)) data ring/http-methods)])
(fn [acc method]
(if (contains? acc method)
(update acc method expand opts)
acc)) data ring/http-methods)])
(defn compile-result [[path data] {:keys [::default-options-endpoint expand] :as opts}]
(defn compile-result [[path data] {:keys [::default-options-endpoint expand meta-merge-fn] :as opts}]
(let [[top childs] (ring/group-keys data)
childs (cond-> childs
(and (not (:options childs)) (not (:handler top)) default-options-endpoint)
(assoc :options (expand default-options-endpoint opts)))
(and (not (:options childs)) (not (:handler top)) default-options-endpoint)
(assoc :options (expand default-options-endpoint opts)))
compile (fn [[path data] opts scope]
(interceptor/compile-result [path data] opts scope))
->endpoint (fn [p d m s]
@ -29,19 +29,19 @@
(assoc :method m))))
->methods (fn [any? data]
(reduce
(fn [acc method]
(cond-> acc
any? (assoc method (->endpoint path data method nil))))
(ring/map->Methods {})
ring/http-methods))]
(fn [acc method]
(cond-> acc
any? (assoc method (->endpoint path data method nil))))
(ring/map->Methods {})
ring/http-methods))]
(if-not (seq childs)
(->methods true top)
(reduce-kv
(fn [acc method data]
(let [data (meta-merge top data)]
(assoc acc method (->endpoint path data method method))))
(->methods (:handler top) data)
childs))))
(fn [acc method data]
(let [data ((or meta-merge-fn meta-merge) top data)]
(assoc acc method (->endpoint path data method method))))
(->methods (:handler top) data)
childs))))
(defn router
"Creates a [[reitit.core/Router]] from raw route data and optionally an options map with
@ -89,7 +89,8 @@
default-interceptors (->> interceptors
(map #(interceptor/into-interceptor % nil (r/options router))))
default-queue (interceptor/queue executor default-interceptors)
enrich-request (ring/create-enrich-request inject-match? inject-router?)]
enrich-request (ring/create-enrich-request inject-match? inject-router?)
enrich-default-request (ring/create-enrich-default-request inject-router?)]
{:name ::router
:enter (fn [{:keys [request] :as context}]
(if-let [match (r/match-by-path router (:uri request))]
@ -101,7 +102,9 @@
context (assoc context :request request)
queue (interceptor/queue executor (concat default-interceptors interceptors))]
(interceptor/enqueue executor context queue))
(interceptor/enqueue executor context default-queue)))
(let [request (enrich-default-request request router)
context (assoc context :request request)]
(interceptor/enqueue executor context default-queue))))
:leave (fn [context]
(if-not (:response context)
(assoc context :response (default-handler (:request context)))
@ -130,7 +133,7 @@
(assoc ::interceptor/queue (partial interceptor/queue executor))
(dissoc :data) ; data is already merged into routes
(cond-> (seq interceptors)
(update-in [:data :interceptors] (partial into (vec interceptors)))))
(update-in [:data :interceptors] (partial into (vec interceptors)))))
router (reitit.http/router (r/routes router) router-opts) ;; will re-compile the interceptors
enrich-request (ring/create-enrich-request inject-match? inject-router?)
enrich-default-request (ring/create-enrich-default-request inject-router?)]

View file

@ -1,7 +1,7 @@
(ns reitit.http.coercion
(:require [reitit.coercion :as coercion]
[reitit.spec :as rs]
[reitit.impl :as impl]))
[reitit.impl :as impl]
[reitit.spec :as rs]))
(defn coerce-request-interceptor
"Interceptor for pluggable request coercion.

View file

@ -1,8 +1,8 @@
(ns reitit.http.spec
(:require [clojure.spec.alpha :as s]
[reitit.ring.spec :as rrs]
[reitit.interceptor :as interceptor]
[reitit.exception :as exception]
[reitit.interceptor :as interceptor]
[reitit.ring.spec :as rrs]
[reitit.spec :as rs]))
;;
@ -22,5 +22,5 @@
[routes {:keys [spec ::rs/wrap] :or {spec ::data, wrap identity}}]
(when-let [problems (rrs/validate-route-data routes :interceptors wrap spec)]
(exception/fail!
::rs/invalid-route-data
{:problems problems})))
::rs/invalid-route-data
{:problems problems})))

View file

@ -0,0 +1 @@
../../../.clj-kondo/module_config.edn

View file

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

View file

@ -20,7 +20,9 @@
(defn- polish [ctx]
(-> ctx
(dissoc ::original ::previous :stack :queue)
(dissoc ::original ::previous :stack :queue
:io.pedestal.interceptor.chain/stack
:io.pedestal.interceptor.chain/queue)
(update :request dissoc ::r/match ::r/router)))
(defn- handle [name stage]
@ -36,16 +38,16 @@
[stages {:keys [enter leave error name] :as interceptor}]
(if (->> (select-keys interceptor stages) (vals) (keep identity) (seq))
(cond-> {:name ::diff}
(and enter (stages :enter)) (assoc :enter (handle name :enter))
(and leave (stages :leave)) (assoc :leave (handle name :leave))
(and error (stages :error)) (assoc :error (handle name :error)))))
(and enter (stages :enter)) (assoc :enter (handle name :enter))
(and leave (stages :leave)) (assoc :leave (handle name :leave))
(and error (stages :error)) (assoc :error (handle name :error)))))
(defn print-context-diffs
"A interceptor chain transformer that adds a context diff printer between all interceptors"
[interceptors]
(reduce
(fn [chain interceptor]
(into chain (keep identity [(diff-interceptor #{:leave :error} interceptor)
interceptor
(diff-interceptor #{:enter} interceptor)])))
[(diff-interceptor #{:enter :leave :error} {:enter identity})] interceptors))
(fn [chain interceptor]
(into chain (keep identity [(diff-interceptor #{:leave :error} interceptor)
interceptor
(diff-interceptor #{:enter} interceptor)])))
[(diff-interceptor #{:enter :leave :error} {:enter identity})] interceptors))

View file

@ -1,10 +1,10 @@
(ns reitit.http.interceptors.exception
(:require [reitit.coercion :as coercion]
[reitit.ring :as ring]
[clojure.spec.alpha :as s]
[clojure.string :as str])
(:import (java.time Instant)
(java.io PrintWriter)))
(:require [clojure.spec.alpha :as s]
[clojure.string :as str]
[reitit.coercion :as coercion]
[reitit.ring :as ring])
(:import (java.io PrintWriter Writer)
(java.time Instant)))
(s/def ::handlers (s/map-of any? fn?))
(s/def ::spec (s/keys :opt-un [::handlers]))
@ -25,17 +25,17 @@
error-handler (or (get handlers type)
(get handlers ex-class)
(some
(partial get handlers)
(descendants type))
(partial get handlers)
(descendants type))
(some
(partial get handlers)
(super-classes ex-class))
(partial get handlers)
(super-classes ex-class))
(get handlers ::default))]
(if-let [wrap (get handlers ::wrap)]
(wrap error-handler error request)
(error-handler error request))))
(defn print! [^PrintWriter writer & more]
(defn print! [^Writer writer & more]
(.write writer (str (str/join " " more) "\n")))
;;
@ -68,7 +68,7 @@
(defn wrap-log-to-console [handler ^Throwable e {:keys [uri request-method] :as req}]
(print! *out* (Instant/now) request-method (pr-str uri) "=>" (.getMessage e))
(.printStackTrace e ^PrintWriter *out*)
(.printStackTrace e (PrintWriter. ^Writer *out*))
(handler e req))
;;

View file

@ -1,8 +1,8 @@
(ns reitit.http.interceptors.multipart
(:require [reitit.coercion :as coercion]
(:require [clojure.spec.alpha :as s]
[reitit.coercion :as coercion]
[reitit.spec]
[ring.middleware.multipart-params :as multipart-params]
[clojure.spec.alpha :as s]
[spec-tools.core :as st])
(:import (java.io File)))
@ -18,14 +18,14 @@
(def temp-file-part
"Spec for file param created by ring.middleware.multipart-params.temp-file store."
(st/spec
{:spec (s/keys :req-un [::filename ::content-type ::tempfile ::size])
:swagger/type "file"}))
{:spec (s/keys :req-un [::filename ::content-type ::tempfile ::size])
:swagger/type "file"}))
(def bytes-part
"Spec for file param created by ring.middleware.multipart-params.byte-array store."
(st/spec
{:spec (s/keys :req-un [::filename ::content-type ::bytes])
:swagger/type "file"}))
{:spec (s/keys :req-un [::filename ::content-type ::bytes])
:swagger/type "file"}))
(defn- coerced-request [request coercers]
(if-let [coerced (if coercers (coercion/coerce-request coercers request))]
@ -49,7 +49,7 @@
:compile (fn [{:keys [parameters coercion]} opts]
(if-let [multipart (:multipart parameters)]
(let [parameter-coercion {:multipart (coercion/->ParameterCoercion
:multipart-params :string true true)}
:multipart-params :string true true)}
opts (assoc opts ::coercion/parameter-coercion parameter-coercion)
coercers (if multipart (coercion/request-coercers coercion parameters opts))]
{:data {:swagger {:consumes ^:replace #{"multipart/form-data"}}}

View file

@ -1,7 +1,7 @@
(ns reitit.http.interceptors.muuntaja
(:require [muuntaja.core :as m]
[muuntaja.interceptor]
[clojure.spec.alpha :as s]))
(:require [clojure.spec.alpha :as s]
[muuntaja.core :as m]
[muuntaja.interceptor]))
(s/def ::muuntaja m/muuntaja?)
(s/def ::spec (s/keys :opt-un [::muuntaja]))
@ -40,10 +40,10 @@
:compile (fn [{:keys [muuntaja parameters]} _]
(if-let [muuntaja (or muuntaja default-muuntaja)]
(merge
(stripped (muuntaja.interceptor/format-interceptor muuntaja))
(if (publish-swagger-data? parameters)
{:data {:swagger {:produces (displace (m/encodes muuntaja))
:consumes (displace (m/decodes muuntaja))}}}))))}))
(stripped (muuntaja.interceptor/format-interceptor muuntaja))
(if (publish-swagger-data? parameters)
{:data {:swagger {:produces (displace (m/encodes muuntaja))
:consumes (displace (m/decodes muuntaja))}}}))))}))
(defn format-negotiate-interceptor
"Interceptor for content-negotiation.
@ -87,9 +87,9 @@
:compile (fn [{:keys [muuntaja parameters]} _]
(if-let [muuntaja (or muuntaja default-muuntaja)]
(merge
(stripped (muuntaja.interceptor/format-request-interceptor muuntaja))
(when (publish-swagger-data? parameters)
{:data {:swagger {:consumes (displace (m/decodes muuntaja))}}}))))}))
(stripped (muuntaja.interceptor/format-request-interceptor muuntaja))
(when (publish-swagger-data? parameters)
{:data {:swagger {:consumes (displace (m/decodes muuntaja))}}}))))}))
(defn format-response-interceptor
"Interceptor for response formatting.
@ -112,6 +112,6 @@
:compile (fn [{:keys [muuntaja parameters]} _]
(if-let [muuntaja (or muuntaja default-muuntaja)]
(merge
(stripped (muuntaja.interceptor/format-response-interceptor muuntaja))
(when (publish-swagger-data? parameters)
{:data {:swagger {:produces (displace (m/encodes muuntaja))}}}))))}))
(stripped (muuntaja.interceptor/format-response-interceptor muuntaja))
(when (publish-swagger-data? parameters)
{:data {:swagger {:produces (displace (m/encodes muuntaja))}}}))))}))

View file

@ -0,0 +1 @@
../../../.clj-kondo/module_config.edn

Some files were not shown because too many files have changed in this diff Show more