malli

https://github.com/metosin/malli :malli:
ikitommi 2021-04-13T04:15:18.390Z

that's the way to do it now.

Ben Sless 2021-04-13T08:17:33.391300Z

Is it by design that schema transformations like mu/assoc don't play well with an unregistered schema?

ikitommi 2021-04-13T09:06:56.391400Z

not by design, could you repro if there is something that doesn’t work?

Ben Sless 2021-04-13T09:28:27.391600Z

(def Foo
  [:map
   [:a int?]])

(mu/assoc Foo :b ::bar)
Minimal example

Ben Sless 2021-04-13T09:30:09.392900Z

Another issue I managed to stumble on, I defined a dependent schema like https://github.com/metosin/malli#content-dependent-simple-schema It works well but throws when I pass it to reitit routes when the coercion is compiled

Ben Sless 2021-04-13T09:34:20.393100Z

Great, now I'm unable to reproduce it 😞

Ben Sless 2021-04-13T09:54:57.393600Z

I managed to get myself into this corner like so: • wanted content dependent schema • wanted to parametrize the schema (makes it extensible) • figured out I'd do it by delaying registry building and schema compilation to run-time. • With registry I need ::my-schema • Can't transform anything with ::my-schema at compile time I can create a placeholder registry for it but it seems like it would lead to errors down the line

Ben Sless 2021-04-13T09:57:40.393800Z

I'd be happy to adopt a better idea

ikitommi 2021-04-13T10:00:34.394Z

me too 🙂 spec partially checks the references eagerly, partially lazily (e.g. s/keys), malli is currently eager.

ikitommi 2021-04-13T10:01:34.394200Z

there is an internal escape hatch: :ref doesn’t check the reference if :malli.core/allow-invalid-refs option is truthy.

ikitommi 2021-04-13T10:01:44.394400Z

it is used with local registries, which can have… holes.

ikitommi 2021-04-13T10:03:39.394600Z

(m/validate
  [:schema {:registry {::foo [:ref ::bar]}} ;; incomplete registry
   [:tuple {:registry {::bar int?}}
    ::bar ::foo]]
  [1 2])
; => true

ikitommi 2021-04-13T10:06:33.394800Z

ideas welcome how to make this good.

Ben Sless 2021-04-13T10:12:25.395800Z

I don't know if it's good, but perhaps a :delay or :defer schema, which delays registry lookup to validation, with ample warning, care, etc.

yuhan 2021-04-13T10:14:07.397400Z

Are there built-in functions to throw errors on invalid input? The plans for instrumentation in the above issue are nice, but I'm looking for something simple like spec/assert

Ben Sless 2021-04-13T10:18:24.397500Z

Perhaps even wrap it in a function which will always emit warnings when it's called, i.e.

(mu/assoc Foo :b (m/schema (m/defer ::bar)))
STDERR: Deferred Warning *at* - instances of deferred schema must be provided with a registry at run time!
You can also throw when instantiating an explainer, transformer, or validator from it, which is when you actually need the registry

ikitommi 2021-04-13T10:21:14.397700Z

could it be just [:ref {:lazy true} ::bar]?

Ben Sless 2021-04-13T10:21:40.397900Z

Ah, laziness has to be explicit

ikitommi 2021-04-13T10:21:49.398100Z

oh, ref’s are lazy already :thinking_face:

Ben Sless 2021-04-13T10:22:15.398300Z

yeah, this didn't work 🙂

Ben Sless 2021-04-13T10:22:20.398500Z

we need lazier laziness

ikitommi 2021-04-13T10:23:42.399100Z

try (m/-lazy ::bar options)

ikitommi 2021-04-13T10:24:05.399700Z

refs resolve eager by default, but one can create lazy refs with that.

ikitommi 2021-04-13T10:24:36.400200Z

