Skip to content

Commit 96eaca5

Browse files
authored
Changes custom functions from registry to option (#17)
* Changes custom functions from registry to option * Adds CHANGELOG entry * Updates documentation
1 parent c8ce51d commit 96eaca5

15 files changed

+431
-2236
lines changed

CHANGELOG.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,59 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.0.0] - 2025-08-??
9+
10+
### Changed
11+
12+
#### Custom Function Architecture Overhaul
13+
- **Breaking Change**: Removed global function registry system in favor of evaluation-time function parameters
14+
- **New API**: Custom functions now passed via `functions:` option in `Predicator.evaluate/3` calls
15+
- **Function Format**: Custom functions use `%{name => {arity, function}}` format where function takes `[args], context` and returns `{:ok, result}` or `{:error, message}`
16+
- **Thread Safety**: Eliminated global state for improved concurrency and thread safety
17+
- **Function Merging**: SystemFunctions always available with custom functions merged in, allowing overrides
18+
- **Simplified Startup**: No application-level function registry initialization required
19+
20+
#### Examples
21+
```elixir
22+
# Old registry-based approach (removed)
23+
Predicator.register_function("double", 1, fn [n], _context -> {:ok, n * 2} end)
24+
Predicator.evaluate("double(21)", %{})
25+
26+
# New evaluation-time approach
27+
custom_functions = %{"double" => {1, fn [n], _context -> {:ok, n * 2} end}}
28+
Predicator.evaluate("double(21)", %{}, functions: custom_functions)
29+
30+
# Custom functions can override built-ins
31+
custom_len = %{"len" => {1, fn [_], _context -> {:ok, "custom_result"} end}}
32+
Predicator.evaluate("len('anything')", %{}, functions: custom_len) # {:ok, "custom_result"}
33+
```
34+
35+
#### Removed APIs
36+
- `Predicator.register_function/3` - Use `functions:` option instead
37+
- `Predicator.clear_custom_functions/0` - No longer needed
38+
- `Predicator.list_custom_functions/0` - No longer needed
39+
- `Predicator.Functions.Registry` module - Entire registry system removed
40+
41+
#### Migration Guide
42+
1. **Replace registry calls**: Convert `register_function` calls to function maps passed to `evaluate/3`
43+
2. **Update function definitions**: Ensure functions return `{:ok, result}` or `{:error, message}`
44+
3. **Remove initialization code**: Delete any registry setup from application startup
45+
4. **Update tests**: Replace registry-based setup with evaluation-time function passing
46+
47+
#### Technical Implementation
48+
- **Evaluator Enhancement**: Modified to accept `:functions` option and merge with system functions
49+
- **SystemFunctions Refactor**: Added `all_functions/0` to provide system functions in evaluator format
50+
- **Clean Architecture**: Removed ETS-based global registry and associated complexity
51+
- **Backward Compatibility**: `evaluate/2` functions continue to work unchanged for expressions without custom functions
52+
53+
### Security
54+
- **Improved Isolation**: Custom functions scoped to individual evaluation calls
55+
- **No Global State**: Eliminates potential race conditions and global state mutations
56+
57+
### Performance
58+
- **Reduced Overhead**: No ETS lookups or global registry management
59+
- **Better Concurrency**: Thread-safe by design with no shared state
60+
861
## [1.1.0] - 2025-08-20
962

1063
### Added

CLAUDE.md

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ list → "[" ( expression ( "," expression )* )? "]"
3737
- **StringVisitor**: Converts AST back to strings
3838
- **InstructionsVisitor**: Converts AST to executable instructions
3939
- **Functions** (`lib/predicator/functions/`): Function system components
40-
- **SystemFunctions**: Built-in system functions (len, upper, abs, max, etc.)
41-
- **Registry**: Custom function registration and dispatch
40+
- **SystemFunctions**: Built-in system functions (len, upper, abs, max, etc.) provided via `all_functions/0`
4241
- **Main API** (`lib/predicator.ex`): Public interface with convenience functions
4342

