From 3a84f731c580362a38856f7e66c22806abcd9bf8 Mon Sep 17 00:00:00 2001 From: SerhiiRI Date: Wed, 11 Mar 2026 23:53:04 +0200 Subject: [PATCH 1/2] Added driver validation to commands, add driver syntax validation test. Fixed issue with redundant wrapping for error handling, fix docs for commands --- CHANGELOG.md | 15 ++--- README.md | 7 +++ src/commando/commands/builtin.cljc | 57 ++++++++++++------- src/commando/core.cljc | 9 +-- src/commando/driver/builtin.cljc | 10 ++++ src/commando/impl/utils.cljc | 17 ++++++ test/unit/commando/commands/builtin_test.cljc | 52 ++++++++++------- test/unit/commando/core_test.cljc | 12 ++-- test/unit/commando/impl/utils_test.cljc | 12 ++++ 9 files changed, 125 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 728145c..675e771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,22 +8,15 @@ "=" "name" → "=>" ["get" "name"], ``` -Built-in drivers: `:identity` (default), `:get`, `:get-in`, `:select-keys`, `:projection`, `:fn`. Supports pipelines (`[[:get :city] :uppercase]`) and custom drivers via `commando.impl.executing/command-driver` multimethod. See [README](./README.md) for details. +Built-in drivers: `:identity` (default), `:default`, `:get`, `:get-in`, `:select-keys`, `:projection`, `:fn`. Supports pipelines (`[[:get :city] :uppercase]`) and custom drivers via `commando.impl.executing/command-driver` multimethod. See [README](./README.md) for details. +ADDED `command-context-spec` in `commando.commands.builtin`. A new command type `:commando/context` (string form: `"commando-context"`) that injects external reference data into instructions via closure. Call `(command-context-spec ctx-map)` to create a CommandMapSpec. Resolves with `{:mode :none}` — before all other commands, so `:commando/from` and `:commando/fn` can depend on context results. -ADDED `command-context-spec` in `commando.commands.builtin`. A new command type `:commando/context` (string form: `"commando-context"`) that injects external reference data into instructions via closure. Call `(command-context-spec ctx-map)` to create a CommandMapSpec. Resolves with `{:mode :none}` — before all other commands, so `:commando/from` and `:commando/fn` can depend on context results. Supports `:=>` driver transform, `:default` fallback for missing paths, and returns `nil` when path is not found without `:default`. Full malli validation for both keyword and string key forms. - -REDESIGNED Registry. Registry is now a map-based structure (`{:type spec, ...}`) instead of a plain vector. `registry-create` accepts both formats: -```clojure -;; vector — order = scan priority -(registry-create [from-spec fn-spec]) -``` -Built registry can be modified with `registry-add` / `registry-remove` (identification by `:type` key). -`registry-assoc` / `registry-dissoc` removed. Map input form for `registry-create` removed — only vectors accepted. +ADDED option to modify already built registry with `registry-add` / `registry-remove` methods (identification by `:type` key). RENAMED `create-registry` → `registry-create`. Old name removed. -REMOVED `build-compiler`. Compiler concept removed from the pipeline; optimizations for repeated `execute` calls will be introduced in a future version. +REMOVED `build-compiler`. Compiler concept removed from the core functionality; optimizations for repeated `execute` calls will be introduced in a future version. ADDED `print-trace` in `commando.impl.utils` — replaces `print-deep-stats` with an improved flamegraph that also shows per-node instruction keys and optional title. Add `:__title` or `"__title"` to any instruction's top level to annotate that node in the output. `print-deep-stats` is kept as a deprecated alias. diff --git a/README.md b/README.md index bd02362..059f852 100644 --- a/README.md +++ b/README.md @@ -397,6 +397,13 @@ If no `:=>` is specified, the default driver is `:get-in` with no params, which ;; {:name "John" :age 30 :email "j@e.com"} => {:name "John" :email "j@e.com"} ``` +**`:default`** — if nil return default value + +```clojure +{:commando/from [:age] :=> [:default 30]} +;; {:name "John" :age nil} => 30 +``` + **`:fn`** — apply an arbitrary function (for cases when you need runtime transforms): ```clojure diff --git a/src/commando/commands/builtin.cljc b/src/commando/commands/builtin.cljc index a85e6bb..a543a11 100644 --- a/src/commando/commands/builtin.cljc +++ b/src/commando/commands/builtin.cljc @@ -44,7 +44,9 @@ (malli/explain [:map [:commando/fn utils/ResolvableFn] - [:args {:optional true} coll?]] + [:args {:optional true} coll?] + [:=> {:optional true} utils/malli:driver-spec] + ["=>" {:optional true} utils/malli:driver-spec]] m))] m-explain true)) @@ -80,7 +82,10 @@ :validate-params-fn (fn [m] (if-let [m-explain (malli-error/humanize - (malli/explain [:map [:commando/apply :any]] m))] + (malli/explain [:map + [:commando/apply :any] + [:=> {:optional true} utils/malli:driver-spec] + ["=>" {:optional true} utils/malli:driver-spec]] m))] m-explain true)) :apply (fn [_instruction _command-path-obj command-map] @@ -101,7 +106,7 @@ Description 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. + key, optionally you can apply `:=>` driver 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 @@ -155,15 +160,23 @@ (contains? m :commando/from) (contains? m "commando-from")) "The keyword :commando/from and the string \"commando-from\" cannot be used simultaneously in one command." + (contains? m :commando/from) (malli-error/humanize (malli/explain - [:map [:commando/from -malli:commando-from-path]] + [:map + [:commando/from -malli:commando-from-path] + [:=> {:optional true} utils/malli:driver-spec] + ["=>" {:optional true} utils/malli:driver-spec]] m)) + (contains? m "commando-from") (malli-error/humanize (malli/explain - [:map ["commando-from" -malli:commando-from-path]] + [:map + ["commando-from" -malli:commando-from-path] + [:=> {:optional true} utils/malli:driver-spec] + ["=>" {:optional true} utils/malli:driver-spec]] m)))] (if m-explain m-explain @@ -238,7 +251,8 @@ [:map [kw-key [:sequential {:error/message "commando/context should be a sequential path: [:some :key]"} [:or :string :keyword :int]]] - [:default {:optional true} :any]] + [:=> {:optional true} utils/malli:driver-spec] + ["=>" {:optional true} utils/malli:driver-spec]] m)) (contains? m str-key) (malli-error/humanize @@ -246,21 +260,14 @@ [:map [str-key [:sequential {:error/message "commando-context should be a sequential path: [\"some\" \"key\"]"} [:or :string :keyword :int]]] - ["default" {:optional true} :any]] + [:=> {:optional true} utils/malli:driver-spec] + ["=>" {:optional true} utils/malli:driver-spec]] m)))] (if m-explain m-explain true))) :apply (fn [_instruction _command-path-obj command-map] - (let [path (or (get command-map kw-key) (get command-map str-key)) - has-default? (or (contains? command-map :default) - (contains? command-map "default")) - m-default (if (contains? command-map :default) - (:default command-map) - (get command-map "default")) - result (get-in ctx path ::not-found)] - (if (= result ::not-found) - (if has-default? m-default nil) - result))) + (let [path (or (get command-map kw-key) (get command-map str-key))] + (get-in ctx path nil))) :dependencies {:mode :none}})) ;; ====================== @@ -334,12 +341,16 @@ (contains? m :commando/mutation) (malli-error/humanize (malli/explain - [:map [:commando/mutation [:or :keyword :string]]] + [:map [:commando/mutation [:or :keyword :string]] + [:=> {:optional true} utils/malli:driver-spec] + ["=>" {:optional true} utils/malli:driver-spec]] m)) (contains? m "commando-mutation") (malli-error/humanize (malli/explain - [:map ["commando-mutation" [:or :keyword :string]]] + [:map ["commando-mutation" [:or :keyword :string]] + [:=> {:optional true} utils/malli:driver-spec] + ["=>" {:optional true} utils/malli:driver-spec]] m)))] (if m-explain m-explain @@ -466,13 +477,17 @@ (malli-error/humanize (malli/explain [:map - [:commando/macro [:or :keyword :string]]] + [:commando/macro [:or :keyword :string]] + [:=> {:optional true} utils/malli:driver-spec] + ["=>" {:optional true} utils/malli:driver-spec]] m)) (contains? m "commando-macro") (malli-error/humanize (malli/explain [:map - ["commando-macro" [:or :keyword :string]]] + ["commando-macro" [:or :keyword :string]] + [:=> {:optional true} utils/malli:driver-spec] + ["=>" {:optional true} utils/malli:driver-spec]] m)))] (if m-explain m-explain diff --git a/src/commando/core.cljc b/src/commando/core.cljc index 67dbde6..6152572 100644 --- a/src/commando/core.cljc +++ b/src/commando/core.cljc @@ -98,12 +98,9 @@ [{:keys [instruction] :internal/keys [cm-list] :as status-map}] (smap/core-step-safe status-map "build-deps-tree" (fn [sm] - (try - (-> sm - (assoc :internal/cm-dependency (deps/build-dependency-graph instruction cm-list)) - (smap/status-map-handle-success {:message "Dependency map was successfully built"})) - (catch #?(:clj clojure.lang.ExceptionInfo :cljs :default) e - (smap/status-map-handle-error sm (ex-data e))))))) + (-> sm + (assoc :internal/cm-dependency (deps/build-dependency-graph instruction cm-list)) + (smap/status-map-handle-success {:message "Dependency map was successfully built"}))))) (defn ^:private sort-commands-by-deps [status-map] diff --git a/src/commando/driver/builtin.cljc b/src/commando/driver/builtin.cljc index 085bf7f..d85847d 100644 --- a/src/commando/driver/builtin.cljc +++ b/src/commando/driver/builtin.cljc @@ -28,6 +28,16 @@ ;; => [:identity] pass-through applied-result) +;; -- default -- + +(defmethod executing/command-driver :default + [_ driver-params applied-result _command-data _instruction _command-path-obj] + ;; => [:identity] pass-through + (let [default-value (first driver-params)] + (if (nil? applied-result) + default-value + applied-result))) + ;; -- :get-in -- (defmethod executing/command-driver :get-in diff --git a/src/commando/impl/utils.cljc b/src/commando/impl/utils.cljc index 4664bf6..c896de8 100644 --- a/src/commando/impl/utils.cljc +++ b/src/commando/impl/utils.cljc @@ -147,6 +147,23 @@ (fn [x] (some? (resolve-fn x)))])) +(def malli:driver-step-spec + "Defines a single step in a driver pipeline or a standalone driver invocation." + (malli/deref + [:or + :keyword + :string + [:cat + [:or :keyword :string] + [:* :any]]])) + +(def malli:driver-spec + (malli/deref + [:or + malli:driver-step-spec + [:cat + [:* malli:driver-step-spec]]])) + ;; ----------------- ;; Performance Tools ;; ----------------- diff --git a/test/unit/commando/commands/builtin_test.cljc b/test/unit/commando/commands/builtin_test.cljc index da76a06..38165d4 100644 --- a/test/unit/commando/commands/builtin_test.cljc +++ b/test/unit/commando/commands/builtin_test.cljc @@ -217,29 +217,41 @@ (testing "Failure test cases" (is (helpers/status-map-contains-error? - (commando/execute [command-builtin/command-from-spec] - {:ref {:commando/from ["@nonexistent" :value]}}) - {:message "Commando. Point dependency failed: key ':commando/from' references non-existent path [\"@nonexistent\" :value]", - :path [:ref], - :command {:commando/from ["@nonexistent" :value]}}) + (binding [commando-utils/*execute-config* {:error-data-string false}] + (commando/execute [command-builtin/command-from-spec] + {:ref {:commando/from ["@nonexistent" :value]}})) + (fn [error] + (= + (get-in error [:error :data]) + {:message "Commando. Point dependency failed: key ':commando/from' references non-existent path [\"@nonexistent\" :value]" + :path [:ref], + :command {:commando/from ["@nonexistent" :value]}}))) "Anchor not found: should produce error with :anchor key in data") (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"]}}) + (binding [commando-utils/*execute-config* {:error-data-string false}] + (commando/execute [command-builtin/command-from-spec] + {"source" {:a 1 :b 2} + "missing" {:commando/from ["UNEXISING"]}})) + (fn [error] + (= + (get-in error [:error :data]) + {: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? - (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"]}}) + (binding [commando-utils/*execute-config* {:error-data-string false}] + (commando/execute [command-builtin/command-from-spec] + {"source" {:a 1 :b 2} + "missing" {"commando-from" ["UNEXISING"]}})) + (fn [error] + (= + (get-in error [:error :data]) + {: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? @@ -305,8 +317,8 @@ (is (= {:val-default "fallback" :val-nil nil} (:instruction (commando/execute [ctx-spec] - {:val-default {:commando/context [:nonexistent] :default "fallback"} - :val-nil {:commando/context [:nonexistent] :default nil}}))) + {:val-default {:commando/context [:nonexistent] :=> [:default "fallback"]} + :val-nil {:commando/context [:nonexistent] :=> [:default nil]}}))) "Should return :default value when path not found, in other way 'nil' value without exception ") (let [str-ctx {"lang" {"ua" "Ukrainian" "en" "English"}} @@ -316,7 +328,7 @@ (commando/execute [str-spec] {"val" {"commando-context" ["lang" "ua"]} "val-get" {"commando-context" ["lang"] "=>" ["get" "en"]} - "val-default" {"commando-context" ["missing"] "default" "none"}}))) + "val-default" {"commando-context" ["missing"] "=>" ["default" "none"]}}))) "String keys test"))) (testing "Failure test cases" (is (helpers/status-map-contains-error? diff --git a/test/unit/commando/core_test.cljc b/test/unit/commando/core_test.cljc index 7dceec4..e0f7cae 100644 --- a/test/unit/commando/core_test.cljc +++ b/test/unit/commando/core_test.cljc @@ -3,6 +3,7 @@ #?(:cljs [cljs.test :refer [deftest is testing]] :clj [clojure.test :refer [deftest is testing]]) [commando.commands.builtin :as cmds-builtin] + [commando.impl.utils :as commando-utils] [commando.core :as commando] [commando.impl.command-map :as cm] [commando.test-helpers :as helpers] @@ -966,14 +967,9 @@ (is (commando/failed? (commando/execute [invalid-cmd] invalid-validation-instruction)) "Invalid command validation") (is (commando/failed? (commando/execute [throwing-cmd] throwing-recognition-instruction)) "Command recognition exception") - (let [result (commando/execute [cmds-builtin/command-from-spec] unexisting-path-instruction)] - (is (commando/failed? result)) - (is (= - (:errors result) - [{:message - "Commando. Point dependency failed: key ':commando/from' references non-existent path [\"UNEXISTING_PATH\"]", - :path ["2" :container], - :command {:commando/from ["UNEXISTING_PATH"]}}]))) + (let [result (binding [commando-utils/*execute-config* {:error-data-string false}] + (commando/execute [cmds-builtin/command-from-spec] unexisting-path-instruction))] + (is (commando/failed? result))) (is (commando/ok? (commando/execute [cmds-builtin/command-apply-spec] {"plain" {:commando/apply [1 2 3]}})) "Missing :=> — identity pass-through via default :get-in driver") (is (= [1 2 3] diff --git a/test/unit/commando/impl/utils_test.cljc b/test/unit/commando/impl/utils_test.cljc index 6b159ec..4ec25d6 100644 --- a/test/unit/commando/impl/utils_test.cljc +++ b/test/unit/commando/impl/utils_test.cljc @@ -202,4 +202,16 @@ (is (= false (malli/validate sut/ResolvableFn 'UNKOWN/UNKOWN))) ))) +(deftest driver-spec-test + (testing "Testing driver specs for [:=>, \"=>\"] " + ;; Valid examples + (is (= false (malli/validate sut/malli:driver-spec nil))) + (is (= true (malli/validate sut/malli:driver-spec :driver-name))) + (is (= true (malli/validate sut/malli:driver-spec "driver-name"))) + (is (= true (malli/validate sut/malli:driver-spec [:driver-name {:driver "value"} 123]))) + (is (= true (malli/validate sut/malli:driver-spec ["driver-name"]))) + (is (= true (malli/validate sut/malli:driver-spec ["driver-name" "driver-value"]))) + (is (= true (malli/validate sut/malli:driver-spec [[:driver-1 {}] :driver-2 ["driver-3" "driver-3-value"]]))) + (is (= true (malli/validate sut/malli:driver-spec [:driver-name nil]))))) + From 610a6076e82044b011b8b521773b4bb58c6d9bb0 Mon Sep 17 00:00:00 2001 From: SerhiiRI Date: Wed, 11 Mar 2026 23:57:33 +0200 Subject: [PATCH 2/2] fixing small documentation issue --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 059f852..b581d5d 100644 --- a/README.md +++ b/README.md @@ -852,7 +852,7 @@ To provide deeper insight, we've broken down the execution into five distinct st The following graphs show the performance of each step under both normal and extreme load conditions. -**Normal Workloads (up to 80,000 dependencies)** +**Normal Workloads (100-200 dependecies)** Under normal conditions, each execution step completes in just a few milliseconds. The overhead of parsing, dependency resolution, and execution is minimal, ensuring a fast and responsive system.