Specter's multi-transform and nested multi-paths does not work as expected


#1

I have a bit of data transformation that would benefit from specters multi-transform, but I hit some unexpected behaviour and would like to know if anyone has seen this before.

The data I am transforming looks a bit like this:

(def value {:albedo {:url "" :factor 1.0}
            :normal {:url "foo/bar.png" :factor 0.1} 
            :tiling {:x 0.0 :y 0.0} 
            :offset {:x 0.0 :y 0.0}})

What I want to do is transform the keys :albedo and :normal, but only if the :url key is not empty. I can do this using the following code:

(->> value
         (specter/setval [(specter/multi-path :albedo :normal)
                          (specter/pred (comp empty? :url))]
                         specter/NONE)
         (specter/transform [(specter/multi-path :albedo :normal)
                             (specter/must :url)]
                            (partial perform-computation)))

As I understand (https://github.com/nathanmarz/specter/wiki/List-of-Macros#multi-transform)[the Specter docs for multi-transform] this could be written as:

(specter/multi-transform [(specter/multi-path :albedo :normal)
                          (specter/multi-path [(specter/pred (comp not-empty :url)) :url (specter/terminal (partial perform-computation))
                                               (specter/pred (comp empty? :url)) (specter/terminal-val specter/NONE)])]
                         value)

However, this ends up only stripping entries with empty :url. So, I decided to produce a simpler example using nested multi-paths in a select:

(specter/select [(specter/multi-path :a :b) (specter/multi-path :d :e)]
                {:a {:d 23
                     :e 99}
                 :b {:d 11
                     :e 33}}) ;=> [23 99 11 33]

This shows that nesting multi-paths should work as expected; it breaks when using multi-transform though, where it only touches the first entry of the nested multi-path:

(specter/multi-transform [(specter/multi-path :a :b)
                          (specter/multi-path [:d (specter/terminal-val :UPDATED)
                                               :e (specter/terminal-val :EEE)])]
                         {:a {:d 23
                              :e 99}
                          :b {:d 11
                              :e 33}}) ;=> {:a {:d :UPDATED :e 99} :b {:d :UPDATED :e 33}}

Has anyone seen this behaviour before, or can you spot what I am doing wrong? Any help would be much appreciated.


#2

Alright, sometimes it really helps explaining the problem: While editing the post above for code layout, I noticed my mistake. The nested multi-path should have multiple vectors inside it when the multi paths are not just a single level; like so:

(specter/multi-transform [(specter/multi-path :a :b)
                          (specter/multi-path [:d (specter/terminal-val :UPDATED)]
                                              [:e (specter/terminal-val :EEE)])]
                         {:a {:d 23
                              :e 99}
                          :b {:d 11
                              :e 33}}) ;=> {:a {:d :UPDATED :e :EEE} :b {:d :UPDATED :e :EEE}}

Letting this post stand in case anyone else stumbles upon the same typo. :slight_smile:


#3

Whoa; somehow I had it in my head that Spectre was derelict from last time I checked (6-9 months ago), despite the very promising YouTube video introducing it. I’m happy to see that the repo is active and promising! I’m glad you reminded me of this.