This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2024-03-15
Channels
- # announcements (1)
- # architecture (8)
- # babashka (7)
- # beginners (5)
- # biff (8)
- # calva (24)
- # cider (9)
- # clerk (14)
- # clj-kondo (7)
- # clojars (14)
- # clojure (49)
- # clojure-europe (11)
- # clojure-nl (1)
- # clojure-norway (90)
- # clojure-uk (3)
- # clojurescript (5)
- # core-typed (70)
- # cursive (35)
- # data-science (4)
- # datalevin (6)
- # datomic (2)
- # emacs (3)
- # fulcro (1)
- # holy-lambda (1)
- # hyperfiddle (5)
- # lsp (26)
- # malli (28)
- # off-topic (9)
- # re-frame (21)
- # releases (1)
- # shadow-cljs (7)
- # squint (6)
- # testing (14)
Hi, how do you serialize function schemas? Or in general, how to serialize the explain data? For example:
(m/explain [:fn pos?] 0)
;; => {:schema [:fn #function[clojure.core/pos?]],
;; :value 0,
;; :errors
;; ({:path [], :in [], :schema [:fn #function[clojure.core/pos?]], :value 0})}
Or with [:multi {:dispatch :type} [:a :int] [:b :string]]
the Cider REPL evaluates it correctly but :schema
at top level or within the errors is a function:
(mc/explain [:multi {:dispatch :type}
[:a :int]
[:b :string]]
"string")
I tried using m/ast
but stumbled upon :fn
and that didn't serialize.
Any alternatives? I could name all my schemas or function schemasSome alternatives I'm aware of:
user=> (m/explain [:fn #'even?] nil)
{:schema [:fn #'clojure.core/even?], :value nil, :errors ({:path [], :in [], :schema [:fn #'clojure.core/even?], :value nil, :type nil})}
user=> (m/explain [:fn '(fn [x] (even? x))] nil)
{:schema [:fn (fn [x] (even? x))], :value nil, :errors ({:path [], :in [], :schema [:fn (fn [x] (even? x))], :value nil, :type :sci/error})}
user=> (m/explain [:fn `(fn [x#] (even? x#))] nil)
{:schema [:fn (clojure.core/fn [x__11833__auto__] (clojure.core/even? x__11833__auto__))], :value nil, :errors ({:path [], :in [], :schema [:fn (clojure.core/fn [x__11833__auto__] (clojure.core/even? x__11833__auto__))], :value nil, :type :sci/error})}
user=> (m/explain [:fn `#(even? %)] nil)
{:schema [:fn (fn* [p1__11870__11871__auto__] (clojure.core/even? p1__11870__11871__auto__))], :value nil, :errors ({:path [], :in [], :schema [:fn (fn* [p1__11870__11871__auto__] (clojure.core/even? p1__11870__11871__auto__))], :value nil, :type :sci/error})}
Is there a way to force a particular selection inside an :or
using a property?
Alternatively, I could transform
[:map
[:value
[:or
[:map [:date :time/local-date]]
[:map [:text :string]]
[:map [:number :int]]]]]
into
[:map
[:value
[:map [:date :time/local-date]]]]
But it doesn't seem so straightforward if the only information I have during runtime is the keyword :date
I could obviously compose the schema programmatically during runtime but I want to be able to generate values at the REPL too, so I'd like to keep my schema defined as-is and just modify it at runtimeWhat are the limits and rules surrounding the explain functionality?
For example the following returns unknown error
(me/humanize
(m/explain [:and
[:not
[:or
[:map ["a" [:map ["b" [:= "1"]]]]]
[:map ["a" [:map ["b" [:= "2"]]]]]]]]
{"a" {"b" "1"}}))
The error is simple to understand by a human but somehow explain
do not understand it. My spec is generated dynamically so just saying I should change how my spec is written will not help. I need to understand why this fails so I know how I should rewrite the dynamic generators to actually support explain.This works
(me/humanize
(m/explain [:or
[:map ["a" [:map ["b" [:= "1"]]]]]
[:map ["a" [:map ["b" [:= "2"]]]]]]
{"a" {"b" "3"}}))
So I guess it is the negation that causes the unknown error
protip: :or schemas give worse error messages than :multi schemas, if you can transform your code to use :multi, they perform better too, since they don’t have to check every branch (they’d report an error for each :or branch).
(humanize
(m/explain [:multi {:dispatch map?}
[true [:multi {:dispatch #(contains? % "a")}
[true [:map ["a" [:multi {:dispatch #(contains? % "b")}
[true [:map ["b" [:not= "1"]]]]
[false :any]]]]]
[false :any]]]
[false :any]]
{"a" {"b" "1"}}))
{"a" {"b" ["should not be 1"]}}
As you noticed, :not
erases any error messages, so you need to formulate the spec positively up to the leaves. Negating a nested map is a very broad spec. That's equivalent to [:not [:map ["a" [:map ["b" [:= "1"]]]]]
.
I trying to understand how big the task would be to rewrite what I have to use :multi as in your example. What I do not is that I have lots of boolean expressions I parse from strings getting an AST that maps very close to exactly what I have in malli which is nice, then I combine then inside a :and
spec in malli. This works nice for my use case of generating data (with some extra magic), however what I am trying now is to use it to also validate data and get good error messages. The expressions I build can get big and complex but they will always have forms like
[:and
[:not [:map ["a" [:map ["b" [:= "1"]]]]]]
[:map ["a" [:map ["b" [:= "1"]]]]]
[:not
[:or
[:map ["c" [:map ["d" [:= "1"]]]]]
[:map ["c" [:map ["d" [:= "2"]]]]]]]
[:or
[:map ["a" [:map ["b" [:= "1"]]]]]
[:map ["c" [:map ["d" [:= "2"]]]]]]]
This is 4 rules combined inside an :and
.
Any pointers to get me started in the right direction for using :muti
for expressions like this, How would I express :and
. To move :not
I guess I would need to use De Morgans Law and rewrite my expressions.
And an expression like
[:not
[:or
[:map ["c" [:map ["d" [:= "1"]]]]]
[:map ["c" [:map ["d" [:= "2"]]]]]]]
dont look to have a straight forward translation to multi. Rather I guess I should try to trans form that to
[:map ["c" [:map ["d" [:or [:not= "1"] [:not= "2"]]]]]]
I looked a bit more into this and I haven't worked out how to completely eliminate :or
. The problem is that it's not obvious what to use as the dispatch function in general. I'm going to think about it over the weekend and get back to you.
Perhaps one solution would be to push the :not
's in to leaves, and allow :or
but improve the error message calculation logic for :or
. I'll look into that too.
Here's my progress on pushing in :not
. Please try it out. Instead of generating [:not s]
instead return the result of (malli.negate/negate s)
. https://github.com/frenchy64/malli/compare/master...fecb20a03d3165816026a47bd10bd8bc488f2647
Oh, but you said that [:or :map :map]
worked right? So perhaps this will work for you^
Hawing the following still return unknown error
(me/humanize
(m/explain
(mneg/negate
[:or
[:map ["a" [:map ["b" [:= "1"]]]]]
[:map ["a" [:map ["b" [:= "2"]]]]]]
){"a" {"b" "1"}}))
I wonder if not using De Morgans Law and rewrite my expression would work the best, will experiment more with that. The the case
[:not
[:or
[:map ["a" [:map ["b" [:= "1"]]]]]
[:map ["a" [:map ["b" [:= "2"]]]]]]]
Would then be rewritten as
[:and
[:map ["a" [:map ["b" [:not= "1"]]]]]
[:map ["a" [:map ["b" [:not= "2"]]]]]]
Which do give me the result I want
(me/humanize
(m/explain [:and
[:map ["a" [:map ["b" [:not= "1"]]]]]
[:map ["a" [:map ["b" [:not= "2"]]]]]]
{"a" {"b" "1"}}))
Will have to do some full testing for more cases once I get back to work, but something like this is what im aiming at maybe
(defn negate-eq
[tree]
(w/postwalk
(fn [s]
(if (= s :=)
:not= s)) tree)
)
(w/postwalk
(fn [s]
(if (vector? s)
(case (first s)
:not
(case (first (second s))
:map (negate-eq s)
:or (into [:and] (mapv negate-eq (drop 1 (second s))))
:and (into [:or] (mapv negate-eq (drop 1 (second s))))
s) s)
s))
[:not
[:or
[:map ["a" [:map ["b" [:= "1"]]]]]
[:map ["a" [:map ["b" [:= "2"]]]]]]])
I think it's more subtle that than. Those aren't equivalent schemas.
(m/validate
[:not
[:or
[:map ["a" [:map ["b" [:= "1"]]]]]
[:map ["a" [:map ["b" [:= "2"]]]]]]]
{})
;=> true
(m/validate
[:and
[:map ["a" [:map ["b" [:not= "1"]]]]]
[:map ["a" [:map ["b" [:not= "2"]]]]]]
{})
;=> false
Following my nose to try and make equivalent schemas is how I ended up with these kinds of negations:
(mn/negate
[:or
[:map ["a" [:map ["b" [:= "1"]]]]]
[:map ["a" [:map ["b" [:= "2"]]]]]])
;=> [:and
; [:or
; [:not #'clojure.core/map?]
; [:map ["a" {:optional true} :never]]
; [:map ["a" [:or [:not #'clojure.core/map?] [:map ["b" {:optional true} :never]] [:map ["b" [:not= "1"]]]]]]]
; [:or
; [:not #'clojure.core/map?]
; [:map ["a" {:optional true} :never]]
; [:map ["a" [:or [:not #'clojure.core/map?] [:map ["b" {:optional true} :never]] [:map ["b" [:not= "2"]]]]]]]]
I will need to try and understand all this, I still dont understand why my expressions are not generating the same schema when I do think the boolean algebra I use is valid so the two expressions should be equal. But I am far from an expert. I will keep working on this with the help from your inputs.
Perhaps it might help to think of specs as sets of values, and the negation of a spec as the set of all the other values.
[:map [:a [:= 1]]
includes {:a 1}
. its negation includes every other value, including non-maps.
Hey there 👋
I have a question about malli's simple type support, specifically whether supporting :float
would be useful in the schema definitions? I wonder because I can see that I can use :double
, and that both Clojure and CLJS seem to prefer double-precision floats, but it could be useful to describe only needing single-precision floats. Does anyone have an opinion to share or some context on why :float
is not currently supported?
Might not be what you are after but you can still use float?
like this for example
(m/validate [:set float?] #{(float 3.3)})
Yup yup that would work, though it seems in malli float?
is sometimes treated as a double too. Which is a little confusing to me, I wonder if :float
is mostly an alias to :double
or is meant to be distinct?
How would I implement the additionalProperties behavior from json-schema in malli?
https://json-schema.org/understanding-json-schema/reference/object#additionalproperties
for additionalProperties: false
with properties and patternProperties - this allows for any property in the properties list + any properties that match the pattern.
I guess my question resumes to:
How can I implement patternProperties
with malli ?
Context: I am exploring a manual malli schema for (docker) compose spec https://github.com/compose-spec/compose-spec/blob/master/schema/compose-spec.json .
I tried https://github.com/metosin/malli/pull/915 but it closes the service
map because of additionalProperties: false so I can't add anything to them .
The implementation in PR#915 does not hanndle proeprties and patternProperties IMO .
And I am too new to malli to consider working on json-schema conversion (yet) .
I think one way to handle this is to use content dependent schemas ? https://github.com/metosin/malli?tab=readme-ov-file#content-dependent-simple-schema