Skip to content

Commit 34383d2

Browse files
jnthntatumcopybara-github
authored andcommitted
Add support for importing/exporting common limits to YAML environment configs.
PiperOrigin-RevId: 874676207
1 parent e8079d0 commit 34383d2

10 files changed

Lines changed: 323 additions & 12 deletions

File tree

bundle/src/main/java/dev/cel/bundle/CelEnvironment.java

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import dev.cel.runtime.CelRuntimeLibrary;
5454
import java.util.Arrays;
5555
import java.util.Optional;
56+
import java.util.function.ObjIntConsumer;
5657

5758
/**
5859
* CelEnvironment is a native representation of a CEL environment for compiler and runtime. This
@@ -74,6 +75,24 @@ public abstract class CelEnvironment {
7475
"strings", CanonicalCelExtension.STRINGS,
7576
"comprehensions", CanonicalCelExtension.COMPREHENSIONS);
7677

78+
private static final ImmutableMap<String, ObjIntConsumer<CelOptions.Builder>> LIMIT_HANDLERS =
79+
ImmutableMap.of(
80+
"cel.limit.expression_code_points",
81+
(options, value) -> options.maxExpressionCodePointSize(value),
82+
"cel.limit.parse_error_recovery",
83+
(options, value) -> options.maxParseErrorRecoveryLimit(value),
84+
"cel.limit.parse_recursion_depth",
85+
(options, value) -> options.maxParseRecursionDepth(value));
86+
87+
private static final ImmutableMap<String, BooleanOptionConsumer> FEATURE_HANDLERS =
88+
ImmutableMap.of(
89+
"cel.feature.macro_call_tracking",
90+
(options, enabled) -> options.populateMacroCalls(enabled),
91+
"cel.feature.backtick_escape_syntax",
92+
(options, enabled) -> options.enableQuotedIdentifierSyntax(enabled),
93+
"cel.feature.cross_type_numeric_comparisons",
94+
(options, enabled) -> options.enableHeterogeneousNumericComparisons(enabled));
95+
7796
/** Environment source in textual format (ex: textproto, YAML). */
7897
public abstract Optional<Source> source();
7998

