-
Notifications
You must be signed in to change notification settings - Fork 22
O'Doyle Rules Cookbook. #18
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| # 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] | ||
| :when (not (o/contains? o/*session* id :thing/owner)) | ||
| :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.) | ||
|
|
||
| ## 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. | ||
|
|
||
| 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 <some-id> :thing/id <some-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, #_...}` | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this example, is the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this example, The benefit of this in my use cases is that I can put a single id for an object, and the rules will fetch all the related objects. This saves me from having to gather everything up before hand, which can often span quite a few related entities and domains.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that makes sense. Is there any benefit to doing the db query from inside a rule, vs querying and inserting from the outside? I tend to avoid interacting with external things (databases, rendering, etc) from inside rules, though this is probably OK. One big gotcha that I mention on the readme is using
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Putting the queries in the rules handles the joins for you automatically, somewhat like what Pathom does. If I insert one id for an entity, the rules engine will make all the queries, find all the relations, load all the related entities, and execute the rules against them all. I've found this to be extremely useful. To give a concrete example: for my main use case, we have a library of rules used by different services in a microservice environment. If I'm in the service that owns an entity, I want to pass in a db connection so I can use transactions. But an entity in a service is almost always related to entities in another service. Those entities I want to fetch over a network call to another service (or from Redis, depending on what it is). Again, I might be using the library from the front end, in which case I want to make a network call for entities, but need to make those calls differently (authorization rules). I can pass a triple to the rules engine indicating which service the rules engine is being used from. Then the rules govern how an entity is loaded based on the fact indicating where it is being called from. These are written once for all services, but handle all the different use cases. So the rules engine turns into a pretty powerful query system + rules engine. For my use case, the query part is as much part of the value proposition as the "normal usage" of the rules. (One potential drawback is that all these network calls need to be blocking. We typically block on network calls anyway, so this hasn't been an issue.)
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the case of |
||
|
|
||
| ::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`. 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. | ||
|
|
||
|
|
||
| ## 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])) | ||
| ``` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using
{:then not=}here you could useo/contains?to check if the fact already exists:The problem with using
{:then not=}here is that you may actually need to update:thing/idfor some reason, and if you do, it'll set the:thing/ownerto nil even if it already had a value.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting! I've run into that limitation with
{:then not=}and worked around it sometimes when I've needed to. Usingo/contains?is perfect for this.Updated to follow your suggestion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@oakes - one mistake I started making here pretty quickly was to do this:
This case can happen, which is strange:
In this case, there is a match, which is counter-intuitive to me.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe
*session*isn't bound to anything in :when blocks. I need to fix that. I'm about to board a plane but I'll look into that soon.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you can, try the latest commit. It should fix this problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you get a chance to try the latest commit and see if it fixes this?