(let [-ref (or (and lazy (-memoize (fn [] (schema (mr/-schema (-registry options) ref) options))))
                      (if-let [s (mr/-schema (-registry options) ref)] (-memoize (fn [] (schema s options))))
                      (when-not allow-invalid-refs
                        (miu/-fail! ::invalid-ref {:type :ref, :ref ref})))

Ben Sless 2021-04-13T10:24:40.400400Z

Cool, it worked 🙂

Ben Sless 2021-04-13T10:24:49.400800Z

Always good to know some black magic

ikitommi 2021-04-13T10:25:06.401300Z

could make a version of that which doesn’t require the options.

Ben Sless 2021-04-13T10:25:35.402Z

It makes me wonder why [:ref {:lazy true} ::bar] didn't work

ikitommi 2021-04-13T10:26:00.402600Z

it’s a property of the IntoSchema, not Schema instance.

ikitommi 2021-04-13T10:27:05.403900Z

by design, all the IntoSchemas are crated using a function, which can take properties how the IntoSchema works. Easy to extend the system that way and DCE drops all the unneeded schemas.

ikitommi 2021-04-13T10:27:53.404700Z

for example, it’s reletively easy to create custom collection schema types:

(defn -collection-schema [{type :type fpred :pred, fempty :empty, fin :in :or {fin (fn [i _] i)} :as opts}] ...)

ikitommi 2021-04-13T10:28:18.405400Z

:ref has:

(defn -ref-schema
  ([]
   (-ref-schema nil))
  ([{:keys [lazy type-properties] :as opts}] ...))

jcf 2021-04-13T10:28:42.405800Z

I have a follow up question from https://clojurians.slack.com/archives/CLDK6MFMK/p1618257034389400 regarding emitting configuration for clj-kondo to pick up (which is an awesome feature by the way!). I have schematised the following code:

(m/=> hash-map-by
  [:=> [:catn [:f [:fn ifn?]] [:coll coll?]] map?])

(defn hash-map-by
  "Returns a map of the items in `coll` keyed by `f`."
  [f coll]
  (into {} (map (juxt f identity)) coll))
The function takes an arbitrary function, f, and a collection that will be converted into a map by applying f and identity to each item in the collection. Pretty standard stuff. 🙂 When I emit clj-kondo config with (mc/emit!), I get the following EDN:
{:lint-as #:malli.schema{defn schema.core/defn},
 :linters {:type-mismatch {:namespaces {example.hash-map {hash-map-by {:arities {2 {:args [:fn :coll], :ret :map}}}}}}}}
Please note, the 2-arity args say :fn and :coll returning a :map which means I get linting issues with something like (hash-map-by :user/id [{:user/id 1} {:user/id 2}]). Is this a bug worthy of a pull request or am I once again demonstrating my naivety? 🙈

ikitommi 2021-04-13T10:28:56.406Z

but, could lift the lazy into a :ref schema property too. so one can say [:ref {:lazy true} ::bar] as data.

ikitommi 2021-04-13T10:29:09.406200Z

if you need that, please write an issue.

ikitommi 2021-04-13T10:32:17.406400Z

currently there is no way to override per schema instance how the clj-kondo works, but would be easy to add. also, having an ifn? schema built-in, it could have the correct clj-kondo type. interested in a PR?

ikitommi 2021-04-13T10:32:27.406600Z

for the latter that is.

ikitommi 2021-04-13T10:32:38.406800Z

for the first, for the second, something like:

Ben Sless 2021-04-13T10:32:41.407Z

Thank you!

Ben Sless 2021-04-13T10:33:10.407200Z

I wonder if I should settle for m/-lazy

ikitommi 2021-04-13T10:33:12.407400Z

[:fn {:clj-kondo/type :ifn} ifn?]

Ben Sless 2021-04-13T10:33:36.407700Z

If I should consider functions prefixed with - as implementation detail, then I'd say that I shouldn't and open that issue

jcf 2021-04-13T10:34:04.407900Z

I'm very interested in implementing this as we'd need it to complete the replacement of clojure.spec with Malli in our codebase, I think.

ikitommi 2021-04-13T10:34:33.408100Z

things starging with - are ok to use: https://github.com/metosin/malli#alpha

jcf 2021-04-13T10:34:51.408400Z

I can create a PR for sure. 💯

Ben Sless 2021-04-13T10:38:03.408600Z

> might evolve during the alpha That's a risk I'm willing to take. I think if m/-lazy develops in any direction it won't be one which will have friction with what I'm trying to do, on the contrary. Thanks again for the help and guidance, you rock

nilern 2021-04-13T10:40:23.409200Z

You can always (assert (thingy-validator dada)) but the error is not so useful

yuhan 2021-04-13T10:43:26.409400Z

Yeah, I wrote my own for now:

(defn malli-assert
  ([schema value]
   (malli-assert schema value ""))
  ([schema value msg]
   (when-not (malli/validate schema value)
     (throw (ex-info (clojure.string/join "/n"
                       (cons msg
                         (flatten
                           (malli.error/humanize
                             (malli/explain schema value)))))
              {:value value})))))

nilern 2021-04-13T10:44:55.409600Z

Make a PR?

yuhan 2021-04-13T10:44:57.409800Z

It's not ideal because humanize returns nested messages according to the path of the error, which I just flatten into a single string

yuhan 2021-04-13T10:45:54.410Z

Ok I'll submit an issue, just wanted to check if it was a design decision not to have an assert

nilern 2021-04-13T10:47:31.410200Z

Maybe AssertionError would be more appropriate :thinking_face:

nilern 2021-04-13T10:50:23.410400Z

And maybe use *assert* and make it a macro

nilern 2021-04-13T10:50:59.410600Z

Spec assert seems to use ex-info and a separate *assert* equivalent var

jcf 2021-04-13T11:00:00.410800Z

@ikitommi can I just clarify what you're thinking in terms of a PR, please? I can add #'ifn? to the predicate-schemas and then these tests pass:

(testing "ifn schemas"
    (let [schema (m/schema ifn?)]
      (is (true? (m/validate schema (fn []))))
      (is (true? (m/validate schema (constantly 1))))
      (is (true? (m/validate schema :keyword)))
      (is (true? (m/validate schema (reify clojure.lang.IFn
                                      (invoke [_] "Invoked!")))))))
Is that what you had in mind when you mentioned having an ifn? schema built in?

ikitommi 2021-04-13T11:09:54.411Z

yes, but also mappings for transformers, generators, json-schema, humanized errors and clj-kondo.

jcf 2021-04-13T11:11:44.411200Z

I'll take a look at implementing the full feature set for the ifn? domain. 👍

jcf 2021-04-13T11:12:26.411400Z

I'll not implement proper generation of interesting functions. Don't want to put us all out of a job. 😉

jcf 2021-04-13T11:21:48.411600Z

Oh, I think I see what you mean. You want ifn? to be parameterised so you can schematize the args and return values…?

jcf 2021-04-13T11:22:48.411800Z

So in the schema generator you can do something more than this:

(defmethod -schema-generator 'ifn? [_ _] (gen/return ::ifn))

jcf 2021-04-13T11:23:30.412Z

I'd need to generate a function that returns valid data given valid arguments.

ikitommi 2021-04-13T11:23:45.412200Z

really, why?

ikitommi 2021-04-13T11:24:04.412400Z

there is already :=> and :function which have proper input & output generators

jcf 2021-04-13T11:24:09.412600Z

Because I thought that was what you wanted. 🙂

jcf 2021-04-13T11:24:13.412800Z

I think I misunderstood.

ikitommi 2021-04-13T11:24:33.413Z

no, just the simple thing, lke fn? but bit different 🙂

jcf 2021-04-13T11:24:36.413200Z

If ifn? can remain a simple predicate, I should be able to have a first pass at a PR before the lunchtime walk. 🙂

ikitommi 2021-04-13T11:24:44.413400Z

👍

yuhan 2021-04-13T18:58:45.415600Z

Is it possible for schemas to self-reference? Naively trying to make a recursive schema causes a stack overflow:

(malli/schema
  ::tree
  {:registry (merge (malli/default-schemas)
               {::node :int
                ::tree [:or
                        ::node
                        [:tuple ::tree ::tree]]})})
 

ikitommi 2021-04-13T19:00:57.416200Z

@qythium use the :ref, luke:

(mg/generate
  (m/schema
    ::tree
    {:registry (merge (m/default-schemas)
                      {::node :int
                       ::tree [:or
                               ::node
                               [:tuple [:ref ::tree] [:ref ::tree]]]})})
  {:seed 3})
; => [[-26764 [[1 73136] [13307055 -1]]] [-381 [[-3 587742] -243724556]]]

yuhan 2021-04-13T19:01:38.416400Z

awesome, thanks!

yuhan 2021-04-13T19:28:24.418900Z

So schema references only in :ref and :map have to be qualified keywords? Strings appear to work too, but not plain keywords

yuhan 2021-04-13T19:31:10.420200Z

Also it seems that the ::foo shorthand syntax doesn't work on the http://malli.io playground

yuhan 2021-04-13T19:36:00.420300Z

Seems like it, I found malli.core/-reference? in the source code which checks for qualified-keyword? or string?, but this doesn't seem to be documented

ikitommi 2021-04-13T19:37:00.421500Z

yes, references should be qualified keywords or strings. http://malli.io … must be a sci-thing.

yuhan 2021-04-13T19:45:07.422100Z

Ok, filed an issue on the http://malli.io github

yuhan 2021-04-13T19:49:38.424400Z

Trying to use unqualified keys as references tripped me up quite a bit at the beginning, since this requirement isn't documented and the error message just says :malli.core/invalid-schema

ikitommi 2021-04-13T19:50:48.425100Z

doc enhancement PRs are most welcome.

ikitommi 2021-04-13T19:51:42.425800Z

(the error keyword could be better here)

yuhan 2021-04-13T19:57:44.427500Z

I'll just file issues for now if that's ok - still in the early stages of experimenting with the library and not confident of writing docs

yuhan 2021-04-13T19:59:58.428700Z

Another strange thing I encountered:

;; This works as a schema
[:map {:registry {::foo :int}}
 ::foo]

;; so does wrapping the keyword in a vector
[:map {:registry {::foo :int}}
 [::foo]]
;; to pass it options
[:map {:registry {::foo :int}}
 [::foo {:optional true}]]


;; These are ok too
[:map {:registry {::foo [:tuple :int :int]}}
 ::foo]
[:map {:registry {::foo [:tuple :int :int]}}
 [::foo {:optional true}]]


;; But not this??
(malli/schema
  [:map {:registry {::foo [:tuple :int :int]}}
   [::foo]])
;; => Execution error (ExceptionInfo) at malli.impl.util/-fail! (util.cljc:16).
;;    :malli.core/invalid-schema {:schema [:tuple :int :int]}