ydn/src/ydn/core.clj
Yann Esposito (Yogsototh) 71817230d1
wip to detect cycles
2024-04-09 22:38:00 +02:00

165 lines
4 KiB
Clojure

(ns ydn.core
"ydn provide the mechanism to introduce self-references that are "
(:require [clojure.edn :as edn]
[clojure.string :as string]
[clojure.walk :refer [prewalk]])
(:refer-clojure :exclude [read read-string]))
(def keyword-re-str
":ref([.][^/\\s}]*/|/)[^/.\\s}]*")
(def keyword-re
(re-pattern (str "^" keyword-re-str "$")))
(defn ref-kw
"returns nil if the string does not matches a valid ref keyword format."
[s]
(first (re-matches keyword-re s)))
(defn key-to-path [kw]
(some-> kw
str
(string/replace #"^:ref[./]?" "")
(string/split #"[./]")
(->> (mapv keyword))))
(defn get-in! [m p]
(or (get-in m p)
(throw (ex-info (format "self reference not found! (path: %s)" (pr-str p)) {:path p}))))
(defn name-or-str [x]
(if (keyword? x)
(name x)
(str x)))
(defn to-ref
[edn s]
(when (ref-kw s)
(name-or-str
(get-in! edn (key-to-path s)))))
(def interpolation-re
(re-pattern
(str "\\$\\{(" keyword-re-str ")\\}")))
(defn apply-interpolation
[s edn]
(string/replace s
interpolation-re
(fn [m] (or (to-ref edn (second m))
(first m)))))
(defn root-namespace [kw]
(some-> kw namespace (string/split #"[.]") first))
(defn apply-self-ref
[val edn]
(cond
(and (keyword? val)
(contains? #{"ref"} (root-namespace val)))
(get-in! edn (key-to-path val))
(string? val) (apply-interpolation val edn)
:else val))
(defn apply-self-refs
[edn]
(prewalk #(apply-self-ref % edn) edn))
(defn fix-apply-self-refs
[ydn-fix-point-limit edn]
(reduce #(if (= %1 %2)
(reduced %1)
(if (= ::error %2)
(throw (ex-info
(format "Too many loops! (%d) You probably have a dependency cycle in your config."
ydn-fix-point-limit)
{:latest %1}))
%2))
(concat
(take ydn-fix-point-limit (iterate apply-self-refs edn))
[::error])))
(defn extract-refs
[s]
(let [matcher (re-matcher interpolation-re s)]
(->> (repeatedly #(re-find matcher))
(take-while some?)
(mapv (comp key-to-path second)))))
(comment
(extract-refs "${:ref.pg/scheme}://${:ref.pg/domain}:${:ref.pg/port}")
(extract-refs "${:ref.pg/scheme}")
)
(defn path-to-kw
[p]
(if (> (count p) 1)
(keyword (string/join "." (mapv name (pop p)))
(name (last p)))
(keyword (name (last p)))
))
(defn rec-deps-list
[prefix-path edn]
(assert (vector? prefix-path))
(apply concat
(for [[k v] edn]
(cond
(keyword? v) (when-let [kw (ref-kw (str v))]
[{(path-to-kw (conj prefix-path k))
(keyword (string/replace kw #"^:ref[./]" ""))
}])
(string? v) (let [targets (extract-refs v)]
(for [target targets]
{(path-to-kw (conj prefix-path k))
(path-to-kw target)}))
(map? v) (apply concat (rec-deps-list (conj prefix-path k) v))
:else nil))))
(defn deps-list
[edn]
(assert (map? edn) "the edn config should be an hash-map")
(into {}
(rec-deps-list [] edn)))
(comment
(deps-list {:x :ref/y
:y :ref/x
:foo {:bar :ref/x}
:z "${:ref/x} & ${:ref/y}"})
)
(defn detect-cycles
[g]
:TODO
)
(defn read-string-with-conf
[{:keys [ydn-fix-point-limit] :as _conf} & args]
(fix-apply-self-refs
ydn-fix-point-limit
(apply edn/read-string args)))
(defn read-with-conf
[{:keys [ydn-fix-point-limit] :as _conf} & args]
(fix-apply-self-refs
ydn-fix-point-limit
(apply edn/read args)))
(def default-conf
{:ydn-fix-point-limit 10})
(defn read-string
"like edn/read-string but also apply self-references"
[& args]
(apply read-string-with-conf default-conf args))
(defn read
"like edn/read but also post apply self-references."
[& args]
(apply read-with-conf default-conf args))