Fork me on GitHub
#reitit
<
2020-09-11
>
witek08:09:39

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!

dharrigan08:09:41

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

dharrigan08:09:49

Here is a real-world example:

dharrigan08:09:08

(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?]])

dharrigan08:09:40

Then, in my route definition...

dharrigan08:09:46

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

dharrigan08:09:05

specs is the namespace containing the definition above

dharrigan08:09:27

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 🙂

dharrigan08:09:48

malli is pretty awesome 🙂

witek08:09:15

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

witek08:09:00

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"

witek08:09:13

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

ikitommi08:09:04

@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?

ikitommi08:09:35

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.

witek08:09:26

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?

ikitommi08:09:12

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.

ikitommi08:09:36

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

ikitommi08:09:49

(also for ring/middleware & http/interceptors)

witek09:09:24

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?...

ikitommi09:09:12

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?]]

ikitommi09:09:47

with malli, you need to say:

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

😀 3
🙏 3
ikitommi09:09:19

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

ikitommi09:09:44

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

witek10:09:46

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?

witek10:09:13

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

ikitommi11:09:20

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 #"," %)}

ikitommi11:09:44

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)

ikitommi11:09:41

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

witek11:09:13

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?]]]

👍 3
witek14:09:03

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?

witek14:09:54

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

dharrigan14:09:00

I have my own exception handler

dharrigan14:09:22

: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]}}))

dharrigan14:09:38

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

dharrigan14:09:05

Then I do something in that along the lines of

dharrigan14:09:08

(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))}))))

dharrigan14:09:32

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

dharrigan14:09:47

(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})))

dharrigan14:09:50

something like that

dharrigan14:09:26

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

dharrigan14:09:35

which is ultimately shoved out as json 🙂

dharrigan14:09:33

When throwing an exception, I do something like this

dharrigan14:09:12

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

dharrigan14:09:47

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