biff

Akshay 2024-10-12T14:12:13.237249Z

Hi, I’m working with the default Biff template and trying to set up validation for query parameters. My route looks like this:

{:get {:handler    foo
       :parameters {:query [:map
                             [:size int?]]]}}}
However, the validation doesn't seem to be working, and I'm not sure what I’m missing. Any ideas on what could be wrong?

Akshay 2024-10-13T15:02:11.241169Z

@foo Thank you for the code and the explanation. šŸ™

šŸ‘Œ 1
šŸ™Œ 1
jf 2024-10-12T15:28:11.961729Z

I've actually not seen documentation that one could do validation like that. What's your source for this?

Akshay 2024-10-12T15:35:20.496189Z

Oh i saw documentation that biff uses reitit so thought it would support it.

jf 2024-10-12T15:38:30.517829Z

hm, I've only just noticed that https://biffweb.com/docs/reference/routing/ does have links at the bottom that could give the impression that you can use regular reitit... just not sure if that's the case.

2024-10-12T22:06:55.530479Z

Biff is using plain reitit, i.e. the value of :routes and :api-routes get passed to reitit as-is, biff doesn't do any pre-computation on them. It does set some https://github.com/jacobobryant/biff/blob/4b850744b9548232cc428a7e0199974920f18e69/src/com/biffweb/impl/misc.clj#L55 though. It took a bunch of fiddling, but I figured out how to make it work. I took the starter biff project and made the changes below. Notes: • metosin/reitit-ring is already in Biff's deps, but it's an old version (0.6.0, I should probably upgrade it) and I got weird "invalid schema" errors until I upgraded. • I think r.r.coercion/coerce-request-middleware is reitit-specific middleware. I had to put it under the :middleware key; if I just tried to include it in the implementations of wrap-api-defaults / wrap-site-defaults I got an exception at request time. • It seems that middleware also has to go after wrap-api-defaults / wrap-site-defaults, perhaps because those middlewares include muuntaja which does some things to the params (🤷 ) • Note that the coerced params will be under the :parameters key, not :params as usual With the following changes, after starting the app I was able to do:

$ curl -XPOST 
{"query":{"size":1}}
And in the app output I get {:query {:size 1}}
diff --git a/starter/deps.edn b/starter/deps.edn
index 588d41f..3aab0c3 100644
--- a/starter/deps.edn
+++ b/starter/deps.edn
@@ -4,6 +4,8 @@
         metosin/muuntaja                    {:mvn/version "0.6.8"}
         ring/ring-defaults                  {:mvn/version "0.3.4"}
         org.clojure/clojure                 {:mvn/version "1.11.1"}
+        metosin/reitit-malli                {:mvn/version "0.7.2"}
+        metosin/reitit-ring                 {:mvn/version "0.7.2"}
 
         ;; Notes on logging: 
         org.slf4j/slf4j-simple     {:mvn/version "2.0.0-alpha5"}
diff --git a/starter/src/com/example.clj b/starter/src/com/example.clj
index 63fafb1..e40739a 100644
--- a/starter/src/com/example.clj
+++ b/starter/src/com/example.clj
@@ -12,7 +12,9 @@
             [clojure.tools.namespace.repl :as tn-repl]
             [malli.core :as malc]
             [malli.registry :as malr]
-            [nrepl.cmdline :as nrepl-cmd])
+            [nrepl.cmdline :as nrepl-cmd]
+            [reitit.coercion.malli :as r.c.malli]
+            [reitit.ring.coercion :as r.r.coercion])
   (:gen-class))
 
 (def modules
@@ -22,9 +24,12 @@
    schema/module
    worker/module])
 
-(def routes [["" {:middleware [mid/wrap-site-defaults]}
+(def routes ["" {:coercion r.c.malli/coercion}
+             ["" {:middleware [mid/wrap-site-defaults
+                               r.r.coercion/coerce-request-middleware]}
               (keep :routes modules)]
-             ["" {:middleware [mid/wrap-api-defaults]}
+             ["" {:middleware [mid/wrap-api-defaults
+                               r.r.coercion/coerce-request-middleware]}
               (keep :api-routes modules)]])
 
 (def handler (-> (biff/reitit-handler {:routes routes})
diff --git a/starter/src/com/example/app.clj b/starter/src/com/example/app.clj
index 265dab1..7108974 100644
--- a/starter/src/com/example/app.clj
+++ b/starter/src/com/example/app.clj
@@ -137,10 +137,11 @@
    [:p "This app was made with "
     [:a.link {:href ""} "Biff"] "."]))
 
-(defn echo [{:keys [params]}]
+(defn echo [{:keys [parameters]}]
+  (prn parameters)
   {:status 200
    :headers {"content-type" "application/json"}
-   :body params})
+   :body parameters})
 
 (def module
   {:static {"/about/" about-page}
@@ -149,5 +150,6 @@
             ["/set-foo" {:post set-foo}]
             ["/set-bar" {:post set-bar}]
             ["/chat" {:get ws-handler}]]
-   :api-routes [["/api/echo" {:post echo}]]
+   :api-routes [["/api/echo" {:post {:parameters {:query [:map [:size int?]]}
+                                     :handler echo}}]]
    :on-tx notify-clients})

Nik 2024-10-12T23:28:36.956179Z

@foo I was trying it out, but I'm seeing nil in parameters BTW where do we use [reitit.coercion.malli :as r.c.malli] Diff

modified   src/in/yojanakosha/finance/app/transaction.clj
@@ -968,8 +968,27 @@
         (split-transaction-detail-page ctx))
       (simple-transaction-page ctx))))
 
