From a7d5e4853ef0733b9791b2907a1a3483ccd4ff1d Mon Sep 17 00:00:00 2001 From: SerhiiRI Date: Tue, 21 Oct 2025 09:25:18 +0300 Subject: [PATCH 1/9] - ADDED commando-macro command. Is a some sort of templates for Instruction that can be reused, or usage can be not so pretty, so you decide to packed it in macro. Another way to usege is if you are have a Instruction that must be built in dynamic way with clojure code. - UPDATE QueryDSL. From now it support 3 different type of resolvers: 1. resolve-instruction-qe for commando-resolve instruction 2. resolve-instruction for execution any kind of instruction 3. resolve-fn for function that must be resolved while it being asked by QueryExpression - UPDATE QueryDSL. Better error handling. - UPDATE Registry. Built registry contain also internal commands of Instruction. --- src/commando/commands/builtin.cljc | 369 +++++++++++++++++-- src/commando/commands/query_dsl.cljc | 513 ++++++++++++++++++++++----- src/commando/core.cljc | 2 - src/commando/impl/registry.cljc | 33 +- 4 files changed, 798 insertions(+), 119 deletions(-) diff --git a/src/commando/commands/builtin.cljc b/src/commando/commands/builtin.cljc index 326f4c1..611205e 100644 --- a/src/commando/commands/builtin.cljc +++ b/src/commando/commands/builtin.cljc @@ -1,16 +1,46 @@ (ns commando.commands.builtin (:require + [commando.core :as commando] [commando.impl.dependency :as deps] - [commando.impl.utils :as utils] - [malli.core :as malli] - [malli.error])) + [commando.impl.utils :as utils] + [malli.core :as malli] + [malli.error :as malli-error])) -(def command-fn-spec +;; ====================== +;; Fn +;; ====================== + +(def + ^{:doc " + Description + command-fn-spec - execute `:commando/fn` function/symbol/keyword + with arguments passed inside `:args` key. + + Example + (:instruction + (commando/execute + [command-fn-spec] + {:commando/fn #'clojure.core/+ + :args [1 2 3]})) + ;; => 6 + + (:instruction + (commando/execute + [command-fn-spec] + {:commando/fn (fn [& [a1 a2 a3]] + (* a1 a2 a3)) + :args [1 2 3]})) + ;; => 6 + + See Also + `commando.core/execute` + `commando.commands.builtin/command-apply-spec`"} + command-fn-spec {:type :commando/fn :recognize-fn #(and (map? %) (contains? % :commando/fn)) :validate-params-fn (fn [m] (if-let [m-explain - (malli.error/humanize + (malli-error/humanize (malli/explain [:map [:commando/fn utils/ResolvableFn] @@ -24,12 +54,32 @@ result (apply m-fn m-args)] result)) :dependencies {:mode :all-inside}}) -(def command-apply-spec +;; ====================== +;; Apply +;; ====================== + +(def ^{:doc " + Description + command-apply-spec - Apply `:=` function/symbol/keyword + to value passed inside `:commando/apply` + + Example + (:instruction + (commando/execute + [command-apply-spec] + {:commando/apply {:value 10} + := :value})) + ;; => 10 + + See Also + `commando.core/execute` + `commando.commands.builtin/command-fn-spec`"} + command-apply-spec {:type :commando/apply :recognize-fn #(and (map? %) (contains? % :commando/apply)) :validate-params-fn (fn [m] (if-let [m-explain - (malli.error/humanize + (malli-error/humanize (malli/explain [:map [:commando/apply :any] [:= utils/ResolvableFn]] m))] @@ -41,15 +91,57 @@ result)) :dependencies {:mode :all-inside}}) +;; ====================== +;; From +;; ====================== + +"" + + +(def + ^{:doc " + Description + command-fn-spec - get value from another command or existing value + in Instruction. Path to another command is passed inside `:commando/from` + key, optionally you can apply `:=` function/symbol/keyword to the result. + + Path can be sequence of keywords, strings or integers, starting absolutely from + the root of Instruction, or relatively from the current command position by + using \"../\" and \"./\" strings in paths. + + [:some 2 \"value\"] - absolute path, started from the root key :some + [\"../\" 2 \"value\"] - relative path, go up one level and then down to [2 \"value\"] + + Example + (:instruction + (commando/execute [command-fn-spec command-from-spec] + {\"value\" + {:commando/fn (fn [& values] (apply + values)) + :args [1 2 3]} + \"value-incremented\" + {:commando/from [\"value\"] := inc}})) + => {\"value\" 6, \"value-incremented\" 7} + + (:instruction + (commando/execute [command-from-spec] + {:a {:value 1 + :result {:commando/from [\"../\" :value]}} + :b {:value 2 + :result {:commando/from [\"../\" :value]}}})) + => {:a {:value 1, :result 1}, :b {:value 2, :result 2}} -(def command-from-spec + See Also + `commando.core/execute` + `commando.commands.builtin/command-fn-spec` + `commando.commands.builtin/command-from-spec`"} + command-from-spec {:type :commando/from :recognize-fn #(and (map? %) (contains? % :commando/from)) :validate-params-fn (fn [m] (if-let [m-explain - (malli.error/humanize + (malli-error/humanize (malli/explain [:map - [:commando/from + [:commando/from [:sequential {:error/message "commando/from should be a sequence path to value in Instruction: [:some 2 \"value\"]"} [:or :string :keyword :int]]] [:= {:optional true} [:or utils/ResolvableFn :string]]] @@ -68,12 +160,39 @@ :dependencies {:mode :point :point-key :commando/from}}) -(def command-from-json-spec +(def ^{:doc " + Description + command-fn-json-spec - get value from another command or existing value + in Instruction. Path to another command is passed inside `\"commando-from\"` + key, optionally you can get value of object by using `\"=\"` key. + + Path can be sequence of keywords, strings or integers, starting absolutely from + the root of Instruction, or relatively from the current command position by + using \"../\" and \"./\" strings in paths. + + [\"some\" 2 \"value\"] - absolute path, started from the root key \"some\" + [\"../\" 2 \"value\"] - relative path, go up one level and then down to [2 \"value\"] + + Example + (:instruction + (commando/execute [command-from-json-spec] + {\"a\" {\"value\" {\"container\" 1} + \"result\" {\"commando-from\" [\"../\" \"value\"] \"=\" \"container\"}} + \"b\" {\"value\" {\"container\" 2} + \"result\" {\"commando-from\" [\"../\" \"value\"] \"=\" \"container\"}}})) + + {\"a\" {\"value\" {\"container\" 1}, \"result\" 1}, + \"b\" {\"value\" {\"container\" 2}, \"result\" 2}} + + See Also + `commando.core/execute` + `commando.commands.builtin/command-fn-spec`"} + command-from-json-spec {:type :commando/from-json :recognize-fn #(and (map? %) (contains? % "commando-from")) :validate-params-fn (fn [m] (if-let [m-explain - (malli.error/humanize + (malli-error/humanize (malli/explain [:map ["commando-from" [:sequential {:error/message "commando-from should be a sequence path to value in Instruction: [\"some\" 2 \"value\"]"} @@ -93,6 +212,10 @@ :dependencies {:mode :point :point-key "commando-from"}}) +;; ====================== +;; Mutation +;; ====================== + (defmulti command-mutation (fn [tx-type _data] tx-type)) (defmethod command-mutation :default [undefined-tx-type _] @@ -104,17 +227,37 @@ "'") {:commando/mutation undefined-tx-type}))) -;; in mutation underscore what is occur in example -;; understand why command-from-spec -;; Fix examples in registry -;; -;; CommandRegistry - wymyślić sproszonę wytlumaczenie. Basics - lepsze wyjaśnienia, dla tego co i jak się wykonuje +(def ^{:doc " + Description + command-mutation-spec - execute mutation of Instruction data. + Mutation type is passed inside `:commando/mutation` key and arguments + to mutation passed inside rest of map. + + To declare mutation create method of `command-mutation` multimethod -(def command-mutation-spec + Example + (defmethod commando.commands.builtin/command-mutation :generate-string [_ {:keys [lenght]}] + {:random-string (apply str (repeatedly (or lenght 10) #(rand-nth \"abcdefghijklmnopqrstuvwxyz0123456789\")))}) + + (defmethod commando.commands.builtin/command-mutation :generate-number [_ {:keys [from to]}] + {:random-number (let [bound (- to from)] (+ from (rand-int bound)))}) + + (:instruction + (commando/execute + [command-mutation-spec] + {:a {:commando/mutation :generate-number :from 10 :to 20} + :b {:commando/mutation :generate-string :lenght 5}})) + => {:a {:random-number 14}, :b {:random-string \"5a379\"}} + + See Also + `commando.core/execute` + `commando.commands.builtin/command-mutation-spec` + `commando.commands.builtin/command-mutation`"} + command-mutation-spec {:type :commando/mutation :recognize-fn #(and (map? %) (contains? % :commando/mutation)) :validate-params-fn (fn [m] - (if-let [m-explain (malli.error/humanize + (if-let [m-explain (malli-error/humanize (malli/explain [:map [:commando/mutation :keyword]] m))] m-explain true)) @@ -123,11 +266,38 @@ (command-mutation m-tx-type m))) :dependencies {:mode :all-inside}}) -(def command-mutation-json-spec +(def ^{:doc " + Description + command-mutation-json-spec - execute mutation of Instruction data. + Mutation type is passed inside `\"commando-mutation\"` key and arguments + to mutation passed inside rest of map. + + To declare mutation create method of `command-mutation` multimethod + + Example + (defmethod commando.commands.builtin/command-mutation \"generate-string\" [_ {:strs [lenght]}] + {\"random-string\" (apply str (repeatedly (or lenght 10) #(rand-nth \"abcdefghijklmnopqrstuvwxyz0123456789\")))}) + + (defmethod commando.commands.builtin/command-mutation \"generate-number\" [_ {:strs [from to]}] + {\"random-number\" (let [bound (- to from)] (+ from (rand-int bound)))}) + + (:instruction + (commando/execute + [command-mutation-json-spec] + {\"a\" {\"commando-mutation\" \"generate-number\" \"from\" 10 \"to\" 20} + \"b\" {\"commando-mutation\" \"generate-string\" \"lenght\" 5}})) + => {\"a\" {\"random-number\" 18}, \"b\" {\"random-string\" \"m3gj1\"}} + + See Also + `commando.core/execute` + `commando.commands.builtin/command-mutation-spec` + `commando.commands.builtin/command-mutation-json-spec` + `commando.commands.builtin/command-mutation`"} + command-mutation-json-spec {:type :commando/mutation-json :recognize-fn #(and (map? %) (contains? % "commando-mutation")) :validate-params-fn (fn [m] - (if-let [m-explain (malli.error/humanize + (if-let [m-explain (malli-error/humanize (malli/explain [:map ["commando-mutation" [:string {:min 1}]]] m))] m-explain true)) @@ -136,4 +306,161 @@ :dependencies {:mode :all-inside}}) +;; ====================== +;; Macro +;; ====================== + +(defmulti command-macro (fn [tx-type _data] tx-type)) +(defmethod command-macro :default + [undefinied-tx-type _] + (throw (ex-info + (str utils/exception-message-header + "command-macro. Undefinied '" undefinied-tx-type "'") + {:commando/macro undefinied-tx-type}))) + +(def ^{:doc " + Description + command-macro-spec - help to define reusable instruction template, + what execute instruction using the same registry as the current one. + To declare macro expand `command-mutation` multimethod. + + Example + Asume we have two vectors with string numbers: + 1) [\"1\", \"2\", \"3\"], 2) [\"4\" \"5\" \"6\"] + we need to parse them to integers and then calculate dot product. + Here the solution using commando commands with one instruction + + (:instruction + (commando/execute + [command-fn-spec command-from-spec command-apply-spec] + {:= :dot-product + :commando/apply + {:vector1-str [\"1\" \"2\" \"3\"] + :vector2-str [\"4\" \"5\" \"6\"] + ;; ------- + ;; Parsing + :vector1 + {:commando/fn (fn [str-vec] + (mapv #(Integer/parseInt %) str-vec)) + :args [{:commando/from [\"../\" \"../\" \"../\" :vector1-str]}]} + :vector2 + {:commando/fn (fn [str-vec] + (mapv #(Integer/parseInt %) str-vec)) + :args [{:commando/from [\"../\" \"../\" \"../\" :vector2-str]}]} + ;; ----------- + ;; Dot Product + :dot-product + {:commando/fn (fn [& [v1 v2]] (reduce + (map * v1 v2))) + :args [{:commando/from [\"../\" \"../\" \"../\" :vector1]} + {:commando/from [\"../\" \"../\" \"../\" :vector2]}]}}})) + => 32 + + But what if we need to calculate those instruction many times with different vectors? + It means we need to repeat the same instruction many times with different input data, + what quickly enlarges our Instruction size(every usage) and makes it unreadable. + + To solve this problem we can use `command-macro` to define reusable instruction template + (defmethod command-macro :vector-dot-product [_macro-type {:keys [vector1-str vector2-str]}] + {:= :dot-product + :commando/apply + {:vector1-str [\"1\" \"2\" \"3\"] + :vector2-str [\"4\" \"5\" \"6\"] + ;; ------- + ;; Parsing + :vector1 + {:commando/fn (fn [str-vec] + (mapv #(Integer/parseInt %) str-vec)) + :args [{:commando/from [\"../\" \"../\" \"../\" :vector1-str]}]} + :vector2 + {:commando/fn (fn [str-vec] + (mapv #(Integer/parseInt %) str-vec)) + :args [{:commando/from [\"../\" \"../\" \"../\" :vector2-str]}]} + ;; ----------- + ;; Dot Product + :dot-product + {:commando/fn (fn [& [v1 v2]] (reduce + (map * v1 v2))) + :args [{:commando/from [\"../\" \"../\" \"../\" :vector1]} + {:commando/from [\"../\" \"../\" \"../\" :vector2]}]}}}) + + and then just use it + + (:instruction + (commando/execute + [command-macro-spec command-fn-spec command-from-spec command-apply-spec] + {:vector-dot-1 + {:commando/macro :vector-dot-product + :vector1-str [\"1\" \"2\" \"3\"] + :vector2-str [\"4\" \"5\" \"6\"]} + :vector-dot-2 + {:commando/macro :vector-dot-product + :vector1-str [\"10\" \"20\" \"30\"] + :vector2-str [\"4\" \"5\" \"6\"]}})) + => {:vector-dot-1 32, :vector-dot-2 32} + + See Also + `commando.core/execute` + `commando.commands.builtin/command-macro`"} + command-macro-spec + {:type :commando/macro + :recognize-fn #(and + (map? %) + (contains? % :commando/macro)) + :validate-params-fn (fn [m] + (if-let [explain-m + (malli-error/humanize + (malli/explain + [:map + [:commando/macro {:optional true} :keyword]] + m))] + explain-m + true)) + :apply (fn [_instruction _command-map m] + (let [macro-type (get m :commando/macro) + macro-data (dissoc m :commando/macro) + result (commando/execute + (utils/command-map-spec-registry) + (command-macro macro-type macro-data))] + (if (= :ok (:status result)) + (:instruction result) + (throw (ex-info (str utils/exception-message-header "command-macro. Failure execution :commando/macro") result))))) + :dependencies {:mode :all-inside}}) + +(def ^{:doc " + Description + command-macro-json-spec - help to define reusable instruction template, + what execute instruction using the same registry as the current one. + To declare macro expand `command-mutation` multimethod. Using string + key \"commando-macro\" for declaring macroses instead of keyword :commando/macro. + + Example + read one from `command-macro-spec`. + + See Also + `commando.core/execute` + `commando.commands.builtin/command-macro` + `commando.commands.builtin/command-macro-spec`"} + command-macro-json-spec + {:type :commando/macro + :recognize-fn #(and + (map? %) + (contains? % "commando-macro")) + :validate-params-fn (fn [m] + (if-let [explain-m + (malli-error/humanize + (malli/explain + [:map + ["commando-macro" {:optional true} :string]] + m))] + explain-m + true)) + :apply (fn [_instruction _command-map m] + (let [macro-type (get m "commando-macro") + macro-data (dissoc m "commando-macro") + result (commando/execute + (utils/command-map-spec-registry) + (command-macro macro-type macro-data))] + (if (= :ok (:status result)) + (:instruction result) + (throw (ex-info (str utils/exception-message-header "command-macro. Failure execution :commando/macro") result))))) + :dependencies {:mode :all-inside}}) diff --git a/src/commando/commands/query_dsl.cljc b/src/commando/commands/query_dsl.cljc index 5753986..033c658 100644 --- a/src/commando/commands/query_dsl.cljc +++ b/src/commando/commands/query_dsl.cljc @@ -1,12 +1,13 @@ (ns commando.commands.query-dsl (:require - [commando.core :as commando] - [commando.impl.utils :as commando-utils] - [malli.core :as malli] + [commando.core :as commando] + [commando.impl.status-map :as smap] + [commando.impl.utils :as commando-utils] + [malli.core :as malli] [malli.error] #?(:clj [clojure.pprint :as pprint]))) -(def ^:private exception-message-header (str commando-utils/exception-message-header "Graph Query. ")) +(def ^:private exception-message-header (str commando-utils/exception-message-header "QueryDSL. ")) (defn ^:private conj-dup-error [coll obj] @@ -118,78 +119,207 @@ :expression-props {}} QueryExpression)) -(deftype ^:private QueryResolve [value Instruction] +(deftype ^:private Resolver [resolver_type resolver_data] Object (toString ^String [_] - (str "#%s" - (pr-str {:default value - :instruction Instruction})))) + (str "# " + (case resolver_type + :instruction-qe (let [{:keys [default-value _instruction]} resolver_data] + (pr-str {:type :instruction-qe :default default-value})) + :instruction (let [{:keys [default-value _instruction]} resolver_data] + (pr-str {:type :instruction :default default-value})) + :fn (let [{:keys [default-value _fn-resolver]} resolver_data] + (pr-str {:type :fn :default default-value})) + (throw (ex-info "# Exception. Undefinied resolver type" {:resolver_type resolver_type})))))) #?(:clj - (do (defmethod print-method QueryResolve [obj ^java.io.Writer writer] (.write writer (pr-str (.toString obj)))) - (defmethod pprint/simple-dispatch QueryResolve [obj] (print-method obj *out*)))) - - -(defn query-resolve? [obj] (instance? QueryResolve obj)) -(defn query-resolve - "Take `default-value`, `Instruction`, and if QueryExpression ask for property, then - execute `Instruction`(internal ivoke of commanod/execute) and return the result, - otherwise return `default-value` - - [:value-covered-by-query-resolve] <- return `default-value` - [{:value-covered-by-query-resolve <- execute `Instruction` and lookup for `:SOME-VALUES` - [:SOME-VALUES]}]" - [default-value Instruction] (new QueryResolve default-value Instruction)) -(defn ^:private resolve-execute - [data QueryExpression QueryExpressionKeyProperties] - (if (query-resolve? data) - (let [patch-query (fn [query-resolve-instruction] - (cond-> query-resolve-instruction - true (assoc :QueryExpression QueryExpression) - true (assoc "QueryExpression" QueryExpression) - QueryExpressionKeyProperties (merge QueryExpressionKeyProperties))) - result (commando/execute - (commando.impl.utils/command-map-spec-registry) - (let [internal-instruction-to-execute (.-Instruction data)] - (cond - (map? internal-instruction-to-execute) (patch-query internal-instruction-to-execute) - (vector? internal-instruction-to-execute) (mapv patch-query internal-instruction-to-execute) - :else (throw (ex-info (str exception-message-header "Unsupported structure for internal resolving functionality") {:part internal-instruction-to-execute})))))] - (if (= :ok (:status result)) (:instruction result) result)) - data)) -(defn ^:private return-values [data] (if (query-resolve? data) (.-value data) data)) + (do (defmethod print-method Resolver [obj ^java.io.Writer writer] (.write writer (pr-str (.toString obj)))) + (defmethod pprint/simple-dispatch Resolver [obj] (print-method obj *out*)))) + +(defn resolve-instruction-qe + "Take a default-value and Instruction with `:commando/resolve` command + on the top-level, and return Resolver object that will be processed + by `->query-run` + + Example + (resolve-instruction-qe + [] + {:commando/resolve :cars-by-model + :model \"Citroen\"} + + + See Also + `commando.commands.query-dsl/->query-run` + `commando.commands.query-dsl/->resolve-instruction` + - the same but for any Instruction can execute your registry. + `commando.commands.query-dsl/->resolve-fn` + - the same but for simple function resolving" + [default-value InstructionWithQueryExpression] + (->Resolver :instruction-qe {:default-value default-value :instruction InstructionWithQueryExpression})) + +(defn resolve-instruction + "Take a default-value and Instruction that can be executed by commando + registry. Return Resolver object that will be processed by `->query-run`. + + Example + (resolve-instruction + 0 + {:vector1 [1 2 3] + :vector2 [3 2 1] + :result {:commando/fn (fn [& [v1 v2]] (reduce + (map * v1 v2))) + :args [{:commando/from [:vector1]} + {:commando/from [:vector2]}]}}) + + See Also + `commando.commands.query-dsl/->query-run` + `commando.commands.query-dsl/->resolve-instruction-qe` + - the same but for Instruction with `:commando/resolve` command + on the top-level. + `commando.commands.query-dsl/->resolve-fn` + - the same but for simple function resolving" + [default-value Instruction] + (->Resolver :instruction {:default-value default-value :instruction Instruction})) + +(defn resolve-fn + "Take a default-value and fn-resolver - simple function that + can optionally accept KeyProperties(passed data from QueryExpression syntax) map + and return the data that can be queried by QueryExpression syntax + by commando registry. Return Resolver object that will be processed + by `->query-run`. + + Example + (resolve-fn + [] + (fn [{:keys [x]}] + (vec (for [i (range (or x 5))] + {:value i})))) + + See Also + `commando.commands.query-dsl/->query-run` + `commando.commands.query-dsl/->resolve-instruction-qe` + - the same but for Instruction with `:commando/resolve` command + on the top-level. + `commando.commands.query-dsl/->resolve-instruction` + - the same but for any Instruction can execute your registry." + [default-value fn-resolver] + (->Resolver :fn {:default-value default-value :fn-resolver fn-resolver})) +(defn resolver? + "Check is the obj a `commando.commands.query_dsl/Resolver` instance" + [obj] (instance? Resolver obj)) + +(defn ^:private run-resolve-instruction-qe [resolver_data QueryExpression QueryExpressionKeyProperties] + (let [{:keys [_default-value instruction]} resolver_data + patch-query (fn [instruction] + (cond-> instruction + (contains? instruction :commando/resolve ) (assoc :QueryExpression QueryExpression) + (contains? instruction "commando-resolve") (assoc "QueryExpression" QueryExpression) + QueryExpressionKeyProperties (merge QueryExpressionKeyProperties))) + result (commando/execute + (commando-utils/command-map-spec-registry) + (cond + (map? instruction) (patch-query instruction) + (vector? instruction) (mapv patch-query instruction) + :else (throw (ex-info (str exception-message-header "Unsupported structure for InstructionWithQueryExpression resolving functionality") {:instruction-qe instruction}))))] + (if (= :ok (:status result)) (:instruction result) result))) + +(defn ^:private run-resolve-instruction [resolver_data KeyProperties] + (let [{:keys [_default-value instruction]} resolver_data + patch-query (fn [instruction] + (cond-> instruction + KeyProperties (merge KeyProperties))) + result (commando/execute + (commando-utils/command-map-spec-registry) + (cond + (map? instruction) (patch-query instruction) + (vector? instruction) (mapv patch-query instruction) + :else (throw (ex-info (str exception-message-header "Unsupported structure for Instruction resolving functionality") {:instruction instruction}))))] + (if (= :ok (:status result)) (:instruction result) result))) + +(defn ^:private run-resolve-fn [resolver_data KeyProperties] + (let [{:keys [_default-value fn-resolver]} resolver_data + result (commando/execute + (commando-utils/command-map-spec-registry) + (fn-resolver KeyProperties))] + (if (= :ok (:status result)) (:instruction result) result))) + +(defn ^:private trying-to-resolve [resolver QueryExpression QueryExpressionKeyProperties ->query-run internal-keys] + (if (resolver? resolver) + (let [resolver_type (.-resolver_type resolver) + resolver_data (.-resolver_data resolver)] + (case resolver_type + ;; :instruction-qe not need to loop with ->query-run to obtain the next QueryExpression values + ;; cause if the are another QueryExpression instruction inside, it has ->query-run by the end of it + :instruction-qe + (run-resolve-instruction-qe resolver_data QueryExpression QueryExpressionKeyProperties) + :instruction + (->query-run + (run-resolve-instruction resolver_data QueryExpressionKeyProperties) + internal-keys) + :fn + (->query-run + (try + (run-resolve-fn resolver_data QueryExpressionKeyProperties) + (catch Exception e + (-> + (smap/status-map-pure) + (smap/status-map-handle-error {:message (str exception-message-header "resolve-fn. Finished return exception") + :error (commando-utils/serialize-exception e)})))) + internal-keys))) + resolver)) + +(defn ^:private trying-to-value [maybe-resolver] + (if (resolver? maybe-resolver) + (:default-value (.-resolver_data maybe-resolver)) + maybe-resolver)) (defn ->query-run [m QueryExpression] (let [{:keys [expression-keys expression-values expression-props]} (QueryExpression->expand-first QueryExpression)] - (reduce (fn [acc k] - (if (contains? m k) - (let [key-properties (get expression-props k) - internal-keys (get expression-values k)] - (if internal-keys - (assoc acc - k - (let [data (get m k)] - (cond - (map? data) (->query-run data internal-keys) - (coll? data) (mapv #(-> % - (resolve-execute internal-keys key-properties) - (->query-run internal-keys)) data) - :else (resolve-execute data internal-keys key-properties)))) - (assoc acc - k - (let [data (get m k)] - (cond - (map? data) data - (coll? data) (mapv return-values data) - :else (return-values data)))))) - (assoc acc - k - {:status :failed - :errors [{:message (str exception-message-header - "QueryExpression attribute '" k "' is unreachable")}]}))) - {} - expression-keys))) + (cond + + (and (map? m) (commando/failed? m)) m + + (map? m) + (reduce (fn [acc k] + (if (contains? m k) + ;; QE + ;; [{[:key {:some-key-properties nil}] + ;; [:internal-key-1 + ;; :internal-key-1]}] + (let [key-properties (get expression-props k) + internal-keys (get expression-values k)] + (assoc acc k + (if internal-keys + ;; QE + ;; [{:key + ;; [:internal-key-1 + ;; :internal-key-1]}] + (let [data (get m k)] + (cond + (map? data) (->query-run data internal-keys) + (coll? data) (mapv #(trying-to-resolve % internal-keys key-properties ->query-run internal-keys) data) + (resolver? data) (trying-to-resolve data internal-keys key-properties ->query-run internal-keys) + :else data)) + ;; QE + ;; [:key] + (let [data (get m k)] + (cond + (map? data) (trying-to-value data) + (coll? data) (mapv trying-to-value data) + (resolver? data) (trying-to-value data) + :else data))))) + (assoc acc + k + {:status :failed + :errors [{:message (str exception-message-header + "QueryExpression. Attribute '" k "' is unreachable.")}]}))) + {} + expression-keys) + + (coll? m) + (mapv (fn [e] (->query-run e QueryExpression)) m) + + :else m))) (defn ->>query-run [QueryExpression m] (->query-run m QueryExpression)) @@ -240,12 +370,12 @@ (-> {:first-name \"Adam\" :last-name \"Nowak\" :info {:age 25 :weight 70 :height 188} - :passport (query-resolve + :passport (resolve-instruction-qe \"- no passport - \" {:commando/resolve :query-passport :first-name \"Adam\" :last-name \"Nowak\"}) - :password (query-resolve + :password (resolve-instruction \"- no password - \" {:commando/mutation :generate-password})} (->query-run QueryExpression))) @@ -286,7 +416,7 @@ :UNEXISTING {:status :failed, :errors [{:message \"Commando. Graph Query. QueryExpression attribute ':UNEXISTING' is unreachable\"}]}} Parts - `commando.commands.query-dsl/query-resolve` run internal call of `commando/execute`. + `commando.commands.query-dsl/resolve-instruction-qe` run internal call of `commando/execute`. `commando.commands.query-dsl/->query-run` trim query data according to passed QueryExpression `commando.commands.query-dsl/command-resolve` multimethod to declare resolvers. @@ -351,14 +481,14 @@ (-> {\"first-name\" \"Adam\" \"last-name\" \"Nowak\" \"info\" {\"age\" 25 \"weight\" 70 \"height\" 188} - \"passport\" (query-resolve - \"- no passport - \" - {\"commando-resolve\" \"query-passport\" - \"first-name\" \"Adam\" - \"last-name\" \"Nowak\"}) - \"password\" (query-resolve - \"- no password - \" - {\"commando-mutation\" \"generate-password\"})} + \"passport\" (resolve-instruction-qe + \"- no passport - \" + {\"commando-resolve\" \"query-passport\" + \"first-name\" \"Adam\" + \"last-name\" \"Nowak\"}) + \"password\" (resolve-instruction + \"- no password - \" + {\"commando-mutation\" \"generate-password\"})} (->query-run QueryExpression))) @@ -401,7 +531,7 @@ :errors [{:message \"Commando. Graph Query. QueryExpression attribute 'UNEXISTING' is unreachable\"}]}} Parts - `commando.commands.query-dsl/query-resolve` run internal call of `commando/execute`. + `commando.commands.query-dsl/resolve-instruction-qe` run internal call of `commando/execute`. `commando.commands.query-dsl/->query-run` trim query data according to passed QueryExpression `commando.commands.query-dsl/command-resolve` multimethod to declare resolvers. @@ -428,3 +558,226 @@ (command-resolve (get m "commando-resolve") (dissoc m "commando-resolve"))) :dependencies {:mode :all-inside}}) +(comment + + (require 'commando.commands.builtin) + (def registry + (commando/create-registry + [command-resolve-spec + commando.commands.builtin/command-fn-spec + commando.commands.builtin/command-from-spec + commando.commands.builtin/command-apply-spec])) + + (defn execute-with-registry [instruction] + (:instruction + (commando.core/execute + registry + instruction))) + + (defmethod command-resolve :test-instruction-qe [_ {:keys [x QueryExpression]}] + (let [x (or x 10)] + (-> {:string "Value" + + :map {:a + {:b {:c x} + :d {:c (inc x) + :f (inc (inc x))}}} + + :coll [{:a + {:b {:c x} + :d {:c (inc x) + :f (inc (inc x))}}} + {:a + {:b {:c x} + :d {:c (dec x) + :f (dec (dec x))}}}] + + :resolve-fn (resolve-fn "default value for resolve-fn" + (fn [{:keys [x]}] + (let [y (or x 1) + range-y (if (< 10 y) 10 y)] + (for [z (range 0 range-y)] + {:a + {:b {:c (+ y z)} + :d {:c (inc (+ y z)) + :f (inc (inc (+ y z)))}}})))) + + :resolve-fn-error (resolve-fn "default value for resolve-fn" + (fn [{:keys [_x]}] + (throw (ex-info "Exception" {:error "no reason"})))) + + :resolve-fn-call (for [x (range 10)] + (resolve-fn (str "default value for resolve-fn-call -> " x) + (fn [properties] + (let [x (or (:x properties) x)] + {:a {:b x}})))) + + :resolve-instruction (resolve-instruction "default value for resolve-instruction" + {:commando/fn (fn [count-elements] + (vec + (for [x (range 0 count-elements)] + {:a + {:b {:c x} + :d {:c (inc x) + :f (inc (inc x))}}}))) + :args [2]}) + + :resolve-instruction-with-error (resolve-instruction "default value for resolve-instruction-with-error" + {:commando/fn (fn [& _body] + (throw (ex-info "Exception" {:error "no reason"}))) + :args []}) + + :resolve-instruction-qe (resolve-instruction-qe "default value for resolve-instruction-qe" + {:commando/resolve :test-instruction-qe + :x 1})} + (->query-run QueryExpression)))) + + ;; ---------------- + ;; Simple attribute + (execute-with-registry + {:commando/resolve :test-instruction-qe + :x 1 + :QueryExpression + [:string]}) + + ;; -------- + ;; Defaults + (execute-with-registry + {:commando/resolve :test-instruction-qe + :x 1 + :QueryExpression + [:string + :map + :coll + :resolve-fn + :resolve-instruction + :resolve-instruction-qe]}) + + ;; ---------------------------- + ;; Resolving data Map and Colls + (execute-with-registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [:string + {:map + [{:a + [:b]}]} + {:coll + [{:a + [:b]}]}]}) + + ;; ---------- + ;; resolve-fn + (execute-with-registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-fn + [{:a + [:b]}]}]}) + + + ;; resolve-fn with overriding `:x` + (execute-with-registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{[:resolve-fn {:x 1000}] + [{:a + [:b]}]}]}) + + ;; --------------------------- + ;; resolver packed in sequence + (execute-with-registry + {:commando/resolve :test-instruction-qe + :QueryExpression + [{:resolve-fn-call + [{:a + [:b]}]}]}) + + ;; resolver packed in sequence with overrding + (execute-with-registry + {:commando/resolve :test-instruction-qe + :QueryExpression + [{[:resolve-fn-call {:x 100}] + [{:a + [:b]}]}]}) + + ;; ------------------- + ;; resolve-instruction + (execute-with-registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-instruction + [{:a + [:b]}]}]}) + + ;; resolve-instruction with overrding `:args` + (execute-with-registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{[:resolve-instruction + {:args [100]}] + [{:a + [:b]}]}]}) + + ;; --------------------- + ;; resolve-instruction-qe + (execute-with-registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-instruction-qe + [{:map [{:a [:b]}]}]}]}) + + ;; loop resolve-instruction-qe with override `:x` + (execute-with-registry + {:commando/resolve :test-instruction-qe + :x 1 + :QueryExpression + [{:resolve-instruction-qe + [{:map [{:a + [:b]}]} + {[:resolve-instruction-qe {:x 1000}] + [{:map [{:a + [:b]}]} + {[:resolve-instruction-qe {:x 10000000}] + [{:map [{:a + [:b]}]}]}]}]}]}) + + ;; ----------------------------- + ;; ERRORS inside the instruction + + ;; 1. Key `:EEE` is not existing + (execute-with-registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [:EEE + {:resolve-fn + [{:a + [:b + :EEE]}]}]}) + + ;; 2. Internal instruction return an error + (execute-with-registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-instruction-with-error + [{:a [:b]}]}]}) + + ;; 3. Cause the resolve-fn is an simple function + ;; call, so there are handled in try..catch and + ;; return as a status-map exception either. + (execute-with-registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-fn-error + [:a]}]}) + + ) diff --git a/src/commando/core.cljc b/src/commando/core.cljc index af126e2..bb95295 100644 --- a/src/commando/core.cljc +++ b/src/commando/core.cljc @@ -1,6 +1,5 @@ (ns commando.core (:require - [commando.commands.builtin] [commando.impl.dependency :as deps] [commando.impl.executing :as executing] [commando.impl.finding-commands :as finding-commands] @@ -160,7 +159,6 @@ (smap/status-map-handle-warning {:message (str utils/exception-message-header "build-compiler. Error building compiler")})) :ok (cond-> status-map - true (update-in [:registry] registry/detach-instruction-commands) true (update-in [:internal/cm-running-order] registry/remove-instruction-commands-from-command-vector) (false? utils/*debug-mode*) (select-keys [:status :registry :internal/cm-running-order :successes :warnings]))))) diff --git a/src/commando/impl/registry.cljc b/src/commando/impl/registry.cljc index d5d0fc6..71cdfdf 100644 --- a/src/commando/impl/registry.cljc +++ b/src/commando/impl/registry.cljc @@ -1,6 +1,6 @@ (ns commando.impl.registry "API for registry. - + A registry is a collection of command specifications that define how to recognize, validate, and execute commands found in instruction map." (:require @@ -39,7 +39,7 @@ (defn attach-instruction-commands [registry] (let [registry-meta (meta registry)] - (with-meta + (with-meta (into (vec registry) [default-command-vec-spec default-command-map-spec @@ -50,7 +50,7 @@ (let [registry-meta (meta registry)] (with-meta (reduce (fn [acc e] - (if + (if (contains? #{default-command-vec-spec default-command-map-spec @@ -72,7 +72,7 @@ "Validates: - All specs are valid according to CommandMapSpec - No duplicate :type values - + Returns {:valid? true} or {:valid? false :errors [...]}" [command-specs] (let [empty-command-spec-list-errors (when (empty? command-specs) @@ -101,7 +101,10 @@ {:valid? false :errors all-errors}))) (defn built? - "Returns true if the given value is a properly built registry." + "Returns true if the given value is a properly built registry. + + Built registry mean that registry was validated and the internal + Instruction commands were attached to list of command specifications." [registry] (and (vector? registry) (:registry/validated (meta registry)))) @@ -119,14 +122,12 @@ A validated registry vector with metadata for caching or throws an error" [command-spec-list] {:pre [(vector? command-spec-list)]} - (let [specs (with-meta command-spec-list - {:registry/validated true - :registry/hash (hash command-spec-list)}) - validation (validate-registry specs) - specs-with-instruction-structure-commands (attach-instruction-commands specs)] - (if (:valid? validation) - specs-with-instruction-structure-commands - (throw (ex-info "Invalid registry specification" - {:errors (:errors validation) - :registry specs}))))) - + (let [validation (validate-registry command-spec-list)] + (if (:valid? validation) + (let [command-spec-list (attach-instruction-commands command-spec-list)] + (with-meta command-spec-list + {:registry/validated true + :registry/hash (hash command-spec-list)})) + (throw (ex-info "Invalid registry specification" + {:errors (:errors validation) + :registry command-spec-list}))))) From 4764761b2d38290caf593dea4dc2f1bfd15d0b3b Mon Sep 17 00:00:00 2001 From: SerhiiRI Date: Tue, 21 Oct 2025 23:42:43 +0300 Subject: [PATCH 2/9] - UPDATED Replacing Throwable->map onto custom multimethod. Cause it multimethod it can be expanded by another Exception types. Serialized object contain neccessary information declared in keywords, and the :data key (even if it contains symbols, etc.) wrapped to string. To avoid default conversion of :data key to string use commando.impl.utils/*debug-mode*. - FIXED Tests for exceptions. --- src/commando/impl/utils.cljc | 128 ++++++++++++++++-- test/unit/commando/core_test.cljc | 43 +++--- test/unit/commando/impl/utils_test.cljc | 173 ++++++++++++++++++++++-- 3 files changed, 305 insertions(+), 39 deletions(-) diff --git a/src/commando/impl/utils.cljc b/src/commando/impl/utils.cljc index 97b8126..918c01c 100644 --- a/src/commando/impl/utils.cljc +++ b/src/commando/impl/utils.cljc @@ -3,6 +3,10 @@ (def exception-message-header "Commando. ") +;; ------------------ +;; Dynamic Properties +;; ------------------ + (def ^:dynamic *debug-mode* "When enabled, debug-stack-map functionality is active in find-commands*." false) @@ -18,18 +22,9 @@ [] (or *command-map-spec-registry* [])) -(defn serialize-exception - "Serializes errors into data structures." - [e] - (cond - (nil? e) nil - #?(:clj (instance? Throwable e) - :cljs (instance? js/Error e)) - (Throwable->map e) - ;; Maps might already be error representations - (map? e) e - ;; Default serialization for other types - :else {:message (str e)})) +;; ------------------ +;; Function Resolvers +;; ------------------ (defn resolve-fn "Normalize `x` to a function (fn? x) => true. @@ -74,4 +69,113 @@ (fn [x] (some? (resolve-fn x)))])) +;; ----------- +;; Error Tools +;; ----------- + +#?(:clj + (defn ^:private stacktrace->vec-str [^Throwable t] + (mapv (fn [^StackTraceElement ste] + [(.getClassName ste) + (.getMethodName ste) + (.getFileName ste) + (.getLineNumber ste)]) + (.getStackTrace t)))) + +#?(:clj (defn ^:private exception-dispatch-fn [e] (class e))) + +#?(:clj (defmulti serialize-exception-fn exception-dispatch-fn)) + +#?(:clj + (defmethod serialize-exception-fn java.lang.Throwable [^Throwable t] + {:type "throwable" + :class (.getName (class t)) + :message (str (.getMessage t)) + :stack-trace (stacktrace->vec-str t) + :cause (when-let [cause (.getCause t)] + (serialize-exception-fn cause)) + :data nil})) + +#?(:clj + (defmethod serialize-exception-fn java.lang.RuntimeException [^Throwable t] + {:type "runtime-exception" + :class (.getName (class t)) + :message (str (.getMessage t)) + :stack-trace (stacktrace->vec-str t) + :cause (when-let [cause (.getCause t)] + (serialize-exception-fn cause)) + :data nil})) + +#?(:clj + (defmethod serialize-exception-fn clojure.lang.ExceptionInfo [^clojure.lang.ExceptionInfo t] + {:type "exception-info" + :class (.getName (class t)) + :message (str (.getMessage t)) + :stack-trace (stacktrace->vec-str t) + :cause (when-let [cause (.getCause t)] + (serialize-exception-fn cause)) + :data (if *debug-mode* + (ex-data t) + (pr-str (ex-data t)))})) + +#?(:clj + (defmethod serialize-exception-fn :default [t] + (when (instance? java.lang.Throwable t) + {:type "generic" + :class (.getName (class t)) + :message (str (.getMessage t)) + :stack-trace (stacktrace->vec-str t) + :cause (when-let [cause (.getCause t)] + (serialize-exception-fn cause)) + :data nil}))) +#?(:cljs + (defn ^:private exception-dispatch-fn [e] + (cond + (instance? cljs.core.ExceptionInfo e) :cljs-exception-info + (instance? js/Error e) :js-error + :else nil))) + +#?(:cljs + (defn ^:private stacktrace->vec-str [^js/Error e] + (if-let [stack (.-stack e)] (str stack) nil))) + +#?(:cljs + (defmulti serialize-exception-fn exception-dispatch-fn)) + +#?(:cljs + (defmethod serialize-exception-fn :cljs-exception-info [^cljs.core.ExceptionInfo e] + (let [cause (.-cause e)] + {:type "exception-info" + :class "cljs.core.ExceptionInfo" + :message (.-message e) + :stack-trace (stacktrace->vec-str e) + :cause (when-let [cause (.-cause e)] + (serialize-exception-fn cause)) + :data (if *debug-mode* + (.-data e) + (pr-str (.-data e)))}))) + +#?(:cljs + (defmethod serialize-exception-fn :js-error [^js/Error e] + {:type "js-error" + :class "js/Error" + :message (or (.-message e) "No message") + :stack-trace (stacktrace->vec-str e) + :cause nil + :data nil})) + +#?(:cljs + (defmethod serialize-exception-fn :default [e] + nil)) + +(defn serialize-exception + "Serializes errors into data structures." + [e] + (cond + (nil? e) nil + #?(:clj (instance? Throwable e) + :cljs (instance? js/Error e)) + (serialize-exception-fn e) + (map? e) e + :else {:message (str e)})) diff --git a/test/unit/commando/core_test.cljc b/test/unit/commando/core_test.cljc index 3b01f07..2ac5fcc 100644 --- a/test/unit/commando/core_test.cljc +++ b/test/unit/commando/core_test.cljc @@ -5,6 +5,7 @@ [commando.commands.builtin :as cmds-builtin] [commando.core :as commando] [commando.impl.command-map :as cm] + [commando.impl.utils :as commando-utils] [malli.core :as malli])) ;; ------- @@ -1367,9 +1368,10 @@ (deftest execute-wrong-validate-params-fn (testing "':validate-params-fn' for commando/from" (is (status-map-contains-error? - (commando/execute - [cmds-builtin/command-from-spec] - {:commando/from "BROKEN" := []}) + (binding [commando-utils/*debug-mode* true] + (commando/execute + [cmds-builtin/command-from-spec] + {:commando/from "BROKEN" := []})) (fn [error] (= (-> error :error :data) {:command-type :commando/from, @@ -1382,9 +1384,10 @@ "should be a string"]}})))) (is (status-map-contains-error? - (commando/execute - [cmds-builtin/command-from-json-spec] - {"commando-from" "BROKEN" "=" ""}) + (binding [commando-utils/*debug-mode* true] + (commando/execute + [cmds-builtin/command-from-json-spec] + {"commando-from" "BROKEN" "=" ""})) (fn [error] (= (-> error :error :data) {:command-type :commando/from-json, @@ -1397,9 +1400,10 @@ (testing "':validate-params-fn' for commando/apply" (is (status-map-contains-error? - (commando/execute - [cmds-builtin/command-apply-spec] - {:commando/apply nil := "123"}) + (binding [commando-utils/*debug-mode* true] + (commando/execute + [cmds-builtin/command-apply-spec] + {:commando/apply nil := "123"})) (fn [error] (= (-> error :error :data) {:command-type :commando/apply, @@ -1411,9 +1415,10 @@ (testing "':validate-params-fn' for commando/fn" (is (status-map-contains-error? - (commando/execute - [cmds-builtin/command-fn-spec] - {:commando/fn "BROKEN" :args "BROKEN"}) + (binding [commando-utils/*debug-mode* true] + (commando/execute + [cmds-builtin/command-fn-spec] + {:commando/fn "BROKEN" :args "BROKEN"})) (fn [error] (= (-> error :error :data) {:command-type :commando/fn, @@ -1427,9 +1432,10 @@ (testing "':validate-params-fn' for commando/mutation" (is (status-map-contains-error? - (commando/execute - [cmds-builtin/command-mutation-spec] - {:commando/mutation nil}) + (binding [commando-utils/*debug-mode* true] + (commando/execute + [cmds-builtin/command-mutation-spec] + {:commando/mutation nil})) (fn [error] (= (-> error :error :data) {:command-type :commando/mutation, @@ -1438,9 +1444,10 @@ :reason {:commando/mutation ["should be a keyword"]}})))) (is (status-map-contains-error? - (commando/execute - [cmds-builtin/command-mutation-json-spec] - {"commando-mutation" 888}) + (binding [commando-utils/*debug-mode* true] + (commando/execute + [cmds-builtin/command-mutation-json-spec] + {"commando-mutation" 888})) (fn [error] (= (-> error :error :data) {:command-type :commando/mutation-json, diff --git a/test/unit/commando/impl/utils_test.cljc b/test/unit/commando/impl/utils_test.cljc index c595fc6..900ccc6 100644 --- a/test/unit/commando/impl/utils_test.cljc +++ b/test/unit/commando/impl/utils_test.cljc @@ -6,15 +6,170 @@ [commando.impl.utils :as sut] [malli.core :as malli])) +(deftest serialize-exception + #?(:clj + (testing "Serialization exception CLJ" + ;; ----------------- + ;; Simple Exceptions + ;; ----------------- + (let [e + (sut/serialize-exception + (RuntimeException/new "controlled exception"))] + (is (= + (dissoc e :stack-trace) + {:type "runtime-exception", + :class "java.lang.RuntimeException", + :message "controlled exception", + :cause nil, + :data nil}))) + + (let [e (sut/serialize-exception + (ex-info "controlled exception" {}))] + (is (= + (dissoc e :stack-trace) + {:type "exception-info", + :class "clojure.lang.ExceptionInfo", + :message "controlled exception", + :cause nil, + :data "{}"}))) + + (let [e (sut/serialize-exception + (Exception/new "controlled exception"))] + (is (= + (dissoc e :stack-trace) + {:type "throwable", + :class "java.lang.Exception", + :message "controlled exception", + :cause nil, + :data nil}))) + -(deftest throw->map - (testing "Serialization exception" - (is - (= - (-> - (sut/serialize-exception (ex-info "Exception Text" {:value1 "a" :value2 "b"})) - (dissoc :via :trace)) - {:cause "Exception Text", :data {:value1 "a", :value2 "b"}})))) + (let [e (sut/serialize-exception + (ex-info "LEVEL1" {:level "1"} + (ex-info "LEVEL2" {:level "2"} + (ex-info "LEVEL2" {:level "3"}))))] + (is (= + (-> e + (dissoc :stack-trace) + (update-in [:cause] dissoc :stack-trace) + (update-in [:cause :cause] dissoc :stack-trace)) + {:type "exception-info", + :class "clojure.lang.ExceptionInfo", + :message "LEVEL1", + :cause + {:type "exception-info", + :class "clojure.lang.ExceptionInfo", + :message "LEVEL2", + :cause + {:type "exception-info", + :class "clojure.lang.ExceptionInfo", + :message "LEVEL2", + :cause nil, + :data "{:level \"3\"}"}, + :data "{:level \"2\"}"}, + :data "{:level \"1\"}"}))) + + (let [e (sut/serialize-exception + (ex-info "LEVEL1" {:level "1"} + (NullPointerException/new "LEVEL2")))] + (is + (= + (-> e + (dissoc :stack-trace) + (update-in [:cause] dissoc :stack-trace)) + {:type "exception-info", + :class "clojure.lang.ExceptionInfo", + :message "LEVEL1", + :cause + {:type "runtime-exception", + :class "java.lang.NullPointerException", + :message "LEVEL2" + :cause nil, + :data nil}, + :data "{:level \"1\"}"}))) + + (let [e (binding [sut/*debug-mode* true] + (try + (malli/assert :int "string") + (catch Exception e + (sut/serialize-exception e))))] + (is + (= + (-> e + (dissoc :stack-trace) + (update-in [:data] map?)) + {:type "exception-info", + :class "clojure.lang.ExceptionInfo", + :message ":malli.core/coercion", + :cause nil, + :data true}))))) + + #?(:cljs + (testing "Serialization exception CLJS" + ;; ----------------- + ;; Simple Exceptions + ;; ----------------- + + (let [e (sut/serialize-exception + (js/Error. "controlled exception"))] + (is (= + (-> e + (dissoc :stack-trace)) + {:type "js-error" + :class "js/Error" + :message "controlled exception" + :cause nil + :data nil}))) + + (let [e (sut/serialize-exception + (ex-info "controlled exception" {}))] + (is (= + (-> e + (dissoc :stack-trace)) + {:type "exception-info", + :class "cljs.core.ExceptionInfo", + :message "controlled exception", + :cause nil + :data "{}"}))) + + (let [e (sut/serialize-exception + (ex-info "LEVEL1" {} + (ex-info "LEVEL2" {} + (js/Error. "LEVEL3"))))] + (is (= + (-> e + (dissoc :stack-trace) + (update-in [:cause] dissoc :stack-trace) + (update-in [:cause :cause] dissoc :stack-trace)) + {:type "exception-info", + :class "cljs.core.ExceptionInfo", + :message "LEVEL1", + :cause {:type "exception-info", + :class "cljs.core.ExceptionInfo", + :message "LEVEL2", + :cause {:type "js-error" + :class "js/Error" + :message "LEVEL3" + :cause nil + :data nil} + :data "{}"} + :data "{}"}))) + + (let [e (binding [sut/*debug-mode* true] + (try + (malli/assert :int "string") + (catch :default e + (sut/serialize-exception e))))] + (is + (= + (-> e + (dissoc :stack-trace) + (update-in [:data] map?)) + {:type "exception-info" + :class "cljs.core.ExceptionInfo" + :message ":malli.core/coercion" + :cause nil + :data true})))))) (deftest resolve-fn #?(:clj @@ -27,7 +182,7 @@ (is (= true (malli/validate sut/ResolvableFn 'str))) (is (= true (malli/validate sut/ResolvableFn str))) (is (= true (malli/validate sut/ResolvableFn :value))) - ;; Unsupported: + ;; Unsupported: (is (= false (malli/validate sut/ResolvableFn "clojure.core/str"))) (is (= false (malli/validate sut/ResolvableFn {}))) (is (= false (malli/validate sut/ResolvableFn []))) From 5e06f7eb8ede546bf83c3f51a23764e5efbfbeee Mon Sep 17 00:00:00 2001 From: SerhiiRI Date: Wed, 22 Oct 2025 13:04:37 +0300 Subject: [PATCH 3/9] - ADDED commando.commands.builtin. testcases for builtin commands. --- test/unit/commando/commands/builtin_test.cljc | 322 ++++++++++++++++++ test/unit/commando/core_test.cljc | 129 +------ test/unit/commando/impl/utils_test.cljc | 35 +- test/unit/commando/test_helpers.cljc | 40 +++ 4 files changed, 388 insertions(+), 138 deletions(-) create mode 100644 test/unit/commando/commands/builtin_test.cljc create mode 100644 test/unit/commando/test_helpers.cljc diff --git a/test/unit/commando/commands/builtin_test.cljc b/test/unit/commando/commands/builtin_test.cljc new file mode 100644 index 0000000..8bd762a --- /dev/null +++ b/test/unit/commando/commands/builtin_test.cljc @@ -0,0 +1,322 @@ +(ns commando.commands.builtin-test + (:require + #?(:cljs [cljs.test :refer [deftest is testing]] + :clj [clojure.test :refer [deftest is testing]]) + [commando.commands.builtin :as command-builtin] + [commando.core :as commando] + [commando.impl.utils :as commando-utils] + [malli.core :as malli] + [commando.test-helpers :as helpers])) + +;; =========================== +;; FN-SPEC +;; =========================== + +(deftest command-fn-spec + (testing "Successfull test cases" + (is (= + {:vec1 [1 2 3], :vec2 [3 2 1], :result-simple 10, :result-with-deps 10} + (:instruction + (commando/execute [command-builtin/command-fn-spec + command-builtin/command-from-spec] + {:vec1 [1 2 3] + :vec2 [3 2 1] + :result-simple {:commando/fn (fn [& [v1 v2]] (reduce + (map * v1 v2))) + :args [[1 2 3] + [3 2 1]]} + :result-with-deps {:commando/fn (fn [& [v1 v2]] (reduce + (map * v1 v2))) + :args [{:commando/from [:vec1]} + {:commando/from [:vec2]}]}}))) + "Uncorrectly processed :commando/fn")) + (testing "Failure test cases" + (is + (helpers/status-map-contains-error? + (binding [commando-utils/*debug-mode* true] + (commando/execute [command-builtin/command-fn-spec] + {:commando/fn "STRING" + :args [[1 2 3] [3 2 1]]})) + (fn [error] + (= + (-> error :error :data) + {:command-type :commando/fn, + :reason + {:commando/fn + [#?(:clj "Expected a fn, var of fn, symbol resolving to a fn" + :cljs "Expected a fn")]}, + :path [], + :value {:commando/fn "STRING", :args [[1 2 3] [3 2 1]]}}))) + "Waiting on error, bacause commando/fn has wrong type for :commando/fn") + (is + (helpers/status-map-contains-error? + (binding [commando-utils/*debug-mode* true] + (commando/execute [command-builtin/command-fn-spec] + {:commando/fn (fn [& [v1 v2]] (reduce + (map * v1 v2))) + :args "BROKEN"})) + (fn [error] + (= + (-> error :error :data (dissoc :value)) + {:command-type :commando/fn, + :reason {:args ["should be a coll"]}, + :path []}))) + "Waiting on error, bacause commando/fn has wrong type for :args key"))) + +;; =========================== +;; APPLY-SPEC +;; =========================== + +(deftest command-apply-spec + (testing "Successfull test cases" + (is (= + {:value 1, :result-simple 2, :result-with-deps 2} + (:instruction + (commando/execute [command-builtin/command-apply-spec + command-builtin/command-from-spec] + {:value 1 + :result-simple {:commando/apply {:value 1} + := (fn [e] (-> e :value inc))} + :result-with-deps {:commando/apply {:commando/from [:value]} + := inc}}))) + "Uncorrectly processed :commando/apply")) + (testing "Failure test cases" + (is + (helpers/status-map-contains-error? + (binding [commando-utils/*debug-mode* true] + (commando/execute [command-builtin/command-apply-spec] + {:commando/apply {:value 1} + := "STRING"})) + (fn [error] + (= + (-> error :error :data) + {:command-type :commando/apply, + :reason + {:= [#?(:clj "Expected a fn, var of fn, symbol resolving to a fn" + :cljs "Expected a fn")]}, + :path [], + :value {:commando/apply {:value 1}, := "STRING"}}))) + "Waiting on error, bacause commando/fn has wrong type for :commando/fn"))) + +;; =========================== +;; FROM-SPEC +;; =========================== + +(deftest command-from-spec + ;; ------------------- + (testing "Successfull test cases" + (is (= {:a 1, :vec 1, :vec-map 1, :result-of-another 1} + (get-in + (commando/execute [command-builtin/command-fn-spec + command-builtin/command-from-spec] + {"values" {:a 1 + :vec [1] + :vec-map [{:a 1}] + :result-of-another {:commando/fn (fn [& values] (apply + values)) + :args [-1 2]}} + "result" {:a {:commando/from ["values" :a]} + :vec {:commando/from ["values" :vec 0]} + :vec-map {:commando/from ["values" :vec-map 0 :a]} + :result-of-another {:commando/from ["values" :result-of-another]}}}) + [:instruction "result"])) + "Uncorrect extracting :commando/from by absolute path") + (is (= {:a {:value 1, :result 1}, + :b {:value 2, :result 2}, + :c {:value 3, :result [3]}, + :d {:result [4 4]}, + :e {:result [5 5]}} + (:instruction + (commando/execute [command-builtin/command-from-spec] + {:a {:value 1 + :result {:commando/from ["../" :value]}} + :b {:value 2 + :result {:commando/from ["../" :value]}} + :c {:value 3 + :result [{:commando/from ["../" "../" :value]}]} + :d {:result [4 + {:commando/from ["../" 0]}]} + :e {:result [5 + {:commando/from ["./" "../" 0]}]}}))) + "Uncorrect extracting :commando/from by relative path") + #?(:clj + (is (= {:=-keyword 1, :=-fn 2, :=-symbol 2, :=-var 2} + (get-in + (commando/execute [command-builtin/command-fn-spec + command-builtin/command-from-spec] + {"value" {:kwd 1} + "result" {:=-keyword {:commando/from ["value" ] := :kwd} + :=-fn {:commando/from ["value"] := (fn [{:keys [kwd]}] (inc kwd))} + :=-symbol {:commando/from ["value" :kwd] := 'inc} + :=-var {:commando/from ["value" :kwd] := #'inc}}}) + [:instruction "result"]) + ) + "Uncorrect commando/from ':=' applicator. CLJ Supports: fn/keyword/var/symbol") + :cljs (is (= {:=-keyword 1, :=-fn 2} + (get-in + (commando/execute [command-builtin/command-fn-spec + command-builtin/command-from-spec] + {"value" {:kwd 1} + "result" {:=-keyword {:commando/from ["value" ] := :kwd} + :=-fn {:commando/from ["value"] := (fn [{:keys [kwd]}] (inc kwd))}}}) + [:instruction "result"]) + ) + "Uncorrect commando/from ':=' applicator. CLJS Supports: fn/keyword"))) + ;; ------------------- + (testing "Failure test cases" + (is + (helpers/status-map-contains-error? + (commando/execute [command-builtin/command-from-spec] + {"source" {:a 1 :b 2} + "missing" {:commando/from ["UNEXISING"]}}) + {:message "Commando. Point dependency failed: key ':commando/from' references non-existent path [\"UNEXISING\"]", + :path ["missing"], + :command {:commando/from ["UNEXISING"]}}) + "Waiting on error, bacause commando/from seding to unexising path") + (is (helpers/status-map-contains-error? + (binding [commando-utils/*debug-mode* true] + (commando/execute + [command-builtin/command-from-spec] + {:commando/from "BROKEN"})) + (fn [error] + (= + (-> error :error :data) + {:command-type :commando/from, + :path [], + :value {:commando/from "BROKEN"} + :reason {:commando/from ["commando/from should be a sequence path to value in Instruction: [:some 2 \"value\"]"]}}))) + "Waiting on error, ':validate-params-fn' for commando/from. Corrupted path \"BROKEN\" ") + (is (helpers/status-map-contains-error? + (binding [commando-utils/*debug-mode* true] + (commando/execute + [command-builtin/command-from-spec] + {:v 1 + :a {:commando/from [:v] := ["BROKEN"]}})) + (fn [error] + (= + (-> error :error :data) + {:command-type :commando/from, + :reason {:= [#?(:clj "Expected a fn, var of fn, symbol resolving to a fn" + :cljs "Expected a fn") + "should be a string"]}, + :path [:a], :value {:commando/from [:v], := ["BROKEN"]}}))) + "Waiting on error, ':validate-params-fn' for commando/from. Wrong type for optional ':=' applicator"))) + + +;; =========================== +;; MUTATION-SPEC +;; =========================== + +(defmethod command-builtin/command-mutation :dot-product [_macro-type {:keys [vector1 vector2]}] + (malli/assert [:+ number?] vector1) + (malli/assert [:+ number?] vector2) + (reduce + (map * vector1 vector2))) + +(deftest command-mutation-spec + (testing "Successfull test cases" + (is (= + {:vector1 [1 2 3], :vector2 [3 2 1], :result-simple 10, :result-with-deps 10} + (:instruction + (commando/execute [command-builtin/command-mutation-spec + command-builtin/command-from-spec] + {:vector1 [1 2 3] + :vector2 [3 2 1] + :result-simple {:commando/mutation :dot-product + :vector1 [1 2 3] + :vector2 [3 2 1]} + :result-with-deps {:commando/mutation :dot-product + :vector1 {:commando/from [:vector1]} + :vector2 {:commando/from [:vector2]}}}))) + "Uncorrectly processed :commando/mutation in dot-product example")) + (testing "Failure test cases" + (is + (helpers/status-map-contains-error? + (binding [commando-utils/*debug-mode* true] + (commando/execute [command-builtin/command-mutation-spec] + {:commando/mutation (fn [] "BROKEN")})) + (fn [error] + (= + (-> error :error :data (dissoc :value)) + {:command-type :commando/mutation + :reason {:commando/mutation ["should be a keyword"]} + :path []}))) + "Waiting on error, bacause commando/mutation has wrong type for :commando/mutation") + (is + (helpers/status-map-contains-error? + (binding [commando-utils/*debug-mode* true] + (commando/execute [command-builtin/command-mutation-spec] + {:commando/mutation :dot-product + :vector1 [1 "_" 3] + :vector2 [3 2 1]})) + (fn [error] + (= + (-> error :error (helpers/remove-stacktrace) (dissoc :data)) + #?(:cljs + {:type "exception-info" + :class "cljs.core.ExceptionInfo" + :message ":malli.core/coercion" + :cause nil} + :clj + {:type "exception-info", + :class "clojure.lang.ExceptionInfo" + :message ":malli.core/coercion" + :cause nil})))) + "Waiting on error, error(malli/assert) raised inside the :dot-product mutation"))) + +;; =========================== +;; MACRO-SPEC +;; =========================== + +(defmethod command-builtin/command-macro :string-vectors-dot-product [_macro-type {:keys [vector1-str vector2-str]}] + {:= :dot-product + :commando/apply + {:vector1-str vector1-str + :vector2-str vector2-str + ;; ------- + ;; Parsing + :vector1 + {:commando/fn (fn [str-vec] + #?(:clj (mapv #(Integer/parseInt %) str-vec) + :cljs (mapv #(js/parseInt %) str-vec))) + :args [{:commando/from [:commando/apply :vector1-str]}]} + :vector2 + {:commando/fn (fn [str-vec] + #?(:clj (mapv #(Integer/parseInt %) str-vec) + :cljs (mapv #(js/parseInt %) str-vec))) + :args [{:commando/from [:commando/apply :vector2-str]}]} + ;; ----------- + ;; Dot Product + :dot-product + {:commando/fn (fn [& [v1 v2]] (reduce + (map * v1 v2))) + :args [{:commando/from [:commando/apply :vector1]} + {:commando/from [:commando/apply :vector2]}]}}}) + +(deftest command-macro-spec + (testing "Successfull test cases" + (is + (= + {:vector-dot-1 32, :vector-dot-2 320} + (:instruction + (commando/execute + [command-builtin/command-macro-spec + command-builtin/command-fn-spec + command-builtin/command-from-spec + command-builtin/command-apply-spec] + {:vector-dot-1 + {:commando/macro :string-vectors-dot-product + :vector1-str ["1" "2" "3"] + :vector2-str ["4" "5" "6"]} + :vector-dot-2 + {:commando/macro :string-vectors-dot-product + :vector1-str ["10" "20" "30"] + :vector2-str ["4" "5" "6"]}}))))) + (testing "Failure test cases" + (is + (helpers/status-map-contains-error? + (binding [commando-utils/*debug-mode* true] + (commando/execute [command-builtin/command-macro-spec] + {:commando/macro (fn [])})) + (fn [error] + (= + (-> error :error :data (dissoc :value)) + {:command-type :commando/macro, + :reason {:commando/macro ["should be a keyword"]}, + :path []}))) + "Waiting on error, bacause commando/mutation has wrong type for :commando/mutation"))) + diff --git a/test/unit/commando/core_test.cljc b/test/unit/commando/core_test.cljc index 2ac5fcc..178038d 100644 --- a/test/unit/commando/core_test.cljc +++ b/test/unit/commando/core_test.cljc @@ -6,26 +6,9 @@ [commando.core :as commando] [commando.impl.command-map :as cm] [commando.impl.utils :as commando-utils] + [commando.test-helpers :as helpers] [malli.core :as malli])) -;; ------- -;; Helpers -;; ------- - -(defn status-map-contains-error? - [status-map error] - (if (commando/failed? status-map) - (let [error-lookup-fn (cond - (string? error) (fn [e] (= (:message e) error)) - (map? error) (fn [e] (= e error)) - (fn? error) error - :else nil)] - (if error-lookup-fn (some? (first (filter error-lookup-fn (:errors status-map)))) false)) - false)) - -;; ----- -;; Tests -;; ----- (def test-add-id-command {:type :test/add-id @@ -783,6 +766,7 @@ cmds-builtin/command-fn-spec cmds-builtin/command-apply-spec cmds-builtin/command-mutation-spec + cmds-builtin/command-macro-spec test-add-id-command]) (def from-instruction @@ -1052,12 +1036,14 @@ (:status (commando/build-compiler (:valid-registry build-compiler-test-data) (:valid-instruction build-compiler-test-data)))) "Returns :ok status for valid registry and instruction") - (is (status-map-contains-error? (commando/build-compiler (:valid-registry build-compiler-test-data) - (:cyclic-instruction build-compiler-test-data)) + (is (helpers/status-map-contains-error? (commando/build-compiler + (:valid-registry build-compiler-test-data) + (:cyclic-instruction build-compiler-test-data)) "Commando. sort-entities-by-deps. Detected cyclic dependency") "Returns :failed status for cyclic dependencies") - (is (status-map-contains-error? (commando/build-compiler (:malformed-registry build-compiler-test-data) - (:valid-instruction build-compiler-test-data)) + (is (helpers/status-map-contains-error? (commando/build-compiler + (:malformed-registry build-compiler-test-data) + (:valid-instruction build-compiler-test-data)) "Invalid registry specification") "Returns :failed status for malformed registry")) (testing "Basic functionality" @@ -1073,18 +1059,19 @@ (:status (commando/build-compiler [cmds-builtin/command-from-spec] (:invalid-ref-instruction build-compiler-test-data)))) "Invalid reference causes failure") - (is (status-map-contains-error? + (is (helpers/status-map-contains-error? (commando/build-compiler [cmds-builtin/command-from-spec] (:invalid-ref-instruction build-compiler-test-data)) "Commando. Point dependency failed: key ':commando/from' references non-existent path [\"nonexistent\"]") "Error information is populated") - (is (status-map-contains-error? (commando/build-compiler [] (:cmd-instruction build-compiler-test-data)) + (is (helpers/status-map-contains-error? (commando/build-compiler [] (:cmd-instruction build-compiler-test-data)) "Invalid registry specification") "Error cause the empty registry")) (testing "Edge cases" (is (commando/ok? (commando/build-compiler [test-add-id-command] {"data" "no-commands"})) "Registry with no matching commands") - (is (status-map-contains-error? (commando/build-compiler (repeat 5 test-add-id-command) - (:large-instruction build-compiler-test-data)) + (is (helpers/status-map-contains-error? (commando/build-compiler + (repeat 5 test-add-id-command) + (:large-instruction build-compiler-test-data)) "Invalid registry specification") "duplicate commands in registry cause an error") (is (= 50 @@ -1364,93 +1351,3 @@ (is (= 3 (get-in (:instruction result1) ["0"])) "Original calculation correct") (is (= 3000 (get-in (:instruction result2) ["0"])) "Modified calculation correct"))))) - -(deftest execute-wrong-validate-params-fn - (testing "':validate-params-fn' for commando/from" - (is (status-map-contains-error? - (binding [commando-utils/*debug-mode* true] - (commando/execute - [cmds-builtin/command-from-spec] - {:commando/from "BROKEN" := []})) - (fn [error] - (= (-> error :error :data) - {:command-type :commando/from, - :path [], - :value {:commando/from "BROKEN", := []} - :reason {:commando/from ["commando/from should be a sequence path to value in Instruction: [:some 2 \"value\"]"], - := [ - #?(:clj "Expected a fn, var of fn, symbol resolving to a fn" - :cljs "Expected a fn") - "should be a string"]}})))) - - (is (status-map-contains-error? - (binding [commando-utils/*debug-mode* true] - (commando/execute - [cmds-builtin/command-from-json-spec] - {"commando-from" "BROKEN" "=" ""})) - (fn [error] - (= (-> error :error :data) - {:command-type :commando/from-json, - :path [], - :value {"commando-from" "BROKEN", "=" ""} - :reason - {"commando-from" ["commando-from should be a sequence path to value in Instruction: [\"some\" 2 \"value\"]"], - "=" ["should be at least 1 character"]}}))))) - - (testing "':validate-params-fn' for commando/apply" - (is - (status-map-contains-error? - (binding [commando-utils/*debug-mode* true] - (commando/execute - [cmds-builtin/command-apply-spec] - {:commando/apply nil := "123"})) - (fn [error] - (= (-> error :error :data) - {:command-type :commando/apply, - :path [], - :value {:commando/apply nil, := "123"} - :reason {:= [#?(:clj "Expected a fn, var of fn, symbol resolving to a fn" - :cljs "Expected a fn")]}}))))) - - (testing "':validate-params-fn' for commando/fn" - (is - (status-map-contains-error? - (binding [commando-utils/*debug-mode* true] - (commando/execute - [cmds-builtin/command-fn-spec] - {:commando/fn "BROKEN" :args "BROKEN"})) - (fn [error] - (= (-> error :error :data) - {:command-type :commando/fn, - :path [], - :value {:commando/fn "BROKEN" :args "BROKEN"} - :reason {:commando/fn - [#?(:clj "Expected a fn, var of fn, symbol resolving to a fn" - :cljs "Expected a fn")], - :args ["should be a coll"]}}))))) - - (testing "':validate-params-fn' for commando/mutation" - (is - (status-map-contains-error? - (binding [commando-utils/*debug-mode* true] - (commando/execute - [cmds-builtin/command-mutation-spec] - {:commando/mutation nil})) - (fn [error] - (= (-> error :error :data) - {:command-type :commando/mutation, - :path [], - :value {:commando/mutation nil} - :reason {:commando/mutation ["should be a keyword"]}})))) - (is - (status-map-contains-error? - (binding [commando-utils/*debug-mode* true] - (commando/execute - [cmds-builtin/command-mutation-json-spec] - {"commando-mutation" 888})) - (fn [error] - (= (-> error :error :data) - {:command-type :commando/mutation-json, - :path [], - :value {"commando-mutation" 888} - :reason {"commando-mutation" ["should be a string"]}})))))) diff --git a/test/unit/commando/impl/utils_test.cljc b/test/unit/commando/impl/utils_test.cljc index 900ccc6..91328d6 100644 --- a/test/unit/commando/impl/utils_test.cljc +++ b/test/unit/commando/impl/utils_test.cljc @@ -3,6 +3,7 @@ #?(:cljs [cljs.test :refer [deftest is testing]] :clj [clojure.test :refer [deftest is testing]]) [clojure.set :as set] + [commando.test-helpers :as helpers] [commando.impl.utils :as sut] [malli.core :as malli])) @@ -16,7 +17,7 @@ (sut/serialize-exception (RuntimeException/new "controlled exception"))] (is (= - (dissoc e :stack-trace) + (helpers/remove-stacktrace e) {:type "runtime-exception", :class "java.lang.RuntimeException", :message "controlled exception", @@ -26,7 +27,7 @@ (let [e (sut/serialize-exception (ex-info "controlled exception" {}))] (is (= - (dissoc e :stack-trace) + (helpers/remove-stacktrace e) {:type "exception-info", :class "clojure.lang.ExceptionInfo", :message "controlled exception", @@ -36,7 +37,7 @@ (let [e (sut/serialize-exception (Exception/new "controlled exception"))] (is (= - (dissoc e :stack-trace) + (helpers/remove-stacktrace e) {:type "throwable", :class "java.lang.Exception", :message "controlled exception", @@ -49,10 +50,7 @@ (ex-info "LEVEL2" {:level "2"} (ex-info "LEVEL2" {:level "3"}))))] (is (= - (-> e - (dissoc :stack-trace) - (update-in [:cause] dissoc :stack-trace) - (update-in [:cause :cause] dissoc :stack-trace)) + (helpers/remove-stacktrace e) {:type "exception-info", :class "clojure.lang.ExceptionInfo", :message "LEVEL1", @@ -74,9 +72,7 @@ (NullPointerException/new "LEVEL2")))] (is (= - (-> e - (dissoc :stack-trace) - (update-in [:cause] dissoc :stack-trace)) + (helpers/remove-stacktrace e) {:type "exception-info", :class "clojure.lang.ExceptionInfo", :message "LEVEL1", @@ -96,8 +92,8 @@ (is (= (-> e - (dissoc :stack-trace) - (update-in [:data] map?)) + (helpers/remove-stacktrace) + (update :data map?)) {:type "exception-info", :class "clojure.lang.ExceptionInfo", :message ":malli.core/coercion", @@ -113,8 +109,7 @@ (let [e (sut/serialize-exception (js/Error. "controlled exception"))] (is (= - (-> e - (dissoc :stack-trace)) + (helpers/remove-stacktrace e) {:type "js-error" :class "js/Error" :message "controlled exception" @@ -124,8 +119,7 @@ (let [e (sut/serialize-exception (ex-info "controlled exception" {}))] (is (= - (-> e - (dissoc :stack-trace)) + (helpers/remove-stacktrace e) {:type "exception-info", :class "cljs.core.ExceptionInfo", :message "controlled exception", @@ -137,10 +131,7 @@ (ex-info "LEVEL2" {} (js/Error. "LEVEL3"))))] (is (= - (-> e - (dissoc :stack-trace) - (update-in [:cause] dissoc :stack-trace) - (update-in [:cause :cause] dissoc :stack-trace)) + (helpers/remove-stacktrace e) {:type "exception-info", :class "cljs.core.ExceptionInfo", :message "LEVEL1", @@ -163,8 +154,8 @@ (is (= (-> e - (dissoc :stack-trace) - (update-in [:data] map?)) + (helpers/remove-stacktrace) + (update :data map?)) {:type "exception-info" :class "cljs.core.ExceptionInfo" :message ":malli.core/coercion" diff --git a/test/unit/commando/test_helpers.cljc b/test/unit/commando/test_helpers.cljc new file mode 100644 index 0000000..9ac4422 --- /dev/null +++ b/test/unit/commando/test_helpers.cljc @@ -0,0 +1,40 @@ +(ns commando.test-helpers + (:require + [commando.core :as commando])) + +(defn remove-stacktrace + "Example + (remove-stacktrace + (commando.impl.utils/serialize-exception + (ex-info \"LEVEL1\" {:level \"1\"} + (ex-info \"LEVEL2\" {:level \"2\"})))) + => + {:type \"exception-info\", + :class \"clojure.lang.ExceptionInfo\", + :message \"LEVEL1\", + :cause + {:type \"exception-info\", + :class \"clojure.lang.ExceptionInfo\", + :message \"LEVEL2\", + :cause nil, + :data \"{:level \\\"2\\\"}\"}, + :data \"{:level \\\"1\\\"}\"}" + [exception] + (-> exception + (dissoc :stack-trace) + (update :cause (fn [cause] + (when cause + (remove-stacktrace cause)))))) + +(defn status-map-contains-error? + [status-map error] + (if (commando/failed? status-map) + (let [error-lookup-fn (cond + (string? error) (fn [e] (= (:message e) error)) + (map? error) (fn [e] (= e error)) + (fn? error) error + :else nil)] + (if error-lookup-fn (some? (first (filter error-lookup-fn (:errors status-map)))) false)) + false)) + + From f7377c9ae62023fad415c7f92778a19526e08898 Mon Sep 17 00:00:00 2001 From: SerhiiRI Date: Wed, 22 Oct 2025 22:08:56 +0300 Subject: [PATCH 4/9] - ADDED QueryDSL. Added test-cases for new types of resolver. Proper unit testing for QueryExpression corner cases like sequentual return data. - FIXED QueryDSL. Fixed small issue with Exception on cljs. - FIXED QueryDSL. Automaticaly handle in ->query-run the any different types which are not the Resolvers object Co-authored-by: Julia47 --- src/commando/commands/query_dsl.cljc | 227 +------ .../commando/commands/query_dsl_test.cljc | 591 +++++++++++++++--- 2 files changed, 490 insertions(+), 328 deletions(-) diff --git a/src/commando/commands/query_dsl.cljc b/src/commando/commands/query_dsl.cljc index 033c658..c4d1aab 100644 --- a/src/commando/commands/query_dsl.cljc +++ b/src/commando/commands/query_dsl.cljc @@ -259,13 +259,13 @@ (->query-run (try (run-resolve-fn resolver_data QueryExpressionKeyProperties) - (catch Exception e + (catch #?(:clj Exception :cljs :default) e (-> (smap/status-map-pure) (smap/status-map-handle-error {:message (str exception-message-header "resolve-fn. Finished return exception") :error (commando-utils/serialize-exception e)})))) internal-keys))) - resolver)) + (->query-run resolver internal-keys))) (defn ^:private trying-to-value [maybe-resolver] (if (resolver? maybe-resolver) @@ -558,226 +558,3 @@ (command-resolve (get m "commando-resolve") (dissoc m "commando-resolve"))) :dependencies {:mode :all-inside}}) -(comment - - (require 'commando.commands.builtin) - (def registry - (commando/create-registry - [command-resolve-spec - commando.commands.builtin/command-fn-spec - commando.commands.builtin/command-from-spec - commando.commands.builtin/command-apply-spec])) - - (defn execute-with-registry [instruction] - (:instruction - (commando.core/execute - registry - instruction))) - - (defmethod command-resolve :test-instruction-qe [_ {:keys [x QueryExpression]}] - (let [x (or x 10)] - (-> {:string "Value" - - :map {:a - {:b {:c x} - :d {:c (inc x) - :f (inc (inc x))}}} - - :coll [{:a - {:b {:c x} - :d {:c (inc x) - :f (inc (inc x))}}} - {:a - {:b {:c x} - :d {:c (dec x) - :f (dec (dec x))}}}] - - :resolve-fn (resolve-fn "default value for resolve-fn" - (fn [{:keys [x]}] - (let [y (or x 1) - range-y (if (< 10 y) 10 y)] - (for [z (range 0 range-y)] - {:a - {:b {:c (+ y z)} - :d {:c (inc (+ y z)) - :f (inc (inc (+ y z)))}}})))) - - :resolve-fn-error (resolve-fn "default value for resolve-fn" - (fn [{:keys [_x]}] - (throw (ex-info "Exception" {:error "no reason"})))) - - :resolve-fn-call (for [x (range 10)] - (resolve-fn (str "default value for resolve-fn-call -> " x) - (fn [properties] - (let [x (or (:x properties) x)] - {:a {:b x}})))) - - :resolve-instruction (resolve-instruction "default value for resolve-instruction" - {:commando/fn (fn [count-elements] - (vec - (for [x (range 0 count-elements)] - {:a - {:b {:c x} - :d {:c (inc x) - :f (inc (inc x))}}}))) - :args [2]}) - - :resolve-instruction-with-error (resolve-instruction "default value for resolve-instruction-with-error" - {:commando/fn (fn [& _body] - (throw (ex-info "Exception" {:error "no reason"}))) - :args []}) - - :resolve-instruction-qe (resolve-instruction-qe "default value for resolve-instruction-qe" - {:commando/resolve :test-instruction-qe - :x 1})} - (->query-run QueryExpression)))) - - ;; ---------------- - ;; Simple attribute - (execute-with-registry - {:commando/resolve :test-instruction-qe - :x 1 - :QueryExpression - [:string]}) - - ;; -------- - ;; Defaults - (execute-with-registry - {:commando/resolve :test-instruction-qe - :x 1 - :QueryExpression - [:string - :map - :coll - :resolve-fn - :resolve-instruction - :resolve-instruction-qe]}) - - ;; ---------------------------- - ;; Resolving data Map and Colls - (execute-with-registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [:string - {:map - [{:a - [:b]}]} - {:coll - [{:a - [:b]}]}]}) - - ;; ---------- - ;; resolve-fn - (execute-with-registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{:resolve-fn - [{:a - [:b]}]}]}) - - - ;; resolve-fn with overriding `:x` - (execute-with-registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{[:resolve-fn {:x 1000}] - [{:a - [:b]}]}]}) - - ;; --------------------------- - ;; resolver packed in sequence - (execute-with-registry - {:commando/resolve :test-instruction-qe - :QueryExpression - [{:resolve-fn-call - [{:a - [:b]}]}]}) - - ;; resolver packed in sequence with overrding - (execute-with-registry - {:commando/resolve :test-instruction-qe - :QueryExpression - [{[:resolve-fn-call {:x 100}] - [{:a - [:b]}]}]}) - - ;; ------------------- - ;; resolve-instruction - (execute-with-registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{:resolve-instruction - [{:a - [:b]}]}]}) - - ;; resolve-instruction with overrding `:args` - (execute-with-registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{[:resolve-instruction - {:args [100]}] - [{:a - [:b]}]}]}) - - ;; --------------------- - ;; resolve-instruction-qe - (execute-with-registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{:resolve-instruction-qe - [{:map [{:a [:b]}]}]}]}) - - ;; loop resolve-instruction-qe with override `:x` - (execute-with-registry - {:commando/resolve :test-instruction-qe - :x 1 - :QueryExpression - [{:resolve-instruction-qe - [{:map [{:a - [:b]}]} - {[:resolve-instruction-qe {:x 1000}] - [{:map [{:a - [:b]}]} - {[:resolve-instruction-qe {:x 10000000}] - [{:map [{:a - [:b]}]}]}]}]}]}) - - ;; ----------------------------- - ;; ERRORS inside the instruction - - ;; 1. Key `:EEE` is not existing - (execute-with-registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [:EEE - {:resolve-fn - [{:a - [:b - :EEE]}]}]}) - - ;; 2. Internal instruction return an error - (execute-with-registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{:resolve-instruction-with-error - [{:a [:b]}]}]}) - - ;; 3. Cause the resolve-fn is an simple function - ;; call, so there are handled in try..catch and - ;; return as a status-map exception either. - (execute-with-registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{:resolve-fn-error - [:a]}]}) - - ) diff --git a/test/unit/commando/commands/query_dsl_test.cljc b/test/unit/commando/commands/query_dsl_test.cljc index d0f278c..f17fb71 100644 --- a/test/unit/commando/commands/query_dsl_test.cljc +++ b/test/unit/commando/commands/query_dsl_test.cljc @@ -2,16 +2,18 @@ (:require #?(:cljs [cljs.test :refer [deftest is testing]] :clj [clojure.test :refer [deftest is testing]]) - [commando.commands.builtin :as cmds-builtin] - [commando.commands.query-dsl :as cmds-query-dsl] - [commando.core :as commando])) - + [clojure.string :as string] + [commando.commands.builtin :as command-builtin] + [commando.commands.query-dsl :as command-query-dsl] + [commando.impl.utils :as commando-utils] + [commando.core :as commando] + [commando.test-helpers :as helpers])) (def ^:private db {:permissions [{:id 1 :permission-name "add-doc" - :options [{:type "pdf" :preview true}, {:type "rtf" :preview false }, {:type "odt" :preview false}]} + :options [{:type "pdf" :preview true}, {:type "rtf" :preview false}, {:type "odt" :preview false}]} {:id 2 :permission-name "remove-doc" :options [{:type "pdf" :preview true}, {:type "rtf" :preview false}, {:type "odt" :preview false}]} @@ -33,7 +35,14 @@ :name "Lois Griffin" :email "lois@yahoo.net" :password "imlois77" - :permission ["remove-doc"]}]}) + :permissions ["remove-doc"]}]}) + +(def registry + (commando/create-registry + [command-query-dsl/command-resolve-spec + command-builtin/command-fn-spec + command-builtin/command-from-spec + command-builtin/command-apply-spec])) (defn get-permissions-by-name [permission-name] (first (filter (fn [x] (= permission-name (:permission-name x))) (:permissions db)))) @@ -41,111 +50,487 @@ (defn get-user-by-email [email] (first (filter (fn [x] (= email (:email x))) (:users db)))) -(defmethod cmds-query-dsl/command-resolve :query-permission +(defmethod command-query-dsl/command-resolve :query-permission [_ {:keys [permission-name QueryExpression]}] (-> (get-permissions-by-name permission-name) - (cmds-query-dsl/->query-run QueryExpression))) -(defmethod cmds-query-dsl/command-resolve :query-user + (command-query-dsl/->query-run QueryExpression))) + +(defmethod command-query-dsl/command-resolve :query-user [_ {:keys [email QueryExpression]}] (let [user-info (get-user-by-email email)] (when user-info - (cmds-query-dsl/->>query-run + (command-query-dsl/->>query-run QueryExpression - {:id (:id user-info) - :name (:name user-info) - :email (:email user-info) - :password (clojure.string/replace (:password user-info) #"." "*") - :permissions (cmds-query-dsl/query-resolve - (:permissions user-info) - (mapv - (fn [permission-name] + {:id (:id user-info) + :name (:name user-info) + :email (:email user-info) + :password (string/replace (:password user-info) #"." "*") + :permissions (mapv + (fn [permission-name] + (command-query-dsl/resolve-instruction-qe + permission-name {:commando/resolve :query-permission - :permission-name permission-name}) - (:permissions user-info)))})))) + :permission-name permission-name})) + (:permissions user-info))})))) (deftest black-box-test-query + (testing "Testing query on mock-data (permissions and users)" + (is + (= + {:id 2, + :name "Peter Griffin", + :email "peter@mail.com", + :password "*********", + :permissions ["add-doc"]} + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-user ;; resolver + :email "peter@mail.com" + :QueryExpression + [:id + :name + :email + :password + :permissions]}))) + "Wrong resolving user and permisssions.") + + (is + (= + {:id 2, + :name "Peter Griffin", + :email "peter@mail.com", + :password "*********", + :permissions + [{:permission-name "add-doc", + :options [{:type "pdf"} {:type "rtf"} {:type "odt"}]}]} + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-user + :email "peter@mail.com" + :QueryExpression + [:id + :name + :email + :password + {:permissions + [:permission-name + {:options + [:type]}]}]}))) + "Test user query with nested and joined permission data. Should return only needed value :type from nested map") + + (is + (= + {:id 2, + :name "Peter Griffin" + :UNEXISTING-FIELD {:status :failed, + :errors [{:message "Commando. QueryDSL. QueryExpression. Attribute ':UNEXISTING-FIELD' is unreachable."}]}} + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-user + :email "peter@mail.com" + :QueryExpression + [:id + :name + :UNEXISTING-FIELD]}))) + "Test query for a non-existent attribute on a user. The resolver should return an error map for the unreachable field.") + + (is + (= + {:id 3, + :name "Lois Griffin", + :email "lois@yahoo.net", + :password "********", + :user-role + {:status :failed, + :errors + [{:message + "Commando. QueryDSL. QueryExpression. Attribute ':user-role' is unreachable."}]}} + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-user + :email "lois@yahoo.net" + :QueryExpression + [:id + :name + :email + :password + {:user-role + [:id + :permission-name]}]}))) + "Test user with a mismatched :user-role key in the DB. The resolver should return error.") + + (is + (= + {:id 2, + :name "Peter Griffin", + :email "peter@mail.com", + :password "*********", + :permissions [{:id 3, :permission-name "none", :options []}]} + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-user + :email "peter@mail.com" + :QueryExpression + [:id + :name + :email + :password + {[:permissions {:permission-name "none"}] + [:id + :permission-name + :options]}]}))) + "Test parameterized query. This EQL query should override the default join logic and query for a specific permission, even one the user does not have.") + + (is + (= + {:id 3 + :permission-name "none" + :options []} + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-permission + :permission-name "none" + :QueryExpression + [:id + :permission-name + :options]}))) + "Test direct query for a permission that has an empty nested list (:options).") + + (is + (nil? + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-user + :email "nonexistent@user.com" + :QueryExpression + [:id :name]}))) + "Test query for a non-existent user. Result should be nil."))) + +(defmethod command-query-dsl/command-resolve :test-instruction-qe [_ {:keys [x QueryExpression]}] + (let [x (or x 10)] + (-> {:string "Value" + + :map {:a + {:b {:c x} + :d {:c (inc x) + :f (inc (inc x))}}} + + :coll [{:a + {:b {:c x} + :d {:c (inc x) + :f (inc (inc x))}}} + {:a + {:b {:c x} + :d {:c (dec x) + :f (dec (dec x))}}}] + + :resolve-fn (command-query-dsl/resolve-fn + "default value for resolve-fn" + (fn [{:keys [x]}] + (let [y (or x 1) + range-y (if (< 10 y) 10 y)] + (for [z (range 0 range-y)] + {:a + {:b {:c (+ y z)} + :d {:c (inc (+ y z)) + :f (inc (inc (+ y z)))}}})))) + + :resolve-fn-error (command-query-dsl/resolve-fn + "default value for resolve-fn" + (fn [{:keys [_x]}] + (throw (ex-info "Exception" {:error "no reason"})))) + + :coll-resolve-fn (for [x (range 10)] + (command-query-dsl/resolve-fn + "default value for resolve-fn-call" + (fn [properties] + (let [x (or (:x properties) x)] + {:a {:b x}})))) + + :resolve-instruction (command-query-dsl/resolve-instruction + "default value for resolve-instruction" + {:commando/fn (fn [count-elements] + (vec + (for [x (range 0 count-elements)] + {:a + {:b {:c x} + :d {:c (inc x) + :f (inc (inc x))}}}))) + :args [2]}) + + :resolve-instruction-with-error (command-query-dsl/resolve-instruction + "default value for resolve-instruction-with-error" + {:commando/fn (fn [& _body] + (throw (ex-info "Exception" {:error "no reason"}))) + :args []}) + + :resolve-instruction-qe (command-query-dsl/resolve-instruction-qe + "default value for resolve-instruction-qe" + {:commando/resolve :test-instruction-qe + :x 1})} + (command-query-dsl/->query-run QueryExpression)))) - (testing "Querying data from resolver" - (is - (= - {:id 2, - :name "Peter Griffin", - :email "peter@mail.com", - :password "*********", - :permissions ["add-doc"]} - (:instruction - (commando.core/execute - [commando.commands.query-dsl/command-resolve-spec] - {:commando/resolve :query-user - :email "peter@mail.com" - :QueryExpression - [:id - :name - :email - :password - :permissions]}))))) - - (testing "Querying and Resolving data" - (is - (= - {:id 2, - :name "Peter Griffin", - :email "peter@mail.com", - :password "*********", - :permissions - [{:permission-name "add-doc", - :options [{:type "pdf"} {:type "rtf"} {:type "odt"}]}]} - (:instruction - (commando.core/execute - [commando.commands.query-dsl/command-resolve-spec] - {:commando/resolve :query-user - :email "peter@mail.com" - :QueryExpression - [:id - :name - :email - :password - {:permissions - [:permission-name - {:options - [:type]}]}]}))))) - - (testing "Querying unexising-data" - (is - (= - {:id 2, - :name "Peter Griffin" - :UNEXISTING-FIELD {:status :failed, :errors [{:message "Commando. Graph Query. QueryExpression attribute ':UNEXISTING-FIELD' is unreachable"}]}} - (:instruction - (commando.core/execute - [commando.commands.query-dsl/command-resolve-spec] - {:commando/resolve :query-user - :email "peter@mail.com" - :QueryExpression - [:id - :name - :UNEXISTING-FIELD]}))))) - - (testing "Overriding EQL data while quering" - (is - (= - {:id 2, - :name "Peter Griffin", - :email "peter@mail.com", - :password "*********", - :permissions [{:id 3, :permission-name "none", :options []}]} - (:instruction - (commando.core/execute - [commando.commands.query-dsl/command-resolve-spec] - {:commando/resolve :query-user - :email "peter@mail.com" - :QueryExpression - [:id - :name - :email - :password - {[:permissions {:permission-name "none"}] - [:id - :permission-name - :options]}]})))))) +(deftest query-expression-test + (testing "Succesfull execution" + (is + (= + {:string "Value"} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 1 + :QueryExpression + [:string]}))) + "Returns a single attribute :string for query.") + + (is + (= + {:string "Value", + :map {:a {:b {:c 1}, :d {:c 2, :f 3}}}, + :coll [{:a {:b {:c 1}, :d {:c 2, :f 3}}} + {:a {:b {:c 1}, :d {:c 0, :f -1}}}], + :resolve-fn "default value for resolve-fn", + :resolve-instruction "default value for resolve-instruction", + :resolve-instruction-qe "default value for resolve-instruction-qe"} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 1 + :QueryExpression + [;; simple data + :string + :map + :coll + ;; data from resolvers + :resolve-fn + :resolve-instruction + :resolve-instruction-qe]}))) + "Returns defaults for queried data.") + + (is + (= + {:string "Value", + :map {:a {:b {:c 20}}}, + :coll [{:a {:b {:c 20}}} {:a {:b {:c 20}}}]} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [:string + {:map + [{:a + [:b]}]} + {:coll + [{:a + [:b]}]}]}))) + "Returns nested data by non-resolver data types.") + + (is + (= + {:resolve-fn [{:a {:b {:c 1}}}]} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-fn + [{:a + [:b]}]}]}))) + "Return data for resolver. The resolving procedure for 'resolve-fn'") + + (is + (= + {:resolve-fn + [{:a {:b {:c 1000}}} + {:a {:b {:c 1001}}} + {:a {:b {:c 1002}}} + {:a {:b {:c 1003}}} + {:a {:b {:c 1004}}} + {:a {:b {:c 1005}}} + {:a {:b {:c 1006}}} + {:a {:b {:c 1007}}} + {:a {:b {:c 1008}}} + {:a {:b {:c 1009}}}]} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{[:resolve-fn {:x 1000}] + [{:a + [:b]}]}]}))) + "Return data for resolver and overriding it params. Resolving procedure for 'resolve-fn'") + + (is + (= + {:coll-resolve-fn + [{:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}}]} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :QueryExpression + [{[:coll-resolve-fn {:x 100}] + [{:a + [:b]}]}]})))) + + (is + (= + {:resolve-instruction + [{:a {:b {:c 0}}} + {:a {:b {:c 1}}}]} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-instruction + [{:a + [:b]}]}]})))) + + (is + (= + {:resolve-instruction + [{:a {:b {:c 0}}} + {:a {:b {:c 1}}} + {:a {:b {:c 2}}} + {:a {:b {:c 3}}} + {:a {:b {:c 4}}}]} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{[:resolve-instruction + {:args [5]}] + [{:a + [:b]}]}]})))) + + (is + (= + {:resolve-instruction-qe {:map {:a {:b {:c 1}}}}} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-instruction-qe + [{:map [{:a [:b]}]}]}]})))) + + (is + (= + {:resolve-instruction-qe + {:map {:a {:b {:c 1}}}, + :resolve-instruction-qe + {:map {:a {:b {:c 1000}}}, + :resolve-instruction-qe + {:map {:a {:b {:c 10000000}}}}}}} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 1 + :QueryExpression + [{:resolve-instruction-qe + [{:map [{:a + [:b]}]} + {[:resolve-instruction-qe {:x 1000}] + [{:map [{:a + [:b]}]} + {[:resolve-instruction-qe {:x 10000000}] + [{:map [{:a + [:b]}]}]}]}]}]}))))) + + (testing "Failing exception" + (is + (= + {:EEE + {:status :failed, + :errors + [{:message + "Commando. QueryDSL. QueryExpression. Attribute ':EEE' is unreachable."}]}, + :resolve-fn + [{:a + {:b {:c 1}, + :EEE + {:status :failed, + :errors + [{:message + "Commando. QueryDSL. QueryExpression. Attribute ':EEE' is unreachable."}]}}}]} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [:EEE + {:resolve-fn + [{:a + [:b + :EEE]}]}]})))) + + (is + (helpers/status-map-contains-error? + (get-in + (binding [commando-utils/*debug-mode* true] + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-fn-error + [:a]}]})) + [:instruction :resolve-fn-error]) + (fn [error] + (= + {:type "exception-info", + :message "Exception" + :cause nil, + :data {:error "no reason"}} + (-> error :error helpers/remove-stacktrace (dissoc :class)))))) + + (is + (helpers/status-map-contains-error? + (get-in + (binding [commando-utils/*debug-mode* true] + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-instruction-with-error + [{:a [:b]}]}]})) + [:instruction :resolve-instruction-with-error]) + (fn [error] + (= + {:type "exception-info", + :message "Exception" + :cause nil, + :data {:error "no reason"}} + (-> error :error helpers/remove-stacktrace (dissoc :class)))))))) From 73385a1b24e366e9cd66067f1d4412f25e5d0390 Mon Sep 17 00:00:00 2001 From: SerhiiRI Date: Thu, 23 Oct 2025 08:56:02 +0300 Subject: [PATCH 5/9] - UPDATED Readme.md . Add documentation about the macros --- README.md | 149 ++++++++++++++++++++--------- src/commando/commands/builtin.cljc | 4 +- 2 files changed, 104 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 17086ff..63a5d17 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ - [command-fn-spec](#command-fn-spec) - [command-apply-spec](#command-apply-spec) - [command-mutation-spec](#command-mutation-spec) + - [command-macro-spec](#command-macro-spec) - [Adding New Commands](#adding-new-commands) - [Status-Map and Internals](#status-map-and-internals) - [Debugging commando](#debugging-commando) @@ -133,12 +134,12 @@ The `:commando/from` command supports relative paths like `"../"`, `"./"` for ac [commands-builtin/command-from-spec] {"incrementing 1" {"1" 1 - "2" {:commando/from ["../" "1"] := inc} - "3" {:commando/from ["../" "2"] := inc}} + "2" {:commando/from ["../" "1"] := inc} + "3" {:commando/from ["../" "2"] := inc}} "decrementing 1" {"1" 1 - "2" {:commando/from ["../" "1"] := dec} - "3" {:commando/from ["../" "2"] := dec}}}) + "2" {:commando/from ["../" "1"] := dec} + "3" {:commando/from ["../" "2"] := dec}}}) ;; => ;; {"incrementing 1" {"1" 1, "2" 2, "3" 3}, ;; "decrementing 1" {"1" 1, "2" 0, "3" -1}} @@ -161,9 +162,9 @@ A convenient wrapper over `apply`. "v2" 2 "sum=" {:commando/fn + - :args [{:commando/from ["v1"]} - {:commando/from ["v2"]} - 3]}}) + :args [{:commando/from ["v1"]} + {:commando/from ["v2"]} + 3]}}) ;; => {"v1" 1 "v2" 2 "sum=" 6} ``` @@ -175,13 +176,13 @@ A wrapper similar to `commando/fn`, but conceptually closer to `commando/from`, (commando/execute [commands-builtin/command-apply-spec] {"0" {:commando/apply - {"1" {:commando/apply - {"2" {:commando/apply - {"3" {:commando/apply {"4" {:final "5"}} - := #(get % "4")}} - := #(get % "3")}} - := #(get % "2")}} - := #(get % "1")}}) + {"1" {:commando/apply + {"2" {:commando/apply + {"3" {:commando/apply {"4" {:final "5"}} + := #(get % "4")}} + := #(get % "3")}} + := #(get % "2")}} + := #(get % "1")}}) ;; => {"0" {:final "5"}} ``` @@ -194,19 +195,19 @@ Imagine the following instruction is your initial database migration, adding use [commands-builtin/command-from-spec commands-builtin/command-mutation-spec] {"add-new-user-01" {:commando/mutation :add-user :name "Bob Anderson" - :permissions [{:commando/from ["perm_send_mail"] := :id} - {:commando/from ["perm_recv_mail"] := :id }]} + :permissions [{:commando/from ["perm_send_mail"] := :id} + {:commando/from ["perm_recv_mail"] := :id }]} "add-new-user-02" {:commando/mutation :add-user :name "Damian Nowak" - :permissions [{:commando/from ["perm_recv_mail"] := :id}]} + :permissions [{:commando/from ["perm_recv_mail"] := :id}]} "perm_recv_mail" {:commando/mutation :add-permission - :name "receive-email-notification"} + :name "receive-email-notification"} "perm_send_mail" {:commando/mutation :add-permission - :name "send-email-notification"}}) + :name "send-email-notification"}}) ``` You can see that you need both :add-permission and :add-user commands. In most cases, such patterns can be abstracted and reused, simplifying your migrations and business logic. -`commando-mutation-spec` uses `defmethod commando/command-mutation` underneath, making it easy to wrap business logic into commands and integrate them into your instructions/migrations: +`commando-mutation-spec` uses `defmethod commando.commands.builtin/command-mutation` underneath, making it easy to wrap business logic into commands and integrate them into your instructions/migrations: ```clojure (defmethod commands-builtin/command-mutation :add-user [_ {:keys [name permissions]}] @@ -224,8 +225,64 @@ You can see that you need both :add-permission and :add-user commands. In most c This approach enables you to quickly encapsulate business logic into reusable commands, which can then be easily composed in your instructions or migrations. +#### command-macro-spec -### Adding new commands +Allows describing reusable command templates that are expanded into regular Commando commands at runtime. This is useful when you want to describe a pattern for building a complex command or a set of related commands without duplicating the same structure throughout an instruction + +Asume we have a Instruction what calculates mean. +```clojure +(commando/execute + [commands-builtin/command-from-spec + commands-builtin/command-apply-spec + commands-builtin/command-fn-spec] + {:= :result + :commando/apply + {:vector-of-numbers [1, 2, 3, 4, 5] + :result + {:fn (fn [& [vector-of-numbers]] + (/ (reduce + 0 vector-of-numbers) + (count vector-of-numbers))) + :args [{:commando/from [:commando/apply :vector-of-numbers]}]}}}) +;; => 3 +``` + +This works, but the structure is not very easy to read when repeated. When you need the same mean calculation many times, the instruction quickly grows and becomes hard to follow. A macro can help by encapsulating the pattern into a readable reusable shortcut. + +Define a macro + +```clojure +(defmethod commands-builtin/command-macro :mean-calc [{vector-of-numbers :vector-of-numbers}] + {:= :result + :commando/apply + {:vector-of-numbers vector-of-numbers + :result + {:fn (fn [& [vector-of-numbers]] + (/ (reduce + 0 vector-of-numbers) + (count vector-of-numbers))) + :args [{:commando/from [:commando/apply :vector-of-numbers]}]}}}) + + +(commando/execute + [commands-builtin/command-macro-spec + commands-builtin/command-from-spec + commands-builtin/command-apply-spec + commands-builtin/command-fn-spec] + {:v1 {:commando/macro :mean-calc :vector-of-numbers [1, 2, 3, 4, 5]} + :v2 {:commando/macro :mean-calc :vector-of-numbers [10, 22, 33]} + :v3 {:commando/macro :mean-calc :vector-of-numbers [7, 8, 1000, 1]}}) +;; => +;; {:v1 3 +;; :v2 21.666 +;; :v3 254} +``` + +command-macro-spec detects entries with `:commando/macro` and calls the multimethod `(defmethod) commands-builtin/command-macro` using the macro identifier (e.g. `:mean-calc`) and the parameter map from the instruction. + +The defmethod should return a Instruction. Commando will then treat that returned map as a fully separate instruction: dependencies (like `:commando/from`) are discovered inside the macro hierarchy. + +Use these macro handlers to hide repeated command structure and keep your instructions shorter and easier to read. + +### Adding new commands As you start using commando, you will start writing your own command specs to match your data needs. @@ -256,9 +313,9 @@ Let's create a new command using a CommandMapSpec configuration map: {:type :CALC= :recognize-fn #(and (map? %) (contains? % :CALC=)) :validate-params-fn (fn [m] - (and - (fn? (:CALC= m)) - (not-empty (:ARGS m)))) + (and + (fn? (:CALC= m)) + (not-empty (:ARGS m)))) :apply (fn [_instruction _command m] (apply (:CALC= m) (:ARGS m))) :dependencies {:mode :all-inside}} @@ -270,7 +327,7 @@ Let's create a new command using a CommandMapSpec configuration map: - `:apply` - the function that directly executes the command as params it receives whole instruction, command spec and as a last argument what was recognized by :cm/recognize - `:dependencies` - describes the type of dependency this command has. Commando supports three modes: - `{:mode :all-inside}` - the command scans itself for dependencies on other commands within its body. - - `{:mode :none}` - the command has no dependencies and can be evaluated whenever. + - `{:mode :none}` - the command has no dependencies and can be evaluated whenever. - `{:mode :point :point-key :commando/from}` - allowing to be dependent anywhere in the instructions. Expects point-key which tells where is the dependency (commando/from as an example uses this) Now you can use it for more expressive operations like "summ=" and "multiply=" as shown below: @@ -278,19 +335,19 @@ Now you can use it for more expressive operations like "summ=" and "multiply=" a ```clojure (def command-registry (commando/create-registry - [;; Add `:commando/from` - commands-builtin/command-from-spec - ;; Add `:CALC=` command to be handled - ;; inside instruction - {:type :CALC= - :recognize-fn #(and (map? %) (contains? % :CALC=)) - :validate-params-fn (fn [m] - (and - (fn? (:CALC= m)) - (not-empty (:ARGS m)))) - :apply (fn [_instruction _command m] - (apply (:CALC= m) (:ARGS m))) - :dependencies {:mode :all-inside}}])) + [;; Add `:commando/from` + commands-builtin/command-from-spec + ;; Add `:CALC=` command to be handled + ;; inside instruction + {:type :CALC= + :recognize-fn #(and (map? %) (contains? % :CALC=)) + :validate-params-fn (fn [m] + (and + (fn? (:CALC= m)) + (not-empty (:ARGS m)))) + :apply (fn [_instruction _command m] + (apply (:CALC= m) (:ARGS m))) + :dependencies {:mode :all-inside}}])) (commando/execute command-registry @@ -322,7 +379,7 @@ The concept of a **command** is not limited to map structures it is basically an [{:type :custom/json :recognize-fn #(and (string? %) (clojure.string/starts-with? % "json")) :apply (fn [_instruction _command-map string-value] - (clojure.data.json/read-str (apply str (drop 4 string-value)) + (clojure.data.json/read-str (apply str (drop 4 string-value)) :key-fn keyword)) :dependencies {:mode :none}}] {:json-command-1 "json{\"some-json-value-1\": 123}" @@ -424,12 +481,12 @@ You can set dynamic variable `commando.impl.utils/*debug-mode* true` to see more (binding [commando-utils/*debug-mode* true] (execute - [commands-builtin/command-from-spec] - {"1" 1 - "2" {:commando/from ["1"]} - "3" {:commando/from ["2"]}})) - -;; RETURN => + [commands-builtin/command-from-spec] + {"1" 1 + "2" {:commando/from ["1"]} + "3" {:commando/from ["2"]}})) + +;; RETURN => {:status :ok, :instruction {"1" 1, "2" 1, "3" 1} :registry @@ -474,7 +531,7 @@ You can set dynamic variable `commando.impl.utils/*debug-mode* true` to see more - [Commando QueryDSL](./doc/query_dsl.md) - [Example Http commando transit handler + Reitit](./doc/example_reitit.clj) -# Versioning +# Versioning We comply with: [Break Versioning](https://www.taoensso.com/break-versioning) `..[-]` diff --git a/src/commando/commands/builtin.cljc b/src/commando/commands/builtin.cljc index 611205e..0f4443f 100644 --- a/src/commando/commands/builtin.cljc +++ b/src/commando/commands/builtin.cljc @@ -95,9 +95,6 @@ ;; From ;; ====================== -"" - - (def ^{:doc " Description @@ -464,3 +461,4 @@ (:instruction result) (throw (ex-info (str utils/exception-message-header "command-macro. Failure execution :commando/macro") result))))) :dependencies {:mode :all-inside}}) + From 3c5344b0891235a030d823e0d0b0e48fa0abf9bd Mon Sep 17 00:00:00 2001 From: Serhii Riznychuk Date: Thu, 23 Oct 2025 12:49:41 +0300 Subject: [PATCH 6/9] Revise and expand Query DSL documentation - UPDATED the Commando Query DSL documentation to enhance clarity and detail, including examples and explanations of core concepts, lazy resolution, parameterization, and real-world applications. --- doc/query_dsl.md | 588 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 478 insertions(+), 110 deletions(-) diff --git a/doc/query_dsl.md b/doc/query_dsl.md index 5e7e8c8..5f8d339 100644 --- a/doc/query_dsl.md +++ b/doc/query_dsl.md @@ -1,15 +1,416 @@ -# Commando Query DSL +
+

Query DSL

+
-This mechanism positions itself as a built-in, lightweight, and somewhat limited alternative to GraphQL or Pathom3 (Pathom3 is a data query and transformation library for Clojure). Commando DSL is much more primitive and requires you to describe dependency resolution yourself. Let's look at how it works. +The Commando Query DSL is a built-in, lightweight query mechanism. It serves as a simple alternative to more comprehensive solutions like GraphQL or Pathom3, but it is more primitive and requires you to define dependency resolution manually. +Its primary purpose is to provide a way to: -## Example Database Setup +1. **Selectively query data, returning only the fields they request.** + +2. **Handle nested data dependencies through lazy-loading resolvers.** + +## Content + +- [Core Concept](#core-concept) +- [Workaround](#workaround) +- [Lazy Resolution](#lazy-resolution) + - [Lazy Resolution Examples](#lazy-resolution-examples) +- [Parameterization and Overriding](#parameterization-and-overriding) + - [Parametrization Examples](#parametrization-examples) +- [Real-World Example](#real-world-example) + - [Examples Queries](#examples-queries) +- [Advanced Topics](#advanced-topics) + - [Combining Mutations and Queries](#combining-mutations-and-queries) + - [Working with JSON](#working-with-json) +- [Summary](#summary) + +## Core Concept + +The Query DSL is enabled by adding `commando.commands.query-dsl/command-resolve-spec` to your `(commando.core/execute [] )` call. + +You define your data "endpoints" by creating new methods for the `commando.commands.query-dsl/command-resolve` multimethod. + +**The `QueryExpression` and `->query-run`** + +A resolver's job is to return a map(or sequence) of data. The client passes a `QueryExpression` to specify which keys from that map they want. The QueryExpression is a simple, EQL-inspired vector. + +You use the `commands-query-dsl/->query-run` function to filter your resolver's resulting map against the client's QueryExpression. + +Here is the "Hello, World!" of the Query DSL: ```clojure (require '[commando.core :as commando]) -(require '[commando.commands.builtin :as commands-builtin]) -(require '[commando.commands.query-dsl :as commands-query-dsl]) +(require '[commando.commands.query-dsl :as query-dsl]) + +;; Define a resolver for :resolve-user +(defmethod query-dsl/command-resolve :resolve-user [_ {:keys [QueryExpression]}] + ;; This map is the "full" data available + (-> {:first-name "Adam" + :last-name "Nowak" + :info {:age 25 + :passport {:number "FE123123"}}} + ;; ->query-run filters the map based on the QueryExpression + (query-dsl/->query-run QueryExpression))) + +;; Execute the command +(commando/execute + [query-dsl/command-resolve-spec] + {:commando/resolve :resolve-user + :QueryExpression + [:first-name ;; Request :first-name + {:info ;; Request :info + [:passport]}]}) ;; and from :info, request :passport +;; => +;; {:status :ok, +;; :instruction +;; {:first-name "Adam", +;; :info {:passport +;; {:number "FE123123"}}}} +``` + +Notice that `:last-name` and `:info {:age ...}` are not returned. The `->query-run` function processed the `QueryExpression` and returned only the requested keys. + +## Workaround + +To simplify examples, we define a small helper function `execute-with-registry` that sets up the command registry with the necessary Query DSL and built-in commands. + +```clojure +(require 'commando.commands.builtin) +(require '[commando.commands.query-dsl :as query-dsl]) + +(defn execute-with-registry [instruction] + (:instruction + (commando.core/execute + [query-dsl/command-resolve-spec + commando.commands.builtin/command-fn-spec + commando.commands.builtin/command-from-spec] + instruction))) +``` + +## Lazy Resolution + +What if a field is expensive to compute and not always needed? Instead of putting the data directly in the map, you can insert a **resolver object**. + +A resolver object that holds: + +1. A **default value**: Returned if the key is requested, but not queried into. +2. A **resolver function/instruction**: Executed only if the client provides a sub-query for that key. + +There are several types of resolver constructors: + +- `query-dsl/resolve-fn`: Lazily runs an arbitrary function. + +- `query-dsl/resolve-instruction`: Lazily runs any Commando instruction (e.g., a mutation/fn/macro/from ... any). + +- `query-dsl/resolve-instruction-qe`: Lazily runs another `:commando/resolve` command, allowing for nested Query DSL queries. This is the most common way to link resolvers. + + +### Lazy Resolution Examples + +```clojure +(defmethod query-dsl/command-resolve :test-instruction-qe [_ {:keys [x QueryExpression]}] + (let [x (or x 10)] + (-> {;; ================================================================ + ;; ordinary data + ;; ================================================================ + :string "Value" + + :map {:a + {:b {:c x} + :d {:c x + :f x}}} + + :coll [{:a + {:b {:c x} + :d {:c x + :f x}}} + {:a + {:b {:c x} + :d {:c x + :f x}}}] + + ;; ================================================================ + ;; resolve-fn examples + ;; ================================================================ + + :resolve-fn (query-dsl/resolve-fn "default value for resolve-fn" + (fn [{:keys [x]}] + (let [x (or x 1)] + {:a + {:b {:c x} + :d {:c x + :f x}}}))) + + :resolve-fn-of-colls (query-dsl/resolve-fn "default value for resolve-fn" + (fn [{:keys [x]}] + (let [x (or x 1)] + (for [y (range 0 10)] + {:a + {:b {:c (+ y x)} + :d {:c (+ y x) + :f (+ y x)}}})))) + + :colls-of-resolve-fn (for [y (range 10)] + (query-dsl/resolve-fn "default value for resolve-fn-call" + (fn [{:keys [x]}] + (let [x (or x 1)] + {:a + {:b {:c (+ y x)} + :d {:c (+ y x) + :f (+ y x)}}})))) + + ;; ================================================================ + ;; resolve-instruction examples + ;; ================================================================ + + :resolve-instruction (query-dsl/resolve-instruction "default value for resolve-instruction" + {:value-x 1 + :result {:commando/fn (fn [& [value]] + {:a + {:b {:c value} + :d {:c (inc value) + :f (inc (inc value))}}}) + :args [{:commando/from [:value-x]}]}}) + + ;; ================================================================ + ;; resolve-instruction-qe examples + ;; ================================================================ + + + :resolve-instruction-qe (query-dsl/resolve-instruction-qe "default value for resolve-instruction-qe" + {:commando/resolve :test-instruction-qe + :x 1}) + :resolve-instruction-qe-of-coll (query-dsl/resolve-instruction-qe "default value for resolve-instruction-qe" + (vec + (for [x (range 5)] + {:commando/resolve :test-instruction-qe + :x x}))) + :coll-of-resolve-instruction-qe (for [x (range 5)] + (query-dsl/resolve-instruction-qe "default value for resolve-instruction-qe" + {:commando/resolve :test-instruction-qe + :x x}))} + (query-dsl/->query-run QueryExpression)))) +``` + +#### Query 1: Querying ordinary data + +Here, we simply query for ordinary data keys (`:string`, `:map`, `:coll`). No lazy resolvers are triggered. + +- `:string` key returns a simple string value. + +- `:map` key returns a nested map, trimmed to requrested sub-query `[:a [:b]]`. + +- `:coll` key returns a vector of maps, each trimmed to the requested sub-query `[:a [:b]]`. From the side of QueryExpression no difference between a single map or a collection of maps, both are queried the same way. + +```clojure +(execute-with-registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [:string + {:map + [{:a + [:b]}]} + {:coll + [{:a + [:b]}]}]}) +;; => +;; {:string "Value", +;; :map {:a {:b {:c 20}}}, +;; :coll [{:a {:b {:c 20}}} +;; {:a {:b {:c 20}}}]} +``` + +#### Query 2: Querying for Default Values + +If we ask for the lazy keys (`:resolve-fn`, `:resolve-instruction-qe`, etc.) _without_ providing a _sub-query_, we get their default values. + +```clojure +(execute-with-registry + {:commando/resolve :test-instruction-qe + :x 1 + :QueryExpression + [:string + :map + :coll + :resolve-fn + :resolve-fn-of-colls + :colls-of-resolve-fn + :resolve-instruction + :resolve-instruction-qe + :resolve-instruction-qe-of-coll + :coll-of-resolve-instruction-qe + ]}) +;; => +;; {:string "Value", +;; :map {:a {:b {:c 1}, :d {:c 1, :f 1}}}, +;; :coll [{:a {:b {:c 1}}} +;; {:a {:b {:c 1}}}] +;; :resolve-fn "default value for resolve-fn", +;; :resolve-fn-of-colls "default value for resolve-fn" +;; :colls-of-resolve-fn +;; ["default value for resolve-fn-call" +;; "default value for resolve-fn-call" +;; "default value for resolve-fn-call" +;; "default value for resolve-fn-call" +;; "default value for resolve-fn-call" +;; "default value for resolve-fn-call" +;; "default value for resolve-fn-call" +;; "default value for resolve-fn-call" +;; "default value for resolve-fn-call" +;; "default value for resolve-fn-call"] +;; :resolve-instruction "default value for resolve-instruction" +;; :resolve-instruction-qe "default value for resolve-instruction-qe" +;; :resolve-instruction-qe-of-coll "default value for resolve-instruction-qe" +;; :coll-of-resolve-instruction-qe +;; ["default value for resolve-instruction-qe" +;; "default value for resolve-instruction-qe" +;; "default value for resolve-instruction-qe" +;; "default value for resolve-instruction-qe" +;; "default value for resolve-instruction-qe"]} +``` + +#### Query 3: Triggering Lazy Resolvers + +Now, if we provide a _sub-query_ for a lazy key, the DSL will execute the resolver and then use the sub-query to _filter its result_. + +Here we provide a sub-query `[{:a [:b]}]` for `:resolve-fn`. This triggers the function, and we get the resolved data back, filtered. + +```clojure +(execute-with-registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-fn + [{:a + [:b]}]}]}) +;; => +;; {:resolve-fn {:a {:b {:c 1}}}} +``` + +The same applies to `resolve-instruction-qe`. Here, we trigger a nested, recursive call to `:test-instruction-qe`. + +```clojure +(execute-with-registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-instruction-qe + [{:map [{:a [:b]}]}]}]}) +;; => +;; {:resolve-instruction-qe {:map {:a {:b {:c 1}}}}} +``` +This recursive/nested resolution is the key to building relationships between your data. + + +## Parameterization and Overriding + +How do you pass parameters to a nested resolver? You can "parameterize" a key in the QueryExpression using the `[ {}]` syntax. + +These parameters are passed to the resolver function (`resolve-fn`) or merged into the instruction map (`resolve-instruction`, `resolve-instruction-qe`). + +This also allows a client to override parameters that might have been set by a parent resolver. + +### Parametrization Examples + +Let's look at a simple resolver and how we can override its parameters. + +```clojure +(defmethod query-dsl/command-resolve :query/mixed-data [_ {:keys [x QueryExpression]}] + (-> + [{:a {:b {:c x} + :d {:c (inc x) :f (dec x)}}} + {:a {:b {:c x} + :d {:c (inc x) :f (dec x)}}} + {:a {:b {:c x} + :d {:c (inc x) :f (dec x)}}}] + (query-dsl/->query-run QueryExpression))) + +(defmethod query-dsl/command-resolve :query/top-level [_ {:keys [x QueryExpression]}] + (let [x (or x 10)] + (-> {:string "value" + + :map {:a + {:b {:c x} + :d {:c x + :f x}}} + + :mixed-data (query-dsl/resolve-instruction-qe + ;; default value + [] + ;; instruction to run + {:commando/resolve :query/mixed-data + :x x})} + (query-dsl/->query-run QueryExpression)))) +``` + +#### Query 1: Query default values + +Asking for `:mixed-data` but don't query into it. We get the default value (an empty vector []). + +```clojure +(execute-with-registry + {:commando/resolve :query/top-level + :x 1 + :QueryExpression + [:string + :mixed-data]}) +;; => +;; {:string "value", +;; :mixed-data []} +``` + +#### Query 2: Nested Resolution with using Sub-Query + +Now, we provide a sub-query for `:mixed-data`. This triggers the `resolve-instruction-qe`, which calls `:query/mixed-data`. The `:x 1` from the top-level(our query) instruction is passed down. + +```clojure +(execute-with-registry + {:commando/resolve :query/top-level + :x 1 + :QueryExpression + [:string + {:mixed-data + [{:a + [{:b + [:c]}]}]}]}) +;; => +;; {:string "value", +;; :mixed-data +;; [{:a {:b {:c 1}}} +;; {:a {:b {:c 1}}} +;; {:a {:b {:c 1}}}]} +``` + +#### Query 3: Sub-Query with Overriding Parameters + +Finally, we use the `[ {}]` syntax. The QueryExpression (`[:mixed-data {:x 1000}]`) itself provides a new value for `:x 1000` just for the resolver under `:mixed-data` key. This new parameter map is merged with the _instruction_ inside the `query-dsl/resolve-instruction-qe` , overriding the original `:x 1`. + +```clojure +(execute-with-registry + {:commando/resolve :query/top-level + :x 1 + :QueryExpression + [:string + {[:mixed-data {:x 1000}] ;; <--- Parameter override + [{:a + [{:b + [:c]}]}]}]}) +;; => +;; {:string "value", +;; :mixed-data +;; [{:a {:b {:c 1000}}} +;; {:a {:b {:c 1000}}} +;; {:a {:b {:c 1000}}}]} +``` + +## Real-World Example + +Let's combine these concepts. Assume we have a "database" of cars and emission standards. + +```clojure (defn db [] {:emission-standard [{:id "Euro 6" :year_from "2014"} @@ -52,97 +453,61 @@ This mechanism positions itself as a built-in, lightweight, and somewhat limited :price_usd 23000}]}) ``` +Now, let's define three resolvers: -## Namespace and Resolver - -The namespace `commando.commands.query-dsl` exposes the `command-resolve-spec` command, which is extended with the multimethod `command-resolve`. Compared to mutations via `commando.core/command-mutation-spec`, this approach is focused on query expressions and their return values. - -### QueryExpression - -A `QueryExpression` is a simplified structure inspired by EQL (Extensible Query Language), designed for passing through so-called resolvers and returning only the requested data keys. - -```clojure -(require '[commando.core :as commando]) -(require '[commando.commands.query-dsl :as commands-query-dsl]) - -(defmethod commands-query-dsl/command-resolve :resolve-user [_ {:keys [QueryExpression]}] - (-> {:first-name "Adam" - :last-name "Nowak" - :info {:age 25 - :passport {:number "FE123123"}}} - (->query-run QueryExpression))) - -(commando/execute - [commands-query-dsl/command-resolve-spec] - {:commando/resolve :resolve-user - :QueryExpression - [:first-name - {:info - [:passport]}]}) - -;; RETURN => -;; {:status :ok, -;; :instruction -;; {:first-name "Adam", -;; :info {:passport -;; {:number "FE123123"}}}} -``` - -Notice that `:last-name` is not returned, as `->query-run` only returns the requested keys. - -- `commands-query-dsl/->query-run` receives a `QueryExpression` and determines what to return to the user as a result. -- `commands-query-dsl/query-resolve` is an object constructor that will be processed by `->query-run`. It takes two arguments: the default value and an inner instruction. +1. `:eco_standard-by-id`: Fetches a standard from the "db". +2. `:car-by-id`: Fetches a single car. Notice how it replaces the :eco_standard ID with a lazy resolve-instruction-qe pointing to our other resolver. This is manual dependency resolution. -## Example Query Resolvers +3. `:car-id-range`: Fetches a list of cars. It fans out the work by mapping a list of IDs to a list of resolve-instruction-qe objects, each one calling :car-by-id. -Let's look at a more complex example: ```clojure -(defmethod commands-query-dsl/command-resolve :eco_standard-by-id [_ {:keys [eco_standard-id QueryExpression]}] +(defmethod query-dsl/command-resolve :eco_standard-by-id [_ {:keys [eco_standard-id QueryExpression]}] (when-let [emission-standard (first (filter #(= eco_standard-id (:id %)) (get (db) :emission-standard)))] (-> emission-standard - (commands-query-dsl/->query-run QueryExpression)))) + (query-dsl/->query-run QueryExpression)))) -(defmethod commands-query-dsl/command-resolve :car-by-id [_ {:keys [car-id engine-as-string? QueryExpression]}] +(defmethod query-dsl/command-resolve :car-by-id [_ {:keys [car-id engine-as-string? QueryExpression]}] (when-let [car-entity (first (filter #(= car-id (:id %)) (get (db) :cars)))] + ;; We modify the car entity before returning it (cond-> car-entity - true (update-in [:details :eco_standard] (fn [eco_standard-id] - ;; (query-resolve take two arg: , ) - ;; If the user will ask about keys inside the :eco_standard - ;; inner Instruction will be executed automatically. - (commands-query-dsl/query-resolve eco_standard-id - {:commando/resolve :eco_standard-by-id - :eco_standard-id eco_standard-id}))) + ;; Replace the :eco_standard string with a lazy resolver + true (update-in [:details :eco_standard] + (fn [eco_standard-id] + ;; (resolve-instruction-qe takes , ) + ;; If the user will ask about keys inside :eco_standard, + ;; this inner Instruction will be executed automatically. + (query-dsl/resolve-instruction-qe eco_standard-id + {:commando/resolve :eco_standard-by-id + :eco_standard-id eco_standard-id}))) + ;; Conditionally modify data based on a parameter engine-as-string? (update-in [:details :engine] (fn [e] (pr-str e))) - true (commands-query-dsl/->query-run QueryExpression)))) + ;; Filter the final result + true (query-dsl/->query-run QueryExpression)))) -(defmethod commands-query-dsl/command-resolve :car-id-range [_ {:keys [ids-to-query QueryExpression]}] +(defmethod query-dsl/command-resolve :car-id-range [_ {:keys [ids-to-query QueryExpression]}] (as-> (set ids-to-query) <> (keep (fn [{:keys [id]}] (when (contains? <> id) id)) (get (db) :cars)) {:car-id-range (mapv (fn [car-id] - (commands-query-dsl/query-resolve car-id + ;; For each ID, return a lazy resolver for that car + (query-dsl/resolve-instruction-qe car-id {:commando/resolve :car-by-id - :car-id car-id})) <>)} - (commands-query-dsl/->query-run <> QueryExpression))) -``` - -To execute a query, use the following shortcut function: - -```clojure -(defn query [instruction-map] - (commando/execute - [commands-query-dsl/command-resolve-spec] - instruction-map)))) + :car-id car-id})) <>)} + (query-dsl/->query-run <> QueryExpression))) ``` -## Sample Queries +we used our `execute-with-registry` to make querying easier: + +### Examples Queries -Let's make a query for three resolvers. On the top level, we write a resolver that filters the list to the IDs we want: +#### Query 1: Top-Level Query Only + +We query for :car-id-range but do not provide a sub-query. The resolver runs, but the nested resolve-instruction-qe calls do not. We get their default values (the car-id strings). ```clojure -(query +(execute-with-registry {:commando/resolve :car-id-range :ids-to-query ["2" "4" "100"] :QueryExpression @@ -151,10 +516,20 @@ Let's make a query for three resolvers. On the top level, we write a resolver th ;; {:car-id-range ["2" "4"]} ``` -When we specify which keys we want in `QueryExpression`, the resolver `:car-by-id` is triggered for each ID and returns only the requested fields: +#### Query 2: Nested Query + +Now we provide a sub-query for `:car-id-range`: + +- This triggers the list of `:car-by-id` resolvers. + +- Each `:car-by-id` resolver runs. + +- We query for `:details :eco_standard`, but we don't query into it. + +- Therefore, we get the default value for `:eco_standard` (the `eco_standard-id` string, "Euro 6"). ```clojure -(query +(execute-with-registry {:commando/resolve :car-id-range :ids-to-query ["2" "4" "100"] :QueryExpression @@ -175,29 +550,16 @@ When we specify which keys we want in `QueryExpression`, the resolver `:car-by-i ;; :details {:eco_standard "Euro 6", :engine {:horsepower 389}}}]} ``` -### Parameterization - -QueryExpression supports parameterization at the declaration level. To override configuration, you can pass parameters for specific resolvers. +#### Query 3: Parameterized and Deeply Nested Query -```clojure -[:car] -[{:car - [:make - :model]}] -;; With added params => -[[:car {:SOME-KEY-PASSED-TO-RESOLVER true}]] -[{[:car {:SOME-KEY-PASSED-TO-RESOLVER true}] - [:make - :model]}] -``` +Now, we use parameterization to modify the behavior of nested resolvers. +1. `[[:car-id-range {:engine-as-string? true}]]`: We pass the `:engine-as-string?` parameter to the `:car-id-range` resolver, which in turn passes it to each `:car-by-id` resolver. You can see the :engine map is now a string. -Parameters are passed only through keys defined for a specific resolver via `commando-query/command-resolve`. - -For example, we add an optional parameter `:engine-as-string?` for serializing the `:engine` key. +2. `[[:eco_standard {:eco_standard-id "Zero Emission"}]]`: We provide a sub-query and an override parameter for `:eco_standard`. This triggers the `:eco_standard-by-id` resolver and overrides its ID, forcing it to return "Zero Emission" for both cars. ```clojure -(query +(execute-with-registry {:commando/resolve :car-id-range :ids-to-query ["2" "4" "100"] :QueryExpression @@ -206,9 +568,9 @@ For example, we add an optional parameter `:engine-as-string?` for serializing t :model {:details [{[:eco_standard {:eco_standard-id "Zero Emission"}] - [:id - :year_from]} - :engine]}]}]}) + [:id + :year_from]} + :engine]}]}]}) ;; RETURN => ;; {:car-id-range ;; [{:make "Toyota", @@ -223,15 +585,19 @@ For example, we add an optional parameter `:engine-as-string?` for serializing t ;; :engine "{:type \"Hybrid\", :horsepower 389}"}}]} ``` -Because the mechanism is limited only by the CommandMapSpec (`commando.toolbox.graph-query-dsl/command-resolve-spec`), you can easily combine it with other mutation commands, etc.: +## Advanced Topics + +### Combining Mutations and Queries + +Because the Query DSL is built on Commando, you can easily combine it with other commands, like mutations or `:commando/from`, in a single `execute` call. ```clojure (commando.core/execute - [commands-query-dsl/command-resolve-spec - commands-builtin/command-mutation-spec - commands-builtin/command-from-spec] + [commando.commands.query-dsl/command-resolve-spec + commando.commands.builtin/command-mutation-spec + commando.commands.builtin/command-from-spec] {"client-that-want-buy-a-car" - {:commando/resolve :find-user-by-login "adam12N"} + {:commando/resolve :find-user-by-login :login "adam12N"} "car-client-want-to-buy" {:commando/resolve :car-by-id :id "2" @@ -242,7 +608,7 @@ Because the mechanism is limited only by the CommandMapSpec (`commando.toolbox.g {:details [{[:eco_standard {:eco_standard-id "Zero Emission"}] [:id - :year_from]} + :year_from]} :engine]}]}} "transaction" {:commando/mutation :car-sell-agreement @@ -253,24 +619,26 @@ Because the mechanism is limited only by the CommandMapSpec (`commando.toolbox.g :option/color "crystal red"}) ``` -## Working with JSON +### Working with JSON + +To work with JSON input (e.g. from an HTTP request), use the command-resolve-json-spec command. Use string keys to describe Instructions (instead `:commando/resolve` use `"commando-resolve"`) and QueryExpressions. -To work with JSON values, use the `command-resolve-json-spec` command. +Note that defmethod dispatches on a string ("instant-car-model") and the parameters map (:strs [QueryExpression]) uses string-based destructuring. Cause QueryExpression uses strings, the resolvers must also use string keys in their returned maps. ```clojure -(defmethod command-resolve "instant-car-model" [_ {:strs [QueryExpression]}] - (->query-run +(defmethod query-dsl/command-resolve "instant-car-model" [_ {:strs [QueryExpression]}] + (query-dsl/->query-run {"id" "4", "make" "BMW", "model" "X5", "details" {"year" 2023, - "engine" {"type" "Hybrid", "horsepower" 389}, - "eco_standard" "Euro 6"}, + "engine" {"type" "Hybrid", "horsepower" 389}, + "eco_standard" "Euro 6"}, "price_usd" 65000} QueryExpression)) (commando.core/execute - [commands-query-dsl/command-resolve-json-spec] + [commando.commands.query-dsl/command-resolve-json-spec] (clojure.data.json/read-str "{\"commando-resolve\":\"instant-car-model\", \"QueryExpression\": @@ -290,4 +658,4 @@ To work with JSON values, use the `command-resolve-json-spec` command. ## Summary -This DSL is designed for advanced users familiar with Clojure and the Commando library. The structure is intentionally simple to encourage custom resolver logic and composability. For a full overview of commands and concepts, see the main README file +This DSL is designed for advanced users familiar with Clojure and the Commando library. The structure is intentionally simple to encourage custom resolver logic and composability. For a full overview of commands and concepts, see the main [README](../README.md) file. From 6d2bf99b8ea40493c286deb210a19bce21d46aba Mon Sep 17 00:00:00 2001 From: SerhiiRI Date: Thu, 23 Oct 2025 13:06:37 +0300 Subject: [PATCH 7/9] set version to 1.0.4 --- CHANGELOG.md | 7 +++++++ README.md | 6 +++--- pom.xml | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb54947..9a81e88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 1.0.4 +ADDED commando.commands.builtin/commando-macro-spec. The new type of command that allow to group instructions by its functionality and use it as a single command. Added Readme information about the macro. +ADDED documentation for commando.commands.builtin commands. Now each built-in command have explanation of its behavior and examples of usage. +UPDATED upgrade commando.commands.query-dsl. Function `resolve-query` was removed and replaced by `resolve-fn`, `resolve-instruction`, `resolve-instructions-qe` function called a **resolvers**. Explanations about the resolvers added to _docs/query-dsl.md_ file. +UPDATED error serialization. `commando.impl.utils` contains new way to serialize errors for CLJ/CLJS. Now all errors are serialized to map with keys: `:type`, `:class`, `:message`, `:data` (if exists) and `:stacktrace` (if exists), `:cause` (if exists). See `commando.impl.utils/serialize-error` for more information. You can expand the error handlers using `serialize-exception-fn` multimethod (but for CLJ only). +ADDED tests for macro-spec, errors and query-dsl changes. + # 1.0.3 UPDATED behavior `:validate-params-fn`. If the function return anything except `true` it ment validation failure. If the function return data, they will be attached to returned error inside status map. Added tests. FIXED align serialization of exeption for CLJ/CLJS diff --git a/README.md b/README.md index 63a5d17..05b4a23 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Clojars Project](https://img.shields.io/clojars/v/org.clojars.funkcjonariusze/commando.svg)](https://clojars.org/org.clojars.funkcjonariusze/commando) [![Run tests](https://github.com/funkcjonariusze/commando/actions/workflows/unit_test.yml/badge.svg)](https://github.com/funkcjonariusze/commando/actions/workflows/unit_test.yml) -[![cljdoc badge](https://cljdoc.org/badge/org.clojars.funkcjonariusze/commando)](https://cljdoc.org/d/org.clojars.funkcjonariusze/commando/1.0.3) +[![cljdoc badge](https://cljdoc.org/badge/org.clojars.funkcjonariusze/commando)](https://cljdoc.org/d/org.clojars.funkcjonariusze/commando/1.0.4) **Commando** is a flexible Clojure library for managing, extracting, and transforming data inside nested map structures aimed to build your own Data DSL. @@ -31,10 +31,10 @@ ```clojure ;; deps.edn with git -{org.clojars.funkcjonariusze/commando {:mvn/version "1.0.3"}} +{org.clojars.funkcjonariusze/commando {:mvn/version "1.0.4"}} ;; leiningen -[org.clojars.funkcjonariusze/commando "1.0.3"] +[org.clojars.funkcjonariusze/commando "1.0.4"] ``` ## Quick Start diff --git a/pom.xml b/pom.xml index fb361cd..f4acc17 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ jar org.clojars.funkcjonariusze commando - 1.0.3 + 1.0.4 commando @@ -42,6 +42,6 @@ https://github.com/funkcjonariusze/commando scm:git:git://github.com/funkcjonariusze/commando.git scm:git:ssh://git@github.com:funkcjonariusze/commando.git - 1.0.3 + 1.0.4 From 57895ee2afd54783e368965747cc6bf398a16982 Mon Sep 17 00:00:00 2001 From: SerhiiRI Date: Mon, 27 Oct 2025 18:54:10 +0200 Subject: [PATCH 8/9] Replaced *debug-mode* on *execute-config* dynamic variable which contain multiple keys('flags') what can change the behavior of commando/execute --- CHANGELOG.md | 2 + README.md | 88 +++++++++++++++---- src/commando/commands/builtin.cljc | 7 +- src/commando/core.cljc | 8 +- src/commando/impl/finding_commands.cljc | 6 +- src/commando/impl/utils.cljc | 37 +++++--- test/unit/commando/commands/builtin_test.cljc | 32 +++++-- .../commando/commands/query_dsl_test.cljc | 8 +- test/unit/commando/impl/utils_test.cljc | 9 +- 9 files changed, 147 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a81e88..fdffb15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ADDED documentation for commando.commands.builtin commands. Now each built-in co UPDATED upgrade commando.commands.query-dsl. Function `resolve-query` was removed and replaced by `resolve-fn`, `resolve-instruction`, `resolve-instructions-qe` function called a **resolvers**. Explanations about the resolvers added to _docs/query-dsl.md_ file. UPDATED error serialization. `commando.impl.utils` contains new way to serialize errors for CLJ/CLJS. Now all errors are serialized to map with keys: `:type`, `:class`, `:message`, `:data` (if exists) and `:stacktrace` (if exists), `:cause` (if exists). See `commando.impl.utils/serialize-error` for more information. You can expand the error handlers using `serialize-exception-fn` multimethod (but for CLJ only). ADDED tests for macro-spec, errors and query-dsl changes. +UPDATED README.md 'Debugging section' was replaced on 'Configuring Execution Behavior' which contains more detailed information how to modify execution behavior. +UPDATED dynamic variable *debug-mode* replaced by the `*execute-config*` which is a map that can contain multiple configuration options. # 1.0.3 UPDATED behavior `:validate-params-fn`. If the function return anything except `true` it ment validation failure. If the function return data, they will be attached to returned error inside status map. Added tests. diff --git a/README.md b/README.md index 05b4a23..e6727cd 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,9 @@ - [command-macro-spec](#command-macro-spec) - [Adding New Commands](#adding-new-commands) - [Status-Map and Internals](#status-map-and-internals) - - [Debugging commando](#debugging-commando) + - [Configuring Execution Behavior](#configuring-execution-behavior) + - [`:debug-result`](#debug-result) + - [`:error-data-string`](#error-data-string) - [Integrations](#integrations) - [Versioning](#versioning) - [License](#license) @@ -134,12 +136,12 @@ The `:commando/from` command supports relative paths like `"../"`, `"./"` for ac [commands-builtin/command-from-spec] {"incrementing 1" {"1" 1 - "2" {:commando/from ["../" "1"] := inc} - "3" {:commando/from ["../" "2"] := inc}} + "2" {:commando/from ["../" "1"] := inc} + "3" {:commando/from ["../" "2"] := inc}} "decrementing 1" {"1" 1 - "2" {:commando/from ["../" "1"] := dec} - "3" {:commando/from ["../" "2"] := dec}}}) + "2" {:commando/from ["../" "1"] := dec} + "3" {:commando/from ["../" "2"] := dec}}}) ;; => ;; {"incrementing 1" {"1" 1, "2" 2, "3" 3}, ;; "decrementing 1" {"1" 1, "2" 0, "3" -1}} @@ -176,13 +178,13 @@ A wrapper similar to `commando/fn`, but conceptually closer to `commando/from`, (commando/execute [commands-builtin/command-apply-spec] {"0" {:commando/apply - {"1" {:commando/apply - {"2" {:commando/apply - {"3" {:commando/apply {"4" {:final "5"}} - := #(get % "4")}} - := #(get % "3")}} - := #(get % "2")}} - := #(get % "1")}}) + {"1" {:commando/apply + {"2" {:commando/apply + {"3" {:commando/apply {"4" {:final "5"}} + := #(get % "4")}} + := #(get % "3")}} + := #(get % "2")}} + := #(get % "1")}}) ;; => {"0" {:final "5"}} ``` @@ -470,17 +472,26 @@ On unsuccessful execution (`:failed`), you get: #]} ``` -### Debugging commando +### Configuring Execution Behavior -You can set dynamic variable `commando.impl.utils/*debug-mode* true` to see more details on how execution went. +The `commando.impl.utils/*execute-config*` dynamic variable allows for fine-grained control over `commando/execute`'s behavior. You can bind this variable to a map containing the following configuration keys: + +- `:debug-result` (boolean) +- `:error-data-string` (boolean) + +#### `:debug-result` + +When set to `true`, the returned status-map will include additional execution information, such as `:internal/cm-list`, `:internal/cm-dependency`, and `:internal/cm-running-order`. This helps in analyzing the instruction's execution flow. + +Here's an example of how to use `:debug-result`: ```clojure (require '[commando.core :as commando]) (require '[commando.commands.builtin :as commands-builtin]) (require '[commando.impl.utils :as commando-utils]) -(binding [commando-utils/*debug-mode* true] - (execute +(binding [commando-utils/*execute-config* {:debug-result true}] + (commando/execute [commands-builtin/command-from-spec] {"1" 1 "2" {:commando/from ["1"]} @@ -517,12 +528,55 @@ You can set dynamic variable `commando.impl.utils/*debug-mode* true` to see more "root,3[from]" #{"root,2[from]"}}} ``` -`:internal/cm-list` - a list of all recognized commands in an instruction. This list also contains the `_map`, `_value`, and the unmentioned `_vector` commands, which are not included in the registry. Commando includes several internal built-in commands that describe the _instruction's structure_. An _instruction_ is a composition of maps, their values, and vectors that represent its structure and help build a clear dependency graph. These commands are removed from the final output after this step. +`:internal/cm-list` - a list of all recognized commands in an instruction. This list also contains the `_map`, `_value`, and the unmentioned `_vector` commands. Commando includes several internal built-in commands that describe the _instruction's structure_. An _instruction_ is a composition of maps, their values, and vectors that represent its structure and help build a clear dependency graph. These commands are removed from the final output after this step, but included in the compiled registry. `:internal/cm-dependency` - describes how parts of an _instruction_ depend on each other. `:internal/cm-running-order` - the correct order in which to execute commands. + +#### `:error-data-string` + +When `:error-data-string` is `true`, the `:data` key within serialized `ExceptionInfo` objects (processed by `commando.impl.utils/serialize-exception`) will contain a string representation of the exception's data. Conversely, if `false`, the `:data` key will hold the raw data structure (map). This setting is particularly useful for controlling the verbosity of error details, in example when examining Malli validation explanations etc. + +```clojure +(def value + (commando/execute [commands-builtin/command-from-spec] + {"a" 10 + "ref" {:commando/from "BROKEN"}})) +(get-in value [:errors 0 :error]) +;; => +;; {:type "exception-info", +;; :class "clojure.lang.ExceptionInfo", +;; :message "Failed while validating params for :commando/from ...", +;; :stack-trace +;; [["commando.impl.finding_commands$instruction_command_spec$fn__14401" "invoke" "finding_commands.cljc" 65] +;; ["clojure.core$some" "invokeStatic" "core.clj" 2718] +;; ... +;; ...], +;; :cause nil, +;; :data "{:command-type :commando/from, :reason #:commando{:from [\"commando/from should be a sequence path to value in Instruction: [:some 2 \\\"value\\\"]\"]}, :path [\"ref\"], :value #:commando{:from \"BROKEN\"}}"} + + +(def value + (binding [sut/*execute-config* {:error-data-string false}] + (commando/execute [commands-builtin/command-from-spec] + {"a" 10 + "ref" {:commando/from "BROKEN"}}))) +(get-in value [:errors 0 :error]) +;; => +;; {:type "exception-info", +;; :class "clojure.lang.ExceptionInfo", +;; ... +;; ... +;; :data +;; {:command-type :commando/from, +;; :reason {:commando/from +;; ["commando/from should be a sequence path to value in Instruction: [:some 2 \"value\"]"]}, +;; :path ["ref"], +;; :value {:commando/from "BROKEN"}}} +``` + # Integrations - [Work with JSON](./doc/json.md) diff --git a/src/commando/commands/builtin.cljc b/src/commando/commands/builtin.cljc index 0f4443f..6bac773 100644 --- a/src/commando/commands/builtin.cljc +++ b/src/commando/commands/builtin.cljc @@ -361,8 +361,8 @@ (defmethod command-macro :vector-dot-product [_macro-type {:keys [vector1-str vector2-str]}] {:= :dot-product :commando/apply - {:vector1-str [\"1\" \"2\" \"3\"] - :vector2-str [\"4\" \"5\" \"6\"] + {:vector1-str vector1-str + :vector2-str vector2-str ;; ------- ;; Parsing :vector1 @@ -393,7 +393,7 @@ {:commando/macro :vector-dot-product :vector1-str [\"10\" \"20\" \"30\"] :vector2-str [\"4\" \"5\" \"6\"]}})) - => {:vector-dot-1 32, :vector-dot-2 32} + => {:vector-dot-1 32, :vector-dot-2 320} See Also `commando.core/execute` @@ -461,4 +461,3 @@ (:instruction result) (throw (ex-info (str utils/exception-message-header "command-macro. Failure execution :commando/macro") result))))) :dependencies {:mode :all-inside}}) - diff --git a/src/commando/core.cljc b/src/commando/core.cljc index bb95295..77e3351 100644 --- a/src/commando/core.cljc +++ b/src/commando/core.cljc @@ -160,7 +160,7 @@ "build-compiler. Error building compiler")})) :ok (cond-> status-map true (update-in [:internal/cm-running-order] registry/remove-instruction-commands-from-command-vector) - (false? utils/*debug-mode*) (select-keys [:status :registry :internal/cm-running-order :successes :warnings]))))) + (false? (:debug-result (utils/execute-config))) (select-keys [:status :registry :internal/cm-running-order :successes :warnings]))))) (defn ^:private compiler->status-map "Cause compiler contains only two :registry and :internal/cm-running-order keys @@ -170,7 +170,7 @@ (contains? compiler :internal/cm-running-order) (contains? compiler :status)) (case (:status compiler) - :ok (if (true? utils/*debug-mode*) + :ok (if (true? (:debug-result (utils/execute-config))) (-> (smap/status-map-pure compiler)) (-> (smap/status-map-pure (select-keys compiler [:registry :internal/cm-running-order :successes :warnings])))) :failed compiler) @@ -187,8 +187,8 @@ (compiler->status-map (build-compiler registry-or-compiler instruction))) (assoc :instruction instruction))] (cond-> (execute-commands! status-map-with-compiler) - (false? utils/*debug-mode*) (dissoc :internal/cm-running-order) - (false? utils/*debug-mode*) (dissoc :registry)))) + (false? (:debug-result (utils/execute-config))) (dissoc :internal/cm-running-order) + (false? (:debug-result (utils/execute-config))) (dissoc :registry)))) (defn failed? [status-map] (smap/failed? status-map)) (defn ok? [status-map] (smap/ok? status-map)) diff --git a/src/commando/impl/finding_commands.cljc b/src/commando/impl/finding_commands.cljc index 5ccd991..fd15e67 100644 --- a/src/commando/impl/finding_commands.cljc +++ b/src/commando/impl/finding_commands.cljc @@ -82,13 +82,13 @@ (let [current-path (first queue) remaining-paths (rest queue) current-value (get-in instruction current-path) - debug-stack (if utils/*debug-mode* (get debug-stack-map current-path (list)) (list))] + debug-stack (if (:debug-result (utils/execute-config)) (get debug-stack-map current-path (list)) (list))] (if-let [command-spec (instruction-command-spec command-registry current-value current-path)] (let [command (cm/->CommandMapPath current-path - (if utils/*debug-mode* (merge command-spec {:__debug_stack debug-stack}) command-spec)) + (if (:debug-result (utils/execute-config)) (merge command-spec {:__debug_stack debug-stack}) command-spec)) child-paths (command-child-paths command-spec current-value current-path) - updated-debug-stack-map (if utils/*debug-mode* + updated-debug-stack-map (if (:debug-result (utils/execute-config)) (reduce #(assoc %1 %2 (conj debug-stack command)) debug-stack-map child-paths) {})] (recur (concat remaining-paths child-paths) (conj found-commands command) updated-debug-stack-map)) diff --git a/src/commando/impl/utils.cljc b/src/commando/impl/utils.cljc index 918c01c..a8c691b 100644 --- a/src/commando/impl/utils.cljc +++ b/src/commando/impl/utils.cljc @@ -7,14 +7,30 @@ ;; Dynamic Properties ;; ------------------ -(def ^:dynamic *debug-mode* - "When enabled, debug-stack-map functionality is active in find-commands*." - false) +(def ^:private -execute-config-default + {:debug-result false + :error-data-string true}) + +(def ^:dynamic + *execute-config* + "Dynamic configuration for `commando/execute` behavior. + - `:debug-result` (boolean): When true, adds additional execution + information to the returned status-map, aiding in instruction analysis. + - `:error-data-string` (boolean): When true, the `:data` key in + serialized `ExceptionInfo` (via `commando.impl.utils/serialize-exception`) + will be a string representation of the data. When false, it will return + the original map structure." + -execute-config-default) + +(defn execute-config + "Returns the effective configuration for `commando/execute`, getting data from dynamic variable `commando.impl.utils/*execute-config*`" + [] + (merge -execute-config-default *execute-config*)) (def ^{:dynamic true :private true :doc "For debugging purposes and some mysterious reason of setting it dynamically during execution"} - *command-map-spec-registry* + *command-map-spec-registry* nil) (defn command-map-spec-registry @@ -22,6 +38,7 @@ [] (or *command-map-spec-registry* [])) + ;; ------------------ ;; Function Resolvers ;; ------------------ @@ -114,9 +131,9 @@ :stack-trace (stacktrace->vec-str t) :cause (when-let [cause (.getCause t)] (serialize-exception-fn cause)) - :data (if *debug-mode* - (ex-data t) - (pr-str (ex-data t)))})) + :data (if (true? (:error-data-string (execute-config))) + (pr-str (ex-data t)) + (ex-data t))})) #?(:clj (defmethod serialize-exception-fn :default [t] @@ -152,9 +169,9 @@ :stack-trace (stacktrace->vec-str e) :cause (when-let [cause (.-cause e)] (serialize-exception-fn cause)) - :data (if *debug-mode* - (.-data e) - (pr-str (.-data e)))}))) + :data (if (true? (:error-data-string (execute-config))) + (pr-str (.-data e)) + (.-data e))}))) #?(:cljs (defmethod serialize-exception-fn :js-error [^js/Error e] diff --git a/test/unit/commando/commands/builtin_test.cljc b/test/unit/commando/commands/builtin_test.cljc index 8bd762a..814ba84 100644 --- a/test/unit/commando/commands/builtin_test.cljc +++ b/test/unit/commando/commands/builtin_test.cljc @@ -31,7 +31,9 @@ (testing "Failure test cases" (is (helpers/status-map-contains-error? - (binding [commando-utils/*debug-mode* true] + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] (commando/execute [command-builtin/command-fn-spec] {:commando/fn "STRING" :args [[1 2 3] [3 2 1]]})) @@ -48,7 +50,9 @@ "Waiting on error, bacause commando/fn has wrong type for :commando/fn") (is (helpers/status-map-contains-error? - (binding [commando-utils/*debug-mode* true] + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] (commando/execute [command-builtin/command-fn-spec] {:commando/fn (fn [& [v1 v2]] (reduce + (map * v1 v2))) :args "BROKEN"})) @@ -80,7 +84,9 @@ (testing "Failure test cases" (is (helpers/status-map-contains-error? - (binding [commando-utils/*debug-mode* true] + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] (commando/execute [command-builtin/command-apply-spec] {:commando/apply {:value 1} := "STRING"})) @@ -170,7 +176,9 @@ :command {:commando/from ["UNEXISING"]}}) "Waiting on error, bacause commando/from seding to unexising path") (is (helpers/status-map-contains-error? - (binding [commando-utils/*debug-mode* true] + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] (commando/execute [command-builtin/command-from-spec] {:commando/from "BROKEN"})) @@ -183,7 +191,9 @@ :reason {:commando/from ["commando/from should be a sequence path to value in Instruction: [:some 2 \"value\"]"]}}))) "Waiting on error, ':validate-params-fn' for commando/from. Corrupted path \"BROKEN\" ") (is (helpers/status-map-contains-error? - (binding [commando-utils/*debug-mode* true] + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] (commando/execute [command-builtin/command-from-spec] {:v 1 @@ -227,7 +237,9 @@ (testing "Failure test cases" (is (helpers/status-map-contains-error? - (binding [commando-utils/*debug-mode* true] + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] (commando/execute [command-builtin/command-mutation-spec] {:commando/mutation (fn [] "BROKEN")})) (fn [error] @@ -239,7 +251,9 @@ "Waiting on error, bacause commando/mutation has wrong type for :commando/mutation") (is (helpers/status-map-contains-error? - (binding [commando-utils/*debug-mode* true] + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] (commando/execute [command-builtin/command-mutation-spec] {:commando/mutation :dot-product :vector1 [1 "_" 3] @@ -309,7 +323,9 @@ (testing "Failure test cases" (is (helpers/status-map-contains-error? - (binding [commando-utils/*debug-mode* true] + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] (commando/execute [command-builtin/command-macro-spec] {:commando/macro (fn [])})) (fn [error] diff --git a/test/unit/commando/commands/query_dsl_test.cljc b/test/unit/commando/commands/query_dsl_test.cljc index f17fb71..8797ea7 100644 --- a/test/unit/commando/commands/query_dsl_test.cljc +++ b/test/unit/commando/commands/query_dsl_test.cljc @@ -497,7 +497,9 @@ (is (helpers/status-map-contains-error? (get-in - (binding [commando-utils/*debug-mode* true] + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] (commando/execute registry {:commando/resolve :test-instruction-qe @@ -517,7 +519,9 @@ (is (helpers/status-map-contains-error? (get-in - (binding [commando-utils/*debug-mode* true] + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] (commando/execute registry {:commando/resolve :test-instruction-qe diff --git a/test/unit/commando/impl/utils_test.cljc b/test/unit/commando/impl/utils_test.cljc index 91328d6..96d6791 100644 --- a/test/unit/commando/impl/utils_test.cljc +++ b/test/unit/commando/impl/utils_test.cljc @@ -84,7 +84,9 @@ :data nil}, :data "{:level \"1\"}"}))) - (let [e (binding [sut/*debug-mode* true] + (let [e (binding [sut/*execute-config* + {:debug-result false + :error-data-string false}] (try (malli/assert :int "string") (catch Exception e @@ -146,7 +148,9 @@ :data "{}"} :data "{}"}))) - (let [e (binding [sut/*debug-mode* true] + (let [e (binding [sut/*execute-config* + {:debug-result false + :error-data-string false}] (try (malli/assert :int "string") (catch :default e @@ -198,3 +202,4 @@ (is (= false (malli/validate sut/ResolvableFn 'UNKOWN))) (is (= false (malli/validate sut/ResolvableFn 'UNKOWN/UNKOWN))) ))) + From c2139e1a6aa4b34dee637380c3f3df369c72cdce Mon Sep 17 00:00:00 2001 From: SerhiiRI Date: Wed, 29 Oct 2025 18:56:15 +0200 Subject: [PATCH 9/9] Adjust documentation issues, typos, removed unsude namespaces, update changelog. --- CHANGELOG.md | 5 +-- doc/query_dsl.md | 4 +-- src/commando/commands/builtin.cljc | 16 ++++----- src/commando/impl/utils.cljc | 45 ++++++++++++++++++------- test/unit/commando/core_test.cljc | 1 - test/unit/commando/impl/utils_test.cljc | 1 - 6 files changed, 45 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdffb15..8f80d0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # 1.0.4 ADDED commando.commands.builtin/commando-macro-spec. The new type of command that allow to group instructions by its functionality and use it as a single command. Added Readme information about the macro. ADDED documentation for commando.commands.builtin commands. Now each built-in command have explanation of its behavior and examples of usage. -UPDATED upgrade commando.commands.query-dsl. Function `resolve-query` was removed and replaced by `resolve-fn`, `resolve-instruction`, `resolve-instructions-qe` function called a **resolvers**. Explanations about the resolvers added to _docs/query-dsl.md_ file. -UPDATED error serialization. `commando.impl.utils` contains new way to serialize errors for CLJ/CLJS. Now all errors are serialized to map with keys: `:type`, `:class`, `:message`, `:data` (if exists) and `:stacktrace` (if exists), `:cause` (if exists). See `commando.impl.utils/serialize-error` for more information. You can expand the error handlers using `serialize-exception-fn` multimethod (but for CLJ only). +UPDATED upgrade commando.commands.query-dsl. Function `resolve-query` was removed and replaced by `resolve-fn`, `resolve-instruction`, `resolve-instruction-qe` function called a **resolvers**. Explanations about the resolvers added to _docs/query-dsl.md_ file. +UPDATED error serialization. `commando.impl.utils` contains new way to serialize errors for CLJ/CLJS. Now all errors are serialized to map with keys: `:type`, `:class`, `:message`, `:data` (if exists) and `:stacktrace` (if exists), `:cause` (if exists). See `commando.impl.utils/serialize-exception` for more information. You can expand the error handlers using `serialize-exception-fn` multimethod (but for CLJ only). ADDED tests for macro-spec, errors and query-dsl changes. UPDATED README.md 'Debugging section' was replaced on 'Configuring Execution Behavior' which contains more detailed information how to modify execution behavior. UPDATED dynamic variable *debug-mode* replaced by the `*execute-config*` which is a map that can contain multiple configuration options. +FIXED Removed `detach-instruction-commands` call from `commando.core/build-compiler`. In `commando.core/build-compiler`, the line that detached instruction commands from the registry was removed. This means the compiled registry now includes internal commands (_map, _value, _vector). # 1.0.3 UPDATED behavior `:validate-params-fn`. If the function return anything except `true` it ment validation failure. If the function return data, they will be attached to returned error inside status map. Added tests. diff --git a/doc/query_dsl.md b/doc/query_dsl.md index 5f8d339..afceed0 100644 --- a/doc/query_dsl.md +++ b/doc/query_dsl.md @@ -2,7 +2,7 @@

Query DSL

-The Commando Query DSL is a built-in, lightweight query mechanism. It serves as a simple alternative to more comprehensive solutions like GraphQL or Pathom3, but it is more primitive and requires you to define dependency resolution manually. +The Commando Query DSL is a built-in, lightweight query mechanism. It serves as a simple alternative to more comprehensive solutions like GraphQL or Pathom3, but it is much simpler and requires you to define dependency resolution manually. Its primary purpose is to provide a way to: @@ -27,7 +27,7 @@ Its primary purpose is to provide a way to: ## Core Concept -The Query DSL is enabled by adding `commando.commands.query-dsl/command-resolve-spec` to your `(commando.core/execute [] )` call. +The Query DSL is enabled by adding `commando.commands.query-dsl/command-resolve-spec` to commando execute registry. You define your data "endpoints" by creating new methods for the `commando.commands.query-dsl/command-resolve` multimethod. diff --git a/src/commando/commands/builtin.cljc b/src/commando/commands/builtin.cljc index 6bac773..9279ab6 100644 --- a/src/commando/commands/builtin.cljc +++ b/src/commando/commands/builtin.cljc @@ -98,7 +98,7 @@ (def ^{:doc " Description - command-fn-spec - get value from another command or existing value + command-from-spec - get value from another command or existing value in Instruction. Path to another command is passed inside `:commando/from` key, optionally you can apply `:=` function/symbol/keyword to the result. @@ -159,7 +159,7 @@ (def ^{:doc " Description - command-fn-json-spec - get value from another command or existing value + command-from-json-spec - get value from another command or existing value in Instruction. Path to another command is passed inside `\"commando-from\"` key, optionally you can get value of object by using `\"=\"` key. @@ -233,8 +233,8 @@ To declare mutation create method of `command-mutation` multimethod Example - (defmethod commando.commands.builtin/command-mutation :generate-string [_ {:keys [lenght]}] - {:random-string (apply str (repeatedly (or lenght 10) #(rand-nth \"abcdefghijklmnopqrstuvwxyz0123456789\")))}) + (defmethod commando.commands.builtin/command-mutation :generate-string [_ {:keys [length]}] + {:random-string (apply str (repeatedly (or length 10) #(rand-nth \"abcdefghijklmnopqrstuvwxyz0123456789\")))}) (defmethod commando.commands.builtin/command-mutation :generate-number [_ {:keys [from to]}] {:random-number (let [bound (- to from)] (+ from (rand-int bound)))}) @@ -243,7 +243,7 @@ (commando/execute [command-mutation-spec] {:a {:commando/mutation :generate-number :from 10 :to 20} - :b {:commando/mutation :generate-string :lenght 5}})) + :b {:commando/mutation :generate-string :length 5}})) => {:a {:random-number 14}, :b {:random-string \"5a379\"}} See Also @@ -272,8 +272,8 @@ To declare mutation create method of `command-mutation` multimethod Example - (defmethod commando.commands.builtin/command-mutation \"generate-string\" [_ {:strs [lenght]}] - {\"random-string\" (apply str (repeatedly (or lenght 10) #(rand-nth \"abcdefghijklmnopqrstuvwxyz0123456789\")))}) + (defmethod commando.commands.builtin/command-mutation \"generate-string\" [_ {:strs [length]}] + {\"random-string\" (apply str (repeatedly (or length 10) #(rand-nth \"abcdefghijklmnopqrstuvwxyz0123456789\")))}) (defmethod commando.commands.builtin/command-mutation \"generate-number\" [_ {:strs [from to]}] {\"random-number\" (let [bound (- to from)] (+ from (rand-int bound)))}) @@ -282,7 +282,7 @@ (commando/execute [command-mutation-json-spec] {\"a\" {\"commando-mutation\" \"generate-number\" \"from\" 10 \"to\" 20} - \"b\" {\"commando-mutation\" \"generate-string\" \"lenght\" 5}})) + \"b\" {\"commando-mutation\" \"generate-string\" \"length\" 5}})) => {\"a\" {\"random-number\" 18}, \"b\" {\"random-string\" \"m3gj1\"}} See Also diff --git a/src/commando/impl/utils.cljc b/src/commando/impl/utils.cljc index a8c691b..d547c77 100644 --- a/src/commando/impl/utils.cljc +++ b/src/commando/impl/utils.cljc @@ -101,7 +101,20 @@ #?(:clj (defn ^:private exception-dispatch-fn [e] (class e))) -#?(:clj (defmulti serialize-exception-fn exception-dispatch-fn)) +#?(:clj (defmulti ^{:doc + "Multimethod for serializing exceptions to maps. +Dispatch based on exception class +To add custom serialization for your exception type: + +Example + (defmethod serialize-exception-fn ClassOfException [e] + {:type \"my-exception\" + :message (.getMessage e) + ...}) + +See + `commando.impl.utils/serialize-exception`"} + serialize-exception-fn exception-dispatch-fn)) #?(:clj (defmethod serialize-exception-fn java.lang.Throwable [^Throwable t] @@ -158,20 +171,26 @@ (if-let [stack (.-stack e)] (str stack) nil))) #?(:cljs - (defmulti serialize-exception-fn exception-dispatch-fn)) + (defmulti ^{:doc + "Multimethod for serializing exceptions to maps. +dispatch differentiate two type of exception. Not supposed +to be extended in cljs + +See + `commando.impl.utils/serialize-exception`"} + serialize-exception-fn exception-dispatch-fn)) #?(:cljs (defmethod serialize-exception-fn :cljs-exception-info [^cljs.core.ExceptionInfo e] - (let [cause (.-cause e)] - {:type "exception-info" - :class "cljs.core.ExceptionInfo" - :message (.-message e) - :stack-trace (stacktrace->vec-str e) - :cause (when-let [cause (.-cause e)] - (serialize-exception-fn cause)) - :data (if (true? (:error-data-string (execute-config))) - (pr-str (.-data e)) - (.-data e))}))) + {:type "exception-info" + :class "cljs.core.ExceptionInfo" + :message (.-message e) + :stack-trace (stacktrace->vec-str e) + :cause (when-let [cause (.-cause e)] + (serialize-exception-fn cause)) + :data (if (true? (:error-data-string (execute-config))) + (pr-str (.-data e)) + (.-data e))})) #?(:cljs (defmethod serialize-exception-fn :js-error [^js/Error e] @@ -183,7 +202,7 @@ :data nil})) #?(:cljs - (defmethod serialize-exception-fn :default [e] + (defmethod serialize-exception-fn :default [_e] nil)) (defn serialize-exception diff --git a/test/unit/commando/core_test.cljc b/test/unit/commando/core_test.cljc index 178038d..1424010 100644 --- a/test/unit/commando/core_test.cljc +++ b/test/unit/commando/core_test.cljc @@ -5,7 +5,6 @@ [commando.commands.builtin :as cmds-builtin] [commando.core :as commando] [commando.impl.command-map :as cm] - [commando.impl.utils :as commando-utils] [commando.test-helpers :as helpers] [malli.core :as malli])) diff --git a/test/unit/commando/impl/utils_test.cljc b/test/unit/commando/impl/utils_test.cljc index 96d6791..11c7f45 100644 --- a/test/unit/commando/impl/utils_test.cljc +++ b/test/unit/commando/impl/utils_test.cljc @@ -2,7 +2,6 @@ (:require #?(:cljs [cljs.test :refer [deftest is testing]] :clj [clojure.test :refer [deftest is testing]]) - [clojure.set :as set] [commando.test-helpers :as helpers] [commando.impl.utils :as sut] [malli.core :as malli]))