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!
Yes. For example I use malli to do my validation.
Here is a real-world example:
(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?]])
Then, in my route definition...
:post {:handler (create-investigation app-config)
:swagger {:produces [investigations-create-api-version]}
:parameters {:body specs/create-investigation}}}]
specs is the namespace containing the definition above
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 ๐
malli is pretty awesome ๐
And malli is already included in reitit with swagger? Or do I need to somehow connect reitit-swagger and malli?
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"
I see. There is reitit.coercion.malli/coercion`` . I have
reitit.coercion.spec
. But it seams not to work with swagger-ui.
@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?
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.
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?
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.
there are examples of the same minalistic swagger app for each of: spec, schema and malli.
(also for ring/middleware & http/interceptors)
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?
...
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?]]
with malli, you need to say:
:parameters {:query [:map [:id keyword?]]}
there could be a reitit-side shortcut for allowing to list the keys using a normal map, but malli doesnโt have that
wrote an issue of that, comments welcome: https://github.com/metosin/reitit/issues/434
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?
Oh, I got it! My type needs to be [:fn {:swagger/type "string"} ... ]
not [vector? ...]
.
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 #"," %)}
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
)
you can set the swagger type as "array"
and use "collectionFormat"
, here's the guide: https://swagger.io/docs/specification/2-0/describing-parameters/
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?]]]
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?
I have found reitit.ring.middleware.exception/wrap-log-to-console
, but how do I activate/use it?
I have my own exception handler
: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]}}))
notice the (exceptions/exception-middleware app-config)
Then I do something in that along the lines of
(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))}))))
inside the function exception-info-handler
, you can pull out whatever you want
(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})))
something like that
so I return to the client a body of {:code "foo" :reference "bar" :error "blah blah formatted"}
which is ultimately shoved out as json ๐
When throwing an exception, I do something like this
(throw (ex-info "Foo not found." {:cause "You done messed up" :status :404 :error :resource.foo.notfound}))))
(inside your exception-info-handler, you can log out to where too, in production I use a log file but also sentry)