Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic rulesets #12

Open
kidpollo opened this issue May 21, 2021 · 10 comments
Open

Dynamic rulesets #12

kidpollo opened this issue May 21, 2021 · 10 comments

Comments

@kidpollo
Copy link

kidpollo commented May 21, 2021

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}]
@kidpollo kidpollo changed the title Dynamic ruleset creation Dynamic rulesets May 21, 2021
@rwstauner
Copy link

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
Copy link
Author

@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
Copy link

i get

[{:id "1", :x 3, :y 1}]
[{:id "1", :x 3, :y 1}]

@rwstauner
Copy link

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
Copy link
Owner

oakes commented Jun 1, 2021

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
Copy link
Owner

oakes commented Jun 1, 2021

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
Copy link
Author

kidpollo commented Jun 1, 2021 via email

@oakes
Copy link
Owner

oakes commented Jun 1, 2021

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
Copy link
Author

kidpollo commented Jun 1, 2021 via email

@Ramblurr
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants