All Downloads are FREE. Search and download functionalities are using the official Maven repository.

rest_resources_viz.extractor.clj Maven / Gradle / Ivy

(ns rest-resources-viz.extractor
  (:require [clojure.tools.cli :as cli]
            [clojure.set :as set]
            [clojure.spec :as s]
            [clojure.spec.test :as stest]
            [fipp.clojure :as fipp]
            [clojure.xml :as xml]
            [clojure.string :as str]
            [clojure.data.xml :as dx]
            [clojure.data.xml.tree :as dxt]
            [clojure.zip :as zip]
            [clojure.data.zip :as dz]
            [clojure.data.zip.xml :as dzx]
            [clojure.java.io :as io]
            [clojure.walk :as walk]
            [clojure.java.classpath :as cp]
            [cheshire.core :as json]
            [com.rpl.specter :as sp]
            [rest-resources-viz.spec :as rspec]))

(defn instrument-all []
  (run! stest/instrument (stest/instrumentable-syms)))

(defn remove-nils [m]
  (let [f (fn [x]
            (if (map? x)
              (let [kvs (filter (comp not nil? second) x)]
                (if (empty? kvs) nil (into {} kvs)))
              x))]
    (walk/postwalk f m)))

(defn children
  [node]
  (:content node))

(defn leaf?
  [node]
  (let [cs (children node)]
    (or (empty? cs) (and (= 1 (count cs)) (string? (first cs))))))

(defn leaf->map
  [node]
  {(:tag node) (first (children node))})

(defn vectorizing-reduce-kv
  "TODO doc and maybe improve the name"
  [m1 m2]
  (reduce-kv
   (fn [m1 k2 v2]
     (update m1 k2 (fn [v1]
                     ;; AR - this was tough
                     (if (and (nil? v1) (string? v2))
                       (str/replace (str/trim v2) "\n" " ")
                       (conj
                        (cond
                          (nil? v1) []
                          (sequential? v1) v1
                          ;; I am making it a vector
                          :else [v1])
                        v2)))))
   m1
   m2))

(defn node->clj
  "Covert xml nodes to Clojure data structures"
  [node]
  (if (leaf? node)
    (leaf->map node)
    {(:tag node) (transduce (map node->clj)
                            (completing vectorizing-reduce-kv)
                            {}
                            (children node))}))

