cel-rs-rb 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/Cargo.toml +9 -0
- data/LICENSE +21 -0
- data/README.md +111 -0
- data/ext/cel/Cargo.toml +14 -0
- data/ext/cel/extconf.rb +6 -0
- data/ext/cel/src/lib.rs +499 -0
- data/lib/cel/version.rb +5 -0
- data/lib/cel.rb +49 -0
- data/spec/cel_spec.rb +149 -0
- data/spec/spec_helper.rb +9 -0
- metadata +137 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4a2986048cff8c1e335c72ffc410487bae67a02251ff9ef9815c34a430199b39
|
|
4
|
+
data.tar.gz: 14df42d51ffda3ecdbfd0a9455a9e11d9990b0f30f11c9f261d5367768d35498
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1af27f75d923ded581e5dfab5d046cd97ccb3837822f2cbae96f751c6dc7e41e2db539e86e61a70520efdf98c314b5a20ba557e242ffe2fee9788b5a9f29b843
|
|
7
|
+
data.tar.gz: 16f711a65c9ef6b529410d8b3d823bb6317d80626460fa5cd43d01bd365b84efb4a6d800897bf8e927b56e8ce216aa2960e811e9f6000c959c0ec9b24c7c8207
|
data/Cargo.toml
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Atkins
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# cel-rs-rb
|
|
2
|
+
|
|
3
|
+
Ruby bindings for the Rust [`cel`](https://crates.io/crates/cel) crate, implemented with [Magnus](https://github.com/matsadler/magnus).
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
|
|
7
|
+
- Ruby-first API over the Rust CEL engine
|
|
8
|
+
- Close semantic compatibility with Rust `cel::Program` + `cel::Context`
|
|
9
|
+
- Ruby variable and function interop
|
|
10
|
+
- Thread-safe concurrent execution
|
|
11
|
+
- GVL released while CEL evaluation runs
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem "cel-rs-rb"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "cel"
|
|
23
|
+
|
|
24
|
+
context = CEL::Context.build(user: {"name" => "Ada"}, scores: [10, 20, 30]) do |ctx|
|
|
25
|
+
ctx.define_function("sum") { |*args| args.sum }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
program = CEL.compile("sum(scores[0], scores[1], scores[2])")
|
|
29
|
+
program.execute(context)
|
|
30
|
+
# => 60
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### API mapping
|
|
34
|
+
|
|
35
|
+
- `CEL.compile(source)` -> `CEL::Program`
|
|
36
|
+
- `CEL::Program.compile(source)` -> `CEL::Program`
|
|
37
|
+
- `CEL::Program#execute(context = nil)` -> Ruby value
|
|
38
|
+
- `CEL::Program#references` -> `{ "variables" => [...], "functions" => [...] }`
|
|
39
|
+
- `CEL::Context.new(empty = false)` -> context with builtins (`false`) or empty (`true`)
|
|
40
|
+
- `CEL::Context#add_variable(name, value)`
|
|
41
|
+
- `CEL::Context#define_function(name) { |*args| ... }`
|
|
42
|
+
- `CEL::Duration.new(seconds)` -> duration value for context variables and duration results
|
|
43
|
+
|
|
44
|
+
## Type support
|
|
45
|
+
|
|
46
|
+
### Ruby -> CEL
|
|
47
|
+
|
|
48
|
+
- `nil` -> `null`
|
|
49
|
+
- `true/false` -> `bool`
|
|
50
|
+
- `Integer` -> `int`
|
|
51
|
+
- `Float` -> `double`
|
|
52
|
+
- `String` / `Symbol` -> `string`
|
|
53
|
+
- binary `String` (`ASCII-8BIT`, e.g. `"abc".b`) -> `bytes`
|
|
54
|
+
- `Time` -> `timestamp`
|
|
55
|
+
- `CEL::Duration` -> `duration`
|
|
56
|
+
- `Array` -> `list`
|
|
57
|
+
- `Hash` with keys `String|Symbol|Integer|Boolean` -> `map`
|
|
58
|
+
|
|
59
|
+
### CEL -> Ruby
|
|
60
|
+
|
|
61
|
+
- `null` -> `nil`
|
|
62
|
+
- scalar primitives map naturally
|
|
63
|
+
- `bytes` -> binary Ruby `String`
|
|
64
|
+
- `timestamp` -> `Time`
|
|
65
|
+
- `duration` -> `CEL::Duration`
|
|
66
|
+
- `list` -> `Array`
|
|
67
|
+
- `map` -> `Hash`
|
|
68
|
+
|
|
69
|
+
## Error classes
|
|
70
|
+
|
|
71
|
+
- `CEL::Error`
|
|
72
|
+
- `CEL::ParseError`
|
|
73
|
+
- `CEL::ExecutionError`
|
|
74
|
+
- `CEL::TypeError`
|
|
75
|
+
|
|
76
|
+
## Thread safety and concurrency
|
|
77
|
+
|
|
78
|
+
- Context data is mutex-protected and immutable during execution snapshots.
|
|
79
|
+
- CEL execution is run with the Ruby GVL released so other Ruby threads can run.
|
|
80
|
+
- Ruby callbacks from CEL functions temporarily re-acquire the GVL.
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
### Tooling
|
|
85
|
+
|
|
86
|
+
Uses [mise](https://mise.jdx.dev/) for versions:
|
|
87
|
+
|
|
88
|
+
```toml
|
|
89
|
+
[tools]
|
|
90
|
+
ruby = "4.0"
|
|
91
|
+
rust = "1.96.0"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Test + lint
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
bundle exec rake
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This runs:
|
|
101
|
+
|
|
102
|
+
- `standard` (format/lint)
|
|
103
|
+
- native extension compile
|
|
104
|
+
- `rspec`
|
|
105
|
+
|
|
106
|
+
CI also runs the test suite on Ruby 3.3, Ruby 3.4, and Ruby 4.0 through Buildkite.
|
|
107
|
+
|
|
108
|
+
## Compatibility notes
|
|
109
|
+
|
|
110
|
+
- Some CEL value variants (e.g. opaque custom Rust types) cannot be fully marshaled to Ruby and raise `CEL::TypeError`.
|
|
111
|
+
- This project targets broad CEL compatibility, but parity for every Rust-only extension point is still evolving.
|
data/ext/cel/Cargo.toml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "cel"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
publish = false
|
|
6
|
+
|
|
7
|
+
[lib]
|
|
8
|
+
crate-type = ["cdylib"]
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
magnus = { version = "0.8.2", features = ["chrono", "rb-sys"] }
|
|
12
|
+
rb-sys = { version = "0.9.128", features = ["stable-api-compiled-fallback"] }
|
|
13
|
+
cel = { version = "0.13.0", features = ["chrono", "regex", "json"] }
|
|
14
|
+
chrono = "0.4.45"
|
data/ext/cel/extconf.rb
ADDED
data/ext/cel/src/lib.rs
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
use cel::{
|
|
2
|
+
Context as CelContext, ExecutionError as CelExecutionError, FunctionContext, ParseErrors,
|
|
3
|
+
Program as CelProgram, ResolveResult, Value as CelValue,
|
|
4
|
+
};
|
|
5
|
+
use magnus::block::Proc;
|
|
6
|
+
use magnus::prelude::*;
|
|
7
|
+
use magnus::typed_data::Obj;
|
|
8
|
+
use magnus::{function, method, Error, IntoValue, RHash, RString, Ruby, TryConvert, Value};
|
|
9
|
+
use rb_sys::{rb_thread_call_with_gvl, rb_thread_call_without_gvl};
|
|
10
|
+
use std::collections::HashMap;
|
|
11
|
+
use std::ffi::c_void;
|
|
12
|
+
use std::panic::{self, AssertUnwindSafe};
|
|
13
|
+
use std::sync::{Arc, Mutex};
|
|
14
|
+
|
|
15
|
+
mod errors {
|
|
16
|
+
use magnus::prelude::*;
|
|
17
|
+
use magnus::{Error, ExceptionClass, RModule, Ruby};
|
|
18
|
+
use std::cell::RefCell;
|
|
19
|
+
|
|
20
|
+
thread_local! {
|
|
21
|
+
static PARSE: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
22
|
+
static EXECUTION: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
23
|
+
static TYPE: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub fn define(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
27
|
+
let standard = ruby.exception_standard_error();
|
|
28
|
+
let base = module.define_error("Error", standard)?;
|
|
29
|
+
let parse = module.define_error("ParseError", base)?;
|
|
30
|
+
let execution = module.define_error("ExecutionError", base)?;
|
|
31
|
+
let ty = module.define_error("TypeError", base)?;
|
|
32
|
+
PARSE.with(|slot| *slot.borrow_mut() = Some(parse));
|
|
33
|
+
EXECUTION.with(|slot| *slot.borrow_mut() = Some(execution));
|
|
34
|
+
TYPE.with(|slot| *slot.borrow_mut() = Some(ty));
|
|
35
|
+
Ok(())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn fallback(msg: String) -> Error {
|
|
39
|
+
let ruby = Ruby::get().expect("ruby runtime");
|
|
40
|
+
Error::new(ruby.exception_runtime_error(), msg)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pub fn parse(msg: impl Into<String>) -> Error {
|
|
44
|
+
let msg = msg.into();
|
|
45
|
+
PARSE.with(|slot| {
|
|
46
|
+
slot.borrow()
|
|
47
|
+
.map(|exc| Error::new(exc, msg.clone()))
|
|
48
|
+
.unwrap_or_else(|| fallback(msg))
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pub fn execution(msg: impl Into<String>) -> Error {
|
|
53
|
+
let msg = msg.into();
|
|
54
|
+
EXECUTION.with(|slot| {
|
|
55
|
+
slot.borrow()
|
|
56
|
+
.map(|exc| Error::new(exc, msg.clone()))
|
|
57
|
+
.unwrap_or_else(|| fallback(msg))
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub fn ty(msg: impl Into<String>) -> Error {
|
|
62
|
+
let msg = msg.into();
|
|
63
|
+
TYPE.with(|slot| {
|
|
64
|
+
slot.borrow()
|
|
65
|
+
.map(|exc| Error::new(exc, msg.clone()))
|
|
66
|
+
.unwrap_or_else(|| fallback(msg))
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn without_gvl<F, T>(f: F) -> T
|
|
72
|
+
where
|
|
73
|
+
F: FnOnce() -> T,
|
|
74
|
+
T: Send,
|
|
75
|
+
{
|
|
76
|
+
struct State<F, T>
|
|
77
|
+
where
|
|
78
|
+
F: FnOnce() -> T,
|
|
79
|
+
T: Send,
|
|
80
|
+
{
|
|
81
|
+
f: Option<F>,
|
|
82
|
+
result: Option<T>,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
unsafe extern "C" fn call<F, T>(ptr: *mut c_void) -> *mut c_void
|
|
86
|
+
where
|
|
87
|
+
F: FnOnce() -> T,
|
|
88
|
+
T: Send,
|
|
89
|
+
{
|
|
90
|
+
let state = &mut *(ptr as *mut State<F, T>);
|
|
91
|
+
let fun = state.f.take().expect("closure missing");
|
|
92
|
+
state.result = Some(fun());
|
|
93
|
+
std::ptr::null_mut()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let mut state = State {
|
|
97
|
+
f: Some(f),
|
|
98
|
+
result: None,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
unsafe {
|
|
102
|
+
rb_thread_call_without_gvl(
|
|
103
|
+
Some(call::<F, T>),
|
|
104
|
+
&mut state as *mut _ as *mut c_void,
|
|
105
|
+
None,
|
|
106
|
+
std::ptr::null_mut(),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
state.result.expect("result missing")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn with_gvl<F, T>(f: F) -> T
|
|
114
|
+
where
|
|
115
|
+
F: FnOnce() -> T,
|
|
116
|
+
{
|
|
117
|
+
struct State<F, T>
|
|
118
|
+
where
|
|
119
|
+
F: FnOnce() -> T,
|
|
120
|
+
{
|
|
121
|
+
f: Option<F>,
|
|
122
|
+
result: Option<T>,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
unsafe extern "C" fn call<F, T>(ptr: *mut c_void) -> *mut c_void
|
|
126
|
+
where
|
|
127
|
+
F: FnOnce() -> T,
|
|
128
|
+
{
|
|
129
|
+
let state = &mut *(ptr as *mut State<F, T>);
|
|
130
|
+
let fun = state.f.take().expect("closure missing");
|
|
131
|
+
state.result = Some(fun());
|
|
132
|
+
std::ptr::null_mut()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let mut state = State {
|
|
136
|
+
f: Some(f),
|
|
137
|
+
result: None,
|
|
138
|
+
};
|
|
139
|
+
unsafe {
|
|
140
|
+
rb_thread_call_with_gvl(Some(call::<F, T>), &mut state as *mut _ as *mut c_void);
|
|
141
|
+
}
|
|
142
|
+
state.result.expect("result missing")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#[derive(Clone)]
|
|
146
|
+
struct CallbackFunction {
|
|
147
|
+
proc: Proc,
|
|
148
|
+
}
|
|
149
|
+
unsafe impl Send for CallbackFunction {}
|
|
150
|
+
unsafe impl Sync for CallbackFunction {}
|
|
151
|
+
|
|
152
|
+
#[derive(Clone)]
|
|
153
|
+
struct FunctionRegistration {
|
|
154
|
+
name: String,
|
|
155
|
+
callback: Arc<CallbackFunction>,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[derive(Default)]
|
|
159
|
+
#[magnus::wrap(class = "CEL::Context", free_immediately, size)]
|
|
160
|
+
struct ContextWrap {
|
|
161
|
+
use_empty: bool,
|
|
162
|
+
variables: Mutex<HashMap<String, CelValue>>,
|
|
163
|
+
functions: Mutex<Vec<FunctionRegistration>>,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#[magnus::wrap(class = "CEL::Program", free_immediately, size)]
|
|
167
|
+
struct ProgramWrap {
|
|
168
|
+
inner: CelProgram,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[derive(Clone)]
|
|
172
|
+
#[magnus::wrap(class = "CEL::Duration", free_immediately, size)]
|
|
173
|
+
struct DurationWrap {
|
|
174
|
+
inner: chrono::Duration,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn ruby_to_cel_value(value: Value) -> Result<CelValue, Error> {
|
|
178
|
+
let ruby = Ruby::get().expect("ruby runtime");
|
|
179
|
+
|
|
180
|
+
if value.is_nil() {
|
|
181
|
+
return Ok(CelValue::Null);
|
|
182
|
+
}
|
|
183
|
+
if value.is_kind_of(ruby.class_true_class()) || value.is_kind_of(ruby.class_false_class()) {
|
|
184
|
+
return Ok(CelValue::Bool(<bool as TryConvert>::try_convert(value)?));
|
|
185
|
+
}
|
|
186
|
+
if value.is_kind_of(ruby.class_integer()) {
|
|
187
|
+
return Ok(CelValue::Int(<i64 as TryConvert>::try_convert(value)?));
|
|
188
|
+
}
|
|
189
|
+
if value.is_kind_of(ruby.class_float()) {
|
|
190
|
+
return Ok(CelValue::Float(<f64 as TryConvert>::try_convert(value)?));
|
|
191
|
+
}
|
|
192
|
+
if value.is_kind_of(ruby.class_string()) {
|
|
193
|
+
let string = <RString as TryConvert>::try_convert(value)?;
|
|
194
|
+
if string.enc_get() == ruby.ascii8bit_encindex() {
|
|
195
|
+
return Ok(CelValue::Bytes(Arc::new(unsafe {
|
|
196
|
+
string.as_slice().to_vec()
|
|
197
|
+
})));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return Ok(CelValue::String(Arc::new(
|
|
201
|
+
<String as TryConvert>::try_convert(value)?,
|
|
202
|
+
)));
|
|
203
|
+
}
|
|
204
|
+
if value.is_kind_of(ruby.class_symbol()) {
|
|
205
|
+
let sym = <magnus::Symbol as TryConvert>::try_convert(value)?;
|
|
206
|
+
return Ok(CelValue::String(Arc::new(sym.name()?.to_string())));
|
|
207
|
+
}
|
|
208
|
+
if value.is_kind_of(ruby.class_time()) {
|
|
209
|
+
return Ok(CelValue::Timestamp(
|
|
210
|
+
<chrono::DateTime<chrono::FixedOffset> as TryConvert>::try_convert(value)?,
|
|
211
|
+
));
|
|
212
|
+
}
|
|
213
|
+
if let Ok(duration) = <Obj<DurationWrap> as TryConvert>::try_convert(value) {
|
|
214
|
+
return Ok(CelValue::Duration(duration.inner));
|
|
215
|
+
}
|
|
216
|
+
if value.is_kind_of(ruby.class_array()) {
|
|
217
|
+
let array = <magnus::RArray as TryConvert>::try_convert(value)?;
|
|
218
|
+
let mut out = Vec::with_capacity(array.len());
|
|
219
|
+
for element in array.into_iter() {
|
|
220
|
+
out.push(ruby_to_cel_value(element)?);
|
|
221
|
+
}
|
|
222
|
+
return Ok(CelValue::List(Arc::new(out)));
|
|
223
|
+
}
|
|
224
|
+
if value.is_kind_of(ruby.class_hash()) {
|
|
225
|
+
let hash = <RHash as TryConvert>::try_convert(value)?;
|
|
226
|
+
let mut out = HashMap::new();
|
|
227
|
+
hash.foreach(|k: Value, v: Value| {
|
|
228
|
+
let key = if let Ok(s) = <String as TryConvert>::try_convert(k) {
|
|
229
|
+
cel::objects::Key::from(s)
|
|
230
|
+
} else if let Ok(sym) = <magnus::Symbol as TryConvert>::try_convert(k) {
|
|
231
|
+
cel::objects::Key::from(sym.name()?.to_string())
|
|
232
|
+
} else if let Ok(i) = <i64 as TryConvert>::try_convert(k) {
|
|
233
|
+
cel::objects::Key::from(i)
|
|
234
|
+
} else if let Ok(b) = <bool as TryConvert>::try_convert(k) {
|
|
235
|
+
cel::objects::Key::from(b)
|
|
236
|
+
} else {
|
|
237
|
+
return Err(errors::ty(
|
|
238
|
+
"Hash keys must be String/Symbol/Integer/Boolean",
|
|
239
|
+
));
|
|
240
|
+
};
|
|
241
|
+
out.insert(key, ruby_to_cel_value(v)?);
|
|
242
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
243
|
+
})?;
|
|
244
|
+
return Ok(CelValue::Map(cel::objects::Map { map: Arc::new(out) }));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
Err(errors::ty("Unsupported Ruby type"))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fn cel_to_ruby(ruby: &Ruby, value: &CelValue) -> Result<Value, Error> {
|
|
251
|
+
Ok(match value {
|
|
252
|
+
CelValue::Int(v) => (*v).into_value_with(ruby),
|
|
253
|
+
CelValue::UInt(v) => (*v).into_value_with(ruby),
|
|
254
|
+
CelValue::Float(v) => (*v).into_value_with(ruby),
|
|
255
|
+
CelValue::String(v) => v.to_string().into_value_with(ruby),
|
|
256
|
+
CelValue::Bytes(v) => ruby
|
|
257
|
+
.enc_str_new(v.as_slice(), ruby.ascii8bit_encoding())
|
|
258
|
+
.into_value_with(ruby),
|
|
259
|
+
CelValue::Bool(v) => (*v).into_value_with(ruby),
|
|
260
|
+
CelValue::Duration(v) => ruby
|
|
261
|
+
.obj_wrap(DurationWrap { inner: *v })
|
|
262
|
+
.into_value_with(ruby),
|
|
263
|
+
CelValue::Timestamp(v) => (*v).into_value_with(ruby),
|
|
264
|
+
CelValue::Null => ruby.qnil().as_value(),
|
|
265
|
+
CelValue::List(v) => {
|
|
266
|
+
let ary = ruby.ary_new();
|
|
267
|
+
for element in v.iter() {
|
|
268
|
+
ary.push(cel_to_ruby(ruby, element)?)?;
|
|
269
|
+
}
|
|
270
|
+
ary.into_value_with(ruby)
|
|
271
|
+
}
|
|
272
|
+
CelValue::Map(v) => {
|
|
273
|
+
let hash = ruby.hash_new();
|
|
274
|
+
for (k, val) in v.map.iter() {
|
|
275
|
+
let key: Value = match k {
|
|
276
|
+
cel::objects::Key::Int(i) => (*i).into_value_with(ruby),
|
|
277
|
+
cel::objects::Key::Uint(u) => (*u).into_value_with(ruby),
|
|
278
|
+
cel::objects::Key::Bool(b) => (*b).into_value_with(ruby),
|
|
279
|
+
cel::objects::Key::String(s) => s.to_string().into_value_with(ruby),
|
|
280
|
+
};
|
|
281
|
+
hash.aset(key, cel_to_ruby(ruby, val)?)?;
|
|
282
|
+
}
|
|
283
|
+
hash.into_value_with(ruby)
|
|
284
|
+
}
|
|
285
|
+
_ => {
|
|
286
|
+
return Err(errors::ty(format!(
|
|
287
|
+
"Unsupported CEL value variant: {value:?}"
|
|
288
|
+
)))
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
impl DurationWrap {
|
|
294
|
+
fn new(seconds: f64) -> Result<Self, Error> {
|
|
295
|
+
if !seconds.is_finite() {
|
|
296
|
+
return Err(errors::ty("Duration seconds must be finite"));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let nanos = (seconds * 1_000_000_000.0).round();
|
|
300
|
+
if nanos < i64::MIN as f64 || nanos > i64::MAX as f64 {
|
|
301
|
+
return Err(errors::ty("Duration is out of range"));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
Ok(Self {
|
|
305
|
+
inner: chrono::Duration::nanoseconds(nanos as i64),
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fn total_seconds(&self) -> f64 {
|
|
310
|
+
let nanos = self.inner.num_nanoseconds().unwrap_or_else(|| {
|
|
311
|
+
if self.inner < chrono::Duration::zero() {
|
|
312
|
+
i64::MIN
|
|
313
|
+
} else {
|
|
314
|
+
i64::MAX
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
nanos as f64 / 1_000_000_000.0
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fn to_s(&self) -> String {
|
|
322
|
+
format!("{}s", self.total_seconds())
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
fn inspect(&self) -> String {
|
|
326
|
+
format!("#<CEL::Duration {}>", self.to_s())
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fn eq(&self, other: Obj<DurationWrap>) -> bool {
|
|
330
|
+
self.inner == other.inner
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
impl ContextWrap {
|
|
335
|
+
fn new(empty: bool) -> Self {
|
|
336
|
+
Self {
|
|
337
|
+
use_empty: empty,
|
|
338
|
+
variables: Mutex::new(HashMap::new()),
|
|
339
|
+
functions: Mutex::new(Vec::new()),
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
fn add_variable(&self, name: String, value: Value) -> Result<(), Error> {
|
|
344
|
+
self.variables
|
|
345
|
+
.lock()
|
|
346
|
+
.unwrap()
|
|
347
|
+
.insert(name, ruby_to_cel_value(value)?);
|
|
348
|
+
Ok(())
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
fn add_function(&self, name: String, proc: Proc) {
|
|
352
|
+
self.functions.lock().unwrap().push(FunctionRegistration {
|
|
353
|
+
name,
|
|
354
|
+
callback: Arc::new(CallbackFunction { proc }),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fn build_context(&self) -> CelContext<'static> {
|
|
359
|
+
let mut ctx = if self.use_empty {
|
|
360
|
+
CelContext::empty()
|
|
361
|
+
} else {
|
|
362
|
+
CelContext::default()
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
for (name, value) in self.variables.lock().unwrap().iter() {
|
|
366
|
+
ctx.add_variable_from_value(name.as_str(), value.clone());
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for registration in self.functions.lock().unwrap().iter() {
|
|
370
|
+
let callback = registration.callback.clone();
|
|
371
|
+
let function_name = registration.name.clone();
|
|
372
|
+
ctx.add_function(
|
|
373
|
+
&function_name,
|
|
374
|
+
move |ftx: &FunctionContext,
|
|
375
|
+
cel::extractors::Arguments(args): cel::extractors::Arguments|
|
|
376
|
+
-> ResolveResult {
|
|
377
|
+
let callback = callback.clone();
|
|
378
|
+
let this = ftx.this.clone();
|
|
379
|
+
let args = args.clone();
|
|
380
|
+
|
|
381
|
+
with_gvl(|| {
|
|
382
|
+
let ruby = Ruby::get().expect("ruby runtime");
|
|
383
|
+
let mut ruby_args = Vec::new();
|
|
384
|
+
|
|
385
|
+
if let Some(target) = this {
|
|
386
|
+
ruby_args.push(cel_to_ruby(&ruby, &target).map_err(|e| {
|
|
387
|
+
CelExecutionError::function_error(ftx.name, e.to_string())
|
|
388
|
+
})?);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for arg in args.iter() {
|
|
392
|
+
ruby_args.push(cel_to_ruby(&ruby, arg).map_err(|e| {
|
|
393
|
+
CelExecutionError::function_error(ftx.name, e.to_string())
|
|
394
|
+
})?);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let proc_result =
|
|
398
|
+
callback.proc.call(ruby_args.as_slice()).map_err(|e| {
|
|
399
|
+
CelExecutionError::function_error(
|
|
400
|
+
ftx.name,
|
|
401
|
+
format!("Ruby callback error: {e}"),
|
|
402
|
+
)
|
|
403
|
+
})?;
|
|
404
|
+
|
|
405
|
+
ruby_to_cel_value(proc_result)
|
|
406
|
+
.map_err(|e| CelExecutionError::function_error(ftx.name, e.to_string()))
|
|
407
|
+
})
|
|
408
|
+
},
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
ctx
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
impl ProgramWrap {
|
|
417
|
+
fn compile(source: String) -> Result<Self, Error> {
|
|
418
|
+
CelProgram::compile(&source)
|
|
419
|
+
.map(|inner| Self { inner })
|
|
420
|
+
.map_err(|e: ParseErrors| errors::parse(e.to_string()))
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
fn execute(&self) -> Result<Value, Error> {
|
|
424
|
+
self.execute_with_context_internal(&CelContext::default())
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
fn execute_with_context(&self, context: &ContextWrap) -> Result<Value, Error> {
|
|
428
|
+
self.execute_with_context_internal(&context.build_context())
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
fn execute_with_context_internal(&self, ctx: &CelContext<'_>) -> Result<Value, Error> {
|
|
432
|
+
let run = || self.inner.execute(ctx);
|
|
433
|
+
let result = panic::catch_unwind(AssertUnwindSafe(|| without_gvl(run)))
|
|
434
|
+
.map_err(|_| errors::execution("CEL execution panicked"))?;
|
|
435
|
+
|
|
436
|
+
let ruby = Ruby::get().expect("ruby runtime");
|
|
437
|
+
result
|
|
438
|
+
.map_err(|e| errors::execution(e.to_string()))
|
|
439
|
+
.and_then(|value| cel_to_ruby(&ruby, &value))
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
fn references(&self) -> Result<RHash, Error> {
|
|
443
|
+
let ruby = Ruby::get().expect("ruby runtime");
|
|
444
|
+
let refs = self.inner.references();
|
|
445
|
+
|
|
446
|
+
let vars = ruby.ary_new();
|
|
447
|
+
for var in refs.variables() {
|
|
448
|
+
vars.push(var)?;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
let funcs = ruby.ary_new();
|
|
452
|
+
for func in refs.functions() {
|
|
453
|
+
funcs.push(func)?;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let out = ruby.hash_new();
|
|
457
|
+
out.aset("variables", vars)?;
|
|
458
|
+
out.aset("functions", funcs)?;
|
|
459
|
+
Ok(out)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
fn expression(&self) -> String {
|
|
463
|
+
format!("{:?}", self.inner.expression())
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#[magnus::init]
|
|
468
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
469
|
+
let module = ruby.define_module("CEL")?;
|
|
470
|
+
errors::define(ruby, &module)?;
|
|
471
|
+
|
|
472
|
+
let context_class = module.define_class("Context", ruby.class_object())?;
|
|
473
|
+
context_class.define_singleton_method("new", function!(ContextWrap::new, 1))?;
|
|
474
|
+
context_class.define_method("add_variable", method!(ContextWrap::add_variable, 2))?;
|
|
475
|
+
context_class.define_method("[]=", method!(ContextWrap::add_variable, 2))?;
|
|
476
|
+
context_class.define_method("add_function", method!(ContextWrap::add_function, 2))?;
|
|
477
|
+
|
|
478
|
+
let duration_class = module.define_class("Duration", ruby.class_object())?;
|
|
479
|
+
duration_class.define_singleton_method("new", function!(DurationWrap::new, 1))?;
|
|
480
|
+
duration_class.define_method("total_seconds", method!(DurationWrap::total_seconds, 0))?;
|
|
481
|
+
duration_class.define_method("to_f", method!(DurationWrap::total_seconds, 0))?;
|
|
482
|
+
duration_class.define_method("to_s", method!(DurationWrap::to_s, 0))?;
|
|
483
|
+
duration_class.define_method("inspect", method!(DurationWrap::inspect, 0))?;
|
|
484
|
+
duration_class.define_method("==", method!(DurationWrap::eq, 1))?;
|
|
485
|
+
|
|
486
|
+
let program_class = module.define_class("Program", ruby.class_object())?;
|
|
487
|
+
program_class.define_singleton_method("compile", function!(ProgramWrap::compile, 1))?;
|
|
488
|
+
program_class.define_method("execute", method!(ProgramWrap::execute, 0))?;
|
|
489
|
+
program_class.define_method(
|
|
490
|
+
"execute_with_context",
|
|
491
|
+
method!(ProgramWrap::execute_with_context, 1),
|
|
492
|
+
)?;
|
|
493
|
+
program_class.define_method("references", method!(ProgramWrap::references, 0))?;
|
|
494
|
+
program_class.define_method("expression", method!(ProgramWrap::expression, 0))?;
|
|
495
|
+
|
|
496
|
+
module.define_singleton_method("compile", function!(ProgramWrap::compile, 1))?;
|
|
497
|
+
|
|
498
|
+
Ok(())
|
|
499
|
+
}
|
data/lib/cel/version.rb
ADDED
data/lib/cel.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cel/version"
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
RUBY_VERSION =~ /(\d+\.\d+)/
|
|
7
|
+
require "cel/#{Regexp.last_match(1)}/cel"
|
|
8
|
+
rescue LoadError
|
|
9
|
+
require "cel/cel"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module CEL
|
|
13
|
+
class Context
|
|
14
|
+
class << self
|
|
15
|
+
alias_method :__native_new, :new
|
|
16
|
+
|
|
17
|
+
def new(empty = false)
|
|
18
|
+
__native_new(!!empty)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build(empty: false, **variables)
|
|
22
|
+
ctx = new(empty)
|
|
23
|
+
variables.each { |k, v| ctx.add_variable(k.to_s, v) }
|
|
24
|
+
yield(ctx) if block_given?
|
|
25
|
+
ctx
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def define_function(name, &block)
|
|
30
|
+
raise ArgumentError, "block required" unless block
|
|
31
|
+
|
|
32
|
+
add_function(name.to_s, block)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Program
|
|
37
|
+
alias_method :__native_execute, :execute
|
|
38
|
+
|
|
39
|
+
def execute(context = nil)
|
|
40
|
+
return __native_execute if context.nil?
|
|
41
|
+
|
|
42
|
+
execute_with_context(context)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def call(context = nil)
|
|
46
|
+
execute(context)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/spec/cel_spec.rb
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
RSpec.describe CEL do
|
|
7
|
+
describe ".compile" do
|
|
8
|
+
it "compiles and executes simple expressions" do
|
|
9
|
+
program = CEL.compile("1 + 1")
|
|
10
|
+
expect(program.execute).to eq(2)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "raises parse errors for invalid programs" do
|
|
14
|
+
expect { CEL.compile("1 +") }.to raise_error(CEL::ParseError)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe CEL::Context do
|
|
19
|
+
it "supports ruby variables for basic types" do
|
|
20
|
+
context = CEL::Context.new
|
|
21
|
+
context.add_variable("nil_value", nil)
|
|
22
|
+
context.add_variable("flag", true)
|
|
23
|
+
context.add_variable("num", 41)
|
|
24
|
+
context.add_variable("float_num", 1.5)
|
|
25
|
+
context.add_variable("name", "cel")
|
|
26
|
+
context.add_variable("ary", [1, 2, 3])
|
|
27
|
+
context.add_variable("obj", {"a" => 1, :b => 2})
|
|
28
|
+
|
|
29
|
+
expect(CEL.compile("nil_value == null").execute(context)).to eq(true)
|
|
30
|
+
expect(CEL.compile("flag && true").execute(context)).to eq(true)
|
|
31
|
+
expect(CEL.compile("num + 1").execute(context)).to eq(42)
|
|
32
|
+
expect(CEL.compile("float_num + 0.5").execute(context)).to eq(2.0)
|
|
33
|
+
expect(CEL.compile("name.startsWith('c')").execute(context)).to eq(true)
|
|
34
|
+
expect(CEL.compile("ary[2]").execute(context)).to eq(3)
|
|
35
|
+
expect(CEL.compile("obj.a + obj.b").execute(context)).to eq(3)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "supports ruby values for CEL bytes, timestamp, and duration types" do
|
|
39
|
+
context = CEL::Context.build(
|
|
40
|
+
bytes: "abc".b,
|
|
41
|
+
at: Time.utc(2023, 5, 29),
|
|
42
|
+
delay: CEL::Duration.new(90)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
expect(CEL.compile("bytes == b'abc'").execute(context)).to eq(true)
|
|
46
|
+
expect(CEL.compile("at == timestamp('2023-05-29T00:00:00Z')").execute(context)).to eq(true)
|
|
47
|
+
expect(CEL.compile("delay == duration('90s')").execute(context)).to eq(true)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "registers ruby functions and supports variadic calls" do
|
|
51
|
+
context = CEL::Context.new
|
|
52
|
+
context.define_function("sum") do |*values|
|
|
53
|
+
values.flatten.sum
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
expect(CEL.compile("sum(1, 2, 3)").execute(context)).to eq(6)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "passes method receiver as first block arg for method calls" do
|
|
60
|
+
context = CEL::Context.new(true)
|
|
61
|
+
context.define_function("startsWith") { |target, prefix| target.start_with?(prefix) }
|
|
62
|
+
|
|
63
|
+
expect(CEL.compile("'hello'.startsWith('he')").execute(context)).to eq(true)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "raises clear type error for unsupported variable types" do
|
|
67
|
+
context = CEL::Context.new
|
|
68
|
+
expect { context.add_variable("bad", Object.new) }.to raise_error(CEL::TypeError)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe CEL::Program do
|
|
73
|
+
it "exposes references" do
|
|
74
|
+
program = CEL.compile("size(foo) > 0")
|
|
75
|
+
refs = program.references
|
|
76
|
+
expect(refs["variables"]).to include("foo")
|
|
77
|
+
expect(refs["functions"]).to include("size")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "ports core CEL suite behavior examples" do
|
|
81
|
+
tests = {
|
|
82
|
+
"size([1,2,3]) == 3" => true,
|
|
83
|
+
"[1,2,3].map(x, x * 2)" => [2, 4, 6],
|
|
84
|
+
"[1,2,3].filter(x, x > 1)" => [2, 3],
|
|
85
|
+
"[1,2,3].exists(x, x == 2)" => true,
|
|
86
|
+
"[1,2,3].all(x, x > 0)" => true,
|
|
87
|
+
"{'a': 1}.contains('a')" => true,
|
|
88
|
+
"optional.of(1).hasValue()" => true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
tests.each do |expr, expected|
|
|
92
|
+
expect(CEL.compile(expr).execute).to eq(expected)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "marshals current CEL scalar value variants back to ruby" do
|
|
97
|
+
bytes = CEL.compile("b'abc'").execute
|
|
98
|
+
expect(bytes).to eq("abc".b)
|
|
99
|
+
expect(bytes.encoding).to eq(Encoding::ASCII_8BIT)
|
|
100
|
+
|
|
101
|
+
timestamp = CEL.compile("timestamp('2023-05-29T00:00:00Z')").execute
|
|
102
|
+
expect(timestamp).to be_a(Time)
|
|
103
|
+
expect(timestamp.getutc.iso8601).to eq("2023-05-29T00:00:00Z")
|
|
104
|
+
|
|
105
|
+
duration = CEL.compile("duration('1m30s')").execute
|
|
106
|
+
expect(duration).to be_a(CEL::Duration)
|
|
107
|
+
expect(duration.total_seconds).to eq(90.0)
|
|
108
|
+
expect(duration).to eq(CEL::Duration.new(90))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "returns execution errors as CEL::ExecutionError" do
|
|
112
|
+
program = CEL.compile("1 / 0")
|
|
113
|
+
expect { program.execute }.to raise_error(CEL::ExecutionError)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "releases GVL so other ruby threads can progress" do
|
|
117
|
+
context = CEL::Context.new
|
|
118
|
+
context.add_variable("items", Array.new(15_000, 1))
|
|
119
|
+
program = CEL.compile("items.map(x, x + 1)")
|
|
120
|
+
|
|
121
|
+
marker = Queue.new
|
|
122
|
+
worker = Thread.new do
|
|
123
|
+
100.times do
|
|
124
|
+
marker << :tick
|
|
125
|
+
sleep(0.001)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
runner = Thread.new { program.execute(context) }
|
|
130
|
+
|
|
131
|
+
sleep(0.01)
|
|
132
|
+
expect(marker.size).to be > 0
|
|
133
|
+
|
|
134
|
+
runner.join
|
|
135
|
+
worker.join
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "is thread-safe when executing concurrently" do
|
|
139
|
+
context = CEL::Context.new
|
|
140
|
+
context.add_variable("n", 10)
|
|
141
|
+
program = CEL.compile("n * 2")
|
|
142
|
+
|
|
143
|
+
threads = Array.new(8) { Thread.new { 50.times.map { program.execute(context) } } }
|
|
144
|
+
values = threads.flat_map(&:value)
|
|
145
|
+
|
|
146
|
+
expect(values.uniq).to eq([20])
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cel-rs-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- CEL Ruby Contributors
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rb_sys
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.9.128
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.9.128
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.4'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.4'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake-compiler
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.3'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.3'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.13'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.13'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: simplecov
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.22'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.22'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: standard
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.55'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.55'
|
|
96
|
+
description: Robust Ruby bindings to the Rust CEL implementation using Magnus
|
|
97
|
+
email: []
|
|
98
|
+
executables: []
|
|
99
|
+
extensions:
|
|
100
|
+
- ext/cel/extconf.rb
|
|
101
|
+
extra_rdoc_files: []
|
|
102
|
+
files:
|
|
103
|
+
- Cargo.toml
|
|
104
|
+
- LICENSE
|
|
105
|
+
- README.md
|
|
106
|
+
- ext/cel/Cargo.toml
|
|
107
|
+
- ext/cel/extconf.rb
|
|
108
|
+
- ext/cel/src/lib.rs
|
|
109
|
+
- lib/cel.rb
|
|
110
|
+
- lib/cel/version.rb
|
|
111
|
+
- spec/cel_spec.rb
|
|
112
|
+
- spec/spec_helper.rb
|
|
113
|
+
homepage: https://github.com/catkins/cel-rs-rb
|
|
114
|
+
licenses:
|
|
115
|
+
- MIT
|
|
116
|
+
metadata:
|
|
117
|
+
homepage_uri: https://github.com/catkins/cel-rs-rb
|
|
118
|
+
source_code_uri: https://github.com/catkins/cel-rs-rb
|
|
119
|
+
rubygems_mfa_required: 'true'
|
|
120
|
+
rdoc_options: []
|
|
121
|
+
require_paths:
|
|
122
|
+
- lib
|
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
124
|
+
requirements:
|
|
125
|
+
- - ">="
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
version: 3.3.0
|
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
129
|
+
requirements:
|
|
130
|
+
- - ">="
|
|
131
|
+
- !ruby/object:Gem::Version
|
|
132
|
+
version: '0'
|
|
133
|
+
requirements: []
|
|
134
|
+
rubygems_version: 4.0.10
|
|
135
|
+
specification_version: 4
|
|
136
|
+
summary: Ruby bindings for the Rust CEL crate
|
|
137
|
+
test_files: []
|