diff --git a/CHANGELOG.md b/CHANGELOG.md index 206aa70..670370d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index a030f49..e21dd71 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 diff --git a/doc/integrant.md b/doc/integrant.md index c7ff288..ff5fca6 100644 --- a/doc/integrant.md +++ b/doc/integrant.md @@ -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. diff --git a/doc/json.md b/doc/json.md index 0860270..acee202 100644 --- a/doc/json.md +++ b/doc/json.md @@ -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. diff --git a/doc/query_dsl.md b/doc/query_dsl.md index afceed0..940ed4e 100644 --- a/doc/query_dsl.md +++ b/doc/query_dsl.md @@ -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. @@ -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\": diff --git a/src/commando/commands/builtin.cljc b/src/commando/commands/builtin.cljc index b7296c8..e1d3d25 100644 --- a/src/commando/commands/builtin.cljc +++ b/src/commando/commands/builtin.cljc @@ -95,6 +95,11 @@ ;; From ;; ====================== +(def ^:private -malli:commando-from-path + (malli/deref + [:sequential {:error/message "commando/from should be a sequence path to value in Instruction: [:some 2 \"value\"]"} + [:or :string :keyword :int]])) + (def ^{:doc " Description @@ -127,87 +132,73 @@ :result {:commando/from [\"../\" :value]}}})) => {:a {:value 1, :result 1}, :b {:value 2, :result 2}} - See Also - `commando.core/execute` - `commando.commands.builtin/command-fn-spec` - `commando.commands.builtin/command-from-spec`"} - command-from-spec - {:type :commando/from - :recognize-fn #(and (map? %) (contains? % :commando/from)) - :validate-params-fn (fn [m] - (if-let [m-explain - (malli-error/humanize - (malli/explain [:map - [:commando/from - [:sequential {:error/message "commando/from should be a sequence path to value in Instruction: [:some 2 \"value\"]"} - [:or :string :keyword :int]]] - [:= {:optional true} [:or utils/ResolvableFn :string]]] - m))] - m-explain - true)) - :apply (fn [instruction command-path-obj command-map] - (let [path-to-another-command (deps/point-target-path instruction command-path-obj) - result (get-in instruction path-to-another-command) - result (let [m-= (:= command-map)] - (if m-= (if (string? m-=) - (get result m-=) - (let [m-= (utils/resolve-fn m-=)] - (m-= result))) result))] - result)) - :dependencies {:mode :point - :point-key :commando/from}}) - -(def ^{:doc " - Description - command-from-json-spec - get value from another command or existing value - in Instruction. Path to another command is passed inside `\"commando-from\"` - key, optionally you can get value of object by using `\"=\"` key. - - Path can be sequence of keywords, strings or integers, starting absolutely from - the root of Instruction, or relatively from the current command position by - using \"../\" and \"./\" strings in paths. - - [\"some\" 2 \"value\"] - absolute path, started from the root key \"some\" - [\"../\" 2 \"value\"] - relative path, go up one level and then down to [2 \"value\"] - - Example (:instruction - (commando/execute [command-from-json-spec] + (commando/execute [command-from-spec] {\"a\" {\"value\" {\"container\" 1} \"result\" {\"commando-from\" [\"../\" \"value\"] \"=\" \"container\"}} \"b\" {\"value\" {\"container\" 2} \"result\" {\"commando-from\" [\"../\" \"value\"] \"=\" \"container\"}}})) - {\"a\" {\"value\" {\"container\" 1}, \"result\" 1}, - \"b\" {\"value\" {\"container\" 2}, \"result\" 2}} + => /{\"a\" {\"value\" {\"container\" 1}, \"result\" 1}, + / \"b\" {\"value\" {\"container\" 2}, \"result\" 2}} See Also `commando.core/execute` - `commando.commands.builtin/command-fn-spec`"} - command-from-json-spec - {:type :commando/from-json - :recognize-fn #(and (map? %) (contains? % "commando-from")) + `commando.commands.builtin/command-fn-spec` + `commando.commands.builtin/command-from-spec`"} + command-from-spec + {:type :commando/from + :recognize-fn #(and (map? %) + (or + (contains? % :commando/from) + (contains? % "commando-from"))) :validate-params-fn (fn [m] - (if-let [m-explain - (malli-error/humanize - (malli/explain [:map - ["commando-from" - [:sequential {:error/message "commando-from should be a sequence path to value in Instruction: [\"some\" 2 \"value\"]"} - [:or :string :int]]] - ["=" {:optional true} [:string {:min 1}]]] m))] - m-explain - true)) + (let [m-explain + (cond + (and + (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] + [:= {:optional true} [:or utils/ResolvableFn :string]]] + m)) + (contains? m "commando-from") + (malli-error/humanize + (malli/explain + [:map + ["commando-from" -malli:commando-from-path] + ["=" {:optional true} [:string {:min 1}]]] + m)))] + (if m-explain + m-explain + true))) :apply (fn [instruction command-path-obj command-map] - (let [path-to-another-command (deps/point-target-path instruction command-path-obj) - result (get-in instruction path-to-another-command) - result (if-let [m-= (get command-map "=")] - (if (string? m-=) - (get result m-=) - result) - result)] - result)) + (cond + (contains? command-map :commando/from) + (let [path-to-another-command (deps/point-target-path instruction command-path-obj) + result (get-in instruction path-to-another-command) + result (let [m-= (:= command-map)] + (if m-= (if (string? m-=) + (get result m-=) + (let [m-= (utils/resolve-fn m-=)] + (m-= result))) result))] + result) + (contains? command-map "commando-from") + (let [path-to-another-command (deps/point-target-path instruction command-path-obj) + result (get-in instruction path-to-another-command) + result (if-let [m-= (get command-map "=")] + (if (string? m-=) + (get result m-=) + result) + result)] + result))) :dependencies {:mode :point - :point-key "commando-from"}}) + :point-key [:commando/from + "commando-from"]}}) ;; ====================== ;; Mutation @@ -227,8 +218,8 @@ (def ^{:doc " Description command-mutation-spec - execute mutation of Instruction data. - Mutation type is passed inside `:commando/mutation` key and arguments - to mutation passed inside rest of map. + Mutation id is passed inside `:commando/mutation` or `\"commando-mutation\"` + key and arguments to mutation passed inside rest of map. To declare mutation create method of `command-mutation` multimethod @@ -246,32 +237,7 @@ :b {:commando/mutation :generate-string :length 5}})) => {:a {:random-number 14}, :b {:random-string \"5a379\"}} - See Also - `commando.core/execute` - `commando.commands.builtin/command-mutation-spec` - `commando.commands.builtin/command-mutation`"} - command-mutation-spec - {:type :commando/mutation - :recognize-fn #(and (map? %) (contains? % :commando/mutation)) - :validate-params-fn (fn [m] - (if-let [m-explain (malli-error/humanize - (malli/explain [:map [:commando/mutation :keyword]] m))] - m-explain - true)) - :apply (fn [_instruction _command-map m] - (let [m-tx-type (:commando/mutation m) m (dissoc m :commando/mutation)] - (command-mutation m-tx-type m))) - :dependencies {:mode :all-inside}}) - -(def ^{:doc " - Description - command-mutation-json-spec - execute mutation of Instruction data. - Mutation type is passed inside `\"commando-mutation\"` key and arguments - to mutation passed inside rest of map. - - To declare mutation create method of `command-mutation` multimethod - - Example + Example with-string keys (defmethod commando.commands.builtin/command-mutation \"generate-string\" [_ {:strs [length]}] {\"random-string\" (apply str (repeatedly (or length 10) #(rand-nth \"abcdefghijklmnopqrstuvwxyz0123456789\")))}) @@ -280,7 +246,7 @@ (:instruction (commando/execute - [command-mutation-json-spec] + [command-mutation-spec] {\"a\" {\"commando-mutation\" \"generate-number\" \"from\" 10 \"to\" 20} \"b\" {\"commando-mutation\" \"generate-string\" \"length\" 5}})) => {\"a\" {\"random-number\" 18}, \"b\" {\"random-string\" \"m3gj1\"}} @@ -288,21 +254,41 @@ See Also `commando.core/execute` `commando.commands.builtin/command-mutation-spec` - `commando.commands.builtin/command-mutation-json-spec` `commando.commands.builtin/command-mutation`"} - command-mutation-json-spec - {:type :commando/mutation-json - :recognize-fn #(and (map? %) (contains? % "commando-mutation")) + command-mutation-spec + {:type :commando/mutation + :recognize-fn #(and (map? %) + (or + (contains? % :commando/mutation) + (contains? % "commando-mutation"))) :validate-params-fn (fn [m] - (if-let [m-explain (malli-error/humanize - (malli/explain [:map ["commando-mutation" [:string {:min 1}]]] m))] - m-explain - true)) + (let [m-explain + (cond + (and + (contains? m :commando/mutation) + (contains? m "commando-mutation")) + "The keyword :commando/mutation and the string \"commando-mutation\" cannot be used simultaneously in one command." + (contains? m :commando/mutation) + (malli-error/humanize + (malli/explain + [:map [:commando/mutation [:or :keyword :string]]] + m)) + (contains? m "commando-mutation") + (malli-error/humanize + (malli/explain + [:map ["commando-mutation" [:or :keyword :string]]] + m)))] + (if m-explain + m-explain + true))) :apply (fn [_instruction _command-map m] - (let [m-tx-type (get m "commando-mutation") m (dissoc m "commando-mutation")] (command-mutation m-tx-type m))) + (cond + (contains? m :commando/mutation) + (command-mutation (get m :commando/mutation) (dissoc m :commando/mutation)) + (contains? m "commando-mutation") + (command-mutation (get m "commando-mutation") (dissoc m "commando-mutation")))) :dependencies {:mode :all-inside}}) - ;; ====================== ;; Macro ;; ====================== @@ -319,7 +305,8 @@ Description command-macro-spec - help to define reusable instruction template, what execute instruction using the same registry as the current one. - To declare macro expand `command-mutation` multimethod. + Macro id is passed inside `:commando/macro` or `\"commando-macro\"` + key and arguments to mutation passed inside rest of map. Example Asume we have two vectors with string numbers: @@ -402,19 +389,36 @@ {:type :commando/macro :recognize-fn #(and (map? %) - (contains? % :commando/macro)) + (or + (contains? % :commando/macro) + (contains? % "commando-macro"))) :validate-params-fn (fn [m] - (if-let [explain-m - (malli-error/humanize - (malli/explain - [:map - [:commando/macro {:optional true} :keyword]] - m))] - explain-m - true)) + (let [m-explain + (cond + (and + (contains? m :commando/macro) + (contains? m "commando-macro")) + "The keyword :commando/macro and the string \"commando-macro\" cannot be used simultaneously in one command." + (contains? m :commando/macro) + (malli-error/humanize + (malli/explain + [:map + [:commando/macro [:or :keyword :string]]] + m)) + (contains? m "commando-macro") + (malli-error/humanize + (malli/explain + [:map + ["commando-macro" [:or :keyword :string]]] + m)))] + (if m-explain + m-explain + true))) :apply (fn [_instruction _command-map m] - (let [macro-type (get m :commando/macro) - macro-data (dissoc m :commando/macro) + (let [[macro-type macro-data] + (cond + (get m :commando/macro) [(get m :commando/macro) (dissoc m :commando/macro)] + (get m "commando-macro") [(get m "commando-macro") (dissoc m "commando-macro")]) result (commando/execute (utils/command-map-spec-registry) (command-macro macro-type macro-data))] @@ -423,41 +427,3 @@ (throw (ex-info (str utils/exception-message-header "command-macro. Failure execution :commando/macro") result))))) :dependencies {:mode :all-inside}}) -(def ^{:doc " - Description - command-macro-json-spec - help to define reusable instruction template, - what execute instruction using the same registry as the current one. - To declare macro expand `command-mutation` multimethod. Using string - key \"commando-macro\" for declaring macroses instead of keyword :commando/macro. - - Example - read one from `command-macro-spec`. - - See Also - `commando.core/execute` - `commando.commands.builtin/command-macro` - `commando.commands.builtin/command-macro-spec`"} - command-macro-json-spec - {:type :commando/macro-json - :recognize-fn #(and - (map? %) - (contains? % "commando-macro")) - :validate-params-fn (fn [m] - (if-let [explain-m - (malli-error/humanize - (malli/explain - [:map - ["commando-macro" {:optional true} :string]] - m))] - explain-m - true)) - :apply (fn [_instruction _command-map m] - (let [macro-type (get m "commando-macro") - macro-data (dissoc m "commando-macro") - result (commando/execute - (utils/command-map-spec-registry) - (command-macro macro-type macro-data))] - (if (= :ok (:status result)) - (:instruction result) - (throw (ex-info (str utils/exception-message-header "command-macro. Failure execution :commando/macro") result))))) - :dependencies {:mode :all-inside}}) diff --git a/src/commando/commands/query_dsl.cljc b/src/commando/commands/query_dsl.cljc index c4d1aab..477b646 100644 --- a/src/commando/commands/query_dsl.cljc +++ b/src/commando/commands/query_dsl.cljc @@ -4,7 +4,7 @@ [commando.impl.status-map :as smap] [commando.impl.utils :as commando-utils] [malli.core :as malli] - [malli.error] + [malli.error :as malli-error] #?(:clj [clojure.pprint :as pprint]))) (def ^:private exception-message-header (str commando-utils/exception-message-header "QueryDSL. ")) @@ -336,21 +336,34 @@ but allow invoking `commando/execute` internally inside the evaluation step, what make it usefull for querying data. - Querying(internal execution) controlled by QueryExpression - custom - DSL, what visually mention the EQL. + Resolve id is passed inside `:commando/resolve` or `\"commando-resolve\"` + key and arguments to resolver passed inside rest of map. + + Querying controlled by QueryExpression - custom DSL, + what visually mention the EQL. QueryExpression Syntax Example - [:a0 - [:b0 {:b0props {}}] - {:c0 - [:a1 - :b1 - {:c1 - [:a2 - :b2]}]} - {[:d0 {:d0props {}}] - [:a1 - :b1]}] + [:a0 + [:b0 {:b0props {}}] + {:c0 + [:a1 + {:c1 + [:a2 + :b2]}]} + {[:d0 {:d0props {}}] + [:a1 + :b1]}] + or even with using strings + [\"a0\" + [\"b0\" {\"b0props\" {}}] + {\"c0\" + [\"a1\" + {\"c1\" + [\"a2\" + \"b2\"]}]} + {[\"d0\" {\"d0props\" {}}] + [\"a1\" + \"b1\"]}] Example (defmethod commando.commands.builtin/command-mutation :generate-password [_ _] @@ -415,55 +428,7 @@ => {:first-name \"Adam\", :UNEXISTING {:status :failed, :errors [{:message \"Commando. Graph Query. QueryExpression attribute ':UNEXISTING' is unreachable\"}]}} - Parts - `commando.commands.query-dsl/resolve-instruction-qe` run internal call of `commando/execute`. - `commando.commands.query-dsl/->query-run` trim query data according to passed QueryExpression - `commando.commands.query-dsl/command-resolve` multimethod to declare resolvers. - - See Also - `commando.core/execute` - `commando.commands.query-dsl/command-mutation-spec` - `commando.commands.builtin/command-mutation-spec`"} - command-resolve-spec - {:type :commando/resolve - :recognize-fn #(and (map? %) (contains? % :commando/resolve)) - :validate-params-fn (fn [m] - (if-let [explain-m - (malli.error/humanize - (malli/explain - [:map - [:commando/resolve :keyword] - [:QueryExpression - {:optional true} - QueryExpressionMalli]] - m))] - explain-m - true)) - :apply (fn [_instruction _command-map m] (command-resolve (:commando/resolve m) (dissoc m :commando/resolve))) - :dependencies {:mode :all-inside}}) - -(def ^{:doc " - Description - command-resolve-json-spec - like command-resolve-spec but - use \"string\" keys instead of Keywords - - Querying(internal execution) controlled by QueryExpression - custom - DSL, what visually mention the EQL. - - QueryExpression syntax example - [\"a0\" - [\"b0\" {\"b0props\" {}}] - {\"c0\" - [\"a1\" - \"b1\" - {\"c1\" - [\"a2\" - \"b2\"]}]} - {[\"d0\" {\"d0props\" {}}] - [\"a1\" - \"b1\"]}] - - Example + Example with using string keys (defmethod commando.commands.builtin/command-mutation \"generate-password\" [_ _] {\"random-string\" (apply str (repeatedly 20 #(rand-nth \"abcdefghijklmnopqrstuvwxyz0123456789\")))}) @@ -495,8 +460,8 @@ ;; Let try to use it! (:instruction (commando.core/execute - [commands-builtin/command-mutation-json-spec - command-resolve-json-spec] + [commands-builtin/command-mutation-spec + command-resolve-spec] {\"commando-resolve\" \"query-user\" \"QueryExpression\" [\"first-name\" @@ -539,22 +504,41 @@ `commando.core/execute` `commando.commands.query-dsl/command-mutation-spec` `commando.commands.builtin/command-mutation-spec`"} - command-resolve-json-spec - {:type :commando/resolve-json - :recognize-fn #(and (map? %) (contains? % "commando-resolve")) + command-resolve-spec + {:type :commando/resolve + :recognize-fn #(and (map? %) (or + (contains? % :commando/resolve) + (contains? % "commando-resolve"))) :validate-params-fn (fn [m] - (if-let [explain-m - (malli.error/humanize - (malli/explain - [:map - ["commando-resolve" [:string {:min 1}]] - ["QueryExpression" - {:optional true} - QueryExpressionMalli]] - m))] - explain-m - true)) + (let [m-explain + (cond + (and + (contains? m :commando/resolve) + (contains? m "commando-resolve")) + "The keyword :commando/resolve and the string \"commando-resolve\" cannot be used simultaneously in one command." + (contains? m :commando/resolve) + (malli-error/humanize + (malli/explain + [:map + [:commando/resolve :keyword] + [:QueryExpression + {:optional true} + QueryExpressionMalli]] + m)) + (contains? m "commando-resolve") + (malli-error/humanize + (malli/explain + [:map + ["commando-resolve" [:string {:min 1}]] + ["QueryExpression" + {:optional true} + QueryExpressionMalli]] + m)))] + (if m-explain + m-explain + true))) :apply (fn [_instruction _command-map m] - (command-resolve (get m "commando-resolve") (dissoc m "commando-resolve"))) + (cond + (contains? m :commando/resolve) (command-resolve (get m :commando/resolve ) (dissoc m :commando/resolve)) + (contains? m "commando-resolve") (command-resolve (get m "commando-resolve") (dissoc m "commando-resolve")))) :dependencies {:mode :all-inside}}) - diff --git a/src/commando/core.cljc b/src/commando/core.cljc index 8abf93f..6b1bd07 100644 --- a/src/commando/core.cljc +++ b/src/commando/core.cljc @@ -20,7 +20,7 @@ - `:dependencies` - declare way the command should build dependency {: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 + {:mode :point :point-key [:commando/from]} - special type of dependency which declare that current command depends from the command it refer by exampled :commando/from key. diff --git a/src/commando/impl/command_map.cljc b/src/commando/impl/command_map.cljc index 309de34..6b1b025 100644 --- a/src/commando/impl/command_map.cljc +++ b/src/commando/impl/command_map.cljc @@ -93,7 +93,7 @@ [:multi {:dispatch :mode} [:none [:map]] [:all-inside [:map]] - [:point [:map [:point-key [:or :keyword :string]]]]]]]] + [:point [:map [:point-key [:+ [:or :keyword :string]]]]]]]]] {:registry (merge (malli/default-schemas) (malli-util/schemas))})) (defn validate-command-spec diff --git a/src/commando/impl/dependency.cljc b/src/commando/impl/dependency.cljc index 50f753a..d281634 100644 --- a/src/commando/impl/dependency.cljc +++ b/src/commando/impl/dependency.cljc @@ -87,21 +87,32 @@ "Throws a standardized error for missing point dependencies." [command-path-obj target-path instruction] (let [deps-config (:dependencies (cm/command-data command-path-obj)) + command-map (get-in instruction (cm/command-path command-path-obj)) + point-key-config (:point-key deps-config) + actual-key (if (sequential? point-key-config) + (reduce (fn [_ point-key] (when (contains? command-map point-key) (reduced point-key))) + (first point-key-config) + point-key-config) + point-key-config) error-msg (str utils/exception-message-header - "Point dependency failed: key '" (:point-key deps-config) + "Point dependency failed: key '" actual-key "' references non-existent path " target-path)] (throw (ex-info error-msg {:message error-msg :path (cm/command-path command-path-obj) - :command (get-in instruction (cm/command-path command-path-obj))})))) + :command command-map})))) (defn point-target-path "Returns the target path for a :point dependency, resolving relative navigation." [instruction command-path-obj] - (let [point-key (get-in (cm/command-data command-path-obj) [:dependencies :point-key]) + (let [point-key-seq (get-in (cm/command-data command-path-obj) [:dependencies :point-key]) command-path (cm/command-path command-path-obj) command-map (get-in instruction command-path) - pointed-path (get command-map point-key)] + pointed-path (reduce (fn [_ point-key] + (when-let [pointed-path (get command-map point-key)] + (reduced pointed-path))) + nil + point-key-seq)] (->> pointed-path (resolve-relative-path command-path) vec))) diff --git a/src/commando/impl/utils.cljc b/src/commando/impl/utils.cljc index 35e8aa2..f85eade 100644 --- a/src/commando/impl/utils.cljc +++ b/src/commando/impl/utils.cljc @@ -48,7 +48,7 @@ - `:uuid` the unique name of execution, generated everytime the user invoke `commando.core/execute` - `:stack` in case of user use `commando.commands.builtin/command-macro-spec`, - or `commando.commands.query-dsl/command-resolve-json-spec` or any sort of + or `commando.commands.query-dsl/command-resolve-spec` or any sort of commands what invoking `commando.core/execute` inside of parent instruction by simulation recursive call, the :stack key will store the invocation stack in vector of :uuids" @@ -295,3 +295,143 @@ See (serialize-exception-fn e) (map? e) e :else {:message (str e)})) + + +;; ----------- +;; Stats Tools +;; ----------- + +;; print stats of execution + +(defn print-stats + "Prints a formatted summary of the execution stats from a status-map. + + Example + (print-stats + (commando.core/execute + [commando.commands.builtin/command-from-spec] + {\"1\" 1 + \"2\" {:commando/from [\"1\"]} + \"3\" {:commando/from [\"2\"]}})) + OUT=> + Execution Stats: + 1 execute-commands! 281.453µs + = execute 1.926956ms + + (print-stats + (binding [commando.impl.utils/*execute-config* + {:debug-result true}] + (commando.core/execute + [commando.commands.builtin/command-from-spec] + {\"1\" 1 + \"2\" {:commando/from [\"1\"]} + \"3\" {:commando/from [\"2\"]}}))) + OUT=> + Execution Stats: + 1 use-registry 141.373µs + 2 find-commands 719.128µs + 3 build-deps-tree 141.061µs + 4 sort-commands-by-deps 112.841µs + 5 execute-commands! 78.601µs + = execute 1.466249ms + + + See More + `commando.impl.utils/*execute-config*`" + ([status-map] + (print-stats status-map nil)) + ([status-map title] + (when-let [stats (:stats status-map)] + (let [max-key-len (apply max 0 (map (comp count name first) stats))] + (println (str "\nExecution Stats" (when title (str "(" title ")")) ":")) + (doseq [[index [stat-key _ formatted]] (map-indexed vector stats)] + (let [key-str (name stat-key) + padding (str/join "" (repeat (- max-key-len (count key-str)) " "))] + (println (str + " " (if (= "execute" key-str) "=" (str (inc index)) ) + " " key-str " " padding formatted)))))))) + + +;; print stats for all internal executions + +(defn ^:private flame-print-stats [stats indent] + (let [max-key-len (apply max 0 (map (comp count name first) stats))] + (doseq [[stat-key _ formatted] stats] + (let [key-str (name stat-key) + padding (str/join "" (repeat (- max-key-len (count key-str)) " "))] + (println (str indent + "" key-str " " padding formatted)))))) + +(defn ^:private flame-print [data & [indent]] + (let [indent (or indent "")] + (doseq [[k v] data] + (println (str indent "———" k)) + (when (:stats v) + (flame-print-stats (:stats v) (str indent " |"))) + (doseq [[child-k child-v] v + :when (map? child-v)] + (when (not= child-k :stats) + (flame-print {child-k child-v} (str indent " :"))))))) + +(defn ^:private flamegraph [data] + (println "Printing Flamegraph for executes:") + (flame-print data)) + +(defn print-deep-stats + "Function print the flamegraph of internals execution. + + Example + (defmethod commando.commands.builtin/command-mutation :rand-n + [_macro-type {:keys [v]}] + (:instruction + (commando.core/execute + [commando.commands.builtin/command-apply-spec] + {:commando/apply v + := (fn [n] (rand-int n))}))) + + (defmethod commando.commands.builtin/command-macro :sum-n + [_macro-type {:keys [v]}] + {:commando/fn (fn [& v-coll] (apply + v-coll)) + :args [v + {:commando/mutation :rand-n + :v 200}]}) + + (print-deep-stats + #(commando.core/execute + [commando.commands.builtin/command-fn-spec + commando.commands.builtin/command-from-spec + commando.commands.builtin/command-macro-spec + commando.commands.builtin/command-mutation-spec] + {:value {:commando/mutation :rand-n :v 200} + :result {:commando/macro :sum-n + :v {:commando/from [:value]}}})) + + OUT=> + Printing Flamegraph for executes: + ———59f2f084-28f6-44fd-bf52-1e561187a2e5 + |execute-commands! 1.123606ms + |execute 1.92817ms + :———e4e245ca-194a-43c6-9d7e-9225e0424c46 + : |execute-commands! 66.344µs + : |execute 287.669µs + :———77de8840-c9d3-4baa-b0d6-8a9806ede29d + : |execute-commands! 372.566µs + : |execute 721.636µs + : :———0aefeb8e-04b2-4e77-b526-6969c08f9bb5 + : : |execute-commands! 39.221µs + : : |execute 264.591µs + " + [execution-fn] + (let [stats-state (atom {}) + result + (binding [*execute-config* + {;; :debug-result true + :hook-execute-end + (fn [e] + (swap! stats-state + (fn [s] + (update-in s (:stack *execute-internals*) + #(merge % {:stats (:stats e)})))))}] + (execution-fn))] + (flamegraph @stats-state) + result)) diff --git a/test/perf/commando/core_perf_test.clj b/test/perf/commando/core_perf_test.clj index 13c0d01..c66c46e 100644 --- a/test/perf/commando/core_perf_test.clj +++ b/test/perf/commando/core_perf_test.clj @@ -1,40 +1,10 @@ (ns commando.core-perf-test (:require - [commando.impl.utils] + [cljfreechart.core :as cljfreechart] [commando.commands.builtin] [commando.commands.query-dsl] [commando.core] - [clojure.string :as str] - [cljfreechart.core :as cljfreechart])) - -;; ===================================== -;; PRINT UTILS -;; ===================================== - -(defn print-stats - "Prints a formatted summary of the execution stats from a status-map." - ([status-map] - (print-stats status-map nil)) - ([status-map title] - (when-let [stats (:stats status-map)] - (let [max-key-len (apply max 0 (map (comp count name first) stats))] - (println (str "\nExecution Stats" (when title (str "(" title ")")) ":")) - (doseq [[index [stat-key _ formatted]] (map-indexed vector stats)] - (let [key-str (name stat-key) - padding (str/join "" (repeat (- max-key-len (count key-str)) " "))] - (println (str - " " (if (= "execute" key-str) "=" (str (inc index)) ) - " " key-str " " padding formatted)))))))) - -(comment - (print-stats - (commando.core/execute - [commando.commands.builtin/command-fn-spec - commando.commands.builtin/command-from-spec - commando.commands.builtin/command-apply-spec] - {"1" 1 - "2" {:commando/from ["1"]} - "3" {:commando/from ["2"]}}))) + [commando.impl.utils :as commando-utils])) ;; ======================================= ;; AVERAGE EXECUTION OF REAL WORLD EXAMPLE @@ -63,7 +33,7 @@ ~@body)) avg-stats# (calculate-average-stats results#)] (print "Repeating instruction " ~n " times") - (print-stats avg-stats#))) + (commando-utils/print-stats avg-stats#))) (defn real-word-calculation-average-of-50 [] (println "\n=====================Benchmark=====================") @@ -217,45 +187,6 @@ ;; FLAME FOR RECURSIVE INVOCATION ;; ============================== -(defn ^:private flame-print-stats [stats indent] - (let [max-key-len (apply max 0 (map (comp count name first) stats))] - (doseq [[stat-key _ formatted] stats] - (let [key-str (name stat-key) - padding (clojure.string/join "" (repeat (- max-key-len (count key-str)) " "))] - (println (str indent - "" key-str " " padding formatted)))))) - -(defn ^:private flame-print [data & [indent]] - (let [indent (or indent "")] - (doseq [[k v] data] - (println (str indent "———" k)) - (when (:stats v) - (flame-print-stats (:stats v) (str indent " |"))) - (doseq [[child-k child-v] v - :when (map? child-v)] - (when (not= child-k :stats) - (flame-print {child-k child-v} (str indent " :"))))))) - -(defn ^:private flamegraph [data] - (println "Printing Flamegraph for executes:") - (flame-print data)) - -(defn ^:private execute-with-flame [registry instruction] - (let [stats-state (atom {}) - result - (binding [commando.impl.utils/*execute-config* - {; :debug-result true - :hook-execute-end - (fn [e] - (swap! stats-state - (fn [s] - (update-in s (:stack commando.impl.utils/*execute-internals*) - #(merge % {:stats (:stats e)})))))}] - (commando.core/execute - registry instruction))] - (flamegraph @stats-state) - result)) - (defmethod commando.commands.query-dsl/command-resolve :query-B [_ {:keys [x QueryExpression]}] (let [x (or x 10)] (-> {:map {:a @@ -312,40 +243,40 @@ (println "\n===================Benchmark=====================") (println "Run commando/execute in depth with using queryDSL") (println "=================================================") - (execute-with-flame - [commando.commands.query-dsl/command-resolve-spec - commando.commands.builtin/command-from-spec - commando.commands.builtin/command-fn-spec] - {:commando/resolve :query-A - :x 1 - :QueryExpression - [{:map - [{:a - [:b]}]} - {:instruction-A [:a]} - {:query-A - [{:map - [{:a - [:b]}]} - {:query-A - [{:map - [{:a - [:b]}]} - {:query-A - [{:map - [{:a - [:b]}]}]}]}]} - {:query-B - [{:map - [{:a - [:b]}]} - {:query-A - [{:map - [{:a - [:b]}]} - {:query-A - [{:instruction-A [:a]}]}]}]}]}) -) + (commando-utils/print-deep-stats + #(commando.core/execute + [commando.commands.query-dsl/command-resolve-spec + commando.commands.builtin/command-from-spec + commando.commands.builtin/command-fn-spec] + {:commando/resolve :query-A + :x 1 + :QueryExpression + [{:map + [{:a + [:b]}]} + {:instruction-A [:a]} + {:query-A + [{:map + [{:a + [:b]}]} + {:query-A + [{:map + [{:a + [:b]}]} + {:query-A + [{:map + [{:a + [:b]}]}]}]}]} + {:query-B + [{:map + [{:a + [:b]}]} + {:query-A + [{:map + [{:a + [:b]}]} + {:query-A + [{:instruction-A [:a]}]}]}]}]}))) ;; ===================================== ;; BUILDING DEPENDECY COMPLEX TEST CASES @@ -398,7 +329,7 @@ (let [result (commando.core/execute [commando.commands.builtin/command-from-spec] instruction-builder) - stats-grouped (reduce (fn [acc [k v label]] + stats-grouped (reduce (fn [acc [k v _label]] (assoc acc k v)) {} (:stats result))] @@ -441,7 +372,7 @@ (assoc "dependecy-token" dependecy-token)))) instruction-stats-result)] (doseq [{:keys [dependecy-token stats]} instruction-stats-result] - (print-stats {:stats stats} (str "Dependency Counts: " dependecy-token))) + (commando-utils/print-stats {:stats stats} (str "Dependency Counts: " dependecy-token))) (cljfreechart/save-chart-as-file (-> chart-data (cljfreechart/make-category-dataset {:group-key "dependecy-token"}) @@ -470,7 +401,7 @@ (assoc "dependecy-token" dependecy-token)))) instruction-stats-result)] (doseq [{:keys [dependecy-token stats]} instruction-stats-result] - (print-stats {:stats stats} (str "Dependency Counts: " dependecy-token))) + (commando-utils/print-stats {:stats stats} (str "Dependency Counts: " dependecy-token))) (cljfreechart/save-chart-as-file (-> chart-data (cljfreechart/make-category-dataset {:group-key "dependecy-token"}) @@ -499,7 +430,7 @@ (assoc "dependecy-token" dependecy-token)))) instruction-stats-result)] (doseq [{:keys [dependecy-token stats]} instruction-stats-result] - (print-stats {:stats stats} (str "Dependency Counts: " dependecy-token))) + (commando-utils/print-stats {:stats stats} (str "Dependency Counts: " dependecy-token))) (cljfreechart/save-chart-as-file (-> chart-data (cljfreechart/make-category-dataset {:group-key "dependecy-token"}) diff --git a/test/unit/commando/commands/builtin_test.cljc b/test/unit/commando/commands/builtin_test.cljc index 814ba84..28d5bce 100644 --- a/test/unit/commando/commands/builtin_test.cljc +++ b/test/unit/commando/commands/builtin_test.cljc @@ -105,11 +105,14 @@ ;; FROM-SPEC ;; =========================== + + + (deftest command-from-spec - ;; ------------------- + ;; ------------------- (testing "Successfull test cases" (is (= {:a 1, :vec 1, :vec-map 1, :result-of-another 1} - (get-in + (get-in (commando/execute [command-builtin/command-fn-spec command-builtin/command-from-spec] {"values" {:a 1 @@ -141,9 +144,27 @@ :e {:result [5 {:commando/from ["./" "../" 0]}]}}))) "Uncorrect extracting :commando/from by relative path") + (is (= {"a" {"value" 1, "result" 1}, + "b" {"value" 2, "result" 2}, + "c" {"value" 3, "result" [3]}, + "d" {"result" [4 4]}, + "e" {"result" [5 5]}} + (:instruction + (commando/execute [command-builtin/command-from-spec] + {"a" {"value" 1 + "result" {"commando-from" ["../" "value"]}} + "b" {"value" 2 + "result" {"commando-from" ["../" "value"]}} + "c" {"value" 3 + "result" [{"commando-from" ["../" "../" "value"]}]} + "d" {"result" [4 + {"commando-from" ["../" 0]}]} + "e" {"result" [5 + {"commando-from" ["./" "../" 0]}]}}))) + "Uncorrect extracting \"commando-from\" by relative path") #?(:clj (is (= {:=-keyword 1, :=-fn 2, :=-symbol 2, :=-var 2} - (get-in + (get-in (commando/execute [command-builtin/command-fn-spec command-builtin/command-from-spec] {"value" {:kwd 1} @@ -155,7 +176,7 @@ ) "Uncorrect commando/from ':=' applicator. CLJ Supports: fn/keyword/var/symbol") :cljs (is (= {:=-keyword 1, :=-fn 2} - (get-in + (get-in (commando/execute [command-builtin/command-fn-spec command-builtin/command-from-spec] {"value" {:kwd 1} @@ -175,6 +196,32 @@ :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"]}}) + "Waiting on error, bacause \"commando-from\" seding to unexising path") + (is + (helpers/status-map-contains-error? + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] + (commando/execute [command-builtin/command-from-spec] + {"value" 1 + "result" {:commando/from ["value"] + "commando-from" ["value"]}})) + (fn [error] + (= + (-> error :error :data) + {:command-type :commando/from, + :reason "The keyword :commando/from and the string \"commando-from\" cannot be used simultaneously in one command.", + :path ["result"], + :value {:commando/from ["value"], "commando-from" ["value"]}}))) + "Using string and keyword form shouldn't be allowed") (is (helpers/status-map-contains-error? (binding [commando-utils/*execute-config* {:debug-result false @@ -218,6 +265,11 @@ (malli/assert [:+ number?] vector2) (reduce + (map * vector1 vector2))) +(defmethod command-builtin/command-mutation "dot-product" [_macro-type {:strs [vector1 vector2]}] + (malli/assert [:+ number?] vector1) + (malli/assert [:+ number?] vector2) + (reduce + (map * vector1 vector2))) + (deftest command-mutation-spec (testing "Successfull test cases" (is (= @@ -233,8 +285,44 @@ :result-with-deps {:commando/mutation :dot-product :vector1 {:commando/from [:vector1]} :vector2 {:commando/from [:vector2]}}}))) - "Uncorrectly processed :commando/mutation in dot-product example")) + "Uncorrectly processed :commando/mutation in dot-product example") + (is (= + {"vector1" [1 2 3], "vector2" [3 2 1], "result-simple" 10, "result-with-deps" 10} + (:instruction + (commando/execute [command-builtin/command-mutation-spec + command-builtin/command-from-spec] + {"vector1" [1 2 3] + "vector2" [3 2 1] + "result-simple" {"commando-mutation" "dot-product" + "vector1" [1 2 3] + "vector2" [3 2 1]} + "result-with-deps" {"commando-mutation" "dot-product" + "vector1" {"commando-from" ["vector1"]} + "vector2" {"commando-from" ["vector2"]}}}))) + "Uncorrectly processed \"commando/mutation\" in dot-product example")) (testing "Failure test cases" + (is + (helpers/status-map-contains-error? + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] + (commando/execute [command-builtin/command-mutation-spec] + {:commando/mutation :dot-product + "commando-mutation" "dot-product" + "vector1" [1 2 3] + "vector2" [3 2 1]})) + (fn [error] + (= + (-> error :error :data) + {:command-type :commando/mutation, + :reason "The keyword :commando/mutation and the string \"commando-mutation\" cannot be used simultaneously in one command.", + :path [], + :value + {:commando/mutation :dot-product, + "commando-mutation" "dot-product", + "vector1" [1 2 3], + "vector2" [3 2 1]}}))) + "Using string and keyword form shouldn't be allowed") (is (helpers/status-map-contains-error? (binding [commando-utils/*execute-config* @@ -246,7 +334,7 @@ (= (-> error :error :data (dissoc :value)) {:command-type :commando/mutation - :reason {:commando/mutation ["should be a keyword"]} + :reason {:commando/mutation ["should be a keyword" "should be a string"]} :path []}))) "Waiting on error, bacause commando/mutation has wrong type for :commando/mutation") (is @@ -277,7 +365,7 @@ ;; MACRO-SPEC ;; =========================== -(defmethod command-builtin/command-macro :string-vectors-dot-product [_macro-type {:keys [vector1-str vector2-str]}] +(defn string-vector-dot-product [vector1-str vector2-str] {:= :dot-product :commando/apply {:vector1-str vector1-str @@ -301,6 +389,11 @@ :args [{:commando/from [:commando/apply :vector1]} {:commando/from [:commando/apply :vector2]}]}}}) +(defmethod command-builtin/command-macro :string-vectors-dot-product [_macro-type {:keys [vector1-str vector2-str]}] + (string-vector-dot-product vector1-str vector2-str)) +(defmethod command-builtin/command-macro "string-vectors-dot-product" [_macro-type {:strs [vector1-str vector2-str]}] + (string-vector-dot-product vector1-str vector2-str)) + (deftest command-macro-spec (testing "Successfull test cases" (is @@ -319,8 +412,53 @@ :vector-dot-2 {:commando/macro :string-vectors-dot-product :vector1-str ["10" "20" "30"] - :vector2-str ["4" "5" "6"]}}))))) + :vector2-str ["4" "5" "6"]}}))) + "Uncorrectly processed :commando/macro for :string-vectors-dot-product example") + (is + (= + {"vector-dot-1" 32, "vector-dot-2" 320} + (:instruction + (commando/execute + [command-builtin/command-macro-spec + command-builtin/command-fn-spec + command-builtin/command-from-spec + command-builtin/command-apply-spec] + {"vector-dot-1" + {"commando-macro" "string-vectors-dot-product" + "vector1-str" ["1" "2" "3"] + "vector2-str" ["4" "5" "6"]} + "vector-dot-2" + {"commando-macro" "string-vectors-dot-product" + "vector1-str" ["10" "20" "30"] + "vector2-str" ["4" "5" "6"]}}))) + "Uncorrectly processed \"commando-macro\" for \"string-vectors-dot-product\" example")) (testing "Failure test cases" + (is + (helpers/status-map-contains-error? + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] + (commando/execute + [command-builtin/command-macro-spec + command-builtin/command-fn-spec + command-builtin/command-from-spec + command-builtin/command-apply-spec] + {:commando/macro :string-vectors-dot-product + "commando-macro" "string-vectors-dot-product" + "vector1-str" ["1" "2" "3"] + "vector2-str" ["4" "5" "6"]})) + (fn [error] + (= + (-> error :error :data) + {:command-type :commando/macro, + :reason "The keyword :commando/macro and the string \"commando-macro\" cannot be used simultaneously in one command.", + :path [], + :value + {:commando/macro :string-vectors-dot-product + "commando-macro" "string-vectors-dot-product" + "vector1-str" ["1" "2" "3"] + "vector2-str" ["4" "5" "6"]}}))) + "Using string and keyword form shouldn't be allowed") (is (helpers/status-map-contains-error? (binding [commando-utils/*execute-config* @@ -332,7 +470,6 @@ (= (-> error :error :data (dissoc :value)) {:command-type :commando/macro, - :reason {:commando/macro ["should be a keyword"]}, + :reason {:commando/macro ["should be a keyword" "should be a string"]}, :path []}))) "Waiting on error, bacause commando/mutation has wrong type for :commando/mutation"))) - diff --git a/test/unit/commando/commands/query_dsl_test.cljc b/test/unit/commando/commands/query_dsl_test.cljc index 8797ea7..eef651e 100644 --- a/test/unit/commando/commands/query_dsl_test.cljc +++ b/test/unit/commando/commands/query_dsl_test.cljc @@ -76,142 +76,142 @@ (deftest black-box-test-query (testing "Testing query on mock-data (permissions and users)" (is - (= - {:id 2, - :name "Peter Griffin", - :email "peter@mail.com", - :password "*********", - :permissions ["add-doc"]} - (:instruction - (commando.core/execute - registry - {:commando/resolve :query-user ;; resolver - :email "peter@mail.com" - :QueryExpression - [:id - :name - :email - :password - :permissions]}))) - "Wrong resolving user and permisssions.") + (= + {:id 2, + :name "Peter Griffin", + :email "peter@mail.com", + :password "*********", + :permissions ["add-doc"]} + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-user ;; resolver + :email "peter@mail.com" + :QueryExpression + [:id + :name + :email + :password + :permissions]}))) + "Wrong resolving user and permisssions.") (is - (= - {:id 2, - :name "Peter Griffin", - :email "peter@mail.com", - :password "*********", - :permissions - [{:permission-name "add-doc", - :options [{:type "pdf"} {:type "rtf"} {:type "odt"}]}]} - (:instruction - (commando.core/execute - registry - {:commando/resolve :query-user - :email "peter@mail.com" - :QueryExpression - [:id - :name - :email - :password - {:permissions - [:permission-name - {:options - [:type]}]}]}))) - "Test user query with nested and joined permission data. Should return only needed value :type from nested map") + (= + {:id 2, + :name "Peter Griffin", + :email "peter@mail.com", + :password "*********", + :permissions + [{:permission-name "add-doc", + :options [{:type "pdf"} {:type "rtf"} {:type "odt"}]}]} + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-user + :email "peter@mail.com" + :QueryExpression + [:id + :name + :email + :password + {:permissions + [:permission-name + {:options + [:type]}]}]}))) + "Test user query with nested and joined permission data. Should return only needed value :type from nested map") (is - (= - {:id 2, - :name "Peter Griffin" - :UNEXISTING-FIELD {:status :failed, - :errors [{:message "Commando. QueryDSL. QueryExpression. Attribute ':UNEXISTING-FIELD' is unreachable."}]}} - (:instruction - (commando.core/execute - registry - {:commando/resolve :query-user - :email "peter@mail.com" - :QueryExpression - [:id - :name - :UNEXISTING-FIELD]}))) - "Test query for a non-existent attribute on a user. The resolver should return an error map for the unreachable field.") + (= + {:id 2, + :name "Peter Griffin" + :UNEXISTING-FIELD {:status :failed, + :errors [{:message "Commando. QueryDSL. QueryExpression. Attribute ':UNEXISTING-FIELD' is unreachable."}]}} + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-user + :email "peter@mail.com" + :QueryExpression + [:id + :name + :UNEXISTING-FIELD]}))) + "Test query for a non-existent attribute on a user. The resolver should return an error map for the unreachable field.") (is - (= - {:id 3, - :name "Lois Griffin", - :email "lois@yahoo.net", - :password "********", - :user-role - {:status :failed, - :errors - [{:message - "Commando. QueryDSL. QueryExpression. Attribute ':user-role' is unreachable."}]}} - (:instruction - (commando.core/execute - registry - {:commando/resolve :query-user - :email "lois@yahoo.net" - :QueryExpression - [:id - :name - :email - :password - {:user-role - [:id - :permission-name]}]}))) - "Test user with a mismatched :user-role key in the DB. The resolver should return error.") + (= + {:id 3, + :name "Lois Griffin", + :email "lois@yahoo.net", + :password "********", + :user-role + {:status :failed, + :errors + [{:message + "Commando. QueryDSL. QueryExpression. Attribute ':user-role' is unreachable."}]}} + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-user + :email "lois@yahoo.net" + :QueryExpression + [:id + :name + :email + :password + {:user-role + [:id + :permission-name]}]}))) + "Test user with a mismatched :user-role key in the DB. The resolver should return error.") (is - (= - {:id 2, - :name "Peter Griffin", - :email "peter@mail.com", - :password "*********", - :permissions [{:id 3, :permission-name "none", :options []}]} - (:instruction - (commando.core/execute - registry - {:commando/resolve :query-user - :email "peter@mail.com" - :QueryExpression - [:id - :name - :email - :password - {[:permissions {:permission-name "none"}] - [:id - :permission-name - :options]}]}))) - "Test parameterized query. This EQL query should override the default join logic and query for a specific permission, even one the user does not have.") + (= + {:id 2, + :name "Peter Griffin", + :email "peter@mail.com", + :password "*********", + :permissions [{:id 3, :permission-name "none", :options []}]} + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-user + :email "peter@mail.com" + :QueryExpression + [:id + :name + :email + :password + {[:permissions {:permission-name "none"}] + [:id + :permission-name + :options]}]}))) + "Test parameterized query. This EQL query should override the default join logic and query for a specific permission, even one the user does not have.") (is - (= - {:id 3 - :permission-name "none" - :options []} - (:instruction - (commando.core/execute - registry - {:commando/resolve :query-permission + (= + {:id 3 :permission-name "none" - :QueryExpression - [:id - :permission-name - :options]}))) - "Test direct query for a permission that has an empty nested list (:options).") + :options []} + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-permission + :permission-name "none" + :QueryExpression + [:id + :permission-name + :options]}))) + "Test direct query for a permission that has an empty nested list (:options).") (is - (nil? - (:instruction - (commando.core/execute - registry - {:commando/resolve :query-user - :email "nonexistent@user.com" - :QueryExpression - [:id :name]}))) - "Test query for a non-existent user. Result should be nil."))) + (nil? + (:instruction + (commando.core/execute + registry + {:commando/resolve :query-user + :email "nonexistent@user.com" + :QueryExpression + [:id :name]}))) + "Test query for a non-existent user. Result should be nil."))) (defmethod command-query-dsl/command-resolve :test-instruction-qe [_ {:keys [x QueryExpression]}] (let [x (or x 10)] @@ -277,264 +277,336 @@ :x 1})} (command-query-dsl/->query-run QueryExpression)))) +(defmethod command-query-dsl/command-resolve "test-instruction-qe" [_ {:strs [x QueryExpression]}] + (let [x (or x 10)] + (-> {"map" {"a" + {"b" {"c" x} + "d" {"c" (inc x) + "f" (inc (inc x))}}} + + "resolve-instruction-qe" + (command-query-dsl/resolve-instruction-qe + "default value for resolve-instruction-qe" + {"commando-resolve" "test-instruction-qe" + "x" 1})} + (command-query-dsl/->query-run QueryExpression)))) + (deftest query-expression-test (testing "Succesfull execution" (is - (= - {:string "Value"} - (:instruction - (commando/execute - registry - {:commando/resolve :test-instruction-qe - :x 1 - :QueryExpression - [:string]}))) - "Returns a single attribute :string for query.") + (= + {:string "Value"} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 1 + :QueryExpression + [:string]}))) + "Returns a single attribute :string for query.") (is - (= - {:string "Value", - :map {:a {:b {:c 1}, :d {:c 2, :f 3}}}, - :coll [{:a {:b {:c 1}, :d {:c 2, :f 3}}} - {:a {:b {:c 1}, :d {:c 0, :f -1}}}], - :resolve-fn "default value for resolve-fn", - :resolve-instruction "default value for resolve-instruction", - :resolve-instruction-qe "default value for resolve-instruction-qe"} - (:instruction - (commando/execute - registry - {:commando/resolve :test-instruction-qe - :x 1 - :QueryExpression - [;; simple data - :string - :map - :coll - ;; data from resolvers - :resolve-fn - :resolve-instruction - :resolve-instruction-qe]}))) - "Returns defaults for queried data.") + (= + {:string "Value", + :map {:a {:b {:c 1}, :d {:c 2, :f 3}}}, + :coll [{:a {:b {:c 1}, :d {:c 2, :f 3}}} + {:a {:b {:c 1}, :d {:c 0, :f -1}}}], + :resolve-fn "default value for resolve-fn", + :resolve-instruction "default value for resolve-instruction", + :resolve-instruction-qe "default value for resolve-instruction-qe"} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 1 + :QueryExpression + [ ;; simple data + :string + :map + :coll + ;; data from resolvers + :resolve-fn + :resolve-instruction + :resolve-instruction-qe]}))) + "Returns defaults for queried data.") (is - (= - {:string "Value", - :map {:a {:b {:c 20}}}, - :coll [{:a {:b {:c 20}}} {:a {:b {:c 20}}}]} - (:instruction - (commando/execute - registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [:string - {:map - [{:a - [:b]}]} - {:coll - [{:a - [:b]}]}]}))) - "Returns nested data by non-resolver data types.") + (= + {:string "Value", + :map {:a {:b {:c 20}}}, + :coll [{:a {:b {:c 20}}} {:a {:b {:c 20}}}]} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [:string + {:map + [{:a + [:b]}]} + {:coll + [{:a + [:b]}]}]}))) + "Returns nested data by non-resolver data types.") (is - (= - {:resolve-fn [{:a {:b {:c 1}}}]} - (:instruction - (commando/execute - registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{:resolve-fn - [{:a - [:b]}]}]}))) - "Return data for resolver. The resolving procedure for 'resolve-fn'") + (= + {:resolve-fn [{:a {:b {:c 1}}}]} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-fn + [{:a + [:b]}]}]}))) + "Return data for resolver. The resolving procedure for 'resolve-fn'") (is - (= - {:resolve-fn - [{:a {:b {:c 1000}}} - {:a {:b {:c 1001}}} - {:a {:b {:c 1002}}} - {:a {:b {:c 1003}}} - {:a {:b {:c 1004}}} - {:a {:b {:c 1005}}} - {:a {:b {:c 1006}}} - {:a {:b {:c 1007}}} - {:a {:b {:c 1008}}} - {:a {:b {:c 1009}}}]} - (:instruction - (commando/execute - registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{[:resolve-fn {:x 1000}] - [{:a - [:b]}]}]}))) - "Return data for resolver and overriding it params. Resolving procedure for 'resolve-fn'") + (= + {:resolve-fn + [{:a {:b {:c 1000}}} + {:a {:b {:c 1001}}} + {:a {:b {:c 1002}}} + {:a {:b {:c 1003}}} + {:a {:b {:c 1004}}} + {:a {:b {:c 1005}}} + {:a {:b {:c 1006}}} + {:a {:b {:c 1007}}} + {:a {:b {:c 1008}}} + {:a {:b {:c 1009}}}]} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{[:resolve-fn {:x 1000}] + [{:a + [:b]}]}]}))) + "Return data for resolver and overriding it params. Resolving procedure for 'resolve-fn'") (is - (= - {:coll-resolve-fn - [{:a {:b 100}} - {:a {:b 100}} - {:a {:b 100}} - {:a {:b 100}} - {:a {:b 100}} - {:a {:b 100}} - {:a {:b 100}} - {:a {:b 100}} - {:a {:b 100}} - {:a {:b 100}}]} - (:instruction - (commando/execute - registry - {:commando/resolve :test-instruction-qe - :QueryExpression - [{[:coll-resolve-fn {:x 100}] - [{:a - [:b]}]}]})))) + (= + {:coll-resolve-fn + [{:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}} + {:a {:b 100}}]} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :QueryExpression + [{[:coll-resolve-fn {:x 100}] + [{:a + [:b]}]}]}))) + "Overriding parameters for sequese of resolvers") (is - (= - {:resolve-instruction - [{:a {:b {:c 0}}} - {:a {:b {:c 1}}}]} - (:instruction - (commando/execute - registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{:resolve-instruction - [{:a - [:b]}]}]})))) + (= + {:resolve-instruction + [{:a {:b {:c 0}}} + {:a {:b {:c 1}}}]} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-instruction + [{:a + [:b]}]}]}))) + "Resolving execution of custom Instruction(have to work like a commando/macro)") (is - (= - {:resolve-instruction - [{:a {:b {:c 0}}} - {:a {:b {:c 1}}} - {:a {:b {:c 2}}} - {:a {:b {:c 3}}} - {:a {:b {:c 4}}}]} - (:instruction - (commando/execute - registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{[:resolve-instruction - {:args [5]}] - [{:a - [:b]}]}]})))) + (= + {:resolve-instruction + [{:a {:b {:c 0}}} + {:a {:b {:c 1}}} + {:a {:b {:c 2}}} + {:a {:b {:c 3}}} + {:a {:b {:c 4}}}]} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{[:resolve-instruction + {:args [5]}] + [{:a + [:b]}]}]}))) + "Resolving execution of custom Instruction(have to work like a commando/macro) with overriding Instruction structure") (is - (= - {:resolve-instruction-qe {:map {:a {:b {:c 1}}}}} - (:instruction - (commando/execute - registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{:resolve-instruction-qe - [{:map [{:a [:b]}]}]}]})))) + (= + {:resolve-instruction-qe {:map {:a {:b {:c 1}}}}} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-instruction-qe + [{:map [{:a [:b]}]}]}]}))) + "Recursion resolving of :test-instruction-qe through the key :resolve-instruction-qe") (is - (= - {:resolve-instruction-qe - {:map {:a {:b {:c 1}}}, - :resolve-instruction-qe - {:map {:a {:b {:c 1000}}}, - :resolve-instruction-qe - {:map {:a {:b {:c 10000000}}}}}}} - (:instruction - (commando/execute - registry - {:commando/resolve :test-instruction-qe - :x 1 - :QueryExpression - [{:resolve-instruction-qe - [{:map [{:a - [:b]}]} - {[:resolve-instruction-qe {:x 1000}] - [{:map [{:a - [:b]}]} - {[:resolve-instruction-qe {:x 10000000}] - [{:map [{:a - [:b]}]}]}]}]}]}))))) + (= + {:resolve-instruction-qe + {:map {:a {:b {:c 1}}}, + :resolve-instruction-qe + {:map {:a {:b {:c 1000}}}, + :resolve-instruction-qe + {:map {:a {:b {:c 10000000}}}}}}} + (:instruction + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 1 + :QueryExpression + [{:resolve-instruction-qe + [{:map [{:a + [:b]}]} + {[:resolve-instruction-qe {:x 1000}] + [{:map [{:a + [:b]}]} + {[:resolve-instruction-qe {:x 10000000}] + [{:map [{:a + [:b]}]}]}]}]}]}))) + "Recursive resolving and overriding of :test-instruction-qe resolver through the key :resolve-instruction-qe") - (testing "Failing exception" (is - (= - {:EEE - {:status :failed, - :errors - [{:message - "Commando. QueryDSL. QueryExpression. Attribute ':EEE' is unreachable."}]}, - :resolve-fn - [{:a - {:b {:c 1}, - :EEE - {:status :failed, - :errors - [{:message - "Commando. QueryDSL. QueryExpression. Attribute ':EEE' is unreachable."}]}}}]} - (:instruction - (commando/execute - registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [:EEE - {:resolve-fn - [{:a - [:b - :EEE]}]}]})))) + (= + {"resolve-instruction-qe" + {"map" {"a" {"b" {"c" 1}}}, + "resolve-instruction-qe" + {"map" {"a" {"b" {"c" 1000}}}, + "resolve-instruction-qe" + {"map" {"a" {"b" {"c" 10000000}}}}}}} + (:instruction + (commando/execute + registry + {"commando-resolve" "test-instruction-qe" + "x" 1 + "QueryExpression" + [{"resolve-instruction-qe" + [{"map" [{"a" + ["b"]}]} + {["resolve-instruction-qe" {"x" 1000}] + [{"map" [{"a" + ["b"]}]} + {["resolve-instruction-qe" {"x" 10000000}] + [{"map" [{"a" + ["b"]}]}]}]}]}]}))) + "Recursive resolving and overriding of \"test-instruction-qe\" resolver through the key \"resolve-instruction-qe\". Case with using string keys either keywords")) + + (testing "Failing exception" (is - (helpers/status-map-contains-error? - (get-in + (helpers/status-map-contains-error? (binding [commando-utils/*execute-config* {:debug-result false :error-data-string false}] - (commando/execute - registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{:resolve-fn-error - [:a]}]})) - [:instruction :resolve-fn-error]) - (fn [error] - (= - {:type "exception-info", - :message "Exception" - :cause nil, - :data {:error "no reason"}} - (-> error :error helpers/remove-stacktrace (dissoc :class)))))) + (commando/execute + registry + {:commando/resolve :test-instruction-qe + "commando-resolve" :test-instruction-qe + :x 1 + :QueryExpression + [:string]})) + (fn [error] + (= + (-> error :error :data) + {:command-type :commando/resolve, + :reason "The keyword :commando/resolve and the string \"commando-resolve\" cannot be used simultaneously in one command.", + :path [], + :value + {:commando/resolve :test-instruction-qe, + "commando-resolve" :test-instruction-qe, + :x 1, + :QueryExpression [:string]}}))) + "Using string and keyword form shouldn't be allowed") (is - (helpers/status-map-contains-error? - (get-in - (binding [commando-utils/*execute-config* - {:debug-result false - :error-data-string false}] + (= + {:EEE + {:status :failed, + :errors + [{:message + "Commando. QueryDSL. QueryExpression. Attribute ':EEE' is unreachable."}]}, + :resolve-fn + [{:a + {:b {:c 1}, + :EEE + {:status :failed, + :errors + [{:message + "Commando. QueryDSL. QueryExpression. Attribute ':EEE' is unreachable."}]}}}]} + (:instruction (commando/execute - registry - {:commando/resolve :test-instruction-qe - :x 20 - :QueryExpression - [{:resolve-instruction-with-error - [{:a [:b]}]}]})) - [:instruction :resolve-instruction-with-error]) - (fn [error] - (= - {:type "exception-info", - :message "Exception" - :cause nil, - :data {:error "no reason"}} - (-> error :error helpers/remove-stacktrace (dissoc :class)))))))) + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [:EEE + {:resolve-fn + [{:a + [:b + :EEE]}]}]}))) + "Resolving is sucessfull by itself, but the Unexisting keys return errors.") + (is + (helpers/status-map-contains-error? + (get-in + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-fn-error + [:a]}]})) + [:instruction :resolve-fn-error]) + (fn [error] + (= + {:type "exception-info", + :message "Exception" + :cause nil, + :data {:error "no reason"}} + (-> error :error helpers/remove-stacktrace (dissoc :class))))) + "throwed Exception inside of `:test-instruction-qe` catched as a normal status-map exceptionn") + + (is + (helpers/status-map-contains-error? + (get-in + (binding [commando-utils/*execute-config* + {:debug-result false + :error-data-string false}] + (commando/execute + registry + {:commando/resolve :test-instruction-qe + :x 20 + :QueryExpression + [{:resolve-instruction-with-error + [{:a [:b]}]}]})) + [:instruction :resolve-instruction-with-error]) + (fn [error] + (= + {:type "exception-info", + :message "Exception" + :cause nil, + :data {:error "no reason"}} + (-> error :error helpers/remove-stacktrace (dissoc :class))))) + "throwed Exception inside of internal resolver by key `:resolve-instruction-with-error` resolver catched as a normal status-map exception"))) diff --git a/test/unit/commando/core_test.cljc b/test/unit/commando/core_test.cljc index 1424010..dbc7d79 100644 --- a/test/unit/commando/core_test.cljc +++ b/test/unit/commando/core_test.cljc @@ -883,7 +883,7 @@ :validate-params-fn (fn [m] (malli/validate [:map [:ARG [:+ :any]]] m)) :apply (fn [instruction _command-path-obj m] (get-in instruction (:ARG m))) :dependencies {:mode :point - :point-key :ARG}}) + :point-key [:ARG]}}) (def custom-registry [custom-op-cmd custom-arg-cmd])