Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 122 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,39 @@
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Custom Functions](#custom-functions)
- [Error Contract](#error-contract)
- [Configuration](#configuration)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [License](#license)

## Overview

Firefly Framework Rule Engine provides a powerful business rule evaluation system based on a custom YAML DSL. Rules are defined in YAML format and parsed into an Abstract Syntax Tree (AST) for efficient evaluation. The engine supports complex conditions, arithmetic operations, loop constructs, function calls, REST API calls, and JsonPath expressions.
Firefly Framework Rule Engine provides a business rule evaluation system based on a custom YAML DSL. Rules are defined in YAML and parsed into an Abstract Syntax Tree (AST) for efficient evaluation. The engine supports rich conditions, arithmetic, loop constructs, function calls, REST API calls, JsonPath expressions, and a pluggable function-registry extension point.

The project is structured as a multi-module build with five sub-modules: interfaces (DTOs and validation), models (database entities and repositories), core (DSL parser, evaluator, and services), SDK (client library), and web (REST controllers). It features Python code generation for rule compilation, batch evaluation, audit trail tracking, and caching for rule definitions.
The project is structured as a multi-module Maven build with five sub-modules: `interfaces` (DTOs and validation), `models` (R2DBC entities and repositories), `core` (DSL parser, evaluator, services, function registry), `web` (Spring WebFlux REST controllers), and `sdk` (generated client). The engine ships with Python code generation for offline rule execution, batch evaluation, audit-trail tracking, and a dedicated cache layer.

The YAML DSL supports variables, conditionals, loops (while, do-while, for-each), list operations, circuit breaker actions, and nested rule invocations, making it suitable for complex business rule scenarios such as credit scoring, eligibility checks, and pricing calculations.
The YAML DSL supports input/computed/constant variable tiers, 30+ comparison operators, logical composition (and/or/not), loops (`forEach`, `while`, `do-while`), inline conditionals (`if/then/else`), 70+ built-in functions (financial, date, string, list, validation, REST, JSON, type-conversion), and circuit-breaker actions for early termination.

## Features

- Custom YAML DSL with lexer, parser, and AST-based evaluation
- Condition types: comparison, logical (AND/OR), expression-based
- Action types: set, calculate, conditional, loops (while, do-while, for-each), function calls
- Expression types: arithmetic, binary, unary, literals, variables, JsonPath, REST calls
- Python code generation and compilation for rule optimization
- Batch rule evaluation for processing multiple inputs
- Rule definition CRUD with database persistence via R2DBC
- Constants management for shared rule variables
- Audit trail tracking for all rule evaluations
- YAML DSL validation with syntax and naming convention checks
- Caching for rule definitions and evaluation results
- REST API controllers for evaluation, definitions, constants, audit, and validation
- Reactive APIs using Project Reactor
- Custom YAML DSL with dedicated lexer + recursive-descent parser + visitor-based evaluator
- 30+ comparison operators including `between`, `in_list`, `matches`, `is_email`, `is_credit_score`, etc.
- Logical composition (`and`, `or`, `not`) with short-circuit evaluation
- Action types: `set`, `calculate`, `run`, `call`, arithmetic (`add`/`subtract`/`multiply`/`divide`), list ops (`append`/`prepend`/`remove`), `forEach`, `while`, `do-while`, `if/then/else`, `circuit_breaker`
- 70+ built-in functions covering math, string, date, list, financial, validation, REST, JSON path, and type conversion
- Pluggable function registry (`CustomFunctionRegistry`) — register your own `RuleFunction` beans and call them from rules
- Constants tier loaded from the database with TTL caching; auto-detection of `UPPER_CASE` references in the AST
- Reactive evaluation API on Project Reactor; synchronous visitor scheduled on `Schedulers.boundedElastic()` so it never blocks the Netty event loop
- Python code generation for offline rule execution
- Batch evaluation with bounded concurrency and per-request timeouts
- Rule-definition CRUD with R2DBC persistence and a cached AST
- Audit-trail tracking for every evaluation (correlated, PII-masked)
- YAML DSL validation: syntax, naming-convention, dependency, function-existence
- RFC 7807 problem-detail error responses; correlation IDs propagated across the chain
- Fail-fast error contract: malformed rules, unknown functions, type-coercion errors, and bad regexes surface as `success=false` with precise diagnostics rather than silently flipping to the else branch
- Spring WebFlux controllers; OpenAPI 3 / Swagger UI

## Requirements

Expand All @@ -61,70 +66,138 @@ The rule engine is a multi-module project. Include the modules you need:
<dependency>
<groupId>org.fireflyframework</groupId>
<artifactId>fireflyframework-rule-engine-core</artifactId>
<version>26.02.07</version>
<version>26.05.07</version>
</dependency>

<!-- DTOs and interfaces -->
<dependency>
<groupId>org.fireflyframework</groupId>
<artifactId>fireflyframework-rule-engine-interfaces</artifactId>
<version>26.02.07</version>
<version>26.05.07</version>
</dependency>

<!-- SDK for client integration -->
<dependency>
<groupId>org.fireflyframework</groupId>
<artifactId>fireflyframework-rule-engine-sdk</artifactId>
<version>26.02.07</version>
<version>26.05.07</version>
</dependency>
```

## Quick Start

### Naming conventions
The DSL is strict about variable naming so the engine can resolve names without ambiguity:

| Tier | Convention | Example |
| ---------------- | ------------ | ------------------------------------ |
| Input variables | `camelCase` | `creditScore`, `annualIncome` |
| Computed values | `snake_case` | `debt_to_income`, `risk_tier` |
| Database constants | `UPPER_CASE` | `MIN_CREDIT_SCORE`, `MAX_DTI` |

### Example rule (YAML DSL)

```yaml
# Example rule definition (YAML DSL)
name: credit-score-check
version: 1
name: "Credit Eligibility"
description: "Two-stage credit and income gate"
version: "1.0.0"

inputs:
- name: creditScore
type: number
- name: income
type: number

rules:
- name: evaluate-eligibility
conditions:
- field: creditScore
operator: ">="
value: 700
- field: income
operator: ">="
value: 50000
actions:
- set:
eligible: true
tier: "premium"
else:
- set:
eligible: false
tier: "standard"
creditScore: "number"
annualIncome: "number"

constants:
- code: MIN_CREDIT_SCORE
defaultValue: 700
- code: MIN_INCOME
defaultValue: 50000

when:
- creditScore at_least MIN_CREDIT_SCORE
- annualIncome at_least MIN_INCOME

then:
- calculate debt_to_income as 0 # placeholder; real rules would compute this
- set tier to if_else(creditScore at_least 800, "PRIME", "PREFERRED")
- set eligible to true

else:
- set tier to "STANDARD"
- set eligible to false

output:
eligible: eligible
tier: tier
```

### Calling the engine from Java

```java
@Service
public class CreditCheckService {

private final RulesEvaluationService evaluationService;

public Mono<RulesEvaluationResponseDTO> evaluate(Map<String, Object> inputs) {
RulesEvaluationRequestDTO request = new RulesEvaluationRequestDTO();
request.setRuleCode("credit-score-check");
request.setInputs(inputs);
return evaluationService.evaluate(request);
public Mono<RulesEvaluationResponseDTO> evaluate(Map<String, Object> inputData) {
RuleEvaluationByCodeRequestDTO request = RuleEvaluationByCodeRequestDTO.builder()
.ruleDefinitionCode("credit_eligibility_v1")
.inputData(inputData)
.build();
return evaluationService.evaluateRuleByCode(request, exchange);
}
}
```

The same evaluation is also reachable via REST: `POST /api/v1/rules/evaluate/direct` (Base64-encoded YAML), `/evaluate/plain` (raw YAML), or `/evaluate/by-code` (stored rule code).

## Custom Functions

You can extend the DSL with your own functions without modifying the engine:

```java
@Configuration
class MyRulesConfig {

@Bean
CommandLineRunner registerCustomFunctions(CustomFunctionRegistry registry) {
return args -> {
registry.register("regional_risk", a ->
Set.of("CA", "NY").contains(a[0]) ? 10 : 0);
registry.register("fraud_score", a ->
fraudService.score(String.valueOf(a[0])));
};
}
}
```

Then in a rule:

```yaml
then:
- run risk_bump as regional_risk(region)
- run fraud as fraud_score(applicantId)
```

Custom functions are checked **before** the built-in catalog (so they can shadow a built-in if you choose). Names are matched case-insensitively. The same function is callable from both expression contexts (`run`/`calculate`) and `call`-action contexts.

## Error Contract

The engine fails loud by design — errors are never silently swallowed:

| Situation | Result |
| -------------------------------------------- | --------------------------------------------------------------------------- |
| Unknown function name | `IllegalArgumentException` -> rule reports `success=false` with the name |
| Non-numeric string in arithmetic | `IllegalArgumentException` naming the operand |
| Bad regex pattern in `matches` | `IllegalArgumentException` naming the pattern |
| Missing bean property | `IllegalArgumentException` naming the class + property |
| Unknown `is_valid` validation type | `IllegalArgumentException` listing supported types |
| Action throws during execution | Rule reports `success=false` with action index + debug string + cause |
| Condition throws during evaluation | Rule reports `success=false` (does not silently flip to the else branch) |
| `circuit_breaker` action triggered | Rule reports `success=true` with `circuitBreakerTriggered=true` + message |
| REST function HTTP failure | Returns a structured `{success:false, error:true, message}` map (chain-friendly) |
| `getCachedAST` / cache read failure | Treated as cache miss (parse path); logged via `doOnError` |
| `set computed_var to null` (e.g., `json_get` missing path) | Variable is stored as null; the rule succeeds |

## Configuration

```yaml
Expand Down
41 changes: 31 additions & 10 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -1158,16 +1158,37 @@ The `RestCallServiceImpl` includes comprehensive URL validation before executing
- **Timing-safe comparison**: Hash verification uses `hmac.compare_digest()` to prevent timing attacks

### 4. Safe Code Evaluation
- **No unsafe reflection**: `ExpressionEvaluator.getPropertyValue()` uses public getter methods only; `field.setAccessible(true)` is not used
- **Division/modulo by zero**: `ExpressionEvaluator` and `ActionExecutor` throw `ArithmeticException` instead of returning null or silently failing
- **Short-circuit evaluation**: AND/OR operators use lazy evaluation to prevent unnecessary side effects
- **Thread-safe code generation**: `PythonCodeGenerator` uses `ThreadLocal` for mutable state to ensure safe concurrent compilation

### 5. Error Handling
- Graceful degradation for missing constants
- Circuit breaker pattern for external dependencies
- Comprehensive logging for audit trails
- Null-safe audit context extraction (defaults to "system" when no web exchange is available)
- **No unsafe reflection**: `ExpressionEvaluator.getPropertyValue()` uses public getter methods only (`getX` / `isX`); `field.setAccessible(true)` is never called. A missing getter throws `IllegalArgumentException` with the class + property name rather than silently returning null.
- **Division/modulo by zero**: `ExpressionEvaluator` and `ActionExecutor` throw `ArithmeticException` rather than returning null or silently failing.
- **Short-circuit evaluation**: AND/OR operators use lazy evaluation to prevent unnecessary side effects.
- **Thread-safe code generation**: `PythonCodeGenerator` uses `ThreadLocal` for mutable state to ensure safe concurrent compilation.

### 5. Error Handling (Fail-Loud Contract)

The engine is intentionally non-silent. Errors propagate to the rule's `success=false`
result with a precise diagnostic message rather than being swallowed and producing a
plausible-but-wrong output.

| Source of failure | Behaviour |
| ------------------------------------------ | ------------------------------------------------------------------------ |
| Unknown function name | `IllegalArgumentException` -> `success=false` |
| Non-numeric string in arithmetic | `IllegalArgumentException` naming the operand |
| Bad regex pattern in `matches` | `IllegalArgumentException` naming the pattern |
| Missing bean property | `IllegalArgumentException` naming class + property |
| Unknown `is_valid` validation type | `IllegalArgumentException` listing supported types |
| Unknown `dateadd`/`datediff` unit | `IllegalArgumentException` listing supported units |
| Action throws mid-execution | Rule reports `success=false` with action index + debug string + cause |
| Condition throws mid-evaluation | Rule reports `success=false` (does not silently flip to the else branch) |
| `circuit_breaker` action triggered | Rule reports `success=true` with `circuitBreakerTriggered=true` |
| Required constant missing in database | `success=false` listing the missing codes |
| REST function HTTP failure | Structured error map `{success:false, error:true, message:...}` (intentional chain-friendly contract; rules can branch on `response.success`) |
| Cache read failure | Treated as cache miss; logged via `doOnError` |

Surrounding mechanisms:
- Graceful degradation for missing **optional** constants (with explicit `defaultValue`).
- Circuit breaker pattern for external dependencies.
- Comprehensive audit trail (every evaluation logged with correlation ID).
- Null-safe audit context extraction (defaults to "system" when no web exchange is available).

### 6. Cache Integrity
- Cache invalidation on all CRUD operations (create, update, delete) for rule definitions
Expand Down
6 changes: 4 additions & 2 deletions docs/b2b-credit-scoring-tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ By the end of this tutorial, you'll understand:

Every Firefly rule follows a consistent structure. Let's start with the basic template:

<!-- doc-test:skip (structural template; placeholder values, not a complete parseable rule) -->
```yaml
# Required metadata
name: "Rule Name"
Expand Down Expand Up @@ -236,6 +237,7 @@ constants:

Our B2B credit scoring will use multiple sequential rules to build a comprehensive assessment. This approach mirrors real-world credit evaluation processes where different aspects are analyzed in stages.

<!-- doc-test:skip (TODO: legacy walkthrough uses C-style ternary `? :` and string-concatenation with colons; rewrite with `if_else()` and quoted strings before re-enabling) -->
```yaml
# Multi-stage evaluation using sequential rules
rules:
Expand Down Expand Up @@ -276,8 +278,8 @@ rules:
)

# Calculate data quality indicators
- calculate revenue_variance as abs(annualRevenue - verifiedAnnualRevenue) / verifiedAnnualRevenue
- calculate deposit_variance as abs(avgMonthlyDeposits - monthlyRevenue) / monthlyRevenue
- run revenue_variance as abs(annualRevenue - verifiedAnnualRevenue) / verifiedAnnualRevenue
- run deposit_variance as abs(avgMonthlyDeposits - monthlyRevenue) / monthlyRevenue

# Overall data completeness and quality check
- set data_validation_complete to (
Expand Down
6 changes: 4 additions & 2 deletions docs/common-patterns-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ output:

**Use Case**: Validating input data before processing

<!-- doc-test:skip (TODO: example uses unquoted error-message strings with embedded colons that break YAML parsing; rewrite messages without colons or wrap action lines in YAML quotes before re-enabling the guard) -->
```yaml
name: "Application Data Validation"
description: "Validate customer application data"
Expand Down Expand Up @@ -407,6 +408,7 @@ output:

**Use Case**: Calculating risk scores from multiple factors

<!-- doc-test:skip (TODO: example uses C-style ternary `? :` which is not in the DSL; rewrite with the `if_else()` function before re-enabling the guard) -->
```yaml
name: "Credit Risk Assessment"
description: "Calculate risk score from multiple financial factors"
Expand Down Expand Up @@ -550,7 +552,7 @@ rules:

- set stage to "COMPLETED"
- set final_decision_value to decision
- calculate processed_at as now()
- run processed_at as now()
- set processing_complete to true
else:
- set decision to "REJECTED"
Expand Down Expand Up @@ -630,7 +632,7 @@ then:
# Store metadata
- set enrichment_sources to sources
- set enrichment_quality to data_quality
- calculate enrichment_timestamp as now()
- run enrichment_timestamp as now()
- set enrichment_customer_id to customerId

else:
Expand Down
Loading
Loading