This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-04-24
Channels
- # ai (41)
- # announcements (4)
- # babashka (14)
- # beginners (24)
- # calva (7)
- # clj-kondo (36)
- # cljsrn (4)
- # clojure (68)
- # clojure-austin (2)
- # clojure-europe (39)
- # clojure-nl (2)
- # clojure-norway (37)
- # clojure-uk (2)
- # clojurescript (7)
- # clr (8)
- # community-development (16)
- # core-async (7)
- # datalevin (67)
- # fulcro (11)
- # helix (1)
- # honeysql (2)
- # london-clojurians (2)
- # off-topic (60)
- # pedestal (3)
- # portal (2)
- # practicalli (1)
- # re-frame (7)
- # reitit (49)
- # releases (3)
- # shadow-cljs (2)
- # xtdb (16)
Why do routes definition take middlewares as data but not handlers? Would it be possible to add support for handlers registry as well?
Another thing - if I have a method handler with associated data, middlewares defined with compile will see it if only if they're inside the map and not of they're global to the route or all routes. Is that expected behavior?
what do you mean with "global to the route or all routes"? All mw inside the route tree sees the data, mw outside (e.g. defined on the ring-handler) will not - they are applied even if the routing misses.
okay, here's the example, hope it makes sense @U055NJ5CC
(require
'[reitit.core :as r]
'[reitit.ring :as ring])
(def mw
{:compile (fn [d o]
(clojure.pprint/pprint {:data d :options o})
identity)})
-------------------------
(->
["/ping" {:get {:name :ping, :handler identity, :middleware [mw]}}]
ring/router
(ring/ring-handler identity {}))
{:data
{:name :ping,
:handler #function[clojure.core/identity],
:middleware [{:compile #function[user/fn--13440]}]},
:options
{:lookup #function[reitit.core/default-router-options/lookup--11046],
:expand #function[reitit.core/eval10712/fn--10713/G--10703--10720],
:coerce #function[reitit.ring/coerce-handler],
:compile #function[reitit.ring/compile-result],
:exception #function[reitit.exception/exception],
:conflicts
#function[reitit.core/default-router-options/throw!--11061],
:reitit.ring/default-options-endpoint
{:no-doc true, :handler #function[reitit.ring/fn--11560/fn--11569]},
:reitit.middleware/compiled 1}}
-------------------------
(->
["/ping" {:get {:name :ping, :handler identity}, :middleware [mw]}]
ring/router
(ring/ring-handler identity {}))
{:data
{:middleware [{:compile #function[user/fn--13440]}],
:name :ping,
:handler #function[clojure.core/identity]},
:options
{:lookup #function[reitit.core/default-router-options/lookup--11046],
:expand #function[reitit.core/eval10712/fn--10713/G--10703--10720],
:coerce #function[reitit.ring/coerce-handler],
:compile #function[reitit.ring/compile-result],
:exception #function[reitit.exception/exception],
:conflicts
#function[reitit.core/default-router-options/throw!--11061],
:reitit.ring/default-options-endpoint
{:no-doc true, :handler #function[reitit.ring/fn--11560/fn--11569]},
:reitit.middleware/compiled 1}}
{:data
{:middleware [{:compile #function[user/fn--13440]}],
:no-doc true,
:handler #function[reitit.ring/fn--11560/fn--11569]},
:options
{:lookup #function[reitit.core/default-router-options/lookup--11046],
:expand #function[reitit.core/eval10712/fn--10713/G--10703--10720],
:coerce #function[reitit.ring/coerce-handler],
:compile #function[reitit.ring/compile-result],
:exception #function[reitit.exception/exception],
:conflicts
#function[reitit.core/default-router-options/throw!--11061],
:reitit.ring/default-options-endpoint
{:no-doc true, :handler #function[reitit.ring/fn--11560/fn--11569]},
:reitit.middleware/compiled 1}}
-------------------------
(->
["/ping" {:get {:name :ping, :handler identity}}]
ring/router
(ring/ring-handler identity {:middleware [mw]}))
{:data nil, :options #:reitit.middleware{:compiled 1}}
This behaves widely differently depending on where I put the same middleware 😬I think I'm missing something about how to include a custom transformation in reitit malli coercion. The following works fine via malli decode:
(def CoercionTest
[:map
[:name :uuid]
[:json-string
{:decode/string #(json/read-str % :key-fn keyword)}
[:map [:json-id Id]]]])
(m/decode
CoercionTest
{:name "405ed88b-d103-4f1b-87df-c4ce6a32a6bb",
:json-string "{\"json-id\": \"3fa85f64-5717-4562-b3fc-2c963f661111\"}"}
mt/string-transformer)
;; => {:name #uuid "405ed88b-d103-4f1b-87df-c4ce6a32a6bb",
;; :json-string {:json-id #uuid "3fa85f64-5717-4562-b3fc-2c963f661111"}}
but if I use the CoercionTest schema in the parameters of an endpoint:
["/coercion-test"
{:post {:summary "Test reitit malli coercion",
:parameters {:body CoercionTest},
:handler coercion-test-handler}}]
the :decode/string fn is never called, and :body params that the handler receives aren't converted:
{:name "3fa85f64-5717-4562-b3fc-2c963f66afa6",
:json-string "{\"json-id\": \"3fa85f64-5717-4562-b3fc-2c963f661111\"}"}
I've tried creating the coercion explicitly with reitit.coercion.malli/create like so
(malli-coercion/create
{:transformers {:string {:default reitit.coercion.malli/string-transformer-provider}}})
(which seems like it would be correct because https://github.com/metosin/reitit/blob/bae6e6b8dd477b5152948e20ad108d73d77024e8/modules/reitit-malli/src/reitit/coercion/malli.cljc#L35, which is what I'm using above with decode) but same result.
I feel like I'm probably missing something really obvious here. Can anyone clue me in?are you sending JSON payloads? In that case, you should define :decode/json
key, not :decode/string
. The latter is for path/query/form params.
reitit + malli configuration and debugging is pita atm. Ideas welcome how to make it simpler!
I am sending JSON payloads, but those are being decoded by muuntaja into edn. Inside that edn there's a string containing JSON (because it needs to be treated differently from the main muuntaja decoding for various reasons -- that's one of the things I'm hoping to get rid of by improving my malli coercion) So once it hits reitit response coercion it's edn:
{:name "405ed88b-d103-4f1b-87df-c4ce6a32a6bb",
:json-string "{\"json-id\": \"3fa85f64-5717-4562-b3fc-2c963f661111\"}"}
Sorry, should have clarified that, it's definitely confusing the way I said it.I don't think any coercion is applied for edn, as it can present most/all values already
I was just reading back through the reitit coercion docs and realized I'm not actually calling coercion/compile-request-coercers
, so now I'm wondering if that could be the problem.
In malli-coercion, there is a config map for which transformers are applied per content-type. You should set something for "application/edn"
too
> I don't think any coercion is applied for edn, as it can present most/all values already
I guess I'm confused by that. So even though I have both reitit.ring.middleware.muuntaja/format-request-middleware
and reitit.ring.coercion/coerce-request-middleware
in the middleware stack, the request coercion is only applied during muuntaja's json-to-edn conversion (by default unless I do your next suggestion below)?
> You should set something for "application/edn"
too
Cool, will dig into that.
> In a plane (to conj), bit laggy internet.
Awesome! I'll only be there virtually this year.
• muuntaja will decode and encode bodies based on content-type • request- and response-coercion middleware select the most suitable malli transformer also based on content-type ◦ only "application/json" has non-default mapping with malli
OK, I think I got it. Awesome, thanks! I'll try adding "application/edn" with -- I guess? -- the default transformer and see how that does. Kind of makes sense, actually, because I noticed I did have to manually convert some uuids. This is my first time using reitit coercion, & my first time using malli, so I'm definitely still finding my way 😄
Yes, add "application/edn" string-provider
and :decode/string
should work with edn too
[not that important, just trying to improve my understanding]
> I don't think any coercion is applied for edn, as it can present most/all values already
Something that confuses me about that: if I trace through the middleware step by step (prior to making the changes you suggested):
• the request comes in as a stream
• :reitit.ring.middleware.muuntaja/format-request
converts the params (body params in this case) to edn in a basic way that doesn't seem to take the actual malli schema referenced in the :parameters
of the endpoint definition into account, eg uuid strings stay strings even though they're specified as :uuid in the schema. Those are added as :body-params
.
• :reitit.ring.coercion/coerce-request
then seems to apply schema-based coercion, eg converting string uuids to actual uuids, and puts the result in {:parameters {:body ...}}
(if I change the schema from :uuid
to :string
then it doesn't convert, so I think it's definitely using the schema). I assumed it was taking the :body-params
as input (in which case it does seem to be applying coercion to edn values), but maybe that's just wrong?
> Yes, add "application/edn" string-provider
and :decode/string
should work with edn too
No such luck on this, unfortunately. I'll keep digging... 🙂
The API for making coercers should be public and documented, that will be a great help Having a "blessed" way of exercising and testing it will also help. I usually mock out my handler and pass a mock request through the entire routing and middleware chain
> I usually mock out my handler and pass a mock request through the entire routing and middleware chain
For sure, that's basically what I'm doing -- no-op handler that just does some inline defs, and one-click request via swagger.
> The API for making coercers should be public and documented, that will be a great help
Mulling it over in the back of my head overnight, that seems like it could be the issue -- that adding the existing string-transformer-provider
for the "application/edn" format isn't sufficient to trigger my custom decoder, and that I need to set up a custom coercer that explicitly includes that decoder. Although it's not clear to me how that relates to the fact that it works with the default string-transformer
when I explicitly malli/decode
(as per the beginning of the OP)...
Ideas on making this easier to configure (for the future):
1. add a named transformer for each different format, .e.g. :edn
for “application/edn”, :transit
for “application/transit”
a. you could say [:json-string {:edn/decode …} …
that applies ONLY when the format is edn => no extra config needed
2. add a named transformers for all body-formats, could be :before
and :after
. One could easily pre- and post-transform data without any extra config. e.g. you could say [:json-string {:before/decode ….}]
and it would happen before anything else. reitit-JSON-transformer would look like this:
(mt/transformer
(mt/transformer {:name :before}) ;; first
(mt/strip-extra-keys-transformer) ;; then strip
(mt/json-transformer) ;; json
(mt/default-value-transformer) ;; add defaults
(mt/transformer {:name :after})) ;; last
Ahhhhh yours totally works for me as well, need to gradually build back up to what I've got and figure out where it's going awry.
Mulling over your configuration thoughts...
For debugging, think about an API that will let a user examine which transformer and decoders their request will trigger, because that's currently black magic for me
> Ahhhhh yours totally works for me as well, need to gradually build back up to what I've got and figure out where it's going awry. Wait, no it doesn't (I accidentally plugged in the un-stringified version initially). Investigating.
> For debugging, think about an API that will let a user examine which transformer and decoders their request will trigger, because that's currently black magic for me One thing that might be really good from a user perspective would be to separate default transformation behavior from explicitly specified decoding, so that eg
[:map [:json-string
{:decode/string #(j/read-value % j/keyword-keys-object-mapper)}
[:map [:id :int]]]]
would always cause that decoder to be called, regardless of what default transformers are in play. I can't immediately think of a use case where a reitit user would want to specify a decoder but not have it run. Not sure how easy that would be from an implementation perspective...> Wait, no it doesn't (I accidentally plugged in the un-stringified version initially). Investigating. Ahh, because yours is doing something different. Sounds like I probably didn't express the original problem well enough. You're testing with
curl -X 'POST' -H 'Content-Type: application/edn' -d '{:json-string "{\"id\": 42}"}'
whereas I'm testing with
curl -X 'POST' '' -H 'Content-Type: application/json' -d '{"json-string": "{\"json-id\": 42}"}'
That is, the issue for me is that the request comes in as json which contains a key whose value is itself stringified json. So the json->edn step works fine, but then after that the resulting edn still contains stringified json that I'm trying to translate.
I assume that even though I can add an "application/edn" transformer, the coercion is still being applied in a single step, ie it's not being run on both the incoming JSON and on the edn that results from initial coercion.
...which makes sense really. What threw me off is that since the initial JSON->edn conversion is being done by the reitit muuntaja middleware, it seemed plausible that the reitit malli coerce-request middleware would then be run on the resulting edn. But if it's looking at the content type of the original request to decide whether to run those transformers, I assume that that wouldn't actually happen.
I remain confused to be honest about the relationship between the reitit.ring.middleware.muuntaja/format-request step and the :reitit.ring.coercion/coerce-request step, and why/whether both are necessary.Maybe it's a fool's errand to try to get request coercion to handle both of those steps and I really just need to explicitly decode the stringified json in the handler. Although it's only being stringified in the first place to prevent default muuntaja coercion, because we need to avoid that on this field, which holds ~arbitrary user-specified data that (unlike everything else) shouldn't be converted to edn. It's a bit of an odd use case.
ok, your content-type is "application/json" here, so use :decode/json
an it should work.
@UK0810AQ2 would like to see that too. Maybe a huddle next week for quick brainstorming on what & how?
> ok, your content-type is "application/json" here, so use :decode/json
an it should work.
Ahhhhhhh that's the thing I was missing all along. I thought, "OK, I"m applying a function to a string, so it must be :decode/string
." 💡
Thanks lots! Appreciate all your help.
@UK0810AQ2 I totally dropped out on the followup. No excuses, I’m sorry.