This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2024-02-23
Channels
- # announcements (18)
- # beginners (26)
- # calva (12)
- # cider (43)
- # cljdoc (4)
- # clojure (38)
- # clojure-europe (11)
- # clojure-nl (1)
- # clojure-norway (12)
- # clojure-sweden (2)
- # clojure-uk (4)
- # cursive (17)
- # data-science (4)
- # datalevin (2)
- # datomic (3)
- # emacs (10)
- # ghostwheel (4)
- # graphql (11)
- # honeysql (1)
- # hyperfiddle (7)
- # introduce-yourself (1)
- # malli (23)
- # nrepl (11)
- # overtone (1)
- # pathom (9)
- # pedestal (2)
- # polylith (1)
- # portal (3)
- # reitit (1)
- # shadow-cljs (12)
- # timbre (4)
- # vim (2)
- # xtdb (4)
Speaking of the transforming data with malli and meander post. I found a situation where integer numbers that were strongly on the negative broke the the meander query adding min 0 to all ints restored it. in practice this showed up as real data working but malli generator data breaking when it was posted into the transformer. Has anyone else had this kind of experience or should I start to get worried? 😄
Actually no. I reverted the changes to my input and output schemas to get you the repro, but it wont happen again. I have to dig into this more now. If you want I can give you the transformer and the schemas though
Update: jackout and jack-in seems to cause it to happen with unmodified schema... here is the schemas and transformer: Input schema:
(def schema-rest-status-simple
[:map
[:routers {:optional true} [:vector [:map [:name :string] [:connections [:map [:created :int] [:active :int]]]]]]
[:composite :boolean]
[:name :string]
[:type :string]
[:policy :string]
[:datasources
[:vector
[:map
[:role :string]
[:groupId :int]
[:standby :boolean]
[:witness :boolean]
[:alertMessage :string]
[:name :string]
[:dataserver [:map [:state :string]]]
[:state :string]
[:replicator
[:map
[:role :string]
[:appliedLatency :double]
[:pipelineSource :string]
[:maxStoredSeqno :int]
[:state :string]
[:relativeLatency :double]
[:seqno :int]
[:minStoredSeqno :int]
[:appliedLastEventId :string]
[:version :string]]]
[:alertStatus :string]
[:manager [:map [:state :string]]]
[:lastError :string]
[:lastShunResult :string]
[:connections [:map [:created :int] [:active :int]]]
[:archive :boolean]]]]
[:coordinators [:vector :string]]
[:multimaster :boolean]])
output
(def schema-rest-outbound
[:schema
{:registry {"Root" [:map
[::m/default [:map-of {:gen/min 1 :gen/max 100} :keyword "Cluster"]]]
"Cluster" [:map
[:id [:string {:default "test"}]]
[:name :string]
[:isComposite :boolean]
[:policy :string]
[:topology :string]
[:dataservices [:map-of {:gen/min 1 :gen/max 4} :keyword "Dataservices"]]]
"Dataservices" [:map
[:id [:string {:default "test"}]]
[:name :string]
[:policy :string]
[:type :string]
[:coordinators [:vector :string]]
[:datasources [:map-of {:gen/min 1 :gen/max 6} :keyword "Datasources"]]]
"Datasources" [:map
[:name :string]
[:role :string]
[:state :string]
[:standby :boolean]
[:archive :boolean]
[:witness :boolean]
[:alert "SourceAlert"]
[:replicator "Replicator"]
[:manager "Manager"]
[:connections "Connections"]
[:dataserver "Dataserver"]]
"SourceAlert" [:map
[:status :string]
[:message :string]
[:lastError :string]
[:lastShunReason :string]]
"Replicator" [:map
[:appliedLatency :double]
[:relativeLatency :double]
[:seqno :int]
[:minStoredSeqno :int]
[:maxStoredSeqno :int]
[:pipelineSource :string]
[:appliedLastEventId :string]
[:version :string]
[:role :string]]
"Manager" [:map
[:state :string]]
"Connections" [:map
[:created :int]
[:active :int]]
"Dataserver" [:map
[:state :string]]}}
"Root"])
Pattern and expression
(def rest-standalone-transformation
{:registry {:rest-status schema-rest-status-simple
:dashboard schema-rest-outbound}
:mappings {:source :rest-status
:target :dashboard
:pattern '{:name ?name
:composite ?composite
:type ?type
:policy ?policy
:dashboard-id ?dashboard-id
:coordinators ?coordinators
:datasources [{:name (me/and !name !index)
:manager {:state !manager-state}
:dataserver {:state !data-state}
:role !role
:state !state
:standby !standby
:archive !archive
:witness !witness
:alertStatus !alert-s
:alertMessage !alert-m
:lastShunResult !shun
:lastError !error
:connections !connections
:replicator {:state !repl-state
:version !version
:appliedLatency !a-latency
:relativeLatency !r-latency
:seqno !seqno
:minStoredSeqno !minseqno
:maxStoredSeqno !maxseqno
:appliedLastEventId !eventId
:pipelineSource !source
:role !repl-role}} ...]}
:expression '{(me/keyword ?dashboard-id)
{:name ?name
:id ?dashboard-id
:isComposite ?composite
:topology ?type
:policy ?policy
:dataservices
{(me/keyword ?dashboard-id)
{:name ?name
:id ?dashboard-id
:type ?type
:policy ?policy
:coordinators ?coordinators
:datasources
{& ([(me/keyword !index)
{:name !name
:role !role
:state !state
:standby !standby
:archive !archive
:witness !witness
:manager {:state !manager-state}
:dataserver {:state !data-state}
:connections !connections
:replicator {:version !version
:state !repl-state
:appliedLatency !a-latency
:relativeLatency !r-latency
:seqno !seqno
:minStoredSeqno !minseqno
:maxStoredSeqno !maxseqno
:pipelineSource !source
:appliedLastEventId !eventId
:role !repl-role}
:alert {:message !alert-m
:status !alert-s
:lastError !error
:lastShunReason !shun}}] ...)}}}}}}})
Matcher function: note it has been modified:
(defn matcher
[{:keys [pattern expression]}]
(eval `(fn [data#]
(let [~'data data#]
~(mre/compile-rewrite-args ; modified: original did not use compile-rewrite-args
(list 'data pattern expression)
nil)))))
Transformer, notice the output of a map over a vector.
(defn transformer
[{:keys [registry mappings]} source-transformer target-transformer]
(let [{:keys [source target]} mappings
xf (comp (map (m/coercer (get registry source) source-transformer))
(map (matcher mappings))
(map (m/coercer (get registry target) target-transformer)))]
(fn [data] (into {} xf data))))
Sorry for the wall of text, it's my first proper meander thing and it is quite... verbose still to use it, i was running this:
(def status->dashboard-status
(transformer
rest-standalone-transformation
(mt/transformer
(mt/string-transformer))
(mt/transformer
(mt/string-transformer)
(mt/default-value-transformer))))
(try
(status->dashboard-status [(assoc (mg/generate schema-rest-status-simple) :dashboard-id "dummy")])
(catch Exception ex
(-> ex ex-data :data :explain merr/humanize prn)
(prn (ex-data ex))))
maybe it's better to not go into that code @U055NJ5CC. I'm feeling this is a tool issue and not actually related to malli:
If I jackout and jack in with calva in cursor., that schema does not work with Malli generator and the provided code.
If i fix the code style issue in schema-rest-status-simple
ie routers map being on one line.
then jackout and back in again and evaluate everything, it works. So it's not actually a number range issue. It's a linebreak issue which makes me believe that there is no way it's nothing else than tool related
I figured that one out it was because of my own stupidity. There was a dependency error that caused the nested usage of meander to not work in the patterns and expressions as it was expected. hence the problems. While fixing that problem I think I finally clicked with Malli after playing with the generator and schema interaction as I debugged. I can basically develop my entire app with the generator. This thing is incredibly powerful. Thank you so much for making it, and making it open source.
Intrestingly, using meander directly worked but the matcher + transformer syntax broke that usecase with the negative numbers
I am trying to introduce Clojure at work since I think I have found a perfect candidate for refactor.
We have a legacy system that save lots of expressions in a database in the form
"~a{b=ii}|c{d=u}&~(e{f='[a-z]'}|a{b=3})"
this basically describe the constraints over values in a map. Where a{b}
is just the path in the map. Now what I have done
is writing a parser where I parse each of these expression and compile them into Malli like this (this is not what the above expression produce just an example of data):
'([:and [:not [:map ["a" [:map ["b" [:re #"dd[A|B|C|F|D|E]"]]]]]] [:or [:map ["c" [:map ["d" [:= "3"]]]]] [:map ["c" [:map ["d" [:= "2"]]]]] [:map ["c" [:map ["d" [:= "9"]]]]] [:map ["c" [:map ["d" [:= "99"]]]]]]]
[:not [:map ["d" [:map ["e" [:re #"4[A|B]"]]]]]]
[:map ["f" [:map ["p" [:= "777"]]]]]
[:map ["d" [:map ["e" [:= "iia"]]]]]
[:map ["a" [:map ["b" [:= "hhhj"]]]]]
[:not [:or [:map ["d" [:map ["e" [:re #"84[B]?$"]]]]] [:map ["a" [:map ["b" [:= "HHJE"]]]]]]])
This works very nice for validation where I can just surround the whole thing with an [:and ...]
and it will validate any data.
However the real value would come if I where able to generate data from this list of specs since that is not available for the legacy system. However generating data will not work when surrounding with [:and ...]
since each [:map]
only describe on key.
What I think I need to do is merge all the sub specs into a single [:map ...] spec, however I would like to know
if this is possible and where to start with such and algorithm. Has any work like this been explored using Malli before? I see there is a union operator but that is not what I need.
The above example show the full extension what the grammar contains which is basically :not :and :or :map := :re
.
Any input for this problem would be very much appreciated.
I guess if I replace every :or
with :union
that would take me one step closer, however what should I do with the :and
?
I think what I will try is to compile into only :not :or
implementing rewrite rules for :and
then I should just be able to use :union
to merge all rules into one I think.
This looks interesting https://github.com/bsless/malli-keys-relations any info why it got abandon?
I have now solved it I think, what I do is a create a schema containing all values without any constraints, I use this schema to generate values, then I validate the results against to [:and ...]
of all the expressions I have. Not sure if there is a smarter way but this looks to work very well, will need more testing before I know for sure.
hi. so the problem is that with :and
? it uses the first branch as generator and rest to validate the generated values. if the first one gives too wide results, the rest don’t easily match.
(def schema1
[:not [:map ["a" [:map ["b" [:re #"dd[A|B|C|F|D|E]"]]]]]])
(def schema2
[:or
[:map ["c" [:map ["d" [:= "3"]]]]]
[:map ["c" [:map ["d" [:= "2"]]]]]
[:map ["c" [:map ["d" [:= "9"]]]]]
[:map ["c" [:map ["d" [:= "99"]]]]]])
(mg/generate [:and schema1 schema2])
;=> Couldn't satisfy such-that predicate after 100 tries.
(mg/generate [:and schema2 schema1])
;=> {"c" {"d" "3"}}
(mg/generate [:and {:gen/schema schema2} schema1 schema2])
;=> {"c" {"d" "2"}}
the :not
doesn’t generate very well:
(mg/sample schema1)
;(nil
; nil
; (#uuid"303ca3c6-7463-4be4-8f01-07a2b2606446")
; 2.0
; nil
; [:R?/L??]
; nil
; ({-5 -})
; #{0 -0.3046875 f/yA. true "ch[Kd4.." -8/3 :-/k_}
; #{[] #{-5}})
Thanks for this, it makes sense that the first and works as a generator, that explains why what I did actually works.
Is it possible to 'query' Malli schemas? For example, "give me all the fields that can accept a collection of integers"
One solution would be to combine specter and deref-recursive. An interesting idea would be deref-recursive into a data script db, then datalog
could you @U0ERZQ1K2 preudocode what you would like to see? e.g. input + output. I would just m/walk
over the schema…
Yes... I'll come back on this with a usable example, I need to convert my stuff from spec/spec-tools first.