0

We use the compojure-api to get us some nice swagger integration in our ring apps. The :swagger {:deprecated true} meta works like a champ to get the swagger page correct, but I have a requirement that I put a specific header on the response when the route is :swagger {:deprecated true}. I am struggling to figure out how to do this with the middleware pattern that I've been using to do similar response header manipulations.

(ns bob.routes
  (:require [clojure.tools.logging :as log]
            [compojure.api.sweet :refer :all]
            [ring.util.http-response :as status]
            [schema.core :as s]
            [ring.swagger.schema :as rs]))

(s/defschema BobResponse {:message (rs/describe String "Message")})

(defn wrap-bob-response-header [handler]
  (fn [request]
    (let [response (handler request)]
      ;; can I reach into the request or the response to see what
      ;; route served this and if it has the :swagger {:deprecated true}
      ;; meta on it and NOT emit the x-bob header if it does?
      (assoc-in response [:headers "x-bob"] "Robert"))))

(defroutes bob-routes
  (context "" []
    :middleware [wrap-bob-response-header]
    :tags ["bob"]
    :description ["Tease out how to do swagger driven response header"]
    (GET "/notdeprectated" [:as request]
      :swagger {:deprecated false}
      :new-relic-name "GET_notdeprecated"
      :return BobResponse
      (status/ok {:message "All is well"}))
    (GET "/isdeprecated" [:as request]
      :swagger {:deprecated true}
      :new-relic-name "GET_isdeprecated"
      :return BobResponse
      (status/ok {:message "You came to the wrong neighborhood."}))))

How do I modify wrap-bob-response-header to only emit x-bob on routes with :swagger {:deprecated true}?

Bob Kuhar
  • 10,838
  • 11
  • 62
  • 115
  • I dont think there is anything to be done inside the wrapper that could do this, since ring handlers are a `request -> response` mapping so any info about which route was matched is unknown before you pass through to the handler and lost by the time you get to the result. The solution is then to modify/replace compojure.core/routing (https://github.com/weavejester/compojure/blob/1.5.1/src/compojure/core.clj#L151) to assoc the matched request onto the response I suppose. – s-ol Mar 27 '19 at 11:28
  • The problem then will be: what about nested routes? – s-ol Mar 27 '19 at 11:30

1 Answers1

1

With Compojure-API, the middleware are invoked in-place, at the path context they are defined at. In your example, the wrap-bob-response-header doesn't yet know where the request is going to go (or will it even match anything). If it knew, you could use the injected route information from the request (see https://github.com/metosin/compojure-api/blob/master/src/compojure/api/api.clj#L71-L73) to determine if the endpoints would have the swagger information set.

What you could do, is mount the header-setting middleware only to the routes that need it.

There is a library called reitit (also by Metosin) which solves this by applying a route-first architecture: the full path lookup is done first and the middleware chain is applied after that. Because of this, all the middleware know the endpoint they are mounted to. Middleware can just query the endpoint data (either at request-time or at compile-time) and act accordingly. They can even decide not to mount to that spesific route.

Reitit is feature-par with compojure-api, just with different syntax, e.g. fully data-driven.

Good examples in the blog: https://www.metosin.fi/blog/reitit-ring/

PS. I'm co-author of both of the libs.

EDIT.

Solution to inject data to the response after a match:

1) create middleware that adds data (or meta-data) to the response

2) add or modify a restructuring handler to mount the middleware from 1 into the endpoint, with the given data (available in the handler)

3) read the data in the response pipeline and act accordingly

(defn wrap-add-response-data [handler data]
  (let [with-data #(assoc % ::data data)]
    (fn
      ([request]
       (with-data (handler request)))
      ([request respond raise]
       (handler #(respond (with-data %)) raise)))))

(defmethod compojure.api.meta/restructure-param :swagger [_ swagger acc]
  (-> acc
      (assoc-in [:info :public :swagger] swagger)
      (update-in [:middleware] into `[[wrap-add-response-data ~swagger]])))

(def app
  (api
    (context "/api" []
      (GET "/:kikka" []
        :swagger {:deprecated? true}
        (ok "jeah")))))

(app {:request-method :get, :uri "/api/kukka"})
; {:status 200, :headers {}, :body "jeah", ::data {:deprecated? true}}
Tommi Reiman
  • 268
  • 1
  • 6
  • Shucks. "mount the header-setting middleware only to the routes that need it." is the issue I'm trying to overcome; duplication of code to support `:swagger {:deprecated false}`. Its weird, though. In my middleware, the route has "happened", I'm modifying the response for pete's sake, there just appears to be nowhere I can look up WHAT route just executed. Am I looking in the wrong place? Is there something in the response that I can use to lookup the route's meta? The thing I'm trying to add, a WARNING header on deprecated route responses, is too low value to change frameworks over – Bob Kuhar Mar 27 '19 at 18:07
  • Nice edit. I have begun considering `(defmethod compojure.api.meta/restructure-param ...` as a means to achieve this. My block is that don't fully understand what all is going on at `(update-in [:middleware] into `[[wrap-add-response-data ~swagger]])`. I'll give this a try when I get a chance. I'm pretty sure I can make this work. – Bob Kuhar Mar 28 '19 at 06:19
  • 1
    It's basically a code generator. Try `macroexpand` on the `(GET...)` form to see what it emits. – Tommi Reiman Mar 28 '19 at 06:32