verdict_rules 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +34 -0
- data/README.md +249 -0
- data/lib/verdict_rules/engine.rb +111 -0
- data/lib/verdict_rules/result.rb +43 -0
- data/lib/verdict_rules/rule.rb +80 -0
- data/lib/verdict_rules/rule_builder.rb +51 -0
- data/lib/verdict_rules/version.rb +5 -0
- data/lib/verdict_rules.rb +11 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c6ebe08f124cc88d8cdcd68dcbf4335c6aae0c714d02dc03a2dd6610b2ba8eb0
|
|
4
|
+
data.tar.gz: 191c1ec6daf71053d0c24d212972d1bb1be45a7625e136f4f6fa2fe50b350597
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 457d29778fd20e42da7fa74db1af6de5678250b031cf77b8fc00ae31e86c50fbf1df5faa0cdc424aa013931f8042e7f4a68e5d0597c0ee47b48e190cc63e8443
|
|
7
|
+
data.tar.gz: 4d15e8bc27ec6977fc8b75ed7c9bb9a328d9c5d502ccaf560500ddf85620691a87ff28d51dcc5c74c54283fcc128e55ce5c05766bd011fa605b49fcc4ca3a13e
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-03-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release
|
|
14
|
+
- Core rule engine with context management
|
|
15
|
+
- Priority-based rule resolution
|
|
16
|
+
- Explainable results with Result object
|
|
17
|
+
- Expressive DSL for rule definition
|
|
18
|
+
- Named rules for better debugging and logging
|
|
19
|
+
- Immutable context for thread safety
|
|
20
|
+
- 100% test coverage
|
|
21
|
+
- Comprehensive examples
|
|
22
|
+
- Full documentation in Portuguese
|
|
23
|
+
|
|
24
|
+
### Features
|
|
25
|
+
- `VerdictRules::Engine` - Main engine for rule evaluation
|
|
26
|
+
- `VerdictRules::Rule` - Individual rule with condition, action, and priority
|
|
27
|
+
- `VerdictRules::Result` - Rich result object explaining outcomes
|
|
28
|
+
- `VerdictRules::RuleBuilder` - DSL builder for clean syntax
|
|
29
|
+
- Support for complex nested contexts
|
|
30
|
+
- Priority system with stable sort
|
|
31
|
+
- Method chaining support
|
|
32
|
+
|
|
33
|
+
[Unreleased]: https://github.com/tibas-ce/verdict_rules/compare/v0.1.0...HEAD
|
|
34
|
+
[0.1.0]: https://github.com/tibas-ce/verdict_rules/releases/tag/v0.1.0
|
data/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# ⚖️ VerdictRules
|
|
4
|
+
|
|
5
|
+
### Rule engine para Ruby com prioridades explícitas, resultados rastreáveis e DSL expressiva.
|
|
6
|
+
|
|
7
|
+
Defina regras de negócio complexas de forma clara, debuggável e pronta para produção.
|
|
8
|
+
|
|
9
|
+
[](https://github.com/tibas-ce/verdict_rules/actions)
|
|
10
|
+
[](https://rubygems.org/gems/verdict_rules)
|
|
11
|
+
[](https://opensource.org/licenses/MIT)
|
|
12
|
+
|
|
13
|
+
[Recursos](#-recursos) •
|
|
14
|
+
[Instalação](#-instalação) •
|
|
15
|
+
[Início Rápido](#-início-rápido) •
|
|
16
|
+
[API](#-api) •
|
|
17
|
+
[Debugging e Observabilidade](#-debugging-e-observabilidade) •
|
|
18
|
+
[Exemplos](#-exemplos)
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 🎯 O que é VerdictRules?
|
|
25
|
+
|
|
26
|
+
**VerdictRules** é uma gem Ruby leve e sem dependências que permite definir regras de negócio de forma declarativa com nome, prioridades explícitas e resultados explicáveis.
|
|
27
|
+
|
|
28
|
+
Perfeita para:
|
|
29
|
+
- 🏦 **Sistemas financeiros**: Aprovação de crédito, detecção de fraude
|
|
30
|
+
- 🛒 **E-commerce**: Regras de preço, lógica de descontos
|
|
31
|
+
- 🔐 **Autorização**: Controle de acesso, sistemas de permissão
|
|
32
|
+
- ✅ **Validação**: Lógica condicional complexa
|
|
33
|
+
- 🎮 **Lógica de jogos**: Sistemas de pontuação, conquistas
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## ✨ Recursos
|
|
38
|
+
|
|
39
|
+
| Recurso | Descrição |
|
|
40
|
+
|---------|-----------|
|
|
41
|
+
| 🎯 **Sistema de Prioridades** | Resolve conflitos quando múltiplas regras são verdadeiras |
|
|
42
|
+
| 🏷️ **Regras Nomeadas** | Identifique exatamente qual regra foi aplicada |
|
|
43
|
+
| 📊 **Resultados Explicáveis** | Saiba qual regra foi aplicada e por quê |
|
|
44
|
+
| ✨ **DSL Expressiva** | Sintaxe limpa e legível |
|
|
45
|
+
| 🔒 **Contexto Imutável** | Comportamento thread-safe e previsível |
|
|
46
|
+
| 🧪 **100% de Cobertura** | Testado e confiável |
|
|
47
|
+
| 📦 **Zero Dependências** | Leve e rápida |
|
|
48
|
+
| 🚀 **Design pronto para produção** | Arquitetura preparada para uso real |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 📦 Instalação
|
|
53
|
+
|
|
54
|
+
Adicione esta linha ao Gemfile da sua aplicação:
|
|
55
|
+
```ruby
|
|
56
|
+
gem 'verdict_rules'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
E então execute:
|
|
60
|
+
```bash
|
|
61
|
+
bundle install
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Ou instale você mesmo:
|
|
65
|
+
```bash
|
|
66
|
+
gem install verdict_rules
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 🚀 Início Rápido
|
|
72
|
+
|
|
73
|
+
### Exemplo Básico
|
|
74
|
+
```ruby
|
|
75
|
+
require "verdict_rules"
|
|
76
|
+
|
|
77
|
+
context = { age: 25, verified: true }
|
|
78
|
+
|
|
79
|
+
engine = VerdictRules::Engine.new(context)
|
|
80
|
+
|
|
81
|
+
engine.rules do
|
|
82
|
+
rule :verified_user, priority: 10 do
|
|
83
|
+
when_condition { |ctx| ctx[:verified] }
|
|
84
|
+
then_action :approve_verified
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
rule :adult_user, priority: 1 do
|
|
88
|
+
when_condition { |ctx| ctx[:age] >= 18 }
|
|
89
|
+
then_action :approve_adult
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
result = engine.evaluate
|
|
94
|
+
|
|
95
|
+
result.value # => :approve_verified
|
|
96
|
+
result.matched_rule.name # => :verified_user
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### Problema que resolve
|
|
102
|
+
|
|
103
|
+
Sem prioridades e rastreabilidade:
|
|
104
|
+
|
|
105
|
+
- decisões imprevisíveis ❌
|
|
106
|
+
- difícil entender por que algo foi aprovado/rejeitado ❌
|
|
107
|
+
- debugging lento e custoso ❌
|
|
108
|
+
|
|
109
|
+
Com VerdictRules:
|
|
110
|
+
|
|
111
|
+
- decisão previsível ✅
|
|
112
|
+
- regra identificável ✅
|
|
113
|
+
- lógica centralizada ✅
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
### Exemplo Real: Produção
|
|
117
|
+
```ruby
|
|
118
|
+
engine.rules do
|
|
119
|
+
rule :vip_customer, priority: 100 do
|
|
120
|
+
when_condition { |ctx| ctx[:vip_customer] }
|
|
121
|
+
then_action({ status: :approved, limit: 50000 })
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
rule :income_not_verified, priority: 10 do
|
|
125
|
+
when_condition { |ctx| !ctx[:verified_income] }
|
|
126
|
+
then_action({ status: :manual_review })
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
rule :good_credit, priority: 1 do
|
|
130
|
+
when_condition { |ctx| ctx[:credit_score] >= 700 }
|
|
131
|
+
then_action({ status: :approved })
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
result = engine.evaluate
|
|
136
|
+
|
|
137
|
+
result.to_h
|
|
138
|
+
# => {
|
|
139
|
+
# value: { status: :manual_review },
|
|
140
|
+
# matched: true,
|
|
141
|
+
# matched_rule: { name: :income_not_verified, priority: 10, ... }
|
|
142
|
+
# }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 🔍 Debugging e Observabilidade
|
|
148
|
+
Projetado para sistemas onde decisões precisam ser auditáveis e explicáveis.
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
result = engine.evaluate
|
|
152
|
+
|
|
153
|
+
result.matched_rule.name
|
|
154
|
+
# => :income_not_verified
|
|
155
|
+
|
|
156
|
+
result.matched_rule.priority
|
|
157
|
+
# => 10
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 📊 Logging estruturado
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
log = {
|
|
164
|
+
event: "rule_evaluation",
|
|
165
|
+
result: result.value,
|
|
166
|
+
matched: result.matched?,
|
|
167
|
+
rule: result.matched_rule&.to_h
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
puts JSON.pretty_generate(log)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Ideal para cenários como:
|
|
174
|
+
|
|
175
|
+
- auditoria
|
|
176
|
+
- debugging em produção
|
|
177
|
+
- sistemas financeiros / críticos
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 🔧 API
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
engine.rule(:name, priority: 10) do
|
|
185
|
+
when_condition { |ctx| ... }
|
|
186
|
+
then_action :result
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
engine.rules do
|
|
190
|
+
rule(:a, priority: 10) { ... }
|
|
191
|
+
rule(:b, priority: 5) { ... }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
engine.evaluate
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 💡 Exemplos
|
|
200
|
+
|
|
201
|
+
Confira o diretório [`examples/`](examples/) para exemplos completos e funcionais:
|
|
202
|
+
|
|
203
|
+
- **[Uso Básico](examples/basic_usage.rb)** - Regras simples sem DSL
|
|
204
|
+
- **[Sistema de Prioridades](examples/priority_and_exceptions.rb)** - Aprovação de crédito com prioridades
|
|
205
|
+
- **[DSL Básica](examples/dsl_basic.rb)** - DSL vs API tradicional
|
|
206
|
+
- **[DSL no Mundo Real](examples/dsl_real_word.rb)** - Sistema de aprovação de compras
|
|
207
|
+
- **[Debugging com regras nomeadas](examples/named_rules_debugging.rb)** - Investigando decisões e logs estruturados
|
|
208
|
+
|
|
209
|
+
Execute-os:
|
|
210
|
+
```bash
|
|
211
|
+
ruby examples/basic_usage.rb
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 📊 Princípios de Design
|
|
217
|
+
|
|
218
|
+
VerdictRules segue estes princípios:
|
|
219
|
+
|
|
220
|
+
- ✅ **Explícito sobre Implícito** - Sem mágica, comportamento claro
|
|
221
|
+
- ✅ **Simples sobre Complexo** - Fácil de entender e debugar
|
|
222
|
+
- ✅ **Testável** - Cada componente tem testes unitários
|
|
223
|
+
- ✅ **Imutável** - Contexto não pode ser modificado
|
|
224
|
+
- ✅ **Explicável** - Resultados dizem por que aconteceram
|
|
225
|
+
- ✅ **Componível** - Misture DSL e API tradicional livremente
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 📄 Licença
|
|
230
|
+
|
|
231
|
+
Este projeto está licenciado sob a Licença MIT - veja o arquivo [LICENSE](LICENSE) para detalhes.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 🙏 Construído usando
|
|
236
|
+
|
|
237
|
+
- Ruby
|
|
238
|
+
- RSpec
|
|
239
|
+
- SimpleCov
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
<div align="center">
|
|
244
|
+
|
|
245
|
+
**[⬆ voltar ao topo](#️-verdictrules)**
|
|
246
|
+
|
|
247
|
+
Feito por [Tibério dos Santos Ferreira](https://github.com/tibas-ce)
|
|
248
|
+
|
|
249
|
+
</div>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Engine é responsável por:
|
|
2
|
+
# - armazenar o contexto de avaliação
|
|
3
|
+
# - gerenciar a lista de regras
|
|
4
|
+
# - avaliar as regras em ordem e retornar a primeira action válida
|
|
5
|
+
# O contexto é fornecido na inicialização e pertence à Engine.
|
|
6
|
+
|
|
7
|
+
module VerdictRules
|
|
8
|
+
class Engine
|
|
9
|
+
attr_reader :context, :rules
|
|
10
|
+
|
|
11
|
+
def initialize(context = {})
|
|
12
|
+
@context = deep_freeze(context.dup)
|
|
13
|
+
@rules = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Adiciona uma regra à engine
|
|
17
|
+
# Regras são automaticamente ordenadas por prioridade (maior primeiro)
|
|
18
|
+
# Em caso de empate, ordem de inserção é mantida (stable sort)
|
|
19
|
+
# Retorna self para permitir chaining
|
|
20
|
+
def add_rule(rule)
|
|
21
|
+
@rules << rule
|
|
22
|
+
sort_rules!
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# DSL: Define uma única regra usando bloco
|
|
27
|
+
# Formas suportadas:
|
|
28
|
+
# rule(priority: 10) { ... }
|
|
29
|
+
# rule(:name, priority: 10) { ... }
|
|
30
|
+
# rule(:name) { ... }
|
|
31
|
+
#
|
|
32
|
+
# Parâmetros:
|
|
33
|
+
# - name: identificador opcional da regra
|
|
34
|
+
# - priority: ordem de avaliação (maior primeiro)
|
|
35
|
+
#
|
|
36
|
+
# O bloco é executado no contexto de RuleBuilder, permitindo:
|
|
37
|
+
# when_condition { |ctx| ... }
|
|
38
|
+
# then_action :value
|
|
39
|
+
# Retorna self para permitir chaining
|
|
40
|
+
def rule(name = nil, priority: 0, &block)
|
|
41
|
+
raise ArgumentError, "block required" unless block_given?
|
|
42
|
+
|
|
43
|
+
builder = RuleBuilder.new(name: name, priority: priority)
|
|
44
|
+
builder.instance_eval(&block)
|
|
45
|
+
add_rule(builder.build)
|
|
46
|
+
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# DSL: Define múltiplas regras em um bloco
|
|
51
|
+
# Executa o bloco no contexto da Engine, permitindo chamadas a `rule`
|
|
52
|
+
# Exemplo:
|
|
53
|
+
# engine.rules do
|
|
54
|
+
# rule(:check_age, priority: 10) { ... }
|
|
55
|
+
# rule(:check_verified, priority: 5) { ... }
|
|
56
|
+
# end
|
|
57
|
+
#
|
|
58
|
+
# Retorna self para permitir chaining
|
|
59
|
+
def rules(&block)
|
|
60
|
+
return @rules unless block_given?
|
|
61
|
+
|
|
62
|
+
instance_eval(&block)
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Avalia as regras utilizando o contexto interno da Engine.
|
|
67
|
+
# Regras são avaliadas em ordem de prioridade (maior primeiro)
|
|
68
|
+
# Decisão de design:
|
|
69
|
+
# - O contexto é definido na inicialização da Engine
|
|
70
|
+
# - O método `evaluate` não recebe argumentos
|
|
71
|
+
# - Cada regra recebe o contexto da Engine durante a avaliação
|
|
72
|
+
# Retorna um Result contendo:
|
|
73
|
+
# - value: a action da primeira regra que bater (ou nil)
|
|
74
|
+
# - matched_rule: a regra que bateu (ou nil)
|
|
75
|
+
# Isso mantém a API simples e evita múltiplas formas de fornecer contexto.
|
|
76
|
+
def evaluate
|
|
77
|
+
rules.each do |rule|
|
|
78
|
+
action = rule.evaluate(context)
|
|
79
|
+
|
|
80
|
+
unless action.nil?
|
|
81
|
+
return Result.new(value: action, matched_rule: rule)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Nenhuma regra bateu
|
|
86
|
+
Result.new(value: nil, matched_rule: nil)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Congela recursivamente hashes e arrays para garantir a imutabilidade do contexto após a inicialização
|
|
92
|
+
def deep_freeze(object)
|
|
93
|
+
case object
|
|
94
|
+
when Hash
|
|
95
|
+
object.each { |key, value| deep_freeze(value) }
|
|
96
|
+
object.freeze
|
|
97
|
+
when Array
|
|
98
|
+
object.each { |value| deep_freeze(value) }
|
|
99
|
+
object.freeze
|
|
100
|
+
else
|
|
101
|
+
object.freeze
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Ordena regras por prioridade (maior primeiro)
|
|
106
|
+
# Em caso de empate, preserva a ordem de inserção para garantir previsibilidade na avaliação.
|
|
107
|
+
def sort_rules!
|
|
108
|
+
@rules = @rules.sort_by.with_index { |rule, index| [-rule.priority, index] }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module VerdictRules
|
|
2
|
+
# Representa o resultado da avaliação de uma Rule Engine. Encapsula o valor retornado e a regra que foi satisfeita (se houver)
|
|
3
|
+
class Result
|
|
4
|
+
attr_reader :value, :matched_rule
|
|
5
|
+
|
|
6
|
+
def initialize(value:, matched_rule:)
|
|
7
|
+
@value = value
|
|
8
|
+
@matched_rule = matched_rule
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Indica se alguma regra foi satisfeita
|
|
12
|
+
def matched?
|
|
13
|
+
!matched_rule.nil?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Retorna representação em hash do resultado da avaliação
|
|
17
|
+
def to_h
|
|
18
|
+
{
|
|
19
|
+
value: value,
|
|
20
|
+
matched: matched?,
|
|
21
|
+
matched_rule: matched_rule
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Representação legível para debugging
|
|
26
|
+
def inspect
|
|
27
|
+
if matched?
|
|
28
|
+
"#<VerdictRules::Result value=#{value.inspect} matched=true rule=#{matched_rule.object_id}>"
|
|
29
|
+
else
|
|
30
|
+
"#<VerdictRules::Result value=#{value.inspect} matched=false>"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Define igualdade semâtica entre dois Results
|
|
35
|
+
def ==(other)
|
|
36
|
+
return false unless other.is_a?(Result)
|
|
37
|
+
|
|
38
|
+
value == other.value && matched_rule == other.matched_rule
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
alias eql? ==
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module VerdictRules
|
|
2
|
+
# Representa uma regra simples composta por:
|
|
3
|
+
# - name: identificador opcional da regra (útil para debug e logging)
|
|
4
|
+
# - uma condition (Proc que recebe o contexto e retorna true/false)
|
|
5
|
+
# - uma action (valor retornado quando a condição é satisfeita)
|
|
6
|
+
# - priority: número usado para ordenar regras (quanto maior, maior prioridade)
|
|
7
|
+
class Rule
|
|
8
|
+
attr_reader :name, :condition, :action, :priority
|
|
9
|
+
|
|
10
|
+
def initialize(name: nil, condition:, action:, priority: 0)
|
|
11
|
+
validate_arguments!(name, condition, action, priority)
|
|
12
|
+
|
|
13
|
+
@name = normalize_name(name)
|
|
14
|
+
@condition = condition
|
|
15
|
+
@action = action
|
|
16
|
+
@priority = priority
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Avalia a regra contra um contexto
|
|
20
|
+
# Retorna a action se a condition for true, nil caso contrário
|
|
21
|
+
def evaluate(context)
|
|
22
|
+
return action if matches?(context)
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Verifica se a condição é satisfeita pelo contexto
|
|
27
|
+
def matches?(context)
|
|
28
|
+
condition.call(context)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Representação em hash (útil para logging e serialização)
|
|
32
|
+
def to_h
|
|
33
|
+
{
|
|
34
|
+
name: name,
|
|
35
|
+
priority: priority,
|
|
36
|
+
action: action
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Representação legível para debugging
|
|
41
|
+
def inspect
|
|
42
|
+
if name
|
|
43
|
+
"#<VerdictRules::Rule name=#{name.inspect} priority=#{priority} action=#{action.inspect}>"
|
|
44
|
+
else
|
|
45
|
+
"#<VerdictRules::Rule priority=#{priority} action=#{action.inspect}>"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Valida os argumentos da regra.
|
|
52
|
+
# Regras de design:
|
|
53
|
+
# - condition deve ser um Proc
|
|
54
|
+
# - action é obrigatória
|
|
55
|
+
# - priority deve ser numérica (pode ser negativa)
|
|
56
|
+
def validate_arguments!(name, condition, action, priority)
|
|
57
|
+
validate_name!(name) unless name.nil?
|
|
58
|
+
raise ArgumentError, "condition" if condition.nil?
|
|
59
|
+
raise ArgumentError, "action" if action.nil?
|
|
60
|
+
raise ArgumentError, "condition must be a Proc" unless condition.is_a?(Proc)
|
|
61
|
+
raise ArgumentError, "priority must be a number" unless priority.is_a?(Numeric)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_name!(name)
|
|
65
|
+
unless name.is_a?(Symbol) || name.is_a?(String)
|
|
66
|
+
raise ArgumentError, "name must be a symbol or string"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if name.is_a?(String) && name.strip.empty?
|
|
70
|
+
raise ArgumentError, "name cannot be empty"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def normalize_name(name)
|
|
75
|
+
return nil if name.nil?
|
|
76
|
+
name = name.strip if name.is_a?(String)
|
|
77
|
+
name.to_sym
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module VerdictRules
|
|
2
|
+
class RuleBuilder
|
|
3
|
+
# Builder responsável por construir instâncias de Rule a partir da DSL
|
|
4
|
+
# Encapsula validações e evita que a Engine lide com estado parcial
|
|
5
|
+
# Usado internamente pelo Engine#rule e Engine#rules
|
|
6
|
+
# name: identificador opcional da regra
|
|
7
|
+
# priority: define a ordem de avaliação (maior primeiro)
|
|
8
|
+
def initialize(name: nil, priority: 0)
|
|
9
|
+
@name = name
|
|
10
|
+
@priority = priority
|
|
11
|
+
@condition = nil
|
|
12
|
+
@action = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Define a condição da regra
|
|
16
|
+
# O bloco a ser chamado posteriormente com o contexto da Engine
|
|
17
|
+
# Exige bloco para garantir previsibilidade do DSL
|
|
18
|
+
def when_condition(&block)
|
|
19
|
+
raise ArgumentError, "block required for when_condition" unless block_given?
|
|
20
|
+
|
|
21
|
+
@condition = block
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Define a ação executada quando a condição é satisfeita
|
|
25
|
+
# Aceita qualquer valor (symbol, string, hash, etc)
|
|
26
|
+
def then_action(value)
|
|
27
|
+
@action = value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Constrói e retorna uma instância de Rule
|
|
31
|
+
# Garante que a DSL foi definida corretamente antes da criação
|
|
32
|
+
# Falha cedo caso condition ou action estejam ausentes
|
|
33
|
+
def build
|
|
34
|
+
validate!
|
|
35
|
+
|
|
36
|
+
Rule.new(
|
|
37
|
+
name: @name,
|
|
38
|
+
condition: @condition,
|
|
39
|
+
action: @action,
|
|
40
|
+
priority: @priority
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def validate!
|
|
47
|
+
raise ArgumentError, "condition is required (use when_condition)" if @condition.nil?
|
|
48
|
+
raise ArgumentError, "action is required (use then_action)" if @action.nil?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "verdict_rules/version"
|
|
4
|
+
require_relative "verdict_rules/result"
|
|
5
|
+
require_relative "verdict_rules/rule"
|
|
6
|
+
require_relative "verdict_rules/rule_builder"
|
|
7
|
+
require_relative "verdict_rules/engine"
|
|
8
|
+
|
|
9
|
+
module VerdictRules
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: verdict_rules
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tibério dos Santos Ferreira
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: simplecov
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.22'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.22'
|
|
40
|
+
description: A gem to define and evaluate business rules with priorities and explainable
|
|
41
|
+
results.
|
|
42
|
+
email:
|
|
43
|
+
- tiberio.ferreiracs@gmail.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- CHANGELOG.md
|
|
49
|
+
- README.md
|
|
50
|
+
- lib/verdict_rules.rb
|
|
51
|
+
- lib/verdict_rules/engine.rb
|
|
52
|
+
- lib/verdict_rules/result.rb
|
|
53
|
+
- lib/verdict_rules/rule.rb
|
|
54
|
+
- lib/verdict_rules/rule_builder.rb
|
|
55
|
+
- lib/verdict_rules/version.rb
|
|
56
|
+
homepage: https://github.com/tibas-ce/verdict_rules
|
|
57
|
+
licenses:
|
|
58
|
+
- MIT
|
|
59
|
+
metadata:
|
|
60
|
+
homepage_uri: https://github.com/tibas-ce/verdict_rules
|
|
61
|
+
source_code_uri: https://github.com/tibas-ce/verdict_rules
|
|
62
|
+
changelog_uri: https://github.com/tibas-ce/verdict_rules/blob/main/CHANGELOG.md
|
|
63
|
+
bug_tracker_uri: https://github.com/tibas-ce/verdict_rules/issues
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: 3.0.0
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 3.6.9
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: A Ruby gem for defining and evaluating business rules with priorities.
|
|
82
|
+
test_files: []
|