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

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])
;; map — explicit keys
(registry-create {:commando/from from-spec, :commando/fn fn-spec})
```
Built registry can be modified with `registry-assoc` / `registry-dissoc` without rebuilding from scratch.

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.

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.

ADDED named anchor navigation for `:commando/from` paths. Declare an anchor with `"__anchor"` or `:__anchor` key in any instruction map, then reference it with `"@name"` as a path segment. The resolver walks up the tree and resolves to the nearest ancestor with that anchor name — independent of nesting depth. Anchors can be combined with existing `"../"` relative navigation in a single path.
Expand Down
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ As Commando is simply a graph-based resolver with easy configuration, it is not
The above function composes "Instructions", "Commands", and a "CommandRegistry".
- **Instruction**: a Clojure map, large or small, containing data and _commands_. The instruction describes the data structure and the transformations to apply.
- **Command**: a data-lexeme that is evaluated and returns a result. The rules for parsing and executing commands are flexible and customizable. Command `:command/from` return value by the absolute or relative path, can optionally apply a function provided under the `:=` key.
- **CommandRegistry**: a vector describing data-lexemes that should be treated as _commands_ by the library.
- **CommandRegistry**: a vector or map of CommandMapSpecs describing data-lexemes that should be treated as _commands_ by the library. When passed as a vector, the order defines the command scan priority. You can also pre-build a registry with `registry-create` and pass it directly.

### Builtin Functionality

Expand Down Expand Up @@ -356,12 +356,10 @@ Let's create a new command using a CommandMapSpec configuration map:
Now you can use it for more expressive operations like "summ=" and "multiply=" as shown below:

```clojure
;; Vector form — order defines scan priority
(def command-registry
(commando/create-registry
[;; Add `:commando/from`
commands-builtin/command-from-spec
;; Add `:CALC=` command to be handled
;; inside instruction
(commando/registry-create
[commands-builtin/command-from-spec
{:type :CALC=
:recognize-fn #(and (map? %) (contains? % :CALC=))
:validate-params-fn (fn [m]
Expand All @@ -372,6 +370,20 @@ Now you can use it for more expressive operations like "summ=" and "multiply=" a
(apply (:CALC= m) (:ARGS m)))
:dependencies {:mode :all-inside}}]))

;; Map form — explicit type keys
(def command-registry
(commando/registry-create
{:commando/from commands-builtin/command-from-spec
:CALC= {: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
{"1" {:values {:a 1 :b -1}}
Expand Down
172 changes: 86 additions & 86 deletions src/commando/core.cljc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns commando.core
(:require
[commando.impl.command-map :as cm]
[commando.impl.dependency :as deps]
[commando.impl.executing :as executing]
[commando.impl.finding-commands :as finding-commands]
Expand All @@ -8,8 +9,15 @@
[commando.impl.status-map :as smap]
[commando.impl.utils :as utils]))

(defn create-registry
"Creates a 'Command' registry from a vector of CommandMapSpecs:
;; -- Registry API --

(defn registry-create
"Creates a 'Command' registry from a map or vector of CommandMapSpecs.

Accepts either:
- A map of {type -> CommandMapSpec}
- A vector of CommandMapSpecs (order defines command scan priority)
- An already-built registry (returned as-is)

Each command specification (CommandMapSpec) should be a map containing at least:
- `:type` - a unique keyword identifying the command type
Expand All @@ -21,43 +29,80 @@
{:mode :all-inside} - all commands inside the current map are depednencies
{:mode :none} - no dependencies, the other commands may depend from it.
{:mode :point :point-key [:commando/from]} - special type of dependency
which declare that current command depends from the command it refer by
which declare that current command depends from the command it refer by
exampled :commando/from key.

Additional optional keys can include:
- `:validate-params-fn` - a function to validate command structures, and catch
invalid parameters at the anylisis stage. Only if the function
- `:validate-params-fn` - a function to validate command structures, and catch
invalid parameters at the anylisis stage. Only if the function
return 'true' it ment that the command structure is valid.
(fn [data] (throw ...)) => Failure
(fn [data] {:reason \"why\"}) => Failure
(fn [data] nil ) => Failure
(fn [data] false ) => Failure
(fn [data] true ) => OK

The function returns a built registry that can be used to resolve Instruction

Example
(create-registry
[{:type :print :recognize-fn ... :execute-fn ...}
commando.commands.builtin/command-fn-spec
commando.commands.builtin/command-apply-spec
commando.commands.builtin/command-mutation-spec
commando.commands.builtin/command-resolve-spec])"
[registry]
(registry/build (vec registry)))
The function returns a built registry that can be used to resolve Instruction

Example (map)
(registry-create
{:commando/from commando.commands.builtin/command-from-spec
:commando/fn commando.commands.builtin/command-fn-spec})

Example (vector — order defines scan priority)
(registry-create
[commando.commands.builtin/command-from-spec
commando.commands.builtin/command-fn-spec])"
([registry]
(registry-create registry nil))
([registry opts]
(cond
(registry/built? registry) registry
(vector? registry) (let [specs-map (into {} (map (juxt :type identity)) registry)
order (mapv :type registry)]
(registry/build specs-map (merge opts {:registry-order order})))
(map? registry) (registry/build registry opts)
:else (throw (ex-info "Registry must be a map, vector, or a built registry"
{:registry registry})))))

(defn registry-assoc
"Adds or replaces a CommandMapSpec in a built registry.
The spec is keyed by `command-map-spec-type` and appended to the scan order
if not already present. Revalidates the registry.

Example:
(-> (registry-create {...})
(registry-assoc :my/cmd my-cmd-spec))"
[built-registry command-map-spec-type command-map-spec]
(registry/registry-assoc built-registry command-map-spec-type command-map-spec))

(defn registry-dissoc
"Removes a CommandMapSpec from a built registry by its type key.
Updates the scan order accordingly. Revalidates the registry.

Example:
(-> (registry-create {...})
(registry-dissoc :my/cmd))"
[built-registry command-map-spec-type]
(registry/registry-dissoc built-registry command-map-spec-type))

;; -- Execute Flow --

(defn ^:private use-registry
[status-map registry]
(let [start-time (utils/now)
result (case (:status status-map)
:failed (-> status-map
(smap/status-map-handle-warning {:message "Skip step with registry check"}))
(smap/status-map-handle-warning {:message "Skip step with registry check"}))
:ok (try (-> status-map
(assoc :registry (if (registry/built? registry) registry (create-registry registry))))
(assoc :registry
(->
(registry-create registry)
(registry/enreach-runtime-registry))))
(catch #?(:clj Exception
:cljs :default)
e
(-> status-map
e
(-> status-map
(smap/status-map-handle-error {:message "Invalid registry specification"
:error (utils/serialize-exception e)})))))]
(smap/status-map-add-measurement result "use-registry" start-time (utils/now))))
Expand Down Expand Up @@ -118,8 +163,7 @@
(smap/status-map-handle-warning {:message (str utils/exception-message-header
"sort-entities-by-deps. Skipping mandatory step")}))
:ok (let [sort-result (graph/topological-sort (:internal/cm-dependency status-map))
status-map (assoc status-map :internal/cm-running-order (vec ;; (reverse (:sorted sort-result))
(:sorted sort-result)))]
status-map (assoc status-map :internal/cm-running-order (vec (:sorted sort-result)))]
(if (not-empty (:cyclic sort-result))
(smap/status-map-handle-error status-map
{:message (str utils/exception-message-header
Expand Down Expand Up @@ -158,72 +202,28 @@
(smap/status-map-handle-success {:message "All commands executed successfully"}))))))]
(smap/status-map-add-measurement result "execute-commands!" start-time (utils/now))))

(defn build-compiler
[registry instruction]
(let [status-map (-> (smap/status-map-pure {:instruction instruction})
(utils/hook-process (:hook-execute-start (utils/execute-config)))
(use-registry registry)
(find-commands)
(build-deps-tree)
(sort-commands-by-deps))]
(case (:status status-map)
:failed (-> status-map
(smap/status-map-handle-warning {:message (str utils/exception-message-header
"build-compiler. Error building compiler")}))
:ok (cond-> status-map
true (update-in [:internal/cm-running-order] registry/remove-instruction-commands-from-command-vector)
(false? (:debug-result (utils/execute-config)))
(select-keys [:uuid
: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
they have to be added to status-map before it be executed."
[compiler]
(if (and (registry/built? (get compiler :registry))
(contains? compiler :internal/cm-running-order)
(contains? compiler :status))
(case (:status compiler)
:ok (if (true? (:debug-result (utils/execute-config)))
(->
(smap/status-map-pure compiler))
(->
(smap/status-map-pure (select-keys compiler
[:uuid
:registry
:internal/cm-running-order
:successes
:warnings]))))
:failed compiler)
(->
(smap/status-map-pure compiler)
(smap/status-map-handle-error {:message "Corrupted compiler structure"}))))

(defn execute
[registry-or-compiler instruction]
{:pre [(or (map? registry-or-compiler) (sequential? registry-or-compiler))]}
[registry instruction]
{:pre [(or (map? registry) (vector? registry))]}
(binding [utils/*execute-internals* (utils/-execute-internals-push (str (random-uuid)))]
(let [ ;; Under (build-compiler) we ment the unfinished status map
start-time (utils/now)
status-map-with-compiler (-> (cond
(map? registry-or-compiler)
(->
(compiler->status-map registry-or-compiler))
(sequential? registry-or-compiler)
(->
(build-compiler registry-or-compiler instruction)
(compiler->status-map)))
(assoc :instruction instruction))]
(cond-> (execute-commands! status-map-with-compiler)
(false? (:debug-result (utils/execute-config))) (dissoc :internal/cm-running-order)
(false? (:debug-result (utils/execute-config))) (dissoc :registry)
:always (smap/status-map-add-measurement "execute" start-time (utils/now))
:always (utils/hook-process (:hook-execute-end (utils/execute-config)))))))
(let [start-time (utils/now)
status-map (-> (smap/status-map-pure {:instruction instruction})
(utils/hook-process (:hook-execute-start (utils/execute-config)))
(use-registry registry)
(find-commands)
(build-deps-tree)
(sort-commands-by-deps))]
(let [status-map-ready
(case (:status status-map)
:failed status-map
:ok (-> status-map
(update :internal/cm-running-order registry/remove-runtime-registry-commands-from-command-list)
(update :registry registry/reset-runtime-registry)))]
(cond-> (execute-commands! (assoc status-map-ready :instruction instruction))
(false? (:debug-result (utils/execute-config))) (dissoc :internal/cm-running-order)
(false? (:debug-result (utils/execute-config))) (dissoc :registry)
:always (smap/status-map-add-measurement "execute" start-time (utils/now))
:always (utils/hook-process (:hook-execute-end (utils/execute-config))))))))

(defn failed? [status-map] (smap/failed? status-map))
(defn ok? [status-map] (smap/ok? status-map))

50 changes: 25 additions & 25 deletions src/commando/impl/finding_commands.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"Finds and validates a command from registry that matches the given `value`.
Returns the command-spec if match is found and valid, nil otherwise.
Throws exception if match is found but validation fails."
[command-registry value path]
[command-spec-vector value path]
(some (fn [command-spec]
(when (command? command-spec value)
(let [value-valid-return (command-valid? command-spec value)]
Expand All @@ -69,31 +69,31 @@
:reason value-valid-return
:path path
:value value}))))))
command-registry))
command-spec-vector))

(defn find-commands
"Traverses the instruction tree (BFS algo) and collects all commands defined by the registry."
[instruction command-registry]
(loop [queue (vec [[]])
found-commands []
debug-stack-map {}]
(if (empty? queue)
found-commands
(let [current-path (first queue)
remaining-paths (subvec queue 1)
current-value (get-in instruction current-path)
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 (: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 (:debug-result (utils/execute-config))
(reduce #(assoc %1 %2 (conj debug-stack command)) debug-stack-map child-paths)
{})]
(recur (into remaining-paths child-paths) (conj found-commands command) updated-debug-stack-map))
;; No match - traverse children if coll, skip if leaf
(recur (into remaining-paths (coll-child-paths current-value current-path))
found-commands
debug-stack-map))))))

(let [command-spec-vector (:registry-runtime command-registry)]
(loop [queue (vec [[]])
found-commands []
debug-stack-map {}]
(if (empty? queue)
found-commands
(let [current-path (first queue)
remaining-paths (subvec queue 1)
current-value (get-in instruction current-path)
debug-stack (if (:debug-result (utils/execute-config)) (get debug-stack-map current-path (list)) (list))]
(if-let [command-spec (instruction-command-spec command-spec-vector current-value current-path)]
(let [command (cm/->CommandMapPath
current-path
(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 (:debug-result (utils/execute-config))
(reduce #(assoc %1 %2 (conj debug-stack command)) debug-stack-map child-paths)
{})]
(recur (into remaining-paths child-paths) (conj found-commands command) updated-debug-stack-map))
;; No match - traverse children if coll, skip if leaf
(recur (into remaining-paths (coll-child-paths current-value current-path))
found-commands
debug-stack-map)))))))
Loading
Loading