diff --git a/.cljfmt.edn b/.cljfmt.edn new file mode 100644 index 00000000..e5b1b4ee --- /dev/null +++ b/.cljfmt.edn @@ -0,0 +1,5 @@ +{:indents {k16.gx.beta.context/merge-err-ctx [[:inner 0]] + merge-err-ctx [[:inner 0]] + + k16.gx.beta.context/with-ctx [[:inner 0]] + with-ctx [[:inner 0]]}} \ No newline at end of file diff --git a/src/k16/gx/beta/context.cljc b/src/k16/gx/beta/context.cljc new file mode 100644 index 00000000..10b1b756 --- /dev/null +++ b/src/k16/gx/beta/context.cljc @@ -0,0 +1,33 @@ +(ns k16.gx.beta.context) + +(def ^:dynamic *runtime* + "GX's execution runtime, contains: + - :err is used for creating/throwing exceptions with contextual data + - :context contains gx context with configurations" + {:err {:error-type :general} + :context {}}) + +(defmacro merge-err-ctx + "Creates error context by merging value to :err in `k16.gx.beta.error/*runtime*`" + [err & body] + `(binding [~`*runtime* (update ~`*runtime* :err merge ~err)] + ~@body)) + +(defmacro with-ctx + "Takes map with two keys :context and :err. Creates execution context" + [ctx & body] + `(binding [~`*runtime* (merge ~`*runtime* ~ctx)] + ~@body)) + +(defn err [] + (get *runtime* :err)) + +(defn context [] + (get *runtime* :context)) + +(comment + (with-ctx {:context {:foo 1}} + *runtime*) + (merge-err-ctx {:node-key :foo} + (err)) + ) diff --git a/src/k16/gx/beta/core.cljc b/src/k16/gx/beta/core.cljc index e0f2f570..6acce7d9 100644 --- a/src/k16/gx/beta/core.cljc +++ b/src/k16/gx/beta/core.cljc @@ -1,52 +1,17 @@ (ns k16.gx.beta.core (:refer-clojure :exclude [ref]) - #?(:cljs (:require-macros [k16.gx.beta.error-context :refer [with-err-ctx]])) - (:require [clojure.walk :as walk] - [malli.core :as m] + #?(:cljs (:require-macros [k16.gx.beta.context])) + (:require [malli.core :as m] [malli.error :as me] [promesa.core :as p] + [k16.gx.beta.normalize :as gx.norm] [k16.gx.beta.impl :as impl] [k16.gx.beta.schema :as gx.schema] [k16.gx.beta.errors :as gx.err] - #?(:clj [k16.gx.beta.error-context :refer [with-err-ctx]])) + [k16.gx.beta.context :refer [merge-err-ctx] :as gx.ctx]) (:import #?(:clj [clojure.lang ExceptionInfo]))) -(def locals #{'gx/ref 'gx/ref-keys}) - -(defn local-form? - [form] - (and (seq? form) - (locals (first form)))) - -(def default-context - {:initial-state :uninitialised - :normalize {;; signal, whish is default for static component nodes - :auto-signal :gx/start - :props-signals #{:gx/start}} - :signal-mapping {} - :signals {:gx/start {:from-states #{:stopped :uninitialised} - :to-state :started} - :gx/stop {:from-states #{:started} - :to-state :stopped - ;; this is used as a sign of anti-signal and aplies - ;; it in reversed order - :deps-from :gx/start}}}) - -#?(:clj - (defn quiet-requiring-resolve - [sym] - (try - (requiring-resolve sym) - (catch Throwable _ nil)))) - -(defn resolve-symbol - [sym] - (when (symbol? sym) - #?(:cljs (impl/namespace-symbol sym) - :clj (some-> sym - (impl/namespace-symbol) - (quiet-requiring-resolve) - (var-get))))) +(def default-context gx.norm/default-context) (defn ref [key] @@ -56,215 +21,6 @@ [& keys] (apply list (conj keys 'gx/ref-keys))) -(defn parse-local - [env form] - (condp = (first form) - 'gx/ref (get env (second form)) - - 'gx/ref-keys (select-keys env (second form)))) - -(defn postwalk-evaluate - "A postwalk runtime signal processor evaluator, works most of the time. - Doesn't support special symbols and macros, basically just function application. - For cljs, consider compiled components or sci-evaluator, would require allowing - for swappable evaluation stategies. Point to docs, to inform how to swap evaluator, - or alternative ways to specify functions (that get compiled) that can be used." - [props form] - (walk/postwalk - (fn [x] - (cond - (local-form? x) - (parse-local props x) - - (and (seq? x) (ifn? (first x))) - (apply (first x) (rest x)) - - :else x)) - form)) - -(defn form->runnable [form-def] - (let [props* (atom #{}) - resolved-form - (->> form-def - (walk/postwalk - (fn [sub-form] - (cond - (locals sub-form) sub-form - - (local-form? sub-form) - (do (swap! props* concat (-> sub-form rest flatten)) - sub-form) - - (special-symbol? sub-form) - (gx.err/throw-gx-err "Special forms are not supported" - {:form-def form-def - :token sub-form}) - - (resolve-symbol sub-form) (resolve-symbol sub-form) - - (symbol? sub-form) - (gx.err/throw-gx-err "Unable to resolve symbol" - {:form-def form-def - :token sub-form}) - - :else sub-form))))] - {:env @props* - :form resolved-form})) - -(defn normalize-signal-def [signal-def] - (let [;; is this map a map based def, or a runnable form - def? (and (map? signal-def) - (some #{:gx/props :gx/props-fn - :gx/processor :gx/deps - :gx/resolved-props} - (keys signal-def))) - with-pushed-down-form - (if def? - signal-def - (let [{:keys [form env]} (form->runnable signal-def)] - {:gx/processor (fn auto-signal-processor [{:keys [props]}] - (postwalk-evaluate props form)) - :gx/deps env - :gx/resolved-props (->> env - (map (fn [dep] - [dep (list 'gx/ref dep)])) - (into {}))})) - resolved-props-fn (some-> with-pushed-down-form - :gx/props-fn - (resolve-symbol)) - with-resolved-props - (if (:gx/resolved-props with-pushed-down-form) - with-pushed-down-form - (let [{:keys [form env]} (form->runnable - (:gx/props with-pushed-down-form))] - (merge with-pushed-down-form - {:gx/resolved-props form - :gx/resolved-props-fn resolved-props-fn - :gx/deps env})))] - with-resolved-props)) - -(defn push-down-props - [{{:keys [props-signals]} :normalize} {:gx/keys [props] :as node-def}] - (if (and (seq props) (seq props-signals)) - (reduce-kv (fn [m k v] - (if (and (contains? props-signals k) - (not (:gx/props v))) - (assoc-in m [k :gx/props] props) - m)) - node-def - node-def) - node-def)) - -(defn remap-signals - [from-signals to-signals] - (cond - (and (seq from-signals) (seq to-signals)) - (if from-signals - (->> to-signals - (map (fn [[k v]] - [k (v from-signals)])) - (into {})) - to-signals) - - (seq from-signals) from-signals - - :else to-signals)) - -(defn flatten-component - "Flattens nested components by creating one root component using - signal mappings from context (if any)" - [context root-component] - (let [root-component (assoc root-component - :gx/signal-mapping - (or - (:gx/signal-mapping root-component) - (:signal-mapping context)))] - (loop [{:gx/keys [component signal-mapping] :as current} root-component] - (if-let [nested component] - (recur (update nested :gx/signal-mapping - #(remap-signals % signal-mapping))) - (if-let [mapping (seq (:gx/signal-mapping current))] - (->> mapping - (map (fn [[k v]] - [k (get current v)])) - (into root-component)) - (dissoc current :gx/signal-mapping)))))) - -(defn resolve-component - "Resolve component by it's symbol and validate against malli schema" - [context component] - (when component - (with-err-ctx {:error-type :normalize-node-component} - (let [resolved (some->> component - (resolve-symbol) - (flatten-component context)) - [issues schema] (when resolved - (gx.schema/validate-component context resolved))] - (cond - (not resolved) - (gx.err/throw-gx-err "Component could not be resolved" - {:component component}) - - issues - (gx.err/throw-gx-err "Component schema error" - {:component resolved - :component-schema schema - :schema-error (set issues)}) - - :else resolved))))) - -(defn normalize-node-def - "Given a component definition, " - [{:keys [context initial-graph]} node-key node-definition] - (if (:gx/normalized? node-definition) - node-definition - (with-err-ctx {:error-type :normalize-node - :node-key node-key - :node-contents (node-key initial-graph)} - (let [{:keys [initial-state]} context - {:keys [auto-signal]} (:normalize context) - ;; set of signals defined in the graph - signals (set (keys (:signals context))) - ;; is this map a map based def, or a runnable form - def? (and (map? node-definition) - (some (into #{} (conj signals :gx/component)) - (keys node-definition))) - with-pushed-down-form - (if def? - node-definition - (->> (disj signals auto-signal) - (map (fn [other-signal] - [other-signal - {;; :value just passes value and - ;; supports state transitions of - ;; auto-components for all other signals - :gx/processor :value}])) - (into {auto-signal node-definition}))) - component (some->> with-pushed-down-form - :gx/component - (resolve-component context)) - ;; merge in component - with-component (impl/deep-merge - component (dissoc with-pushed-down-form - :gx/component)) - normalized-def (merge - (push-down-props context with-component) - {:gx/state initial-state - :gx/value nil}) - - signal-defs (select-keys normalized-def signals) - normalised-signal-defs - (->> signal-defs - (map (fn [[signal-key signal-def]] - [signal-key (normalize-signal-def signal-def)])) - (into {}))] - (merge normalized-def - normalised-signal-defs - ;; Useful information, but lets consider semantics before - ;; using the value to determine behaviour - {:gx/type (if def? :component :static) - :gx/normalized? true}))))) - (defn signal-dependencies [{:keys [signals]}] (->> signals @@ -289,7 +45,9 @@ "Given a graph definition and config, return a normalised form. Idempotent. This acts as the static analysis step of the graph. Returns tuple of error explanation (if any) and normamized graph." - [{:keys [context graph] :or {context default-context} :as gx-map}] + [{:keys [context graph] + :or {context default-context} + :as gx-map}] (let [config-issues (validate-context context) gx-map (assoc gx-map :context context) ;; remove previous normalization errors @@ -297,13 +55,9 @@ (not (:initial-graph gx-map)) (assoc :initial-graph graph) :always (dissoc :failures))] (try - (cond - config-issues (throw (ex-info "GX Context error" config-issues)) - :else (->> graph - (map (fn [[k v]] - [k (normalize-node-def gx-map' k v)])) - (into {}) - (assoc gx-map' :graph))) + (if config-issues + (throw (ex-info "GX Context error" config-issues)) + (gx.norm/normalize-graph gx-map')) (catch ExceptionInfo e (update gx-map' :failures conj (gx.err/ex->gx-err-data e)))))) @@ -322,7 +76,7 @@ ([gx-map signal-key] (topo-sort gx-map signal-key #{})) ([{:keys [context graph]} signal-key priority-selector] - (with-err-ctx {:error-type :deps-sort :signal-key signal-key} + (merge-err-ctx {:error-type :deps-sort :signal-key signal-key} (try (if-let [signal-config (get-in context [:signals signal-key])] (let [deps-from (or (:deps-from signal-config) signal-key) @@ -380,7 +134,7 @@ (defn props-validate-error [schema props] (when-let [error (and schema (m/explain schema props))] - (with-err-ctx {:error-type :props-validation} + (merge-err-ctx {:error-type :props-validation} (gx.err/gx-err-data "Props validation error" {:props-value props :props-schema schema @@ -405,7 +159,7 @@ #?(:cljs (defn- wrap-error-cljs [e arg-map err-ctx] - (with-err-ctx err-ctx + (merge-err-ctx err-ctx (wrap-error e arg-map)))) #?(:clj @@ -436,9 +190,11 @@ Static nodes just recalculates its values. If node does not support signal then do nothing." [{:keys [context graph initial-graph]} node-key signal-key] - (let [signal-config (-> context :signals signal-key) + (let [evaluate-fn (-> context :normalize :form-evaluator) + signal-config (-> context :signals signal-key) {:keys [deps-from from-states to-state]} signal-config - node (get graph node-key) + node-def (get graph node-key) + node (gx.norm/normalize-node context node-def) node-state (:gx/state node) signal-def (get node signal-key) {:gx/keys [processor props-schema resolved-props]} signal-def @@ -448,19 +204,13 @@ (if (and deps-from (not resolved-props)) (get node deps-from) signal-def) - ;; _ (validate-signal graph node-key signal-key graph-config) - ;; - ;; :deps-from is ignored if component have :props - ;; props (if (and (not props) deps-from) - ;; (-> node deps-from :gx/props) - ;; props) dep-nodes (select-keys graph deps) dep-nodes-vals (system-value {:graph dep-nodes}) failed-dep-node-keys (->> {:graph dep-nodes} (system-failure) (filter second) (map first))] - (with-err-ctx {:node-contents (node-key initial-graph)} + (merge-err-ctx {:node-contents (node-key initial-graph)} (cond (or ;; signal isn't defined for this state transition (not (contains? from-states node-state)) @@ -475,11 +225,11 @@ (ifn? processor) ;; Binding vars is not passed to nested async code ;; Workaround for CLJS: propagating error context manually - (let [err-ctx gx.err/*err-ctx*] + (let [err-ctx (gx.ctx/err)] (p/let [props-result (if (fn? resolved-props-fn) (run-props-fn resolved-props-fn dep-nodes-vals) - (postwalk-evaluate dep-nodes-vals resolved-props)) - validate-error (with-err-ctx err-ctx + (evaluate-fn dep-nodes-vals resolved-props)) + validate-error (merge-err-ctx err-ctx (props-validate-error props-schema props-result)) [error result] (when-not validate-error (run-processor @@ -517,11 +267,22 @@ (cond (seq sorted) (p/let [node-key (first sorted) - node (with-err-ctx {:error-type :node-signal - :signal-key signal-key - :node-key node-key} + node (merge-err-ctx {:error-type :node-signal + :signal-key signal-key + :node-key node-key} (node-signal gxm node-key signal-key)) next-gxm (assoc-in gxm [:graph node-key] node)] (p/recur (merge-node-failure next-gxm node) (rest sorted))) :else gxm)))))) + +(comment + (def graph {:a {:nested-a 1}, + :z '(get (gx/ref :a) :nested-a), + :y '(println "starting"), + :b #:gx{:start '(+ (gx/ref :z) 2), :stop '(println "stopping")}, + :c #:gx{:component 'k16.gx.beta.core-test/test-component}, + :x #:gx{:component 'k16.gx.beta.core-test/test-component-2}}) + + (normalize {:context default-context + :graph graph})) diff --git a/src/k16/gx/beta/error_context.clj b/src/k16/gx/beta/error_context.clj deleted file mode 100644 index be586fc5..00000000 --- a/src/k16/gx/beta/error_context.clj +++ /dev/null @@ -1,8 +0,0 @@ -(ns k16.gx.beta.error-context) - -(defmacro with-err-ctx - "Creates error context by merging value to `k16.gx.beta.error/*err-ctx*`" - [ctx & body] - `(binding [~'k16.gx.beta.errors/*err-ctx* - (merge ~'k16.gx.beta.errors/*err-ctx* ~ctx)] - ~@body)) diff --git a/src/k16/gx/beta/errors.cljc b/src/k16/gx/beta/errors.cljc index f696714e..4dc150b4 100644 --- a/src/k16/gx/beta/errors.cljc +++ b/src/k16/gx/beta/errors.cljc @@ -1,16 +1,11 @@ -(ns k16.gx.beta.errors) - -(defrecord ErrorContext [error-type node-key node-contents signal-key]) - -(def ^:dynamic *err-ctx* - "Error context is used for creating/throwing exceptions with contextual data" - (map->ErrorContext {:error-type :general})) +(ns k16.gx.beta.errors + (:require [k16.gx.beta.context :as gx.context])) (defn gx-err-data ([internal-data] (gx-err-data nil internal-data)) ([message internal-data] - (->> *err-ctx* + (->> (gx.context/err) (filter (fn [[_ v]] v)) (into (if message {:message message} {})) (merge {:internal-data internal-data})))) @@ -24,7 +19,7 @@ (defn ex->gx-err-data [ex] (->> (ex-data ex) - (merge *err-ctx*) + (merge (gx.context/err)) (filter (fn [[_ v]] v)) (into {:message (ex-message ex)}))) diff --git a/src/k16/gx/beta/normalize.cljc b/src/k16/gx/beta/normalize.cljc new file mode 100644 index 00000000..6f9637b5 --- /dev/null +++ b/src/k16/gx/beta/normalize.cljc @@ -0,0 +1,481 @@ +(ns k16.gx.beta.normalize + #?(:cljs (:require-macros + [k16.gx.beta.context :refer [merge-err-ctx]])) + (:require [clojure.walk :as walk] + [k16.gx.beta.errors :as gx.err] + [k16.gx.beta.impl :as impl] + [k16.gx.beta.schema :as gx.schema] + #?(:clj [k16.gx.beta.context :refer [merge-err-ctx]]) + [clojure.set :as set])) + +(def locals #{'gx/ref 'gx/ref-keys}) + +(defn local-form? + [form] + (and (seq? form) + (locals (first form)))) + +#?(:clj + (defn quiet-requiring-resolve + [sym] + (try + (requiring-resolve sym) + (catch Throwable _ nil)))) + +(defn resolve-symbol + [sym] + (when (symbol? sym) + #?(:cljs (impl/namespace-symbol sym) + :clj (some-> sym + (impl/namespace-symbol) + (quiet-requiring-resolve) + (var-get))))) + +(defn parse-local + [env form] + (condp = (first form) + 'gx/ref (get env (second form)) + + 'gx/ref-keys (select-keys env (second form)))) + +(defn postwalk-evaluate + "A postwalk runtime signal processor evaluator, works most of the time. + Doesn't support special symbols and macros, basically just function application. + For cljs, consider compiled components or sci-evaluator, would require allowing + for swappable evaluation stategies. Point to docs, to inform how to swap evaluator, + or alternative ways to specify functions (that get compiled) that can be used." + ([props form] + (postwalk-evaluate props form true)) + ([props form parse-locals?] + (walk/postwalk + (fn [x] + (cond + (local-form? x) + (if parse-locals? + (parse-local props x) + x) + + (and (seq? x) (ifn? (first x))) + (apply (first x) (rest x)) + + :else x)) + form))) + +(def default-context + {:initial-state :uninitialised + :normalize {:form-evaluator postwalk-evaluate + ;; signal, whish is default for auto component nodes + :auto-signal :gx/start + :props-signals #{:gx/start}} + :signal-mapping {} + :signals {:gx/start {:from-states #{:stopped :uninitialised} + :to-state :started} + :gx/stop {:from-states #{:started} + :to-state :stopped + ;; this is used as a sign of anti-signal and aplies + ;; it in reversed order + :deps-from :gx/start}}}) + +(defprotocol IRunnableForm + (parse [this]) + (run [this] [this props] [this props parse-locals?])) + +(defrecord RunnableForm [context env form] + IRunnableForm + (parse [_this] + ((-> context :normalize :form-evaluator) nil form false)) + (run [_this props] + ((-> context :normalize :form-evaluator) props form))) + +(defn form->runnable + ([context form-def] + (form->runnable context form-def false)) + ([context form-def quiet?] + (let [props* (atom #{}) + resolved-form + (->> form-def + (walk/postwalk + (fn [sub-form] + (cond + (locals sub-form) sub-form + + (local-form? sub-form) + (do (swap! props* concat (-> sub-form rest flatten)) + sub-form) + + (special-symbol? sub-form) + (gx.err/throw-gx-err "Special forms are not supported" + {:form-def form-def + :token sub-form}) + + (resolve-symbol sub-form) (resolve-symbol sub-form) + + (and quiet? (symbol? sub-form)) nil + + (symbol? sub-form) + (gx.err/throw-gx-err "Unable to resolve symbol" + {:form-def form-def + :token sub-form}) + + :else sub-form))))] + (->RunnableForm context @props* resolved-form)))) + +(defn quiet-form->runnable + [context form-def] + (form->runnable context form-def true)) + +(defn empty-node-instance + [context] + {:gx/value nil + :gx/state (-> context :initial-state)}) + +(defn deps->resolved-props [deps] + (->> deps + (map (fn [dep] [dep (list 'gx/ref dep)])) + (into {}))) + +(defn form->signal-def [context form] + (let [runnable (form->runnable context form) + processor (fn auto-signal-processor [{:keys [props]}] + (run runnable props)) + deps (:env runnable) + normalized-signal {:gx/processor processor + :gx/deps deps}] + normalized-signal)) + +(defn normalize-signal [context signal-def] + (let [processor-defined? (:gx/processor signal-def) + partial-signal (if processor-defined? + signal-def + (form->signal-def context signal-def)) + + {:gx/keys [deps processor resolved-props] + :or {deps #{}}} + partial-signal + + resolved-props (or resolved-props (deps->resolved-props deps))] + (merge + (when processor-defined? + signal-def) + {:gx/processor processor + :gx/deps deps + :gx/resolved-props resolved-props}))) + +(comment + (def s1 + (normalize-signal + default-context + {:gx/processor (fn [{:keys [props]}] props)})) + ((:gx/processor s1) {:props {:a 1}})) + +(defn normalize-sm-auto [context sm-def] + (let [normalized-signal (normalize-signal context sm-def) + auto-signal (-> context :normalize :auto-signal) + other-signals (-> context :signals keys set (disj auto-signal))] + (->> other-signals + (map (fn [other-signal] [other-signal {:gx/processor :value + :gx/deps #{} + :gx/resolved-props {}}])) + (into {auto-signal normalized-signal}) + (merge (empty-node-instance context))))) + +(comment + (->> {:port 8080} + (normalize-sm-auto default-context)) + + (->> {:port '(gx/ref :z)} + (normalize-sm-auto default-context))) + +(defn init-props + [context component-def] + (update-vals + component-def + (fn [signal-def] + (if (and (map? signal-def) + (some #{:gx/props :gx/props-fn} (keys signal-def))) + (let [{:gx/keys [props props-fn]} signal-def + parsed-props (some->> props (form->runnable context)) + parsed-props-fn (some->> props-fn (form->runnable context))] + (merge signal-def + {:gx/deps (set (or (:env parsed-props) [])) + :gx/resolved-props (:form parsed-props) + :gx/resolved-props-fn (:form parsed-props-fn)})) + signal-def)))) + +(comment + (init-props default-context + {:gx/start {:gx/props-fn 'k16.gx.beta.normalize/my-props-fn + :gx/props '{:a (gx/ref :b)}} + :gx/value {}})) + +(defn remap-signals + [from-signals to-signals] + (cond + (and (seq from-signals) (seq to-signals)) + (if from-signals + (->> to-signals + (map (fn [[k v]] + [k (v from-signals)])) + (into {})) + to-signals) + + (seq from-signals) from-signals + + :else to-signals)) + +(defn flatten-component + "Flattens nested components by creating one root component using + signal mappings from context (if any)" + [context root-component] + (let [root-component (assoc root-component + :gx/signal-mapping + (or + (:gx/signal-mapping root-component) + (:signal-mapping context)))] + (loop [{:gx/keys [component signal-mapping] :as current} root-component] + (if-let [nested component] + (recur (update nested :gx/signal-mapping + #(remap-signals % signal-mapping))) + (if-let [mapping (seq (:gx/signal-mapping current))] + (->> mapping + (map (fn [[k v]] + [k (get current v)])) + (into root-component)) + (dissoc current :gx/signal-mapping)))))) + +(defn push-down-props + [{{:keys [props-signals]} :normalize} {:gx/keys [props] :as node-def}] + (if (and (seq props) (seq props-signals)) + (reduce-kv (fn [m k v] + (if (and (contains? props-signals k) + (not (:gx/props v))) + (assoc-in m [k :gx/props] props) + m)) + node-def + node-def) + node-def)) + +(defn normalize-sm-with-component [context sm-def] + (merge-err-ctx {:error-type :normalize-node-component} + (let [component-def (:gx/component sm-def) + parsed-component (some->> component-def + (quiet-form->runnable context) + (parse) + (flatten-component context)) + [issues schema component] + (some->> parsed-component + (impl/deep-merge sm-def) + (merge (empty-node-instance context)) + (push-down-props context) + (init-props context) + (gx.schema/validate-component context))] + (cond + (not component) + (gx.err/throw-gx-err "Component could not be resolved" + {:component component-def}) + + (seq issues) + (gx.err/throw-gx-err "Component schema error" + {:component parsed-component + :component-schema schema + :schema-error (set issues)}) + + :else component)))) + +(comment + (defn ^:export my-props-fn + [{:keys [a]}] + (assoc a :full-name + (str (:name a) " " (:last-name a)))) + + (def ^:export my-new-component + {:gx/start {:gx/props '(gx/ref :a) + :gx/processor + (fn my-new-component-handler + [{:keys [props]}] + (atom props))}}) + + (try + (normalize-node + default-context + [:comp {:gx/component 'k16.gx.beta.normalize/my-new-component + :gx/start {:gx/props-fn 'k16.gx.beta.normalize/my-props-fn + :gx/props '(gx/ref :a)}}]) + (catch clojure.lang.ExceptionInfo e + (ex-data e))) + + (sm-def-type + default-context + {:gx/component 'k16.gx.beta.normalize/my-new-component})) + +(defn normalize-sm-inline [context sm-def] + (let [signals-only (select-keys sm-def (-> context :signals keys))] + (merge (empty-node-instance context) + sm-def + (update-vals signals-only (partial normalize-signal context))))) + +(defn context->defined-signals [context] + (into #{} + (concat + (keys (:signals context)) + (vals (:signal-mapping context))))) + +(defn normal-sm-def? [context sm-def] + (and (map? sm-def) + (let [sm-valid-keys (into #{:gx/component} + (context->defined-signals context)) + sm-def-keys (keys sm-def)] + (some sm-valid-keys sm-def-keys)))) + +(declare normalize-sm) + +(defn normalize-component-in-context [context normal-sm-def] + (tap> {:direction :down + :node (:node normal-sm-def) + :def normal-sm-def + :context context + :c-signals (:signals context) + :c-signal-mapping (:signal-mapping context)}) + + (let [{:gx/keys [component signal-mapping]} normal-sm-def + + signal-mapping (set/rename-keys + (:signal-mapping context) + signal-mapping) + + ;; collect and normalise component if once exists + ;; recursively calls normalize-sm + component + (normalize-sm + (merge + context + {:signals (-> (:signals context) + (set/rename-keys (:gx/signal-mapping component))) + :signal-mapping signal-mapping}) + component) + + ;; dissoc the empty node keys so that coming + ;; back on the normalise step doesnt include them + mergeable-component + (let [comp-min (apply dissoc component (keys (empty-node-instance context))) + comp-comp (:gx/component comp-min) + comp-signals (dissoc comp-min :gx/component)] + (merge + (when comp-comp + {:gx/component comp-comp}) + (set/rename-keys comp-signals (set/map-invert signal-mapping))))] + + (tap> {:direction :up + :node (:node normal-sm-def) + :def normal-sm-def + :context context + :c-signals (:signals context) + :c-signal-mapping (:signal-mapping context) + + :inner {:context (merge context + {:signals (-> (:signals context) + (set/rename-keys (:gx/signal-mapping component))) + :signal-mapping signal-mapping}) + :component component} + :to-merge mergeable-component}) + + {:component component + :mergable-component mergeable-component})) + +(defn ensure-resolved [ref] + (cond + (symbol? ref) (resolve-symbol ref) + :else ref)) + +(defn normalize-sm [context sm-def] + (let [;; is this sm-def in normal form + normal? (normal-sm-def? context sm-def) + + normal-sm (if normal? + sm-def + (normalize-sm-auto context sm-def)) + + {:keys [component mergeable-component]} + (when (:gx/component normal-sm) + (normalize-component-in-context + context + (update normal-sm :gx/component ensure-resolved))) + + ;; merge component + sm-with-component (impl/deep-merge + mergeable-component + normal-sm) + + ;; top-level-props (:gx/props sm-with-component) + + ;; select signal definitions + signal-defs (select-keys sm-with-component (context->defined-signals context)) + ;; normalise signal definitions + normalized-signals (update-vals signal-defs #(normalize-signal context %)) + + sm (impl/deep-merge + mergeable-component + normalized-signals)] + + (merge + (empty-node-instance context) + ;; include the component heritage + (when component + {:gx/component component}) + ;; sm-with-component + sm))) + +(comment + (normalize-sm default-context 3) + (-> (normalize-sm default-context {:gx/component {:gx/start {:gx/processor (fn [{:keys [props]}] props)}}}) + :gx/start :gx/processor + (apply [{:props 5}]))) + +(defn- sm-def-type + [context sm-def] + (cond + ;; (:gx/normalized? sm-def) ::normalized-sm + + (normal-sm-def? context sm-def) + (let [{:gx/keys [component]} sm-def] + (cond + (nil? component) ::inline-sm + (symbol? component) ::component-def + ;; (function-call? component) ::component-constructor + :else ::unsupported-component)) + + :else ::auto-sm)) + +(defmulti normalize-sm' sm-def-type) + +(defmethod normalize-sm' ::component-def + [context sm-def] + (normalize-sm-with-component context sm-def)) + +(defmethod normalize-sm' ::inline-sm + [context sm-def] + (normalize-sm-inline context sm-def)) + +(defmethod normalize-sm' ::auto-sm + [context sm-def] + (normalize-sm-auto context sm-def)) + +(defmethod normalize-sm' ::normalized-sm + [_context node-def] + node-def) + +(defn normalize-node + [context sm-def] + (merge-err-ctx {:error-type :normalize-node + :node-contents sm-def} + (normalize-sm' context sm-def))) + +(defn normalize-graph + [{:keys [context graph] :as gx-map}] + (merge-err-ctx {:error-type :normalize-node} + (let [normalized (->> graph + (map (fn [[k sm-def]] + (merge-err-ctx {:node-key k} + [k (normalize-node context sm-def)]))) + (into {}))] + (assoc gx-map :graph normalized)))) diff --git a/src/k16/gx/beta/schema.cljc b/src/k16/gx/beta/schema.cljc index f7ff484a..63af4f69 100644 --- a/src/k16/gx/beta/schema.cljc +++ b/src/k16/gx/beta/schema.cljc @@ -17,6 +17,7 @@ [:signal-mapping {:optional true} [:map-of keyword? keyword?]] [:normalize [:map + [:form-evaluator fn?] [:auto-signal keyword?] [:props-signals [:set keyword?]]]] [:signals @@ -36,10 +37,11 @@ [:map gx-props [:gx/processor ifn?] - [:gx/props-schema {:optional true} any?] + [:gx/props-schema {:optional true} some?] + [:gx/props-fn {:optional true} some?] [:gx/resolved-props-fn {:optional true} [:maybe fn?]] [:gx/deps {:optional true} coll?] - [:gx/resolved-props {:optional true} [:maybe any?]]]) + [:gx/resolved-props {:optional true} [:maybe some?]]]) (def ?NormalizedNodeDefinition [:map @@ -53,7 +55,7 @@ [:gx/normalized? {:optional true} boolean?] [:gx/value {:optional true} any?]]) -(defn create-component-schema +(defn normalized-node-schema [context] (let [signals (->> context :signals @@ -64,16 +66,21 @@ (mu/closed-schema (mu/merge ?NormalizedNodeDefinition signals)))) +(defn normalized? + [context component] + (m/validate (normalized-node-schema context) component)) + (defn validate-component [context component] - (let [schema (create-component-schema context)] + (let [schema (normalized-node-schema context)] [(->> component (m/explain schema) (me/humanize)) - (m/-form schema)])) + (m/-form schema) + component])) (defn validate-graph [{:keys [graph context]}] - (let [graph-schema [:map-of keyword? (create-component-schema context)]] + (let [graph-schema [:map-of keyword? (normalized-node-schema context)]] (me/humanize (m/explain graph-schema graph)))) diff --git a/test/k16/gx/beta/core_test.cljc b/test/k16/gx/beta/core_test.cljc index f3493a1d..0f8c37aa 100644 --- a/test/k16/gx/beta/core_test.cljc +++ b/test/k16/gx/beta/core_test.cljc @@ -2,6 +2,8 @@ (:require [k16.gx.beta.core :as gx] [k16.gx.beta.registry :as gx.reg :include-macros true] [k16.gx.beta.schema :as gx.schema] + [clojure.data :refer [diff]] + [k16.gx.beta.normalize :as gx.norm] #?(:clj [clojure.test :as t :refer [deftest is testing]]) #?@(:cljs [[cljs.test :as t :refer-macros [deftest is testing]] [promesa.core :as p] @@ -13,7 +15,7 @@ ;; this component is linked in fixtures/graphs.edn (def test-component {:gx/start {:gx/props-schema TestCoponentProps - :gx/props {:a (gx/ref :a)} + :gx/props {:a '(gx/ref :a)} :gx/processor (fn [{:keys [props _value]}] (let [a (:a props)] @@ -22,7 +24,7 @@ (def test-component-2 {:gx/start {:gx/props-schema TestCoponentProps - :gx/props {:a (gx/ref :a)} + :gx/props {:a '(gx/ref :a)} :gx/processor (fn [{:keys [props _value]}] (let [a (:a props)] @@ -32,13 +34,14 @@ (defn load-config [] (gx.reg/load-graph! "test/fixtures/graph.edn")) -(def context gx/default-context) +(def context gx.norm/default-context) (comment (let [graph (load-config) gx-map (gx/normalize {:context context :graph graph})] - (gx.schema/validate-graph gx-map)) + gx-map + #_(gx/signal-sync gx-map :gx/start)) ) (deftest graph-tests @@ -104,7 +107,8 @@ (deftest failed-normalization-test (let [custom-context {:initial-state :uninitialised :normalize {:auto-signal :custom/start - :props-signals #{:custom/start}} + :props-signals #{:custom/start} + :form-evaluator gx.norm/postwalk-evaluate} :signals {:custom/start {:from-states #{:stopped :uninitialized} :to-state :started} @@ -156,19 +160,20 @@ (run-checks gx-started gx-stopped) (done)))))) +(comment + (let [graph {:a {:nested-a 1} + :c {:gx/component 'k16.gx.beta.core-test/test-component}}] + (gx/normalize {:context context + :graph graph}))) + (deftest subsequent-normalizations-test (let [gx-norm-1 (gx/normalize {:context context :graph (load-config)}) gx-norm-2 (gx/normalize gx-norm-1) gx-norm-3 (gx/normalize gx-norm-2)] - (testing "normalization should add :gx/normalized? flag" - (is (= #{true} (set (map :gx/normalized? (vals (:graph gx-norm-1))))))) (testing "all graphs should be equal" - (is (= gx-norm-1 gx-norm-2 gx-norm-3))) - (testing "should normalize and flag new node in graph " - (let [new-gx (assoc-in gx-norm-3 [:graph :new-node] '(* 4 (gx/ref :z))) - new-gx-norm (gx/normalize new-gx)] - (is (:gx/normalized? (:new-node (:graph new-gx-norm)))))))) + (is (= [nil nil] (take 2 (diff gx-norm-1 gx-norm-2)))) + (is (= [nil nil] (take 2 (diff gx-norm-1 gx-norm-3))))))) (deftest dependency-error-test (let [graph {:a (gx/ref :b) @@ -185,12 +190,12 @@ :signal-key :gx/start} failure)))) -(defn my-props-fn +(defn ^:export my-props-fn [{:keys [a]}] (assoc a :full-name (str (:name a) " " (:last-name a)))) -(def my-new-component +(def ^:export my-new-component {:gx/start {:gx/props (gx/ref :a) :gx/processor (fn my-new-component-handler @@ -200,21 +205,25 @@ (deftest props-fn-test (let [run-checks (fn [gx-started] - (is (= @(:comp (gx/system-value gx-started)) - {:name "John" :last-name "Doe" :full-name "John Doe"}))) - graph (gx.reg/load-graph! "test/fixtures/props_fn.edn") - gx-map {:context context :graph graph} + (is (= {:name "John" :last-name "Doe" :full-name "John Doe"} + @(:comp (gx/system-value gx-started))))) + graph {:a {:name "John" + :last-name "Doe"} + :comp + {:gx/component 'k16.gx.beta.core-test/my-new-component + :gx/start {:gx/props-fn 'k16.gx.beta.core-test/my-props-fn}}} + gx-map {:graph graph} started (gx/signal gx-map :gx/start)] #?(:clj (run-checks @started) :cljs (t/async done (p/then started (fn [s] (run-checks s) (done))))))) -(deftest postwalk-evaluate-test +#_(deftest postwalk-evaluate-test (let [env {:http/server {:port 8080} :db/url "jdbc://foo/bar/baz"}] - (t/are [arg result] (= result (gx/postwalk-evaluate env arg)) + (t/are [arg result] (= result (gx/-postwalk-evaluate env arg)) (gx/ref :http/server) {:port 8080} (gx/ref-keys [:http/server :db/url]) {:http/server {:port 8080} @@ -244,8 +253,7 @@ :d '(throw "starting") :b {:gx/start '(+ (gx/ref :z) 2) :gx/stop '(println "stopping")}} - gx-norm (gx/normalize {:graph graph - :context gx/default-context})] + gx-norm (gx/normalize {:graph graph})] (is (= {:error-type :normalize-node, :node-key :d, :node-contents '(throw "starting"), @@ -259,8 +267,7 @@ :d '(println "starting") :b {:gx/start '(+ (gx/ref :z) 2) :gx/stop '(some-not-found-symbol "stopping")}} - gx-norm (gx/normalize {:graph graph - :context gx/default-context}) + gx-norm (gx/normalize {:graph graph}) failure (-> gx-norm :failures first)] (is (= {:error-type :normalize-node, :node-key :b, @@ -280,8 +287,7 @@ :c '(inc :bar) :b {:gx/start '(/ (gx/ref :z) 0) :gx/stop '(println "stopping")}} - gx-norm (gx/normalize {:graph graph - :context gx/default-context}) + gx-norm (gx/normalize {:graph graph}) expect (list {:internal-data {:ex-message "java.lang.ArithmeticException: Divide by zero; Divide by zero", :args {:props {:z 1}, :value nil}}, @@ -354,7 +360,7 @@ :c '(gx/ref :b) :d '(gx/ref :c)} gx-map {:graph graph - :context gx/default-context} + :context gx.norm/default-context} expect (list {:internal-data {:dep-node-keys '(:c)}, :message "Failure in dependencies", :error-type :node-signal, @@ -433,8 +439,7 @@ (deftest component-processor-unresolved-test (testing "should resolve all components during normalization stage" (let [graph {:c {:gx/component 'k16.gx.beta.core-test/non-existent}} - gx-map (gx/normalize {:graph graph - :context context})] + gx-map (gx/normalize {:graph graph})] (is (= {:message "Component could not be resolved", :error-type :normalize-node-component, :node-key :c, @@ -445,8 +450,7 @@ (first (:failures gx-map))))) (let [graph {:c {:gx/component 'k16.gx.beta.core-test/invalid-component}} - gx-map (gx/normalize {:graph graph - :context context})] + gx-map (gx/normalize {:graph graph})] (is (= {:message "Component schema error", :error-type :normalize-node-component, :node-key :c, @@ -461,8 +465,7 @@ (update :internal-data dissoc :component-schema))))) (let [graph {:c {:gx/component 'k16.gx.beta.core-test/invalid-component-2}} - gx-map (gx/normalize {:graph graph - :context context})] + gx-map (gx/normalize {:graph graph})] (is (= {:message "Component schema error", :error-type :normalize-node-component, :node-key :c, @@ -537,7 +540,8 @@ (deftest validate-context-test (let [context {:initial-state :uninitialised :normalize {:auto-signal :gx/start - :props-signals #{:gx/start}} + :props-signals #{:gx/start} + :form-evaluator gx.norm/postwalk-evaluate} :signal-mapping {} :signals {:gx/start {:from-states #{:stopped :uninitialised} :to-state :started diff --git a/test/k16/gx/beta/nested_components_test.cljc b/test/k16/gx/beta/nested_components_test.cljc index 6f4c11a5..8a7cb915 100644 --- a/test/k16/gx/beta/nested_components_test.cljc +++ b/test/k16/gx/beta/nested_components_test.cljc @@ -1,10 +1,11 @@ (ns k16.gx.beta.nested-components-test (:require [k16.gx.beta.core :as gx] + [k16.gx.beta.normalize :as gx.norm] #?(:clj [clojure.test :as t :refer [deftest is]]) #?@(:cljs [[cljs.test :as t :refer-macros [deftest is]]]))) (def ^:export nested-level-2 - {:l2/start {:gx/processor identity} + {:l2/start {:gx/processor (fn [_] :hello-from-nested-level-2)} :l2/stop {:gx/processor identity}}) (def ^:export nested-level-1 @@ -15,28 +16,12 @@ (def ^:export root {:gx/component nested-level-1}) -(comment - (let [context (assoc gx/default-context - :signal-mapping - {:gx/start :l1/start - :gx/stop :l1/stop})] - (gx/flatten-component context root)) - ;; => #:gx{:start #:gx{:processor #function[clojure.core/identity]}, - ;; :stop #:gx{:processor #function[clojure.core/identity]}} -) - (deftest nested-component-resolve-test - (let [resolved (gx/resolve-component (assoc gx/default-context - :signal-mapping - {:gx/start :l1/start - :gx/stop :l1/stop}) - 'k16.gx.beta.nested-components-test/root)] - (is (= #:gx{:component - #:gx{:component - #:l2{:start #:gx{:processor identity}, - :stop #:gx{:processor identity}}, - :signal-mapping #:l1{:start :l2/start, :stop :l2/stop}}, - :signal-mapping #:gx{:start :l1/start, :stop :l1/stop}, - :start #:gx{:processor identity}, - :stop #:gx{:processor identity}} - resolved)))) + (let [resolved (gx.norm/normalize-sm' + (assoc gx/default-context + :signal-mapping + {:gx/start :l1/start + :gx/stop :l1/stop}) + {:gx/component 'k16.gx.beta.nested-components-test/root})] + (is (= :hello-from-nested-level-2 + (-> resolved :gx/start :gx/processor (apply [nil]))))))