@@ -112,6 +131,9 @@ public abstract class CelEnvironment {
112131
/** Feature flags to enable in the environment. */
113132
public abstract ImmutableSet<FeatureFlag> features();
114133

134+
/** Limits to set in the environment. */
135+
public abstract ImmutableSet<Limit> limits();
136+
115137
/** Builder for {@link CelEnvironment}. */
116138
@AutoValue.Builder
117139
public abstract static class Builder {
@@ -168,7 +190,14 @@ public Builder setFeatures(FeatureFlag... featureFlags) {
168190
return setFeatures(ImmutableSet.copyOf(featureFlags));
169191
}
170192

171-
public abstract Builder setFeatures(ImmutableSet<FeatureFlag> macros);
193+
public abstract Builder setFeatures(ImmutableSet<FeatureFlag> featureFlags);
194+
195+
@CanIgnoreReturnValue
196+
public Builder setLimits(Limit... limits) {
197+
return setLimits(ImmutableSet.copyOf(limits));
198+
}
199+
200+
public abstract Builder setLimits(ImmutableSet<Limit> limits);
172201

173202
abstract CelEnvironment autoBuild();
174203

@@ -200,13 +229,14 @@ public static Builder newBuilder() {
200229
.setContainer(CelContainer.ofName(""))
201230
.setVariables(ImmutableSet.of())
202231
.setFunctions(ImmutableSet.of())
203-
.setFeatures(ImmutableSet.of());
232+
.setFeatures(ImmutableSet.of())
233+
.setLimits(ImmutableSet.of());
204234
}
205235

206236
/** Extends the provided {@link CelCompiler} environment with this configuration. */
207237
public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions)
208238
throws CelEnvironmentException {
209-
celOptions = applyFeatureFlags(celOptions);
239+
celOptions = applyEnvironmentOptions(celOptions);
210240
try {
211241
CelTypeProvider celTypeProvider = celCompiler.getTypeProvider();
212242
CelCompilerBuilder compilerBuilder =
@@ -236,7 +266,7 @@ public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions)
236266

237267
/** Extends the provided {@link Cel} environment with this configuration. */
238268
public Cel extend(Cel cel, CelOptions celOptions) throws CelEnvironmentException {
239-
celOptions = applyFeatureFlags(celOptions);
269+
celOptions = applyEnvironmentOptions(celOptions);
240270
try {
241271
// Casting is necessary to only extend the compiler here
242272
CelCompiler celCompiler = extend((CelCompiler) cel, celOptions);
@@ -249,18 +279,22 @@ public Cel extend(Cel cel, CelOptions celOptions) throws CelEnvironmentException
249279
}
250280
}
251281

252-
private CelOptions applyFeatureFlags(CelOptions celOptions) {
282+
private CelOptions applyEnvironmentOptions(CelOptions celOptions) {
253283
CelOptions.Builder optionsBuilder = celOptions.toBuilder();
254284
for (FeatureFlag featureFlag : features()) {
255-
if (featureFlag.name().equals("cel.feature.macro_call_tracking")) {
256-
optionsBuilder.populateMacroCalls(featureFlag.enabled());
257-
} else if (featureFlag.name().equals("cel.feature.backtick_escape_syntax")) {
258-
optionsBuilder.enableQuotedIdentifierSyntax(featureFlag.enabled());
259-
} else if (featureFlag.name().equals("cel.feature.cross_type_numeric_comparisons")) {
260-
optionsBuilder.enableHeterogeneousNumericComparisons(featureFlag.enabled());
261-
} else {
285+
BooleanOptionConsumer consumer = FEATURE_HANDLERS.get(featureFlag.name());
286+
if (consumer == null) {
262287
throw new IllegalArgumentException("Unknown feature flag: " + featureFlag.name());
263288
}
289+
consumer.accept(optionsBuilder, featureFlag.enabled());
290+
}
291+
for (Limit limit : limits()) {
292+
int value = limit.value() < 0 ? -1 : limit.value();
293+
ObjIntConsumer<CelOptions.Builder> consumer = LIMIT_HANDLERS.get(limit.name());
294+
if (consumer == null) {
295+
throw new IllegalArgumentException("Unknown limit: " + limit.name());
296+
}
297+
consumer.accept(optionsBuilder, value);
264298
}
265299
return optionsBuilder.build();
266300
}
@@ -672,6 +706,25 @@ public static FeatureFlag create(String name, boolean enabled) {
672706
}
673707
}
674708

709+
/**
710+
* Represents a configurable limit in the environment.
711+
*
712+
* <p>A negative value indicates no limit. If not specified, the limit should be set to the
713+
* library default.
714+
*/
715+
@AutoValue
716+
public abstract static class Limit {
717+
/** Normalized name of the limit (e.g. cel.limit.expression_code_points */
718+
public abstract String name();
719+
720+
/** The value of the limit, -1 means no limit. */
721+
public abstract int value();
722+
723+
public static Limit create(String name, int value) {
724+
return new AutoValue_CelEnvironment_Limit(name, value);
725+
}
726+
}
727+
675728
/**
676729
* Represents a configuration for a canonical CEL extension that can be enabled in the
677730
* environment.
@@ -995,4 +1048,9 @@ public static OverloadSelector.Builder newBuilder() {
9951048
}
9961049
}
9971050
}
1051+
1052+
@FunctionalInterface
1053+
private static interface BooleanOptionConsumer {
1054+
void accept(CelOptions.Builder options, boolean value);
1055+
}
9981056
}

bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,23 @@ private void addOptions(CelEnvironment.Builder envBuilder, CelOptions options) {
221221
featureFlags.add(CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true));
222222
}
223223
envBuilder.setFeatures(featureFlags.build());
224+
ImmutableSet.Builder<CelEnvironment.Limit> limits = ImmutableSet.builder();
225+
if (options.maxExpressionCodePointSize() != CelOptions.DEFAULT.maxExpressionCodePointSize()) {
226+
limits.add(
227+
CelEnvironment.Limit.create(
228+
"cel.limit.expression_code_points", options.maxExpressionCodePointSize()));
229+
}
230+
if (options.maxParseErrorRecoveryLimit() != CelOptions.DEFAULT.maxParseErrorRecoveryLimit()) {
231+
limits.add(
232+
CelEnvironment.Limit.create(
233+
"cel.limit.parse_error_recovery", options.maxParseErrorRecoveryLimit()));
234+
}
235+
if (options.maxParseRecursionDepth() != CelOptions.DEFAULT.maxParseRecursionDepth()) {
236+
limits.add(
237+
CelEnvironment.Limit.create(
238+
"cel.limit.parse_recursion_depth", options.maxParseRecursionDepth()));
239+
}
240+
envBuilder.setLimits(limits.build());
224241
}
225242

226243
/**

bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import dev.cel.common.formats.YamlHelper.YamlNodeType;
4444
import dev.cel.common.formats.YamlParserContextImpl;
4545
import dev.cel.common.internal.CelCodePointArray;
46+
import java.util.Optional;
4647
import org.jspecify.annotations.Nullable;
4748
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
4849
import org.yaml.snakeyaml.nodes.MappingNode;
@@ -188,6 +189,70 @@ private ImmutableSet<CelEnvironment.FeatureFlag> parseFeatures(
188189
return featureFlags.build();
189190
}
190191

192+
private ImmutableSet<CelEnvironment.Limit> parseLimits(ParserContext<Node> ctx, Node node) {
193+
long valueId = ctx.collectMetadata(node);
194+
if (!validateYamlType(node, YamlNodeType.LIST, YamlNodeType.TEXT)) {
195+
ctx.reportError(valueId, "Unsupported limits format");
196+
}
197+
198+
ImmutableSet.Builder<CelEnvironment.Limit> limits = ImmutableSet.builder();
199+
200+
SequenceNode featureListNode = (SequenceNode) node;
201+
for (Node featureMapNode : featureListNode.getValue()) {
202+
long featureMapId = ctx.collectMetadata(featureMapNode);
203+
if (!assertYamlType(ctx, featureMapId, featureMapNode, YamlNodeType.MAP)) {
204+
continue;
205+
}
206+
207+
MappingNode featureMap = (MappingNode) featureMapNode;
208+
String name = "";
209+
Optional<Integer> value = Optional.empty();
210+
// Shorthand syntax for limit: "cel.limit.foo: 1"
211+
if (featureMap.getValue().size() == 1) {
212+
NodeTuple nodeTuple = featureMap.getValue().get(0);
213+
Node keyNode = nodeTuple.getKeyNode();
214+
Node valueNode = nodeTuple.getValueNode();
215+
String keyName = ((ScalarNode) keyNode).getValue();
216+
if (!keyName.equals("name") && !keyName.equals("value")) {
217+
limits.add(CelEnvironment.Limit.create(keyName, newInteger(ctx, valueNode)));
218+
continue;
219+
}
220+
// Fall through to check against the long syntax.
221+
}
222+
// Long syntax for limit:
223+
// limits:
224+
// - name: cel.limit.foo
225+
// value: 1
226+
for (NodeTuple nodeTuple : featureMap.getValue()) {
227+
Node keyNode = nodeTuple.getKeyNode();
228+
long keyId = ctx.collectMetadata(keyNode);
229+
Node valueNode = nodeTuple.getValueNode();
230+
String keyName = ((ScalarNode) keyNode).getValue();
231+
switch (keyName) {
232+
case "name":
233+
name = newString(ctx, valueNode);
234+
break;
235+
case "value":
236+
value = Optional.of(newInteger(ctx, valueNode));
237+
break;
238+
default:
239+
ctx.reportError(keyId, String.format("Unsupported limits tag: %s", keyName));
240+
break;
241+
}
242+
}
243+
if (name.isEmpty()) {
244+
ctx.reportError(featureMapId, "Missing required attribute(s): name");
245+
continue;
246+
}
247+
if (!value.isPresent()) {
248+
ctx.reportError(featureMapId, "Missing required attribute(s): value");
249+
continue;
250+
}
251+
limits.add(CelEnvironment.Limit.create(name, value.get()));
252+
}
253+
return limits.build();
254+
}
255+
191256
private ImmutableSet<Alias> parseAliases(ParserContext<Node> ctx, Node node) {
192257
ImmutableSet.Builder<Alias> aliasSetBuilder = ImmutableSet.builder();
193258
long valueId = ctx.collectMetadata(node);
@@ -804,6 +869,9 @@ private CelEnvironment.Builder parseConfig(ParserContext<Node> ctx, Node node) {
804869
case "features":
805870
builder.setFeatures(parseFeatures(ctx, valueNode));
806871
break;
872+
case "limits":
873+
builder.setLimits(parseLimits(ctx, valueNode));
874+
break;
807875
default:
808876
ctx.reportError(id, "Unknown config tag: " + fieldName);
809877
// continue handling the rest of the nodes

bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ private CelEnvironmentYamlSerializer() {
6161
this.multiRepresenters.put(CelEnvironment.Alias.class, new RepresentAlias());
6262
this.multiRepresenters.put(CelContainer.class, new RepresentContainer());
6363
this.multiRepresenters.put(CelEnvironment.FeatureFlag.class, new RepresentFeatureFlag());
64+
this.multiRepresenters.put(CelEnvironment.Limit.class, new RepresentLimit());
6465
}
6566

6667
public static String toYaml(CelEnvironment environment) {
@@ -98,6 +99,9 @@ public Node representData(Object data) {
9899
if (!environment.features().isEmpty()) {
99100
configMap.put("features", environment.features().asList());
100101
}
102+
if (!environment.limits().isEmpty()) {
103+
configMap.put("limits", environment.limits().asList());
104+
}
101105
return represent(configMap.buildOrThrow());
102106
}
103107
}
@@ -275,4 +279,17 @@ public Node representData(Object data) {
275279
.buildOrThrow());
276280
}
277281
}
282+
283+
private final class RepresentLimit implements Represent {
284+
285+
@Override
286+
public Node representData(Object data) {
287+
CelEnvironment.Limit limit = (CelEnvironment.Limit) data;
288+
return represent(
289+
ImmutableMap.builder()
290+
.put("name", limit.name())
291+
.put("value", limit.value() < 0 ? -1 : limit.value())
292+
.buildOrThrow());
293+
}
294+
}
278295
}

bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,5 +260,34 @@ public void container() {
260260
assertThat(container.abbreviations()).containsExactly("foo.Bar", "baz.Qux").inOrder();
261261
assertThat(container.aliases()).containsAtLeast("nm", "user.name", "id", "user.id").inOrder();
262262
}
263+
264+
@Test
265+
public void options() {
266+
Cel cel =
267+
CelFactory.standardCelBuilder()
268+
.setOptions(
269+
CelOptions.current()
270+
.maxExpressionCodePointSize(100)
271+
.maxParseErrorRecoveryLimit(10)
272+
.maxParseRecursionDepth(10)
273+
.enableQuotedIdentifierSyntax(true)
274+
.enableHeterogeneousNumericComparisons(true)
275+
.populateMacroCalls(true)
276+
.build())
277+
.build();
278+
279+
CelEnvironmentExporter exporter = CelEnvironmentExporter.newBuilder().build();
280+
CelEnvironment celEnvironment = exporter.export(cel);
281+
assertThat(celEnvironment.features())
282+
.containsExactly(
283+
CelEnvironment.FeatureFlag.create("cel.feature.backtick_escape_syntax", true),
284+
CelEnvironment.FeatureFlag.create("cel.feature.cross_type_numeric_comparisons", true),
285+
CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true));
286+
assertThat(celEnvironment.limits())
287+
.containsExactly(
288+
CelEnvironment.Limit.create("cel.limit.expression_code_points", 100),
289+
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
290+
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10));
291+
}
263292
}
264293

bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,37 @@ public void extend_allFeatureFlags() throws Exception {
124124
assertThat(result).isTrue();
125125
}
126126

127+
@Test
128+
public void extend_allLimits() throws Exception {
129+
CelEnvironment environment =
130+
CelEnvironment.newBuilder()
131+
.setLimits(
132+
CelEnvironment.Limit.create("cel.limit.expression_code_points", 20),
133+
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
134+
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10))
135+
.build();
136+
137+
Cel cel =
138+
environment.extend(
139+
CelFactory.standardCelBuilder()
140+
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
141+
.build(),
142+
CelOptions.DEFAULT);
143+
CelOptions checkerOptions = cel.toCheckerBuilder().options();
144+
assertThat(checkerOptions.maxExpressionCodePointSize()).isEqualTo(20);
145+
assertThat(checkerOptions.maxParseErrorRecoveryLimit()).isEqualTo(10);
146+
assertThat(checkerOptions.maxParseRecursionDepth()).isEqualTo(10);
147+
148+
CelAbstractSyntaxTree ast = cel.compile("1 + 2 + 3 + 4 + 5").getAst();
149+
Long result = (Long) cel.createProgram(ast).eval();
150+
assertThat(result).isEqualTo(15L);
151+
152+
CelValidationResult validationResult = cel.compile("1 + 2 + 3 + 4 + 5 + 6");
153+
assertThat(validationResult.hasError()).isTrue();
154+
assertThat(validationResult.getErrorString())
155+
.contains("expression code point size exceeds limit: size: 21, limit 20");
156+
}
157+
127158
@Test
128159
public void extend_unsupportedFeatureFlag_throws() throws Exception {
129160
CelEnvironment environment =
@@ -143,6 +174,25 @@ public void extend_unsupportedFeatureFlag_throws() throws Exception {
143174
assertThat(e).hasMessageThat().contains("Unknown feature flag: unknown.feature");
144175
}
145176

177+
@Test
178+
public void extend_unsupportedLimit_throws() throws Exception {
179+
CelEnvironment environment =
180+
CelEnvironment.newBuilder()
181+
.setLimits(CelEnvironment.Limit.create("unknown.limit", 5))
182+
.build();
183+
184+
IllegalArgumentException e =
185+
assertThrows(
186+
IllegalArgumentException.class,
187+
() ->
188+
environment.extend(
189+
CelFactory.standardCelBuilder()
190+
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
191+
.build(),
192+
CelOptions.DEFAULT));
193+
assertThat(e).hasMessageThat().contains("Unknown limit: unknown.limit");
194+
}
195+
146196
@Test
147197
public void extensionVersion_specific() throws Exception {
148198
CelEnvironment environment =

0 commit comments

Comments
 (0)