malli

https://github.com/metosin/malli :malli:
Markus Str 2020-11-03T11:32:51.405900Z

Hi, I'm getting started with malli and this migh be a noob question, but why is this an invalid schema error? (I took the source of string? in cljs and changed it to str2? Weirdly string? is valid and the alias isn't

(defn ^boolean str2?
  "Returns true if x is a JavaScript string."
  [x]
  (goog/isString x))
(def Test
  [:map
   [:url str2?]
   [:det map?]
   ]
  )

(-> Test
    (m/explain {:url "sa" :det {:a 5}})
    (me/humanize)
    )

Lucy Wang 2020-11-04T12:13:32.438800Z

> good point. maybe for some people serializing a schema is not important? to be frank I think quite some (if not most) people don't need that ...

Markus Str 2020-11-03T11:34:44.406Z

Why can't I define a boolean predicate function there?

borkdude 2020-11-03T11:41:15.406200Z

@strasser.ms Any reason you're not using string?

Lucy Wang 2020-11-03T11:44:01.406400Z

@strasser.ms try [:fn str2]

đź‘Ť 1
ikitommi 2020-11-03T11:51:49.406700Z

the default shemas are listed here: https://github.com/metosin/malli/blob/master/src/malli/core.cljc#L1223-L1269

borkdude 2020-11-03T11:53:43.407Z

FWIW string? on CLJS is:

(defn ^boolean string?
  "Returns true if x is a JavaScript string."
  [x]
  (goog/isString x))

Markus Str 2020-11-03T11:53:54.407200Z

thanks for the quick repsonse @borkdude, str2? was just for figuring out if my function was the issue

borkdude 2020-11-03T11:54:08.407400Z

ah ok

Markus Str 2020-11-03T11:54:13.407600Z

@wxitb2017 thanks that works actually, but error messages are not defined then

Markus Str 2020-11-03T11:54:19.407800Z

example:

