Skip to content

Resources and Liberator

Marcin Kulik edited this page Jan 8, 2017 · 4 revisions

Resources

Compojure-api also allows http-endpoints to be modeled as data-driven resources, using enhanced Ring-Swagger resource definitions. Resources might be good in the following cases:

  1. Wanting to add swagger-docs and/or schema-based coercion to your existing resource-based apis, like the Liberator.
  2. Generate endpoints based on external data, e.g.
  • generate REST or CRUD-apis from (database) entity definitions
  • generate RPC-apis from function/data definitions

Usage

Resources are created with function compojure.api.resource/resource, which is also imported in the compojure.api.sweet namespace. Both request & response coercion can be disabled for resources, so that they can be used just to generate swagger-docs on top of existing/legacy apis.

Resources are routed with context.

(context "/hello" []
  :middleware [[require-role :admin]]
  (resource
    {:description "hello-resource"
     :responses {200 {:schema {:message s/Str}}}
     :post {:summary "post-hello"
            :parameters {:body-params {:name s/Str}}
            :handler (fnk [[:body-params name]]
                       (ok {:message (format "hello, %s!" name)}))}
     :get {:summary "get-hello"
           :parameters {:query-params {:name s/Str}}
           :handler (fnk [[:query-params name]]
                      (ok {:message (format "hello, %s!" name)}))}}))

Source code

(defn resource
  "Creates a nested compojure-api Route from enhanced ring-swagger operations map and options.
  By default, applies both request- and response-coercion based on those definitions.

  Options:

  - **:coercion**       A function from request->type->coercion-matcher, used
                        in resource coercion for :body, :string and :response.
                        Setting value to `(constantly nil)` disables both request- &
                        response coercion. See tests and wiki for details.

  Enhancements to ring-swagger operations map:

  1) :parameters use ring request keys (query-params, path-params, ...) instead of
  swagger-params (query, path, ...). This keeps things simple as ring keys are used in
  the handler when destructuring the request.

  2) at resource root, one can add any ring-swagger operation definitions, which will be
  available for all operations, using the following rules:

    2.1) :parameters are deep-merged into operation :parameters
    2.2) :responses are merged into operation :responses (operation can fully override them)
    2.3) all others (:produces, :consumes, :summary,...) are deep-merged by compojure-api

  3) special key `:handler` either under operations or at top-level. Value should be a
  ring-handler function, responsible for the actual request processing. Handler lookup
  order is the following: operations-level, top-level.

  4) request-coercion is applied once, using deep-merged parameters for a given
  operation or resource-level if only resource-level handler is defined.

  5) response-coercion is applied once, using merged responses for a given
  operation or resource-level if only resource-level handler is defined.

  Note: Swagger operations are generated only from declared operations (:get, :post, ..),
  despite the top-level handler could process more operations.

  Example:

  (resource
    {:parameters {:query-params {:x Long}}
     :responses {500 {:schema {:reason s/Str}}}
     :get {:parameters {:query-params {:y Long}}
           :responses {200 {:schema {:total Long}}}
           :handler (fn [request]
                      (ok {:total (+ (-> request :query-params :x)
                                     (-> request :query-params :y))}))}
     :post {}
     :handler (constantly
                (internal-server-error {:reason \"not implemented\"}))})"
  ([info]
   (resource info {}))
  ([info options]
   (let [info (merge-parameters-and-responses info)
         root-info (swaggerize (root-info info))
         childs (create-childs info)
         handler (create-handler info options)]
     (routes/create nil nil root-info childs handler))))

More examples

Hello World

(resource
  {:handler (constantly (ok "hello world"))})

Shared parameters, more swagger-info & a fallback-handler

(resource
  {:description "shared description, can be overridden"
   :parameters {:path-params {:id Long}}
   :get {:description "get user"
         :summary "get-user ftw!"
         :handler get-user}
   :put {:parameters {:body-params User}
         :responses {200 {:schema User}}
         :handler modify-user}
   :delete {:description "delete's a user"
            :handler delete-user}
   :handler (constantly
              (internal-server-error
                {:reason "other methods not implemented"}))})

Integrated with routing

Inlined (as a closure):

(context "/plus" []
  :middleware [require-admin]
  (resource
    {:get {:parameters {:query-params {:x Long, :y Long}}
           :responses {200 {:schema {:total Long}}}
           :handler my-plus-request-handler-with-coerced-inputs-and-outputs}}))

Predefined (bit faster):

(def plus-resource
  (resource
    {:get {:parameters {:query-params {:x Long, :y Long}}
           :responses {200 {:schema {:total Long}}}
           :handler my-plus-request-handler-with-coerced-inputs-and-outputs}}))

(context "/plus" []
  :middleware [require-admin]
  plus-resource)

More examples

Integration with Liberator

  • set the Liberator resource as top-level handler
  • return raw responses to enable response coercion via :as-response or ring-response, see the guide
  • read the coerced parameters from the liberator context: (get-in ctx [:request :body-parameters])
  • optionally disable coercion on resource just to get just swagger-docs
(def user-resource
  (resource
    {:parameters {:path-params {:id Long}}
     :get {:parameters {:query-params {(s/optional-key :fields) [String]}}
           :responses {200 {:schema User}}
           :summary "returns a User (or fields of it)"}
     :put {:parameters {:body-params User}
           :responses {200 {:schema User}}
           :summary "Updates a user"}
     :delete {:summary "Deletes a user"}
     :handler my-liberator-resource}
    ;; add this if you just want the swagger-docs
    {:coercion (constantly nil)}))

(context "/user" []
  user-resource)

Feel free to update this guide. Initial discussion here.