diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 000000000..d7ed7627e --- /dev/null +++ b/.bazelignore @@ -0,0 +1 @@ +codelab diff --git a/codelab/BUILD.bazel b/codelab/BUILD.bazel new file mode 100644 index 000000000..eea160dc4 --- /dev/null +++ b/codelab/BUILD.bazel @@ -0,0 +1,14 @@ +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//codelab:__subpackages__"], +) + +java_library( + name = "codelab", + exports = ["//codelab/src/main/codelab"], +) + +java_library( + name = "solutions", + exports = ["//codelab/src/main/codelab/solutions"], +) diff --git a/codelab/src/main/codelab/BUILD.bazel b/codelab/src/main/codelab/BUILD.bazel new file mode 100644 index 000000000..2a4be0843 --- /dev/null +++ b/codelab/src/main/codelab/BUILD.bazel @@ -0,0 +1,23 @@ +package( + default_applicable_licenses = [ + "//:license", + ], + default_visibility = ["//codelab:__pkg__"], +) + +java_library( + name = "codelab", + srcs = glob(["*.java"]), + deps = [ + "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", # unuseddeps: keep + "@maven//:com_google_guava_guava", # unuseddeps: keep + "//common", # unuseddeps: keep + "//common:compiler_common", # unuseddeps: keep + "//common/types", # unuseddeps: keep + "//common/types:type_providers", # unuseddeps: keep + "//compiler", # unuseddeps: keep + "//compiler:compiler_builder", # unuseddeps: keep + "//runtime", # unuseddeps: keep + # + ], +) diff --git a/codelab/src/main/codelab/Exercise1.java b/codelab/src/main/codelab/Exercise1.java new file mode 100644 index 000000000..dfa80fe51 --- /dev/null +++ b/codelab/src/main/codelab/Exercise1.java @@ -0,0 +1,46 @@ +// Copyright 2022 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 codelab; + +import dev.cel.common.CelAbstractSyntaxTree; + +/** + * Exercise1 evaluates a simple literal expression: "Hello, World!" + * + *
Compile, eval, profit! + */ +final class Exercise1 { + /** + * Compile the input {@code expression} and produce an AST. This method parses and type-checks the + * given expression to validate the syntax and type-agreement of the expression. + * + * @throws IllegalArgumentException If the expression is malformed due to syntactic or semantic + * errors. + */ + @SuppressWarnings("DoNotCallSuggester") + CelAbstractSyntaxTree compile(String expression) { + throw new UnsupportedOperationException("To be implemented"); + } + + /** + * Evaluates the compiled AST. + * + * @throws IllegalArgumentException If the compiled expression in AST fails to evaluate. + */ + @SuppressWarnings("DoNotCallSuggester") + Object eval(CelAbstractSyntaxTree ast) { + throw new UnsupportedOperationException("To be implemented"); + } +} diff --git a/codelab/src/main/codelab/Exercise2.java b/codelab/src/main/codelab/Exercise2.java new file mode 100644 index 000000000..2720e995e --- /dev/null +++ b/codelab/src/main/codelab/Exercise2.java @@ -0,0 +1,52 @@ +// Copyright 2022 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 codelab; + +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.types.CelType; +import java.util.Map; + +/** + * Exercise2 shows how to declare and use variables in expressions through two examples: + * + *
Declare a "contains" member function on map types that return a boolean indicating whether the
+ * map contains the key-value pair.
+ */
+final class Exercise4 {
+
+ /**
+ * Compiles the input expression.
+ *
+ * @throws IllegalArgumentException If the expression is malformed due to syntactic or semantic
+ * errors.
+ */
+ CelAbstractSyntaxTree compile(String expression) {
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .addVar("request", StructTypeReference.create(Request.getDescriptor().getFullName()))
+ .addMessageTypes(Request.getDescriptor())
+ .setResultType(SimpleType.BOOL)
+ // Provide the custom `contains` function declaration here.
+ .build();
+
+ try {
+ return celCompiler.compile(expression).getAst();
+ } catch (CelValidationException e) {
+ throw new IllegalArgumentException("Failed to compile expression.", e);
+ }
+ }
+
+ /**
+ * Evaluates the compiled AST with the user provided parameter values.
+ *
+ * @throws IllegalArgumentException If the compiled expression in AST fails to evaluate.
+ */
+ Object eval(CelAbstractSyntaxTree ast, Map Compile, eval, profit!
+ */
+final class Exercise1 {
+ /**
+ * Compile the input {@code expression} and produce an AST. This method parses and type-checks the
+ * given expression to validate the syntax and type-agreement of the expression.
+ *
+ * @throws IllegalArgumentException If the expression is malformed due to syntactic or semantic
+ * errors.
+ */
+ CelAbstractSyntaxTree compile(String expression) {
+ // Construct a CelCompiler instance.
+ // "String" is the expected output type for the type-checked expression
+ // CelCompiler is immutable and when statically configured can be moved to a static final
+ // member.
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder().setResultType(SimpleType.STRING).build();
+
+ CelAbstractSyntaxTree ast;
+ try {
+ // Parse the expression
+ ast = celCompiler.parse(expression).getAst();
+ } catch (CelValidationException e) {
+ // Report syntactic errors, if present
+ throw new IllegalArgumentException(
+ "Failed to parse expression. Reason: " + e.getMessage(), e);
+ }
+
+ try {
+ // Type-check the expression for correctness
+ ast = celCompiler.check(ast).getAst();
+ } catch (CelValidationException e) {
+ // Report semantic errors, if present.
+ throw new IllegalArgumentException(
+ "Failed to type-check expression. Reason: " + e.getMessage(), e);
+ }
+ return ast;
+ }
+
+ /**
+ * Evaluates the compiled AST.
+ *
+ * @throws IllegalArgumentException If the compiled expression in AST fails to evaluate.
+ */
+ Object eval(CelAbstractSyntaxTree ast) {
+ // Construct a CelRuntime instance
+ // CelRuntime is immutable just like the compiler and can be moved to a static final member.
+ CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build();
+
+ try {
+ // Plan the program
+ CelRuntime.Program program = celRuntime.createProgram(ast);
+
+ // Evaluate the program without any additional arguments.
+ return program.eval();
+ } catch (CelEvaluationException e) {
+ // Report any evaluation errors, if present
+ throw new IllegalArgumentException(
+ "Evaluation error has occurred. Reason: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/codelab/src/main/codelab/solutions/Exercise2.java b/codelab/src/main/codelab/solutions/Exercise2.java
new file mode 100644
index 000000000..5a1c1e8cc
--- /dev/null
+++ b/codelab/src/main/codelab/solutions/Exercise2.java
@@ -0,0 +1,82 @@
+// Copyright 2022 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 codelab.solutions;
+
+import com.google.rpc.context.AttributeContext;
+import dev.cel.common.CelAbstractSyntaxTree;
+import dev.cel.common.CelValidationException;
+import dev.cel.common.types.CelType;
+import dev.cel.common.types.SimpleType;
+import dev.cel.compiler.CelCompiler;
+import dev.cel.compiler.CelCompilerFactory;
+import dev.cel.runtime.CelEvaluationException;
+import dev.cel.runtime.CelRuntime;
+import dev.cel.runtime.CelRuntimeFactory;
+import java.util.Map;
+
+/**
+ * Exercise2 shows how to declare and use variables in expressions through two examples:
+ *
+ * Declare a "contains" member function on map types that return a boolean indicating whether the
+ * map contains the key-value pair.
+ */
+final class Exercise4 {
+
+ /**
+ * Compiles the input expression.
+ *
+ * @throws IllegalArgumentException If the expression is malformed due to syntactic or semantic
+ * errors.
+ */
+ CelAbstractSyntaxTree compile(String expression) {
+ // Useful components of the type-signature for 'contains'.
+ TypeParamType typeParamA = TypeParamType.create("A");
+ TypeParamType typeParamB = TypeParamType.create("B");
+ MapType mapTypeAB = MapType.create(typeParamA, typeParamB);
+
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .addVar("request", StructTypeReference.create(Request.getDescriptor().getFullName()))
+ .addMessageTypes(Request.getDescriptor())
+ .setResultType(SimpleType.BOOL)
+ .addFunctionDeclarations(
+ newFunctionDeclaration(
+ "contains",
+ newMemberOverload(
+ "map_contains_key_value",
+ SimpleType.BOOL,
+ ImmutableList.of(mapTypeAB, typeParamA, typeParamB))))
+ .build();
+
+ try {
+ return celCompiler.compile(expression).getAst();
+ } catch (CelValidationException e) {
+ throw new IllegalArgumentException("Failed to compile expression.", e);
+ }
+ }
+
+ /**
+ * Evaluates the compiled AST with the user provided parameter values.
+ *
+ * @throws IllegalArgumentException If the compiled expression in AST fails to evaluate.
+ */
+ Object eval(CelAbstractSyntaxTree ast, Map If a logical operation can short-circuit a branch that results in an error, CEL evaluation
+ * will return the logical result instead of propagating the error.
+ */
+ @Test
+ @TestParameters("{expression: 'true || true', expectedResult: true}")
+ @TestParameters("{expression: 'true || false', expectedResult: true}")
+ @TestParameters("{expression: 'false || true', expectedResult: true}")
+ @TestParameters("{expression: 'false || false', expectedResult: false}")
+ @TestParameters("{expression: 'true || (1 / 0 > 2)', expectedResult: true}")
+ @TestParameters("{expression: '(1 / 0 > 2) || true', expectedResult: true}")
+ public void evaluate_logicalOrShortCircuits_success(String expression, boolean expectedResult) {
+ Object evaluatedResult = exercise3.compileAndEvaluate(expression);
+
+ assertThat(evaluatedResult).isEqualTo(expectedResult);
+ }
+
+ /** Demonstrates a case where an error is surfaced to the user. */
+ @Test
+ @TestParameters("{expression: 'false || (1 / 0 > 2)'}")
+ @TestParameters("{expression: '(1 / 0 > 2) || false'}")
+ public void evaluate_logicalOrFailure_throwsException(String expression) {
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class, () -> exercise3.compileAndEvaluate(expression));
+
+ assertThat(exception).hasMessageThat().contains("Evaluation error has occurred.");
+ assertThat(exception).hasCauseThat().isInstanceOf(CelEvaluationException.class);
+ assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error: / by zero");
+ }
+
+ @Test
+ @TestParameters("{expression: 'todo: Fill me in using logical AND! (&&)', expectedResult: true}")
+ public void evaluate_logicalAndShortCircuits_success(String expression, boolean expectedResult) {
+ Object evaluatedResult = exercise3.compileAndEvaluate(expression);
+
+ assertThat(evaluatedResult).isEqualTo(expectedResult);
+ }
+
+ @Test
+ @TestParameters("{expression: 'todo: Fill me in using logical AND! (&&)'}")
+ public void evaluate_logicalAndFailure_throwsException(String expression) {
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class, () -> exercise3.compileAndEvaluate(expression));
+
+ assertThat(exception).hasMessageThat().contains("Evaluation error has occurred.");
+ assertThat(exception).hasCauseThat().isInstanceOf(CelEvaluationException.class);
+ assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error: / by zero");
+ }
+
+ @Test
+ @TestParameters("{expression: 'false ? (1 / 0) > 2 : false', expectedResult: false}")
+ @TestParameters("{expression: 'todo: Fill me in using ternary! A ? B : C', expectedResult: true}")
+ public void evaluate_ternaryShortCircuits_success(String expression, boolean expectedResult) {
+ Object evaluatedResult = exercise3.compileAndEvaluate(expression);
+
+ assertThat(evaluatedResult).isEqualTo(expectedResult);
+ }
+
+ @Test
+ @TestParameters("{expression: 'todo: Fill me in using ternary! A ? B : C'}")
+ public void evaluate_ternaryFailure_throwsException(String expression) {
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class, () -> exercise3.compileAndEvaluate(expression));
+
+ assertThat(exception).hasMessageThat().contains("Evaluation error has occurred.");
+ assertThat(exception).hasCauseThat().isInstanceOf(CelEvaluationException.class);
+ assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error: / by zero");
+ }
+}
diff --git a/codelab/src/test/codelab/Exercise4Test.java b/codelab/src/test/codelab/Exercise4Test.java
new file mode 100644
index 000000000..dc6d4524e
--- /dev/null
+++ b/codelab/src/test/codelab/Exercise4Test.java
@@ -0,0 +1,51 @@
+// Copyright 2022 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 codelab;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+import com.google.rpc.context.AttributeContext;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import com.google.testing.junit.testparameterinjector.TestParameters;
+import dev.cel.common.CelAbstractSyntaxTree;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(TestParameterInjector.class)
+public final class Exercise4Test {
+ private final Exercise4 exercise4 = new Exercise4();
+
+ @Test
+ @TestParameters("{group: 'admin', expectedResult: true}")
+ @TestParameters("{group: 'users', expectedResult: false}")
+ public void evaluate_requestContainsGroup_success(String group, boolean expectedResult) {
+ CelAbstractSyntaxTree ast = exercise4.compile("request.auth.claims.contains('group', 'admin')");
+ AttributeContext.Auth auth =
+ AttributeContext.Auth.newBuilder()
+ .setPrincipal("user:me@acme.co")
+ .setClaims(
+ Struct.newBuilder()
+ .putFields("group", Value.newBuilder().setStringValue(group).build()))
+ .build();
+ AttributeContext.Request request = AttributeContext.Request.newBuilder().setAuth(auth).build();
+
+ Object evaluatedResult = exercise4.eval(ast, ImmutableMap.of("request", request));
+
+ assertThat(evaluatedResult).isEqualTo(expectedResult);
+ }
+}
diff --git a/codelab/src/test/codelab/solutions/BUILD.bazel b/codelab/src/test/codelab/solutions/BUILD.bazel
new file mode 100644
index 000000000..72faee51f
--- /dev/null
+++ b/codelab/src/test/codelab/solutions/BUILD.bazel
@@ -0,0 +1,63 @@
+package(default_applicable_licenses = [
+ "//:license",
+])
+
+java_test(
+ name = "Exercise1Test",
+ srcs = ["Exercise1Test.java"],
+ test_class = "codelab.solutions.Exercise1Test",
+ deps = [
+ "//:java_truth",
+ "//codelab:solutions",
+ "//common",
+ "//compiler",
+ "//compiler:compiler_builder",
+ "@maven//:junit_junit",
+ ],
+)
+
+java_test(
+ name = "Exercise2Test",
+ srcs = ["Exercise2Test.java"],
+ test_class = "codelab.solutions.Exercise2Test",
+ deps = [
+ "//:java_truth",
+ "//codelab:solutions",
+ "//common",
+ "//common/types",
+ "@com_google_googleapis//google/rpc/context:attribute_context_java_proto",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_protobuf_protobuf_java",
+ "@maven//:com_google_testparameterinjector_test_parameter_injector",
+ "@maven//:junit_junit",
+ ],
+)
+
+java_test(
+ name = "Exercise3Test",
+ srcs = ["Exercise3Test.java"],
+ test_class = "codelab.solutions.Exercise3Test",
+ deps = [
+ "//:java_truth",
+ "//codelab:solutions",
+ "//runtime",
+ "@maven//:com_google_testparameterinjector_test_parameter_injector",
+ "@maven//:junit_junit",
+ ],
+)
+
+java_test(
+ name = "Exercise4Test",
+ srcs = ["Exercise4Test.java"],
+ test_class = "codelab.solutions.Exercise4Test",
+ deps = [
+ "//:java_truth",
+ "//codelab:solutions",
+ "//common",
+ "@com_google_googleapis//google/rpc/context:attribute_context_java_proto",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_protobuf_protobuf_java",
+ "@maven//:com_google_testparameterinjector_test_parameter_injector",
+ "@maven//:junit_junit",
+ ],
+)
diff --git a/codelab/src/test/codelab/solutions/Exercise1Test.java b/codelab/src/test/codelab/solutions/Exercise1Test.java
new file mode 100644
index 000000000..8b01690d9
--- /dev/null
+++ b/codelab/src/test/codelab/solutions/Exercise1Test.java
@@ -0,0 +1,73 @@
+// Copyright 2022 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 codelab.solutions;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import dev.cel.common.CelAbstractSyntaxTree;
+import dev.cel.compiler.CelCompiler;
+import dev.cel.compiler.CelCompilerFactory;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class Exercise1Test {
+ private static final String TEST_EXPRESSION = "'Hello, World!'";
+
+ private final Exercise1 exercise1 = new Exercise1();
+
+ @Test
+ public void compile_stringExpression_parseSuccess() {
+ CelAbstractSyntaxTree ast = exercise1.compile(TEST_EXPRESSION);
+ assertThat(ast).isNotNull();
+ }
+
+ @Test
+ public void compile_stringExpression_typecheckSuccess() {
+ CelAbstractSyntaxTree ast = exercise1.compile(TEST_EXPRESSION);
+ assertThat(ast.isChecked()).isTrue();
+ }
+
+ @Test
+ public void compile_booleanExpression_throwsValidationException() {
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () -> exercise1.compile("true == true"));
+
+ assertThat(exception).hasMessageThat().contains("Failed to type-check expression.");
+ assertThat(exception).hasMessageThat().contains("expected type 'string' but found 'bool'");
+ }
+
+ @Test
+ public void evaluate_stringExpression_success() {
+ CelAbstractSyntaxTree ast = exercise1.compile(TEST_EXPRESSION);
+
+ Object evaluatedResult = exercise1.eval(ast);
+
+ assertThat(evaluatedResult).isEqualTo("Hello, World!");
+ }
+
+ @Test
+ public void evaluate_divideByZeroExpression_throwsEvaluationException() throws Exception {
+ CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build();
+ CelAbstractSyntaxTree ast = compiler.compile("1/0").getAst();
+
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () -> exercise1.eval(ast));
+
+ assertThat(exception).hasMessageThat().contains("evaluation error: / by zero");
+ }
+}
diff --git a/codelab/src/test/codelab/solutions/Exercise2Test.java b/codelab/src/test/codelab/solutions/Exercise2Test.java
new file mode 100644
index 000000000..0c0cb4f9f
--- /dev/null
+++ b/codelab/src/test/codelab/solutions/Exercise2Test.java
@@ -0,0 +1,67 @@
+// Copyright 2022 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 codelab.solutions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+import com.google.rpc.context.AttributeContext;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import com.google.testing.junit.testparameterinjector.TestParameters;
+import dev.cel.common.CelAbstractSyntaxTree;
+import dev.cel.common.types.SimpleType;
+import dev.cel.common.types.StructTypeReference;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(TestParameterInjector.class)
+public final class Exercise2Test {
+ private final Exercise2 exercise2 = new Exercise2();
+
+ @Test
+ @TestParameters("{value: 5, expectedResult: false}")
+ @TestParameters("{value: -5, expectedResult: true}")
+ public void evaluate_negativeExpression(long value, boolean expectedResult) {
+ CelAbstractSyntaxTree ast = exercise2.compile("value < 0", "value", SimpleType.INT);
+
+ Object evaluatedResult = exercise2.eval(ast, ImmutableMap.of("value", value));
+ assertThat(evaluatedResult).isEqualTo(expectedResult);
+ }
+
+ @Test
+ @TestParameters("{group: 'admin', expectedResult: true}")
+ @TestParameters("{group: 'users', expectedResult: false}")
+ public void evaluate_requestAuthorization(String group, boolean expectedResult) {
+ CelAbstractSyntaxTree ast =
+ exercise2.compile(
+ "request.auth.claims.group == 'admin'",
+ "request",
+ StructTypeReference.create("google.rpc.context.AttributeContext.Request"));
+
+ AttributeContext.Auth auth =
+ AttributeContext.Auth.newBuilder()
+ .setPrincipal("user:me@acme.co")
+ .setClaims(
+ Struct.newBuilder()
+ .putFields("group", Value.newBuilder().setStringValue(group).build()))
+ .build();
+ AttributeContext.Request request = AttributeContext.Request.newBuilder().setAuth(auth).build();
+
+ Object evaluatedResult = exercise2.eval(ast, ImmutableMap.of("request", request));
+ assertThat(evaluatedResult).isEqualTo(expectedResult);
+ }
+}
diff --git a/codelab/src/test/codelab/solutions/Exercise3Test.java b/codelab/src/test/codelab/solutions/Exercise3Test.java
new file mode 100644
index 000000000..47d1c3470
--- /dev/null
+++ b/codelab/src/test/codelab/solutions/Exercise3Test.java
@@ -0,0 +1,113 @@
+// Copyright 2022 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 codelab.solutions;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import com.google.testing.junit.testparameterinjector.TestParameters;
+import dev.cel.runtime.CelEvaluationException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(TestParameterInjector.class)
+public final class Exercise3Test {
+ private final Exercise3 exercise3 = new Exercise3();
+
+ /**
+ * Demonstrates CEL's unique feature of commutative logical operators.
+ *
+ * If a logical operation can short-circuit a branch that results in an error, CEL evaluation
+ * will return the logical result instead of propagating the error.
+ */
+ @Test
+ @TestParameters("{expression: 'true || true', expectedResult: true}")
+ @TestParameters("{expression: 'true || false', expectedResult: true}")
+ @TestParameters("{expression: 'false || true', expectedResult: true}")
+ @TestParameters("{expression: 'false || false', expectedResult: false}")
+ @TestParameters("{expression: 'true || (1 / 0 > 2)', expectedResult: true}")
+ @TestParameters("{expression: '(1 / 0 > 2) || true', expectedResult: true}")
+ public void evaluate_logicalOrShortCircuits_success(String expression, boolean expectedResult) {
+ Object evaluatedResult = exercise3.compileAndEvaluate(expression);
+
+ assertThat(evaluatedResult).isEqualTo(expectedResult);
+ }
+
+ /** Demonstrates a case where an error is surfaced to the user. */
+ @Test
+ @TestParameters("{expression: 'false || (1 / 0 > 2)'}")
+ @TestParameters("{expression: '(1 / 0 > 2) || false'}")
+ public void evaluate_logicalOrFailure_throwsException(String expression) {
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class, () -> exercise3.compileAndEvaluate(expression));
+
+ assertThat(exception).hasMessageThat().contains("Evaluation error has occurred.");
+ assertThat(exception).hasCauseThat().isInstanceOf(CelEvaluationException.class);
+ assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error: / by zero");
+ }
+
+ @Test
+ @TestParameters("{expression: 'true && true', expectedResult: true}")
+ @TestParameters("{expression: 'true && false', expectedResult: false}")
+ @TestParameters("{expression: 'false && true', expectedResult: false}")
+ @TestParameters("{expression: 'false && false', expectedResult: false}")
+ @TestParameters("{expression: 'false && (1 / 0 > 2)', expectedResult: false}")
+ @TestParameters("{expression: '(1 / 0 > 2) && false', expectedResult: false}")
+ public void evaluate_logicalAndShortCircuits_success(String expression, boolean expectedResult) {
+ Object evaluatedResult = exercise3.compileAndEvaluate(expression);
+
+ assertThat(evaluatedResult).isEqualTo(expectedResult);
+ }
+
+ @Test
+ @TestParameters("{expression: 'true && (1 / 0 > 2)'}")
+ @TestParameters("{expression: '(1 / 0 > 2) && true'}")
+ public void evaluate_logicalAndFailure_throwsException(String expression) {
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class, () -> exercise3.compileAndEvaluate(expression));
+
+ assertThat(exception).hasMessageThat().contains("Evaluation error has occurred.");
+ assertThat(exception).hasCauseThat().isInstanceOf(CelEvaluationException.class);
+ assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error: / by zero");
+ }
+
+ @Test
+ @TestParameters("{expression: 'false ? (1 / 0) > 2 : false', expectedResult: false}")
+ @TestParameters("{expression: 'false ? (1 / 0) > 2 : true', expectedResult: true}")
+ @TestParameters("{expression: 'true ? false : (1 / 0) > 2', expectedResult: false}")
+ @TestParameters("{expression: 'true ? true : (1 / 0) > 2', expectedResult: true}")
+ public void evaluate_ternaryShortCircuits_success(String expression, boolean expectedResult) {
+ Object evaluatedResult = exercise3.compileAndEvaluate(expression);
+
+ assertThat(evaluatedResult).isEqualTo(expectedResult);
+ }
+
+ @Test
+ @TestParameters("{expression: '(1 / 0) > 2 ? true : true'}")
+ @TestParameters("{expression: 'true ? (1 / 0) > 2 : true'}")
+ @TestParameters("{expression: 'false ? true : (1 / 0) > 2'}")
+ public void evaluate_ternaryFailure_throwsException(String expression) {
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class, () -> exercise3.compileAndEvaluate(expression));
+
+ assertThat(exception).hasMessageThat().contains("Evaluation error has occurred.");
+ assertThat(exception).hasCauseThat().isInstanceOf(CelEvaluationException.class);
+ assertThat(exception).hasCauseThat().hasMessageThat().contains("evaluation error: / by zero");
+ }
+}
diff --git a/codelab/src/test/codelab/solutions/Exercise4Test.java b/codelab/src/test/codelab/solutions/Exercise4Test.java
new file mode 100644
index 000000000..1a734f93a
--- /dev/null
+++ b/codelab/src/test/codelab/solutions/Exercise4Test.java
@@ -0,0 +1,51 @@
+// Copyright 2022 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 codelab.solutions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+import com.google.rpc.context.AttributeContext;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import com.google.testing.junit.testparameterinjector.TestParameters;
+import dev.cel.common.CelAbstractSyntaxTree;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(TestParameterInjector.class)
+public final class Exercise4Test {
+ private final Exercise4 exercise4 = new Exercise4();
+
+ @Test
+ @TestParameters("{group: 'admin', expectedResult: true}")
+ @TestParameters("{group: 'users', expectedResult: false}")
+ public void evaluate_requestContainsGroup_success(String group, boolean expectedResult) {
+ CelAbstractSyntaxTree ast = exercise4.compile("request.auth.claims.contains('group', 'admin')");
+ AttributeContext.Auth auth =
+ AttributeContext.Auth.newBuilder()
+ .setPrincipal("user:me@acme.co")
+ .setClaims(
+ Struct.newBuilder()
+ .putFields("group", Value.newBuilder().setStringValue(group).build()))
+ .build();
+ AttributeContext.Request request = AttributeContext.Request.newBuilder().setAuth(auth).build();
+
+ Object evaluatedResult = exercise4.eval(ast, ImmutableMap.of("request", request));
+
+ assertThat(evaluatedResult).isEqualTo(expectedResult);
+ }
+}
+ *
+ */
+final class Exercise2 {
+
+ /**
+ * Compiles the input expression with provided variable information.
+ *
+ * @throws IllegalArgumentException If the expression is malformed due to syntactic or semantic
+ * errors.
+ */
+ CelAbstractSyntaxTree compile(String expression, String variableName, CelType variableType) {
+ // Compile (parse + type-check) the expression
+ // CelCompiler is immutable and when statically configured can be moved to a static final
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .addVar(variableName, variableType)
+ .addMessageTypes(AttributeContext.Request.getDescriptor())
+ .setResultType(SimpleType.BOOL)
+ .build();
+ try {
+ return celCompiler.compile(expression).getAst();
+ } catch (CelValidationException e) {
+ throw new IllegalArgumentException(
+ "Failed to compile expression. Reason: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Evaluates the compiled AST with the user provided parameter values.
+ *
+ * @throws IllegalArgumentException If the compiled expression in AST fails to evaluate.
+ */
+ Object eval(CelAbstractSyntaxTree ast, Map
+ *
+ *
+ * @return true If the key was found AND the value at the key equals to the value being checked
+ */
+ @SuppressWarnings("unchecked") // Type-checker guarantees casting safety.
+ private static boolean mapContainsKeyValue(Object[] args) {
+ // The declaration of the function ensures that only arguments which match
+ // the mapContainsKey signature will be provided to the function.
+ Map