clojureedn

Is there a way we can re-use value of defined keys in EDN?


Is there a way I can reuse values of key defined in EDN?

For ex: If I have following EDN

{
   :abc "value1"
   :def :abc
}

When I read above EDN in Clojure, I want for 'def' key, value1 which is value of :abc to be passed. But currently :abc string is getting is passed when I read it using EDN read-string function


Solution

  • There's no such thing in plain EDN. But you could build a two phase pass, either by replacing things first on the EDN string itself, or possibly better to do it after the EDN was parsed by manipulating a data-strucure for example:

    (require '[clojure.edn :as edn])
    (require '[clojure.walk :as walk])
    
    (defrecord self [key])
    (defn tag-self
      [v]
      (->self v))
    
    (defn custom-readers []
      {'x/self tag-self})
    
    (defn replace-self
      [coll]
      (walk/prewalk
       (fn [form]
         (if (map? form)
           (reduce-kv
            (fn [acc k v]
              (assoc acc k (if (= self (type v))
                             (get form (:key v))
                             v)))
            {}
            form)
           form))
       coll))
    
    (replace-self
     (edn/read-string
      {:readers (custom-readers)}
      "[{:abc \"value1\"
         :def #x/self :abc}
        {:abc {:bbb \"value2\"
               :def #x/self :bbb}
         :def #x/self :abc
         :xyz {:aaa \"value3\"
               :nope #x/self :abc
               :def #x/self :aaa}}]"))
    

    Will return:

    [{:abc "value1", :def "value1"}
     {:abc {:bbb "value2", :def "value2"},
      :def {:bbb "value2", :def "value2"},
      :xyz {:aaa "value3", :nope nil, :def "value3"}}]
    

    The trick is that I mark the references to another key of the current map using the #x/self tag reader that I created, which will wrap the value in a self record. Now we just walk over the returned data-structure, and every time we encounter a self type, we replace it by the value of its :key attribute in the same map.

    The above replace-self walker is recursive, so you can see that it can handle nested maps and it can even replace self references inside map that are themselves self referenced. It does local scoping only though, so :nope #x/self :abc did not get replaced by the value of the :abc in the parent map, and since there was no :abc key in its own map, its value was set to nil.

    The custom tag is optional, I did it this way cause now it works for any type of key. But you could use keyword? instead in replace-self as well if you want this to be the behavior for them:

    (defn replace-self
      [coll]
      (walk/prewalk
       (fn [form]
         (if (map? form)
           (reduce-kv
            (fn [acc k v]
              (assoc acc k (if (keyword? v)
                             (v form)
                             v)))
            {}
            form)
           form))
       coll))
    
    (replace-self
     (edn/read-string
      "[{:abc \"value1\"
         :def :abc}
        {:abc {:bbb \"value2\"
               :def :bbb}
         :def :abc
         :xyz {:aaa \"value3\"
               :nope :abc
               :def :aaa}}]"))
    

    This will return the same as before, just no need for a custom tag and the self record:

    [{:abc "value1", :def "value1"}
     {:abc {:bbb "value2", :def "value2"},
      :def {:bbb "value2", :def "value2"},
      :xyz {:aaa "value3", :nope nil, :def "value3"}}]