Skip to content

Releases: oakes/odoyle-rules

1.3.1

15 Sep 19:20
Compare
Choose a tag to compare

This release fixes a bug in remove-rule if it is called while there are still rules queued up to be fired. The matches for the removed rule were not being removed, and when they were fired they sometimes threw an NPE. Thanks to @nivekuil for the bug report.

1.3.0

31 Aug 21:19
Compare
Choose a tag to compare

This release adds remove-rule, which (unsurprisingly) does the opposite of add-rule. This can be useful especially during development, when you need to dynamically remove and add new versions of a rule.

I skipped 1.2.0 because I botched the clojars release and cannot un-botch it, in case you were wondering :D

1.1.0

25 May 00:57
Compare
Choose a tag to compare

This release enables attributes to be matched with binding symbols in the :what block. Up until now, only the id and value could have a binding symbol.

This can be a helpful tool for debugging or tracking. You can now make a quick rule that prints out every time a fact with a given id and/or value is inserted, regardless of the attribute.

You can even make a tuple like [id attr value], which matches on every fact! Why is this useful? It usually isn't...but one cool use case is when you want to retract every fact with a given id. Now, you can make a rule like this:

[:what
 [id ::remove? true]
 [id attr      value]
 :then
 (o/retract! id attr)]

Then, when you insert [123 ::remove? true], the rule will trigger on every fact whose id is 123 (thanks to the join on id) and they will all be retracted.

1.0.0

13 Oct 11:29
Compare
Choose a tag to compare

billy-madison-adam-sandler

After over two years, Adam Sandler's favorite Clojure rules engine has reached 1.0. I decided to bump the version to this important milestone because the previous version was 0.12.0 and 13 is an unlucky number. I call it superstitious versioning, or SupVer for short.

Here's why O'Doyle rules even more:

Improved debugging with wrap-rule

There is a new function to help with debugging called wrap-rule. You can use it on some or all of your rules to intercept the various places where things execute in the engine. In the simplest case, this is just a convenient way to add logging, but like any simple tool you are free to use it in any creative way you wish. See the new debugging section for more.

The *session* and *match* dynamic vars are no longer necessary

This was a longstanding design mistake I always wanted to correct. The *session* and *match* vars never really needed to exist, and now you can just reference session and match in your rules directly (unless you're using those as binding symbols in your :what block!). This may be a modest performance improvement, but mainly it is just better style. If you can pass values as explicit args instead of dynamic vars, you should do so.

The dynamic vars remain for backwards compatibility. There is only one thing that required a breaking change: Defining rules dynamically with ->rule. Previously, the syntax looked like this:

;; notice it's a vector, and the functions do not
;; receive an explicit `session`
(o/->rule
  ::character
  [:what
   '[id ::x x]
   '[id ::y y]
   :when
   (fn [{:keys [x y] :as match}]
     (and (pos? x) (pos? y)))
   :then
   (fn [match]
     (println "This will fire twice"))
   :then-finally
   (fn []
     (println "This will fire once"))])

Now it looks like this:

;; notice it's a map, and the functions *do*
;; receive an explicit `session`
(o/->rule
  ::character
  {:what
   '[[id ::x x]
     [id ::y y]]
   :when
   (fn [session {:keys [x y] :as match}]
     (and (pos? x) (pos? y)))
   :then
   (fn [session match]
     (println "This will fire twice"))
   :then-finally
   (fn [session]
     (println "This will fire once"))})

Changing it from a vector to a map serves a few purposes. Firstly, it just makes more sense as a map. Secondly, it allows me to throw a nice error if you try to make a rule with the old syntax, instead of just throwing a mysterious arity exception when the functions are eventually run.

Error to prevent a {:then not=} footgun

The ability to pass arbitrary functions to a tuple's :then option is really powerful. That said, there was a footgun in the library that was easy to run into that led to confusing behavior. Initially, I just added a warning in the README, but as of this version, it correctly detects the problem and throws a helpful error message guiding you on how to fix it. Fail fast, just like the O'Doyle family:

