From c92ae379ed1270c81f0526c22a6c06c2003de896 Mon Sep 17 00:00:00 2001 From: Jose Gomez Date: Wed, 6 Jan 2021 10:21:16 -0600 Subject: [PATCH] Add support to specify query binding arguments as symbols instead of only keywords so that defquery syntax looks closer to function definition syntax. --- CHANGELOG.md | 3 ++ CONTRIBUTORS.md | 3 +- src/main/clojure/clara/rules.cljc | 9 +++-- src/main/clojure/clara/rules/dsl.clj | 3 +- src/main/clojure/clara/rules/platform.cljc | 10 ++++++ src/test/clojure/clara/test_rules.clj | 39 +++++++++++++++++----- src/test/common/clara/test_common.cljc | 33 ++++++++++++++++-- 7 files changed, 85 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d2317a4..7152e418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ This is a history of changes to clara-rules. +# 0.22.0 +* Add support to specify query binding arguments as symbols instead of only keywords so that defquery syntax looks closer to function definition syntax. + # 0.21.0 * Add names to anonymous functions generated by rule compilation; these names will be in the class names of the generated objects. [Issue 261](https://github.com/cerner/clara-rules/issues/261) and [issue 291](https://github.com/cerner/clara-rules/issues/291) * Add types information to alpha nodes. [Issue 237](https://github.com/cerner/clara-rules/issues/237) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index e0ddb1e0..e27fd28a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -20,4 +20,5 @@ Community [@dgoeke]: https://github.com/dgoeke [@sparkofreason]: https://github.com/sparkofreason [@bfontaine]: https://github.com/bfontaine -[@sunilgunisetty]: https://github.com/sunilgunisetty \ No newline at end of file +[@sunilgunisetty]: https://github.com/sunilgunisetty +[@k13gomez]: https://github.com/k13gomez diff --git a/src/main/clojure/clara/rules.cljc b/src/main/clojure/clara/rules.cljc index a1ad10ea..ea0e404a 100644 --- a/src/main/clojure/clara/rules.cljc +++ b/src/main/clojure/clara/rules.cljc @@ -48,13 +48,16 @@ "Runs the given query with the optional given parameters against the session. The optional parameters should be in map form. For example, a query call might be: - (query session get-by-last-name :last-name \"Jones\") + (query session get-by-last-name :?last-name \"Jones\") The query itself may be either the var created by a defquery statement, or the actual name of the query. " [session query & params] - (eng/query session query (apply hash-map params))) + (let [params-map (->> (for [[param value] (apply hash-map params)] + [(platform/query-param param) value]) + (into {}))] + (eng/query session query params-map))) (defn insert! "To be executed within a rule's right-hand side, this inserts a new fact or facts into working memory. @@ -425,4 +428,4 @@ See the [query authoring documentation](http://www.clara-rules.org/docs/queries/ (map first) ; Take the symbols for the rule/query vars )] (doseq [psym production-syms] - (ns-unmap *ns* psym)))))) \ No newline at end of file + (ns-unmap *ns* psym)))))) diff --git a/src/main/clojure/clara/rules/dsl.clj b/src/main/clojure/clara/rules/dsl.clj index c46b8876..0f1aeec0 100644 --- a/src/main/clojure/clara/rules/dsl.clj +++ b/src/main/clojure/clara/rules/dsl.clj @@ -7,6 +7,7 @@ [clojure.walk :as walk] [clara.rules.engine :as eng] [clara.rules.compiler :as com] + [clara.rules.platform :as platform] [clara.rules.schema :as schema] [schema.core :as sc]) (:refer-clojure :exclude [qualified-keyword?])) @@ -277,7 +278,7 @@ query {:lhs (list 'quote (mapv #(resolve-vars % (destructure-syms %)) conditions)) - :params (set params)} + :params (set (map platform/query-param params))} symbols (set (filter symbol? (com/flatten-expression lhs))) matching-env (into {} diff --git a/src/main/clojure/clara/rules/platform.cljc b/src/main/clojure/clara/rules/platform.cljc index daecf560..9470e282 100644 --- a/src/main/clojure/clara/rules/platform.cljc +++ b/src/main/clojure/clara/rules/platform.cljc @@ -7,6 +7,16 @@ [^String description] (throw #?(:clj (IllegalArgumentException. description) :cljs (js/Error. description)))) +(defn query-param + "Coerces a query param to a parameter keyword such as :?param, if an unsupported type is + supplied then an exception will be thrown" + [p] + (cond + (keyword? p) p + (symbol? p) (keyword p) + :else + (throw-error (str "Query bindings must be specified as a keyword or symbol: " p)))) + ;; This class wraps Clojure objects to ensure Clojure's equality and hash ;; semantics are visible to Java code. This allows these Clojure objects ;; to be safely used in things like Java Sets or Maps. diff --git a/src/test/clojure/clara/test_rules.clj b/src/test/clojure/clara/test_rules.clj index 50886bf8..cf108e2d 100644 --- a/src/test/clojure/clara/test_rules.clj +++ b/src/test/clojure/clara/test_rules.clj @@ -521,8 +521,14 @@ (= ?t temperature) (= ?l location)]) +(defquery hot-query + [?l] + [Temperature (>= temperature 50) + (= ?t temperature) + (= ?l location)]) + (deftest test-defquery - (let [session (-> (mk-session [cold-query]) + (let [session (-> (mk-session [cold-query hot-query]) (insert (->Temperature 15 "MCI")) (insert (->Temperature 20 "MCI")) ; Test multiple items in result. (insert (->Temperature 10 "ORD")) @@ -530,16 +536,33 @@ (insert (->Temperature 80 "BOS")) fire-rules)] - ;; Query by location. - (is (= #{{:?l "BOS" :?t 35}} - (set (query session cold-query :?l "BOS")))) - (is (= #{{:?l "MCI" :?t 15} {:?l "MCI" :?t 20}} - (set (query session cold-query :?l "MCI")))) + (testing "query by location with :?keyword params" + (is (= #{{:?l "BOS" :?t 35}} + (set (query session cold-query :?l "BOS")))) - (is (= #{{:?l "ORD" :?t 10}} - (set (query session cold-query :?l "ORD")))))) + (is (= #{{:?l "BOS" :?t 80}} + (set (query session hot-query :?l "BOS")))) + + (is (= #{{:?l "MCI" :?t 15} {:?l "MCI" :?t 20}} + (set (query session cold-query :?l "MCI")))) + + (is (= #{{:?l "ORD" :?t 10}} + (set (query session cold-query :?l "ORD"))))) + + (testing "query by location with '?symbol params" + (is (= #{{:?l "BOS" :?t 35}} + (set (query session cold-query '?l "BOS")))) + + (is (= #{{:?l "BOS" :?t 80}} + (set (query session hot-query '?l "BOS")))) + + (is (= #{{:?l "MCI" :?t 15} {:?l "MCI" :?t 20}} + (set (query session cold-query '?l "MCI")))) + + (is (= #{{:?l "ORD" :?t 10}} + (set (query session cold-query '?l "ORD"))))))) (deftest test-rules-from-ns ;; Validate that rules behave identically when loaded from vars that contain a single diff --git a/src/test/common/clara/test_common.cljc b/src/test/common/clara/test_common.cljc index 118fc054..4fda77de 100644 --- a/src/test/common/clara/test_common.cljc +++ b/src/test/common/clara/test_common.cljc @@ -1,13 +1,15 @@ (ns clara.test-common "Common tests for Clara in Clojure and ClojureScript." (:require #?(:clj [clojure.test :refer :all] - :cljs [cljs.test :refer-macros [is deftest]]) + :cljs [cljs.test :refer-macros [is deftest testing]]) #?(:clj [clara.rules :refer :all] :cljs [clara.rules :refer [insert insert! fire-rules query] :refer-macros [defrule defsession defquery]]) - [clara.rules.accumulators :as acc])) + [clara.rules.accumulators :as acc] + + [clara.rules.platform :as platform])) (defn- has-fact? [token fact] (some #{fact} (map first (:matches token)))) @@ -96,6 +98,33 @@ [:threshold [{value :value}] (= ?threshold value)] [:not [:temperature [{value :value}] (< value ?threshold)]]) +(defquery temperature-below-value-using-symbol-arg + [?value] + [:temperature [{value :value}] (< value ?value)]) + +(defquery temperature-below-value-using-keyword-arg + [:?value] + [:temperature [{value :value}] (< value ?value)]) + +(deftest test-query-definition-bindings-args + (testing "can define queries using symbol or keyword arguments" + (is (= (dissoc temperature-below-value-using-symbol-arg :name) (dissoc temperature-below-value-using-keyword-arg :name))))) + +(deftest test-query-param-args + (testing "noop query-params using keyword arguments" + (is (= (platform/query-param :?value) :?value))) + (testing "coerce query-params using symbol arguments" + (is (= (platform/query-param '?value) :?value))) + (testing "can not coerce query-params using string arguments" + (try + (platform/query-param "?value") + (is false "Running the rules in this test should cause an exception.") + (catch #?(:clj java.lang.IllegalArgumentException + :cljs js/Error) e + (is (= "Query bindings must be specified as a keyword or symbol: ?value" + #?(:clj (.getMessage e) + :cljs (.-message e)))))))) + (defsession negation-with-filter-session [none-below-threshold] :fact-type-fn :type) (deftest test-negation-with-filter