Learn AST - Clojure

Context

Convert json value to key path by changing AST.

{:deps {org.clojure/clojure {:mvn/version "1.10.1"}
        ;; complient is used for autocompletion
        ;; add your libs here (and restart the runtime to pick up changes)
        compliment/compliment {:mvn/version "0.3.9"}
        cheshire/cheshire {:mvn/version "5.10.0"}
        instaparse/instaparse {:mvn/version "1.4.10"}}}
deps.edn
Extensible Data Notation
{:hello (clojure-version)}
0.0s
Clojure

Test Data

(def json-data "[{
    \"k1\": \"v1\",
    \"k2\": {
      \"k21\": \"v21\",
      \"k22\": \"v22\"
    }
  },
  {
    \"k4\": \"v4\",
    \"k5\": \"v5\",
    \"k6\": \"v6\"
}
]")
(println json-data)
0.4s
Clojure

AST

(require '[instaparse.core :as insta])
(def parser
  (insta/parser "
JSON = ARRAY|OBJECT
<VALUE> = NULL|NUMBER|STRING|TRUE|FALSE|ARRAY|OBJECT
ARRAY = BRACKET_OPEN VALUE WHITESPACE* (COMMA WHITESPACE* VALUE)* BRACKET_CLOSE TERMINAL*
<BRACKET_OPEN> = <'['>
<BRACKET_CLOSE> = <']'>
<WHITESPACE> = <#'\\s+'>
<COMMA> = <','>
KEY_VALUE_PAIR = STRING WHITESPACE* COLON WHITESPACE* VALUE
OBJECT = CURLY_OPEN WHITESPACE* KEY_VALUE_PAIR WHITESPACE* (COMMA WHITESPACE* KEY_VALUE_PAIR WHITESPACE*)* CURLY_CLOSE TERMINAL*
<CURLY_OPEN> = <'{'>
<CURLY_CLOSE> = <'}'>
<COLON> = <':'>
STRING = #'\"[^\"]+\"'
NULL = <#'null'>
NUMBER = #'\\d+'
TRUE = <#'true'>
FALSE = <#'false'>
<TERMINAL> = <#'\\n'>
"))
7.0s
Clojure
; comment for no need
;(insta/visualize (parser (slurp "/path/to/output/test.json")) :output-file "/path/to/output/test.png" :options {:dpi 63})
;(parser (slurp "/path/to/output/test.json"))
0.0s
Clojure
(def json-tree (parser json-data))
(print json-tree)
0.5s
Clojure

Tree Trave 0 (failure version)

(defn transform-key [x]
    (-> x
        (clojure.string/replace "\"" "")
        (clojure.string/upper-case)))
(defn bf "return elements in tree, breath-first"
   [[el left right]] ;; a tree is a seq of one element,
                     ;; followed by left and right child trees
   (if (nil? el)
       ""
       (if (= el :OBJECT) (do (print "{") (bf left) (bf right) (print "}, "))
           (if (= el :KEY_VALUE_PAIR) (do (print (str (transform-key (get left 1)) ": ")) (bf right)) (print left)))))
           
(defn transform-value [x]
    (if (= x "") x (str x ".")))
           
(defn bf2 "return elements in tree, breath-first"
   [[el left right] path] ;; a tree is a seq of one element,
                     ;; followed by left and right child trees
   (if (nil? el)
       ""
       (if (= el :OBJECT) (do (print "{") (bf2 left path) (bf2 right path) (print "}, "))
           (if (= el :KEY_VALUE_PAIR) (do (print (str (transform-key (get left 1)) ": ")) (bf2 right (str (transform-value path) (transform-key (get left 1))))) (print (str "\"" path "\", "))))))
           
(defn bf3 "return elements in tree, breath-first"
    [[el & children :as tree] path] ;; a tree is a seq of one element,
                     ;; followed by left and right child trees
    (if (nil? el)
        (do)
        (if (= el :JSON)
            (do (print (get (vec children) 0))
                (bf3 (get (vec children) 0) path))
                (if (= el :ARRAY)
                    (let [[first & rest-list] (get (vec children) 0)
                            rest (vec rest-list)]
                        (do (print "[") (bf3 first path) (bf3 rest path) (print "], ")))
                    (if (= el :OBJECT)
                        (let [[_ [_ key] & rest-list] (get (vec children) 0)
                            [rest] (vec rest-list)]
                            (do (print "{") 
                                (print (str (transform-key key) ": "))
                                (bf3 rest (transform-key key))
                                (print "}, ")))
                        (if (= el :KEY_VALUE_PAIR)
                            (do (print (str (transform-key (get first 1)) ": ")) 
                                (bf3 rest (str (transform-value path) (transform-key (get first 1)))))
                            (print (str "\"" path "\", "))))))))
                            
(bf3 json-tree "")
1.0s
Clojure

Related SO Question:

https://stackoverflow.com/questions/67656049/how-to-travel-a-multiple-branches-tree-and-collect-data-in-clojure

Tree Travel 1 (@Chris)

(declare g h)
(defn f [[_ body]]
  (g body))
  
(defn g [[type & value]]
  (condp = type
    :ARRAY (map g value)
    :OBJECT (reduce h {} value)
    :STRING (first value)))
(defn h [m [_ [_ k] v]]
  (assoc m k (g v)))
  
(print (f json-tree))
0.4s
v1Clojure
(defn kt [k] (clojure.string/replace k "\"" ""))
(defn g [[type & value] & [prev-key]]
  (condp = type
    :ARRAY (map g value)
    :OBJECT (reduce (fn [m [_ [_ k] v]]
                      (let [[sub-type & sub-value] v
                            curr-key (if prev-key (str (kt prev-key) "." (kt k)) (kt k))]
                        (assoc m (kt k) (condp = sub-type
                                     :STRING curr-key
                                     (g v curr-key)))))
                    {} value)))
(defn f [[_ body]]
  (g body))
(def json-map (f json-tree))
0.1s
v2Clojure
(require '[cheshire.core :as json])
(json/generate-string json-map {:pretty true})
2.1s
Clojure

Tree Travel 2 (@nt)

(require '[cheshire.core :as json])
(def r (json/parse-string json-data keyword))
(clojure.pprint/pprint r)
0.8s
Clojure
(defn key-paths
  "映射key路径"
  [pre-key x]
  (cond
    (map? x)
    (->> x
         (map (fn [[k v]] [k
                           (key-paths (if pre-key
                                           (str pre-key "." (name k))
                                           (name k))
                                         v)]))
         (into {}))
    (sequential? x) (mapv #(key-paths pre-key %) x)
    :else pre-key))
(def map-key-paths #(key-paths nil %))
0.1s
Clojure
(clojure.pprint/pprint (map-key-paths r))
0.4s
Clojure

Final Version

(require '[instaparse.core :as insta])
(require '[cheshire.core :as json])
(def parser
  (insta/parser "
JSON = ARRAY|OBJECT
<VALUE> = NULL|NUMBER|STRING|TRUE|FALSE|ARRAY|OBJECT
ARRAY = BRACKET_OPEN VALUE WHITESPACE* (COMMA WHITESPACE* VALUE)* BRACKET_CLOSE TERMINAL*
<BRACKET_OPEN> = <'['>
<BRACKET_CLOSE> = <']'>
<WHITESPACE> = <#'\\s+'>
<COMMA> = <','>
KEY_VALUE_PAIR = STRING WHITESPACE* COLON WHITESPACE* VALUE
OBJECT = CURLY_OPEN WHITESPACE* KEY_VALUE_PAIR WHITESPACE* (COMMA WHITESPACE* KEY_VALUE_PAIR WHITESPACE*)* CURLY_CLOSE TERMINAL*
<CURLY_OPEN> = <'{'>
<CURLY_CLOSE> = <'}'>
<COLON> = <':'>
STRING = #'\"[^\"]+\"'
NULL = <#'null'>
NUMBER = #'\\d+'
TRUE = <#'true'>
FALSE = <#'false'>
<TERMINAL> = <#'\\n'>
"))
(def json-tree (parser (slurp "./input.json")))
(defn kt [k] (clojure.string/replace k "\"" ""))
(defn g [[type & value] & [prev-key]]
  (condp = type
    :ARRAY (map g value)
    :OBJECT (reduce (fn [m [_ [_ k] v]]
                      (let [[sub-type & sub-value] v
                            curr-key (if prev-key (str (kt prev-key) "." (kt k)) (kt k))]
                        (assoc m (kt k) (condp = sub-type
                                     :STRING curr-key
                                     (g v curr-key)))))
                    {} value)))
(defn f [[_ body]]
  (g body))
(def json-map (f json-tree))
(json/generate-string json-map {:pretty true})
Clojure

After run and get the output, just go https://www.freeformatter.com/json-escape.html to UNESCAPE the content and get final json.

Runtimes (1)