(defn twitter-url? [s] (re-find #"http[s]?://twitter.*\d+" s))
(def Test
  [:map
   [:url [:fn twitter-url?]]
   [:det map?]
   ]
  )
(-> Test
    (m/explain {:url "<https://twitter.com/dennybritz/status/1323580850433269760>" :det {:a 5}})
    (me/humanize)
    )

Markus Str 2020-11-03T11:55:26.408Z

(-&gt; Test
    (m/explain {:url "<https://notwitterurl>" :det {:a 5}})
    (me/humanize)
    )

Markus Str 2020-11-03T11:55:32.408200Z

{:url ["unknown error"]} then

borkdude 2020-11-03T11:55:41.408400Z

@strasser.ms Have you tried https://github.com/metosin/malli#custom-error-messages?

borkdude 2020-11-03T11:56:39.408800Z

Maybe malli expects the function to return a boolean instead of nil? don't know

Markus Str 2020-11-03T11:58:20.409Z

Thanks for the pointer

ikitommi 2020-11-03T11:59:22.409200Z

nils should be ok, also regexs:

[:map
 [:x #"http[s]?://twitter.*\d+"]
 [:y [:fn {:error/message "invalid"} (constantly false)]]

Markus Str 2020-11-03T12:08:07.409400Z

Ah interesting, so regex work, but I can't have something like:

[:x #(re-find #"http[s]?://twitter.*\d+" %)]
Probably have to add to the default schema registry then, to have it work naturally like, [:x twitter-url?] I guess?

Markus Str 2020-11-03T12:10:02.409600Z

I'm going to read the docs for the third time; seems my mental model is not there yet!

Markus Str 2020-11-03T12:13:00.409800Z

(defn twitter-url? [s] (re-find #"http[s]?://twitter.*\d+" s))
(def -twitter-url? [:fn {:error/message "invalid twitter url"} twitter-url?])

(def Test
  [:map
   [:url -twitter-url?]
   [:det map?]
   ]
  )
(-&gt; Test
    (m/explain {:url "<https://twitter.com/dennybritz/status/1323580850433269760>" :det {:a 5}})
    (me/humanize)
    )
So this is how I could do it; only backdraw is that I have two functions now; one for normal use, the other specifically for validation

ikitommi 2020-11-03T12:16:01.410Z

could make the predicate schemas implement IFn, so you could:

(def twitter-url?
  (m/schema 
    [:re {:error/message "invalid twitter url"}
       #"http[s]?://twitter.*\d+"]))

(twitter-url? "<https://twitter.com/dennybritz/status/1323580850433269760>")
; =&gt; true

(m/validat twitter-url? "<https://twitter.com/dennybritz/status/1323580850433269760>")
; =&gt; true

Markus Str 2020-11-03T12:28:01.410500Z

Looks promising. For me personally (and I might be totally off) it could be nice for malli to also accept predicate functions directly inside the spec-vector

(defn twitter-url? [s] (re-find #"http[s]?://twitter.*\d+" s))
(def Test
  [:map
   [:url twitter-url?]
   ]
  )
For now I'll read up on the registry and how to add it there

borkdude 2020-11-03T12:34:12.410800Z

What's the reason it requires [:fn ..] right now @ikitommi?

2020-11-03T13:13:39.413400Z

is there something off with generators and the [:and] clause ?

(def schema [:and map?
             [:map
              [:foo string?]]])

(mg/generate schema)
throws an error ("Couldn't satisfy such-that predicate after 100 tries.") this, however, works:
(def schema [:and
             [:map
              [:foo string?]]
             map?])

(mg/generate schema)
my hunch is that it's unable to "deduce" that it should first try to generate the :map , and starts by generating a completely random map which of course never qualifies the [:map [:foo string?]] schema?

ikitommi 2020-11-03T13:17:08.414Z

yes, the order matters

2020-11-03T13:31:15.415Z

still seems like there's something off with :and :thinking_face:

(def base [:map
           [:foo [:enum :a :b]]])
(def x [:and base
        [:multi {:dispatch :foo}
         [:a [:map [:i string?]]]
         [:b [:map [:j pos-int?]]]]])
seems like mg/generate is unable to generate values for this schema as well

ikitommi 2020-11-03T14:25:34.415700Z

@lmergen

(defmethod -schema-generator :and [schema options] (gen/such-that (m/validator schema options) (-&gt; schema (m/children options) first (generator options)) 100))

ikitommi 2020-11-03T14:26:03.416300Z

^:-- the first one is used for the generator.

2020-11-03T14:26:10.416600Z

aha!

2020-11-03T14:26:54.417200Z

but that means there isn't really a way for me to work around it except writing a custom generator, right ?

2020-11-03T14:28:58.418100Z

(which is what i just did)

ikitommi 2020-11-03T14:29:20.418600Z

you could merge the base schemas, but merge might not work with :multi. but, there are options, like:

(mg/generate
  [:and
   [:multi {:dispatch :foo}
    [:a [:merge ::base [:map [:i string?]]]]
    [:b [:merge ::base [:map [:j pos-int?]]]]]]
  {:registry (merge
               (m/default-schemas)
               (mu/schemas)
               {::base [:map [:foo [:enum :a :b]]]})})

2020-11-03T14:30:01.419400Z

i see

ikitommi 2020-11-03T14:31:36.420700Z

merge doesn’t work with :multi currently, this would be best way to do it (if it worked):

(mu/merge
  [:map [:foo [:enum :a :b]]]
  [:multi {:dispatch :foo}
   [:a [:map [:i string?]]]
   [:b [:map [:j pos-int?]]]])
;[:multi {:dispatch :foo}
; [:a [:map [:i string?]]]
; [:b [:map [:j pos-int?]]]]

2020-11-03T14:32:06.421600Z

i just wrote a helper function which just does this:

(defn merged
  "Given a list of generators, returns a generator that applies `merge`
  to all the generator results."
  [&amp; gens]
  (gen/fmap (fn [args]
              (reduce merge {} args))
            (apply gen/tuple gens)))

(def schema [:and {:gen/gen (merged (mg/geneator a) (mg/generator b)} a b])

đź‘Ť 1
ikitommi 2020-11-03T14:32:11.421800Z

:multi should do type-inferring, so that all map-like :multis act like a map.

2020-11-03T14:32:18.422Z

yes

2020-11-03T14:32:23.422200Z

that's what i was going to say

2020-11-03T14:32:31.422500Z

:multi is pretty much always a map anyway

ikitommi 2020-11-03T14:34:11.423100Z

unless it’s a tuple / sequence:

(m/validate
  [:multi {:dispatch 'first}
   [:sized [:tuple keyword? [:map [:size int?]]]]
   [:human [:tuple keyword? [:map [:name string?] [:address [:map [:country keyword?]]]]]]]
  [:human {:name "seppo", :address {:country :sweden}}])
; true

2020-11-03T14:34:30.423400Z

right

ikitommi 2020-11-03T14:35:04.424Z

but, it’s easy to figure out if it looks like a map and could be merged.

ikitommi 2020-11-03T16:21:03.424200Z

could add an option to allow that shortcut (as there is regexs), but in short: it’s too easy to write schemas which do not serialize.

ikitommi 2020-11-03T16:22:24.424400Z

but if one is not looking for serialization, it would be easier for sure.

borkdude 2020-11-03T16:22:30.424600Z

good point. maybe for some people serializing a schema is not important?

borkdude 2020-11-03T16:22:36.424800Z

jinx

ikitommi 2020-11-03T16:23:27.425Z

need to revisit these for 1.0.0. but will add optional support for plain functions.

🙌 1
ikitommi 2020-11-03T16:24:39.425200Z

also, the default registry being immutable. it’s currently easy to use immutable registries, but hard to get a global mutable registry. both could be easy.

borkdude 2020-11-03T16:26:48.425400Z

Do you think malli will be able to do something like grasp?

borkdude 2020-11-03T16:27:03.425600Z

as in, support the things that spec does to match s-expressions

ikitommi 2020-11-03T16:27:41.425800Z

I believe so, as soon as there are the sequence schemas.

Maciej Falski 2020-11-03T19:02:10.432300Z

Hi, I’ve just started with malli, and I’m spiking converting our spec definitions to malli-based. Among others, we’ve defined specs with generators and json decoding for java-time (aka java8) types , like instant, duration , and interval. It’s easy with spec-tools. Now I’m trying to do the same with malli and this is what I came up with:

(def Instant
  (m/-simple-schema
    {:pred            t/instant?
     :type-properties {:error/message "should be an instant"
                       :decode/json   t/instant
                       :encode/json   str
                       :gen/gen       (gen/fmap (fn [[d h m s]] (t/plus (t/instant) (t/days d) (t/hours h) (t/minutes m) (t/seconds s)))
                                                (gen/tuple gen/int gen/int gen/int gen/int))}}))
Does it makes sense? Would there be a better way? I’m aware of this https://github.com/metosin/malli/issues/49, but it’s still open.