-
Notifications
You must be signed in to change notification settings - Fork 150
Building Documented Apis
Namespace compojure.api.sweet
is a public entry point for most of the routing stuff. It imports the enhanced route macros from compojure.api.core
, swagger-routes from compojure.api.swagger
, apis from compojure.api.api
and a few extras from ring.swagger.json-schema
. In most cases, it's the only import you need.
The following examples expect the following imports
(require '[compojure.api.sweet :refer :all])
(require '[ring.util.http-response :refer :all])
compojure.api.middleware/api-middleware
wraps the basic functionality for an web api. It's a enhanced version of compojure.handler/api
adding the following:
- catching slingshotted http-errors (
ring.middleware.http-response/catch-response
) - catching unhandled exceptions (
compojure.api.middleware/wrap-exceptions
) - support for different protocols via
ring.middleware.format-params/wrap-restful-params
andring.middleware.format-response/wrap-restful-response
- default supported protocols are:
:json-kw
,:yaml-kw
,:edn
,:transit-json
and:transit-msgpack
- default supported protocols are:
- enabled protocol support is also published into Swagger docs via
ring.swagger.middleware/wrap-swagger-data
All middlewares are preconfigured with good/opinionated defaults, but one can override the configurations by passing an options Map into the api-middleware
. See api-docs for details.
The top-level route-function in compojure-api is compojure.api.api/api
. It takes an optional options map and a sequence of ring-handlers for request processing. Api mounts the api-middleware
and creates a route-tree from the handlers and injects it to the to be used in swagger-docs and bi-directional routing. Optionally, one can configure all the swagger artefacts also via api options under key :swagger
. See api-docs for details.
(def app
(api
(GET "/ping" []
(ok {:ping "pong"}))))
(slurp (:body (app {:request-method :get :uri "/ping"})))
; => "{\"ping\":\"pong\"}"
(def app
(api
{:api {invalid-routes-fn handle-missing-routes-fn}
:exceptions {:handlers {::ex/default custom-exception-handler
::custom-error custom-error-handler}}
:formats [:json-kw :edn :transit-json]
:format {:response-opts {:transit-json {:handlers transit/writers}}
:params-opts {:transit-json {:handlers transit/readers}}}
:coercion my-domain-coercion
:ring-swagger {:ignore-missing-mappings? true}
:swagger {:ui "/"
:spec "/swagger.json"
:options {:ui {:validatorUrl nil}
:data {:info {:version "1.0.0", :title "Thingies API"}}
:tags [{:name "math", :description "Math with parameters"}]}}}
(GET "/ping" []
(ok {:ping "pong"}))))
compojure.api.core/defapi
is just a shortcut for defining an api:
(defapi app
(context "/api" []
(GET "/ping" []
(ok {:ping "pong"}))))
To help with setting up custom middleware, there is a compojure.api.core/middleware
macro:
(require '[ring.middleware.head :refer [wrap-head]])
(defapi app
(middleware [wrap-head]
(context "/api" []
(GET "/ping" []
(ok {:ping "pong"})))))
TODO: :middleware
, new options..
Compojure-api wraps the vanilla Compojure route macros in compojure.api.core
. They can be used just like the orginials, but also define a new way of extending their behavior via restructuring.
; just like a compojure route
(GET "/ping" []
(ok {:ping "pong"}))
; with automatic query-string and response model coercion (with support for generated swagger-docs)
(GET "/plus" []
:query-params [x :- Long, y :- Long]
:return {:result Long}
(ok {:result (+ x y)}))
Compojure-api uses Swagger for route documentation.
To enable Swagger route documentation in your application:
- Wrap your application into an
api
(ordefapi
).- Add
:no-doc true
metadata to any routes you don't want to appear in the documentation
- Add
- Add
compojure.api.swagger/swagger-routes
route to publish the swagger spec -
optionally Mount
compojure.api.swagger/swagger-ui
to add the Swagger-UI to the web app.
If the embedded (Ring-)Swagger-UI isn't enough for you, you can exclude it from dependencies and create & package your own UI from the sources:
[metosin/compojure-api "0.20.3" :exclusions [metosin/ring-swagger-ui]]
(defroutes legacy-route
(GET "/ping/:id" [id]
(ok {:id id})))
(defapi app
(swagger-routes)
(context "/api" []
legacy-route
(POST "/echo" {body :body-params} (ok body))))
The above sample application mounts swagger-docs to root /
and serves the swagger-docs from /swagger.json
.
The resulting swagger-spec data (published by the swagger-routes
) is combined from three sources:
- api creation-time route & schema information, generated for you by the lib
- Run-time extra information from the middleware passed in with the request
- User-set custom information
Passed in automatically via request injection.
By default, the application wire-format serialization capabilities (:produces
and :consumes
)
are injected in automatically by the api
machinery.
One can contribute extra arbitrary swagger-data (like swagger security definitions) to the docs via
the ring.swagger.middleware/wrap-swagger-data
middleware.
The swagger-docs
can be used without parameters, but one can also set any valid root-level Swagger Data with it.
(swagger-routes)
(swagger-routes
{:data {:info {:version "1.0.0"
:title "Sausages"
:description "Sausage description"
:termsOfService "http://helloreverb.com/terms/"
:contact {:name "My API Team"
:email "[email protected]"
:url "http://www.metosin.fi"}
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}}
:tags [{:name "kikka", :description "kukka"}]}})
See the Swagger-spec for more details.
As one might accidentally pass invalid swagger data in, you should validate the end results. See wiki for details.
One can configure Ring-Swagger by providing options to api-middleware
for the key :ring-swagger
. See Ring-Swagger docs for possible options and examples.
(defapi app
{:ring-swagger {:ignore-missing-mappings? true}})
(swagger-routes)
...)
To ensure that your API is valid, one can call compojure.api.swagger/validate
. It takes the api (the ring handler returned by api
or defapi
) as an parameter and returns the api or throws an Exception. The validation does the following:
- if the api is not a swagger api (does not the
swagger-docs
mounted) and compiles, it's valid - if the api is a swagger api (does have the
swagger-docs
mounted):- Ring Swagger is called to verify that all Schemas can be transformed to Swagger JSON Schemas
- the swagger-spec endpoint is called with 200 response status
(require '[compojure.api.sweet :refer :all])
(require '[compojure.api.swagger :refer [validate])
(defrecord NonSwaggerRecord [data])
(def app
(validate
(api
(swagger-routes)
(GET "/ping" []
:return NonSwaggerRecord
(ok (->NonSwaggerRecord "ping"))))))
; clojure.lang.Compiler$CompilerException: java.lang.IllegalArgumentException:
; don't know how to create json-type of: class compojure.api.integration_test.NonSwaggerRecord
TODO: optionally validate the swagger spec itself against the JSON Schema.
Inspired by the awesome bidi, Compojure-api also supports bi-directional routing. Routes can be attached with a :name
and other endpoints can refer to them via path-for
macro (or path-for*
function). path-for
takes the route-name and optionally a map of path-parameters needed to construct the full route. Normal ring-swagger path-parameter serialization is used, so one can use all supported Schema
elements as the provided parameters.
Route names should be keywords. Compojure-api ensures that there are no duplicate endpoint names within an api
, raising a IllegalArgumentException
at compile-time if it finds multiple routes with the same name. Route name is published as :x-name
into the Swagger docs.
(fact "bi-directional routing with path-parameters"
(let [app (api
(GET "/lost-in/:country/:zip" []
:name :lost
:path-params [country :- (s/enum :FI :EN), zip :- s/Int]
(ok {:country country, :zip zip}))
(GET "/api/ping" []
(moved-permanently
(path-for :lost {:country :FI, :zip 33200}))))]
(fact "path-for resolution"
(let [[status body] (GET app "/api/ping" {})]
status => 200
body => {:country "FI"
:zip 33200}))))
Compojure-api uses the Schema library to describe data models, backed up by ring-swagger for mapping the models into Swagger JSON Schemas. With Map-based schemas, Keyword keys should be used instead of Strings.
Input and output schemas are coerced automatically using a schema coercion matcher linked to a coercion type. There are three types of coercion and currently two different coercion matchers available (from Ring-Swagger).
The following table provides the default mapping from type -> coercion matcher.
type | default coercion matcher | used with |
---|---|---|
:body |
json-schema-coercion-matcher |
request body |
:string |
query-schema-coercion-matcher |
query, path, header and form parameters |
:response |
json-schema-coercion-matcher |
response body |
One can override the default coercion behavior by providing a coercion function of type
ring-request->coercion-type->coercion-matcher
either by:
- api-middleware option
:coercion
- route-level restructuring
:coercion
As the coercion function takes in the ring-request, one can select a coercion matcher based on the user-selected wire format or any other header. The plan is to provide extendable protocol-based coercion out-of-the-box (Transit doesn't need any coercion, XML requires some extra love with sequences). Stay tuned.
Examples of overriding the default coercion can found in the the tests.
All coercion code uses ring.swagger.schema/coerce!
internally, which throws managed exceptions when a value
can't be coerced. The api-middleware
catches these exceptions and returns the validation error as a serializable
Clojure data structure, sent to the client.
One can also call ring.swagger.schema/coerce!
manually:
(require '[ring.swagger.schema :refer [coerce!])
(require '[schema.core :as s])
(s/defschema Thingie {:id Long
:tag (s/enum :kikka :kukka)})
(coerce! Thingie {:id 123, :tag "kikka"})
; => {:id 123 :tag :kikka}
(coerce! Thingie {:id 123, :tags "kakka"})
; => ExceptionInfo throw+: {:type :ring.swagger.schema/validation, :error {:tags disallowed-key, :tag missing-required-key}} ring.swagger.schema/coerce! (schema.clj:88)
The enhanced route-macros allow you to define extra meta-data by adding a) meta-data as a map or b) as pair of
keyword-values in Liberator-style. Keys are used as a dispatch value into restructure
multimethod,
which generates extra code into the endpoints. If one tries to use a key that doesn't have a dispatch function,
a compile-time error is raised.
There are a number of available keys in the meta namespace, which are always available. These include:
- input & output schema definitions (with automatic coercion and swagger-data extraction)
- extra swagger-documentation like
:summary
,:description
,:tags
One can also easily create one's own dispatch handlers, just add a new dispatch function to the multimethod.
(s/defschema User {:name s/Str
:sex (s/enum :male :female)
:address {:street s/Str
:zip s/Str}})
(POST "/echo" []
:summary "echoes a user from a body" ; for swagger-documentation
:body [user User] ; validates/coerces the body to be User-schema, assigns it to user (lexically scoped for the endpoint body) & generates the needed swagger-docs
:return User ; validates/coerces the 200 response to be User-schema, generates needed swagger-docs
(ok user)) ; the body itself.
Everything happens at compile-time, so you can macroexpand the previous to learn what happens behind the scenes.
You can also wrap models in containers (vector
and set
) and add descriptions:
(POST "/echos" []
:return [User]
:body [users (describe #{Users} "a set of users")]
(ok users))
Schema-predicate wrappings work too:
(POST "/nachos" []
:return (s/maybe {:a s/Str})
(ok nil))
And anonymous schemas:
(PUT "/echos" []
:return [{:id Long, :name String}]
:body [body #{{:id Long, :name String}}]
(ok body))
All parameters can also be destructured using the Plumbing syntax with optional type-annotations:
(GET "/sum" []
:query-params [x :- Long, y :- Long]
(ok {:total (+ x y)}))
(GET "/times/:x/:y" []
:path-params [x :- Long, y :- Long]
(ok {:total (* x y)}))
(POST "/divide" []
:return Double
:form-params [x :- Long, y :- Long]
(ok {:total (/ x y)}))
(POST "/minus" []
:body-params [x :- Long, y :- Long]
(ok {:total (- x y)}))
(POST "/power" []
:header-params [x :- Long, y :- Long]
(ok {:total (long (Math/pow x y))})
Raw values / primitives (e.g. not sequences or maps) can be returned when a :return
-metadata is set. Swagger,
ECMA-404 and ECMA-262 allow this
(while RFC4627 forbids it).
note setting a :return
value as String
allows you to return raw strings (as JSON or whatever protocols your
app supports), as opposed to the Ring Spec.
(context "/primitives" []
(GET "/plus" []
:return Long
:query-params [x :- Long {y :- Long 1}]
:summary "x+y with query-parameters. y defaults to 1."
(ok (+ x y)))
(GET "/datetime-now" []
:return org.joda.time.DateTime
:summary "current datetime"
(ok (org.joda.time.DateTime.)))
(GET "/hello" []
:return String
:query-params [name :- String]
:description "<h1>hello world.</h1>"
:summary "echoes a string from query-params"
(ok (str "hello, " name))))
Key :responses
takes a map of HTTP status codes to Schema definitions maps
(with optional :schema
, :description
and :headers
keys). :schema
defines the return model
and gets automatic coercion for it. If a route tries to return an invalid response value,
an InternalServerError
is raised with the schema validation errors.
(GET "/" []
:query-params [return :- (s/enum :200 :301 :403 :404)]
:responses {301 {:schema s/Str, :description "new place!", :headers {:location s/Str}}
403 {:schema {:code s/Str}, :description "spiders?"}}
404 {:schema {:reason s/Str}, :description "lost?"}
:return Total
:summary "multiple returns models"
(case return
:200 (ok {:total 42})
:301 (moved-permanently "http://www.new-total.com")
:403 (forbidden {:code "forest"})
:404 (not-found {:reason "lost"})))
The :return
maps the model just to the response 200, so one can also say:
(GET "/" []
:query-params [return :- (s/enum :200 :403 :404)]
:responses {200 {:schema Total, :description "happy path"}
403 {:schema {:code s/Str}, :description "spiders?"}}
404 {:schema {:reason s/Str}, :description "lost?"}}
:summary "multiple returns models"
(case return
:200 (ok {:total 42})
:403 (forbidden {:code "forest"})
:404 (not-found {:reason "lost"})))
There is also a :default
status code available, which stands for "all undefined codes".
You can either use the normal restructuring (:query
, :path
etc.) to get the swagger docs and
disable the coercion with:
(api
:coercion (constantly nil)
...
or use the :swagger
restructuring at your route, which pushes the swagger docs for the routes:
(GET "/route" [q]
:swagger {:x-name :boolean
:operationId "echoBoolean"
:description "Echoes a boolean"
:parameters {:query {:q s/Bool}}}
;; q might be anything here.
(ok {:q q}))
Key :middleware
takes a vector of middleware to be applied to the route.
Note that the middleware don't see any restructured bindings from within the route body.
They are executed inside the route so you can safely edit request etc. and the changes
won't leak to other routes in the same context.
(DELETE "/user/:id" []
:middleware [audit-support (for-roles :admin)]
(ok {:name "Pertti"}))