Fork me on GitHub
#reitit
<
2023-04-24
>
Ben Sless04:04:06

Why do routes definition take middlewares as data but not handlers? Would it be possible to add support for handlers registry as well?

ikitommi18:04:53

It would make sense.

Ben Sless04:04:29

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?

ikitommi18:04:12

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.

ikitommi18:04:46

if the routing misses, there is no route data to work with.

Ben Sless04:04:14

I'll make a minimal repro later today

👍 2
Ben Sless13:04:40

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 😬

Ben Sless13:05:58

Bump? I'm not even sure if this is incorrect

Ben Sless09:04:54

footgun - nested routes that for some reason both have :parameters data 🙃

🫂 2
☝️ 2
ikitommi18:04:42

about to fix that next

eggsyntax18:04:08

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?

ikitommi19:04:08

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.

ikitommi19:04:56

reitit + malli configuration and debugging is pita atm. Ideas welcome how to make it simpler!

👍 2
eggsyntax19:04:27

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.

ikitommi19:04:45

I don't think any coercion is applied for edn, as it can present most/all values already

eggsyntax19:04:46

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.

ikitommi19:04:12

No, that is not needed with ring

👍 2
ikitommi19:04:41

In malli-coercion, there is a config map for which transformers are applied per content-type. You should set something for "application/edn" too

ikitommi19:04:15

In a plane (to conj), bit laggy internet.

eggsyntax19:04:39

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

👍 2
ikitommi19:04:12

• 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

ikitommi19:04:55

configuratiob could be simpler, but is explicit at least (I think)

👍 2
eggsyntax19:04:22

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 😄

eggsyntax19:04:40

Really much appreciated, helping out from the plane is super above and beyond 😆

ikitommi19:04:48

Yes, add "application/edn" string-provider and :decode/string should work with edn too

💯 2
eggsyntax20:04:07

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

eggsyntax20:04:14

[not that important, just trying to improve my understanding]

eggsyntax20:04:58

> Yes, add "application/edn" string-provider and :decode/string should work with edn too No such luck on this, unfortunately. I'll keep digging... 🙂

Ben Sless05:04:31

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

2
👍 2
eggsyntax13:04:39

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

ikitommi13:04:24

quick poke on it, seems to work correctly.

👍 2
eggsyntax13:04:29

Thanks, that's really useful (but blows my overnight theory 😆)!

🙃 2
ikitommi13:04:33

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

ikitommi13:04:25

need to draft few variations of this..

eggsyntax13:04:34

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

Ben Sless13:04:34

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

👍 2
eggsyntax13:04:27

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

eggsyntax13:04:47

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

👍 2
eggsyntax14:04:02

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

eggsyntax14:04:56

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.

ikitommi16:04:16

ok, your content-type is "application/json" here, so use :decode/json an it should work.

ikitommi16:04:46

@UK0810AQ2 would like to see that too. Maybe a huddle next week for quick brainstorming on what & how?

👍 2
eggsyntax17:04:53

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

👍 2
Ben Sless17:04:16

Sure, I can make time

👌 2
ikitommi12:05:44

@UK0810AQ2 I totally dropped out on the followup. No excuses, I’m sorry.

Ben Sless13:05:25

No worries, I had production incidents to keep me busy 🙃 Feel free to reach out when possible