Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Sweet web apis with Compojure & Swagger

License

NotificationsYou must be signed in to change notification settings

metosin/compojure-api

Repository files navigation

Stuff on top ofCompojure for making sweet web apis.

Latest version

Clojars Project

Migration from Swagger 1.2 to 2.0

If you are upgrading your existing pre0.20.0 compojure-api app to use0.20.0 or later, you have to migrate the Swagger modelsfrom 1.2 to 2.0. SeeMigration guide for details.

Sample application

(nsexamples.thingie  (:require [ring.util.http-response:refer:all]            [compojure.api.sweet:refer:all]            [schema.core:as s]));;;; Schemas;;(s/defschemaThingie   {:id Long:hot Boolean:tag (s/enum:kikka:kukka):chief [{:name String:type #{{:id String}}}]});;;; Routes;;(defroutes*legacy-route  (GET*"/legacy/:value" [value]    (ok {:value value})))(defapiapp  (swagger-ui)  (swagger-docs     {:info {:title"Sample api"}})      (GET*"/" []:no-doctrue    (ok"hello world"))  (context*"/api" []:tags ["thingie"]    (GET*"/plus" []:return       Long:query-params [x:- Long, {y:- Long1}]:summary"x+y with query-parameters. y defaults to 1."      (ok (+ x y)))    (POST*"/minus" []:return      Long:body-params [x:- Long, y:- Long]:summary"x-y with body-parameters."      (ok (- x y)))    (GET*"/times/:x/:y" []:return      Long:path-params [x:- Long, y:- Long]:summary"x*y with path-parameters"      (ok (* x y)))    (POST*"/divide" []:return      Double:form-params [x:- Long, y:- Long]:summary"x/y with form-parameters"      (ok (/ x y)))    (GET*"/power" []:return      Long:header-params [x:- Long, y:- Long]:summary"x^y with header-parameters"      (ok (long (Math/pow x y))))    legacy-route    (PUT*"/echo" []:return   [{:hot Boolean}]:body     [body [{:hot Boolean}]]:summary"echoes a vector of anonymous hotties"      (ok body))    (POST*"/echo" []:return   (s/maybe Thingie):body     [thingie (s/maybe Thingie)]:summary"echoes a Thingie from json-body"      (ok thingie)))  (context*"/context" []:tags ["context*"]:summary"summary inherited from context"    (context*"/:kikka" []:path-params [kikka:- s/Str]:query-params [kukka:- s/Str]      (GET*"/:kakka" []:path-params [kakka:- s/Str]        (ok {:kikka kikka:kukka kukka:kakka kakka})))))

To try it yourself, clone this repository and do either:

  1. lein run
  2. lein repl &(go)

Quick start for new project

Clone theexamples-repository.

Use a Leiningen template, with or without tests:

lein new compojure-api my-apilein new compojure-api my-api +midjelein new compojure-api my-api +clojure-test

Building Documented Apis

Api middleware

There is prepackaged middlewarecompojure.api.middleware/api-middleware for common web api usage. It's a enhanced version ofcompojure.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 viaring.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
    • enabled protocol support is also published into Swagger docs viaring.swagger.middleware/wrap-swagger-data.

All middlewares are preconfigured with good/opinionated defaults, but one can override the configurations by passing a options Map into theapi-middleware. Seeapi-docs for details.

Api macro

To get all the benefits of Compojure-api, one should wrap the apis intocompojure.api.core/api-macro. It is responsible for creating and publishingthe compile-time route-tree from your api, enabling the Swagger documentation. It takes an optional map as a first parameter which is passed directlyto the underlayingapi-middleware for configuring the used middlewares.

(nsexample.handler  (:require [ring.util.http-response:refer [ok]]            [compojure.api.core:refer:all]            [compojure.core:refer:all]))(defapp  (api    {:formats [:json-kw]}    (context"/api" []      (GET"/ping" [] (ok {:ping"pong"})))))      (slurp (:body (app {:request-method:get:uri"/api/ping"}))); => "{\"ping\":\"pong\"}"

Defapi

compojure.api.core/defapi is just a shortcut for defining an api:

(defapiapp  {:formats [:json-kw]}  (context"/api" []    (GET"/ping" [] (ok {:ping"pong"}))))

Custom middlewares

To help setting up custom middleware there is acompojure.api.core/middlewares macro:

(require '[ring.middleware.head:refer [wrap-head]])(defapiapp  (middlewares [wrap-head]    (context"/api" []      (GET"/ping" [] (ok {:ping"pong"})))))

If you want to wrap you complete app in some middlewares,defapi returns handler which you can wrap just like with regulardefroutes:

(defapiapp'...)(defapp (-> app' (wrap-head)))

Route macros

One can use eithervanilla Compojure routes or their enhanced versions fromcompojure.api.core.Enhanced versions have* in their name (GET*,POST*,context*,defroutes* etc.) so that they don't get mixed up with the originals.Enhanced version can be used exactly as their ancestors but also allow new features via extendable meta-data handlers.

Sweet

Namespacecompojure.api.sweet is a public entry point for all routing macros. It imports the enchanced route macros fromcompojure.api.core,swagger-stuff fromcompojure.api.swagger and few extras fromcompojure.core.

There is alsocompojure.api.legacy namespace which contains rest of the public vars fromcompojure.core (theGET,POST etc. endpoint macros which are not contained insweet). Usingsweet in conjunction withlegacy should provide a drop-in-replacement forcompojure.core - with new new route goodies.

sample sweet application

(nsexample.handler2  (:require [ring.util.http-response:refer [ok]]            [compojure.api.sweet:refer:all]))(defapiapp  (context*"/api" []    (GET*"/user/:id" [id] (ok {:id id}))    (POST*"/echo" {body:body-params} (ok body))))

Route documentation

Compojure-api usesSwagger for route documentation.

Enabling Swagger route documentation in your application is done by:

  • Wrap your api-applicaiton into anapi (ordefapi).
    • uses macro-peeling & source linking to reconstruct the route tree from route macros at macro-expansion time (~no runtime penalty)
    • if you intend to split your routes behind multiple Vars viadefroutes, usedefroutes* instead so that their routes get also collected.Note: since0.20.0 thedefroutes* are automatically referenced over a Var to get smoother development flow.
    • Add:no-doc metadata to any routes you don't want to appear in the documentation
  • Addcompojure.api.swagger/swagger-docs route to publish the swagger spec
  • optionally Mountcompojure.api.swagger/swagger-ui to add theSwagger-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 thesources:

[metosin/compojure-api"0.20.3":exclusions [metosin/ring-swagger-ui]]

Sample Swagger 2.0 App

(nsexample.handler3  (:require [ring.util.http-response:refer [ok]]            [compojure.api.sweet:refer:all]))(defroutes*legacy-route  (GET*"/ping/:id" [id]    (ok {:id id})))(defapiapp  (swagger-ui)  (swagger-docs)  (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 Swagger Docs

The resulting swagger-spec data (published by theswagger-docs) is combined from three sources:

  • Compile-time route & schema information, generated for you by the lib
  • Run-time extra information from the middlewares, passed in with the request
  • User-set custom information

Compile-time route & schema information

Passed in automatically via request injection.

Run-time injected information

By default, the application wire-format serialization capabilities (:produces and:consumes)are injected in automatially by theapi machinery.

One can contribute extra arbitrary swagger-data (like swagger security definitions) to the docs viaring.swagger.middleware/wrap-swagger-data middleware.

User-set custom information

Theswagger-docs can be used without parameters, but one can set any valid root-level Swagger Data via it.

With defaults:
(swagger-docs)
With API Info and Tag descriptions set:
(swagger-docs  {:info {:version"1.0.0":title"Sausages":description"Sausage description":termsOfService"http://helloreverb.com/terms/":contact {:name"My API Team":email"foo@example.com":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 theSwagger-spec for more details.

As one might accidentally pass invalid swagger data in, you should validate the end results.Seewiki for details.

Customizing Swagger output

One can configure Ring-Swagger by providing options toapi-middleware for key:ring-swagger. SeeRing-Swagger docs for possible options and examples.

(defapiapp  {:ring-swagger {:ignore-missing-mappings?true}})  (swagger-docs)  (swagger-ui)  ...)

Api Validation

To ensure that your API is valid, one can callcompojure.api.swagger/validate. It takes the api (the ring handler returned byapi ordefapi) as an parameter and returns the api of throws an Exception. The validation does the following:

  1. if the api is not an swagger api (does not theswagger-docs mounted) and compiles, it's valid
  2. if the api is an swagger api (does have theswagger-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 responses status
(require '[compojure.api.sweet:refer:all])(require '[compojure.api.swagger:refer [validate])(defrecordNonSwaggerRecord [data])(defapp  (validate    (api      (swagger-docs)      (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: optionallyvalidate the swagger spec itself againt the JSON Schema.

Bi-directional routing

Inspired by the awesomebidi, Compojure-api also supports bi-directional routing. Routes can be attached with a:name and other endpoints can refer to them viapath-for macro (orpath-for* function).path-for takes the route-name and optionally a mapof path-parameters needed to construct the full route. Normal ring-swagger path-parameter serialization is used, so one can use all supported Schemaelements as the provided parameters.

Route names should be keywords. Compojure-api ensures that there are no duplicate endpoint names within anapi, raising aIllegalArgumentExceptionat compile-time if it founds multiple routes with 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,:zip33200}))))]      (fact"path-for resolution"        (let [[status body] (get* app"/api/ping" {})]          status =>200          body => {:country"FI":zip33200}))))

Component integration

Stuert Sierra'sComponent is a great library for managing the statefulresources of your app. There areseveral strategiesto use it. Here are some samples how to use Component with compojure-api:

Lexical bind with Components as a function arguments

(defncreate-handler [{:keys [db]:as system}]  (api    (swagger-docs)    (swagger-ui)    (GET*"/user/:id" []:path-params [id:- s/Str]      (ok (get-user db id)))))

Passing Components via request

Use either:components-option ofapi-middleware orwrap-components-middlewareto associate the components with your API.

Components can be read from the request usingcompojure.api.middleware/get-components or usingthe:components restucturing with letk-syntax.

(require '[compojure.api.middleware:as mw])(defapihandler  (GET*"/user/:id" []:path-params [id:- s/Str]:components [db]    (ok (get-user db id))))(defnapp (mw/wrap-components handler (create-system))
(defapiapp  {:components (create-system)}  (GET*"/user/:id" []:path-params [id:- s/Str]:components [db]    (ok (get-user db id))))

To see this in action, trylein run and navigate to Components api group.

Exception handling

All exceptions should be handled gracefully. Compojure-api ships with customizable exception handling with gooddefaults. Customization is done viaapi options - delegating tocompojure.api.middleware/wrap-exceptions, whichdoes the real work. It catches all thrown exceptions and selects a custom handler based on the thrown exceptionex-data or Slingshot value of key:type. If an exception doesn't have ex-data (e.g. legacy Java Exceptions),:compojure.api.exception/default type is used. Exception handlers are 3-arity functions, getting the exception,ex-data and request as arguments. Below are the default type definitions and default handling:

typewhatdefault
:compojure.api.exception/request-parsingInput data de-serialization errors.400 + error in body
:compojure.api.exception/request-validationRequest Schema coercion errors.400 + schema error in body
:compojure.api.exception/response-validationResponse Schema coercion errors.500 + schema error in body
:compojure.api.exception/defaultEverything else.500 + print stacktrace + safe message

example to override the default case + add a custom exception type + handler for it:

(defncustom-handler [^Exception e data request]  (internal-server-error {:message (.getMessage e)}))(defncalm-handler [^Exception e data request]  (enhance-your-calm {:message (.getMessage e),:data data}))(defapi  {:exceptions {:handlers {:compojure.api.exception/default custom-handler::calm calm-handler}}}  (GET*"/bang" [] (throw (RuntimeException."kosh")))  (GET*"/calm" [] (throw (ex-info"fail" {:type::calm,:oil"snake"}))))

Schemas

Compojure-api uses theSchema to describe data models, backed up byring-swagger for mapping the models int Swagger JSON Schemas.With Map-based schemas, Keyword keys should be used instead of Strings.

Coercion

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.

typedefault coercion matcherused with
:bodyjson-schema-coercion-matcherrequest body
:stringquery-schema-coercion-matcherquery, path, header and form parameters
:responsejson-schema-coercion-matcherresponse body

One can override the default coercion behavior by providing a coercion function of typering-request->coercion-type->coercion-matcher either by:

  1. api-middleware option:coercion
  2. route-level restructuring:coercion

As the coercion function takes in the ring-request, one can select coercion matcher based on the user selected wireformat or any other header. The plan is to provide extendable protocol-based coercion out-of-the-box (Transit doesn'tneed any coercion, XML requires some extra love with sequences). Stay tuned.

Examples on overriding the default coercion can found in thethe tests.

All coercion code uses the internallyring.swagger.schema/coerce!, which throws managed exceptions when a valuecan't be coerced. Theapi-middleware catches these exceptions and returns the validation error as serializableClojure data structure, sent to the client.

One can also callring.swagger.schema/coerce! manually:

(require '[ring.swagger.schema:refer [coerce!])(require '[schema.core:as s])(s/defschemaThingie {:id Long:tag (s/enum:kikka:kukka)})(coerce! Thingie {:id123,:tag"kikka"}); => {:id 123 :tag :kikka}(coerce! Thingie {:id123,:tags"kakka"}); => ExceptionInfo throw+: {:type :ring.swagger.schema/validation, :error {:tags disallowed-key, :tag missing-required-key}}  ring.swagger.schema/coerce! (schema.clj:88)

Models, routes and meta-data

The enhanced route-macros allow you to define extra meta-data by adding a) meta-data as a map or b) as pair ofkeyword-values in Liberator-style. Keys are used as a dispatch value intorestructure 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 is lot's of available keys inmetanamespace, 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 own dispatch handlers, just add new dispatch function to the multimethod.

(s/defschemaUser {: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, assignes it to user (lexically scoped for the endpoint body) & generated 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.

More about models

You can also wrap models in containers (vector andset) and add descriptions:

(POST*"/echos" []:return [User]:body [users (describe #{Users}"a set on users")]  (ok users))

Schema-predicate wrappings work too:

(POST*"/nachos" []:return (s/maybe {:a s/Str})  (oknil))

And anonymous schemas:

  (PUT*"/echos" []:return   [{:id Long,:name String}]:body     [body #{{:id Long,:name String}}]    (ok body))

Query, Path, Header and Body parameters

All parameters can also be destructured using thePlumbing 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))})

Returning raw values

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 asString allows you to return raw strings (as JSON or whatever protocols yourapp supports), opposed to theRing Spec.

(context"/primitives" []  (GET*"/plus" []:return       Long:query-params [x:- Long {y:- Long1}]: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]:notes"<h1>hello world.</h1>":summary"echoes a string from query-params"    (ok (str"hello," name))))

Response-models

Key:responses takes a map of http-status-code to schema-definitions map(with optional:schema,:description and:headers keys).:schema defines the return modeland get's automatic coercion for it. If a route tries to return an invalid response value,anInternalServerError is raised with the schema validation errors.

(GET*"/" []:query-params [return:- (s/enum:200:403:404)]:responses    {403 {:schema {:code s/Str},:description"spiders?"}}404 {:schema {:reason s/Str},:description"lost?"}}:return       Total:summary"multiple returns models"  (case return:200 (ok {:total42}):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 {:total42}):403 (forbidden {:code"forest"}):404 (not-found {:reason"lost"})))

There is also a:default status code available, which stands for "all undefined codes".

I just want the swagger-docs, without coercion

You can either use the normal restructuring (:query,:path etc.) to get the swagger docs anddisable the coercion with:

(api:coercion (constantlynil)  ...

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"Ehcoes a boolean":parameters {:query {:q s/Bool}}};; q might be anything here.  (ok {:q q}))

Swagger-aware File-uploads

Mostly provided by Ring-Swagger. Restructuring:multipart-params pushes alsomultipart/form-data as the onlyavailable consumption.

;; versions before 0.23.0(require '[compojure.api.upload:as upload]);; versions 0.23.0+(require '[ring.swagger.upload:as upload])(POST*"/upload" []:multipart-params [file:- upload/TempFileUpload]:middlewares [upload/wrap-multipart-params]  (ok (dissoc file:tempfile)))

Route-specific middlewares

Key:middlewares takes a vector of middlewares to be applied to the route.Note that the middlewares 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 changeswon't leak to other routes in the same context.

 (DELETE*"/user/:id" []:middlewares [audit-support (for-roles:admin)]   (ok {:name"Pertti"}))

Creating your own metadata handlers

Compojure-api handles the route metadata by calling the multimethodcompojure.api.meta/restructure-param withmetadata key as a dispatch value.

Multimethods take three parameters:

  1. metadata key
  2. metadata value
  3. accumulator map with keys
    • :lets, a vector of let bindings applied first before the actual body
    • :letks, a vector of letk bindings applied second before the actual body
    • :middlewares, a vector of route specific middlewares (applied from left to right)
    • :parameters, meta-data of a route (without the key & value for the current multimethod)
    • :body, a sequence of the actual route body

.. and should return the modified accumulator. Multimethod calls are reduced to produce the final accumulator forcode generation. Defined key-value -based metadatas for routes are guaranteed to run on top-to-bottom order of the soall the potentiallet andletk variable overrides can be solved by the client. Default implementation is to keepthe key & value as a route metadata.

You can add your own metadata-handlers by implementing the multimethod:

(defmethodcompojure.api.meta/restructure-param:auth  [_ token {:keys [parameters lets body middlewares]:as acc}]"Make sure the request has X-AccessToken header and that it's value is 123. Binds the value into a variable"  (-> acc      (update-in [:lets] into [{{token"x-accesstoken"}:headers} '+compojure-api-request+])      (assoc:body `((if (= ~token"123")                      (do ~@body)                      (ring.util.http-response/forbidden"Auth required"))))))

Using it:

(GET*"/current-session" []:auth token  (ok {:token token}))

macroexpanding-1 it too see what's get generated:

(clojure.pprint/pprint  (macroexpand-1 `(GET*"/current-session" []:auth token                    (ok {:token token}))))(compojure.core/GET"/current-session" [:as +compojure-api-request+] (clojure.core/let [{{examples.thingie/token"x-accesstoken"}:headers} +compojure-api-request+]  (do   (if    (clojure.core/= examples.thingie/token"123")    (do (ring.util.http-response/ok {:token examples.thingie/token}))    (ring.util.http-response/forbidden"Auth required")))))

Other resources

License

Copyright © 2014-2015Metosin Oy

Distributed under the Eclipse Public License, the same as Clojure.


[8]ページ先頭

©2009-2025 Movatter.jp