cel-rs-rb 0.1.0 → 0.3.1
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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +117 -6
- data/ext/cel/Cargo.toml +1 -1
- data/ext/cel/src/lib.rs +53 -16
- data/lib/cel/version.rb +1 -1
- data/lib/cel.rb +12 -0
- data/spec/cel_spec.rb +174 -9
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4649bfaf71cdf145a7cce4befa9b00e6e74d9a5d4463253a0bfee3ad13e516fa
|
|
4
|
+
data.tar.gz: '0940f3b1af9507a22f77c7471c3d96a045afe2c86bd977617d45eae902c8f570'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94a4d0a7ad34e64a5b4afbfc6895b5432df83b0397af2d985a1d589fff8790efe23a6abd6fadebe2c9955d039553cdd4f48ad52e9785f6513d8b025d64a53dcf
|
|
7
|
+
data.tar.gz: f17cc32784ddf733bd690b3f688a04a8586f1ed9a23e281be88c4432d5052fa0691318cf9d389b0678f9e8e6201390075afeee8ae061dbc2aee749d4ef622fd0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Keep registered Ruby callback Procs and their captured values alive while their
|
|
8
|
+
`CEL::Context` is reachable. Prevents use-after-free after the caller drops its
|
|
9
|
+
own reference and Ruby runs garbage collection. Callbacks are also pinned during
|
|
10
|
+
compaction, including when collection runs inside a callback.
|
data/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# cel-rs-rb
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://rubygems.org/gems/cel-rs-rb)
|
|
4
|
+
|
|
5
|
+
Ruby bindings for the Rust [`cel`](https://crates.io/crates/cel) 0.15 crate, implemented with [Magnus](https://github.com/matsadler/magnus).
|
|
4
6
|
|
|
5
7
|
## Goals
|
|
6
8
|
|
|
@@ -30,10 +32,69 @@ program.execute(context)
|
|
|
30
32
|
# => 60
|
|
31
33
|
```
|
|
32
34
|
|
|
35
|
+
### Optional values
|
|
36
|
+
|
|
37
|
+
Use optional access to handle missing fields, and `orValue` to supply a default:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
context = CEL::Context.build(user: {})
|
|
41
|
+
CEL.compile("user.?name.orValue('unknown')").execute(context)
|
|
42
|
+
# => "unknown"
|
|
43
|
+
|
|
44
|
+
context = CEL::Context.build(user: {"name" => "Ada"})
|
|
45
|
+
CEL.compile("user.?name.optMap(n, n.size()).orValue(0)").execute(context)
|
|
46
|
+
# => 3
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Unwrap optional values inside CEL with `orValue` or `value()` before returning
|
|
50
|
+
them to Ruby. Returning an optional directly raises `CEL::TypeError`; it is not
|
|
51
|
+
automatically converted to its contained value or `nil`.
|
|
52
|
+
|
|
53
|
+
### `max` and `min`
|
|
54
|
+
|
|
55
|
+
Default contexts include `max` and `min`. They take a list or positional arguments and
|
|
56
|
+
return `nil` when given no values:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
CEL.compile("max(2, 9, -3)").execute # => 9
|
|
60
|
+
CEL.compile("min([2, 9, -3])").execute # => -3
|
|
61
|
+
CEL.compile("max([])").execute # => nil
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Extension libraries
|
|
65
|
+
|
|
66
|
+
Opt into upstream CEL extension libraries when compiling a program:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
program = CEL.compile(
|
|
70
|
+
"[label.lowerAscii(), scores.sort(), math.greatest(scores), base64.encode(b'hi')]",
|
|
71
|
+
extensions: [:strings, :lists, :math, :encoders]
|
|
72
|
+
)
|
|
73
|
+
program.execute(CEL::Context.build(label: "TacoCat", scores: [2, 9, -3]))
|
|
74
|
+
# => ["tacocat", [-3, 2, 9], 9, "aGk="]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Library | Examples |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| [`strings`](https://docs.rs/cel/0.15.0/cel/extensions/fn.strings.html) | `'TacoCat'.lowerAscii()`, `'a,b'.split(',')` |
|
|
80
|
+
| [`lists`](https://docs.rs/cel/0.15.0/cel/extensions/fn.lists.html) | `lists.range(3)`, `[3, 1, 2].sort()`, `items.sortBy(x, x.score)` |
|
|
81
|
+
| [`math`](https://docs.rs/cel/0.15.0/cel/extensions/fn.math.html) | `math.greatest(1, 5, 3)`, `math.least([1, 5, 3])` |
|
|
82
|
+
| [`encoders`](https://docs.rs/cel/0.15.0/cel/extensions/fn.encoders.html) | `base64.encode(b'hi')`, `base64.decode('aGk=')` |
|
|
83
|
+
|
|
84
|
+
`CEL::Program.compile` accepts the same `extensions:` option. Names can be
|
|
85
|
+
symbols or strings; unknown or repeated names raise `ArgumentError`.
|
|
86
|
+
|
|
87
|
+
Extensions are disabled by default and registered per program, for both macro
|
|
88
|
+
expansion and execution. They work with bare `execute` or a default context,
|
|
89
|
+
including its variables and Ruby callbacks. They do not change other programs
|
|
90
|
+
or add functions to explicitly empty contexts. Ruby callbacks that conflict
|
|
91
|
+
with enabled extension functions raise `CEL::ExecutionError` during execution,
|
|
92
|
+
just like conflicts with standard CEL builtins.
|
|
93
|
+
|
|
33
94
|
### API mapping
|
|
34
95
|
|
|
35
|
-
- `CEL.compile(source)` -> `CEL::Program`
|
|
36
|
-
- `CEL::Program.compile(source)` -> `CEL::Program`
|
|
96
|
+
- `CEL.compile(source, extensions: [])` -> `CEL::Program`
|
|
97
|
+
- `CEL::Program.compile(source, extensions: [])` -> `CEL::Program`
|
|
37
98
|
- `CEL::Program#execute(context = nil)` -> Ruby value
|
|
38
99
|
- `CEL::Program#references` -> `{ "variables" => [...], "functions" => [...] }`
|
|
39
100
|
- `CEL::Context.new(empty = false)` -> context with builtins (`false`) or empty (`true`)
|
|
@@ -75,9 +136,14 @@ program.execute(context)
|
|
|
75
136
|
|
|
76
137
|
## Thread safety and concurrency
|
|
77
138
|
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
-
|
|
139
|
+
- Compiled `CEL::Program` instances can be shared and executed concurrently from
|
|
140
|
+
multiple Ruby threads with separate execution contexts.
|
|
141
|
+
- Each thread may pass its own `CEL::Context`; context data is mutex-protected
|
|
142
|
+
and copied into an immutable execution snapshot for each run.
|
|
143
|
+
- CEL evaluation runs with the Ruby GVL released, so other Ruby threads can keep
|
|
144
|
+
progressing while a program executes.
|
|
145
|
+
- Ruby callbacks from CEL functions temporarily re-acquire the GVL before
|
|
146
|
+
invoking Ruby code.
|
|
81
147
|
|
|
82
148
|
## Development
|
|
83
149
|
|
|
@@ -105,7 +171,52 @@ This runs:
|
|
|
105
171
|
|
|
106
172
|
CI also runs the test suite on Ruby 3.3, Ruby 3.4, and Ruby 4.0 through Buildkite.
|
|
107
173
|
|
|
174
|
+
## Releasing
|
|
175
|
+
|
|
176
|
+
See [CHANGELOG.md](CHANGELOG.md) for release notes. Update it alongside each version bump.
|
|
177
|
+
|
|
178
|
+
Releases publish a generic `ruby` platform gem and **precompiled native gems**
|
|
179
|
+
for six platforms (`x86_64-linux`, `aarch64-linux`, `x86_64-linux-musl`,
|
|
180
|
+
`aarch64-linux-musl`, `arm64-darwin`, `x86_64-darwin`) to
|
|
181
|
+
[rubygems.org](https://rubygems.org/gems/cel-rs-rb). The generic gem keeps
|
|
182
|
+
RubyGems' latest-version metadata aligned; supported platforms should still
|
|
183
|
+
resolve to their matching precompiled native gem.
|
|
184
|
+
|
|
185
|
+
### Cutting a release
|
|
186
|
+
|
|
187
|
+
The gem version comes from `lib/cel/version.rb`, not from the tag, so bump the
|
|
188
|
+
file and tag the same version:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# 1. Bump CEL::VERSION in lib/cel/version.rb (e.g. "0.2.0")
|
|
192
|
+
# 2. Refresh Gemfile.lock's local gem version
|
|
193
|
+
bundle install
|
|
194
|
+
|
|
195
|
+
# 3. Commit the bump
|
|
196
|
+
git add lib/cel/version.rb Gemfile.lock
|
|
197
|
+
git commit -m "Release v0.2.0"
|
|
198
|
+
|
|
199
|
+
# 4. Tag that commit (the v-prefix triggers the release pipeline)
|
|
200
|
+
git tag v0.2.0
|
|
201
|
+
git push origin main
|
|
202
|
+
git push origin v0.2.0
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`Gemfile.lock` includes a local `cel-rs-rb` path entry, so commit its version
|
|
206
|
+
change alongside `lib/cel/version.rb`.
|
|
207
|
+
|
|
208
|
+
The `release:verify-tag` step fails the build if the tag and
|
|
209
|
+
`CEL::VERSION` disagree, so the tag (`v0.2.0`) must match `version.rb`
|
|
210
|
+
(`0.2.0`).
|
|
211
|
+
|
|
108
212
|
## Compatibility notes
|
|
109
213
|
|
|
214
|
+
- Compilation uses the standard CEL environment, including optional access syntax (`user.?name`) and the `optMap` / `optFlatMap` macros.
|
|
215
|
+
- The upstream strings, lists, math, and encoders extension libraries are opt-in through the `extensions:` compile option.
|
|
216
|
+
- cel 0.15 no longer includes `max` and `min` by default, so default contexts register them for compatibility with cel 0.13. See [`max` and `min`](#max-and-min).
|
|
217
|
+
- When upgrading from cel 0.13, replace list/map `.contains(value)` expressions with `value in collection`; `.contains()` now only accepts strings. Map literals with duplicate keys raise `CEL::ExecutionError`, and string `size()` counts Unicode code points rather than bytes. See the [cel 0.15 release notes](https://github.com/cel-rust/cel-rust/releases/tag/v0.15.0) for further CEL semantic changes.
|
|
218
|
+
- `string(bytes)` now rejects invalid UTF-8. Numeric conversions reject out-of-range results, and parsed timestamps and durations must fit CEL's allowed ranges.
|
|
219
|
+
- `all()` and `exists()` can now absorb an earlier evaluation error when a later element determines the result. For example, `[2, 1].exists(e, 6 / (2 - e) == 6)` returns `true` despite the first element's division by zero.
|
|
220
|
+
- Ruby callbacks cannot shadow standard CEL builtins (such as `size`) in a default context; the compatibility functions `max` and `min` are exceptions. Conflicts raise `CEL::ExecutionError` when `Program#execute(context)` builds the execution snapshot, even if the expression does not call the conflicting function. Use `CEL::Context.new(true)` or `CEL::Context.build(empty: true)` to define functions without builtins. Registering the same custom callback name again replaces the previous callback.
|
|
110
221
|
- Some CEL value variants (e.g. opaque custom Rust types) cannot be fully marshaled to Ruby and raise `CEL::TypeError`.
|
|
111
222
|
- This project targets broad CEL compatibility, but parity for every Rust-only extension point is still evolving.
|
data/ext/cel/Cargo.toml
CHANGED
|
@@ -10,5 +10,5 @@ crate-type = ["cdylib"]
|
|
|
10
10
|
[dependencies]
|
|
11
11
|
magnus = { version = "0.8.2", features = ["chrono", "rb-sys"] }
|
|
12
12
|
rb-sys = { version = "0.9.128", features = ["stable-api-compiled-fallback"] }
|
|
13
|
-
cel = { version = "0.
|
|
13
|
+
cel = { version = "0.15.0", features = ["chrono", "regex", "json"] }
|
|
14
14
|
chrono = "0.4.45"
|
data/ext/cel/src/lib.rs
CHANGED
|
@@ -155,17 +155,27 @@ struct FunctionRegistration {
|
|
|
155
155
|
callback: Arc<CallbackFunction>,
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
#[derive(Default)]
|
|
159
|
-
#[magnus
|
|
158
|
+
#[derive(Default, magnus::TypedData)]
|
|
159
|
+
#[magnus(class = "CEL::Context", free_immediately, size, mark)]
|
|
160
160
|
struct ContextWrap {
|
|
161
161
|
use_empty: bool,
|
|
162
162
|
variables: Mutex<HashMap<String, CelValue>>,
|
|
163
163
|
functions: Mutex<Vec<FunctionRegistration>>,
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
impl magnus::DataTypeFunctions for ContextWrap {
|
|
167
|
+
fn mark(&self, marker: &magnus::gc::Marker) {
|
|
168
|
+
for registration in self.functions.lock().unwrap().iter() {
|
|
169
|
+
// Non-movable marking also pins the Proc during Ruby compaction.
|
|
170
|
+
marker.mark(registration.callback.proc);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
166
175
|
#[magnus::wrap(class = "CEL::Program", free_immediately, size)]
|
|
167
176
|
struct ProgramWrap {
|
|
168
177
|
inner: CelProgram,
|
|
178
|
+
env: Arc<cel::Env>,
|
|
169
179
|
}
|
|
170
180
|
|
|
171
181
|
#[derive(Clone)]
|
|
@@ -355,11 +365,16 @@ impl ContextWrap {
|
|
|
355
365
|
});
|
|
356
366
|
}
|
|
357
367
|
|
|
358
|
-
fn build_context(&self) -> CelContext<'static> {
|
|
368
|
+
fn build_context(&self, env: Arc<cel::Env>) -> Result<CelContext<'static, 'static>, Error> {
|
|
359
369
|
let mut ctx = if self.use_empty {
|
|
360
370
|
CelContext::empty()
|
|
361
371
|
} else {
|
|
362
|
-
CelContext::
|
|
372
|
+
let mut ctx = CelContext::with_env(env);
|
|
373
|
+
ctx.add_function("max", cel::functions::max)
|
|
374
|
+
.map_err(|e| errors::execution(e.to_string()))?;
|
|
375
|
+
ctx.add_function("min", cel::functions::min)
|
|
376
|
+
.map_err(|e| errors::execution(e.to_string()))?;
|
|
377
|
+
ctx
|
|
363
378
|
};
|
|
364
379
|
|
|
365
380
|
for (name, value) in self.variables.lock().unwrap().iter() {
|
|
@@ -375,7 +390,11 @@ impl ContextWrap {
|
|
|
375
390
|
cel::extractors::Arguments(args): cel::extractors::Arguments|
|
|
376
391
|
-> ResolveResult {
|
|
377
392
|
let callback = callback.clone();
|
|
378
|
-
let this = ftx
|
|
393
|
+
let this = ftx
|
|
394
|
+
.this
|
|
395
|
+
.as_ref()
|
|
396
|
+
.map(|value| CelValue::try_from(value.as_ref()))
|
|
397
|
+
.transpose()?;
|
|
379
398
|
let args = args.clone();
|
|
380
399
|
|
|
381
400
|
with_gvl(|| {
|
|
@@ -406,29 +425,49 @@ impl ContextWrap {
|
|
|
406
425
|
.map_err(|e| CelExecutionError::function_error(ftx.name, e.to_string()))
|
|
407
426
|
})
|
|
408
427
|
},
|
|
409
|
-
)
|
|
428
|
+
)
|
|
429
|
+
.map_err(|e| errors::execution(e.to_string()))?;
|
|
410
430
|
}
|
|
411
431
|
|
|
412
|
-
ctx
|
|
432
|
+
Ok(ctx)
|
|
413
433
|
}
|
|
414
434
|
}
|
|
415
435
|
|
|
416
436
|
impl ProgramWrap {
|
|
417
|
-
fn compile(source: String) -> Result<Self, Error> {
|
|
418
|
-
|
|
419
|
-
|
|
437
|
+
fn compile(source: String, extensions: Vec<String>) -> Result<Self, Error> {
|
|
438
|
+
let ruby = Ruby::get().expect("ruby runtime");
|
|
439
|
+
let mut env = cel::Env::stdlib();
|
|
440
|
+
for name in extensions {
|
|
441
|
+
let extension = match name.as_str() {
|
|
442
|
+
"strings" => cel::extensions::strings,
|
|
443
|
+
"lists" => cel::extensions::lists,
|
|
444
|
+
"math" => cel::extensions::math,
|
|
445
|
+
"encoders" => cel::extensions::encoders,
|
|
446
|
+
_ => {
|
|
447
|
+
return Err(Error::new(
|
|
448
|
+
ruby.exception_arg_error(),
|
|
449
|
+
format!("Unknown CEL extension: {name}"),
|
|
450
|
+
))
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
env.add_extension(extension)
|
|
454
|
+
.map_err(|e| Error::new(ruby.exception_arg_error(), e.to_string()))?;
|
|
455
|
+
}
|
|
456
|
+
let env = Arc::new(env);
|
|
457
|
+
env.compile(&source)
|
|
458
|
+
.map(|inner| Self { inner, env })
|
|
420
459
|
.map_err(|e: ParseErrors| errors::parse(e.to_string()))
|
|
421
460
|
}
|
|
422
461
|
|
|
423
462
|
fn execute(&self) -> Result<Value, Error> {
|
|
424
|
-
self.
|
|
463
|
+
self.execute_with_context(&ContextWrap::new(false))
|
|
425
464
|
}
|
|
426
465
|
|
|
427
466
|
fn execute_with_context(&self, context: &ContextWrap) -> Result<Value, Error> {
|
|
428
|
-
self.execute_with_context_internal(&context.build_context())
|
|
467
|
+
self.execute_with_context_internal(&context.build_context(self.env.clone())?)
|
|
429
468
|
}
|
|
430
469
|
|
|
431
|
-
fn execute_with_context_internal(&self, ctx: &CelContext<'_>) -> Result<Value, Error> {
|
|
470
|
+
fn execute_with_context_internal(&self, ctx: &CelContext<'_, '_>) -> Result<Value, Error> {
|
|
432
471
|
let run = || self.inner.execute(ctx);
|
|
433
472
|
let result = panic::catch_unwind(AssertUnwindSafe(|| without_gvl(run)))
|
|
434
473
|
.map_err(|_| errors::execution("CEL execution panicked"))?;
|
|
@@ -484,7 +523,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
484
523
|
duration_class.define_method("==", method!(DurationWrap::eq, 1))?;
|
|
485
524
|
|
|
486
525
|
let program_class = module.define_class("Program", ruby.class_object())?;
|
|
487
|
-
program_class.define_singleton_method("compile", function!(ProgramWrap::compile,
|
|
526
|
+
program_class.define_singleton_method("compile", function!(ProgramWrap::compile, 2))?;
|
|
488
527
|
program_class.define_method("execute", method!(ProgramWrap::execute, 0))?;
|
|
489
528
|
program_class.define_method(
|
|
490
529
|
"execute_with_context",
|
|
@@ -493,7 +532,5 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
493
532
|
program_class.define_method("references", method!(ProgramWrap::references, 0))?;
|
|
494
533
|
program_class.define_method("expression", method!(ProgramWrap::expression, 0))?;
|
|
495
534
|
|
|
496
|
-
module.define_singleton_method("compile", function!(ProgramWrap::compile, 1))?;
|
|
497
|
-
|
|
498
535
|
Ok(())
|
|
499
536
|
}
|
data/lib/cel/version.rb
CHANGED
data/lib/cel.rb
CHANGED
|
@@ -10,6 +10,10 @@ rescue LoadError
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
module CEL
|
|
13
|
+
def self.compile(source, extensions: [])
|
|
14
|
+
Program.compile(source, extensions: extensions)
|
|
15
|
+
end
|
|
16
|
+
|
|
13
17
|
class Context
|
|
14
18
|
class << self
|
|
15
19
|
alias_method :__native_new, :new
|
|
@@ -34,6 +38,14 @@ module CEL
|
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
class Program
|
|
41
|
+
class << self
|
|
42
|
+
alias_method :__native_compile, :compile
|
|
43
|
+
|
|
44
|
+
def compile(source, extensions: [])
|
|
45
|
+
__native_compile(source, extensions.map(&:to_s))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
37
49
|
alias_method :__native_execute, :execute
|
|
38
50
|
|
|
39
51
|
def execute(context = nil)
|
data/spec/cel_spec.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
require "time"
|
|
5
|
+
require "open3"
|
|
5
6
|
|
|
6
7
|
RSpec.describe CEL do
|
|
7
8
|
describe ".compile" do
|
|
@@ -13,6 +14,49 @@ RSpec.describe CEL do
|
|
|
13
14
|
it "raises parse errors for invalid programs" do
|
|
14
15
|
expect { CEL.compile("1 +") }.to raise_error(CEL::ParseError)
|
|
15
16
|
end
|
|
17
|
+
|
|
18
|
+
it "enables each upstream extension for compilation and execution" do
|
|
19
|
+
tests = {
|
|
20
|
+
strings: ["'TacoCat'.lowerAscii()", "tacocat"],
|
|
21
|
+
lists: ["[{'n': 8}, {'n': 2}, {'n': 5}].sortBy(x, x.n).map(x, x.n)", [2, 5, 8]],
|
|
22
|
+
math: ["math.greatest(2, 9, -3)", 9],
|
|
23
|
+
encoders: ["base64.encode(b'hi')", "aGk="]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
tests.each do |extension, (expression, expected)|
|
|
27
|
+
program = CEL.compile(expression, extensions: [extension])
|
|
28
|
+
expect(program.execute).to eq(expected)
|
|
29
|
+
expect(program.execute(CEL::Context.new)).to eq(expected)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "combines extensions with context variables and Ruby callbacks" do
|
|
34
|
+
context = CEL::Context.build(label: "TacoCat", scores: [2, 9, -3]) do |ctx|
|
|
35
|
+
ctx.define_function("decorate") { |value| "[#{value}]" }
|
|
36
|
+
end
|
|
37
|
+
program = CEL::Program.compile(
|
|
38
|
+
"[decorate(label.lowerAscii()), scores.sort(), math.greatest(scores), base64.encode(b'hi')]",
|
|
39
|
+
extensions: ["strings", "lists", "math", "encoders"]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
expect(program.execute(context)).to eq(["[tacocat]", [-3, 2, 9], 9, "aGk="])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "keeps extensions local to a program and excludes them from empty contexts" do
|
|
46
|
+
context = CEL::Context.new
|
|
47
|
+
enabled = CEL.compile("'ABC'.lowerAscii()", extensions: [:strings])
|
|
48
|
+
expect(enabled.execute(context)).to eq("abc")
|
|
49
|
+
expect { CEL.compile("'ABC'.lowerAscii()").execute(context) }.to raise_error(CEL::ExecutionError)
|
|
50
|
+
expect { enabled.execute(CEL::Context.new(true)) }.to raise_error(CEL::ExecutionError)
|
|
51
|
+
|
|
52
|
+
context.define_function("lowerAscii") { |value| value }
|
|
53
|
+
expect { enabled.execute(context) }.to raise_error(CEL::ExecutionError, /lowerAscii/)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "rejects unknown and repeated extension registrations" do
|
|
57
|
+
expect { CEL.compile("1", extensions: [:unknown]) }.to raise_error(ArgumentError, /unknown/)
|
|
58
|
+
expect { CEL.compile("1", extensions: [:strings, "strings"]) }.to raise_error(ArgumentError)
|
|
59
|
+
end
|
|
16
60
|
end
|
|
17
61
|
|
|
18
62
|
describe CEL::Context do
|
|
@@ -56,6 +100,40 @@ RSpec.describe CEL do
|
|
|
56
100
|
expect(CEL.compile("sum(1, 2, 3)").execute(context)).to eq(6)
|
|
57
101
|
end
|
|
58
102
|
|
|
103
|
+
it "keeps registered callbacks alive through garbage collection and compaction" do
|
|
104
|
+
# Isolate native GC failures and discard the registering thread's stack roots.
|
|
105
|
+
output, status = Open3.capture2e(
|
|
106
|
+
RbConfig.ruby, "-I", File.expand_path("../lib", __dir__), "-rcel", "-rweakref", "-e", <<~RUBY
|
|
107
|
+
context, callbacks = Thread.new do
|
|
108
|
+
ctx = CEL::Context.new
|
|
109
|
+
prefix = +"retained:token:"
|
|
110
|
+
decorate = proc { |value| prefix + value }
|
|
111
|
+
collect = proc do
|
|
112
|
+
GC.start
|
|
113
|
+
GC.verify_compaction_references(expand_heap: true, toward: :empty)
|
|
114
|
+
17
|
|
115
|
+
end
|
|
116
|
+
ctx.define_function("decorate", &decorate)
|
|
117
|
+
ctx.define_function("collect", &collect)
|
|
118
|
+
[ctx, [WeakRef.new(decorate), WeakRef.new(collect)]]
|
|
119
|
+
end.value
|
|
120
|
+
|
|
121
|
+
program = CEL.compile("[decorate('ok'), collect(), decorate('again')]")
|
|
122
|
+
3.times do
|
|
123
|
+
GC.start
|
|
124
|
+
GC.verify_compaction_references(expand_heap: true, toward: :empty)
|
|
125
|
+
raise "registered callback was collected" unless callbacks.all?(&:weakref_alive?)
|
|
126
|
+
result = program.execute(context)
|
|
127
|
+
raise "incorrect callback result: \#{result.inspect}" unless result == [
|
|
128
|
+
"retained:token:ok", 17, "retained:token:again"
|
|
129
|
+
]
|
|
130
|
+
end
|
|
131
|
+
RUBY
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
expect(status.success?).to eq(true), output
|
|
135
|
+
end
|
|
136
|
+
|
|
59
137
|
it "passes method receiver as first block arg for method calls" do
|
|
60
138
|
context = CEL::Context.new(true)
|
|
61
139
|
context.define_function("startsWith") { |target, prefix| target.start_with?(prefix) }
|
|
@@ -63,6 +141,31 @@ RSpec.describe CEL do
|
|
|
63
141
|
expect(CEL.compile("'hello'.startsWith('he')").execute(context)).to eq(true)
|
|
64
142
|
end
|
|
65
143
|
|
|
144
|
+
it "marshals callback receivers, arguments, and results" do
|
|
145
|
+
context = CEL::Context.build(prefix: {"label" => "result"})
|
|
146
|
+
context.define_function("combine") do |target, values, suffix|
|
|
147
|
+
{target["label"] => values + [suffix]}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
expect(CEL.compile("prefix.combine([2, 5], 9)").execute(context)).to eq("result" => [2, 5, 9])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "reports builtin callback conflicts when building the execution context" do
|
|
154
|
+
context = CEL::Context.new
|
|
155
|
+
context.define_function("size") { |_| 99 }
|
|
156
|
+
|
|
157
|
+
expect { CEL.compile("size([1, 2])").execute(context) }
|
|
158
|
+
.to raise_error(CEL::ExecutionError, /size/)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "replaces callbacks with the same name" do
|
|
162
|
+
context = CEL::Context.new
|
|
163
|
+
context.define_function("adjust") { |value| value + 1 }
|
|
164
|
+
context.define_function("adjust") { |value| value + 7 }
|
|
165
|
+
|
|
166
|
+
expect(CEL.compile("adjust(3)").execute(context)).to eq(10)
|
|
167
|
+
end
|
|
168
|
+
|
|
66
169
|
it "raises clear type error for unsupported variable types" do
|
|
67
170
|
context = CEL::Context.new
|
|
68
171
|
expect { context.add_variable("bad", Object.new) }.to raise_error(CEL::TypeError)
|
|
@@ -77,6 +180,36 @@ RSpec.describe CEL do
|
|
|
77
180
|
expect(refs["functions"]).to include("size")
|
|
78
181
|
end
|
|
79
182
|
|
|
183
|
+
it "registers max and min by default" do
|
|
184
|
+
tests = {
|
|
185
|
+
"max(2, 9, -3)" => 9,
|
|
186
|
+
"min(2, 9, -3)" => -3,
|
|
187
|
+
"max([2, 9, -3])" => 9,
|
|
188
|
+
"min([2, 9, -3])" => -3,
|
|
189
|
+
"max()" => nil,
|
|
190
|
+
"min([])" => nil
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
[nil, CEL::Context.new].each do |context|
|
|
194
|
+
tests.each do |expression, expected|
|
|
195
|
+
expect(CEL.compile(expression).execute(context)).to eq(expected)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "leaves max and min out of empty contexts and allows custom callbacks" do
|
|
201
|
+
["max", "min"].each do |name|
|
|
202
|
+
program = CEL.compile("#{name}(2, 9)")
|
|
203
|
+
empty = CEL::Context.new(true)
|
|
204
|
+
expect { program.execute(empty) }.to raise_error(CEL::ExecutionError)
|
|
205
|
+
|
|
206
|
+
[empty, CEL::Context.new].each do |context|
|
|
207
|
+
context.define_function(name) { |a, b| a + b }
|
|
208
|
+
expect(program.execute(context)).to eq(11)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
80
213
|
it "ports core CEL suite behavior examples" do
|
|
81
214
|
tests = {
|
|
82
215
|
"size([1,2,3]) == 3" => true,
|
|
@@ -84,8 +217,13 @@ RSpec.describe CEL do
|
|
|
84
217
|
"[1,2,3].filter(x, x > 1)" => [2, 3],
|
|
85
218
|
"[1,2,3].exists(x, x == 2)" => true,
|
|
86
219
|
"[1,2,3].all(x, x > 0)" => true,
|
|
87
|
-
"{'a': 1}
|
|
88
|
-
"optional.of(1).hasValue()" => true
|
|
220
|
+
"'a' in {'a': 1}" => true,
|
|
221
|
+
"optional.of(1).hasValue()" => true,
|
|
222
|
+
"size('café😊')" => 5,
|
|
223
|
+
"{'name': 'Ada'}.?name.optMap(n, n.size()).orValue(0)" => 3,
|
|
224
|
+
"{}.?name.optMap(n, n.size()).orValue(0)" => 0,
|
|
225
|
+
"{'user': {'name': 'Ada'}}.?user.optFlatMap(u, u.?name).orValue('missing')" => "Ada",
|
|
226
|
+
"[2, 1].exists(e, 6 / (2 - e) == 6)" => true
|
|
89
227
|
}
|
|
90
228
|
|
|
91
229
|
tests.each do |expr, expected|
|
|
@@ -113,6 +251,10 @@ RSpec.describe CEL do
|
|
|
113
251
|
expect { program.execute }.to raise_error(CEL::ExecutionError)
|
|
114
252
|
end
|
|
115
253
|
|
|
254
|
+
it "rejects duplicate map literal keys" do
|
|
255
|
+
expect { CEL.compile("{'a': 1, 'a': 2}").execute }.to raise_error(CEL::ExecutionError)
|
|
256
|
+
end
|
|
257
|
+
|
|
116
258
|
it "releases GVL so other ruby threads can progress" do
|
|
117
259
|
context = CEL::Context.new
|
|
118
260
|
context.add_variable("items", Array.new(15_000, 1))
|
|
@@ -135,15 +277,38 @@ RSpec.describe CEL do
|
|
|
135
277
|
worker.join
|
|
136
278
|
end
|
|
137
279
|
|
|
138
|
-
it "
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
280
|
+
it "executes the same program concurrently with independent contexts" do
|
|
281
|
+
program = CEL.compile("items.map(x, x + offset).filter(x, x > cutoff).size() + id")
|
|
282
|
+
ready = Queue.new
|
|
283
|
+
start = Queue.new
|
|
142
284
|
|
|
143
|
-
threads = Array.new(8)
|
|
144
|
-
|
|
285
|
+
threads = Array.new(8) do |id|
|
|
286
|
+
Thread.new do
|
|
287
|
+
ready << true
|
|
288
|
+
start.pop
|
|
289
|
+
|
|
290
|
+
offset = id % 3
|
|
291
|
+
context = CEL::Context.build(
|
|
292
|
+
id: id,
|
|
293
|
+
items: Array.new(200, id),
|
|
294
|
+
offset: offset,
|
|
295
|
+
cutoff: id
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
25.times.map { program.execute(context) }
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
8.times { ready.pop }
|
|
302
|
+
8.times { start << true }
|
|
303
|
+
results = threads.map(&:value)
|
|
304
|
+
|
|
305
|
+
expected = Array.new(8) do |id|
|
|
306
|
+
offset = id % 3
|
|
307
|
+
value = (offset.zero? ? 0 : 200) + id
|
|
308
|
+
Array.new(25, value)
|
|
309
|
+
end
|
|
145
310
|
|
|
146
|
-
expect(
|
|
311
|
+
expect(results).to eq(expected)
|
|
147
312
|
end
|
|
148
313
|
end
|
|
149
314
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cel-rs-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- CEL Ruby Contributors
|
|
@@ -100,6 +100,7 @@ extensions:
|
|
|
100
100
|
- ext/cel/extconf.rb
|
|
101
101
|
extra_rdoc_files: []
|
|
102
102
|
files:
|
|
103
|
+
- CHANGELOG.md
|
|
103
104
|
- Cargo.toml
|
|
104
105
|
- LICENSE
|
|
105
106
|
- README.md
|
|
@@ -110,12 +111,12 @@ files:
|
|
|
110
111
|
- lib/cel/version.rb
|
|
111
112
|
- spec/cel_spec.rb
|
|
112
113
|
- spec/spec_helper.rb
|
|
113
|
-
homepage: https://github.com/
|
|
114
|
+
homepage: https://github.com/buildkite/cel-rs-rb
|
|
114
115
|
licenses:
|
|
115
116
|
- MIT
|
|
116
117
|
metadata:
|
|
117
|
-
homepage_uri: https://github.com/
|
|
118
|
-
source_code_uri: https://github.com/
|
|
118
|
+
homepage_uri: https://github.com/buildkite/cel-rs-rb
|
|
119
|
+
source_code_uri: https://github.com/buildkite/cel-rs-rb
|
|
119
120
|
rubygems_mfa_required: 'true'
|
|
120
121
|
rdoc_options: []
|
|
121
122
|
require_paths:
|
|
@@ -131,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
131
132
|
- !ruby/object:Gem::Version
|
|
132
133
|
version: '0'
|
|
133
134
|
requirements: []
|
|
134
|
-
rubygems_version: 4.0.
|
|
135
|
+
rubygems_version: 4.0.20
|
|
135
136
|
specification_version: 4
|
|
136
137
|
summary: Ruby bindings for the Rust CEL crate
|
|
137
138
|
test_files: []
|