malli

https://github.com/metosin/malli :malli:
borkdude 2021-03-04T13:24:24.134Z

Nice tweet @mikethompson :) https://twitter.com/wazound/status/1367303612028751872

borkdude 2021-03-04T13:25:02.134700Z

I hear similar sentiments in the latest #defnpodcast - there they called it the Osborne effect. https://en.wikipedia.org/wiki/Osborne_effect#:~:text=The%20Osborne%20effect%20is%20a,announcing%20a%20future%20product%20prematurely.

borkdude 2021-03-04T13:30:26.137700Z

I have the exact same problem with babashka: waiting for spec2 so not including spec1: and this can result in years of having nothing perhaps

2021-03-04T13:31:26.138700Z

I didn't hear that podcast, but yeah, I feel like Clojure needs an outcome. We're stuck in something of a twilight zone. And this is an important issue. And right now Malli is just better. Unless there is something pending with spec ... it feels like we need permission to get on with making Malli "it"

borkdude 2021-03-04T13:33:53.140700Z

In some way, no matter how good your library is, you can't compete with core since people will just believe that "core is better" no matter what you do, which isn't fair maybe. But "better" is probably not the right way to describe it: spec and malli have different approaches. spec is more of an RDF approach where each unique attribute name describes a schema

2021-03-04T13:34:35.141500Z

I'm the same as you: should re-frame support spec or Malli. I've held off for a long time.

borkdude 2021-03-04T13:35:15.142100Z

Why should re-frame have to choose here? Can't this be decided in some add-on lib? Why do you need to address this concern in re-frame itself?

2021-03-04T13:35:32.142600Z

I could

borkdude 2021-03-04T13:35:47.143300Z

Re-frame events are already associated with globally namespaced keywords which maybe aligns well with spec

2021-03-04T13:36:27.144200Z

But if ever spec is ratified as the one true way for Clojure, I'd like for it to be the one true way for re-frame too

2021-03-04T13:36:46.144500Z

Anyways

borkdude 2021-03-04T13:37:16.144800Z

Maybe it can be made pluggable

juhoteperi 2021-03-04T13:45:09.145500Z

Reitit supports all three so the application can choose.

juhoteperi 2021-03-04T13:45:48.146400Z

Pluggable solution is probably also quite important in Cljs apps, because either Spec or Malli both add about 100KB JS to the output bundle (before gzip).

juhoteperi 2021-03-04T13:46:53.147100Z

I was surprised by this recently when I checked project output report in app that uses Malli, but re-chain uses Spec internally: https://github.com/ingesolvoll/re-chain/issues/6

ikitommi 2021-03-04T15:38:50.150800Z

Interesting discussion. About Malli cljs-size. The core has been developed DCE in mind, you can make a Malli bundle with just validation of strings, numbers, maps, vector and sets and it’s few kilos zipped. Currently, many extensions (humanized errors, generation, json schema etc) are implemented using multimethods, so pulling anything out of those, makes it much bigger.

ikitommi 2021-03-04T15:39:08.151200Z

The Code Size Expression Problem.

ikitommi 2021-03-04T15:40:45.151300Z

ikitommi 2021-03-04T15:40:59.151900Z

here’s the bare-bones malli.

ikitommi 2021-03-04T15:42:39.152700Z

@mikethompson what would the spec/malli integration look like? do you need something?

juhoteperi 2021-03-04T16:02:38.154Z

And Reitit Malli coercion is built for Ring model, so it includes lots of features that are unnecessary for frontend routing. When we get around to implementing separate frontend coercion, it will drop dependency on some unused parts.

juhoteperi 2021-03-04T16:03:03.154200Z

Like the json schema generation parts.

emccue 2021-03-04T16:12:40.155Z

^i'm not opposed to the json schema stuff but it does feel odd that it isn't a separate artifact

juhoteperi 2021-03-04T16:15:15.155900Z

It is separate namespace on Malli. On Reitit coercion impl. the parameter validation and generating the json-schemas for routes is closely related so they are on the same module currently.

ikitommi 2021-03-04T16:17:10.157500Z

yes, there should be a lite-version of reitit coercion, with just the encoding & decoding part - without throwing exceptions. Frontend would use that: simple, pure and small.

ikitommi 2021-03-04T16:18:21.158700Z

malli is designed so that malli.core is the essential ns, all others are optional (ok, there is nowadays malli.impl too). It has been easier to optimize the whole as everyhing is in single repo.

arundilipan 2021-03-04T17:39:35.160500Z

Hi everyone, I’m not sure if this is a bug, but I’m running into a quirk with the validation and error reporting of :catn schemas

arundilipan 2021-03-04T17:40:01.161100Z

For example if you had a schema like:

