This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-08-06
Channels
- # announcements (3)
- # architecture (16)
- # beginners (5)
- # cherry (1)
- # cider (3)
- # cljsrn (2)
- # clojure (54)
- # clojure-dev (11)
- # clojure-europe (14)
- # datalevin (26)
- # emacs (8)
- # helix (5)
- # honeysql (5)
- # hyperfiddle (40)
- # lsp (12)
- # malli (23)
- # missionary (7)
- # nrepl (2)
- # off-topic (18)
- # releases (2)
- # yamlscript (1)
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.This kind of check where the underlying data is mutable (database) you should make the check in the db most probably.
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.
Your db probably already has a uniqueness constraint and you can catch a possible violation quite easily (outside malli).
i think what tempts me here is the standardization of error message generation and validation
Yep, it may sound tempting. But you are not going to catch all the possible error situations of your app with malli 🙂
So maybe you can reuse the error reporting from malli with other error messages as well?
Though you can’t control error messages that are from 3rd party libraries or Clojure itself
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.]}
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 😄
Doesn’t seem like too bad abuse to me. 😉 You can also try your luck with malli.error
namespace directly
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.