4443
## Development Commands
@@ -98,13 +97,12 @@ lib/predicator/
9897
├── lexer.ex # Tokenization with position tracking
9998
├── parser.ex # Recursive descent parser
10099
├── compiler.ex # AST to instructions conversion
101-
├── evaluator.ex # Instruction execution engine
100+
├── evaluator.ex # Instruction execution engine with custom function support
102101
├── visitor.ex # Visitor behavior definition
103102
├── types.ex # Type specifications
104-
├── application.ex # OTP application
103+
├── application.ex # OTP application (simplified - no registry init)
105104
├── functions/ # Function system components
106-
│ ├── system_functions.ex # Built-in functions (len, upper, abs, etc.)
107-
│ └── registry.ex # Function registration and dispatch
105+
│ └── system_functions.ex # Built-in functions (len, upper, abs, etc.)
108106
└── visitors/ # AST transformation modules
109107
├── string_visitor.ex # AST to string decompilation
110108
└── instructions_visitor.ex # AST to instructions conversion
@@ -115,28 +113,33 @@ test/predicator/
115113
├── compiler_test.exs
116114
├── evaluator_test.exs
117115
├── predicator_test.exs # Integration tests
118-
├── functions/ # Function system tests
119-
│ ├── system_functions_test.exs
120-
│ └── registry_test.exs
121116
└── visitors/ # Visitor tests
122117
├── string_visitor_test.exs
123118
└── instructions_visitor_test.exs
124119
```
125120

126121
## Recent Additions (2025)
127122

128-
### Function Call System
129-
- **Built-in Functions**: System functions automatically available
123+
### Function System (v2.0.0 - Architecture Overhaul)
124+
- **Built-in Functions**: System functions automatically available in all evaluations
130125
- **String functions**: `len(string)`, `upper(string)`, `lower(string)`, `trim(string)`
131126
- **Numeric functions**: `abs(number)`, `max(a, b)`, `min(a, b)`
132127
- **Date functions**: `year(date)`, `month(date)`, `day(date)`
133-
- **Custom Functions**: Register anonymous functions with `Predicator.register_function/3`
134-
- **Function Registry**: ETS-based registry with arity validation and error handling
128+
- **Custom Functions**: Provided per evaluation via `functions:` option in `evaluate/3`
129+
- **Function Format**: `%{name => {arity, function}}` where function takes `[args], context` and returns `{:ok, result}` or `{:error, message}`
130+
- **Function Merging**: Custom functions merged with system functions, allowing overrides
131+
- **Thread Safety**: No global state - functions scoped to individual evaluation calls
135132
- **Examples**:
136-
- `len(name) > 5`
137-
- `upper(status) = "ACTIVE"`
138-
- `year(created_date) = 2024`
139-
- `max(score1, score2) > 85`
133+
```elixir
134+
custom_functions = %{
135+
"double" => {1, fn [n], _context -> {:ok, n * 2} end},
136+
"len" => {1, fn [_], _context -> {:ok, "custom_override"} end} # Override built-in
137+
}
138+
139+
Predicator.evaluate("double(score) > 100", %{"score" => 60}, functions: custom_functions)
140+
Predicator.evaluate("len('anything')", %{}, functions: custom_functions) # Uses override
141+
Predicator.evaluate("len('hello')", %{}) # Uses built-in (returns 5)
142+
```
140143

141144
### Date and DateTime Support
142145
- **Syntax**: `#2024-01-15#` (date), `#2024-01-15T10:30:00Z#` (datetime)
@@ -168,6 +171,20 @@ test/predicator/
168171
- `user.settings.theme = "dark" AND user.profile.active`
169172
- **Backwards Compatible**: Simple variable names work exactly as before
170173

174+
## Breaking Changes
175+
176+
### v2.0.0 - Custom Function Architecture Overhaul
177+
- **Removed**: Global function registry system (`Predicator.Functions.Registry` module)
178+
- **Removed**: `Predicator.register_function/3`, `Predicator.clear_custom_functions/0`, `Predicator.list_custom_functions/0`
179+
- **Changed**: Custom functions now passed via `functions:` option in `evaluate/3` calls instead of global registration
180+
- **Benefit**: Thread-safe, no global state, per-evaluation function scoping
181+
- **Migration**: Replace registry calls with function maps passed to `evaluate/3`
182+
183+
### v1.1.0 - Nested Access Parsing
184+
- **Changed**: Variables containing dots (e.g., `"user.email"`) now parsed as nested access paths
185+
- **Impact**: Context keys like `"user.profile.name"` will no longer match identifier `user.profile.name`
186+
- **Solution**: Use proper nested data structures instead of flat keys with dots
187+
171188
## Common Tasks
172189

