From 0186d3745010876c0228245c8d772f149897da2c Mon Sep 17 00:00:00 2001 From: Thomas Cothran Date: Sat, 25 Jun 2022 17:55:17 -0400 Subject: [PATCH 1/4] Start O'Doyle Rules Cookbook. * Initializing values * Loading data --- docs/cookbook.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/cookbook.md diff --git a/docs/cookbook.md b/docs/cookbook.md new file mode 100644 index 0000000..fed08c7 --- /dev/null +++ b/docs/cookbook.md @@ -0,0 +1,87 @@ +# Odoyle's Rules Cookbook + +## Initializing values + +Suppose we have an aggregate: + +```clojure +(o/ruleset + {#_... + ::agregate-things + [:what + [thing-id :thing/name thing-name] + [thing-id :thing/owner person-id] + :then-finally + (->> (o/rules o/*session* ::aggregate-things) + (group-by :thing-id) + (o/insert o/*session* :global :domain/things) + (o/reset!))]}) +``` + +The trouble is that `:thing/owner` may be an attribute of some things, but not all of them. We want to aggregate all the things. + +The solution is to use a rule that initializes a `nil` value for `:thing/owner`: + +```clojure +(o/ruleset + {#_... + ::initialize-thing-owner + [:what [_ :thing/id id {:then not=}] + :then (o/insert! id :thing/owner nil)]}) +``` + +The value may not always be nil. It may also often be 0 or another neutral value. Note that `{:then not=}` prevents the rule from firing more than once. (However, if `[id :thing/id id]` is used, this will not work.) + +## Loading Data + +Instead of gathering the data required for your rules, shaping it into the form you need it, and inserting it with `o/insert`, you can use rules to make the process automatic. + +For example: + +```clojure +(ns app.rules + (:require [odoyle.rules :as o] + [app.thing.db :refer [fetch-thing]])) + ;; fetch-thing queries the db + ;; which returns a namespaced map + +(def rules + (o/ruleset + {::fetch-thing + [:what [_ :thing/id id {:then not=}] + [:then (o/insert! id (fetch-thing id))]]})) +``` + +Now, instead of calling `fetch-thing` and then `o/insert` from the outside, you can use `(o/insert :thing/id )`, and the rule will fetch for you. + +The power of this approach is that O'Doyle rules becomes a query engine. + +Suppose that you have `thing/owner` attribute, which is a set of the things a person owns. Combining two fetchers reveals the power of this approach: + +```clojure +(ns app.rules + (:require [odoyle.rules :as o] + [app.person.db :refer [fetch-person]] + [app.thing.db :refer [fetch-thing]])) + +(def rules + (o/ruleset + {::fetch-thing + [:what [_ :thing/id id {:then not=}] + :then ;; fetch-thing returns a namespaced map + (o/insert! id (fetch-thing id))] ;; `{:thing/name "name", :thing/owner 123, #_...}` + + ::fetch-person + [:what [_ :person/id id {:then not=}] + :then (o/insert! id (fetch-person-id))] ;; fetch-person returns a namespaced map + + ::person-owner-link + [:what [_ :thing/owner person-id {:then not=}] + :then (o/insert! person-id :person/id person-id)]})) +``` + +The `::person-owner-link` links `:thing/owner` to `:person/id`. Now, if you insert a `:thing/id`, not only is the thing fetched, but the related owner. + +By providing a single id, all related domain entities can be fetched, whether from a database or a service. + + From 9b26ca7b269bd41cb9c4aeff69b9535cdaf895d1 Mon Sep 17 00:00:00 2001 From: Thomas Cothran Date: Wed, 29 Jun 2022 19:18:55 -0400 Subject: [PATCH 2/4] Cookbook: section on splitting rulesets --- docs/cookbook.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/cookbook.md b/docs/cookbook.md index fed08c7..062da68 100644 --- a/docs/cookbook.md +++ b/docs/cookbook.md @@ -85,3 +85,22 @@ The `::person-owner-link` links `:thing/owner` to `:person/id`. Now, if you inse By providing a single id, all related domain entities can be fetched, whether from a database or a service. +## Splitting rule sets by namespaces + +If you have a number of different rulesets, you can split them by namespaces, and then merge them into a single ruleset. + +For example: + +```clojure +(ns app.rules + (:require [odoyle.rules :as o] + [app.domain-a.rules :as a] + [app.domain-b.rules :as b])) + +(def base-rules + (o/ruleset + {::some-rule ...})) + +(def ruleset + (reduce into [base-rules a/ruleset b/ruleset])) +``` From c251762970ba08e6e8e4455980c996a321e20aa0 Mon Sep 17 00:00:00 2001 From: Thomas Cothran Date: Sat, 2 Jul 2022 15:04:32 -0400 Subject: [PATCH 3/4] Cookbook: better way to initialize values --- docs/cookbook.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/cookbook.md b/docs/cookbook.md index 062da68..9fa0606 100644 --- a/docs/cookbook.md +++ b/docs/cookbook.md @@ -26,7 +26,8 @@ The solution is to use a rule that initializes a `nil` value for `:thing/owner`: (o/ruleset {#_... ::initialize-thing-owner - [:what [_ :thing/id id {:then not=}] + [:what [_ :thing/id id] + :when (not (o/contains? o/*session* id :thing/owner)) :then (o/insert! id :thing/owner nil)]}) ``` From 8f0c9c9dc80e020be209080d46762623e7fc7cd1 Mon Sep 17 00:00:00 2001 From: Thomas Cothran Date: Sun, 3 Jul 2022 14:30:12 -0400 Subject: [PATCH 4/4] Cookbook: clarify query engine joins --- docs/cookbook.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/cookbook.md b/docs/cookbook.md index 9fa0606..114c97c 100644 --- a/docs/cookbook.md +++ b/docs/cookbook.md @@ -33,7 +33,7 @@ The solution is to use a rule that initializes a `nil` value for `:thing/owner`: The value may not always be nil. It may also often be 0 or another neutral value. Note that `{:then not=}` prevents the rule from firing more than once. (However, if `[id :thing/id id]` is used, this will not work.) -## Loading Data +## Build a Query System Instead of gathering the data required for your rules, shaping it into the form you need it, and inserting it with `o/insert`, you can use rules to make the process automatic. @@ -81,7 +81,9 @@ Suppose that you have `thing/owner` attribute, which is a set of the things a pe :then (o/insert! person-id :person/id person-id)]})) ``` -The `::person-owner-link` links `:thing/owner` to `:person/id`. Now, if you insert a `:thing/id`, not only is the thing fetched, but the related owner. +The `::person-owner-link` links `:thing/owner` to `:person/id`. The call to `fetch-thing` returns a map with `:thing/owner`. This is inserted into the session. + +`::person-owner-link` then fires, inserting a `:person/id`. This in turn triggers `::fetch-person` to query the database for all the `:person` information, and insert it into the session. By providing a single id, all related domain entities can be fetched, whether from a database or a service.