Fork me on GitHub
#malli
<
2024-02-23
>
Tommi Martin13:02:52

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? 😄

ikitommi13:02:12

sound really strange. do you have a repro of this?

Tommi Martin13:02:39

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

Tommi Martin13:02:22

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))))

Tommi Martin13:02:54

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))))

Tommi Martin14:02:01

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

Tommi Martin18:02:05

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.

Tommi Martin13:02:07

Intrestingly, using meander directly worked but the matcher + transformer syntax broke that usecase with the negative numbers

Henrik Larsson17:02:10

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.

Henrik Larsson08:02:24

I guess if I replace every :or with :union that would take me one step closer, however what should I do with the :and?

Henrik Larsson08:02:25

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.

Henrik Larsson11:02:50

This looks interesting https://github.com/bsless/malli-keys-relations any info why it got abandon?

Henrik Larsson18:02:12

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.

ikitommi17:02:19

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.

ikitommi17:02:01

you could: 1. put the most spesific first 2. use a custom generator at :and

ikitommi17:02:03

(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"}}

ikitommi17:02:36

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}})

ikitommi17:02:48

hope this helps

Henrik Larsson09:03:00

Thanks for this, it makes sense that the first and works as a generator, that explains why what I did actually works.

frozenlock18:02:53

Is it possible to 'query' Malli schemas? For example, "give me all the fields that can accept a collection of integers"

steveb8n07:02:37

One solution would be to combine specter and deref-recursive. An interesting idea would be deref-recursive into a data script db, then datalog

👀 1
ikitommi17:02:55

could you @U0ERZQ1K2 preudocode what you would like to see? e.g. input + output. I would just m/walk over the schema…

frozenlock16:02:57

Yes... I'll come back on this with a usable example, I need to convert my stuff from spec/spec-tools first.