specter

Latest version: 1.1.3
igrishaev 2018-07-15T16:09:29.000026Z

Hi! In specter, is there a possibility to perform an upsert operation? For example, I’ve got a vector of maps like this:

[{:id 1 :name "John"}
 {:id 2 :name "Jim"}
 {:id 3 :name "Jane"}]
Now a new map arrives and, depending on its id I need either to update (merge) it with an existing map or append it to the end of the vector. I now how to implement both insert/update separately, but a single transform/setval api call would be much more preferable.

igrishaev 2018-07-15T16:10:48.000033Z

As I see it, the only thing I don’t understand is how to compose a transformation path.

nathanmarz 2018-07-15T19:35:06.000068Z

@igrishaev you can do it with a single path with zippers;

(require '[com.rpl.specter.zipper :as z])

(def truefn (constantly true))

(def target
  (recursive-path [id] p
    (cond-path
      [z/NODE #(= (:id %) id)]
      [z/NODE (submap nil)]

      (not-selected? z/NEXT)
      [z/INNER-RIGHT AFTER-ELEM]

      truefn
      [z/NEXT p]
      )))

(defn upsert [data n]
  (setval [z/VECTOR-ZIP z/DOWN (target (:id n))] n data))

(upsert data {:id 1 :name "Joze"})
;; => [{:id 1, :name "Joze"} {:id 2, :name "Jim"} {:id 3, :name "Jane"}]
(upsert data {:id 0 :name "Joze"})
;; => [{:id 1, :name "John"} {:id 2, :name "Jim"} {:id 3, :name "Jane"} {:id 0, :name "Joze"}]

nathanmarz 2018-07-15T19:35:26.000033Z

but I would do it like this:

(defn upsert [data n]
  (let [[data2 new] (replace-in [ALL #(= (:id %) (:id n)) (submap nil)]
                      (fn [_] [n nil])
                      data)]
    (if (nil? new)
      (setval AFTER-ELEM n data)
      data2
      )))