diff --git a/README.md b/README.md
index b9cc5f3a9..b61d940f9 100644
--- a/README.md
+++ b/README.md
@@ -55,14 +55,14 @@ CEL-Java is available in Maven Central Repository. [Download the JARs here][8] o
dev.celcel
- 0.5.2
+ 0.6.0
```
**Gradle**
```gradle
-implementation 'dev.cel:cel:0.5.2'
+implementation 'dev.cel:cel:0.6.0'
```
Then run this example:
diff --git a/WORKSPACE b/WORKSPACE
index e965fca90..09fdb0443 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -73,8 +73,9 @@ maven_install(
"com.google.truth.extensions:truth-proto-extension:1.4.2",
"com.google.truth:truth:1.4.2",
"org.antlr:antlr4-runtime:" + ANTLR4_VERSION,
- "org.jspecify:jspecify:0.2.0",
+ "org.jspecify:jspecify:0.3.0",
"org.threeten:threeten-extra:1.8.0",
+ "org.yaml:snakeyaml:2.2",
],
repositories = [
"https://maven.google.com",
diff --git a/bundle/src/main/java/dev/cel/bundle/CelImpl.java b/bundle/src/main/java/dev/cel/bundle/CelImpl.java
index ad062d113..317eeb49c 100644
--- a/bundle/src/main/java/dev/cel/bundle/CelImpl.java
+++ b/bundle/src/main/java/dev/cel/bundle/CelImpl.java
@@ -87,6 +87,11 @@ public CelValidationResult check(CelAbstractSyntaxTree ast) {
return compiler.get().check(ast);
}
+ @Override
+ public CelTypeProvider getTypeProvider() {
+ return compiler.get().getTypeProvider();
+ }
+
@Override
public CelRuntime.Program createProgram(CelAbstractSyntaxTree ast) throws CelEvaluationException {
return runtime.get().createProgram(ast);
diff --git a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java
index f1b1dfb00..ef751df18 100644
--- a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java
+++ b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java
@@ -107,7 +107,7 @@
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/checker/src/main/java/dev/cel/checker/CelChecker.java b/checker/src/main/java/dev/cel/checker/CelChecker.java
index 4022212cb..077850be1 100644
--- a/checker/src/main/java/dev/cel/checker/CelChecker.java
+++ b/checker/src/main/java/dev/cel/checker/CelChecker.java
@@ -17,6 +17,7 @@
import com.google.errorprone.annotations.Immutable;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelValidationResult;
+import dev.cel.common.types.CelTypeProvider;
/** Public interface for type-checking parsed CEL expressions. */
@Immutable
@@ -29,5 +30,8 @@ public interface CelChecker {
*/
CelValidationResult check(CelAbstractSyntaxTree ast);
+ /** Returns the underlying type provider. */
+ CelTypeProvider getTypeProvider();
+
CelCheckerBuilder toCheckerBuilder();
}
diff --git a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java
index 950c919e3..a177b259d 100644
--- a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java
+++ b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java
@@ -75,6 +75,7 @@ public final class CelCheckerLegacyImpl implements CelChecker, EnvVisitable {
@SuppressWarnings("Immutable")
private final TypeProvider typeProvider;
+ private final CelTypeProvider celTypeProvider;
private final boolean standardEnvironmentEnabled;
// Builder is mutable by design. APIs must make defensive copies in and out of this class.
@@ -98,6 +99,11 @@ public CelValidationResult check(CelAbstractSyntaxTree ast) {
return new CelValidationResult(checkedAst, ImmutableList.of());
}
+ @Override
+ public CelTypeProvider getTypeProvider() {
+ return this.celTypeProvider;
+ }
+
@Override
public CelCheckerBuilder toCheckerBuilder() {
return new Builder(checkerBuilder);
@@ -422,6 +428,7 @@ public CelCheckerLegacyImpl build() {
functionDeclarations.build(),
Optional.fromNullable(expectedResultType),
legacyProvider,
+ messageTypeProvider,
standardEnvironmentEnabled,
this);
}
@@ -469,6 +476,7 @@ private CelCheckerLegacyImpl(
ImmutableSet functionDeclarations,
Optional expectedResultType,
TypeProvider typeProvider,
+ CelTypeProvider celTypeProvider,
boolean standardEnvironmentEnabled,
Builder checkerBuilder) {
this.celOptions = celOptions;
@@ -477,6 +485,7 @@ private CelCheckerLegacyImpl(
this.functionDeclarations = functionDeclarations;
this.expectedResultType = expectedResultType;
this.typeProvider = typeProvider;
+ this.celTypeProvider = celTypeProvider;
this.standardEnvironmentEnabled = standardEnvironmentEnabled;
this.checkerBuilder = new Builder(checkerBuilder);
}
diff --git a/checker/src/main/java/dev/cel/checker/DescriptorTypeProvider.java b/checker/src/main/java/dev/cel/checker/DescriptorTypeProvider.java
index e2fe17351..b5f849d50 100644
--- a/checker/src/main/java/dev/cel/checker/DescriptorTypeProvider.java
+++ b/checker/src/main/java/dev/cel/checker/DescriptorTypeProvider.java
@@ -41,7 +41,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* The {@code DescriptorTypeProvider} provides type information for one or more {@link Descriptor}
diff --git a/checker/src/main/java/dev/cel/checker/Env.java b/checker/src/main/java/dev/cel/checker/Env.java
index e1846f1b8..8cc354fb2 100644
--- a/checker/src/main/java/dev/cel/checker/Env.java
+++ b/checker/src/main/java/dev/cel/checker/Env.java
@@ -48,7 +48,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Environment used during checking of expressions. Provides name resolution and error reporting.
diff --git a/checker/src/main/java/dev/cel/checker/ExprChecker.java b/checker/src/main/java/dev/cel/checker/ExprChecker.java
index 658c7db06..f4f485e08 100644
--- a/checker/src/main/java/dev/cel/checker/ExprChecker.java
+++ b/checker/src/main/java/dev/cel/checker/ExprChecker.java
@@ -47,7 +47,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* The expression type checker.
diff --git a/checker/src/main/java/dev/cel/checker/TypeFormatter.java b/checker/src/main/java/dev/cel/checker/TypeFormatter.java
index 450518068..3cdd1a511 100644
--- a/checker/src/main/java/dev/cel/checker/TypeFormatter.java
+++ b/checker/src/main/java/dev/cel/checker/TypeFormatter.java
@@ -18,7 +18,7 @@
import dev.cel.common.annotations.Internal;
import dev.cel.common.types.CelType;
import dev.cel.common.types.CelTypes;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Class to format {@link Type} objects into {@code String} values.
diff --git a/checker/src/main/java/dev/cel/checker/TypeProvider.java b/checker/src/main/java/dev/cel/checker/TypeProvider.java
index 745789498..10ce2a7d6 100644
--- a/checker/src/main/java/dev/cel/checker/TypeProvider.java
+++ b/checker/src/main/java/dev/cel/checker/TypeProvider.java
@@ -22,7 +22,7 @@
import dev.cel.common.types.CelTypes;
import java.util.Optional;
import java.util.function.Function;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* The {@code TypeProvider} defines methods to lookup types and enums, and resolve field types.
diff --git a/checker/src/main/java/dev/cel/checker/TypeProviderLegacyImpl.java b/checker/src/main/java/dev/cel/checker/TypeProviderLegacyImpl.java
index 497d15698..4d1e0ea32 100644
--- a/checker/src/main/java/dev/cel/checker/TypeProviderLegacyImpl.java
+++ b/checker/src/main/java/dev/cel/checker/TypeProviderLegacyImpl.java
@@ -26,7 +26,7 @@
import dev.cel.common.types.StructType;
import dev.cel.common.types.TypeType;
import java.util.Optional;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* The {@code TypeProviderLegacyImpl} acts as a bridge between the old and new type provider APIs
diff --git a/checker/src/main/java/dev/cel/checker/Types.java b/checker/src/main/java/dev/cel/checker/Types.java
index 5e54dfd6d..7f249fc92 100644
--- a/checker/src/main/java/dev/cel/checker/Types.java
+++ b/checker/src/main/java/dev/cel/checker/Types.java
@@ -39,7 +39,7 @@
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Utilities for dealing with the {@link Type} proto.
diff --git a/checker/src/test/java/dev/cel/checker/CelIssueTest.java b/checker/src/test/java/dev/cel/checker/CelIssueTest.java
index e0d34fd0c..a200a5fd5 100644
--- a/checker/src/test/java/dev/cel/checker/CelIssueTest.java
+++ b/checker/src/test/java/dev/cel/checker/CelIssueTest.java
@@ -16,9 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
-import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
import dev.cel.common.CelIssue;
import dev.cel.common.CelSource;
import org.junit.Test;
@@ -28,8 +26,6 @@
@RunWith(JUnit4.class)
public final class CelIssueTest {
- private static final Joiner JOINER = Joiner.on('\n');
-
@Test
public void toDisplayString_narrow() throws Exception {
CelSource source =
@@ -38,7 +34,7 @@ public void toDisplayString_narrow() throws Exception {
ImmutableList.of(
CelIssue.formatError(1, 1, "No such field"),
CelIssue.formatError(2, 20, "Syntax error, missing paren"));
- assertThat(JOINER.join(Iterables.transform(issues, error -> error.toDisplayString(source))))
+ assertThat(CelIssue.toDisplayString(issues, source))
.isEqualTo(
"ERROR: issues-test:1:2: No such field\n"
+ " | a.b\n"
@@ -53,7 +49,7 @@ public void toDisplayString_wideAndNarrow() throws Exception {
CelSource source = CelSource.newBuilder("你好吗\n我b很好\n").setDescription("issues-test").build();
ImmutableList issues =
ImmutableList.of(CelIssue.formatError(2, 3, "Unexpected character '好'"));
- assertThat(JOINER.join(Iterables.transform(issues, error -> error.toDisplayString(source))))
+ assertThat(CelIssue.toDisplayString(issues, source))
.isEqualTo("ERROR: issues-test:2:4: Unexpected character '好'\n" + " | 我b很好\n" + " | ...^");
}
@@ -73,7 +69,8 @@ public void toDisplayString_emojis() throws Exception {
+ " IDENTIFIER}"),
CelIssue.formatError(1, 35, "Syntax error: token recognition error at: '😁'"),
CelIssue.formatError(1, 36, "Syntax error: missing IDENTIFIER at ''"));
- assertThat(JOINER.join(Iterables.transform(issues, error -> error.toDisplayString(source))))
+
+ assertThat(CelIssue.toDisplayString(issues, source))
.isEqualTo(
"ERROR: issues-test:1:33: Syntax error: extraneous input 'in' expecting {'[', '{',"
+ " '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT,"
diff --git a/common/BUILD.bazel b/common/BUILD.bazel
index 649c49186..3af0c0c66 100644
--- a/common/BUILD.bazel
+++ b/common/BUILD.bazel
@@ -5,7 +5,10 @@ package(
java_library(
name = "common",
- exports = ["//common/src/main/java/dev/cel/common"],
+ exports = [
+ "//common/src/main/java/dev/cel/common",
+ "//common/src/main/java/dev/cel/common:source_location", # TODO: Split callers
+ ],
)
java_library(
@@ -59,3 +62,14 @@ java_library(
name = "proto_json_adapter",
exports = ["//common/src/main/java/dev/cel/common:proto_json_adapter"],
)
+
+java_library(
+ name = "source",
+ visibility = ["//visibility:public"],
+ exports = ["//common/src/main/java/dev/cel/common:source"],
+)
+
+java_library(
+ name = "source_location",
+ exports = ["//common/src/main/java/dev/cel/common:source_location"],
+)
diff --git a/common/src/main/java/dev/cel/common/BUILD.bazel b/common/src/main/java/dev/cel/common/BUILD.bazel
index 633279a0b..dbb8890d5 100644
--- a/common/src/main/java/dev/cel/common/BUILD.bazel
+++ b/common/src/main/java/dev/cel/common/BUILD.bazel
@@ -14,7 +14,6 @@ COMMON_SOURCES = [
"CelException.java",
"CelProtoAbstractSyntaxTree.java", # TODO Split target after migrating callers
"CelSource.java",
- "CelSourceLocation.java",
]
# keep sorted
@@ -27,6 +26,12 @@ COMPILER_COMMON_SOURCES = [
"CelVarDecl.java",
]
+# keep sorted
+SOURCE_SOURCES = [
+ "CelSourceHelper.java",
+ "Source.java",
+]
+
# keep sorted
PROTO_V1ALPHA1_AST_SOURCE = [
"CelProtoV1Alpha1AbstractSyntaxTree.java",
@@ -39,6 +44,8 @@ java_library(
],
deps = [
":error_codes",
+ ":source",
+ ":source_location",
"//:auto_value",
"//common/annotations",
"//common/ast",
@@ -62,6 +69,8 @@ java_library(
],
deps = [
":common",
+ ":source",
+ ":source_location",
"//:auto_value",
"//common/annotations",
"//common/internal:safe_string_formatter",
@@ -175,3 +184,28 @@ java_library(
"@maven//:com_google_protobuf_protobuf_java_util",
],
)
+
+java_library(
+ name = "source_location",
+ srcs = ["CelSourceLocation.java"],
+ tags = [
+ "alt_dep=//common:source_location",
+ "avoid_dep",
+ ],
+ deps = [
+ "//:auto_value",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "source",
+ srcs = SOURCE_SOURCES,
+ deps = [
+ ":source_location",
+ "//common/annotations",
+ "//common/internal",
+ "@maven//:com_google_guava_guava",
+ ],
+)
diff --git a/common/src/main/java/dev/cel/common/CelIssue.java b/common/src/main/java/dev/cel/common/CelIssue.java
index 7f7417a53..2f507d5a7 100644
--- a/common/src/main/java/dev/cel/common/CelIssue.java
+++ b/common/src/main/java/dev/cel/common/CelIssue.java
@@ -14,10 +14,14 @@
package dev.cel.common;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
import com.google.auto.value.AutoValue;
+import com.google.common.base.Joiner;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.Immutable;
import dev.cel.common.internal.SafeStringFormatter;
+import java.util.Collection;
import java.util.Optional;
import java.util.PrimitiveIterator;
@@ -28,6 +32,7 @@
@Immutable
@SuppressWarnings("UnicodeEscape") // Suppressed to distinguish half-width and full-width chars.
public abstract class CelIssue {
+ private static final Joiner JOINER = Joiner.on('\n');
/** Severity of a CelIssue. */
public enum Severity {
@@ -73,8 +78,14 @@ public static CelIssue formatError(int line, int column, String message) {
private static final char WIDE_DOT = '\uff0e';
private static final char WIDE_HAT = '\uff3e';
+ /** Returns a human-readable error with all issues joined in a single string. */
+ public static String toDisplayString(Collection issues, Source source) {
+ return JOINER.join(
+ issues.stream().map(iss -> iss.toDisplayString(source)).collect(toImmutableList()));
+ }
+
/** Returns a string representing this error that is suitable for displaying to humans. */
- public String toDisplayString(CelSource source) {
+ public String toDisplayString(Source source) {
// Based onhttps://github.com/google/cel-go/blob/v0.5.1/common/error.go#L42.
String result =
SafeStringFormatter.format(
diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java
index f421bee68..fb5ec6ad7 100644
--- a/common/src/main/java/dev/cel/common/CelOptions.java
+++ b/common/src/main/java/dev/cel/common/CelOptions.java
@@ -93,6 +93,8 @@ public abstract class CelOptions {
public abstract int comprehensionMaxIterations();
+ public abstract boolean unwrapWellKnownTypesOnFunctionDispatch();
+
public abstract Builder toBuilder();
public ImmutableSet toExprFeatures() {
@@ -182,7 +184,8 @@ public static Builder newBuilder() {
.resolveTypeDependencies(true)
.enableUnknownTracking(false)
.enableCelValue(false)
- .comprehensionMaxIterations(-1);
+ .comprehensionMaxIterations(-1)
+ .unwrapWellKnownTypesOnFunctionDispatch(true);
}
/**
@@ -466,6 +469,16 @@ public abstract static class Builder {
*/
public abstract Builder comprehensionMaxIterations(int value);
+ /**
+ * If disabled, CEL runtime will no longer adapt the function dispatch results for protobuf's
+ * well known types to other types. This option is enabled by default.
+ *
+ * @deprecated This will be removed in the future. Please update your codebase to be conformant
+ * with CEL specification.
+ */
+ @Deprecated
+ public abstract Builder unwrapWellKnownTypesOnFunctionDispatch(boolean value);
+
public abstract CelOptions build();
}
}
diff --git a/common/src/main/java/dev/cel/common/CelSource.java b/common/src/main/java/dev/cel/common/CelSource.java
index aea607ccd..1c8d4dbe8 100644
--- a/common/src/main/java/dev/cel/common/CelSource.java
+++ b/common/src/main/java/dev/cel/common/CelSource.java
@@ -16,9 +16,9 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
import com.google.auto.value.AutoValue;
-import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -36,8 +36,7 @@
/** Represents the source content of an expression and related metadata. */
@Immutable
-public final class CelSource {
- private static final Splitter LINE_SPLITTER = Splitter.on('\n');
+public final class CelSource implements Source {
private final CelCodePointArray codePoints;
private final String description;
@@ -55,14 +54,17 @@ private CelSource(Builder builder) {
this.extensions = checkNotNull(builder.extensions.build());
}
+ @Override
public CelCodePointArray getContent() {
return codePoints;
}
+ @Override
public String getDescription() {
return description;
}
+ @Override
public ImmutableMap getPositionsMap() {
return positions;
}
@@ -106,27 +108,12 @@ public Optional getLocationOffset(int line, int column) {
* Get the line and column in the source expression text for the given code point {@code offset}.
*/
public Optional getOffsetLocation(int offset) {
- return getOffsetLocationImpl(lineOffsets, offset);
+ return CelSourceHelper.getOffsetLocation(codePoints, offset);
}
- /**
- * Get the text from the source expression that corresponds to {@code line}.
- *
- * @param line the line number starting from 1.
- */
+ @Override
public Optional getSnippet(int line) {
- checkArgument(line > 0);
- int start = findLineOffset(lineOffsets, line);
- if (start == -1) {
- return Optional.empty();
- }
- int end = findLineOffset(lineOffsets, line + 1);
- if (end == -1) {
- end = codePoints.size();
- } else {
- end--;
- }
- return Optional.of(end != start ? codePoints.slice(start, end).toString() : "");
+ return CelSourceHelper.getSnippet(codePoints, line);
}
/**
@@ -138,49 +125,16 @@ public Optional getSnippet(int line) {
*/
private static Optional getLocationOffsetImpl(
List lineOffsets, int line, int column) {
- checkArgument(line > 0);
- checkArgument(column >= 0);
- int offset = findLineOffset(lineOffsets, line);
+ if (line <= 0 || column < 0) {
+ return Optional.empty();
+ }
+ int offset = CelSourceHelper.findLineOffset(lineOffsets, line);
if (offset == -1) {
return Optional.empty();
}
return Optional.of(offset + column);
}
- /**
- * Get the line and column in the source expression text for the given code point {@code offset}.
- */
- public static Optional getOffsetLocationImpl(
- List lineOffsets, int offset) {
- checkArgument(offset >= 0);
- LineAndOffset lineAndOffset = findLine(lineOffsets, offset);
- return Optional.of(CelSourceLocation.of(lineAndOffset.line, offset - lineAndOffset.offset));
- }
-
- private static int findLineOffset(List lineOffsets, int line) {
- if (line == 1) {
- return 0;
- }
- if (line > 1 && line <= lineOffsets.size()) {
- return lineOffsets.get(line - 2);
- }
- return -1;
- }
-
- private static LineAndOffset findLine(List lineOffsets, int offset) {
- int line = 1;
- for (int index = 0; index < lineOffsets.size(); index++) {
- if (lineOffsets.get(index) > offset) {
- break;
- }
- line++;
- }
- if (line == 1) {
- return new LineAndOffset(line, 0);
- }
- return new LineAndOffset(line, lineOffsets.get(line - 2));
- }
-
public Builder toBuilder() {
return new Builder(codePoints, lineOffsets)
.setDescription(description)
@@ -194,13 +148,11 @@ public static Builder newBuilder() {
}
public static Builder newBuilder(String text) {
- List lineOffsets = new ArrayList<>();
- int lineOffset = 0;
- for (String line : LINE_SPLITTER.split(text)) {
- lineOffset += (int) (line.codePoints().count() + 1);
- lineOffsets.add(lineOffset);
- }
- return new Builder(CelCodePointArray.fromString(text), lineOffsets);
+ return newBuilder(CelCodePointArray.fromString(text));
+ }
+
+ public static Builder newBuilder(CelCodePointArray codePointArray) {
+ return new Builder(codePointArray, codePointArray.lineOffsets());
}
/** Builder for {@link CelSource}. */
@@ -212,6 +164,7 @@ public static final class Builder {
private final Map macroCalls;
private final ImmutableSet.Builder extensions;
+ private final boolean lineOffsetsAlreadyComputed;
private String description;
private Builder() {
@@ -225,6 +178,7 @@ private Builder(CelCodePointArray codePoints, List lineOffsets) {
this.macroCalls = new HashMap<>();
this.extensions = ImmutableSet.builder();
this.description = "";
+ this.lineOffsetsAlreadyComputed = !lineOffsets.isEmpty();
}
@CanIgnoreReturnValue
@@ -236,6 +190,9 @@ public Builder setDescription(String description) {
@CanIgnoreReturnValue
public Builder addLineOffsets(int lineOffset) {
checkArgument(lineOffset >= 0);
+ checkState(
+ !lineOffsetsAlreadyComputed,
+ "Line offsets were already been computed through the provided code points.");
lineOffsets.add(lineOffset);
return this;
}
@@ -331,7 +288,7 @@ public Optional getLocationOffset(int line, int column) {
* offset}.
*/
public Optional getOffsetLocation(int offset) {
- return getOffsetLocationImpl(lineOffsets, offset);
+ return CelSourceHelper.getOffsetLocation(codePoints, offset);
}
@CheckReturnValue
@@ -355,17 +312,6 @@ public CelSource build() {
}
}
- private static final class LineAndOffset {
-
- private LineAndOffset(int line, int offset) {
- this.line = line;
- this.offset = offset;
- }
-
- int line;
- int offset;
- }
-
/**
* Tag for an extension that were used while parsing or type checking the source expression. For
* example, optimizations that require special runtime support may be specified. These are used to
diff --git a/common/src/main/java/dev/cel/common/CelSourceHelper.java b/common/src/main/java/dev/cel/common/CelSourceHelper.java
new file mode 100644
index 000000000..13168edbe
--- /dev/null
+++ b/common/src/main/java/dev/cel/common/CelSourceHelper.java
@@ -0,0 +1,95 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.common;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableList;
+import dev.cel.common.annotations.Internal;
+import dev.cel.common.internal.CelCodePointArray;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Helper methods for common source handling in CEL.
+ *
+ *
CEL Library Internals. Do Not Use.
+ */
+@Internal
+public final class CelSourceHelper {
+
+ /** Extract the snippet text that corresponds to {@code line}. */
+ public static Optional getSnippet(CelCodePointArray content, int line) {
+ checkArgument(line > 0);
+ ImmutableList lineOffsets = content.lineOffsets();
+ int start = findLineOffset(lineOffsets, line);
+ if (start == -1) {
+ return Optional.empty();
+ }
+ int end = findLineOffset(lineOffsets, line + 1);
+ if (end == -1) {
+ end = content.size();
+ } else {
+ end--;
+ }
+ return Optional.of(end != start ? content.slice(start, end).toString() : "");
+ }
+
+ /**
+ * Get the line and column in the source expression text for the given code point {@code offset}.
+ */
+ public static Optional getOffsetLocation(
+ CelCodePointArray content, int offset) {
+ checkArgument(offset >= 0);
+ LineAndOffset lineAndOffset = findLine(content.lineOffsets(), offset);
+ return Optional.of(CelSourceLocation.of(lineAndOffset.line, offset - lineAndOffset.offset));
+ }
+
+ private static LineAndOffset findLine(List lineOffsets, int offset) {
+ int line = 1;
+ for (Integer lineOffset : lineOffsets) {
+ if (lineOffset > offset) {
+ break;
+ }
+ line++;
+ }
+ if (line == 1) {
+ return new LineAndOffset(line, 0);
+ }
+ return new LineAndOffset(line, lineOffsets.get(line - 2));
+ }
+
+ private static final class LineAndOffset {
+ private LineAndOffset(int line, int offset) {
+ this.line = line;
+ this.offset = offset;
+ }
+
+ private final int line;
+ private final int offset;
+ }
+
+ static int findLineOffset(List lineOffsets, int line) {
+ if (line == 1) {
+ return 0;
+ }
+ if (line > 1 && line <= lineOffsets.size()) {
+ return lineOffsets.get(line - 2);
+ }
+ return -1;
+ }
+
+ private CelSourceHelper() {}
+}
diff --git a/common/src/main/java/dev/cel/common/CelValidationException.java b/common/src/main/java/dev/cel/common/CelValidationException.java
index ef136f8a7..522706e83 100644
--- a/common/src/main/java/dev/cel/common/CelValidationException.java
+++ b/common/src/main/java/dev/cel/common/CelValidationException.java
@@ -15,15 +15,12 @@
package dev.cel.common;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
import java.util.List;
/** Base class for all checked exceptions explicitly thrown by the library during parsing. */
public final class CelValidationException extends CelException {
- private static final Joiner JOINER = Joiner.on('\n');
// Truncates all errors beyond this limit in the message.
private static final int MAX_ERRORS_TO_REPORT = 1000;
@@ -46,17 +43,14 @@ public CelValidationException(CelSource source, List errors) {
private static String safeJoinErrorMessage(CelSource source, List errors) {
if (errors.size() <= MAX_ERRORS_TO_REPORT) {
- return JOINER.join(Iterables.transform(errors, error -> error.toDisplayString(source)));
+ return CelIssue.toDisplayString(errors, source);
}
List truncatedErrors = errors.subList(0, MAX_ERRORS_TO_REPORT);
- StringBuilder sb = new StringBuilder();
- JOINER.appendTo(
- sb, Iterables.transform(truncatedErrors, error -> error.toDisplayString(source)));
- sb.append(
- String.format("%n...and %d more errors (truncated)", errors.size() - MAX_ERRORS_TO_REPORT));
- return sb.toString();
+ return CelIssue.toDisplayString(truncatedErrors, source)
+ + String.format(
+ "%n...and %d more errors (truncated)", errors.size() - MAX_ERRORS_TO_REPORT);
}
/** Returns the {@link CelSource} that was being validated. */
diff --git a/common/src/main/java/dev/cel/common/CelValidationResult.java b/common/src/main/java/dev/cel/common/CelValidationResult.java
index a7e6eceb8..61152c493 100644
--- a/common/src/main/java/dev/cel/common/CelValidationResult.java
+++ b/common/src/main/java/dev/cel/common/CelValidationResult.java
@@ -17,14 +17,12 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.Comparator.comparing;
-import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.annotations.InlineMe;
import dev.cel.common.annotations.Internal;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* CelValidationResult encapsulates the {@code CelAbstractSyntaxTree} and {@code CelIssue} set which
@@ -33,8 +31,6 @@
@Immutable
public final class CelValidationResult {
- private static final Joiner JOINER = Joiner.on('\n');
-
@SuppressWarnings("Immutable")
private final @Nullable Throwable failure;
@@ -115,7 +111,7 @@ public ImmutableList getAllIssues() {
/** Convert all issues to a human-readable string. */
public String getIssueString() {
- return JOINER.join(Iterables.transform(issues, iss -> iss.toDisplayString(source)));
+ return CelIssue.toDisplayString(issues, source);
}
/**
@@ -131,7 +127,7 @@ public String getDebugString() {
/** Convert the {@code CelIssue}s with {@code ERROR} severity to an error string. */
public String getErrorString() {
- return JOINER.join(Iterables.transform(getErrors(), error -> error.toDisplayString(source)));
+ return CelIssue.toDisplayString(getErrors(), source);
}
private static boolean issueIsError(CelIssue iss) {
diff --git a/common/src/main/java/dev/cel/common/CelVarDecl.java b/common/src/main/java/dev/cel/common/CelVarDecl.java
index 9c3930c3a..0543ed225 100644
--- a/common/src/main/java/dev/cel/common/CelVarDecl.java
+++ b/common/src/main/java/dev/cel/common/CelVarDecl.java
@@ -14,13 +14,9 @@
package dev.cel.common;
-import dev.cel.expr.Decl;
-import dev.cel.expr.Decl.IdentDecl;
import com.google.auto.value.AutoValue;
-import com.google.common.annotations.VisibleForTesting;
import com.google.errorprone.annotations.CheckReturnValue;
import dev.cel.common.types.CelType;
-import dev.cel.common.types.CelTypes;
/** Abstract representation of a CEL variable declaration. */
@AutoValue
@@ -37,13 +33,4 @@ public abstract class CelVarDecl {
public static CelVarDecl newVarDeclaration(String name, CelType type) {
return new AutoValue_CelVarDecl(name, type);
}
-
- /** Converts a {@link CelVarDecl} to a protobuf equivalent form {@code Decl} */
- @VisibleForTesting
- public static Decl celVarToDecl(CelVarDecl varDecl) {
- return Decl.newBuilder()
- .setName(varDecl.name())
- .setIdent(IdentDecl.newBuilder().setType(CelTypes.celTypeToType(varDecl.type())))
- .build();
- }
}
diff --git a/common/src/main/java/dev/cel/common/Source.java b/common/src/main/java/dev/cel/common/Source.java
new file mode 100644
index 000000000..2d43a9581
--- /dev/null
+++ b/common/src/main/java/dev/cel/common/Source.java
@@ -0,0 +1,52 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.common;
+
+import com.google.common.collect.ImmutableMap;
+import dev.cel.common.annotations.Internal;
+import dev.cel.common.internal.CelCodePointArray;
+import java.util.Optional;
+
+/**
+ * Common interface definition for source properties.
+ *
+ *
CEL Library Internals. Do Not Use. Consumers should instead use the canonical implementations
+ * such as CelSource.
+ */
+@Internal
+public interface Source {
+
+ /** Gets the original textual content of this source, represented in an array of code points. */
+ CelCodePointArray getContent();
+
+ /**
+ * Gets the description of this source that may optionally be set (example: location of the file
+ * containing the source).
+ */
+ String getDescription();
+
+ /**
+ * Gets the map of each parsed node ID (ex: expression ID, policy ID) to their source positions.
+ */
+ ImmutableMap getPositionsMap();
+
+ /**
+ * Get the text from the source text that corresponds to {@code line}. Snippets are split based on
+ * the newline ('\n').
+ *
+ * @param line the line number starting from 1.
+ */
+ Optional getSnippet(int line);
+}
diff --git a/common/src/main/java/dev/cel/common/ast/CelExpr.java b/common/src/main/java/dev/cel/common/ast/CelExpr.java
index 6af628551..4c08a6d05 100644
--- a/common/src/main/java/dev/cel/common/ast/CelExpr.java
+++ b/common/src/main/java/dev/cel/common/ast/CelExpr.java
@@ -34,6 +34,7 @@
*
This is the native type equivalent of Expr message in syntax.proto.
*/
@AutoValue
+@AutoValue.CopyAnnotations
@Immutable
@SuppressWarnings("unchecked") // Class ensures only the super type is used
public abstract class CelExpr implements Expression {
diff --git a/common/src/main/java/dev/cel/common/internal/AdaptingTypes.java b/common/src/main/java/dev/cel/common/internal/AdaptingTypes.java
index 53fe59503..48158bd91 100644
--- a/common/src/main/java/dev/cel/common/internal/AdaptingTypes.java
+++ b/common/src/main/java/dev/cel/common/internal/AdaptingTypes.java
@@ -23,7 +23,7 @@
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Collection of types which support bidirectional adaptation between CEL and Java native value
diff --git a/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java b/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java
index f4f9ef6c3..251f09d61 100644
--- a/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java
+++ b/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java
@@ -19,6 +19,7 @@
import static com.google.common.base.Preconditions.checkPositionIndexes;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.Immutable;
import dev.cel.common.annotations.Internal;
@@ -38,21 +39,23 @@ public final class BasicCodePointArray extends CelCodePointArray {
private final int offset;
private final int size;
+ private final ImmutableList lineOffsets;
- BasicCodePointArray(char[] codePoints, int size) {
- this(codePoints, 0, size);
+ BasicCodePointArray(char[] codePoints, int size, ImmutableList lineOffsets) {
+ this(codePoints, 0, lineOffsets, size);
}
- BasicCodePointArray(char[] codePoints, int offset, int size) {
+ BasicCodePointArray(char[] codePoints, int offset, ImmutableList lineOffsets, int size) {
this.codePoints = checkNotNull(codePoints);
this.offset = offset;
this.size = size;
+ this.lineOffsets = lineOffsets;
}
@Override
public BasicCodePointArray slice(int i, int j) {
checkPositionIndexes(i, j, size());
- return new BasicCodePointArray(codePoints, offset + i, j - i);
+ return new BasicCodePointArray(codePoints, offset + i, lineOffsets, j - i);
}
@Override
@@ -66,6 +69,11 @@ public int size() {
return size;
}
+ @Override
+ public ImmutableList lineOffsets() {
+ return lineOffsets;
+ }
+
@Override
public String toString() {
return new String(codePoints, offset, size);
diff --git a/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java b/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java
index 178b2ac55..94d94b5dc 100644
--- a/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java
+++ b/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Strings.isNullOrEmpty;
+import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.Immutable;
import dev.cel.common.annotations.Internal;
import java.util.PrimitiveIterator;
@@ -41,6 +42,9 @@ public abstract class CelCodePointArray {
/** Returns the number of code points. */
public abstract int size();
+ /** Returns the line offsets. */
+ public abstract ImmutableList lineOffsets();
+
public final int length() {
return size();
}
@@ -60,8 +64,11 @@ public static CelCodePointArray fromString(String text) {
PrimitiveIterator.OfInt codePoints = text.codePoints().iterator();
byte[] byteArray = new byte[text.length()];
int byteIndex = 0;
+
+ LineOffsetContext lineOffsetContext = new LineOffsetContext();
while (codePoints.hasNext()) {
int codePoint = codePoints.nextInt();
+ lineOffsetContext.process(codePoint);
if (codePoint <= 0xff) {
byteArray[byteIndex++] = (byte) codePoint;
continue;
@@ -76,6 +83,7 @@ public static CelCodePointArray fromString(String text) {
charArray[charIndex++] = (char) codePoint;
while (codePoints.hasNext()) {
codePoint = codePoints.nextInt();
+ lineOffsetContext.process(codePoint);
if (codePoint <= 0xffff) {
charArray[charIndex++] = (char) codePoint;
continue;
@@ -89,11 +97,15 @@ public static CelCodePointArray fromString(String text) {
intArray[intIndex++] = codePoint;
while (codePoints.hasNext()) {
codePoint = codePoints.nextInt();
+ lineOffsetContext.process(codePoint);
intArray[intIndex++] = codePoint;
}
- return new SupplementalCodePointArray(intArray, intIndex);
+
+ return new SupplementalCodePointArray(
+ intArray, intIndex, lineOffsetContext.buildLineOffsets());
}
- return new BasicCodePointArray(charArray, charIndex);
+
+ return new BasicCodePointArray(charArray, charIndex, lineOffsetContext.buildLineOffsets());
}
int[] intArray = new int[text.length()];
int intIndex = 0;
@@ -104,10 +116,36 @@ public static CelCodePointArray fromString(String text) {
intArray[intIndex++] = codePoint;
while (codePoints.hasNext()) {
codePoint = codePoints.nextInt();
+ lineOffsetContext.process(codePoint);
intArray[intIndex++] = codePoint;
}
- return new SupplementalCodePointArray(intArray, intIndex);
+
+ return new SupplementalCodePointArray(
+ intArray, intIndex, lineOffsetContext.buildLineOffsets());
+ }
+
+ return new Latin1CodePointArray(byteArray, byteIndex, lineOffsetContext.buildLineOffsets());
+ }
+
+ private static class LineOffsetContext {
+ private static final int NEWLINE_CODE_POINT = 10;
+
+ private final ImmutableList.Builder lineOffsetBuilder;
+ private int lineOffsetCodePoints;
+
+ private void process(int codePoint) {
+ lineOffsetCodePoints++;
+ if (codePoint == NEWLINE_CODE_POINT) {
+ lineOffsetBuilder.add(lineOffsetCodePoints);
+ }
+ }
+
+ private ImmutableList buildLineOffsets() {
+ return lineOffsetBuilder.add(lineOffsetCodePoints + 1).build();
+ }
+
+ private LineOffsetContext() {
+ this.lineOffsetBuilder = ImmutableList.builder();
}
- return new Latin1CodePointArray(byteArray, byteIndex);
}
}
diff --git a/common/src/main/java/dev/cel/common/internal/EmptyCodePointArray.java b/common/src/main/java/dev/cel/common/internal/EmptyCodePointArray.java
index 95352d83b..8bca7bf31 100644
--- a/common/src/main/java/dev/cel/common/internal/EmptyCodePointArray.java
+++ b/common/src/main/java/dev/cel/common/internal/EmptyCodePointArray.java
@@ -14,6 +14,7 @@
package dev.cel.common.internal;
+import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.DoNotCall;
import com.google.errorprone.annotations.Immutable;
import dev.cel.common.annotations.Internal;
@@ -55,6 +56,11 @@ public int size() {
return 0;
}
+ @Override
+ public ImmutableList lineOffsets() {
+ return ImmutableList.of(1);
+ }
+
@Override
public String toString() {
return "";
diff --git a/common/src/main/java/dev/cel/common/internal/Errors.java b/common/src/main/java/dev/cel/common/internal/Errors.java
index 3ee3ad19c..b2860355f 100644
--- a/common/src/main/java/dev/cel/common/internal/Errors.java
+++ b/common/src/main/java/dev/cel/common/internal/Errors.java
@@ -28,7 +28,7 @@
import java.util.Deque;
import java.util.List;
import java.util.Optional;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* An object which manages error reporting. Enriches error messages by source context pointing to
diff --git a/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java b/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java
index 854df7fa7..a06448aba 100644
--- a/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java
+++ b/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java
@@ -20,6 +20,7 @@
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.Immutable;
import dev.cel.common.annotations.Internal;
@@ -38,21 +39,24 @@ public final class Latin1CodePointArray extends CelCodePointArray {
private final int offset;
private final int size;
+ private final ImmutableList lineOffsets;
- Latin1CodePointArray(byte[] codePoints, int size) {
- this(codePoints, 0, size);
+ Latin1CodePointArray(byte[] codePoints, int size, ImmutableList lineOffsets) {
+ this(codePoints, 0, lineOffsets, size);
}
- Latin1CodePointArray(byte[] codePoints, int offset, int size) {
+ Latin1CodePointArray(
+ byte[] codePoints, int offset, ImmutableList lineOffsets, int size) {
this.codePoints = checkNotNull(codePoints);
this.offset = offset;
this.size = size;
+ this.lineOffsets = lineOffsets;
}
@Override
public Latin1CodePointArray slice(int i, int j) {
checkPositionIndexes(i, j, size());
- return new Latin1CodePointArray(codePoints, offset + i, j - i);
+ return new Latin1CodePointArray(codePoints, offset + i, lineOffsets, j - i);
}
@Override
@@ -66,6 +70,11 @@ public int size() {
return size;
}
+ @Override
+ public ImmutableList lineOffsets() {
+ return lineOffsets;
+ }
+
@Override
public String toString() {
return new String(codePoints, offset, size, ISO_8859_1);
diff --git a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java
index b63825dc4..29fece49c 100644
--- a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java
+++ b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java
@@ -59,7 +59,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* The {@code ProtoAdapter} utilities handle conversion between native Java objects which represent
diff --git a/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java b/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java
index 8090a591d..30f2fce27 100644
--- a/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java
+++ b/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java
@@ -19,6 +19,7 @@
import static com.google.common.base.Preconditions.checkPositionIndexes;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.Immutable;
import dev.cel.common.annotations.Internal;
@@ -38,21 +39,24 @@ public final class SupplementalCodePointArray extends CelCodePointArray {
private final int offset;
private final int size;
+ private final ImmutableList lineOffsets;
- SupplementalCodePointArray(int[] codePoints, int size) {
- this(codePoints, 0, size);
+ SupplementalCodePointArray(int[] codePoints, int size, ImmutableList lineOffsets) {
+ this(codePoints, 0, lineOffsets, size);
}
- SupplementalCodePointArray(int[] codePoints, int offset, int size) {
+ SupplementalCodePointArray(
+ int[] codePoints, int offset, ImmutableList lineOffsets, int size) {
this.codePoints = checkNotNull(codePoints);
this.offset = offset;
this.size = size;
+ this.lineOffsets = lineOffsets;
}
@Override
public SupplementalCodePointArray slice(int i, int j) {
checkPositionIndexes(i, j, size());
- return new SupplementalCodePointArray(codePoints, offset + i, j - i);
+ return new SupplementalCodePointArray(codePoints, offset + i, lineOffsets, j - i);
}
@Override
@@ -66,6 +70,11 @@ public int size() {
return size;
}
+ @Override
+ public ImmutableList lineOffsets() {
+ return lineOffsets;
+ }
+
@Override
public String toString() {
return new String(codePoints, offset, size);
diff --git a/common/src/main/java/dev/cel/common/types/SimpleType.java b/common/src/main/java/dev/cel/common/types/SimpleType.java
index 378c669f1..cbf02e9c6 100644
--- a/common/src/main/java/dev/cel/common/types/SimpleType.java
+++ b/common/src/main/java/dev/cel/common/types/SimpleType.java
@@ -15,8 +15,10 @@
package dev.cel.common.types;
import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.Immutable;
+import java.util.Optional;
/** Simple types represent scalar, dynamic, and error values. */
@AutoValue
@@ -42,6 +44,19 @@ public abstract class SimpleType extends CelType {
public static final CelType TIMESTAMP = create(CelKind.TIMESTAMP, "google.protobuf.Timestamp");
public static final CelType UINT = create(CelKind.UINT, "uint");
+ private static final ImmutableMap TYPE_MAP =
+ ImmutableMap.of(
+ DYN.name(), DYN,
+ BOOL.name(), BOOL,
+ BYTES.name(), BYTES,
+ DOUBLE.name(), DOUBLE,
+ DURATION.name(), DURATION,
+ INT.name(), INT,
+ NULL_TYPE.name(), NULL_TYPE,
+ STRING.name(), STRING,
+ TIMESTAMP.name(), TIMESTAMP,
+ UINT.name(), UINT);
+
@Override
public abstract CelKind kind();
@@ -56,6 +71,11 @@ public boolean isAssignableFrom(CelType other) {
|| (other instanceof NullableType && other.isAssignableFrom(this));
}
+ /** Returns a matching SimpleType by its name if one exists. */
+ public static Optional findByName(String typeName) {
+ return Optional.ofNullable(TYPE_MAP.get(typeName));
+ }
+
private static CelType create(CelKind kind, String name) {
return new AutoValue_SimpleType(kind, name);
}
diff --git a/common/src/main/java/dev/cel/common/values/ListValue.java b/common/src/main/java/dev/cel/common/values/ListValue.java
index 9cf6aa806..814884d34 100644
--- a/common/src/main/java/dev/cel/common/values/ListValue.java
+++ b/common/src/main/java/dev/cel/common/values/ListValue.java
@@ -24,7 +24,7 @@
import java.util.Comparator;
import java.util.List;
import java.util.function.UnaryOperator;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* ListValue is an abstract representation of a generic list containing zero or more {@link
diff --git a/common/src/main/java/dev/cel/common/values/MapValue.java b/common/src/main/java/dev/cel/common/values/MapValue.java
index ff1bb3f56..0f79b0464 100644
--- a/common/src/main/java/dev/cel/common/values/MapValue.java
+++ b/common/src/main/java/dev/cel/common/values/MapValue.java
@@ -26,7 +26,7 @@
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* MapValue is an abstract representation of a generic map containing {@link CelValue} as keys and
diff --git a/common/src/main/java/dev/cel/common/values/OptionalValue.java b/common/src/main/java/dev/cel/common/values/OptionalValue.java
index 45270ebb2..3866ef72b 100644
--- a/common/src/main/java/dev/cel/common/values/OptionalValue.java
+++ b/common/src/main/java/dev/cel/common/values/OptionalValue.java
@@ -21,7 +21,7 @@
import dev.cel.common.types.SimpleType;
import java.util.NoSuchElementException;
import java.util.Optional;
-import org.jspecify.nullness.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* First-class support for CEL optionals. Supports similar semantics to java.util.Optional. Also
diff --git a/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java b/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java
index 78c16c165..48e28894a 100644
--- a/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java
+++ b/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java
@@ -150,11 +150,7 @@ public void getSource_hasDescriptionEqualToSourceLocation() {
public void parsedExpression_createAst() {
CelExpr celExpr = CelExpr.newBuilder().setId(1).setConstant(CelConstant.ofValue(2)).build();
CelSource celSource =
- CelSource.newBuilder("expression")
- .setDescription("desc")
- .addPositions(1, 5)
- .addLineOffsets(10)
- .build();
+ CelSource.newBuilder("expression").setDescription("desc").addPositions(1, 5).build();
CelAbstractSyntaxTree ast = CelAbstractSyntaxTree.newParsedAst(celExpr, celSource);
diff --git a/common/src/test/java/dev/cel/common/CelSourceTest.java b/common/src/test/java/dev/cel/common/CelSourceTest.java
index a97eeb853..d8b3701e3 100644
--- a/common/src/test/java/dev/cel/common/CelSourceTest.java
+++ b/common/src/test/java/dev/cel/common/CelSourceTest.java
@@ -181,4 +181,15 @@ public void source_withExtension() {
.containsExactly(Component.COMPONENT_PARSER, Component.COMPONENT_TYPE_CHECKER);
assertThat(celSource.getExtensions()).hasSize(1);
}
+
+ @Test
+ public void source_lineOffsetsAlreadyComputed_throws() {
+ CelSource.Builder sourceBuilder = CelSource.newBuilder("text");
+
+ IllegalStateException e =
+ assertThrows(IllegalStateException.class, () -> sourceBuilder.addLineOffsets(1));
+ assertThat(e)
+ .hasMessageThat()
+ .contains("Line offsets were already been computed through the provided code points.");
+ }
}
diff --git a/common/src/test/java/dev/cel/common/internal/BUILD.bazel b/common/src/test/java/dev/cel/common/internal/BUILD.bazel
index 41d98f6c8..af0a673aa 100644
--- a/common/src/test/java/dev/cel/common/internal/BUILD.bazel
+++ b/common/src/test/java/dev/cel/common/internal/BUILD.bazel
@@ -10,6 +10,7 @@ java_library(
srcs = glob(["*.java"]),
resources = ["//common/src/test/resources"],
deps = [
+ "//:auto_value",
"//:java_truth",
"//common",
"//common:options",
diff --git a/common/src/test/java/dev/cel/common/internal/CelCodePointArrayTest.java b/common/src/test/java/dev/cel/common/internal/CelCodePointArrayTest.java
new file mode 100644
index 000000000..7cb02c5a8
--- /dev/null
+++ b/common/src/test/java/dev/cel/common/internal/CelCodePointArrayTest.java
@@ -0,0 +1,77 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.common.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(TestParameterInjector.class)
+public final class CelCodePointArrayTest {
+
+ @Test
+ public void computeLineOffset(
+ @TestParameter(valuesProvider = LineOffsetDataProvider.class) LineOffsetTestCase testCase) {
+ CelCodePointArray codePointArray = CelCodePointArray.fromString(testCase.text());
+
+ assertThat(codePointArray.lineOffsets())
+ .containsExactlyElementsIn(testCase.offsets())
+ .inOrder();
+ }
+
+ @AutoValue
+ abstract static class LineOffsetTestCase {
+ abstract String text();
+
+ abstract ImmutableList offsets();
+
+ static LineOffsetTestCase of(String text, Integer... offsets) {
+ return of(text, Arrays.asList(offsets));
+ }
+
+ static LineOffsetTestCase of(String text, List offsets) {
+ return new AutoValue_CelCodePointArrayTest_LineOffsetTestCase(
+ text, ImmutableList.copyOf(offsets));
+ }
+ }
+
+ private static final class LineOffsetDataProvider extends TestParameterValuesProvider {
+
+ @Override
+ protected List provideValues(Context context) {
+ return Arrays.asList(
+ // Empty
+ LineOffsetTestCase.of("", 1),
+ // ISO-8859-1
+ LineOffsetTestCase.of("hello world", 12),
+ LineOffsetTestCase.of("hello\nworld", 6, 12),
+ LineOffsetTestCase.of("hello\nworld\n\nfoo\n", 6, 12, 13, 17, 18),
+ // BMP
+ LineOffsetTestCase.of("abc 가나다", 8),
+ LineOffsetTestCase.of("abc\n가나다\n我b很好\n", 4, 8, 13, 14),
+ // SMP
+ LineOffsetTestCase.of(" text 가나다 😦😁😑 ", 15),
+ LineOffsetTestCase.of(" text\n가나다 \n😦😁😑\n\n", 6, 11, 15, 16, 17));
+ }
+ }
+}
diff --git a/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java b/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java
index ac81f5d0c..13da89f2f 100644
--- a/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java
+++ b/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java
@@ -74,6 +74,11 @@ public CelValidationResult check(CelAbstractSyntaxTree ast) {
return checker.check(ast);
}
+ @Override
+ public CelTypeProvider getTypeProvider() {
+ return checker.getTypeProvider();
+ }
+
@Override
public void accept(EnvVisitor envVisitor) {
if (checker instanceof EnvVisitable) {
diff --git a/extensions/BUILD.bazel b/extensions/BUILD.bazel
index ef0e9c9fd..70956e336 100644
--- a/extensions/BUILD.bazel
+++ b/extensions/BUILD.bazel
@@ -22,3 +22,8 @@ java_library(
name = "optional_library",
exports = ["//extensions/src/main/java/dev/cel/extensions:optional_library"],
)
+
+java_library(
+ name = "sets",
+ exports = ["//extensions/src/main/java/dev/cel/extensions:sets"],
+)
diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel
index bf42c2ae8..da8aba9a0 100644
--- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel
+++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel
@@ -18,6 +18,7 @@ java_library(
":encoders",
":math",
":protos",
+ ":sets",
":strings",
"//common:options",
"@maven//:com_google_guava_guava",
@@ -126,3 +127,20 @@ java_library(
"@maven//:com_google_protobuf_protobuf_java",
],
)
+
+java_library(
+ name = "sets",
+ srcs = ["CelSetsExtensions.java"],
+ tags = [
+ ],
+ deps = [
+ "//checker:checker_builder",
+ "//common:compiler_common",
+ "//common/internal:comparison_functions",
+ "//common/types",
+ "//compiler:compiler_builder",
+ "//runtime",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ ],
+)
diff --git a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java
index c3d9fdbbf..5515d6a89 100644
--- a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java
+++ b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java
@@ -1,4 +1,4 @@
-// Copyright 2022 Google LLC
+// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ public final class CelExtensions {
private static final CelProtoExtensions PROTO_EXTENSIONS = new CelProtoExtensions();
private static final CelBindingsExtensions BINDINGS_EXTENSIONS = new CelBindingsExtensions();
private static final CelEncoderExtensions ENCODER_EXTENSIONS = new CelEncoderExtensions();
+ private static final CelSetsExtensions SET_EXTENSIONS = new CelSetsExtensions();
/**
* Extended functions for string manipulation.
@@ -170,5 +171,40 @@ public static CelEncoderExtensions encoders() {
return ENCODER_EXTENSIONS;
}
+ /**
+ * Extended functions for Set manipulation.
+ *
+ *
Refer to README.md for available functions.
+ *
+ *
This will include all functions denoted in {@link CelSetExtensions.Function}, including any
+ * future additions. To expose only a subset of functions, use {@link
+ * #sets(CelSetExtensions.Function...)} instead.
+ */
+ public static CelSetsExtensions sets() {
+ return SET_EXTENSIONS;
+ }
+
+ /**
+ * Extended functions for Set manipulation.
+ *
+ *
Refer to README.md for available functions.
+ *
+ *
This will include only the specific functions denoted by {@link CelSetsExtensions.Function}.
+ */
+ public static CelSetsExtensions sets(CelSetsExtensions.Function... functions) {
+ return sets(ImmutableSet.copyOf(functions));
+ }
+
+ /**
+ * Extended functions for Set manipulation.
+ *
+ *
Refer to README.md for available functions.
+ *
+ *
This will include only the specific functions denoted by {@link CelSetsExtensions.Function}.
+ */
+ public static CelSetsExtensions sets(Set functions) {
+ return new CelSetsExtensions(functions);
+ }
+
private CelExtensions() {}
}
diff --git a/extensions/src/main/java/dev/cel/extensions/CelSetsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelSetsExtensions.java
new file mode 100644
index 000000000..10fcbdd0c
--- /dev/null
+++ b/extensions/src/main/java/dev/cel/extensions/CelSetsExtensions.java
@@ -0,0 +1,240 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.extensions;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.Immutable;
+import dev.cel.checker.CelCheckerBuilder;
+import dev.cel.common.CelFunctionDecl;
+import dev.cel.common.CelOverloadDecl;
+import dev.cel.common.internal.ComparisonFunctions;
+import dev.cel.common.types.ListType;
+import dev.cel.common.types.SimpleType;
+import dev.cel.common.types.TypeParamType;
+import dev.cel.compiler.CelCompilerLibrary;
+import dev.cel.runtime.CelRuntime;
+import dev.cel.runtime.CelRuntimeBuilder;
+import dev.cel.runtime.CelRuntimeLibrary;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Internal implementation of CEL Set extensions.
+ *
+ *
Invoking in operator will result in O(n) complexity. We need to wire in the CEL optimizers to
+ * rewrite the AST into a map to achieve a O(1) lookup.
+ */
+@Immutable
+@SuppressWarnings({"unchecked"}) // Unchecked: Type-checker guarantees casting safety.
+public final class CelSetsExtensions implements CelCompilerLibrary, CelRuntimeLibrary {
+
+ private static final String SET_CONTAINS_FUNCTION = "sets.contains";
+ private static final String SET_CONTAINS_OVERLOAD_DOC =
+ "Returns whether the first list argument contains all elements in the second list"
+ + " argument. The list may contain elements of any type and standard CEL"
+ + " equality is used to determine whether a value exists in both lists. If the"
+ + " second list is empty, the result will always return true.";
+ private static final String SET_EQUIVALENT_FUNCTION = "sets.equivalent";
+ private static final String SET_EQUIVALENT_OVERLOAD_DOC =
+ "Returns whether the first and second list are set equivalent. Lists are set equivalent if"
+ + " for every item in the first list, there is an element in the second which is equal."
+ + " The lists may not be of the same size as they do not guarantee the elements within"
+ + " them are unique, so size does not factor intothe computation.";
+ private static final String SET_INTERSECTS_FUNCTION = "sets.intersects";
+ private static final String SET_INTERSECTS_OVERLOAD_DOC =
+ "Returns whether the first and second list intersect. Lists intersect if there is at least"
+ + " one element in the first list which is equal to an element in the second list. The"
+ + " lists may not be of the same size as they do not guarantee the elements within them"
+ + " are unique, so size does not factor into the computation. If either list is empty,"
+ + " the result will be false.";
+
+ /** Denotes the set extension function. */
+ public enum Function {
+ CONTAINS(
+ CelFunctionDecl.newFunctionDeclaration(
+ SET_CONTAINS_FUNCTION,
+ CelOverloadDecl.newGlobalOverload(
+ "list_sets_contains_list",
+ SET_CONTAINS_OVERLOAD_DOC,
+ SimpleType.BOOL,
+ ListType.create(TypeParamType.create("T")),
+ ListType.create(TypeParamType.create("T")))),
+ CelRuntime.CelFunctionBinding.from(
+ "list_sets_contains_list",
+ Collection.class,
+ Collection.class,
+ CelSetsExtensions::containsAll)),
+ EQUIVALENT(
+ CelFunctionDecl.newFunctionDeclaration(
+ SET_EQUIVALENT_FUNCTION,
+ CelOverloadDecl.newGlobalOverload(
+ "list_sets_equivalent_list",
+ SET_EQUIVALENT_OVERLOAD_DOC,
+ SimpleType.BOOL,
+ ListType.create(TypeParamType.create("T")),
+ ListType.create(TypeParamType.create("T")))),
+ CelRuntime.CelFunctionBinding.from(
+ "list_sets_equivalent_list",
+ Collection.class,
+ Collection.class,
+ (listA, listB) -> containsAll(listA, listB) && containsAll(listB, listA))),
+ INTERSECTS(
+ CelFunctionDecl.newFunctionDeclaration(
+ SET_INTERSECTS_FUNCTION,
+ CelOverloadDecl.newGlobalOverload(
+ "list_sets_intersects_list",
+ SET_INTERSECTS_OVERLOAD_DOC,
+ SimpleType.BOOL,
+ ListType.create(TypeParamType.create("T")),
+ ListType.create(TypeParamType.create("T")))),
+ CelRuntime.CelFunctionBinding.from(
+ "list_sets_intersects_list",
+ Collection.class,
+ Collection.class,
+ CelSetsExtensions::setIntersects));
+
+ private final CelFunctionDecl functionDecl;
+ private final ImmutableSet functionBindings;
+
+ Function(CelFunctionDecl functionDecl, CelRuntime.CelFunctionBinding... functionBindings) {
+ this.functionDecl = functionDecl;
+ this.functionBindings = ImmutableSet.copyOf(functionBindings);
+ }
+ }
+
+ private final ImmutableSet functions;
+
+ CelSetsExtensions() {
+ this(ImmutableSet.copyOf(Function.values()));
+ }
+
+ CelSetsExtensions(Set functions) {
+ this.functions = ImmutableSet.copyOf(functions);
+ }
+
+ @Override
+ public void setCheckerOptions(CelCheckerBuilder checkerBuilder) {
+ functions.forEach(function -> checkerBuilder.addFunctionDeclarations(function.functionDecl));
+ }
+
+ @Override
+ public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) {
+ functions.forEach(function -> runtimeBuilder.addFunctionBindings(function.functionBindings));
+ }
+
+ /**
+ * This implementation iterates over the specified collection, checking each element returned by
+ * the iterator in turn to see if it's contained in this collection. If all elements are so
+ * contained true is returned, otherwise false.
+ *
+ *
This is picked verbatim as implemented in the Java standard library
+ * Collections.containsAll() method.
+ *
+ * @see #contains(Object)
+ */
+ private static boolean containsAll(Collection list, Collection subList) {
+ for (T e : subList) {
+ if (!contains(e, list)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * This implementation iterates over the elements in the collection, checking each element in turn
+ * for equality with the specified element.
+ *
+ *
This is picked verbatim as implemented in the Java standard library Collections.contains()
+ * method.
+ *
+ *
Source:
+ * https://hg.openjdk.org/jdk8u/jdk8u-dev/jdk/file/c5d02f908fb2/src/share/classes/java/util/AbstractCollection.java#l98
+ */
+ private static boolean contains(Object o, Collection list) {
+ Iterator> it = list.iterator();
+ if (o == null) {
+ while (it.hasNext()) {
+ if (it.next() == null) {
+ return true;
+ }
+ }
+ } else {
+ while (it.hasNext()) {
+ Object item = it.next();
+ if (objectsEquals(item, o)) { // TODO: Support Maps.
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static boolean objectsEquals(Object o1, Object o2) {
+ if (o1 == o2) {
+ return true;
+ }
+ if (o1 == null || o2 == null) {
+ return false;
+ }
+ if (isNumeric(o1) && isNumeric(o2)) {
+ if (o1.getClass().equals(o2.getClass())) {
+ return o1.equals(o2);
+ }
+ return ComparisonFunctions.numericEquals((Number) o1, (Number) o2);
+ }
+ if (isList(o1) && isList(o2)) {
+ Collection> list1 = (Collection>) o1;
+ Collection> list2 = (Collection>) o2;
+ if (list1.size() != list2.size()) {
+ return false;
+ }
+ Iterator> iterator1 = list1.iterator();
+ Iterator> iterator2 = list2.iterator();
+ boolean result = true;
+ while (iterator1.hasNext() && iterator2.hasNext()) {
+ Object p1 = iterator1.next();
+ Object p2 = iterator2.next();
+ result = result && objectsEquals(p1, p2);
+ }
+ return result;
+ }
+ return o1.equals(o2);
+ }
+
+ private static boolean isNumeric(Object o) {
+ return o instanceof Number;
+ }
+
+ private static boolean isList(Object o) {
+ return o instanceof List;
+ }
+
+ private static boolean setIntersects(Collection listA, Collection listB) {
+ if (listA.isEmpty() || listB.isEmpty()) {
+ return false;
+ }
+ for (T element : listB) {
+ if (contains(element, listA)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/extensions/src/main/java/dev/cel/extensions/README.md b/extensions/src/main/java/dev/cel/extensions/README.md
index 1c78f0364..1163f334c 100644
--- a/extensions/src/main/java/dev/cel/extensions/README.md
+++ b/extensions/src/main/java/dev/cel/extensions/README.md
@@ -338,3 +338,71 @@ Example:
base64.encode(b'hello') // return 'aGVsbG8='
+## Sets
+
+Sets provides set relationship tests.
+
+There is no set type within CEL, and while one may be introduced in the future,
+there are cases where a `list` type is known to behave like a set. For such
+cases, this library provides some basic functionality for determining set
+containment, equivalence, and intersection.
+
+### Sets.Contains
+
+Returns whether the first list argument contains all elements in the second list
+argument. The list may contain elements of any type and standard CEL equality is
+used to determine whether a value exists in both lists. If the second list is
+empty, the result will always return true.
+
+```
+sets.contains(list(T), list(T)) -> bool
+```
+
+Examples:
+
+```
+sets.contains([], []) // true
+sets.contains([], [1]) // false
+sets.contains([1, 2, 3, 4], [2, 3]) // true
+sets.contains([1, 2.0, 3u], [1.0, 2u, 3]) // true
+```
+
+### Sets.Equivalent
+
+Returns whether the first and second list are set equivalent. Lists are set
+equivalent if for every item in the first list, there is an element in the
+second which is equal. The lists may not be of the same size as they do not
+guarantee the elements within them are unique, so size does not factor into the
+computation.
+
+```
+sets.equivalent(list(T), list(T)) -> bool
+```
+
+Examples:
+
+```
+sets.equivalent([], []) // true
+sets.equivalent([1], [1, 1]) // true
+sets.equivalent([1], [1u, 1.0]) // true
+sets.equivalent([1, 2, 3], [3u, 2.0, 1]) // true
+```
+
+### Sets.Intersects
+
+Returns whether the first list has at least one element whose value is equal to
+an element in the second list. If either list is empty, the result will be
+false.
+
+```
+sets.intersects(list(T), list(T)) -> bool
+```
+
+Examples:
+
+```
+sets.intersects([1], []) // false
+sets.intersects([1], [1, 2]) // true
+sets.intersects([[1], [2, 3]], [[1, 2], [2, 3.0]]) // true
+```
+
diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel
index c2a362a81..1edea7cb0 100644
--- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel
+++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel
@@ -23,6 +23,7 @@ java_library(
"//extensions",
"//extensions:math",
"//extensions:optional_library",
+ "//extensions:sets",
"//extensions:strings",
"//parser:macro",
"//runtime",
diff --git a/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java
new file mode 100644
index 000000000..70396ee45
--- /dev/null
+++ b/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java
@@ -0,0 +1,328 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.extensions;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import com.google.testing.junit.testparameterinjector.TestParameters;
+import dev.cel.common.CelAbstractSyntaxTree;
+import dev.cel.common.CelValidationException;
+import dev.cel.common.CelValidationResult;
+import dev.cel.common.types.ListType;
+import dev.cel.common.types.SimpleType;
+import dev.cel.compiler.CelCompiler;
+import dev.cel.compiler.CelCompilerFactory;
+import dev.cel.extensions.CelSetsExtensions.Function;
+import dev.cel.runtime.CelEvaluationException;
+import dev.cel.runtime.CelRuntime;
+import dev.cel.runtime.CelRuntimeFactory;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(TestParameterInjector.class)
+public final class CelSetsExtensionsTest {
+ private static final CelCompiler COMPILER =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .addLibraries(CelExtensions.sets())
+ .addVar("list", ListType.create(SimpleType.INT))
+ .addVar("subList", ListType.create(SimpleType.INT))
+ .build();
+
+ private static final CelRuntime RUNTIME =
+ CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(CelExtensions.sets()).build();
+
+ @Test
+ public void contains_integerListWithSameValue_succeeds() throws Exception {
+ ImmutableList list = ImmutableList.of(1, 2, 3, 4);
+ ImmutableList subList = ImmutableList.of(1, 2, 3, 4);
+ CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains(list, subList)").getAst();
+ CelRuntime.Program program = RUNTIME.createProgram(ast);
+
+ Object result = program.eval(ImmutableMap.of("list", list, "subList", subList));
+
+ assertThat(result).isEqualTo(true);
+ }
+
+ @Test
+ public void contains_integerListAsExpression_succeeds() throws Exception {
+ CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains([1, 1], [1])").getAst();
+ CelRuntime.Program program = RUNTIME.createProgram(ast);
+
+ Object result = program.eval();
+
+ assertThat(result).isEqualTo(true);
+ }
+
+ @Test
+ @TestParameters("{list: [1, 2, 3, 4], subList: [1, 2, 3, 4], expected: true}")
+ @TestParameters("{list: [5, 4, 3, 2, 1], subList: [1, 2, 3], expected: true}")
+ @TestParameters("{list: [5, 4, 3, 2, 1], subList: [1, 1, 1, 1, 1], expected: true}")
+ @TestParameters("{list: [], subList: [], expected: true}")
+ @TestParameters("{list: [1], subList: [], expected: true}")
+ @TestParameters("{list: [], subList: [1], expected: false}")
+ @TestParameters("{list: [1], subList: [1], expected: true}")
+ @TestParameters("{list: [1], subList: [1, 1], expected: true}")
+ @TestParameters("{list: [1, 1], subList: [1, 1], expected: true}")
+ @TestParameters("{list: [2, 1], subList: [1], expected: true}")
+ @TestParameters("{list: [1, 2, 3, 4], subList: [2, 3], expected: true}")
+ @TestParameters("{list: [1], subList: [2], expected: false}")
+ @TestParameters("{list: [1], subList: [1, 2], expected: false}")
+ public void contains_withIntTypes_succeeds(
+ List list, List subList, boolean expected) throws Exception {
+ CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains(list, subList)").getAst();
+ CelRuntime.Program program = RUNTIME.createProgram(ast);
+
+ Object result = program.eval(ImmutableMap.of("list", list, "subList", subList));
+
+ assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ @TestParameters("{list: [1.0], subList: [1.0, 1.0], expected: true}")
+ @TestParameters("{list: [1.0, 1.00], subList: [1], expected: true}")
+ @TestParameters("{list: [1.0], subList: [1.00], expected: true}")
+ @TestParameters("{list: [1.414], subList: [], expected: true}")
+ @TestParameters("{list: [], subList: [1.414], expected: false}")
+ @TestParameters("{list: [3.14, 2.71], subList: [2.71], expected: true}")
+ @TestParameters("{list: [3.9], subList: [3.1], expected: false}")
+ @TestParameters("{list: [3.2], subList: [3.1], expected: false}")
+ @TestParameters("{list: [2, 3.0], subList: [2, 3], expected: true}")
+ public void contains_withDoubleTypes_succeeds(
+ List list, List subList, boolean expected) throws Exception {
+ CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains(list, subList)").getAst();
+ CelRuntime.Program program = RUNTIME.createProgram(ast);
+
+ Object result = program.eval(ImmutableMap.of("list", list, "subList", subList));
+
+ assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ @TestParameters("{expression: 'sets.contains([[1], [2, 3]], [[2, 3]])', expected: true}")
+ @TestParameters("{expression: 'sets.contains([[1], [2], [3]], [[2, 3]])', expected: false}")
+ @TestParameters(
+ "{expression: 'sets.contains([[1, 2], [2, 3]], [[1], [2, 3.0]])', expected: false}")
+ @TestParameters("{expression: 'sets.contains([[1], [2, 3.0]], [[2, 3]])', expected: true}")
+ public void contains_withNestedLists_succeeds(String expression, boolean expected)
+ throws Exception {
+ CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst();
+ CelRuntime.Program program = RUNTIME.createProgram(ast);
+
+ Object result = program.eval();
+
+ assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ @TestParameters("{expression: 'sets.contains([1, \"1\"], [1])', expected: true}")
+ @TestParameters("{expression: 'sets.contains([1], [1, \"1\"])', expected: false}")
+ public void contains_withMixingIntAndString_succeeds(String expression, boolean expected)
+ throws Exception {
+ CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst();
+ CelRuntime.Program program = RUNTIME.createProgram(ast);
+
+ Object result = program.eval();
+
+ assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ @TestParameters("{expression: 'sets.contains([1], [\"1\"])'}")
+ @TestParameters("{expression: 'sets.contains([\"1\"], [1])'}")
+ public void contains_withMixingIntAndString_throwsException(String expression) throws Exception {
+ CelValidationResult invalidData = COMPILER.compile(expression);
+
+ assertThat(invalidData.getErrors()).hasSize(1);
+ assertThat(invalidData.getErrors().get(0).getMessage())
+ .contains("found no matching overload for 'sets.contains'");
+ }
+
+ @Test
+ public void contains_withMixedValues_succeeds() throws Exception {
+ CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains([1, 2], [2u, 2.0])").getAst();
+ CelRuntime.Program program = RUNTIME.createProgram(ast);
+
+ Object result = program.eval();
+
+ assertThat(result).isEqualTo(true);
+ }
+
+ @Test
+ @TestParameters("{expression: 'sets.contains([[1], [2, 3.0]], [[2, 3]])', expected: true}")
+ @TestParameters(
+ "{expression: 'sets.contains([[1], [2, 3.0], [[[[[5]]]]]], [[[[[[5]]]]]])', expected: true}")
+ @TestParameters(
+ "{expression: 'sets.contains([[1], [2, 3.0], [[[[[5]]]]]], [[[[[[5, 1]]]]]])', expected:"
+ + " false}")
+ @TestParameters(
+ "{expression: 'sets.contains([[1], [2, 3.0], [[[[[5, 1]]]]]], [[[[[[5]]]]]])', expected:"
+ + " false}")
+ @TestParameters(
+ "{expression: 'sets.contains([[[[[[5]]]]]], [[1], [2, 3.0], [[[[[5]]]]]])', expected: false}")
+ public void contains_withMultiLevelNestedList_succeeds(String expression, boolean expected)
+ throws Exception {
+ CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst();
+ CelRuntime.Program program = RUNTIME.createProgram(ast);
+
+ Object result = program.eval();
+
+ assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ @TestParameters("{expression: 'sets.equivalent([], [])', expected: true}")
+ @TestParameters("{expression: 'sets.equivalent([1], [1])', expected: true}")
+ @TestParameters("{expression: 'sets.equivalent([1], [1, 1])', expected: true}")
+ @TestParameters("{expression: 'sets.equivalent([1, 1], [1])', expected: true}")
+ @TestParameters("{expression: 'sets.equivalent([[1], [2, 3]], [[1], [2, 3]])', expected: true}")
+ @TestParameters("{expression: 'sets.equivalent([2, 1], [1])', expected: false}")
+ @TestParameters("{expression: 'sets.equivalent([1], [1, 2])', expected: false}")
+ @TestParameters("{expression: 'sets.equivalent([1, 2], [1, 2, 3])', expected: false}")
+ @TestParameters("{expression: 'sets.equivalent([1, 2], [2, 2, 2])', expected: false}")
+ public void equivalent_withIntTypes_succeeds(String expression, boolean expected)
+ throws Exception {
+ CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst();
+ CelRuntime.Program program = RUNTIME.createProgram(ast);
+
+ Object result = program.eval();
+
+ assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ @TestParameters("{expression: 'sets.equivalent([1, 2, 3], [3u, 2.0, 1])', expected: true}")
+ @TestParameters("{expression: 'sets.equivalent([1], [1u, 1.0])', expected: true}")
+ @TestParameters("{expression: 'sets.equivalent([1], [1u, 1.0])', expected: true}")
+ @TestParameters(
+ "{expression: 'sets.equivalent([[1.0], [2, 3]], [[1], [2, 3.0]])', expected: true}")
+ @TestParameters("{expression: 'sets.equivalent([1, 2], [2u, 2, 2.0])', expected: false}")
+ @TestParameters("{expression: 'sets.equivalent([1, 2], [1u, 2, 2.3])', expected: false}")
+ public void equivalent_withMixedTypes_succeeds(String expression, boolean expected)
+ throws Exception {
+ CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst();
+ CelRuntime.Program program = RUNTIME.createProgram(ast);
+
+ Object result = program.eval();
+
+ assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ @TestParameters("{expression: 'sets.intersects([], [])', expected: false}")
+ @TestParameters("{expression: 'sets.intersects([1], [])', expected: false}")
+ @TestParameters("{expression: 'sets.intersects([], [1])', expected: false}")
+ @TestParameters("{expression: 'sets.intersects([1], [1])', expected: true}")
+ @TestParameters("{expression: 'sets.intersects([1], [2])', expected: false}")
+ @TestParameters("{expression: 'sets.intersects([1], [1, 1])', expected: true}")
+ @TestParameters("{expression: 'sets.intersects([1, 1], [1])', expected: true}")
+ @TestParameters("{expression: 'sets.intersects([2, 1], [1])', expected: true}")
+ @TestParameters("{expression: 'sets.intersects([1], [1, 2])', expected: true}")
+ @TestParameters("{expression: 'sets.intersects([1], [1.0, 2])', expected: true}")
+ @TestParameters("{expression: 'sets.intersects([1, 2], [2u, 2, 2.0])', expected: true}")
+ @TestParameters("{expression: 'sets.intersects([1, 2], [1, 2, 2.3])', expected: true}")
+ @TestParameters("{expression: 'sets.intersects([0, 1, 2], [1, 2, 2.3])', expected: true}")
+ @TestParameters("{expression: 'sets.intersects([1, 2], [1u, 2, 2.3])', expected: true}")
+ @TestParameters(
+ "{expression: 'sets.intersects([[1], [2, 3]], [[1, 2], [2, 3.0]])', expected: true}")
+ @TestParameters("{expression: 'sets.intersects([1], [\"1\", 2])', expected: false}")
+ @TestParameters("{expression: 'sets.intersects([1], [1.1, 2])', expected: false}")
+ @TestParameters("{expression: 'sets.intersects([1], [1.1, 2u])', expected: false}")
+ public void intersects_withMixedTypes_succeeds(String expression, boolean expected)
+ throws Exception {
+ CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst();
+ CelRuntime.Program program = RUNTIME.createProgram(ast);
+
+ Object result = program.eval();
+
+ assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ public void setsExtension_containsFunctionSubset_succeeds() throws Exception {
+ CelSetsExtensions setsExtensions = CelExtensions.sets(Function.CONTAINS);
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build();
+ CelRuntime celRuntime =
+ CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(setsExtensions).build();
+
+ Object evaluatedResult =
+ celRuntime.createProgram(celCompiler.compile("sets.contains([1, 2], [2])").getAst()).eval();
+
+ assertThat(evaluatedResult).isEqualTo(true);
+ }
+
+ @Test
+ public void setsExtension_equivalentFunctionSubset_succeeds() throws Exception {
+ CelSetsExtensions setsExtensions = CelExtensions.sets(Function.EQUIVALENT);
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build();
+ CelRuntime celRuntime =
+ CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(setsExtensions).build();
+
+ Object evaluatedResult =
+ celRuntime
+ .createProgram(celCompiler.compile("sets.equivalent([1, 1], [1])").getAst())
+ .eval();
+
+ assertThat(evaluatedResult).isEqualTo(true);
+ }
+
+ @Test
+ public void setsExtension_intersectsFunctionSubset_succeeds() throws Exception {
+ CelSetsExtensions setsExtensions = CelExtensions.sets(Function.INTERSECTS);
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build();
+ CelRuntime celRuntime =
+ CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(setsExtensions).build();
+
+ Object evaluatedResult =
+ celRuntime
+ .createProgram(celCompiler.compile("sets.intersects([1, 1], [1])").getAst())
+ .eval();
+
+ assertThat(evaluatedResult).isEqualTo(true);
+ }
+
+ @Test
+ public void setsExtension_compileUnallowedFunction_throws() {
+ CelSetsExtensions setsExtensions = CelExtensions.sets(Function.EQUIVALENT);
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build();
+
+ assertThrows(
+ CelValidationException.class,
+ () -> celCompiler.compile("sets.contains([1, 2], [2])").getAst());
+ }
+
+ @Test
+ public void setsExtension_evaluateUnallowedFunction_throws() throws Exception {
+ CelSetsExtensions setsExtensions = CelExtensions.sets(Function.CONTAINS, Function.EQUIVALENT);
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build();
+ CelRuntime celRuntime =
+ CelRuntimeFactory.standardCelRuntimeBuilder()
+ .addLibraries(CelExtensions.sets(Function.EQUIVALENT))
+ .build();
+
+ CelAbstractSyntaxTree ast = celCompiler.compile("sets.contains([1, 2], [2])").getAst();
+
+ assertThrows(CelEvaluationException.class, () -> celRuntime.createProgram(ast).eval());
+ }
+}
diff --git a/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java b/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java
index 8d35efc2c..aa90f1528 100644
--- a/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java
+++ b/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java
@@ -40,6 +40,8 @@
import dev.cel.common.navigation.TraversalOrder;
import dev.cel.common.types.CelType;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
@@ -95,6 +97,70 @@ public CelMutableAst wrapAstWithNewCelBlock(
return CelMutableAst.of(blockExpr, ast.source());
}
+ /**
+ * Constructs a new global call wrapped in an AST with the provided ASTs as its argument. This
+ * will preserve all macro source information contained within the arguments.
+ */
+ public CelMutableAst newGlobalCall(String function, Collection args) {
+ return newCallAst(Optional.empty(), function, args);
+ }
+
+ /**
+ * Constructs a new global call wrapped in an AST with the provided ASTs as its argument. This
+ * will preserve all macro source information contained within the arguments.
+ */
+ public CelMutableAst newGlobalCall(String function, CelMutableAst... args) {
+ return newGlobalCall(function, Arrays.asList(args));
+ }
+
+ /**
+ * Constructs a new member call wrapped in an AST the provided ASTs as its arguments. This will
+ * preserve all macro source information contained within the arguments.
+ */
+ public CelMutableAst newMemberCall(CelMutableAst target, String function, CelMutableAst... args) {
+ return newMemberCall(target, function, Arrays.asList(args));
+ }
+
+ /**
+ * Constructs a new member call wrapped in an AST the provided ASTs as its arguments. This will
+ * preserve all macro source information contained within the arguments.
+ */
+ public CelMutableAst newMemberCall(
+ CelMutableAst target, String function, Collection args) {
+ return newCallAst(Optional.of(target), function, args);
+ }
+
+ private CelMutableAst newCallAst(
+ Optional target, String function, Collection args) {
+ long maxId = 0;
+ CelMutableSource combinedSource = CelMutableSource.newInstance();
+ for (CelMutableAst arg : args) {
+ CelMutableAst stableArg = stabilizeAst(arg, maxId);
+ maxId = getMaxId(stableArg);
+ combinedSource = combine(combinedSource, stableArg.source());
+ }
+
+ Optional maybeTarget = Optional.empty();
+ if (target.isPresent()) {
+ CelMutableAst stableTarget = stabilizeAst(target.get(), maxId);
+ combinedSource = combine(combinedSource, stableTarget.source());
+ maxId = getMaxId(stableTarget);
+
+ maybeTarget = Optional.of(stableTarget);
+ }
+
+ List exprArgs =
+ args.stream().map(CelMutableAst::expr).collect(toCollection(ArrayList::new));
+ CelMutableCall newCall =
+ maybeTarget
+ .map(celMutableAst -> CelMutableCall.create(celMutableAst.expr(), function, exprArgs))
+ .orElseGet(() -> CelMutableCall.create(function, exprArgs));
+
+ CelMutableExpr newCallExpr = CelMutableExpr.ofCall(++maxId, newCall);
+
+ return CelMutableAst.of(newCallExpr, combinedSource);
+ }
+
/**
* Generates a new bind macro using the provided initialization and result expression, then
* replaces the subtree using the new bind expr at the designated expr ID.
@@ -111,16 +177,17 @@ public CelMutableAst wrapAstWithNewCelBlock(
public CelMutableAst replaceSubtreeWithNewBindMacro(
CelMutableAst ast,
String varName,
- CelMutableExpr varInit,
+ CelMutableAst varInit,
CelMutableExpr resultExpr,
long exprIdToReplace,
boolean populateMacroSource) {
- // Copy the incoming expressions to prevent modifying the root
- long maxId = max(getMaxId(varInit), getMaxId(ast));
+ // Stabilize incoming varInit AST to avoid collision with the main AST
+ long maxId = getMaxId(ast);
+ varInit = stabilizeAst(varInit, maxId);
StableIdGenerator stableIdGenerator = CelExprIdGeneratorFactory.newStableIdGenerator(maxId);
CelMutableExpr newBindMacroExpr =
newBindMacroExpr(
- varName, varInit, CelMutableExpr.newInstance(resultExpr), stableIdGenerator);
+ varName, varInit.expr(), CelMutableExpr.newInstance(resultExpr), stableIdGenerator);
CelMutableSource celSource = CelMutableSource.newInstance();
if (populateMacroSource) {
CelMutableExpr newBindMacroSourceExpr =
@@ -128,9 +195,10 @@ public CelMutableAst replaceSubtreeWithNewBindMacro(
// In situations where the existing AST already contains a macro call (ex: nested cel.binds),
// its macro source must be normalized to make it consistent with the newly generated bind
// macro.
+ celSource = combine(ast.source(), varInit.source());
celSource =
normalizeMacroSource(
- ast.source(),
+ celSource,
-1, // Do not replace any of the subexpr in the macro map.
newBindMacroSourceExpr,
stableIdGenerator::renumberId);
@@ -142,6 +210,26 @@ public CelMutableAst replaceSubtreeWithNewBindMacro(
return replaceSubtree(ast, newBindAst, exprIdToReplace);
}
+ /**
+ * See {@link #replaceSubtreeWithNewBindMacro(CelMutableAst, String, CelMutableAst,
+ * CelMutableExpr, long, boolean)}.
+ */
+ public CelMutableAst replaceSubtreeWithNewBindMacro(
+ CelMutableAst ast,
+ String varName,
+ CelMutableExpr varInit,
+ CelMutableExpr resultExpr,
+ long exprIdToReplace,
+ boolean populateMacroSource) {
+ return replaceSubtreeWithNewBindMacro(
+ ast,
+ varName,
+ CelMutableAst.of(varInit, CelMutableSource.newInstance()),
+ resultExpr,
+ exprIdToReplace,
+ populateMacroSource);
+ }
+
/** Renumbers all the expr IDs in the given AST in a consecutive manner starting from 1. */
public CelMutableAst renumberIdsConsecutively(CelMutableAst mutableAst) {
StableIdGenerator stableIdGenerator = CelExprIdGeneratorFactory.newStableIdGenerator(0);
diff --git a/optimizer/src/test/java/dev/cel/optimizer/AstMutatorTest.java b/optimizer/src/test/java/dev/cel/optimizer/AstMutatorTest.java
index 8d56c2a3a..cdfa1cb00 100644
--- a/optimizer/src/test/java/dev/cel/optimizer/AstMutatorTest.java
+++ b/optimizer/src/test/java/dev/cel/optimizer/AstMutatorTest.java
@@ -446,6 +446,81 @@ public void replaceSubtreeWithNewBindMacro_replaceRootWithNestedBindMacro() thro
assertConsistentMacroCalls(mutatedAst);
}
+ @Test
+ public void replaceSubtreeWithNewBindMacro_varInitContainsMacro_replaceRoot() throws Exception {
+ // Arrange
+ CelAbstractSyntaxTree ast = CEL.compile("true && true").getAst();
+ CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast);
+ CelMutableAst varInitAst = CelMutableAst.fromCelAst(CEL.compile("[].exists(x, x)").getAst());
+ String variableName = "@r0";
+ CelMutableExpr resultExpr =
+ CelMutableExpr.ofCall(
+ CelMutableCall.create(
+ Operator.LOGICAL_AND.getFunction(),
+ CelMutableExpr.ofIdent(variableName),
+ CelMutableExpr.ofIdent(variableName)));
+
+ // Act
+ // Perform the initial replacement. (true && true) -> cel.bind(@r0, [].exists(x, x), @r0 && @r0)
+ mutableAst =
+ AST_MUTATOR.replaceSubtreeWithNewBindMacro(
+ mutableAst,
+ variableName,
+ varInitAst,
+ resultExpr,
+ ast.getExpr().id(),
+ true); // Replace &&
+
+ CelAbstractSyntaxTree mutatedAst = mutableAst.toParsedAst();
+ assertThat(mutatedAst.getSource().getMacroCalls()).hasSize(2);
+ assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(false);
+ assertThat(CEL_UNPARSER.unparse(mutatedAst))
+ .isEqualTo("cel.bind(@r0, [].exists(x, x), @r0 && @r0)");
+ assertConsistentMacroCalls(mutatedAst);
+ }
+
+ @Test
+ public void replaceSubtreeWithNewBindMacro_astAndVarInitContainsMacro_replaceRhs()
+ throws Exception {
+ // Arrange
+ CelAbstractSyntaxTree ast = CEL.compile("[true].exists(y,y) && false").getAst();
+ CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast);
+ CelMutableAst varInitAst = CelMutableAst.fromCelAst(CEL.compile("[].exists(x, x)").getAst());
+ String variableName = "@r0";
+ CelMutableExpr resultExpr = CelMutableExpr.ofIdent(variableName);
+ long falseExprId =
+ CelNavigableAst.fromAst(ast)
+ .getRoot()
+ .children()
+ .map(CelNavigableExpr::expr)
+ .filter(
+ x ->
+ x.getKind().equals(Kind.CONSTANT)
+ && x.constant().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE))
+ .filter(x -> !x.constant().booleanValue())
+ .findAny()
+ .get()
+ .id();
+
+ // Act
+ // Perform the initial replacement. (true && true) -> cel.bind(@r0, [].exists(x, x), @r0 && @r0)
+ mutableAst =
+ AST_MUTATOR.replaceSubtreeWithNewBindMacro(
+ mutableAst,
+ variableName,
+ varInitAst,
+ resultExpr,
+ falseExprId, // Replace false
+ true);
+
+ CelAbstractSyntaxTree mutatedAst = mutableAst.toParsedAst();
+ assertThat(mutatedAst.getSource().getMacroCalls()).hasSize(3);
+ assertThat(CEL.createProgram(CEL.check(mutatedAst).getAst()).eval()).isEqualTo(false);
+ assertThat(CEL_UNPARSER.unparse(mutatedAst))
+ .isEqualTo("[true].exists(y, y) && cel.bind(@r0, [].exists(x, x), @r0)");
+ assertConsistentMacroCalls(mutatedAst);
+ }
+
@Test
public void replaceSubtree_macroReplacedWithConstExpr_macroCallCleared() throws Exception {
CelAbstractSyntaxTree ast =
@@ -1034,6 +1109,29 @@ public void replaceSubtree_iterationLimitReached_throws() throws Exception {
assertThat(e).hasMessageThat().isEqualTo("Max iteration count reached.");
}
+ @Test
+ public void newGlobalCallAst_success() throws Exception {
+ CelMutableAst argAst1 = CelMutableAst.fromCelAst(CEL.compile("[1].exists(x, x >= 1)").getAst());
+ CelMutableAst argAst2 = CelMutableAst.fromCelAst(CEL.compile("'hello'").getAst());
+
+ CelMutableAst callAst = AST_MUTATOR.newGlobalCall("func", argAst1, argAst2);
+
+ assertThat(CEL_UNPARSER.unparse(callAst.toParsedAst()))
+ .isEqualTo("func([1].exists(x, x >= 1), \"hello\")");
+ }
+
+ @Test
+ public void newMemberCallAst_success() throws Exception {
+ CelMutableAst targetAst = CelMutableAst.fromCelAst(CEL.compile("'hello'").getAst());
+ CelMutableAst argAst1 = CelMutableAst.fromCelAst(CEL.compile("[1].exists(x, x >= 1)").getAst());
+ CelMutableAst argAst2 = CelMutableAst.fromCelAst(CEL.compile("'world'").getAst());
+
+ CelMutableAst callAst = AST_MUTATOR.newMemberCall(targetAst, "func", argAst1, argAst2);
+
+ assertThat(CEL_UNPARSER.unparse(callAst.toParsedAst()))
+ .isEqualTo("\"hello\".func([1].exists(x, x >= 1), \"world\")");
+ }
+
/**
* Asserts that the expressions that appears in source_info's macro calls are consistent with the
* actual expr nodes in the AST.
diff --git a/policy/BUILD.bazel b/policy/BUILD.bazel
new file mode 100644
index 000000000..4773984b4
--- /dev/null
+++ b/policy/BUILD.bazel
@@ -0,0 +1,74 @@
+package(
+ default_applicable_licenses = ["//:license"],
+ default_visibility = ["//visibility:public"],
+)
+
+java_library(
+ name = "policy",
+ exports = ["//policy/src/main/java/dev/cel/policy"],
+)
+
+java_library(
+ name = "compiled_rule",
+ exports = ["//policy/src/main/java/dev/cel/policy:compiled_rule"],
+)
+
+java_library(
+ name = "value_string",
+ exports = ["//policy/src/main/java/dev/cel/policy:value_string"],
+)
+
+java_library(
+ name = "source",
+ exports = ["//policy/src/main/java/dev/cel/policy:source"],
+)
+
+java_library(
+ name = "validation_exception",
+ exports = ["//policy/src/main/java/dev/cel/policy:validation_exception"],
+)
+
+java_library(
+ name = "config",
+ exports = ["//policy/src/main/java/dev/cel/policy:config"],
+)
+
+java_library(
+ name = "config_parser",
+ exports = ["//policy/src/main/java/dev/cel/policy:config_parser"],
+)
+
+java_library(
+ name = "parser",
+ exports = ["//policy/src/main/java/dev/cel/policy:parser"],
+)
+
+java_library(
+ name = "parser_context",
+ exports = ["//policy/src/main/java/dev/cel/policy:parser_context"],
+)
+
+java_library(
+ name = "parser_factory",
+ exports = ["//policy/src/main/java/dev/cel/policy:parser_factory"],
+)
+
+java_library(
+ name = "compiler_factory",
+ exports = ["//policy/src/main/java/dev/cel/policy:compiler_factory"],
+)
+
+java_library(
+ name = "parser_builder",
+ exports = ["//policy/src/main/java/dev/cel/policy:parser_builder"],
+)
+
+java_library(
+ name = "compiler",
+ exports = ["//policy/src/main/java/dev/cel/policy:compiler"],
+)
+
+java_library(
+ name = "compiler_builder",
+ exports = ["//policy/src/main/java/dev/cel/policy:compiler_builder"],
+)
diff --git a/policy/src/main/java/dev/cel/policy/BUILD.bazel b/policy/src/main/java/dev/cel/policy/BUILD.bazel
new file mode 100644
index 000000000..15e2977c2
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/BUILD.bazel
@@ -0,0 +1,334 @@
+package(
+ default_applicable_licenses = ["//:license"],
+ default_visibility = [
+ "//policy:__pkg__",
+ ],
+)
+
+java_library(
+ name = "policy",
+ srcs = [
+ "CelPolicy.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":required_fields_checker",
+ ":source",
+ ":value_string",
+ "//:auto_value",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "source",
+ srcs = [
+ "CelPolicySource.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ "//:auto_value",
+ "//common:source",
+ "//common:source_location",
+ "//common/internal",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "validation_exception",
+ srcs = [
+ "CelPolicyValidationException.java",
+ ],
+ tags = [
+ ],
+)
+
+java_library(
+ name = "config",
+ srcs = [
+ "CelPolicyConfig.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":required_fields_checker",
+ ":source",
+ ":validation_exception",
+ "//:auto_value",
+ "//bundle:cel",
+ "//common:compiler_common",
+ "//common:options",
+ "//common/types",
+ "//common/types:type_providers",
+ "//extensions",
+ "//extensions:optional_library",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "config_parser",
+ srcs = [
+ "CelPolicyConfigParser.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":config",
+ ":validation_exception",
+ ],
+)
+
+java_library(
+ name = "parser_factory",
+ srcs = ["CelPolicyParserFactory.java"],
+ tags = [
+ ],
+ deps = [
+ ":config_parser",
+ ":parser_builder",
+ ":yaml_config_parser",
+ ":yaml_parser",
+ "@maven//:org_yaml_snakeyaml",
+ ],
+)
+
+java_library(
+ name = "yaml_parser",
+ srcs = [
+ "CelPolicyYamlParser.java",
+ ],
+ deps = [
+ ":common_internal",
+ ":parser",
+ ":parser_builder",
+ ":parser_context",
+ ":policy",
+ ":source",
+ ":validation_exception",
+ ":value_string",
+ "//common:compiler_common",
+ "//common/internal",
+ "@maven//:com_google_guava_guava",
+ "@maven//:org_yaml_snakeyaml",
+ ],
+)
+
+java_library(
+ name = "parser",
+ srcs = [
+ "CelPolicyParser.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":parser_context",
+ ":policy",
+ ":validation_exception",
+ ],
+)
+
+java_library(
+ name = "parser_builder",
+ srcs = [
+ "CelPolicyParserBuilder.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":parser",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ ],
+)
+
+java_library(
+ name = "compiler",
+ srcs = [
+ "CelPolicyCompiler.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":compiled_rule",
+ ":policy",
+ ":validation_exception",
+ "//common",
+ ],
+)
+
+java_library(
+ name = "compiler_builder",
+ srcs = [
+ "CelPolicyCompilerBuilder.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":compiler",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ ],
+)
+
+java_library(
+ name = "compiler_factory",
+ srcs = ["CelPolicyCompilerFactory.java"],
+ tags = [
+ ],
+ deps = [
+ ":compiler_builder",
+ ":compiler_impl",
+ "//bundle:cel",
+ "//checker:checker_builder",
+ "//compiler",
+ "//compiler:compiler_builder",
+ "//parser:parser_builder",
+ "//runtime",
+ ],
+)
+
+java_library(
+ name = "value_string",
+ srcs = [
+ "ValueString.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ "//:auto_value",
+ ],
+)
+
+java_library(
+ name = "parser_context",
+ srcs = [
+ "ParserContext.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":policy",
+ ":value_string",
+ "//common:compiler_common",
+ ],
+)
+
+java_library(
+ name = "compiled_rule",
+ srcs = ["CelCompiledRule.java"],
+ deps = [
+ ":value_string",
+ "//:auto_value",
+ "//bundle:cel",
+ "//common",
+ "//common:compiler_common",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "compiler_impl",
+ srcs = [
+ "CelPolicyCompilerImpl.java",
+ ],
+ visibility = ["//visibility:private"],
+ deps = [
+ ":compiled_rule",
+ ":compiler",
+ ":compiler_builder",
+ ":policy",
+ ":rule_composer",
+ ":source",
+ ":validation_exception",
+ ":value_string",
+ "//bundle:cel",
+ "//common",
+ "//common:compiler_common",
+ "//common/ast",
+ "//common/types",
+ "//common/types:type_providers",
+ "//optimizer",
+ "//optimizer:optimization_exception",
+ "//optimizer:optimizer_builder",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "required_fields_checker",
+ srcs = [
+ "RequiredFieldsChecker.java",
+ ],
+ visibility = ["//visibility:private"],
+ deps = [
+ "//:auto_value",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "yaml_config_parser",
+ srcs = [
+ "CelPolicyYamlConfigParser.java",
+ ],
+ visibility = ["//visibility:private"],
+ deps = [
+ ":common_internal",
+ ":config",
+ ":config_parser",
+ ":parser_context",
+ ":source",
+ ":validation_exception",
+ "//common:compiler_common",
+ "//common/internal",
+ "@maven//:com_google_guava_guava",
+ "@maven//:org_yaml_snakeyaml",
+ ],
+)
+
+java_library(
+ name = "rule_composer",
+ srcs = ["RuleComposer.java"],
+ visibility = ["//visibility:private"],
+ deps = [
+ ":compiled_rule",
+ "//:auto_value",
+ "//bundle:cel",
+ "//common",
+ "//common:compiler_common",
+ "//common:mutable_ast",
+ "//common/ast",
+ "//extensions:optional_library",
+ "//optimizer:ast_optimizer",
+ "//optimizer:mutable_ast",
+ "//parser:operator",
+ "//policy:value_string",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "common_internal",
+ srcs = [
+ "YamlHelper.java",
+ "YamlParserContextImpl.java",
+ ],
+ visibility = ["//visibility:private"],
+ deps = [
+ ":parser_context",
+ ":source",
+ ":value_string",
+ "//common",
+ "//common:compiler_common",
+ "@maven//:com_google_guava_guava",
+ "@maven//:org_yaml_snakeyaml",
+ ],
+)
diff --git a/policy/src/main/java/dev/cel/policy/CelCompiledRule.java b/policy/src/main/java/dev/cel/policy/CelCompiledRule.java
new file mode 100644
index 000000000..4c39fee94
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelCompiledRule.java
@@ -0,0 +1,119 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import com.google.auto.value.AutoOneOf;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import dev.cel.bundle.Cel;
+import dev.cel.common.CelAbstractSyntaxTree;
+import dev.cel.common.CelVarDecl;
+import java.util.Optional;
+
+/**
+ * Abstract representation of a compiled rule. This contains set of compiled variables and match
+ * statements which defines an expression graph for a policy.
+ */
+@AutoValue
+public abstract class CelCompiledRule {
+ public abstract Optional id();
+
+ public abstract ImmutableList variables();
+
+ public abstract ImmutableList matches();
+
+ public abstract Cel cel();
+
+ /**
+ * A compiled policy variable (ex: variables.foo). Note that this is not the same thing as the
+ * variables declared in the config.
+ */
+ @AutoValue
+ public abstract static class CelCompiledVariable {
+ public abstract String name();
+
+ /** Compiled variable in AST. */
+ public abstract CelAbstractSyntaxTree ast();
+
+ /** The variable declaration used to compile this variable in {@link #ast}. */
+ public abstract CelVarDecl celVarDecl();
+
+ static CelCompiledVariable create(
+ String name, CelAbstractSyntaxTree ast, CelVarDecl celVarDecl) {
+ return new AutoValue_CelCompiledRule_CelCompiledVariable(name, ast, celVarDecl);
+ }
+ }
+
+ /** A compiled Match. */
+ @AutoValue
+ public abstract static class CelCompiledMatch {
+ public abstract CelAbstractSyntaxTree condition();
+
+ public abstract Result result();
+
+ /** Encapsulates the result of this match when condition is met. (either an output or a rule) */
+ @AutoOneOf(CelCompiledMatch.Result.Kind.class)
+ public abstract static class Result {
+ public abstract OutputValue output();
+
+ public abstract CelCompiledRule rule();
+
+ public abstract Kind kind();
+
+ static Result ofOutput(long id, CelAbstractSyntaxTree ast) {
+ return AutoOneOf_CelCompiledRule_CelCompiledMatch_Result.output(
+ OutputValue.create(id, ast));
+ }
+
+ static Result ofRule(CelCompiledRule value) {
+ return AutoOneOf_CelCompiledRule_CelCompiledMatch_Result.rule(value);
+ }
+
+ /** Kind for {@link Result}. */
+ public enum Kind {
+ OUTPUT,
+ RULE
+ }
+ }
+
+ /**
+ * Encapsulates the output value of the match with its original ID that was used to compile
+ * with.
+ */
+ @AutoValue
+ public abstract static class OutputValue {
+ public abstract long id();
+
+ public abstract CelAbstractSyntaxTree ast();
+
+ public static OutputValue create(long id, CelAbstractSyntaxTree ast) {
+ return new AutoValue_CelCompiledRule_CelCompiledMatch_OutputValue(id, ast);
+ }
+ }
+
+ static CelCompiledMatch create(
+ CelAbstractSyntaxTree condition, CelCompiledMatch.Result result) {
+ return new AutoValue_CelCompiledRule_CelCompiledMatch(condition, result);
+ }
+ }
+
+ static CelCompiledRule create(
+ Optional id,
+ ImmutableList variables,
+ ImmutableList matches,
+ Cel cel) {
+ return new AutoValue_CelCompiledRule(id, variables, matches, cel);
+ }
+}
diff --git a/policy/src/main/java/dev/cel/policy/CelPolicy.java b/policy/src/main/java/dev/cel/policy/CelPolicy.java
new file mode 100644
index 000000000..45a2c666c
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelPolicy.java
@@ -0,0 +1,244 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.auto.value.AutoOneOf;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Abstract representation of a policy. It declares a name, rule, and evaluation semantic for a
+ * given expression graph.
+ */
+@AutoValue
+public abstract class CelPolicy {
+
+ public abstract ValueString name();
+
+ public abstract Rule rule();
+
+ public abstract CelPolicySource policySource();
+
+ public abstract ImmutableMap metadata();
+
+ /** Creates a new builder to construct a {@link CelPolicy} instance. */
+ public static Builder newBuilder() {
+ return new AutoValue_CelPolicy.Builder()
+ .setName(ValueString.of(0, ""))
+ .setRule(Rule.newBuilder().build())
+ .setMetadata(ImmutableMap.of());
+ }
+
+ /** Builder for {@link CelPolicy}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract CelPolicySource policySource();
+
+ public abstract Builder setName(ValueString name);
+
+ public abstract Builder setRule(Rule rule);
+
+ public abstract Builder setPolicySource(CelPolicySource policySource);
+
+ // This should stay package-private to encourage add/set methods to be used instead.
+ abstract ImmutableMap.Builder metadataBuilder();
+
+ public abstract Builder setMetadata(ImmutableMap value);
+
+ @CanIgnoreReturnValue
+ public Builder putMetadata(String key, Object value) {
+ metadataBuilder().put(key, value);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder putMetadata(Map map) {
+ metadataBuilder().putAll(map);
+ return this;
+ }
+
+ public abstract CelPolicy build();
+ }
+
+ /**
+ * Rule declares a rule identifier, description, along with a set of variables and match
+ * statements.
+ */
+ @AutoValue
+ public abstract static class Rule {
+
+ public abstract Optional id();
+
+ public abstract Optional description();
+
+ public abstract ImmutableSet variables();
+
+ public abstract ImmutableSet matches();
+
+ /** Builder for {@link Rule}. */
+ public static Builder newBuilder() {
+ return new AutoValue_CelPolicy_Rule.Builder()
+ .setVariables(ImmutableSet.of())
+ .setMatches(ImmutableSet.of());
+ }
+
+ /** Creates a new builder to construct a {@link Rule} instance. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Rule.Builder setId(ValueString id);
+
+ public abstract Rule.Builder setDescription(ValueString description);
+
+ abstract ImmutableSet variables();
+
+ abstract ImmutableSet.Builder variablesBuilder();
+
+ abstract ImmutableSet matches();
+
+ abstract ImmutableSet.Builder matchesBuilder();
+
+ @CanIgnoreReturnValue
+ public Builder addVariables(Variable... variables) {
+ return addVariables(Arrays.asList(variables));
+ }
+
+ @CanIgnoreReturnValue
+ public Builder addVariables(Iterable variables) {
+ this.variablesBuilder().addAll(checkNotNull(variables));
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder addMatches(Match... matches) {
+ return addMatches(Arrays.asList(matches));
+ }
+
+ @CanIgnoreReturnValue
+ public Builder addMatches(Iterable matches) {
+ this.matchesBuilder().addAll(checkNotNull(matches));
+ return this;
+ }
+
+ abstract Rule.Builder setVariables(ImmutableSet variables);
+
+ abstract Rule.Builder setMatches(ImmutableSet matches);
+
+ public abstract Rule build();
+ }
+ }
+
+ /**
+ * Match declares a condition (defaults to true) as well as an output or a rule. Either the output
+ * or the rule field may be set, but not both.
+ */
+ @AutoValue
+ public abstract static class Match {
+
+ public abstract ValueString condition();
+
+ public abstract Result result();
+
+ /** Encapsulates the result of this match when condition is met. (either an output or a rule) */
+ @AutoOneOf(Match.Result.Kind.class)
+ public abstract static class Result {
+ public abstract ValueString output();
+
+ public abstract Rule rule();
+
+ public abstract Kind kind();
+
+ public static Result ofOutput(ValueString value) {
+ return AutoOneOf_CelPolicy_Match_Result.output(value);
+ }
+
+ public static Result ofRule(Rule value) {
+ return AutoOneOf_CelPolicy_Match_Result.rule(value);
+ }
+
+ /** Kind for {@link Result}. */
+ public enum Kind {
+ OUTPUT,
+ RULE
+ }
+ }
+
+ /** Builder for {@link Match}. */
+ @AutoValue.Builder
+ public abstract static class Builder implements RequiredFieldsChecker {
+
+ public abstract Builder setCondition(ValueString condition);
+
+ public abstract Builder setResult(Result result);
+
+ abstract Optional result();
+
+ @Override
+ public ImmutableList requiredFields() {
+ return ImmutableList.of(RequiredField.of("output or a rule", this::result));
+ }
+
+ public abstract Match build();
+ }
+
+ /** Creates a new builder to construct a {@link Match} instance. */
+ public static Builder newBuilder() {
+ return new AutoValue_CelPolicy_Match.Builder();
+ }
+ }
+
+ /** Variable is a named expression which may be referenced in subsequent expressions. */
+ @AutoValue
+ public abstract static class Variable {
+
+ public abstract ValueString name();
+
+ public abstract ValueString expression();
+
+ /** Builder for {@link Variable}. */
+ @AutoValue.Builder
+ public abstract static class Builder implements RequiredFieldsChecker {
+
+ abstract Optional name();
+
+ abstract Optional expression();
+
+ public abstract Builder setName(ValueString name);
+
+ public abstract Builder setExpression(ValueString expression);
+
+ @Override
+ public ImmutableList requiredFields() {
+ return ImmutableList.of(
+ RequiredField.of("name", this::name), RequiredField.of("expression", this::expression));
+ }
+
+ public abstract Variable build();
+ }
+
+ /** Creates a new builder to construct a {@link Variable} instance. */
+ public static Builder newBuilder() {
+ return new AutoValue_CelPolicy_Variable.Builder();
+ }
+ }
+}
diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyCompiler.java b/policy/src/main/java/dev/cel/policy/CelPolicyCompiler.java
new file mode 100644
index 000000000..e0af6d85b
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompiler.java
@@ -0,0 +1,45 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import dev.cel.common.CelAbstractSyntaxTree;
+
+/** Public interface for compiling CEL policies. */
+public interface CelPolicyCompiler {
+
+ /**
+ * Combines the {@link #compileRule} and {@link #compose} into a single call.
+ *
+ *
This generates a single CEL AST from a collection of policy expressions associated with a
+ * CEL environment.
+ */
+ default CelAbstractSyntaxTree compile(CelPolicy policy) throws CelPolicyValidationException {
+ return compose(policy, compileRule(policy));
+ }
+
+ /**
+ * Produces a {@link CelCompiledRule} from the policy which contains a set of compiled variables
+ * and match statements. Compiled rule defines an expression graph, which can be composed into a
+ * single expression via {@link #compose} call.
+ */
+ CelCompiledRule compileRule(CelPolicy policy) throws CelPolicyValidationException;
+
+ /**
+ * Composes {@link CelCompiledRule}, representing an expression graph, into a single expression
+ * value.
+ */
+ CelAbstractSyntaxTree compose(CelPolicy policy, CelCompiledRule compiledRule)
+ throws CelPolicyValidationException;
+}
diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyCompilerBuilder.java b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerBuilder.java
new file mode 100644
index 000000000..13bac2885
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerBuilder.java
@@ -0,0 +1,36 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.CheckReturnValue;
+
+/** Interface for building an instance of {@link CelPolicyCompiler} */
+public interface CelPolicyCompilerBuilder {
+
+ /** Sets the prefix for the policy variables. Default is `variables.`. */
+ @CanIgnoreReturnValue
+ CelPolicyCompilerBuilder setVariablesPrefix(String prefix);
+
+ /**
+ * Limit the number of iteration while composing rules into a single AST. An exception is thrown
+ * if the iteration count exceeds the set value.
+ */
+ @CanIgnoreReturnValue
+ CelPolicyCompilerBuilder setIterationLimit(int iterationLimit);
+
+ @CheckReturnValue
+ CelPolicyCompiler build();
+}
diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyCompilerFactory.java b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerFactory.java
new file mode 100644
index 000000000..8641e5bb7
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerFactory.java
@@ -0,0 +1,46 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import dev.cel.bundle.Cel;
+import dev.cel.bundle.CelFactory;
+import dev.cel.checker.CelChecker;
+import dev.cel.compiler.CelCompiler;
+import dev.cel.compiler.CelCompilerFactory;
+import dev.cel.parser.CelParser;
+import dev.cel.runtime.CelRuntime;
+
+/** Factory class for producing policy compilers. */
+public final class CelPolicyCompilerFactory {
+
+ /** Create a builder for constructing a {@link CelPolicyCompiler} instance. */
+ public static CelPolicyCompilerBuilder newPolicyCompiler(Cel cel) {
+ return CelPolicyCompilerImpl.newBuilder(cel);
+ }
+
+ /** Create a builder for constructing a {@link CelPolicyCompiler} instance. */
+ public static CelPolicyCompilerBuilder newPolicyCompiler(
+ CelCompiler celCompiler, CelRuntime celRuntime) {
+ return newPolicyCompiler(CelFactory.combine(celCompiler, celRuntime));
+ }
+
+ /** Create a builder for constructing a {@link CelPolicyCompiler} instance. */
+ public static CelPolicyCompilerBuilder newPolicyCompiler(
+ CelParser celParser, CelChecker celChecker, CelRuntime celRuntime) {
+ return newPolicyCompiler(CelCompilerFactory.combine(celParser, celChecker), celRuntime);
+ }
+
+ private CelPolicyCompilerFactory() {}
+}
diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java
new file mode 100644
index 000000000..8ca38dad4
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java
@@ -0,0 +1,271 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import dev.cel.bundle.Cel;
+import dev.cel.common.CelAbstractSyntaxTree;
+import dev.cel.common.CelIssue;
+import dev.cel.common.CelSource;
+import dev.cel.common.CelSourceLocation;
+import dev.cel.common.CelValidationException;
+import dev.cel.common.CelVarDecl;
+import dev.cel.common.ast.CelConstant;
+import dev.cel.common.ast.CelExpr;
+import dev.cel.common.types.CelType;
+import dev.cel.common.types.SimpleType;
+import dev.cel.optimizer.CelOptimizationException;
+import dev.cel.optimizer.CelOptimizer;
+import dev.cel.optimizer.CelOptimizerFactory;
+import dev.cel.policy.CelCompiledRule.CelCompiledMatch;
+import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
+import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
+import dev.cel.policy.CelPolicy.Match;
+import dev.cel.policy.CelPolicy.Variable;
+import dev.cel.policy.RuleComposer.RuleCompositionException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+/** Package-private implementation for policy compiler. */
+final class CelPolicyCompilerImpl implements CelPolicyCompiler {
+ private static final String DEFAULT_VARIABLE_PREFIX = "variables.";
+ private static final int DEFAULT_ITERATION_LIMIT = 1000;
+ private final Cel cel;
+ private final String variablesPrefix;
+ private final int iterationLimit;
+
+ @Override
+ public CelCompiledRule compileRule(CelPolicy policy) throws CelPolicyValidationException {
+ CompilerContext compilerContext = new CompilerContext(policy.policySource());
+ CelCompiledRule compiledRule = compileRuleImpl(policy.rule(), cel, compilerContext);
+ if (compilerContext.hasError()) {
+ throw new CelPolicyValidationException(compilerContext.getIssueString());
+ }
+
+ return compiledRule;
+ }
+
+ @Override
+ public CelAbstractSyntaxTree compose(CelPolicy policy, CelCompiledRule compiledRule)
+ throws CelPolicyValidationException {
+ CelOptimizer optimizer =
+ CelOptimizerFactory.standardCelOptimizerBuilder(compiledRule.cel())
+ .addAstOptimizers(
+ RuleComposer.newInstance(compiledRule, variablesPrefix, iterationLimit))
+ .build();
+
+ CelAbstractSyntaxTree ast;
+
+ try {
+ // This is a minimal expression used as a basis of stitching together all the rules into a
+ // single graph.
+ ast = cel.compile("true").getAst();
+ ast = optimizer.optimize(ast);
+ } catch (CelValidationException | CelOptimizationException e) {
+ if (e.getCause() instanceof RuleCompositionException) {
+ RuleCompositionException re = (RuleCompositionException) e.getCause();
+ CompilerContext compilerContext = new CompilerContext(policy.policySource());
+ // The exact CEL error message produced from composition failure isn't too useful for users.
+ // Ex: ERROR: :1:1: found no matching overload for '_?_:_' applied to '(bool, map(int, int),
+ // bool)' (candidates: (bool, %A0, %A0))
+ // Transform the error messages in a user-friendly way while retaining the original
+ // CelValidationException as its originating cause.
+
+ ImmutableList transformedIssues =
+ re.compileException.getErrors().stream()
+ .map(x -> CelIssue.formatError(x.getSourceLocation(), re.failureReason))
+ .collect(toImmutableList());
+ for (long id : re.errorIds) {
+ compilerContext.addIssue(id, transformedIssues);
+ }
+
+ throw new CelPolicyValidationException(compilerContext.getIssueString(), re.getCause());
+ }
+
+ // Something has gone seriously wrong.
+ throw new CelPolicyValidationException("Unexpected error while composing rules.", e);
+ }
+
+ return ast;
+ }
+
+ private CelCompiledRule compileRuleImpl(
+ CelPolicy.Rule rule, Cel ruleCel, CompilerContext compilerContext) {
+ ImmutableList.Builder variableBuilder = ImmutableList.builder();
+ for (Variable variable : rule.variables()) {
+ ValueString expression = variable.expression();
+ CelAbstractSyntaxTree varAst;
+ CelType outputType = SimpleType.DYN;
+ try {
+ varAst = ruleCel.compile(expression.value()).getAst();
+ outputType = varAst.getResultType();
+ } catch (CelValidationException e) {
+ compilerContext.addIssue(expression.id(), e.getErrors());
+ // A sentinel AST representing an error is created to allow compiler checks to continue
+ varAst = newErrorAst();
+ }
+ String variableName = variable.name().value();
+ CelVarDecl newVariable =
+ CelVarDecl.newVarDeclaration(variablesPrefix + variableName, outputType);
+ ruleCel = ruleCel.toCelBuilder().addVarDeclarations(newVariable).build();
+ variableBuilder.add(CelCompiledVariable.create(variableName, varAst, newVariable));
+ }
+
+ ImmutableList.Builder matchBuilder = ImmutableList.builder();
+ for (Match match : rule.matches()) {
+ CelAbstractSyntaxTree conditionAst;
+ try {
+ conditionAst = ruleCel.compile(match.condition().value()).getAst();
+ if (!conditionAst.getResultType().equals(SimpleType.BOOL)) {
+ compilerContext.addIssue(
+ match.condition().id(),
+ CelIssue.formatError(1, 0, "condition must produce a boolean output."));
+ }
+ } catch (CelValidationException e) {
+ compilerContext.addIssue(match.condition().id(), e.getErrors());
+ continue;
+ }
+
+ Result matchResult;
+ switch (match.result().kind()) {
+ case OUTPUT:
+ CelAbstractSyntaxTree outputAst;
+ ValueString output = match.result().output();
+ try {
+ outputAst = ruleCel.compile(output.value()).getAst();
+ } catch (CelValidationException e) {
+ compilerContext.addIssue(output.id(), e.getErrors());
+ continue;
+ }
+
+ matchResult = Result.ofOutput(output.id(), outputAst);
+ break;
+ case RULE:
+ CelCompiledRule nestedRule =
+ compileRuleImpl(match.result().rule(), ruleCel, compilerContext);
+ matchResult = Result.ofRule(nestedRule);
+ break;
+ default:
+ throw new IllegalArgumentException("Unexpected kind: " + match.result().kind());
+ }
+
+ matchBuilder.add(CelCompiledMatch.create(conditionAst, matchResult));
+ }
+
+ return CelCompiledRule.create(rule.id(), variableBuilder.build(), matchBuilder.build(), cel);
+ }
+
+ private static CelAbstractSyntaxTree newErrorAst() {
+ return CelAbstractSyntaxTree.newParsedAst(
+ CelExpr.ofConstant(0, CelConstant.ofValue("*error*")), CelSource.newBuilder().build());
+ }
+
+ private static final class CompilerContext {
+ private final ArrayList issues;
+ private final CelPolicySource celPolicySource;
+
+ private void addIssue(long id, CelIssue... issues) {
+ addIssue(id, Arrays.asList(issues));
+ }
+
+ private void addIssue(long id, List issues) {
+ for (CelIssue issue : issues) {
+ CelSourceLocation absoluteLocation = computeAbsoluteLocation(id, issue);
+ this.issues.add(CelIssue.formatError(absoluteLocation, issue.getMessage()));
+ }
+ }
+
+ private CelSourceLocation computeAbsoluteLocation(long id, CelIssue issue) {
+ int policySourceOffset =
+ Optional.ofNullable(celPolicySource.getPositionsMap().get(id)).orElse(-1);
+ if (policySourceOffset == -1) {
+ return CelSourceLocation.NONE;
+ }
+ CelSourceLocation policySourceLocation =
+ celPolicySource.getOffsetLocation(policySourceOffset).orElse(null);
+ if (policySourceLocation == null) {
+ return CelSourceLocation.NONE;
+ }
+
+ int absoluteLine = issue.getSourceLocation().getLine() + policySourceLocation.getLine() - 1;
+ int absoluteColumn = issue.getSourceLocation().getColumn() + policySourceLocation.getColumn();
+ int absoluteOffset = celPolicySource.getContent().lineOffsets().get(absoluteLine - 2);
+
+ return celPolicySource
+ .getOffsetLocation(absoluteOffset + absoluteColumn)
+ .orElse(CelSourceLocation.NONE);
+ }
+
+ private boolean hasError() {
+ return !issues.isEmpty();
+ }
+
+ private String getIssueString() {
+ return CelIssue.toDisplayString(issues, celPolicySource);
+ }
+
+ private CompilerContext(CelPolicySource celPolicySource) {
+ this.issues = new ArrayList<>();
+ this.celPolicySource = celPolicySource;
+ }
+ }
+
+ static final class Builder implements CelPolicyCompilerBuilder {
+ private final Cel cel;
+ private String variablesPrefix;
+ private int iterationLimit;
+
+ private Builder(Cel cel) {
+ this.cel = cel;
+ }
+
+ @Override
+ @CanIgnoreReturnValue
+ public Builder setVariablesPrefix(String prefix) {
+ this.variablesPrefix = checkNotNull(prefix);
+ return this;
+ }
+
+ @Override
+ @CanIgnoreReturnValue
+ public Builder setIterationLimit(int iterationLimit) {
+ this.iterationLimit = iterationLimit;
+ return this;
+ }
+
+ @Override
+ public CelPolicyCompiler build() {
+ return new CelPolicyCompilerImpl(cel, this.variablesPrefix, this.iterationLimit);
+ }
+ }
+
+ static Builder newBuilder(Cel cel) {
+ return new Builder(cel)
+ .setVariablesPrefix(DEFAULT_VARIABLE_PREFIX)
+ .setIterationLimit(DEFAULT_ITERATION_LIMIT);
+ }
+
+ private CelPolicyCompilerImpl(Cel cel, String variablesPrefix, int iterationLimit) {
+ this.cel = checkNotNull(cel);
+ this.variablesPrefix = checkNotNull(variablesPrefix);
+ this.iterationLimit = iterationLimit;
+ }
+}
diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyConfig.java b/policy/src/main/java/dev/cel/policy/CelPolicyConfig.java
new file mode 100644
index 000000000..c5acc43f5
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelPolicyConfig.java
@@ -0,0 +1,511 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.CheckReturnValue;
+import dev.cel.bundle.Cel;
+import dev.cel.bundle.CelBuilder;
+import dev.cel.common.CelFunctionDecl;
+import dev.cel.common.CelOptions;
+import dev.cel.common.CelOverloadDecl;
+import dev.cel.common.CelVarDecl;
+import dev.cel.common.types.CelType;
+import dev.cel.common.types.CelTypeProvider;
+import dev.cel.common.types.ListType;
+import dev.cel.common.types.MapType;
+import dev.cel.common.types.OptionalType;
+import dev.cel.common.types.SimpleType;
+import dev.cel.common.types.TypeParamType;
+import dev.cel.extensions.CelExtensions;
+import dev.cel.extensions.CelOptionalLibrary;
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * CelPolicyConfig represents the policy configuration object to extend the CEL's compilation and
+ * runtime environment with.
+ */
+@AutoValue
+public abstract class CelPolicyConfig {
+
+ /** Config source. */
+ public abstract CelPolicySource configSource();
+
+ /** Name of the config. */
+ public abstract String name();
+
+ /**
+ * An optional description of the config (example: location of the file containing the config
+ * content).
+ */
+ public abstract String description();
+
+ /**
+ * Container name to use as the namespace for resolving CEL expression variables and functions.
+ */
+ public abstract String container();
+
+ /**
+ * Canonical extensions to enable in the environment, such as Optional, String and Math
+ * extensions.
+ */
+ public abstract ImmutableSet extensions();
+
+ /** New variable declarations to add in the compilation environment. */
+ public abstract ImmutableSet variables();
+
+ /** New function declarations to add in the compilation environment. */
+ public abstract ImmutableSet functions();
+
+ /** Builder for {@link CelPolicyConfig}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder setConfigSource(CelPolicySource value);
+
+ public abstract Builder setName(String name);
+
+ public abstract Builder setDescription(String description);
+
+ public abstract Builder setContainer(String container);
+
+ public abstract Builder setExtensions(ImmutableSet extensions);
+
+ public abstract Builder setVariables(ImmutableSet variables);
+
+ public abstract Builder setFunctions(ImmutableSet functions);
+
+ @CheckReturnValue
+ public abstract CelPolicyConfig build();
+ }
+
+ /** Creates a new builder to construct a {@link CelPolicyConfig} instance. */
+ public static Builder newBuilder() {
+ return new AutoValue_CelPolicyConfig.Builder()
+ .setName("")
+ .setDescription("")
+ .setContainer("")
+ .setExtensions(ImmutableSet.of())
+ .setVariables(ImmutableSet.of())
+ .setFunctions(ImmutableSet.of());
+ }
+
+ /** Extends the provided {@code cel} environment with this configuration. */
+ public Cel extend(Cel cel, CelOptions celOptions) throws CelPolicyValidationException {
+ try {
+ CelTypeProvider celTypeProvider = cel.getTypeProvider();
+ CelBuilder celBuilder =
+ cel.toCelBuilder()
+ .setTypeProvider(celTypeProvider)
+ .setContainer(container())
+ .addVarDeclarations(
+ variables().stream()
+ .map(v -> v.toCelVarDecl(celTypeProvider))
+ .collect(toImmutableList()))
+ .addFunctionDeclarations(
+ functions().stream()
+ .map(f -> f.toCelFunctionDecl(celTypeProvider))
+ .collect(toImmutableList()));
+
+ addAllExtensions(celBuilder, celOptions);
+
+ return celBuilder.build();
+ } catch (RuntimeException e) {
+ throw new CelPolicyValidationException(e.getMessage(), e);
+ }
+ }
+
+ private void addAllExtensions(CelBuilder celBuilder, CelOptions celOptions) {
+ for (ExtensionConfig extensionConfig : extensions()) {
+ switch (extensionConfig.name()) {
+ case "bindings":
+ celBuilder.addCompilerLibraries(CelExtensions.bindings());
+ break;
+ case "encoders":
+ celBuilder.addCompilerLibraries(CelExtensions.encoders());
+ celBuilder.addRuntimeLibraries(CelExtensions.encoders());
+ break;
+ case "math":
+ celBuilder.addCompilerLibraries(CelExtensions.math(celOptions));
+ celBuilder.addRuntimeLibraries(CelExtensions.math(celOptions));
+ break;
+ case "optional":
+ celBuilder.addCompilerLibraries(CelOptionalLibrary.INSTANCE);
+ celBuilder.addRuntimeLibraries(CelOptionalLibrary.INSTANCE);
+ break;
+ case "protos":
+ celBuilder.addCompilerLibraries(CelExtensions.protos());
+ break;
+ case "strings":
+ celBuilder.addCompilerLibraries(CelExtensions.strings());
+ celBuilder.addRuntimeLibraries(CelExtensions.strings());
+ break;
+ case "sets":
+ celBuilder.addCompilerLibraries(CelExtensions.sets());
+ celBuilder.addRuntimeLibraries(CelExtensions.sets());
+ break;
+ default:
+ throw new IllegalArgumentException("Unrecognized extension: " + extensionConfig.name());
+ }
+ }
+ }
+
+ /** Represents a policy variable declaration. */
+ @AutoValue
+ public abstract static class VariableDecl {
+
+ /** Fully qualified variable name. */
+ public abstract String name();
+
+ /** The type of the variable. */
+ public abstract TypeDecl type();
+
+ /** Builder for {@link VariableDecl}. */
+ @AutoValue.Builder
+ public abstract static class Builder implements RequiredFieldsChecker {
+
+ public abstract Optional name();
+
+ public abstract Optional type();
+
+ public abstract Builder setName(String name);
+
+ public abstract Builder setType(TypeDecl typeDecl);
+
+ @Override
+ public ImmutableList requiredFields() {
+ return ImmutableList.of(
+ RequiredField.of("name", this::name), RequiredField.of("type", this::type));
+ }
+
+ /** Builds a new instance of {@link VariableDecl}. */
+ public abstract VariableDecl build();
+ }
+
+ public static Builder newBuilder() {
+ return new AutoValue_CelPolicyConfig_VariableDecl.Builder();
+ }
+
+ /** Creates a new builder to construct a {@link VariableDecl} instance. */
+ public static VariableDecl create(String name, TypeDecl type) {
+ return newBuilder().setName(name).setType(type).build();
+ }
+
+ /** Converts this policy variable declaration into a {@link CelVarDecl}. */
+ public CelVarDecl toCelVarDecl(CelTypeProvider celTypeProvider) {
+ return CelVarDecl.newVarDeclaration(name(), type().toCelType(celTypeProvider));
+ }
+ }
+
+ /** Represents a policy function declaration. */
+ @AutoValue
+ public abstract static class FunctionDecl {
+
+ public abstract String name();
+
+ public abstract ImmutableSet overloads();
+
+ /** Builder for {@link FunctionDecl}. */
+ @AutoValue.Builder
+ public abstract static class Builder implements RequiredFieldsChecker {
+
+ public abstract Optional name();
+
+ public abstract Optional> overloads();
+
+ public abstract Builder setName(String name);
+
+ public abstract Builder setOverloads(ImmutableSet overloads);
+
+ @Override
+ public ImmutableList requiredFields() {
+ return ImmutableList.of(
+ RequiredField.of("name", this::name), RequiredField.of("overloads", this::overloads));
+ }
+
+ /** Builds a new instance of {@link FunctionDecl}. */
+ public abstract FunctionDecl build();
+ }
+
+ /** Creates a new builder to construct a {@link FunctionDecl} instance. */
+ public static Builder newBuilder() {
+ return new AutoValue_CelPolicyConfig_FunctionDecl.Builder();
+ }
+
+ /** Creates a new {@link FunctionDecl} with the provided function name and its overloads. */
+ public static FunctionDecl create(String name, ImmutableSet overloads) {
+ return newBuilder().setName(name).setOverloads(overloads).build();
+ }
+
+ /** Converts this policy function declaration into a {@link CelFunctionDecl}. */
+ public CelFunctionDecl toCelFunctionDecl(CelTypeProvider celTypeProvider) {
+ return CelFunctionDecl.newFunctionDeclaration(
+ name(),
+ overloads().stream()
+ .map(o -> o.toCelOverloadDecl(celTypeProvider))
+ .collect(toImmutableList()));
+ }
+ }
+
+ /** Represents an overload declaration on a policy function. */
+ @AutoValue
+ public abstract static class OverloadDecl {
+
+ /**
+ * A unique overload ID. Required. This should follow the typical naming convention used in CEL
+ * (e.g: targetType_func_argType1_argType...)
+ */
+ public abstract String id();
+
+ /** Target of the function overload if it's a receiver style (example: foo in `foo.f(...)`) */
+ public abstract Optional target();
+
+ /** List of function overload type values. */
+ public abstract ImmutableList arguments();
+
+ /** Return type of the overload. Required. */
+ public abstract TypeDecl returnType();
+
+ /** Builder for {@link OverloadDecl}. */
+ @AutoValue.Builder
+ public abstract static class Builder implements RequiredFieldsChecker {
+
+ public abstract Optional id();
+
+ public abstract Optional returnType();
+
+ public abstract Builder setId(String overloadId);
+
+ public abstract Builder setTarget(TypeDecl target);
+
+ // This should stay package-private to encourage add/set methods to be used instead.
+ abstract ImmutableList.Builder argumentsBuilder();
+
+ public abstract Builder setArguments(ImmutableList args);
+
+ @CanIgnoreReturnValue
+ public Builder addArguments(Iterable args) {
+ this.argumentsBuilder().addAll(checkNotNull(args));
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder addArguments(TypeDecl... args) {
+ return addArguments(Arrays.asList(args));
+ }
+
+ public abstract Builder setReturnType(TypeDecl returnType);
+
+ @Override
+ public ImmutableList requiredFields() {
+ return ImmutableList.of(
+ RequiredField.of("id", this::id), RequiredField.of("return", this::returnType));
+ }
+
+ /** Builds a new instance of {@link OverloadDecl}. */
+ @CheckReturnValue
+ public abstract OverloadDecl build();
+ }
+
+ /** Creates a new builder to construct a {@link OverloadDecl} instance. */
+ public static Builder newBuilder() {
+ return new AutoValue_CelPolicyConfig_OverloadDecl.Builder().setArguments(ImmutableList.of());
+ }
+
+ /** Converts this policy function overload into a {@link CelOverloadDecl}. */
+ public CelOverloadDecl toCelOverloadDecl(CelTypeProvider celTypeProvider) {
+ CelOverloadDecl.Builder builder =
+ CelOverloadDecl.newBuilder()
+ .setIsInstanceFunction(false)
+ .setOverloadId(id())
+ .setResultType(returnType().toCelType(celTypeProvider));
+
+ target()
+ .ifPresent(
+ t ->
+ builder
+ .setIsInstanceFunction(true)
+ .addParameterTypes(t.toCelType(celTypeProvider)));
+
+ for (TypeDecl type : arguments()) {
+ builder.addParameterTypes(type.toCelType(celTypeProvider));
+ }
+
+ return builder.build();
+ }
+ }
+
+ /**
+ * Represents an abstract type declaration used to declare functions and variables in a policy.
+ */
+ @AutoValue
+ public abstract static class TypeDecl {
+
+ public abstract String name();
+
+ public abstract ImmutableList params();
+
+ public abstract Boolean isTypeParam();
+
+ /** Builder for {@link TypeDecl}. */
+ @AutoValue.Builder
+ public abstract static class Builder implements RequiredFieldsChecker {
+
+ public abstract Optional name();
+
+ public abstract Builder setName(String name);
+
+ // This should stay package-private to encourage add/set methods to be used instead.
+ abstract ImmutableList.Builder paramsBuilder();
+
+ public abstract Builder setParams(ImmutableList typeDecls);
+
+ @CanIgnoreReturnValue
+ public Builder addParams(TypeDecl... params) {
+ return addParams(Arrays.asList(params));
+ }
+
+ @CanIgnoreReturnValue
+ public Builder addParams(Iterable params) {
+ this.paramsBuilder().addAll(checkNotNull(params));
+ return this;
+ }
+
+ public abstract Builder setIsTypeParam(boolean isTypeParam);
+
+ @Override
+ public ImmutableList requiredFields() {
+ return ImmutableList.of(RequiredField.of("type_name", this::name));
+ }
+
+ @CheckReturnValue
+ public abstract TypeDecl build();
+ }
+
+ /** Creates a new {@link TypeDecl} with the provided name. */
+ public static TypeDecl create(String name) {
+ return newBuilder().setName(name).build();
+ }
+
+ public static Builder newBuilder() {
+ return new AutoValue_CelPolicyConfig_TypeDecl.Builder().setIsTypeParam(false);
+ }
+
+ /** Converts this type declaration into a {@link CelType}. */
+ public CelType toCelType(CelTypeProvider celTypeProvider) {
+ switch (name()) {
+ case "list":
+ if (params().size() != 1) {
+ throw new IllegalArgumentException(
+ "List type has unexpected param count: " + params().size());
+ }
+
+ CelType elementType = params().get(0).toCelType(celTypeProvider);
+ return ListType.create(elementType);
+ case "map":
+ if (params().size() != 2) {
+ throw new IllegalArgumentException(
+ "Map type has unexpected param count: " + params().size());
+ }
+
+ CelType keyType = params().get(0).toCelType(celTypeProvider);
+ CelType valueType = params().get(1).toCelType(celTypeProvider);
+ return MapType.create(keyType, valueType);
+ default:
+ if (isTypeParam()) {
+ return TypeParamType.create(name());
+ }
+
+ CelType simpleType = SimpleType.findByName(name()).orElse(null);
+ if (simpleType != null) {
+ return simpleType;
+ }
+
+ if (name().equals(OptionalType.NAME)) {
+ checkState(
+ params().size() == 1,
+ "Optional type must have exactly 1 parameter. Found %s",
+ params().size());
+ return OptionalType.create(params().get(0).toCelType(celTypeProvider));
+ }
+
+ return celTypeProvider
+ .findType(name())
+ .orElseThrow(() -> new IllegalArgumentException("Undefined type name: " + name()));
+ }
+ }
+ }
+
+ /**
+ * Represents a configuration for a canonical CEL extension that can be enabled in the
+ * environment.
+ */
+ @AutoValue
+ public abstract static class ExtensionConfig {
+
+ /** Name of the extension (ex: bindings, optional, math, etc).". */
+ public abstract String name();
+
+ /**
+ * Version of the extension. Presently, this field is ignored as CEL-Java extensions are not
+ * versioned.
+ */
+ public abstract Integer version();
+
+ /** Builder for {@link ExtensionConfig}. */
+ @AutoValue.Builder
+ public abstract static class Builder implements RequiredFieldsChecker {
+
+ public abstract Optional name();
+
+ public abstract Optional version();
+
+ public abstract Builder setName(String name);
+
+ public abstract Builder setVersion(Integer version);
+
+ @Override
+ public ImmutableList requiredFields() {
+ return ImmutableList.of(RequiredField.of("name", this::name));
+ }
+
+ /** Builds a new instance of {@link ExtensionConfig}. */
+ public abstract ExtensionConfig build();
+ }
+
+ /** Creates a new builder to construct a {@link ExtensionConfig} instance. */
+ public static Builder newBuilder() {
+ return new AutoValue_CelPolicyConfig_ExtensionConfig.Builder().setVersion(0);
+ }
+
+ /** Create a new extension config with the specified name and version set to 0. */
+ public static ExtensionConfig of(String name) {
+ return of(name, 0);
+ }
+
+ /** Create a new extension config with the specified name and version. */
+ public static ExtensionConfig of(String name, int version) {
+ return newBuilder().setName(name).setVersion(version).build();
+ }
+ }
+}
diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyConfigParser.java b/policy/src/main/java/dev/cel/policy/CelPolicyConfigParser.java
new file mode 100644
index 000000000..9b03a1dd9
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelPolicyConfigParser.java
@@ -0,0 +1,31 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+/** Public interface for parsing CEL policy sources. */
+public interface CelPolicyConfigParser {
+
+ /** Parsers the input {@code policyConfigSource} and returns a {@link CelPolicyConfig}. */
+ CelPolicyConfig parse(String policyConfigSource) throws CelPolicyValidationException;
+
+ /**
+ * Parses the input {@code policyConfigSource} and returns a {@link CelPolicyConfig}.
+ *
+ *
The {@code description} may be used to help tailor error messages for the location where the
+ * {@code policySource} originates, e.g. a file name or form UI element.
+ */
+ CelPolicyConfig parse(String policyConfigSource, String description)
+ throws CelPolicyValidationException;
+}
diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyParser.java b/policy/src/main/java/dev/cel/policy/CelPolicyParser.java
new file mode 100644
index 000000000..cfeceb89b
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelPolicyParser.java
@@ -0,0 +1,96 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import dev.cel.policy.ParserContext.PolicyParserContext;
+
+/** CelPolicyParser is the interface for parsing policies into a canonical Policy representation. */
+public interface CelPolicyParser {
+
+ /** Parses the input {@code policySource} and returns a {@link CelPolicy}. */
+ CelPolicy parse(String policySource) throws CelPolicyValidationException;
+
+ /**
+ * Parses the input {@code policySource} and returns a {@link CelPolicy}.
+ *
+ *
The {@code description} may be used to help tailor error messages for the location where the
+ * {@code policySource} originates, e.g. a file name or form UI element.
+ */
+ CelPolicy parse(String policySource, String description) throws CelPolicyValidationException;
+
+ /**
+ * TagVisitor declares a set of interfaces for handling custom tags which would otherwise be
+ * unsupported within the policy, rule, match, or variable objects.
+ *
+ * @param Type of the node (ex: YAML).
+ */
+ interface TagVisitor {
+
+ /**
+ * visitPolicyTag accepts a parser context, field id, tag name, yaml node, and parent Policy to
+ * allow for continued parsing within a custom tag.
+ */
+ default void visitPolicyTag(
+ PolicyParserContext ctx,
+ long id,
+ String tagName,
+ T node,
+ CelPolicy.Builder policyBuilder) {
+ ctx.reportError(id, String.format("Unsupported policy tag: %s", tagName));
+ }
+
+ /**
+ * visitRuleTag accepts a parser context, field id, tag name, yaml node, as well as the parent
+ * policy and current rule to allow for continued parsing within custom tags.
+ */
+ default void visitRuleTag(
+ PolicyParserContext ctx,
+ long id,
+ String tagName,
+ T node,
+ CelPolicy.Builder policyBuilder,
+ CelPolicy.Rule.Builder ruleBuilder) {
+ ctx.reportError(id, String.format("Unsupported rule tag: %s", tagName));
+ }
+
+ /**
+ * visitMatchTag accepts a parser context, field id, tag name, yaml node, as well as the parent
+ * policy and current match to allow for continued parsing within custom tags.
+ */
+ default void visitMatchTag(
+ PolicyParserContext ctx,
+ long id,
+ String tagName,
+ T node,
+ CelPolicy.Builder policyBuilder,
+ CelPolicy.Match.Builder matchBuilder) {
+ ctx.reportError(id, String.format("Unsupported match tag: %s", tagName));
+ }
+
+ /**
+ * visitVariableTag accepts a parser context, field id, tag name, yaml node, as well as the
+ * parent policy and current variable to allow for continued parsing within custom tags.
+ */
+ default void visitVariableTag(
+ PolicyParserContext ctx,
+ long id,
+ String tagName,
+ T node,
+ CelPolicy.Builder policyBuilder,
+ CelPolicy.Variable.Builder variableBuilder) {
+ ctx.reportError(id, String.format("Unsupported variable tag: %s", tagName));
+ }
+ }
+}
diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyParserBuilder.java b/policy/src/main/java/dev/cel/policy/CelPolicyParserBuilder.java
new file mode 100644
index 000000000..ba34a1a60
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelPolicyParserBuilder.java
@@ -0,0 +1,35 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.CheckReturnValue;
+import dev.cel.policy.CelPolicyParser.TagVisitor;
+
+/**
+ * Interface for building an instance of {@link CelPolicyParser}.
+ *
+ * @param Type of the node (Ex: YAML).
+ */
+public interface CelPolicyParserBuilder {
+
+ /** Adds a custom tag visitor to allow for handling of custom tags. */
+ @CanIgnoreReturnValue
+ CelPolicyParserBuilder addTagVisitor(TagVisitor tagVisitor);
+
+ /** Builds a new instance of {@link CelPolicyParser}. */
+ @CheckReturnValue
+ CelPolicyParser build();
+}
diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyParserFactory.java b/policy/src/main/java/dev/cel/policy/CelPolicyParserFactory.java
new file mode 100644
index 000000000..3897bef41
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelPolicyParserFactory.java
@@ -0,0 +1,39 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import org.yaml.snakeyaml.nodes.Node;
+
+/** Factory class for producing policy parser and policy config parsers. */
+public final class CelPolicyParserFactory {
+
+ /**
+ * Configure a builder to construct a {@link CelPolicyParser} instance that takes in a YAML
+ * document.
+ */
+ public static CelPolicyParserBuilder newYamlParserBuilder() {
+ return CelPolicyYamlParser.newBuilder();
+ }
+
+ /**
+ * Configure a builder to construct a {@link CelPolicyConfigParser} instance that takes in a YAML
+ * document.
+ */
+ public static CelPolicyConfigParser newYamlConfigParser() {
+ return CelPolicyYamlConfigParser.newInstance();
+ }
+
+ private CelPolicyParserFactory() {}
+}
diff --git a/policy/src/main/java/dev/cel/policy/CelPolicySource.java b/policy/src/main/java/dev/cel/policy/CelPolicySource.java
new file mode 100644
index 000000000..7918be872
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelPolicySource.java
@@ -0,0 +1,74 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CheckReturnValue;
+import dev.cel.common.CelSourceHelper;
+import dev.cel.common.CelSourceLocation;
+import dev.cel.common.Source;
+import dev.cel.common.internal.CelCodePointArray;
+import java.util.Map;
+import java.util.Optional;
+
+/** CelPolicySource represents the source content of a policy and its related metadata. */
+@AutoValue
+public abstract class CelPolicySource implements Source {
+
+ @Override
+ public abstract CelCodePointArray getContent();
+
+ @Override
+ public abstract String getDescription();
+
+ @Override
+ public abstract ImmutableMap getPositionsMap();
+
+ @Override
+ public Optional getSnippet(int line) {
+ return CelSourceHelper.getSnippet(getContent(), line);
+ }
+
+ /**
+ * Get the line and column in the source expression text for the given code point {@code offset}.
+ */
+ public Optional getOffsetLocation(int offset) {
+ return CelSourceHelper.getOffsetLocation(getContent(), offset);
+ }
+
+ /** Builder for {@link CelPolicySource}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder setContent(CelCodePointArray content);
+
+ public abstract Builder setDescription(String description);
+
+ public abstract Builder setPositionsMap(Map value);
+
+ @CheckReturnValue
+ public abstract CelPolicySource build();
+ }
+
+ public abstract Builder toBuilder();
+
+ public static Builder newBuilder(CelCodePointArray celCodePointArray) {
+ return new AutoValue_CelPolicySource.Builder()
+ .setDescription("")
+ .setContent(celCodePointArray)
+ .setPositionsMap(ImmutableMap.of());
+ }
+}
diff --git a/runtime/src/main/java/dev/cel/runtime/IncompleteData.java b/policy/src/main/java/dev/cel/policy/CelPolicyValidationException.java
similarity index 58%
rename from runtime/src/main/java/dev/cel/runtime/IncompleteData.java
rename to policy/src/main/java/dev/cel/policy/CelPolicyValidationException.java
index 4859f63c6..1f7dc6ac2 100644
--- a/runtime/src/main/java/dev/cel/runtime/IncompleteData.java
+++ b/policy/src/main/java/dev/cel/policy/CelPolicyValidationException.java
@@ -1,4 +1,4 @@
-// Copyright 2022 Google LLC
+// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,15 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package dev.cel.runtime;
-
-import dev.cel.common.annotations.Internal;
+package dev.cel.policy;
/**
- * Add an interface for interpreter to check if an object is an instance of {@link PartialMessage}.
- *
- *
Deprecated. New clients should use {@link CelAttribute} based unknowns.
+ * CelPolicyValidationException encapsulates all issues that arise when parsing or compiling a
+ * policy.
*/
-@Deprecated
-@Internal
-public interface IncompleteData {}
+public final class CelPolicyValidationException extends Exception {
+
+ CelPolicyValidationException(String message) {
+ super(message);
+ }
+
+ CelPolicyValidationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyYamlConfigParser.java b/policy/src/main/java/dev/cel/policy/CelPolicyYamlConfigParser.java
new file mode 100644
index 000000000..f0525f7ef
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelPolicyYamlConfigParser.java
@@ -0,0 +1,401 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import static dev.cel.policy.YamlHelper.ERROR;
+import static dev.cel.policy.YamlHelper.assertRequiredFields;
+import static dev.cel.policy.YamlHelper.assertYamlType;
+import static dev.cel.policy.YamlHelper.newBoolean;
+import static dev.cel.policy.YamlHelper.newInteger;
+import static dev.cel.policy.YamlHelper.newString;
+import static dev.cel.policy.YamlHelper.parseYamlSource;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import dev.cel.common.CelIssue;
+import dev.cel.common.internal.CelCodePointArray;
+import dev.cel.policy.CelPolicyConfig.ExtensionConfig;
+import dev.cel.policy.CelPolicyConfig.FunctionDecl;
+import dev.cel.policy.CelPolicyConfig.OverloadDecl;
+import dev.cel.policy.CelPolicyConfig.TypeDecl;
+import dev.cel.policy.CelPolicyConfig.VariableDecl;
+import dev.cel.policy.YamlHelper.YamlNodeType;
+import org.yaml.snakeyaml.nodes.MappingNode;
+import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.nodes.NodeTuple;
+import org.yaml.snakeyaml.nodes.ScalarNode;
+import org.yaml.snakeyaml.nodes.SequenceNode;
+
+/** Package-private class for parsing YAML config files. */
+final class CelPolicyYamlConfigParser implements CelPolicyConfigParser {
+ // Sentinel values to be returned for various declarations when parsing failure is encountered.
+ private static final TypeDecl ERROR_TYPE_DECL = TypeDecl.create(ERROR);
+ private static final VariableDecl ERROR_VARIABLE_DECL =
+ VariableDecl.create(ERROR, ERROR_TYPE_DECL);
+ private static final FunctionDecl ERROR_FUNCTION_DECL =
+ FunctionDecl.create(ERROR, ImmutableSet.of());
+ private static final ExtensionConfig ERROR_EXTENSION_DECL = ExtensionConfig.of(ERROR);
+
+ @Override
+ public CelPolicyConfig parse(String policyConfigSource) throws CelPolicyValidationException {
+ return parse(policyConfigSource, "");
+ }
+
+ @Override
+ public CelPolicyConfig parse(String policyConfigSource, String description)
+ throws CelPolicyValidationException {
+ ParserImpl parser = new ParserImpl();
+
+ return parser.parseYaml(policyConfigSource, description);
+ }
+
+ private ImmutableSet parseVariables(ParserContext ctx, Node node) {
+ long valueId = ctx.collectMetadata(node);
+ ImmutableSet.Builder variableSetBuilder = ImmutableSet.builder();
+ if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) {
+ return variableSetBuilder.build();
+ }
+
+ SequenceNode variableListNode = (SequenceNode) node;
+ for (Node elementNode : variableListNode.getValue()) {
+ variableSetBuilder.add(parseVariable(ctx, elementNode));
+ }
+
+ return variableSetBuilder.build();
+ }
+
+ private VariableDecl parseVariable(ParserContext ctx, Node node) {
+ long variableId = ctx.collectMetadata(node);
+ if (!assertYamlType(ctx, variableId, node, YamlNodeType.MAP)) {
+ return ERROR_VARIABLE_DECL;
+ }
+
+ MappingNode variableMap = (MappingNode) node;
+ VariableDecl.Builder builder = VariableDecl.newBuilder();
+ for (NodeTuple nodeTuple : variableMap.getValue()) {
+ Node keyNode = nodeTuple.getKeyNode();
+ long keyId = ctx.collectMetadata(keyNode);
+ Node valueNode = nodeTuple.getValueNode();
+ String keyName = ((ScalarNode) keyNode).getValue();
+ switch (keyName) {
+ case "name":
+ builder.setName(newString(ctx, valueNode));
+ break;
+ case "type":
+ builder.setType(parseTypeDecl(ctx, valueNode));
+ break;
+ default:
+ ctx.reportError(keyId, String.format("Unsupported variable tag: %s", keyName));
+ break;
+ }
+ }
+
+ if (!assertRequiredFields(ctx, variableId, builder.getMissingRequiredFieldNames())) {
+ return ERROR_VARIABLE_DECL;
+ }
+
+ return builder.build();
+ }
+
+ private ImmutableSet parseFunctions(ParserContext ctx, Node node) {
+ long valueId = ctx.collectMetadata(node);
+ ImmutableSet.Builder functionSetBuilder = ImmutableSet.builder();
+ if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) {
+ return functionSetBuilder.build();
+ }
+
+ SequenceNode functionListNode = (SequenceNode) node;
+ for (Node elementNode : functionListNode.getValue()) {
+ functionSetBuilder.add(parseFunction(ctx, elementNode));
+ }
+
+ return functionSetBuilder.build();
+ }
+
+ private FunctionDecl parseFunction(ParserContext ctx, Node node) {
+ long functionId = ctx.collectMetadata(node);
+ if (!assertYamlType(ctx, functionId, node, YamlNodeType.MAP)) {
+ return ERROR_FUNCTION_DECL;
+ }
+
+ MappingNode functionMap = (MappingNode) node;
+ FunctionDecl.Builder builder = FunctionDecl.newBuilder();
+ for (NodeTuple nodeTuple : functionMap.getValue()) {
+ Node keyNode = nodeTuple.getKeyNode();
+ long keyId = ctx.collectMetadata(keyNode);
+ Node valueNode = nodeTuple.getValueNode();
+ String keyName = ((ScalarNode) keyNode).getValue();
+ switch (keyName) {
+ case "name":
+ builder.setName(newString(ctx, valueNode));
+ break;
+ case "overloads":
+ builder.setOverloads(parseOverloads(ctx, valueNode));
+ break;
+ default:
+ ctx.reportError(keyId, String.format("Unsupported function tag: %s", keyName));
+ break;
+ }
+ }
+
+ if (!assertRequiredFields(ctx, functionId, builder.getMissingRequiredFieldNames())) {
+ return ERROR_FUNCTION_DECL;
+ }
+
+ return builder.build();
+ }
+
+ private static ImmutableSet parseOverloads(ParserContext ctx, Node node) {
+ long listId = ctx.collectMetadata(node);
+ ImmutableSet.Builder overloadSetBuilder = ImmutableSet.builder();
+ if (!assertYamlType(ctx, listId, node, YamlNodeType.LIST)) {
+ return overloadSetBuilder.build();
+ }
+
+ SequenceNode overloadListNode = (SequenceNode) node;
+ for (Node overloadMapNode : overloadListNode.getValue()) {
+ long overloadMapId = ctx.collectMetadata(overloadMapNode);
+ if (!assertYamlType(ctx, overloadMapId, overloadMapNode, YamlNodeType.MAP)) {
+ continue;
+ }
+
+ MappingNode mapNode = (MappingNode) overloadMapNode;
+ OverloadDecl.Builder overloadDeclBuilder = OverloadDecl.newBuilder();
+ for (NodeTuple nodeTuple : mapNode.getValue()) {
+ Node keyNode = nodeTuple.getKeyNode();
+ long keyId = ctx.collectMetadata(keyNode);
+ if (!assertYamlType(ctx, keyId, keyNode, YamlNodeType.STRING, YamlNodeType.TEXT)) {
+ continue;
+ }
+
+ Node valueNode = nodeTuple.getValueNode();
+ String fieldName = ((ScalarNode) keyNode).getValue();
+ switch (fieldName) {
+ case "id":
+ overloadDeclBuilder.setId(newString(ctx, valueNode));
+ break;
+ case "args":
+ overloadDeclBuilder.addArguments(parseOverloadArguments(ctx, valueNode));
+ break;
+ case "return":
+ overloadDeclBuilder.setReturnType(parseTypeDecl(ctx, valueNode));
+ break;
+ case "target":
+ overloadDeclBuilder.setTarget(parseTypeDecl(ctx, valueNode));
+ break;
+ default:
+ ctx.reportError(keyId, String.format("Unsupported overload tag: %s", fieldName));
+ break;
+ }
+ }
+
+ if (assertRequiredFields(
+ ctx, overloadMapId, overloadDeclBuilder.getMissingRequiredFieldNames())) {
+ overloadSetBuilder.add(overloadDeclBuilder.build());
+ }
+ }
+
+ return overloadSetBuilder.build();
+ }
+
+ private static ImmutableList parseOverloadArguments(
+ ParserContext ctx, Node node) {
+ long listValueId = ctx.collectMetadata(node);
+ if (!assertYamlType(ctx, listValueId, node, YamlNodeType.LIST)) {
+ return ImmutableList.of();
+ }
+ SequenceNode paramsListNode = (SequenceNode) node;
+ ImmutableList.Builder builder = ImmutableList.builder();
+ for (Node elementNode : paramsListNode.getValue()) {
+ builder.add(parseTypeDecl(ctx, elementNode));
+ }
+
+ return builder.build();
+ }
+
+ private static ImmutableSet parseExtensions(ParserContext ctx, Node node) {
+ long valueId = ctx.collectMetadata(node);
+ ImmutableSet.Builder extensionConfigBuilder = ImmutableSet.builder();
+ if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) {
+ return extensionConfigBuilder.build();
+ }
+
+ SequenceNode extensionListNode = (SequenceNode) node;
+ for (Node elementNode : extensionListNode.getValue()) {
+ extensionConfigBuilder.add(parseExtension(ctx, elementNode));
+ }
+
+ return extensionConfigBuilder.build();
+ }
+
+ private static ExtensionConfig parseExtension(ParserContext ctx, Node node) {
+ long extensionId = ctx.collectMetadata(node);
+ if (!assertYamlType(ctx, extensionId, node, YamlNodeType.MAP)) {
+ return ERROR_EXTENSION_DECL;
+ }
+
+ MappingNode extensionMap = (MappingNode) node;
+ ExtensionConfig.Builder builder = ExtensionConfig.newBuilder();
+ for (NodeTuple nodeTuple : extensionMap.getValue()) {
+ Node keyNode = nodeTuple.getKeyNode();
+ long keyId = ctx.collectMetadata(keyNode);
+ Node valueNode = nodeTuple.getValueNode();
+ String keyName = ((ScalarNode) keyNode).getValue();
+ switch (keyName) {
+ case "name":
+ builder.setName(newString(ctx, valueNode));
+ break;
+ case "version":
+ builder.setVersion(newInteger(ctx, valueNode));
+ break;
+ default:
+ ctx.reportError(keyId, String.format("Unsupported extension tag: %s", keyName));
+ break;
+ }
+ }
+
+ if (!assertRequiredFields(ctx, extensionId, builder.getMissingRequiredFieldNames())) {
+ return ERROR_EXTENSION_DECL;
+ }
+
+ return builder.build();
+ }
+
+ private static TypeDecl parseTypeDecl(ParserContext ctx, Node node) {
+ TypeDecl.Builder builder = TypeDecl.newBuilder();
+ long id = ctx.collectMetadata(node);
+ if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) {
+ return ERROR_TYPE_DECL;
+ }
+
+ MappingNode mapNode = (MappingNode) node;
+ for (NodeTuple nodeTuple : mapNode.getValue()) {
+ Node keyNode = nodeTuple.getKeyNode();
+ long keyId = ctx.collectMetadata(keyNode);
+ if (!assertYamlType(ctx, keyId, keyNode, YamlNodeType.STRING, YamlNodeType.TEXT)) {
+ continue;
+ }
+
+ Node valueNode = nodeTuple.getValueNode();
+ String fieldName = ((ScalarNode) keyNode).getValue();
+ switch (fieldName) {
+ case "type_name":
+ builder.setName(newString(ctx, valueNode));
+ break;
+ case "is_type_param":
+ builder.setIsTypeParam(newBoolean(ctx, valueNode));
+ break;
+ case "params":
+ long listValueId = ctx.collectMetadata(node);
+ if (!assertYamlType(ctx, listValueId, valueNode, YamlNodeType.LIST)) {
+ break;
+ }
+ SequenceNode paramsListNode = (SequenceNode) valueNode;
+ for (Node elementNode : paramsListNode.getValue()) {
+ builder.addParams(parseTypeDecl(ctx, elementNode));
+ }
+ break;
+ default:
+ ctx.reportError(keyId, String.format("Unsupported type decl tag: %s", fieldName));
+ break;
+ }
+ }
+
+ if (!assertRequiredFields(ctx, id, builder.getMissingRequiredFieldNames())) {
+ return ERROR_TYPE_DECL;
+ }
+
+ return builder.build();
+ }
+
+ private class ParserImpl {
+
+ private CelPolicyConfig parseYaml(String source, String description)
+ throws CelPolicyValidationException {
+ Node node;
+ try {
+ node = parseYamlSource(source);
+ } catch (RuntimeException e) {
+ throw new CelPolicyValidationException("YAML document is malformed: " + e.getMessage(), e);
+ }
+
+ CelPolicySource configSource =
+ CelPolicySource.newBuilder(CelCodePointArray.fromString(source))
+ .setDescription(description)
+ .build();
+ ParserContext ctx = YamlParserContextImpl.newInstance(configSource);
+ CelPolicyConfig.Builder policyConfig = parseConfig(ctx, node);
+ configSource = configSource.toBuilder().setPositionsMap(ctx.getIdToOffsetMap()).build();
+
+ if (!ctx.getIssues().isEmpty()) {
+ throw new CelPolicyValidationException(
+ CelIssue.toDisplayString(ctx.getIssues(), configSource));
+ }
+
+ return policyConfig.setConfigSource(configSource).build();
+ }
+
+ private CelPolicyConfig.Builder parseConfig(ParserContext ctx, Node node) {
+ CelPolicyConfig.Builder builder = CelPolicyConfig.newBuilder();
+ long id = ctx.collectMetadata(node);
+ if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) {
+ return builder;
+ }
+
+ MappingNode rootNode = (MappingNode) node;
+ for (NodeTuple nodeTuple : rootNode.getValue()) {
+ Node keyNode = nodeTuple.getKeyNode();
+ long keyId = ctx.collectMetadata(keyNode);
+ if (!assertYamlType(ctx, keyId, keyNode, YamlNodeType.STRING, YamlNodeType.TEXT)) {
+ continue;
+ }
+
+ Node valueNode = nodeTuple.getValueNode();
+ String fieldName = ((ScalarNode) keyNode).getValue();
+ switch (fieldName) {
+ case "name":
+ builder.setName(newString(ctx, valueNode));
+ break;
+ case "description":
+ builder.setDescription(newString(ctx, valueNode));
+ break;
+ case "container":
+ builder.setContainer(newString(ctx, valueNode));
+ break;
+ case "variables":
+ builder.setVariables(parseVariables(ctx, valueNode));
+ break;
+ case "functions":
+ builder.setFunctions(parseFunctions(ctx, valueNode));
+ break;
+ case "extensions":
+ builder.setExtensions(parseExtensions(ctx, valueNode));
+ break;
+ default:
+ ctx.reportError(id, "Unknown config tag: " + fieldName);
+ // continue handling the rest of the nodes
+ }
+ }
+
+ return builder;
+ }
+ }
+
+ static CelPolicyYamlConfigParser newInstance() {
+ return new CelPolicyYamlConfigParser();
+ }
+
+ private CelPolicyYamlConfigParser() {}
+}
diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java
new file mode 100644
index 000000000..541dfdfb3
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java
@@ -0,0 +1,344 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static dev.cel.policy.YamlHelper.ERROR;
+import static dev.cel.policy.YamlHelper.assertRequiredFields;
+import static dev.cel.policy.YamlHelper.assertYamlType;
+
+import com.google.common.collect.ImmutableSet;
+import dev.cel.common.CelIssue;
+import dev.cel.common.internal.CelCodePointArray;
+import dev.cel.policy.CelPolicy.Match;
+import dev.cel.policy.CelPolicy.Match.Result;
+import dev.cel.policy.CelPolicy.Variable;
+import dev.cel.policy.ParserContext.PolicyParserContext;
+import dev.cel.policy.YamlHelper.YamlNodeType;
+import java.util.List;
+import java.util.Map;
+import org.yaml.snakeyaml.nodes.MappingNode;
+import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.nodes.NodeTuple;
+import org.yaml.snakeyaml.nodes.ScalarNode;
+import org.yaml.snakeyaml.nodes.SequenceNode;
+
+final class CelPolicyYamlParser implements CelPolicyParser {
+
+ // Sentinel values for parsing errors
+ private static final ValueString ERROR_VALUE = ValueString.newBuilder().setValue(ERROR).build();
+ private static final Match ERROR_MATCH =
+ Match.newBuilder().setCondition(ERROR_VALUE).setResult(Result.ofOutput(ERROR_VALUE)).build();
+ private static final Variable ERROR_VARIABLE =
+ Variable.newBuilder().setExpression(ERROR_VALUE).setName(ERROR_VALUE).build();
+
+ private final TagVisitor tagVisitor;
+
+ @Override
+ public CelPolicy parse(String policySource) throws CelPolicyValidationException {
+ return parse(policySource, "");
+ }
+
+ @Override
+ public CelPolicy parse(String policySource, String description)
+ throws CelPolicyValidationException {
+ ParserImpl parser = new ParserImpl(tagVisitor, policySource, description);
+ return parser.parseYaml();
+ }
+
+ private static class ParserImpl implements PolicyParserContext {
+
+ private final TagVisitor tagVisitor;
+ private final CelPolicySource policySource;
+ private final ParserContext ctx;
+
+ private CelPolicy parseYaml() throws CelPolicyValidationException {
+ Node node;
+ try {
+ node = YamlHelper.parseYamlSource(policySource.getContent().toString());
+ } catch (RuntimeException e) {
+ throw new CelPolicyValidationException("YAML document is malformed: " + e.getMessage(), e);
+ }
+
+ CelPolicy celPolicy = parsePolicy(this, node);
+
+ if (!ctx.getIssues().isEmpty()) {
+ throw new CelPolicyValidationException(
+ CelIssue.toDisplayString(ctx.getIssues(), celPolicy.policySource()));
+ }
+
+ return celPolicy;
+ }
+
+ @Override
+ public CelPolicy parsePolicy(PolicyParserContext ctx, Node node) {
+ CelPolicy.Builder policyBuilder = CelPolicy.newBuilder();
+ long id = ctx.collectMetadata(node);
+ if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) {
+ return policyBuilder.setPolicySource(policySource).build();
+ }
+
+ MappingNode rootNode = (MappingNode) node;
+ for (NodeTuple nodeTuple : rootNode.getValue()) {
+ Node keyNode = nodeTuple.getKeyNode();
+ long keyId = ctx.collectMetadata(keyNode);
+ if (!assertYamlType(ctx, keyId, keyNode, YamlNodeType.STRING, YamlNodeType.TEXT)) {
+ continue;
+ }
+
+ Node valueNode = nodeTuple.getValueNode();
+ String fieldName = ((ScalarNode) keyNode).getValue();
+ switch (fieldName) {
+ case "name":
+ policyBuilder.setName(ctx.newValueString(valueNode));
+ break;
+ case "rule":
+ policyBuilder.setRule(parseRule(ctx, policyBuilder, valueNode));
+ break;
+ default:
+ tagVisitor.visitPolicyTag(ctx, keyId, fieldName, valueNode, policyBuilder);
+ break;
+ }
+ }
+
+ return policyBuilder
+ .setPolicySource(policySource.toBuilder().setPositionsMap(ctx.getIdToOffsetMap()).build())
+ .build();
+ }
+
+ @Override
+ public CelPolicy.Rule parseRule(
+ PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) {
+ long valueId = ctx.collectMetadata(node);
+ CelPolicy.Rule.Builder ruleBuilder = CelPolicy.Rule.newBuilder();
+ if (!assertYamlType(ctx, valueId, node, YamlNodeType.MAP)) {
+ return ruleBuilder.build();
+ }
+
+ for (NodeTuple nodeTuple : ((MappingNode) node).getValue()) {
+ Node key = nodeTuple.getKeyNode();
+ long tagId = ctx.collectMetadata(key);
+ if (!assertYamlType(ctx, tagId, key, YamlNodeType.STRING, YamlNodeType.TEXT)) {
+ continue;
+ }
+ String fieldName = ((ScalarNode) key).getValue();
+ Node value = nodeTuple.getValueNode();
+ switch (fieldName) {
+ case "id":
+ ruleBuilder.setId(ctx.newValueString(value));
+ break;
+ case "description":
+ ruleBuilder.setDescription(ctx.newValueString(value));
+ break;
+ case "variables":
+ ruleBuilder.addVariables(parseVariables(ctx, policyBuilder, value));
+ break;
+ case "match":
+ ruleBuilder.addMatches(parseMatches(ctx, policyBuilder, value));
+ break;
+ default:
+ tagVisitor.visitRuleTag(ctx, tagId, fieldName, value, policyBuilder, ruleBuilder);
+ break;
+ }
+ }
+ return ruleBuilder.build();
+ }
+
+ private ImmutableSet parseMatches(
+ PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) {
+ long valueId = ctx.collectMetadata(node);
+ ImmutableSet.Builder matchesBuilder = ImmutableSet.builder();
+ if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) {
+ return matchesBuilder.build();
+ }
+
+ SequenceNode matchListNode = (SequenceNode) node;
+ for (Node elementNode : matchListNode.getValue()) {
+ matchesBuilder.add(parseMatch(ctx, policyBuilder, elementNode));
+ }
+
+ return matchesBuilder.build();
+ }
+
+ @Override
+ public CelPolicy.Match parseMatch(
+ PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) {
+ long nodeId = ctx.collectMetadata(node);
+ if (!assertYamlType(ctx, nodeId, node, YamlNodeType.MAP)) {
+ return ERROR_MATCH;
+ }
+ MappingNode matchNode = (MappingNode) node;
+ CelPolicy.Match.Builder matchBuilder =
+ CelPolicy.Match.newBuilder().setCondition(ValueString.of(ctx.nextId(), "true"));
+ for (NodeTuple nodeTuple : matchNode.getValue()) {
+ Node key = nodeTuple.getKeyNode();
+ long tagId = ctx.collectMetadata(key);
+ if (!assertYamlType(ctx, tagId, key, YamlNodeType.STRING, YamlNodeType.TEXT)) {
+ continue;
+ }
+ String fieldName = ((ScalarNode) key).getValue();
+ Node value = nodeTuple.getValueNode();
+ switch (fieldName) {
+ case "condition":
+ matchBuilder.setCondition(ctx.newValueString(value));
+ break;
+ case "output":
+ matchBuilder
+ .result()
+ .filter(result -> result.kind().equals(Match.Result.Kind.RULE))
+ .ifPresent(
+ result -> ctx.reportError(tagId, "Only the rule or the output may be set"));
+ matchBuilder.setResult(Match.Result.ofOutput(ctx.newValueString(value)));
+ break;
+ case "rule":
+ matchBuilder
+ .result()
+ .filter(result -> result.kind().equals(Match.Result.Kind.OUTPUT))
+ .ifPresent(
+ result -> ctx.reportError(tagId, "Only the rule or the output may be set"));
+ matchBuilder.setResult(Match.Result.ofRule(parseRule(ctx, policyBuilder, value)));
+ break;
+ default:
+ tagVisitor.visitMatchTag(ctx, tagId, fieldName, value, policyBuilder, matchBuilder);
+ break;
+ }
+ }
+
+ if (!assertRequiredFields(ctx, nodeId, matchBuilder.getMissingRequiredFieldNames())) {
+ return ERROR_MATCH;
+ }
+
+ return matchBuilder.build();
+ }
+
+ private ImmutableSet parseVariables(
+ PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) {
+ long valueId = ctx.collectMetadata(node);
+ ImmutableSet.Builder variableBuilder = ImmutableSet.builder();
+ if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) {
+ return variableBuilder.build();
+ }
+
+ SequenceNode variableListNode = (SequenceNode) node;
+ for (Node elementNode : variableListNode.getValue()) {
+ variableBuilder.add(parseVariable(ctx, policyBuilder, elementNode));
+ }
+
+ return variableBuilder.build();
+ }
+
+ @Override
+ public CelPolicy.Variable parseVariable(
+ PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) {
+ long id = ctx.collectMetadata(node);
+ if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) {
+ return ERROR_VARIABLE;
+ }
+ MappingNode variableMap = (MappingNode) node;
+ Variable.Builder builder = Variable.newBuilder();
+
+ for (NodeTuple nodeTuple : variableMap.getValue()) {
+ Node keyNode = nodeTuple.getKeyNode();
+ long keyId = ctx.collectMetadata(keyNode);
+ Node valueNode = nodeTuple.getValueNode();
+ String keyName = ((ScalarNode) keyNode).getValue();
+ switch (keyName) {
+ case "name":
+ builder.setName(ctx.newValueString(valueNode));
+ break;
+ case "expression":
+ builder.setExpression(ctx.newValueString(valueNode));
+ break;
+ default:
+ tagVisitor.visitVariableTag(ctx, keyId, keyName, valueNode, policyBuilder, builder);
+ break;
+ }
+ }
+
+ if (!assertRequiredFields(ctx, id, builder.getMissingRequiredFieldNames())) {
+ return ERROR_VARIABLE;
+ }
+
+ return builder.build();
+ }
+
+ private ParserImpl(TagVisitor tagVisitor, String source, String description) {
+ this.tagVisitor = tagVisitor;
+ this.policySource =
+ CelPolicySource.newBuilder(CelCodePointArray.fromString(source))
+ .setDescription(description)
+ .build();
+ this.ctx = YamlParserContextImpl.newInstance(policySource);
+ }
+
+ @Override
+ public long nextId() {
+ return ctx.nextId();
+ }
+
+ @Override
+ public long collectMetadata(Node node) {
+ return ctx.collectMetadata(node);
+ }
+
+ @Override
+ public void reportError(long id, String message) {
+ ctx.reportError(id, message);
+ }
+
+ @Override
+ public List getIssues() {
+ return ctx.getIssues();
+ }
+
+ @Override
+ public Map getIdToOffsetMap() {
+ return ctx.getIdToOffsetMap();
+ }
+
+ @Override
+ public ValueString newValueString(Node node) {
+ return ctx.newValueString(node);
+ }
+ }
+
+ static final class Builder implements CelPolicyParserBuilder {
+
+ private TagVisitor tagVisitor;
+
+ private Builder() {
+ this.tagVisitor = new TagVisitor() {};
+ }
+
+ @Override
+ public CelPolicyParserBuilder addTagVisitor(TagVisitor tagVisitor) {
+ this.tagVisitor = tagVisitor;
+ return this;
+ }
+
+ @Override
+ public CelPolicyParser build() {
+ return new CelPolicyYamlParser(tagVisitor);
+ }
+ }
+
+ static Builder newBuilder() {
+ return new Builder();
+ }
+
+ private CelPolicyYamlParser(TagVisitor tagVisitor) {
+ this.tagVisitor = checkNotNull(tagVisitor);
+ }
+}
diff --git a/policy/src/main/java/dev/cel/policy/ParserContext.java b/policy/src/main/java/dev/cel/policy/ParserContext.java
new file mode 100644
index 000000000..13b816414
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/ParserContext.java
@@ -0,0 +1,64 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import dev.cel.common.CelIssue;
+import dev.cel.policy.CelPolicy.Match;
+import dev.cel.policy.CelPolicy.Rule;
+import dev.cel.policy.CelPolicy.Variable;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ParserContext declares a set of interfaces for managing metadata, such as node IDs, parsing
+ * errors and source offsets.
+ */
+public interface ParserContext {
+
+ /**
+ * NextID returns a monotonically increasing identifier for a source fragment. This ID is
+ * implicitly created and tracked within the CollectMetadata method.
+ */
+ long nextId();
+
+ /**
+ * CollectMetadata records the source position information of a given node, and returns the id
+ * associated with the source metadata which is returned in the Policy SourceInfo object.
+ */
+ long collectMetadata(T node);
+
+ void reportError(long id, String message);
+
+ List getIssues();
+
+ Map getIdToOffsetMap();
+
+ /** NewString creates a new ValueString from the YAML node. */
+ ValueString newValueString(T node);
+
+ /**
+ * PolicyParserContext declares a set of interfaces for creating and managing metadata
+ * specifically for {@link CelPolicy}.
+ */
+ interface PolicyParserContext extends ParserContext {
+ CelPolicy parsePolicy(PolicyParserContext ctx, T node);
+
+ Rule parseRule(PolicyParserContext ctx, CelPolicy.Builder policyBuilder, T node);
+
+ Match parseMatch(PolicyParserContext ctx, CelPolicy.Builder policyBuilder, T node);
+
+ Variable parseVariable(PolicyParserContext ctx, CelPolicy.Builder policyBuilder, T node);
+ }
+}
diff --git a/policy/src/main/java/dev/cel/policy/RequiredFieldsChecker.java b/policy/src/main/java/dev/cel/policy/RequiredFieldsChecker.java
new file mode 100644
index 000000000..e46b2822a
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/RequiredFieldsChecker.java
@@ -0,0 +1,49 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/**
+ * Interface to be implemented on a builder that can be used to verify all required fields being
+ * set.
+ */
+interface RequiredFieldsChecker {
+
+ ImmutableList requiredFields();
+
+ default ImmutableList getMissingRequiredFieldNames() {
+ return requiredFields().stream()
+ .filter(entry -> !entry.fieldValue().get().isPresent())
+ .map(RequiredField::displayName)
+ .collect(toImmutableList());
+ }
+
+ @AutoValue
+ abstract class RequiredField {
+ abstract String displayName();
+
+ abstract Supplier> fieldValue();
+
+ static RequiredField of(String displayName, Supplier> fieldValue) {
+ return new AutoValue_RequiredFieldsChecker_RequiredField(displayName, fieldValue);
+ }
+ }
+}
diff --git a/policy/src/main/java/dev/cel/policy/RuleComposer.java b/policy/src/main/java/dev/cel/policy/RuleComposer.java
new file mode 100644
index 000000000..732b60394
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/RuleComposer.java
@@ -0,0 +1,188 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.stream.Collectors.toCollection;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.Lists;
+import dev.cel.bundle.Cel;
+import dev.cel.common.CelAbstractSyntaxTree;
+import dev.cel.common.CelMutableAst;
+import dev.cel.common.CelValidationException;
+import dev.cel.common.ast.CelConstant.Kind;
+import dev.cel.extensions.CelOptionalLibrary.Function;
+import dev.cel.optimizer.AstMutator;
+import dev.cel.optimizer.CelAstOptimizer;
+import dev.cel.parser.Operator;
+import dev.cel.policy.CelCompiledRule.CelCompiledMatch;
+import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue;
+import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Package-private class for composing various rules into a single expression using optimizer. */
+final class RuleComposer implements CelAstOptimizer {
+ private final CelCompiledRule compiledRule;
+ private final String variablePrefix;
+ private final AstMutator astMutator;
+
+ @Override
+ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) {
+ RuleOptimizationResult result = optimizeRule(cel, compiledRule);
+ return OptimizationResult.create(result.ast().toParsedAst());
+ }
+
+ @AutoValue
+ abstract static class RuleOptimizationResult {
+ abstract CelMutableAst ast();
+
+ abstract Boolean isOptionalResult();
+
+ static RuleOptimizationResult create(CelMutableAst ast, boolean isOptionalResult) {
+ return new AutoValue_RuleComposer_RuleOptimizationResult(ast, isOptionalResult);
+ }
+ }
+
+ private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRule) {
+ cel =
+ cel.toCelBuilder()
+ .addVarDeclarations(
+ compiledRule.variables().stream()
+ .map(CelCompiledVariable::celVarDecl)
+ .collect(toImmutableList()))
+ .build();
+
+ CelMutableAst matchAst = astMutator.newGlobalCall(Function.OPTIONAL_NONE.getFunction());
+ boolean isOptionalResult = true;
+ // Keep track of the last output ID that might cause type-check failure while attempting to
+ // compose the subgraphs.
+ long lastOutputId = 0;
+ for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) {
+ CelAbstractSyntaxTree conditionAst = match.condition();
+ boolean isTriviallyTrue =
+ conditionAst.getExpr().constantOrDefault().getKind().equals(Kind.BOOLEAN_VALUE)
+ && conditionAst.getExpr().constant().booleanValue();
+ switch (match.result().kind()) {
+ case OUTPUT:
+ OutputValue matchOutput = match.result().output();
+ CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast());
+ if (isTriviallyTrue) {
+ matchAst = outAst;
+ isOptionalResult = false;
+ lastOutputId = matchOutput.id();
+ continue;
+ }
+ if (isOptionalResult) {
+ outAst = astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), outAst);
+ }
+
+ matchAst =
+ astMutator.newGlobalCall(
+ Operator.CONDITIONAL.getFunction(),
+ CelMutableAst.fromCelAst(conditionAst),
+ outAst,
+ matchAst);
+ assertComposedAstIsValid(
+ cel, matchAst, "conflicting output types found.", matchOutput.id(), lastOutputId);
+ lastOutputId = matchOutput.id();
+ continue;
+ case RULE:
+ CelCompiledRule matchNestedRule = match.result().rule();
+ RuleOptimizationResult nestedRule = optimizeRule(cel, matchNestedRule);
+ CelMutableAst nestedRuleAst = nestedRule.ast();
+ if (isOptionalResult && !nestedRule.isOptionalResult()) {
+ nestedRuleAst =
+ astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), nestedRuleAst);
+ }
+ if (!isOptionalResult && nestedRule.isOptionalResult()) {
+ matchAst = astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), matchAst);
+ isOptionalResult = true;
+ }
+ if (!isOptionalResult && !nestedRule.isOptionalResult()) {
+ throw new IllegalArgumentException("Subrule early terminates policy");
+ }
+ matchAst = astMutator.newMemberCall(nestedRuleAst, Function.OR.getFunction(), matchAst);
+ assertComposedAstIsValid(
+ cel,
+ matchAst,
+ String.format(
+ "failed composing the subrule '%s' due to conflicting output types.",
+ matchNestedRule.id().map(ValueString::value).orElse("")),
+ lastOutputId);
+ break;
+ }
+ }
+
+ CelMutableAst result = matchAst;
+ for (CelCompiledVariable variable : Lists.reverse(compiledRule.variables())) {
+ result =
+ astMutator.replaceSubtreeWithNewBindMacro(
+ result,
+ variablePrefix + variable.name(),
+ CelMutableAst.fromCelAst(variable.ast()),
+ result.expr(),
+ result.expr().id(),
+ true);
+ }
+
+ result = astMutator.renumberIdsConsecutively(result);
+
+ return RuleOptimizationResult.create(result, isOptionalResult);
+ }
+
+ static RuleComposer newInstance(
+ CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) {
+ return new RuleComposer(compiledRule, variablePrefix, iterationLimit);
+ }
+
+ private void assertComposedAstIsValid(
+ Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) {
+ assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
+ }
+
+ private void assertComposedAstIsValid(
+ Cel cel, CelMutableAst composedAst, String failureMessage, List ids) {
+ try {
+ cel.check(composedAst.toParsedAst()).getAst();
+ } catch (CelValidationException e) {
+ ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new));
+ throw new RuleCompositionException(failureMessage, e, ids);
+ }
+ }
+
+ private RuleComposer(CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) {
+ this.compiledRule = checkNotNull(compiledRule);
+ this.variablePrefix = variablePrefix;
+ this.astMutator = AstMutator.newInstance(iterationLimit);
+ }
+
+ static final class RuleCompositionException extends RuntimeException {
+ final String failureReason;
+ final List errorIds;
+ final CelValidationException compileException;
+
+ private RuleCompositionException(
+ String failureReason, CelValidationException e, List errorIds) {
+ super(e);
+ this.failureReason = failureReason;
+ this.errorIds = errorIds;
+ this.compileException = e;
+ }
+ }
+}
diff --git a/policy/src/main/java/dev/cel/policy/ValueString.java b/policy/src/main/java/dev/cel/policy/ValueString.java
new file mode 100644
index 000000000..24c56f4ee
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/ValueString.java
@@ -0,0 +1,49 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import com.google.auto.value.AutoValue;
+
+/** ValueString contains an identifier corresponding to source metadata and a simple string. */
+@AutoValue
+public abstract class ValueString {
+
+ /** A unique identifier. This is populated by the parser. */
+ public abstract long id();
+
+ public abstract String value();
+
+ @AutoValue.Builder
+ abstract static class Builder {
+
+ abstract Builder setId(long id);
+
+ abstract Builder setValue(String value);
+
+ abstract ValueString build();
+ }
+
+ public abstract Builder toBuilder();
+
+ /** Builder for {@link ValueString}. */
+ public static Builder newBuilder() {
+ return new AutoValue_ValueString.Builder().setId(0).setValue("");
+ }
+
+ /** Creates a new {@link ValueString} instance with the specified ID and string value. */
+ public static ValueString of(long id, String value) {
+ return newBuilder().setId(id).setValue(value).build();
+ }
+}
diff --git a/policy/src/main/java/dev/cel/policy/YamlHelper.java b/policy/src/main/java/dev/cel/policy/YamlHelper.java
new file mode 100644
index 000000000..d340ce329
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/YamlHelper.java
@@ -0,0 +1,111 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.base.Joiner;
+import java.io.StringReader;
+import java.util.List;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.nodes.ScalarNode;
+
+final class YamlHelper {
+ static final String ERROR = "*error*";
+
+ enum YamlNodeType {
+ MAP("tag:yaml.org,2002:map"),
+ STRING("tag:yaml.org,2002:str"),
+ BOOLEAN("tag:yaml.org,2002:bool"),
+ INTEGER("tag:yaml.org,2002:int"),
+ DOUBLE("tag:yaml.org,2002:float"),
+ TEXT("!txt"),
+ LIST("tag:yaml.org,2002:seq"),
+ ;
+
+ private final String tag;
+
+ String tag() {
+ return tag;
+ }
+
+ YamlNodeType(String tag) {
+ this.tag = tag;
+ }
+ }
+
+ static Node parseYamlSource(String policyContent) {
+ Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
+
+ return yaml.compose(new StringReader(policyContent));
+ }
+
+ static boolean assertRequiredFields(
+ ParserContext ctx, long id, List missingRequiredFields) {
+ if (missingRequiredFields.isEmpty()) {
+ return true;
+ }
+
+ ctx.reportError(
+ id,
+ String.format(
+ "Missing required attribute(s): %s", Joiner.on(", ").join(missingRequiredFields)));
+ return false;
+ }
+
+ static boolean assertYamlType(
+ ParserContext ctx, long id, Node node, YamlNodeType... expectedNodeTypes) {
+ String nodeTag = node.getTag().getValue();
+ for (YamlNodeType expectedNodeType : expectedNodeTypes) {
+ if (expectedNodeType.tag().equals(nodeTag)) {
+ return true;
+ }
+ }
+ ctx.reportError(
+ id,
+ String.format(
+ "Got yaml node type %s, wanted type(s) [%s]",
+ nodeTag, stream(expectedNodeTypes).map(YamlNodeType::tag).collect(joining(" "))));
+ return false;
+ }
+
+ static Integer newInteger(ParserContext ctx, Node node) {
+ long id = ctx.collectMetadata(node);
+ if (!assertYamlType(ctx, id, node, YamlNodeType.INTEGER)) {
+ return 0;
+ }
+
+ return Integer.parseInt(((ScalarNode) node).getValue());
+ }
+
+ static boolean newBoolean(ParserContext ctx, Node node) {
+ long id = ctx.collectMetadata(node);
+ if (!assertYamlType(ctx, id, node, YamlNodeType.BOOLEAN)) {
+ return false;
+ }
+
+ return Boolean.parseBoolean(((ScalarNode) node).getValue());
+ }
+
+ static String newString(ParserContext ctx, Node node) {
+ return ctx.newValueString(node).value();
+ }
+
+ private YamlHelper() {}
+}
diff --git a/policy/src/main/java/dev/cel/policy/YamlParserContextImpl.java b/policy/src/main/java/dev/cel/policy/YamlParserContextImpl.java
new file mode 100644
index 000000000..9dbf77f72
--- /dev/null
+++ b/policy/src/main/java/dev/cel/policy/YamlParserContextImpl.java
@@ -0,0 +1,150 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import static dev.cel.policy.YamlHelper.ERROR;
+import static dev.cel.policy.YamlHelper.assertYamlType;
+
+import com.google.common.base.Strings;
+import dev.cel.common.CelIssue;
+import dev.cel.common.CelSourceLocation;
+import dev.cel.policy.YamlHelper.YamlNodeType;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.DumperOptions.ScalarStyle;
+import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.nodes.ScalarNode;
+
+/** Package-private class to assist with storing policy parsing context. */
+final class YamlParserContextImpl implements ParserContext {
+
+ private final ArrayList issues;
+ private final HashMap idToLocationMap;
+ private final HashMap idToOffsetMap;
+ private final CelPolicySource policySource;
+ private long id;
+
+ @Override
+ public void reportError(long id, String message) {
+ issues.add(CelIssue.formatError(idToLocationMap.get(id), message));
+ }
+
+ @Override
+ public List getIssues() {
+ return issues;
+ }
+
+ @Override
+ public Map getIdToOffsetMap() {
+ return idToOffsetMap;
+ }
+
+ @Override
+ public ValueString newValueString(Node node) {
+ long id = collectMetadata(node);
+ if (!assertYamlType(this, id, node, YamlNodeType.STRING, YamlNodeType.TEXT)) {
+ return ValueString.of(id, ERROR);
+ }
+
+ ScalarNode scalarNode = (ScalarNode) node;
+ ScalarStyle style = scalarNode.getScalarStyle();
+ if (style.equals(ScalarStyle.FOLDED) || style.equals(ScalarStyle.LITERAL)) {
+ CelSourceLocation location = idToLocationMap.get(id);
+ int line = location.getLine();
+ int column = location.getColumn();
+
+ String indent = Strings.padStart("", column, ' ');
+ String text = policySource.getSnippet(line).orElse("");
+ StringBuilder raw = new StringBuilder();
+ while (text.startsWith(indent)) {
+ line++;
+ raw.append(text);
+ text = policySource.getSnippet(line).orElse("");
+ if (text.isEmpty()) {
+ break;
+ }
+ if (text.startsWith(indent)) {
+ raw.append("\n");
+ }
+ }
+
+ idToOffsetMap.compute(id, (k, offset) -> offset - column);
+
+ return ValueString.of(id, raw.toString());
+ }
+
+ return ValueString.of(id, scalarNode.getValue());
+ }
+
+ @Override
+ public long collectMetadata(Node node) {
+ long id = nextId();
+ int line = node.getStartMark().getLine() + 1; // Yaml lines are 0 indexed
+ int column = node.getStartMark().getColumn();
+ if (node instanceof ScalarNode) {
+ DumperOptions.ScalarStyle style = ((ScalarNode) node).getScalarStyle();
+ switch (style) {
+ case SINGLE_QUOTED:
+ case DOUBLE_QUOTED:
+ column++;
+ break;
+ case LITERAL:
+ case FOLDED:
+ // For multi-lines, actual string content begins on next line
+ line++;
+ // Columns must be computed from the indentation
+ column = 0;
+ String snippet = policySource.getSnippet(line).orElse("");
+ for (char c : snippet.toCharArray()) {
+ if (!Character.isWhitespace(c)) {
+ break;
+ }
+ column++;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ idToLocationMap.put(id, CelSourceLocation.of(line, column));
+
+ int offset = 0;
+ if (line > 1) {
+ offset = policySource.getContent().lineOffsets().get(line - 2) + column;
+ }
+ idToOffsetMap.put(id, offset);
+
+ return id;
+ }
+
+ @Override
+ public long nextId() {
+ return ++id;
+ }
+
+ static ParserContext newInstance(CelPolicySource source) {
+ return new YamlParserContextImpl(source);
+ }
+
+ private YamlParserContextImpl(CelPolicySource source) {
+ this.issues = new ArrayList<>();
+ this.idToLocationMap = new HashMap<>();
+ this.idToOffsetMap = new HashMap<>();
+ this.policySource = source;
+ }
+}
diff --git a/policy/src/test/java/dev/cel/policy/BUILD.bazel b/policy/src/test/java/dev/cel/policy/BUILD.bazel
new file mode 100644
index 000000000..8606ca254
--- /dev/null
+++ b/policy/src/test/java/dev/cel/policy/BUILD.bazel
@@ -0,0 +1,50 @@
+load("//:testing.bzl", "junit4_test_suites")
+
+package(default_applicable_licenses = ["//:license"])
+
+java_library(
+ name = "tests",
+ testonly = True,
+ srcs = glob(["*.java"]),
+ resources = [
+ "//policy/src/test/resources:policy_yaml_files",
+ ],
+ deps = [
+ "//:java_truth",
+ "//bundle:cel",
+ "//common",
+ "//common:options",
+ "//common/internal",
+ "//common/resources/testdata/proto3:test_all_types_java_proto",
+ "//compiler",
+ "//extensions:optional_library",
+ "//parser",
+ "//parser:macro",
+ "//parser:unparser",
+ "//policy",
+ "//policy:compiler_factory",
+ "//policy:config",
+ "//policy:config_parser",
+ "//policy:parser",
+ "//policy:parser_context",
+ "//policy:parser_factory",
+ "//policy:source",
+ "//policy:validation_exception",
+ "//policy:value_string",
+ "//runtime",
+ "@maven//:com_google_api_grpc_proto_google_common_protos",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_testparameterinjector_test_parameter_injector",
+ "@maven//:junit_junit",
+ "@maven//:org_yaml_snakeyaml",
+ ],
+)
+
+junit4_test_suites(
+ name = "test_suites",
+ sizes = [
+ "small",
+ ],
+ src_dir = "src/test/java",
+ deps = [":tests"],
+)
diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerFactoryTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerFactoryTest.java
new file mode 100644
index 000000000..1f323256b
--- /dev/null
+++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerFactoryTest.java
@@ -0,0 +1,47 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import dev.cel.compiler.CelCompilerFactory;
+import dev.cel.parser.CelParserFactory;
+import dev.cel.runtime.CelRuntimeFactory;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(TestParameterInjector.class)
+public final class CelPolicyCompilerFactoryTest {
+
+ @Test
+ public void newPolicyCompiler_compilerRuntimeCombined() {
+ assertThat(
+ CelPolicyCompilerFactory.newPolicyCompiler(
+ CelCompilerFactory.standardCelCompilerBuilder().build(),
+ CelRuntimeFactory.standardCelRuntimeBuilder().build()))
+ .isNotNull();
+ }
+
+ @Test
+ public void newPolicyCompiler_parserCheckerRuntimeCombined() {
+ assertThat(
+ CelPolicyCompilerFactory.newPolicyCompiler(
+ CelParserFactory.standardCelParserBuilder().build(),
+ CelCompilerFactory.standardCelCheckerBuilder().build(),
+ CelRuntimeFactory.standardCelRuntimeBuilder().build()))
+ .isNotNull();
+ }
+}
diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java
new file mode 100644
index 000000000..30915be4a
--- /dev/null
+++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java
@@ -0,0 +1,337 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.policy;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static com.google.common.truth.Truth.assertThat;
+import static dev.cel.policy.PolicyTestHelper.readFromYaml;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import com.google.testing.junit.testparameterinjector.TestParameterValue;
+import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider;
+import dev.cel.bundle.Cel;
+import dev.cel.bundle.CelFactory;
+import dev.cel.common.CelAbstractSyntaxTree;
+import dev.cel.common.CelOptions;
+import dev.cel.extensions.CelOptionalLibrary;
+import dev.cel.parser.CelStandardMacro;
+import dev.cel.parser.CelUnparserFactory;
+import dev.cel.policy.PolicyTestHelper.K8sTagHandler;
+import dev.cel.policy.PolicyTestHelper.PolicyTestSuite;
+import dev.cel.policy.PolicyTestHelper.PolicyTestSuite.PolicyTestSection;
+import dev.cel.policy.PolicyTestHelper.PolicyTestSuite.PolicyTestSection.PolicyTestCase;
+import dev.cel.policy.PolicyTestHelper.PolicyTestSuite.PolicyTestSection.PolicyTestCase.PolicyTestInput;
+import dev.cel.policy.PolicyTestHelper.TestYamlPolicy;
+import dev.cel.runtime.CelRuntime.CelFunctionBinding;
+import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(TestParameterInjector.class)
+public final class CelPolicyCompilerImplTest {
+
+ private static final CelPolicyParser POLICY_PARSER =
+ CelPolicyParserFactory.newYamlParserBuilder().addTagVisitor(new K8sTagHandler()).build();
+ private static final CelPolicyConfigParser POLICY_CONFIG_PARSER =
+ CelPolicyParserFactory.newYamlConfigParser();
+ private static final CelOptions CEL_OPTIONS =
+ CelOptions.current().populateMacroCalls(true).build();
+
+ @Test
+ public void compileYamlPolicy_success(@TestParameter TestYamlPolicy yamlPolicy) throws Exception {
+ // Read config and produce an environment to compile policies
+ String configSource = yamlPolicy.readConfigYamlContent();
+ CelPolicyConfig policyConfig = POLICY_CONFIG_PARSER.parse(configSource);
+ Cel cel = policyConfig.extend(newCel(), CEL_OPTIONS);
+ // Read the policy source
+ String policySource = yamlPolicy.readPolicyYamlContent();
+ CelPolicy policy = POLICY_PARSER.parse(policySource);
+
+ CelAbstractSyntaxTree ast =
+ CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy);
+
+ assertThat(CelUnparserFactory.newUnparser().unparse(ast)).isEqualTo(yamlPolicy.getUnparsed());
+ }
+
+ @Test
+ public void compileYamlPolicy_containsCompilationError_throws(
+ @TestParameter TestErrorYamlPolicy testCase) throws Exception {
+ // Read config and produce an environment to compile policies
+ String configSource = testCase.readConfigYamlContent();
+ CelPolicyConfig policyConfig = POLICY_CONFIG_PARSER.parse(configSource);
+ Cel cel = policyConfig.extend(newCel(), CEL_OPTIONS);
+ // Read the policy source
+ String policySource = testCase.readPolicyYamlContent();
+ CelPolicy policy = POLICY_PARSER.parse(policySource, testCase.getPolicyFilePath());
+
+ CelPolicyValidationException e =
+ assertThrows(
+ CelPolicyValidationException.class,
+ () -> CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy));
+
+ assertThat(e).hasMessageThat().isEqualTo(testCase.readExpectedErrorsBaseline());
+ }
+
+ @Test
+ public void compileYamlPolicy_multilineContainsError_throws(
+ @TestParameter MultilineErrorTest testCase) throws Exception {
+ String policyContent = testCase.yaml;
+ CelPolicy policy = POLICY_PARSER.parse(policyContent);
+
+ CelPolicyValidationException e =
+ assertThrows(
+ CelPolicyValidationException.class,
+ () -> CelPolicyCompilerFactory.newPolicyCompiler(newCel()).build().compile(policy));
+
+ assertThat(e).hasMessageThat().isEqualTo(testCase.expected);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void evaluateYamlPolicy_withCanonicalTestData(
+ @TestParameter(valuesProvider = EvaluablePolicyTestDataProvider.class)
+ EvaluablePolicyTestData testData)
+ throws Exception {
+ // Setup
+ // Read config and produce an environment to compile policies
+ String configSource = testData.yamlPolicy.readConfigYamlContent();
+ CelPolicyConfig policyConfig = POLICY_CONFIG_PARSER.parse(configSource);
+ Cel cel = policyConfig.extend(newCel(), CEL_OPTIONS);
+ // Read the policy source
+ String policySource = testData.yamlPolicy.readPolicyYamlContent();
+ CelPolicy policy = POLICY_PARSER.parse(policySource);
+ CelAbstractSyntaxTree expectedOutputAst = cel.compile(testData.testCase.getOutput()).getAst();
+ Object expectedOutput = cel.createProgram(expectedOutputAst).eval();
+
+ // Act
+ // Compile then evaluate the policy
+ CelAbstractSyntaxTree compiledPolicyAst =
+ CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy);
+ ImmutableMap.Builder inputBuilder = ImmutableMap.builder();
+ for (Map.Entry entry : testData.testCase.getInput().entrySet()) {
+ String exprInput = entry.getValue().getExpr();
+ if (isNullOrEmpty(exprInput)) {
+ inputBuilder.put(entry.getKey(), entry.getValue().getValue());
+ } else {
+ CelAbstractSyntaxTree exprInputAst = cel.compile(exprInput).getAst();
+ inputBuilder.put(entry.getKey(), cel.createProgram(exprInputAst).eval());
+ }
+ }
+ Object evalResult = cel.createProgram(compiledPolicyAst).eval(inputBuilder.buildOrThrow());
+
+ // Assert
+ // Note that policies may either produce an optional or a non-optional result,
+ // if all the rules included nested ones can always produce a default result when none of the
+ // condition matches
+ if (testData.yamlPolicy.producesOptionalResult()) {
+ Optional