odoyle-rules-billy-madison-rules

0.12.0

06 Aug 09:32
Compare
Choose a tag to compare

This release includes a small change so the o/*session* dynamic var is bound correctly in :when blocks. This is particularly useful if you want to use o/contains? function in your :when block, since it needs the session as its first arg.

0.11.0

19 Jun 19:19
Compare
Choose a tag to compare

This release adds a new function, odoyle.rules/contains?, to check if a fact is in the session. This is useful when you want to insert a fact if and only if it isn't already in the session. You can even use it within a rule, like this:

[:what
 [id ::score score]
 :when
 (>= score 5)
 :then
 (when-not (o/contains? o/*session* ::event ::game-finished)
   (o/insert! ::event ::game-finished {:winner id}))]

I also fixed a small bug with the new ->rule function that allows you to create rules dynamically. Using {:then not=} in a dynamic rule no longer throws a spec error.

0.10.0

01 Jun 18:14
Compare
Choose a tag to compare

This release adds the ability to define rules dynamically using ->rule. This allows you to define rules without relying on the ruleset macro, so their :what blocks can be based on information only available at runtime. See the Defining rules dynamically section for more.

0.9.0

23 Apr 05:07
Compare
Choose a tag to compare

This release throws an error if spec is instrumented and an attribute is inserted that does not have a corresponding spec defined. This is a very useful error even if you are lazy and just define any? specs, because it will protect you from typos in your attribute names -- including the very common mistake of using the wrong namespace in your qualified keywords.

This is particularly important for a rules engine, because if you fat-finger an attribute name, you normally won't get an error -- your rules just won't fire. With this new error, you won't have to deal with that frustrating experience.

If you do not want o'doyle to require specs for every attribute, but you still want to instrument spec elsewhere, just call (clojure.spec.test.alpha/unstrument 'odoyle.rules/insert) after your instrument call.

0.8.0

19 Feb 16:48
Compare
Choose a tag to compare

This release fixes a form of unintentional non-deterministic behavior that could happen when multiple rules receive the same data. Thanks to @telekid for the bug report.

It also makes it slightly easier to use from ClojureScript -- you can just do a normal :require rather than :require-macros. Thanks to @davesann for the PR.

Thank you to Clojurists Together for supporting this release.

0.7.0

05 Feb 19:03
Compare
Choose a tag to compare

This release makes it much nicer to detect and fix infinite loops. Previously, you just got an ugly StackOverflowException with a very long and unhelpful stack trace. Now, it actually detects which rules are involved in the infinite loop and includes them in the error message.

For example, if you run the code in the "Avoiding infinite loops" section of the README, but you remove the {:then false}, the exception now tells you exactly which rule is the culprit:

Recursion limit hit.
This may be an infinite loop.
The current recursion limit is 16 (set by the :recursion-limit option of fire-rules).

Cycle detected! :odoyle.readme/move-player is triggering itself.

Try using {:then false} to prevent triggering rules in an infinite loop.

Infinite loops can also form when several rules call each other in a cycle. O'Doyle is now smart enough to detect this and show the whole cycle in the error message:

Recursion limit hit.
This may be an infinite loop.
The current recursion limit is 16 (set by the :recursion-limit option of fire-rules).

Cycle detected! :odoyle.rules-test/rule6 is triggering itself.
Cycle detected! :odoyle.rules-test/rule1 -> :odoyle.rules-test/rule2 -> :odoyle.rules-test/rule3 -> :odoyle.rules-test/rule1
Cycle detected! :odoyle.rules-test/rule4 -> :odoyle.rules-test/rule5 -> :odoyle.rules-test/rule4

Try using {:then false} to prevent triggering rules in an infinite loop.

Infinite loops are probably the main sharp edge of using O'Doyle so methinks this will make it a lot less of a drag. In fact, infinite loops might actually be fun now. Sometimes I make them on purpose just to see that beautiful error message. Not even joking.

Thank you to Clojurists Together for supporting this release.