reitit

Andrey Subbotin 2025-05-23T07:45:44.580409Z

Hey all, I've stumbled upon a problem I can't seemingly resolve easily: 1. I use reitit.http.coercion/coerce-request-interceptor in my interceptor chain to validate request params. 2. An interceptor running earlier in the chain, loads an i18n bundle and makes a translation function available on the request. 3. I'd like to use the function to translate possible error messages for the params. 4. Yet, the :parameters key on the route consumed by the coerce-request-interceptor can only be a schema directly. There doesn't seem to be a way to generate the schema for each request after the router has been constructed. I'd love to be able to define something like the below on a route, where translated needs to be injected somehow from the request...

...
:parameters (fn [translated]
  [:map
   [:email [:re {:error/message (translated "Please provide an email address.")}
            #"^\S+@\S+\.\S+$"]]
   [:password [:string {:error/message (translated "Please provide a password.")
                        :min           1}]]])
...
Right now I have the :parameters value as this:
{:form
 [:map
  [:email :string]
  [:password :string]]}
And then I have a separate interceptor that runs a function not unlike the one above to actually supply proper translated error messages. It all works, but I end up having the schema defined in two places and I don't like this. Any other options, ideas on how this could be structured? Can I change the matched route definition somehow during the request processing so that the coerce-request-interceptor sees an updated :parameters value?

opqdonut 2025-05-26T06:11:58.665419Z

Interesting question. I don't see an easy way around this given your constraints. One way would be to use malli's own localisation and to have maps like:

{:error/message {:en "please provide an email address" :fi "sähköpostiosoite puuttuu"}}

opqdonut 2025-05-26T06:33:45.409199Z

Another way would be to add some indirection, and have some sort of translation keys / identifiers in the schema, and then have an interceptor that post-processes the coercion failure response.

Andrey Subbotin 2025-05-31T06:27:04.210639Z

Hm... Using malli's own localisation is not an option as the set of target languages and translations is not known at build time. The localisation bundles are packaged separately and only the one in use is available at run time in our case. The indirection via some translation keys would have worked, but it also would have introduced a requirement to reconcile the keys used with the translations available, which we kind of aim to avoid by using the direct source language (that being English in this project) strings as translation message IDs. What I have ended up doing is creating a new interceptor, that basically does the same thing as the coerce-request-interceptor and also translates the error messages in the schema:

(defn- translated-coerce-request-interceptor
  "Interceptor for pluggable request coercion.
  Expects a :coercion of type `reitit.coercion/Coercion`
  and :parameters from route data, otherwise does not mount."
  []
  {:name ::translated-coerce-request-interceptor
   :compile
   (fn [{:keys [coercion parameters request]} opts]
     (cond
       ;; no coercion, skip
       (not coercion)                nil
       ;; just coercion, don't mount
       (not (or parameters request)) {}
       ;; mount
       :else
       {:enter
        (fn [ctx]
          (let [cur-request (:request ctx)
                coercers    (coercion/request-coercers
                             coercion
                             (if (fn? parameters)
                               (parameters (:i18n/translated cur-request))
                               parameters)
                             request opts)
                coerced     (coercion/coerce-request coercers cur-request)
                cur-request (assoc cur-request :parameters coerced)]
            (assoc ctx :request cur-request)))}))})

Andrey Subbotin 2025-05-31T06:28:54.046159Z

The trade-off, as far as I get it, is the coercers are constructed on each request, but it doesn't seem to impact the overall request handling time much, and it seems to be acceptable in our case.