reitit

https://cljdoc.org/d/metosin/reitit/ https://github.com/metosin/reitit/
witek 2020-09-11T08:06:39.058700Z

Hello. I am playing with reitit , creating a REST API for datahike with swagger-ui. I would like to create an http endpoint for querying. So i have a get path with a q request parameter. But when i put :parameters {:query {:q vector?}} in my router configuration, swagger-ui goes into endless loading-loop as soon as I open the section for this request. Putting :parameters {:query {:q string?}}`` works, but then I loose the validation. Is there a way to use more sophisticated validations then int?, string?, etc.? Thank you!

dharrigan 2020-09-11T08:15:41.059400Z

Yes. For example I use malli to do my validation.

dharrigan 2020-09-11T08:15:49.059700Z

Here is a real-world example:

dharrigan 2020-09-11T08:16:08.059900Z

(def create-investigation [:map
                           {:closed true}
                           [:source [:map
                                     [:id [:string {:min 1 :max 64}]]
                                     [:type [:enum {:swagger/type "string"} "VRN" "DEVICE" "POLICY" "CLAIM" "CONTRACT"]]]]
                           [:from [:fn {:swagger/type "string" :swagger/format "date-time" :error/message iso8601-message} date-time-parser]]
                           [:to {:optional true} [:fn {:swagger/type "string" :swagger/format "date-time" :error/message iso8601-message} date-time-parser]]
                           [:fleetIds {:optional true} [:vector string?]]
                           [:note {:optional true} string?]
                           [:tenantId string?]])

dharrigan 2020-09-11T08:16:40.060200Z

Then, in my route definition...

dharrigan 2020-09-11T08:16:46.060400Z

:post {:handler (create-investigation app-config)
                   :swagger {:produces [investigations-create-api-version]}
                   :parameters {:body specs/create-investigation}}}]

dharrigan 2020-09-11T08:17:05.060700Z

specs is the namespace containing the definition above

dharrigan 2020-09-11T08:18:27.061400Z

date-time-parser is a function that takes the "thing" and does some primitive formatting of the input to try to cope with the weird stuff that people type in ๐Ÿ™‚

dharrigan 2020-09-11T08:19:48.061600Z

malli is pretty awesome ๐Ÿ™‚

witek 2020-09-11T08:27:15.063Z

And malli is already included in reitit with swagger? Or do I need to somehow connect reitit-swagger and malli?

witek 2020-09-11T08:34:00.064300Z

