Merge branch 'master' into rework-pr-589

This commit is contained in:
Joel Kaasinen 2023-08-30 08:29:06 +03:00
commit f1d26791fc
97 changed files with 1379 additions and 939 deletions

View file

@ -70,9 +70,24 @@ jobs:
run: ./scripts/test.sh cljs run: ./scripts/test.sh cljs
lint: lint:
name: Lint name: Lint cljdoc.edn
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Verify cljdoc.edn - name: Verify cljdoc.edn
run: curl -fsSL https://raw.githubusercontent.com/cljdoc/cljdoc/master/script/verify-cljdoc-edn | bash -s doc/cljdoc.edn run: curl -fsSL https://raw.githubusercontent.com/cljdoc/cljdoc/master/script/verify-cljdoc-edn | bash -s doc/cljdoc.edn
check-cljdoc:
name: Check cljdoc analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Setup Clojure
uses: DeLaGuardo/setup-clojure@11.0
with:
lein: 2.9.5
cli: 1.11.0.1100
- name: Install cljdoc analyzer
run: clojure -Ttools install io.github.cljdoc/cljdoc-analyzer '{:git/tag "RELEASE"}' :as cljdoc-analyzer
- name: CljDoc Check
run: ./scripts/cljdoc-check.sh

View file

@ -14,7 +14,44 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
## UNRELEASED ## UNRELEASED
## 0.7.0-alpha5 (2023-06-14)
* **BREAKING**: `compile-request-coercers` returns a map with `:data` and `:coerce` instead of plain `:coerce` function
* **BREAKING**: Parameter and Response schemas are merged into the route data vector - so they can be properly merged into the compiled result, fixes [#422](https://github.com/metosin/reitit/issues/422) - merging multiple schemas together works with `Malli` and `Schema`, partially with `data-spec` but not with `spec`.
* Fixed some module dependencies so Cljdoc can properly analyze all the modules
* Updated dependencies:
```clojure
[metosin/schema-tools "0.13.1"] is available but we use "0.13.0"
[com.fasterxml.jackson.core/jackson-core "2.15.1"] is available but we use "2.14.2"
[com.fasterxml.jackson.core/jackson-databind "2.15.1"] is available but we use "2.14.2"
```
**[compare](https://github.com/metosin/reitit/compare/0.7.0-alpha4...0.7.0-alpha5)**
## 0.7.0-alpha4 (2023-05-17)
* OpenAPI 3 parameter descriptions get populated from malli/spec/schema descriptions. [#612](https://github.com/metosin/reitit/issues/612)
**[compare](https://github.com/metosin/reitit/compare/0.7.0-alpha3...0.7.0-alpha4)**
## 0.7.0-alpha3 (2023-05-05)
* Compile `reitit.Trie` with Java 1.8 target for compatibility
**[compare](https://github.com/metosin/reitit/compare/0.7.0-alpha2...0.7.0-alpha3)**
## 0.7.0-alpha2 (2023-05-04)
* Fix reading fragment string on `Html5History` initialization
* Add fragment string parameter to reitit-frontend functions ([#604](https://github.com/metosin/reitit/pull/604))
**[compare](https://github.com/metosin/reitit/compare/0.7.0-alpha1...0.7.0-alpha2)**
## 0.7.0-alpha1 (2023-05-03)
* Initial Openapi3 support. See [docs](./doc/ring/openapi.md). Works for simple cases but might still have some rough edges. [#84](https://github.com/metosin/reitit/issues/84) * Initial Openapi3 support. See [docs](./doc/ring/openapi.md). Works for simple cases but might still have some rough edges. [#84](https://github.com/metosin/reitit/issues/84)
* Frontend: provide easy way to update current query params. [#600](https://github.com/metosin/reitit/issues/600)
* Updated dependencies: * Updated dependencies:
```clojure ```clojure
@ -23,6 +60,8 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[ring/ring-core "1.10.0"] is available but we use "1.9.6" [ring/ring-core "1.10.0"] is available but we use "1.9.6"
``` ```
**[compare](https://github.com/metosin/reitit/compare/0.6.0...0.7.0-alpha1)**
## 0.6.0 (2023-02-21) ## 0.6.0 (2023-02-21)
* Add reitit-frontend support for fragment string [#581](https://github.com/metosin/reitit/pull/581) * Add reitit-frontend support for fragment string [#581](https://github.com/metosin/reitit/pull/581)
@ -50,6 +89,8 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[com.fasterxml.jackson.core/jackson-databind "2.14.2"] is available but we use "2.14.1" [com.fasterxml.jackson.core/jackson-databind "2.14.2"] is available but we use "2.14.1"
``` ```
**[compare](https://github.com/metosin/reitit/compare/0.5.18...0.6.0)**
## 0.5.18 (2022-04-05) ## 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)) * 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))
@ -57,10 +98,13 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
* update jackson-databind for CVE-2020-36518 [#544](https://github.com/metosin/reitit/pull/544) * 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) * Balance parenthesis in docs [#547](https://github.com/metosin/reitit/pull/547)
**[compare](https://github.com/metosin/reitit/compare/0.5.17...0.5.18)**
## 0.5.17 (2022-03-10) ## 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) * FIX match-by-path is broken if there are no non-conflicting wildcard routes [#538](https://github.com/metosin/reitit/issues/538)
**[compare](https://github.com/metosin/reitit/compare/0.5.16...0.5.17)**
## 0.5.16 (2022-02-15) ## 0.5.16 (2022-02-15)

View file

@ -52,7 +52,7 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
All main modules bundled: All main modules bundled:
```clj ```clj
[metosin/reitit "0.6.0"] [metosin/reitit "0.7.0-alpha5"]
``` ```
Optionally, the parts can be required separately. Optionally, the parts can be required separately.
@ -143,7 +143,7 @@ Invalid request:
## More examples ## More examples
* [`reitit-ring` with coercion, swagger and default middleware](https://github.com/metosin/reitit/blob/master/examples/ring-swagger/src/example/server.clj) * [`reitit-ring` with coercion, swagger and default middleware](https://github.com/metosin/reitit/blob/master/examples/ring-malli-swagger/src/example/server.clj)
* [`reitit-frontend`, the easy way](https://github.com/metosin/reitit/blob/master/examples/frontend/src/frontend/core.cljs) * [`reitit-frontend`, the easy way](https://github.com/metosin/reitit/blob/master/examples/frontend/src/frontend/core.cljs)
* [`reitit-frontend` with Keechma-style controllers](https://github.com/metosin/reitit/blob/master/examples/frontend-controllers/src/frontend/core.cljs) * [`reitit-frontend` with Keechma-style controllers](https://github.com/metosin/reitit/blob/master/examples/frontend-controllers/src/frontend/core.cljs)
* [`reitit-http` with Pedestal](https://github.com/metosin/reitit/blob/master/examples/pedestal/src/example/server.clj) * [`reitit-http` with Pedestal](https://github.com/metosin/reitit/blob/master/examples/pedestal/src/example/server.clj)

View file

@ -24,6 +24,7 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
* `reitit-spec` [clojure.spec](https://clojure.org/about/spec) coercion * `reitit-spec` [clojure.spec](https://clojure.org/about/spec) coercion
* `reitit-schema` [Schema](https://github.com/plumatic/schema) coercion * `reitit-schema` [Schema](https://github.com/plumatic/schema) coercion
* `reitit-swagger` [Swagger2](https://swagger.io/) apidocs * `reitit-swagger` [Swagger2](https://swagger.io/) apidocs
* `reitit-openapi` OpenAPI 3 apidocs
* `reitit-swagger-ui` Integrated [Swagger UI](https://github.com/swagger-api/swagger-ui). * `reitit-swagger-ui` Integrated [Swagger UI](https://github.com/swagger-api/swagger-ui).
* `reitit-frontend` Tools for [frontend routing](frontend/basics.md) * `reitit-frontend` Tools for [frontend routing](frontend/basics.md)
* `reitit-http` http-routing with Pedestal-style Interceptors * `reitit-http` http-routing with Pedestal-style Interceptors
@ -40,7 +41,7 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
All bundled: All bundled:
```clj ```clj
[metosin/reitit "0.6.0"] [metosin/reitit "0.7.0-alpha5"]
``` ```
Optionally, the parts can be required separately. Optionally, the parts can be required separately.

View file

@ -3,7 +3,7 @@
Routers can be configured via options. The following options are available for the `reitit.core/router`: Routers can be configured via options. The following options are available for the `reitit.core/router`:
| key | description | key | description
|---------------|------------- |-----------------|-------------
| `:path` | Base-path for routes | `:path` | Base-path for routes
| `:routes` | Initial resolved routes (default `[]`) | `:routes` | Initial resolved routes (default `[]`)
| `:data` | Initial route data (default `{}`) | `:data` | Initial route data (default `{}`)
@ -11,9 +11,12 @@ Routers can be configured via options. The following options are available for t
| `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon}) | `: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`) | `: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` | `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
| `:meta-merge` | 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 | `:compile` | Function of `route opts => result` to compile a route handler
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects | `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes | `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes
| `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`) | `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`)
| `:meta-merge` | Function of `left right => merged` to merge route-data (default `meta-merge.core/meta-merge`)
| `:update-paths` | Sequence of Vectors with elements `update-path` and `function`, used to preprocess route data
| `:router` | Function of `routes opts => router` to override the actual router implementation | `:router` | Function of `routes opts => router` to override the actual router implementation

View file

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

View file

@ -12,7 +12,8 @@
metosin/reitit-swagger-ui metosin/reitit-swagger-ui
metosin/reitit-frontend metosin/reitit-frontend
metosin/reitit-sieppari metosin/reitit-sieppari
metosin/reitit-pedestal] metosin/reitit-pedestal
fi.metosin/reitit-openapi]
:cljdoc.doc/tree :cljdoc.doc/tree
[["Introduction" {:file "doc/README.md"}] [["Introduction" {:file "doc/README.md"}]
["Basics" {} ["Basics" {}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -160,14 +160,14 @@ You can also specify request and response body schemas per content-type. The syn
(ring/router (ring/router
["/api" ["/api"
["/example" {:post {:coercion reitit.coercion.schema/coercion ["/example" {:post {:coercion reitit.coercion.schema/coercion
:parameters {:request {:content {"application/json" {:y s/Int} :request {:content {"application/json" {:schema {:y s/Int}}
"application/edn" {:z s/Int}} "application/edn" {:schema {:z s/Int}}}
;; default if no content-type matches: ;; default if no content-type matches:
:body {:yy s/Int}}} :body {:yy s/Int}}
:responses {200 {:content {"application/json" {:w s/Int} :responses {200 {:content {"application/json" {:schema {:w s/Int}}
"application/edn" {:x s/Int}} "application/edn" {:schema {:x s/Int}}}
;; default if no content-type matches: ;; default if no content-type matches:
:body {:ww s/Int}} :body {:ww s/Int}}}
:handler ...}}]] :handler ...}}]]
{:data {:middleware [rrc/coerce-exceptions-middleware {:data {:middleware [rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware rrc/coerce-request-middleware

View file

@ -1,7 +1,7 @@
# Default Middleware # Default Middleware
```clj ```clj
[metosin/reitit-middleware "0.6.0"] [metosin/reitit-middleware "0.7.0-alpha5"]
``` ```
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.
@ -59,4 +59,4 @@ Partial sample output:
## Example app ## Example app
See an example app with the default middleware in action: https://github.com/metosin/reitit/blob/master/examples/ring-swagger/src/example/server.clj. See an example app with the default middleware in action: <https://github.com/metosin/reitit/blob/master/examples/ring-malli-swagger/src/example/server.clj>.

View file

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

View file

@ -5,8 +5,11 @@
Reitit can generate [OpenAPI 3.1.0](https://spec.openapis.org/oas/v3.1.0) Reitit can generate [OpenAPI 3.1.0](https://spec.openapis.org/oas/v3.1.0)
documentation. The feature works similarly to [Swagger documentation](swagger.md). documentation. The feature works similarly to [Swagger documentation](swagger.md).
The [http-swagger](../../examples/http-swagger) and The
[ring-malli-swagger](../../examples/ring-malli-swagger) examples also [ring-malli-swagger](../../examples/ring-malli-swagger)
and
[ring-spec-swagger](../../examples/ring-spec-swagger)
examples also
have OpenAPI documentation. have OpenAPI documentation.
## OpenAPI data ## OpenAPI data
@ -31,6 +34,76 @@ Coercion keys also contribute to the docs:
Use `:request` parameter coercion (instead of `:body`) to unlock per-content-type coercions. See [Coercion](coercion.md). Use `:request` parameter coercion (instead of `:body`) to unlock per-content-type coercions. See [Coercion](coercion.md).
## Annotating schemas
You can use malli properties, schema-tools data or spec-tools data to
annotate your models with examples, descriptions and defaults that
show up in the OpenAPI spec.
Malli:
```clj
["/plus"
{:post
{:parameters
{:body [:map
[:x
{:title "X parameter"
:description "Description for X parameter"
:json-schema/default 42}
int?]
[:y int?]]}}}]
```
Schema:
```clj
["/plus"
{:post
{:parameters
{:body {:x (schema-tools.core/schema s/Num {:description "Description for X parameter"
:openapi/example 13
:openapi/default 42})
:y int?}}}}]
```
Spec:
```clj
["/plus"
{:post
{:parameters
{:body (spec-tools.data-spec/spec ::foo
{:x (schema-tools.core/spec {:spec int?
:description "Description for X parameter"
:openapi/example 13
:openapi/default 42})
:y int?}}}}}]
```
## Custom OpenAPI data
The `:openapi` route data key can be used to add top-level or
route-level information to the generated OpenAPI spec. This is useful
for providing `"securitySchemes"`, `"examples"` or other OpenAPI keys
that are not generated automatically by reitit.
```clj
["/foo"
{:post {:parameters {:body {:name string? :age int?}}
:openapi {:requestBody
{:content
{"application/json"
{:examples {"Pyry" {:summary "Pyry, 45y"
:value {:name "Pyry" :age 45}}
"Cat" {:summary "Cat, 8y"
:value {:name "Cat" :age 8}}}}}}}
...}}]
```
See [the ring-malli-swagger example](../../examples/ring-malli-swagger) for
working examples of `"securitySchemes"` and `"examples"`.
## OpenAPI spec ## OpenAPI spec
Serving the OpenAPI specification is handled by `reitit.openapi/create-openapi-handler`. It takes no arguments and returns a ring handler which collects at request-time data from all routes and returns an OpenAPI specification as Clojure data, to be encoded by a response formatter. Serving the OpenAPI specification is handled by `reitit.openapi/create-openapi-handler`. It takes no arguments and returns a ring handler which collects at request-time data from all routes and returns an OpenAPI specification as Clojure data, to be encoded by a response formatter.

View file

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

View file

@ -1,7 +1,7 @@
# Swagger Support # Swagger Support
``` ```
[metosin/reitit-swagger "0.6.0"] [metosin/reitit-swagger "0.7.0-alpha5"]
``` ```
Reitit supports [Swagger2](https://swagger.io/) documentation, thanks to [schema-tools](https://github.com/metosin/schema-tools) and [spec-tools](https://github.com/metosin/spec-tools). Documentation is extracted from route definitions, coercion `:parameters` and `:responses` and from a set of new documentation keys. Reitit supports [Swagger2](https://swagger.io/) documentation, thanks to [schema-tools](https://github.com/metosin/schema-tools) and [spec-tools](https://github.com/metosin/spec-tools). Documentation is extracted from route definitions, coercion `:parameters` and `:responses` and from a set of new documentation keys.
@ -47,7 +47,7 @@ If you need to post-process the generated spec, just wrap the handler with a cus
[Swagger-ui](https://github.com/swagger-api/swagger-ui) is a user interface to visualize and interact with the Swagger specification. To make things easy, there is a pre-integrated version of the swagger-ui as a separate module. [Swagger-ui](https://github.com/swagger-api/swagger-ui) is a user interface to visualize and interact with the Swagger specification. To make things easy, there is a pre-integrated version of the swagger-ui as a separate module.
``` ```
[metosin/reitit-swagger-ui "0.6.0"] [metosin/reitit-swagger-ui "0.7.0-alpha5"]
``` ```
`reitit.swagger-ui/create-swagger-ui-handler` can be used to create a ring-handler to serve the swagger-ui. It accepts the following options: `reitit.swagger-ui/create-swagger-ui-handler` can be used to create a ring-handler to serve the swagger-ui. It accepts the following options:
@ -145,7 +145,7 @@ Another way to serve the swagger-ui is using the [default handler](default_handl
* missed routes are handled by `create-default-handler` * missed routes are handled by `create-default-handler`
* served via [ring-jetty](https://github.com/ring-clojure/ring/tree/master/ring-jetty-adapter) * served via [ring-jetty](https://github.com/ring-clojure/ring/tree/master/ring-jetty-adapter)
Whole example project is in [`/examples/ring-swagger`](https://github.com/metosin/reitit/tree/master/examples/ring-swagger). Whole example project is in [`/examples/ring-spec-swagger`](https://github.com/metosin/reitit/tree/master/examples/ring-spec-swagger).
```clj ```clj
(ns example.server (ns example.server

View file

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

View file

@ -44,7 +44,3 @@ Coercion with Malli and Swagger generation.
## ring-spec-swagger ## ring-spec-swagger
Coercion with Spec and Swagger generation. 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" :description "Reitit Buddy Auth App"
:dependencies [[org.clojure/clojure "1.10.1"] :dependencies [[org.clojure/clojure "1.10.1"]
[ring/ring-jetty-adapter "1.8.1"] [ring/ring-jetty-adapter "1.8.1"]
[metosin/reitit "0.6.0"] [metosin/reitit "0.7.0-alpha5"]
[buddy "2.0.0"]] [buddy "2.0.0"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

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

View file

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

View file

@ -18,7 +18,7 @@
[:div [:div
[:ul [:ul
[:li [:a {:href (rfe/href ::item {:id 1})} "Item 1"]] [:li [:a {:href (rfe/href ::item {:id 1})} "Item 1"]]
[:li [:a {:href (rfe/href ::item {:id 2} {:foo "bar"})} "Item 2"]]] [:li [:a {:href (rfe/href ::item {:id 2} {:foo "bar"} "zzz")} "Item 2"]]]
(when id (when id
[:h2 "Selected item " id]) [:h2 "Selected item " id])
[:p "Query params: " [:pre (pr-str query)]] [:p "Query params: " [:pre (pr-str query)]]

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.10.0"] :dependencies [[org.clojure/clojure "1.10.0"]
[ring/ring-jetty-adapter "1.7.1"] [ring/ring-jetty-adapter "1.7.1"]
[aleph "0.4.7-alpha5"] [aleph "0.4.7-alpha5"]
[metosin/reitit "0.6.0"] [metosin/reitit "0.7.0-alpha5"]
[metosin/ring-swagger-ui "5.0.0-alpha.0"]] [metosin/ring-swagger-ui "5.0.0-alpha.0"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -77,7 +77,7 @@
{:get {:summary "downloads a file" {:get {:summary "downloads a file"
:swagger {:produces ["image/png"]} :swagger {:produces ["image/png"]}
:responses {200 {:description "an image" :responses {200 {:description "an image"
:content {"image/png" any?}}} :content {"image/png" {:schema any?}}}}
:handler (fn [_] :handler (fn [_]
{:status 200 {:status 200
:headers {"Content-Type" "image/png"} :headers {"Content-Type" "image/png"}
@ -112,6 +112,22 @@
{:status 200 {:status 200
:body {:total (+ x y)}})} :body {:total (+ x y)}})}
:post {:summary "plus with data-spec body parameters" :post {:summary "plus with data-spec body parameters"
;; OpenAPI3 named examples for request & response
:openapi {:requestBody
{:content
{"application/json"
{:examples {"add-one-one" {:summary "1+1"
:value {:x 1 :y 1}}
"add-one-two" {:summary "1+2"
:value {:x 1 :y 2}}}}}}
:responses
{200
{:content
{"application/json"
{:examples {"two" {:summary "2"
:value {:total 2}}
"three" {:summary "3"
:value {:total 3}}}}}}}}
:parameters {:body {:x int?, :y int?}} :parameters {:body {:x int?, :y int?}}
:responses {200 {:body {:total int?}}} :responses {200 {:body {:total int?}}}
:handler (fn [{{{:keys [x y]} :body} :parameters}] :handler (fn [{{{:keys [x y]} :body} :parameters}]

View file

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

View file

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

View file

@ -3,7 +3,7 @@
:dependencies [[org.clojure/clojure "1.10.0"] :dependencies [[org.clojure/clojure "1.10.0"]
[io.pedestal/pedestal.service "0.5.5"] [io.pedestal/pedestal.service "0.5.5"]
[io.pedestal/pedestal.jetty "0.5.5"] [io.pedestal/pedestal.jetty "0.5.5"]
[metosin/reitit-malli "0.6.0"] [metosin/reitit-malli "0.7.0-alpha5"]
[metosin/reitit-pedestal "0.6.0"] [metosin/reitit-pedestal "0.7.0-alpha5"]
[metosin/reitit "0.6.0"]] [metosin/reitit "0.7.0-alpha5"]]
:repl-options {:init-ns server}) :repl-options {:init-ns server})

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
# reitit-ring, malli, swagger # reitit-ring, malli, swagger, openapi 3
## Usage ## Usage

View file

@ -3,7 +3,7 @@
:dependencies [[org.clojure/clojure "1.10.0"] :dependencies [[org.clojure/clojure "1.10.0"]
[metosin/jsonista "0.2.6"] [metosin/jsonista "0.2.6"]
[ring/ring-jetty-adapter "1.7.1"] [ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.6.0"] [metosin/reitit "0.7.0-alpha5"]
[metosin/ring-swagger-ui "5.0.0-alpha.0"]] [metosin/ring-swagger-ui "5.0.0-alpha.0"]]
:repl-options {:init-ns example.server} :repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]]}}) :profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]]}})

View file

@ -27,6 +27,10 @@
:swagger {:info {:title "my-api" :swagger {:info {:title "my-api"
:description "swagger docs with [malli](https://github.com/metosin/malli) and reitit-ring" :description "swagger docs with [malli](https://github.com/metosin/malli) and reitit-ring"
:version "0.0.1"} :version "0.0.1"}
;; used in /secure APIs below
:securityDefinitions {"auth" {:type :apiKey
:in :header
:name "Example-Api-Key"}}
:tags [{:name "files", :description "file api"} :tags [{:name "files", :description "file api"}
{:name "math", :description "math api"}]} {:name "math", :description "math api"}]}
:handler (swagger/create-swagger-handler)}}] :handler (swagger/create-swagger-handler)}}]
@ -34,7 +38,11 @@
{:get {:no-doc true {:get {:no-doc true
:openapi {:info {:title "my-api" :openapi {:info {:title "my-api"
:description "openapi3 docs with [malli](https://github.com/metosin/malli) and reitit-ring" :description "openapi3 docs with [malli](https://github.com/metosin/malli) and reitit-ring"
:version "0.0.1"}} :version "0.0.1"}
;; used in /secure APIs below
:components {:securitySchemes {"auth" {:type :apiKey
:in :header
:name "Example-Api-Key"}}}}
:handler (openapi/create-openapi-handler)}}] :handler (openapi/create-openapi-handler)}}]
["/files" ["/files"
@ -53,7 +61,7 @@
{:get {:summary "downloads a file" {:get {:summary "downloads a file"
:swagger {:produces ["image/png"]} :swagger {:produces ["image/png"]}
:responses {200 {:description "an image" :responses {200 {:description "an image"
:content {"image/png" any?}}} :content {"image/png" {:schema string?}}}}
:handler (fn [_] :handler (fn [_]
{:status 200 {:status 200
:headers {"Content-Type" "image/png"} :headers {"Content-Type" "image/png"}
@ -85,10 +93,42 @@
:json-schema/default 42} :json-schema/default 42}
int?] int?]
[:y int?]]} [:y int?]]}
;; OpenAPI3 named examples for request & response
:openapi {:requestBody
{:content
{"application/json"
{:examples {"add-one-one" {:summary "1+1"
:value {:x 1 :y 1}}
"add-one-two" {:summary "1+2"
:value {:x 1 :y 2}}}}}}
:responses
{200
{:content
{"application/json"
{:examples {"two" {:summary "2"
:value {:total 2}}
"three" {:summary "3"
:value {:total 3}}}}}}}}
:responses {200 {:body [:map [:total int?]]}} :responses {200 {:body [:map [:total int?]]}}
:handler (fn [{{{:keys [x y]} :body} :parameters}] :handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200 {:status 200
:body {:total (+ x y)}})}}]]] :body {:total (+ x y)}})}}]]
["/secure"
{:tags ["secure"]
:openapi {:security [{"auth" []}]}
:swagger {:security [{"auth" []}]}}
["/get"
{:get {:summary "endpoint authenticated with a header"
:responses {200 {:body [:map [:secret :string]]}
401 {:body [:map [:error :string]]}}
:handler (fn [request]
;; In a real app authentication would be handled by middleware
(if (= "secret" (get-in request [:headers "example-api-key"]))
{:status 200
:body {:secret "I am a marmot"}}
{:status 401
:body {:error "unauthorized"}}))}}]]]
{;;:reitit.middleware/transform dev/print-request-diffs ;; pretty diffs {;;:reitit.middleware/transform dev/print-request-diffs ;; pretty diffs
;;:validate spec/validate ;; enable spec validation for route data ;;:validate spec/validate ;; enable spec validation for route data

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View file

@ -1,4 +1,4 @@
# reitit-ring, clojure.spec, swagger # reitit-ring, clojure.spec, swagger, openapi 3
## Usage ## Usage
@ -7,6 +7,10 @@
(start) (start)
``` ```
- Swagger spec served at <http://localhost:3000/swagger.json>
- Openapi spec served at <http://localhost:3000/openapi.json>
- Swagger UI served at <http://localhost:3000/>
To test the endpoints using [httpie](https://httpie.org/): To test the endpoints using [httpie](https://httpie.org/):
```bash ```bash

View file

@ -2,6 +2,7 @@
:description "Reitit Ring App with Swagger" :description "Reitit Ring App with Swagger"
:dependencies [[org.clojure/clojure "1.10.0"] :dependencies [[org.clojure/clojure "1.10.0"]
[ring/ring-jetty-adapter "1.7.1"] [ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.6.0"]] [metosin/reitit "0.7.0-alpha5"]
[metosin/ring-swagger-ui "5.0.0-alpha.0"]]
:repl-options {:init-ns example.server} :repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]]}}) :profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]]}})

View file

@ -1,6 +1,7 @@
(ns example.server (ns example.server
(:require [reitit.ring :as ring] (:require [reitit.ring :as ring]
[reitit.coercion.spec] [reitit.coercion.spec]
[reitit.openapi :as openapi]
[reitit.swagger :as swagger] [reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui] [reitit.swagger-ui :as swagger-ui]
[reitit.ring.coercion :as coercion] [reitit.ring.coercion :as coercion]
@ -44,9 +45,15 @@
{:get {:no-doc true {:get {:no-doc true
:swagger {:info {:title "my-api"}} :swagger {:info {:title "my-api"}}
:handler (swagger/create-swagger-handler)}}] :handler (swagger/create-swagger-handler)}}]
["/openapi.json"
{:get {:no-doc true
:openapi {:info {:title "my-api"
:description "openapi3-docs with reitit-http"
:version "0.0.1"}}
:handler (openapi/create-openapi-handler)}}]
["/files" ["/files"
{:swagger {:tags ["files"]}} {:tags ["files"]}
["/upload" ["/upload"
{:post {:summary "upload a file" {:post {:summary "upload a file"
@ -60,6 +67,8 @@
["/download" ["/download"
{:get {:summary "downloads a file" {:get {:summary "downloads a file"
:swagger {:produces ["image/png"]} :swagger {:produces ["image/png"]}
:responses {200 {:description "an image"
:content {"image/png" {:schema string?}}}}
:handler (fn [_] :handler (fn [_]
{:status 200 {:status 200
:headers {"Content-Type" "image/png"} :headers {"Content-Type" "image/png"}
@ -67,7 +76,7 @@
(io/resource "reitit.png"))})}}]] (io/resource "reitit.png"))})}}]]
["/math" ["/math"
{:swagger {:tags ["math"]}} {:tags ["math"]}
["/plus" ["/plus"
{:get {:summary "plus with spec query parameters" {:get {:summary "plus with spec query parameters"
@ -111,6 +120,9 @@
(swagger-ui/create-swagger-ui-handler (swagger-ui/create-swagger-ui-handler
{:path "/" {:path "/"
:config {:validatorUrl nil :config {:validatorUrl nil
:urls [{:name "swagger" :url "swagger.json"}
{:name "openapi" :url "openapi.json"}]
:urls.primaryName "openapi"
:operationsSorter "alpha"}}) :operationsSorter "alpha"}})
(ring/create-default-handler)))) (ring/create-default-handler))))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View file

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

View file

@ -0,0 +1 @@
{}

View file

@ -1,23 +0,0 @@
# Ring with Swagger example
## 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/spec/plus x:=1 y:=20
http GET :3000/swagger.json
```
<img src="https://raw.githubusercontent.com/metosin/reitit/master/examples/ring-swagger/swagger.png" />
## License
Copyright © 2017-2018 Metosin Oy

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 KiB

View file

@ -1,105 +0,0 @@
(ns example.server
(:require [reitit.ring :as ring]
[reitit.coercion.spec]
[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]
;; Uncomment to use
; [reitit.ring.middleware.dev :as dev]
[ring.adapter.jetty :as jetty]
[muuntaja.core :as m]
[clojure.java.io :as io]))
(def app
(ring/ring-handler
(ring/router
[["/swagger.json"
{:get {:no-doc true
:swagger {:info {:title "my-api"
:description "with reitit-ring"}}
:handler (swagger/create-swagger-handler)}}]
["/files"
{:swagger {:tags ["files"]}}
["/upload"
{:post {:summary "upload a file"
:parameters {:multipart {:file multipart/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 spec query parameters"
: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?}}
: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.spec/coercion
: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/create-exception-middleware
{::exception/default (partial exception/wrap-log-to-console exception/default-handler)})
;; 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.

Before

Width:  |  Height:  |  Size: 228 KiB

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-core "0.6.0" (defproject metosin/reitit-core "0.7.0-alpha5"
:description "Snappy data-driven router for Clojure(Script)" :description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"
@ -10,4 +10,5 @@
:parent-project {:path "../../project.clj" :parent-project {:path "../../project.clj"
:inherit [:deploy-repositories :managed-dependencies]} :inherit [:deploy-repositories :managed-dependencies]}
:java-source-paths ["java-src"] :java-source-paths ["java-src"]
:javac-options ["-Xlint:unchecked" "-target" "1.8" "-source" "1.8"]
:dependencies [[meta-merge]]) :dependencies [[meta-merge]])

View file

@ -13,6 +13,8 @@
(-get-name [this] "Keyword name for the coercion") (-get-name [this] "Keyword name for the coercion")
(-get-options [this] "Coercion options") (-get-options [this] "Coercion options")
(-get-apidocs [this specification data] "Returns api documentation") (-get-apidocs [this specification data] "Returns api documentation")
;; TODO doc options:
(-get-model-apidocs [this specification model options] "Convert model into a format that can be used in api docs")
(-compile-model [this model name] "Compiles a model") (-compile-model [this model name] "Compiles a model")
(-open-model [this model] "Returns a new model which allows extra keys in maps") (-open-model [this model] "Returns a new model which allows extra keys in maps")
(-encode-error [this error] "Converts error in to a serializable format") (-encode-error [this error] "Converts error in to a serializable format")
@ -37,7 +39,6 @@
(def ^:no-doc default-parameter-coercion (def ^:no-doc default-parameter-coercion
{:query (->ParameterCoercion :query-params :string true true) {:query (->ParameterCoercion :query-params :string true true)
:body (->ParameterCoercion :body-params :body false false) :body (->ParameterCoercion :body-params :body false false)
:request (->ParameterCoercion :body-params :request false false)
:form (->ParameterCoercion :form-params :string true true) :form (->ParameterCoercion :form-params :string true true)
:header (->ParameterCoercion :headers :string true true) :header (->ParameterCoercion :headers :string true true)
:path (->ParameterCoercion :path-params :string true true) :path (->ParameterCoercion :path-params :string true true)
@ -83,25 +84,45 @@
value) value)
;; TODO: support faster key walking, walk/keywordize-keys is quite slow... ;; TODO: support faster key walking, walk/keywordize-keys is quite slow...
(defn request-coercer [coercion type model {::keys [extract-request-format parameter-coercion serialize-failed-result] (defn request-coercer [coercion type model {::keys [extract-request-format parameter-coercion serialize-failed-result skip]
:or {extract-request-format extract-request-format-default :or {extract-request-format extract-request-format-default
parameter-coercion default-parameter-coercion}}] parameter-coercion default-parameter-coercion
skip #{}}}]
(if coercion (if coercion
(if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)] (when-let [{:keys [keywordize? open? in style]} (parameter-coercion type)]
(when-not (skip style)
(let [transform (comp (if keywordize? walk/keywordize-keys identity) in) (let [transform (comp (if keywordize? walk/keywordize-keys identity) in)
->open (if open? #(-open-model coercion %) identity) ->open (if open? #(-open-model coercion %) identity)
format-schema-pairs (if (= :request style) coercer (-request-coercer coercion style (->open model))]
(conj (:content model) [:default (:body model)]) (when coercer
[[:default model]])
format->coercer (some->> (for [[format schema] format-schema-pairs
:when schema]
[format (-request-coercer coercion (case style :request :body style) (->open schema))])
(filter second)
(seq)
(into {}))]
(when format->coercer
(fn [request] (fn [request]
(let [value (transform request) (let [value (transform request)
format (extract-request-format request)
result (coercer value format)]
(if (error? result)
(request-coercion-failed! result coercion value in request serialize-failed-result)
result)))))))))
(defn get-default-schema [request-or-response]
(or (-> request-or-response :content :default :schema)
(:body request-or-response)))
(defn get-default [request-or-response]
(or (-> request-or-response :content :default)
(some->> request-or-response :body (assoc {} :schema))))
(defn content-request-coercer [coercion {:keys [content body]} {::keys [extract-request-format serialize-failed-result]
:or {extract-request-format extract-request-format-default}}]
(when coercion
(let [in :body-params
format->coercer (some->> (concat (when body
[[:default (-request-coercer coercion :body body)]])
(for [[format {:keys [schema]}] content, :when schema]
[format (-request-coercer coercion :body schema)]))
(filter second) (seq) (into (array-map)))]
(when format->coercer
(fn [request]
(let [value (in request)
format (extract-request-format request) format (extract-request-format request)
coercer (or (format->coercer format) coercer (or (format->coercer format)
(format->coercer :default) (format->coercer :default)
@ -109,7 +130,7 @@
result (coercer value format)] result (coercer value format)]
(if (error? result) (if (error? result)
(request-coercion-failed! result coercion value in request serialize-failed-result) (request-coercion-failed! result coercion value in request serialize-failed-result)
result)))))))) result)))))))
(defn extract-response-format-default [request _] (defn extract-response-format-default [request _]
(-> request :muuntaja/response :format)) (-> request :muuntaja/response :format))
@ -117,17 +138,18 @@
(defn response-coercer [coercion {:keys [content body]} {:keys [extract-response-format serialize-failed-result] (defn response-coercer [coercion {:keys [content body]} {:keys [extract-response-format serialize-failed-result]
:or {extract-response-format extract-response-format-default}}] :or {extract-response-format extract-response-format-default}}]
(if coercion (if coercion
(let [per-format-coercers (some->> (for [[format schema] content] (let [format->coercer (some->> (concat (when body
[format (-response-coercer coercion schema)]) [[:default (-response-coercer coercion body)]])
(filter second) (for [[format {:keys [schema]}] content, :when schema]
(seq) [format (-response-coercer coercion schema)]))
(into {})) (filter second) (seq) (into (array-map)))]
default (when body (-response-coercer coercion body))] (when format->coercer
(when (or per-format-coercers default)
(fn [request response] (fn [request response]
(let [format (extract-response-format request response) (let [format (extract-response-format request response)
value (:body response) value (:body response)
coercer (get per-format-coercers format (or default -identity-coercer)) coercer (or (format->coercer format)
(format->coercer :default)
-identity-coercer)
result (coercer value format)] result (coercer value format)]
(if (error? result) (if (error? result)
(response-coercion-failed! result coercion value request response serialize-failed-result) (response-coercion-failed! result coercion value request response serialize-failed-result)
@ -151,52 +173,23 @@
(impl/fast-assoc response :body (coercer request response)) (impl/fast-assoc response :body (coercer request response))
response))) response)))
(defn request-coercers [coercion parameters opts] (defn request-coercers
(some->> (for [[k v] parameters ([coercion parameters opts]
:when v] (some->> (for [[k v] parameters, :when v]
[k (request-coercer coercion k v opts)]) [k (request-coercer coercion k v opts)])
(filter second) (filter second) (seq) (into {})))
(seq) ([coercion parameters route-request opts]
(into {}))) (let [crc (when route-request (some->> (content-request-coercer coercion route-request opts) (array-map :request)))
rcs (request-coercers coercion parameters (cond-> opts route-request (assoc ::skip #{:body})))]
(if (and crc rcs) (into crc (vec rcs)) (or crc rcs)))))
(defn response-coercers [coercion responses opts] (defn response-coercers [coercion responses opts]
(some->> (for [[status model] responses] (some->> (for [[status model] responses]
[status (response-coercer coercion model opts)]) [status (response-coercer coercion model opts)])
(filter second) (filter second) (seq) (into {})))
(seq)
(into {})))
;;
;; api-docs
;;
(defn -warn-unsupported-coercions [{:keys [parameters responses] :as data}]
(when (:request parameters)
(println "WARNING [reitit.coercion]: swagger apidocs don't support :request coercion"))
(when (some :content (vals responses))
(println "WARNING [reitit.coercion]: swagger apidocs don't support :responses :content coercion")))
(defn get-apidocs [coercion specification data]
(let [swagger-parameter {:query :query
:body :body
:form :formData
:header :header
:path :path
:multipart :formData}]
(case specification
:openapi (-get-apidocs coercion specification data)
:swagger (do
(-warn-unsupported-coercions data)
(->> (update
data
:parameters
(fn [parameters]
(->> parameters
(map (fn [[k v]] [(swagger-parameter k) v]))
(filter first)
(into {}))))
(-get-apidocs coercion specification))))))
(defn -compile-parameters [data coercion]
(impl/path-update data [[[:parameters any?] #(-compile-model coercion % nil)]]))
;; ;;
;; integration ;; integration
@ -204,17 +197,29 @@
(defn compile-request-coercers (defn compile-request-coercers
"A router :compile implementation which reads the `:parameters` "A router :compile implementation which reads the `:parameters`
and `:coercion` data to create compiled coercers into Match under and `:coercion` data to both compile the schemas and create compiled coercers
`:result. A pre-requisite to use [[coerce!]]." into Match under `:result with the following keys:
[[_ {:keys [parameters coercion]}] opts]
| key | description
| ----------|-------------
| `:data` | data with compiled schemas
| `:coerce` | function of `Match -> coerced parameters` to coerce parameters
A pre-requisite to use [[coerce!]].
NOTE: this is not needed with ring/http, where the coercion compilation is
managed in the request coercion middleware/interceptors."
[[_ {:keys [parameters coercion] :as data}] opts]
(if (and parameters coercion) (if (and parameters coercion)
(request-coercers coercion parameters opts))) (let [{:keys [parameters] :as data} (-compile-parameters data coercion)]
{:data data
:coerce (request-coercers coercion parameters opts)})))
(defn coerce! (defn coerce!
"Returns a map of coerced input parameters using pre-compiled "Returns a map of coerced input parameters using pre-compiled coercers in `Match`
coercers under `:result` (provided by [[compile-request-coercers]]. under path `[:result :coerce]` (provided by [[compile-request-coercers]].
Throws `ex-info` if parameters can't be coerced Throws `ex-info` if parameters can't be coerced. If coercion or parameters
If coercion or parameters are not defined, return `nil`" are not defined, returns `nil`"
[match] [match]
(if-let [coercers (:result match)] (if-let [coercers (-> match :result :coerce)]
(coerce-request coercers match))) (coerce-request coercers match)))

View file

@ -307,6 +307,7 @@
:coerce (fn coerce [route _] route) :coerce (fn coerce [route _] route)
:compile (fn compile [[_ {:keys [handler]}] _] handler) :compile (fn compile [[_ {:keys [handler]}] _] handler)
:exception exception/exception :exception exception/exception
:update-paths [[[:parameters any?] impl/accumulate]]
:conflicts (fn throw! [conflicts] (exception/fail! :path-conflicts conflicts))}) :conflicts (fn throw! [conflicts] (exception/fail! :path-conflicts conflicts))})
(defn router (defn router
@ -315,7 +316,7 @@
are available: are available:
| key | description | key | description
| -------------|------------- | ----------------|-------------
| `:path` | Base-path for routes | `:path` | Base-path for routes
| `:routes` | Initial resolved routes (default `[]`) | `:routes` | Initial resolved routes (default `[]`)
| `:data` | Initial route data (default `{}`) | `:data` | Initial route data (default `{}`)
@ -327,6 +328,8 @@
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects | `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes | `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes
| `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`) | `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`)
| `:meta-merge` | Function of `left right => merged` to merge route-data (default `meta-merge.core/meta-merge`)
| `:update-paths` | Sequence of Vectors with elements `update-path` and `function`, used to preprocess route data
| `:router` | Function of `routes opts => router` to override the actual router implementation" | `:router` | Function of `routes opts => router` to override the actual router implementation"
([raw-routes] ([raw-routes]
(router raw-routes {})) (router raw-routes {}))

View file

@ -9,6 +9,49 @@
(:import (java.net URLEncoder URLDecoder) (:import (java.net URLEncoder URLDecoder)
(java.util HashMap Map)))) (java.util HashMap Map))))
;;
;; path-update
;;
(defn -match [path path-map]
(letfn [(match [x f] (if (fn? f) (f x) (= x f)))]
(reduce
(fn [_ [ps f]]
(when (and (>= (count path) (count ps)) (every? identity (map match path ps)))
(reduced f)))
nil path-map)))
(defn -path-vals [m path-map]
(letfn [(-path-vals [l p m]
(reduce
(fn [l [k v]]
(let [p' (conj p k)
f (-match p' path-map)]
(cond
f (conj l [p' (f v)])
(and (map? v) (seq v)) (-path-vals l p' v)
:else (conj l [p' v]))))
l m))]
(-path-vals [] [] m)))
(defn -assoc-in-path-vals [c]
(reduce (partial apply assoc-in) {} c))
(defn path-update [m path-map]
(-> (-path-vals m path-map)
(-assoc-in-path-vals)))
(defn accumulator? [x]
(-> x meta ::accumulator))
(defn accumulate
([x] (if-not (accumulator? x) (with-meta [x] {::accumulator true}) x))
([x y] (into (accumulate x) y)))
;;
;; impl
;;
(defn parse [path opts] (defn parse [path opts]
(let [path #?(:clj (.intern ^String (trie/normalize path opts)) :cljs (trie/normalize path opts)) (let [path #?(:clj (.intern ^String (trie/normalize path opts)) :cljs (trie/normalize path opts))
path-parts (trie/split-path path opts) path-parts (trie/split-path path opts)
@ -60,8 +103,10 @@
(defn map-data [f routes] (defn map-data [f routes]
(mapv (fn [[p ds]] [p (f p ds)]) routes)) (mapv (fn [[p ds]] [p (f p ds)]) routes))
(defn meta-merge [left right opts] (defn meta-merge [left right {:keys [meta-merge update-paths]}]
((or (:meta-merge opts) mm/meta-merge) left right)) (let [update (if update-paths #(path-update % update-paths) identity)
merge (or meta-merge mm/meta-merge)]
(merge (update left) (update right))))
(defn merge-data [opts p x] (defn merge-data [opts p x]
(reduce (reduce

View file

@ -82,8 +82,11 @@
(s/def :reitit.core.coercion/model any?) (s/def :reitit.core.coercion/model any?)
(s/def :reitit.core.coercion/schema any?)
(s/def :reitit.core.coercion/map-model (s/keys :opt-un [:reitit.core.coercion/schema]))
(s/def :reitit.core.coercion/content (s/def :reitit.core.coercion/content
(s/map-of string? :reitit.core.coercion/model)) (s/map-of (s/or :string string?, :default #{:default}) :reitit.core.coercion/map-model))
(s/def :reitit.core.coercion/query :reitit.core.coercion/model) (s/def :reitit.core.coercion/query :reitit.core.coercion/model)
(s/def :reitit.core.coercion/body :reitit.core.coercion/model) (s/def :reitit.core.coercion/body :reitit.core.coercion/model)

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-dev "0.6.0" (defproject metosin/reitit-dev "0.7.0-alpha5"
:description "Snappy data-driven router for Clojure(Script)" :description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"
@ -11,4 +11,6 @@
:dependencies [[metosin/reitit-core] :dependencies [[metosin/reitit-core]
[com.bhauman/spell-spec] [com.bhauman/spell-spec]
[expound] [expound]
[fipp]]) [fipp]
[org.clojure/core.rrb-vector]
[mvxcvi/arrangement]])

View file

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

View file

@ -2,6 +2,7 @@
(:require [clojure.set :as set] (:require [clojure.set :as set]
[reitit.coercion :as coercion] [reitit.coercion :as coercion]
[reitit.core :as r] [reitit.core :as r]
[reitit.impl :as impl]
goog.Uri goog.Uri
goog.Uri.QueryData)) goog.Uri.QueryData))
@ -36,6 +37,16 @@
(.setQueryData uri (goog.Uri.QueryData/createFromMap (clj->js new-query))) (.setQueryData uri (goog.Uri.QueryData/createFromMap (clj->js new-query)))
(.toString uri))) (.toString uri)))
(defn
^{:see-also ["reitit.core/match->path"]}
match->path
"Create routing path from given match and optional query-string map and
optional fragment string."
[match query-params fragment]
(when-let [path (r/match->path match query-params)]
(cond-> path
(and fragment (seq fragment)) (str "#" (impl/form-encode fragment)))))
(defn match-by-path (defn match-by-path
"Given routing tree and current path, return match with possibly "Given routing tree and current path, return match with possibly
coerced parameters. Return nil if no match found. coerced parameters. Return nil if no match found.

View file

@ -52,11 +52,13 @@
pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them
differently, convert the collections to strings first." differently, convert the collections to strings first."
([name] ([name]
(rfh/href @history name nil nil)) (rfh/href @history name nil nil nil))
([name path-params] ([name path-params]
(rfh/href @history name path-params nil)) (rfh/href @history name path-params nil nil))
([name path-params query-params] ([name path-params query-params]
(rfh/href @history name path-params query-params))) (rfh/href @history name path-params query-params nil))
([name path-params query-params fragment]
(rfh/href @history name path-params query-params fragment)))
(defn (defn
^{:see-also ["reitit.frontend.history/push-state"]} ^{:see-also ["reitit.frontend.history/push-state"]}
@ -74,11 +76,13 @@
See also: See also:
https://developer.mozilla.org/en-US/docs/Web/API/History/pushState" https://developer.mozilla.org/en-US/docs/Web/API/History/pushState"
([name] ([name]
(rfh/push-state @history name nil nil)) (rfh/push-state @history name nil nil nil))
([name path-params] ([name path-params]
(rfh/push-state @history name path-params nil)) (rfh/push-state @history name path-params nil nil))
([name path-params query-params] ([name path-params query-params]
(rfh/push-state @history name path-params query-params))) (rfh/push-state @history name path-params query-params nil))
([name path-params query-params fragment]
(rfh/push-state @history name path-params query-params fragment)))
(defn (defn
^{:see-also ["reitit.frontend.history/replace-state"]} ^{:see-also ["reitit.frontend.history/replace-state"]}
@ -96,11 +100,13 @@
See also: See also:
https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState"
([name] ([name]
(rfh/replace-state @history name nil nil)) (rfh/replace-state @history name nil nil nil))
([name path-params] ([name path-params]
(rfh/replace-state @history name path-params nil)) (rfh/replace-state @history name path-params nil nil))
([name path-params query-params] ([name path-params query-params]
(rfh/replace-state @history name path-params query-params))) (rfh/replace-state @history name path-params query-params nil))
([name path-params query-params fragment]
(rfh/replace-state @history name path-params query-params fragment)))
;; This duplicates previous two, but the map parameter will be easier way to ;; This duplicates previous two, but the map parameter will be easier way to
;; extend the functions, e.g. to work with fragment string. Toggling push vs ;; extend the functions, e.g. to work with fragment string. Toggling push vs
@ -125,7 +131,7 @@
https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState"
([name] ([name]
(rfh/navigate @history name)) (rfh/navigate @history name))
([name {:keys [path-params query-params replace] :as opts}] ([name {:keys [path-params query-params replace fragment] :as opts}]
(rfh/navigate @history name opts))) (rfh/navigate @history name opts)))
(defn (defn

View file

@ -10,7 +10,7 @@
(-init [this] "Create event listeners") (-init [this] "Create event listeners")
(-stop [this] "Remove event listeners") (-stop [this] "Remove event listeners")
(-on-navigate [this path] "Find a match for current routing path and call on-navigate callback") (-on-navigate [this path] "Find a match for current routing path and call on-navigate callback")
(-get-path [this] "Get the current routing path") (-get-path [this] "Get the current routing path, including query string and fragment")
(-href [this path] "Converts given routing path to browser location")) (-href [this path] "Converts given routing path to browser location"))
;; This version listens for both pop-state and hash-change for ;; This version listens for both pop-state and hash-change for
@ -92,6 +92,7 @@
;; isContentEditable property is inherited from parents, ;; isContentEditable property is inherited from parents,
;; so if the anchor is inside contenteditable div, the property will be true. ;; so if the anchor is inside contenteditable div, the property will be true.
(not (.-isContentEditable el)) (not (.-isContentEditable el))
;; NOTE: Why doesn't this use frontend variant instead of core?
(reitit/match-by-path router (.getPath uri))))) (reitit/match-by-path router (.getPath uri)))))
(defrecord Html5History [on-navigate router listen-key click-listen-key] (defrecord Html5History [on-navigate router listen-key click-listen-key]
@ -132,7 +133,8 @@
nil) nil)
(-get-path [this] (-get-path [this]
(str (.. js/window -location -pathname) (str (.. js/window -location -pathname)
(.. js/window -location -search))) (.. js/window -location -search)
(.. js/window -location -hash)))
(-href [this path] (-href [this path]
path)) path))
@ -193,8 +195,10 @@
([history name path-params] ([history name path-params]
(href history name path-params nil)) (href history name path-params nil))
([history name path-params query-params] ([history name path-params query-params]
(href history name path-params query-params nil))
([history name path-params query-params fragment]
(let [match (rf/match-by-name! (:router history) name path-params)] (let [match (rf/match-by-name! (:router history) name path-params)]
(-href history (reitit/match->path match query-params))))) (-href history (rf/match->path match query-params fragment)))))
(defn (defn
^{:see-also ["reitit.core/match->path"]} ^{:see-also ["reitit.core/match->path"]}
@ -211,12 +215,14 @@
See also: See also:
https://developer.mozilla.org/en-US/docs/Web/API/History/pushState" https://developer.mozilla.org/en-US/docs/Web/API/History/pushState"
([history name] ([history name]
(push-state history name nil nil)) (push-state history name nil nil nil))
([history name path-params] ([history name path-params]
(push-state history name path-params nil)) (push-state history name path-params nil nil))
([history name path-params query-params] ([history name path-params query-params]
(push-state history name path-params query-params nil))
([history name path-params query-params fragment]
(let [match (rf/match-by-name! (:router history) name path-params) (let [match (rf/match-by-name! (:router history) name path-params)
path (reitit/match->path match query-params)] path (rf/match->path match query-params fragment)]
;; pushState and replaceState don't trigger popstate event so call on-navigate manually ;; pushState and replaceState don't trigger popstate event so call on-navigate manually
(.pushState js/window.history nil "" (-href history path)) (.pushState js/window.history nil "" (-href history path))
(-on-navigate history path)))) (-on-navigate history path))))
@ -237,12 +243,14 @@
See also: See also:
https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState"
([history name] ([history name]
(replace-state history name nil nil)) (replace-state history name nil nil nil))
([history name path-params] ([history name path-params]
(replace-state history name path-params nil)) (replace-state history name path-params nil nil))
([history name path-params query-params] ([history name path-params query-params]
(replace-state history name path-params query-params nil))
([history name path-params query-params fragment]
(let [match (rf/match-by-name! (:router history) name path-params) (let [match (rf/match-by-name! (:router history) name path-params)
path (reitit/match->path match query-params)] path (rf/match->path match query-params fragment)]
(.replaceState js/window.history nil "" (-href history path)) (.replaceState js/window.history nil "" (-href history path))
(-on-navigate history path)))) (-on-navigate history path))))
@ -265,9 +273,9 @@
https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState"
([history name] ([history name]
(navigate history name nil)) (navigate history name nil))
([history name {:keys [path-params query-params replace] :as opts}] ([history name {:keys [path-params query-params fragment replace] :as opts}]
(let [match (rf/match-by-name! (:router history) name path-params) (let [match (rf/match-by-name! (:router history) name path-params)
path (reitit/match->path match query-params)] path (rf/match->path match query-params fragment)]
(if replace (if replace
(.replaceState js/window.history nil "" (-href history path)) (.replaceState js/window.history nil "" (-href history path))
(.pushState js/window.history nil "" (-href history path))) (.pushState js/window.history nil "" (-href history path)))

View file

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

View file

@ -22,11 +22,12 @@
compile (fn [[path data] opts scope] compile (fn [[path data] opts scope]
(interceptor/compile-result [path data] opts scope)) (interceptor/compile-result [path data] opts scope))
->endpoint (fn [p d m s] ->endpoint (fn [p d m s]
(let [d (ring/-compile-coercion d)]
(let [compiled (compile [p d] opts s)] (let [compiled (compile [p d] opts s)]
(-> compiled (-> compiled
(map->Endpoint) (map->Endpoint)
(assoc :path p) (assoc :path p)
(assoc :method m)))) (assoc :method m)))))
->methods (fn [any? data] ->methods (fn [any? data]
(reduce (reduce
(fn [acc method] (fn [acc method]
@ -67,6 +68,7 @@
([data opts] ([data opts]
(let [opts (merge {:coerce coerce-handler (let [opts (merge {:coerce coerce-handler
:compile compile-result :compile compile-result
:update-paths (ring/-update-paths impl/accumulate)
::default-options-endpoint ring/default-options-endpoint} opts)] ::default-options-endpoint ring/default-options-endpoint} opts)]
(when (contains? opts ::default-options-handler) (when (contains? opts ::default-options-handler)
(ex/fail! (str "Option :reitit.http/default-options-handler is deprecated." (ex/fail! (str "Option :reitit.http/default-options-handler is deprecated."

View file

@ -10,15 +10,15 @@
[] []
{:name ::coerce-request {:name ::coerce-request
:spec ::rs/parameters :spec ::rs/parameters
:compile (fn [{:keys [coercion parameters]} opts] :compile (fn [{:keys [coercion parameters request]} opts]
(cond (cond
;; no coercion, skip ;; no coercion, skip
(not coercion) nil (not coercion) nil
;; just coercion, don't mount ;; just coercion, don't mount
(not parameters) {} (not (or parameters request)) {}
;; mount ;; mount
:else :else
(if-let [coercers (coercion/request-coercers coercion parameters opts)] (if-let [coercers (coercion/request-coercers coercion parameters request opts)]
{:enter (fn [ctx] {:enter (fn [ctx]
(let [request (:request ctx) (let [request (:request ctx)
coerced (coercion/coerce-request coercers request) coerced (coercion/coerce-request coercers request)

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-interceptors "0.6.0" (defproject metosin/reitit-interceptors "0.7.0-alpha5"
:description "Reitit, common interceptors bundled" :description "Reitit, common interceptors bundled"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"
@ -11,4 +11,5 @@
:inherit [:deploy-repositories :managed-dependencies]} :inherit [:deploy-repositories :managed-dependencies]}
:dependencies [[metosin/reitit-ring] :dependencies [[metosin/reitit-ring]
[lambdaisland/deep-diff] [lambdaisland/deep-diff]
[metosin/muuntaja]]) [metosin/muuntaja]
[metosin/spec-tools]])

View file

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

View file

@ -106,92 +106,6 @@
;; malli options ;; malli options
:options nil}) :options nil})
(defn -get-apidocs-openapi
[coercion {:keys [parameters responses content-types] :or {content-types ["application/json"]}} options]
(let [{:keys [body request multipart]} parameters
parameters (dissoc parameters :request :body :multipart)
->schema-object (fn [schema opts]
(let [current-opts (merge options opts)]
(json-schema/transform (coercion/-compile-model coercion schema current-opts)
current-opts)))]
(merge
(when (seq parameters)
{:parameters
(->> (for [[in schema] parameters
:let [{:keys [properties required] :as root} (->schema-object schema {:in in :type :parameter})
required? (partial contains? (set required))]
[k schema] properties]
(merge {:in (name in)
:name k
:required (required? k)
:schema schema}
(select-keys root [:description])))
(into []))})
(when body
;; body uses a single schema to describe every :requestBody
;; the schema-object transformer should be able to transform into distinct content-types
{:requestBody {:content (into {}
(map (fn [content-type]
(let [schema (->schema-object body {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content-types)}})
(when request
;; request allow to different :requestBody per content-type
{:requestBody
{:content (merge
(when (:body request)
(into {}
(map (fn [content-type]
(let [schema (->schema-object (:body request) {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content-types))
(into {}
(map (fn [[content-type requestBody]]
(let [schema (->schema-object requestBody {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
(:content request)))}})
(when multipart
{:requestBody
{:content
{"multipart/form-data"
{:schema
(->schema-object multipart {:in :requestBody
:type :schema
:content-type "multipart/form-data"})}}}})
(when responses
{:responses
(into {}
(map (fn [[status {:keys [body content]
:as response}]]
(let [content (merge
(when body
(into {}
(map (fn [content-type]
(let [schema (->schema-object body {:in :responses
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content-types))
(when content
(into {}
(map (fn [[content-type schema]]
(let [schema (->schema-object schema {:in :responses
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content)))]
[status (merge (select-keys response [:description])
(when content
{:content content}))])))
responses)}))))
(defn create (defn create
([] ([]
(create nil)) (create nil))
@ -199,12 +113,20 @@
(let [{:keys [transformers lite compile options error-keys encode-error] :as opts} (merge default-options opts) (let [{:keys [transformers lite compile options error-keys encode-error] :as opts} (merge default-options opts)
show? (fn [key] (contains? error-keys key)) show? (fn [key] (contains? error-keys key))
transformers (walk/prewalk #(if (satisfies? TransformationProvider %) (-transformer % opts) %) transformers) transformers (walk/prewalk #(if (satisfies? TransformationProvider %) (-transformer % opts) %) transformers)
compile (if lite (fn [schema options] (compile (binding [l/*options* options] (l/schema schema)) options)) compile (if lite (fn [schema options]
(compile (binding [l/*options* options] (l/schema schema)) options))
compile)] compile)]
^{:type ::coercion/coercion} ^{:type ::coercion/coercion}
(reify coercion/Coercion (reify coercion/Coercion
(-get-name [_] :malli) (-get-name [_] :malli)
(-get-options [_] opts) (-get-options [_] opts)
(-get-model-apidocs [this specification model options]
(case specification
:openapi (json-schema/transform model (merge opts options))
(throw
(ex-info
(str "Can't produce Malli apidocs for " specification)
{:type specification, :coercion :malli}))))
(-get-apidocs [this specification {:keys [parameters responses] :as data}] (-get-apidocs [this specification {:keys [parameters responses] :as data}]
(case specification (case specification
:swagger (swagger/swagger-spec :swagger (swagger/swagger-spec
@ -225,12 +147,15 @@
(if (:schema $) (if (:schema $)
(update $ :schema #(coercion/-compile-model this % nil)) (update $ :schema #(coercion/-compile-model this % nil))
$))]))}))) $))]))})))
:openapi (-get-apidocs-openapi this data options) ;; :openapi handled in reitit.openapi/-get-apidocs-openapi
(throw (throw
(ex-info (ex-info
(str "Can't produce Schema apidocs for " specification) (str "Can't produce Malli apidocs for " specification)
{:type specification, :coercion :schema})))) {:type specification, :coercion :malli}))))
(-compile-model [_ model _] (compile model options)) (-compile-model [_ model _]
(if (= 1 (count model))
(compile (first model) options)
(reduce (fn [x y] (mu/merge x y options)) (map #(compile % options) model))))
(-open-model [_ schema] schema) (-open-model [_ schema] schema)
(-encode-error [_ error] (-encode-error [_ error]
(cond-> error (cond-> error
@ -241,8 +166,8 @@
(seq error-keys) (select-keys error-keys) (seq error-keys) (select-keys error-keys)
encode-error (encode-error))) encode-error (encode-error)))
(-request-coercer [_ type schema] (-request-coercer [_ type schema]
(-coercer (compile schema options) type transformers :decode opts)) (-coercer schema type transformers :decode opts))
(-response-coercer [_ schema] (-response-coercer [_ schema]
(-coercer (compile schema options) :response transformers :encode opts)))))) (-coercer schema :response transformers :encode opts))))))
(def coercion (create default-options)) (def coercion (create default-options))

View file

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

View file

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

View file

@ -73,6 +73,96 @@
(defn- openapi-path [path opts] (defn- openapi-path [path opts]
(-> path (trie/normalize opts) (str/replace #"\{\*" "{"))) (-> path (trie/normalize opts) (str/replace #"\{\*" "{")))
(defn -get-apidocs-openapi
[coercion {:keys [request parameters responses content-types] :or {content-types ["application/json"]}}]
(let [{:keys [body multipart]} parameters
parameters (dissoc parameters :request :body :multipart)
->content (fn [data schema]
(merge
{:schema schema}
(select-keys data [:description :examples])
(:openapi data)))
->schema-object #(coercion/-get-model-apidocs coercion :openapi %1 %2)]
(merge
(when (seq parameters)
{:parameters
(->> (for [[in schema] parameters
:let [{:keys [properties required]} (->schema-object schema {:in in :type :parameter})
required? (partial contains? (set required))]
[k schema] properties]
(merge {:in (name in)
:name k
:required (required? k)
:schema schema}
(select-keys schema [:description])))
(into []))})
(when body
;; body uses a single schema to describe every :requestBody
;; the schema-object transformer should be able to transform into distinct content-types
{:requestBody {:content (into {}
(map (fn [content-type]
(let [schema (->schema-object body {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content-types)}})
(when request
;; request allow to different :requestBody per content-type
{:requestBody
{:content (merge
(select-keys request [:description])
(when-let [{:keys [schema] :as data} (coercion/get-default request)]
(into {}
(map (fn [content-type]
(let [schema (->schema-object schema {:in :requestBody
:type :schema
:content-type content-type})]
[content-type (->content data schema)])))
content-types))
(into {}
(map (fn [[content-type {:keys [schema] :as data}]]
(let [schema (->schema-object schema {:in :requestBody
:type :schema
:content-type content-type})]
[content-type (->content data schema)])))
(:content request)))}})
(when multipart
{:requestBody
{:content
{"multipart/form-data"
{:schema
(->schema-object multipart {:in :requestBody
:type :schema
:content-type "multipart/form-data"})}}}})
(when responses
{:responses
(into {}
(map (fn [[status {:keys [content], :as response}]]
(let [default (coercion/get-default-schema response)
content (-> (merge
(when default
(into {}
(map (fn [content-type]
(let [schema (->schema-object default {:in :responses
:type :schema
:content-type content-type})]
[content-type (->content nil schema)])))
content-types))
(when content
(into {}
(map (fn [[content-type {:keys [schema] :as data}]]
(let [schema (->schema-object schema {:in :responses
:type :schema
:content-type content-type})]
[content-type (->content data schema)])))
content)))
(dissoc :default))]
[status (merge (select-keys response [:description])
(when content
{:content content}))]))
responses))}))))
(defn create-openapi-handler (defn create-openapi-handler
"Stability: alpha "Stability: alpha
@ -99,7 +189,7 @@
(apply meta-merge (keep (comp :openapi :data) middleware)) (apply meta-merge (keep (comp :openapi :data) middleware))
(apply meta-merge (keep (comp :openapi :data) interceptors)) (apply meta-merge (keep (comp :openapi :data) interceptors))
(if coercion (if coercion
(coercion/get-apidocs coercion :openapi data)) (-get-apidocs-openapi coercion data))
(select-keys data [:tags :summary :description]) (select-keys data [:tags :summary :description])
(strip-top-level-keys openapi))])) (strip-top-level-keys openapi))]))
transform-path (fn [[p _ c]] transform-path (fn [[p _ c]]

View file

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

View file

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

View file

@ -3,6 +3,7 @@
#?@(:clj [[ring.util.mime-type :as mime-type] #?@(:clj [[ring.util.mime-type :as mime-type]
[ring.util.response :as response]]) [ring.util.response :as response]])
[reitit.core :as r] [reitit.core :as r]
[reitit.coercion :as coercion]
[reitit.exception :as ex] [reitit.exception :as ex]
[reitit.impl :as impl] [reitit.impl :as impl]
[reitit.middleware :as middleware])) [reitit.middleware :as middleware]))
@ -28,16 +29,43 @@
(update acc method expand opts) (update acc method expand opts)
acc)) data http-methods)]) acc)) data http-methods)])
(defn -update-paths [f]
(let [not-request? #(not= :request %)
http-method? #(contains? http-methods %)]
[;; default parameters
[[:parameters not-request?] f]
[[http-method? :parameters not-request?] f]
;; default responses
[[:responses any? :body] f]
[[http-method? :responses any? :body] f]
;; openapi3 request
[[:request :content any? :schema] f]
[[http-method? :request :content any? :schema] f]
;; openapi3 LEGACY body
[[:request :body] f]
[[http-method? :request :body] f]
;; openapi3 responses
[[:responses any? :content any? :schema] f]
[[http-method? :responses any? :content any? :schema] f]]))
(defn -compile-coercion [{:keys [coercion] :as data}]
(cond-> data coercion (impl/path-update (-update-paths #(coercion/-compile-model coercion % nil)))))
(defn compile-result [[path data] {:keys [::default-options-endpoint expand] :as opts}] (defn compile-result [[path data] {:keys [::default-options-endpoint expand] :as opts}]
(let [[top childs] (group-keys data) (let [[top childs] (group-keys data)
childs (cond-> childs childs (cond-> childs
(and (not (:options childs)) (not (:handler top)) default-options-endpoint) (and (not (:options childs)) (not (:handler top)) default-options-endpoint)
(assoc :options (expand default-options-endpoint opts))) (assoc :options (expand default-options-endpoint opts)))
->endpoint (fn [p d m s] ->endpoint (fn [p d m s]
(let [d (-compile-coercion d)]
(-> (middleware/compile-result [p d] opts s) (-> (middleware/compile-result [p d] opts s)
(map->Endpoint) (map->Endpoint)
(assoc :path p) (assoc :path p)
(assoc :method m))) (assoc :method m))))
->methods (fn [any? data] ->methods (fn [any? data]
(reduce (reduce
(fn [acc method] (fn [acc method]
@ -97,6 +125,7 @@
([data opts] ([data opts]
(let [opts (merge {:coerce coerce-handler (let [opts (merge {:coerce coerce-handler
:compile compile-result :compile compile-result
:update-paths (-update-paths impl/accumulate)
::default-options-endpoint default-options-endpoint} ::default-options-endpoint default-options-endpoint}
opts)] opts)]
(when (contains? opts ::default-options-handler) (when (contains? opts ::default-options-handler)

View file

@ -24,15 +24,15 @@
and :parameters from route data, otherwise does not mount." and :parameters from route data, otherwise does not mount."
{:name ::coerce-request {:name ::coerce-request
:spec ::rs/parameters :spec ::rs/parameters
:compile (fn [{:keys [coercion parameters]} opts] :compile (fn [{:keys [coercion parameters request]} opts]
(cond (cond
;; no coercion, skip ;; no coercion, skip
(not coercion) nil (not coercion) nil
;; just coercion, don't mount ;; just coercion, don't mount
(not parameters) {} (not (or parameters request)) {}
;; mount ;; mount
:else :else
(if-let [coercers (coercion/request-coercers coercion parameters opts)] (if-let [coercers (coercion/request-coercers coercion parameters request opts)]
(fn [handler] (fn [handler]
(fn (fn
([request] ([request]

View file

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

View file

@ -47,68 +47,38 @@
(reify coercion/Coercion (reify coercion/Coercion
(-get-name [_] :schema) (-get-name [_] :schema)
(-get-options [_] opts) (-get-options [_] opts)
(-get-apidocs [this specification {:keys [parameters responses content-types] (-get-model-apidocs [_ specification model options]
(case specification
:openapi (openapi/transform model (merge opts options))
(throw
(ex-info
(str "Can't produce Schema apidocs for " specification)
{:type specification, :coercion :schema}))))
(-get-apidocs [_ specification {:keys [request parameters responses content-types]
:or {content-types ["application/json"]}}] :or {content-types ["application/json"]}}]
;; TODO: this looks identical to spec, refactor when schema is done. ;; TODO: this looks identical to spec, refactor when schema is done.
(case specification (case specification
:swagger (swagger/swagger-spec :swagger (swagger/swagger-spec
(merge (merge
(if parameters (if parameters
{::swagger/parameters {::swagger/parameters parameters})
(into
(empty parameters)
(for [[k v] parameters]
[k (coercion/-compile-model this v nil)]))})
(if responses (if responses
{::swagger/responses {::swagger/responses
(into (into
(empty responses) (empty responses)
(for [[k response] responses] (for [[k response] responses]
[k (as-> response $ [k (-> response
(set/rename-keys $ {:body :schema}) (dissoc :content)
(if (:schema $) (set/rename-keys {:body :schema}))]))})))
(update $ :schema #(coercion/-compile-model this % nil)) ;; :openapi handled in reitit.openapi/-get-apidocs-openapi
$))]))})))
:openapi (merge
(when (seq (dissoc parameters :body :request :multipart))
(openapi/openapi-spec {::openapi/parameters
(into
(empty parameters)
(for [[k v] (dissoc parameters :body :request)]
[k (coercion/-compile-model this v nil)]))}))
(when (:body parameters)
{:requestBody (openapi/openapi-spec
{::openapi/content (zipmap content-types (repeat (:body parameters)))})})
(when (:request parameters)
{:requestBody (openapi/openapi-spec
{::openapi/content (merge
(when-let [default (get-in parameters [:request :body])]
(zipmap content-types (repeat default)))
(:content (:request parameters)))})})
(when (:multipart parameters)
{:requestBody
(openapi/openapi-spec
{::openapi/content {"multipart/form-data" (:multipart parameters)}})})
(when responses
{:responses
(into
(empty responses)
(for [[k {:keys [body content] :as response}] responses]
[k (merge
(select-keys response [:description])
(when (or body content)
(openapi/openapi-spec
{::openapi/content (merge
(when body
(zipmap content-types (repeat (coercion/-compile-model this body nil))))
(when response
(:content response)))})))]))}))
(throw (throw
(ex-info (ex-info
(str "Can't produce Schema apidocs for " specification) (str "Can't produce Schema apidocs for " specification)
{:type specification, :coercion :schema})))) {:type specification, :coercion :schema}))))
(-compile-model [_ model _] model) (-compile-model [_ model _]
(if (= 1 (count model))
(first model)
(apply st/merge model)))
(-open-model [_ schema] (st/open-schema schema)) (-open-model [_ schema] (st/open-schema schema))
(-encode-error [_ error] (-encode-error [_ error]
(-> error (-> error

View file

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

View file

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

View file

@ -1,7 +1,9 @@
(ns reitit.coercion.spec (ns reitit.coercion.spec
(:require [clojure.set :as set] (:require [clojure.set :as set]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[meta-merge.core :as mm]
[reitit.coercion :as coercion] [reitit.coercion :as coercion]
[reitit.exception :as ex]
[spec-tools.core :as st #?@(:cljs [:refer [Spec]])] [spec-tools.core :as st #?@(:cljs [:refer [Spec]])]
[spec-tools.data-spec :as ds #?@(:cljs [:refer [Maybe]])] [spec-tools.data-spec :as ds #?@(:cljs [:refer [Maybe]])]
[spec-tools.openapi.core :as openapi] [spec-tools.openapi.core :as openapi]
@ -66,7 +68,7 @@
(st/create-spec {:spec this})) (st/create-spec {:spec this}))
nil nil
(into-spec [this _])) (into-spec [_ _]))
(defn stringify-pred [pred] (defn stringify-pred [pred]
(str (if (seq? pred) (seq pred) pred))) (str (if (seq? pred) (seq pred) pred)))
@ -86,81 +88,51 @@
(reify coercion/Coercion (reify coercion/Coercion
(-get-name [_] :spec) (-get-name [_] :spec)
(-get-options [_] opts) (-get-options [_] opts)
(-get-apidocs [this specification {:keys [parameters responses content-types] (-get-model-apidocs [_ specification model options]
(case specification
:openapi (openapi/transform model (merge opts options))
(throw
(ex-info
(str "Can't produce Spec apidocs for " specification)
{:type specification, :coercion :spec}))))
(-get-apidocs [_ specification {:keys [request parameters responses content-types]
:or {content-types ["application/json"]}}] :or {content-types ["application/json"]}}]
(case specification (case specification
:swagger (swagger/swagger-spec :swagger (swagger/swagger-spec
(merge (merge
(if parameters (if parameters
{::swagger/parameters {::swagger/parameters parameters})
(into
(empty parameters)
(for [[k v] parameters]
[k (coercion/-compile-model this v nil)]))})
(if responses (if responses
{::swagger/responses {::swagger/responses
(into (into
(empty responses) (empty responses)
(for [[k response] responses] (for [[k response] responses]
[k (as-> response $ [k (as-> response $
(set/rename-keys $ {:body :schema}) (dissoc $ :content)
(if (:schema $) (set/rename-keys $ {:body :schema}))]))})))
(update $ :schema #(coercion/-compile-model this % nil)) ;; :openapi handled in reitit.openapi/-get-apidocs-openapi
$))]))})))
:openapi (merge
(when (seq (dissoc parameters :body :request :multipart))
(openapi/openapi-spec {::openapi/parameters
(into (empty parameters)
(for [[k v] (dissoc parameters :body :request)]
[k (coercion/-compile-model this v nil)]))}))
(when (:body parameters)
{:requestBody (openapi/openapi-spec
{::openapi/content (zipmap content-types (repeat (coercion/-compile-model this (:body parameters) nil)))})})
(when (:request parameters)
{:requestBody (openapi/openapi-spec
{::openapi/content (merge
(when-let [default (get-in parameters [:request :body])]
(zipmap content-types (repeat (coercion/-compile-model this default nil))))
(into {}
(for [[format model] (:content (:request parameters))]
[format (coercion/-compile-model this model nil)])))})})
(when (:multipart parameters)
{:requestBody
(openapi/openapi-spec
{::openapi/content
{"multipart/form-data"
(coercion/-compile-model this (:multipart parameters) nil)}})})
(when responses
{:responses
(into
(empty responses)
(for [[k {:keys [body content] :as response}] responses]
[k (merge
(select-keys response [:description])
(when (or body content)
(openapi/openapi-spec
{::openapi/content (merge
(when body
(zipmap content-types (repeat (coercion/-compile-model this (:body response) nil))))
(when response
(into {}
(for [[format model] (:content response)]
[format (coercion/-compile-model this model nil)]))))})))]))}))
(throw (throw
(ex-info (ex-info
(str "Can't produce Spec apidocs for " specification) (str "Can't produce Spec apidocs for " specification)
{:specification specification, :coercion :spec})))) {:specification specification, :coercion :spec}))))
(-compile-model [_ model name] (-compile-model [_ model name]
(into-spec model name)) (into-spec
(cond
;; we are safe!
(= (count model) 1) (first model)
;; here be dragons, best effort
(every? map? model) (apply mm/meta-merge model)
;; fail fast
:else (ex/fail! ::model-error {:message "Can't merge nested clojure specs", :spec model}))
name))
(-open-model [_ spec] spec) (-open-model [_ spec] spec)
(-encode-error [_ error] (-encode-error [_ error]
(let [problems (-> error :problems ::s/problems)] (let [problems (-> error :problems ::s/problems)]
(-> error (-> error
(update :spec (comp str s/form)) (update :spec (comp str s/form))
(assoc :problems (mapv #(update % :pred stringify-pred) problems))))) (assoc :problems (mapv #(update % :pred stringify-pred) problems)))))
(-request-coercer [this type spec] (-request-coercer [_ type spec]
(let [spec (coercion/-compile-model this spec nil) (let [{:keys [formats default]} (transformers type)]
{:keys [formats default]} (transformers type)]
(fn [value format] (fn [value format]
(if-let [transformer (or (get formats format) default)] (if-let [transformer (or (get formats format) default)]
(let [coerced (st/coerce spec value transformer)] (let [coerced (st/coerce spec value transformer)]

View file

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

View file

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

View file

@ -69,6 +69,30 @@
(defn- swagger-path [path opts] (defn- swagger-path [path opts]
(-> path (trie/normalize opts) (str/replace #"\{\*" "{"))) (-> path (trie/normalize opts) (str/replace #"\{\*" "{")))
(defn -warn-unsupported-coercions [{:keys [request responses] :as _data}]
(when request
(println "WARNING [reitit.coercion]: swagger apidocs don't support :request coercion"))
(when (some :content (vals responses))
(println "WARNING [reitit.coercion]: swagger apidocs don't support :responses :content coercion")))
(defn -get-swagger-apidocs [coercion data]
(let [swagger-parameter {:query :query
:body :body
:form :formData
:header :header
:path :path
:multipart :formData}]
(-warn-unsupported-coercions data)
(->> (update
data
:parameters
(fn [parameters]
(->> parameters
(map (fn [[k v]] [(swagger-parameter k) v]))
(filter first)
(into {}))))
(coercion/-get-apidocs coercion :swagger))))
(defn create-swagger-handler (defn create-swagger-handler
"Create a ring handler to emit swagger spec. Collects all routes from router which have "Create a ring handler to emit swagger spec. Collects all routes from router which have
an intersecting `[:swagger :id]` and which are not marked with `:no-doc` route data." an intersecting `[:swagger :id]` and which are not marked with `:no-doc` route data."
@ -95,7 +119,7 @@
(apply meta-merge (keep (comp :swagger :data) middleware)) (apply meta-merge (keep (comp :swagger :data) middleware))
(apply meta-merge (keep (comp :swagger :data) interceptors)) (apply meta-merge (keep (comp :swagger :data) interceptors))
(if coercion (if coercion
(coercion/get-apidocs coercion :swagger data)) (-get-swagger-apidocs coercion data))
(select-keys data [:tags :summary :description :operationId]) (select-keys data [:tags :summary :description :operationId])
(strip-top-level-keys swagger))])) (strip-top-level-keys swagger))]))
transform-path (fn [[p _ c]] transform-path (fn [[p _ c]]

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit "0.6.0" (defproject metosin/reitit "0.7.0-alpha5"
:description "Snappy data-driven router for Clojure(Script)" :description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"
@ -19,7 +19,7 @@
[metosin/reitit-http] [metosin/reitit-http]
[metosin/reitit-interceptors] [metosin/reitit-interceptors]
[metosin/reitit-swagger] [metosin/reitit-swagger]
[metosin/reitit-openapi] [fi.metosin/reitit-openapi]
[metosin/reitit-swagger-ui] [metosin/reitit-swagger-ui]
[metosin/reitit-frontend] [metosin/reitit-frontend]
[metosin/reitit-sieppari] [metosin/reitit-sieppari]

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-parent "0.6.0" (defproject metosin/reitit-parent "0.7.0-alpha5"
:description "Snappy data-driven router for Clojure(Script)" :description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit" :url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"
@ -15,44 +15,47 @@
:url "https://github.com/metosin/reitit"} :url "https://github.com/metosin/reitit"}
;; TODO: need to verify that the code actually worked with Java1.8, see #242 ;; TODO: need to verify that the code actually worked with Java1.8, see #242
:javac-options ["-Xlint:unchecked" "-target" "1.8" "-source" "1.8"] :javac-options ["-Xlint:unchecked" "-target" "1.8" "-source" "1.8"]
:managed-dependencies [[metosin/reitit "0.6.0"] :managed-dependencies [[metosin/reitit "0.7.0-alpha5"]
[metosin/reitit-core "0.6.0"] [metosin/reitit-core "0.7.0-alpha5"]
[metosin/reitit-dev "0.6.0"] [metosin/reitit-dev "0.7.0-alpha5"]
[metosin/reitit-spec "0.6.0"] [metosin/reitit-spec "0.7.0-alpha5"]
[metosin/reitit-malli "0.6.0"] [metosin/reitit-malli "0.7.0-alpha5"]
[metosin/reitit-schema "0.6.0"] [metosin/reitit-schema "0.7.0-alpha5"]
[metosin/reitit-ring "0.6.0"] [metosin/reitit-ring "0.7.0-alpha5"]
[metosin/reitit-middleware "0.6.0"] [metosin/reitit-middleware "0.7.0-alpha5"]
[metosin/reitit-http "0.6.0"] [metosin/reitit-http "0.7.0-alpha5"]
[metosin/reitit-interceptors "0.6.0"] [metosin/reitit-interceptors "0.7.0-alpha5"]
[metosin/reitit-swagger "0.6.0"] [metosin/reitit-swagger "0.7.0-alpha5"]
[metosin/reitit-openapi "0.6.0"] [fi.metosin/reitit-openapi "0.7.0-alpha5"]
[metosin/reitit-swagger-ui "0.6.0"] [metosin/reitit-swagger-ui "0.7.0-alpha5"]
[metosin/reitit-frontend "0.6.0"] [metosin/reitit-frontend "0.7.0-alpha5"]
[metosin/reitit-sieppari "0.6.0"] [metosin/reitit-sieppari "0.7.0-alpha5"]
[metosin/reitit-pedestal "0.6.0"] [metosin/reitit-pedestal "0.7.0-alpha5"]
[metosin/ring-swagger-ui "4.18.1"] [metosin/ring-swagger-ui "4.18.1"]
[metosin/spec-tools "0.10.5"] [metosin/spec-tools "0.10.5"]
[metosin/schema-tools "0.13.0"] [metosin/schema-tools "0.13.1"]
[metosin/muuntaja "0.6.8"] [metosin/muuntaja "0.6.8"]
[metosin/jsonista "0.3.7"] [metosin/jsonista "0.3.7"]
[metosin/sieppari "0.0.0-alpha13"] [metosin/sieppari "0.0.0-alpha13"]
[metosin/malli "0.11.0"] [metosin/malli "0.11.0"]
;; https://clojureverse.org/t/depending-on-the-right-versions-of-jackson-libraries/5111 ;; https://clojureverse.org/t/depending-on-the-right-versions-of-jackson-libraries/5111
[com.fasterxml.jackson.core/jackson-core "2.14.2"] [com.fasterxml.jackson.core/jackson-core "2.15.1"]
[com.fasterxml.jackson.core/jackson-databind "2.14.2"] [com.fasterxml.jackson.core/jackson-databind "2.15.1"]
[meta-merge "1.0.0"] [meta-merge "1.0.0"]
[fipp "0.6.26" :exclusions [org.clojure/core.rrb-vector]] [fipp "0.6.26" :exclusions [org.clojure/core.rrb-vector]]
;; Deep-diff uses this version, override olders versiom from fipp.
[org.clojure/core.rrb-vector "0.0.14"]
[expound "0.9.0"] [expound "0.9.0"]
[lambdaisland/deep-diff "0.0-47"] [lambdaisland/deep-diff "0.0-47"]
[com.bhauman/spell-spec "0.1.2"] [com.bhauman/spell-spec "0.1.2"]
[mvxcvi/arrangement "2.1.0"]
[ring/ring-core "1.10.0"] [ring/ring-core "1.10.0"]
[io.pedestal/pedestal.service "0.5.10"]] [io.pedestal/pedestal.service "0.5.10"]]
:plugins [[jonase/eastwood "1.3.0"] :plugins [[jonase/eastwood "1.4.0"]
;[lein-virgil "0.1.7"] ;[lein-virgil "0.1.7"]
[lein-ancient "1.0.0-RC3"] [lein-ancient "1.0.0-RC3"]
[lein-doo "0.1.11"] [lein-doo "0.1.11"]
@ -87,7 +90,7 @@
[org.clojure/clojurescript "1.10.773"] [org.clojure/clojurescript "1.10.773"]
;; modules dependencies ;; modules dependencies
[metosin/schema-tools "0.13.0"] [metosin/schema-tools "0.13.1"]
[metosin/spec-tools "0.10.5"] [metosin/spec-tools "0.10.5"]
[metosin/muuntaja "0.6.8"] [metosin/muuntaja "0.6.8"]
[metosin/sieppari "0.0.0-alpha13"] [metosin/sieppari "0.0.0-alpha13"]
@ -105,6 +108,7 @@
[ikitommi/immutant-web "3.0.0-alpha1"] [ikitommi/immutant-web "3.0.0-alpha1"]
[metosin/ring-http-response "0.9.3"] [metosin/ring-http-response "0.9.3"]
[metosin/ring-swagger-ui "4.18.1"] [metosin/ring-swagger-ui "4.18.1"]
[org.clojure/tools.analyzer "1.1.1"]
[criterium "0.4.6"] [criterium "0.4.6"]
[org.clojure/test.check "1.1.1"] [org.clojure/test.check "1.1.1"]
@ -115,8 +119,8 @@
[io.pedestal/pedestal.service "0.5.10"] [io.pedestal/pedestal.service "0.5.10"]
[org.clojure/core.async "1.6.673"] [org.clojure/core.async "1.6.673"]
[manifold "0.4.0"] [manifold "0.4.1"]
[funcool/promesa "10.0.594"] [funcool/promesa "11.0.664"]
[com.clojure-goes-fast/clj-async-profiler "1.0.3"] [com.clojure-goes-fast/clj-async-profiler "1.0.3"]
[ring-cors "0.1.13"] [ring-cors "0.1.13"]
@ -134,8 +138,8 @@
[io.pedestal/pedestal.jetty "0.5.10"] [io.pedestal/pedestal.jetty "0.5.10"]
[calfpath "0.8.1"] [calfpath "0.8.1"]
[org.clojure/core.async "1.6.673"] [org.clojure/core.async "1.6.673"]
[manifold "0.4.0"] [manifold "0.4.1"]
[funcool/promesa "10.0.594"] [funcool/promesa "11.0.664"]
[metosin/sieppari] [metosin/sieppari]
[yada "1.2.16"] [yada "1.2.16"]
[aleph "0.6.1"] [aleph "0.6.1"]

14
scripts/cljdoc-check.sh Executable file
View file

@ -0,0 +1,14 @@
#!/bin/bash
set -e
# Need pom and jar for analyze local.
# Need repo version installed to the local m2 for up-to-date dependencies between modules.
# Install will run jar and pom tasks already.
./scripts/lein-modules install
for i in modules/*; do
cd $i
clojure -J-Dclojure.main.report=stderr -Tcljdoc-analyzer analyze-local
cd ../..
done

View file

@ -2,7 +2,11 @@
ext="sedbak$$" ext="sedbak$$"
# metosin/reitit-*
find . -name project.clj -exec sed -i.$ext "s/\[metosin\/reitit\(.*\) \".*\"\]/[metosin\/reitit\1 \"$1\"\]/g" '{}' \; find . -name project.clj -exec sed -i.$ext "s/\[metosin\/reitit\(.*\) \".*\"\]/[metosin\/reitit\1 \"$1\"\]/g" '{}' \;
find . -name project.clj -exec sed -i.$ext "s/defproject metosin\/reitit\(.*\) \".*\"/defproject metosin\/reitit\1 \"$1\"/g" '{}' \; find . -name project.clj -exec sed -i.$ext "s/defproject metosin\/reitit\(.*\) \".*\"/defproject metosin\/reitit\1 \"$1\"/g" '{}' \;
# fi.metosin/reitit-*
find . -name project.clj -exec sed -i.$ext "s/\[fi.metosin\/reitit\(.*\) \".*\"\]/[fi.metosin\/reitit\1 \"$1\"\]/g" '{}' \;
find . -name project.clj -exec sed -i.$ext "s/defproject fi.metosin\/reitit\(.*\) \".*\"/defproject fi.metosin\/reitit\1 \"$1\"/g" '{}' \;
sed -i.$ext "s/\[metosin\/reitit\(.*\) \".*\"\]/[metosin\/reitit\1 \"$1\"\]/g" README.md doc/**/*.md sed -i.$ext "s/\[metosin\/reitit\(.*\) \".*\"\]/[metosin\/reitit\1 \"$1\"\]/g" README.md doc/**/*.md
find . -name "*.$ext" -exec rm '{}' \; find . -name "*.$ext" -exec rm '{}' \;

View file

@ -7,34 +7,55 @@
[reitit.coercion.spec] [reitit.coercion.spec]
[reitit.core :as r] [reitit.core :as r]
[schema.core :as s] [schema.core :as s]
[clojure.spec.alpha :as cs]
[spec-tools.data-spec :as ds]) [spec-tools.data-spec :as ds])
#?(:clj #?(:clj
(:import (clojure.lang ExceptionInfo)))) (:import (clojure.lang ExceptionInfo))))
(cs/def ::number int?)
(cs/def ::keyword keyword?)
(cs/def ::int int?)
(cs/def ::ints (cs/coll-of int? :kind vector))
(cs/def ::map (cs/map-of int? int?))
(deftest coercion-test (deftest coercion-test
(let [r (r/router (let [r (r/router
[["/schema" {:coercion reitit.coercion.schema/coercion} [["/schema" {:coercion reitit.coercion.schema/coercion}
["/:number/:keyword" {:parameters {:path {:number s/Int ["/:number" {:parameters {:path {:number s/Int}}}
:keyword s/Keyword} ["/:keyword" {:parameters {:path {:keyword s/Keyword}
:query (s/maybe {:int s/Int, :ints [s/Int], :map {s/Int s/Int}})}}]] :query (s/maybe {:int s/Int, :ints [s/Int], :map {s/Int s/Int}})}}]]]
["/malli" {:coercion reitit.coercion.malli/coercion} ["/malli" {:coercion reitit.coercion.malli/coercion}
["/:number/:keyword" {:parameters {:path [:map [:number int?] [:keyword keyword?]] ["/:number" {:parameters {:path [:map [:number int?]]}}
["/:keyword" {:parameters {:path [:map [:keyword keyword?]]
:query [:maybe [:map [:int int?] :query [:maybe [:map [:int int?]
[:ints [:vector int?]] [:ints [:vector int?]]
[:map [:map-of int? int?]]]]}}]] [:map [:map-of int? int?]]]]}}]]]
["/malli-lite" {:coercion reitit.coercion.malli/coercion} ["/malli-lite" {:coercion reitit.coercion.malli/coercion}
["/:number/:keyword" {:parameters {:path {:number int? ["/:number" {:parameters {:path {:number int?}}}
:keyword keyword?} ["/:keyword" {:parameters {:path {:keyword keyword?}
:query (l/maybe {:int int? :query (l/maybe {:int int?
:ints (l/vector int?) :ints (l/vector int?)
:map (l/map-of int? int?)})}}]] :map (l/map-of int? int?)})}}]]]
["/spec" {:coercion reitit.coercion.spec/coercion}
["/:number/:keyword" {:parameters {:path {:number int? #_["/spec" {:coercion reitit.coercion.spec/coercion}
:keyword keyword?} ["/:number" {:parameters {:path (cs/keys :req-un [::number])}}
:query (ds/maybe {:int int?, :ints [int?], :map {int? int?}})}}]] ["/:keyword" {:parameters {:path (cs/keys :req-un [::keyword])
:query (cs/nilable (cs/keys :req-un [::int ::ints ::map]))}}]]]
["/spec-shallow" {:coercion reitit.coercion.spec/coercion}
["/:number/:keyword" {:parameters {:path (cs/keys :req-un [::number ::keyword])
:query (cs/nilable (cs/keys :req-un [::int ::ints ::map]))}}]]
["/data-spec" {:coercion reitit.coercion.spec/coercion}
["/:number" {:parameters {:path {:number int?}}}
["/:keyword" {:parameters {:path {:keyword keyword?}
:query (ds/maybe {:int int?, :ints [int?], :map {int? int?}})}}]]]
["/none" ["/none"
["/:number/:keyword" {:parameters {:path {:number int? ["/:number" {:parameters {:path {:number int?}}}
:keyword keyword?}}}]]] ["/:keyword" {:parameters {:path {:keyword keyword?}}}]]]]
{:compile coercion/compile-request-coercers})] {:compile coercion/compile-request-coercers})]
(testing "schema-coercion" (testing "schema-coercion"
@ -73,17 +94,40 @@
(let [m (r/match-by-path r "/malli-lite/kikka/abba")] (let [m (r/match-by-path r "/malli-lite/kikka/abba")]
(is (thrown? ExceptionInfo (coercion/coerce! m)))))) (is (thrown? ExceptionInfo (coercion/coerce! m))))))
;; TODO: :map-of fails with string-keys #_(testing "spec-coercion"
(testing "spec-coercion" (testing "fails"
(testing "succeeds"
(let [m (r/match-by-path r "/spec/1/abba")] (let [m (r/match-by-path r "/spec/1/abba")]
(is (thrown? ExceptionInfo (coercion/coerce! m))))
(let [m (r/match-by-path r "/spec/1/abba")]
(is (thrown? ExceptionInfo (coercion/coerce! m)))))
(testing "throws with invalid input"
(let [m (r/match-by-path r "/spec/kikka/abba")]
(is (thrown? ExceptionInfo (coercion/coerce! m))))))
(testing "spec-coercion (shallow)"
(testing "succeeds"
(let [m (r/match-by-path r "/spec-shallow/1/abba")]
(def MATCH m)
(is (= {:path {:keyword :abba, :number 1}, :query nil} (is (= {:path {:keyword :abba, :number 1}, :query nil}
(coercion/coerce! m)))) (coercion/coerce! m))))
(let [m (r/match-by-path r "/schema/1/abba")] (let [m (r/match-by-path r "/spec-shallow/1/abba")]
(is (= {:path {:keyword :abba, :number 1}, :query {:int 10, :ints [1, 2, 3], :map {1 1, #_#_2 2}}} (is (= {:path {:keyword :abba, :number 1}, :query {:int 10, :ints [1, 2, 3], :map {1 1, #_#_2 2}}}
(coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 "1"}, #_#_"2" "2"})))))) (coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 "1"}, #_#_"2" "2"}))))))
(testing "throws with invalid input" (testing "throws with invalid input"
(let [m (r/match-by-path r "/spec/kikka/abba")] (let [m (r/match-by-path r "/spec-shallow/kikka/abba")]
(is (thrown? ExceptionInfo (coercion/coerce! m))))))
;; TODO: :map-of fails with string-keys
#_(testing "data-spec-coercion"
(testing "succeeds"
(let [m (r/match-by-path r "/data-spec/1/abba")]
(is (= {:path {:keyword :abba, :number 1}, :query nil}
(coercion/coerce! m))))
(let [m (r/match-by-path r "/data-spec/1/abba")]
(is (= {:path {:keyword :abba, :number 1}, :query {:int 10, :ints [1, 2, 3], :map {1 1, #_#_2 2}}}
(coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 "1"}, #_#_"2" "2"}))))))
(testing "throws with invalid input"
(let [m (r/match-by-path r "/data-spec/kikka/abba")]
(is (thrown? ExceptionInfo (coercion/coerce! m)))))) (is (thrown? ExceptionInfo (coercion/coerce! m))))))
(testing "no coercion defined" (testing "no coercion defined"

View file

@ -267,14 +267,14 @@
(let [pong (constantly "ok") (let [pong (constantly "ok")
routes ["/api" {:mw [:api]} routes ["/api" {:mw [:api]}
["/ping" :kikka] ["/ping" :kikka]
["/user/:id" {:parameters {:id "String"}} ["/user/:id" {:parameters {:path {:id :string}}}
["/:sub-id" {:parameters {:sub-id "String"}}]] ["/:sub-id" {:parameters {:path {:sub-id :string}}}]]
["/pong" pong] ["/pong" pong]
["/admin" {:mw [:admin] :roles #{:admin}} ["/admin" {:mw [:admin] :roles #{:admin}}
["/user" {:roles ^:replace #{:user}}] ["/user" {:roles ^:replace #{:user}}]
["/db" {:mw [:db]}]]] ["/db" {:mw [:db]}]]]
expected [["/api/ping" {:mw [:api], :name :kikka}] expected [["/api/ping" {:mw [:api], :name :kikka}]
["/api/user/:id/:sub-id" {:mw [:api], :parameters {:id "String", :sub-id "String"}}] ["/api/user/:id/:sub-id" {:mw [:api], :parameters {:path [{:id :string} {:sub-id :string}]}}]
["/api/pong" {:mw [:api], :handler pong}] ["/api/pong" {:mw [:api], :handler pong}]
["/api/admin/user" {:mw [:api :admin], :roles #{:user}}] ["/api/admin/user" {:mw [:api :admin], :roles #{:user}}]
["/api/admin/db" {:mw [:api :admin :db], :roles #{:admin}}]] ["/api/admin/db" {:mw [:api :admin :db], :roles #{:admin}}]]
@ -282,7 +282,7 @@
(is (= expected (impl/resolve-routes routes (r/default-router-options)))) (is (= expected (impl/resolve-routes routes (r/default-router-options))))
(is (= (r/map->Match (is (= (r/map->Match
{:template "/api/user/:id/:sub-id" {:template "/api/user/:id/:sub-id"
:data {:mw [:api], :parameters {:id "String", :sub-id "String"}} :data {:mw [:api], :parameters {:path [{:id :string} {:sub-id :string}]}}
:path "/api/user/1/2" :path "/api/user/1/2"
:path-params {:id "1", :sub-id "2"}}) :path-params {:id "1", :sub-id "2"}})
(r/match-by-path router "/api/user/1/2")))))) (r/match-by-path router "/api/user/1/2"))))))

View file

@ -171,3 +171,39 @@
:path-parts ["https://google.com"] :path-parts ["https://google.com"]
:path-params #{}} :path-params #{}}
(impl/parse "https://google.com" nil)))) (impl/parse "https://google.com" nil))))
(deftest path-update-test
(is (= {:get {:responses {200 {:body [[:map [:total :int]]]}}
:parameters {:query [[:map [:x :int]]]}},
:parameters {:query [[:map [:x :int]]]}
:post {}}
(impl/path-update
{:parameters {:query [:map [:x :int]]}
:get {:parameters {:query [:map [:x :int]]}
:responses {200 {:body [:map [:total :int]]}}}
:post {}}
[[[:parameters any?] vector]
[[any? :parameters any?] vector]
[[:responses any? :body] vector]
[[any? :responses any? :body] vector]]))))
(deftest meta-merge-test
(is (= {:get {:responses {200 {:body [[:map [:total :int]]
[:map [:total :int]]]}},
:parameters {:query [[:map [:x :int]]
[:map [:y :int]]]}},
:parameters {:query [[:map [:x :int]]
[:map [:y :int]]]},
:post {:parameters {:query [[:map [:y :int]]]}}}
(impl/meta-merge
{:parameters {:query [:map [:x :int]]}
:get {:parameters {:query [:map [:x :int]]}
:responses {200 {:body [:map [:total :int]]}}}}
{:parameters {:query [:map [:y :int]]}
:get {:parameters {:query [:map [:y :int]]}
:responses {200 {:body [:map [:total :int]]}}}
:post {:parameters {:query [:map [:y :int]]}}}
{:update-paths [[[:parameters any?] vector]
[[any? :parameters any?] vector]
[[:responses any? :body] vector]
[[any? :responses any? :body] vector]]}))))

View file

@ -17,6 +17,7 @@
[reitit.swagger-ui :as swagger-ui] [reitit.swagger-ui :as swagger-ui]
[schema.core :as s] [schema.core :as s]
[schema-tools.core] [schema-tools.core]
[spec-tools.core :as st]
[spec-tools.data-spec :as ds])) [spec-tools.data-spec :as ds]))
(defn validate (defn validate
@ -64,7 +65,10 @@
:description "kosh"}}} :description "kosh"}}}
:responses {200 {:description "success" :responses {200 {:description "success"
:body {:total int?}} :body {:total int?}}
500 {:description "fail"}} 500 {:description "fail"}
504 {:description "default"
:content {:default {:schema {:error string?}}}
:body {:masked string?}}}
:handler (fn [{{{:keys [z]} :path :handler (fn [{{{:keys [z]} :path
xs :body} :parameters}] xs :body} :parameters}]
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]] {:status 200, :body {:total (+ (reduce + xs) z)}})}}]]
@ -90,7 +94,10 @@
:content {"application/json" {:schema {:type "string"}}}}}} :content {"application/json" {:schema {:type "string"}}}}}}
:responses {200 {:description "success" :responses {200 {:description "success"
:body [:map [:total int?]]} :body [:map [:total int?]]}
500 {:description "fail"}} 500 {:description "fail"}
504 {:description "default"
:content {:default {:schema {:error string?}}}
:body {:masked string?}}}
:handler (fn [{{{:keys [z]} :path :handler (fn [{{{:keys [z]} :path
xs :body} :parameters}] xs :body} :parameters}]
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]] {:status 200, :body {:total (+ (reduce + xs) z)}})}}]]
@ -116,7 +123,10 @@
:description "kosh"}}} :description "kosh"}}}
:responses {200 {:description "success" :responses {200 {:description "success"
:body {:total s/Int}} :body {:total s/Int}}
500 {:description "fail"}} 500 {:description "fail"}
504 {:description "default"
:content {:default {:schema {:error s/Str}}}
:body {:masked s/Str}}}
:handler (fn [{{{:keys [z]} :path :handler (fn [{{{:keys [z]} :path
xs :body} :parameters}] xs :body} :parameters}]
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]]] {:status 200, :body {:total (+ (reduce + xs) z)}})}}]]]
@ -147,19 +157,16 @@
:version "0.0.1"} :version "0.0.1"}
:paths {"/api/spec/plus/{z}" {:get {:parameters [{:in "query" :paths {"/api/spec/plus/{z}" {:get {:parameters [{:in "query"
:name "x" :name "x"
:description ""
:required true :required true
:schema {:type "integer" :schema {:type "integer"
:format "int64"}} :format "int64"}}
{:in "query" {:in "query"
:name "y" :name "y"
:description ""
:required true :required true
:schema {:type "integer" :schema {:type "integer"
:format "int64"}} :format "int64"}}
{:in "path" {:in "path"
:name "z" :name "z"
:description ""
:required true :required true
:schema {:type "integer" :schema {:type "integer"
:format "int64"}}] :format "int64"}}]
@ -178,7 +185,6 @@
:post {:parameters [{:in "path" :post {:parameters [{:in "path"
:name "z" :name "z"
:required true :required true
:description ""
:schema {:type "integer" :schema {:type "integer"
:format "int64"}}] :format "int64"}}]
:requestBody {:content {"application/json" {:schema {:oneOf [{:items {:type "integer" :requestBody {:content {"application/json" {:schema {:oneOf [{:items {:type "integer"
@ -192,7 +198,11 @@
:type "object"}}}} :type "object"}}}}
400 {:content {"application/json" {:schema {:type "string"}}} 400 {:content {"application/json" {:schema {:type "string"}}}
:description "kosh"} :description "kosh"}
500 {:description "fail"}} 500 {:description "fail"}
504 {:description "default"
:content {"application/json" {:schema {:properties {"error" {:type "string"}}
:required ["error"]
:type "object"}}}}}
:summary "plus with body"}} :summary "plus with body"}}
"/api/malli/plus/{z}" {:get {:parameters [{:in "query" "/api/malli/plus/{z}" {:get {:parameters [{:in "query"
:name :x :name :x
@ -230,23 +240,25 @@
:type "object"}}}} :type "object"}}}}
400 {:description "kosh" 400 {:description "kosh"
:content {"application/json" {:schema {:type "string"}}}} :content {"application/json" {:schema {:type "string"}}}}
500 {:description "fail"}} 500 {:description "fail"}
504 {:description "default"
:content {"application/json" {:schema {:additionalProperties false
:properties {:error {:type "string"}}
:required [:error]
:type "object"}}}}}
:summary "plus with body"}} :summary "plus with body"}}
"/api/schema/plus/{z}" {:get {:parameters [{:description "" "/api/schema/plus/{z}" {:get {:parameters [{:in "query"
:in "query"
:name "x" :name "x"
:required true :required true
:schema {:format "int32" :schema {:format "int32"
:type "integer"}} :type "integer"}}
{:description "" {:in "query"
:in "query"
:name "y" :name "y"
:required true :required true
:schema {:type "integer" :schema {:type "integer"
:format "int32"}} :format "int32"}}
{:in "path" {:in "path"
:name "z" :name "z"
:description ""
:required true :required true
:schema {:type "integer" :schema {:type "integer"
:format "int32"}}] :format "int32"}}]
@ -263,7 +275,6 @@
:summary "plus"} :summary "plus"}
:post {:parameters [{:in "path" :post {:parameters [{:in "path"
:name "z" :name "z"
:description ""
:required true :required true
:schema {:type "integer" :schema {:type "integer"
:format "int32"}}] :format "int32"}}]
@ -279,10 +290,15 @@
:type "object"}}}} :type "object"}}}}
400 {:description "kosh" 400 {:description "kosh"
:content {"application/json" {:schema {:type "string"}}}} :content {"application/json" {:schema {:type "string"}}}}
500 {:description "fail"}} 500 {:description "fail"}
504 {:description "default"
:content {"application/json" {:schema {:additionalProperties false
:properties {"error" {:type "string"}}
:required ["error"]
:type "object"}}}}}
:summary "plus with body"}}}}] :summary "plus with body"}}}}]
(is (= expected spec)) (is (= expected spec))
(is (nil? (validate spec)))))) (is (= nil (validate spec))))))
(defn spec-paths [app uri] (defn spec-paths [app uri]
(-> {:request-method :get, :uri uri} app :body :paths keys)) (-> {:request-method :get, :uri uri} app :body :paths keys))
@ -364,10 +380,12 @@
(deftest all-parameter-types-test (deftest all-parameter-types-test
(doseq [[coercion ->schema] (doseq [[coercion ->schema]
[[#'malli/coercion (fn [nom] [:map [nom :string]])] [[#'malli/coercion (fn [nom] [:map [nom [:string {:description (str "description " nom)}]]])]
[#'schema/coercion (fn [nom] {nom s/Str})] [#'schema/coercion (fn [nom] {nom (schema-tools.core/schema s/Str
[#'spec/coercion (fn [nom] {nom string?})]]] {:description (str "description " nom)})})]
(testing coercion [#'spec/coercion (fn [nom] {nom (st/spec {:spec string?
:description (str "description " nom)})})]]]
(testing (str coercion)
(let [app (ring/ring-handler (let [app (ring/ring-handler
(ring/router (ring/router
[["/parameters" [["/parameters"
@ -394,18 +412,22 @@
(is (match? [{:in "query" (is (match? [{:in "query"
:name "q" :name "q"
:required true :required true
:description "description :q"
:schema {:type "string"}} :schema {:type "string"}}
{:in "header" {:in "header"
:name "h" :name "h"
:required true :required true
:description "description :h"
:schema {:type "string"}} :schema {:type "string"}}
{:in "cookie" {:in "cookie"
:name "c" :name "c"
:required true :required true
:description "description :c"
:schema {:type "string"}} :schema {:type "string"}}
{:in "path" {:in "path"
:name "p" :name "p"
:required true :required true
:description "description :p"
:schema {:type "string"}}] :schema {:type "string"}}]
(-> spec (-> spec
(get-in [:paths "/parameters" :post :parameters]) (get-in [:paths "/parameters" :post :parameters])
@ -432,9 +454,85 @@
(testing "spec is valid" (testing "spec is valid"
(is (nil? (validate spec)))))))) (is (nil? (validate spec))))))))
(deftest examples-test
(doseq [[coercion ->schema]
[[#'malli/coercion (fn [nom] [:map
{:json-schema/example {nom "EXAMPLE2"}}
[nom [:string {:json-schema/example "EXAMPLE"}]]])]
[#'schema/coercion (fn [nom] (schema-tools.core/schema
{nom (schema-tools.core/schema s/Str {:openapi/example "EXAMPLE"})}
{:openapi/example {nom "EXAMPLE2"}}))]
[#'spec/coercion (fn [nom]
(assoc
(ds/spec ::foo {nom (st/spec string? {:openapi/example "EXAMPLE"})})
:openapi/example {nom "EXAMPLE2"}))]]]
(testing (str coercion)
(let [app (ring/ring-handler
(ring/router
[["/examples"
{:post {:decription "examples"
:coercion @coercion
:request {:body (->schema :b)}
:parameters {:query (->schema :q)}
:responses {200 {:description "success"
:body (->schema :ok)}}
:openapi {:requestBody
{:content
{"application/json"
{:examples
{"named-example" {:description "a named example"
:value {:b "named"}}}}}}
:responses
{200
{:content
{"application/json"
{:examples
{"response-example" {:value {:ok "response"}}}}}}}}
:handler identity}}]
["/openapi.json"
{:get {:handler (openapi/create-openapi-handler)
:openapi {:info {:title "" :version "0.0.1"}}
:no-doc true}}]]
{:data {:middleware [openapi/openapi-feature]}}))
spec (-> {:request-method :get
:uri "/openapi.json"}
app
:body)]
(testing "query parameter"
(is (match? [{:in "query"
:name "q"
:required true
:schema {:type "string"
:example "EXAMPLE"}}]
(-> spec
(get-in [:paths "/examples" :post :parameters])
normalize))))
(testing "body parameter"
(is (match? {:schema {:type "object"
:properties {:b {:type "string"
:example "EXAMPLE"}}
:required ["b"]
:example {:b "EXAMPLE2"}}
:examples {:named-example {:description "a named example"
:value {:b "named"}}}}
(-> spec
(get-in [:paths "/examples" :post :requestBody :content "application/json"])
normalize))))
(testing "body response"
(is (match? {:schema {:type "object"
:properties {:ok {:type "string"
:example "EXAMPLE"}}
:required ["ok"]
:example {:ok "EXAMPLE2"}}
:examples {:response-example {:value {:ok "response"}}}}
(-> spec
(get-in [:paths "/examples" :post :responses 200 :content "application/json"])
normalize))))
(testing "spec is valid"
(is (nil? (validate spec))))))))
(deftest multipart-test (deftest multipart-test
(doseq [[coercion file-schema string-schema] (doseq [[coercion file-schema string-schema] [[#'malli/coercion
[[#'malli/coercion
reitit.ring.malli/bytes-part reitit.ring.malli/bytes-part
:string] :string]
[#'schema/coercion [#'schema/coercion
@ -447,7 +545,7 @@
[#'spec/coercion [#'spec/coercion
reitit.http.interceptors.multipart/bytes-part reitit.http.interceptors.multipart/bytes-part
string?]]] string?]]]
(testing coercion (testing (str coercion)
(let [app (ring/ring-handler (let [app (ring/ring-handler
(ring/router (ring/router
[["/upload" [["/upload"
@ -481,21 +579,20 @@
(is (nil? (validate spec)))))))) (is (nil? (validate spec))))))))
(deftest per-content-type-test (deftest per-content-type-test
(doseq [[coercion ->schema] (doseq [[coercion ->schema] [[malli/coercion (fn [nom] [:map [nom :string]])]
[[#'malli/coercion (fn [nom] [:map [nom :string]])] [schema/coercion (fn [nom] {nom s/Str})]
[#'schema/coercion (fn [nom] {nom s/Str})] [spec/coercion (fn [nom] {nom string?})]]]
[#'spec/coercion (fn [nom] {nom string?})]]] (testing (str coercion)
(testing coercion
(let [app (ring/ring-handler (let [app (ring/ring-handler
(ring/router (ring/router
[["/parameters" [["/parameters"
{:post {:description "parameters" {:post {:description "parameters"
:coercion @coercion :coercion coercion
:parameters {:request {:content {"application/json" (->schema :b) :request {:content {"application/json" {:schema (->schema :b)}
"application/edn" (->schema :c)}}} "application/edn" {:schema (->schema :c)}}}
:responses {200 {:description "success" :responses {200 {:description "success"
:content {"application/json" (->schema :ok) :content {"application/json" {:schema (->schema :ok)}
"application/edn" (->schema :edn)}}} "application/edn" {:schema (->schema :edn)}}}}
:handler (fn [req] :handler (fn [req]
{:status 200 {:status 200
:body (-> req :parameters :request)})}}] :body (-> req :parameters :request)})}}]
@ -510,37 +607,38 @@
spec (-> {:request-method :get spec (-> {:request-method :get
:uri "/openapi.json"} :uri "/openapi.json"}
app app
:body)] :body)
spec-coercion (= coercion spec/coercion)]
(testing "body parameter" (testing "body parameter"
(is (match? (merge {:type "object" (is (= (merge {:type "object"
:properties {:b {:type "string"}} :properties {:b {:type "string"}}
:required ["b"]} :required ["b"]}
(when-not (#{#'spec/coercion} coercion) (when-not spec-coercion
{:additionalProperties false})) {:additionalProperties false}))
(-> spec (-> spec
(get-in [:paths "/parameters" :post :requestBody :content "application/json" :schema]) (get-in [:paths "/parameters" :post :requestBody :content "application/json" :schema])
normalize))) normalize)))
(is (match? (merge {:type "object" (is (= (merge {:type "object"
:properties {:c {:type "string"}} :properties {:c {:type "string"}}
:required ["c"]} :required ["c"]}
(when-not (#{#'spec/coercion} coercion) (when-not spec-coercion
{:additionalProperties false})) {:additionalProperties false}))
(-> spec (-> spec
(get-in [:paths "/parameters" :post :requestBody :content "application/edn" :schema]) (get-in [:paths "/parameters" :post :requestBody :content "application/edn" :schema])
normalize)))) normalize))))
(testing "body response" (testing "body response"
(is (match? (merge {:type "object" (is (= (merge {:type "object"
:properties {:ok {:type "string"}} :properties {:ok {:type "string"}}
:required ["ok"]} :required ["ok"]}
(when-not (#{#'spec/coercion} coercion) (when-not spec-coercion
{:additionalProperties false})) {:additionalProperties false}))
(-> spec (-> spec
(get-in [:paths "/parameters" :post :responses 200 :content "application/json" :schema]) (get-in [:paths "/parameters" :post :responses 200 :content "application/json" :schema])
normalize))) normalize)))
(is (match? (merge {:type "object" (is (= (merge {:type "object"
:properties {:edn {:type "string"}} :properties {:edn {:type "string"}}
:required ["edn"]} :required ["edn"]}
(when-not (#{#'spec/coercion} coercion) (when-not spec-coercion
{:additionalProperties false})) {:additionalProperties false}))
(-> spec (-> spec
(get-in [:paths "/parameters" :post :responses 200 :content "application/edn" :schema]) (get-in [:paths "/parameters" :post :responses 200 :content "application/edn" :schema])
@ -569,23 +667,22 @@
(is (nil? (validate spec)))))))) (is (nil? (validate spec))))))))
(deftest default-content-type-test (deftest default-content-type-test
(doseq [[coercion ->schema] (doseq [[coercion ->schema] [[malli/coercion (fn [nom] [:map [nom :string]])]
[[#'malli/coercion (fn [nom] [:map [nom :string]])] [schema/coercion (fn [nom] {nom s/Str})]
[#'schema/coercion (fn [nom] {nom s/Str})] [spec/coercion (fn [nom] {nom string?})]]]
[#'spec/coercion (fn [nom] {nom string?})]]] (testing (str coercion)
(testing coercion
(doseq [content-type ["application/json" "application/edn"]] (doseq [content-type ["application/json" "application/edn"]]
(testing (str "default content type " content-type) (testing (str "default content type " content-type)
(let [app (ring/ring-handler (let [app (ring/ring-handler
(ring/router (ring/router
[["/parameters" [["/parameters"
{:post {:description "parameters" {:post {:description "parameters"
:coercion @coercion :coercion coercion
:content-types [content-type] ;; TODO should this be under :openapi ? :content-types [content-type] ;; TODO should this be under :openapi ?
:parameters {:request {:content {"application/transit" (->schema :transit)} :request {:content {"application/transit" {:schema (->schema :transit)}}
:body (->schema :default)}} :body (->schema :default)}
:responses {200 {:description "success" :responses {200 {:description "success"
:content {"application/transit" (->schema :transit)} :content {"application/transit" {:schema (->schema :transit)}}
:body (->schema :default)}} :body (->schema :default)}}
:handler (fn [req] :handler (fn [req]
{:status 200 {:status 200
@ -623,8 +720,7 @@
[["/parameters" [["/parameters"
{:post {:description "parameters" {:post {:description "parameters"
:coercion malli/coercion :coercion malli/coercion
:parameters {:request :request {:body
{:body
[:schema [:schema
{:registry {"friend" [:map {:registry {"friend" [:map
[:age int?] [:age int?]
@ -632,7 +728,7 @@
"pet" [:map "pet" [:map
[:name :string] [:name :string]
[:friends [:vector [:ref "friend"]]]]}} [:friends [:vector [:ref "friend"]]]]}}
"friend"]}} "friend"]}
:handler (fn [req] :handler (fn [req]
{:status 200 {:status 200
:body (-> req :parameters :request)})}}] :body (-> req :parameters :request)})}}]
@ -670,3 +766,53 @@
spec)) spec))
(testing "spec is valid" (testing "spec is valid"
(is (nil? (validate spec)))))) (is (nil? (validate spec))))))
(deftest openapi-malli-tests
(let [app (ring/ring-handler
(ring/router
[["/openapi.json"
{:get {:no-doc true
:handler (openapi/create-openapi-handler)}}]
["/malli" {:coercion malli/coercion}
["/plus" {:post {:summary "plus with body"
:request {:description "body description"
:content {"application/json" {:schema {:x int?, :y int?}
:examples {"1+1" {:x 1, :y 1}
"1+2" {:x 1, :y 2}}
:openapi {:example {:x 2, :y 2}}}}}
:responses {200 {:description "success"
:content {"application/json" {:schema {:total int?}
:examples {"2" {:total 2}
"3" {:total 3}}
:openapi {:example {:total 4}}}}}}
:handler (fn [request]
(let [{:keys [x y]} (-> request :parameters :body)]
{:status 200, :body {:total (+ x y)}}))}}]]]
{:validate reitit.ring.spec/validate
:data {:middleware [openapi/openapi-feature
rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]}}))]
(is (= {"/malli/plus" {:post {:requestBody {:content {:description "body description",
"application/json" {:schema {:type "object",
:properties {:x {:type "integer"},
:y {:type "integer"}},
:required [:x :y],
:additionalProperties false},
:examples {"1+1" {:x 1, :y 1}, "1+2" {:x 1, :y 2}},
:example {:x 2, :y 2}}}},
:responses {200 {:description "success",
:content {"application/json" {:schema {:type "object",
:properties {:total {:type "integer"}},
:required [:total],
:additionalProperties false},
:examples {"2" {:total 2}, "3" {:total 3}},
:example {:total 4}}}}},
:summary "plus with body"}}})
(-> {:request-method :get
:uri "/openapi.json"}
(app)
:body
:paths))))

View file

@ -234,14 +234,12 @@
([] {}) ([] {})
([left] left) ([left] left)
([left right] ([left right]
(if (and (map? left) (map? right) (let [pleft (-> left :parameters :path)
(contains? left :parameters) pright (-> right :parameters :path)]
(contains? right :parameters)) (if (and (map? left) (map? right) pleft pright)
(-> (merge-with custom-meta-merge-checking-parameters left right) (-> (merge-with custom-meta-merge-checking-parameters left right)
(assoc :parameters (merge-with mu/merge (assoc-in [:parameters :path] (reduce mu/merge (concat pleft pright))))
(:parameters left) (meta-merge left right))))
(:parameters right))))
(meta-merge left right)))
([left right & more] ([left right & more]
(reduce custom-meta-merge-checking-parameters left (cons right more)))) (reduce custom-meta-merge-checking-parameters left (cons right more))))
@ -586,44 +584,65 @@
(deftest per-content-type-test (deftest per-content-type-test
(doseq [[coercion json-request edn-request default-request json-response edn-response default-response] (doseq [[coercion json-request edn-request default-request json-response edn-response default-response]
[[#'malli/coercion [[malli/coercion
[:map [:request [:enum :json]] [:response any?]] [:map [:request [:enum :json]] [:response any?]]
[:map [:request [:enum :edn]] [:response any?]] [:map [:request [:enum :edn]] [:response any?]]
[:map [:request [:enum :default]] [:response any?]] [:map [:request [:enum :default]] [:response any?]]
[:map [:request any?] [:response [:enum :json]]] [:map [:request any?] [:response [:enum :json]]]
[:map [:request any?] [:response [:enum :edn]]] [:map [:request any?] [:response [:enum :edn]]]
[:map [:request any?] [:response [:enum :default]]]] [:map [:request any?] [:response [:enum :default]]]]
[#'schema/coercion [schema/coercion
{:request (s/eq :json) :response s/Any} {:request (s/eq :json) :response s/Any}
{:request (s/eq :edn) :response s/Any} {:request (s/eq :edn) :response s/Any}
{:request (s/eq :default) :response s/Any} {:request (s/eq :default) :response s/Any}
{:request s/Any :response (s/eq :json)} {:request s/Any :response (s/eq :json)}
{:request s/Any :response (s/eq :edn)} {:request s/Any :response (s/eq :edn)}
{:request s/Any :response (s/eq :default)}] {:request s/Any :response (s/eq :default)}]
[#'spec/coercion [spec/coercion
{:request (clojure.spec.alpha/spec #{:json}) :response any?} {:request (clojure.spec.alpha/spec #{:json}) :response any?}
{:request (clojure.spec.alpha/spec #{:edn}) :response any?} {:request (clojure.spec.alpha/spec #{:edn}) :response any?}
{:request (clojure.spec.alpha/spec #{:default}) :response any?} {:request (clojure.spec.alpha/spec #{:default}) :response any?}
{:request any? :response (clojure.spec.alpha/spec #{:json})} {:request any? :response (clojure.spec.alpha/spec #{:json})}
{:request any? :response (clojure.spec.alpha/spec #{:end})} {:request any? :response (clojure.spec.alpha/spec #{:end})}
{:request any? :response (clojure.spec.alpha/spec #{:default})}]]] {:request any? :response (clojure.spec.alpha/spec #{:default})}]]]
(testing coercion (testing (str coercion)
(let [app (ring/ring-handler (doseq [{:keys [name app]}
[{:name "using top-level :body"
:app (ring/ring-handler
(ring/router (ring/router
[["/foo" {:post {:parameters {:request {:content {"application/json" json-request ["/foo" {:post {:request {:content {"application/json" {:schema json-request}
"application/edn" edn-request} "application/edn" {:schema edn-request}}
:body default-request}} :body default-request}
:responses {200 {:content {"application/json" json-response :responses {200 {:content {"application/json" {:schema json-response}
"application/edn" edn-response} "application/edn" {:schema edn-response}}
:body default-response}} :body default-response}}
:handler (fn [req] :handler (fn [req]
{:status 200 {:status 200
:body (-> req :parameters :request)})}}]] :body (-> req :parameters :request)})}}]
{:validate reitit.ring.spec/validate {:validate reitit.ring.spec/validate
:data {:middleware [rrc/coerce-request-middleware :data {:middleware [rrc/coerce-request-middleware
rrc/coerce-response-middleware] rrc/coerce-response-middleware]
:coercion @coercion}})) :coercion coercion}}))}
call (fn [request] {:name "using :default content"
:app (ring/ring-handler
(ring/router
["/foo" {:post {:request {:content {"application/json" {:schema json-request}
"application/edn" {:schema edn-request}
:default {:schema default-request}}
:body json-request} ;; not applied as :default exists
:responses {200 {:content {"application/json" {:schema json-response}
"application/edn" {:schema edn-response}
:default {:schema default-response}}
:body json-response}} ;; not applied as :default exists
:handler (fn [req]
{:status 200
:body (-> req :parameters :request)})}}]
{:validate reitit.ring.spec/validate
:data {:middleware [rrc/coerce-request-middleware
rrc/coerce-response-middleware]
:coercion coercion}}))}]]
(testing name
(let [call (fn [request]
(try (try
(app request) (app request)
(catch ExceptionInfo e (catch ExceptionInfo e
@ -654,7 +673,7 @@
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]} (is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
(call (request "application/json" "application/edn" {:request :json :response :json})))) (call (request "application/json" "application/edn" {:request :json :response :json}))))
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]} (is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
(call (request "application/json" "application/transit" {:request :json :response :json}))))))))) (call (request "application/json" "application/transit" {:request :json :response :json})))))))))))
#?(:clj #?(:clj

View file

@ -138,7 +138,6 @@
rrc/coerce-request-middleware rrc/coerce-request-middleware
rrc/coerce-response-middleware]}}))) rrc/coerce-response-middleware]}})))
(require '[fipp.edn])
(deftest swagger-test (deftest swagger-test
(testing "endpoints work" (testing "endpoints work"
(testing "spec" (testing "spec"
@ -451,7 +450,7 @@
(ring/router (ring/router
[["/parameters" [["/parameters"
{:post {:coercion spec/coercion {:post {:coercion spec/coercion
:parameters {:request {:content {"application/json" {:x string?}}}} :request {:content {"application/json" {:x string?}}}
:handler identity}}] :handler identity}}]
["/swagger.json" ["/swagger.json"
{:get {:no-doc true {:get {:no-doc true
@ -479,7 +478,7 @@
[#'spec/coercion [#'spec/coercion
reitit.http.interceptors.multipart/bytes-part reitit.http.interceptors.multipart/bytes-part
string?]]] string?]]]
(testing coercion (testing (str coercion)
(let [app (ring/ring-handler (let [app (ring/ring-handler
(ring/router (ring/router
[["/upload" [["/upload"

View file

@ -282,3 +282,16 @@
(testing "Need to coerce current values manually" (testing "Need to coerce current values manually"
(is (= "foo?foo=2" (is (= "foo?foo=2"
(rf/set-query-params "foo?foo=1" (fn [q] (update q :foo #(inc (js/parseInt %))))))))) (rf/set-query-params "foo?foo=1" (fn [q] (update q :foo #(inc (js/parseInt %)))))))))
(deftest match->path-test
(is (= "foo"
(rf/match->path {:path "foo"} nil nil)
(rf/match->path {:path "foo"} {} "")))
(is (= "foo?a=1&b=&c=foo+bar"
;; NOTE: This encoding differs from set-query
(rf/match->path {:path "foo"} {:a "1" :b "" :c "foo bar"} nil)))
(is (= "foo#aaa"
(rf/match->path {:path "foo"} nil "aaa")))
(testing "Fragment encoding"
(is (= "foo#foo+bar+%25"
(rf/match->path {:path "foo"} nil "foo bar %")))))

View file

@ -29,10 +29,10 @@
1 (do (is (some? (:popstate-listener history))) 1 (do (is (some? (:popstate-listener history)))
(is (= "/" url) (is (= "/" url)
"start at root") "start at root")
(rfe/push-state ::foo)) (rfe/push-state ::foo nil {:a 1} "foo bar"))
;; 0. / ;; 0. /
;; 1. /foo ;; 1. /foo?a=1#foo+bar
2 (do (is (= "/foo" url) 2 (do (is (= "/foo?a=1#foo+bar" url)
"push-state") "push-state")
(.back js/window.history)) (.back js/window.history))
;; 0. / ;; 0. /

View file

@ -26,6 +26,8 @@
(rfh/href history ::bar {:id 5}))) (rfh/href history ::bar {:id 5})))
(is (= "#/bar/5?q=x" (is (= "#/bar/5?q=x"
(rfh/href history ::bar {:id 5} {:q "x"}))) (rfh/href history ::bar {:id 5} {:q "x"})))
(is (= "#/bar/5?q=x#foo"
(rfh/href history ::bar {:id 5} {:q "x"} "foo")))
(let [{:keys [value messages]} (capture-console (let [{:keys [value messages]} (capture-console
(fn [] (fn []
(rfh/href history ::asd)))] (rfh/href history ::asd)))]