This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2024-03-26
Channels
- # announcements (9)
- # babashka (36)
- # beginners (13)
- # biff (24)
- # calva (12)
- # clj-kondo (18)
- # clojure (65)
- # clojure-brasil (1)
- # clojure-europe (11)
- # clojure-nl (1)
- # clojure-norway (87)
- # clojure-uk (4)
- # clojurescript (28)
- # datahike (25)
- # fulcro (12)
- # hyperfiddle (16)
- # malli (74)
- # missionary (1)
- # music (2)
- # off-topic (24)
- # polylith (4)
- # releases (3)
- # tools-deps (23)
I found a closed issue https://github.com/metosin/malli/issues/872 that says that Malli does not report issues about invalid schemas. It seems like this problem belongs to https://github.com/metosin/malli/issues/18 . If this is not being fixed right now, I could try to fix the issue myself by making a schema that validates schemas. If we don't count malli's readme, is there any good resource where I can get a precise description about all schemas and what they expect?
you don’t necessarily need to do that- if you call malli.core/schema
on an invalid schema it will throw an exception that has some context
well, I have my schemas defined in a registry map and when I check out with some simple examples, everything seems to be working but once I try to instrument all the functions (that I typed via metadata), I get malli's child errors that don't tell much. It just says something about :enum
but I only use it once and it has options, so I am not sure why it would complain.
:data {:type :malli.core/child-error, :message :malli.core/child-error, :data {:type :enum, :properties nil, :children nil, :min 1, :max nil}}
after some painful debugging I realized what went wrong. I put :enum
in registry because I thought I will need a bunch of types and I should probably put them in a registry. It seems that anything that is redefined in registry never get any parameters, so schemas like :enum
, :or
, :not
that expect at least one argument will fail. I am not sure that's the expected behavior
(I defined that in a local registry if that changes things)
Here's a minimal example
(m/schema [:enum "foo" "bar"]) ;; ok
(m/schema [:enum "foo"] {:registry {:enum (:enum (m/default-schemas))}}) ;; ok
(m/schema [:enum {:registry {:enum (:enum (m/default-schemas))}} "foo" "bar"]) ;; not ok
{:type :malli.core/child-error,
:message :malli.core/child-error,
:data {:type :enum, :properties nil, :children nil, :min 1, :max nil}}
Is there a way to express at-least-on-of these keys for a map? I have a padding:
[:map
[:top {:optional true} number?]
[:bottom {:optional true} number?]
[:left {:optional true} number?]
[:right {:optional true} number?]]
Where it’s fine if either of the sides is mentioned, but an empty map is probably a mistake.With a bit of duplication:
[:or
[:map
[:top number?]
[:bottom {:optional true} number?]
[:left {:optional true} number?]
[:right {:optional true} number?]]
[:map
[:top {:optional true} number?]
[:bottom number?]
[:left {:optional true} number?]
[:right {:optional true} number?]]
[:map
[:top {:optional true} number?]
[:bottom {:optional true} number?]
[:left number?]
[:right {:optional true} number?]]
[:map
[:top {:optional true} number?]
[:bottom {:optional true} number?]
[:left {:optional true} number?]
[:right number?]]]
see the Malli http://playground.ioI can't think of another way without using :fn
schemas.
Thanks, @U03N9E40Q2F 🙏
How about [:and :YOUR_SCHEMA [:not [:map {:closed true}]]
?
(I can see that it appears to, but I don't understand how.)
Map has an attribute :closed
and when it's set to true
it rejects all maps that have extra keys specified. Since we didn't specify any key, it means that it will reject everything except an empty map and that's why we add :not
You could also reject empty maps with [:not [:map-of [:not :any] :any]]
because map-of
asks all keys to satisfy something impossible and that will happen on empty maps because there is nothing to check, so that means that all keys satisfy the impossible. Anyhow this is a really confusing example but it is kind of fun to look at.
Is your http://malli.io link the link you tried to give me first, @U03N9E40Q2F? That one didn’t work, but the http://malli.io one sure looks like a playground.
@U0ETXRFEW ah, I seem to have messed up the paste somehow. The first link I gave was to http://malli.io with the example I gave. Which works, but I think @U02S4QZAH61’s example is much neater.
I took a stab at adding keyset constraints to malli https://github.com/metosin/malli/pull/1025 Here's how you'd write padding with that:
(def Padding
[:map
{:keys [[:or :top :bottom :left :right]]}
[:top {:optional true} number?]
[:bottom {:optional true} number?]
[:left {:optional true} number?]
[:right {:optional true} number?]])
(m/validate Padding {:left 1 :right 10 :up 25 :down 50})
;=> true
(me/humanize
(m/explain Padding {}))
; => ["should provide at least one key: :top :bottom :left :right"]
(mg/sample Padding {:size 5})
; => ({:top -0.5} {:top -1} {:left 0} {:left -1} {:left 1.0})
Very cool @U055XFK8V! As a malli noob I wonder if this points at a way to swap the semantics to mean that entries are optional by default. So something like
[:map
{:req [[:or :id :name]]}
[:id :int]
[:name :string]
[:speed number?]
[:color :style/color]]
Would mean:
> If you give a map of valid either :id
or :name
, I’m good, as long as any :speed
, or :color
also provided checks out.
Obviously, I don’t know what I’m doing, just a thought that popped to mind.@U055XFK8V Isn't this a confusing behavior since I am used to fact that normally :or
's children are schemas. If we only want to constrain the keys maybe it would make more sense to conform to existing behavior. Perhaps something like [:cat [:* :any] [:enum :top :left :bottom :right] [:* :any]]
. I have heard that seqexps are relatively expensive, so perhaps it would be a better idea to include something like [:exists SCHEMA]
which is true if the collection has an element that conforms SCHEMA
.
@U0ETXRFEW oh yeah, my solution might not be exactly what you need because it can accept {:botom 3}
(with one t) or any other map that has something in it. My solution would work fine if your entire schema would be closed.
For the record, upon closer inspection my particular use case doesn’t need this. It turns out it’s fine with all dimensions lacking. Still needed to learn about this, so am very happy for all the info in this thread!
@UK0810AQ2 is there somewhere I can read about symbolic schema manipulation / simplification?
Doesn't malli do some optimizations under the hood?
With such manipulation in place, how would the OP use case be solved? And if it also solves my wish for swapping the required semantics, that would also be nice to see an example of. 😃
@U055XFK8V that looks very nice, we'd use that at my job. the nested vectors on :keys
is a little strange, what's the reason for it?
@UEENNMX0T thanks! it's the syntax of s/keys
but datafied:
(s/keys :req [::x ::y (or ::secret (and ::user ::pwd))] :opt [::z])
=>
[:map {:keys [[:or ::secret [:and ::user ::pwd]]]}
::x ::y [::pwd {:optional true}]
[::secret {:optional true}]
[::user {:optional true}]
[::pwd {:optional true}]
oh that makes sense
@U02S4QZAH61 it's a good point that :or
etc is overloaded. spec is the inspiration ^. Sequence expressions are for sequential things, and keysets are not sequential, so I'm not sure there's existing work for this.
@U0ETXRFEW yeah optional by default if you give keyset constraints would help with succinctness. I'm not sure it can be bolted on entirely cleanly, maybe we need a new schema.
@U055XFK8V oh yeah, I am just so used to the fact that maps (and its keys or values) can be converted to sequences and can be fed to various Clojure functions.
@U02S4QZAH61 I recently made a schema that checks a map based on a sequence expression. It's very odd, it doesn't quite make sense, but could almost be the seeds of something useful. Here's the equivalent to [:map-of :int :int]
.
[:into-map
[:* [:tuple :int :int]]]
i wonder if borrowing the :keys
syntax from spec duplicates too much from existing malli behavior, instead of adding a new key to the properties map on :map
: [:map {:or [:top :bottom :left :right]} [:top ...] [:bottom ...] ...]
@UEENNMX0T that's cool! :keys
would become :and
, and everything is more succinct.
@U055XFK8V if we had some set predicates built-in, we could treat keys as a set and use those set predicates on them. Something like includes?
(that could also benefit sequences).
I guess all set predicates could be used on sequences, so maybe there is not a big point differentiating between them.
oh yeah, it's called contains?
, pardon my Clojure
@U02S4QZAH61 I think there might be something to the DSL I created for keysets that could be generalized. It's a constraint language about set inclusion, the atomic constraint is #(contains? % k)
.
contains?
is for literals. I don't know what would be the Clojure-y way but there should be some collection predicate that says "if there is a member of x that satisfies f, return true" normally I would call that any?
but it means a different thing in Clojure.
Isn't it the same thing as [:and [:map ...] [:map-of X Y]
?
Yeah. I'm sort of seeing a correspondence between :contains <=> :map
and :set <=> :map-of
. That's why I thought of whether a ::m/default
could be relevant.
the one problem with :map-of
is that it does not have a context about the entire key set or value set (values are not guaranteed to be a set but yeah)
If you haven't read it yet, you might want to check out my idea about schema-validation. It could also open doors to schemas that accept arguments (that could mean generics)
(I recently started learning Haskell, types have bad influence on me)
figuring out the locally nameless representation right now so I can implement capture-avoiding substitution. but here's what it might look like:
;; spec for clojure.core/map transducer
[:schema {:registry {"Reducer" (m/tfn [a b] [:=> [:cat b a] b])
"Transducer" (m/tfn [in out] (m/all [r] [:=> [:cat [:ref "Reducer" out r]] [:ref "Reducer" in r]]))}
(m/all [a c] [:=> [:cat [:=> a c]] [:ref "Transducer" a c]])]
:ref
can take multiple arguments?
oh, I think :ref
always takes just 1 one argument, at least that's what I remember and what I see in the readme. What are those symbols after the first child? Are they typenames?
@U02S4QZAH61 what I just wrote is part of my WIP branch. Sorry I have a habit of dropping wishful thinking and causing confusion.
oh yeah, I figured out, it's a WIP schema, I am just not sure what these would represent
I have a simple macro (m/tfn [x y]...)
that just does (let [x 'x y 'x] ...)
. Then you get a schema like '[:tfn [x y] [:=> ... x ... y]]
.
you can then instantiate them with :ref
(or maybe another operation like :inst
that I haven't decided on).
ah okay, understood
I did an introductory talk on the basic idea here FWIW https://www.youtube.com/watch?v=QE3PZLlefUE
i do this type of thing with a fn
(defn at-least-one-key?
[keys]
[:fn {:error/message (str "At least one of " keys " should be supplied")}
(fn [data]
(seq (select-keys data keys)))])
then i have my schema defined like
(def fcm-notification-body
[:and
[:map
[:title {:optional true} non-empty-string]
[:body {:optional true} non-empty-string]
[:image {:optional true} url?]
[:data {:optional true} [:map-of :keyword :string]]]
(at-least-one-key? [:title :body :image :data])])
Apologies if this was solved already but felt it was a nice exampleThanks, @U069RSXM1CP! I’ll remember this recipe. Though, I do like data better than functions, so would be nice with a declarative solution.
👍 yeah i was trying to do it declaritively at first and had things like chunky :or with repeated stuff. might be able to be expressed by saying its a closed map and not empty but havnt fiddled much, once i got used to using the :fn stuff ive done it alot. You can describe custom generation logic if that is the concern 😄
i thought i was really clever for a second with this type of idea
[:and
[:map {:closed true}
[:keyone {:optional true} :int]
[:keytwo {:optional true} :int]]
[:not [:map {:closed true}]]]
but was mentions above already, works tho (ie, 1st schema is the real one, 2nd is an empty map, basically saying, To be valid, you must NOT be empty, and you MUST provide a key that is valid
it doesnt really make it so you can have various conditionals tho, like IF keyone is over 100, then keytwo must be below 50.How could I find the offending function name when instrumenting defns?
For example I got a :malli.core/invalid-output
exception with the offending value and function signature but I do not get informed about the function itself. I don't have many functions, so I can deduce the offender by its signature but it seems like a clumsy way of handling defn signature violations.
how do I attach custom transformers to malli schema for coercion? My end goal is to define transformers for coercion at the schema level, then have the reitit.malli.coercion take care of the coercing whenever requests are received.
(m/coerce
[:map
[:x [:bigdec? {:decode/string->bigdec {:enter #(bigdec %)}}]]
[:y [:time/instant {:decode/string->instant {:enter str->inst}}]]]
{:x "123"
:y "2023-01-01T00:00:00Z"}
(mt/transformer
{:name :math}
{:name :string->instant}))
this works (with str->inst being a function defined in ns), but I need to know how this can be applied when combined with reitit's malli coercion.I took a stab at adding keyset constraints to malli https://github.com/metosin/malli/pull/1025 Here's how you'd write padding with that:
(def Padding
[:map
{:keys [[:or :top :bottom :left :right]]}
[:top {:optional true} number?]
[:bottom {:optional true} number?]
[:left {:optional true} number?]
[:right {:optional true} number?]])
(m/validate Padding {:left 1 :right 10 :up 25 :down 50})
;=> true
(me/humanize
(m/explain Padding {}))
; => ["should provide at least one key: :top :bottom :left :right"]
(mg/sample Padding {:size 5})
; => ({:top -0.5} {:top -1} {:left 0} {:left -1} {:left 1.0})