Fork me on GitHub
#malli
<
2024-01-20
>
Joel03:01:28

(def registry
  {:my/group [:vector :my/item]
   :my/item  [:int {:min 0 :max 480}]})
For :my/group I’d like to know that it’s a collection of :my/item’s. I tried the following
(let [k   :my/group
      s   (some->> (m/schema [:schema {:registry registry} k])
                   (m/deref-all))
      t   (m/type s)
      sub (first (mu/subschemas s))]
  (if (contains? #{:vector :set} t)
    {:type t :item-type (m/children (:schema sub))}))

; => {:type :vector, :item-type [:my/item]}
The :item-type is almost what I need but even though the returned [:my/item] looks like a list of one keyword, it’s actually of type :malli.core/schema I can’t figure out, one, if I’m going about this reasonably, and two, how to get that keyword looking schema to be an actual keyword as it appears in the original schema.

ikitommi10:01:40

you can call m/form to get the data representation out:

(let [schema (m/deref-all
              [:schema {:registry {:my/group [:vector :my/item]
                                   :my/item [:int {:min 0 :max 480}]}}
               :my/group])
      type (m/type schema)]
  (if (#{:vector :set} type)
    {:type type
     :item-type (-> schema (m/children) (first) (m/form))}))
; => {:type :vector, :item-type :my/item}

thanks3 1
ikitommi10:01:58

if you need to do this recursive, you can use m/walk:

(m/walk
 [:schema {:registry {:my/group [:vector :my/item]
                      :my/item [:int {:min 0 :max 480}]}}
  :my/group]
 (fn [schema _ children _]
   (cond-> {:type (m/type schema)}
     children (assoc :children children)))
 {::m/walk-schema-refs true})
;{:type :schema,
; :children [{:type :malli.core/schema,
;             :children [{:type :vector
;                         :children [{:type :malli.core/schema
;                                     :children [{:type :int}]}]}]}]}

👀 1
ikitommi09:01:35

kinda nice?

🎉 4
ikitommi09:01:35

would like to: • rename m/-type -> m/-namem/kind -> m/type (reading :type property) … but this would be a massive breaking change.

ikitommi09:01:20

anyway, this gets rid of a lot of extra code in generators, tranformers, error messages and applications like json-schema

ikitommi09:01:32

e.g. no need to map separately all of int?, pos-int?, neg-int?, nat-int?, :int … just :int and map everything to it via the new :kind

Ben Sless09:01:59

Letting users set it via property sounds like a fun source of bugs

ikitommi10:01:10

Do you see some new category of errors here? Users are anyway in control of mostly everything already…

Ben Sless11:01:51

Yes. Adding the kind property can contradict what the code actually does. Same problem with doc strings accumulating skew but with actual behavioral effect

Ben Sless11:01:27

It's good for an internal property but letting users set it gives me nightmares

Noah Bogart13:01:46

Would it be possible to limit it to only :fn schemas?

Ben Sless15:01:32

[:fn {:kind :string} pos?] Enjoy finding the source of this bug in a large code base

ikitommi19:01:58

how is that different to:

[:and :string [:fn pos?]]
also, many other ways to create invalid logic:
[:int {:gen/schema :string}]
[:string {:decode/string parse-long}]

ikitommi19:01:45

limiting only to :fn - anything is possible, just would need reasoning for limiting just to that, e.g.

[:enum {:kind :uuid} 
 #uuid"f4ba0d9c-7b93-4030-8bf4-d731d5c3dc4d"
 #uuid"954cb679-a36e-4b85-aaae-0844ad79a219"]
is the simplest way to describe a enum of uuids that also contains explicit type information (for decoding, encoding, clj-kondo etc)

ikitommi19:01:53

as the :and works already, you could say:

[:and :uuid [:enum #uuid"f4ba0d9c-7b93-4030-8bf4-d731d5c3dc4d" #uuid"954cb679-a36e-4b85-aaae-0844ad79a219"]]
… and get almost the same benefits.

ikitommi19:01:23

for :type-properties, something like this is anyway good to get rid of those extras:

'double? {:error/message {:en "should be a double"}}
   'boolean? {:error/message {:en "should be a boolean"}}
   'string? {:error/message {:en "should be a string"}}

Ben Sless19:01:59

It isn't, right, everything is a compromise. It's also one of the reasons I'm generally averse to fn schemas. This is just what I'm paranoid about. The more subtle the detail the more interesting the bug will end up being. Maybe in the grand scheme of things it's an acceptable tradeoff

Karol Wójcik14:01:09

How one can create a relation between the generated values? For instance I want to make sure that when the :type property in map is number the generator generate only numbers in :content property of the map.

Ben Sless16:01:16

Does a multi schema not cover it?

❤️ 1
Karol Wójcik20:01:23

Cool. Was not aware of multischema existence. Thanks!

Nikolas Pafitis23:01:16

I just opened this feature request to support ClojureDart on GH, but I'd like to see some discussion here https://github.com/metosin/malli/issues/996.

ikitommi09:01:53

so, multimethods is/was an issue.

Nikolas Pafitis14:01:11

I see. If ClojureDarts implements multimethods, and properly handle reader conditionals, it becomes relatively straight forward to enable malli in ClojureDart. As an example in namespace malli.impl.util the following can be changed to use the :default reader conditional instead of :cljs as that functionality would be expected to be provided by any clojure implementation.

(defn -vmap
  ([os] (-vmap identity os))
  ([f os] #?(:clj  (let [c (count os)]
                     (if-not (zero? c)
                       (let [oa (object-array c), iter (.iterator ^Iterable os)]
                         (loop [n 0] (when (.hasNext iter) (aset oa n (f (.next iter))) (recur (unchecked-inc n))))
                         #?(:bb (vec oa)
                            :clj (LazilyPersistentVector/createOwning oa))) []))
             :cljs (into [] (map f) os))))

👍 1
ikitommi16:01:50

yes, happy to see a PR of those changes to reader conditionals if everything else works.

1