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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ UPDATED build-deps-tree. Instead of searching dependency using the list of comma

FIXED find-commands. StackOverflowException in case of long lists of dependencies in the one level.

REMOVED all `*-json-spec` builin commands were joined with it origin forms. Like `commando-from-json-spec` at now are handled by the original `commando-from-spec`, user just may use `:commando/from` either `"commando-from"` key to defining logic. Covering this special "string-based" instructions with tests.

UPDATED documentation about how to use commando DSL [with an JSON structure](./doc/json.md).

ADDED to `commando.impl.utils` two helper functions: `print-stats` - to print status-map `:stats` key into output; `print-deep-stats` - printing the flamegraph basing on `:stats` of every internal `commando/execution`(very helpfull for debugging macroses or query_dsl)

# 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.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ Let's create a new command using a CommandMapSpec configuration map:
- `: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 :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)
- `{:mode :point :point-key [:commando/from]}` - allowing to be dependent anywhere in the instructions. Expects point-key(-s) 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:

Expand Down Expand Up @@ -516,7 +516,7 @@ Here's an example of how to use `:debug-result`:
:recognize-fn #function[commando.commands.builtin/fn],
:validate-params-fn #function[commando.commands.builtin/fn],
:apply #function[commando.commands.builtin/fn],
:dependencies {:mode :point, :point-key :commando/from}}],
:dependencies {:mode :point, :point-key [:commando/from]}}],
:warnings [],
:errors [],
:successes
Expand Down
2 changes: 1 addition & 1 deletion doc/integrant.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Let's define CommandMapSpec for `:integrant/from`:
(ig/ref (:integrant/component-alias integrant-component))
(throw (ex-info "`:integrant/from` Exception. term pointing on something that not a `:integrant/component` term " term-data)))))
:dependencies {:mode :point
:point-key :integrant/from}})
:point-key [:integrant/from]}})
```

Just like Commando’s basic commands, you specify how to recognize a component reference, how to validate it, and what to produce on evaluation.
Expand Down
117 changes: 95 additions & 22 deletions doc/json.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,118 @@
# Working with JSON

Commando supports the idea of describing instructions using JSON structures. This is useful for storing, editing, and transporting command instructions, especially when interoperability between systems is required.
Since Commando is a technology that allows you to create your own DSLs, a critical aspect is the format of the data structures you process. JSON is a common choice for APIs, serialization, and database storage. Therefore, your DSL must be adaptable and work seamlessly outside the Clojure ecosystem.

For example, imagine you want to calculate the scalar (dot) product of two vectors described as JSON:
## The Challenge: Keywords vs. Strings

