From 5593808ac40846d4de65db33ccb5050e2e039494 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 29 Aug 2023 17:45:20 +0000 Subject: [PATCH] Implement standalone validator and general interfaces to perform custom AST validation. TimestampLiteralValidator is added as a canonical validator demonstrating its usage. PiperOrigin-RevId: 561076631 --- .../main/java/dev/cel/bundle/CelFactory.java | 7 + .../src/main/java/dev/cel/bundle/CelImpl.java | 5 + .../java/dev/cel/checker/ExprChecker.java | 3 +- .../src/main/java/dev/cel/common/BUILD.bazel | 1 - .../dev/cel/common/CelAbstractSyntaxTree.java | 58 +++---- .../main/java/dev/cel/common/CelIssue.java | 3 +- .../common/CelProtoAbstractSyntaxTree.java | 2 +- .../CelProtoV1Alpha1AbstractSyntaxTree.java | 2 +- .../dev/cel/common/CelValidationResult.java | 16 +- .../src/test/java/dev/cel/common/BUILD.bazel | 2 - .../cel/common/CelAbstractSyntaxTreeTest.java | 2 +- .../main/java/dev/cel/compiler/BUILD.bazel | 1 - .../dev/cel/compiler/CelCompilerFactory.java | 8 +- .../dev/cel/compiler/CelCompilerImpl.java | 7 +- .../src/main/java/dev/cel/parser/BUILD.bazel | 1 - .../src/main/java/dev/cel/parser/Parser.java | 2 +- .../dev/cel/parser/CelUnparserImplTest.java | 3 +- validator/BUILD.bazel | 26 +++ .../main/java/dev/cel/validator/BUILD.bazel | 73 ++++++++ .../dev/cel/validator/CelAstValidator.java | 69 ++++++++ .../java/dev/cel/validator/CelValidator.java | 35 ++++ .../cel/validator/CelValidatorBuilder.java | 34 ++++ .../cel/validator/CelValidatorFactory.java | 47 ++++++ .../dev/cel/validator/CelValidatorImpl.java | 93 +++++++++++ .../dev/cel/validator/validators/BUILD.bazel | 24 +++ .../validators/TimestampLiteralValidator.java | 72 ++++++++ .../test/java/dev/cel/validator/BUILD.bazel | 30 ++++ .../validator/CelValidatorFactoryTest.java | 61 +++++++ .../cel/validator/CelValidatorImplTest.java | 127 ++++++++++++++ .../dev/cel/validator/validators/BUILD.bazel | 35 ++++ .../TimestampLiteralValidatorTest.java | 158 ++++++++++++++++++ validator/validators/BUILD.bazel | 9 + 32 files changed, 968 insertions(+), 48 deletions(-) create mode 100644 validator/BUILD.bazel create mode 100644 validator/src/main/java/dev/cel/validator/BUILD.bazel create mode 100644 validator/src/main/java/dev/cel/validator/CelAstValidator.java create mode 100644 validator/src/main/java/dev/cel/validator/CelValidator.java create mode 100644 validator/src/main/java/dev/cel/validator/CelValidatorBuilder.java create mode 100644 validator/src/main/java/dev/cel/validator/CelValidatorFactory.java create mode 100644 validator/src/main/java/dev/cel/validator/CelValidatorImpl.java create mode 100644 validator/src/main/java/dev/cel/validator/validators/BUILD.bazel create mode 100644 validator/src/main/java/dev/cel/validator/validators/TimestampLiteralValidator.java create mode 100644 validator/src/test/java/dev/cel/validator/BUILD.bazel create mode 100644 validator/src/test/java/dev/cel/validator/CelValidatorFactoryTest.java create mode 100644 validator/src/test/java/dev/cel/validator/CelValidatorImplTest.java create mode 100644 validator/src/test/java/dev/cel/validator/validators/BUILD.bazel create mode 100644 validator/src/test/java/dev/cel/validator/validators/TimestampLiteralValidatorTest.java create mode 100644 validator/validators/BUILD.bazel diff --git a/bundle/src/main/java/dev/cel/bundle/CelFactory.java b/bundle/src/main/java/dev/cel/bundle/CelFactory.java index b2a1cfdc..c69c4a3a 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelFactory.java +++ b/bundle/src/main/java/dev/cel/bundle/CelFactory.java @@ -16,7 +16,9 @@ import dev.cel.checker.CelCheckerLegacyImpl; import dev.cel.common.CelOptions; +import dev.cel.compiler.CelCompiler; import dev.cel.parser.CelParserImpl; +import dev.cel.runtime.CelRuntime; /** Helper class to configure the entire CEL stack in a common interface. */ public final class CelFactory { @@ -35,4 +37,9 @@ public static CelBuilder standardCelBuilder() { .setOptions(CelOptions.current().build()) .setStandardEnvironmentEnabled(true); } + + /** Combines a prebuilt {@link CelCompiler} and {@link CelRuntime} into {@link Cel}. */ + public static Cel combine(CelCompiler celCompiler, CelRuntime celRuntime) { + return CelImpl.combine(celCompiler, celRuntime); + } } diff --git a/bundle/src/main/java/dev/cel/bundle/CelImpl.java b/bundle/src/main/java/dev/cel/bundle/CelImpl.java index 40a4cea4..81680c86 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelImpl.java +++ b/bundle/src/main/java/dev/cel/bundle/CelImpl.java @@ -100,6 +100,11 @@ public void accept(EnvVisitor envVisitor) { } } + /** Combines a prebuilt {@link CelCompiler} and {@link CelRuntime} into {@link CelImpl}. */ + static CelImpl combine(CelCompiler compiler, CelRuntime runtime) { + return new CelImpl(Suppliers.memoize(() -> compiler), Suppliers.memoize(() -> runtime)); + } + /** * Create a new builder for constructing a {@code CelImpl} instance. * diff --git a/checker/src/main/java/dev/cel/checker/ExprChecker.java b/checker/src/main/java/dev/cel/checker/ExprChecker.java index a15c54c2..5c0c21ff 100644 --- a/checker/src/main/java/dev/cel/checker/ExprChecker.java +++ b/checker/src/main/java/dev/cel/checker/ExprChecker.java @@ -137,7 +137,8 @@ public static CelAbstractSyntaxTree typecheck( // by DYN. Map typeMap = Maps.transformValues(env.getTypeMap(), checker.inferenceContext::finalize); - return new CelAbstractSyntaxTree(expr, ast.getSource(), env.getRefMap(), typeMap); + + return CelAbstractSyntaxTree.newCheckedAst(expr, ast.getSource(), env.getRefMap(), typeMap); } private final Env env; diff --git a/common/src/main/java/dev/cel/common/BUILD.bazel b/common/src/main/java/dev/cel/common/BUILD.bazel index a77bbe4b..dd39523f 100644 --- a/common/src/main/java/dev/cel/common/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/BUILD.bazel @@ -108,7 +108,6 @@ java_library( ], deps = [ ":common", - "//:auto_value", "//common/ast:expr_v1alpha1_converter", "//common/types:cel_v1alpha1_types", "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", diff --git a/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java b/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java index 9f251323..0f22a041 100644 --- a/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java +++ b/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java @@ -14,8 +14,6 @@ package dev.cel.common; -import static com.google.common.collect.ImmutableMap.toImmutableMap; - import dev.cel.expr.CheckedExpr; import dev.cel.expr.Expr; import dev.cel.expr.ParsedExpr; @@ -28,13 +26,11 @@ import dev.cel.common.annotations.Internal; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; -import dev.cel.common.ast.CelExprConverter; import dev.cel.common.ast.CelReference; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypes; import dev.cel.common.types.SimpleType; import java.util.Map; -import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Optional; @@ -56,46 +52,44 @@ public final class CelAbstractSyntaxTree { private final ImmutableMap types; - /** Internal: Consumers should not be creating an instance of this class directly. */ - @Internal - public CelAbstractSyntaxTree(CelExpr celExpr, CelSource celSource) { - this(celExpr, celSource, ImmutableMap.of(), ImmutableMap.of()); + /** + * Constructs a new instance of CelAbstractSyntaxTree that represent a parsed expression. + * + *

Note that ASTs should not be manually constructed except for special circumstances such as + * validating or optimizing an AST. + */ + public static CelAbstractSyntaxTree newParsedAst(CelExpr celExpr, CelSource celSource) { + return new CelAbstractSyntaxTree(celExpr, celSource); } - /** Internal: Consumers should not be creating an instance of this class directly. */ + /** + * Constructs a new instance of CelAbstractSyntaxTree that represent a checked expression. + * + *

CEL Library Internals. Do not construct a type-checked AST by hand. Use a CelCompiler to + * type-check a parsed AST instead. + */ @Internal - public CelAbstractSyntaxTree( + public static CelAbstractSyntaxTree newCheckedAst( CelExpr celExpr, CelSource celSource, Map references, Map types) { - this.celExpr = celExpr; - this.celSource = celSource; - this.references = ImmutableMap.copyOf(references); - this.types = ImmutableMap.copyOf(types); + return new CelAbstractSyntaxTree(celExpr, celSource, references, types); } - CelAbstractSyntaxTree(ParsedExpr parsedExpr, CelSource celSource) { - this( - CheckedExpr.newBuilder() - .setExpr(parsedExpr.getExpr()) - .setSourceInfo(parsedExpr.getSourceInfo()) - .build(), - celSource); + private CelAbstractSyntaxTree(CelExpr celExpr, CelSource celSource) { + this(celExpr, celSource, ImmutableMap.of(), ImmutableMap.of()); } - CelAbstractSyntaxTree(CheckedExpr checkedExpr, CelSource celSource) { - this.celExpr = CelExprConverter.fromExpr(checkedExpr.getExpr()); + private CelAbstractSyntaxTree( + CelExpr celExpr, + CelSource celSource, + Map references, + Map types) { + this.celExpr = celExpr; this.celSource = celSource; - this.references = - checkedExpr.getReferenceMapMap().entrySet().stream() - .collect( - toImmutableMap( - Entry::getKey, - v -> CelExprConverter.exprReferenceToCelReference(v.getValue()))); - this.types = - checkedExpr.getTypeMapMap().entrySet().stream() - .collect(toImmutableMap(Entry::getKey, v -> CelTypes.typeToCelType(v.getValue()))); + this.references = ImmutableMap.copyOf(references); + this.types = ImmutableMap.copyOf(types); } /** diff --git a/common/src/main/java/dev/cel/common/CelIssue.java b/common/src/main/java/dev/cel/common/CelIssue.java index 92a77753..4ad48cea 100644 --- a/common/src/main/java/dev/cel/common/CelIssue.java +++ b/common/src/main/java/dev/cel/common/CelIssue.java @@ -29,9 +29,10 @@ public abstract class CelIssue { /** Severity of a CelIssue. */ - public static enum Severity { + public enum Severity { ERROR, WARNING, + INFORMATION, DEPRECATED; } diff --git a/common/src/main/java/dev/cel/common/CelProtoAbstractSyntaxTree.java b/common/src/main/java/dev/cel/common/CelProtoAbstractSyntaxTree.java index 55527dd0..fa0719e8 100644 --- a/common/src/main/java/dev/cel/common/CelProtoAbstractSyntaxTree.java +++ b/common/src/main/java/dev/cel/common/CelProtoAbstractSyntaxTree.java @@ -41,7 +41,7 @@ public final class CelProtoAbstractSyntaxTree { private CelProtoAbstractSyntaxTree(CheckedExpr checkedExpr) { this.checkedExpr = checkedExpr; this.ast = - new CelAbstractSyntaxTree( + CelAbstractSyntaxTree.newCheckedAst( CelExprConverter.fromExpr(checkedExpr.getExpr()), CelSource.newBuilder() .addAllLineOffsets(checkedExpr.getSourceInfo().getLineOffsetsList()) diff --git a/common/src/main/java/dev/cel/common/CelProtoV1Alpha1AbstractSyntaxTree.java b/common/src/main/java/dev/cel/common/CelProtoV1Alpha1AbstractSyntaxTree.java index 38d32635..80fa58f3 100644 --- a/common/src/main/java/dev/cel/common/CelProtoV1Alpha1AbstractSyntaxTree.java +++ b/common/src/main/java/dev/cel/common/CelProtoV1Alpha1AbstractSyntaxTree.java @@ -44,7 +44,7 @@ public final class CelProtoV1Alpha1AbstractSyntaxTree { private CelProtoV1Alpha1AbstractSyntaxTree(CheckedExpr checkedExpr) { this.checkedExpr = checkedExpr; this.ast = - new CelAbstractSyntaxTree( + CelAbstractSyntaxTree.newCheckedAst( CelExprV1Alpha1Converter.fromExpr(checkedExpr.getExpr()), CelSource.newBuilder() .addAllLineOffsets(checkedExpr.getSourceInfo().getLineOffsetsList()) diff --git a/common/src/main/java/dev/cel/common/CelValidationResult.java b/common/src/main/java/dev/cel/common/CelValidationResult.java index c172dbae..a7e6eceb 100644 --- a/common/src/main/java/dev/cel/common/CelValidationResult.java +++ b/common/src/main/java/dev/cel/common/CelValidationResult.java @@ -22,6 +22,7 @@ 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; @@ -112,11 +113,22 @@ public ImmutableList getAllIssues() { return issues; } - /** Convert the {@code CelIssue} set to a debug string. */ - public String getDebugString() { + /** Convert all issues to a human-readable string. */ + public String getIssueString() { return JOINER.join(Iterables.transform(issues, iss -> iss.toDisplayString(source))); } + /** + * Convert the {@code CelIssue} set to a debug string. + * + * @deprecated Use {@link #getIssueString()} instead. + */ + @Deprecated + @InlineMe(replacement = "this.getIssueString()") + public String getDebugString() { + return getIssueString(); + } + /** 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))); diff --git a/common/src/test/java/dev/cel/common/BUILD.bazel b/common/src/test/java/dev/cel/common/BUILD.bazel index af0e8b82..c565f5bc 100644 --- a/common/src/test/java/dev/cel/common/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/BUILD.bazel @@ -13,11 +13,9 @@ java_library( "//common", "//common:features", "//common:options", - "//common:proto_ast", "//common:proto_v1alpha1_ast", "//common/ast", "//common/internal", - "//common/resources/testdata/proto3:test_all_types_java_proto", "//common/types", "//common/types:cel_types", "//common/types:cel_v1alpha1_types", diff --git a/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java b/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java index 3f83badb..bb35a061 100644 --- a/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java +++ b/common/src/test/java/dev/cel/common/CelAbstractSyntaxTreeTest.java @@ -190,7 +190,7 @@ public void parsedExpression_createAst() { .addLineOffsets(10) .build(); - CelAbstractSyntaxTree ast = new CelAbstractSyntaxTree(celExpr, celSource); + CelAbstractSyntaxTree ast = CelAbstractSyntaxTree.newParsedAst(celExpr, celSource); assertThat(ast).isNotNull(); assertThat(ast.getExpr()).isEqualTo(celExpr); diff --git a/compiler/src/main/java/dev/cel/compiler/BUILD.bazel b/compiler/src/main/java/dev/cel/compiler/BUILD.bazel index fa730560..fc20e817 100644 --- a/compiler/src/main/java/dev/cel/compiler/BUILD.bazel +++ b/compiler/src/main/java/dev/cel/compiler/BUILD.bazel @@ -28,7 +28,6 @@ java_library( ], deps = [ ":compiler_builder", - "//:auto_value", "//checker", "//checker:checker_builder", "//checker:checker_legacy_environment", diff --git a/compiler/src/main/java/dev/cel/compiler/CelCompilerFactory.java b/compiler/src/main/java/dev/cel/compiler/CelCompilerFactory.java index 7f6ae447..dbeaeba0 100644 --- a/compiler/src/main/java/dev/cel/compiler/CelCompilerFactory.java +++ b/compiler/src/main/java/dev/cel/compiler/CelCompilerFactory.java @@ -14,9 +14,11 @@ package dev.cel.compiler; +import dev.cel.checker.CelChecker; import dev.cel.checker.CelCheckerBuilder; import dev.cel.checker.CelCheckerLegacyImpl; import dev.cel.common.CelOptions; +import dev.cel.parser.CelParser; import dev.cel.parser.CelParserImpl; /** Factory class for creating builders for type-checker and compiler instances. */ @@ -41,11 +43,15 @@ public static CelCheckerBuilder standardCelCheckerBuilder() { * configured by default. */ public static CelCompilerBuilder standardCelCompilerBuilder() { - return CelCompilerImpl.newBuilder(CelParserImpl.newBuilder(), CelCheckerLegacyImpl.newBuilder()) .setOptions(CelOptions.current().build()) .setStandardEnvironmentEnabled(true); } + /** Combines a prebuilt {@link CelParser} and {@link CelChecker} into {@link CelCompiler}. */ + public static CelCompiler combine(CelParser celParser, CelChecker celChecker) { + return CelCompilerImpl.combine(celParser, celChecker); + } + private CelCompilerFactory() {} } diff --git a/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java b/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java index 23e23eeb..9eb70841 100644 --- a/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java +++ b/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java @@ -48,7 +48,7 @@ /** * CelCompiler implementation which uses either the legacy or modernized CEL-Java stack to offer a - * stream-lined expression parse/type-check experience, via a single {@code compile} method. * + * stream-lined expression parse/type-check experience, via a single {@code compile} method. * *

CEL Library Internals. Do Not Use. Consumers should use factories, such as {@link * CelCompilerFactory} instead to instantiate a compiler. @@ -82,6 +82,11 @@ public void accept(EnvVisitor envVisitor) { } } + /** Combines a prebuilt {@link CelParser} and {@link CelChecker} into {@link CelCompilerImpl}. */ + static CelCompilerImpl combine(CelParser parser, CelChecker checker) { + return new CelCompilerImpl(parser, checker); + } + /** * Create a new builder for constructing a {@code CelCompiler} instance. * diff --git a/parser/src/main/java/dev/cel/parser/BUILD.bazel b/parser/src/main/java/dev/cel/parser/BUILD.bazel index c1e62df3..f4c5c70d 100644 --- a/parser/src/main/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/main/java/dev/cel/parser/BUILD.bazel @@ -47,7 +47,6 @@ java_library( ":macro", ":operator", ":parser_builder", - "//:auto_value", "//common", "//common:compiler_common", "//common:options", diff --git a/parser/src/main/java/dev/cel/parser/Parser.java b/parser/src/main/java/dev/cel/parser/Parser.java index b2e9cc08..07512ba2 100644 --- a/parser/src/main/java/dev/cel/parser/Parser.java +++ b/parser/src/main/java/dev/cel/parser/Parser.java @@ -162,7 +162,7 @@ static CelValidationResult parse(CelParserImpl parser, CelSource source, CelOpti sourceInfo.build(), parseFailure, ImmutableList.copyOf(exprFactory.getIssuesList())); } return new CelValidationResult( - new CelAbstractSyntaxTree(expr, sourceInfo.build()), + CelAbstractSyntaxTree.newParsedAst(expr, sourceInfo.build()), ImmutableList.copyOf(exprFactory.getIssuesList())); } diff --git a/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java b/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java index 610843b0..43dd0138 100644 --- a/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java +++ b/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java @@ -228,7 +228,8 @@ public void unparse_fails( Throwable.class, () -> unparser.unparse( - new CelAbstractSyntaxTree(invalidExpr, CelSource.newBuilder().build()))); + CelAbstractSyntaxTree.newParsedAst( + invalidExpr, CelSource.newBuilder().build()))); assertThat(thrown).hasMessageThat().contains("unexpected"); } diff --git a/validator/BUILD.bazel b/validator/BUILD.bazel new file mode 100644 index 00000000..fbf29028 --- /dev/null +++ b/validator/BUILD.bazel @@ -0,0 +1,26 @@ +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//visibility:public"], # TODO: Expose when ready +) + +java_library( + name = "validator", + exports = ["//validator/src/main/java/dev/cel/validator"], +) + +java_library( + name = "validator_builder", + exports = ["//validator/src/main/java/dev/cel/validator:validator_builder"], +) + +java_library( + name = "ast_validator", + exports = ["//validator/src/main/java/dev/cel/validator:ast_validator"], +) + +java_library( + name = "validator_impl", + testonly = 1, + visibility = ["//validator/src/test/java/dev/cel/validator:__pkg__"], + exports = ["//validator/src/main/java/dev/cel/validator:validator_impl"], +) diff --git a/validator/src/main/java/dev/cel/validator/BUILD.bazel b/validator/src/main/java/dev/cel/validator/BUILD.bazel new file mode 100644 index 00000000..4ffe9ec0 --- /dev/null +++ b/validator/src/main/java/dev/cel/validator/BUILD.bazel @@ -0,0 +1,73 @@ +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//validator:__pkg__", + ], +) + +java_library( + name = "validator", + srcs = [ + "CelValidatorFactory.java", + ], + tags = [ + ], + deps = [ + ":validator_builder", + ":validator_impl", + "//bundle:cel", + "//checker:checker_builder", + "//compiler", + "//compiler:compiler_builder", + "//parser:parser_builder", + "//runtime", + ], +) + +java_library( + name = "validator_builder", + srcs = [ + "CelValidator.java", + "CelValidatorBuilder.java", + ], + tags = [ + ], + deps = [ + ":ast_validator", + "//common", + "//common:compiler_common", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "validator_impl", + srcs = [ + "CelValidatorImpl.java", + ], + tags = [ + ], + deps = [ + ":ast_validator", + ":validator_builder", + "//bundle:cel", + "//common", + "//common:compiler_common", + "//common/navigation", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "ast_validator", + srcs = ["CelAstValidator.java"], + tags = [ + ], + deps = [ + "//bundle:cel", + "//common", + "//common:compiler_common", + "//common/navigation", + "@maven//:com_google_guava_guava", + ], +) diff --git a/validator/src/main/java/dev/cel/validator/CelAstValidator.java b/validator/src/main/java/dev/cel/validator/CelAstValidator.java new file mode 100644 index 00000000..dbd28962 --- /dev/null +++ b/validator/src/main/java/dev/cel/validator/CelAstValidator.java @@ -0,0 +1,69 @@ +// Copyright 2023 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.validator; + +import com.google.common.collect.ImmutableList; +import dev.cel.bundle.Cel; +import dev.cel.common.CelIssue; +import dev.cel.common.CelIssue.Severity; +import dev.cel.common.CelSource; +import dev.cel.common.navigation.CelNavigableAst; + +/** Public interface for performing a single, custom validation on an AST. */ +public interface CelAstValidator { + + void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory); + + /** Factory for populating issues while performing AST validation. */ + final class IssuesFactory { + private final ImmutableList.Builder issuesBuilder; + private final CelNavigableAst navigableAst; + + IssuesFactory(CelNavigableAst navigableAst) { + this.navigableAst = navigableAst; + this.issuesBuilder = ImmutableList.builder(); + } + + /** Adds an error for the expression. */ + public void addError(long exprId, String message) { + add(exprId, message, Severity.ERROR); + } + + /** Adds a warning for the expression. */ + public void addWarning(long exprId, String message) { + add(exprId, message, Severity.WARNING); + } + + /** Adds an info for the expression. */ + public void addInfo(long exprId, String message) { + add(exprId, message, Severity.INFORMATION); + } + + private void add(long exprId, String message, Severity severity) { + CelSource source = navigableAst.getAst().getSource(); + int position = source.getPositionsMap().get(exprId); + issuesBuilder.add( + CelIssue.newBuilder() + .setSeverity(severity) + .setMessage(message) + .setSourceLocation(source.getOffsetLocation(position).get()) + .build()); + } + + public ImmutableList getIssues() { + return issuesBuilder.build(); + } + } +} diff --git a/validator/src/main/java/dev/cel/validator/CelValidator.java b/validator/src/main/java/dev/cel/validator/CelValidator.java new file mode 100644 index 00000000..88c83b8c --- /dev/null +++ b/validator/src/main/java/dev/cel/validator/CelValidator.java @@ -0,0 +1,35 @@ +// Copyright 2023 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.validator; + +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationResult; + +/** Public interface for validating an AST. */ +public interface CelValidator { + + /** + * Performs custom validation of the provided AST. + * + *

This invokes all the AST validators present in this CelValidator instance via {@link + * CelValidatorBuilder#addAstValidators} in their added order. Any exceptions thrown within the + * AST validator will be propagated to the caller and will abort the validation process. + * + *

The result of the validation is bundled inside {@link CelValidationResult}. + * + * @param ast A type-checked AST. + */ + CelValidationResult validate(CelAbstractSyntaxTree ast); +} diff --git a/validator/src/main/java/dev/cel/validator/CelValidatorBuilder.java b/validator/src/main/java/dev/cel/validator/CelValidatorBuilder.java new file mode 100644 index 00000000..b4acd105 --- /dev/null +++ b/validator/src/main/java/dev/cel/validator/CelValidatorBuilder.java @@ -0,0 +1,34 @@ +// Copyright 2023 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.validator; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.CheckReturnValue; + +/** Interface for building an instance of CelValidator. */ +public interface CelValidatorBuilder { + + /** Adds one or more validator to perform custom AST validation. */ + @CanIgnoreReturnValue + CelValidatorBuilder addAstValidators(CelAstValidator... astValidators); + + /** Adds one or more validator to perform custom AST validation. */ + @CanIgnoreReturnValue + CelValidatorBuilder addAstValidators(Iterable astValidators); + + /** Build a new instance of the {@link CelValidator}. */ + @CheckReturnValue + CelValidator build(); +} diff --git a/validator/src/main/java/dev/cel/validator/CelValidatorFactory.java b/validator/src/main/java/dev/cel/validator/CelValidatorFactory.java new file mode 100644 index 00000000..3569bcec --- /dev/null +++ b/validator/src/main/java/dev/cel/validator/CelValidatorFactory.java @@ -0,0 +1,47 @@ +// Copyright 2023 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.validator; + +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 constructing an {@link CelValidator} instance. */ +public final class CelValidatorFactory { + + /** Create a new builder for constructing a {@link CelValidator} instance. */ + public static CelValidatorBuilder standardCelValidatorBuilder(Cel cel) { + return CelValidatorImpl.newBuilder(cel); + } + + /** Create a new builder for constructing a {@link CelValidator} instance. */ + public static CelValidatorBuilder standardCelValidatorBuilder( + CelCompiler celCompiler, CelRuntime celRuntime) { + return standardCelValidatorBuilder(CelFactory.combine(celCompiler, celRuntime)); + } + + /** Create a new builder for constructing a {@link CelValidator} instance. */ + public static CelValidatorBuilder standardCelValidatorBuilder( + CelParser celParser, CelChecker celChecker, CelRuntime celRuntime) { + return standardCelValidatorBuilder( + CelCompilerFactory.combine(celParser, celChecker), celRuntime); + } + + private CelValidatorFactory() {} +} diff --git a/validator/src/main/java/dev/cel/validator/CelValidatorImpl.java b/validator/src/main/java/dev/cel/validator/CelValidatorImpl.java new file mode 100644 index 00000000..971868c8 --- /dev/null +++ b/validator/src/main/java/dev/cel/validator/CelValidatorImpl.java @@ -0,0 +1,93 @@ +// Copyright 2023 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.validator; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelIssue; +import dev.cel.common.CelValidationResult; +import dev.cel.common.navigation.CelNavigableAst; +import dev.cel.validator.CelAstValidator.IssuesFactory; +import java.util.Arrays; + +/** + * {@link CelValidator} implementation to handle AST validation. An instance should be created using + * {@link CelValidatorFactory}. + */ +final class CelValidatorImpl implements CelValidator { + private final Cel cel; + private final ImmutableSet astValidators; + + CelValidatorImpl(Cel cel, ImmutableSet astValidators) { + this.cel = cel; + this.astValidators = astValidators; + } + + @Override + public CelValidationResult validate(CelAbstractSyntaxTree ast) { + if (!ast.isChecked()) { + throw new IllegalArgumentException("AST must be type-checked."); + } + + ImmutableList.Builder issueBuilder = ImmutableList.builder(); + + for (CelAstValidator validator : astValidators) { + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + IssuesFactory issuesFactory = new IssuesFactory(navigableAst); + validator.validate(navigableAst, cel, issuesFactory); + issueBuilder.addAll(issuesFactory.getIssues()); + } + + return new CelValidationResult(ast, issueBuilder.build()); + } + + /** Create a new builder for constructing a {@link CelValidator} instance. */ + static Builder newBuilder(Cel cel) { + return new Builder(cel); + } + + /** Builder class for {@link CelValidatorImpl}. */ + static final class Builder implements CelValidatorBuilder { + private final Cel cel; + private final ImmutableSet.Builder astValidators; + + private Builder(Cel cel) { + this.cel = cel; + this.astValidators = ImmutableSet.builder(); + } + + @Override + public CelValidatorBuilder addAstValidators(CelAstValidator... astValidators) { + checkNotNull(astValidators); + return addAstValidators(Arrays.asList(astValidators)); + } + + @Override + public CelValidatorBuilder addAstValidators(Iterable astValidators) { + checkNotNull(astValidators); + this.astValidators.addAll(astValidators); + return this; + } + + @Override + public CelValidator build() { + return new CelValidatorImpl(cel, astValidators.build()); + } + } +} diff --git a/validator/src/main/java/dev/cel/validator/validators/BUILD.bazel b/validator/src/main/java/dev/cel/validator/validators/BUILD.bazel new file mode 100644 index 00000000..406dd7b0 --- /dev/null +++ b/validator/src/main/java/dev/cel/validator/validators/BUILD.bazel @@ -0,0 +1,24 @@ +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//validator/validators:__pkg__", + ], +) + +java_library( + name = "timestamp", + srcs = [ + "TimestampLiteralValidator.java", + ], + tags = [ + ], + deps = [ + "//bundle:cel", + "//common", + "//common/ast", + "//common/navigation", + "//validator:ast_validator", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) diff --git a/validator/src/main/java/dev/cel/validator/validators/TimestampLiteralValidator.java b/validator/src/main/java/dev/cel/validator/validators/TimestampLiteralValidator.java new file mode 100644 index 00000000..6160908b --- /dev/null +++ b/validator/src/main/java/dev/cel/validator/validators/TimestampLiteralValidator.java @@ -0,0 +1,72 @@ +// Copyright 2023 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.validator.validators; + +import com.google.common.collect.ImmutableList; +import com.google.protobuf.Timestamp; +import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelSource; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.navigation.CelNavigableAst; +import dev.cel.common.navigation.CelNavigableExpr; +import dev.cel.validator.CelAstValidator; +import java.util.Optional; + +/** TimestampValidator ensures that timestamp literal arguments are valid. */ +public class TimestampLiteralValidator implements CelAstValidator { + public static final TimestampLiteralValidator INSTANCE = new TimestampLiteralValidator(); + + @Override + public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) { + navigableAst + .getRoot() + .descendants() + .filter( + node -> + node.getKind().equals(Kind.CONSTANT) + && node.parent().isPresent() + && node.parent().get().expr().call().function().equals("timestamp")) + .map(CelNavigableExpr::expr) + .forEach( + timestampExpr -> { + try { + CelExpr timestampCall = + CelExpr.ofCallExpr( + 1, + Optional.empty(), + "timestamp", + ImmutableList.of(CelExpr.ofConstantExpr(2, timestampExpr.constant()))); + + CelAbstractSyntaxTree ast = + CelAbstractSyntaxTree.newParsedAst( + timestampCall, CelSource.newBuilder().build()); + ast = cel.check(ast).getAst(); + Object result = cel.createProgram(ast).eval(); + + if (!(result instanceof Timestamp)) { + throw new IllegalStateException( + "Expected timestamp type but got " + result.getClass()); + } + } catch (Exception e) { + issuesFactory.addError( + timestampExpr.id(), "Timestamp validation failed. Reason: " + e.getMessage()); + } + }); + } + + private TimestampLiteralValidator() {} +} diff --git a/validator/src/test/java/dev/cel/validator/BUILD.bazel b/validator/src/test/java/dev/cel/validator/BUILD.bazel new file mode 100644 index 00000000..f6f94f62 --- /dev/null +++ b/validator/src/test/java/dev/cel/validator/BUILD.bazel @@ -0,0 +1,30 @@ +load("//:testing.bzl", "junit4_test_suites") + +package(default_applicable_licenses = ["//:license"]) + +java_library( + name = "tests", + testonly = 1, + srcs = glob(["*.java"]), + deps = [ + "//:java_truth", + "//bundle:cel", + "//common:compiler_common", + "//compiler", + "//parser", + "//runtime", + "//validator", + "//validator:validator_builder", + "//validator:validator_impl", + "@maven//:junit_junit", + ], +) + +junit4_test_suites( + name = "test_suites", + sizes = [ + "small", + ], + src_dir = "src/test/java", + deps = [":tests"], +) diff --git a/validator/src/test/java/dev/cel/validator/CelValidatorFactoryTest.java b/validator/src/test/java/dev/cel/validator/CelValidatorFactoryTest.java new file mode 100644 index 00000000..46ad1fc5 --- /dev/null +++ b/validator/src/test/java/dev/cel/validator/CelValidatorFactoryTest.java @@ -0,0 +1,61 @@ +// Copyright 2023 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.validator; + +import static com.google.common.truth.Truth.assertThat; + +import dev.cel.bundle.CelFactory; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelParserFactory; +import dev.cel.runtime.CelRuntimeFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CelValidatorFactoryTest { + + @Test + public void standardCelValidatorBuilder_withParserCheckerAndRuntime() { + CelValidatorBuilder builder = + CelValidatorFactory.standardCelValidatorBuilder( + CelParserFactory.standardCelParserBuilder().build(), + CelCompilerFactory.standardCelCheckerBuilder().build(), + CelRuntimeFactory.standardCelRuntimeBuilder().build()); + + assertThat(builder).isNotNull(); + assertThat(builder.build()).isNotNull(); + } + + @Test + public void standardCelValidatorBuilder_withCompilerAndRuntime() { + CelValidatorBuilder builder = + CelValidatorFactory.standardCelValidatorBuilder( + CelCompilerFactory.standardCelCompilerBuilder().build(), + CelRuntimeFactory.standardCelRuntimeBuilder().build()); + + assertThat(builder).isNotNull(); + assertThat(builder.build()).isNotNull(); + } + + @Test + public void standardCelValidatorBuilder_withCel() { + CelValidatorBuilder builder = + CelValidatorFactory.standardCelValidatorBuilder(CelFactory.standardCelBuilder().build()); + + assertThat(builder).isNotNull(); + assertThat(builder.build()).isNotNull(); + } +} diff --git a/validator/src/test/java/dev/cel/validator/CelValidatorImplTest.java b/validator/src/test/java/dev/cel/validator/CelValidatorImplTest.java new file mode 100644 index 00000000..495b10e9 --- /dev/null +++ b/validator/src/test/java/dev/cel/validator/CelValidatorImplTest.java @@ -0,0 +1,127 @@ +// Copyright 2023 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.validator; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelIssue.Severity; +import dev.cel.common.CelValidationResult; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CelValidatorImplTest { + private static final Cel CEL = CelFactory.standardCelBuilder().build(); + + // private static final CelValidatorImpl CEL_VALIDATOR = new CelValidatorImpl(CEL, + + @Test + public void constructCelValidator_success() { + CelValidator celValidator = + CelValidatorImpl.newBuilder(CEL) + .addAstValidators( + (navigableAst, cel, issuesFactory) -> { + // no-op + }) + .build(); + + assertThat(celValidator).isNotNull(); + assertThat(celValidator).isInstanceOf(CelValidatorImpl.class); + } + + @Test + public void validator_inOrder() throws Exception { + List list = new ArrayList<>(); + CelValidator celValidator = + CelValidatorImpl.newBuilder(CEL) + .addAstValidators((navigableAst, cel, issuesFactory) -> list.add(1)) + .addAstValidators((navigableAst, cel, issuesFactory) -> list.add(2)) + .addAstValidators((navigableAst, cel, issuesFactory) -> list.add(3)) + .build(); + + CelValidationResult result = celValidator.validate(CEL.compile("'test'").getAst()); + + assertThat(result.hasError()).isFalse(); + assertThat(list).containsExactly(1, 2, 3).inOrder(); + } + + @Test + public void validator_whenAstValidatorThrows_throwsException() { + CelValidator celValidator = + CelValidatorImpl.newBuilder(CEL) + .addAstValidators( + (navigableAst, cel, issuesFactory) -> issuesFactory.addError(1, "Test error")) + .addAstValidators( + (navigableAst, cel, issuesFactory) -> { + throw new IllegalArgumentException("Test exception"); + }) + .build(); + + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> celValidator.validate(CEL.compile("'test'").getAst())); + assertThat(e).hasMessageThat().contains("Test exception"); + } + + @Test + public void validator_addIssueWithAllSeverities() throws Exception { + CelValidator celValidator = + CelValidatorImpl.newBuilder(CEL) + .addAstValidators( + (navigableAst, cel, issuesFactory) -> issuesFactory.addError(1, "Test error")) + .addAstValidators( + (navigableAst, cel, issuesFactory) -> issuesFactory.addWarning(1, "Test warning")) + .addAstValidators( + (navigableAst, cel, issuesFactory) -> issuesFactory.addInfo(1, "Test info")) + .build(); + + CelValidationResult result = celValidator.validate(CEL.compile("'test'").getAst()); + + assertThat(result.hasError()).isTrue(); + assertThat(result.getAllIssues()).hasSize(3); + assertThat(result.getAllIssues().get(0).getSeverity()).isEqualTo(Severity.ERROR); + assertThat(result.getAllIssues().get(1).getSeverity()).isEqualTo(Severity.WARNING); + assertThat(result.getAllIssues().get(2).getSeverity()).isEqualTo(Severity.INFORMATION); + assertThat(result.getIssueString()) + .contains( + "ERROR: :1:1: Test error\n" + + " | 'test'\n" + + " | ^\n" + + "WARNING: :1:1: Test warning\n" + + " | 'test'\n" + + " | ^\n" + + "INFORMATION: :1:1: Test info\n" + + " | 'test'\n" + + " | ^"); + } + + @Test + public void parsedAst_throwsException() throws Exception { + CelValidator celValidator = CelValidatorImpl.newBuilder(CEL).build(); + + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> celValidator.validate(CEL.parse("'test'").getAst())); + assertThat(e).hasMessageThat().contains("AST must be type-checked."); + } +} diff --git a/validator/src/test/java/dev/cel/validator/validators/BUILD.bazel b/validator/src/test/java/dev/cel/validator/validators/BUILD.bazel new file mode 100644 index 00000000..41da55ca --- /dev/null +++ b/validator/src/test/java/dev/cel/validator/validators/BUILD.bazel @@ -0,0 +1,35 @@ +load("//:testing.bzl", "junit4_test_suites") + +package(default_applicable_licenses = ["//:license"]) + +java_library( + name = "tests", + testonly = 1, + srcs = glob(["*.java"]), + deps = [ + "//:java_truth", + "//bundle:cel", + "//common", + "//common:compiler_common", + "//common:options", + "//common/types", + "//runtime", + "//validator", + "//validator:validator_builder", + "//validator/validators:timestamp", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_protobuf_protobuf_java_util", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +junit4_test_suites( + name = "test_suites", + sizes = [ + "small", + ], + src_dir = "src/test/java", + deps = [":tests"], +) diff --git a/validator/src/test/java/dev/cel/validator/validators/TimestampLiteralValidatorTest.java b/validator/src/test/java/dev/cel/validator/validators/TimestampLiteralValidatorTest.java new file mode 100644 index 00000000..b2a30ee4 --- /dev/null +++ b/validator/src/test/java/dev/cel/validator/validators/TimestampLiteralValidatorTest.java @@ -0,0 +1,158 @@ +// Copyright 2023 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.validator.validators; + +import static com.google.common.truth.Truth.assertThat; +import static dev.cel.common.CelFunctionDecl.newFunctionDeclaration; +import static dev.cel.common.CelOverloadDecl.newGlobalOverload; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Timestamp; +import com.google.protobuf.util.Timestamps; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelIssue.Severity; +import dev.cel.common.CelOptions; +import dev.cel.common.CelValidationResult; +import dev.cel.common.types.SimpleType; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime.CelFunctionBinding; +import dev.cel.validator.CelValidator; +import dev.cel.validator.CelValidatorFactory; +import java.text.ParseException; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class TimestampLiteralValidatorTest { + private static final CelOptions CEL_OPTIONS = + CelOptions.current().enableTimestampEpoch(true).build(); + + private static final Cel CEL = CelFactory.standardCelBuilder().setOptions(CEL_OPTIONS).build(); + + private static final CelValidator CEL_VALIDATOR = + CelValidatorFactory.standardCelValidatorBuilder(CEL) + .addAstValidators(TimestampLiteralValidator.INSTANCE) + .build(); + + @Test + @TestParameters("{source: timestamp(0)}") + @TestParameters("{source: timestamp(1624124124)}") + @TestParameters("{source: timestamp('2021-06-19T17:35:24Z')}") + @TestParameters("{source: timestamp('1972-01-01T10:00:20.021-05:00')}") + public void timestamp_validFormat(String source) throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(source).getAst(); + + CelValidationResult result = CEL_VALIDATOR.validate(ast); + + assertThat(result.hasError()).isFalse(); + assertThat(result.getAllIssues()).isEmpty(); + assertThat(CEL.createProgram(ast).eval()).isInstanceOf(Timestamp.class); + } + + @Test + public void timestampsInCallArgument_validFormat() throws Exception { + String source = + "string(timestamp(1524124124)) + ':' + string(timestamp('2021-06-19T17:35:24Z'))"; + CelAbstractSyntaxTree ast = CEL.compile(source).getAst(); + + CelValidationResult result = CEL_VALIDATOR.validate(ast); + + assertThat(result.hasError()).isFalse(); + assertThat(result.getAllIssues()).isEmpty(); + assertThat(CEL.createProgram(ast).eval()).isInstanceOf(String.class); + } + + @Test + public void timestamp_withVariable_noOp() throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .addVar("str_var", SimpleType.STRING) + .setOptions(CEL_OPTIONS) + .build(); + CelAbstractSyntaxTree ast = cel.compile("timestamp(str_var)").getAst(); + + CelValidationResult result = CEL_VALIDATOR.validate(ast); + + // Static AST validation cannot handle variables. This expectedly contains no errors + assertThat(result.hasError()).isFalse(); + assertThat(result.getAllIssues()).isEmpty(); + // However, the same AST fails on evaluation when a bad variable is passed in. + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, + () -> CEL.createProgram(ast).eval(ImmutableMap.of("str_var", "bad"))); + assertThat(e) + .hasMessageThat() + .contains("evaluation error: Failed to parse timestamp: invalid timestamp \"bad\""); + } + + @Test + public void timestamp_withFunction_noOp() throws Exception { + Cel cel = + CelFactory.standardCelBuilder() + .addFunctionDeclarations( + newFunctionDeclaration( + "testFunc", + newGlobalOverload("testFuncOverloadId", SimpleType.INT, SimpleType.STRING))) + .addFunctionBindings( + CelFunctionBinding.from( + "testFuncOverloadId", + String.class, + stringArg -> { + try { + return Timestamps.parse(stringArg).getSeconds(); + } catch (ParseException e) { + throw new RuntimeException(e); + } + })) + .setOptions(CEL_OPTIONS) + .build(); + CelAbstractSyntaxTree ast = cel.compile("timestamp(testFunc('bad'))").getAst(); + + CelValidationResult result = CEL_VALIDATOR.validate(ast); + + // Static AST validation cannot handle functions. This expectedly contains no errors + assertThat(result.hasError()).isFalse(); + assertThat(result.getAllIssues()).isEmpty(); + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); + // However, the same AST fails on evaluation when the function dispatch fails. + assertThat(e) + .hasMessageThat() + .contains("evaluation error: Function 'testFuncOverloadId' failed with arg(s) 'bad'"); + } + + @Test + public void timestamp_invalidFormat() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("timestamp('bad')").getAst(); + + CelValidationResult result = CEL_VALIDATOR.validate(ast); + + assertThat(result.hasError()).isTrue(); + assertThat(result.getAllIssues()).hasSize(1); + assertThat(result.getAllIssues().get(0).getSeverity()).isEqualTo(Severity.ERROR); + assertThat(result.getAllIssues().get(0).toDisplayString(ast.getSource())) + .isEqualTo( + "ERROR: :1:11: Timestamp validation failed. Reason: evaluation error: Failed to" + + " parse timestamp: invalid timestamp \"bad\"\n" + + " | timestamp('bad')\n" + + " | ..........^"); + } +} diff --git a/validator/validators/BUILD.bazel b/validator/validators/BUILD.bazel new file mode 100644 index 00000000..93a2abdb --- /dev/null +++ b/validator/validators/BUILD.bazel @@ -0,0 +1,9 @@ +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//visibility:public"], # TODO: Expose when ready +) + +java_library( + name = "timestamp", + exports = ["//validator/src/main/java/dev/cel/validator/validators:timestamp"], +)