Skip to content

Commit d18ff9e

Browse files
authored
Add substring() function to stdlib and improve logging (#21)
* Remove warning log * Add substring function to stdlib, use stdlib in CLI
1 parent f08ada9 commit d18ff9e

File tree

16 files changed

+379
-28
lines changed

16 files changed

+379
-28
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
- Minor removed warning level logging in cel crate
1011

1112
## [0.5.2] - 2025-09-12
1213

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cel"
3-
version = "0.5.2"
3+
version = "0.5.3"
44
edition = "2021"
55

66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

docs/getting-started/quick-start.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,8 @@ assert result == 6 # → 6 (unsigned integers convert to regular int)
261261
result = evaluate('"hello world".size()')
262262
assert result == 11 # → 11 (string length via size() method)
263263

264-
result = evaluate('"hello"[1]')
265-
assert result == "e" # → "e" (zero-indexed string character access)
264+
# Note: String indexing like "hello"[1] is not supported in CEL
265+
# Use string methods instead: startsWith(), endsWith(), contains(), matches()
266266

267267
result = evaluate('"test".startsWith("te")')
268268
assert result == True # → True (rich string method support)

docs/how-to-guides/error-handling.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ try:
1717
evaluate("1 + + 2") # Invalid syntax
1818
assert False, "Expected ValueError"
1919
except ValueError as e:
20-
assert "Failed to compile expression" in str(e)
21-
# → ValueError: Failed to compile expression (graceful failure)
20+
assert "Failed to parse expression" in str(e)
21+
# → ValueError: Failed to parse expression (graceful failure)
2222

2323
try:
2424
evaluate("") # Empty expression
2525
assert False, "Expected ValueError"
2626
except ValueError as e:
27-
assert "Invalid syntax" in str(e) or "malformed" in str(e)
28-
# → ValueError: Invalid syntax or malformed (safe error handling)
27+
assert "Failed to parse expression" in str(e)
28+
# → ValueError: Failed to parse expression (safe error handling)
2929
```
3030

3131
### `RuntimeError` - Variable and Function Errors
@@ -99,14 +99,14 @@ try:
9999
evaluate("'unclosed quote", {})
100100
assert False, "Should have raised ValueError"
101101
except ValueError as e:
102-
assert "Invalid syntax or malformed string" in str(e)
102+
assert "Failed to parse expression" in str(e)
103103
# → ValueError: Malformed input handled safely (no crash)
104104

105105
try:
106106
evaluate('"mixed quotes\'', {})
107107
assert False, "Should have raised ValueError"
108108
except ValueError as e:
109-
assert "Invalid syntax or malformed string" in str(e)
109+
assert "Failed to parse expression" in str(e)
110110
# → ValueError: Quote mismatch detected (process remains stable)
111111
```
112112

@@ -339,7 +339,7 @@ invalid_syntax = 'user.age >=' # Incomplete comparison
339339
success, result, errors = safe_user_expression_eval(invalid_syntax, context)
340340
assert success == False, "Invalid syntax should be rejected"
341341
assert len(errors) > 0, "Should report syntax errors"
342-
# → False, errors: ['Evaluation error: Failed to compile'] (malformed input caught)
342+
# → False, errors: ['Evaluation error: Failed to parse'] (malformed input caught)
343343

344344
# Test 4: Empty expression
345345
success, result, errors = safe_user_expression_eval('', context)

docs/reference/cel-compliance.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ This implementation correctly follows the CEL specification where maps can have
9292

9393
#### Other Operators
9494
- `?:` (ternary conditional) - Conditional expressions
95-
- `[]` (indexing) - Lists, maps, strings
95+
- `[]` (indexing) - Lists and maps only (string indexing not supported)
9696
- `.` (member access) - Object property access
9797

9898
### ✅ Built-in Functions
@@ -119,9 +119,12 @@ This implementation correctly follows the CEL specification where maps can have
119119
- **endsWith()**: `"hello".endsWith("lo")``True`
120120
- **matches()**: `"hello world".matches(".*world")``True`
121121
- **String concatenation**: `"hello" + " world"``"hello world"`
122-
- **String indexing**: `"hello"[1]``"e"`
123122
- **String size**: `size("hello")``5`
124123

124+
#### ❌ String Indexing Not Supported
125+
- **String indexing**: `"hello"[1]` is **NOT** supported (returns "No such key" error)
126+
- **Workaround**: Use `substring()` function (when available) or Python context functions
127+
125128
### ✅ Collection Macros
126129
- **all()**: `[1,2,3].all(x, x > 0)``True`
127130
- **exists()**: `[1,2,3].exists(x, x == 2)``True`
@@ -148,13 +151,39 @@ This section focuses on what you need to know to use CEL effectively in your app
148151
### 🔧 Safe Patterns & Workarounds
149152

150153
#### String Processing Workarounds
154+
155+
**Using cel.stdlib (Recommended)**
156+
157+
This library provides Python implementations of missing CEL functions:
158+
159+
```python
160+
from cel import Context, evaluate
161+
from cel.stdlib import add_stdlib_to_context
162+
163+
# Add all standard library functions at once
164+
context = Context()
165+
add_stdlib_to_context(context)
166+
167+
# substring() is now available as a function (not a method)
168+
result = evaluate('substring("hello world", 0, 5)', context) # → "hello"
169+
result = evaluate('substring("hello world", 6)', context) # → "world"
170+
171+
# Note: Use function syntax, not method syntax
172+
# ✅ substring("hello", 2, 4) - correct
173+
# ❌ "hello".substring(2, 4) - not supported
174+
```
175+
176+
**Using Custom Python Functions**
177+
178+
You can also add your own custom functions:
179+
151180
```python
152181
from cel import Context, evaluate
153182

154-
# Since lowerAscii(), upperAscii(), indexOf() are missing:
183+
# Add custom functions for missing CEL features
155184
context = Context()
156185
context.add_function("lower", str.lower)
157-
context.add_function("upper", str.upper)
186+
context.add_function("upper", str.upper)
158187
context.add_function("find", str.find)
159188

160189
# Add variables to the context

docs/reference/cli-reference.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,22 @@ cel --version
1515

1616
The `cel` command-line tool provides a convenient way to evaluate CEL expressions from the command line, in scripts, or interactively. It supports context loading, file processing, and various output formats.
1717

18+
## Standard Library Functions
19+
20+
The CLI automatically includes all [standard library functions](../reference/cel-compliance.md#using-celstdlib-recommended) from `cel.stdlib`. These functions are available without any additional setup:
21+
22+
### Available Functions
23+
24+
- **`substring(str, start, end?)`** - Extract substring from a string
25+
```bash
26+
cel 'substring("hello world", 0, 5)' # → hello
27+
cel 'substring("hello world", 6)' # → world
28+
```
29+
30+
**Note**: Use function syntax `substring("text", 0, 5)`, not method syntax `"text".substring(0, 5)`.
31+
32+
For programmatic use (Python API), import and use `cel.stdlib.add_stdlib_to_context()` to add these functions to your context.
33+
1834
## Options
1935

2036
### Global Options

docs/reference/python-api.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,18 +208,18 @@ from cel import evaluate
208208
# Invalid syntax raises ValueError
209209
try:
210210
evaluate("1 + + 2") # Invalid syntax
211-
# → ValueError: Failed to compile expression: ...
211+
# → ValueError: Failed to parse expression: ...
212212
assert False, "Should have raised ValueError"
213213
except ValueError as e:
214-
assert "Failed to compile expression" in str(e)
214+
assert "Failed to parse expression" in str(e)
215215

216216
# Empty expression raises ValueError
217217
try:
218218
evaluate("")
219-
# → ValueError: Invalid syntax or malformed expression
219+
# → ValueError: Failed to parse expression
220220
assert False, "Should have raised ValueError"
221221
except ValueError as e:
222-
assert "Invalid syntax" in str(e) or "malformed" in str(e)
222+
assert "Failed to parse expression" in str(e)
223223
```
224224

225225
#### `RuntimeError` - Variable and Function Errors

python/cel/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Import the Rust extension
22

3-
from . import cli
3+
from . import cli, stdlib
44
from .cel import *

python/cel/cli.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
# Import directly from relative modules to avoid circular imports
4141
from .cel import Context, evaluate
42+
from .stdlib import add_stdlib_to_context
4243

4344
# Initialize Rich console
4445
console = Console()
@@ -74,7 +75,7 @@ class CELLexer(RegexLexer):
7475
# Built-in functions
7576
(
7677
r"\b(size|has|timestamp|duration|int|uint|double|string|bytes|"
77-
r"startsWith|endsWith|contains|matches)\b(?=\()",
78+
r"startsWith|endsWith|contains|matches|substring)\b(?=\()",
7879
token.Name.Function,
7980
),
8081
# String literals
@@ -192,7 +193,10 @@ def _update_cel_context(self):
192193
if self.context:
193194
self._cel_context = Context(self.context)
194195
else:
195-
self._cel_context = None
196+
self._cel_context = Context()
197+
198+
# Always add stdlib functions to the context
199+
add_stdlib_to_context(self._cel_context)
196200

197201
def evaluate(self, expression: str) -> Any:
198202
"""Evaluate a CEL expression."""
@@ -241,6 +245,7 @@ def __init__(self, evaluator: CELEvaluator, history_limit: int = 10):
241245
"double",
242246
"string",
243247
"bytes",
248+
"substring", # stdlib function
244249
]
245250

246251
# Command dispatch dictionary for cleaner organization

0 commit comments

Comments
 (0)