From fbf7c27845e8926b07a105d280ffff63d9861579 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 9 May 2024 10:51:18 -0700 Subject: [PATCH] Add code for Exercise 9 Codelabs PiperOrigin-RevId: 632202746 --- codelab/src/main/codelab/BUILD.bazel | 4 + codelab/src/main/codelab/Exercise9.java | 99 ++++++++++ .../src/main/codelab/solutions/BUILD.bazel | 4 + .../src/main/codelab/solutions/Exercise9.java | 173 ++++++++++++++++++ codelab/src/test/codelab/BUILD.bazel | 16 ++ codelab/src/test/codelab/Exercise9Test.java | 95 ++++++++++ .../src/test/codelab/solutions/BUILD.bazel | 15 ++ .../test/codelab/solutions/Exercise9Test.java | 95 ++++++++++ 8 files changed, 501 insertions(+) create mode 100644 codelab/src/main/codelab/Exercise9.java create mode 100644 codelab/src/main/codelab/solutions/Exercise9.java create mode 100644 codelab/src/test/codelab/Exercise9Test.java create mode 100644 codelab/src/test/codelab/solutions/Exercise9Test.java diff --git a/codelab/src/main/codelab/BUILD.bazel b/codelab/src/main/codelab/BUILD.bazel index 117d1ccd..6c9f9025 100644 --- a/codelab/src/main/codelab/BUILD.bazel +++ b/codelab/src/main/codelab/BUILD.bazel @@ -9,9 +9,12 @@ java_library( name = "codelab", srcs = glob(["*.java"]), deps = [ + "//bundle:cel", # unuseddeps: keep "//common", # unuseddeps: keep "//common:compiler_common", # unuseddeps: keep "//common:proto_json_adapter", # unuseddeps: keep + "//common/ast", # unuseddeps: keep + "//common/navigation", # unuseddeps: keep "//common/types", # unuseddeps: keep "//common/types:type_providers", # unuseddeps: keep "//compiler", # unuseddeps: keep @@ -24,6 +27,7 @@ java_library( "//parser:macro", # unuseddeps: keep "//runtime", # unuseddeps: keep "//validator", # unuseddeps: keep + "//validator:ast_validator", # unuseddeps: keep "//validator:validator_builder", # unuseddeps: keep "//validator/validators:duration", # unuseddeps: keep "//validator/validators:homogeneous_literal", # unuseddeps: keep diff --git a/codelab/src/main/codelab/Exercise9.java b/codelab/src/main/codelab/Exercise9.java new file mode 100644 index 00000000..85705390 --- /dev/null +++ b/codelab/src/main/codelab/Exercise9.java @@ -0,0 +1,99 @@ +// 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 codelab; + +import com.google.rpc.context.AttributeContext; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.validator.CelValidator; +import dev.cel.validator.CelValidatorFactory; + +/** + * Exercise9 demonstrates how to author a custom AST validator to perform domain specific + * validations. + * + *

Given a `google.rpc.context.AttributeContext.Request` message, validate that its fields follow + * the expected HTTP specification. + * + *

Given an expression containing an expensive function call, validate that it is not nested + * within a macro. + */ +final class Exercise9 { + private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.ALL) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "is_prime_number", + CelOverloadDecl.newGlobalOverload( + "is_prime_number_int", + "Invokes an expensive RPC call to check if the value is a prime number.", + SimpleType.BOOL, + SimpleType.INT))) + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); + private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); + private static final CelValidator CEL_VALIDATOR = + CelValidatorFactory.standardCelValidatorBuilder(CEL_COMPILER, CEL_RUNTIME) + // Add your custom AST validators here + .build(); + + /** + * Compiles the input expression. + * + * @throws CelValidationException If the expression contains parsing or type-checking errors. + */ + CelAbstractSyntaxTree compile(String expression) throws CelValidationException { + return CEL_COMPILER.compile(expression).getAst(); + } + + /** Validates a type-checked AST. */ + CelValidationResult validate(CelAbstractSyntaxTree checkedAst) { + return CEL_VALIDATOR.validate(checkedAst); + } + + /** Evaluates the compiled AST. */ + Object eval(CelAbstractSyntaxTree ast) throws CelEvaluationException { + return CEL_RUNTIME.createProgram(ast).eval(); + } + + /** + * Performs general validation on AttributeContext.Request message. The validator raises errors if + * the HTTP request is malformed and semantically invalid (e.g: contains disallowed HTTP methods). + * Warnings are presented if there's potential problems with the contents of the request (e.g: + * using "http" instead of "https" for scheme). + */ + static final class AttributeContextRequestValidator { + // Implement validate method here + } + + /** Prevents nesting an expensive function call within a macro. */ + static final class ComprehensionSafetyValidator { + // Implement validate method here + } +} diff --git a/codelab/src/main/codelab/solutions/BUILD.bazel b/codelab/src/main/codelab/solutions/BUILD.bazel index ec29a3e7..44f9632e 100644 --- a/codelab/src/main/codelab/solutions/BUILD.bazel +++ b/codelab/src/main/codelab/solutions/BUILD.bazel @@ -9,9 +9,12 @@ java_library( name = "solutions", srcs = glob(["*.java"]), deps = [ + "//bundle:cel", "//common", "//common:compiler_common", "//common:proto_json_adapter", + "//common/ast", + "//common/navigation", "//common/types", "//common/types:type_providers", "//compiler", @@ -24,6 +27,7 @@ java_library( "//parser:macro", "//runtime", "//validator", + "//validator:ast_validator", "//validator:validator_builder", "//validator/validators:duration", "//validator/validators:homogeneous_literal", diff --git a/codelab/src/main/codelab/solutions/Exercise9.java b/codelab/src/main/codelab/solutions/Exercise9.java new file mode 100644 index 00000000..2b45c353 --- /dev/null +++ b/codelab/src/main/codelab/solutions/Exercise9.java @@ -0,0 +1,173 @@ +// 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 codelab.solutions; + +import com.google.common.collect.ImmutableSet; +import com.google.rpc.context.AttributeContext; +import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.CelStruct; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.navigation.CelNavigableAst; +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.validator.CelAstValidator; +import dev.cel.validator.CelValidator; +import dev.cel.validator.CelValidatorFactory; + +/** + * Exercise9 demonstrates how to author a custom AST validator to perform domain specific + * validations. + * + *

