oakes / odoyle-rules

A rules engine for Clojure(Script)
The Unlicense
530 stars 20 forks source link

Dynamic rulesets #12

Open kidpollo opened 3 years ago

kidpollo commented 3 years ago

I've been evaluating this awesome project great work!

For my usecase I have to dynamically create rulesets. I think I have implementation for this but my macro fu is not the best I dont love I had to use eval. Is this something that you would like to make part of this project? I can submit a pr if so.

(defmacro ->ruleset
  [ruls]
  `(let [rules# ~ruls]
     (reduce
      ~(fn [v {:keys [rule-name fn-name conditions when-body then-body then-finally-body arg]}]
         (conj v (odoyle.rules/->Rule rule-name
                                      (mapv odoyle.rules/map->Condition conditions)
                                      (when (some? when-body) ;; need some? because it could be `false`
                                        (eval `(fn ~fn-name [~arg] ~when-body)))
                                      (when then-body
                                        (eval `(fn ~fn-name [~arg] ~@then-body)))
                                      (when then-finally-body
                                        (eval `(fn ~fn-name [] ~@then-finally-body))))))
      []
      (mapv odoyle.rules/->rule (odoyle.rules/parse ::o/rules rules#)))))

(defmacro ->rule
  [rule-key rule]
  `(let [rule# ~rule
         rule-key# ~rule-key]
     (~(fn [{:keys [rule-name fn-name conditions when-body then-body then-finally-body arg]}]
         (odoyle.rules/->Rule rule-name
                              (mapv odoyle.rules/map->Condition conditions)
                              (when (some? when-body) ;; need some? because it could be `false`
                                (eval `(fn ~fn-name [~arg] ~when-body)))
                              (when then-body
                                (eval `(fn ~fn-name [~arg] ~@then-body)))
                              (when then-finally-body
                                (eval `(fn ~fn-name [] ~@then-finally-body)))))
      (o/->rule [rule-key# (o/parse ::o/rule rule#)]))))

(def generated-rule
  (quote
   [:what
    [id :player/x x]
    [id :player/y y]
    :then
    (o/insert! id :session/player o/*match*)]))

  (->rule :session/player generated-rule)

(def generated-ruleset
  {:session/player
   generated-rule})

  (->ruleset generated-ruleset)

(-> (o/->session)
    (o/add-rule (->rule :session/player generated-rule))

    (o/insert "1" {:player/x 3
                   :player/y 1})

    o/fire-rules
    (o/query-all :session/player))
;; => [{:id "1", :x 3, :y 1}]

(-> (reduce o/add-rule (o/->session)
            (->ruleset generated-ruleset))

    (o/insert "1" {:player/x 3
                   :player/y 1})

    o/fire-rules
    (o/query-all :session/player))
;; => [{:id "1", :x 3, :y 1}]
rwstauner commented 3 years ago

I haven't tried it with more input data than the first example, but it might be doable with less macro:

(defmacro ->fn
  ([fn-name body]
    `(fn ~fn-name [] ~body))
  ([fn-name arg body]
    `(fn ~fn-name [~arg] ~body)))

(defn parsed->rule
  [{:keys [rule-name fn-name conditions when-body then-body then-finally-body arg]}]
  (o/->Rule rule-name
            (mapv o/map->Condition conditions)
            (when (some? when-body) ;; need some? because it could be `false`
              (->fn fn-name arg when-body))
            (when then-body
              (->fn fn-name arg (first then-body)))
            (when then-finally-body
              (->fn fn-name (first then-finally-body)))))

(defn ->rule
  [rule-key rule]
  (parsed->rule (o/->rule [rule-key (o/parse ::o/rule rule)])))

(defn ->ruleset
  "Returns a vector of rules after transforming the given map."
  [rules]
  (->> (map o/->rule
            (o/parse ::o/rules rules))
       (mapv parsed->rule)))
kidpollo commented 3 years ago

@rwstauner did not work with my example data. I like its less macroey but need to debut why it did not work for me

rwstauner commented 3 years ago

i get

[{:id "1", :x 3, :y 1}]
[{:id "1", :x 3, :y 1}]
rwstauner commented 3 years ago

the first code was limiting then bodies to 1 which i didn't realize was incorrect. in revising i'm struggling with nested expansion, but it seems like it can be done with eval and no macros:

(defn parsed->rule
  [{:keys [rule-name fn-name conditions when-body then-body then-finally-body arg]}]
  (o/->Rule rule-name
            (mapv o/map->Condition conditions)
            (when (some? when-body) ;; need some? because it could be `false`
              (eval `(fn ~fn-name [~arg] ~when-body)))
            (when then-body
              (eval `(fn ~fn-name [~arg] ~@then-body)))
            (when then-finally-body
              (eval `(fn ~fn-name [] ~@then-finally-body)))))

(defn ->rule
  [rule-key rule]
  (parsed->rule (o/->rule [rule-key (o/parse ::o/rule rule)])))

(defn ->ruleset
  "Returns a vector of rules after transforming the given map."
  [rules]
  (->> (map o/->rule
            (o/parse ::o/rules rules))
       (mapv parsed->rule)))
oakes commented 3 years ago

Really neat. I think @rwstauner's solution is good, but you don't actually even need eval, if you're willing to give up some syntactic convenience. The macro or eval is really just saving you from having to explicitly make fns with the correct arguments and destructuring the bindings explicitly. Now that i think of it, i really should provide support for this approach in the library itself. I'll look into it when i have time. Here it is without macros or eval.

(require '[odoyle.rules :as o])

(defn parsed->rule 
  [{:keys [rule-name fn-name conditions when-body then-body then-finally-body arg]}]
  (o/->Rule rule-name
            (mapv o/map->Condition conditions)
            when-body
            (first then-body)
            (first then-finally-body)))

(defn ->rule
  [rule-key rule]
  (parsed->rule (o/->rule [rule-key (o/parse ::o/rule rule)])))

(defn ->ruleset
  "Returns a vector of rules after transforming the given map."
  [rules]
  (->> (map o/->rule
            (o/parse ::o/rules rules))
       (mapv parsed->rule)))

(def generated-rule
  [:what
   '[id :player/x x]
   '[id :player/y y]
   :when 
   (fn [{:keys [x y] :as match}]
     (and (pos? x) (pos? y)))
   :then
   (fn [{:keys [id] :as match}]
     (o/insert! id :session/player match))
   :then-finally
   (fn []
     (println "Query from inside: " (o/query-all o/*session* :session/player)))])

(println "Query from outside: "
  (-> (o/->session)
      (o/add-rule (->rule :session/player generated-rule))

      (o/insert 1 {:player/x 3 :player/y 1})

      (o/insert 2 {:player/x 5 :player/y 2})

      (o/insert 3 {:player/x 7 :player/y -1})

      o/fire-rules
      (o/query-all :session/player)))

; => Query from inside:  [{:id 1, :x 3, :y 1} {:id 2, :x 5, :y 2}]
; => Query from outside:  [{:id 1, :x 3, :y 1} {:id 2, :x 5, :y 2}]
oakes commented 3 years ago

I just added this if you want to give it a shot. It is a new arity of odoyle.rules/->rule so the code above is now just this:

(require '[odoyle.rules :as o])

(def generated-rule
  [:what
   '[id :player/x x]
   '[id :player/y y]
   :when 
   (fn [{:keys [x y] :as match}]
     (and (pos? x) (pos? y)))
   :then
   (fn [{:keys [id] :as match}]
     (o/insert! id :session/player match))
   :then-finally
   (fn []
     (println "Query from inside: " (o/query-all o/*session* :session/player)))])

(println "Query from outside: "
  (-> (o/->session)
      (o/add-rule (o/->rule :session/player generated-rule))

      (o/insert 1 {:player/x 3 :player/y 1})

      (o/insert 2 {:player/x 5 :player/y 2})

      (o/insert 3 {:player/x 7 :player/y -1})

      o/fire-rules
      (o/query-all :session/player)))
kidpollo commented 3 years ago

Thanks Zach! I’ll check it out later today! We are gearing up to using this lib :) we are stoked. Thanks again

On Tue, Jun 1, 2021 at 7:48 AM Zach Oakes @.***> wrote:

I just added this if you want to give it a shot. It is a new arity of odoyle.rules/->rule so the code above is now just this:

(require '[odoyle.rules :as o])

(def generated-rule [:what '[id :player/x x] '[id :player/y y] :when (fn [{:keys [x y] :as match}] (and (pos? x) (pos? y))) :then (fn [{:keys [id] :as match}] (o/insert! id :session/player match)) :then-finally (fn [] (println "Query from inside: " (o/query-all o/session :session/player)))])

(println "Query from outside: " (-> (o/->session) (o/add-rule (o/->rule :session/player generated-rule))

  (o/insert 1 {:player/x 3 :player/y 1})

  (o/insert 2 {:player/x 5 :player/y 2})

  (o/insert 3 {:player/x 7 :player/y -1})

  o/fire-rules
  (o/query-all :session/player)))

You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/oakes/odoyle-rules/issues/12#issuecomment-852187713, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAB75WOUJIOGDICDO4GRBLTQTXNPANCNFSM45JXYY6Q .

oakes commented 3 years ago

I ended up cutting a new release because why not :D

I also wrote a new section in the readme about it: Defining rules dynamically

kidpollo commented 3 years ago

Very nice !

On Tue, Jun 1, 2021 at 11:23 AM Zach Oakes @.***> wrote:

I ended up cutting a new release because why not :D

I also wrote a new section in the readme about it: Defining rules dynamically https://github.com/oakes/odoyle-rules#defining-rules-dynamically

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/oakes/odoyle-rules/issues/12#issuecomment-852347428, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAB75QDV3NQJFOR4FANBELTQUQQ3ANCNFSM45JXYY6Q .

Ramblurr commented 3 years ago

I am very excited to see this :) I've wanted to use odoyle rules in conjunction with home assistant to power my smart home.

I had a basic prototype working but the inability to dynamically create rules made it difficult. Now I can dust that off and give it a go again.