[:catn 
  [:amount [:fn {:error/fn '(fn [{:keys [value]} _] (str "Received value " value ", should be > 0"))
        :decode/string malli.transform/-string->double}
    '#(> % 0)]]
  [:type [:enum "A" "B" "C"]]]

arundilipan 2021-03-04T17:45:01.163700Z

If I try and (malli.core/explain schema [1.0 "D"]) , and humanize it, the error I get back is "Received value 1.0, should be > 0" , rather than "should be one of "A", "B", or "C"

borkdude 2021-03-04T17:47:47.164100Z

Isn't the idea of :catn that you provide names for the schema elements?

borkdude 2021-03-04T17:47:56.164300Z

Else you should just use :cat

arundilipan 2021-03-04T17:56:45.164600Z

Fixed the example, I do intend to use :catn to provide names for the schema elements

ikitommi 2021-03-04T20:00:31.165500Z

@arundilipan seems to work:

(-> [:catn
     [:amount [:fn {:error/fn '(fn [{:keys [value]} _] (str "Received value " value ", should be > 0"))
                    :decode/string malli.transform/-string->double}
               '#(> % 0)]]
     [:type [:enum "A" "B" "C"]]]
    (m/explain [1.0 "D"])
    (malli.error/humanize))
; => [nil ["should be either A, B or C"]]

borkdude 2021-03-04T20:11:59.165900Z

should :catn not give back error messages by name?

ikitommi 2021-03-04T20:18:47.167100Z

error messages by name?

borkdude 2021-03-04T20:19:10.167600Z

{:amount nil :type ["should be ..."]}

arundilipan 2021-03-04T20:19:34.168800Z

That’s for :map i think

borkdude 2021-03-04T20:19:54.169500Z

are the positions in :cat always unambiguous? I know for spec they are certainly not with s/cat

ikitommi 2021-03-04T20:19:57.169700Z

hmm. not sure. the current humanize just mimics the raw result, not parsed one.

ikitommi 2021-03-04T20:20:12.170Z

but the info is there:

ikitommi 2021-03-04T20:20:17.170300Z

(-> [:catn
     [:amount [:double {:min 0}]]
     [:type [:enum "A" "B" "C"]]]
    (m/explain [1.0 "D"]))
;{:schema [:catn [:amount [:double {:min 0}]] [:type [:enum "A" "B" "C"]]],
; :value [1.0 "D"],
; :errors (#Error{:path [:type 0], :in [1], :schema [:enum "A" "B" "C"], :value "D"})}

ikitommi 2021-03-04T20:21:12.171100Z

e.g. it’s a sequence, errors are positioned by :in.

borkdude 2021-03-04T20:21:23.171400Z

maybe for tools like expound it would be nice to have the humanized error along with the path?

borkdude 2021-03-04T20:21:34.171600Z

ah I see

borkdude 2021-03-04T20:21:46.171900Z

so using the explain output you can get to the error message by path

borkdude 2021-03-04T20:21:56.172200Z

using get-in or so

ikitommi 2021-03-04T20:23:37.173300Z

for map, the explain info is:

(-> [:map
     [:amount [:double {:min 0}]]
     [:type [:enum "A" "B" "C"]]]
    (m/explain {:amount 1.0
                :type "D"}))
;{:schema [:map [:amount [:double {:min 0}]] [:type [:enum "A" "B" "C"]]],
; :value {:amount 1.0, :type "D"},
; :errors (#Error{:path [:type 0], :in [:type], :schema [:enum "A" "B" "C"], :value "D"})}

ikitommi 2021-03-04T20:24:32.174200Z

but yes, the branch name is available in the error data for :catn, to be printed nicely etc.

ikitommi 2021-03-04T20:26:02.175100Z

@arundilipan merged a humanized error for :double, so this works now too:

(-> [:catn
     [:amount [:double {:min 0}]]
     [:type [:enum "A" "B" "C"]]]
    (m/explain [1.0 "D"])
    (malli.error/humanize))
; => => [nil ["should be either A, B or C"]] 

borkdude 2021-03-04T21:10:42.176600Z

user=> (require '[malli.error :as e])
nil
user=> (require '[malli.core :as m])
nil
user=> (e/humanize (m/explain [:cat int? int? [:? int?] [:? string?]] [1 2 :foo]))
[nil nil ["should be an int" "should be a string" "unknown error"]]
Looks legit, except maybe for the "unknown error"?

borkdude 2021-03-04T21:13:47.177100Z

That's probably better expressed as:

(m/explain [:cat int? int? [:orn [:x int?] [:y string?]]] [1 2 :foo])
Just wanted to see what malli would make of it

ikitommi 2021-03-04T22:04:33.178200Z

good catch @borkdude, the regex error types didn’t have humanized forms. fixed in master:

(-> [:cat int? int?]
    (m/explain [1])
    (me/humanize))
; => [nil ["end of input"]]

(-> [:cat int? int?]
    (m/explain [1 2 3])
    (me/humanize))
; => [nil nil ["input remaining"]]

(-> [:cat int? int? [:? int?] [:? string?]]
    (m/explain [1 2 :foo])
    (me/humanize))
; => [nil nil ["should be an int" "should be a string" "input remaining"]]

borkdude 2021-03-04T22:05:57.178500Z

nice!

ikitommi 2021-03-04T22:08:17.178800Z

with :or:

(-> [:cat int? int? [:or int? string?]]
    (m/explain [1 2 :foo])
    (me/humanize))
; => [nil nil ["should be an int" "should be a string"]]

borkdude 2021-03-04T22:14:00.179100Z

yeah, that already worked right?

1👍