├── 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 | [](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 |
--------------------------------------------------------------------------------