It seams, it is not. When I put :parameters {:query {:q [:map]}}`` , I get an error: :reitit.exception{:cause #error { :cause "Unable to resolve spec: :map"

witek 2020-09-11T08:42:13.065200Z

I see. There is reitit.coercion.malli/coercion`` . I have reitit.coercion.spec. But it seams not to work with swagger-ui.

ikitommi 2020-09-11T08:53:04.067300Z

@witek both the old and the new swagger-ui are bit buggy. there is https://editor.swagger.io/ to play with what works and what doesnโ€™t. Can cook up a working swagger definition in there?

ikitommi 2020-09-11T08:54:35.069200Z

whatever works there can be created from reitit, if not using the spec|schema|malli->swagger auto converter, there is a way to manually create the swagger spec.

witek 2020-09-11T08:56:26.070300Z

I switched to malli coercion. But now I can not use keyword? parameters anymore. specifying :parameters {:path {:id keyword?}} produces error: :malli.core/invalid-schema {:schema {:id #function[clojure.core/keyword?]}} . How do I specify a parameter as a keyword when using malli coercion?

ikitommi 2020-09-11T08:59:12.072400Z

sadly, there isnโ€™t a decent schema format error tool yet. there is #malli to get help, but you should read https://github.com/metosin/malli README first or just check out the working example from https://github.com/metosin/reitit/tree/master/examples/ring-malli-swagger.

ikitommi 2020-09-11T08:59:36.073Z

there are examples of the same minalistic swagger app for each of: spec, schema and malli.

ikitommi 2020-09-11T08:59:49.073400Z

(also for ring/middleware & http/interceptors)

witek 2020-09-11T09:08:24.077Z

Well malli directly (`(m/validate keyword? :mykey)`) works. But providing `:parameters {:path {:id keyword?}}`` in reitit produces error: :malli.core/invalid-schema {:schema {:id #function[clojure.core/keyword?]}}``. The examples have only int? parameters, no keyword?...

ikitommi 2020-09-11T09:09:12.077900Z

Schema:

{:kikka s/Str
 (s/optional-key :kukka) s/Int
 s/Keyword s/Any}
Spec:
(s/def ::kikka string?)
(s/def ::kukka int?)

(s/keys :req-un [::kikka], :opt-un [:kukka])
Malli:
[:map
 [:kikka string?]
 [:kukka {:optional true} int?]]

ikitommi 2020-09-11T09:09:47.078700Z

with malli, you need to say:

:parameters {:query [:map [:id keyword?]]}

๐Ÿ˜€ 1
๐Ÿ™ 1
ikitommi 2020-09-11T09:10:19.079400Z

there could be a reitit-side shortcut for allowing to list the keys using a normal map, but malli doesnโ€™t have that

ikitommi 2020-09-11T09:15:44.080Z

wrote an issue of that, comments welcome: https://github.com/metosin/reitit/issues/434

witek 2020-09-11T10:04:46.083600Z

OK, now I have :parameters {:query [:map [:q [vector? {:swagger/type "string"}]]] and swagger-ui works. But when I call the request, providing q as a string (`[:find]`) then reitit coercion fails: "humanized": {"q": ["should be a vector"]} . So how do I get reitit to convert the string from the request parameter to a vector which is required by my handler? Or do I have to do it manually in my handler?

witek 2020-09-11T10:13:13.085100Z

Oh, I got it! My type needs to be [:fn {:swagger/type "string"} ... ] not [vector? ...] .

ikitommi 2020-09-11T11:31:20.089Z

you can also add decoding logic to the schema. If you want to have a vector, this should work:

[:vector 
 {:swagger {:type "string", :example "1,2,3"}
  :decode/json #(str/split #"," %)}

ikitommi 2020-09-11T11:33:44.091800Z

I recall there is a swagger way of saying that the parameter should be a list of stuff with delimeter x, so the ui creates a list input out of it (and concats the values with x)

ikitommi 2020-09-11T11:37:41.093900Z

you can set the swagger type as "array" and use "collectionFormat", here's the guide: https://swagger.io/docs/specification/2-0/describing-parameters/

witek 2020-09-11T11:43:13.095Z

I'm ok with swagger taking the parameter as a string. I just want it converted to a vector when passed to the request handler. This is what now works for me: :query [:map [:q [:vector {:swagger/type "string" :decode/string edn/read-string} any?]]]

๐Ÿ‘ 1
witek 2020-09-11T14:19:03.098400Z

Sometimes I have an error in my request handler and it throws an Exception. The middleware exception/exception-middleware seams to catch them and write type and class of the exception to the response. But then all other exception info including the stack trace is lost. It is not printed to the output anymore. What is the idiomatic way to get this exception info while development? What ist the idiomatic way to get it logged in production?

witek 2020-09-11T14:27:54.099100Z

I have found reitit.ring.middleware.exception/wrap-log-to-console , but how do I activate/use it?

dharrigan 2020-09-11T14:33:00.099400Z

I have my own exception handler

dharrigan 2020-09-11T14:33:22.099600Z

:middleware [swagger/swagger-feature
                        muuntaja/format-middleware
                        (exceptions/exception-middleware app-config)
                        parameters/parameters-middleware
                        coercion/coerce-exceptions-middleware
                        coercion/coerce-request-middleware
                        coercion/coerce-response-middleware]}}))

dharrigan 2020-09-11T14:33:38.100Z

notice the (exceptions/exception-middleware app-config)

dharrigan 2020-09-11T14:34:05.100300Z

Then I do something in that along the lines of

dharrigan 2020-09-11T14:35:08.100900Z

(defn exception-middleware
  [app-config]
    (exception/create-exception-middleware
     (merge
      exception/default-handlers
       clojure.lang.ExceptionInfo exception-info-handler      
       ::exception/wrap (fn [handler exception request] (log/error exception) (handler exception request))}))))

dharrigan 2020-09-11T14:35:32.101300Z

inside the function exception-info-handler, you can pull out whatever you want

dharrigan 2020-09-11T14:35:47.101500Z

(defn exception-info-handler
  [exception-info _] ; exception(-info) and request (not-used) come from reitit.
  (let [uuid (.toString (UUID/randomUUID))
        {:keys [cause status error]} (ex-data exception-info)
        {:keys [code msg]} (split-error-message (i18n [error]))
        formatted-error (format-error error msg cause)
        reference (first (split uuid hypen))
        body {:code code :reference reference :error formatted-error}]
    (condp = status
      :404 {:status not-found :body body}
      :409 {:status conflict :body body}
      {:status bad-request :body body})))

dharrigan 2020-09-11T14:35:50.101700Z

something like that

dharrigan 2020-09-11T14:37:26.102300Z

so I return to the client a body of {:code "foo" :reference "bar" :error "blah blah formatted"}

dharrigan 2020-09-11T14:37:35.102500Z

which is ultimately shoved out as json ๐Ÿ™‚

dharrigan 2020-09-11T14:38:33.102800Z

When throwing an exception, I do something like this

dharrigan 2020-09-11T14:39:12.103300Z

(throw (ex-info "Foo not found." {:cause "You done messed up" :status :404 :error :resource.foo.notfound}))))

dharrigan 2020-09-11T14:44:47.103800Z

(inside your exception-info-handler, you can log out to where too, in production I use a log file but also sentry)