```js
{"vector-1": {"x": 1, "y": 2},
"vector-2": {"x": 4, "y": 5},
"scalar-product-value":
{"commando-mutation": "dot-product",
"v1": {"commando-from": ["vector-1"]},
"v2": {"commando-from": ["vector-2"]}}}
Commando is idiomatic Clojure and heavily relies on namespaced keywords (e.g., `:commando/from`, `:commando/mutation`). JSON, however, does not support keywords; it only uses strings for object keys. This presents a challenge when an instruction needs to be represented in JSON format.

## The Solution: String-Based Commands

Commando's built-in commands are designed to work with string-based keys out of the box, allowing for seamless JSON interoperability. When parsing instructions, Commando recognizes both the keyword version (e.g., `:commando/mutation`) and its string counterpart (`"commando-mutation"`).

This allows you to define instructions in pure JSON, slurp in clojure, parse and have them executed by Commando.

### Example: Vector Dot Product

Imagine you want to calculate the scalar (dot) product of two vectors described in a JSON file.

**`vectors.json`:**
```json
{
"vector-1": { "x": 1, "y": 2 },
"vector-2": { "x": 4, "y": 5 },
"scalar-product-value": {
"commando-mutation": "dot-product",
"v1": { "commando-from": ["vector-1"] },
"v2": { "commando-from": ["vector-2"] }
}
}
```

Since JSON does not support namespaced keywords like Clojure does, we use alternative built-in keys, replacing `commando/mutation` with `"commando-mutation"`. This allows Commando to parse and execute structured instructions from JSON as if they were native Clojure maps.
Notice the use of `"commando-mutation"` and `"commando-from"` as string keys.

Let's declare a mutation handler for the `"commando-mutation"` command—a function that will help us obtain the scalar product of two vectors:
To handle the custom `"dot-product"` mutation, you define a `defmethod` for `commando.commands.builtin/command-mutation` that dispatches on the string `"dot-product"`. When destructuring the parameters map, you must also use `:strs` to correctly access the string-keyed values (`v1`, `v2`).

```clojure
(require '[commando.commands.builtin :as commands-builtin])
(require '[commando.commands.builtin :as commands-builtin]
'[commando.core :as commando]
'[clojure.data.json :as json])

;; Define the mutation handler for the "dot-product" string identifier
(defmethod commands-builtin/command-mutation "dot-product" [_ {:strs [v1 v2]}]
(->> ["x" "y"]
(map #(* (get v1 %) (get v2 %)))
(reduce + 0)))

;; Read the JSON file and execute the instruction
(let [json-string (slurp "vectors.json")
instruction (json/read-str json-string)]
(commando/execute
[commands-builtin/command-mutation-spec
commands-builtin/command-from-spec]
instruction))
```

Now, let's see how the instruction looks in practice:
When executed, Commando correctly resolves the dependencies and applies the mutation, producing the final instruction map:

```clojure
(require '[commando.core :as commando])
(require '[commando.commands.builtin :as commands-builtin])

(commando/execute
[commands-builtin/command-mutation-json-spec
commands-builtin/command-from-json-spec]
(clojure.data.json/read-str
(slurp "vector-scalar.json")))
;; =>
{:instruction
{:status :ok
:instruction
{"vector-1" {"x" 1, "y" 2},
"vector-2" {"x" 4, "y" 5},
"scalar-product-value" 14}}
```

By supporting string-based keys for its commands, Commando makes it easy to build powerful, data-driven systems that can be defined and serialized using the ubiquitous JSON format. For more details on creating custom commands, see the [main README](../README.md).

## Important Note on String-Based Commands

It's important to understand that only a select few core commands have direct string-based equivalents for JSON interoperability. These are primarily:

* `commando.commands.builtin/command-macro-spec` (`"commando-macro"`)
* `commando.commands.builtin/command-from-spec` (`"commando-from"`)
* `commando.commands.builtin/command-mutation-spec` (`"commando-mutation"`)
* `commando.commands.query-dsl/command-resolve-spec` (`"commando-resolve"`)

Other commands, such as `:commando/apply` or `:commando/fn`, are more tightly coupled with Clojure's functional mechanisms and do not have direct string-based aliases.

### Leveraging `commando-macro-spec` for JSON Instructions

For scenarios where you need to define complex logic using string keys in JSON, but still want to utilize Clojure-specific commands, `commando-macro-spec` (with its string alias `"commando-macro"`) is your most powerful tool.

You can define a macro with a string identifier in your Clojure code, and within that macro's `defmethod`, you can use any Clojure-idiomatic commands (e.g., `:commando/apply`, `:commando/from`, or custom Clojure-based commands).

This allows you to declare high-level logic in your JSON instruction using string keys, while encapsulating the more intricate, Clojure-specific command structures within the macro definition. The macro acts as a bridge, expanding the JSON-friendly instruction into a full Clojure-based Commando instruction at runtime.

Here is a brief example illustrating the concept.

**JSON Instruction:**
```json
{
"calculation-result": {
"commando-macro": "calculate-and-format",
"input-a": 10,
"input-b": 25
}
}
```

**Commando Macro Definition:**
```clojure
(require '[commando.commands.builtin :as commands-builtin])

(defmethod commands-builtin/command-macro "calculate-and-format"
[_ {:strs [input-a input-b]}]
;; Inside the macro, we can use Clojure-native commands with keywords
;; to define the complex logic that will be expanded at runtime.
{:= :formatted-output
:commando/apply
{:raw-result {:commando/fn (fn [& [a b]] (+ a b))
:args [input-a input-b]}
:formatted-output {:commando/fn (fn [& args] (apply str args))
:args ["The result is: " {:commando/from [:commando/apply :raw-result]}]}}})
;; => "35"
```

In this example, the JSON file uses the string-based `"commando-macro"` to invoke `"calculate-and-format"`. The corresponding `defmethod` in Clojure takes the string inputs, then expands into a more complex instruction using keyword-based commands like `:commando/apply`, `:commando/fn`, and `:commando/from` to perform the actual logic.
6 changes: 3 additions & 3 deletions doc/query_dsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -619,9 +619,9 @@ Because the Query DSL is built on Commando, you can easily combine it with other
:option/color "crystal red"})
```

### Working with JSON
### Working with Strings

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 input (e.g. from an HTTP request), use string keys to describe Instructions (instead `:commando/resolve` use `"commando-resolve"`) and QueryExpressions(also with string keys).

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.

Expand All @@ -638,7 +638,7 @@ Note that defmethod dispatches on a string ("instant-car-model") and the paramet
QueryExpression))

(commando.core/execute
[commando.commands.query-dsl/command-resolve-json-spec]
[commando.commands.query-dsl/command-resolve-spec]
(clojure.data.json/read-str
"{\"commando-resolve\":\"instant-car-model\",
\"QueryExpression\":
Expand Down
Loading
Loading