README
¶
🚀 Go Rules Engine
Un moteur de règles métier puissant et flexible pour Go, inspiré de json-rules-engine. Évaluez des conditions complexes et déclenchez des événements basés sur des faits dynamiques.
✨ Fonctionnalités
- 🎯 Règles définies en JSON ou en code - Chargez vos règles depuis des fichiers JSON ou créez-les directement en Go
- 🔄 Conditions complexes - Supportez les opérateurs
alletanyavec imbrication infinie - 📊 Opérateurs riches -
equal,not_equal,greater_than,less_than,in,not_in,contains,not_contains - 🎪 Système d'événements - Callbacks personnalisés et handlers globaux pour réagir aux résultats
- 💾 Faits dynamiques - Calculez des valeurs à la volée avec des callbacks
- 🧮 Support JSONPath - Accédez à des données imbriquées avec
$.path.to.value - ⚡ Priorités de règles - Contrôlez l'ordre d'évaluation avec des priorités
- 🔒 Thread-safe - Protégé par des mutex pour un usage concurrent
- ✅ 100% de couverture de tests - Code robuste et testé en profondeur
📦 Installation
go get github.com/deadelus/go-rules-engine
🚀 Démarrage rapide
Exemple basique
package main
import (
"fmt"
gorulesengine "github.com/deadelus/go-rules-engine/src"
)
func main() {
// 1. Créer le moteur de règles
engine := gorulesengine.NewEngine()
// 2. Définir une règle
rule := &gorulesengine.Rule{
Name: "adult-user",
Priority: 10,
Conditions: gorulesengine.ConditionSet{
All: []gorulesengine.ConditionNode{
{
Condition: &gorulesengine.Condition{
Fact: "age",
Operator: "greater_than",
Value: 18,
},
},
},
},
Event: gorulesengine.Event{
Type: "user-is-adult",
Params: map[string]interface{}{
"message": "Utilisateur majeur détecté",
},
},
}
// 3. Ajouter la règle au moteur
engine.AddRule(rule)
// 4. Créer l'almanac avec des faits
almanac := gorulesengine.NewAlmanac([]*gorulesengine.Fact{})
almanac.AddFact("age", 25)
// 5. Exécuter le moteur
results, err := engine.Run(almanac)
if err != nil {
panic(err)
}
// 6. Afficher les résultats
for _, result := range results {
if result.Result {
fmt.Printf("✅ Règle '%s' déclenchée!\n", result.Rule.Name)
fmt.Printf(" Event: %s\n", result.Event.Type)
}
}
}
Charger des règles depuis JSON
package main
import (
"encoding/json"
"fmt"
gorulesengine "github.com/deadelus/go-rules-engine/src"
)
func main() {
// JSON de la règle
ruleJSON := `{
"name": "premium-user",
"priority": 10,
"conditions": {
"all": [
{
"condition": {
"fact": "accountType",
"operator": "equal",
"value": "premium"
}
},
{
"condition": {
"fact": "revenue",
"operator": "greater_than",
"value": 1000
}
}
]
},
"event": {
"type": "premium-user-detected",
"params": {
"discount": 20
}
}
}`
var rule gorulesengine.Rule
json.Unmarshal([]byte(ruleJSON), &rule)
engine := gorulesengine.NewEngine()
engine.AddRule(&rule)
almanac := gorulesengine.NewAlmanac([]*gorulesengine.Fact{})
almanac.AddFact("accountType", "premium")
almanac.AddFact("revenue", 1500)
results, _ := engine.Run(almanac)
fmt.Printf("Règles déclenchées: %d\n", len(results))
}
Charger des règles ET des facts depuis JSON
package main
import (
"encoding/json"
"fmt"
gorulesengine "github.com/deadelus/go-rules-engine/src"
)
func main() {
// JSON des règles
rulesJSON := `[
{
"name": "high-value-order",
"priority": 100,
"conditions": {
"all": [
{
"condition": {
"fact": "user.isPremium",
"operator": "equal",
"value": true
}
},
{
"condition": {
"fact": "order.total",
"operator": "greater_than",
"value": 100
}
}
]
},
"event": {
"type": "premium-discount",
"params": {"discount": 25}
}
}
]`
// JSON des facts (données)
factsJSON := `{
"user": {
"id": 12345,
"isPremium": true,
"name": "Alice"
},
"order": {
"id": "ORD-001",
"total": 150.50
}
}`
// Charger les règles
var rules []*gorulesengine.Rule
json.Unmarshal([]byte(rulesJSON), &rules)
// Charger les facts
var factsData map[string]interface{}
json.Unmarshal([]byte(factsJSON), &factsData)
// Créer l'engine et ajouter les règles
engine := gorulesengine.NewEngine()
for _, rule := range rules {
engine.AddRule(rule)
}
// Créer l'almanac et ajouter les facts
almanac := gorulesengine.NewAlmanac([]*gorulesengine.Fact{})
for key, value := range factsData {
almanac.AddFact(gorulesengine.FactID(key), value)
}
// Exécuter
results, _ := engine.Run(almanac)
fmt.Printf("Règles déclenchées: %d\n", len(results))
}
📖 Documentation
Architecture
Le moteur de règles est composé de plusieurs composants clés :
1. Engine - Le moteur principal
engine := gorulesengine.NewEngine()
engine.AddRule(rule)
results, err := engine.Run(almanac)
Méthodes :
AddRule(rule *Rule)- Ajoute une règle au moteurRun(almanac *Almanac) ([]RuleResult, error)- Exécute toutes les règlesRegisterCallback(name string, callback Callback)- Enregistre un callback nomméOn(outcome string, handler EventHandler)- Handler global pour success/failureOnEvent(eventType string, handler EventHandler)- Handler spécifique à un type d'événement
2. Rule - Une règle métier
rule := &gorulesengine.Rule{
Name: "my-rule",
Priority: 10, // Plus élevé = exécuté en premier
Conditions: conditionSet,
Event: event,
OnSuccess: strPtr("mySuccessCallback"), // Optionnel
OnFailure: strPtr("myFailureCallback"), // Optionnel
}
3. Condition - Une condition à évaluer
condition := &gorulesengine.Condition{
Fact: "age",
Operator: "greater_than",
Value: 18,
Path: "$.user.age", // Optionnel: JSONPath pour données imbriquées
}
Opérateurs disponibles :
equal- Égaliténot_equal- Différent degreater_than- Supérieur àgreater_than_or_equal- Supérieur ou égal àless_than- Inférieur àless_than_or_equal- Inférieur ou égal àin- Dans la listenot_in- Pas dans la listecontains- Contient (pour strings et arrays)not_contains- Ne contient pas
4. ConditionSet - Groupement de conditions
// Toutes les conditions doivent être vraies (AND)
conditionSet := gorulesengine.ConditionSet{
All: []gorulesengine.ConditionNode{
{Condition: &condition1},
{Condition: &condition2},
},
}
// Au moins une condition doit être vraie (OR)
conditionSet := gorulesengine.ConditionSet{
Any: []gorulesengine.ConditionNode{
{Condition: &condition1},
{Condition: &condition2},
},
}
// Imbrication (AND de OR)
conditionSet := gorulesengine.ConditionSet{
All: []gorulesengine.ConditionNode{
{Condition: &condition1},
{
ConditionSet: &gorulesengine.ConditionSet{
Any: []gorulesengine.ConditionNode{
{Condition: &condition2},
{Condition: &condition3},
},
},
},
},
}
5. Almanac - Stockage des faits
almanac := gorulesengine.NewAlmanac([]*gorulesengine.Fact{})
// Ajouter des faits simples
almanac.AddFact("age", 25)
almanac.AddFact("country", "FR")
// Ajouter des faits dynamiques
almanac.AddFact("temperature", gorulesengine.Fact{
ID: "temperature",
Calculate: func(params map[string]interface{}, almanac *gorulesengine.Almanac) (interface{}, error) {
// Logique de calcul personnalisée
return fetchTemperature(), nil
},
})
// Récupérer un fait
value, err := almanac.GetFactValue("age", nil)
6. Event - Événement déclenché
event := gorulesengine.Event{
Type: "user-approved",
Params: map[string]interface{}{
"userId": 123,
"reason": "All conditions met",
},
}
Système de callbacks et handlers
Callbacks nommés (définis dans les règles JSON)
engine := gorulesengine.NewEngine()
// Enregistrer le callback
engine.RegisterCallback("sendEmail", func(event gorulesengine.Event, almanac *gorulesengine.Almanac, ruleResult gorulesengine.RuleResult) error {
fmt.Printf("Envoi d'email pour: %s\n", event.Type)
return nil
})
// Dans la règle JSON
rule := &gorulesengine.Rule{
Name: "email-rule",
OnSuccess: strPtr("sendEmail"), // Référence au callback
// ...
}
Handlers globaux
// Handler pour toutes les règles réussies
engine.On("success", func(event gorulesengine.Event, almanac *gorulesengine.Almanac, ruleResult gorulesengine.RuleResult) error {
fmt.Printf("✅ Règle réussie: %s\n", ruleResult.Rule.Name)
return nil
})
// Handler pour toutes les règles échouées
engine.On("failure", func(event gorulesengine.Event, almanac *gorulesengine.Almanac, ruleResult gorulesengine.RuleResult) error {
fmt.Printf("❌ Règle échouée: %s\n", ruleResult.Rule.Name)
return nil
})
Handlers par type d'événement
// Handler spécifique pour un type d'événement
engine.OnEvent("user-approved", func(event gorulesengine.Event, almanac *gorulesengine.Almanac, ruleResult gorulesengine.RuleResult) error {
userId := event.Params["userId"]
fmt.Printf("Utilisateur %v approuvé!\n", userId)
return nil
})
Support JSONPath
Accédez à des données imbriquées dans vos faits :
almanac := gorulesengine.NewAlmanac([]*gorulesengine.Fact{})
almanac.AddFact("user", map[string]interface{}{
"profile": map[string]interface{}{
"age": 25,
"address": map[string]interface{}{
"city": "Paris",
},
},
})
// Utilisez JSONPath dans les conditions
condition := &gorulesengine.Condition{
Fact: "user",
Path: "$.profile.address.city",
Operator: "equal",
Value: "Paris",
}
Gestion des erreurs
Le moteur utilise un système d'erreurs typées pour une meilleure traçabilité :
results, err := engine.Run(almanac)
if err != nil {
var ruleErr *gorulesengine.RuleEngineError
if errors.As(err, &ruleErr) {
fmt.Printf("Type: %s, Message: %s\n", ruleErr.Type, ruleErr.Msg)
}
}
Types d'erreurs :
ErrEngine- Erreur générale du moteurErrAlmanac- Erreur liée aux faits (almanac)ErrFact- Erreur de calcul de faitErrRule- Erreur dans la définition de la règleErrCondition- Erreur d'évaluation de conditionErrOperator- Opérateur invalide ou non trouvéErrEvent- Erreur liée aux événementsErrJSON- Erreur de parsing JSON
🧪 Tests
Le projet dispose d'une couverture de tests de 100% :
# Exécuter tous les tests
go test ./src -v
# Avec couverture
go test ./src -coverprofile=coverage.out
go tool cover -html=coverage.out
# Voir le résumé
go tool cover -func=coverage.out | tail -1
# Output: total: (statements) 100.0%
🔍 Qualité du code
Le code respecte toutes les conventions Go et passe les linters sans avertissement :
# go vet (vérification statique)
go vet ./src/...
# golint (style Go)
golint ./src/...
# Format du code
go fmt ./src/...
Standards respectés:
- ✅ Conventions de nommage Go (CamelCase, pas de ALL_CAPS)
- ✅ Documentation GoDoc complète sur toutes les exports
- ✅ Gestion d'erreurs appropriée
- ✅ Code thread-safe avec mutexes
- ✅ Tests exhaustifs avec 100% de couverture
🗺️ Roadmap
✅ Phases complétées
- Phase 1: Structures de base (Condition, Rule, Fact)
- Phase 2: Almanac et gestion des faits
- Phase 3: Opérateurs (equal, greater_than, less_than, etc.)
- Phase 4: Évaluation des conditions (all/any, imbrication)
- Phase 5: Engine avec système d'événements
- Phase 6: Support JSON et désérialisation
- Phase 7: Features avancées (callbacks, handlers, JSONPath)
- Tests complets avec 100% de couverture
🚧 Phases à venir
Phase 8: API ergonomique et builders
Builders fluent pour créer des règles
rule := NewRuleBuilder().
WithName("adult-user").
WithPriority(10).
WithCondition(Equal("age", 18)).
WithEvent("user-is-adult", nil).
Build()
Helpers de conditions
condition := All(
GreaterThan("age", 18),
Equal("country", "FR"),
Any(
Equal("status", "premium"),
Equal("status", "vip"),
),
)
Phase 9: Documentation et exemples
- Documentation GoDoc complète
- Exemples dans
examples/-
examples/full-demo.go- Démonstration complète de toutes les fonctionnalités -
examples/basic/- Cas simple -
examples/json/- Chargement JSON -
examples/advanced/- Features avancées -
examples/custom-operator/- Opérateurs personnalisés
-
Phase 10: Nouveaux opérateurs
-
regex- Vérifier si la valeur correspond à une expression régulière
Phase 11: Performance et optimisation
- Benchmarks complets
- Cache des résultats de conditions
- Évaluation parallèle des règles indépendantes
- Profilage mémoire et CPU
Phase 12: Features avancées
- Tri des fact par
priority - Support de règles async
- Persistance des résultats
- Métriques et monitoring
- Hot-reload des règles
- API REST optionnelle
🤝 Contribution
Les contributions sont les bienvenues ! Pour contribuer :
- Forkez le projet
- Créez une branche (
git checkout -b feature/amazing-feature) - Committez vos changements (
git commit -m 'Add amazing feature') - Pushez vers la branche (
git push origin feature/amazing-feature) - Ouvrez une Pull Request
Guidelines :
- Écrivez des tests pour toutes les nouvelles fonctionnalités
- Maintenez la couverture à 100%
- Suivez les conventions Go (gofmt, golint)
- Documentez vos fonctions publiques
📄 License
Ce projet est sous licence MIT. Voir le fichier LICENSE pour plus de détails.
Copyright (c) 2026 Geoffrey Trambolho (@deadelus)
🙏 Remerciements
Inspiré par json-rules-engine de CacheControl.
📞 Contact
Créé par @deadelus
⭐ N'oubliez pas de donner une étoile si ce projet vous aide !