Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# 1.0.6

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 `:=` 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
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- [command-apply-spec](#command-apply-spec)
- [command-mutation-spec](#command-mutation-spec)
- [command-macro-spec](#command-macro-spec)
- [command-context-spec](#command-context-spec)
- [Adding New Commands](#adding-new-commands)
- [Status-Map and Internals](#status-map-and-internals)
- [Configuring Execution Behavior](#configuring-execution-behavior)
Expand Down Expand Up @@ -305,6 +306,34 @@ The defmethod should return a Instruction. Commando will then treat that returne

Use these macro handlers to hide repeated command structure and keep your instructions shorter and easier to read.

#### command-context-spec

Injects external reference data (dictionaries, config, feature flags) into instructions without duplicating it. Unlike other commands, `command-context-spec` is a **function** — call it with your context map to get a CommandMapSpec. The data is captured via closure and resolves before other commands (`{:mode :none}`), so `:commando/from` and `:commando/fn` can reference context results through the standard dependency mechanism.

```clojure
(def game-config
{:heroes {"warrior" {:hp 120 :damage 15}
"mage" {:hp 80 :damage 25}}
:buffs {:fire-sword 1.5 :shield 2.0}
:settings {:difficulty "hard" :max-level 60}})

(commando/execute
[(commands-builtin/command-context-spec game-config)
commands-builtin/command-from-spec
commands-builtin/command-fn-spec]
{:warrior {:commando/context [:heroes "warrior"]}
:fire-bonus {:commando/context [:buffs :fire-sword]}
:hit-damage {:commando/fn * :args [{:commando/from [:warrior] := :damage}
{:commando/from [:fire-bonus]}]}
:arena-name {:commando/context [:arenas :default] :default "Colosseum"}})
;; => {:warrior {:hp 120 :damage 15}
;; :fire-bonus 1.5
;; :hit-damage 22.5
;; :arena-name "Colosseum"}
```

Missing path returns `nil`; use `:default` for an explicit fallback. The `:=` key applies a transform to the resolved value, same as in `:commando/from`. String-key form (`"commando-context"`, `"default"`, `"="`) is available for JSON compatibility.

### Adding new commands

As you start using commando, you will start writing your own command specs to match your data needs.
Expand Down
102 changes: 102 additions & 0 deletions src/commando/commands/builtin.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,108 @@
:point-key [:commando/from
"commando-from"]}})

;; ======================
;; Context
;; ======================

(defn command-context-spec
"Creates a CommandMapSpec that resolves references to external context data
captured via closure. Context is immutable per registry and resolves before
other commands (dependency mode :none).

ctx — a map with arbitrary structure to look up values from.

Instruction usage (keyword keys):
{:commando/context [:path :to :data]}
{:commando/context [:path :to :data] := :key}
{:commando/context [:path :to :data] :default 0}

Instruction usage (string keys, JSON-compatible):
{\"commando-context\" [\"path\" \"to\" \"data\"]}
{\"commando-context\" [\"path\" \"to\" \"data\"] \"=\" \"key\"}
{\"commando-context\" [\"path\" \"to\" \"data\"] \"default\" 0}

Parameters:
:commando/context — sequential path for get-in on ctx
:= — optional transform function/keyword applied to the resolved value
:default — optional fallback value when path is not found in ctx.
Without :default, missing path throws an error.

Example:
(def my-ctx {:rates {:vat 0.20} :codes {\"01\" \"Kyiv\"}})

(:instruction
(commando/execute
[(command-context-spec my-ctx)
command-from-spec command-fn-spec]
{:vat {:commando/context [:rates :vat]}
:city {:commando/context [:codes \"01\"]}
:missing {:commando/context [:nonexistent] :default \"N/A\"}
:total {:commando/fn * :args [{:commando/from [:vat]} 1000]}}))
;; => {:vat 0.20 :city \"Kyiv\" :missing \"N/A\" :total 200.0}

See Also
`commando.core/execute`
`commando.commands.builtin/command-from-spec`"
[ctx]
{:pre [(map? ctx)]}
(let [kw-key :commando/context
str-key "commando-context"]
{:type kw-key
:recognize-fn #(and (map? %)
(or (contains? % kw-key)
(contains? % str-key)))
:validate-params-fn
(fn [m]
(let [m-explain
(cond
(and (contains? m kw-key) (contains? m str-key))
"The keyword :commando/context and the string \"commando-context\" cannot be used simultaneously in one command."
(contains? m kw-key)
(malli-error/humanize
(malli/explain
[:map
[kw-key [:sequential {:error/message "commando/context should be a sequential path: [:some :key]"}
[:or :string :keyword :int]]]
[:= {:optional true} [:or utils/ResolvableFn :string]]
[:default {:optional true} :any]]
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]]]
["=" {:optional true} [:string {:min 1}]]
["default" {:optional true} :any]]
m)))]
(if m-explain m-explain true)))
:apply
(fn [_instruction _command-path-obj command-map]
(let [[path m-= m-default has-default?]
(cond
(contains? command-map kw-key)
[(get command-map kw-key)
(:= command-map)
(:default command-map)
(contains? command-map :default)]
(contains? command-map str-key)
[(get command-map str-key)
(get command-map "=")
(get command-map "default")
(or (contains? command-map "default")
(contains? command-map :default))])
result (get-in ctx path ::not-found)]
(if (= result ::not-found)
(if has-default? m-default nil)
(if m-=
(if (string? m-=)
(get result m-=)
(let [resolved (utils/resolve-fn m-=)]
(resolved result)))
result))))
:dependencies {:mode :none}}))

;; ======================
;; Mutation
;; ======================
Expand Down
72 changes: 69 additions & 3 deletions test/unit/commando/commands/builtin_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,6 @@
;; FROM-SPEC
;; ===========================




(deftest command-from-spec
;; -------------------
(testing "Successfull test cases"
Expand Down Expand Up @@ -315,6 +312,75 @@
:path [:a], :value {:commando/from [:v], := ["BROKEN"]}})))
"Waiting on error, ':validate-params-fn' for commando/from. Wrong type for optional ':=' applicator")))

;; ===========================
;; CONTEXT-SPEC
;; ===========================

(def test-ctx
{:colors {:red "#FF0000" :blue "#0000FF"}
:numbers [10 20 30]
:nested {:a {:b {:c 42}}}})

(deftest command-context-spec-success
(let [ctx-spec (command-builtin/command-context-spec test-ctx)]
(testing "Successfull test cases"
(is (= {:color "#FF0000" :number 10 :deep 42}
(:instruction
(commando/execute {:commando/context ctx-spec}
{:color {:commando/context [:colors :red]}
:number {:commando/context [:numbers 0]}
:deep {:commando/context [:nested :a :b :c]}})))
"Should resolve keyword path in context with varios deepnest")

(is (= {:val "#0000FF" :count 2}
(:instruction
(commando/execute {:commando/context ctx-spec}
{:count {:commando/context [:colors] := count}
:val {:commando/context [:colors] := :blue}})))
"Should apply := fn to resolved value")

(is (= {:val-default "fallback" :val-nil nil}
(:instruction
(commando/execute {:commando/context ctx-spec}
{: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"}}
str-spec (command-builtin/command-context-spec str-ctx)]
(is (= {"val" "Ukrainian" "val-default" "none" "val-=" "English"}
(:instruction
(commando/execute {:commando/context str-spec}
{"val" {"commando-context" ["lang" "ua"]}
"val-=" {"commando-context" ["lang"] "=" "en"}
"val-default" {"commando-context" ["missing"] "default" "none"}})))
"String keys test")))
(testing "Failure test cases"
(is (helpers/status-map-contains-error?
(binding [commando-utils/*execute-config*
{:debug-result false :error-data-string false}]
(commando/execute {:commando/context ctx-spec}
{:val {:commando/context "NOT-A-PATH"}}))
(fn [error]
(= (-> error :error :data)
{:command-type :commando/context
:reason {:commando/context ["commando/context should be a sequential path: [:some :key]"]}
:path [:val]
:value {:commando/context "NOT-A-PATH"}})))
"Should fail validation when path is not sequential")
(is (helpers/status-map-contains-error?
(binding [commando-utils/*execute-config*
{:debug-result false :error-data-string false}]
(commando/execute {:commando/context ctx-spec}
{:val {:commando/context [:colors] "commando-context" ["colors"]}}))
(fn [error]
(= (-> error :error :data)
{:command-type :commando/context
:reason "The keyword :commando/context and the string \"commando-context\" cannot be used simultaneously in one command."
:path [:val]
:value {:commando/context [:colors] "commando-context" ["colors"]}})))
"Should not allow both keyword and string form"))))


;; ===========================
;; MUTATION-SPEC
Expand Down
Loading