Given a `google.rpc.context.AttributeContext.Request` message, validate that its fields follow + * the expected HTTP specification. + * + *

Given an expression containing an expensive function call, validate that it is not nested + * within a macro. + */ +final class Exercise9 { + private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.ALL) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "is_prime_number", + CelOverloadDecl.newGlobalOverload( + "is_prime_number_int", + "Invokes an expensive RPC call to check if the value is a prime number.", + SimpleType.BOOL, + SimpleType.INT))) + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); + private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(AttributeContext.Request.getDescriptor()) + .build(); + private static final CelValidator CEL_VALIDATOR = + CelValidatorFactory.standardCelValidatorBuilder(CEL_COMPILER, CEL_RUNTIME) + .addAstValidators( + new AttributeContextRequestValidator(), // + new ComprehensionSafetyValidator()) + .build(); + + /** + * Compiles the input expression. + * + * @throws CelValidationException If the expression contains parsing or type-checking errors. + */ + CelAbstractSyntaxTree compile(String expression) throws CelValidationException { + return CEL_COMPILER.compile(expression).getAst(); + } + + /** Validates a type-checked AST. */ + CelValidationResult validate(CelAbstractSyntaxTree checkedAst) { + return CEL_VALIDATOR.validate(checkedAst); + } + + /** Evaluates the compiled AST. */ + Object eval(CelAbstractSyntaxTree ast) throws CelEvaluationException { + return CEL_RUNTIME.createProgram(ast).eval(); + } + + /** + * Performs general validation on AttributeContext.Request message. The validator raises errors if + * the HTTP request is malformed and semantically invalid (e.g: contains disallowed HTTP methods). + * Warnings are presented if there's potential problems with the contents of the request (e.g: + * using "http" instead of "https" for scheme). + */ + static final class AttributeContextRequestValidator implements CelAstValidator { + private static final ImmutableSet ALLOWED_HTTP_METHODS = + ImmutableSet.of("GET", "POST", "PUT", "DELETE"); + + @Override + public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) { + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind().equals(Kind.STRUCT)) + .map(node -> node.expr().struct()) + .filter( + struct -> struct.messageName().equals("google.rpc.context.AttributeContext.Request")) + .forEach( + struct -> { + for (CelStruct.Entry entry : struct.entries()) { + String fieldKey = entry.fieldKey(); + if (fieldKey.equals("method")) { + String entryStringValue = getStringValue(entry.value()); + if (!ALLOWED_HTTP_METHODS.contains(entryStringValue)) { + issuesFactory.addError( + entry.value().id(), entryStringValue + " is not an allowed HTTP method."); + } + } else if (fieldKey.equals("scheme")) { + String entryStringValue = getStringValue(entry.value()); + if (!entryStringValue.equals("https")) { + issuesFactory.addWarning( + entry.value().id(), "Prefer using https for safety."); + } + } + } + }); + } + + /** + * Reads the underlying string value from the expression. + * + * @throws UnsupportedOperationException if the expression is not a constant string value. + */ + private static String getStringValue(CelExpr celExpr) { + return celExpr.constant().stringValue(); + } + } + + /** Prevents nesting an expensive function call within a macro. */ + static final class ComprehensionSafetyValidator implements CelAstValidator { + private static final String EXPENSIVE_FUNCTION_NAME = "is_prime_number"; + + @Override + public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) { + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind().equals(Kind.COMPREHENSION)) + .forEach( + comprehensionNode -> { + boolean isFunctionWithinMacro = + comprehensionNode + .descendants() + .anyMatch( + node -> + node.expr() + .callOrDefault() + .function() + .equals(EXPENSIVE_FUNCTION_NAME)); + if (isFunctionWithinMacro) { + issuesFactory.addError( + comprehensionNode.id(), + EXPENSIVE_FUNCTION_NAME + " function cannot be used within CEL macros."); + } + }); + } + } +} diff --git a/codelab/src/test/codelab/BUILD.bazel b/codelab/src/test/codelab/BUILD.bazel index 6bc72557..8772578c 100644 --- a/codelab/src/test/codelab/BUILD.bazel +++ b/codelab/src/test/codelab/BUILD.bazel @@ -134,6 +134,22 @@ java_test( ], ) +java_test( + name = "Exercise9Test", + srcs = ["Exercise9Test.java"], + tags = ["notap"], + test_class = "codelab.Exercise9Test", + deps = [ + "//:java_truth", + "//codelab", + "//common", + "//common:compiler_common", + "@maven//:com_google_api_grpc_proto_google_common_protos", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + test_suite( name = "exercise_test_suite", tags = ["notap"], diff --git a/codelab/src/test/codelab/Exercise9Test.java b/codelab/src/test/codelab/Exercise9Test.java new file mode 100644 index 00000000..7157d61c --- /dev/null +++ b/codelab/src/test/codelab/Exercise9Test.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 codelab; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.rpc.context.AttributeContext; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class Exercise9Test { + private final Exercise9 exercise9 = new Exercise9(); + + @Test + public void validate_invalidHttpMethod_returnsError() throws Exception { + String expression = + "google.rpc.context.AttributeContext.Request { \n" + + "scheme: 'http', " + + "method: 'GETTT', " // method is misspelled. + + "host: 'cel.dev' \n" + + "}"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :2:25: GETTT is not an allowed HTTP method.\n" + + " | scheme: 'http', method: 'GETTT', host: 'cel.dev' \n" + + " | ........................^"); + assertThrows(CelValidationException.class, validationResult::getAst); + } + + @Test + public void validate_schemeIsHttp_returnsWarning() throws Exception { + String expression = + "google.rpc.context.AttributeContext.Request { \n" + + "scheme: 'http', " // https is preferred but not required. + + "method: 'GET', " + + "host: 'cel.dev' \n" + + "}"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isFalse(); + assertThat(validationResult.getIssueString()) + .isEqualTo( + "WARNING: :2:9: Prefer using https for safety.\n" + + " | scheme: 'http', method: 'GET', host: 'cel.dev' \n" + + " | ........^"); + // Because the validation result does not contain any errors, you can still evaluate it. + assertThat(exercise9.eval(validationResult.getAst())) + .isEqualTo( + AttributeContext.Request.newBuilder() + .setScheme("http") + .setMethod("GET") + .setHost("cel.dev") + .build()); + } + + @Test + public void validate_isPrimeNumberWithinMacro_returnsError() throws Exception { + String expression = "[2,3,5].all(x, is_prime_number(x))"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:12: is_prime_number function cannot be used within CEL macros.\n" + + " | [2,3,5].all(x, is_prime_number(x))\n" + + " | ...........^"); + } +} diff --git a/codelab/src/test/codelab/solutions/BUILD.bazel b/codelab/src/test/codelab/solutions/BUILD.bazel index 3f434f83..0ac187a9 100644 --- a/codelab/src/test/codelab/solutions/BUILD.bazel +++ b/codelab/src/test/codelab/solutions/BUILD.bazel @@ -125,3 +125,18 @@ java_test( "@maven//:junit_junit", ], ) + +java_test( + name = "Exercise9Test", + srcs = ["Exercise9Test.java"], + test_class = "codelab.solutions.Exercise9Test", + deps = [ + "//:java_truth", + "//codelab:solutions", + "//common", + "//common:compiler_common", + "@maven//:com_google_api_grpc_proto_google_common_protos", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) diff --git a/codelab/src/test/codelab/solutions/Exercise9Test.java b/codelab/src/test/codelab/solutions/Exercise9Test.java new file mode 100644 index 00000000..622df45c --- /dev/null +++ b/codelab/src/test/codelab/solutions/Exercise9Test.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 codelab.solutions; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.rpc.context.AttributeContext; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class Exercise9Test { + private final Exercise9 exercise9 = new Exercise9(); + + @Test + public void validate_invalidHttpMethod_returnsError() throws Exception { + String expression = + "google.rpc.context.AttributeContext.Request { \n" + + "scheme: 'http', " + + "method: 'GETTT', " // method is misspelled. + + "host: 'cel.dev' \n" + + "}"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :2:25: GETTT is not an allowed HTTP method.\n" + + " | scheme: 'http', method: 'GETTT', host: 'cel.dev' \n" + + " | ........................^"); + assertThrows(CelValidationException.class, validationResult::getAst); + } + + @Test + public void validate_schemeIsHttp_returnsWarning() throws Exception { + String expression = + "google.rpc.context.AttributeContext.Request { \n" + + "scheme: 'http', " // https is preferred but not required. + + "method: 'GET', " + + "host: 'cel.dev' \n" + + "}"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isFalse(); + assertThat(validationResult.getIssueString()) + .isEqualTo( + "WARNING: :2:9: Prefer using https for safety.\n" + + " | scheme: 'http', method: 'GET', host: 'cel.dev' \n" + + " | ........^"); + // Because the validation result does not contain any errors, you can still evaluate it. + assertThat(exercise9.eval(validationResult.getAst())) + .isEqualTo( + AttributeContext.Request.newBuilder() + .setScheme("http") + .setMethod("GET") + .setHost("cel.dev") + .build()); + } + + @Test + public void validate_isPrimeNumberWithinMacro_returnsError() throws Exception { + String expression = "[2,3,5].all(x, is_prime_number(x))"; + CelAbstractSyntaxTree ast = exercise9.compile(expression); + + CelValidationResult validationResult = exercise9.validate(ast); + + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .isEqualTo( + "ERROR: :1:12: is_prime_number function cannot be used within CEL macros.\n" + + " | [2,3,5].all(x, is_prime_number(x))\n" + + " | ...........^"); + } +}