+(defn test-form [_]
+  [:div
+   (biff/form {:action "/echo?size=1"}
+              [:input {:type :number
+                       :value 20
+                       :name :quantity}])
+   [:output "Result"]])
+
+(defn echo [{:keys [params parameters]}]
+  (prn "Params" params)
+  (prn "Parameters" parameters)
+  {:status 200
+   :headers {"content-type" "application/json"}
+   :body parameters})
+
 (def module
-  {:routes [["/finance/transaction"
+  {:routes [["/echo" {:get test-form
+                      :post {:handler echo
+                             :parameters {:form [:map [:quantity double?]]
+                                          :query [:map [:size int?]]}}}]
+            ["/finance/transaction"

2024-10-12T23:31:21.409609Z

r.c.malli/coercion is used in the routes var in your main namespace:

-(def routes [["" {:middleware [mid/wrap-site-defaults]}
+(def routes ["" {:coercion r.c.malli/coercion}          ; <--- here
+             ["" {:middleware [mid/wrap-site-defaults
+                               r.r.coercion/coerce-request-middleware]}
               (keep :routes modules)]
-             ["" {:middleware [mid/wrap-api-defaults]}
+             ["" {:middleware [mid/wrap-api-defaults
+                               r.r.coercion/coerce-request-middleware]}
               (keep :api-routes modules)]])
if you're missing that, that's probably why you're getting nil for parameters

āœ… 1
Nik 2024-10-12T23:38:49.733479Z

Ah, I missed that. Working now, thanks. Coincidentally today I was going to handle the problem of parameters handling for many of my routes. This will save lot of time otherwise spent in trial and error.

šŸ‘Œ 1
Nik 2024-10-13T00:22:53.225059Z

BTW not sure if anyone has encountered following use case - how reitit handles nested parameters a[bcd][quantity] => :a {:bcd {:quantity "20.02"}} a[0][quantity] => :a {"0" {:quantity "20.02"}} (I think other language/frameworks give :a [{:quantity "20.02"}]` a[][quantity] => Gives error right now (at the version biff uses) but when I read code it is supposed to give {:a [quantity]} https://github.com/ring-clojure/ring/blob/1.3.2/ring-core/src/ring/middleware/nested_params.clj I'm not sure this is due to outdated version or bug in reitit The function looks the same. Dep chain => biff -> metosin-reitit (0.6.0) -> ring-core (1.3.2) This is due to outdated version. With the new changes it is working now šŸ˜ But only for params For coercion (parameters) it seems the value is missing. How to Reproduce (after making changes mentioned above) - Form

(defn test-form [_]
  [:div
   (biff/form {:action "/echo?size=1"}
              [:input {:type :text
                       :value "Warke"
                       :name :fname}]
              [:input {:type :text
                       :value "Nikhil"
                       :name :fname}]
              [:input {:type :number
                       :value 20.02
                       :name (str "amount")}]
              [:input {:type :number
                       :value 50.02
                       :name (str "a[quantity]")}]
              [:input {:type :submit}])
   ])
(defn echo [{:keys [params parameters]}]
  (prn "Params" params)
  (prn "Parameters" parameters)
  {:status 200
   :headers {"content-type" "application/json"}
   :body parameters})
In routes
{:routes [["/echo" {:get test-form
                      :post {:handler echo
                             :parameters {:form [:map
                                                 [:amount double?]
                                                 [:fname [:set :string]]
                                                 [:a {:optional true} [:map [:quantity string?]]]
                                              ]
                                         }}}]
            }
Output
"Params" {:__anti-forgery-token "Xwu5kXcudOa+YZPQoIETRvjp8HqPDrmVC/lUGxFyO77yji0EIIrsWGdJxigzihX/0qyEuOD1qD9wx0b5", :fname ["Warke" "Nikhil"], :amount "20.02", :a {:quantity "50.02"}, :size "1"}
"Parameters" {:form {:fname #{"Nikhil" "Warke"}, :amount 20.02}, :query {:size 1}}
If you don't put optional true for :a it will throw missing key error If anyone figures out how to make it, please share šŸ™šŸ¼

jf 2024-10-12T15:45:32.002189Z

I'm guessing I know the answer to this one (don't see any options for this at https://biffweb.com/docs/api/rum), but if one wanted to customize the properties of body as output by com.biffweb/base-html , would it be possible? I don't really have any issues with the properties of body as set by base-html right now, but it would be "nice" to be able to set a colour as well. It's a minor thing, but on a mac when I scroll my page past the edges, you get a peek of white (from body's "natural" colour). If I could specify a class for body, I could avoid that.

2024-10-12T21:02:32.783569Z

I'm guessing that the answer you're guessing you know is the correct one šŸ™‚ As it is now you'd have to copy https://github.com/jacobobryant/biff/blob/4b850744b9548232cc428a7e0199974920f18e69/src/com/biffweb/impl/rum.clj#L29 into your project and modify from there. Someone else asked about this recently too. I probably should factor most of that function out into a base-head function, so in your own project you can just do

(defn base-html [{:base/keys [lang] :as opts} & contents]
  [:html
   {:lang lang
    :style {:min-height "100%"
            :height "auto"}}
   [:head
    (biff/base-head opts)]
   [:body
    {:style {:position "absolute"
             :width "100%"
             :min-height "100%"
             :display "flex"
             :flex-direction "column"}}
    contents]])
Maybe even have the starter project begin with that function in ui.clj instead of using biff/base-html at all...

šŸ‘šŸ¼ 1
šŸ™ 1
Nik 2024-10-13T00:35:06.835659Z

I had same use case (background colors on apple devices). I went with css for now but would love to have more control over body Another use case is having some default stylings (padding etc) on body so that forms and anchor with hx-boost can work while 'switching pages` I'm building a mobile app so my preferences is to swap out header+body+footer inside body. At the moment you can either use main for that or you can use div#shell instead of body)