diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index d2f0105b..47f70b8d 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -70,9 +70,24 @@ jobs: run: ./scripts/test.sh cljs lint: - name: Lint + name: Lint cljdoc.edn runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Verify cljdoc.edn run: curl -fsSL https://raw.githubusercontent.com/cljdoc/cljdoc/master/script/verify-cljdoc-edn | bash -s doc/cljdoc.edn + + 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a43f061..97dcfdd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,44 @@ We use [Break Versioning][breakver]. The version numbers follow a `.... data` to expand route arg to route data (default `reitit.core/expand`) -| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil` -| `:meta-merge` | Function which follows the signature of `meta-merge.core/meta-merge`, useful for when you want to have more control over the meta merging -| `:compile` | Function of `route opts => result` to compile a route handler -| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects -| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes -| `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`) -| `:router` | Function of `routes opts => router` to override the actual router implementation +| key | description +|-----------------|------------- +| `:path` | Base-path for routes +| `:routes` | Initial resolved routes (default `[]`) +| `:data` | Initial route data (default `{}`) +| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this +| `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon}) +| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`) +| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil` +| `:compile` | Function of `route opts => result` to compile a route handler +| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects +| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes +| `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`) +| `: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 + + diff --git a/doc/basics/error_messages.md b/doc/basics/error_messages.md index 9eb8b339..3545c27a 100644 --- a/doc/basics/error_messages.md +++ b/doc/basics/error_messages.md @@ -22,7 +22,7 @@ The default exception formatting uses `reitit.exception/exception`. It produces ## Pretty Errors ```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. diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index b03b0bd3..5a8cfdc5 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -12,7 +12,8 @@ metosin/reitit-swagger-ui metosin/reitit-frontend metosin/reitit-sieppari - metosin/reitit-pedestal] + metosin/reitit-pedestal + fi.metosin/reitit-openapi] :cljdoc.doc/tree [["Introduction" {:file "doc/README.md"}] ["Basics" {} diff --git a/doc/http/default_interceptors.md b/doc/http/default_interceptors.md index 2fda5873..946b8d24 100644 --- a/doc/http/default_interceptors.md +++ b/doc/http/default_interceptors.md @@ -1,7 +1,7 @@ # Default Interceptors ```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. diff --git a/doc/http/interceptors.md b/doc/http/interceptors.md index fe84137e..aeca65bc 100644 --- a/doc/http/interceptors.md +++ b/doc/http/interceptors.md @@ -5,7 +5,7 @@ Reitit has also support for [interceptors](http://pedestal.io/reference/intercep ## Reitit-http ```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. diff --git a/doc/http/pedestal.md b/doc/http/pedestal.md index 7b1d10de..3c21b5bd 100644 --- a/doc/http/pedestal.md +++ b/doc/http/pedestal.md @@ -3,7 +3,7 @@ [Pedestal](http://pedestal.io/) is a backend web framework for Clojure. `reitit-pedestal` provides an alternative routing engine for Pedestal. ```clj -[metosin/reitit-pedestal "0.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)? @@ -26,8 +26,8 @@ A minimalistic example on how to to swap the default-router with a reitit router ```clj ; [io.pedestal/pedestal.service "0.5.5"] ; [io.pedestal/pedestal.jetty "0.5.5"] -; [metosin/reitit-pedestal "0.6.0"] -; [metosin/reitit "0.6.0"] +; [metosin/reitit-pedestal "0.7.0-alpha5"] +; [metosin/reitit "0.7.0-alpha5"] (require '[io.pedestal.http :as server]) (require '[reitit.pedestal :as pedestal]) diff --git a/doc/http/sieppari.md b/doc/http/sieppari.md index 180f0bc5..9f7e341e 100644 --- a/doc/http/sieppari.md +++ b/doc/http/sieppari.md @@ -1,7 +1,7 @@ # Sieppari ```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). diff --git a/doc/http/transforming_interceptor_chain.md b/doc/http/transforming_interceptor_chain.md index b4b9793a..1ca869b9 100644 --- a/doc/http/transforming_interceptor_chain.md +++ b/doc/http/transforming_interceptor_chain.md @@ -65,7 +65,7 @@ There is an extra option in http-router (actually, in the underlying interceptor ### Printing Context Diffs ```clj -[metosin/reitit-interceptors "0.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: diff --git a/doc/ring/coercion.md b/doc/ring/coercion.md index 8ca8fb68..32f51e68 100644 --- a/doc/ring/coercion.md +++ b/doc/ring/coercion.md @@ -157,21 +157,21 @@ You can also specify request and response body schemas per content-type. The syn ```clj (def app (ring/ring-handler - (ring/router - ["/api" - ["/example" {:post {:coercion reitit.coercion.schema/coercion - :parameters {:request {:content {"application/json" {:y s/Int} - "application/edn" {:z s/Int}} - ;; default if no content-type matches: - :body {:yy s/Int}}} - :responses {200 {:content {"application/json" {:w s/Int} - "application/edn" {:x s/Int}} - ;; default if no content-type matches: - :body {:ww s/Int}} - :handler ...}}]] - {:data {:middleware [rrc/coerce-exceptions-middleware - rrc/coerce-request-middleware - rrc/coerce-response-middleware]}}))) + (ring/router + ["/api" + ["/example" {:post {:coercion reitit.coercion.schema/coercion + :request {:content {"application/json" {:schema {:y s/Int}} + "application/edn" {:schema {:z s/Int}}} + ;; default if no content-type matches: + :body {:yy s/Int}} + :responses {200 {:content {"application/json" {:schema {:w s/Int}} + "application/edn" {:schema {:x s/Int}}} + ;; default if no content-type matches: + :body {:ww s/Int}}} + :handler ...}}]] + {:data {:middleware [rrc/coerce-exceptions-middleware + rrc/coerce-request-middleware + rrc/coerce-response-middleware]}}))) ``` ## Pretty printing spec errors diff --git a/doc/ring/default_middleware.md b/doc/ring/default_middleware.md index c8b4160a..2f501d2c 100644 --- a/doc/ring/default_middleware.md +++ b/doc/ring/default_middleware.md @@ -1,7 +1,7 @@ # Default Middleware ```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. @@ -59,4 +59,4 @@ Partial sample output: ## 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: . diff --git a/doc/ring/exceptions.md b/doc/ring/exceptions.md index 43b3f1e0..a4376a5a 100644 --- a/doc/ring/exceptions.md +++ b/doc/ring/exceptions.md @@ -1,7 +1,7 @@ # Exception Handling with Ring ```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. diff --git a/doc/ring/openapi.md b/doc/ring/openapi.md index d3e127cf..76197d56 100644 --- a/doc/ring/openapi.md +++ b/doc/ring/openapi.md @@ -5,8 +5,11 @@ 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). -The [http-swagger](../../examples/http-swagger) and -[ring-malli-swagger](../../examples/ring-malli-swagger) examples also +The +[ring-malli-swagger](../../examples/ring-malli-swagger) +and +[ring-spec-swagger](../../examples/ring-spec-swagger) +examples also have OpenAPI documentation. ## 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). +## 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 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. diff --git a/doc/ring/ring.md b/doc/ring/ring.md index 2621e9bb..f94db5f7 100644 --- a/doc/ring/ring.md +++ b/doc/ring/ring.md @@ -5,7 +5,7 @@ Read more about the [Ring Concepts](https://github.com/ring-clojure/ring/wiki/Concepts). ```clj -[metosin/reitit-ring "0.6.0"] +[metosin/reitit-ring "0.7.0-alpha5"] ``` ## `reitit.ring/router` diff --git a/doc/ring/swagger.md b/doc/ring/swagger.md index d576512b..d9bdad0b 100644 --- a/doc/ring/swagger.md +++ b/doc/ring/swagger.md @@ -1,7 +1,7 @@ # 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. @@ -47,7 +47,7 @@ If you need to post-process the generated spec, just wrap the handler with a cus [Swagger-ui](https://github.com/swagger-api/swagger-ui) is a user interface to visualize and interact with the Swagger specification. To make things easy, there is a pre-integrated version of the swagger-ui as a separate module. ``` -[metosin/reitit-swagger-ui "0.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: @@ -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` * 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 (ns example.server diff --git a/doc/ring/transforming_middleware_chain.md b/doc/ring/transforming_middleware_chain.md index 7e4d1a6a..ff742c80 100644 --- a/doc/ring/transforming_middleware_chain.md +++ b/doc/ring/transforming_middleware_chain.md @@ -59,7 +59,7 @@ There is an extra option in the Ring router (actually, in the underlying middlew ### Printing Request Diffs ```clj -[metosin/reitit-middleware "0.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: diff --git a/examples/README.md b/examples/README.md index 27134eb3..dbc42b05 100644 --- a/examples/README.md +++ b/examples/README.md @@ -44,7 +44,3 @@ Coercion with Malli and Swagger generation. ## ring-spec-swagger Coercion with Spec and Swagger generation. - -## ring-swagger - -Coercion with Spec and Swagger generation. Same as previous! diff --git a/examples/buddy-auth/project.clj b/examples/buddy-auth/project.clj index a13f1eed..9be472d7 100644 --- a/examples/buddy-auth/project.clj +++ b/examples/buddy-auth/project.clj @@ -2,6 +2,6 @@ :description "Reitit Buddy Auth App" :dependencies [[org.clojure/clojure "1.10.1"] [ring/ring-jetty-adapter "1.8.1"] - [metosin/reitit "0.6.0"] + [metosin/reitit "0.7.0-alpha5"] [buddy "2.0.0"]] :repl-options {:init-ns example.server}) diff --git a/examples/frontend-auth/project.clj b/examples/frontend-auth/project.clj index 21e85672..22c81fde 100644 --- a/examples/frontend-auth/project.clj +++ b/examples/frontend-auth/project.clj @@ -10,9 +10,9 @@ [ring "1.7.1"] [hiccup "1.0.5"] [org.clojure/clojurescript "1.10.439"] - [metosin/reitit "0.6.0"] - [metosin/reitit-schema "0.6.0"] - [metosin/reitit-frontend "0.6.0"] + [metosin/reitit "0.7.0-alpha5"] + [metosin/reitit-schema "0.7.0-alpha5"] + [metosin/reitit-frontend "0.7.0-alpha5"] ;; Just for pretty printting the match [fipp "0.6.14"]] diff --git a/examples/frontend-controllers/project.clj b/examples/frontend-controllers/project.clj index 21e85672..22c81fde 100644 --- a/examples/frontend-controllers/project.clj +++ b/examples/frontend-controllers/project.clj @@ -10,9 +10,9 @@ [ring "1.7.1"] [hiccup "1.0.5"] [org.clojure/clojurescript "1.10.439"] - [metosin/reitit "0.6.0"] - [metosin/reitit-schema "0.6.0"] - [metosin/reitit-frontend "0.6.0"] + [metosin/reitit "0.7.0-alpha5"] + [metosin/reitit-schema "0.7.0-alpha5"] + [metosin/reitit-frontend "0.7.0-alpha5"] ;; Just for pretty printting the match [fipp "0.6.14"]] diff --git a/examples/frontend-controllers/src/frontend/core.cljs b/examples/frontend-controllers/src/frontend/core.cljs index 6a2388f2..851e11ce 100644 --- a/examples/frontend-controllers/src/frontend/core.cljs +++ b/examples/frontend-controllers/src/frontend/core.cljs @@ -18,7 +18,7 @@ [:div [:ul [: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 [:h2 "Selected item " id]) [:p "Query params: " [:pre (pr-str query)]] diff --git a/examples/frontend-links/project.clj b/examples/frontend-links/project.clj index f41ec97a..9b054a1e 100644 --- a/examples/frontend-links/project.clj +++ b/examples/frontend-links/project.clj @@ -10,9 +10,9 @@ [ring "1.7.1"] [hiccup "1.0.5"] [org.clojure/clojurescript "1.10.520"] - [metosin/reitit "0.6.0"] - [metosin/reitit-spec "0.6.0"] - [metosin/reitit-frontend "0.6.0"] + [metosin/reitit "0.7.0-alpha5"] + [metosin/reitit-spec "0.7.0-alpha5"] + [metosin/reitit-frontend "0.7.0-alpha5"] ;; Just for pretty printting the match [fipp "0.6.14"]] diff --git a/examples/frontend-prompt/project.clj b/examples/frontend-prompt/project.clj index f41ec97a..9b054a1e 100644 --- a/examples/frontend-prompt/project.clj +++ b/examples/frontend-prompt/project.clj @@ -10,9 +10,9 @@ [ring "1.7.1"] [hiccup "1.0.5"] [org.clojure/clojurescript "1.10.520"] - [metosin/reitit "0.6.0"] - [metosin/reitit-spec "0.6.0"] - [metosin/reitit-frontend "0.6.0"] + [metosin/reitit "0.7.0-alpha5"] + [metosin/reitit-spec "0.7.0-alpha5"] + [metosin/reitit-frontend "0.7.0-alpha5"] ;; Just for pretty printting the match [fipp "0.6.14"]] diff --git a/examples/frontend-re-frame/project.clj b/examples/frontend-re-frame/project.clj index 594516c1..b7e3f404 100644 --- a/examples/frontend-re-frame/project.clj +++ b/examples/frontend-re-frame/project.clj @@ -1,7 +1,7 @@ (defproject frontend-re-frame "0.1.0-SNAPSHOT" :dependencies [[org.clojure/clojure "1.10.0"] [org.clojure/clojurescript "1.10.520"] - [metosin/reitit "0.6.0"] + [metosin/reitit "0.7.0-alpha5"] [reagent "0.8.1"] [re-frame "0.10.6"]] diff --git a/examples/frontend/project.clj b/examples/frontend/project.clj index b5c6395a..188f954f 100644 --- a/examples/frontend/project.clj +++ b/examples/frontend/project.clj @@ -10,9 +10,9 @@ [ring "1.8.1"] [hiccup "1.0.5"] [org.clojure/clojurescript "1.10.773"] - [metosin/reitit "0.6.0"] - [metosin/reitit-spec "0.6.0"] - [metosin/reitit-frontend "0.6.0"] + [metosin/reitit "0.7.0-alpha5"] + [metosin/reitit-spec "0.7.0-alpha5"] + [metosin/reitit-frontend "0.7.0-alpha5"] ;; Just for pretty printting the match [fipp "0.6.23"]] diff --git a/examples/http-swagger/project.clj b/examples/http-swagger/project.clj index 47b8c500..d34040da 100644 --- a/examples/http-swagger/project.clj +++ b/examples/http-swagger/project.clj @@ -3,6 +3,6 @@ :dependencies [[org.clojure/clojure "1.10.0"] [ring/ring-jetty-adapter "1.7.1"] [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"]] :repl-options {:init-ns example.server}) diff --git a/examples/http-swagger/src/example/server.clj b/examples/http-swagger/src/example/server.clj index d0a9164e..9d14ce91 100644 --- a/examples/http-swagger/src/example/server.clj +++ b/examples/http-swagger/src/example/server.clj @@ -77,7 +77,7 @@ {:get {:summary "downloads a file" :swagger {:produces ["image/png"]} :responses {200 {:description "an image" - :content {"image/png" any?}}} + :content {"image/png" {:schema any?}}}} :handler (fn [_] {:status 200 :headers {"Content-Type" "image/png"} @@ -112,6 +112,22 @@ {:status 200 :body {:total (+ x y)}})} :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?}} :responses {200 {:body {:total int?}}} :handler (fn [{{{:keys [x y]} :body} :parameters}] diff --git a/examples/http/project.clj b/examples/http/project.clj index 3bb3be89..aa919bf5 100644 --- a/examples/http/project.clj +++ b/examples/http/project.clj @@ -5,5 +5,5 @@ [funcool/promesa "1.9.0"] [manifold "0.1.8"] [ring/ring-jetty-adapter "1.7.1"] - [metosin/reitit "0.6.0"]] + [metosin/reitit "0.7.0-alpha5"]] :repl-options {:init-ns example.server}) diff --git a/examples/just-coercion-with-ring/project.clj b/examples/just-coercion-with-ring/project.clj index ad56a2a4..cf6e0038 100644 --- a/examples/just-coercion-with-ring/project.clj +++ b/examples/just-coercion-with-ring/project.clj @@ -2,4 +2,4 @@ :description "Reitit coercion with vanilla ring" :dependencies [[org.clojure/clojure "1.10.0"] [ring/ring-jetty-adapter "1.7.1"] - [metosin/reitit "0.6.0"]]) + [metosin/reitit "0.7.0-alpha5"]]) diff --git a/examples/pedestal-malli-swagger/project.clj b/examples/pedestal-malli-swagger/project.clj index 5213eb92..fd0547f3 100644 --- a/examples/pedestal-malli-swagger/project.clj +++ b/examples/pedestal-malli-swagger/project.clj @@ -3,7 +3,7 @@ :dependencies [[org.clojure/clojure "1.10.0"] [io.pedestal/pedestal.service "0.5.5"] [io.pedestal/pedestal.jetty "0.5.5"] - [metosin/reitit-malli "0.6.0"] - [metosin/reitit-pedestal "0.6.0"] - [metosin/reitit "0.6.0"]] + [metosin/reitit-malli "0.7.0-alpha5"] + [metosin/reitit-pedestal "0.7.0-alpha5"] + [metosin/reitit "0.7.0-alpha5"]] :repl-options {:init-ns server}) diff --git a/examples/pedestal-swagger/project.clj b/examples/pedestal-swagger/project.clj index 82c87a79..0ca087f6 100644 --- a/examples/pedestal-swagger/project.clj +++ b/examples/pedestal-swagger/project.clj @@ -3,6 +3,6 @@ :dependencies [[org.clojure/clojure "1.10.0"] [io.pedestal/pedestal.service "0.5.5"] [io.pedestal/pedestal.jetty "0.5.5"] - [metosin/reitit-pedestal "0.6.0"] - [metosin/reitit "0.6.0"]] + [metosin/reitit-pedestal "0.7.0-alpha5"] + [metosin/reitit "0.7.0-alpha5"]] :repl-options {:init-ns example.server}) diff --git a/examples/pedestal/project.clj b/examples/pedestal/project.clj index 82c87a79..0ca087f6 100644 --- a/examples/pedestal/project.clj +++ b/examples/pedestal/project.clj @@ -3,6 +3,6 @@ :dependencies [[org.clojure/clojure "1.10.0"] [io.pedestal/pedestal.service "0.5.5"] [io.pedestal/pedestal.jetty "0.5.5"] - [metosin/reitit-pedestal "0.6.0"] - [metosin/reitit "0.6.0"]] + [metosin/reitit-pedestal "0.7.0-alpha5"] + [metosin/reitit "0.7.0-alpha5"]] :repl-options {:init-ns example.server}) diff --git a/examples/ring-example/project.clj b/examples/ring-example/project.clj index 60909de3..e47d5113 100644 --- a/examples/ring-example/project.clj +++ b/examples/ring-example/project.clj @@ -2,5 +2,5 @@ :description "Reitit Ring App" :dependencies [[org.clojure/clojure "1.10.0"] [ring/ring-jetty-adapter "1.7.1"] - [metosin/reitit "0.6.0"]] + [metosin/reitit "0.7.0-alpha5"]] :repl-options {:init-ns example.server}) diff --git a/examples/ring-integrant/project.clj b/examples/ring-integrant/project.clj index 864c540a..3a876f2b 100644 --- a/examples/ring-integrant/project.clj +++ b/examples/ring-integrant/project.clj @@ -2,7 +2,7 @@ :description "Reitit Ring App with Integrant" :dependencies [[org.clojure/clojure "1.10.1"] [ring/ring-jetty-adapter "1.7.1"] - [metosin/reitit "0.6.0"] + [metosin/reitit "0.7.0-alpha5"] [integrant "0.7.0"]] :main example.server :repl-options {:init-ns user} diff --git a/examples/ring-malli-lite-swagger/project.clj b/examples/ring-malli-lite-swagger/project.clj index 406e05c6..9750eb53 100644 --- a/examples/ring-malli-lite-swagger/project.clj +++ b/examples/ring-malli-lite-swagger/project.clj @@ -3,6 +3,6 @@ :dependencies [[org.clojure/clojure "1.10.0"] [metosin/jsonista "0.2.6"] [ring/ring-jetty-adapter "1.7.1"] - [metosin/reitit "0.6.0"]] + [metosin/reitit "0.7.0-alpha5"]] :repl-options {:init-ns example.server} :profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]]}}) diff --git a/examples/ring-malli-swagger/README.md b/examples/ring-malli-swagger/README.md index 6162591b..ee281fb2 100644 --- a/examples/ring-malli-swagger/README.md +++ b/examples/ring-malli-swagger/README.md @@ -1,4 +1,4 @@ -# reitit-ring, malli, swagger +# reitit-ring, malli, swagger, openapi 3 ## Usage diff --git a/examples/ring-malli-swagger/project.clj b/examples/ring-malli-swagger/project.clj index a2991dae..193c7596 100644 --- a/examples/ring-malli-swagger/project.clj +++ b/examples/ring-malli-swagger/project.clj @@ -3,7 +3,7 @@ :dependencies [[org.clojure/clojure "1.10.0"] [metosin/jsonista "0.2.6"] [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} :profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]]}}) diff --git a/examples/ring-malli-swagger/src/example/server.clj b/examples/ring-malli-swagger/src/example/server.clj index b7ab91b4..c2dec67f 100644 --- a/examples/ring-malli-swagger/src/example/server.clj +++ b/examples/ring-malli-swagger/src/example/server.clj @@ -27,6 +27,10 @@ :swagger {:info {:title "my-api" :description "swagger docs with [malli](https://github.com/metosin/malli) and reitit-ring" :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"} {:name "math", :description "math api"}]} :handler (swagger/create-swagger-handler)}}] @@ -34,7 +38,11 @@ {:get {:no-doc true :openapi {:info {:title "my-api" :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)}}] ["/files" @@ -53,7 +61,7 @@ {:get {:summary "downloads a file" :swagger {:produces ["image/png"]} :responses {200 {:description "an image" - :content {"image/png" any?}}} + :content {"image/png" {:schema string?}}}} :handler (fn [_] {:status 200 :headers {"Content-Type" "image/png"} @@ -85,10 +93,42 @@ :json-schema/default 42} 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?]]}} :handler (fn [{{{:keys [x y]} :body} :parameters}] {: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 ;;:validate spec/validate ;; enable spec validation for route data diff --git a/examples/ring-malli-swagger/swagger.png b/examples/ring-malli-swagger/swagger.png index 9d5a55b8..7f0fd089 100644 Binary files a/examples/ring-malli-swagger/swagger.png and b/examples/ring-malli-swagger/swagger.png differ diff --git a/examples/ring-spec-swagger/README.md b/examples/ring-spec-swagger/README.md index 3eb99d0c..00c30dca 100644 --- a/examples/ring-spec-swagger/README.md +++ b/examples/ring-spec-swagger/README.md @@ -1,4 +1,4 @@ -# reitit-ring, clojure.spec, swagger +# reitit-ring, clojure.spec, swagger, openapi 3 ## Usage @@ -7,6 +7,10 @@ (start) ``` +- Swagger spec served at +- Openapi spec served at +- Swagger UI served at + To test the endpoints using [httpie](https://httpie.org/): ```bash diff --git a/examples/ring-spec-swagger/project.clj b/examples/ring-spec-swagger/project.clj index d0566b74..cba17655 100644 --- a/examples/ring-spec-swagger/project.clj +++ b/examples/ring-spec-swagger/project.clj @@ -2,6 +2,7 @@ :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"]] + [metosin/reitit "0.7.0-alpha5"] + [metosin/ring-swagger-ui "5.0.0-alpha.0"]] :repl-options {:init-ns example.server} :profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]]}}) diff --git a/examples/ring-spec-swagger/src/example/server.clj b/examples/ring-spec-swagger/src/example/server.clj index 31ac2bc0..7991aaa2 100644 --- a/examples/ring-spec-swagger/src/example/server.clj +++ b/examples/ring-spec-swagger/src/example/server.clj @@ -1,6 +1,7 @@ (ns example.server (:require [reitit.ring :as ring] [reitit.coercion.spec] + [reitit.openapi :as openapi] [reitit.swagger :as swagger] [reitit.swagger-ui :as swagger-ui] [reitit.ring.coercion :as coercion] @@ -44,9 +45,15 @@ {:get {:no-doc true :swagger {:info {:title "my-api"}} :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" - {:swagger {:tags ["files"]}} + {:tags ["files"]} ["/upload" {:post {:summary "upload a file" @@ -60,6 +67,8 @@ ["/download" {:get {:summary "downloads a file" :swagger {:produces ["image/png"]} + :responses {200 {:description "an image" + :content {"image/png" {:schema string?}}}} :handler (fn [_] {:status 200 :headers {"Content-Type" "image/png"} @@ -67,7 +76,7 @@ (io/resource "reitit.png"))})}}]] ["/math" - {:swagger {:tags ["math"]}} + {:tags ["math"]} ["/plus" {:get {:summary "plus with spec query parameters" @@ -111,6 +120,9 @@ (swagger-ui/create-swagger-ui-handler {:path "/" :config {:validatorUrl nil + :urls [{:name "swagger" :url "swagger.json"} + {:name "openapi" :url "openapi.json"}] + :urls.primaryName "openapi" :operationsSorter "alpha"}}) (ring/create-default-handler)))) diff --git a/examples/ring-spec-swagger/swagger.png b/examples/ring-spec-swagger/swagger.png index 9d5a55b8..e7ba4003 100644 Binary files a/examples/ring-spec-swagger/swagger.png and b/examples/ring-spec-swagger/swagger.png differ diff --git a/examples/ring-swagger/.gitignore b/examples/ring-swagger/.gitignore deleted file mode 100644 index c53038ec..00000000 --- a/examples/ring-swagger/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -/target -/classes -/checkouts -pom.xml -pom.xml.asc -*.jar -*.class -/.lein-* -/.nrepl-port -.hgignore -.hg/ diff --git a/examples/ring-swagger/.lein-failures b/examples/ring-swagger/.lein-failures new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/examples/ring-swagger/.lein-failures @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/examples/ring-swagger/README.md b/examples/ring-swagger/README.md deleted file mode 100644 index 90585d1e..00000000 --- a/examples/ring-swagger/README.md +++ /dev/null @@ -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 -``` - - - -## License - -Copyright © 2017-2018 Metosin Oy diff --git a/examples/ring-swagger/project.clj b/examples/ring-swagger/project.clj deleted file mode 100644 index bdba0419..00000000 --- a/examples/ring-swagger/project.clj +++ /dev/null @@ -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}) diff --git a/examples/ring-swagger/resources/reitit.png b/examples/ring-swagger/resources/reitit.png deleted file mode 100644 index c89c3654..00000000 Binary files a/examples/ring-swagger/resources/reitit.png and /dev/null differ diff --git a/examples/ring-swagger/src/example/server.clj b/examples/ring-swagger/src/example/server.clj deleted file mode 100644 index 58d01810..00000000 --- a/examples/ring-swagger/src/example/server.clj +++ /dev/null @@ -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)) diff --git a/examples/ring-swagger/swagger.png b/examples/ring-swagger/swagger.png deleted file mode 100644 index 9d5a55b8..00000000 Binary files a/examples/ring-swagger/swagger.png and /dev/null differ diff --git a/modules/reitit-core/project.clj b/modules/reitit-core/project.clj index 6c7c6680..2b015eea 100644 --- a/modules/reitit-core/project.clj +++ b/modules/reitit-core/project.clj @@ -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)" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" @@ -10,4 +10,5 @@ :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} :java-source-paths ["java-src"] + :javac-options ["-Xlint:unchecked" "-target" "1.8" "-source" "1.8"] :dependencies [[meta-merge]]) diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc index 2c1af0c7..2bf11482 100644 --- a/modules/reitit-core/src/reitit/coercion.cljc +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -13,6 +13,8 @@ (-get-name [this] "Keyword name for the coercion") (-get-options [this] "Coercion options") (-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") (-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") @@ -37,7 +39,6 @@ (def ^:no-doc default-parameter-coercion {:query (->ParameterCoercion :query-params :string true true) :body (->ParameterCoercion :body-params :body false false) - :request (->ParameterCoercion :body-params :request false false) :form (->ParameterCoercion :form-params :string true true) :header (->ParameterCoercion :headers :string true true) :path (->ParameterCoercion :path-params :string true true) @@ -83,33 +84,53 @@ value) ;; 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 - parameter-coercion default-parameter-coercion}}] + parameter-coercion default-parameter-coercion + skip #{}}}] (if coercion - (if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)] - (let [transform (comp (if keywordize? walk/keywordize-keys identity) in) - ->open (if open? #(-open-model coercion %) identity) - format-schema-pairs (if (= :request style) - (conj (:content model) [:default (:body model)]) - [[: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] - (let [value (transform request) - format (extract-request-format request) - coercer (or (format->coercer format) - (format->coercer :default) - -identity-coercer) - result (coercer value format)] - (if (error? result) - (request-coercion-failed! result coercion value in request serialize-failed-result) - result)))))))) + (when-let [{:keys [keywordize? open? in style]} (parameter-coercion type)] + (when-not (skip style) + (let [transform (comp (if keywordize? walk/keywordize-keys identity) in) + ->open (if open? #(-open-model coercion %) identity) + coercer (-request-coercer coercion style (->open model))] + (when coercer + (fn [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) + coercer (or (format->coercer format) + (format->coercer :default) + -identity-coercer) + result (coercer value format)] + (if (error? result) + (request-coercion-failed! result coercion value in request serialize-failed-result) + result))))))) (defn extract-response-format-default [request _] (-> request :muuntaja/response :format)) @@ -117,17 +138,18 @@ (defn response-coercer [coercion {:keys [content body]} {:keys [extract-response-format serialize-failed-result] :or {extract-response-format extract-response-format-default}}] (if coercion - (let [per-format-coercers (some->> (for [[format schema] content] - [format (-response-coercer coercion schema)]) - (filter second) - (seq) - (into {})) - default (when body (-response-coercer coercion body))] - (when (or per-format-coercers default) + (let [format->coercer (some->> (concat (when body + [[:default (-response-coercer coercion body)]]) + (for [[format {:keys [schema]}] content, :when schema] + [format (-response-coercer coercion schema)])) + (filter second) (seq) (into (array-map)))] + (when format->coercer (fn [request response] (let [format (extract-response-format request 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)] (if (error? result) (response-coercion-failed! result coercion value request response serialize-failed-result) @@ -151,52 +173,23 @@ (impl/fast-assoc response :body (coercer request response)) response))) -(defn request-coercers [coercion parameters opts] - (some->> (for [[k v] parameters - :when v] - [k (request-coercer coercion k v opts)]) - (filter second) - (seq) - (into {}))) +(defn request-coercers + ([coercion parameters opts] + (some->> (for [[k v] parameters, :when v] + [k (request-coercer coercion k v opts)]) + (filter second) (seq) (into {}))) + ([coercion parameters route-request opts] + (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] (some->> (for [[status model] responses] [status (response-coercer coercion model opts)]) - (filter second) - (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)))))) + (filter second) (seq) (into {}))) +(defn -compile-parameters [data coercion] + (impl/path-update data [[[:parameters any?] #(-compile-model coercion % nil)]])) ;; ;; integration @@ -204,17 +197,29 @@ (defn compile-request-coercers "A router :compile implementation which reads the `:parameters` - and `:coercion` data to create compiled coercers into Match under - `:result. A pre-requisite to use [[coerce!]]." - [[_ {:keys [parameters coercion]}] opts] + and `:coercion` data to both compile the schemas and create compiled coercers + into Match under `:result with the following keys: + + | 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) - (request-coercers coercion parameters opts))) + (let [{:keys [parameters] :as data} (-compile-parameters data coercion)] + {:data data + :coerce (request-coercers coercion parameters opts)}))) (defn coerce! - "Returns a map of coerced input parameters using pre-compiled - coercers under `:result` (provided by [[compile-request-coercers]]. - Throws `ex-info` if parameters can't be coerced - If coercion or parameters are not defined, return `nil`" + "Returns a map of coerced input parameters using pre-compiled coercers in `Match` + under path `[:result :coerce]` (provided by [[compile-request-coercers]]. + Throws `ex-info` if parameters can't be coerced. If coercion or parameters + are not defined, returns `nil`" [match] - (if-let [coercers (:result match)] + (if-let [coercers (-> match :result :coerce)] (coerce-request coercers match))) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 0b32fd73..96264581 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -307,6 +307,7 @@ :coerce (fn coerce [route _] route) :compile (fn compile [[_ {:keys [handler]}] _] handler) :exception exception/exception + :update-paths [[[:parameters any?] impl/accumulate]] :conflicts (fn throw! [conflicts] (exception/fail! :path-conflicts conflicts))}) (defn router @@ -314,20 +315,22 @@ Selects implementation based on route details. The following options are available: - | key | description - | -------------|------------- - | `:path` | Base-path for routes - | `:routes` | Initial resolved routes (default `[]`) - | `:data` | Initial route data (default `{}`) - | `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this - | `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon}) - | `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`) - | `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil` - | `:compile` | Function of `route opts => result` to compile a route handler - | `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects - | `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes - | `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`) - | `:router` | Function of `routes opts => router` to override the actual router implementation" + | key | description + | ----------------|------------- + | `:path` | Base-path for routes + | `:routes` | Initial resolved routes (default `[]`) + | `:data` | Initial route data (default `{}`) + | `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this + | `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon}) + | `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`) + | `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil` + | `:compile` | Function of `route opts => result` to compile a route handler + | `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects + | `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes + | `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`) + | `: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" ([raw-routes] (router raw-routes {})) ([raw-routes opts] diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index 8c6d1cbb..9afd570a 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -9,6 +9,49 @@ (:import (java.net URLEncoder URLDecoder) (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] (let [path #?(:clj (.intern ^String (trie/normalize path opts)) :cljs (trie/normalize path opts)) path-parts (trie/split-path path opts) @@ -60,8 +103,10 @@ (defn map-data [f routes] (mapv (fn [[p ds]] [p (f p ds)]) routes)) -(defn meta-merge [left right opts] - ((or (:meta-merge opts) mm/meta-merge) left right)) +(defn meta-merge [left right {:keys [meta-merge update-paths]}] + (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] (reduce diff --git a/modules/reitit-core/src/reitit/spec.cljc b/modules/reitit-core/src/reitit/spec.cljc index d5ddaf84..3da7d9ea 100644 --- a/modules/reitit-core/src/reitit/spec.cljc +++ b/modules/reitit-core/src/reitit/spec.cljc @@ -82,8 +82,11 @@ (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/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/body :reitit.core.coercion/model) diff --git a/modules/reitit-dev/project.clj b/modules/reitit-dev/project.clj index 1add183f..112fc024 100644 --- a/modules/reitit-dev/project.clj +++ b/modules/reitit-dev/project.clj @@ -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)" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" @@ -11,4 +11,6 @@ :dependencies [[metosin/reitit-core] [com.bhauman/spell-spec] [expound] - [fipp]]) + [fipp] + [org.clojure/core.rrb-vector] + [mvxcvi/arrangement]]) diff --git a/modules/reitit-frontend/project.clj b/modules/reitit-frontend/project.clj index 254a2b67..06c4de15 100644 --- a/modules/reitit-frontend/project.clj +++ b/modules/reitit-frontend/project.clj @@ -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" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index 907a07a7..ba122802 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -2,6 +2,7 @@ (:require [clojure.set :as set] [reitit.coercion :as coercion] [reitit.core :as r] + [reitit.impl :as impl] goog.Uri goog.Uri.QueryData)) @@ -36,6 +37,16 @@ (.setQueryData uri (goog.Uri.QueryData/createFromMap (clj->js new-query))) (.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 "Given routing tree and current path, return match with possibly coerced parameters. Return nil if no match found. @@ -56,8 +67,8 @@ fragment (when (.hasFragment uri) (.getFragment uri)) match (assoc match - :query-params q - :fragment fragment) + :query-params q + :fragment fragment) ;; Return uncoerced values if coercion is not enabled - so ;; that tha parameters are always accessible from same property. parameters (or (coerce! match) diff --git a/modules/reitit-frontend/src/reitit/frontend/easy.cljs b/modules/reitit-frontend/src/reitit/frontend/easy.cljs index bcd47f88..b50bbb48 100644 --- a/modules/reitit-frontend/src/reitit/frontend/easy.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/easy.cljs @@ -52,11 +52,13 @@ pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them differently, convert the collections to strings first." ([name] - (rfh/href @history name nil nil)) + (rfh/href @history name nil nil nil)) ([name path-params] - (rfh/href @history name path-params nil)) + (rfh/href @history name path-params nil nil)) ([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 ^{:see-also ["reitit.frontend.history/push-state"]} @@ -74,11 +76,13 @@ See also: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState" ([name] - (rfh/push-state @history name nil nil)) + (rfh/push-state @history name nil nil nil)) ([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] - (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 ^{:see-also ["reitit.frontend.history/replace-state"]} @@ -96,11 +100,13 @@ See also: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" ([name] - (rfh/replace-state @history name nil nil)) + (rfh/replace-state @history name nil nil nil)) ([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] - (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 ;; 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" ([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))) (defn diff --git a/modules/reitit-frontend/src/reitit/frontend/history.cljs b/modules/reitit-frontend/src/reitit/frontend/history.cljs index 5764e10f..0f93a958 100644 --- a/modules/reitit-frontend/src/reitit/frontend/history.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs @@ -10,7 +10,7 @@ (-init [this] "Create event listeners") (-stop [this] "Remove event listeners") (-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")) ;; This version listens for both pop-state and hash-change for @@ -92,6 +92,7 @@ ;; isContentEditable property is inherited from parents, ;; so if the anchor is inside contenteditable div, the property will be true. (not (.-isContentEditable el)) + ;; NOTE: Why doesn't this use frontend variant instead of core? (reitit/match-by-path router (.getPath uri))))) (defrecord Html5History [on-navigate router listen-key click-listen-key] @@ -132,7 +133,8 @@ nil) (-get-path [this] (str (.. js/window -location -pathname) - (.. js/window -location -search))) + (.. js/window -location -search) + (.. js/window -location -hash))) (-href [this path] path)) @@ -193,8 +195,10 @@ ([history name path-params] (href history name path-params nil)) ([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)] - (-href history (reitit/match->path match query-params))))) + (-href history (rf/match->path match query-params fragment))))) (defn ^{:see-also ["reitit.core/match->path"]} @@ -211,12 +215,14 @@ See also: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState" ([history name] - (push-state history name nil nil)) + (push-state history name nil nil nil)) ([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] + (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) - 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 js/window.history nil "" (-href history path)) (-on-navigate history path)))) @@ -237,12 +243,14 @@ See also: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" ([history name] - (replace-state history name nil nil)) + (replace-state history name nil nil nil)) ([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] + (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) - path (reitit/match->path match query-params)] + path (rf/match->path match query-params fragment)] (.replaceState js/window.history nil "" (-href history path)) (-on-navigate history path)))) @@ -265,9 +273,9 @@ https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" ([history name] (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) - path (reitit/match->path match query-params)] + path (rf/match->path match query-params fragment)] (if replace (.replaceState js/window.history nil "" (-href history path)) (.pushState js/window.history nil "" (-href history path))) diff --git a/modules/reitit-http/project.clj b/modules/reitit-http/project.clj index 78ded043..00225a28 100644 --- a/modules/reitit-http/project.clj +++ b/modules/reitit-http/project.clj @@ -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" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-http/src/reitit/http.cljc b/modules/reitit-http/src/reitit/http.cljc index bf0f3ae5..b6038555 100644 --- a/modules/reitit-http/src/reitit/http.cljc +++ b/modules/reitit-http/src/reitit/http.cljc @@ -22,11 +22,12 @@ compile (fn [[path data] opts scope] (interceptor/compile-result [path data] opts scope)) ->endpoint (fn [p d m s] - (let [compiled (compile [p d] opts s)] - (-> compiled - (map->Endpoint) - (assoc :path p) - (assoc :method m)))) + (let [d (ring/-compile-coercion d)] + (let [compiled (compile [p d] opts s)] + (-> compiled + (map->Endpoint) + (assoc :path p) + (assoc :method m))))) ->methods (fn [any? data] (reduce (fn [acc method] @@ -67,6 +68,7 @@ ([data opts] (let [opts (merge {:coerce coerce-handler :compile compile-result + :update-paths (ring/-update-paths impl/accumulate) ::default-options-endpoint ring/default-options-endpoint} opts)] (when (contains? opts ::default-options-handler) (ex/fail! (str "Option :reitit.http/default-options-handler is deprecated." diff --git a/modules/reitit-http/src/reitit/http/coercion.cljc b/modules/reitit-http/src/reitit/http/coercion.cljc index 8e63db28..4807f3a3 100644 --- a/modules/reitit-http/src/reitit/http/coercion.cljc +++ b/modules/reitit-http/src/reitit/http/coercion.cljc @@ -10,15 +10,15 @@ [] {:name ::coerce-request :spec ::rs/parameters - :compile (fn [{:keys [coercion parameters]} opts] + :compile (fn [{:keys [coercion parameters request]} opts] (cond ;; no coercion, skip (not coercion) nil ;; just coercion, don't mount - (not parameters) {} + (not (or parameters request)) {} ;; mount :else - (if-let [coercers (coercion/request-coercers coercion parameters opts)] + (if-let [coercers (coercion/request-coercers coercion parameters request opts)] {:enter (fn [ctx] (let [request (:request ctx) coerced (coercion/coerce-request coercers request) diff --git a/modules/reitit-interceptors/project.clj b/modules/reitit-interceptors/project.clj index cc7224e5..74496368 100644 --- a/modules/reitit-interceptors/project.clj +++ b/modules/reitit-interceptors/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-interceptors "0.6.0" +(defproject metosin/reitit-interceptors "0.7.0-alpha5" :description "Reitit, common interceptors bundled" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" @@ -11,4 +11,5 @@ :inherit [:deploy-repositories :managed-dependencies]} :dependencies [[metosin/reitit-ring] [lambdaisland/deep-diff] - [metosin/muuntaja]]) + [metosin/muuntaja] + [metosin/spec-tools]]) diff --git a/modules/reitit-malli/project.clj b/modules/reitit-malli/project.clj index 044d3bba..a2b24039 100644 --- a/modules/reitit-malli/project.clj +++ b/modules/reitit-malli/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-malli "0.6.0" +(defproject metosin/reitit-malli "0.7.0-alpha5" :description "Reitit: Malli coercion" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-malli/src/reitit/coercion/malli.cljc b/modules/reitit-malli/src/reitit/coercion/malli.cljc index 1e72f7b1..eb97457e 100644 --- a/modules/reitit-malli/src/reitit/coercion/malli.cljc +++ b/modules/reitit-malli/src/reitit/coercion/malli.cljc @@ -106,92 +106,6 @@ ;; malli options :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 ([] (create nil)) @@ -199,12 +113,20 @@ (let [{:keys [transformers lite compile options error-keys encode-error] :as opts} (merge default-options opts) show? (fn [key] (contains? error-keys key)) 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)] ^{:type ::coercion/coercion} (reify coercion/Coercion (-get-name [_] :malli) (-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}] (case specification :swagger (swagger/swagger-spec @@ -225,12 +147,15 @@ (if (:schema $) (update $ :schema #(coercion/-compile-model this % nil)) $))]))}))) - :openapi (-get-apidocs-openapi this data options) + ;; :openapi handled in reitit.openapi/-get-apidocs-openapi (throw (ex-info - (str "Can't produce Schema apidocs for " specification) - {:type specification, :coercion :schema})))) - (-compile-model [_ model _] (compile model options)) + (str "Can't produce Malli apidocs for " specification) + {:type specification, :coercion :malli})))) + (-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) (-encode-error [_ error] (cond-> error @@ -241,8 +166,8 @@ (seq error-keys) (select-keys error-keys) encode-error (encode-error))) (-request-coercer [_ type schema] - (-coercer (compile schema options) type transformers :decode opts)) + (-coercer schema type transformers :decode opts)) (-response-coercer [_ schema] - (-coercer (compile schema options) :response transformers :encode opts)))))) + (-coercer schema :response transformers :encode opts)))))) (def coercion (create default-options)) diff --git a/modules/reitit-middleware/project.clj b/modules/reitit-middleware/project.clj index c81b27f8..3d1d9105 100644 --- a/modules/reitit-middleware/project.clj +++ b/modules/reitit-middleware/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-middleware "0.6.0" +(defproject metosin/reitit-middleware "0.7.0-alpha5" :description "Reitit, common middleware bundled" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-openapi/project.clj b/modules/reitit-openapi/project.clj index 079f8cac..947e2e03 100644 --- a/modules/reitit-openapi/project.clj +++ b/modules/reitit-openapi/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-openapi "0.6.0" +(defproject fi.metosin/reitit-openapi "0.7.0-alpha5" :description "Reitit: OpenAPI-support" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-openapi/src/reitit/openapi.cljc b/modules/reitit-openapi/src/reitit/openapi.cljc index 89bfd3df..68dc2caa 100644 --- a/modules/reitit-openapi/src/reitit/openapi.cljc +++ b/modules/reitit-openapi/src/reitit/openapi.cljc @@ -73,6 +73,96 @@ (defn- openapi-path [path opts] (-> 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 "Stability: alpha @@ -99,7 +189,7 @@ (apply meta-merge (keep (comp :openapi :data) middleware)) (apply meta-merge (keep (comp :openapi :data) interceptors)) (if coercion - (coercion/get-apidocs coercion :openapi data)) + (-get-apidocs-openapi coercion data)) (select-keys data [:tags :summary :description]) (strip-top-level-keys openapi))])) transform-path (fn [[p _ c]] diff --git a/modules/reitit-pedestal/project.clj b/modules/reitit-pedestal/project.clj index 5e7ed9a3..7f3108f7 100644 --- a/modules/reitit-pedestal/project.clj +++ b/modules/reitit-pedestal/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-pedestal "0.6.0" +(defproject metosin/reitit-pedestal "0.7.0-alpha5" :description "Reitit + Pedestal Integration" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-ring/project.clj b/modules/reitit-ring/project.clj index 2b6b8053..2c7fe9aa 100644 --- a/modules/reitit-ring/project.clj +++ b/modules/reitit-ring/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-ring "0.6.0" +(defproject metosin/reitit-ring "0.7.0-alpha5" :description "Reitit: Ring routing" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc index 5c292734..07332ec1 100644 --- a/modules/reitit-ring/src/reitit/ring.cljc +++ b/modules/reitit-ring/src/reitit/ring.cljc @@ -3,6 +3,7 @@ #?@(:clj [[ring.util.mime-type :as mime-type] [ring.util.response :as response]]) [reitit.core :as r] + [reitit.coercion :as coercion] [reitit.exception :as ex] [reitit.impl :as impl] [reitit.middleware :as middleware])) @@ -28,16 +29,43 @@ (update acc method expand opts) 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}] (let [[top childs] (group-keys data) childs (cond-> childs (and (not (:options childs)) (not (:handler top)) default-options-endpoint) (assoc :options (expand default-options-endpoint opts))) ->endpoint (fn [p d m s] - (-> (middleware/compile-result [p d] opts s) - (map->Endpoint) - (assoc :path p) - (assoc :method m))) + (let [d (-compile-coercion d)] + (-> (middleware/compile-result [p d] opts s) + (map->Endpoint) + (assoc :path p) + (assoc :method m)))) ->methods (fn [any? data] (reduce (fn [acc method] @@ -97,6 +125,7 @@ ([data opts] (let [opts (merge {:coerce coerce-handler :compile compile-result + :update-paths (-update-paths impl/accumulate) ::default-options-endpoint default-options-endpoint} opts)] (when (contains? opts ::default-options-handler) diff --git a/modules/reitit-ring/src/reitit/ring/coercion.cljc b/modules/reitit-ring/src/reitit/ring/coercion.cljc index efbe83f7..8d7cbe0f 100644 --- a/modules/reitit-ring/src/reitit/ring/coercion.cljc +++ b/modules/reitit-ring/src/reitit/ring/coercion.cljc @@ -24,15 +24,15 @@ and :parameters from route data, otherwise does not mount." {:name ::coerce-request :spec ::rs/parameters - :compile (fn [{:keys [coercion parameters]} opts] + :compile (fn [{:keys [coercion parameters request]} opts] (cond ;; no coercion, skip (not coercion) nil ;; just coercion, don't mount - (not parameters) {} + (not (or parameters request)) {} ;; mount :else - (if-let [coercers (coercion/request-coercers coercion parameters opts)] + (if-let [coercers (coercion/request-coercers coercion parameters request opts)] (fn [handler] (fn ([request] diff --git a/modules/reitit-schema/project.clj b/modules/reitit-schema/project.clj index e5e44be5..7034f962 100644 --- a/modules/reitit-schema/project.clj +++ b/modules/reitit-schema/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-schema "0.6.0" +(defproject metosin/reitit-schema "0.7.0-alpha5" :description "Reitit: Plumatic Schema coercion" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-schema/src/reitit/coercion/schema.cljc b/modules/reitit-schema/src/reitit/coercion/schema.cljc index ce66e153..b2fd14d1 100644 --- a/modules/reitit-schema/src/reitit/coercion/schema.cljc +++ b/modules/reitit-schema/src/reitit/coercion/schema.cljc @@ -47,68 +47,38 @@ (reify coercion/Coercion (-get-name [_] :schema) (-get-options [_] opts) - (-get-apidocs [this specification {:keys [parameters responses content-types] - :or {content-types ["application/json"]}}] - ;; TODO: this looks identical to spec, refactor when schema is done. + (-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"]}}] + ;; TODO: this looks identical to spec, refactor when schema is done. (case specification :swagger (swagger/swagger-spec (merge (if parameters - {::swagger/parameters - (into - (empty parameters) - (for [[k v] parameters] - [k (coercion/-compile-model this v nil)]))}) + {::swagger/parameters parameters}) (if responses {::swagger/responses (into (empty responses) (for [[k response] responses] - [k (as-> response $ - (set/rename-keys $ {:body :schema}) - (if (:schema $) - (update $ :schema #(coercion/-compile-model this % nil)) - $))]))}))) - :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)))})))]))})) - + [k (-> response + (dissoc :content) + (set/rename-keys {:body :schema}))]))}))) + ;; :openapi handled in reitit.openapi/-get-apidocs-openapi (throw (ex-info (str "Can't produce Schema apidocs for " specification) {: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)) (-encode-error [_ error] (-> error diff --git a/modules/reitit-sieppari/project.clj b/modules/reitit-sieppari/project.clj index ba69d102..2909224c 100644 --- a/modules/reitit-sieppari/project.clj +++ b/modules/reitit-sieppari/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-sieppari "0.6.0" +(defproject metosin/reitit-sieppari "0.7.0-alpha5" :description "Reitit: Sieppari Interceptors" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-spec/project.clj b/modules/reitit-spec/project.clj index b9cf5f84..c6362647 100644 --- a/modules/reitit-spec/project.clj +++ b/modules/reitit-spec/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-spec "0.6.0" +(defproject metosin/reitit-spec "0.7.0-alpha5" :description "Reitit: clojure.spec coercion" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-spec/src/reitit/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc index d5edd78b..bf4bed82 100644 --- a/modules/reitit-spec/src/reitit/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc @@ -1,7 +1,9 @@ (ns reitit.coercion.spec (:require [clojure.set :as set] [clojure.spec.alpha :as s] + [meta-merge.core :as mm] [reitit.coercion :as coercion] + [reitit.exception :as ex] [spec-tools.core :as st #?@(:cljs [:refer [Spec]])] [spec-tools.data-spec :as ds #?@(:cljs [:refer [Maybe]])] [spec-tools.openapi.core :as openapi] @@ -66,7 +68,7 @@ (st/create-spec {:spec this})) nil - (into-spec [this _])) + (into-spec [_ _])) (defn stringify-pred [pred] (str (if (seq? pred) (seq pred) pred))) @@ -86,81 +88,51 @@ (reify coercion/Coercion (-get-name [_] :spec) (-get-options [_] opts) - (-get-apidocs [this specification {:keys [parameters responses content-types] - :or {content-types ["application/json"]}}] + (-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"]}}] (case specification :swagger (swagger/swagger-spec (merge (if parameters - {::swagger/parameters - (into - (empty parameters) - (for [[k v] parameters] - [k (coercion/-compile-model this v nil)]))}) + {::swagger/parameters parameters}) (if responses {::swagger/responses (into (empty responses) (for [[k response] responses] [k (as-> response $ - (set/rename-keys $ {:body :schema}) - (if (:schema $) - (update $ :schema #(coercion/-compile-model this % nil)) - $))]))}))) - :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)]))))})))]))})) + (dissoc $ :content) + (set/rename-keys $ {:body :schema}))]))}))) + ;; :openapi handled in reitit.openapi/-get-apidocs-openapi (throw (ex-info (str "Can't produce Spec apidocs for " specification) {:specification specification, :coercion :spec})))) (-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) (-encode-error [_ error] (let [problems (-> error :problems ::s/problems)] (-> error (update :spec (comp str s/form)) (assoc :problems (mapv #(update % :pred stringify-pred) problems))))) - (-request-coercer [this type spec] - (let [spec (coercion/-compile-model this spec nil) - {:keys [formats default]} (transformers type)] + (-request-coercer [_ type spec] + (let [{:keys [formats default]} (transformers type)] (fn [value format] (if-let [transformer (or (get formats format) default)] (let [coerced (st/coerce spec value transformer)] diff --git a/modules/reitit-swagger-ui/project.clj b/modules/reitit-swagger-ui/project.clj index 318076a3..e15a7641 100644 --- a/modules/reitit-swagger-ui/project.clj +++ b/modules/reitit-swagger-ui/project.clj @@ -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" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-swagger/project.clj b/modules/reitit-swagger/project.clj index a52ca20d..86366ef3 100644 --- a/modules/reitit-swagger/project.clj +++ b/modules/reitit-swagger/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-swagger "0.6.0" +(defproject metosin/reitit-swagger "0.7.0-alpha5" :description "Reitit: Swagger-support" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-swagger/src/reitit/swagger.cljc b/modules/reitit-swagger/src/reitit/swagger.cljc index 6c18d5bb..496c0ada 100644 --- a/modules/reitit-swagger/src/reitit/swagger.cljc +++ b/modules/reitit-swagger/src/reitit/swagger.cljc @@ -69,6 +69,30 @@ (defn- swagger-path [path opts] (-> 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 "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." @@ -95,7 +119,7 @@ (apply meta-merge (keep (comp :swagger :data) middleware)) (apply meta-merge (keep (comp :swagger :data) interceptors)) (if coercion - (coercion/get-apidocs coercion :swagger data)) + (-get-swagger-apidocs coercion data)) (select-keys data [:tags :summary :description :operationId]) (strip-top-level-keys swagger))])) transform-path (fn [[p _ c]] diff --git a/modules/reitit/project.clj b/modules/reitit/project.clj index 14be2f42..bba1362f 100644 --- a/modules/reitit/project.clj +++ b/modules/reitit/project.clj @@ -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)" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" @@ -19,7 +19,7 @@ [metosin/reitit-http] [metosin/reitit-interceptors] [metosin/reitit-swagger] - [metosin/reitit-openapi] + [fi.metosin/reitit-openapi] [metosin/reitit-swagger-ui] [metosin/reitit-frontend] [metosin/reitit-sieppari] diff --git a/project.clj b/project.clj index 801a7706..4387218e 100644 --- a/project.clj +++ b/project.clj @@ -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)" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" @@ -15,44 +15,47 @@ :url "https://github.com/metosin/reitit"} ;; TODO: need to verify that the code actually worked with Java1.8, see #242 :javac-options ["-Xlint:unchecked" "-target" "1.8" "-source" "1.8"] - :managed-dependencies [[metosin/reitit "0.6.0"] - [metosin/reitit-core "0.6.0"] - [metosin/reitit-dev "0.6.0"] - [metosin/reitit-spec "0.6.0"] - [metosin/reitit-malli "0.6.0"] - [metosin/reitit-schema "0.6.0"] - [metosin/reitit-ring "0.6.0"] - [metosin/reitit-middleware "0.6.0"] - [metosin/reitit-http "0.6.0"] - [metosin/reitit-interceptors "0.6.0"] - [metosin/reitit-swagger "0.6.0"] - [metosin/reitit-openapi "0.6.0"] - [metosin/reitit-swagger-ui "0.6.0"] - [metosin/reitit-frontend "0.6.0"] - [metosin/reitit-sieppari "0.6.0"] - [metosin/reitit-pedestal "0.6.0"] + :managed-dependencies [[metosin/reitit "0.7.0-alpha5"] + [metosin/reitit-core "0.7.0-alpha5"] + [metosin/reitit-dev "0.7.0-alpha5"] + [metosin/reitit-spec "0.7.0-alpha5"] + [metosin/reitit-malli "0.7.0-alpha5"] + [metosin/reitit-schema "0.7.0-alpha5"] + [metosin/reitit-ring "0.7.0-alpha5"] + [metosin/reitit-middleware "0.7.0-alpha5"] + [metosin/reitit-http "0.7.0-alpha5"] + [metosin/reitit-interceptors "0.7.0-alpha5"] + [metosin/reitit-swagger "0.7.0-alpha5"] + [fi.metosin/reitit-openapi "0.7.0-alpha5"] + [metosin/reitit-swagger-ui "0.7.0-alpha5"] + [metosin/reitit-frontend "0.7.0-alpha5"] + [metosin/reitit-sieppari "0.7.0-alpha5"] + [metosin/reitit-pedestal "0.7.0-alpha5"] [metosin/ring-swagger-ui "4.18.1"] [metosin/spec-tools "0.10.5"] - [metosin/schema-tools "0.13.0"] + [metosin/schema-tools "0.13.1"] [metosin/muuntaja "0.6.8"] [metosin/jsonista "0.3.7"] [metosin/sieppari "0.0.0-alpha13"] [metosin/malli "0.11.0"] ;; 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-databind "2.14.2"] + [com.fasterxml.jackson.core/jackson-core "2.15.1"] + [com.fasterxml.jackson.core/jackson-databind "2.15.1"] [meta-merge "1.0.0"] [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"] [lambdaisland/deep-diff "0.0-47"] [com.bhauman/spell-spec "0.1.2"] + [mvxcvi/arrangement "2.1.0"] [ring/ring-core "1.10.0"] [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-ancient "1.0.0-RC3"] [lein-doo "0.1.11"] @@ -87,7 +90,7 @@ [org.clojure/clojurescript "1.10.773"] ;; modules dependencies - [metosin/schema-tools "0.13.0"] + [metosin/schema-tools "0.13.1"] [metosin/spec-tools "0.10.5"] [metosin/muuntaja "0.6.8"] [metosin/sieppari "0.0.0-alpha13"] @@ -105,6 +108,7 @@ [ikitommi/immutant-web "3.0.0-alpha1"] [metosin/ring-http-response "0.9.3"] [metosin/ring-swagger-ui "4.18.1"] + [org.clojure/tools.analyzer "1.1.1"] [criterium "0.4.6"] [org.clojure/test.check "1.1.1"] @@ -115,8 +119,8 @@ [io.pedestal/pedestal.service "0.5.10"] [org.clojure/core.async "1.6.673"] - [manifold "0.4.0"] - [funcool/promesa "10.0.594"] + [manifold "0.4.1"] + [funcool/promesa "11.0.664"] [com.clojure-goes-fast/clj-async-profiler "1.0.3"] [ring-cors "0.1.13"] @@ -134,8 +138,8 @@ [io.pedestal/pedestal.jetty "0.5.10"] [calfpath "0.8.1"] [org.clojure/core.async "1.6.673"] - [manifold "0.4.0"] - [funcool/promesa "10.0.594"] + [manifold "0.4.1"] + [funcool/promesa "11.0.664"] [metosin/sieppari] [yada "1.2.16"] [aleph "0.6.1"] diff --git a/scripts/cljdoc-check.sh b/scripts/cljdoc-check.sh new file mode 100755 index 00000000..a8b4e005 --- /dev/null +++ b/scripts/cljdoc-check.sh @@ -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 diff --git a/scripts/set-version b/scripts/set-version index d85ca323..0da13efe 100755 --- a/scripts/set-version +++ b/scripts/set-version @@ -2,7 +2,11 @@ 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/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 find . -name "*.$ext" -exec rm '{}' \; diff --git a/test/cljc/reitit/coercion_test.cljc b/test/cljc/reitit/coercion_test.cljc index 97ab0aae..ee18c59c 100644 --- a/test/cljc/reitit/coercion_test.cljc +++ b/test/cljc/reitit/coercion_test.cljc @@ -7,34 +7,55 @@ [reitit.coercion.spec] [reitit.core :as r] [schema.core :as s] + [clojure.spec.alpha :as cs] [spec-tools.data-spec :as ds]) #?(:clj (: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 (let [r (r/router [["/schema" {:coercion reitit.coercion.schema/coercion} - ["/:number/:keyword" {:parameters {:path {:number s/Int - :keyword s/Keyword} - :query (s/maybe {:int s/Int, :ints [s/Int], :map {s/Int s/Int}})}}]] + ["/:number" {:parameters {:path {:number s/Int}}} + ["/:keyword" {:parameters {:path {:keyword s/Keyword} + :query (s/maybe {:int s/Int, :ints [s/Int], :map {s/Int s/Int}})}}]]] + ["/malli" {:coercion reitit.coercion.malli/coercion} - ["/:number/:keyword" {:parameters {:path [:map [:number int?] [:keyword keyword?]] - :query [:maybe [:map [:int int?] - [:ints [:vector int?]] - [:map [:map-of int? int?]]]]}}]] + ["/:number" {:parameters {:path [:map [:number int?]]}} + ["/:keyword" {:parameters {:path [:map [:keyword keyword?]] + :query [:maybe [:map [:int int?] + [:ints [:vector int?]] + [:map [:map-of int? int?]]]]}}]]] + ["/malli-lite" {:coercion reitit.coercion.malli/coercion} - ["/:number/:keyword" {:parameters {:path {:number int? - :keyword keyword?} - :query (l/maybe {:int int? - :ints (l/vector int?) - :map (l/map-of int? int?)})}}]] - ["/spec" {:coercion reitit.coercion.spec/coercion} - ["/:number/:keyword" {:parameters {:path {:number int? - :keyword keyword?} - :query (ds/maybe {:int int?, :ints [int?], :map {int? int?}})}}]] + ["/:number" {:parameters {:path {:number int?}}} + ["/:keyword" {:parameters {:path {:keyword keyword?} + :query (l/maybe {:int int? + :ints (l/vector int?) + :map (l/map-of int? int?)})}}]]] + + #_["/spec" {:coercion reitit.coercion.spec/coercion} + ["/:number" {:parameters {:path (cs/keys :req-un [::number])}} + ["/: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" - ["/:number/:keyword" {:parameters {:path {:number int? - :keyword keyword?}}}]]] + ["/:number" {:parameters {:path {:number int?}}} + ["/:keyword" {:parameters {:path {:keyword keyword?}}}]]]] {:compile coercion/compile-request-coercers})] (testing "schema-coercion" @@ -73,19 +94,42 @@ (let [m (r/match-by-path r "/malli-lite/kikka/abba")] (is (thrown? ExceptionInfo (coercion/coerce! m)))))) - ;; TODO: :map-of fails with string-keys - (testing "spec-coercion" - (testing "succeeds" + #_(testing "spec-coercion" + (testing "fails" (let [m (r/match-by-path r "/spec/1/abba")] - (is (= {:path {:keyword :abba, :number 1}, :query nil} - (coercion/coerce! m)))) - (let [m (r/match-by-path r "/schema/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"})))))) + (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} + (coercion/coerce! m)))) + (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}}} + (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 "/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)))))) + (testing "no coercion defined" (testing "doesn't coerce" (let [m (r/match-by-path r "/none/1/abba")] diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index fc112239..ab93cce6 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -267,14 +267,14 @@ (let [pong (constantly "ok") routes ["/api" {:mw [:api]} ["/ping" :kikka] - ["/user/:id" {:parameters {:id "String"}} - ["/:sub-id" {:parameters {:sub-id "String"}}]] + ["/user/:id" {:parameters {:path {:id :string}}} + ["/:sub-id" {:parameters {:path {:sub-id :string}}}]] ["/pong" pong] ["/admin" {:mw [:admin] :roles #{:admin}} ["/user" {:roles ^:replace #{:user}}] ["/db" {:mw [:db]}]]] 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/admin/user" {:mw [:api :admin], :roles #{:user}}] ["/api/admin/db" {:mw [:api :admin :db], :roles #{:admin}}]] @@ -282,7 +282,7 @@ (is (= expected (impl/resolve-routes routes (r/default-router-options)))) (is (= (r/map->Match {: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-params {:id "1", :sub-id "2"}}) (r/match-by-path router "/api/user/1/2")))))) diff --git a/test/cljc/reitit/impl_test.cljc b/test/cljc/reitit/impl_test.cljc index 0059723b..c4ae1a39 100644 --- a/test/cljc/reitit/impl_test.cljc +++ b/test/cljc/reitit/impl_test.cljc @@ -171,3 +171,39 @@ :path-parts ["https://google.com"] :path-params #{}} (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]]})))) diff --git a/test/cljc/reitit/openapi_test.clj b/test/cljc/reitit/openapi_test.clj index 0788432e..3724b7ac 100644 --- a/test/cljc/reitit/openapi_test.clj +++ b/test/cljc/reitit/openapi_test.clj @@ -17,6 +17,7 @@ [reitit.swagger-ui :as swagger-ui] [schema.core :as s] [schema-tools.core] + [spec-tools.core :as st] [spec-tools.data-spec :as ds])) (defn validate @@ -31,101 +32,110 @@ (def app (ring/ring-handler - (ring/router - ["/api" - {:openapi {:id ::math}} + (ring/router + ["/api" + {:openapi {:id ::math}} - ["/openapi.json" - {:get {:no-doc true - :openapi {:info {:title "my-api" - :version "0.0.1"}} - :handler (openapi/create-openapi-handler)}}] + ["/openapi.json" + {:get {:no-doc true + :openapi {:info {:title "my-api" + :version "0.0.1"}} + :handler (openapi/create-openapi-handler)}}] - ["/spec" {:coercion spec/coercion} - ["/plus/:z" - {:get {:summary "plus" - :tags [:plus :spec] - :parameters {:query {:x int?, :y int?} - :path {:z int?}} - :openapi {:operationId "spec-plus" - :deprecated true - :responses {400 {:description "kosh" - :content {"application/json" {:schema {:type "string"}}}}}} - :responses {200 {:description "success" - :body {:total int?}} - 500 {:description "fail"}} - :handler (fn [{{{:keys [x y]} :query - {:keys [z]} :path} :parameters}] - {:status 200, :body {:total (+ x y z)}})} - :post {:summary "plus with body" - :parameters {:body (ds/maybe [int?]) - :path {:z int?}} - :openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}} - :description "kosh"}}} - :responses {200 {:description "success" - :body {:total int?}} - 500 {:description "fail"}} - :handler (fn [{{{:keys [z]} :path - xs :body} :parameters}] - {:status 200, :body {:total (+ (reduce + xs) z)}})}}]] + ["/spec" {:coercion spec/coercion} + ["/plus/:z" + {:get {:summary "plus" + :tags [:plus :spec] + :parameters {:query {:x int?, :y int?} + :path {:z int?}} + :openapi {:operationId "spec-plus" + :deprecated true + :responses {400 {:description "kosh" + :content {"application/json" {:schema {:type "string"}}}}}} + :responses {200 {:description "success" + :body {:total int?}} + 500 {:description "fail"}} + :handler (fn [{{{:keys [x y]} :query + {:keys [z]} :path} :parameters}] + {:status 200, :body {:total (+ x y z)}})} + :post {:summary "plus with body" + :parameters {:body (ds/maybe [int?]) + :path {:z int?}} + :openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}} + :description "kosh"}}} + :responses {200 {:description "success" + :body {:total int?}} + 500 {:description "fail"} + 504 {:description "default" + :content {:default {:schema {:error string?}}} + :body {:masked string?}}} + :handler (fn [{{{:keys [z]} :path + xs :body} :parameters}] + {:status 200, :body {:total (+ (reduce + xs) z)}})}}]] - ["/malli" {:coercion malli/coercion} - ["/plus/*z" - {:get {:summary "plus" - :tags [:plus :malli] - :parameters {:query [:map [:x int?] [:y int?]] - :path [:map [:z int?]]} - :openapi {:responses {400 {:description "kosh" - :content {"application/json" {:schema {:type "string"}}}}}} - :responses {200 {:description "success" - :body [:map [:total int?]]} - 500 {:description "fail"}} - :handler (fn [{{{:keys [x y]} :query - {:keys [z]} :path} :parameters}] - {:status 200, :body {:total (+ x y z)}})} - :post {:summary "plus with body" - :parameters {:body [:maybe [:vector int?]] - :path [:map [:z int?]]} - :openapi {:responses {400 {:description "kosh" - :content {"application/json" {:schema {:type "string"}}}}}} - :responses {200 {:description "success" - :body [:map [:total int?]]} - 500 {:description "fail"}} - :handler (fn [{{{:keys [z]} :path - xs :body} :parameters}] - {:status 200, :body {:total (+ (reduce + xs) z)}})}}]] + ["/malli" {:coercion malli/coercion} + ["/plus/*z" + {:get {:summary "plus" + :tags [:plus :malli] + :parameters {:query [:map [:x int?] [:y int?]] + :path [:map [:z int?]]} + :openapi {:responses {400 {:description "kosh" + :content {"application/json" {:schema {:type "string"}}}}}} + :responses {200 {:description "success" + :body [:map [:total int?]]} + 500 {:description "fail"}} + :handler (fn [{{{:keys [x y]} :query + {:keys [z]} :path} :parameters}] + {:status 200, :body {:total (+ x y z)}})} + :post {:summary "plus with body" + :parameters {:body [:maybe [:vector int?]] + :path [:map [:z int?]]} + :openapi {:responses {400 {:description "kosh" + :content {"application/json" {:schema {:type "string"}}}}}} + :responses {200 {:description "success" + :body [:map [:total int?]]} + 500 {:description "fail"} + 504 {:description "default" + :content {:default {:schema {:error string?}}} + :body {:masked string?}}} + :handler (fn [{{{:keys [z]} :path + xs :body} :parameters}] + {:status 200, :body {:total (+ (reduce + xs) z)}})}}]] - ["/schema" {:coercion schema/coercion} - ["/plus/*z" - {:get {:summary "plus" - :tags [:plus :schema] - :parameters {:query {:x s/Int, :y s/Int} - :path {:z s/Int}} - :openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}} - :description "kosh"}}} - :responses {200 {:description "success" - :body {:total s/Int}} - 500 {:description "fail"}} - :handler (fn [{{{:keys [x y]} :query - {:keys [z]} :path} :parameters}] - {:status 200, :body {:total (+ x y z)}})} - :post {:summary "plus with body" - :parameters {:body (s/maybe [s/Int]) - :path {:z s/Int}} - :openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}} - :description "kosh"}}} - :responses {200 {:description "success" - :body {:total s/Int}} - 500 {:description "fail"}} - :handler (fn [{{{:keys [z]} :path - xs :body} :parameters}] - {:status 200, :body {:total (+ (reduce + xs) z)}})}}]]] + ["/schema" {:coercion schema/coercion} + ["/plus/*z" + {:get {:summary "plus" + :tags [:plus :schema] + :parameters {:query {:x s/Int, :y s/Int} + :path {:z s/Int}} + :openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}} + :description "kosh"}}} + :responses {200 {:description "success" + :body {:total s/Int}} + 500 {:description "fail"}} + :handler (fn [{{{:keys [x y]} :query + {:keys [z]} :path} :parameters}] + {:status 200, :body {:total (+ x y z)}})} + :post {:summary "plus with body" + :parameters {:body (s/maybe [s/Int]) + :path {:z s/Int}} + :openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}} + :description "kosh"}}} + :responses {200 {:description "success" + :body {:total s/Int}} + 500 {:description "fail"} + 504 {:description "default" + :content {:default {:schema {:error s/Str}}} + :body {:masked s/Str}}} + :handler (fn [{{{:keys [z]} :path + xs :body} :parameters}] + {:status 200, :body {:total (+ (reduce + xs) z)}})}}]]] - {:validate reitit.ring.spec/validate - :data {:middleware [openapi/openapi-feature - rrc/coerce-exceptions-middleware - rrc/coerce-request-middleware - rrc/coerce-response-middleware]}}))) + {:validate reitit.ring.spec/validate + :data {:middleware [openapi/openapi-feature + rrc/coerce-exceptions-middleware + rrc/coerce-request-middleware + rrc/coerce-response-middleware]}}))) (deftest openapi-test (testing "endpoints work" @@ -147,19 +157,16 @@ :version "0.0.1"} :paths {"/api/spec/plus/{z}" {:get {:parameters [{:in "query" :name "x" - :description "" :required true :schema {:type "integer" :format "int64"}} {:in "query" :name "y" - :description "" :required true :schema {:type "integer" :format "int64"}} {:in "path" :name "z" - :description "" :required true :schema {:type "integer" :format "int64"}}] @@ -178,7 +185,6 @@ :post {:parameters [{:in "path" :name "z" :required true - :description "" :schema {:type "integer" :format "int64"}}] :requestBody {:content {"application/json" {:schema {:oneOf [{:items {:type "integer" @@ -192,7 +198,11 @@ :type "object"}}}} 400 {:content {"application/json" {:schema {:type "string"}}} :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"}} "/api/malli/plus/{z}" {:get {:parameters [{:in "query" :name :x @@ -230,23 +240,25 @@ :type "object"}}}} 400 {:description "kosh" :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"}} - "/api/schema/plus/{z}" {:get {:parameters [{:description "" - :in "query" + "/api/schema/plus/{z}" {:get {:parameters [{:in "query" :name "x" :required true :schema {:format "int32" :type "integer"}} - {:description "" - :in "query" + {:in "query" :name "y" :required true :schema {:type "integer" :format "int32"}} {:in "path" :name "z" - :description "" :required true :schema {:type "integer" :format "int32"}}] @@ -263,7 +275,6 @@ :summary "plus"} :post {:parameters [{:in "path" :name "z" - :description "" :required true :schema {:type "integer" :format "int32"}}] @@ -279,10 +290,15 @@ :type "object"}}}} 400 {:description "kosh" :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"}}}}] (is (= expected spec)) - (is (nil? (validate spec)))))) + (is (= nil (validate spec)))))) (defn spec-paths [app uri] (-> {:request-method :get, :uri uri} app :body :paths keys)) @@ -293,21 +309,21 @@ {:get {:no-doc true :handler (openapi/create-openapi-handler)}}] app (ring/ring-handler - (ring/router - [["/common" {:openapi {:id #{::one ::two}}} - ping-route] + (ring/router + [["/common" {:openapi {:id #{::one ::two}}} + ping-route] - ["/one" {:openapi {:id ::one}} - ping-route - spec-route] + ["/one" {:openapi {:id ::one}} + ping-route + spec-route] - ["/two" {:openapi {:id ::two}} - ping-route - spec-route - ["/deep" {:openapi {:id ::one}} - ping-route]] - ["/one-two" {:openapi {:id #{::one ::two}}} - spec-route]]))] + ["/two" {:openapi {:id ::two}} + ping-route + spec-route + ["/deep" {:openapi {:id ::one}} + ping-route]] + ["/one-two" {:openapi {:id #{::one ::two}}} + spec-route]]))] (is (= ["/common/ping" "/one/ping" "/two/deep/ping"] (spec-paths app "/one/openapi.json"))) (is (= ["/common/ping" "/two/ping"] @@ -317,9 +333,9 @@ (deftest openapi-ui-config-test (let [app (swagger-ui/create-swagger-ui-handler - {:path "/" - :url "/openapi.json" - :config {:jsonEditor true}})] + {:path "/" + :url "/openapi.json" + :config {:jsonEditor true}})] (is (= 302 (:status (app {:request-method :get, :uri "/"})))) (is (= 200 (:status (app {:request-method :get, :uri "/index.html"})))) (is (= {:jsonEditor true, :url "/openapi.json"} @@ -328,12 +344,12 @@ (deftest without-openapi-id-test (let [app (ring/ring-handler - (ring/router - [["/ping" - {:get (constantly "ping")}] - ["/openapi.json" - {:get {:no-doc true - :handler (openapi/create-openapi-handler)}}]]))] + (ring/router + [["/ping" + {:get (constantly "ping")}] + ["/openapi.json" + {:get {:no-doc true + :handler (openapi/create-openapi-handler)}}]]))] (is (= ["/ping"] (spec-paths app "/openapi.json"))) (is (= #{::openapi/default} (-> {:request-method :get :uri "/openapi.json"} @@ -341,14 +357,14 @@ (deftest with-options-endpoint-test (let [app (ring/ring-handler - (ring/router - [["/ping" - {:options (constantly "options")}] - ["/pong" - (constantly "options")] - ["/openapi.json" - {:get {:no-doc true - :handler (openapi/create-openapi-handler)}}]]))] + (ring/router + [["/ping" + {:options (constantly "options")}] + ["/pong" + (constantly "options")] + ["/openapi.json" + {:get {:no-doc true + :handler (openapi/create-openapi-handler)}}]]))] (is (= ["/ping" "/pong"] (spec-paths app "/openapi.json"))) (is (= #{::openapi/default} (-> {:request-method :get :uri "/openapi.json"} @@ -364,10 +380,12 @@ (deftest all-parameter-types-test (doseq [[coercion ->schema] - [[#'malli/coercion (fn [nom] [:map [nom :string]])] - [#'schema/coercion (fn [nom] {nom s/Str})] - [#'spec/coercion (fn [nom] {nom string?})]]] - (testing coercion + [[#'malli/coercion (fn [nom] [:map [nom [:string {:description (str "description " nom)}]]])] + [#'schema/coercion (fn [nom] {nom (schema-tools.core/schema s/Str + {:description (str "description " nom)})})] + [#'spec/coercion (fn [nom] {nom (st/spec {:spec string? + :description (str "description " nom)})})]]] + (testing (str coercion) (let [app (ring/ring-handler (ring/router [["/parameters" @@ -394,22 +412,26 @@ (is (match? [{:in "query" :name "q" :required true + :description "description :q" :schema {:type "string"}} {:in "header" :name "h" :required true + :description "description :h" :schema {:type "string"}} {:in "cookie" :name "c" :required true + :description "description :c" :schema {:type "string"}} {:in "path" :name "p" :required true + :description "description :p" :schema {:type "string"}}] - (-> spec - (get-in [:paths "/parameters" :post :parameters]) - normalize)))) + (-> spec + (get-in [:paths "/parameters" :post :parameters]) + normalize)))) (testing "body parameter" (is (match? (merge {:type "object" :properties {:b {:type "string"}} @@ -432,22 +454,98 @@ (testing "spec is valid" (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 - (doseq [[coercion file-schema string-schema] - [[#'malli/coercion - reitit.ring.malli/bytes-part - :string] - [#'schema/coercion - (schema-tools.core/schema {:filename s/Str - :content-type s/Str - :bytes s/Num} - {:openapi {:type "string" - :format "binary"}}) - s/Str] - [#'spec/coercion - reitit.http.interceptors.multipart/bytes-part - string?]]] - (testing coercion + (doseq [[coercion file-schema string-schema] [[#'malli/coercion + reitit.ring.malli/bytes-part + :string] + [#'schema/coercion + (schema-tools.core/schema {:filename s/Str + :content-type s/Str + :bytes s/Num} + {:openapi {:type "string" + :format "binary"}}) + s/Str] + [#'spec/coercion + reitit.http.interceptors.multipart/bytes-part + string?]]] + (testing (str coercion) (let [app (ring/ring-handler (ring/router [["/upload" @@ -481,21 +579,20 @@ (is (nil? (validate spec)))))))) (deftest per-content-type-test - (doseq [[coercion ->schema] - [[#'malli/coercion (fn [nom] [:map [nom :string]])] - [#'schema/coercion (fn [nom] {nom s/Str})] - [#'spec/coercion (fn [nom] {nom string?})]]] - (testing coercion + (doseq [[coercion ->schema] [[malli/coercion (fn [nom] [:map [nom :string]])] + [schema/coercion (fn [nom] {nom s/Str})] + [spec/coercion (fn [nom] {nom string?})]]] + (testing (str coercion) (let [app (ring/ring-handler (ring/router [["/parameters" {:post {:description "parameters" - :coercion @coercion - :parameters {:request {:content {"application/json" (->schema :b) - "application/edn" (->schema :c)}}} + :coercion coercion + :request {:content {"application/json" {:schema (->schema :b)} + "application/edn" {:schema (->schema :c)}}} :responses {200 {:description "success" - :content {"application/json" (->schema :ok) - "application/edn" (->schema :edn)}}} + :content {"application/json" {:schema (->schema :ok)} + "application/edn" {:schema (->schema :edn)}}}} :handler (fn [req] {:status 200 :body (-> req :parameters :request)})}}] @@ -510,41 +607,42 @@ spec (-> {:request-method :get :uri "/openapi.json"} app - :body)] + :body) + spec-coercion (= coercion spec/coercion)] (testing "body parameter" - (is (match? (merge {:type "object" - :properties {:b {:type "string"}} - :required ["b"]} - (when-not (#{#'spec/coercion} coercion) - {:additionalProperties false})) - (-> spec - (get-in [:paths "/parameters" :post :requestBody :content "application/json" :schema]) - normalize))) - (is (match? (merge {:type "object" - :properties {:c {:type "string"}} - :required ["c"]} - (when-not (#{#'spec/coercion} coercion) - {:additionalProperties false})) - (-> spec - (get-in [:paths "/parameters" :post :requestBody :content "application/edn" :schema]) - normalize)))) + (is (= (merge {:type "object" + :properties {:b {:type "string"}} + :required ["b"]} + (when-not spec-coercion + {:additionalProperties false})) + (-> spec + (get-in [:paths "/parameters" :post :requestBody :content "application/json" :schema]) + normalize))) + (is (= (merge {:type "object" + :properties {:c {:type "string"}} + :required ["c"]} + (when-not spec-coercion + {:additionalProperties false})) + (-> spec + (get-in [:paths "/parameters" :post :requestBody :content "application/edn" :schema]) + normalize)))) (testing "body response" - (is (match? (merge {:type "object" - :properties {:ok {:type "string"}} - :required ["ok"]} - (when-not (#{#'spec/coercion} coercion) - {:additionalProperties false})) - (-> spec - (get-in [:paths "/parameters" :post :responses 200 :content "application/json" :schema]) - normalize))) - (is (match? (merge {:type "object" - :properties {:edn {:type "string"}} - :required ["edn"]} - (when-not (#{#'spec/coercion} coercion) - {:additionalProperties false})) - (-> spec - (get-in [:paths "/parameters" :post :responses 200 :content "application/edn" :schema]) - normalize)))) + (is (= (merge {:type "object" + :properties {:ok {:type "string"}} + :required ["ok"]} + (when-not spec-coercion + {:additionalProperties false})) + (-> spec + (get-in [:paths "/parameters" :post :responses 200 :content "application/json" :schema]) + normalize))) + (is (= (merge {:type "object" + :properties {:edn {:type "string"}} + :required ["edn"]} + (when-not spec-coercion + {:additionalProperties false})) + (-> spec + (get-in [:paths "/parameters" :post :responses 200 :content "application/edn" :schema]) + normalize)))) (testing "validation" (let [query {:request-method :post :uri "/parameters" @@ -569,23 +667,22 @@ (is (nil? (validate spec)))))))) (deftest default-content-type-test - (doseq [[coercion ->schema] - [[#'malli/coercion (fn [nom] [:map [nom :string]])] - [#'schema/coercion (fn [nom] {nom s/Str})] - [#'spec/coercion (fn [nom] {nom string?})]]] - (testing coercion + (doseq [[coercion ->schema] [[malli/coercion (fn [nom] [:map [nom :string]])] + [schema/coercion (fn [nom] {nom s/Str})] + [spec/coercion (fn [nom] {nom string?})]]] + (testing (str coercion) (doseq [content-type ["application/json" "application/edn"]] (testing (str "default content type " content-type) (let [app (ring/ring-handler (ring/router [["/parameters" {:post {:description "parameters" - :coercion @coercion + :coercion coercion :content-types [content-type] ;; TODO should this be under :openapi ? - :parameters {:request {:content {"application/transit" (->schema :transit)} - :body (->schema :default)}} + :request {:content {"application/transit" {:schema (->schema :transit)}} + :body (->schema :default)} :responses {200 {:description "success" - :content {"application/transit" (->schema :transit)} + :content {"application/transit" {:schema (->schema :transit)}} :body (->schema :default)}} :handler (fn [req] {:status 200 @@ -623,16 +720,15 @@ [["/parameters" {:post {:description "parameters" :coercion malli/coercion - :parameters {:request - {:body - [:schema - {:registry {"friend" [:map - [:age int?] - [:pet [:ref "pet"]]] - "pet" [:map - [:name :string] - [:friends [:vector [:ref "friend"]]]]}} - "friend"]}} + :request {:body + [:schema + {:registry {"friend" [:map + [:age int?] + [:pet [:ref "pet"]]] + "pet" [:map + [:name :string] + [:friends [:vector [:ref "friend"]]]]}} + "friend"]} :handler (fn [req] {:status 200 :body (-> req :parameters :request)})}}] @@ -670,3 +766,53 @@ spec)) (testing "spec is valid" (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)))) diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc index 66d151ed..ec2bd1fe 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -234,14 +234,12 @@ ([] {}) ([left] left) ([left right] - (if (and (map? left) (map? right) - (contains? left :parameters) - (contains? right :parameters)) - (-> (merge-with custom-meta-merge-checking-parameters left right) - (assoc :parameters (merge-with mu/merge - (:parameters left) - (:parameters right)))) - (meta-merge left right))) + (let [pleft (-> left :parameters :path) + pright (-> right :parameters :path)] + (if (and (map? left) (map? right) pleft pright) + (-> (merge-with custom-meta-merge-checking-parameters left right) + (assoc-in [:parameters :path] (reduce mu/merge (concat pleft pright)))) + (meta-merge left right)))) ([left right & more] (reduce custom-meta-merge-checking-parameters left (cons right more)))) @@ -586,75 +584,96 @@ (deftest per-content-type-test (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 :edn]] [:response any?]] [:map [:request [:enum :default]] [:response any?]] [:map [:request any?] [:response [:enum :json]]] [:map [:request any?] [:response [:enum :edn]]] [:map [:request any?] [:response [:enum :default]]]] - [#'schema/coercion + [schema/coercion {:request (s/eq :json) :response s/Any} {:request (s/eq :edn) :response s/Any} {:request (s/eq :default) :response s/Any} {:request s/Any :response (s/eq :json)} {:request s/Any :response (s/eq :edn)} {:request s/Any :response (s/eq :default)}] - [#'spec/coercion + [spec/coercion {:request (clojure.spec.alpha/spec #{:json}) :response any?} {:request (clojure.spec.alpha/spec #{:edn}) :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 #{:end})} {:request any? :response (clojure.spec.alpha/spec #{:default})}]]] - (testing coercion - (let [app (ring/ring-handler - (ring/router - [["/foo" {:post {:parameters {:request {:content {"application/json" json-request - "application/edn" edn-request} - :body default-request}} - :responses {200 {:content {"application/json" json-response - "application/edn" edn-response} - :body default-response}} - :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}})) - call (fn [request] - (try - (app request) - (catch ExceptionInfo e - (select-keys (ex-data e) [:type :in])))) - request (fn [request-format response-format body] - {:request-method :post - :uri "/foo" - :muuntaja/request {:format request-format} - :muuntaja/response {:format response-format} - :body-params body})] - (testing "succesful call" - (is (= {:status 200 :body {:request :json, :response :json}} - (call (request "application/json" "application/json" {:request :json :response :json})))) - (is (= {:status 200 :body {:request :edn, :response :json}} - (call (request "application/edn" "application/json" {:request :edn :response :json})))) - (is (= {:status 200 :body {:request :default, :response :default}} - (call (request "application/transit" "application/transit" {:request :default :response :default}))))) - (testing "request validation fails" - (is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} - (call (request "application/edn" "application/json" {:request :json :response :json})))) - (is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} - (call (request "application/json" "application/json" {:request :edn :response :json})))) - (is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} - (call (request "application/transit" "application/json" {:request :edn :response :json}))))) - (testing "response validation fails" - (is (= {:type :reitit.coercion/response-coercion :in [:response :body]} - (call (request "application/json" "application/json" {:request :json :response :edn})))) - (is (= {:type :reitit.coercion/response-coercion :in [:response :body]} - (call (request "application/json" "application/edn" {:request :json :response :json})))) - (is (= {:type :reitit.coercion/response-coercion :in [:response :body]} - (call (request "application/json" "application/transit" {:request :json :response :json}))))))))) + (testing (str coercion) + (doseq [{:keys [name app]} + [{:name "using top-level :body" + :app (ring/ring-handler + (ring/router + ["/foo" {:post {:request {:content {"application/json" {:schema json-request} + "application/edn" {:schema edn-request}} + :body default-request} + :responses {200 {:content {"application/json" {:schema json-response} + "application/edn" {:schema edn-response}} + :body default-response}} + :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}}))} + {: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 + (app request) + (catch ExceptionInfo e + (select-keys (ex-data e) [:type :in])))) + request (fn [request-format response-format body] + {:request-method :post + :uri "/foo" + :muuntaja/request {:format request-format} + :muuntaja/response {:format response-format} + :body-params body})] + (testing "succesful call" + (is (= {:status 200 :body {:request :json, :response :json}} + (call (request "application/json" "application/json" {:request :json :response :json})))) + (is (= {:status 200 :body {:request :edn, :response :json}} + (call (request "application/edn" "application/json" {:request :edn :response :json})))) + (is (= {:status 200 :body {:request :default, :response :default}} + (call (request "application/transit" "application/transit" {:request :default :response :default}))))) + (testing "request validation fails" + (is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} + (call (request "application/edn" "application/json" {:request :json :response :json})))) + (is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} + (call (request "application/json" "application/json" {:request :edn :response :json})))) + (is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} + (call (request "application/transit" "application/json" {:request :edn :response :json}))))) + (testing "response validation fails" + (is (= {:type :reitit.coercion/response-coercion :in [:response :body]} + (call (request "application/json" "application/json" {:request :json :response :edn})))) + (is (= {:type :reitit.coercion/response-coercion :in [:response :body]} + (call (request "application/json" "application/edn" {:request :json :response :json})))) + (is (= {:type :reitit.coercion/response-coercion :in [:response :body]} + (call (request "application/json" "application/transit" {:request :json :response :json}))))))))))) #?(:clj diff --git a/test/cljc/reitit/swagger_test.clj b/test/cljc/reitit/swagger_test.clj index 9b8f604c..40902704 100644 --- a/test/cljc/reitit/swagger_test.clj +++ b/test/cljc/reitit/swagger_test.clj @@ -138,7 +138,6 @@ rrc/coerce-request-middleware rrc/coerce-response-middleware]}}))) -(require '[fipp.edn]) (deftest swagger-test (testing "endpoints work" (testing "spec" @@ -451,7 +450,7 @@ (ring/router [["/parameters" {:post {:coercion spec/coercion - :parameters {:request {:content {"application/json" {:x string?}}}} + :request {:content {"application/json" {:x string?}}} :handler identity}}] ["/swagger.json" {:get {:no-doc true @@ -479,7 +478,7 @@ [#'spec/coercion reitit.http.interceptors.multipart/bytes-part string?]]] - (testing coercion + (testing (str coercion) (let [app (ring/ring-handler (ring/router [["/upload" diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs index a0e0abe0..12d461fa 100644 --- a/test/cljs/reitit/frontend/core_test.cljs +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -282,3 +282,16 @@ (testing "Need to coerce current values manually" (is (= "foo?foo=2" (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 %"))))) diff --git a/test/cljs/reitit/frontend/easy_test.cljs b/test/cljs/reitit/frontend/easy_test.cljs index abe11545..e36d9eee 100644 --- a/test/cljs/reitit/frontend/easy_test.cljs +++ b/test/cljs/reitit/frontend/easy_test.cljs @@ -29,10 +29,10 @@ 1 (do (is (some? (:popstate-listener history))) (is (= "/" url) "start at root") - (rfe/push-state ::foo)) + (rfe/push-state ::foo nil {:a 1} "foo bar")) ;; 0. / - ;; 1. /foo - 2 (do (is (= "/foo" url) + ;; 1. /foo?a=1#foo+bar + 2 (do (is (= "/foo?a=1#foo+bar" url) "push-state") (.back js/window.history)) ;; 0. / diff --git a/test/cljs/reitit/frontend/history_test.cljs b/test/cljs/reitit/frontend/history_test.cljs index ffab3970..ff3df965 100644 --- a/test/cljs/reitit/frontend/history_test.cljs +++ b/test/cljs/reitit/frontend/history_test.cljs @@ -26,6 +26,8 @@ (rfh/href history ::bar {:id 5}))) (is (= "#/bar/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 (fn [] (rfh/href history ::asd)))]