173190
### Adding New Operators
@@ -200,8 +217,7 @@ test/predicator/
200217
- **Property Testing**: Comprehensive input validation
201218
- **Error Path Testing**: All error conditions covered
202219
- **Round-trip Testing**: AST → String → AST consistency
203-
- **Current Test Count**: 428 tests (64 doctests + 364 regular tests)
204-
- **Coverage**: 92.6% overall, 100% on critical components
220+
- **Current Test Count**: 569 tests (65 doctests + 504 regular tests)
205221

206222
## Code Standards
207223

@@ -230,4 +246,4 @@ test/predicator/
230246
### Development Environment
231247
- Elixir ~> 1.11 required
232248
- All dependencies in development/test only
233-
- No runtime dependencies for core functionality
249+
- No runtime dependencies for core functionality

README.md

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,8 @@ iex> Predicator.evaluate("'coding' in user.hobbies", list_context)
233233
Predicator uses a multi-stage compilation pipeline:
234234

235235
```
236-
Expression String Lexer → Parser → Compiler → Evaluator
237-
↓ ↓ ↓
236+
Expression String Lexer → Parser → Compiler → Evaluator
237+
↓ ↓ ↓
238238
'score > 85 OR admin' → Tokens → AST → Instructions → Result
239239
```
240240

@@ -278,38 +278,60 @@ iex> Predicator.evaluate("score AND", %{})
278278

279279
## Advanced Usage
280280

281-
### Custom Function Registration
281+
### Custom Functions
282282

283-
You can register your own custom functions for use in expressions:
283+
You can provide custom functions when evaluating expressions using the `functions:` option:
284284

285285
```elixir
286-
# Register a simple function
287-
Predicator.register_function("double", 1, fn [n], _context ->
288-
{:ok, n * 2}
289-
end)
286+
# Define custom functions in a map
287+
custom_functions = %{
288+
"double" => {1, fn [n], _context -> {:ok, n * 2} end},
289+
"user_role" => {0, fn [], context ->
290+
{:ok, Map.get(context, "current_user_role", "guest")}
291+
end},
292+
"divide" => {2, fn [a, b], _context ->
293+
if b == 0 do
294+
{:error, "Division by zero"}
295+
else
296+
{:ok, a / b}
297+
end
298+
end}
299+
}
290300

291-
# Use in expressions
292-
iex> Predicator.evaluate("double(score) > 100", %{"score" => 60})
301+
# Use custom functions in expressions
302+
iex> Predicator.evaluate("double(score) > 100", %{"score" => 60}, functions: custom_functions)
293303
{:ok, true}
294304

295-
# Context-aware function
296-
Predicator.register_function("user_role", 0, fn [], context ->
297-
{:ok, Map.get(context, "current_user_role", "guest")}
298-
end)
305+
iex> Predicator.evaluate("user_role() = 'admin'", %{"current_user_role" => "admin"}, functions: custom_functions)
306+
{:ok, true}
299307

300-
iex> Predicator.evaluate("user_role() = 'admin'", %{"current_user_role" => "admin"})
308+
iex> Predicator.evaluate("divide(10, 2) = 5", %{}, functions: custom_functions)
301309
{:ok, true}
302310

303-
# Function with error handling
304-
Predicator.register_function("divide", 2, fn [a, b], _context ->
305-
if b == 0 do
306-
{:error, "Division by zero"}
307-
else
308-
{:ok, a / b}
309-
end
310-
end)
311+
iex> Predicator.evaluate("divide(10, 0)", %{}, functions: custom_functions)
312+
{:error, "Division by zero"}
313+
314+
# Custom functions can override built-in functions
315+
override_functions = %{
316+
"len" => {1, fn [_], _context -> {:ok, "custom_result"} end}
317+
}
318+
319+
iex> Predicator.evaluate("len('anything')", %{}, functions: override_functions)
320+
{:ok, "custom_result"}
321+
322+
# Without custom functions, built-ins work as expected
323+
iex> Predicator.evaluate("len('hello')", %{})
324+
{:ok, 5}
311325
```
312326

327+
#### Function Format
328+
329+
Custom functions must follow this format:
330+
- **Map Key**: Function name (string)
331+
- **Map Value**: `{arity, function}` tuple where:
332+
- `arity`: Number of arguments the function expects (integer)
333+
- `function`: Anonymous function that takes `[args], context` and returns `{:ok, result}` or `{:error, message}`
334+
313335
### String Formatting Options
314336

315337
The StringVisitor supports multiple formatting modes:

0 commit comments

Comments
 (0)