specter

Latest version: 1.1.3
mathpunk 2020-10-27T18:04:10.025100Z

I think I've got a use case that specter might help, but I'm brand new to it. I've got data that looks like this:

{:name "Conditional Sections E2E Workflow"
 :application {:name "Conditional Sections"
               :id "someId"}} 
There's nested maps, and any time there's an :id key in the map, I need to look up the value of :id in another map, and assoc that value in place.

mathpunk 2020-10-27T18:09:31.025800Z

I'm reading about navigators now... if y'all have a pointer to the one/ones I need, I'd appreciate it

idiomancy 2020-10-27T18:11:35.027700Z

usually the best way to get help around here is to give a sample input and output. Someone will invariably take up the challenge as a little brain teaser to occupy themselves while they wait for their next meeting to start and post the answer

mathpunk 2020-10-27T18:12:26.028Z

I can do that, I'll find a good one

mathpunk 2020-10-27T18:27:39.029300Z

Okay, suppose there is a map called state floating about, like shown. Here's the given data, and the desired data:

{:state {"FzPIxXKk" {:name "Conditional Sections E2E Workflow"
                          :id "var"}}
      :given {:name "Conditional Sections E2E Workflow"
              :application {:name "Conditional Sections"
                            :id "FzPIxXKk"}}
      :desired {:name "Conditional Sections E2E Workflow"
                :application {:name "Conditional Sections"
                              :id "var"}}}

mathpunk 2020-10-27T18:29:23.030Z

For reference, here is the brittle "let's hope we got all the cases" code I have for doing this:

(defn return-with-id [exchange state]
  (assoc-in exchange [:required :id] (get (get state (get-in exchange [:required :id])) :id)))

(defn return-with-application [exchange state]
  (assoc-in exchange [:required :application :id] (get (get state (get-in exchange [:required :application :id])) :id)))

(defn return-with-parent [exchange state]
  (assoc-in exchange [:required :parent :id] (get (get state (get-in exchange [:required :parent :id])) :id)))

(defn return-with-target [exchange state]
  (assoc-in exchange [:required :target :id] (get (get state (get-in exchange [:required :target :id])) :id)))

(defn return [state exchange]
  (if (seq? exchange)
    (throw (Exception. "Multiple requirements needed, invalidating my assumption about our data"))
    (cond-> exchange
      (get state (get-in exchange [:required :id])) (return-with-id state)
      (get state (get-in exchange [:required :application :id])) (return-with-application state)
      (get state (get-in exchange [:required :target :id])) (return-with-target state)
      (get state (get-in exchange [:required :parent :id])) (return-with-parent state))))

mathpunk 2020-10-27T18:30:08.031Z

I could replace those copypasta functions with one function that takes a path -- but it would still be, explicit paths.

mathpunk 2020-10-27T18:42:30.031500Z

Then I'd have,

(defn update-with-path [state exchange path]
  (cond-> exchange
    (get state (get-in exchange (concat [:required] path)))
    (assoc-in (concat [:required] path) (get (get state (get-in exchange (concat [:required] path))) :id))))

(defn return [state exchange]
  (if (seq? exchange)
    (throw (Exception. "Multiple requirements needed, invalidating my assumption about our data"))
    (let [update-fn (partial update-with-path state)]
      (-> exchange
          (update-fn [:id])
          (update-fn [:application :id])
          (update-fn [:target :id])
          (update-fn [:parent :id])))))

mathpunk 2020-10-27T18:43:35.032Z

And hey, that does work:

{:state {"FzPIxXKk" {:name "Conditional Sections E2E Workflow"
                     :id "var"}}
 :given {:method "POST"
         :url "<http://localhost:8080/api/v1/internal/workflows>"
         :required {:name "Conditional Sections E2E Workflow"
                    :application {:name "Conditional Sections"
                                  :id "FzPIxXKk"}}} ;; &lt;-- needs updating
 :result {:method "POST"
          :url "<http://localhost:8080/api/v1/internal/workflows>"
          :required {:name "Conditional Sections E2E Workflow"
                     :application {:name "Conditional Sections"
                                   :id "var"}}}} ;; &lt;-- updated

mathpunk 2020-10-27T18:43:57.032300Z

but like i said, not generic, seems brittle, gotta know what paths to :id's exist in order to explicitly supply them

nathanmarz 2020-10-27T19:14:00.033Z

@mathpunk take a look at recursive-path, MAP-VALS, and stay-then-continue

nathanmarz 2020-10-27T19:14:17.033500Z

you can define a navigator that goes to every nested map, and then from there do your lookup of the id

nathanmarz 2020-10-27T19:15:07.034500Z

e.g. (transform [ALL-MAPS (must :id)] (fn [id] (lookup-val-for-id id)) data)

mathpunk 2020-10-27T19:17:55.034800Z

great, thank you!