malli

https://github.com/metosin/malli :malli:
ikitommi 2021-07-03T13:15:46.275100Z

too hot, quick poke on instrumenting functions. moving functioin wrapping code from malli.generate into malli.core (just one fn) ->`m/-instrument`, can be used to enforce function schmas. First user will be malli.instrument utilities, but could be used directly in client code too:

(def pow2
  (m/-instrument
    {:schema [:=> [:cat :int] [:int {:max 6}]]}
    (fn [x] (* x x))))

(pow2 2)
; => 4

(pow2 "2")
; =throws=> :malli.core/invalid-input {:input [:cat :int], :args ["2"], :schema [:=> [:cat :int] [:int {:max 6}]]}

(pow2 4)
; =throws=> :malli.core/invalid-output {:output [:int {:max 6}], :value 16, :args [4], :schema [:=> [:cat :int] [:int {:max 6}]]}

(pow2 4 2)
; =throws=> :malli.core/invalid-arity {:arity 2, :arities #{{:min 1, :max 1}}, :args [4 2], :input [:cat :int], :schema [:=> [:cat :int] [:int {:max 6}]]}

ikitommi 2021-07-03T13:17:03.276Z

like everything else, can be configured to behave differently. e.g. just printing out the errors (or to tap ’em):

(def multi-arity-pow
  (m/-instrument
    {:schema [:function
              [:=> [:cat :int] [:int {:max 6}]]
              [:=> [:cat :int :int] [:int {:max 6}]]]
     :wrap #{:input, :output}
     :report (fn [error props]
               (println "\n" error "\n")
               (clojure.pprint/pprint props))}
    (fn
      ([x] (* x x))
      ([x y] (* x y)))))

(multi-arity-pow 4)
;:malli.core/invalid-output
;
;{:output [:int {:max 6}],
; :value 16,
; :args [4],
; :schema [:=> [:cat :int] [:int {:max 6}]]}
; => 16

(multi-arity-pow 5 0.1)
;:malli.core/invalid-input
;
;{:input [:cat :int :int],
; :args [5 0.1],
; :schema [:=> [:cat :int :int] [:int {:max 6}]]}
;
;:malli.core/invalid-output
;
;{:output [:int {:max 6}],
; :value 0.5,
; :args [5 0.1],
; :schema [:=> [:cat :int :int] [:int {:max 6}]]}
;=> 0.5

ikitommi 2021-07-03T13:25:47.277400Z

oh, and the generators use this too, generator for :=> looks dead simple now:

(defn -=>-gen [schema options]
  (let [output-generator (generator (:output (m/-function-info schema)) options)]
    (gen/return (m/-instrument {:schema schema} (fn [& _] (generate output-generator options))))))

ikitommi 2021-07-03T14:04:04.278700Z

dunno if there is a way to emit clj-kondo annotations from schematized fns, which are not vars :thinking_face:

respatialized 2021-07-03T17:08:16.282700Z

Say I've got a :orn schema that I'm matching multiple values against:

[:orn [:map1 [:map {:closed true
                    :description "map1"}
              [:a :string]]]
     [:map2  [:map {:description "map2"}
              [:a :string]
              [:b {:optional true} :string]]]]
is there a way to see which of these subschemas were responsible for a given value matching the top-level form - something like m/explain but for values that do conform to the schema?

ikitommi 2021-07-03T17:11:32.283100Z

@afoltzm try m/parse

1
respatialized 2021-07-03T17:16:31.286800Z

@ikitommi thanks, I knew there was likely something I was missing. my earlier examples didn't use :orn - it seems like with an unnamed :or schema only the conforming value is returned by m/parse. is there a way to return the matching schema for unnamed :or subschemas? the use case I'm thinking of here is matching values against some collection of schemas that may not necessarily be known in advance, and so can't easily be named with :orn.

respatialized 2021-07-03T17:17:17.287300Z

it seems like I may be able to implement something in terms of m/walk if it's not built in?

ikitommi 2021-07-03T17:21:13.289300Z

currrently, no. Could add an property to :or to hint that parse should return the index of the branch or an option to parse like :malli.core/parse-indexes true. Or, you can just create :or with number-indexed branches:

(m/parse
  [:orn
   [1 :int]
   [2 :boolean]] 
  true)
; => [2 true]

1✔️
respatialized 2021-07-03T17:21:50.289900Z

ah, gotcha. I think numbered branches is likely the easiest workaround for the time being.

1✅
ikitommi 2021-07-03T17:23:14.290300Z

(defn or->orn [s]
  (m/into-schema :orn (m/properties s) (map-indexed vector (m/children s))))

(or->orn [:or :int :boolean])
; => [:orn [0 :int] [1 :boolean]]

1