Fork me on GitHub
#malli
<
2023-08-06
>
mengu14:08:15

do validations such as uniqueness belong to a malli schema? what’s the stance there?

(def User
  [:map
   [:id :uuid]
   [:username [:and :string [:not username-exists?]]]
   [:email [:and :string [:not email-exists?]]]
   [:password :string]])
feels like it’s not to be honest because the exists? fns also need another argument, db to be able to run a query and I couldn’t figure out a safe way to do that.

mengu14:08:43

this way I know I broke at least the generate fn for example

valtteri15:08:48

Talking to the database should happen outside malli

valtteri15:08:47

This kind of check where the underlying data is mutable (database) you should make the check in the db most probably.

valtteri15:08:52

It would be possible to read all the usernames from the db and construct a malli schema that forces uniqueness at that point in time but that would be highly inefficient.

valtteri15:08:09

Your db probably already has a uniqueness constraint and you can catch a possible violation quite easily (outside malli).

mengu16:08:40

i think what tempts me here is the standardization of error message generation and validation

mengu16:08:45

of course I can let the db to throw constraint exception and catch it there

valtteri16:08:50

Yep, it may sound tempting. But you are not going to catch all the possible error situations of your app with malli 🙂

valtteri16:08:10

(at least in most apps you don’t)

mengu16:08:19

of course

valtteri16:08:06

So maybe you can reuse the error reporting from malli with other error messages as well?

valtteri16:08:21

Though you can’t control error messages that are from 3rd party libraries or Clojure itself

valtteri16:08:33

But I guess you’re talking about errors returned by your API?

valtteri16:08:41

Or dev-time error reporting?

mengu16:08:50

errors returned by my API indeed

mengu16:08:48

so, right now, here’s what I’ve come up with:

(def SignupRequest
  [:map
   [:username [:string {:min 3, :max 100}]]
   [:email [:re {:error/message "must be a valid e-mail address."}
            #"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"]]
   [:password [:string {:min 8}]]])

(defn gen-unique-constraints [db]
  [:map
   [:email    [:fn {:error/message "is already taken."} #(email-available? db %1)]]
   [:username [:fn {:error/message "is already taken."} #(username-available? db %1)]]])

(defn is-valid-user? [db posted-user]
  (let [unique-schema  (gen-unique-constraints db)
        unique-user?   (->> posted-user (m/explain unique-schema) me/humanize)
        valid-request? (->> posted-user (m/explain SignupRequest) me/humanize)]
    (merge-with into unique-user? valid-request?)))
so now I get a result like:
{:username [is already taken.], :email [must be a valid e-mail address.]}

mengu16:08:44

i’ve separated the actual request from the second validation but merged the errors into one. not sure if i’m abusing here but i’m not not liking it 😄

valtteri16:08:24

Doesn’t seem like too bad abuse to me. 😉 You can also try your luck with malli.error namespace directly

valtteri16:08:04

Tommi (the author) makes everything modular and composable

mengu16:08:20

I’ll take a look at that

mengu16:08:26

tbh so far I’m contempt 😄

pithyless10:08:44

Some thoughts to consider over coffee: The DB uniqueness constraints need to be checked atomically, so unless you're doing this all within a locked DB transaction, you still need to check again during the actual DB write, catch the potential errors, and render the user error messages. So probably there is a second piece of code that does something very similar - what does the malli validation beforehand give you? This looks as if you're trying to repurpose malli into a declarative control-flow library. Once you start adding more and more logic and branching to the control flow, you may realize you want something with more control knobs and hooks than malli can provide. Just like clojure.spec was repurposed a lot for data coercion (which had lots of rough edges), I think repurposing malli for control-flow also may have some rough edges that are not immediately obvious.

☝️ 2