Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -845,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.

Expand Down
57 changes: 36 additions & 21 deletions src/commando/commands/builtin.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -238,29 +251,23 @@
[: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
(malli/explain
[: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}}))

;; ======================
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 3 additions & 6 deletions src/commando/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
10 changes: 10 additions & 0 deletions src/commando/driver/builtin.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions src/commando/impl/utils.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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
;; -----------------
Expand Down
52 changes: 32 additions & 20 deletions test/unit/commando/commands/builtin_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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"}}
Expand All @@ -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?
Expand Down
12 changes: 4 additions & 8 deletions test/unit/commando/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
12 changes: 12 additions & 0 deletions test/unit/commando/impl/utils_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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])))))


Loading