Skip to content

Commit 58b4156

Browse files
authored
Fix macro invocation and add more examples (#64)
* Document some public functions and modify macros interface a bit * fix * Add few more examples * rebase * Fix macro * fix env
1 parent 8437278 commit 58b4156

File tree

7 files changed

+346
-34
lines changed

7 files changed

+346
-34
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ done
3535

3636
or run directly with `cargo run --example <name>` for interactive experimentation.
3737

38+
### Example overview
39+
40+
| Example | Highlights |
41+
| --- | --- |
42+
| `simple` | Basic expression evaluation, child runtimes, and scalar values. |
43+
| `create_function` | Registering function declarations plus native Rust implementations. |
44+
| `concurrency` | Sharing a runtime builder across threads for parallel evaluation. |
45+
| `user_role_derive` | Using the `derive` feature to expose Rust structs and methods to CEL. |
46+
| `comprehensions` | Practical use of CEL macros like `exists`, `filter`, `map`, and `all`. |
47+
| `env_snapshot` | Building, serializing, and rehydrating a reusable environment snapshot. |
48+
3849
## CLI
3950

4051
The `cellang-cli` crate exposes a developer-friendly command line interface for inspecting CEL programs.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use cellang::Runtime;
2+
use cellang::value::{IntoValue, ListValue, MapValue, Value};
3+
use miette::Result;
4+
5+
fn main() -> Result<()> {
6+
let mut builder = Runtime::builder();
7+
builder.set_variable("assets", sample_assets())?;
8+
let runtime = builder.build();
9+
10+
let has_high_risk =
11+
runtime.eval("assets.exists(asset, asset.risk >= 75)")?;
12+
assert_eq!(has_high_risk, Value::Bool(true));
13+
14+
let prod_names = runtime.eval(
15+
"assets.filter(asset, 'prod' in asset.tags).map(asset, asset.name)",
16+
)?;
17+
assert_eq!(prod_names, vec!["scanner", "api"].into_value());
18+
19+
let all_positive =
20+
runtime.eval("assets.map(asset, asset.risk).all(risk, risk >= 0)")?;
21+
assert_eq!(all_positive, Value::Bool(true));
22+
23+
Ok(())
24+
}
25+
26+
fn sample_assets() -> Value {
27+
let entries = vec![
28+
asset("scanner", 80, &["prod", "pci"]),
29+
asset("api", 65, &["prod"]),
30+
asset("etl", 40, &["batch"]),
31+
];
32+
Value::List(ListValue::from(entries))
33+
}
34+
35+
fn asset(name: &str, risk: i64, tags: &[&str]) -> Value {
36+
let mut record = MapValue::new();
37+
record.insert("name", name);
38+
record.insert("risk", risk);
39+
record.insert(
40+
"tags",
41+
ListValue::from(tags.iter().copied().collect::<Vec<_>>()),
42+
);
43+
Value::Map(record)
44+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use cellang::types::{
2+
FieldDecl, FunctionDecl, IdentDecl, NamedType, OverloadDecl, StructType,
3+
Type,
4+
};
5+
use cellang::value::{ListValue, StructValue, Value};
6+
use cellang::{Env, Runtime};
7+
use miette::{IntoDiagnostic, Result};
8+
9+
fn main() -> Result<()> {
10+
let env_cache =
11+
serde_json::to_vec(&build_policy_env()).into_diagnostic()?;
12+
13+
let scenarios =
14+
[("analytics", "write", true), ("security", "delete", false)];
15+
16+
for (owner, desired_access, expected) in scenarios {
17+
let env: Env = serde_json::from_slice(&env_cache).into_diagnostic()?;
18+
let mut builder = Runtime::builder();
19+
builder.import_env_owned(env)?;
20+
builder.register_function("same_tenant", same_tenant)?;
21+
builder.set_variable("request", request_value())?;
22+
builder.set_variable("resource_owner", owner)?;
23+
builder.set_variable("desired_access", desired_access)?;
24+
let runtime = builder.build();
25+
26+
let decision = runtime.eval(
27+
"same_tenant(request.subject, resource_owner) && \
28+
request.resource.accesses.exists(access, access == desired_access)",
29+
)?;
30+
assert_eq!(decision, Value::Bool(expected));
31+
}
32+
33+
Ok(())
34+
}
35+
36+
fn build_policy_env() -> Env {
37+
let mut builder = Env::builder();
38+
39+
let mut resource = StructType::new("example.Resource")
40+
.with_doc("Resource being evaluated in a policy");
41+
resource
42+
.add_field("kind", FieldDecl::new(Type::String))
43+
.unwrap();
44+
resource
45+
.add_field(
46+
"accesses",
47+
FieldDecl::new(Type::list(Type::String))
48+
.with_doc("Allowed operations on the resource"),
49+
)
50+
.unwrap();
51+
52+
let mut request = StructType::new("example.AccessRequest")
53+
.with_doc("Wrapper around the incoming access request");
54+
request
55+
.add_field("subject", FieldDecl::new(Type::String))
56+
.unwrap();
57+
request
58+
.add_field(
59+
"resource",
60+
FieldDecl::new(Type::struct_type(resource.name.clone()))
61+
.with_doc("Resource being accessed"),
62+
)
63+
.unwrap();
64+
65+
builder.add_type(NamedType::Struct(resource)).unwrap();
66+
builder
67+
.add_type(NamedType::Struct(request.clone()))
68+
.unwrap();
69+
70+
builder
71+
.add_ident(
72+
IdentDecl::new("request", Type::struct_type(request.name.clone()))
73+
.with_doc("Access request provided at evaluation time"),
74+
)
75+
.unwrap();
76+
builder
77+
.add_ident(
78+
IdentDecl::new("resource_owner", Type::String)
79+
.with_doc("Tenant that owns the resource"),
80+
)
81+
.unwrap();
82+
builder
83+
.add_ident(
84+
IdentDecl::new("desired_access", Type::String)
85+
.with_doc("Operation requested by the caller"),
86+
)
87+
.unwrap();
88+
89+
builder.add_function(same_tenant_decl()).unwrap();
90+
91+
builder.build()
92+
}
93+
94+
fn same_tenant(requester: String, owner: String) -> bool {
95+
requester == owner
96+
}
97+
98+
fn same_tenant_decl() -> FunctionDecl {
99+
let mut decl = FunctionDecl::new("same_tenant")
100+
.with_doc("Compares two tenant identifiers");
101+
decl.add_overload(OverloadDecl::new(
102+
"same_tenant_string_string",
103+
vec![Type::String, Type::String],
104+
Type::Bool,
105+
))
106+
.unwrap();
107+
decl
108+
}
109+
110+
fn request_value() -> Value {
111+
let mut resource = StructValue::new("example.Resource");
112+
resource.set_field("kind", "dashboard");
113+
resource.set_field("accesses", ListValue::from(vec!["read", "write"]));
114+
115+
let mut request = StructValue::new("example.AccessRequest");
116+
request.set_field("subject", "analytics");
117+
request.set_field("resource", resource);
118+
119+
Value::Struct(request)
120+
}
File renamed without changes.

crates/cellang/src/interpreter.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -995,7 +995,7 @@ fn ensure_arity(
995995
mod tests {
996996
use super::*;
997997
use crate::runtime::Runtime;
998-
use crate::value::{StructValue, Value};
998+
use crate::value::{IntoValue, ListValue, MapValue, StructValue, Value};
999999

10001000
#[test]
10011001
fn evaluates_arithmetic_with_variables() {
@@ -1037,6 +1037,32 @@ mod tests {
10371037
);
10381038
}
10391039

1040+
#[test]
1041+
fn comprehension_macros_allow_function_calls() {
1042+
let runtime = runtime_with_assets();
1043+
1044+
let has_high_risk =
1045+
eval(&runtime, "exists(assets, asset, asset.risk >= 75)")
1046+
.expect("exists() macro should evaluate");
1047+
assert_eq!(has_high_risk, Value::Bool(true));
1048+
1049+
let prod_names = eval(&runtime, "map(assets, asset, asset.name)")
1050+
.expect("map() macro should evaluate");
1051+
assert_eq!(prod_names, vec!["scanner", "api", "etl"].into_value());
1052+
1053+
let filtered =
1054+
eval(&runtime, "filter(assets, asset, asset.risk >= 75)")
1055+
.expect("filter() macro should evaluate");
1056+
assert_eq!(
1057+
filtered,
1058+
Value::List(ListValue::from(vec![asset(
1059+
"scanner",
1060+
80,
1061+
&["prod", "pci"],
1062+
)])),
1063+
);
1064+
}
1065+
10401066
fn runtime_with_user() -> Runtime {
10411067
let mut builder = Runtime::builder();
10421068
let mut user = StructValue::new("example.User");
@@ -1047,4 +1073,31 @@ mod tests {
10471073
.expect("user variable to set");
10481074
builder.build()
10491075
}
1076+
1077+
fn runtime_with_assets() -> Runtime {
1078+
let mut builder = Runtime::builder();
1079+
builder
1080+
.set_variable("assets", sample_assets())
1081+
.expect("assets variable to set");
1082+
builder.build()
1083+
}
1084+
1085+
fn sample_assets() -> Value {
1086+
Value::List(ListValue::from(vec![
1087+
asset("scanner", 80, &["prod", "pci"]),
1088+
asset("api", 65, &["prod"]),
1089+
asset("etl", 40, &["batch"]),
1090+
]))
1091+
}
1092+
1093+
fn asset(name: &str, risk: i64, tags: &[&str]) -> Value {
1094+
let mut record = MapValue::new();
1095+
record.insert("name", name);
1096+
record.insert("risk", risk);
1097+
record.insert(
1098+
"tags",
1099+
ListValue::from(tags.iter().copied().collect::<Vec<_>>()),
1100+
);
1101+
Value::Map(record)
1102+
}
10501103
}

crates/cellang/src/macros.rs

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -113,49 +113,39 @@ pub fn detect_macro_call<'src>(
113113
ComprehensionKind::All,
114114
name,
115115
args,
116-
is_method,
117116
ExpectedArity::Fixed(3),
118-
true,
119117
)?)
120118
}
121119
"exists" if registry.is_enabled(MacroKind::Exists) => {
122120
Some(parse_comprehension(
123121
ComprehensionKind::Exists,
124122
name,
125123
args,
126-
is_method,
127124
ExpectedArity::Fixed(3),
128-
true,
129125
)?)
130126
}
131127
"exists_one" if registry.is_enabled(MacroKind::ExistsOne) => {
132128
Some(parse_comprehension(
133129
ComprehensionKind::ExistsOne,
134130
name,
135131
args,
136-
is_method,
137132
ExpectedArity::Fixed(3),
138-
true,
139133
)?)
140134
}
141135
"map" if registry.is_enabled(MacroKind::Map) => {
142136
Some(parse_comprehension(
143137
ComprehensionKind::Map,
144138
name,
145139
args,
146-
is_method,
147140
ExpectedArity::Either(3, 4),
148-
true,
149141
)?)
150142
}
151143
"filter" if registry.is_enabled(MacroKind::Filter) => {
152144
Some(parse_comprehension(
153145
ComprehensionKind::Filter,
154146
name,
155147
args,
156-
is_method,
157148
ExpectedArity::Fixed(3),
158-
true,
159149
)?)
160150
}
161151
_ => None,
@@ -205,16 +195,8 @@ fn parse_comprehension<'src>(
205195
kind: ComprehensionKind,
206196
macro_name: &str,
207197
args: &'src [TokenTree<'src>],
208-
is_method: bool,
209198
arity: ExpectedArity,
210-
require_method: bool,
211199
) -> Result<MacroInvocation<'src>, RuntimeError> {
212-
if require_method && !is_method {
213-
return Err(RuntimeError::new(format!(
214-
"Macro '{macro_name}' must be invoked using receiver call syntax"
215-
)));
216-
}
217-
218200
let len_ok = match arity {
219201
ExpectedArity::Fixed(size) => args.len() == size,
220202
ExpectedArity::Either(a, b) => args.len() == a || args.len() == b,

0 commit comments

Comments
 (0)