Add support for importing/exporting common limits to YAML environment configs. by copybara-service[bot] · Pull Request #971 · cel-expr/cel-java · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 70 additions & 12 deletions bundle/src/main/java/dev/cel/bundle/CelEnvironment.java
17 changes: 17 additions & 0 deletions bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,23 @@ private void addOptions(CelEnvironment.Builder envBuilder, CelOptions options) {
featureFlags.add(CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true));
}
envBuilder.setFeatures(featureFlags.build());
ImmutableSet.Builder<CelEnvironment.Limit> limits = ImmutableSet.builder();
if (options.maxExpressionCodePointSize() != CelOptions.DEFAULT.maxExpressionCodePointSize()) {
limits.add(
CelEnvironment.Limit.create(
"cel.limit.expression_code_points", options.maxExpressionCodePointSize()));
}
if (options.maxParseErrorRecoveryLimit() != CelOptions.DEFAULT.maxParseErrorRecoveryLimit()) {
limits.add(
CelEnvironment.Limit.create(
"cel.limit.parse_error_recovery", options.maxParseErrorRecoveryLimit()));
}
if (options.maxParseRecursionDepth() != CelOptions.DEFAULT.maxParseRecursionDepth()) {
limits.add(
CelEnvironment.Limit.create(
"cel.limit.parse_recursion_depth", options.maxParseRecursionDepth()));
}
envBuilder.setLimits(limits.build());
}

/**
Expand Down
68 changes: 68 additions & 0 deletions bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import dev.cel.common.formats.YamlHelper.YamlNodeType;
import dev.cel.common.formats.YamlParserContextImpl;
import dev.cel.common.internal.CelCodePointArray;
import java.util.Optional;
import org.jspecify.annotations.Nullable;
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
import org.yaml.snakeyaml.nodes.MappingNode;
Expand Down Expand Up @@ -188,6 +189,70 @@ private ImmutableSet<CelEnvironment.FeatureFlag> parseFeatures(
return featureFlags.build();
}

private ImmutableSet<CelEnvironment.Limit> parseLimits(ParserContext<Node> ctx, Node node) {
long valueId = ctx.collectMetadata(node);
if (!validateYamlType(node, YamlNodeType.LIST, YamlNodeType.TEXT)) {
ctx.reportError(valueId, "Unsupported limits format");
}

ImmutableSet.Builder<CelEnvironment.Limit> limits = ImmutableSet.builder();

SequenceNode featureListNode = (SequenceNode) node;
for (Node featureMapNode : featureListNode.getValue()) {
long featureMapId = ctx.collectMetadata(featureMapNode);
if (!assertYamlType(ctx, featureMapId, featureMapNode, YamlNodeType.MAP)) {
continue;
}

MappingNode featureMap = (MappingNode) featureMapNode;
String name = "";
Optional<Integer> value = Optional.empty();
// Shorthand syntax for limit: "cel.limit.foo: 1"
if (featureMap.getValue().size() == 1) {
NodeTuple nodeTuple = featureMap.getValue().get(0);
Node keyNode = nodeTuple.getKeyNode();
Node valueNode = nodeTuple.getValueNode();
String keyName = ((ScalarNode) keyNode).getValue();
if (!keyName.equals("name") && !keyName.equals("value")) {
limits.add(CelEnvironment.Limit.create(keyName, newInteger(ctx, valueNode)));
continue;
}
// Fall through to check against the long syntax.
}
// Long syntax for limit:
// limits:
// - name: cel.limit.foo
// value: 1
for (NodeTuple nodeTuple : featureMap.getValue()) {
Node keyNode = nodeTuple.getKeyNode();
long keyId = ctx.collectMetadata(keyNode);
Node valueNode = nodeTuple.getValueNode();
String keyName = ((ScalarNode) keyNode).getValue();
switch (keyName) {
case "name":
name = newString(ctx, valueNode);
break;
case "value":
value = Optional.of(newInteger(ctx, valueNode));
break;
default:
ctx.reportError(keyId, String.format("Unsupported limits tag: %s", keyName));
break;
}
}
if (name.isEmpty()) {
ctx.reportError(featureMapId, "Missing required attribute(s): name");
continue;
}
if (!value.isPresent()) {
ctx.reportError(featureMapId, "Missing required attribute(s): value");
continue;
}
limits.add(CelEnvironment.Limit.create(name, value.get()));
}
return limits.build();
}

private ImmutableSet<Alias> parseAliases(ParserContext<Node> ctx, Node node) {
ImmutableSet.Builder<Alias> aliasSetBuilder = ImmutableSet.builder();
long valueId = ctx.collectMetadata(node);
Expand Down Expand Up @@ -804,6 +869,9 @@ private CelEnvironment.Builder parseConfig(ParserContext<Node> ctx, Node node) {
case "features":
builder.setFeatures(parseFeatures(ctx, valueNode));
break;
case "limits":
builder.setLimits(parseLimits(ctx, valueNode));
break;
default:
ctx.reportError(id, "Unknown config tag: " + fieldName);
// continue handling the rest of the nodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ private CelEnvironmentYamlSerializer() {
this.multiRepresenters.put(CelEnvironment.Alias.class, new RepresentAlias());
this.multiRepresenters.put(CelContainer.class, new RepresentContainer());
this.multiRepresenters.put(CelEnvironment.FeatureFlag.class, new RepresentFeatureFlag());
this.multiRepresenters.put(CelEnvironment.Limit.class, new RepresentLimit());
}

public static String toYaml(CelEnvironment environment) {
Expand Down Expand Up @@ -98,6 +99,9 @@ public Node representData(Object data) {
if (!environment.features().isEmpty()) {
configMap.put("features", environment.features().asList());
}
if (!environment.limits().isEmpty()) {
configMap.put("limits", environment.limits().asList());
}
return represent(configMap.buildOrThrow());
}
}
Expand Down Expand Up @@ -275,4 +279,17 @@ public Node representData(Object data) {
.buildOrThrow());
}
}

private final class RepresentLimit implements Represent {

@Override
public Node representData(Object data) {
CelEnvironment.Limit limit = (CelEnvironment.Limit) data;
return represent(
ImmutableMap.builder()
.put("name", limit.name())
.put("value", limit.value() < 0 ? -1 : limit.value())
.buildOrThrow());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -260,5 +260,34 @@ public void container() {
assertThat(container.abbreviations()).containsExactly("foo.Bar", "baz.Qux").inOrder();
assertThat(container.aliases()).containsAtLeast("nm", "user.name", "id", "user.id").inOrder();
}

@Test
public void options() {
Cel cel =
CelFactory.standardCelBuilder()
.setOptions(
CelOptions.current()
.maxExpressionCodePointSize(100)
.maxParseErrorRecoveryLimit(10)
.maxParseRecursionDepth(10)
.enableQuotedIdentifierSyntax(true)
.enableHeterogeneousNumericComparisons(true)
.populateMacroCalls(true)
.build())
.build();

CelEnvironmentExporter exporter = CelEnvironmentExporter.newBuilder().build();
CelEnvironment celEnvironment = exporter.export(cel);
assertThat(celEnvironment.features())
.containsExactly(
CelEnvironment.FeatureFlag.create("cel.feature.backtick_escape_syntax", true),
CelEnvironment.FeatureFlag.create("cel.feature.cross_type_numeric_comparisons", true),
CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true));
assertThat(celEnvironment.limits())
.containsExactly(
CelEnvironment.Limit.create("cel.limit.expression_code_points", 100),
CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10));
}
}

50 changes: 50 additions & 0 deletions bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java
Loading
Loading