├── examples ├── simple │ ├── .gitignore │ ├── project.clj │ ├── README.md │ └── src │ │ └── example │ │ └── handler.clj ├── resources │ ├── .gitignore │ ├── screenshot.png │ ├── project.clj │ ├── README.md │ └── src │ │ └── example │ │ └── handler.clj ├── metrics-ring │ ├── .gitignore │ ├── project.clj │ ├── README.md │ └── src │ │ └── example │ │ └── handler.clj ├── reusable-resources │ ├── .gitignore │ ├── screenshot.png │ ├── project.clj │ ├── src │ │ └── example │ │ │ ├── domain.clj │ │ │ ├── handler.clj │ │ │ └── entity.clj │ └── README.md ├── async │ ├── .gitignore │ ├── README.md │ ├── project.clj │ └── src │ │ └── example │ │ └── handler.clj ├── coercion │ ├── .gitignore │ ├── project.clj │ ├── README.md │ └── src │ │ └── example │ │ ├── handler.clj │ │ ├── schema.clj │ │ ├── data_spec.clj │ │ └── spec.clj ├── README.md └── thingie │ ├── dev-src │ └── user.clj │ └── src │ └── examples │ ├── ordered.clj │ ├── dates.clj │ ├── server.clj │ ├── pizza.clj │ └── thingie.clj ├── dev-resources ├── json │ ├── json10b.json │ ├── json100b.json │ ├── json1k.json │ └── json10k.json └── screenshot.png ├── ISSUE_TEMPLATE.md ├── src └── compojure │ └── api │ ├── methods.clj │ ├── impl │ ├── json.clj │ └── logging.clj │ ├── request.clj │ ├── main.clj │ ├── coercion │ ├── core.clj │ ├── schema.clj │ └── spec.clj │ ├── async.clj │ ├── validator.clj │ ├── compojure_compat.clj │ ├── help.clj │ ├── upload.clj │ ├── common.clj │ ├── coerce.clj │ ├── exception.clj │ ├── core.clj │ ├── coercion.clj │ ├── swagger.clj │ ├── api.clj │ ├── routes.clj │ └── resource.clj ├── scripts ├── update-legacy-changelog.sh ├── check-dependabot ├── sync-dependabot └── build-docs.sh ├── .gitignore ├── test └── compojure │ └── api │ ├── test_domain.clj │ ├── help_test.clj │ ├── swagger_ordering_test.clj │ ├── common_test.clj │ ├── test_utils.clj │ ├── middleware_test.clj │ ├── compojure_perf_test.clj │ ├── dev │ └── gen.clj │ ├── swagger_test.clj │ ├── routes_test.clj │ └── perf_test.clj ├── .github ├── dependabot.yml ├── CONTRIBUTING.md └── workflows │ └── build.yml ├── docs └── release-checklist.md ├── test19 └── compojure │ └── api │ └── coercion │ ├── spec_coercion_explain_test.clj │ └── issue336_test.clj ├── dependabot ├── dependency-tree.txt ├── verbose-dependency-tree.txt └── pom.xml ├── project.clj └── README.md /examples/simple/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /dev-resources/json/json10b.json: -------------------------------------------------------------------------------- 1 | {"imu":42} -------------------------------------------------------------------------------- /examples/resources/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /examples/metrics-ring/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /examples/reusable-resources/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Library Version(s) 2 | 3 | ## Problem 4 | -------------------------------------------------------------------------------- /examples/async/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .lein-repl-history 3 | -------------------------------------------------------------------------------- /examples/coercion/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .lein-repl-history 3 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Example projects 2 | 3 | ## TODO 4 | 5 | * Component 6 | * Buddy 7 | * Mount -------------------------------------------------------------------------------- /dev-resources/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metosin/compojure-api/HEAD/dev-resources/screenshot.png -------------------------------------------------------------------------------- /examples/resources/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metosin/compojure-api/HEAD/examples/resources/screenshot.png -------------------------------------------------------------------------------- /dev-resources/json/json100b.json: -------------------------------------------------------------------------------- 1 | {"number":100,"boolean":true,"list":[{"kikka":"kukka"}],"nested":{"map":"this is value","secret":1}} -------------------------------------------------------------------------------- /src/compojure/api/methods.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.methods) 2 | 3 | (def all-methods #{:get :head :patch :delete :options :post :put}) 4 | -------------------------------------------------------------------------------- /examples/reusable-resources/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metosin/compojure-api/HEAD/examples/reusable-resources/screenshot.png -------------------------------------------------------------------------------- /scripts/update-legacy-changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | git fetch origin 5 | git show origin/1.1.x:CHANGELOG.md > CHANGELOG-1.1.x.md 6 | -------------------------------------------------------------------------------- /src/compojure/api/impl/json.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc compojure.api.impl.json 2 | "Internal JSON formatting" 3 | (:require [muuntaja.core :as m])) 4 | 5 | (def muuntaja 6 | (m/create)) 7 | -------------------------------------------------------------------------------- /examples/thingie/dev-src/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [reloaded.repl :refer [set-init! system init start stop go reset]])) 3 | 4 | (set-init! #(do (require 'examples.server) ((resolve 'examples.server/new-system)))) 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | /pom.xml 5 | /pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .project 11 | .settings 12 | bower_components 13 | *.log 14 | gh-pages 15 | .cpcache 16 | -------------------------------------------------------------------------------- /examples/simple/project.clj: -------------------------------------------------------------------------------- 1 | (defproject example "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :dependencies [[org.clojure/clojure "1.8.0"] 4 | [metosin/compojure-api "1.1.10"]] 5 | :ring {:handler example.handler/app} 6 | :uberjar-name "server.jar" 7 | :profiles {:dev {:plugins [[lein-ring "0.10.0"]]}}) 8 | -------------------------------------------------------------------------------- /examples/resources/project.clj: -------------------------------------------------------------------------------- 1 | (defproject example "0.1.0-SNAPSHOT" 2 | :description "Example on Compojure-api resources" 3 | :dependencies [[org.clojure/clojure "1.8.0"] 4 | [metosin/compojure-api "1.1.10"]] 5 | :ring {:handler example.handler/app} 6 | :uberjar-name "server.jar" 7 | :profiles {:dev {:plugins [[lein-ring "0.10.0"]]}}) 8 | -------------------------------------------------------------------------------- /examples/reusable-resources/project.clj: -------------------------------------------------------------------------------- 1 | (defproject example "0.1.0-SNAPSHOT" 2 | :description "Example on reusable Compojure-api resources" 3 | :dependencies [[org.clojure/clojure "1.8.0"] 4 | [metosin/compojure-api "1.1.10"]] 5 | :ring {:handler example.handler/app} 6 | :uberjar-name "server.jar" 7 | :profiles {:dev {:plugins [[lein-ring "0.10.0"]]}}) 8 | -------------------------------------------------------------------------------- /examples/coercion/project.clj: -------------------------------------------------------------------------------- 1 | (defproject example "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :dependencies [[org.clojure/clojure "1.9.0-alpha17"] 4 | [metosin/compojure-api "2.0.0-alpha6"] 5 | [metosin/spec-tools "0.3.2"]] 6 | :ring {:handler example.handler/app, :async? true} 7 | :uberjar-name "server.jar" 8 | :profiles {:dev {:plugins [[lein-ring "0.12.0"]]}}) 9 | -------------------------------------------------------------------------------- /examples/metrics-ring/project.clj: -------------------------------------------------------------------------------- 1 | (defproject example "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :dependencies [[org.clojure/clojure "1.8.0"] 4 | [metosin/compojure-api "1.1.10"] 5 | 6 | [metrics-clojure "2.8.0"] 7 | [metrics-clojure-ring "2.8.0"]] 8 | 9 | :ring {:handler example.handler/app} 10 | :uberjar-name "server.jar" 11 | :profiles {:dev {:plugins [[lein-ring "0.10.0"]]}}) 12 | -------------------------------------------------------------------------------- /src/compojure/api/request.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.request) 2 | 3 | (def coercion 4 | "Request-scoped coercion" 5 | ::coercion) 6 | 7 | (def swagger 8 | "Vector of extra swagger data" 9 | ::swagger) 10 | 11 | (def ring-swagger 12 | "Ring-swagger options" 13 | ::ring-swagger) 14 | 15 | (def paths 16 | "Paths" 17 | ::paths) 18 | 19 | (def lookup 20 | "Reverse routing tree" 21 | ::lookup) 22 | 23 | (def muuntaja 24 | "Muuntaja instance" 25 | ::muuntaja) 26 | -------------------------------------------------------------------------------- /examples/async/README.md: -------------------------------------------------------------------------------- 1 | # Asynchronous compojure-api app 2 | 3 | ## Usage 4 | 5 | ### Run the application locally 6 | 7 | `lein ring server` 8 | 9 | ### Packaging and running as standalone jar 10 | 11 | ``` 12 | lein do clean, ring uberjar 13 | java -jar target/server.jar 14 | ``` 15 | 16 | ### Packaging as war 17 | 18 | `lein ring uberwar` 19 | 20 | ## License 21 | 22 | Copyright © 2017 [Metosin Oy](http://www.metosin.fi) 23 | 24 | Distributed under the Eclipse Public License, the same as Clojure. 25 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # Simple compojure-api app 2 | 3 | ## Usage 4 | 5 | ### Run the application locally 6 | 7 | `lein ring server` 8 | 9 | ### Packaging and running as standalone jar 10 | 11 | ``` 12 | lein do clean, ring uberjar 13 | java -jar target/server.jar 14 | ``` 15 | 16 | ### Packaging as war 17 | 18 | `lein ring uberwar` 19 | 20 | ## License 21 | 22 | Copyright © 2016-2017 [Metosin Oy](http://www.metosin.fi) 23 | 24 | Distributed under the Eclipse Public License, the same as Clojure. 25 | -------------------------------------------------------------------------------- /examples/coercion/README.md: -------------------------------------------------------------------------------- 1 | # Compojure-api app with different coercion 2 | 3 | ## Usage 4 | 5 | ### Run the application locally 6 | 7 | `lein ring server` 8 | 9 | ### Packaging and running as standalone jar 10 | 11 | ``` 12 | lein do clean, ring uberjar 13 | java -jar target/server.jar 14 | ``` 15 | 16 | ### Packaging as war 17 | 18 | `lein ring uberwar` 19 | 20 | ## License 21 | 22 | Copyright © 2017 [Metosin Oy](http://www.metosin.fi) 23 | 24 | Distributed under the Eclipse Public License, the same as Clojure. 25 | -------------------------------------------------------------------------------- /src/compojure/api/main.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.main 2 | (:require [clojure.string :as s]) 3 | (:gen-class)) 4 | 5 | (defn resolve-start-fn [] 6 | (let [start (some-> "./project.clj" 7 | slurp 8 | read-string 9 | (->> (drop 1)) 10 | (->> (apply hash-map)) 11 | :start) 12 | names (-> start str (s/split #"/") first symbol)] 13 | (require names) 14 | (resolve start))) 15 | 16 | (defn -main [& args] 17 | (apply (resolve-start-fn) args)) 18 | -------------------------------------------------------------------------------- /test/compojure/api/test_domain.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.test-domain 2 | (:require [schema.core :as s] 3 | [compojure.api.sweet :refer :all] 4 | [ring.util.http-response :refer [ok]])) 5 | 6 | (s/defschema Topping {:name s/Str}) 7 | (s/defschema Pizza {:toppings (s/maybe [Topping])}) 8 | 9 | (s/defschema Beef {:name s/Str}) 10 | (s/defschema Burger {:ingredients (s/maybe [Beef])}) 11 | 12 | (def burger-routes 13 | (routes 14 | (POST "/burger" [] 15 | :return Burger 16 | :body [burger Burger] 17 | (ok burger)))) 18 | -------------------------------------------------------------------------------- /examples/async/project.clj: -------------------------------------------------------------------------------- 1 | (defproject example "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :dependencies [[org.clojure/clojure "1.9.0"] 4 | [metosin/compojure-api "2.0.0-alpha25" :exclude [compojure, metosin/muuntaja]] 5 | [ring/ring "1.6.3"] 6 | [compojure "1.6.1"] 7 | [manifold "0.1.8"] 8 | [org.clojure/core.async "0.4.474"]] 9 | :ring {:handler example.handler/app 10 | :async? true} 11 | :uberjar-name "server.jar" 12 | :profiles {:dev {:plugins [[lein-ring "0.11.0"]]}}) 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" 9 | directory: "/dependabot" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /examples/metrics-ring/README.md: -------------------------------------------------------------------------------- 1 | # Simple compojure-api app 2 | 3 | Demonstrates how to use [metrics-clojure](https://github.com/sjl/metrics-clojure) with compojure-api. 4 | 5 | ## Usage 6 | 7 | ### Run the application locally 8 | 9 | `lein ring server` 10 | 11 | ### Packaging and running as standalone jar 12 | 13 | ``` 14 | lein do clean, ring uberjar 15 | java -jar target/server.jar 16 | ``` 17 | 18 | ### Packaging as war 19 | 20 | `lein ring uberwar` 21 | 22 | ## License 23 | 24 | Copyright © 2016-2017 [Metosin Oy](http://www.metosin.fi) 25 | 26 | Distributed under the Eclipse Public License, the same as Clojure. 27 | -------------------------------------------------------------------------------- /scripts/check-dependabot: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Check that project.clj is in sync with the committed dependabot pom.xml. 3 | 4 | set -e 5 | 6 | ./scripts/sync-dependabot 7 | set +e 8 | if git diff --ignore-all-space --exit-code dependabot/pom.xml dependabot/dependency-tree.txt ; then 9 | echo 'project.clj and dependabot/pom.xml are in sync.' 10 | exit 0 11 | else 12 | echo 13 | echo 'project.clj and dependabot/pom.xml are out of sync! Please run ./scripts/sync-dependabot locally and commit the results.' 14 | echo 'If this is a PR from dependabot, you must manually update the version in project.clj' 15 | exit 1 16 | fi 17 | -------------------------------------------------------------------------------- /scripts/sync-dependabot: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | 5 | SHA=$(git rev-parse HEAD) 6 | 7 | lein with-profile -dev pom 8 | mkdir -p dependabot 9 | mv pom.xml dependabot 10 | # lein pom uses the origin git remote to add metadata. remove for reproducibility. 11 | bb '(spit "dependabot/pom.xml" (-> "dependabot/pom.xml" slurp xml/parse-str (update :content (partial remove #(some-> % :tag name #{"scm" "url"}))) xml/emit-str))' 12 | cd dependabot 13 | mvn --no-transfer-progress dependency:tree -Dexcludes=org.clojure:clojure -DoutputFile=dependency-tree.txt 14 | mvn --no-transfer-progress dependency:tree -Dverbose -Dexcludes=org.clojure:clojure -DoutputFile=verbose-dependency-tree.txt 15 | -------------------------------------------------------------------------------- /examples/thingie/src/examples/ordered.clj: -------------------------------------------------------------------------------- 1 | (ns examples.ordered 2 | (:require [compojure.api.sweet :refer :all] 3 | [ring.util.http-response :refer :all])) 4 | 5 | (def more-ordered-routes 6 | (routes 7 | (GET "/6" [] (ok)) 8 | (GET "/7" [] (ok)) 9 | (GET "/8" [] (ok)))) 10 | 11 | (def ordered-routes 12 | (context "/ordered" [] 13 | :tags ["ordered"] 14 | (context "/a" [] 15 | (GET "/1" [] (ok)) 16 | (GET "/2" [] (ok)) 17 | (GET "/3" [] (ok)) 18 | (context "/b" [] 19 | (GET "/4" [] (ok)) 20 | (GET "/5" [] (ok))) 21 | (context "/c" [] 22 | more-ordered-routes 23 | (GET "/9" [] (ok)) 24 | (GET "/10" [] (ok)))))) 25 | -------------------------------------------------------------------------------- /test/compojure/api/help_test.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.help-test 2 | (:require [compojure.api.help :as help] 3 | [compojure.api.meta :as meta] 4 | [clojure.test :refer [deftest is testing]])) 5 | 6 | (deftest help-for-api-meta-test 7 | (testing "all restructure-param methods have a help text" 8 | (let [restructure-method-names (-> meta/restructure-param methods keys) 9 | meta-help-topics (-> (methods help/help-for) 10 | (dissoc ::help/default) 11 | keys 12 | (->> (filter #(= :meta (first %))) 13 | (map second)))] 14 | (is (= (set restructure-method-names) (set meta-help-topics)))))) 15 | -------------------------------------------------------------------------------- /examples/coercion/src/example/handler.clj: -------------------------------------------------------------------------------- 1 | (ns example.handler 2 | (:require [compojure.api.sweet :refer [api]] 3 | [example.schema] 4 | [example.spec] 5 | [example.data-spec])) 6 | 7 | (def app 8 | (api 9 | {:swagger 10 | {:ui "/" 11 | :spec "/swagger.json" 12 | :data {:info {:title "Compojure-api demo" 13 | :description "Demonstrating 2.0.0 (alpha) coercion"} 14 | :tags [{:name "schema", :description "math with schema coercion"} 15 | {:name "spec", :description "math with clojure.spec coercion"} 16 | {:name "data-spec", :description "math with data-specs coercion"}]}}} 17 | 18 | example.schema/routes 19 | example.spec/routes 20 | example.data-spec/routes)) 21 | -------------------------------------------------------------------------------- /scripts/build-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rev=$(git rev-parse HEAD) 4 | remoteurl=$(git ls-remote --get-url origin) 5 | 6 | git fetch 7 | if [[ -z $(git branch -r --list origin/gh-pages) ]]; then 8 | ( 9 | mkdir gh-pages 10 | cd gh-pages 11 | git init 12 | git remote add origin ${remoteurl} 13 | git checkout -b gh-pages 14 | git commit --allow-empty -m "Init" 15 | git push -u origin gh-pages 16 | ) 17 | elif [[ ! -d gh-pages ]]; then 18 | git clone --branch gh-pages ${remoteurl} gh-pages 19 | else 20 | ( 21 | cd gh-pages 22 | git fetch 23 | git reset --hard origin/gh-pages 24 | ) 25 | fi 26 | 27 | mkdir -p gh-pages/doc 28 | lein doc 29 | ( 30 | cd gh-pages 31 | git add --all 32 | git commit -m "Build docs from ${rev}." 33 | git push origin gh-pages 34 | ) 35 | -------------------------------------------------------------------------------- /examples/reusable-resources/src/example/domain.clj: -------------------------------------------------------------------------------- 1 | (ns example.domain 2 | (:require [schema.core :as s] 3 | [plumbing.core :as p] 4 | [clojure.string :as str])) 5 | 6 | (s/defschema Pizza 7 | {:id s/Int 8 | :name s/Str 9 | (s/optional-key :description) s/Str 10 | :size (s/enum :L :M :S) 11 | :origin {:country (s/enum :FI :PO) 12 | :city s/Str}}) 13 | 14 | (s/defschema Kebab 15 | {:id s/Int 16 | :name s/Str 17 | :type (s/enum :doner :shish :souvlaki)}) 18 | 19 | (s/defschema Sausage 20 | {:id s/Int 21 | :type (s/enum :musta :jauho) 22 | :meat s/Int}) 23 | 24 | (s/defschema Beer 25 | {:id s/Int 26 | :type (s/enum :ipa :apa)}) 27 | 28 | (defn entities [] 29 | (p/for-map [[n v] (ns-publics 'example.domain) 30 | :when (s/schema-name @v)] 31 | (str/lower-case n) @v)) 32 | -------------------------------------------------------------------------------- /examples/coercion/src/example/schema.clj: -------------------------------------------------------------------------------- 1 | (ns example.schema 2 | (:require [compojure.api.sweet :refer [context POST resource]] 3 | [ring.util.http-response :refer [ok]] 4 | [schema.core :as s])) 5 | 6 | (s/defschema Total 7 | {:total s/Int}) 8 | 9 | (def routes 10 | (context "/schema" [] 11 | :tags ["schema"] 12 | 13 | (POST "/plus" [] 14 | :summary "plus with schema" 15 | :body-params [x :- s/Int, {y :- s/Int 0}] 16 | :return Total 17 | (ok {:total (+ x y)})) 18 | 19 | (context "/plus" [] 20 | (resource 21 | {:get 22 | {:summary "data-driven plus with schema" 23 | :parameters {:query-params {:x s/Str, :y s/Str}} 24 | :responses {200 {:schema Total}} 25 | :handler (fn [{{:keys [x y]} :query-params}] 26 | (ok {:total (+ x y)}))}})))) -------------------------------------------------------------------------------- /examples/coercion/src/example/data_spec.clj: -------------------------------------------------------------------------------- 1 | (ns example.data-spec 2 | (:require [compojure.api.sweet :refer [context POST resource]] 3 | [ring.util.http-response :refer [ok]])) 4 | 5 | (def routes 6 | (context "/data-spec" [] 7 | :tags ["data-spec"] 8 | :coercion :spec 9 | 10 | (POST "/plus" [] 11 | :summary "plus with clojure.spec using data-specs" 12 | :body-params [x :- int?, {y :- int? 0}] 13 | :return {:total int?} 14 | (ok {:total (+ x y)})) 15 | 16 | (context "/plus" [] 17 | (resource 18 | {:get 19 | {:summary "data-driven plus with clojure.spec using data-specs" 20 | :parameters {:query-params {:x int?, :y int?}} 21 | :responses {200 {:schema {:total int?}}} 22 | :handler (fn [{{:keys [x y]} :query-params}] 23 | (ok {:total (+ x y)}))}})))) 24 | -------------------------------------------------------------------------------- /src/compojure/api/impl/logging.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc compojure.api.impl.logging 2 | "Internal Compojure-api logging utility" 3 | (:require [clojure.string :as str])) 4 | 5 | ;; Cursive-users 6 | (declare log!) 7 | 8 | ;; use c.t.l logging if available, default to console logging 9 | (try 10 | (eval 11 | `(do 12 | (require 'clojure.tools.logging) 13 | (defmacro ~'log! [& ~'args] 14 | `(clojure.tools.logging/log ~@~'args)))) 15 | (catch Exception _ 16 | (let [log (fn [level more] (println (.toUpperCase (name level)) (str/join " " more)))] 17 | (defn log! [level x & more] 18 | (if (instance? Throwable x) 19 | (do 20 | (log level more) 21 | (.printStackTrace ^Throwable x)) 22 | (log level (into [x] more)))) 23 | (log! :warn "clojure.tools.logging not found on classpath, compojure.api logging to console.")))) 24 | -------------------------------------------------------------------------------- /dev-resources/json/json1k.json: -------------------------------------------------------------------------------- 1 | {"results":[{"gender":"male","name":{"title":"mr","first":"morris","last":"lambert"},"location":{"street":"7239 hillcrest rd","city":"nowra","state":"australian capital territory","postcode":7541},"email":"morris.lambert@example.com","login":{"username":"smallbird414","password":"carole","salt":"yO9OBSsk","md5":"658323a603522238fb32a86b82eafd55","sha1":"289f6e9a8ccd42b539e0c43283e788aeb8cd0f6e","sha256":"57bca99b2b4e78aa2171eda4db3f35e7631ca3b30f157bdc7ea089a855c66668"},"dob":"1950-07-13 09:18:34","registered":"2012-04-07 00:05:32","phone":"08-2274-7839","cell":"0452-558-702","id":{"name":"TFN","value":"740213762"},"picture":{"large":"https://randomuser.me/api/portraits/men/95.jpg","medium":"https://randomuser.me/api/portraits/med/men/95.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/95.jpg"},"nat":"AU"}],"info":{"seed":"fb0c2b3c7cedc7af","results":1,"page":1,"version":"1.1"}} 2 | -------------------------------------------------------------------------------- /examples/resources/README.md: -------------------------------------------------------------------------------- 1 | # Resources 2 | 3 | Simple [compojure-api](https://github.com/metosin/compojure-api)-project using [`resources`](https://github.com/metosin/compojure-api/blob/master/src/compojure/api/resource.clj). 4 | 5 | Demonstrates how to use resources to build data-driven apis. 6 | 7 | (not REST, just http-apis here). 8 | 9 | 10 | 11 | ## Usage 12 | 13 | ### Run the application locally 14 | 15 | `lein ring server` 16 | 17 | ### Packaging and running as standalone jar 18 | 19 | ``` 20 | lein do clean, ring uberjar 21 | java -jar target/server.jar 22 | ``` 23 | 24 | ### Packaging as war 25 | 26 | `lein ring uberwar` 27 | 28 | ## License 29 | 30 | Copyright © 2014-2017 [Metosin Oy](http://www.metosin.fi) 31 | 32 | Distributed under the Eclipse Public License, the same as Clojure. 33 | -------------------------------------------------------------------------------- /examples/thingie/src/examples/dates.clj: -------------------------------------------------------------------------------- 1 | (ns examples.dates 2 | (:require [compojure.api.sweet :refer :all] 3 | [schema.core :as s] 4 | [ring.util.http-response :refer :all]) 5 | (:import (java.util Date) 6 | (org.joda.time LocalDate DateTime))) 7 | 8 | (s/defschema Dates {:date Date 9 | :date-time DateTime 10 | :local-date LocalDate}) 11 | 12 | (defn sample [] {:date (Date.) 13 | :date-time (DateTime.) 14 | :local-date (LocalDate.)}) 15 | 16 | (def date-routes 17 | (routes 18 | (GET "/dates" [] 19 | :return Dates 20 | :summary "returns dates" 21 | (ok (sample))) 22 | (POST "/dates" [] 23 | :return Dates 24 | :body [sample (describe Dates "read response from GET /dates in here to see symmetric handling of dates")] 25 | :summary "echos date input." 26 | (ok sample)))) 27 | -------------------------------------------------------------------------------- /src/compojure/api/coercion/core.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.coercion.core) 2 | 3 | (defprotocol Coercion 4 | (get-name [this]) 5 | (get-apidocs [this model data]) 6 | (make-open [this model]) 7 | (encode-error [this error]) 8 | (coerce-request [this model value type format request]) 9 | (accept-response? [this model]) 10 | (coerce-response [this model value type format request])) 11 | 12 | (defrecord CoercionError []) 13 | 14 | (defmulti named-coercion identity :default ::default) 15 | 16 | (defmethod named-coercion ::default [x] 17 | (let [message (if (= :spec x) 18 | (str "spec-coercion is not enabled. " 19 | "you most likely are missing the " 20 | "required deps: org.clojure/clojure 1.9+ " 21 | "and metosin/spec-tools.") 22 | (str "cant find named-coercion for " x))] 23 | (throw (ex-info message {:name x})))) 24 | -------------------------------------------------------------------------------- /examples/reusable-resources/README.md: -------------------------------------------------------------------------------- 1 | # Reusable resources 2 | 3 | Simple [compojure-api](https://github.com/metosin/compojure-api)-project using [`resources`](https://github.com/metosin/compojure-api/blob/master/src/compojure/api/resource.clj). 4 | 5 | Demonstrates how to build reusable resource apis - both predefined & runtime-generated. 6 | 7 | (not REST, just http-apis here). 8 | 9 | 10 | 11 | ## Usage 12 | 13 | ### Run the application locally 14 | 15 | `lein ring server` 16 | 17 | ### Packaging and running as standalone jar 18 | 19 | ``` 20 | lein do clean, ring uberjar 21 | java -jar target/server.jar 22 | ``` 23 | 24 | ### Packaging as war 25 | 26 | `lein ring uberwar` 27 | 28 | ## License 29 | 30 | Copyright © 2014-2017 [Metosin Oy](http://www.metosin.fi) 31 | 32 | Distributed under the Eclipse Public License, the same as Clojure. 33 | -------------------------------------------------------------------------------- /docs/release-checklist.md: -------------------------------------------------------------------------------- 1 | # Release checklist 2 | 3 | Are you going to publish a new release of compojure-api? Great! Please use this 4 | checklist to ensure that the release is properly made. The goal is to make it 5 | easy for both the users and the maintainers to know what's included in the 6 | release. 7 | 8 | * [ ] You pulled the latest master before starting to create a release. 9 | * [ ] `CHANGELOG.md` contains a high-level summary of the changes in the new release. 10 | * [ ] Breaking changes, if any, have been highlighted. 11 | * [ ] A JAR has been deployed to Clojars. 12 | * [ ] Your working tree was clean when you built the JAR. 13 | * [ ] The JAR is signed with a public key that has been published on the keyservers. 14 | * [ ] The release has been tagged in git. 15 | * [ ] The tag has been pushed to GitHub. 16 | * [ ] The tag points to the same commit as the JAR on Clojars. 17 | * [ ] The API reference has been updated by running `scripts/build-docs.sh`. 18 | -------------------------------------------------------------------------------- /examples/coercion/src/example/spec.clj: -------------------------------------------------------------------------------- 1 | (ns example.spec 2 | (:require [compojure.api.sweet :refer [context POST resource]] 3 | [ring.util.http-response :refer [ok]] 4 | [clojure.spec.alpha :as s] 5 | [spec-tools.spec :as spec])) 6 | 7 | (s/def ::x spec/int?) 8 | (s/def ::y spec/int?) 9 | (s/def ::total spec/int?) 10 | (s/def ::total-map (s/keys :req-un [::total])) 11 | 12 | (def routes 13 | (context "/spec" [] 14 | :tags ["spec"] 15 | :coercion :spec 16 | 17 | (POST "/plus" [] 18 | :summary "plus with clojure.spec using data-specs" 19 | :body-params [x :- ::x, {y :- ::y 0}] 20 | :return ::total-map 21 | (ok {:total (+ x y)})) 22 | 23 | (context "/plus" [] 24 | (resource 25 | {:get 26 | {:summary "data-driven plus with clojure.spec using specs" 27 | :parameters {:query-params (s/keys :req-un [::x ::y])} 28 | :responses {200 {:schema ::total-map}} 29 | :handler (fn [{{:keys [x y]} :query-params}] 30 | (ok {:total (+ x y)}))}})))) 31 | -------------------------------------------------------------------------------- /examples/simple/src/example/handler.clj: -------------------------------------------------------------------------------- 1 | (ns example.handler 2 | (:require [compojure.api.sweet :refer :all] 3 | [ring.util.http-response :refer :all] 4 | [schema.core :as s])) 5 | 6 | (s/defschema Pizza 7 | {:name s/Str 8 | (s/optional-key :description) s/Str 9 | :size (s/enum :L :M :S) 10 | :origin {:country (s/enum :FI :PO) 11 | :city s/Str}}) 12 | 13 | (def app 14 | (api 15 | {:swagger 16 | {:ui "/" 17 | :spec "/swagger.json" 18 | :data {:info {:title "Simple" 19 | :description "Compojure Api example"} 20 | :tags [{:name "api", :description "some apis"}]}}} 21 | 22 | (context "/api" [] 23 | :tags ["api"] 24 | 25 | (GET "/plus" [] 26 | :return {:result Long} 27 | :query-params [x :- Long, y :- Long] 28 | :summary "adds two numbers together" 29 | (ok {:result (+ x y)})) 30 | 31 | (POST "/echo" [] 32 | :return Pizza 33 | :body [pizza Pizza] 34 | :summary "echoes a Pizza" 35 | (ok pizza))))) 36 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Contributions are welcome. 4 | 5 | Please file bug reports and feature requests to https://github.com/metosin/compojure-api/issues. 6 | 7 | ## Making changes 8 | 9 | * Fork the repository on Github 10 | * Create a topic branch from where you want to base your work (usually the master branch) 11 | * Check the formatting rules from existing code (no trailing whitepace, mostly default indentation) 12 | * Ensure any new code is well-tested, and if possible, any issue fixed is covered by one or more new tests 13 | * Verify that all tests pass using ```lein midje``` 14 | * Push your code to your fork of the repository 15 | * Make a Pull Request 16 | 17 | ## Commit messages 18 | 19 | 1. Separate subject from body with a blank line 20 | 2. Limit the subject line to 50 characters 21 | 3. Capitalize the subject line 22 | 4. Do not end the subject line with a period 23 | 5. Use the imperative mood in the subject line 24 | - "Add x", "Fix y", "Support z", "Remove x" 25 | 6. Wrap the body at 72 characters 26 | 7. Use the body to explain what and why vs. how 27 | -------------------------------------------------------------------------------- /examples/thingie/src/examples/server.clj: -------------------------------------------------------------------------------- 1 | (ns examples.server 2 | (:gen-class) 3 | (:require [org.httpkit.server :as httpkit] 4 | [compojure.api.middleware :refer [wrap-components]] 5 | [com.stuartsierra.component :as component] 6 | [examples.thingie :refer [app]] 7 | [reloaded.repl :refer [go set-init!]])) 8 | 9 | (defrecord Example [] 10 | component/Lifecycle 11 | (start [this] 12 | (assoc this :example "hello world")) 13 | (stop [this] 14 | this)) 15 | 16 | (defrecord HttpKit [] 17 | component/Lifecycle 18 | (start [this] 19 | (println "Server started at http://localhost:3000") 20 | (assoc this :http-kit (httpkit/run-server 21 | (wrap-components 22 | #'app 23 | (select-keys this [:example])) 24 | {:port 3000}))) 25 | (stop [this] 26 | (if-let [http-kit (:http-kit this)] 27 | (http-kit)) 28 | (dissoc this :http-kit))) 29 | 30 | (defn new-system [] 31 | (component/system-map 32 | :example (->Example) 33 | :http-kit (component/using (->HttpKit) [:example]))) 34 | 35 | (defn -main [] 36 | (set-init! #(new-system)) 37 | (go)) 38 | -------------------------------------------------------------------------------- /src/compojure/api/async.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.async 2 | (:require [compojure.response :as response] 3 | [compojure.api.common :as common] 4 | compojure.api.routes)) 5 | 6 | (common/when-ns 'manifold.deferred 7 | ;; Compojure is smart enough to get the success value out of deferred by 8 | ;; itself, but we want to catch the exceptions as well. 9 | (extend-protocol compojure.response/Sendable 10 | manifold.deferred.IDeferred 11 | (send* [deferred request respond raise] 12 | (manifold.deferred/on-realized deferred #(response/send % request respond raise) raise)))) 13 | 14 | (common/when-ns 'clojure.core.async 15 | (extend-protocol compojure.response/Sendable 16 | clojure.core.async.impl.channels.ManyToManyChannel 17 | (send* [channel request respond raise] 18 | (clojure.core.async/go 19 | (let [message (clojure.core.async/> response :body (m/decode json/muuntaja "application/json"))] 19 | 20 | (when-not (= status 200) 21 | (throw (ex-info (str "Coudn't read swagger spec from " uri) 22 | {:status status 23 | :body body}))) 24 | 25 | (when-let [errors (seq (rsv/validate body))] 26 | (throw (ex-info (str "Invalid swagger spec from " uri) 27 | {:errors errors 28 | :body body}))))) 29 | api) 30 | -------------------------------------------------------------------------------- /examples/metrics-ring/src/example/handler.clj: -------------------------------------------------------------------------------- 1 | (ns example.handler 2 | (:require [compojure.api.sweet :refer :all] 3 | [ring.util.http-response :refer :all] 4 | [metrics.ring.instrument :refer [instrument]] 5 | [metrics.core :refer [default-registry]] 6 | [metrics.ring.expose :refer [render-metrics]] 7 | [schema.core :as s])) 8 | 9 | (s/defschema Pizza 10 | {:name s/Str 11 | (s/optional-key :description) s/Str 12 | :size (s/enum :L :M :S) 13 | :origin {:country (s/enum :FI :PO) 14 | :city s/Str}}) 15 | 16 | (def app 17 | (instrument 18 | (api 19 | {:swagger 20 | {:ui "/" 21 | :spec "/swagger.json" 22 | :data {:info {:title "Simple" 23 | :description "Compojure Api example"} 24 | :tags [{:name "api", :description "some apis"}]}}} 25 | 26 | (context "/api" [] 27 | :tags ["api"] 28 | 29 | (GET "/metrics" [] 30 | :summary "Application level metrics." 31 | (ok (render-metrics default-registry))) 32 | 33 | (GET "/plus" [] 34 | :return {:result Long} 35 | :query-params [x :- Long, y :- Long] 36 | :summary "adds two numbers together" 37 | (ok {:result (+ x y)})) 38 | 39 | (POST "/echo" [] 40 | :return Pizza 41 | :body [pizza Pizza] 42 | :summary "echoes a Pizza" 43 | (ok pizza)))))) 44 | -------------------------------------------------------------------------------- /src/compojure/api/compojure_compat.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.compojure-compat 2 | "Compatibility for older Compojure versions." 3 | (:require [clout.core :as clout] 4 | [compojure.core :as c])) 5 | 6 | ;; Copy-pasted from Compojure 1.6 to maintain backwards-compatibility with 7 | ;; Compojure 1.5. Essentially the same code existed in Compojure 1.5 but with a 8 | ;; different name. 9 | ;; 10 | 11 | (defn- context-request [request route] 12 | (if-let [params (clout/route-matches route request)] 13 | (let [uri (:uri request) 14 | path (:path-info request uri) 15 | context (or (:context request) "") 16 | subpath (:__path-info params) 17 | params (dissoc params :__path-info)] 18 | (-> request 19 | (#'c/assoc-route-params (#'c/decode-route-params params)) 20 | (assoc :path-info (if (= subpath "") "/" subpath) 21 | :context (#'c/remove-suffix uri subpath)))))) 22 | 23 | (defn ^:no-doc make-context [route make-handler] 24 | (letfn [(handler 25 | ([request] 26 | ((make-handler request) request)) 27 | ([request respond raise] 28 | ((make-handler request) request respond raise)))] 29 | (if (#{":__path-info" "/:__path-info"} (:source route)) 30 | handler 31 | (fn 32 | ([request] 33 | (if-let [request (context-request request route)] 34 | (handler request))) 35 | ([request respond raise] 36 | (if-let [request (context-request request route)] 37 | (handler request respond raise) 38 | (respond nil))))))) 39 | -------------------------------------------------------------------------------- /examples/reusable-resources/src/example/handler.clj: -------------------------------------------------------------------------------- 1 | (ns example.handler 2 | (:require [compojure.api.sweet :refer [api context routes GET]] 3 | [ring.util.http-response :refer :all] 4 | [clojure.string :as str] 5 | [example.entity :as entity] 6 | [example.domain :as domain])) 7 | 8 | (def app 9 | (let [db (atom {}) 10 | entities (domain/entities) 11 | entity-resource (partial entity/resource db) 12 | entity-resource-routes (->> entities vals (map entity-resource) (apply routes)) 13 | entity-tags (->> entities keys (map (fn [name] {:name name, :description (str "api to manage " name "s")})))] 14 | 15 | (api 16 | {:swagger 17 | {:ui "/" 18 | :spec "/swagger.json" 19 | :data {:info {:title "Reusable resources" 20 | :description (str "Example app using `compojure.api.resource`.
" 21 | "The `*runtime*`-routes are generated at runtime, " 22 | "based on the path.
Despite the swagger-ui only " 23 | "shows `sausage` as runtime entity api,
apis exist for all " 24 | "defined entities: `pizza`, `kebab`, `sausage` and `beer`.
" 25 | "try `/runtime/pizza/`, `/runtime/kebab/` etc.") 26 | :contact {:url "https://github.com/metosin/compojure-api/"}} 27 | :tags entity-tags}}} 28 | 29 | entity-resource-routes 30 | 31 | (context "/runtime" request 32 | (if-let [entity (or (some-> request :path-info (str/replace #"/" "")) "sausage")] 33 | (entity-resource (entities entity) "*runtime*")))))) 34 | -------------------------------------------------------------------------------- /examples/async/src/example/handler.clj: -------------------------------------------------------------------------------- 1 | (ns example.handler 2 | "Asynchronous compojure-api application." 3 | (:require [compojure.api.sweet :refer :all] 4 | [ring.util.http-response :refer :all] 5 | [manifold.deferred :as d] 6 | [clojure.core.async :as async] 7 | compojure.api.async)) 8 | 9 | (def app 10 | (api 11 | {:swagger 12 | {:ui "/" 13 | :spec "/swagger.json" 14 | :data {:info {:title "Simple" 15 | :description "Compojure Api example"} 16 | :tags [{:name "api", :description "some apis"}]}}} 17 | 18 | (context "/api" [] 19 | :tags ["api"] 20 | 21 | (GET "/plus" [] 22 | :return {:result Long} 23 | :query-params [x :- Long, y :- Long] 24 | :summary "adds two numbers together" 25 | (ok {:result (+ x y)})) 26 | 27 | (GET "/minus" [] 28 | :return {:result Long} 29 | :query-params [x :- Long, y :- Long] 30 | :summary "subtract two numbers from each other" 31 | (fn [_ respond _] 32 | (future 33 | (respond (ok {:result (- x y)}))) 34 | nil)) 35 | 36 | (GET "/times" [] 37 | :return {:result Long} 38 | :query-params [x :- Long, y :- Long] 39 | :summary "multiply two numbers together" 40 | (d/success-deferred 41 | (ok {:result (* x y)}))) 42 | 43 | (GET "/divide" [] 44 | :return {:result Float} 45 | :query-params [x :- Long, y :- Long] 46 | :summary "divide two numbers together" 47 | (async/go (ok {:result (float (/ x y))})))) 48 | 49 | (context "/resource" [] 50 | (resource 51 | {:responses {200 {:schema {:total Long}}} 52 | :handler (fn [_ respond _] 53 | (future 54 | (respond (ok {:total 42}))) 55 | nil)})))) 56 | -------------------------------------------------------------------------------- /test/compojure/api/common_test.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.common-test 2 | (:require [compojure.api.common :as common] 3 | [clojure.test :refer [deftest testing is]] 4 | [criterium.core :as cc])) 5 | 6 | (deftest group-with-test 7 | (is (= (common/group-with pos? [1 -10 2 -4 -1 999]) [[1 2 999] [-10 -4 -1]])) 8 | (is (= (common/group-with pos? [1 2 999]) [[1 2 999] nil]))) 9 | 10 | (deftest extract-parameters-test 11 | 12 | (testing "expect body" 13 | (is (= (common/extract-parameters [] true) [{} nil])) 14 | (is (= (common/extract-parameters [{:a 1}] true) [{} [{:a 1}]])) 15 | (is (= (common/extract-parameters [:a 1] true) [{:a 1} nil])) 16 | (is (= (common/extract-parameters [{:a 1} {:b 2}] true) [{:a 1} [{:b 2}]])) 17 | (is (= (common/extract-parameters [:a 1 {:b 2}] true) [{:a 1} [{:b 2}]]))) 18 | 19 | (testing "don't expect body" 20 | (is (= (common/extract-parameters [] false) [{} nil])) 21 | (is (= (common/extract-parameters [{:a 1}] false) [{:a 1} nil])) 22 | (is (= (common/extract-parameters [:a 1] false) [{:a 1} nil])) 23 | (is (= (common/extract-parameters [{:a 1} {:b 2}] false) [{:a 1} [{:b 2}]])) 24 | (is (= (common/extract-parameters [:a 1 {:b 2}] false) [{:a 1} [{:b 2}]])))) 25 | 26 | (deftest merge-vector-test 27 | (is (= (common/merge-vector nil) nil)) 28 | (is (= (common/merge-vector [{:a 1}]) {:a 1})) 29 | (is (= (common/merge-vector [{:a 1} {:b 2}]) {:a 1 :b 2}))) 30 | 31 | (deftest fast-merge-map-test 32 | (let [x {:a 1, :b 2, :c 3} 33 | y {:a 2, :d 4, :e 5}] 34 | (is (= (common/fast-map-merge x y) {:a 2, :b 2, :c 3 :d 4, :e 5})) 35 | (is (= (common/fast-map-merge x y) (merge x y))))) 36 | 37 | (comment 38 | (require '[criterium.core :as cc]) 39 | 40 | ;; 163ns 41 | (cc/quick-bench 42 | (common/fast-map-merge 43 | {:a 1, :b 2, :c 3} 44 | {:a 2, :d 4, :e 5})) 45 | 46 | ;; 341ns 47 | (cc/quick-bench 48 | (merge 49 | {:a 1, :b 2, :c 3} 50 | {:a 2, :d 4, :e 5}))) 51 | -------------------------------------------------------------------------------- /src/compojure/api/help.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.help 2 | (:require [schema.core :as s] 3 | [clojure.string :as str])) 4 | 5 | (def Topic (s/maybe s/Keyword)) 6 | (def Subject (s/maybe (s/cond-pre s/Str s/Keyword s/Symbol))) 7 | 8 | ;; 9 | ;; content formatting 10 | ;; 11 | 12 | (defn text [& s] 13 | (->> s 14 | (map #(if (seq? %) (apply text %) %)) 15 | (str/join "\n"))) 16 | 17 | (defn title [& s] 18 | (str "\u001B[32m" (text s) "\u001B[0m")) 19 | 20 | (defn code [& s] 21 | (str "\u001B[33m" (text s) "\u001B[0m")) 22 | 23 | (defmulti help-for (fn [topic subject] [topic subject]) :default ::default) 24 | 25 | (defn- subject-text [topic subject] 26 | (text 27 | "" 28 | (title subject) 29 | "" 30 | (help-for topic subject) 31 | "")) 32 | 33 | (defn- topic-text [topic] 34 | (let [subjects (-> (methods help-for) 35 | (dissoc ::default) 36 | (keys) 37 | (->> (filter #(-> % first (= topic)))))] 38 | (text 39 | "Topic:\n" 40 | (title topic) 41 | "\nSubjects:" 42 | (->> subjects 43 | (map (partial apply subject-text)) 44 | (map (partial str "\n")))))) 45 | 46 | (defn- help-text [] 47 | (let [methods (dissoc (methods help-for) ::default)] 48 | (text 49 | "Usage:" 50 | "" 51 | (code 52 | "(help)" 53 | "(help topic)" 54 | "(help topic subject)") 55 | "\nTopics:\n" 56 | (title (->> methods keys (map first) (distinct) (sort))) 57 | "\nTopics & subjects:\n" 58 | (title (->> methods keys (map (partial str/join " ")) (sort)))))) 59 | 60 | (defmethod help-for ::default [_ _] (help-text)) 61 | 62 | (s/defn ^:always-validate help 63 | ([] 64 | (println "------------------------------------------------------------") 65 | (println (help-text))) 66 | ([topic :- Topic] 67 | (println "------------------------------------------------------------") 68 | (println (topic-text topic))) 69 | ([topic :- Topic, subject :- Subject] 70 | (println "------------------------------------------------------------") 71 | (println (subject-text topic subject)))) 72 | -------------------------------------------------------------------------------- /src/compojure/api/upload.clj: -------------------------------------------------------------------------------- 1 | ;; NOTE: This namespace is generated by compojure.api.dev.gen 2 | (ns compojure.api.upload (:require ring.middleware.multipart-params ring.swagger.upload)) 3 | (def ^{:arglists (quote ([handler] [handler options])), :doc "Middleware to parse multipart parameters from a request. Adds the\n following keys to the request map:\n\n :multipart-params - a map of multipart parameters\n :params - a merged map of all types of parameter\n\n The following options are accepted\n\n :encoding - character encoding to use for multipart parsing.\n Overrides the encoding specified in the request. If not\n specified, uses the encoding specified in a part named\n \"_charset_\", or the content type for each part, or\n request character encoding if the part has no encoding,\n or \"UTF-8\" if no request character encoding is set.\n\n :fallback-encoding - specifies the character encoding used in parsing if a\n part of the request does not specify encoding in its\n content type or no part named \"_charset_\" is present.\n Has no effect if :encoding is also set.\n\n :store - a function that stores a file upload. The function\n should expect a map with :filename, :content-type and\n :stream keys, and its return value will be used as the\n value for the parameter in the multipart parameter map.\n The default storage function is the temp-file-store.\n\n :progress-fn - a function that gets called during uploads. The\n function should expect four parameters: request,\n bytes-read, content-length, and item-count."} wrap-multipart-params ring.middleware.multipart-params/wrap-multipart-params) 4 | (def ^{:doc "Schema for file param created by ring.middleware.multipart-params.temp-file store."} TempFileUpload ring.swagger.upload/TempFileUpload) 5 | (def ^{:doc "Schema for file param created by ring.middleware.multipart-params.byte-array store."} ByteArrayUpload ring.swagger.upload/ByteArrayUpload) 6 | -------------------------------------------------------------------------------- /test19/compojure/api/coercion/spec_coercion_explain_test.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.coercion.spec-coercion-explain-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [clojure.spec.alpha :as s] 4 | [spec-tools.spec :as spec] 5 | [compojure.api.test-utils :refer :all] 6 | [compojure.api.sweet :refer :all] 7 | [compojure.api.request :as request] 8 | [compojure.api.coercion :as coercion])) 9 | 10 | (s/def ::birthdate spec/inst?) 11 | 12 | (s/def ::name string?) 13 | 14 | (s/def ::languages 15 | (s/coll-of 16 | (s/and spec/keyword? #{:clj :cljs}) 17 | :into #{})) 18 | 19 | (s/def ::spec 20 | (s/keys 21 | :req-un [::name ::languages ::age] 22 | :opt-un [::birthdate])) 23 | 24 | (def valid-value {:name "foo" :age "24" :languages ["clj"] :birthdate "1968-01-02T15:04:05Z"}) 25 | (def coerced-value {:name "foo" :age "24" :languages #{:clj} :birthdate #inst "1968-01-02T15:04:05Z"}) 26 | (def invalid-value {:name "foo" :age "24" :lanxguages ["clj"] :birthdate "1968-01-02T15:04:05Z"}) 27 | 28 | (deftest request-coercion-test 29 | (let [c! #(coercion/coerce-request! ::spec :body-params :body false false %)] 30 | 31 | (testing "default coercion" 32 | (is (= (c! {:body-params valid-value 33 | :muuntaja/request {:format "application/json"} 34 | ::request/coercion :spec}) 35 | coerced-value)) 36 | (is (thrown? Exception 37 | (c! {:body-params invalid-value 38 | :muuntaja/request {:format "application/json"} 39 | ::request/coercion :spec}))) 40 | (try 41 | (c! {:body-params invalid-value 42 | :muuntaja/request {:format "application/json"} 43 | ::request/coercion :spec}) 44 | (catch Exception e 45 | (let [data (ex-data e) 46 | spec-problems (get-in data [:problems ::s/problems])] 47 | (is (= (count spec-problems) 1)) 48 | (is (= (select-keys (first spec-problems) 49 | [:in :path :val :via]) 50 | {:in [] 51 | :path [] 52 | :val {:age "24" 53 | :birthdate #inst "1968-01-02T15:04:05Z" 54 | :name "foo"} 55 | :via [::spec]})))))))) 56 | -------------------------------------------------------------------------------- /test19/compojure/api/coercion/issue336_test.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.coercion.issue336-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [compojure.api.test-utils :refer :all] 4 | [ring.util.http-response :refer :all] 5 | [compojure.api.sweet :refer :all] 6 | [clojure.spec.alpha :as s] 7 | [spec-tools.spec :as spec] 8 | [spec-tools.core :as st])) 9 | 10 | (s/def ::customer-id spec/string?) 11 | (s/def ::requestor-id spec/string?) 12 | (s/def ::requestor-email spec/string?) 13 | (s/def ::requestor-name spec/string?) 14 | (s/def ::endpoint spec/string?) 15 | (s/def ::from-year spec/int?) 16 | (s/def ::from-month spec/int?) 17 | (s/def ::to-year spec/int?) 18 | (s/def ::to-month spec/int?) 19 | 20 | (s/def ::input-settings (s/and (s/keys :req-un [::endpoint 21 | ::customer-id 22 | ::requestor-id] 23 | :opt-un [::from-year 24 | ::from-month 25 | ::to-year 26 | ::to-month 27 | ::requestor-email 28 | ::requestor-name]))) 29 | 30 | (def app 31 | (api 32 | {:swagger 33 | {:ui "/" 34 | :spec "/swagger.json" 35 | :data {:info {:title "Futomaki" 36 | :description "API for counter stats over the Sushi protocol"} 37 | :tags [{:name "Reports", :description "Retrieve information per report definition"}]}}} 38 | 39 | (context "/api" [] 40 | :tags ["api"] 41 | :coercion :spec 42 | 43 | (context "/jr1" [] 44 | (resource 45 | {:get 46 | {:summary "Number of successful full-text article requests by month and journal" 47 | :parameters {:query-params ::input-settings} 48 | :response {200 {:schema ::input-settings}} 49 | :handler (fn [{:keys [query-params]}] 50 | (ok query-params))}}))))) 51 | 52 | (deftest coercion-works-with-s-and-test 53 | (let [data {:endpoint "http://sushi.cambridge.org/GetReport" 54 | :customer-id "abc" 55 | :requestor-id "abc"} 56 | [status body] (get* app "/api/jr1" data)] 57 | (is (= status 200)) 58 | (is (= body data)))) 59 | -------------------------------------------------------------------------------- /src/compojure/api/common.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.common 2 | (:require [linked.core :as linked])) 3 | 4 | (defn plain-map? 5 | "checks whether input is a map, but not a record" 6 | [x] (and (map? x) (not (record? x)))) 7 | 8 | (defn extract-parameters 9 | "Extract parameters from head of the list. Parameters can be: 10 | 11 | 1. a map (if followed by any form) `[{:a 1 :b 2} :body]` => `{:a 1 :b 2}` 12 | 2. list of keywords & values `[:a 1 :b 2 :body]` => `{:a 1 :b 2}` 13 | 3. else => `{}` 14 | 15 | Returns a tuple with parameters and body without the parameters" 16 | [c expect-body] 17 | (cond 18 | (and (plain-map? (first c)) (or (not expect-body) (seq (rest c)))) 19 | [(first c) (seq (rest c))] 20 | 21 | (keyword? (first c)) 22 | (let [parameters (->> c 23 | (partition 2) 24 | (take-while (comp keyword? first)) 25 | (mapcat identity) 26 | (apply array-map)) 27 | form (drop (* 2 (count parameters)) c)] 28 | [parameters (seq form)]) 29 | 30 | :else 31 | [{} (seq c)])) 32 | 33 | (defn group-with 34 | "Groups a sequence with predicate returning a tuple of sequences." 35 | [pred coll] 36 | [(seq (filter pred coll)) 37 | (seq (remove pred coll))]) 38 | 39 | (defn merge-vector 40 | "Merges vector elements, optimized for 1 arity (x10 faster than merge)." 41 | [v] 42 | (if (get v 1) 43 | (apply merge v) 44 | (get v 0))) 45 | 46 | (defn fast-map-merge 47 | [x y] 48 | (reduce-kv 49 | (fn [m k v] 50 | (assoc m k v)) 51 | x 52 | y)) 53 | 54 | (defn fifo-memoize [f size] 55 | "Returns a memoized version of a referentially transparent f. The 56 | memoized version of the function keeps a cache of the mapping from arguments 57 | to results and, when calls with the same arguments are repeated often, has 58 | higher performance at the expense of higher memory use. FIFO with size entries." 59 | (let [cache (atom (linked/map))] 60 | (fn [& xs] 61 | (or (@cache xs) 62 | (let [value (apply f xs)] 63 | (swap! cache (fn [mem] 64 | (let [mem (assoc mem xs value)] 65 | (if (>= (count mem) size) 66 | (dissoc mem (-> mem first first)) 67 | mem)))) 68 | value))))) 69 | 70 | ;; NB: when-ns eats all exceptions inside the body, including those about 71 | ;; unresolvable symbols. Keep this in mind when debugging the definitions below. 72 | 73 | (defmacro when-ns [ns & body] 74 | `(try 75 | (eval 76 | '(do 77 | (require ~ns) 78 | ~@body)) 79 | (catch Exception ~'_))) 80 | 81 | -------------------------------------------------------------------------------- /examples/thingie/src/examples/pizza.clj: -------------------------------------------------------------------------------- 1 | (ns examples.pizza 2 | (:require [schema.core :as s] 3 | [compojure.api.sweet :refer :all] 4 | [ring.util.http-response :refer :all] 5 | [ring.swagger.schema :as rs])) 6 | 7 | ;; 8 | ;; Pizza Store 9 | ;; 10 | 11 | (s/defschema Topping {:type (s/enum :cheese :olives :ham :pepperoni :habanero) 12 | :qty Long}) 13 | 14 | (s/defschema Pizza {:id Long 15 | :name String 16 | :price Double 17 | :hot Boolean 18 | (s/optional-key :description) String 19 | :toppings (describe [Topping] "List of toppings of the Pizza")}) 20 | 21 | (s/defschema NewPizza (dissoc Pizza :id)) 22 | 23 | ;; Repository 24 | 25 | (defonce id-seq (atom 0)) 26 | (defonce pizzas (atom (array-map))) 27 | 28 | (defn get-pizza [id] (@pizzas id)) 29 | (defn get-pizzas [] (-> pizzas deref vals reverse)) 30 | (defn delete! [id] (swap! pizzas dissoc id) nil) 31 | 32 | (defn add! [new-pizza] 33 | (let [id (swap! id-seq inc) 34 | pizza (rs/coerce! Pizza (assoc new-pizza :id id))] 35 | (swap! pizzas assoc id pizza) 36 | pizza)) 37 | 38 | (defn update! [pizza] 39 | (let [pizza (rs/coerce! Pizza pizza)] 40 | (swap! pizzas assoc (:id pizza) pizza) 41 | (get-pizza (:id pizza)))) 42 | 43 | ;; Data 44 | 45 | (when (empty? @pizzas) 46 | (add! {:name "Frutti" :price 9.50 :hot false :toppings [{:type :cheese :qty 2} 47 | {:type :olives :qty 1}]}) 48 | (add! {:name "Il Diablo" :price 12 :hot true :toppings [{:type :ham :qty 3} 49 | {:type :habanero :qty 1}]})) 50 | 51 | ;; Routes 52 | 53 | (def pizza-routes 54 | (routes 55 | (context "/api" [] 56 | :tags ["pizzas"] 57 | (context "/pizzas" [] 58 | (GET "/" [] 59 | :return [Pizza] 60 | :summary "Gets all Pizzas" 61 | (ok (get-pizzas))) 62 | (GET "/:id" [] 63 | :path-params [id :- Long] 64 | :return (s/maybe Pizza) 65 | :summary "Gets a pizza" 66 | (ok (get-pizza id))) 67 | (POST "/" [] 68 | :return Pizza 69 | :body [pizza (describe NewPizza "new pizza")] 70 | :summary "Adds a pizza" 71 | (ok (add! pizza))) 72 | (PUT "/" [] 73 | :return Pizza 74 | :body [pizza Pizza] 75 | :summary "Updates a pizza" 76 | (ok (update! pizza))) 77 | (DELETE "/:id" [] 78 | :path-params [id :- Long] 79 | :summary "Deletes a Pizza" 80 | (ok (delete! id))))) 81 | 82 | (context "/foreign" [] 83 | :tags ["foreign"] 84 | (GET "/bar/:foo" [] 85 | :path-params [foo :- s/Str] 86 | (ok {:bar foo})) 87 | (GET "/info" [] 88 | :summary "from examples.pizza ns" 89 | (ok {:source "examples.pizza"}))))) 90 | -------------------------------------------------------------------------------- /src/compojure/api/coerce.clj: -------------------------------------------------------------------------------- 1 | ;; 1.1.x 2 | (ns compojure.api.coerce 3 | (:require [schema.coerce :as sc] 4 | [compojure.api.middleware :as mw] 5 | [compojure.api.exception :as ex] 6 | [clojure.walk :as walk] 7 | [schema.utils :as su] 8 | [linked.core :as linked])) 9 | 10 | (defn memoized-coercer 11 | "Returns a memoized version of a referentially transparent coercer fn. The 12 | memoized version of the function keeps a cache of the mapping from arguments 13 | to results and, when calls with the same arguments are repeated often, has 14 | higher performance at the expense of higher memory use. FIFO with 10000 entries. 15 | Cache will be filled if anonymous coercers are used (does not match the cache)" 16 | [] 17 | (let [cache (atom (linked/map)) 18 | cache-size 10000] 19 | (fn [& args] 20 | (or (@cache args) 21 | (let [coercer (apply sc/coercer args)] 22 | (swap! cache (fn [mem] 23 | (let [mem (assoc mem args coercer)] 24 | (if (>= (count mem) cache-size) 25 | (dissoc mem (-> mem first first)) 26 | mem)))) 27 | coercer))))) 28 | 29 | (defn cached-coercer [request] 30 | (or (-> request mw/get-options :coercer) sc/coercer)) 31 | 32 | (defn coerce-response! [request {:keys [status] :as response} responses] 33 | (-> (when-let [schema (or (:schema (get responses status)) 34 | (:schema (get responses :default)))] 35 | (when-let [matchers (mw/coercion-matchers request)] 36 | (when-let [matcher (matchers :response)] 37 | (let [coercer (cached-coercer request) 38 | coerce (coercer schema matcher) 39 | body (coerce (:body response))] 40 | (if (su/error? body) 41 | (throw (ex-info 42 | (str "Response validation failed: " (su/error-val body)) 43 | (assoc body :type ::ex/response-validation 44 | :response response))) 45 | (assoc response 46 | :compojure.api.meta/serializable? true 47 | :body body)))))) 48 | (or response))) 49 | 50 | (defn body-coercer-middleware [handler responses] 51 | (fn [request] 52 | (coerce-response! request (handler request) responses))) 53 | 54 | (defn coerce! [schema key type request] 55 | (let [value (walk/keywordize-keys (key request))] 56 | (if-let [matchers (mw/coercion-matchers request)] 57 | (if-let [matcher (matchers type)] 58 | (let [coercer (cached-coercer request) 59 | coerce (coercer schema matcher) 60 | result (coerce value)] 61 | (if (su/error? result) 62 | (throw (ex-info 63 | (str "Request validation failed: " (su/error-val result)) 64 | (assoc result :type ::ex/request-validation))) 65 | result)) 66 | value) 67 | value))) 68 | -------------------------------------------------------------------------------- /examples/resources/src/example/handler.clj: -------------------------------------------------------------------------------- 1 | (ns example.handler 2 | (:require [compojure.api.sweet :refer :all] 3 | [ring.util.http-response :refer :all] 4 | [ring.util.http-status :as http-status] 5 | [schema.core :as s])) 6 | 7 | ;; 8 | ;; Schemas 9 | ;; 10 | 11 | (s/defschema Pizza 12 | {:id s/Int 13 | :name s/Str 14 | (s/optional-key :description) s/Str 15 | :size (s/enum :L :M :S) 16 | :origin {:country (s/enum :FI :PO) 17 | :city s/Str}}) 18 | 19 | (s/defschema NewPizza (dissoc Pizza :id)) 20 | (s/defschema UpdatedPizza NewPizza) 21 | 22 | ;; 23 | ;; Database 24 | ;; 25 | 26 | (def pizzas (atom {})) 27 | 28 | (let [ids (atom 0)] 29 | (defn update-pizza! [maybe-id maybe-pizza] 30 | (let [id (or maybe-id (swap! ids inc))] 31 | (if maybe-pizza 32 | (swap! pizzas assoc id (assoc maybe-pizza :id id)) 33 | (swap! pizzas dissoc id)) 34 | (@pizzas id)))) 35 | 36 | ;; 37 | ;; Application 38 | ;; 39 | 40 | (def app 41 | (api 42 | {:swagger 43 | {:ui "/" 44 | :spec "/swagger.json" 45 | :data {:info {:title "Resource sample" 46 | :description "Example app using `compojure.api.resource`." 47 | :contact {:url "https://github.com/metosin/compojure-api/examples/resources"}} 48 | :tags [{:name "pizza", :description "pizzas"}]}}} 49 | 50 | (context "/pizza/" [] 51 | (resource 52 | {:tags ["pizza"] 53 | :get {:summary "get pizzas" 54 | :description "get all pizzas!" 55 | :responses {http-status/ok {:schema [Pizza]}} 56 | :handler (fn [_] (ok (vals @pizzas)))} 57 | :post {:summary "add's a pizza" 58 | :parameters {:body-params NewPizza} 59 | :responses {http-status/created {:schema Pizza 60 | :description "the created pizza" 61 | :headers {"Location" s/Str}}} 62 | :handler (fn [{body :body-params}] 63 | (let [{:keys [id] :as pizza} (update-pizza! nil body)] 64 | (created (path-for ::pizza {:id id}) pizza)))}})) 65 | 66 | (context "/pizza/:id" [] 67 | :path-params [id :- s/Int] 68 | 69 | (resource 70 | {:tags ["pizza"] 71 | :get {:x-name ::pizza 72 | :summary "gets a pizza" 73 | :responses {http-status/ok {:schema Pizza}} 74 | :handler (fn [_] 75 | (if-let [pizza (@pizzas id)] 76 | (ok pizza) 77 | (not-found)))} 78 | :put {:summary "updates a pizza" 79 | :parameters {:body-params UpdatedPizza} 80 | :responses {http-status/ok {:schema Pizza}} 81 | :handler (fn [{body :body-params}] 82 | (if (@pizzas id) 83 | (ok (update-pizza! id body)) 84 | (not-found)))} 85 | :delete {:summary "deletes a pizza" 86 | :handler (fn [_] 87 | (update-pizza! id nil) 88 | (no-content))}})))) 89 | -------------------------------------------------------------------------------- /examples/reusable-resources/src/example/entity.clj: -------------------------------------------------------------------------------- 1 | (ns example.entity 2 | (:require [compojure.api.sweet :as sweet] 3 | [ring.util.http-response :refer :all] 4 | [ring.util.http-status :as http-status] 5 | [clojure.string :as str] 6 | [schema.core :as s])) 7 | 8 | ;; 9 | ;; Database 10 | ;; 11 | 12 | (defn update! [db name maybe-id maybe-entity] 13 | (let [id (or maybe-id (::ids (swap! db update ::ids (fnil inc 0))))] 14 | (if maybe-entity 15 | (swap! db update name assoc id (assoc maybe-entity :id id)) 16 | (swap! db update name dissoc id)) 17 | (get-in @db [name id]))) 18 | 19 | ;; 20 | ;; Routes 21 | ;; 22 | 23 | (defn resource 24 | ([db Schema] 25 | (resource db Schema nil)) 26 | ([db Schema tag] 27 | (let [entity (str/lower-case (s/schema-name Schema)) 28 | tag (or tag entity) 29 | update! (partial update! db entity) 30 | qualified-name (keyword (-> Schema meta :ns str) (-> Schema meta :name (str tag))) 31 | NewSchema (s/schema-with-name (dissoc Schema :id) (str "New" (s/schema-name Schema))) 32 | UpdatedSchema (s/schema-with-name NewSchema (str "Updated" (s/schema-name Schema)))] 33 | 34 | (sweet/routes 35 | (sweet/context (format "/%s/" entity) [] 36 | (sweet/resource 37 | {:tags [tag] 38 | :get {:summary (format "get %ss" entity) 39 | :description (format "get all %ss!" entity) 40 | :responses {http-status/ok {:schema [Schema]}} 41 | :handler (fn [_] (ok (-> @db (get entity) vals)))} 42 | :post {:summary "add's a pizza" 43 | :parameters {:body-params NewSchema} 44 | :responses {http-status/created {:schema Schema 45 | :description (format "the created %s" entity) 46 | :headers {"Location" s/Str}}} 47 | :handler (fn [{body :body-params}] 48 | (let [{:keys [id] :as entity} (update! nil body)] 49 | (created (sweet/path-for qualified-name {:id id}) entity)))}})) 50 | 51 | (sweet/context (format "/%s/:id" entity) [] 52 | :path-params [id :- s/Int] 53 | 54 | (sweet/resource 55 | {:tags [tag] 56 | :get {:x-name qualified-name 57 | :summary (format "gets a %s" entity) 58 | :responses {http-status/ok {:schema Schema}} 59 | :handler (fn [_] 60 | (if-let [entity (get-in @db [entity id])] 61 | (ok entity) 62 | (not-found)))} 63 | :put {:summary (str "updates a %s" entity) 64 | :parameters {:body-params UpdatedSchema} 65 | :responses {http-status/ok {:schema Schema}} 66 | :handler (fn [{body :body-params}] 67 | (if (get-in @db [entity id]) 68 | (ok (update! id body)) 69 | (not-found)))} 70 | :delete {:summary (str "deletes a %s" entity) 71 | :handler (fn [_] 72 | (update! id nil) 73 | (no-content))}})))))) 74 | -------------------------------------------------------------------------------- /dependabot/dependency-tree.txt: -------------------------------------------------------------------------------- 1 | metosin:compojure-api:jar:2.0.0-alpha34-SNAPSHOT 2 | +- prismatic:schema:jar:1.1.12:compile 3 | +- prismatic:plumbing:jar:0.5.5:compile 4 | | \- de.kotka:lazymap:jar:3.1.0:compile 5 | +- ikitommi:linked:jar:1.3.1-alpha1:compile 6 | +- metosin:muuntaja:jar:0.6.6:compile 7 | | +- metosin:jsonista:jar:0.2.5:compile 8 | | | \- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.10.0:compile 9 | | \- com.cognitect:transit-clj:jar:0.8.319:compile 10 | | \- com.cognitect:transit-java:jar:0.8.337:compile 11 | | +- org.msgpack:msgpack:jar:0.6.12:compile 12 | | | +- com.googlecode.json-simple:json-simple:jar:1.1.1:compile 13 | | | \- org.javassist:javassist:jar:3.18.1-GA:compile 14 | | \- javax.xml.bind:jaxb-api:jar:2.3.0:compile 15 | +- com.fasterxml.jackson.datatype:jackson-datatype-joda:jar:2.10.1:compile 16 | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.10.1:compile 17 | | +- com.fasterxml.jackson.core:jackson-core:jar:2.10.1:compile 18 | | \- com.fasterxml.jackson.core:jackson-databind:jar:2.10.1:compile 19 | +- ring:ring-core:jar:1.8.0:compile 20 | | +- ring:ring-codec:jar:1.1.2:compile 21 | | | \- commons-codec:commons-codec:jar:1.11:compile 22 | | +- commons-io:commons-io:jar:2.6:compile 23 | | +- commons-fileupload:commons-fileupload:jar:1.4:compile 24 | | +- crypto-random:crypto-random:jar:1.2.0:compile 25 | | \- crypto-equality:crypto-equality:jar:1.0.0:compile 26 | +- compojure:compojure:jar:1.6.1:compile 27 | | +- org.clojure:tools.macro:jar:0.1.5:compile 28 | | +- clout:clout:jar:2.2.1:compile 29 | | | \- instaparse:instaparse:jar:1.4.8:compile 30 | | \- medley:medley:jar:1.0.0:compile 31 | +- metosin:spec-tools:jar:0.10.6:compile 32 | | \- org.clojure:spec.alpha:jar:0.3.218:compile 33 | +- metosin:ring-http-response:jar:0.9.1:compile 34 | | \- potemkin:potemkin:jar:0.4.5:compile 35 | | \- clj-tuple:clj-tuple:jar:0.2.2:compile 36 | +- metosin:ring-swagger-ui:jar:3.24.3:compile 37 | +- metosin:ring-swagger:jar:1.0.0:compile 38 | | +- cheshire:cheshire:jar:5.8.1:compile 39 | | | +- com.fasterxml.jackson.dataformat:jackson-dataformat-smile:jar:2.9.6:compile 40 | | | +- com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:jar:2.9.6:compile 41 | | | \- tigris:tigris:jar:0.1.1:compile 42 | | +- metosin:schema-tools:jar:0.11.0:compile 43 | | +- metosin:scjsv:jar:0.5.0:compile 44 | | | \- com.github.java-json-tools:json-schema-validator:jar:2.2.10:compile 45 | | | +- com.github.java-json-tools:json-schema-core:jar:1.2.10:compile 46 | | | | +- com.github.java-json-tools:jackson-coreutils:jar:1.9:compile 47 | | | | | +- com.google.guava:guava:jar:16.0.1:compile 48 | | | | | \- com.github.fge:msg-simple:jar:1.1:compile 49 | | | | | \- com.github.fge:btf:jar:1.2:compile 50 | | | | +- com.github.fge:uri-template:jar:0.9:compile 51 | | | | \- org.mozilla:rhino:jar:1.7.7.1:compile 52 | | | +- javax.mail:mailapi:jar:1.4.3:compile 53 | | | | \- javax.activation:activation:jar:1.1:compile 54 | | | +- com.googlecode.libphonenumber:libphonenumber:jar:8.0.0:compile 55 | | | +- com.google.code.findbugs:jsr305:jar:3.0.1:compile 56 | | | \- net.sf.jopt-simple:jopt-simple:jar:5.0.3:compile 57 | | \- org.tobereplaced:lettercase:jar:1.0.0:compile 58 | +- clj-time:clj-time:jar:0.15.2:compile 59 | +- joda-time:joda-time:jar:2.10.5:compile 60 | \- riddley:riddley:jar:0.2.0:compile 61 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run tests 3 | 4 | on: 5 | push: 6 | branches: [master, "1.1.x"] 7 | pull_request: 8 | branches: [master, "1.1.x"] 9 | 10 | env: 11 | ACTIONS_CACHE_VERSION: 0 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | matrix: 17 | jdk: [8, 11, 17, 21, 22] 18 | 19 | name: Java ${{ matrix.jdk }} 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Setup Java ${{ matrix.jdk }} 26 | uses: actions/setup-java@v3.11.0 27 | with: 28 | distribution: temurin 29 | java-version: ${{ matrix.jdk }} 30 | - name: Maven Cache 31 | id: maven-cache 32 | uses: actions/cache@v4 33 | with: 34 | path: | 35 | ~/.m2/repository 36 | ~/.gitlibs 37 | key: m2-cache-${{ env.ACTIONS_CACHE_VERSION }}-${{ hashFiles('project.clj') }}-${{ matrix.jdk }} 38 | restore-keys: | 39 | m2-cache-${{ env.ACTIONS_CACHE_VERSION }}-${{ hashFiles('project.clj') }}- 40 | m2-cache-${{ env.ACTIONS_CACHE_VERSION }}- 41 | - name: Setup Clojure 42 | uses: DeLaGuardo/setup-clojure@master 43 | with: 44 | lein: latest 45 | - name: Setup Babashka 46 | run: bash < <(curl -s https://raw.githubusercontent.com/babashka/babashka/master/install) 47 | - name: Check dependabot is in sync with project.clj 48 | run: ./scripts/check-dependabot 49 | - name: Run tests 50 | run: lein do clean, all test, all check 51 | deploy: 52 | concurrency: deploy 53 | needs: test 54 | if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/1.1.x') }} 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v2 59 | - name: Maven Cache 60 | id: maven-cache 61 | uses: actions/cache@v3 62 | with: 63 | path: | 64 | ~/.m2/repository 65 | ~/.gitlibs 66 | key: m2-cache-${{ env.ACTIONS_CACHE_VERSION }}-${{ hashFiles('project.clj') }}-${{ matrix.jdk }} 67 | restore-keys: | 68 | m2-cache-${{ env.ACTIONS_CACHE_VERSION }}-${{ hashFiles('project.clj') }}- 69 | m2-cache-${{ env.ACTIONS_CACHE_VERSION }}- 70 | - name: Prepare java 71 | uses: actions/setup-java@v4 72 | with: 73 | distribution: 'adopt' 74 | java-version: '11' 75 | - name: deploy 76 | env: 77 | CLOJARS_USER: metosinci 78 | CLOJARS_TOKEN: "${{ secrets.CLOJARS_DEPLOY_TOKEN }}" 79 | COMMIT_MSG: ${{ github.event.head_commit.message }} 80 | run: | 81 | git config --global user.email "abonnairesergeant@gmail.com" 82 | git config --global user.name "Ambrose Bonnaire-Sergeant" 83 | 84 | if [[ "$COMMIT_MSG" == "Release :major" ]]; then 85 | lein release :major 86 | elif [[ "$COMMIT_MSG" == "Release :minor" ]]; then 87 | lein release :minor 88 | elif [[ "$COMMIT_MSG" == "Release :patch" ]]; then 89 | lein release :patch 90 | elif [[ "$COMMIT_MSG" == "Release :alpha" ]]; then 91 | lein release :alpha 92 | elif [[ "$COMMIT_MSG" == "Release :beta" ]]; then 93 | lein release :beta 94 | elif [[ "$COMMIT_MSG" == "Release :rc" ]]; then 95 | lein release :rc 96 | else 97 | lein deploy snapshot 98 | fi 99 | -------------------------------------------------------------------------------- /src/compojure/api/exception.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.exception 2 | (:require [ring.util.http-response :as response] 3 | [clojure.walk :as walk] 4 | [compojure.api.impl.logging :as logging] 5 | [compojure.api.coercion.core :as cc] 6 | [compojure.api.coercion.schema])) 7 | 8 | ;; 9 | ;; Default exception handlers 10 | ;; 11 | 12 | (defn safe-handler 13 | "Writes :error to log with the exception message & stacktrace. 14 | 15 | Error response only contains class of the Exception so that it won't accidentally 16 | expose secret details." 17 | [^Exception e data req] 18 | (logging/log! :error e (.getMessage e)) 19 | (response/internal-server-error {:type "unknown-exception" 20 | :class (.getName (.getClass e))})) 21 | 22 | ;; TODO: coercion should handle how to publish data 23 | (defn response-validation-handler 24 | "Creates error response based on a response error. The following keys are available: 25 | 26 | :type type of the exception (::response-validation) 27 | :coercion coercion instance used 28 | :in location of the value ([:response :body]) 29 | :schema schema to be validated against 30 | :error schema error 31 | :request raw request 32 | :response raw response" 33 | [e data req] 34 | (response/internal-server-error 35 | (-> data 36 | (dissoc :request :response) 37 | (update :coercion cc/get-name) 38 | (assoc :value (-> data :response :body)) 39 | (->> (cc/encode-error (:coercion data)))))) 40 | 41 | ;; TODO: coercion should handle how to publish data 42 | (defn request-validation-handler 43 | "Creates error response based on Schema error. The following keys are available: 44 | 45 | :type type of the exception (::request-validation) 46 | :coercion coercion instance used 47 | :value value that was validated 48 | :in location of the value (e.g. [:request :query-params]) 49 | :schema schema to be validated against 50 | :error schema error 51 | :request raw request" 52 | [e data req] 53 | (response/bad-request 54 | (-> data 55 | (dissoc :request) 56 | (update :coercion cc/get-name) 57 | (->> (cc/encode-error (:coercion data)))))) 58 | 59 | (defn http-response-handler 60 | "reads response from ex-data :response" 61 | [_ {:keys [response]} _] 62 | response) 63 | 64 | (defn schema-error-handler 65 | "Creates error response based on Schema error." 66 | [e data req] 67 | (response/bad-request 68 | {:errors (compojure.api.coercion.schema/stringify (:error data))})) 69 | 70 | (defn request-parsing-handler 71 | [^Exception ex data req] 72 | (let [cause (.getCause ex) 73 | original (.getCause cause)] 74 | (response/bad-request 75 | (merge (select-keys data [:type :format :charset]) 76 | (if original {:original (.getMessage original)}) 77 | {:message (.getMessage cause)})))) 78 | ;; 79 | ;; Logging 80 | ;; 81 | 82 | (defn with-logging 83 | "Wrap compojure-api exception-handler a function which will log the 84 | exception message and stack-trace with given log-level." 85 | ([handler] (with-logging handler :error)) 86 | ([handler log-level] 87 | {:pre [(#{:trace :debug :info :warn :error :fatal} log-level)]} 88 | (fn [^Exception e data req] 89 | (logging/log! log-level e (.getMessage e)) 90 | (handler e data req)))) 91 | 92 | ;; 93 | ;; Mappings from other Exception types to our base types 94 | ;; 95 | 96 | (def mapped-exception-types 97 | {:ring.swagger.schema/validation ::request-validation 98 | :muuntaja/decode ::request-parsing}) 99 | -------------------------------------------------------------------------------- /src/compojure/api/core.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.core 2 | (:require [compojure.api.meta :as meta] 3 | [compojure.api.async] 4 | [compojure.api.routes :as routes] 5 | [compojure.api.middleware :as mw] 6 | [compojure.core :as compojure] 7 | [clojure.tools.macro :as macro])) 8 | 9 | (defn ring-handler 10 | "Creates vanilla ring-handler from any invokable thing (e.g. compojure-api route)" 11 | [handler] 12 | (fn 13 | ([request] (handler request)) 14 | ([request respond raise] (handler request respond raise)))) 15 | 16 | (defn routes 17 | "Create a Ring handler by combining several handlers into one." 18 | [& handlers] 19 | (let [handlers (seq (keep identity (flatten handlers)))] 20 | (routes/map->Route 21 | {:childs (vec handlers) 22 | :handler (meta/routing handlers)}))) 23 | 24 | (defmacro defroutes 25 | "Define a Ring handler function from a sequence of routes. 26 | The name may optionally be followed by a doc-string and metadata map." 27 | {:style/indent 1} 28 | [name & routes] 29 | (let [[name routes] (macro/name-with-attributes name routes)] 30 | `(def ~name (routes ~@routes)))) 31 | 32 | (defmacro let-routes 33 | "Takes a vector of bindings and a body of routes. 34 | 35 | Equivalent to: `(let [...] (routes ...))`" 36 | {:style/indent 1} 37 | [bindings & body] 38 | `(let ~bindings (routes ~@body))) 39 | 40 | (defn undocumented 41 | "Routes without route-documentation. Can be used to wrap routes, 42 | not satisfying compojure.api.routes/Routing -protocol." 43 | [& handlers] 44 | (let [handlers (keep identity handlers)] 45 | (routes/map->Route {:handler (meta/routing handlers)}))) 46 | 47 | (defmacro middleware 48 | "Wraps routes with given middlewares using thread-first macro. 49 | 50 | Note that middlewares will be executed even if routes in body 51 | do not match the request uri. Be careful with middleware that 52 | has side-effects." 53 | {:style/indent 1 54 | :deprecated "1.1.14" 55 | :superseded-by "route-middleware"} 56 | [middleware & body] 57 | (assert (= "true" (System/getProperty "compojure.api.core.allow-dangerous-middleware")) 58 | (str "compojure.api.core.middleware is deprecated because of security issues. " 59 | "Please use route-middleware instead. " 60 | "Set compojure.api.core.allow-dangerous-middleware=true to keep using middleware.")) 61 | `(let [body# (routes ~@body) 62 | wrap-mw# (mw/compose-middleware ~middleware)] 63 | (routes/create nil nil {} [body#] (wrap-mw# body#)))) 64 | 65 | (defn route-middleware 66 | "Wraps routes with given middlewares using thread-first macro." 67 | {:style/indent 1 68 | :supercedes "middleware"} 69 | [middleware & body] 70 | (let [handler (apply routes body) 71 | x-handler (compojure/wrap-routes handler (mw/compose-middleware middleware))] 72 | ;; use original handler for docs and wrapped handler for implementation 73 | (routes/map->Route 74 | {:childs [handler] 75 | :handler x-handler}))) 76 | 77 | (defmacro context {:style/indent 2} [& args] (meta/restructure nil args {:context? true :&form &form :&env &env})) 78 | 79 | (defmacro GET {:style/indent 2} [& args] (meta/restructure :get args nil)) 80 | (defmacro ANY {:style/indent 2} [& args] (meta/restructure nil args nil)) 81 | (defmacro HEAD {:style/indent 2} [& args] (meta/restructure :head args nil)) 82 | (defmacro PATCH {:style/indent 2} [& args] (meta/restructure :patch args nil)) 83 | (defmacro DELETE {:style/indent 2} [& args] (meta/restructure :delete args nil)) 84 | (defmacro OPTIONS {:style/indent 2} [& args] (meta/restructure :options args nil)) 85 | (defmacro POST {:style/indent 2} [& args] (meta/restructure :post args nil)) 86 | (defmacro PUT {:style/indent 2} [& args] (meta/restructure :put args nil)) 87 | -------------------------------------------------------------------------------- /src/compojure/api/coercion/schema.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.coercion.schema 2 | (:require [schema.coerce :as sc] 3 | [schema.utils :as su] 4 | [ring.swagger.coerce :as coerce] 5 | [compojure.api.coercion.core :as cc] 6 | [clojure.walk :as walk] 7 | [schema.core :as s] 8 | [compojure.api.common :as common]) 9 | (:import (java.io File) 10 | (schema.core OptionalKey RequiredKey) 11 | (schema.utils ValidationError NamedError))) 12 | 13 | (def string-coercion-matcher coerce/query-schema-coercion-matcher) 14 | (def json-coercion-matcher coerce/json-schema-coercion-matcher) 15 | 16 | (defn stringify 17 | "Stringifies Schema records recursively." 18 | [error] 19 | (walk/prewalk 20 | (fn [x] 21 | (cond 22 | (class? x) (.getName ^Class x) 23 | (instance? OptionalKey x) (pr-str (list 'opt (:k x))) 24 | (instance? RequiredKey x) (pr-str (list 'req (:k x))) 25 | (and (satisfies? s/Schema x) (record? x)) (try (pr-str (s/explain x)) (catch Exception _ x)) 26 | (instance? ValidationError x) (str (su/validation-error-explain x)) 27 | (instance? NamedError x) (str (su/named-error-explain x)) 28 | :else x)) 29 | error)) 30 | 31 | (def memoized-coercer 32 | (common/fifo-memoize sc/coercer 1000)) 33 | 34 | ;; don't use coercion for certain types 35 | (defmulti coerce-response? #(if (or (class? %) 36 | (keyword? %)) 37 | % 38 | ::default) 39 | :default ::default) 40 | (defmethod coerce-response? ::default [_] true) 41 | (defmethod coerce-response? File [_] false) 42 | 43 | (defrecord SchemaCoercion [name options] 44 | cc/Coercion 45 | (get-name [_] name) 46 | 47 | (get-apidocs [_ _ data] data) 48 | 49 | (make-open [_ schema] 50 | (if (map? schema) 51 | (assoc schema s/Keyword s/Any) 52 | schema)) 53 | 54 | (encode-error [_ error] 55 | (-> error 56 | (update :schema pr-str) 57 | (update :errors stringify))) 58 | 59 | (coerce-request [_ schema value type format request] 60 | (let [legacy? (fn? options) 61 | type-options (if legacy? 62 | (when-let [provider (options request)] 63 | (provider type)) 64 | (options type))] 65 | (if-let [matcher (if legacy? 66 | type-options 67 | (or (get (get type-options :formats) format) 68 | (get type-options :default)))] 69 | (let [coerce (memoized-coercer schema matcher) 70 | coerced (coerce value)] 71 | (if (su/error? coerced) 72 | (let [errors (su/error-val coerced)] 73 | (cc/map->CoercionError 74 | {:schema schema 75 | :errors errors})) 76 | coerced)) 77 | value))) 78 | 79 | (accept-response? [_ model] 80 | (coerce-response? model)) 81 | 82 | (coerce-response [this schema value type format request] 83 | (cc/coerce-request this schema value type format request))) 84 | 85 | (def default-options 86 | {:body {:default (constantly nil) 87 | :formats {"application/json" json-coercion-matcher 88 | "application/msgpack" json-coercion-matcher 89 | "application/x-yaml" json-coercion-matcher}} 90 | :string {:default string-coercion-matcher} 91 | :response {:default (constantly nil)}}) 92 | 93 | (defn create-coercion [options] 94 | {:pre [(or (map? options) 95 | (fn? options))]} 96 | (->SchemaCoercion :schema options)) 97 | 98 | (def default-coercion (create-coercion default-options)) 99 | 100 | (defmethod cc/named-coercion :schema [_] default-coercion) 101 | -------------------------------------------------------------------------------- /src/compojure/api/coercion.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.coercion 2 | (:require [clojure.walk :as walk] 3 | [compojure.api.exception :as ex] 4 | [compojure.api.request :as request] 5 | [compojure.api.coercion.core :as cc] 6 | ;; side effects 7 | [compojure.api.coercion.schema :as cschema] 8 | compojure.api.coercion.spec) 9 | (:import (compojure.api.coercion.core CoercionError))) 10 | 11 | (def default-coercion :schema) 12 | 13 | (defn set-request-coercion [request coercion] 14 | (assoc request ::request/coercion coercion)) 15 | 16 | (defn get-request-coercion [request] 17 | (if-let [entry (find request ::request/coercion)] 18 | (val entry) 19 | default-coercion)) 20 | 21 | (defn resolve-coercion [coercion] 22 | (cond 23 | (nil? coercion) nil 24 | (keyword? coercion) (cc/named-coercion coercion) 25 | (satisfies? cc/Coercion coercion) coercion 26 | (fn? coercion) (cschema/create-coercion coercion) 27 | :else (throw (ex-info (str "invalid coercion " coercion) {:coercion coercion})))) 28 | 29 | (defn get-apidocs [maybe-coercion spec info] 30 | (if-let [coercion (resolve-coercion maybe-coercion)] 31 | (cc/get-apidocs coercion spec info))) 32 | 33 | (defn coerce-request! [model in type keywordize? open? request] 34 | (let [transform (if keywordize? walk/keywordize-keys identity) 35 | value (transform (in request))] 36 | (if-let [coercion (-> request 37 | (get-request-coercion) 38 | (resolve-coercion))] 39 | (let [model (if open? (cc/make-open coercion model) model) 40 | format (some-> request :muuntaja/request :format) 41 | result (cc/coerce-request coercion model value type format request)] 42 | (if (instance? CoercionError result) 43 | (throw (ex-info 44 | (str "Request validation failed: " (pr-str result)) 45 | (merge 46 | (into {} result) 47 | {:type ::ex/request-validation 48 | :coercion coercion 49 | :value value 50 | :in [:request in] 51 | :request request}))) 52 | result)) 53 | value))) 54 | 55 | (defn coerce-response! [request {:keys [status body] :as response} responses] 56 | (if-let [model (or (:schema (get responses status)) 57 | (:schema (get responses :default)))] 58 | (if-let [coercion (-> request 59 | (get-request-coercion) 60 | (resolve-coercion))] 61 | (let [format (or (-> response :muuntaja/content-type) 62 | (some-> request :muuntaja/response :format)) 63 | accept? (cc/accept-response? coercion model)] 64 | (if accept? 65 | (let [result (cc/coerce-response coercion model body :response format response)] 66 | (if (instance? CoercionError result) 67 | (throw (ex-info 68 | (str "Response validation failed: " (pr-str result)) 69 | (merge 70 | (into {} result) 71 | {:type ::ex/response-validation 72 | :coercion coercion 73 | :value body 74 | :in [:response :body] 75 | :request request 76 | :response response}))) 77 | (assoc response 78 | :compojure.api.meta/serializable? true 79 | :body result))) 80 | response)) 81 | response) 82 | response)) 83 | 84 | ;; 85 | ;; middleware 86 | ;; 87 | 88 | (defn wrap-coerce-response [handler responses] 89 | (fn 90 | ([request] 91 | (coerce-response! request (handler request) responses)) 92 | ([request respond raise] 93 | (handler 94 | request 95 | (fn [response] 96 | (try 97 | (respond (coerce-response! request response responses)) 98 | (catch Exception e 99 | (raise e)))) 100 | raise)))) 101 | -------------------------------------------------------------------------------- /src/compojure/api/swagger.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.swagger 2 | (:require [compojure.api.core :as c] 3 | [compojure.api.middleware :as mw] 4 | [compojure.api.request :as request] 5 | [ring.util.http-response :refer [ok]] 6 | [ring.swagger.common :as rsc] 7 | [ring.swagger.middleware :as rsm] 8 | [ring.swagger.core :as swagger] 9 | [ring.swagger.swagger-ui :as swagger-ui] 10 | [ring.swagger.swagger2 :as swagger2] 11 | [compojure.api.routes :as routes] 12 | [spec-tools.swagger.core])) 13 | 14 | (defn base-path [request] 15 | (let [context (swagger/context request)] 16 | (if (= "" context) "/" context))) 17 | 18 | (defn swagger-spec-path 19 | [app] 20 | (some-> app 21 | routes/get-routes 22 | routes/route-lookup-table 23 | ::swagger 24 | keys 25 | first)) 26 | 27 | (defn transform-operations [swagger] 28 | (swagger2/transform-operations routes/non-nil-routes swagger)) 29 | 30 | (defn swagger-ui [options] 31 | (assert (map? options) "Since 1.1.11, compojure.api.swagger/swagger-ui takes just one map as argument, with `:path` for the path.") 32 | (c/undocumented 33 | (swagger-ui/swagger-ui options))) 34 | 35 | (defn swagger-docs [{:keys [path] :or {path "/swagger.json"} :as options}] 36 | (assert (map? options) "Since 1.1.11, compojure.api.swagger/swagger-docs takes just one map as argument, with `:path` for the path.") 37 | (let [extra-info (dissoc options :path)] 38 | (c/GET path request 39 | :no-doc true 40 | :name ::swagger 41 | (let [runtime-info1 (mw/get-swagger-data request) 42 | runtime-info2 (rsm/get-swagger-data request) 43 | base-path {:basePath (base-path request)} 44 | options (::request/ring-swagger request) 45 | paths (::request/paths request) 46 | swagger (apply rsc/deep-merge (keep identity [base-path paths extra-info runtime-info1 runtime-info2])) 47 | spec (spec-tools.swagger.core/swagger-spec (swagger2/swagger-json swagger options))] 48 | (ok spec))))) 49 | 50 | ;; 51 | ;; Public api 52 | ;; 53 | 54 | (def swagger-defaults {:ui "/", :spec "/swagger.json"}) 55 | 56 | (defn swagger-routes 57 | "Returns routes for swagger-articats (ui & spec). Accepts an options map, with the 58 | following options: 59 | 60 | **:ui** Path for the swagger-ui (defaults to \"/\"). 61 | Setting the value to nil will cause the swagger-ui not to be mounted 62 | 63 | **:spec** Path for the swagger-spec (defaults to \"/swagger.json\") 64 | Setting the value to nil will cause the swagger-ui not to be mounted 65 | 66 | **:data** Swagger data in the Ring-Swagger format. 67 | 68 | **:options** 69 | **:ui** Options to configure the ui 70 | **:spec** Options to configure the spec. Nada at the moment. 71 | 72 | Example options: 73 | 74 | {:ui \"/api-docs\" 75 | :spec \"/swagger.json\" 76 | :options {:ui {:jsonEditor true} 77 | :spec {}} 78 | :data {:basePath \"/app\" 79 | :info {:version \"1.0.0\" 80 | :title \"Sausages\" 81 | :description \"Sausage description\" 82 | :termsOfService \"http://helloreverb.com/terms/\" 83 | :contact {:name \"My API Team\" 84 | :email \"foo@example.com\" 85 | :url \"http://www.metosin.fi\"} 86 | :license {:name: \"Eclipse Public License\" 87 | :url: \"http://www.eclipse.org/legal/epl-v10.html\"}} 88 | :tags [{:name \"sausages\", :description \"Sausage api-set\"}]}}" 89 | ([] (swagger-routes {})) 90 | ([options] 91 | (let [{:keys [ui spec data] {ui-options :ui} :options} (merge swagger-defaults options) 92 | path (apply str (remove clojure.string/blank? [(:basePath data) spec]))] 93 | (if (or ui spec) 94 | (c/routes 95 | (if ui (swagger-ui (merge (if spec {:swagger-docs path}) ui-options {:path ui}))) 96 | (if spec (swagger-docs (assoc data :path spec)))))))) 97 | -------------------------------------------------------------------------------- /test/compojure/api/test_utils.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.test-utils 2 | (:require [clojure.string :as str] 3 | [peridot.core :as p] 4 | [muuntaja.core :as m] 5 | [compojure.api.routes :as routes] 6 | [compojure.api.middleware :as mw]) 7 | (:import (java.io InputStream))) 8 | 9 | (def muuntaja (mw/create-muuntaja)) 10 | 11 | (defn slurp-body [body] 12 | (if (satisfies? muuntaja.protocols/IntoInputStream body) 13 | (m/slurp body) 14 | body)) 15 | 16 | (defn parse-body [body] 17 | (if (or (string? body) (instance? InputStream body)) 18 | (m/decode muuntaja "application/json" (slurp-body body)) 19 | body)) 20 | 21 | (defn extract-schema-name [ref-str] 22 | (last (str/split ref-str #"/"))) 23 | 24 | (defn find-definition [spec ref] 25 | (let [schema-name (keyword (extract-schema-name ref))] 26 | (get-in spec [:definitions schema-name]))) 27 | 28 | (defn json-stream [x] 29 | (m/encode muuntaja "application/json" x)) 30 | 31 | (def json-string (comp slurp json-stream)) 32 | 33 | (defn parse [x] 34 | (m/decode muuntaja "application/json" x)) 35 | 36 | (defn follow-redirect [state] 37 | (if (some-> state :response :headers (get "Location")) 38 | (p/follow-redirect state) 39 | state)) 40 | 41 | (def ^:dynamic *async?* (= "true" (System/getProperty "compojure-api.test.async"))) 42 | 43 | (defn- call-async [handler request] 44 | (let [result (promise)] 45 | (handler request #(result [:ok %]) #(result [:fail %])) 46 | (if-let [[status value] (deref result 1500 nil)] 47 | (if (= status :ok) 48 | value 49 | (throw value)) 50 | (throw (Exception. (str "Timeout while waiting for the request handler. " 51 | request)))))) 52 | 53 | (defn call 54 | "Call handler synchronously or asynchronously depending on *async?*." 55 | [handler request] 56 | (if *async?* 57 | (call-async handler request) 58 | (handler request))) 59 | 60 | (defn raw-get* [app uri & [params headers]] 61 | (let [{{:keys [status body headers]} :response} 62 | (-> (cond->> app *async?* (partial call-async)) 63 | (p/session) 64 | (p/request uri 65 | :request-method :get 66 | :params (or params {}) 67 | :headers (or headers {})) 68 | follow-redirect)] 69 | [status (slurp-body body) headers])) 70 | 71 | (defn get* [app uri & [params headers]] 72 | (let [[status body headers] 73 | (raw-get* app uri params headers)] 74 | [status (parse-body body) headers])) 75 | 76 | (defn form-post* [app uri params] 77 | (let [{{:keys [status body]} :response} 78 | (-> (p/session app) 79 | (p/request uri 80 | :request-method :post 81 | :params params))] 82 | [status (parse-body body)])) 83 | 84 | (defn raw-put-or-post* [app uri method data content-type headers] 85 | (let [{{:keys [status body]} :response} 86 | (-> (p/session app) 87 | (p/request uri 88 | :request-method method 89 | :headers (or headers {}) 90 | :content-type (or content-type "application/json") 91 | :body (.getBytes data)))] 92 | [status (slurp-body body)])) 93 | 94 | (defn raw-post* [app uri & [data content-type headers]] 95 | (raw-put-or-post* app uri :post data content-type headers)) 96 | 97 | (defn post* [app uri & [data]] 98 | (let [[status body] (raw-post* app uri data)] 99 | [status (parse-body body)])) 100 | 101 | (defn put* [app uri & [data]] 102 | (let [[status body] (raw-put-or-post* app uri :put data nil nil)] 103 | [status (parse-body body)])) 104 | 105 | (defn headers-post* [app uri headers] 106 | (let [[status body] (raw-post* app uri "" nil headers)] 107 | [status (parse-body body)])) 108 | 109 | ;; 110 | ;; ring-request 111 | ;; 112 | 113 | (defn ring-request [m format data] 114 | {:uri "/echo" 115 | :request-method :post 116 | :body (m/encode m format data) 117 | :headers {"content-type" format 118 | "accept" format}}) 119 | 120 | ;; 121 | ;; get-spec 122 | ;; 123 | 124 | (defn extract-paths [app] 125 | (-> app routes/get-routes routes/all-paths)) 126 | 127 | (defn get-spec [app] 128 | (let [[status spec] (get* app "/swagger.json" {})] 129 | (assert (= status 200)) 130 | (if (:paths spec) 131 | (update-in spec [:paths] (fn [paths] 132 | (into 133 | (empty paths) 134 | (for [[k v] paths] 135 | [(if (= k (keyword "/")) 136 | "/" (str "/" (name k))) v])))) 137 | spec))) 138 | -------------------------------------------------------------------------------- /test/compojure/api/middleware_test.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.middleware-test 2 | (:require [compojure.api.middleware :refer :all] 3 | [compojure.api.exception :as ex] 4 | [clojure.test :refer [deftest is testing]] 5 | [ring.util.http-response :refer [ok]] 6 | [ring.util.http-status :as status] 7 | [ring.util.test]) 8 | (:import (java.io PrintStream ByteArrayOutputStream))) 9 | 10 | (defmacro without-err 11 | "Evaluates exprs in a context in which *err* is bound to a fresh 12 | StringWriter. Returns the string created by any nested printing 13 | calls." 14 | [& body] 15 | `(let [s# (PrintStream. (ByteArrayOutputStream.)) 16 | err# (System/err)] 17 | (System/setErr s#) 18 | (try 19 | ~@body 20 | (finally 21 | (System/setErr err#))))) 22 | 23 | (deftest encode?-test 24 | (doseq [[?body ?serializable? ?res :as test-case] 25 | [[5 true true] 26 | [5 false false] 27 | ["foobar" true true] 28 | ["foobar" false false] 29 | [{:foobar "1"} false true] 30 | [{:foobar "1"} true true] 31 | [[1 2 3] false true] 32 | [[1 2 3] true true] 33 | [(ring.util.test/string-input-stream "foobar") false false]]] 34 | 35 | (testing (pr-str test-case) 36 | (is (= (encode? nil 37 | {:body ?body 38 | :compojure.api.meta/serializable? ?serializable?}) 39 | ?res))))) 40 | 41 | (def default-options (:exceptions api-middleware-defaults-v2)) 42 | 43 | (defn- call-async [handler request] 44 | (let [result (promise)] 45 | (handler request #(result [:ok %]) #(result [:fail %])) 46 | (if-let [[status value] (deref result 1500 nil)] 47 | (if (= status :ok) 48 | value 49 | (throw value)) 50 | (throw (Exception. "Timeout while waiting for the request handler."))))) 51 | 52 | (deftest wrap-exceptions-test 53 | (with-out-str 54 | (without-err 55 | (let [exception (RuntimeException. "kosh") 56 | exception-class (.getName (.getClass exception)) 57 | handler (-> (fn [_] (throw exception)) 58 | (wrap-exceptions default-options)) 59 | async-handler (-> (fn [_ _ raise] (raise exception)) 60 | (wrap-exceptions default-options))] 61 | 62 | (testing "converts exceptions into safe internal server errors" 63 | (is (= {:status status/internal-server-error 64 | :body {:class exception-class 65 | :type "unknown-exception"}} 66 | (-> (handler {}) 67 | (select-keys [:status :body])))) 68 | (is (= {:status status/internal-server-error 69 | :body {:class exception-class 70 | :type "unknown-exception"}} 71 | (-> (call-async async-handler {}) 72 | (select-keys [:status :body])))))))) 73 | 74 | (with-out-str 75 | (without-err 76 | (testing "Thrown ex-info type can be matched" 77 | (let [handler (-> (fn [_] (throw (ex-info "kosh" {:type ::test}))) 78 | (wrap-exceptions (assoc-in default-options [:handlers ::test] (fn [ex _ _] {:status 500 :body "hello"}))))] 79 | (is (= {:status status/internal-server-error 80 | :body "hello"} 81 | (select-keys (handler {}) [:status :body]))))))) 82 | 83 | (without-err 84 | (testing "Default handler logs exceptions to console" 85 | (let [handler (-> (fn [_] (throw (RuntimeException. "kosh"))) 86 | (wrap-exceptions default-options))] 87 | (is (= "ERROR kosh\n" (with-out-str (handler {}))))))) 88 | 89 | (without-err 90 | (testing "Default request-parsing handler does not log messages" 91 | (let [handler (-> (fn [_] (throw (ex-info "Error parsing request" {:type ::ex/request-parsing} (RuntimeException. "Kosh")))) 92 | (wrap-exceptions default-options))] 93 | (is (= "" (with-out-str (handler {}))))))) 94 | 95 | (without-err 96 | (testing "Logging can be added to a exception handler" 97 | (let [handler (-> (fn [_] (throw (ex-info "Error parsing request" {:type ::ex/request-parsing} (RuntimeException. "Kosh")))) 98 | (wrap-exceptions (assoc-in default-options [:handlers ::ex/request-parsing] (ex/with-logging ex/request-parsing-handler :info))))] 99 | (is (= "INFO Error parsing request\n" (with-out-str (handler {})))))))) 100 | 101 | (deftest issue-228-test ; "compose-middeleware strips nils aways. #228" 102 | (let [times2-mw (fn [handler] 103 | (fn [request] 104 | (* 2 (handler request))))] 105 | (is (= 6 (((compose-middleware [nil times2-mw nil]) (constantly 3)) nil))))) 106 | -------------------------------------------------------------------------------- /src/compojure/api/api.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.api 2 | (:require [compojure.api.core :as c] 3 | [compojure.api.swagger :as swagger] 4 | [compojure.api.middleware :as mw] 5 | [compojure.api.request :as request] 6 | [compojure.api.routes :as routes] 7 | [compojure.api.common :as common] 8 | [ring.swagger.common :as rsc] 9 | [ring.swagger.middleware :as rsm])) 10 | 11 | (def api-defaults-v1 12 | (merge 13 | mw/api-middleware-defaults-v1 14 | {:api {:invalid-routes-fn routes/log-invalid-child-routes 15 | :disable-api-middleware? false} 16 | :swagger {:ui nil, :spec nil}})) 17 | 18 | (def api-defaults-v2 19 | (merge 20 | mw/api-middleware-defaults-v2 21 | {:api {:invalid-routes-fn routes/log-invalid-child-routes 22 | :disable-api-middleware? false} 23 | :swagger {:ui nil, :spec nil}})) 24 | 25 | (defn 26 | ^{:doc (str 27 | "Returns a ring handler wrapped in compojure.api.middleware/api-middlware. 28 | Creates the route-table at api creation time and injects that into the request via 29 | middlewares. Api and the mounted api-middleware can be configured by optional 30 | options map as the first parameter: 31 | 32 | (api 33 | {:exceptions {:handlers {:compojure.api.exception/default my-logging-handler}} 34 | :api {:invalid-routes-fn (constantly nil)} 35 | :swagger {:spec \"/swagger.json\" 36 | :ui \"/api-docs\" 37 | :data {:info {:version \"1.0.0\" 38 | :title \"My API\" 39 | :description \"the description\"}}}} 40 | (context \"/api\" [] 41 | ...)) 42 | 43 | ### direct api options: 44 | 45 | - **:api** All api options are under `:api`. 46 | - **:invalid-routes-fn** A 2-arity function taking handler and a sequence of 47 | invalid routes (not satisfying compojure.api.route.Routing) 48 | setting value to nil ignores invalid routes completely. 49 | defaults to `compojure.api.routes/log-invalid-child-routes` 50 | - **:disable-api-middleware?** boolean to disable the `api-middleware` from api. 51 | - **:swagger** Options to configure the Swagger-routes. Defaults to nil. 52 | See `compojure.api.swagger/swagger-routes` for details. 53 | 54 | ### api-middleware options 55 | 56 | See `compojure.api.middleware/api-middleware` for more available options. 57 | 58 | " (:doc (meta #'compojure.api.middleware/api-middleware)))} 59 | api 60 | [& body] 61 | (let [[options handlers] (common/extract-parameters body false) 62 | _ (assert (not (contains? options :format)) 63 | (str "ERROR: Option [:format] is not used with 2.* version.\n" 64 | "Compojure-api uses now Muuntaja insted of ring-middleware-format,\n" 65 | "the new formatting options for it should be under [:formats]. See\n" 66 | "[[api-middleware]] documentation for more details.\n")) 67 | options (if (:format options) 68 | (assert nil ":format") 69 | (rsc/deep-merge api-defaults-v2 options)) 70 | handler (apply c/routes (concat [(swagger/swagger-routes (:swagger options))] handlers)) 71 | partial-api-route (routes/map->Route 72 | {:childs [handler] 73 | :info {:coercion (:coercion options)}}) 74 | routes (routes/get-routes partial-api-route (:api options)) 75 | paths (-> routes routes/ring-swagger-paths swagger/transform-operations) 76 | lookup (routes/route-lookup-table routes) 77 | swagger-data (get-in options [:swagger :data]) 78 | enable-api-middleware? (not (get-in options [:api :disable-api-middleware?])) 79 | api-middleware-options ((if (:format options) mw/api-middleware-options-v1 mw/api-middleware-options-v2) 80 | (dissoc options :api :swagger)) 81 | api-handler (-> handler 82 | (cond-> swagger-data (rsm/wrap-swagger-data swagger-data)) 83 | (cond-> enable-api-middleware? (mw/api-middleware 84 | api-middleware-options)) 85 | (mw/wrap-inject-data 86 | {::request/paths paths 87 | ::request/lookup lookup}))] 88 | (assoc partial-api-route :handler api-handler))) 89 | 90 | (defmacro 91 | ^{:superseded-by "api" 92 | :deprecated "2.0.0" 93 | :doc (str 94 | "Deprecated: please use (def name (api ...body..)) 95 | 96 | Defines an api. 97 | 98 | API middleware options: 99 | 100 | " (:doc (meta #'compojure.api.middleware/api-middleware)))} 101 | defapi 102 | [name & body] 103 | {:style/indent 1} 104 | `(def ~name (api ~@body))) 105 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject metosin/compojure-api "2.0.0-alpha34-SNAPSHOT" 2 | :description "Compojure Api" 3 | :url "https://github.com/metosin/compojure-api" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html" 6 | :distribution :repo 7 | :comments "same as Clojure"} 8 | :exclusions [frankiesardo/linked] 9 | :dependencies [[prismatic/schema "1.1.12"] 10 | [prismatic/plumbing "0.5.5"] 11 | [ikitommi/linked "1.3.1-alpha1"] ;; waiting for the original 12 | [metosin/muuntaja "0.6.6"] 13 | [com.fasterxml.jackson.datatype/jackson-datatype-joda "2.10.1"] 14 | [ring/ring-core "1.8.0"] 15 | [compojure "1.6.1" ] 16 | [metosin/spec-tools "0.10.6"] 17 | [metosin/ring-http-response "0.9.1"] 18 | [metosin/ring-swagger-ui "3.24.3"] 19 | [metosin/ring-swagger "1.0.0"] 20 | 21 | ;; Fix dependency conflicts 22 | [clj-time "0.15.2"] 23 | [joda-time "2.10.5"] 24 | [riddley "0.2.0"]] 25 | :pedantic? :abort 26 | :profiles {:uberjar {:aot :all 27 | :ring {:handler examples.thingie/app} 28 | :source-paths ["examples/thingie/src"] 29 | :dependencies [[org.clojure/clojure "1.9.0"] 30 | [http-kit "2.3.0"] 31 | [reloaded.repl "0.2.4"] 32 | [com.stuartsierra/component "0.4.0"]]} 33 | :dev {:plugins [[lein-clojars "0.9.1"] 34 | [lein-ring "0.12.5"] 35 | [funcool/codeina "0.5.0"]] 36 | :dependencies [[org.clojure/clojure "1.9.0"] 37 | [org.clojure/core.unify "0.6.0"] 38 | [org.clojure/core.async "0.6.532"] 39 | [javax.servlet/javax.servlet-api "4.0.1"] 40 | [peridot "0.5.2"] 41 | [com.stuartsierra/component "0.4.0"] 42 | [expound "0.8.2"] 43 | [metosin/jsonista "0.2.5"] 44 | [reloaded.repl "0.2.4"] 45 | [metosin/muuntaja-msgpack "0.6.6"] 46 | [metosin/muuntaja-yaml "0.6.6"] 47 | [org.immutant/immutant "2.1.10"] 48 | [http-kit "2.3.0"] 49 | [criterium "0.4.5"]] 50 | :jvm-opts ["-Dcompojure.api.meta.static-context-coach={:default :assert :verbose true}"] 51 | :test-paths ["test19"] 52 | :ring {:handler examples.thingie/app 53 | :reload-paths ["src" "examples/thingie/src"]} 54 | :source-paths ["examples/thingie/src" "examples/thingie/dev-src"] 55 | :main examples.server} 56 | :perf {:jvm-opts ^:replace ["-server" 57 | "-Xmx4096m" 58 | "-Dclojure.compiler.direct-linking=true"]} 59 | :logging {:dependencies [[org.clojure/tools.logging "0.5.0"] 60 | [org.slf4j/jcl-over-slf4j "1.7.30"] 61 | [org.slf4j/jul-to-slf4j "1.7.30"] 62 | [org.slf4j/log4j-over-slf4j "1.7.30"] 63 | [ch.qos.logback/logback-classic "1.2.3" ]]} 64 | :1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]} 65 | :1.11 {:dependencies [[org.clojure/clojure "1.11.3"]]} 66 | :1.12 {:dependencies [[org.clojure/clojure "1.12.0-alpha11"]]} 67 | :async {:jvm-opts ["-Dcompojure-api.test.async=true"] 68 | :dependencies [[manifold "0.1.8" :exclusions [org.clojure/tools.logging]]]}} 69 | :eastwood {:namespaces [:source-paths] 70 | :add-linters [:unused-namespaces]} 71 | :codeina {:sources ["src"] 72 | :target "gh-pages/doc" 73 | :src-uri "http://github.com/metosin/compojure-api/blob/master/" 74 | :src-uri-prefix "#L"} 75 | :deploy-repositories [["snapshot" {:url "https://clojars.org/repo" 76 | :username [:gpg :env/clojars_user] 77 | :password [:gpg :env/clojars_token] 78 | :sign-releases false}] 79 | ["release" {:url "https://clojars.org/repo" 80 | :username [:gpg :env/clojars_user] 81 | :password [:gpg :env/clojars_token] 82 | :sign-releases false}]] 83 | :release-tasks [["clean"] 84 | ["vcs" "assert-committed"] 85 | ["change" "version" "leiningen.release/bump-version" "release"] 86 | ["vcs" "commit"] 87 | ["vcs" "tag" "--no-sign"] 88 | ["deploy" "release"] 89 | ["change" "version" "leiningen.release/bump-version"] 90 | ["vcs" "commit"] 91 | ["vcs" "push"]] 92 | :aliases {"all" ["with-profile" "dev:dev,async:dev,1.10:dev,1.11:dev,1.12"] 93 | "start-thingie" ["run"] 94 | "aot-uberjar" ["with-profile" "uberjar" "do" "clean," "ring" "uberjar"] 95 | "test-ancient" ["test"] 96 | "perf" ["with-profile" "default,dev,perf"] 97 | "deploy!" ^{:doc "Recompile sources, then deploy if tests succeed."} 98 | ["do" ["clean"] ["test"] ["deploy" "clojars"]]}) 99 | -------------------------------------------------------------------------------- /src/compojure/api/coercion/spec.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.coercion.spec 2 | (:require [schema.core] 3 | [clojure.spec.alpha :as s] 4 | [spec-tools.core :as st] 5 | [spec-tools.data-spec :as ds] 6 | [clojure.walk :as walk] 7 | [compojure.api.coercion.core :as cc] 8 | [spec-tools.swagger.core :as swagger] 9 | [compojure.api.common :as common]) 10 | (:import (clojure.lang IPersistentMap) 11 | (schema.core RequiredKey OptionalKey) 12 | (spec_tools.core Spec) 13 | (spec_tools.data_spec Maybe))) 14 | 15 | (def string-transformer 16 | (st/type-transformer 17 | st/string-transformer 18 | st/strip-extra-keys-transformer 19 | {:name :string})) 20 | 21 | (def json-transformer 22 | (st/type-transformer 23 | st/json-transformer 24 | st/strip-extra-keys-transformer 25 | {:name :json})) 26 | 27 | (defn default-transformer 28 | ([] (default-transformer :default)) 29 | ([name] (st/type-transformer {:name name}))) 30 | 31 | (defprotocol Specify 32 | (specify [this name])) 33 | 34 | (extend-protocol Specify 35 | IPersistentMap 36 | (specify [this name] 37 | (-> (->> 38 | (walk/postwalk 39 | (fn [x] 40 | (if (and (map? x) (not (record? x))) 41 | (->> (for [[k v] (dissoc x schema.core/Keyword) 42 | :let [k (cond 43 | ;; Schema required 44 | (instance? RequiredKey k) 45 | (ds/req (schema.core/explicit-schema-key k)) 46 | 47 | ;; Schema options 48 | (instance? OptionalKey k) 49 | (ds/opt (schema.core/explicit-schema-key k)) 50 | 51 | :else 52 | k)]] 53 | [k v]) 54 | (into {})) 55 | x)) 56 | this) 57 | (ds/spec name)) 58 | (dissoc :name))) 59 | 60 | Maybe 61 | (into-spec [this name] 62 | (ds/spec name this)) 63 | 64 | Spec 65 | (specify [this _] this) 66 | 67 | Object 68 | (specify [this _] 69 | (st/create-spec {:spec this}))) 70 | 71 | (def memoized-specify 72 | (common/fifo-memoize #(specify %1 (keyword "spec" (name (gensym "")))) 1000)) 73 | 74 | (defn maybe-memoized-specify [spec] 75 | (if (keyword? spec) 76 | (specify spec nil) 77 | (memoized-specify spec))) 78 | 79 | (defn stringify-pred [pred] 80 | (str (if (instance? clojure.lang.LazySeq pred) 81 | (seq pred) 82 | pred))) 83 | 84 | (defmulti coerce-response? identity :default ::default) 85 | (defmethod coerce-response? ::default [_] true) 86 | 87 | (defrecord SpecCoercion [name options] 88 | cc/Coercion 89 | (get-name [_] name) 90 | 91 | (get-apidocs [_ _ {:keys [parameters responses] :as info}] 92 | (cond-> (dissoc info :parameters :responses) 93 | parameters (assoc 94 | ::swagger/parameters 95 | (into 96 | (empty parameters) 97 | (for [[k v] parameters] 98 | [k (maybe-memoized-specify v)]))) 99 | responses (assoc 100 | ::swagger/responses 101 | (into 102 | (empty responses) 103 | (for [[k response] responses] 104 | [k (update response :schema #(some-> % maybe-memoized-specify))]))))) 105 | 106 | (make-open [_ spec] spec) 107 | 108 | (encode-error [_ error] 109 | (let [problems (-> error :problems ::s/problems)] 110 | (-> error 111 | (update :spec (comp str s/form)) 112 | (assoc :problems (mapv #(update % :pred stringify-pred) problems))))) 113 | 114 | (coerce-request [_ spec value type format _] 115 | (let [spec (maybe-memoized-specify spec) 116 | type-options (options type)] 117 | (if-let [transformer (or (get (get type-options :formats) format) 118 | (get type-options :default))] 119 | (let [coerced (st/coerce spec value transformer)] 120 | (if (s/valid? spec coerced) 121 | coerced 122 | (let [conformed (st/conform spec coerced transformer)] 123 | (if (s/invalid? conformed) 124 | (let [problems (st/explain-data spec coerced transformer)] 125 | (cc/map->CoercionError 126 | {:spec spec 127 | :problems problems})) 128 | (s/unform spec conformed))))) 129 | value))) 130 | 131 | (accept-response? [_ spec] 132 | (boolean (coerce-response? spec))) 133 | 134 | (coerce-response [this spec value type format request] 135 | (cc/coerce-request this spec value type format request))) 136 | 137 | (def default-options 138 | {:body {:default (default-transformer) 139 | :formats {"application/json" json-transformer 140 | "application/msgpack" json-transformer 141 | "application/x-yaml" json-transformer}} 142 | :string {:default string-transformer} 143 | :response {:default (default-transformer) 144 | :formats {"application/json" (default-transformer :json) 145 | "application/msgpack" (default-transformer :json) 146 | "application/x-yaml" (default-transformer :json)}}}) 147 | 148 | (defn create-coercion [options] 149 | (->SpecCoercion :spec options)) 150 | 151 | (def default-coercion (create-coercion default-options)) 152 | 153 | (defmethod cc/named-coercion :spec [_] default-coercion) 154 | -------------------------------------------------------------------------------- /test/compojure/api/compojure_perf_test.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.compojure-perf-test 2 | (:require [compojure.core :as c] 3 | [compojure.api.sweet :as s] 4 | [criterium.core :as cc] 5 | [ring.util.http-response :refer :all])) 6 | 7 | ;; 8 | ;; start repl with `lein perf repl` 9 | ;; perf measured with the following setup: 10 | ;; 11 | ;; Model Name: MacBook Pro 12 | ;; Model Identifier: MacBookPro11,3 13 | ;; Processor Name: Intel Core i7 14 | ;; Processor Speed: 2,5 GHz 15 | ;; Number of Processors: 1 16 | ;; Total Number of Cores: 4 17 | ;; L2 Cache (per Core): 256 KB 18 | ;; L3 Cache: 6 MB 19 | ;; Memory: 16 GB 20 | ;; 21 | 22 | (defn title [s] 23 | (println 24 | (str "\n\u001B[35m" 25 | (apply str (repeat (+ 6 (count s)) "#")) 26 | "\n## " s " ##\n" 27 | (apply str (repeat (+ 6 (count s)) "#")) 28 | "\u001B[0m\n"))) 29 | 30 | (defn compojure-bench [] 31 | 32 | ;; 3.8µs 33 | ;; 2.6µs 34 | (let [app (c/routes 35 | (c/GET "/a/b/c/1" [] "ok") 36 | (c/GET "/a/b/c/2" [] "ok") 37 | (c/GET "/a/b/c/3" [] "ok") 38 | (c/GET "/a/b/c/4" [] "ok") 39 | (c/GET "/a/b/c/5" [] "ok")) 40 | 41 | call #(app {:request-method :get :uri "/a/b/c/5"})] 42 | 43 | (title "Compojure - GET flattened") 44 | (assert (-> (call) :body (= "ok"))) 45 | (cc/quick-bench (call))) 46 | 47 | ;; 15.9µs 48 | ;; 11.6µs 49 | (let [app (c/context "/a" [] 50 | (c/context "/b" [] 51 | (c/context "/c" [] 52 | (c/GET "/1" [] "ok") 53 | (c/GET "/2" [] "ok") 54 | (c/GET "/3" [] "ok") 55 | (c/GET "/4" [] "ok") 56 | (c/GET "/5" [] "ok")) 57 | (c/GET "/1" [] "ok") 58 | (c/GET "/2" [] "ok") 59 | (c/GET "/3" [] "ok") 60 | (c/GET "/4" [] "ok") 61 | (c/GET "/5" [] "ok")) 62 | (c/GET "/1" [] "ok") 63 | (c/GET "/2" [] "ok") 64 | (c/GET "/3" [] "ok") 65 | (c/GET "/4" [] "ok") 66 | (c/GET "/5" [] "ok")) 67 | 68 | call #(app {:request-method :get :uri "/a/b/c/5"})] 69 | 70 | (title "Compojure - GET with context") 71 | (assert (-> (call) :body (= "ok"))) 72 | (cc/quick-bench (call)))) 73 | 74 | (defn compojure-api-bench [] 75 | 76 | ;; 3.8µs 77 | ;; 2.7µs 78 | (let [app (s/routes 79 | (s/GET "/a/b/c/1" [] "ok") 80 | (s/GET "/a/b/c/2" [] "ok") 81 | (s/GET "/a/b/c/3" [] "ok") 82 | (s/GET "/a/b/c/4" [] "ok") 83 | (s/GET "/a/b/c/5" [] "ok")) 84 | 85 | call #(app {:request-method :get :uri "/a/b/c/5"})] 86 | 87 | (title "Compojure API - GET flattened") 88 | (assert (-> (call) :body (= "ok"))) 89 | (cc/quick-bench (call))) 90 | 91 | ;; 20.0µs 92 | ;; 17.0µs 93 | ;; 11.4µs static-context (-30%) 94 | (let [app (s/context "/a" [] 95 | (s/context "/b" [] 96 | (s/context "/c" [] 97 | (s/GET "/1" [] "ok") 98 | (s/GET "/2" [] "ok") 99 | (s/GET "/3" [] "ok") 100 | (s/GET "/4" [] "ok") 101 | (s/GET "/5" [] "ok")) 102 | (s/GET "/1" [] "ok") 103 | (s/GET "/2" [] "ok") 104 | (s/GET "/3" [] "ok") 105 | (s/GET "/4" [] "ok") 106 | (s/GET "/5" [] "ok")) 107 | (s/GET "/1" [] "ok") 108 | (s/GET "/2" [] "ok") 109 | (s/GET "/3" [] "ok") 110 | (s/GET "/4" [] "ok") 111 | (s/GET "/5" [] "ok")) 112 | 113 | call #(app {:request-method :get :uri "/a/b/c/5"})] 114 | 115 | (title "Compojure API - GET with context") 116 | (assert (-> (call) :body (= "ok"))) 117 | (cc/quick-bench (call)))) 118 | 119 | (defn compojure-api-mw-bench [] 120 | 121 | ;; 47.0µs (15 + 3906408 calls) 122 | ;; 10.9µs (77 + 0 calls) - static-context (-75%) 123 | (let [calls (atom nil) 124 | mw (fn [handler x] (swap! calls update x (fnil inc 0)) (fn [req] (handler req))) 125 | app (s/context "/a" [] 126 | :middleware [[mw :a]] 127 | (s/context "/b" [] 128 | :middleware [[mw :b]] 129 | (s/context "/c" [] 130 | :middleware [[mw :c]] 131 | (s/GET "/1" [] :middleware [[mw :c1]] "ok") 132 | (s/GET "/2" [] :middleware [[mw :c2]] "ok") 133 | (s/GET "/3" [] :middleware [[mw :c3]] "ok") 134 | (s/GET "/4" [] :middleware [[mw :c4]] "ok") 135 | (s/GET "/5" [] :middleware [[mw :c5]] "ok")) 136 | (s/GET "/1" [] :middleware [[mw :b1]] "ok") 137 | (s/GET "/2" [] :middleware [[mw :b2]] "ok") 138 | (s/GET "/3" [] :middleware [[mw :b3]] "ok") 139 | (s/GET "/4" [] :middleware [[mw :b4]] "ok") 140 | (s/GET "/5" [] :middleware [[mw :b5]] "ok")) 141 | (s/GET "/1" [] :middleware [[mw :a1]] "ok") 142 | (s/GET "/2" [] :middleware [[mw :a2]] "ok") 143 | (s/GET "/3" [] :middleware [[mw :a3]] "ok") 144 | (s/GET "/4" [] :middleware [[mw :a4]] "ok") 145 | (s/GET "/5" [] :middleware [[mw :a5]] "ok")) 146 | 147 | call #(app {:request-method :get :uri "/a/b/c/5"})] 148 | 149 | (clojure.pprint/pprint {:calls @calls, :total (->> @calls vals (reduce +))}) 150 | (reset! calls nil) 151 | (title "Compojure API - GET with context with middleware") 152 | (assert (-> (call) :body (= "ok"))) 153 | (cc/quick-bench (call)) 154 | (clojure.pprint/pprint {:calls @calls, :total (->> @calls vals (reduce +))}))) 155 | 156 | (defn bench [] 157 | (compojure-bench) 158 | (compojure-api-bench) 159 | (compojure-api-mw-bench)) 160 | 161 | (comment 162 | (bench)) 163 | -------------------------------------------------------------------------------- /dependabot/verbose-dependency-tree.txt: -------------------------------------------------------------------------------- 1 | metosin:compojure-api:jar:2.0.0-alpha34-SNAPSHOT 2 | +- prismatic:schema:jar:1.1.12:compile 3 | +- prismatic:plumbing:jar:0.5.5:compile 4 | | +- (prismatic:schema:jar:1.1.7:compile - omitted for conflict with 1.1.12) 5 | | \- de.kotka:lazymap:jar:3.1.0:compile 6 | +- ikitommi:linked:jar:1.3.1-alpha1:compile 7 | +- metosin:muuntaja:jar:0.6.6:compile 8 | | +- metosin:jsonista:jar:0.2.5:compile 9 | | | +- (com.fasterxml.jackson.core:jackson-databind:jar:2.10.0:compile - omitted for conflict with 2.10.1) 10 | | | \- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.10.0:compile 11 | | | +- (com.fasterxml.jackson.core:jackson-annotations:jar:2.10.0:compile - omitted for conflict with 2.10.1) 12 | | | +- (com.fasterxml.jackson.core:jackson-core:jar:2.10.0:compile - omitted for conflict with 2.10.1) 13 | | | \- (com.fasterxml.jackson.core:jackson-databind:jar:2.10.0:compile - omitted for conflict with 2.10.1) 14 | | \- com.cognitect:transit-clj:jar:0.8.319:compile 15 | | \- com.cognitect:transit-java:jar:0.8.337:compile 16 | | +- (com.fasterxml.jackson.core:jackson-core:jar:2.8.7:compile - omitted for conflict with 2.10.1) 17 | | +- org.msgpack:msgpack:jar:0.6.12:compile 18 | | | +- com.googlecode.json-simple:json-simple:jar:1.1.1:compile 19 | | | \- org.javassist:javassist:jar:3.18.1-GA:compile 20 | | +- (commons-codec:commons-codec:jar:1.10:compile - omitted for conflict with 1.11) 21 | | \- javax.xml.bind:jaxb-api:jar:2.3.0:compile 22 | +- com.fasterxml.jackson.datatype:jackson-datatype-joda:jar:2.10.1:compile 23 | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.10.1:compile 24 | | +- com.fasterxml.jackson.core:jackson-core:jar:2.10.1:compile 25 | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.10.1:compile 26 | | | +- (com.fasterxml.jackson.core:jackson-annotations:jar:2.10.1:compile - omitted for duplicate) 27 | | | \- (com.fasterxml.jackson.core:jackson-core:jar:2.10.1:compile - omitted for duplicate) 28 | | \- (joda-time:joda-time:jar:2.9.9:compile - omitted for conflict with 2.10.5) 29 | +- ring:ring-core:jar:1.8.0:compile 30 | | +- ring:ring-codec:jar:1.1.2:compile 31 | | | \- commons-codec:commons-codec:jar:1.11:compile 32 | | +- commons-io:commons-io:jar:2.6:compile 33 | | +- commons-fileupload:commons-fileupload:jar:1.4:compile 34 | | | \- (commons-io:commons-io:jar:2.2:compile - omitted for conflict with 2.6) 35 | | +- crypto-random:crypto-random:jar:1.2.0:compile 36 | | | \- (commons-codec:commons-codec:jar:1.6:compile - omitted for conflict with 1.11) 37 | | \- crypto-equality:crypto-equality:jar:1.0.0:compile 38 | +- compojure:compojure:jar:1.6.1:compile 39 | | +- org.clojure:tools.macro:jar:0.1.5:compile 40 | | +- clout:clout:jar:2.2.1:compile 41 | | | \- instaparse:instaparse:jar:1.4.8:compile 42 | | +- medley:medley:jar:1.0.0:compile 43 | | +- (ring:ring-core:jar:1.6.3:compile - omitted for conflict with 1.8.0) 44 | | \- (ring:ring-codec:jar:1.1.0:compile - omitted for conflict with 1.1.2) 45 | +- metosin:spec-tools:jar:0.10.6:compile 46 | | \- org.clojure:spec.alpha:jar:0.3.218:compile 47 | +- metosin:ring-http-response:jar:0.9.1:compile 48 | | +- (ring:ring-core:jar:1.7.1:compile - omitted for conflict with 1.8.0) 49 | | \- potemkin:potemkin:jar:0.4.5:compile 50 | | +- clj-tuple:clj-tuple:jar:0.2.2:compile 51 | | \- (riddley:riddley:jar:0.1.12:compile - omitted for conflict with 0.2.0) 52 | +- metosin:ring-swagger-ui:jar:3.24.3:compile 53 | +- metosin:ring-swagger:jar:1.0.0:compile 54 | | +- cheshire:cheshire:jar:5.8.1:compile 55 | | | +- (com.fasterxml.jackson.core:jackson-core:jar:2.9.6:compile - omitted for conflict with 2.10.1) 56 | | | +- com.fasterxml.jackson.dataformat:jackson-dataformat-smile:jar:2.9.6:compile 57 | | | | \- (com.fasterxml.jackson.core:jackson-core:jar:2.9.6:compile - omitted for conflict with 2.10.1) 58 | | | +- com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:jar:2.9.6:compile 59 | | | | \- (com.fasterxml.jackson.core:jackson-core:jar:2.9.6:compile - omitted for conflict with 2.10.1) 60 | | | \- tigris:tigris:jar:0.1.1:compile 61 | | +- (metosin:ring-http-response:jar:0.9.1:compile - omitted for duplicate) 62 | | +- (ring:ring-core:jar:1.7.1:compile - omitted for conflict with 1.8.0) 63 | | +- metosin:schema-tools:jar:0.11.0:compile 64 | | | \- (prismatic:schema:jar:1.1.9:compile - omitted for conflict with 1.1.12) 65 | | +- (prismatic:schema:jar:1.1.10:compile - omitted for conflict with 1.1.12) 66 | | +- (prismatic:plumbing:jar:0.5.5:compile - omitted for duplicate) 67 | | +- metosin:scjsv:jar:0.5.0:compile 68 | | | +- (cheshire:cheshire:jar:5.8.1:compile - omitted for duplicate) 69 | | | \- com.github.java-json-tools:json-schema-validator:jar:2.2.10:compile 70 | | | +- com.github.java-json-tools:json-schema-core:jar:1.2.10:compile 71 | | | | +- com.github.java-json-tools:jackson-coreutils:jar:1.9:compile 72 | | | | | +- (com.fasterxml.jackson.core:jackson-databind:jar:2.2.3:compile - omitted for conflict with 2.10.1) 73 | | | | | +- com.google.guava:guava:jar:16.0.1:compile 74 | | | | | +- com.github.fge:msg-simple:jar:1.1:compile 75 | | | | | | +- com.github.fge:btf:jar:1.2:compile 76 | | | | | | | \- (com.google.code.findbugs:jsr305:jar:2.0.1:compile - omitted for conflict with 3.0.1) 77 | | | | | | \- (com.google.code.findbugs:jsr305:jar:2.0.1:compile - omitted for conflict with 3.0.1) 78 | | | | | \- (com.google.code.findbugs:jsr305:jar:2.0.1:compile - omitted for conflict with 3.0.1) 79 | | | | +- com.github.fge:uri-template:jar:0.9:compile 80 | | | | | +- (com.github.fge:msg-simple:jar:1.1:compile - omitted for duplicate) 81 | | | | | +- (com.google.guava:guava:jar:16.0.1:compile - omitted for duplicate) 82 | | | | | \- (com.google.code.findbugs:jsr305:jar:2.0.1:compile - omitted for conflict with 3.0.1) 83 | | | | +- org.mozilla:rhino:jar:1.7.7.1:compile 84 | | | | \- (com.google.code.findbugs:jsr305:jar:3.0.1:compile - omitted for duplicate) 85 | | | +- javax.mail:mailapi:jar:1.4.3:compile 86 | | | | \- javax.activation:activation:jar:1.1:compile 87 | | | +- (joda-time:joda-time:jar:2.9.7:compile - omitted for conflict with 2.10.5) 88 | | | +- com.googlecode.libphonenumber:libphonenumber:jar:8.0.0:compile 89 | | | +- com.google.code.findbugs:jsr305:jar:3.0.1:compile 90 | | | \- net.sf.jopt-simple:jopt-simple:jar:5.0.3:compile 91 | | +- (clj-time:clj-time:jar:0.15.1:compile - omitted for conflict with 0.15.2) 92 | | +- org.tobereplaced:lettercase:jar:1.0.0:compile 93 | | \- (potemkin:potemkin:jar:0.4.5:compile - omitted for duplicate) 94 | +- clj-time:clj-time:jar:0.15.2:compile 95 | | \- (joda-time:joda-time:jar:2.10:compile - omitted for conflict with 2.10.5) 96 | +- joda-time:joda-time:jar:2.10.5:compile 97 | \- riddley:riddley:jar:0.2.0:compile 98 | -------------------------------------------------------------------------------- /dependabot/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | metosin 4 | compojure-api 5 | jar 6 | 2.0.0-alpha34-SNAPSHOT 7 | compojure-api 8 | Compojure Api 9 | 10 | 11 | 12 | Eclipse Public License 13 | http://www.eclipse.org/legal/epl-v10.html 14 | repo 15 | same as Clojure 16 | 17 | 18 | 19 | 20 | src 21 | test 22 | 23 | 24 | resources 25 | 26 | 27 | 28 | 29 | resources 30 | 31 | 32 | target 33 | target/classes 34 | 35 | 36 | 37 | 38 | central 39 | https://repo1.maven.org/maven2/ 40 | 41 | false 42 | 43 | 44 | true 45 | 46 | 47 | 48 | clojars 49 | https://repo.clojars.org/ 50 | 51 | true 52 | 53 | 54 | true 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | prismatic 64 | schema 65 | 1.1.12 66 | 67 | 68 | linked 69 | frankiesardo 70 | 71 | 72 | 73 | 74 | prismatic 75 | plumbing 76 | 0.5.5 77 | 78 | 79 | linked 80 | frankiesardo 81 | 82 | 83 | 84 | 85 | ikitommi 86 | linked 87 | 1.3.1-alpha1 88 | 89 | 90 | linked 91 | frankiesardo 92 | 93 | 94 | 95 | 96 | metosin 97 | muuntaja 98 | 0.6.6 99 | 100 | 101 | linked 102 | frankiesardo 103 | 104 | 105 | 106 | 107 | com.fasterxml.jackson.datatype 108 | jackson-datatype-joda 109 | 2.10.1 110 | 111 | 112 | linked 113 | frankiesardo 114 | 115 | 116 | 117 | 118 | ring 119 | ring-core 120 | 1.8.0 121 | 122 | 123 | linked 124 | frankiesardo 125 | 126 | 127 | 128 | 129 | compojure 130 | compojure 131 | 1.6.1 132 | 133 | 134 | linked 135 | frankiesardo 136 | 137 | 138 | 139 | 140 | metosin 141 | spec-tools 142 | 0.10.6 143 | 144 | 145 | linked 146 | frankiesardo 147 | 148 | 149 | 150 | 151 | metosin 152 | ring-http-response 153 | 0.9.1 154 | 155 | 156 | linked 157 | frankiesardo 158 | 159 | 160 | 161 | 162 | metosin 163 | ring-swagger-ui 164 | 3.24.3 165 | 166 | 167 | linked 168 | frankiesardo 169 | 170 | 171 | 172 | 173 | metosin 174 | ring-swagger 175 | 1.0.0 176 | 177 | 178 | linked 179 | frankiesardo 180 | 181 | 182 | 183 | 184 | clj-time 185 | clj-time 186 | 0.15.2 187 | 188 | 189 | linked 190 | frankiesardo 191 | 192 | 193 | 194 | 195 | joda-time 196 | joda-time 197 | 2.10.5 198 | 199 | 200 | linked 201 | frankiesardo 202 | 203 | 204 | 205 | 206 | riddley 207 | riddley 208 | 0.2.0 209 | 210 | 211 | linked 212 | frankiesardo 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compojure-api 2 | 3 | **Psst!** If you're starting a new project, why not try out [reitit](https://github.com/metosin/reitit)? 4 | 5 | Stuff on top of [Compojure](https://github.com/weavejester/compojure) for making sweet web apis. 6 | 7 | - [Schema](https://github.com/Prismatic/schema) & [clojure.spec](https://clojure.org/about/spec) (2.0.0) for input & output data coercion 8 | - [Swagger](http://swagger.io/) for api documentation, via [ring-swagger](https://github.com/metosin/ring-swagger) & [spec-tools](https://github.com/metosin/spec-tools) 9 | - [Async](https://github.com/metosin/compojure-api/wiki/Async) with async-ring, [manifold](https://github.com/ztellman/manifold) and [core.async](https://github.com/clojure/core.async) (2.0.0) 10 | - Client negotiable formats: [JSON](http://www.json.org/), [EDN](https://github.com/edn-format/edn) & [Transit](https://github.com/cognitect/transit-format), optionally [YAML](http://yaml.org/) and [MessagePack](http://msgpack.org/) 11 | - Data-driven [resources](https://github.com/metosin/compojure-api/wiki/Resources-and-Liberator) 12 | - [Bi-directional](https://github.com/metosin/compojure-api/wiki/Routing#bi-directional-routing) routing 13 | - Bundled middleware for common api behavior ([exception handling](https://github.com/metosin/compojure-api/wiki/Exception-handling), parameters & formats) 14 | - Extendable route DSL via [metadata handlers](https://github.com/metosin/compojure-api/wiki/Creating-your-own-metadata-handlers) 15 | - Route functions & macros for putting things together, including the [Swagger-UI](https://github.com/wordnik/swagger-ui) via [ring-swagger-ui](https://github.com/metosin/ring-swagger-ui) 16 | - Requires Clojure 1.9.0 & Java 1.8 17 | 18 | [API Docs](http://metosin.github.io/compojure-api/doc/) & [Wiki](https://github.com/metosin/compojure-api/wiki) 19 | 20 | ## Latest version 21 | 22 | [![Clojars Project](http://clojars.org/metosin/compojure-api/latest-version.svg)](http://clojars.org/metosin/compojure-api) 23 | 24 | Latest non-alpha: `[metosin/compojure-api "1.1.14"]`. 25 | 26 | See [CHANGELOG](https://github.com/metosin/compojure-api/blob/master/CHANGELOG.md) for details. 27 | 28 | ## For information and help 29 | 30 | ### [Read the Version 1.0 Blog Post](http://www.metosin.fi/blog/compojure-api-100/) 31 | 32 | ### [Schema & Spec Coercion with 2.0.0](https://github.com/metosin/compojure-api/wiki/Coercion) 33 | 34 | ### [Check wiki for documentation](https://github.com/metosin/compojure-api/wiki) 35 | 36 | [Clojurians slack](https://clojurians.slack.com/) ([join](http://clojurians.net/)) has a channel [#ring-swagger](https://clojurians.slack.com/messages/ring-swagger/) for talk about any libraries using Ring-swagger. You can also ask questions about Compojure-api and Ring-swagger on other channels at Clojurians Slack or at #clojure on Freenode IRC (mention `compojure-api` or `ring-swagger` to highlight us). 37 | 38 | ## Examples 39 | 40 | ### Hello World Api 41 | 42 | ```clj 43 | (require '[compojure.api.sweet :refer :all]) 44 | (require '[ring.util.http-response :refer :all]) 45 | 46 | (def app 47 | (api 48 | (GET "/hello" [] 49 | :query-params [name :- String] 50 | (ok {:message (str "Hello, " name)})))) 51 | ``` 52 | 53 | ### Hello World, async 54 | 55 | ```clj 56 | (require '[compojure.api.sweet :refer :all]) 57 | (require '[clojure.core.async :as a]) 58 | 59 | (GET "/hello-async" [] 60 | :query-params [name :- String] 61 | (a/go 62 | (a/* requires server to be run in [async mode](https://github.com/metosin/compojure-api/wiki/Async) 67 | 68 | ### Hello World, async & data-driven 69 | 70 | ```clj 71 | (require '[compojure.api.sweet :refer :all]) 72 | (require '[clojure.core.async :as a]) 73 | (require '[schema.core :as s]) 74 | 75 | (context "/hello-async" [] 76 | (resource 77 | {:get 78 | {:parameters {:query-params {:name String}} 79 | :responses {200 {:schema {:message String}} 80 | 404 {} 81 | 500 {:schema s/Any}} 82 | :handler (fn [{{:keys [name]} :query-params}] 83 | (a/go 84 | (a/* Note that empty body responses can be specified with `{}` or `{:schema s/Any}` 89 | 90 | ### Hello World, async, data-driven & clojure.spec 91 | 92 | ```clj 93 | (require '[compojure.api.sweet :refer :all]) 94 | (require '[clojure.core.async :as a]) 95 | (require '[clojure.spec.alpha :as s]) 96 | 97 | (s/def ::name string?) 98 | (s/def ::message string?) 99 | 100 | (context "/hello-async" [] 101 | (resource 102 | {:coercion :spec 103 | :get {:parameters {:query-params (s/keys :req-un [::name])} 104 | :responses {200 {:schema (s/keys :req-un [::message])}} 105 | :handler (fn [{{:keys [name]} :query-params}] 106 | (a/go 107 | (a/hello world." 136 | :summary "echos a string from query-params" 137 | (ok (str "hello, " name)))) 138 | 139 | (context "/context" [] 140 | :summary "summary inherited from context" 141 | :tags ["context"] 142 | 143 | (context "/:kikka" [] 144 | :path-params [kikka :- s/Str] 145 | :query-params [kukka :- s/Str] 146 | 147 | (GET "/:kakka" [] 148 | :return {:kikka s/Str 149 | :kukka s/Str 150 | :kakka s/Str} 151 | :path-params [kakka :- s/Str] 152 | (ok {:kikka kikka 153 | :kukka kukka 154 | :kakka kakka})))) 155 | 156 | (context "/echo" [] 157 | :tags ["echo"] 158 | 159 | (POST "/recursion" [] 160 | :return Recursive 161 | :body [body (describe Recursive "Recursive Schema")] 162 | :summary "echoes a the json-body" 163 | (ok body)) 164 | 165 | (PUT "/anonymous" [] 166 | :return [{:hot Boolean}] 167 | :body [body [{:hot (s/either Boolean String)}]] 168 | :summary "echoes a vector of anonymous hotties" 169 | (ok body))) 170 | 171 | (context "/foreign" [] 172 | :tags ["foreign"] 173 | 174 | (POST "/pizza" [] 175 | :summary "Foreign schema with unknown subschemas" 176 | :return (s/maybe Pizza) 177 | :body [body Pizza] 178 | (ok body))) 179 | 180 | (context "/foreign" [] 181 | :tags ["abc"] 182 | 183 | (GET "/abc" [] 184 | :summary "Foreign schema with unknown subschemas" 185 | :return (s/maybe Pizza) 186 | (ok)) 187 | (GET "/info" [] 188 | :summary "from examples.thingie ns" 189 | (ok {:source "examples.thingie"}))) 190 | 191 | (context "/upload" [] 192 | :tags ["upload"] 193 | 194 | (POST "/file" [] 195 | :summary "ring-based file upload" 196 | :multipart-params [foo :- TempFileUpload] 197 | :middleware [wrap-multipart-params] 198 | (ok (dissoc foo :tempfile))) 199 | 200 | (POST "/byte-array" [] 201 | :summary "ring-based byte-array upload" 202 | :multipart-params [foo :- ByteArrayUpload] 203 | :middleware [[wrap-multipart-params {:store (ring.middleware.multipart-params.byte-array/byte-array-store)}]] 204 | (ok (dissoc foo :bytes)))) 205 | 206 | (context "/download" [] 207 | :tags ["download"] 208 | 209 | (GET "/file" [] 210 | :summary "file download" 211 | :return File 212 | :produces ["image/png"] 213 | (-> (io/resource "screenshot.png") 214 | (io/input-stream) 215 | (ok) 216 | (header "Content-Type" "image/png")))) 217 | 218 | (context "/component" [] 219 | :tags ["component"] 220 | (GET "/example" [] 221 | :components [example] 222 | (ok example))) 223 | 224 | ordered-routes)) 225 | -------------------------------------------------------------------------------- /test/compojure/api/dev/gen.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.dev.gen 2 | (:require [clojure.string :as str] 3 | [clojure.set :as set] 4 | [clojure.walk :as walk])) 5 | 6 | (def impl-local-sym '+impl+) 7 | 8 | (defn normalize-argv [argv] 9 | {:post [(or (empty? %) 10 | (apply distinct? %)) 11 | (not-any? #{impl-local-sym} %)]} 12 | (into [] (map-indexed (fn [i arg] 13 | (if (symbol? arg) 14 | (do (assert (not (namespace arg))) 15 | (if (some #(Character/isDigit (char %)) (name arg)) 16 | (symbol (apply str (concat 17 | (remove #(Character/isDigit (char %)) (name arg)) 18 | [i]))) 19 | arg)) 20 | (symbol (str "arg" i))))) 21 | argv)) 22 | 23 | (defn normalize-arities [arities] 24 | (cond-> arities 25 | (= 1 (count arities)) first)) 26 | 27 | (defn import-fn [sym] 28 | {:pre [(namespace sym)]} 29 | (let [vr (find-var sym) 30 | m (meta vr) 31 | n (:name m) 32 | arglists (:arglists m) 33 | protocol (:protocol m) 34 | when-class (-> sym meta :when-class) 35 | _ (assert (not when-class)) 36 | forward-meta (into (sorted-map) (select-keys m [:tag :arglists :doc :deprecated])) 37 | _ (assert (not= n impl-local-sym)) 38 | _ (when (:macro m) 39 | (throw (IllegalArgumentException. 40 | (str "Calling import-fn on a macro: " sym)))) 41 | form (if protocol 42 | (list* 'defn (with-meta n (dissoc forward-meta :arglists)) 43 | (map (fn [argv] 44 | {:pre [(not-any? #{'&} argv)]} 45 | (list argv (list* sym argv))) 46 | arglists)) 47 | (list 'def (with-meta n forward-meta) sym))] 48 | (cond->> form 49 | #_#_when-class (list 'java-time.util/when-class when-class)))) 50 | 51 | (defn import-macro [sym] 52 | (let [vr (find-var sym) 53 | m (meta vr) 54 | _ (when-not (:macro m) 55 | (throw (IllegalArgumentException. 56 | (str "Calling import-macro on a non-macro: " sym)))) 57 | n (:name m) 58 | arglists (:arglists m)] 59 | (list* 'defmacro n 60 | (concat 61 | (some-> (not-empty (into (sorted-map) (select-keys m [:doc :deprecated]))) 62 | list) 63 | (normalize-arities 64 | (map (fn [argv] 65 | (let [argv (normalize-argv argv)] 66 | (list argv 67 | (if (some #{'&} argv) 68 | (list* 'list* (list 'quote sym) (remove #{'&} argv)) 69 | (list* 'list (list 'quote sym) argv))))) 70 | arglists)))))) 71 | 72 | (defn import-vars 73 | "Imports a list of vars from other namespaces." 74 | [& syms] 75 | (let [unravel (fn unravel [x] 76 | (if (sequential? x) 77 | (->> x 78 | rest 79 | (mapcat unravel) 80 | (map 81 | #(with-meta 82 | (symbol 83 | (str (first x) 84 | (when-let [n (namespace %)] 85 | (str "." n))) 86 | (name %)) 87 | (meta %)))) 88 | [x])) 89 | syms (mapcat unravel syms)] 90 | (map (fn [sym] 91 | (let [vr (if-some [rr (resolve 'clojure.core/requiring-resolve)] 92 | (rr sym) 93 | (do (require (-> sym namespace symbol)) 94 | (resolve sym))) 95 | _ (assert vr (str sym " is unresolvable")) 96 | m (meta vr)] 97 | (if (:macro m) 98 | (import-macro sym) 99 | (import-fn sym)))) 100 | syms))) 101 | 102 | (def compojure-api-sweet-impl-info 103 | {:vars '([compojure.api.core routes defroutes let-routes undocumented middleware route-middleware 104 | context GET ANY HEAD PATCH DELETE OPTIONS POST PUT] 105 | [compojure.api.api api defapi] 106 | [compojure.api.resource resource] 107 | [compojure.api.routes path-for] 108 | [compojure.api.swagger swagger-routes] 109 | [ring.swagger.json-schema describe])}) 110 | 111 | (defn gen-compojure-api-sweet-ns-forms [nsym] 112 | (concat 113 | [";; NOTE: This namespace is generated by compojure.api.dev.gen" 114 | `(~'ns ~nsym 115 | (:require compojure.api.core 116 | compojure.api.api 117 | compojure.api.routes 118 | compojure.api.resource 119 | compojure.api.swagger 120 | ring.swagger.json-schema))] 121 | (apply import-vars (:vars compojure-api-sweet-impl-info)))) 122 | 123 | (def compojure-api-upload-impl-info 124 | {:vars '([ring.middleware.multipart-params wrap-multipart-params] 125 | [ring.swagger.upload TempFileUpload ByteArrayUpload])}) 126 | 127 | (defn gen-compojure-api-upload-ns-forms [nsym] 128 | (concat 129 | [";; NOTE: This namespace is generated by compojure.api.dev.gen" 130 | `(~'ns ~nsym 131 | (:require ring.middleware.multipart-params 132 | ring.swagger.upload))] 133 | (apply import-vars (:vars compojure-api-upload-impl-info)))) 134 | 135 | (defn print-form [form] 136 | (with-bindings 137 | (cond-> {#'*print-meta* true 138 | #'*print-length* nil 139 | #'*print-level* nil} 140 | (resolve '*print-namespace-maps*) 141 | (assoc (resolve '*print-namespace-maps*) false)) 142 | (cond 143 | (string? form) (println form) 144 | :else (println (pr-str (walk/postwalk 145 | (fn [v] 146 | (if (meta v) 147 | (if (symbol? v) 148 | (vary-meta v #(not-empty 149 | (cond-> (sorted-map) 150 | (some? (:tag %)) (assoc :tag (:tag %)) 151 | (some? (:doc %)) (assoc :doc (:doc %)) 152 | ((some-fn true? string?) (:deprecated %)) (assoc :deprecated (:deprecated %)) 153 | (string? (:superseded-by %)) (assoc :superseded-by (:superseded-by %)) 154 | (string? (:supercedes %)) (assoc :supercedes (:supercedes %)) 155 | (some? (:arglists %)) (assoc :arglists (list 'quote (doall (map normalize-argv (:arglists %)))))))) 156 | (with-meta v nil)) 157 | v)) 158 | form))))) 159 | nil) 160 | 161 | (defn print-compojure-api-ns [{:keys [f nsym]}] 162 | (assert f) 163 | (run! print-form (f nsym))) 164 | 165 | (def compojure-api-sweet-nsym 166 | (with-meta 167 | 'compojure.api.sweet 168 | ;;TODO ns meta 169 | nil)) 170 | 171 | (def compojure-api-upload-nsym 172 | (with-meta 173 | 'compojure.api.upload 174 | ;;TODO ns meta 175 | nil)) 176 | 177 | (def compojure-api-sweet-conf {:nsym compojure-api-sweet-nsym 178 | :f #'gen-compojure-api-sweet-ns-forms}) 179 | (def compojure-api-upload-conf {:nsym compojure-api-upload-nsym 180 | :f #'gen-compojure-api-upload-ns-forms}) 181 | 182 | (def gen-source->nsym 183 | {"src/compojure/api/sweet.clj" compojure-api-sweet-conf 184 | "src/compojure/api/upload.clj" compojure-api-upload-conf}) 185 | 186 | (defn spit-compojure-api-ns [] 187 | (doseq [[source conf] gen-source->nsym] 188 | (spit source (with-out-str (print-compojure-api-ns conf))))) 189 | 190 | (comment 191 | (print-compojure-api-ns compojure-api-sweet-conf) 192 | (print-compojure-api-ns compojure-api-upload-conf) 193 | (spit-compojure-api-ns) 194 | ) 195 | -------------------------------------------------------------------------------- /src/compojure/api/routes.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.routes 2 | (:require [compojure.core :refer :all] 3 | [clojure.string :as string] 4 | [compojure.api.methods :as methods] 5 | [compojure.api.request :as request] 6 | [compojure.api.impl.logging :as logging] 7 | [compojure.api.impl.json :as json] 8 | [compojure.api.common :as common] 9 | [muuntaja.core :as m] 10 | [ring.swagger.common :as rsc] 11 | [clojure.string :as str] 12 | [linked.core :as linked] 13 | [compojure.response] 14 | [schema.core :as s] 15 | [compojure.api.coercion :as coercion]) 16 | (:import (clojure.lang AFn IFn Var IDeref) 17 | (java.io Writer))) 18 | 19 | ;; 20 | ;; Route records 21 | ;; 22 | 23 | (defn- ->path [path] 24 | (if-not (= path "/") path)) 25 | 26 | (defn- ->paths [p1 p2] 27 | (->path (str (->path p1) (->path p2)))) 28 | 29 | (defprotocol Routing 30 | (-get-routes [handler options])) 31 | 32 | (extend-protocol Routing 33 | nil 34 | (-get-routes [_ _] []) 35 | Var 36 | (-get-routes [this options] 37 | (-get-routes @this options))) 38 | 39 | (defn filter-routes [{:keys [childs] :as handler} {:keys [invalid-routes-fn]}] 40 | (let [[valid-childs invalid-childs] (common/group-with (partial satisfies? Routing) childs)] 41 | (when (and invalid-routes-fn invalid-childs) 42 | (invalid-routes-fn handler invalid-childs)) 43 | valid-childs)) 44 | 45 | (defn get-routes 46 | ([handler] 47 | (get-routes handler nil)) 48 | ([handler options] 49 | (mapv 50 | (fn [route] 51 | (update-in route [0] (fn [uri] (if (str/blank? uri) "/" uri)))) 52 | (-get-routes handler options)))) 53 | 54 | (defn get-static-context-routes 55 | ([handler] 56 | (get-static-context-routes handler nil)) 57 | ([handler options] 58 | (filter (fn [[_ _ info]] (get info :static-context?)) 59 | (get-routes handler options)))) 60 | 61 | (defn- realize-childs [route] 62 | (update route :childs #(if (instance? IDeref %) @% %))) 63 | 64 | (defn- filter-childs [route] 65 | (update route :childs (partial filter (partial satisfies? Routing)))) 66 | 67 | (defrecord Route [path method info childs handler] 68 | Routing 69 | (-get-routes [this options] 70 | (let [this (-> this realize-childs) 71 | valid-childs (filter-routes this options) 72 | make-method-path-fn (fn [m] [path m info])] 73 | (if (-> this filter-childs :childs seq) 74 | (vec 75 | (for [[p m i] (mapcat #(-get-routes % options) valid-childs)] 76 | [(->paths path p) m (rsc/deep-merge info i)])) 77 | (into [] (cond 78 | (and path method) [(make-method-path-fn method)] 79 | path (mapv make-method-path-fn methods/all-methods)))))) 80 | 81 | compojure.response/Renderable 82 | (render [_ request] 83 | (handler request)) 84 | 85 | ;; Sendable implementation in compojure.api.async 86 | 87 | IFn 88 | (invoke [_ request] 89 | (handler request)) 90 | (invoke [_ request respond raise] 91 | (handler request respond raise)) 92 | 93 | (applyTo [this args] 94 | (AFn/applyToHelper this args))) 95 | 96 | (defn create [path method info childs handler] 97 | (->Route path method info childs handler)) 98 | 99 | (defmethod print-method Route 100 | [this ^Writer w] 101 | (let [childs (some-> this realize-childs filter-childs :childs seq vec)] 102 | (.write w (str "#Route" 103 | (cond-> (dissoc this :handler :childs) 104 | (not (:path this)) (dissoc :path) 105 | (not (seq (:info this))) (dissoc :info) 106 | (not (:method this)) (dissoc :method) 107 | childs (assoc :childs childs)))))) 108 | 109 | ;; 110 | ;; Invalid route handlers 111 | ;; 112 | 113 | (defn fail-on-invalid-child-routes 114 | [handler invalid-childs] 115 | (throw (ex-info "Not all child routes satisfy compojure.api.routing/Routing." 116 | (merge (select-keys handler [:path :method]) {:invalid (vec invalid-childs)})))) 117 | 118 | (defn log-invalid-child-routes [handler invalid-childs] 119 | (logging/log! :warn (str "Not all child routes satisfy compojure.api.routing/Routing. " 120 | (select-keys handler [:path :method]) ", invalid child routes: " 121 | (vec invalid-childs)))) 122 | 123 | 124 | ;; 125 | ;; Swagger paths 126 | ;; 127 | 128 | (defn- path-params 129 | "Finds path-parameter keys in an uri. 130 | Regex copied from Clout and Ring-swagger." 131 | [s] 132 | (map (comp keyword second) (re-seq #":([\p{L}_][\p{L}_0-9-]*)" s))) 133 | 134 | (defn- string-path-parameters [uri] 135 | (let [params (path-params uri)] 136 | (if (seq params) 137 | (zipmap params (repeat String))))) 138 | 139 | (defn- ensure-path-parameters [path info] 140 | (if (seq (path-params path)) 141 | (update-in info [:parameters :path] #(dissoc (merge (string-path-parameters path) %) s/Keyword)) 142 | info)) 143 | 144 | (defn ring-swagger-paths [routes] 145 | {:paths 146 | (reduce 147 | (fn [acc [path method info]] 148 | (if-not (:no-doc info) 149 | (if-let [public-info (->> (get info :public {}) 150 | (coercion/get-apidocs (:coercion info) "swagger"))] 151 | (update-in 152 | acc [path method] 153 | (fn [old-info] 154 | (let [public-info (or old-info public-info)] 155 | (ensure-path-parameters path public-info)))) 156 | acc) 157 | acc)) 158 | (linked/map) 159 | routes)}) 160 | 161 | ;; 162 | ;; Route lookup 163 | ;; 164 | 165 | (defn- duplicates [seq] 166 | (for [[id freq] (frequencies seq) 167 | :when (> freq 1)] id)) 168 | 169 | (defn all-paths [routes] 170 | (reduce 171 | (fn [acc [path method info]] 172 | (let [public-info (get info :public {})] 173 | (update-in acc [path method] 174 | (fn [old-info] 175 | (let [public-info (or old-info public-info)] 176 | (ensure-path-parameters path public-info)))))) 177 | (linked/map) 178 | routes)) 179 | 180 | (defn route-lookup-table [routes] 181 | (let [entries (for [[path endpoints] (all-paths routes) 182 | [method {:keys [x-name parameters]}] endpoints 183 | :let [params (:path parameters)] 184 | :when x-name] 185 | [x-name {path (merge 186 | {:method method} 187 | (if params 188 | {:params params}))}]) 189 | route-names (map first entries) 190 | duplicate-route-names (duplicates route-names)] 191 | (when (seq duplicate-route-names) 192 | (throw (ex-info 193 | (str "Found multiple routes with same name: " 194 | (string/join "," duplicate-route-names)) 195 | {:entries entries}))) 196 | (into {} entries))) 197 | 198 | ;; 199 | ;; Endpoint Trasformers 200 | ;; 201 | 202 | (defn non-nil-routes [endpoint] 203 | (or endpoint {})) 204 | 205 | ;; 206 | ;; Bidirectional-routing 207 | ;; 208 | 209 | (defn- un-quote [s] 210 | (str/replace s #"^\"(.+(?=\"$))\"$" "$1")) 211 | 212 | (defn- path-string [m s params] 213 | (-> s 214 | (str/replace #":([^/]+)" " :$1 ") 215 | (str/split #" ") 216 | (->> (map 217 | (fn [[head :as token]] 218 | (if (= head \:) 219 | (let [key (keyword (subs token 1)) 220 | value (key params)] 221 | (if value 222 | (un-quote (slurp (m/encode m "application/json" value))) 223 | (throw 224 | (IllegalArgumentException. 225 | (str "Missing path-parameter " key " for path " s))))) 226 | token))) 227 | (apply str)))) 228 | 229 | (defn path-for* 230 | "Extracts the lookup-table from request and finds a route by name." 231 | [route-name request & [params]] 232 | (let [m (or (::request/muuntaja request) json/muuntaja) 233 | [path details] (some-> request 234 | ::request/lookup 235 | route-name 236 | first) 237 | path-params (:params details)] 238 | (if (seq path-params) 239 | (path-string m path params) 240 | path))) 241 | 242 | (defmacro path-for 243 | "Extracts the lookup-table from request and finds a route by name." 244 | [route-name & [params]] 245 | `(path-for* ~route-name ~'+compojure-api-request+ ~params)) 246 | -------------------------------------------------------------------------------- /test/compojure/api/swagger_test.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.swagger-test 2 | (:require [schema.core :as s] 3 | [compojure.api.sweet :refer :all] 4 | [compojure.api.swagger :as swagger] 5 | compojure.core 6 | [compojure.api.test-utils :refer :all] 7 | [clojure.test :refer [deftest is testing]])) 8 | 9 | (defmacro optional-routes [p & body] (when p `(routes ~@body))) 10 | (defmacro GET+ [p & body] `(GET ~(str "/xxx" p) ~@body)) 11 | 12 | (deftest extracting-compojure-paths-test 13 | 14 | (testing "all compojure.api.core macros are interpreted" 15 | (let [app (context "/a" [] 16 | (routes 17 | (context "/b" a 18 | (let-routes [] 19 | (GET "/c" [] identity) 20 | (POST "/d" [] identity) 21 | (PUT "/e" [] identity) 22 | (DELETE "/f" [] identity) 23 | (OPTIONS "/g" [] identity) 24 | (PATCH "/h" [] identity))) 25 | (context "/:i/:j" [] 26 | (GET "/k/:l/m/:n" [] identity))))] 27 | 28 | (is (= (extract-paths app) 29 | {"/a/b/c" {:get {}} 30 | "/a/b/d" {:post {}} 31 | "/a/b/e" {:put {}} 32 | "/a/b/f" {:delete {}} 33 | "/a/b/g" {:options {}} 34 | "/a/b/h" {:patch {}} 35 | "/a/:i/:j/k/:l/m/:n" {:get {:parameters {:path {:i String 36 | :j String 37 | :l String 38 | :n String}}}}})))) 39 | 40 | (testing "runtime code in route is NOT ignored" 41 | (is (= (extract-paths 42 | (context "/api" [] 43 | (if false 44 | (GET "/true" [] identity) 45 | (PUT "/false" [] identity)))) 46 | {"/api/false" {:put {}}}))) 47 | 48 | (testing "route-macros are expanded" 49 | (is (= (extract-paths 50 | (context "/api" [] 51 | (optional-routes true (GET "/true" [] identity)) 52 | (optional-routes false (PUT "/false" [] identity)))) 53 | {"/api/true" {:get {}}}))) 54 | 55 | (testing "endpoint-macros are expanded" 56 | (is (= (extract-paths 57 | (context "/api" [] 58 | (GET+ "/true" [] identity))) 59 | {"/api/xxx/true" {:get {}}}))) 60 | 61 | (testing "Vanilla Compojure defroutes are NOT followed" 62 | (compojure.core/defroutes even-more-routes (GET "/even" [] identity)) 63 | (compojure.core/defroutes more-routes (context "/more" [] even-more-routes)) 64 | (is (= (extract-paths 65 | (context "/api" [] 66 | (GET "/true" [] identity) 67 | more-routes)) 68 | {"/api/true" {:get {}}}))) 69 | 70 | (testing "Compojure Api defroutes and def routes are followed" 71 | (def even-more-routes (GET "/even" [] identity)) 72 | (defroutes more-routes (context "/more" [] even-more-routes)) 73 | (is (= (extract-paths 74 | (context "/api" [] 75 | (GET "/true" [] identity) 76 | more-routes)) 77 | {"/api/true" {:get {}} 78 | "/api/more/even" {:get {}}}))) 79 | 80 | (testing "Parameter regular expressions are discarded" 81 | (is (= (extract-paths 82 | (context "/api" [] 83 | (GET ["/:param" :param #"[a-z]+"] [] identity))) 84 | {"/api/:param" {:get {:parameters {:path {:param String}}}}})))) 85 | 86 | (deftest context-meta-data-test-1 87 | (is (= (extract-paths 88 | (context "/api/:id" [] 89 | :summary "top-summary" 90 | :path-params [id :- String] 91 | :tags [:kiss] 92 | (GET "/kikka" [] 93 | identity) 94 | (context "/ipa" [] 95 | :summary "mid-summary" 96 | :tags [:wasp] 97 | (GET "/kukka/:kukka" [] 98 | :summary "bottom-summary" 99 | :path-params [kukka :- String] 100 | :tags [:venom]) 101 | (GET "/kakka" [] 102 | identity)))) 103 | 104 | {"/api/:id/kikka" {:get {:summary "top-summary" 105 | :tags #{:kiss} 106 | :parameters {:path {:id String}}}} 107 | "/api/:id/ipa/kukka/:kukka" {:get {:summary "bottom-summary" 108 | :tags #{:venom} 109 | :parameters {:path {:id String 110 | :kukka String}}}} 111 | "/api/:id/ipa/kakka" {:get {:summary "mid-summary" 112 | :tags #{:wasp} 113 | :parameters {:path {:id String}}}}}))) 114 | 115 | (deftest duplicate-context-merge-test 116 | (let [app (routes 117 | (context "/api" [] 118 | :tags [:kiss] 119 | (GET "/kakka" [] 120 | identity)) 121 | (context "/api" [] 122 | :tags [:kiss] 123 | (GET "/kukka" [] 124 | identity)))] 125 | (is (= (extract-paths app) 126 | {"/api/kukka" {:get {:tags #{:kiss}}} 127 | "/api/kakka" {:get {:tags #{:kiss}}}})))) 128 | 129 | (def r1 130 | (GET "/:id" [] 131 | :path-params [id :- s/Str] 132 | identity)) 133 | (def r2 134 | (GET "/kukka/:id" [] 135 | :path-params [id :- Long] 136 | identity)) 137 | 138 | (deftest defined-routes-path-params-test 139 | (is (= (extract-paths (routes r1 r2)) 140 | {"/:id" {:get {:parameters {:path {:id String}}}} 141 | "/kukka/:id" {:get {:parameters {:path {:id Long}}}}}))) 142 | 143 | ;;FIXME is this a duplicate of context-meta-data-test-1? 144 | (deftest context-meta-data-test-2 145 | (is (= (extract-paths 146 | (context "/api/:id" [] 147 | :summary "top-summary" 148 | :path-params [id :- String] 149 | :tags [:kiss] 150 | (GET "/kikka" [] 151 | identity) 152 | (context "/ipa" [] 153 | :summary "mid-summary" 154 | :tags [:wasp] 155 | (GET "/kukka/:kukka" [] 156 | :summary "bottom-summary" 157 | :path-params [kukka :- String] 158 | :tags [:venom]) 159 | (GET "/kakka" [] 160 | identity)))) 161 | 162 | {"/api/:id/kikka" {:get {:summary "top-summary" 163 | :tags #{:kiss} 164 | :parameters {:path {:id String}}}} 165 | "/api/:id/ipa/kukka/:kukka" {:get {:summary "bottom-summary" 166 | :tags #{:venom} 167 | :parameters {:path {:id String 168 | :kukka String}}}} 169 | "/api/:id/ipa/kakka" {:get {:summary "mid-summary" 170 | :tags #{:wasp} 171 | :parameters {:path {:id String}}}}}))) 172 | 173 | (deftest path-params-followed-by-an-extension-test 174 | (is (= (extract-paths 175 | (GET "/:foo.json" [] 176 | :path-params [foo :- String] 177 | identity)) 178 | {"/:foo.json" {:get {:parameters {:path {:foo String}}}}}))) 179 | 180 | (deftest swagger-routes-basePath-test 181 | (testing "swagger-routes basePath can be changed" 182 | (doseq [[?given-options ?expected-swagger-docs-path ?expected-base-path :as test-case] 183 | [[{} "/swagger.json" "/" {:data {:basePath "/app"}} "/app/swagger.json" "/app"] 184 | [{:data {:basePath "/app"} :options {:ui {:swagger-docs "/imaginary.json"}}} "/imaginary.json" "/app"]]] 185 | (testing (pr-str test-case) 186 | (let [app (api {} (swagger-routes ?given-options))] 187 | (is (= (-> 188 | (get* app "/swagger.json") 189 | (nth 1) 190 | :basePath) 191 | ?expected-base-path)) 192 | (is (= (nth (raw-get* app "/conf.js") 1) 193 | (str "window.API_CONF = {\"url\":\"" ?expected-swagger-docs-path "\"};")))))))) 194 | 195 | ;;"change of contract in 1.2.0 with swagger-docs % swagger-ui" 196 | (deftest change-1-2-0-swagger-docs-ui-test 197 | (testing "swagger-ui" 198 | (is (thrown? AssertionError (swagger/swagger-ui "/path")))) 199 | (testing "swagger-docs" 200 | (is (thrown? AssertionError (swagger/swagger-docs "/path"))))) 201 | -------------------------------------------------------------------------------- /src/compojure/api/resource.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.resource 2 | (:require [compojure.api.routes :as routes] 3 | [compojure.api.coercion :as coercion] 4 | [compojure.api.methods :as methods] 5 | [ring.swagger.common :as rsc] 6 | [schema.core :as s] 7 | [plumbing.core :as p] 8 | [compojure.api.async] 9 | [compojure.api.middleware :as mw] 10 | [compojure.api.coercion.core :as cc])) 11 | 12 | (def ^:private +mappings+ 13 | {:methods methods/all-methods 14 | :parameters {:query-params [:query-params :query :string true] 15 | :body-params [:body-params :body :body false] 16 | :form-params [:form-params :formData :string true] 17 | :header-params [:headers :header :string true] 18 | :path-params [:route-params :path :string true]}}) 19 | 20 | (defn- swaggerize [info] 21 | (as-> info info 22 | (reduce-kv 23 | (fn [acc ring-key [_ swagger-key]] 24 | (if-let [schema (get-in acc [:parameters ring-key])] 25 | (update acc :parameters #(-> % (dissoc ring-key) (assoc swagger-key schema))) 26 | acc)) 27 | info 28 | (:parameters +mappings+)) 29 | (dissoc info :handler))) 30 | 31 | (defn- inject-coercion [request info] 32 | (if (contains? info :coercion) 33 | (coercion/set-request-coercion request (:coercion info)) 34 | request)) 35 | 36 | (defn- coerce-request [request info ks] 37 | (reduce-kv 38 | (fn [request ring-key [compojure-key _ type open?]] 39 | (if-let [model (get-in info (concat ks [:parameters ring-key]))] 40 | (let [coerced (coercion/coerce-request! 41 | model compojure-key type (not= :body type) open? request)] 42 | (if open? 43 | (update request ring-key merge coerced) 44 | (assoc request ring-key coerced))) 45 | request)) 46 | (inject-coercion request info) 47 | (:parameters +mappings+))) 48 | 49 | (defn- coerce-response [response info request ks] 50 | (coercion/coerce-response! request response (get-in info (concat ks [:responses])))) 51 | 52 | (defn- maybe-async [async? x] 53 | (if (and async? x) [x true])) 54 | 55 | (defn- maybe-sync [x] 56 | (if x [x false])) 57 | 58 | (defn- resolve-handler [info path-info route request-method async?] 59 | (and 60 | (or 61 | ;; directly under a context 62 | (= path-info "/") 63 | ;; under an compojure endpoint 64 | route 65 | ;; vanilla ring 66 | (nil? path-info)) 67 | (let [[handler async] (or 68 | (maybe-async async? (get-in info [request-method :async-handler])) 69 | (maybe-sync (get-in info [request-method :handler])) 70 | (maybe-async async? (get-in info [:async-handler])) 71 | (maybe-sync (get-in info [:handler])))] 72 | (if handler 73 | [handler async])))) 74 | 75 | (defn- middleware-chain [info request-method handler] 76 | (let [direct-mw (:middleware info) 77 | method-mw (:middleware (get info request-method)) 78 | middleware (mw/compose-middleware (concat direct-mw method-mw))] 79 | (middleware handler))) 80 | 81 | (defn- create-childs [info] 82 | (map 83 | (fn [[method info]] 84 | (routes/map->Route 85 | {:path "/" 86 | :method method 87 | :info {:public (swaggerize info)}})) 88 | (select-keys info (:methods +mappings+)))) 89 | 90 | (defn- handle-sync [info {:keys [request-method path-info :compojure/route] :as request}] 91 | (when-let [[raw-handler] (resolve-handler info path-info route request-method false)] 92 | (let [ks (if (contains? info request-method) [request-method] []) 93 | handler (middleware-chain info request-method raw-handler)] 94 | (-> (coerce-request request info ks) 95 | (handler) 96 | (compojure.response/render request) 97 | (coerce-response info request ks))))) 98 | 99 | (defn- handle-async [info {:keys [request-method path-info :compojure/route] :as request} respond raise] 100 | (if-let [[raw-handler async?] (resolve-handler info path-info route request-method true)] 101 | (let [ks (if (contains? info request-method) [request-method] []) 102 | respond-coerced (fn [response] 103 | (respond 104 | (try (coerce-response response info request ks) 105 | (catch Throwable e (raise e))))) 106 | handler (middleware-chain info request-method raw-handler)] 107 | (try 108 | (as-> (coerce-request request info ks) $ 109 | (if async? 110 | (handler $ #(compojure.response/send % $ respond-coerced raise) raise) 111 | (compojure.response/send (handler $) $ respond-coerced raise))) 112 | (catch Throwable e 113 | (raise e)))) 114 | (respond nil))) 115 | 116 | (defn- create-handler [info] 117 | (fn 118 | ([request] 119 | (handle-sync info request)) 120 | ([request respond raise] 121 | (handle-async info request respond raise)))) 122 | 123 | (defn- merge-parameters-and-responses [info] 124 | (let [methods (select-keys info (:methods +mappings+))] 125 | (-> info 126 | (merge 127 | (p/for-map [[method method-info] methods 128 | :let [responses (merge 129 | (:responses info) 130 | (:responses method-info))]] 131 | method (cond-> (->> method-info (rsc/deep-merge (select-keys info [:parameters]))) 132 | (seq responses) (assoc :responses responses))))))) 133 | 134 | (defn- public-root-info [info] 135 | (-> (reduce dissoc info (:methods +mappings+)) 136 | (dissoc :parameters :responses :coercion))) 137 | 138 | ;; 139 | ;; Public api 140 | ;; 141 | 142 | (s/defschema Options 143 | {(s/optional-key :coercion) s/Any}) 144 | 145 | ; TODO: validate input against ring-swagger schema, fail for missing handlers 146 | ; TODO: extract parameter schemas from handler fnks? 147 | (defn resource 148 | "Creates a nested compojure-api Route from enchanced ring-swagger operations map. 149 | By default, applies both request- and response-coercion based on those definitions. 150 | 151 | Extra keys: 152 | 153 | - **:middleware** Middleware in duct-format either at top-level or under methods. 154 | Top-level mw are applied first if route matches, method-level 155 | mw are applied next if method matches 156 | 157 | - **:coercion** A named coercion or instance of Coercion 158 | in resource coercion for :body, :string and :response. 159 | Setting value to `nil` disables both request- & 160 | response coercion. See tests and wiki for details. 161 | 162 | Enhancements to ring-swagger operations map: 163 | 164 | 1) :parameters use ring request keys (query-params, path-params, ...) instead of 165 | swagger-params (query, path, ...). This keeps things simple as ring keys are used in 166 | the handler when destructuring the request. 167 | 168 | 2) at resource root, one can add any ring-swagger operation definitions, which will be 169 | available for all operations, using the following rules: 170 | 171 | 2.1) :parameters are deep-merged into operation :parameters 172 | 2.2) :responses are merged into operation :responses (operation can fully override them) 173 | 2.3) all others (:produces, :consumes, :summary,...) are deep-merged by compojure-api 174 | 175 | 3) special keys `:handler` and/or `:async-handler` either under operations or at top-level. 176 | They should be 1-ary and 3-ary Ring handler functions, respectively, that are responsible 177 | for the actual request processing. Handler lookup order is the following: 178 | 179 | 3.1) If called asynchronously, operations-level :async-handler 180 | 3.2) Operations-level :handler 181 | 3.3) If called asynchronously, top-level :async-handler 182 | 3.4) Top-level :handler 183 | 184 | 4) request-coercion is applied once, using deep-merged parameters for a given 185 | operation or resource-level if only resource-level handler is defined. 186 | 187 | 5) response-coercion is applied once, using merged responses for a given 188 | operation or resource-level if only resource-level handler is defined. 189 | 190 | Note: Swagger operations are generated only from declared operations (:get, :post, ..), 191 | despite the top-level handler could process more operations. 192 | 193 | Example: 194 | 195 | (resource 196 | {:parameters {:query-params {:x Long}} 197 | :responses {500 {:schema {:reason s/Str}}} 198 | :get {:parameters {:query-params {:y Long}} 199 | :responses {200 {:schema {:total Long}}} 200 | :handler (fn [request] 201 | (ok {:total (+ (-> request :query-params :x) 202 | (-> request :query-params :y))}))} 203 | :post {} 204 | :handler (constantly 205 | (internal-server-error {:reason \"not implemented\"}))})" 206 | [data] 207 | (let [data (merge-parameters-and-responses data) 208 | public-info (swaggerize (public-root-info data)) 209 | info (merge {:public public-info} (select-keys data [:coercion])) 210 | childs (create-childs data) 211 | handler (create-handler data)] 212 | (routes/map->Route 213 | {:info info 214 | :childs childs 215 | :handler handler}))) 216 | -------------------------------------------------------------------------------- /test/compojure/api/routes_test.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.routes-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [compojure.api.sweet :refer :all] 4 | [compojure.api.routes :as routes] 5 | [ring.util.http-response :refer :all] 6 | [ring.util.http-predicates :refer :all] 7 | [compojure.api.test-utils :refer :all] 8 | [schema.core :as s] 9 | [jsonista.core :as j]) 10 | (:import (org.joda.time LocalDate) 11 | (clojure.lang ExceptionInfo))) 12 | 13 | (deftest path-string-test 14 | 15 | (testing "missing path parameter" 16 | (is (thrown? IllegalArgumentException (#'routes/path-string muuntaja "/api/:kikka" {})))) 17 | 18 | (testing "missing serialization" 19 | (is (thrown-with-msg? 20 | ExceptionInfo #"Malformed application/json" 21 | (#'routes/path-string muuntaja "/api/:kikka" {:kikka (reify Comparable)})))) 22 | 23 | (testing "happy path" 24 | (is (= "/a/2015-05-22/12345/d/kikka/f" 25 | (#'routes/path-string muuntaja "/a/:b/:c/d/:e/f" {:b (LocalDate/parse "2015-05-22") 26 | :c 12345 27 | :e :kikka}))))) 28 | 29 | (deftest string-path-parameters-test 30 | (is (= {:foo String} (#'routes/string-path-parameters "/:foo.json")))) 31 | 32 | (deftest nested-routes-test 33 | (let [mw (fn [handler] 34 | (fn ([request] (handler request)) 35 | ([request raise respond] (handler request raise respond)))) 36 | more-routes (fn [version] 37 | (routes 38 | (GET "/more" [] 39 | (ok {:message version})))) 40 | routes (context "/api/:version" [] 41 | :path-params [version :- String] 42 | (GET "/ping" [] 43 | (ok {:message (str "pong - " version)})) 44 | (POST "/ping" [] 45 | (ok {:message (str "pong - " version)})) 46 | (ANY "/foo" [] 47 | (ok {:message (str "bar - " version)})) 48 | (route-middleware [mw] 49 | (GET "/hello" [] 50 | :return {:message String} 51 | :summary "cool ping" 52 | :query-params [name :- String] 53 | (ok {:message (str "Hello, " name)})) 54 | (more-routes version))) 55 | app (api 56 | {} 57 | (swagger-routes) 58 | routes)] 59 | 60 | (testing "all routes can be invoked" 61 | (let [[status body] (get* app "/api/v1/hello" {:name "Tommi"})] 62 | (is (= 200 status)) 63 | (is (= body {:message "Hello, Tommi"}))) 64 | 65 | (let [[status body] (get* app "/api/v1/ping")] 66 | (is (= status 200)) 67 | (is (= body {:message "pong - v1"}))) 68 | 69 | (let [[status body] (get* app "/api/v2/ping")] 70 | (is (= status 200)) 71 | (is (= body {:message "pong - v2"}))) 72 | 73 | (let [[status body] (get* app "/api/v3/more")] 74 | (is (= status 200)) 75 | (is (= body {:message "v3"})))) 76 | 77 | (testing "routes can be extracted at runtime" 78 | (is (= [["/swagger.json" :get {:no-doc true 79 | :coercion :schema 80 | :name :compojure.api.swagger/swagger 81 | :public {:x-name :compojure.api.swagger/swagger}}] 82 | ["/api/:version/ping" :get {:coercion :schema 83 | :public {:parameters {:path {:version String, s/Keyword s/Any}}}}] 84 | ["/api/:version/ping" :post {:coercion :schema 85 | :public {:parameters {:path {:version String, s/Keyword s/Any}}}}] 86 | ;; 'ANY' expansion 87 | ["/api/:version/foo" :get {:coercion :schema 88 | :public {:parameters {:path {:version String, s/Keyword s/Any}}}}] 89 | ["/api/:version/foo" :patch {:coercion :schema 90 | :public {:parameters {:path {:version String, s/Keyword s/Any}}}}] 91 | ["/api/:version/foo" :delete {:coercion :schema 92 | :public {:parameters {:path {:version String, s/Keyword s/Any}}}}] 93 | ["/api/:version/foo" :head {:coercion :schema 94 | :public {:parameters {:path {:version String, s/Keyword s/Any}}}}] 95 | ["/api/:version/foo" :post {:coercion :schema 96 | :public {:parameters {:path {:version String, s/Keyword s/Any}}}}] 97 | ["/api/:version/foo" :options {:coercion :schema 98 | :public {:parameters {:path {:version String, s/Keyword s/Any}}}}] 99 | ["/api/:version/foo" :put {:coercion :schema 100 | :public {:parameters {:path {:version String, s/Keyword s/Any}}}}] 101 | ;; 102 | ["/api/:version/hello" :get {:coercion :schema 103 | :public {:parameters {:query {:name String, s/Keyword s/Any} 104 | :path {:version String, s/Keyword s/Any}} 105 | :responses {200 {:description "", :schema {:message String}}} 106 | :summary "cool ping"}}] 107 | ["/api/:version/more" :get {:coercion :schema 108 | :public {:parameters {:path {:version String, s/Keyword s/Any}}}}]] 109 | (routes/get-routes app)))) 110 | 111 | (testing "swagger-docs can be generated" 112 | (is (= (sort ["/api/{version}/ping" 113 | "/api/{version}/foo" 114 | "/api/{version}/hello" 115 | "/api/{version}/more"]) 116 | (-> app get-spec :paths keys sort)))))) 117 | 118 | (def more-routes 119 | (routes 120 | (GET "/more" [] 121 | (ok {:gary "moore"})))) 122 | 123 | (deftest issue-219-test ;"following var-routes, #219" 124 | (let [routes (context "/api" [] #'more-routes)] 125 | (is (= (routes/get-routes routes) [["/api/more" :get {:static-context? true}]])))) 126 | 127 | (deftest dynamic-routes-test 128 | (let [more-routes (fn [version] 129 | (GET (str "/" version) [] 130 | (ok {:message version}))) 131 | routes (context "/api/:version" [] 132 | :path-params [version :- String] 133 | (more-routes version)) 134 | app (api 135 | {} 136 | (swagger-routes) 137 | routes)] 138 | 139 | (testing "all routes can be invoked" 140 | (let [[status body] (get* app "/api/v3/v3")] 141 | (is (= status 200)) 142 | (is (= body {:message "v3"}))) 143 | 144 | (let [[status body] (get* app "/api/v6/v6")] 145 | (is (= status 200)) 146 | (is (= body {:message "v6"})))) 147 | 148 | (testing "routes can be extracted at runtime" 149 | (is (= (routes/get-routes app) 150 | [["/swagger.json" :get {:no-doc true, 151 | :coercion :schema 152 | :name :compojure.api.swagger/swagger 153 | :public {:x-name :compojure.api.swagger/swagger}}] 154 | ["/api/:version/[]" :get {:coercion :schema 155 | :public {:parameters {:path {:version String, s/Keyword s/Any}}}}]]))) 156 | 157 | (testing "swagger-docs can be generated" 158 | (is (= (-> app get-spec :paths keys) 159 | ["/api/{version}/[]"]))))) 160 | 161 | (deftest route-merging-test 162 | (is (= (routes/get-routes (routes (routes))) [])) 163 | (is (= (routes/get-routes (routes (swagger-routes {:spec nil}))) [])) 164 | (is (= (routes/get-routes (routes (routes (GET "/ping" [] "pong")))) [["/ping" :get {}]]))) 165 | 166 | (deftest invalid-route-options-test 167 | (let [r (routes (constantly nil))] 168 | 169 | (testing "ignore 'em all" 170 | (is (= (routes/get-routes r) [])) 171 | (is (= (routes/get-routes r nil) [])) 172 | (is (= (routes/get-routes r {:invalid-routes-fn nil}) []))) 173 | 174 | (testing "log warnings" 175 | (let [a (atom [])] 176 | (with-redefs [compojure.api.impl.logging/log! (fn [& args] (swap! a conj args))] 177 | (is (= [] (routes/get-routes r {:invalid-routes-fn routes/log-invalid-child-routes})))) 178 | (is (= 1 (count @a))))) 179 | 180 | (testing "throw exception" 181 | (is (thrown? Exception (routes/get-routes r {:invalid-routes-fn routes/fail-on-invalid-child-routes})))))) 182 | 183 | (deftest context-routes-with-compojure-destructuring-test 184 | (let [app (context "/api" req 185 | (GET "/ping" [] (ok (:magic req))))] 186 | (is (= {:just "works"} 187 | (:body (app {:request-method :get :uri "/api/ping" :magic {:just "works"}})))))) 188 | 189 | (deftest dynamic-context-routes-test 190 | (let [endpoint? (atom true) 191 | app (context "/api" [] 192 | :dynamic true 193 | (when @endpoint? 194 | (GET "/ping" [] (ok "pong"))))] 195 | (testing "the endpoint exists" 196 | (is (= (:body (app {:request-method :get :uri "/api/ping"})) "pong"))) 197 | 198 | (reset! endpoint? false) 199 | (testing "the endpoint does not exist" 200 | (is (= (app {:request-method :get :uri "/api/ping"}) nil))))) 201 | 202 | (deftest listing-static-context-routes-test 203 | (let [app (routes 204 | (context "/static" [] 205 | (GET "/ping" [] (ok "pong"))) 206 | (context "/dynamic" req 207 | (GET "/ping" [] (ok "pong"))))] 208 | (is (= (routes/get-static-context-routes app) 209 | [["/static/ping" :get {:static-context? true}]])))) 210 | -------------------------------------------------------------------------------- /dev-resources/json/json10k.json: -------------------------------------------------------------------------------- 1 | {"results":[{"gender":"male","name":{"title":"mr","first":"علی رضا","last":"محمدخان"},"location":{"street":"4326 پارک دانشجو","city":"آبادان","state":"خوزستان","postcode":36902},"email":"علی رضا.محمدخان@example.com","login":{"username":"lazypeacock819","password":"taxman","salt":"peX2qakA","md5":"0c39ef1320ec7f799065f3b3385a2f4e","sha1":"cd51cda2b75943b111707094d8a5652542d2dff0","sha256":"a1f97d14b6a878b963482de8d6aa789f5baaf9872e510031028b213241787a73"},"dob":"1953-04-25 02:27:20","registered":"2004-10-03 21:41:05","phone":"050-46102037","cell":"0990-753-1209","id":{"name":"","value":null},"picture":{"large":"https://randomuser.me/api/portraits/men/24.jpg","medium":"https://randomuser.me/api/portraits/med/men/24.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/24.jpg"},"nat":"IR"},{"gender":"female","name":{"title":"ms","first":"andrea","last":"peña"},"location":{"street":"7112 calle de la almudena","city":"la palma","state":"islas baleares","postcode":63878},"email":"andrea.peña@example.com","login":{"username":"purpleleopard218","password":"harvard","salt":"hc9Uu10H","md5":"0d1c50b840053c61f68eb11b9ff5c44b","sha1":"5bd624f5e4567f6340cb00a07d6d9cdb2046b219","sha256":"839fee4ad0d19068b4dab72546bed62fe48ae69456e4b1b383125484510950b7"},"dob":"1953-01-27 09:21:17","registered":"2005-07-01 21:44:40","phone":"986-752-877","cell":"623-685-112","id":{"name":"DNI","value":"93357290-Y"},"picture":{"large":"https://randomuser.me/api/portraits/women/81.jpg","medium":"https://randomuser.me/api/portraits/med/women/81.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/81.jpg"},"nat":"ES"},{"gender":"female","name":{"title":"miss","first":"sue","last":"romero"},"location":{"street":"247 white oak dr","city":"mackay","state":"new south wales","postcode":1327},"email":"sue.romero@example.com","login":{"username":"orangefish402","password":"llll","salt":"AjO4nICn","md5":"19521ff63dc4e49e85c5a80ed219231c","sha1":"dde550b31afc7ff7d852e4b3252e1fa5070e00e3","sha256":"d7b83280c4eb3b095295af9b7ef51b306fc3c7df3843c26bfec320da42bbf16f"},"dob":"1952-01-13 15:26:37","registered":"2014-04-04 13:07:34","phone":"01-5269-1704","cell":"0492-962-203","id":{"name":"TFN","value":"599157736"},"picture":{"large":"https://randomuser.me/api/portraits/women/13.jpg","medium":"https://randomuser.me/api/portraits/med/women/13.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/13.jpg"},"nat":"AU"},{"gender":"female","name":{"title":"mrs","first":"donna","last":"murphy"},"location":{"street":"8988 north road","city":"greystones","state":"kilkenny","postcode":53668},"email":"donna.murphy@example.com","login":{"username":"silverkoala656","password":"33333333","salt":"wWUk5zn2","md5":"9172751676aa17561fdd3174dd4c9326","sha1":"5c073d90d70e6e72c70e8c08dde95734cdad840e","sha256":"6fe569f123ad796a7ff0649f542a2333ccacd92bd70ca6d52d64cd317cdd0a33"},"dob":"1969-09-12 21:14:56","registered":"2012-06-10 23:04:02","phone":"061-625-5539","cell":"081-759-2651","id":{"name":"PPS","value":"7736182T"},"picture":{"large":"https://randomuser.me/api/portraits/women/68.jpg","medium":"https://randomuser.me/api/portraits/med/women/68.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/68.jpg"},"nat":"IE"},{"gender":"female","name":{"title":"mademoiselle","first":"marion","last":"faure"},"location":{"street":"4623 rue de l'abbé-roger-derry","city":"gollion","state":"bern","postcode":1333},"email":"marion.faure@example.com","login":{"username":"yellowbear160","password":"747474","salt":"ZIx9zcNs","md5":"15969c42e7eaf5f2ce0492b86a165393","sha1":"7c578c86f87b28a0f396a0f14d1ce3a870b31d16","sha256":"fe1bc95f4fdbb789c80d5912c576074e03fa3ab509a6de3e6fe9ad256d67233d"},"dob":"1946-02-10 21:20:20","registered":"2005-03-07 01:57:56","phone":"(805)-136-2619","cell":"(066)-198-2825","id":{"name":"AVS","value":"756.TAAE.FXMI.39"},"picture":{"large":"https://randomuser.me/api/portraits/women/20.jpg","medium":"https://randomuser.me/api/portraits/med/women/20.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/20.jpg"},"nat":"CH"},{"gender":"female","name":{"title":"ms","first":"carmen","last":"ellis"},"location":{"street":"90 bruce st","city":"hobart","state":"south australia","postcode":7153},"email":"carmen.ellis@example.com","login":{"username":"smallrabbit833","password":"berkeley","salt":"wWW0PXS7","md5":"16cdf0978f51bcede9fe63fbf52c7744","sha1":"99bf5b7219f65c2f19a6e1b7bfe9642a8c6465a4","sha256":"d96f60965aedf1f82f2b59b1789e32230bd26495a1b7257df8efc1e92bcf537d"},"dob":"1960-12-15 09:15:17","registered":"2007-06-30 17:58:06","phone":"02-5809-6469","cell":"0428-863-957","id":{"name":"TFN","value":"346283503"},"picture":{"large":"https://randomuser.me/api/portraits/women/85.jpg","medium":"https://randomuser.me/api/portraits/med/women/85.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/85.jpg"},"nat":"AU"},{"gender":"female","name":{"title":"miss","first":"emma","last":"gregory"},"location":{"street":"8541 mill road","city":"birmingham","state":"berkshire","postcode":"T3E 2XL"},"email":"emma.gregory@example.com","login":{"username":"goldenrabbit210","password":"cameron1","salt":"UvELNRRe","md5":"0db2f9fd269b3c43d3b906f46ebf5d78","sha1":"4881220c87d85434553f31be9d79893a1f8e35fa","sha256":"a6349fffe4c01b245d2d7107059dac008711174fd88201c908736dedc3b6448e"},"dob":"1947-05-04 06:23:14","registered":"2015-03-16 01:40:15","phone":"016977 97466","cell":"0795-686-594","id":{"name":"NINO","value":"PL 94 41 63 V"},"picture":{"large":"https://randomuser.me/api/portraits/women/82.jpg","medium":"https://randomuser.me/api/portraits/med/women/82.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/82.jpg"},"nat":"GB"},{"gender":"female","name":{"title":"miss","first":"debbie","last":"gilbert"},"location":{"street":"7338 grange road","city":"letterkenny","state":"tipperary","postcode":52466},"email":"debbie.gilbert@example.com","login":{"username":"heavygoose267","password":"coventry","salt":"NS7ZrqkE","md5":"775729a0cf119f1c32fdab5bf93e1927","sha1":"15052613cdce515dc8e1fccaa0c9f9a584e48faa","sha256":"4cb3c00151faf26ed85e9ba61f22376973e755f5219633b0247a69e7ef22bc01"},"dob":"1987-02-05 19:15:58","registered":"2005-10-14 13:25:45","phone":"031-061-6514","cell":"081-331-8027","id":{"name":"PPS","value":"9152780T"},"picture":{"large":"https://randomuser.me/api/portraits/women/69.jpg","medium":"https://randomuser.me/api/portraits/med/women/69.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/69.jpg"},"nat":"IE"},{"gender":"female","name":{"title":"ms","first":"charlene","last":"miller"},"location":{"street":"2718 oak lawn ave","city":"chesapeake","state":"rhode island","postcode":16733},"email":"charlene.miller@example.com","login":{"username":"bluetiger146","password":"older","salt":"tCyeeeyO","md5":"d567deed2bd685e872c7672844d416cc","sha1":"35dd6ce7ab488a5b1a4eed2d136860c8a0e1d5dd","sha256":"c5eb257b13d5bef4ba16cd7fd7189c1c7d0e09967a44d80ebc14e12e9aa73d10"},"dob":"1982-02-22 18:45:05","registered":"2010-06-04 18:47:51","phone":"(048)-109-3917","cell":"(867)-929-6436","id":{"name":"SSN","value":"172-89-2931"},"picture":{"large":"https://randomuser.me/api/portraits/women/93.jpg","medium":"https://randomuser.me/api/portraits/med/women/93.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/93.jpg"},"nat":"US"},{"gender":"male","name":{"title":"mr","first":"آرمین","last":"نكو نظر"},"location":{"street":"5745 میرزای شیرازی","city":"ساری","state":"سمنان","postcode":68243},"email":"آرمین.نكونظر@example.com","login":{"username":"heavymouse181","password":"daewoo","salt":"lAREYXd7","md5":"0cfffb14d991d31eeb27dc04734c2595","sha1":"65a84f96764d5abf58ffcc0df42ec8632a51cb05","sha256":"adad007af1894f671f58d841321a8db8ca4cd44073c3d1d1f443bf737b49da94"},"dob":"1981-12-12 14:53:24","registered":"2007-04-02 21:59:32","phone":"089-20174880","cell":"0999-186-4226","id":{"name":"","value":null},"picture":{"large":"https://randomuser.me/api/portraits/men/90.jpg","medium":"https://randomuser.me/api/portraits/med/men/90.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/90.jpg"},"nat":"IR"},{"gender":"female","name":{"title":"miss","first":"sheila","last":"dean"},"location":{"street":"8647 robinson rd","city":"grants pass","state":"nevada","postcode":48562},"email":"sheila.dean@example.com","login":{"username":"smallelephant691","password":"qian","salt":"M7a9TwKA","md5":"0814e040674d1081eb2f690a1b0d4ef3","sha1":"47dbb30166d7af1c35eb5ad45106bd328b5f095e","sha256":"5c687ab2e9a13108c280467f4ef20c092a49d9b5be722c586e9a40db708e1bf9"},"dob":"1991-07-13 13:12:06","registered":"2008-11-27 02:19:50","phone":"(877)-584-7016","cell":"(981)-462-9845","id":{"name":"SSN","value":"295-99-5901"},"picture":{"large":"https://randomuser.me/api/portraits/women/24.jpg","medium":"https://randomuser.me/api/portraits/med/women/24.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/24.jpg"},"nat":"US"},{"gender":"female","name":{"title":"miss","first":"olivia","last":"hein"},"location":{"street":"9022 mühlenweg","city":"holzminden","state":"thüringen","postcode":65296},"email":"olivia.hein@example.com","login":{"username":"orangeleopard758","password":"excite","salt":"QdkX2c25","md5":"68052177fb719d2e57a52da40318e2fe","sha1":"cb00df46d06887e0732b3ff94bba37ed33413a64","sha256":"89147b5a4ee19723cc367fc2b7f0a4a482ce692076111bd48212b88a6d74873a"},"dob":"1988-05-02 15:15:45","registered":"2010-01-24 00:08:23","phone":"0601-9202186","cell":"0179-7462091","id":{"name":"","value":null},"picture":{"large":"https://randomuser.me/api/portraits/women/26.jpg","medium":"https://randomuser.me/api/portraits/med/women/26.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/26.jpg"},"nat":"DE"},{"gender":"male","name":{"title":"mr","first":"mathias","last":"johansen"},"location":{"street":"2875 gyvelvej","city":"askeby","state":"midtjylland","postcode":88770},"email":"mathias.johansen@example.com","login":{"username":"bigmeercat405","password":"hughes","salt":"tRVWblrQ","md5":"69220f7f2fda0a922024babf5c71f548","sha1":"64264d1f3703fdb529e4d7e2a2ce4c7bfe5e4ea6","sha256":"ad73321a24ddf3c9cc9b23e056c97e50b006cfde75c8a1f1fbb3d06e5ab04b82"},"dob":"1957-04-07 17:58:27","registered":"2003-05-10 09:57:39","phone":"13309317","cell":"70639222","id":{"name":"CPR","value":"197517-4938"},"picture":{"large":"https://randomuser.me/api/portraits/men/35.jpg","medium":"https://randomuser.me/api/portraits/med/men/35.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/35.jpg"},"nat":"DK"}],"info":{"seed":"b13cc7728ab0cf73","results":13,"page":1,"version":"1.1"}} 2 | -------------------------------------------------------------------------------- /test/compojure/api/perf_test.clj: -------------------------------------------------------------------------------- 1 | (ns compojure.api.perf-test 2 | (:require [compojure.api.sweet :refer :all] 3 | [compojure.api.test-utils :as h] 4 | [criterium.core :as cc] 5 | [ring.util.http-response :refer :all] 6 | [schema.core :as s] 7 | [muuntaja.core :as m] 8 | [clojure.java.io :as io]) 9 | (:import (java.io ByteArrayInputStream))) 10 | 11 | ;; 12 | ;; start repl with `lein perf repl` 13 | ;; perf measured with the following setup: 14 | ;; 15 | ;; Model Name: MacBook Pro 16 | ;; Model Identifier: MacBookPro11,3 17 | ;; Processor Name: Intel Core i7 18 | ;; Processor Speed: 2,5 GHz 19 | ;; Number of Processors: 1 20 | ;; Total Number of Cores: 4 21 | ;; L2 Cache (per Core): 256 KB 22 | ;; L3 Cache: 6 MB 23 | ;; Memory: 16 GB 24 | ;; 25 | 26 | (defn title [s] 27 | (println 28 | (str "\n\u001B[35m" 29 | (apply str (repeat (+ 6 (count s)) "#")) 30 | "\n## " s " ##\n" 31 | (apply str (repeat (+ 6 (count s)) "#")) 32 | "\u001B[0m\n"))) 33 | 34 | (defn post* [app uri json] 35 | (-> 36 | (app {:uri uri 37 | :request-method :post 38 | :headers {"content-type" "application/json"} 39 | :body (io/input-stream (.getBytes json))}) 40 | :body 41 | slurp)) 42 | 43 | (s/defschema Order {:id s/Str 44 | :name s/Str 45 | (s/optional-key :description) s/Str 46 | :address (s/maybe {:street s/Str 47 | :country (s/enum "FI" "PO")}) 48 | :orders [{:name #"^k" 49 | :price s/Any 50 | :shipping s/Bool}]}) 51 | 52 | ;; slurps also the body, which is not needed in real life! 53 | (defn bench [] 54 | 55 | ; 27µs 56 | ; 27µs (-0%) 57 | ; 25µs (1.0.0) 58 | ; 25µs (muuntaja) 59 | ; 32µs (jsonista) 60 | (let [app (api 61 | (GET "/30" [] 62 | (ok {:result 30}))) 63 | call #(h/get* app "/30")] 64 | 65 | (title "GET JSON") 66 | (println (call)) 67 | (assert (= {:result 30} (second (call)))) 68 | (cc/quick-bench (call))) 69 | 70 | ;; 73µs 71 | ;; 53µs (-27%) 72 | ;; 50µs (1.0.0) 73 | ;; 38µs (muuntaja), -24% 74 | ;; 34µs (muuntaja), -11% 75 | (let [app (api 76 | (POST "/plus" [] 77 | :return {:result s/Int} 78 | :body-params [x :- s/Int, y :- s/Int] 79 | (ok {:result (+ x y)}))) 80 | data (h/json-string {:x 10, :y 20}) 81 | call #(post* app "/plus" data)] 82 | 83 | (title "JSON POST with 2-way coercion") 84 | (assert (= {:result 30} (h/parse (call)))) 85 | (cc/quick-bench (call))) 86 | 87 | ;; 85µs 88 | ;; 67µs (-21%) 89 | ;; 66µs (1.0.0) 90 | ;; 56µs (muuntaja), -15% 91 | ;; 49µs (jsonista), -13% 92 | (let [app (api 93 | (context "/a" [] 94 | (context "/b" [] 95 | (context "/c" [] 96 | (POST "/plus" [] 97 | :return {:result s/Int} 98 | :body-params [x :- s/Int, y :- s/Int] 99 | (ok {:result (+ x y)})))))) 100 | data (h/json-string {:x 10, :y 20}) 101 | call #(post* app "/a/b/c/plus" data)] 102 | 103 | (title "JSON POST with 2-way coercion + contexts") 104 | (assert (= {:result 30} (h/parse (call)))) 105 | (cc/quick-bench (call))) 106 | 107 | ;; 266µs 108 | ;; 156µs (-41%) 109 | ;; 146µs (1.0.0) 110 | ;; 74µs (muuntaja), -49% 111 | ;; 51µs (jsonista), -30% 112 | (let [app (api 113 | (POST "/echo" [] 114 | :return Order 115 | :body [order Order] 116 | (ok order))) 117 | data (h/json-string {:id "123" 118 | :name "Tommi's order" 119 | :description "Totally great order" 120 | :address {:street "Randomstreet 123" 121 | :country "FI"} 122 | :orders [{:name "k1" 123 | :price 123.0 124 | :shipping true} 125 | {:name "k2" 126 | :price 42.0 127 | :shipping false}]}) 128 | call #(post* app "/echo" data)] 129 | 130 | (title "JSON POST with nested data") 131 | (s/validate Order (h/parse (call))) 132 | (cc/quick-bench (call)))) 133 | 134 | (defn resource-bench [] 135 | 136 | (let [resource-map {:post {:responses {200 {:schema {:result s/Int}}} 137 | :parameters {:body-params {:x s/Int, :y s/Int}} 138 | :handler (fn [{{:keys [x y]} :body-params}] 139 | (ok {:result (+ x y)}))}}] 140 | 141 | ;; 62µs 142 | ;; 44µs (muuntaja) 143 | (let [my-resource (resource resource-map) 144 | app (api 145 | (context "/plus" [] 146 | my-resource)) 147 | data (h/json-string {:x 10, :y 20}) 148 | call #(post* app "/plus" data)] 149 | 150 | (title "JSON POST to pre-defined resource with 2-way coercion") 151 | (assert (= {:result 30} (h/parse (call)))) 152 | (cc/quick-bench (call))) 153 | 154 | ;; 68µs 155 | ;; 52µs (muuntaja) 156 | (let [app (api 157 | (context "/plus" [] 158 | (resource resource-map))) 159 | data (h/json-string {:x 10, :y 20}) 160 | call #(post* app "/plus" data)] 161 | 162 | (title "JSON POST to inlined resource with 2-way coercion") 163 | (assert (= {:result 30} (h/parse (call)))) 164 | (cc/quick-bench (call))) 165 | 166 | ;; 26µs 167 | (let [my-resource (resource resource-map) 168 | app my-resource 169 | data {:x 10, :y 20} 170 | call #(app {:request-method :post :uri "/irrelevant" :body-params data})] 171 | 172 | (title "direct POST to pre-defined resource with 2-way coercion") 173 | (assert (= {:result 30} (:body (call)))) 174 | (cc/quick-bench (call))) 175 | 176 | ;; 30µs 177 | (let [my-resource (resource resource-map) 178 | app (context "/plus" [] 179 | my-resource) 180 | data {:x 10, :y 20} 181 | call #(app {:request-method :post :uri "/plus" :body-params data})] 182 | 183 | (title "POST to pre-defined resource with 2-way coercion") 184 | (assert (= {:result 30} (:body (call)))) 185 | (cc/quick-bench (call))) 186 | 187 | ;; 40µs 188 | (let [app (context "/plus" [] 189 | (resource resource-map)) 190 | data {:x 10, :y 20} 191 | call #(app {:request-method :post :uri "/plus" :body-params data})] 192 | 193 | (title "POST to inlined resource with 2-way coercion") 194 | (assert (= {:result 30} (:body (call)))) 195 | (cc/quick-bench (call))))) 196 | 197 | (defn e2e-json-comparison-different-payloads [] 198 | (let [json-request (fn [data] 199 | {:uri "/echo" 200 | :request-method :post 201 | :headers {"content-type" "application/json" 202 | "accept" "application/json"} 203 | :body (h/json-string data)}) 204 | request-stream (fn [request] 205 | (let [b (.getBytes ^String (:body request))] 206 | (fn [] 207 | (assoc request :body (ByteArrayInputStream. b))))) 208 | app (api 209 | {:formats (assoc m/default-options :return :bytes)} 210 | (POST "/echo" [] 211 | :body [body s/Any] 212 | (ok body)))] 213 | (doseq [file ["dev-resources/json/json10b.json" 214 | "dev-resources/json/json100b.json" 215 | "dev-resources/json/json1k.json" 216 | "dev-resources/json/json10k.json" 217 | "dev-resources/json/json100k.json"] 218 | :let [data (h/parse (slurp file)) 219 | request (json-request data) 220 | request! (request-stream request)]] 221 | 222 | "10b" 223 | ;; 42µs 224 | ;; 24µs (muuntaja), -43% 225 | ;; 18µs (muuntaja+jsonista), -43% 226 | 227 | "100b" 228 | ;; 79µs 229 | ;; 39µs (muuntaja), -50% 230 | ;; 20µs (muuntaja+jsonista), -44% 231 | 232 | "1k" 233 | ;; 367µs 234 | ;; 92µs (muuntaja), -75% 235 | ;; 29µs (muuntaja+jsonista), -65% 236 | 237 | "10k" 238 | ;; 2870µs 239 | ;; 837µs (muuntaja), -70% 240 | ;; 147µs (muuntaja+jsonista) -81% 241 | 242 | "100k" 243 | ;; 10800µs 244 | ;; 8050µs (muuuntaja), -25% 245 | ;; 1260µs (muuntaja+jsonista 0.5.0) -84% 246 | 247 | (title file) 248 | (cc/quick-bench (-> (request!) app :body slurp))))) 249 | 250 | (defn e2e-json-comparison-different-payloads-no-slurp [] 251 | (let [json-request (fn [data] 252 | {:uri "/echo" 253 | :request-method :post 254 | :headers {"content-type" "application/json" 255 | "accept" "application/json"} 256 | :body (h/json-string data)}) 257 | request-stream (fn [request] 258 | (let [b (.getBytes ^String (:body request))] 259 | (fn [] 260 | (assoc request :body (ByteArrayInputStream. b))))) 261 | app (api 262 | {:formats (assoc m/default-options :return :bytes)} 263 | (POST "/echo" [] 264 | :body [body s/Any] 265 | (ok body)))] 266 | (doseq [file ["dev-resources/json/json10b.json" 267 | "dev-resources/json/json100b.json" 268 | "dev-resources/json/json1k.json" 269 | "dev-resources/json/json10k.json" 270 | "dev-resources/json/json100k.json"] 271 | :let [data (h/parse (slurp file)) 272 | request (json-request data) 273 | request! (request-stream request)]] 274 | 275 | "10b" 276 | ;; 38µs (1.x) 277 | ;; 14µs (2.0.0-alpha21) 278 | 279 | "100b" 280 | ;; 74µs (1.x) 281 | ;; 16µs (2.0.0-alpha21) 282 | 283 | "1k" 284 | ;; 322µs (1.x) 285 | ;; 24µs (2.0.0-alpha21) 286 | 287 | "10k" 288 | ;; 3300µs (1.x) 289 | ;; 120µs (2.0.0-alpha21) 290 | 291 | "100k" 292 | ;; 10600µs (1.x) 293 | ;; 1000µs (2.0.0-alpha21) 294 | 295 | (title file) 296 | ;;(println (-> (request!) app :body slurp)) 297 | (cc/quick-bench (app (request!)))))) 298 | 299 | (comment 300 | (bench) 301 | (resource-bench) 302 | (e2e-json-comparison-different-payloads) 303 | (e2e-json-comparison-different-payloads-no-slurp)) 304 | 305 | (comment 306 | (bench) 307 | (resource-bench)) 308 | 309 | (comment 310 | (let [api1 (api 311 | (GET "/30" [] (ok))) 312 | api2 (api 313 | {:api {:disable-api-middleware? true}} 314 | (GET "/30" [] (ok))) 315 | app (GET "/30" [] (ok)) 316 | 317 | request {:request-method :get, :uri "/30"} 318 | count 100000 319 | call1 #(api1 request) 320 | call2 #(api2 request) 321 | call3 #(app request)] 322 | 323 | (title "api1") 324 | (time 325 | (dotimes [_ count] 326 | (call1))) 327 | (cc/quick-bench (call1)) 328 | 329 | (title "api2") 330 | (time 331 | (dotimes [_ count] 332 | (call2))) 333 | #_(cc/quick-bench (call2)) 334 | 335 | (title "app") 336 | (time 337 | (dotimes [_ count] 338 | (call3))) 339 | #_(cc/quick-bench (call3)))) 340 | --------------------------------------------------------------------------------