(comment
  ;; The following was presenting the issue
  (s/explain :graph-data/entity (->> (cp/classpath-jarfiles)
                                     classpath-resource-xmls!
                                     (filter (partial re-find #"shipments-shipping-address"))
                                     (into [] (comp (map parse-resource-xml)
                                                    (map descend-to-family)
                                                    (map node->clj)))
                                     first
                                     :family)))

(defn spit-xml
  "Spit an xml, the opts will be passed to clojure.java.io/writer

  Like in cheshire, if the opts contains :pretty true, the output will
  be pretty printed."
  [f node & [opts]]
  (with-open [w (io/writer f)]
    (if-not (:pretty opts)
      (dx/emit node w)
      (dx/indent node w))))

(defn keywordize
  "Convert \"family.resource\" into :family/resource

  If no \".\" is found, the string is converted to keyword as is"
  [family s]
  (when s
    (let [ss (remove empty? (str/split s #"\." 2))]
      (when (seq ss)
        (if (= 2 (count ss))
          (apply keyword ss)
          (keyword family (first ss)))))))

(s/fdef resource->relationship
  :args (s/cat :from-fn (s/fspec :args (s/cat :resource :resource/entity)
                                 :ret qualified-keyword?)
               :to-fn (s/fspec :args (s/cat :resource :resource/entity)
                               :ret qualified-keyword?)
               :resource :resource/entity)
  :ret :relationship/entity)

(defn resource->relationship
  "Produce a relationship from a resource

  The return value of (to-fn resource) will be assigned to the :to
  field, prepended with the family (required), so it should return a
  string. See spec."
  [from-fn to-fn resource]
  (let [f-id (:family-id resource)
        id (:id resource)]
    {:name (name id)
     :family-id f-id
     :from (keywordize (name f-id) (from-fn resource))
     :to (keywordize (name f-id) (to-fn resource))
     :rel (name id)}))

(defn descend-to-family
  [definitions-node]
  (-> definitions-node :content first))

(defn parse-resource-xml
  "Return the resource xml"
  [resource-xml-path]
  (-> resource-xml-path
      io/resource
      slurp
      (dx/parse-str :namespace-aware false :skip-whitespace true)))

;;;;;;;;;;;;;;;;;;;
;; add-family-id ;;
;;;;;;;;;;;;;;;;;;;
(s/def :add-family-id/resource (s/coll-of (s/keys :req-un [:resource/name :resource/uri]
                                                  :opt-un [:resource/alias :resource/description])
                                          :kind vector?))
(s/def :add-family-id-relationship/from string?)
(s/def :add-family-id-relationship/to string?)
(s/def :add-family-id/relationship (s/coll-of (s/keys :req-un [:relationship/name :relationship/rel
                                                               :add-family-id-relationship/from :add-family-id-relationship/to]
                                                      :opt-un [:relationship/description :relationship/rev])
                                              :kind vector?))
(s/def :add-family-id/family (s/keys :req-un [:family/name]
                                     :opt-un [:family/description :add-family-id/resource :add-family-id/relationship]))
(s/def :add-family-id/definitions (s/keys :req-un [:add-family-id/family]))

(s/fdef add-family-id
  :args (s/cat :definitions :add-family-id/definitions))

(defn add-family-id
  [definitions]
  (sp/transform [(sp/must :family)]
                #(assoc % :id (keyword (:name %)))
                definitions))

;;;;;;;;;;;;;;;;;;;;;;;;;
;; propagate-family-id ;;
;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def :propagate-family-id/family (s/merge :add-family-id/family
                                            (s/keys :req-un [:family/id])))

(s/def :propagate-family-id/definitions (s/keys :req-un [:propagate-family-id/family]))

(s/fdef propagate-family-id
  :args (s/cat :definitions :propagate-family-id/definitions))

(defn propagate-family-id
  [definitions]
  (sp/transform [(sp/must :family)
                 (sp/collect-one [:id])
                 sp/MAP-VALS
                 vector?
                 sp/ALL
                 :family-id]
                (fn [id _] id)
                definitions))

;;;;;;;;;;;;;;;;;;;;;
;; add-resource-id ;;
;;;;;;;;;;;;;;;;;;;;;
(s/def :add-resource-id/resource (s/coll-of (s/keys :req-un [:resource/name :resource/uri :resource/family-id]
                                                    :opt-un [:resource/alias :resource/description])
                                            :kind vector?))
(s/def :add-resource-id/family (s/merge :propagate-family-id/family
                                        (s/keys :opt-un [:add-resource-id/resource])))
(s/def :add-resource-id/definitions (s/keys :req-un [:add-resource-id/family]))

(s/fdef add-resource-id
  :args (s/cat :definitions :add-resource-id/definitions))

(defn add-resource-id
  [definitions]
  (sp/transform [(sp/must :family)
                 (sp/must :resource)
                 sp/ALL
                 (sp/collect-one [(sp/submap [:family-id :name])])
                 :id]
                (fn [m _] (keywordize (-> m :family-id name) (:name m)))
                definitions))

;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; sanitize-relationship ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def :sanitize-relationship/resource (s/coll-of (s/keys :req-un [:resource/family-id :resource/id :resource/name :resource/uri]
                                                          :opt-un [:resource/alias :resource/description])
                                                  :kind vector?))
(s/def :sanitize-relationship/family (s/keys :req-un [:family/name]
                                             :opt-un [:family/description
                                                      :sanitize-relationship/resource
                                                      :add-family-id/relationship]))
(s/def :sanitize-relationship/definitions (s/keys :req-un [:sanitize-relationship/family]))

(s/fdef sanitize-relationship
  :args (s/cat :definitions :sanitize-relationship/definitions))

(defn sanitize-relationship
  [family]
  (sp/transform [(sp/must :family)
                 (sp/must :relationship)
                 sp/ALL
                 (sp/collect-one [:family-id])]
                (fn [family-id rel]
                  (into rel
                        [(when-let [s (:from rel)] [:from (keywordize (name family-id) s)])
                         (when-let [s (:to rel)] [:to (keywordize (name family-id) s)])]))
                family))

;;;;;;;;;;;;;;;;;;;;;;
;; normalize-family ;;
;;;;;;;;;;;;;;;;;;;;;;
(s/def :normalize-family/relationship (s/coll-of :relationship/entity :kind vector?))
(s/def :normalize-family/family (s/keys :req-un [:family/name]
                                        :opt-un [:family/description
                                                 :sanitize-relationship/resource
                                                 :normalize-family/relationship]))
(s/def :normalize-family/definitions (s/keys :req-un [:normalize-family/family]))

(s/fdef normalize-family
  :args (s/cat :defs (s/coll-of :normalize-family/definitions :kind vector?)))

(defn normalize-family
  [definitions]
  (merge {:family (let [[base others] (transduce (map :family)
                                                 (completing (fn [[base others] fam]
                                                               (cond
                                                                 (= "base" (:name fam)) [fam others]
                                                                 (not-any? #(= (:name %) (:name fam)) others) [base (conj others fam)]
                                                                 :else [base others])))
                                                 [nil #{}]
                                                 (sp/setval [sp/ALL :family sp/MAP-VALS vector?] sp/NONE definitions))]
                    ;; set base as first item
                    (into [base] others))}
         (transduce (map identity)
                    (completing (fn [acc [k v]]
                                  (update acc k #(into (or % []) v))))
                    {}
                    (sp/select [sp/ALL :family sp/ALL (sp/pred (comp vector? second))]
                               definitions))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; add-list-of-relationship ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def :add-list-of-relationship/family (s/coll-of :family/entity :kind vector? :distinct true))
(s/def :add-list-of-relationship/resource (s/coll-of :resource/entity :kind vector?))
(s/def :add-list-of-relationship/relationship (s/coll-of :relationship/entity :kind vector?))
(s/def :add-list-of-relationship/graph (s/keys :req-un [:add-list-of-relationship/family]
                                               :opt-un [:add-list-of-relationship/resource
                                                        :add-list-of-relationship/relationship]))

(s/fdef add-list-of-relationship
  :args (s/cat :defs :add-list-of-relationship/graph))

(defn add-list-of-relationship
  [definitions]
  (assoc definitions :list-of-relationship (->> definitions
                                                (sp/select-one [:resource (sp/filterer #(:list-of %))])
                                                (mapv (partial resource->relationship :name :list-of)))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; add-pagination-relationship ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn add-pagination-relationship
  [definitions]
  (assoc definitions :pagination-relationship (->> definitions
                                                   (sp/select-one [:resource (sp/filterer #(:paginates %))])
                                                   (mapv (partial resource->relationship :name :paginates)))))

(defn add-alias-relationship
  [definitions]
  (assoc definitions :alias-relationship (->> definitions
                                              (sp/select-one [:resource (sp/filterer #(:alias %))])
                                              (mapv (partial resource->relationship :name :alias)))))

(comment
  (sp/select-one [:resource (sp/filterer #(:list-of %))] gd))

(defn xml-files->graph-data
  "Transform data in order to obtain a format that is good for a graph
  visualization.

  The input is a string sequence of paths and the ruturn is the a
  Clojure map."
  [xml-files]
  (->> xml-files
       (into [] (comp (map parse-resource-xml)
                      (map descend-to-family)
                      (map node->clj)
                      (map add-family-id)
                      (map propagate-family-id)
                      (map add-resource-id)
                      (map sanitize-relationship)))
       normalize-family
       add-list-of-relationship
       add-pagination-relationship
       add-alias-relationship)) ;; TODO - spec the final version

(defn xml-files->definitions
  "Aggregate  under 

  The input is a string sequence of paths and the ruturn is the a
  Clojure map of xml nodes."
  [xml-files]
  (->> xml-files
       (into [] (comp (map parse-resource-xml)
                      (map descend-to-family)))
       (dx/element :definitions {})))

(defn resource-xml?
  "Is the file path a resource xml?"
  [file-path]
  (re-find #"META-INF\/rest-definitions\/.*\.xml$" file-path))

(defn classpath-resource-xmls!
  "Return the classpath resource files or throws if none can be found."
  []
  (if-let [res-xmls (seq (into [] (comp (map cp/filenames-in-jar)
                                        (mapcat identity)
                                        (filter resource-xml?))
                               (cp/classpath-jarfiles)))]
    res-xmls
    (throw (ex-info "Cannot find rest resources xml files on the classpath" {}))))

(defn spit-graph-data-edn!
  "Write to file the resource graph data in json"
  [f & [opts]]
  (let [graph-data (xml-files->graph-data (classpath-resource-xmls!))]
    (s/assert* :graph-data/entity graph-data)
    (apply spit f (if-not (:pretty opts)
                    (pr-str graph-data)
                    (with-out-str (fipp/pprint graph-data))) (flatten opts))))

(defn spit-graph-data-json!
  "Write to file the resource graph data in json"
  [f & [opts]]
  (let [graph-data (xml-files->graph-data (classpath-resource-xmls!))]
    (s/assert* :graph-data/entity graph-data)
    (apply spit f (json/encode graph-data opts) (flatten opts))))

(defn spit-family-xml!
  "Emit an aggregate version of the xml definitions

  The xml will have the form:
    
      
      
      
      
      ...
    

  If the opts contains :pretty true, the output will be pretty printed."
  [f & [opts]]
  (spit-xml f (xml-files->graph-data (classpath-resource-xmls!)) opts))

(defn usage [options-summary]
  (->> [""
        "Dump resource data to disk."
        ""
        "Usage: boot extract ...options..."
        ""
        "Options:"
        options-summary]
       (str/join \newline)))

(defn error-msg [errors]
  (str "The following errors occurred while parsing your command:\n\n"
       (str/join \newline errors)))

(defn exit [status msg]
  (if (= status 0)
    (println msg)
    (binding [*out* *err*]
      (println msg)))
  (System/exit status))

(def cli-options
  [["-f" "--family-xml FILE-PATH" "Dumps an xml with all the families defined."]
   ["-g" "--graph-edn FILE-PATH" "Dumps an edn containing the graph data."]
   ["-p" "--pretty" "Enable pretty printing of the data."]
   ["-h" "--help" "Prints out the help"]])

(defn -main [args]
  (let [{:keys [options arguments errors summary]} (cli/parse-opts args cli-options)]
    (cond
      (:help options) (exit 0 (usage summary))
      (:family-xml options) (apply spit-family-xml! (:family-xml options) :pretty (:pretty options))
      (:graph-edn options) (spit-graph-data-edn! (:graph-edn options) {:pretty (:pretty options)})
      errors (exit 1 (error-msg errors))
      :else (exit 1 (usage summary)))))

(comment
  (def file-path "META-INF/rest-definitions/profiles.xml")
  (def xml-root (-> r parse-resource-xml! zip/xml-zip))
  (def root {:tag :root :attrs {} :content (list (->> file-path parse-resource-xml!))})
  (def json-defs (-> root element->map second json/encode))
  (spit-graph-data-json! "data/graph-data.json" {:pretty true})
  (spit-graph-data-edn! "data/graph-data.edn" {:pretty true})
  (def issue-xml (->> (cp/classpath-jarfiles)
                      classpath-resource-xmls!
                      (filter (partial re-find #"shipments-shipping-address"))
                      (into [] (comp (map parse-resource-xml)
                                     (map descend-to-family)
                                     #_(map node->clj)))
                      first))
  (def relationship #clojure.data.xml.node.Element{:tag :relationship, :attrs {}, :content (#clojure.data.xml.node.Element{:tag :name, :attrs {}, :content ("default-wishlist-from-root")} #clojure.data.xml.node.Element{:tag :description, :attrs {}, :content ("Link from root resource to default wishlist.")} #clojure.data.xml.node.Element{:tag :rel, :attrs {}, :content ("defaultwishlist")} #clojure.data.xml.node.Element{:tag :from, :attrs {}, :content ("base.root")} #clojure.data.xml.node.Element{:tag :to, :attrs {}, :content ("default-wishlist")})})
  (def entity #clojure.data.xml.node.Element{:tag :entity, :attrs {}, :content (#clojure.data.xml.node.Element{:tag :name, :attrs {}, :content ("line-item")} #clojure.data.xml.node.Element{:tag :description, :attrs {}, :content ("A line item in a cart.")} #clojure.data.xml.node.Element{:tag :property, :attrs {}, :content (#clojure.data.xml.node.Element{:tag :name, :attrs {}, :content ("quantity")} #clojure.data.xml.node.Element{:tag :description, :attrs {}, :content ("The total number of items in the line item.")} #clojure.data.xml.node.Element{:tag :integer, :attrs {}, :content ()})} #clojure.data.xml.node.Element{:tag :property, :attrs {}, :content (#clojure.data.xml.node.Element{:tag :name, :attrs {}, :content ("line-item-id")} #clojure.data.xml.node.Element{:tag :description, :attrs {}, :content ("The internal line item identifier.")} #clojure.data.xml.node.Element{:tag :internal, :attrs {}, :content ()} #clojure.data.xml.node.Element{:tag :string, :attrs {}, :content ()})} #clojure.data.xml.node.Element{:tag :property, :attrs {}, :content (#clojure.data.xml.node.Element{:tag :name, :attrs {}, :content ("item-id")} #clojure.data.xml.node.Element{:tag :description, :attrs {}, :content ("The internal item identifier.")} #clojure.data.xml.node.Element{:tag :internal, :attrs {}, :content ()} #clojure.data.xml.node.Element{:tag :string, :attrs {}, :content ()})} #clojure.data.xml.node.Element{:tag :property, :attrs {}, :content (#clojure.data.xml.node.Element{:tag :name, :attrs {}, :content ("cart-id")} #clojure.data.xml.node.Element{:tag :description, :attrs {}, :content ("The internal cart identifier.")} #clojure.data.xml.node.Element{:tag :internal, :attrs {}, :content ()} #clojure.data.xml.node.Element{:tag :string, :attrs {}, :content ()})} #clojure.data.xml.node.Element{:tag :property, :attrs {}, :content (#clojure.data.xml.node.Element{:tag :name, :attrs {}, :content ("configuration")} #clojure.data.xml.node.Element{:tag :description, :attrs {}, :content ("The details of the line item configuration.")} #clojure.data.xml.node.Element{:tag :is-a, :attrs {}, :content ("line-item-configuration")})})})
  )




© 2015 - 2025 Weber Informatics LLC | Privacy Policy