https://github.com/rsucasasf/cloj-rules-engine
Rules engine written in Clojure
https://github.com/rsucasasf/cloj-rules-engine
clojure java-interop rules-engine
Last synced: 24 days ago
JSON representation
Rules engine written in Clojure
- Host: GitHub
- URL: https://github.com/rsucasasf/cloj-rules-engine
- Owner: rsucasasf
- License: epl-1.0
- Created: 2017-05-24T10:43:56.000Z (almost 9 years ago)
- Default Branch: master
- Last Pushed: 2017-06-28T11:45:50.000Z (over 8 years ago)
- Last Synced: 2025-10-21T22:50:52.862Z (5 months ago)
- Topics: clojure, java-interop, rules-engine
- Language: Clojure
- Size: 122 KB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# cloj-rules-engine
[](https://travis-ci.org/rsucasasf/cloj-rules-engine)
[](https://opensource.org/licenses/EPL-1.0)
[](https://github.com/rsucasasf/cloj-rules-engine)
[](https://codecov.io/gh/rsucasasf/cloj-rules-engine)
[](https://clojars.org/clojars.org/cloj-rules-engine)
**cloj-rules-engine** is a very simple rules engine written in [Clojure](https://clojure.org/) and designed to work with Java.
- Maven:
*pom.xml*:
```xml
clojars
http://clojars.org/repo/
```
```xml
clojars.org
cloj-rules-engine
0.1.2-SNAPSHOT
```
java code:
```java
...
cloj_rules_engine.ClojRules clrules = new cloj_rules_engine.ClojRules();
...
clrules.initialize("rules.clj");
...
```
- Clojure:
*project.clj*:
```clojure
[clojars.org/cloj-rules-engine "0.1.2-SNAPSHOT"]
```
clojure code:
```clojure
(ns ...
(:require [cloj-rules-engine.rules-mng :as cre]
...))
...
(cre/initialize "RULES_PATH")
...
```
-----------------------
**Table of Contents**
- [Description](#description)
- Features
- Main methods
- Things to do / limitations / tips
- [Prerequisites](#prerequisites)
- [Usage](#usage)
- [Complex Rules](#complex-rules)
- [License](#license)
-----------------------
## Description
**cloj-rules-engine** is a rules engine written in Clojure.

### **Features**:
- Each **rule** has a **condition** (composed of conditional expressions -written in Clojure- that refer to facts), and a set of **actions** that are activated if the condition is satisfied. **Facts** are the data upon which rules operate. The fired actions are a a set of string identifiers.
- Rules are expressed in a simple and easy to read Clojure format ([clojure *maps*](http://www.deadcoderising.com/2015-04-clojure-basics-dealing-wit-maps/))
```clojure
{
:RULE_1 {:cond "(and (< #A 10) (> #B 50))"
:actions ["action-1"]}
...
}
```
- Rules with 'probabilities': if condition is satisfied, then the actions have a **probability** of being fired. These probabilities are values between **0** (no chance to be fired - 0%) and **1** (will be fired - 100%). These actions are evaluated in order. This means that if an action is fired, the rest of the actions are ignored.
```clojure
:RULE_2 {:cond "(> #A 10)"
:actions [{"action-B" 0.5} {"action-C" 0.5}]
```
- Facts can be expressed via clojure maps (Clojure) or via PersistentArrayMap objects (Java):
```clojure
(update-map-facts {"#A" "14"})
```
```java
PersistentArrayMap facts_map = new PersistentArrayMap(new Object[] {
"#A", "14"
});
clrules.updateMapFacts(facts_map);
```
- This library can be used from Java or Clojure code
- Third party libraries used in this project:
| Libs | Version | License |
| ------------------------- |:-------------:| -----------------------------------------:|
| [clojure](https://clojure.org/) | 1.8.0 | [](https://opensource.org/licenses/EPL-1.0) |
| [tools.logging](https://github.com/clojure/tools.logging) | 0.3.1 | [](https://opensource.org/licenses/EPL-1.0) |
| [log4j](http://logging.apache.org/log4j/1.2/) | 1.2.17 | [](https://opensource.org/licenses/Apache-2.0) |
| [data.json](https://github.com/clojure/data.json) | 0.2.6 | [](https://opensource.org/licenses/EPL-1.0) |
| [proto-repl](https://github.com/jasongilman/proto-repl) | 0.3.1 | [](https://opensource.org/licenses/MIT) |
| [math.numeric-tower](https://github.com/clojure/math.numeric-tower/) | 0.0.4 | [](https://opensource.org/licenses/EPL-1.0) |
| [lein-cloverage](https://github.com/cloverage/cloverage) | 1.0.9 | [](https://opensource.org/licenses/EPL-1.0) |
#### Main methods:
- **initialize** loads rules map from absolute or relative path. Returns *true* if everything is okay.
```clojure
(initialize "rules.clj")
```
```java
clrules.initialize("rules.clj");
```
- **update-map-facts** update / initialize facts
```clojure
(update-map-facts {"#A" "14"})
```
```java
clrules.updateMapFacts(facts_map);
```
- **get-rules-actions** evaluates rules based on current facts, and return a list (String) of 'fired' actions
```clojure
(get-rules-actions)
```
```java
clrules.getRulesActions();
```
- **get-fired-rules**
- **initialize-from-json**
- **get-rules-actions-probs** (**ONLY Rules with 'probabilities'**) valuates rules based on current facts, and return a list (String) of 'fired' actions
### Things to do / limitations / tips
- (**RULES DEFINITION**) The set of rules are defined using Clojure syntax => Clojure maps. Parameters / facts have the following formatt: #*FACTNAME*
- No underscores allowed.
- A *hash* (#) before the fact name
- Regular expression used to validate fact / parameter names: `#"\#[A-Za-z][A-Za-z0-9]*"`
- (**RULES EVALUATION**) The rules and facts are evaluated following the steps of the next example:
1. In this example we have 2 facts or parameters: *#A* and *#B*
2. First, we create a new file to define the rules conditions:
```clojure
:RULE_1 {:cond "(and (< #A 10) (> #B 50))"
:actions ["action-1"]}
```
3. Then, when using the library, we set or update the facts
```clojure
(update-map-facts {"#A" 33, "#B" 66}))
```
4. After the new facts' values have been updated, the rules conditions are transformed to clojure syntax in the following way:
```clojure
(when (and (< 33 10) (> 66 50)) :RULE_1)
```
5. Finally, if condition is satisfied (using clojure *eval* function inside **get-rules-actions** method), the rule is tagged as fired
- (**RULES DEFINITION**) Conditions are clojure expressions surrounded by quotes.
- (**RULES DEFINITION**) When creating the set of rules, use a hash for each of the parameters / facts (i.e. *#A* and *#B*):
```clojure
:RULE_1 {:cond "(and (< #A 10) (> #B 50))"
:actions ["action-1"]}
```
- (**RULES DEFINITION**) Use `str` function, single quotes or double quotes (and escape character) if you want to eval String variables.
```clojure
:RULE_5 {:cond "(= (str #D) (str 50))"
:actions ["action-E"]}
:RULE_6 {:cond "(= #D \"goldenaxe\")"
:actions ["action-F"]}
:RULE_7 {:cond "(= #D 'goldenaxe2')"
:actions ["action-G"]}
```
- (**TESTING FACTS**) When creating / updating facts, escape string values that will be used as string
```clojure
(update-map-facts {"#A" 15, "#D" "\"goldenaxe\""}))
```
- If a rule is evaluated and 'fired', it won't be fired until facts are updated. In order to get all the 'fired' rules, call the **get-fired-rules** method / function
-----------------------
## Prerequisites
1. Java version 8
2. [Leiningen][] 2.0.0 or above installed.
[leiningen]: https://github.com/technomancy/leiningen
-----------------------
## Usage
First, define a set of rules (*"rules.clj"*):
```clojure
{
:RULE_1 {:cond "(and (< #A 10) (> #B 50))"
:actions ["action-1"]
:desc "Rule description: 'launch' action-1 if 'a' is lower than 10 and if 'b' is greater than 50"}
:RULE_2 {:cond "(> #A 10)"
:actions ["action-2"]}
}
```
And then, ...
### From Clojure
```clojure
(initialize "rules.clj")
(update-map-facts {"#A" "14"})
(get-rules-actions)
(get-fired-rules)
```
Or...
```clojure
(if (initialize "rules.clj")
(when (update-map-facts {"#A" "15", "#B" 13, "#D" "\"goldenaxe\""})
(get-rules-actions))
false)
```
### From Java
1. [Create a jar or add dependency to maven](#export-to-maven-local-repo)
2. Java code to use the library:
```java
cloj_rules_engine.ClojRules clrules = new cloj_rules_engine.ClojRules();
...
clrules.initialize("rules.clj");
PersistentArrayMap facts_map = new PersistentArrayMap(new Object[] {
"#A", "5",
"#B", "51"
});
clrules.updateMapFacts(facts_map);
clrules.getRulesActions();
clrules.getFiredRules(); // get fired rules in json format
```
-----------------------
## Complex Rules
- You can use functions from [clojure.math.numeric-tower](https://clojure.github.io/math.numeric-tower/) when defining rules: `expt`, `abs`, `gcd`, `lcm`, `floor` ...
```clojure
:RULE_8 {:cond "(> (sqrt #C) 10)"
:actions ["action-H-sqrt"]
:desc "Rule description: 'launch' action-H-sqrt if square root of #C is greater than 10."}
```
- You can also use clojure functions (from **org.clojure/clojure**) that return boolean values: `every?`, `even?`, `odd?` ...
```clojure
:RULE_9 {:cond "(every? even? (list #A #B #C))"
:actions ["action-I-even?"]
:desc "Rule description: 'launch' action-I-even? if all elements from list are even."}
```
- Or custom functions: `#(> % 10)`
```clojure
:RULE_10 {:cond "(every? #(> % 10) [#A #B #C])"
:actions ["action-J-func"]
:desc "Rule description: 'launch' action-J-func if all elements from list / vector are greater than 10"}
```
- (**warning**: not ready yet - only works with **string** vectors or lists) Use lists or vectors as parameters:
```clojure
:RULE_11 {:cond "(every? #(> % 100) #LIST1)"
:actions ["action-K-func"]
:desc "Rule description: 'launch' action-K-func if all elements from list / vector '#LIST1' are greater than 10"}
```
```clojure
(update-map-facts {"#A" "21", "#B" 43, "#C" 1000, "#LIST1" "[121 321 123 122 1233]"})
```
-----------------------
## License
Copyright © 2017 Roi Sucasas Font
Distributed under the Eclipse Public License, the same as Clojure.