diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cabe945a..18849c4d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,7 @@ jobs: strategy: max-parallel: 4 matrix: - java-version: [21, 24] + java-version: [21, 25] steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: @@ -68,19 +68,6 @@ jobs: - name: Microbenchmarks run: ./gradlew jmh - - name: Cache Bazel stuff - if: ${{ matrix.java-version == '21' }} - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 - with: - path: | - ~/.cache/bazel - key: bazel-${{ hashFiles('**/.gitmodules') }} - restore-keys: bazel- - - - name: Conformance tests - if: ${{ matrix.java-version == '21' }} - run: conformance/run-conformance-tests.sh - - name: Capture test results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: failure() diff --git a/README.md b/README.md index fbe11630..4907af39 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The CEL specification can be found [here](https://github.com/google/cel-spec). - [Motivation](#motivation) - [Arbitrary Java classes](#arbitrary-java-classes) - [Unsigned 64-bit `uint`](#unsigned-64-bit-uint) + - [Protobuf enum semantics](#protobuf-enum-semantics) - [Native image and package verification](#native-image-and-package-verification) - [Not yet implemented](#not-yet-implemented) - [Unclear double-to-int rounding behavior](#unclear-double-to-int-rounding-behavior) @@ -394,6 +395,24 @@ but `123u == 123u` and `123 == 123` are. If you have a `uint32` or `uint64` in protobuf objects, or use `uint`s in CEL expressions, wrap those values with `org.projectnessie.cel.common.ULong`. +### Protobuf enum semantics + +CEL-Java follows the CEL-Spec v0.25.2 language definition for protobuf enum values: protobuf enum +constants and enum fields are represented as CEL `int` values. + +The upstream CEL-Spec conformance testdata also contains strong-enum cases where enum values +preserve their enum type. CEL-Java does not currently enable those strong-enum conformance cases. +They are not part of the v0.25.2 language-definition baseline and are mutually incompatible with the +legacy enum-as-int conformance sections in a single-mode runtime. + +Use numeric enum values directly, or use `int(...)` when writing expressions that should remain clear +if strong enum support is added in the future: + +```cel +TestAllTypes.NestedEnum.BAR == 1 +int(TestAllTypes.NestedEnum.BAR) == 1 +``` + ### Native image and package verification Native-image and package behavior must be verified in the consuming application's exact build. @@ -401,7 +420,7 @@ Native-image and package behavior must be verified in the consuming application' prove Quarkus native-image or package compatibility for every application. Before using CEL conditions in release-critical authorization paths, run JVM condition tests, the -consuming project's normal build, dependency tree review for protobuf/ANTLR/Jackson conflicts, and +consuming project's normal build, dependency tree review for protobuf/Jackson conflicts, and package/native-image verification if native execution is part of the release path. ### Not yet implemented @@ -433,17 +452,17 @@ The CEL-Go implementation does not pass these CEL-spec conformance tests: ```text --- FAIL: TestSimpleFile/conversions/int/double_truncate (0.01s) - simple_test.go:219: double_truncate: Eval got [int64_value:2], want [int64_value:1] + double_truncate: Eval got [int64_value:2], want [int64_value:1] --- FAIL: TestSimpleFile/conversions/int/double_truncate_neg (0.01s) - simple_test.go:219: double_truncate_neg: Eval got [int64_value:-8], want [int64_value:-7] + double_truncate_neg: Eval got [int64_value:-8], want [int64_value:-7] --- FAIL: TestSimpleFile/conversions/int/double_half_pos (0.01s) - simple_test.go:219: double_half_pos: Eval got [int64_value:12], want [int64_value:11] + double_half_pos: Eval got [int64_value:12], want [int64_value:11] --- FAIL: TestSimpleFile/conversions/int/double_half_neg (0.01s) - simple_test.go:219: double_half_neg: Eval got [int64_value:-4], want [int64_value:-3] + double_half_neg: Eval got [int64_value:-4], want [int64_value:-3] --- FAIL: TestSimpleFile/conversions/uint/double_truncate (0.01s) - simple_test.go:219: double_truncate: Eval got [uint64_value:2], want [uint64_value:1] + double_truncate: Eval got [uint64_value:2], want [uint64_value:1] --- FAIL: TestSimpleFile/conversions/uint/double_half (0.01s) - simple_test.go:219: double_half: Eval got [uint64_value:26], want [uint64_value:25] + double_half: Eval got [uint64_value:26], want [uint64_value:25] ``` ## Building and testing CEL-Java @@ -460,7 +479,7 @@ Build requirements: Runtime requirements: -- Java 8 or newer +- Java 17 or newer `./gradlew publishToMavenLocal` deploys the current development version to the local Maven repository, in case you want to use CEL-Java snapshot artifacts from another project. @@ -470,6 +489,11 @@ repository, in case you want to use CEL-Java snapshot artifacts from another pro The project uses Google Java style and the Spotless plugin. Run `./gradlew spotlessApply` to fix formatting issues. -To run the CEL-spec conformance tests, Go, Bazel, and their toolchains are required. From the -CEL-Java repo, run `conformance/run-conformance-tests.sh`. That script performs the necessary Gradle -and Bazel builds. +To run the CEL-Spec conformance tests, use the JUnit-based conformance suite: + +```shell +./gradlew :cel-conformance:test +``` + +The conformance suite reads upstream CEL-Spec textproto testdata from the `submodules/cel-spec` +submodule. It does not require Bazel, Go, or a separate conformance server. diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts index b50161b0..c0772de1 100644 --- a/bom/build.gradle.kts +++ b/bom/build.gradle.kts @@ -24,7 +24,6 @@ plugins { dependencies { constraints { api(project(":cel-core")) - api(project(":cel-generated-antlr")) api(project(":cel-generated-pb")) api(project(":cel-generated-pb3")) api(project(":cel-conformance")) diff --git a/build-logic/src/main/kotlin/Java.kt b/build-logic/src/main/kotlin/Java.kt index 9a0e1196..0708a60b 100644 --- a/build-logic/src/main/kotlin/Java.kt +++ b/build-logic/src/main/kotlin/Java.kt @@ -46,7 +46,7 @@ fun Project.nessieConfigureJava() { tasks.withType().configureEach { options.encoding = "UTF-8" - options.release.set(8) + options.release.set(17) } tasks.withType().configureEach { @@ -59,8 +59,8 @@ fun Project.nessieConfigureJava() { configure { withJavadocJar() withSourcesJar() - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 modularity.inferModulePath.set(true) } } diff --git a/build-logic/src/main/kotlin/cel-conventions.gradle.kts b/build-logic/src/main/kotlin/cel-conventions.gradle.kts index 49cdeef8..8a66f9c5 100644 --- a/build-logic/src/main/kotlin/cel-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/cel-conventions.gradle.kts @@ -20,8 +20,7 @@ nessieConfigureJava() val nonPublishedProjects = setOf( - "conformance", - "jacoco", + "cel-conformance", "cel-quarkus-smoke-standalone", "cel-quarkus-smoke-core-pb3-jackson3", ) diff --git a/build.gradle.kts b/build.gradle.kts index df3713e0..62a9c29a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -102,7 +102,6 @@ idea.project.settings { afterSync( ":cel-generated-pb:jar", ":cel-generated-pb:testJar", - ":cel-generated-antlr:shadowJar", ) } } diff --git a/conformance/README.md b/conformance/README.md index 8854a558..eb881226 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -1,57 +1,40 @@ -# Running CEL-spec conformance tests against CEL-Java +# Running CEL-Spec conformance tests against CEL-Java -If your environment is already setup, just run the shell script -```shell -./run-conformance-tests.sh -``` - -## Requirements & Setup +The CEL-Java conformance suite is a JUnit test suite that reads upstream CEL-Spec +simple testdata from the `submodules/cel-spec` Git submodule. -The CEL-spec conformance test suite is written in Go and uses the bazel build tool. +Run it with: -Required tools: -* [Bazel build tool](https://bazel.build/) (see [bazelisk](https://github.com/bazelbuild/bazelisk?tab=readme-ov-file#installation)) - - See [Bazel web site](https://bazel.build/install) for installation instructions. - Do **not** use the `bazel-bootstrap` package on Ubuntu, because it comes with - pre-compiled classes that are built with Java 17 or newer, preventing it from - running bazel using older Java versions. -* gcc - - On Ubuntu run `apt-get install gcc` +```shell +./gradlew :cel-conformance:test +``` -Other required dependencies like "Go" will be managed by bazel. +The suite does not require Bazel, Go, a separate conformance server, or the old +upstream `simple_test` binary. -## FAQ +## Test selection -### Bazel build hangs +The curated conformance file list and skip list live in: -If the bazel build does not start, i.e. it gets stuck with messages like -``` -Starting local Bazel server and connecting to it... -... still trying to connect to local Bazel server after 10 seconds ... -... still trying to connect to local Bazel server after 20 seconds ... -... still trying to connect to local Bazel server after 30 seconds ... +```text +conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java ``` -then kill all bazel processes make sure that Java 11 is the current one. It seems, that the -above *may* happen when with Java 8. -### Bazel build fails from `./run-conformance-tests.sh` +Each skip uses the upstream conformance path: -If the bazel build fails with an error like this (note the `Failed to create temporary file`), -run the bazel build once from the console: - -```shell -cd submodules/cel-spec - -bazel build ... +```text +file/section/test ``` -If the build still fails, try the following options: +or a whole-section path: -```shell -bazel build ... --sandbox_writable_path="${HOME}/.ccache" --strategy=CppCompile=standalone +```text +file/section ``` -After the build succeeds once from that directory, you can use the `./run-conformance-tests.sh` -script. +Unmatched skips fail the test suite, so stale skips are visible when upstream +testdata changes. + +Optional CEL-Spec files such as extension libraries, optionals, and type +deduction are intentionally not enabled by default. Add those separately with +explicit skip reasoning. diff --git a/conformance/build.gradle.kts b/conformance/build.gradle.kts index d1cae24b..8f4e2769 100644 --- a/conformance/build.gradle.kts +++ b/conformance/build.gradle.kts @@ -22,7 +22,6 @@ import org.gradle.api.tasks.compile.JavaCompile plugins { `java-library` - id("com.gradleup.shadow") id("cel-conventions") } @@ -33,22 +32,20 @@ val mainProtoResourcesDir = layout.buildDirectory.dir("generated/proto-resources val syncMainProtoSources = tasks.register("syncMainProtoSources") { into(syncedMainProtoDir) - from(layout.settingsDirectory.dir("submodules/googleapis/google/rpc")) { into("google/rpc") } - from(layout.settingsDirectory.dir("submodules/googleapis/google/api/expr/conformance")) { - into("google/api/expr/conformance") + from(layout.settingsDirectory.dir("submodules/cel-spec/proto/cel/expr")) { + include("checked.proto", "eval.proto", "syntax.proto", "value.proto") + into("cel/expr") + } + from(layout.settingsDirectory.dir("submodules/cel-spec/proto/cel/expr/conformance/test")) { + include("simple.proto") + into("cel/expr/conformance/test") } } val emptyTestProtoDir = layout.buildDirectory.dir("pb-src/test/proto") sourceSets.main { - java.setSrcDirs( - listOf( - layout.projectDirectory.dir("src/main/java"), - layout.buildDirectory.dir("generated/sources/proto/main/java"), - layout.buildDirectory.dir("generated/sources/proto/main/grpc"), - ) - ) + java.setSrcDirs(listOf(layout.buildDirectory.dir("generated/sources/proto/main/java"))) resources.setSrcDirs(listOf(mainProtoResourcesDir)) extensions.configure("proto") { setSrcDirs(listOf(syncedMainProtoDir)) @@ -68,15 +65,11 @@ configurations.all { exclude(group = "org.projectnessie.cel", module = "cel-gene dependencies { implementation(project(":cel-core")) implementation(project(":cel-generated-pb3")) - implementation(testFixtures(project(":cel-core"))) implementation(testFixtures(project(":cel-generated-pb3"))) implementation(libs.protobuf.java) { version { strictly(libs.versions.protobuf3.get()) } } - implementation(libs.grpc.protobuf) - implementation(libs.grpc.stub) - runtimeOnly(libs.grpc.netty.shaded) - compileOnly(libs.tomcat.annotations.api) + testImplementation(testFixtures(project(":cel-core"))) testImplementation(platform(libs.junit.bom)) testImplementation(libs.bundles.junit.testing) @@ -84,10 +77,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.named("shadowJar") { - manifest { attributes("Main-Class" to "org.projectnessie.cel.server.ConformanceServer") } -} - // *.proto files taken from https://github.com/google/cel-spec/ repo, available as a git submodule configure { // Configure the protoc executable @@ -95,10 +84,6 @@ configure { // Download from repositories artifact = "com.google.protobuf:protoc:${libs.versions.protobuf3.get()}" } - plugins { - this.create("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:${libs.versions.grpc.get()}" } - } - generateProtoTasks { all().configureEach { this.plugins.create("grpc") {} } } } tasks.named("generateProto") { dependsOn(syncMainProtoSources) } diff --git a/conformance/conformance-server.sh b/conformance/conformance-server.sh deleted file mode 100755 index 2425b553..00000000 --- a/conformance/conformance-server.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright (C) 2021 The Authors of CEL-Java -# -# 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 -# -# http://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. -# - -wd="$(dirname "$0")" - -pid_file="${wd}/conformance-server.pid" - -function kill_server() { - if [[ -f ${pid_file} ]] ; then - kill "$(cat "${pid_file}")" && rm "${pid_file}" - fi -} - -trap kill_server SIGINT SIGTERM - -java -Djava.net.preferIPv4Stack=true -jar "${wd}"/build/libs/cel-conformance-*-all.jar "${@}" & -java_pid=$! -echo "${java_pid}" > "${pid_file}" -wait diff --git a/conformance/run-conformance-tests.sh b/conformance/run-conformance-tests.sh deleted file mode 100755 index 1e614aa6..00000000 --- a/conformance/run-conformance-tests.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright (C) 2021 The Authors of CEL-Java -# -# 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 -# -# http://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. -# - -wd="$(dirname $0)" - -cd "${wd}/.." || exit 1 - -./gradlew :cel-conformance:shadowJar || exit 1 - -server_pid_file="$(realpath ./conformance/conformance-server.pid)" - -cd submodules/cel-spec || exit 1 - -# Bazel version 6.4.0 works, 7.0.2 does not work with the conformance tests -export USE_BAZEL_VERSION="6.5.0" - -if [[ -z "${CC:-}" ]] && command -v gcc >/dev/null 2>&1; then - export CC="$(command -v gcc)" -fi -if [[ -z "${CXX:-}" ]] && command -v g++ >/dev/null 2>&1; then - export CXX="$(command -v g++)" -fi - -bazel sync --configure || exit 1 -bazel build ... || exit 1 - -cel_java_skips=( - # proto2 enums are generated as Java enums, means: it is not possible to assign arbitrary - # ordinals, so these tests cannot work against the CEL-Java implementation (limitation of the - # protobuf/Java implementation for proto2). - "--skip_test=enums/legacy_proto2/select_big,select_neg,assign_standalone_int_big,assign_standalone_int_neg" - # Without the checker, it is quite difficult to verify whether an assignment is allowed (by - # the CEL spec), especially from a 'map' to a 'struct' as in these tests using the expression - # `TestAllTypes{single_struct: {1: 'uno'}}`. Note: the checker catches this case and the - # Java implementation currently converts the 'int(1)' to a 'string("1")', which is not strictly - # allowed, but OTOH overall not a serious issue. - "--skip_test=dynamic/struct/field_assign_proto2_bad" - "--skip_test=dynamic/struct/field_assign_proto3_bad" - # The test expects a -0.0d, but in Java `-0.0d==0.0d` is true, so -0.0d is evaluates as "not set", - # so it returns the field as empty. - "--skip_test=dynamic/float/field_assign_proto3_round_to_zero" - # "Malicious" protobuf message. The actual CEL-spec test produces a request with a too deeply - # nested protobuf-object-structure, which gets rejected during gRPC/protobuf request - # deserialization. Just skip those tests. - "--skip_test=parse/nest/message_literal" - # Proto equality specialties don't seem to be in effect for Java - "--skip_test=comparisons/eq_wrapper/eq_proto_nan_equal" - "--skip_test=comparisons/ne_literal/ne_proto_nan_not_equal" - - # TODO Actual known issue to fix, a protobuf Any returned via this test is wrapped twice (Any in Any). - "--skip_test=dynamic/any/var" -) - -cel_go_skips=( - "--skip_test=dynamic/int32/field_assign_proto2_range,field_assign_proto3_range" - "--skip_test=dynamic/uint32/field_assign_proto2_range,field_assign_proto3_range" - "--skip_test=dynamic/float/field_assign_proto2_range,field_assign_proto3_range" - "--skip_test=enums/legacy_proto2/assign_standalone_int_too_big,assign_standalone_int_too_neg" - "--skip_test=enums/legacy_proto3/assign_standalone_int_too_big,assign_standalone_int_too_neg" - "--skip_test=enums/strong_proto2" - "--skip_test=enums/strong_proto3" - # This conformance test is invalid nowadays - "--skip_test=fields/qualified_identifier_resolution/map_key_float" - # Unclear why the 'to_json_string' is expected to return a string, unlike the preceding to_json_number test. - "--skip_test=wrappers/uint64/to_json_string" - # TODO implement proper "toJson" at some point - "--skip_test=wrappers/field_mask/to_json" - "--skip_test=wrappers/timestamp/to_json" - "--skip_test=wrappers/empty/to_json" -) - -test_files=( - "tests/simple/testdata/basic.textproto" - "tests/simple/testdata/comparisons.textproto" - "tests/simple/testdata/conversions.textproto" - "tests/simple/testdata/dynamic.textproto" - "tests/simple/testdata/enums.textproto" - "tests/simple/testdata/fields.textproto" - "tests/simple/testdata/fp_math.textproto" - "tests/simple/testdata/integer_math.textproto" - "tests/simple/testdata/lists.textproto" - "tests/simple/testdata/logic.textproto" - "tests/simple/testdata/macros.textproto" - "tests/simple/testdata/namespace.textproto" - "tests/simple/testdata/parse.textproto" - "tests/simple/testdata/plumbing.textproto" - "tests/simple/testdata/proto2.textproto" - "tests/simple/testdata/proto3.textproto" - "tests/simple/testdata/string.textproto" - # TODO add when implemnting the string-extensions "tests/simple/testdata/string_ext.textproto" - "tests/simple/testdata/timestamps.textproto" - "tests/simple/testdata/unknowns.textproto" - "tests/simple/testdata/wrappers.textproto" -) - -bazel-bin/tests/simple/simple_test_/simple_test \ - --server ../../conformance/conformance-server.sh \ - "${cel_java_skips[@]}" \ - "${cel_go_skips[@]}" \ - "${test_files[@]}" -code=$? - -if [[ -f ${server_pid_file} ]] ; then - kill "$(cat "${server_pid_file}")" && rm "${server_pid_file}" || exit 1 -fi - -exit $code diff --git a/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServer.java b/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServer.java deleted file mode 100644 index 952e2ee6..00000000 --- a/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServer.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2021 The Authors of CEL-Java - * - * 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 - * - * http://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 org.projectnessie.cel.server; - -import io.grpc.Server; -import io.grpc.ServerBuilder; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.List; - -public class ConformanceServer implements AutoCloseable { - - private final Server server; - - public ConformanceServer(Server server) { - this.server = server; - } - - public static String getListenHost(Server server) { - List addrs = server.getListenSockets(); - SocketAddress addr = addrs.get(0); - InetSocketAddress ia = (InetSocketAddress) addr; - InetAddress a = ia.getAddress(); - - String host; - if (a instanceof Inet6Address) { - if (a.isAnyLocalAddress()) { - host = "::1"; - } else { - host = a.getCanonicalHostName(); - } - } else { - if (a.isAnyLocalAddress()) { - host = "127.0.0.1"; - } else { - host = a.getCanonicalHostName(); - } - } - - return host; - } - - public void blockUntilShutdown() throws InterruptedException { - server.awaitTermination(); - } - - @Override - public void close() throws Exception { - server.shutdown().awaitTermination(); - } - - public static void main(String[] args) throws Exception { - ConformanceServiceImpl service = new ConformanceServiceImpl(); - - for (String arg : args) { - if ("--verbose".equals(arg) || "-v".equals(arg)) { - service.setVerboseEvalErrors(true); - } - } - - Server c = ServerBuilder.forPort(0).addService(service).build(); - - Thread hook = new Thread(c::shutdown); - - try (ConformanceServer cs = new ConformanceServer(c.start())) { - System.out.printf("Listening on %s:%d%n", getListenHost(cs.server), cs.server.getPort()); - - Runtime.getRuntime().addShutdownHook(hook); - cs.blockUntilShutdown(); - } finally { - try { - Runtime.getRuntime().removeShutdownHook(hook); - } catch (IllegalStateException e) { - // ignore (might happen, when a JVM shutdown is already in progress) - } - } - } -} diff --git a/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServiceImpl.java b/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServiceImpl.java deleted file mode 100644 index a3d86aa6..00000000 --- a/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServiceImpl.java +++ /dev/null @@ -1,428 +0,0 @@ -/* - * Copyright (C) 2021 The Authors of CEL-Java - * - * 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 - * - * http://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 org.projectnessie.cel.server; - -import static org.projectnessie.cel.CEL.astToCheckedExpr; -import static org.projectnessie.cel.CEL.astToParsedExpr; -import static org.projectnessie.cel.CEL.checkedExprToAst; -import static org.projectnessie.cel.CEL.parsedExprToAst; -import static org.projectnessie.cel.Env.newCustomEnv; -import static org.projectnessie.cel.Env.newEnv; -import static org.projectnessie.cel.EnvOption.clearMacros; -import static org.projectnessie.cel.EnvOption.container; -import static org.projectnessie.cel.EnvOption.declarations; -import static org.projectnessie.cel.EnvOption.types; -import static org.projectnessie.cel.Library.StdLib; -import static org.projectnessie.cel.common.types.BoolT.True; -import static org.projectnessie.cel.common.types.BytesT.bytesOf; -import static org.projectnessie.cel.common.types.DoubleT.doubleOf; -import static org.projectnessie.cel.common.types.Err.isError; -import static org.projectnessie.cel.common.types.Err.newErr; -import static org.projectnessie.cel.common.types.IntT.intOf; -import static org.projectnessie.cel.common.types.StringT.stringOf; -import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; -import static org.projectnessie.cel.common.types.Types.boolOf; -import static org.projectnessie.cel.common.types.UintT.uintOf; -import static org.projectnessie.cel.common.types.UnknownT.isUnknown; -import static org.projectnessie.cel.common.types.UnknownT.unknownOf; - -import com.google.api.expr.conformance.v1alpha1.CheckRequest; -import com.google.api.expr.conformance.v1alpha1.CheckResponse; -import com.google.api.expr.conformance.v1alpha1.ConformanceServiceGrpc.ConformanceServiceImplBase; -import com.google.api.expr.conformance.v1alpha1.EvalRequest; -import com.google.api.expr.conformance.v1alpha1.EvalResponse; -import com.google.api.expr.conformance.v1alpha1.IssueDetails; -import com.google.api.expr.conformance.v1alpha1.ParseRequest; -import com.google.api.expr.conformance.v1alpha1.ParseResponse; -import com.google.api.expr.conformance.v1alpha1.SourcePosition; -import com.google.api.expr.v1alpha1.ErrorSet; -import com.google.api.expr.v1alpha1.ExprValue; -import com.google.api.expr.v1alpha1.ListValue; -import com.google.api.expr.v1alpha1.MapValue; -import com.google.api.expr.v1alpha1.MapValue.Entry; -import com.google.api.expr.v1alpha1.UnknownSet; -import com.google.api.expr.v1alpha1.Value; -import com.google.protobuf.Any; -import com.google.protobuf.ByteString; -import com.google.protobuf.Duration; -import com.google.protobuf.Message; -import com.google.protobuf.Timestamp; -import com.google.rpc.Code; -import com.google.rpc.Status; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import org.projectnessie.cel.Ast; -import org.projectnessie.cel.Env; -import org.projectnessie.cel.Env.AstIssuesTuple; -import org.projectnessie.cel.EnvOption; -import org.projectnessie.cel.Program; -import org.projectnessie.cel.Program.EvalResult; -import org.projectnessie.cel.common.CELError; -import org.projectnessie.cel.common.types.Err; -import org.projectnessie.cel.common.types.IteratorT; -import org.projectnessie.cel.common.types.NullT; -import org.projectnessie.cel.common.types.TypeT; -import org.projectnessie.cel.common.types.Types; -import org.projectnessie.cel.common.types.ref.Type; -import org.projectnessie.cel.common.types.ref.TypeAdapter; -import org.projectnessie.cel.common.types.ref.Val; -import org.projectnessie.cel.common.types.traits.Lister; -import org.projectnessie.cel.common.types.traits.Mapper; - -public class ConformanceServiceImpl extends ConformanceServiceImplBase { - - private boolean verboseEvalErrors; - - public void setVerboseEvalErrors(boolean verboseEvalErrors) { - this.verboseEvalErrors = verboseEvalErrors; - } - - @Override - public void parse( - ParseRequest request, io.grpc.stub.StreamObserver responseObserver) { - try { - String sourceText = request.getCelSource(); - if (sourceText.trim().isEmpty()) { - throw new IllegalArgumentException("No source code."); - } - - // NOTE: syntax_version isn't currently used - List parseOptions = new ArrayList<>(); - if (request.getDisableMacros()) { - parseOptions.add(clearMacros()); - } - - Env env = newEnv(parseOptions.toArray(new EnvOption[0])); - AstIssuesTuple astIss = env.parse(sourceText); - - ParseResponse.Builder response = ParseResponse.newBuilder(); - if (!astIss.hasIssues()) { - // Success - response.setParsedExpr(astToParsedExpr(astIss.getAst())); - } else { - // Failure - appendErrors(astIss.getIssues().getErrors(), response::addIssuesBuilder); - } - - responseObserver.onNext(response.build()); - responseObserver.onCompleted(); - } catch (Exception e) { - responseObserver.onError( - io.grpc.Status.fromCode(io.grpc.Status.Code.UNKNOWN) - .withDescription(stacktrace(e)) - .asException()); - } - } - - @Override - public void check( - CheckRequest request, io.grpc.stub.StreamObserver responseObserver) { - try { - // Build the environment. - List checkOptions = new ArrayList<>(); - if (!request.getNoStdEnv()) { - checkOptions.add(StdLib()); - } - - checkOptions.add(container(request.getContainer())); - checkOptions.add(declarations(request.getTypeEnvList())); - checkOptions.add( - types( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance(), - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance())); - Env env = newCustomEnv(checkOptions.toArray(new EnvOption[0])); - - // Check the expression. - AstIssuesTuple astIss = env.check(parsedExprToAst(request.getParsedExpr())); - CheckResponse.Builder resp = CheckResponse.newBuilder(); - - if (!astIss.hasIssues()) { - // Success - resp.setCheckedExpr(astToCheckedExpr(astIss.getAst())); - } else { - // Failure - appendErrors(astIss.getIssues().getErrors(), resp::addIssuesBuilder); - } - - responseObserver.onNext(resp.build()); - responseObserver.onCompleted(); - } catch (Exception e) { - responseObserver.onError( - io.grpc.Status.fromCode(io.grpc.Status.Code.UNKNOWN) - .withDescription(stacktrace(e)) - .asException()); - } - } - - @Override - public void eval( - EvalRequest request, io.grpc.stub.StreamObserver responseObserver) { - try { - Env env = - newEnv( - container(request.getContainer()), - types( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance(), - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance())); - - Program prg; - Ast ast; - - switch (request.getExprKindCase()) { - case PARSED_EXPR: - ast = parsedExprToAst(request.getParsedExpr()); - break; - case CHECKED_EXPR: - ast = checkedExprToAst(request.getCheckedExpr()); - break; - default: - throw new IllegalArgumentException("No expression."); - } - - prg = env.program(ast); - - Map args = new HashMap<>(); - request - .getBindingsMap() - .forEach( - (name, exprValue) -> { - Val refVal = exprValueToRefValue(env.getTypeAdapter(), exprValue); - args.put(name, refVal); - }); - - // NOTE: the EvalState is currently discarded - EvalResult res = prg.eval(args); - ExprValue resultExprVal; - if (!isError(res.getVal())) { - resultExprVal = refValueToExprValue(res.getVal()); - } else { - Err err = (Err) res.getVal(); - - if (verboseEvalErrors) { - System.err.printf( - "%n" + "Eval error (not necessarily a bug!!!):%n" + " error: %s%n" + "%s", - err, err.hasCause() ? (stacktrace(err.toRuntimeException()) + "\n") : ""); - } - - resultExprVal = - ExprValue.newBuilder() - .setError( - ErrorSet.newBuilder().addErrors(Status.newBuilder().setMessage(err.toString()))) - .build(); - } - - EvalResponse.Builder resp = EvalResponse.newBuilder().setResult(resultExprVal); - - responseObserver.onNext(resp.build()); - responseObserver.onCompleted(); - } catch (Exception e) { - responseObserver.onError( - io.grpc.Status.fromCode(io.grpc.Status.Code.UNKNOWN) - .withDescription(stacktrace(e)) - .asException()); - } - } - - static String stacktrace(Throwable t) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - t.printStackTrace(pw); - pw.flush(); - return sw.toString(); - } - - /** - * appendErrors converts the errors from errs to Status messages and appends them to the list of - * issues. - */ - static void appendErrors(List errs, Supplier builderSupplier) { - errs.forEach(e -> errToStatus(e, IssueDetails.Severity.ERROR, builderSupplier.get())); - } - - /** ErrToStatus converts an Error to a Status message with the given severity. */ - static void errToStatus(CELError e, IssueDetails.Severity severity, Status.Builder status) { - IssueDetails.Builder detail = - IssueDetails.newBuilder() - .setSeverity(severity) - .setPosition( - SourcePosition.newBuilder() - .setLine(e.getLocation().line()) - .setColumn(e.getLocation().column()) - .build()); - - status - .setCode(Code.INVALID_ARGUMENT_VALUE) - .setMessage(e.getMessage()) - .addDetails(Any.pack(detail.build())); - } - - /** RefValueToExprValue converts between ref.Val and exprpb.ExprValue. */ - static ExprValue refValueToExprValue(Val res) { - if (isUnknown(res)) { - return ExprValue.newBuilder() - .setUnknown(UnknownSet.newBuilder().addExprs(res.intValue())) - .build(); - } - Value v = refValueToValue(res); - return ExprValue.newBuilder().setValue(v).build(); - } - - // TODO(jimlarson): The following conversion code should be moved to - // common/types/provider.go and consolidated/refactored as appropriate. - // In particular, make judicious use of types.NativeToValue(). - - /** - * RefValueToValue converts between ref.Val and Value. The ref.Val must not be error or unknown. - */ - static Value refValueToValue(Val res) { - switch (res.type().typeEnum()) { - case Bool: - return Value.newBuilder().setBoolValue(res.booleanValue()).build(); - case Bytes: - return Value.newBuilder().setBytesValue(res.convertToNative(ByteString.class)).build(); - case Double: - return Value.newBuilder().setDoubleValue(res.convertToNative(Double.class)).build(); - case Int: - return Value.newBuilder().setInt64Value(res.intValue()).build(); - case Null: - return Value.newBuilder().setNullValueValue(0).build(); - case String: - return Value.newBuilder().setStringValue(res.value().toString()).build(); - case Type: - return Value.newBuilder().setTypeValue(((TypeT) res).typeName()).build(); - case Uint: - return Value.newBuilder().setUint64Value(res.intValue()).build(); - case Duration: - Duration d = res.convertToNative(Duration.class); - return Value.newBuilder().setObjectValue(Any.pack(d)).build(); - case Timestamp: - Timestamp t = res.convertToNative(Timestamp.class); - return Value.newBuilder().setObjectValue(Any.pack(t)).build(); - case List: - Lister l = (Lister) res; - ListValue.Builder elts = ListValue.newBuilder(); - for (IteratorT i = l.iterator(); i.hasNext() == True; ) { - Val v = i.next(); - elts.addValues(refValueToValue(v)); - } - return Value.newBuilder().setListValue(elts).build(); - case Map: - Mapper m = (Mapper) res; - MapValue.Builder elems = MapValue.newBuilder(); - for (IteratorT i = m.iterator(); i.hasNext() == True; ) { - Val k = i.next(); - Val v = m.get(k); - Value kv = refValueToValue(k); - Value vv = refValueToValue(v); - elems.addEntriesBuilder().setKey(kv).setValue(vv); - } - return Value.newBuilder().setMapValue(elems).build(); - case Object: - // Object type - Message pb = (Message) res.value(); - Value.Builder v = Value.newBuilder(); - // Somehow the conformance tests - if (pb instanceof ListValue) { - v.setListValue((ListValue) pb); - } else if (pb instanceof MapValue) { - v.setMapValue((MapValue) pb); - } else { - v.setObjectValue(Any.pack(pb)); - } - return v.build(); - default: - throw new IllegalStateException(String.format("Unknown %s", res.type().typeEnum())); - } - } - - /** ExprValueToRefValue converts between exprpb.ExprValue and ref.Val. */ - static Val exprValueToRefValue(TypeAdapter adapter, ExprValue ev) { - switch (ev.getKindCase()) { - case VALUE: - return valueToRefValue(adapter, ev.getValue()); - case ERROR: - // An error ExprValue is a repeated set of rpcpb.Status - // messages, with no convention for the status details. - // To convert this to a types.Err, we need to convert - // these Status messages to a single string, and be - // able to decompose that string on output so we can - // round-trip arbitrary ExprValue messages. - // TODO(jimlarson) make a convention for this. - return newErr("XXX add details later"); - case UNKNOWN: - return unknownOf(ev.getUnknown().getExprs(0)); - } - throw new IllegalArgumentException("unknown ExprValue kind " + ev.getKindCase()); - } - - /** ValueToRefValue converts between exprpb.Value and ref.Val. */ - static Val valueToRefValue(TypeAdapter adapter, Value v) { - switch (v.getKindCase()) { - case NULL_VALUE: - return NullT.NullValue; - case BOOL_VALUE: - return boolOf(v.getBoolValue()); - case INT64_VALUE: - return intOf(v.getInt64Value()); - case UINT64_VALUE: - return uintOf(v.getUint64Value()); - case DOUBLE_VALUE: - return doubleOf(v.getDoubleValue()); - case STRING_VALUE: - return stringOf(v.getStringValue()); - case BYTES_VALUE: - return bytesOf(v.getBytesValue().toByteArray()); - case OBJECT_VALUE: - Any any = v.getObjectValue(); - return adapter.nativeToValue(any); - case MAP_VALUE: - MapValue m = v.getMapValue(); - Map entries = new HashMap<>(); - for (Entry entry : m.getEntriesList()) { - Val key = valueToRefValue(adapter, entry.getKey()); - Val pb = valueToRefValue(adapter, entry.getValue()); - entries.put(key, pb); - } - return adapter.nativeToValue(entries); - case LIST_VALUE: - ListValue l = v.getListValue(); - List elts = - l.getValuesList().stream() - .map(el -> valueToRefValue(adapter, el)) - .collect(Collectors.toList()); - return adapter.nativeToValue(elts); - case TYPE_VALUE: - String typeName = v.getTypeValue(); - Type tv = Types.getTypeByName(typeName); - if (tv != null) { - return tv; - } - return newObjectTypeValue(typeName); - default: - throw new IllegalArgumentException("unknown value " + v.getKindCase()); - } - } -} diff --git a/conformance/src/main/proto/google/api/expr/conformance b/conformance/src/main/proto/google/api/expr/conformance deleted file mode 120000 index 04adab4d..00000000 --- a/conformance/src/main/proto/google/api/expr/conformance +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../submodules/googleapis/google/api/expr/conformance \ No newline at end of file diff --git a/conformance/src/main/proto/google/rpc b/conformance/src/main/proto/google/rpc deleted file mode 120000 index 9b33cdfa..00000000 --- a/conformance/src/main/proto/google/rpc +++ /dev/null @@ -1 +0,0 @@ -../../../../../submodules/googleapis/google/rpc/ \ No newline at end of file diff --git a/conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java b/conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java new file mode 100644 index 00000000..99a10d4d --- /dev/null +++ b/conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java @@ -0,0 +1,796 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.conformance; + +import static java.util.stream.Collectors.toCollection; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.abort; +import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.projectnessie.cel.CEL.astToCheckedExpr; +import static org.projectnessie.cel.CEL.astToParsedExpr; +import static org.projectnessie.cel.CEL.checkedExprToAst; +import static org.projectnessie.cel.CEL.parsedExprToAst; +import static org.projectnessie.cel.Env.newCustomEnv; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.clearMacros; +import static org.projectnessie.cel.EnvOption.container; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.macros; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.Library.StdLib; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.BytesT.bytesOf; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.Err.isError; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; +import static org.projectnessie.cel.common.types.Types.boolOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; +import static org.projectnessie.cel.extension.EncodersLib.encoders; +import static org.projectnessie.cel.extension.MathLib.math; +import static org.projectnessie.cel.extension.NetworkLib.network; +import static org.projectnessie.cel.extension.OptionalLib.optionals; +import static org.projectnessie.cel.extension.ProtoLib.proto; +import static org.projectnessie.cel.extension.StringsLib.strings; + +import com.google.api.expr.v1alpha1.CheckedExpr; +import com.google.api.expr.v1alpha1.Decl; +import com.google.api.expr.v1alpha1.ErrorSet; +import com.google.api.expr.v1alpha1.ExprValue; +import com.google.api.expr.v1alpha1.ListValue; +import com.google.api.expr.v1alpha1.MapValue; +import com.google.api.expr.v1alpha1.MapValue.Entry; +import com.google.api.expr.v1alpha1.ParsedExpr; +import com.google.api.expr.v1alpha1.Type; +import com.google.api.expr.v1alpha1.UnknownSet; +import com.google.api.expr.v1alpha1.Value; +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.TextFormat; +import com.google.protobuf.Timestamp; +import com.google.protobuf.TypeRegistry; +import com.google.rpc.Status; +import dev.cel.expr.conformance.test.SimpleTest; +import dev.cel.expr.conformance.test.SimpleTest.ResultMatcherCase; +import dev.cel.expr.conformance.test.SimpleTestFile; +import dev.cel.expr.conformance.test.SimpleTestSection; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.TestFactory; +import org.projectnessie.cel.Ast; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.Program.EvalResult; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.IteratorT; +import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.TypeT; +import org.projectnessie.cel.common.types.Types; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.common.types.traits.Mapper; +import org.projectnessie.cel.parser.Macro; + +class SimpleConformanceTest { + + private static final Path TESTDATA_DIR = testdataDir(); + private static final TextFormat.Printer TEXT_PRINTER = + TextFormat.printer().emittingSingleLine(true); + + private static final List TEST_FILES = + List.of( + "basic.textproto", + "bindings_ext.textproto", + "block_ext.textproto", + "comparisons.textproto", + "conversions.textproto", + "dynamic.textproto", + "encoders_ext.textproto", + "enums.textproto", + "fields.textproto", + "fp_math.textproto", + "integer_math.textproto", + "lists.textproto", + "logic.textproto", + "macros.textproto", + "macros2.textproto", + "math_ext.textproto", + "namespace.textproto", + "network_ext.textproto", + "optionals.textproto", + "parse.textproto", + "plumbing.textproto", + "proto2.textproto", + "proto2_ext.textproto", + "proto3.textproto", + "string.textproto", + "string_ext.textproto", + "timestamps.textproto", + "type_deduction.textproto", + "unknowns.textproto", + "wrappers.textproto"); + + private static final Set SKIP_TESTS = + SkipList.parse( + // Strong enum semantics require typed enum values rather than treating enum literals as + // ints. + "enums/strong_proto2/literal_global", + "enums/strong_proto2/literal_nested", + "enums/strong_proto2/literal_zero", + "enums/strong_proto2/type_global", + "enums/strong_proto2/type_nested", + "enums/strong_proto2/select_default", + "enums/strong_proto2/field_type", + "enums/strong_proto2/assign_standalone_int", + "enums/strong_proto2/convert_int_inrange", + "enums/strong_proto2/convert_int_big", + "enums/strong_proto2/convert_int_neg", + "enums/strong_proto2/convert_int_too_big", + "enums/strong_proto2/convert_int_too_neg", + "enums/strong_proto2/convert_string", + "enums/strong_proto2/convert_string_bad", + "enums/strong_proto3/literal_global", + "enums/strong_proto3/literal_nested", + "enums/strong_proto3/literal_zero", + "enums/strong_proto3/type_global", + "enums/strong_proto3/type_nested", + "enums/strong_proto3/select_default", + "enums/strong_proto3/select", + "enums/strong_proto3/select_big", + "enums/strong_proto3/select_neg", + "enums/strong_proto3/field_type", + "enums/strong_proto3/assign_standalone_int", + "enums/strong_proto3/assign_standalone_int_big", + "enums/strong_proto3/assign_standalone_int_neg", + "enums/strong_proto3/convert_int_inrange", + "enums/strong_proto3/convert_int_big", + "enums/strong_proto3/convert_int_neg", + "enums/strong_proto3/convert_int_too_big", + "enums/strong_proto3/convert_int_too_neg", + "enums/strong_proto3/convert_string", + "enums/strong_proto3/convert_string_bad"); + + private static final Set matchedSkips = new LinkedHashSet<>(); + private static final AtomicInteger total = new AtomicInteger(); + private static final AtomicInteger passed = new AtomicInteger(); + private static final AtomicInteger skipped = new AtomicInteger(); + + @TestFactory + Stream simpleConformance() { + List files = new ArrayList<>(); + TEST_FILES.forEach(fileName -> files.add(dynamicContainer(fileName, fileTests(fileName)))); + files.add(dynamicTest("skip list matches testdata", this::assertAllSkipsMatched)); + return files.stream(); + } + + @AfterAll + static void printSummary() { + System.out.printf( + "Conformance tests: %d total, %d passed, %d skipped%n", + total.get(), passed.get(), skipped.get()); + } + + private Stream fileTests(String fileName) { + SimpleTestFile file; + try { + file = parseSimpleFile(TESTDATA_DIR.resolve(fileName)); + } catch (IOException e) { + throw new IllegalStateException("Cannot parse conformance testdata " + fileName, e); + } + + return file.getSectionList().stream() + .map( + section -> + dynamicContainer( + section.getName(), + section.getTestList().stream() + .map(test -> dynamicTest(test.getName(), () -> run(file, section, test))))); + } + + private void run(SimpleTestFile file, SimpleTestSection section, SimpleTest test) + throws InvalidProtocolBufferException { + total.incrementAndGet(); + String sectionPath = file.getName() + "/" + section.getName(); + String testPath = sectionPath + "/" + test.getName(); + if (SKIP_TESTS.contains(sectionPath)) { + matchedSkips.add(sectionPath); + skipped.incrementAndGet(); + abort("Skipped conformance section " + sectionPath); + } + if (SKIP_TESTS.contains(testPath)) { + matchedSkips.add(testPath); + skipped.incrementAndGet(); + abort("Skipped conformance test " + testPath); + } + + ConformanceCaseRunner.run(testPath, test); + passed.incrementAndGet(); + } + + private void assertAllSkipsMatched() { + Set unmatched = + SKIP_TESTS.stream() + .filter(skip -> !matchedSkips.contains(skip)) + .collect(toCollection(LinkedHashSet::new)); + if (!unmatched.isEmpty()) { + fail("Skip did not match any test or section: " + unmatched); + } + } + + private static SimpleTestFile parseSimpleFile(Path testFile) throws IOException { + TypeRegistry typeRegistry = + TypeRegistry.newBuilder() + .add(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) + .add(dev.cel.expr.conformance.proto2.NestedTestAllTypes.getDescriptor()) + .add(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor()) + .add(dev.cel.expr.conformance.proto3.NestedTestAllTypes.getDescriptor()) + .build(); + + SimpleTestFile.Builder builder = SimpleTestFile.newBuilder(); + ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); + dev.cel.expr.conformance.proto2.TestAllTypesExtensions.registerAllExtensions(extensionRegistry); + TextFormat.Parser.newBuilder() + .setTypeRegistry(typeRegistry) + .build() + .merge(Files.readString(testFile, StandardCharsets.UTF_8), extensionRegistry, builder); + return builder.build(); + } + + private static Path testdataDir() { + Path rootRelative = Path.of("submodules/cel-spec/tests/simple/testdata"); + if (Files.isDirectory(rootRelative)) { + return rootRelative; + } + + Path moduleRelative = Path.of("../submodules/cel-spec/tests/simple/testdata"); + if (Files.isDirectory(moduleRelative)) { + return moduleRelative; + } + + throw new IllegalStateException("Cannot locate CEL-Spec simple conformance testdata"); + } + + private static final class ConformanceCaseRunner { + private ConformanceCaseRunner() {} + + private static void run(String testPath, SimpleTest test) + throws InvalidProtocolBufferException { + if (test.getName().isEmpty()) { + throw new IllegalArgumentException("simple test has no name"); + } + if (test.getExpr().isEmpty()) { + throw new IllegalArgumentException("test has no expression"); + } + + ParsedExpr parsedExpr = ConformanceEvaluator.parse(test); + CheckedExpr checkedExpr = null; + if (!test.getDisableCheck()) { + checkedExpr = ConformanceEvaluator.check(test, parsedExpr); + if (!checkedExpr.getTypeMapMap().containsKey(parsedExpr.getExpr().getId())) { + throw new AssertionError("no type for top-level expression"); + } + if (test.getResultMatcherCase() == ResultMatcherCase.TYPED_RESULT) { + Type expectedType = convert(test.getTypedResult().getDeducedType(), Type.class); + Type actualType = checkedExpr.getTypeMapOrThrow(parsedExpr.getExpr().getId()); + if (!actualType.equals(expectedType)) { + throw new AssertionError( + "deduced type mismatch, got " + + print(actualType) + + ", want " + + print(expectedType)); + } + } + } + + if (test.getCheckOnly()) { + return; + } + + match(testPath, test, ConformanceEvaluator.evalParsed(test, parsedExpr)); + if (checkedExpr != null) { + match(testPath, test, ConformanceEvaluator.evalChecked(test, checkedExpr)); + } + } + } + + private static final class ConformanceEvaluator { + private ConformanceEvaluator() {} + + private static ParsedExpr parse(SimpleTest test) { + String sourceText = test.getExpr(); + if (sourceText.trim().isEmpty()) { + throw new IllegalArgumentException("No source code."); + } + + List parseOptions = new ArrayList<>(); + if (test.getDisableMacros()) { + parseOptions.add(clearMacros()); + } + if (usesTestOnlyBlockMacros(test.getExpr())) { + parseOptions.add(macros(Macro.TestOnlyBlockMacros)); + } + if (usesOptionals(test.getExpr())) { + parseOptions.add(optionals()); + } + + Env env = newEnv(parseOptions.toArray(new EnvOption[0])); + AstIssuesTuple astIss = env.parse(sourceText); + if (astIss.hasIssues()) { + throw new AssertionError("fatal parse errors: " + astIss.getIssues().getErrors()); + } + return astToParsedExpr(astIss.getAst()); + } + + private static CheckedExpr check(SimpleTest test, ParsedExpr parsedExpr) + throws InvalidProtocolBufferException { + List typeEnv = new ArrayList<>(); + for (dev.cel.expr.Decl decl : test.getTypeEnvList()) { + typeEnv.add(convert(decl, Decl.class)); + } + + Env env = + newCustomEnv( + conformanceEnvOptions(test, StdLib(), declarations(typeEnv)) + .toArray(new EnvOption[0])); + + AstIssuesTuple astIss = env.check(parsedExprToAst(parsedExpr)); + if (astIss.hasIssues()) { + throw new AssertionError("fatal check errors: " + astIss.getIssues().getErrors()); + } + return astToCheckedExpr(astIss.getAst()); + } + + private static ExprValue evalParsed(SimpleTest test, ParsedExpr parsedExpr) { + return eval(test, parsedExprToAst(parsedExpr)); + } + + private static ExprValue evalChecked(SimpleTest test, CheckedExpr checkedExpr) { + return eval(test, checkedExprToAst(checkedExpr)); + } + + private static ExprValue eval(SimpleTest test, Ast ast) { + Env env = newEnv(conformanceEnvOptions(test).toArray(new EnvOption[0])); + + Program program = env.program(ast); + Map args = new HashMap<>(); + test.getBindingsMap() + .forEach( + (name, exprValue) -> + args.put(name, exprValueToRefValue(env.getTypeAdapter(), exprValue))); + + EvalResult res = program.eval(args); + if (!isError(res.getVal())) { + return refValueToExprValue(res.getVal()); + } + + Err err = (Err) res.getVal(); + return ExprValue.newBuilder() + .setError(ErrorSet.newBuilder().addErrors(Status.newBuilder().setMessage(err.toString()))) + .build(); + } + + private static List conformanceEnvOptions(SimpleTest test, EnvOption... options) { + List envOptions = new ArrayList<>(); + envOptions.add(container(test.getContainer())); + envOptions.add( + types( + dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(), + dev.cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.getDefaultInstance(), + dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance())); + if (test.getExpr().startsWith("proto.hasExt(") + || test.getExpr().startsWith("proto.getExt(")) { + envOptions.add(proto()); + } + if (usesStringExtensions(test.getExpr())) { + envOptions.add(strings()); + } + if (test.getExpr().contains("base64.")) { + envOptions.add(encoders()); + } + if (test.getExpr().contains("math.")) { + envOptions.add(math()); + } + if (usesNetworkExtensions(test.getExpr())) { + envOptions.add(network()); + } + if (usesOptionals(test.getExpr())) { + envOptions.add(optionals()); + } + envOptions.addAll(List.of(options)); + return envOptions; + } + + private static boolean usesStringExtensions(String expression) { + return expression.contains(".charAt(") + || expression.contains(".indexOf(") + || expression.contains(".lastIndexOf(") + || expression.contains(".lowerAscii(") + || expression.contains(".upperAscii(") + || expression.contains(".replace(") + || expression.contains(".split(") + || expression.contains(".substring(") + || expression.contains(".trim(") + || expression.contains(".join(") + || expression.contains("strings.quote(") + || expression.contains(".format(") + || expression.contains(".reverse("); + } + + private static boolean usesOptionals(String expression) { + return expression.contains("optional.") + || expression.contains(".?") + || expression.contains("[?") + || expression.contains("{?"); + } + + private static boolean usesNetworkExtensions(String expression) { + return expression.contains("ip(") + || expression.contains("cidr(") + || expression.contains("isIP(") + || expression.contains("ip.isCanonical(") + || expression.contains("net.IP") + || expression.contains("net.CIDR"); + } + + private static boolean usesTestOnlyBlockMacros(String expression) { + return expression.contains("cel.block(") + || expression.contains("cel.index(") + || expression.contains("cel.iterVar(") + || expression.contains("cel.accuVar("); + } + } + + private static void match(String testPath, SimpleTest test, ExprValue actual) + throws InvalidProtocolBufferException { + switch (test.getResultMatcherCase()) { + case VALUE: + matchValue(testPath, convert(test.getValue(), Value.class), actual); + return; + case TYPED_RESULT: + matchValue(testPath, convert(test.getTypedResult().getResult(), Value.class), actual); + return; + case EVAL_ERROR: + matchError(testPath, convert(test.getEvalError(), ErrorSet.class), actual); + return; + case ANY_EVAL_ERRORS: + if (actual.getKindCase() == ExprValue.KindCase.ERROR) { + return; + } + throw new AssertionError("got " + print(actual) + ", want one of several eval errors"); + case UNKNOWN: + matchUnknown(testPath, convert(test.getUnknown(), UnknownSet.class), actual); + return; + case ANY_UNKNOWNS: + if (actual.getKindCase() == ExprValue.KindCase.UNKNOWN) { + return; + } + throw new AssertionError("got " + print(actual) + ", want one of several unknowns"); + case RESULTMATCHER_NOT_SET: + matchValue(testPath, Value.newBuilder().setBoolValue(true).build(), actual); + return; + } + throw new AssertionError("unsupported result matcher " + test.getResultMatcherCase()); + } + + private static void matchValue(String testPath, Value expected, ExprValue actual) { + if (actual.getKindCase() != ExprValue.KindCase.VALUE) { + throw new AssertionError("got " + print(actual) + ", want value " + print(expected)); + } + if (!valuesEqual(expected, actual.getValue())) { + throw new AssertionError( + testPath + + ": eval got [" + + print(actual.getValue()) + + "], want [" + + print(expected) + + "]"); + } + } + + private static void matchError(String testPath, ErrorSet expected, ExprValue actual) { + if (actual.getKindCase() != ExprValue.KindCase.ERROR) { + throw new AssertionError( + testPath + ": got " + print(actual) + ", want error " + print(expected)); + } + } + + private static void matchUnknown(String testPath, UnknownSet expected, ExprValue actual) { + if (actual.getKindCase() != ExprValue.KindCase.UNKNOWN) { + throw new AssertionError( + testPath + ": got " + print(actual) + ", want unknown " + print(expected)); + } + } + + private static boolean valuesEqual(Value expected, Value actual) { + if (expected.getKindCase() != actual.getKindCase()) { + return false; + } + + switch (expected.getKindCase()) { + case DOUBLE_VALUE: + double expectedValue = expected.getDoubleValue(); + double actualValue = actual.getDoubleValue(); + return expectedValue == actualValue + || (Double.isNaN(expectedValue) && Double.isNaN(actualValue)); + case MAP_VALUE: + return mapsEqual(expected.getMapValue(), actual.getMapValue()); + case LIST_VALUE: + if (expected.getListValue().getValuesCount() != actual.getListValue().getValuesCount()) { + return false; + } + for (int i = 0; i < expected.getListValue().getValuesCount(); i++) { + if (!valuesEqual( + expected.getListValue().getValues(i), actual.getListValue().getValues(i))) { + return false; + } + } + return true; + default: + return expected.equals(actual); + } + } + + private static boolean mapsEqual(MapValue expected, MapValue actual) { + if (expected.getEntriesCount() != actual.getEntriesCount()) { + return false; + } + boolean[] matched = new boolean[actual.getEntriesCount()]; + for (MapValue.Entry expectedEntry : expected.getEntriesList()) { + boolean found = false; + for (int i = 0; i < actual.getEntriesCount(); i++) { + if (!matched[i] + && valuesEqual(expectedEntry.getKey(), actual.getEntries(i).getKey()) + && valuesEqual(expectedEntry.getValue(), actual.getEntries(i).getValue())) { + matched[i] = true; + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + private static Val exprValueToRefValue(TypeAdapter adapter, dev.cel.expr.ExprValue ev) { + try { + return exprValueToRefValue(adapter, convert(ev, ExprValue.class)); + } catch (InvalidProtocolBufferException e) { + throw new IllegalArgumentException("invalid expression value", e); + } + } + + private static Val exprValueToRefValue(TypeAdapter adapter, ExprValue ev) { + return switch (ev.getKindCase()) { + case VALUE -> valueToRefValue(adapter, ev.getValue()); + case ERROR -> newErr("XXX add details later"); + case UNKNOWN -> unknownOf(ev.getUnknown().getExprs(0)); + default -> throw new IllegalArgumentException("unknown ExprValue kind " + ev.getKindCase()); + }; + } + + private static Val valueToRefValue(TypeAdapter adapter, Value v) { + switch (v.getKindCase()) { + case NULL_VALUE: + return NullT.NullValue; + case BOOL_VALUE: + return boolOf(v.getBoolValue()); + case INT64_VALUE: + return intOf(v.getInt64Value()); + case UINT64_VALUE: + return uintOf(v.getUint64Value()); + case DOUBLE_VALUE: + return doubleOf(v.getDoubleValue()); + case STRING_VALUE: + return stringOf(v.getStringValue()); + case BYTES_VALUE: + return bytesOf(v.getBytesValue().toByteArray()); + case OBJECT_VALUE: + return adapter.nativeToValue(v.getObjectValue()); + case MAP_VALUE: + Map entries = new HashMap<>(); + for (Entry entry : v.getMapValue().getEntriesList()) { + entries.put( + valueToRefValue(adapter, entry.getKey()), valueToRefValue(adapter, entry.getValue())); + } + return adapter.nativeToValue(entries); + case LIST_VALUE: + List elements = new ArrayList<>(); + for (Value element : v.getListValue().getValuesList()) { + elements.add(valueToRefValue(adapter, element)); + } + return adapter.nativeToValue(elements); + case TYPE_VALUE: + String typeName = v.getTypeValue(); + org.projectnessie.cel.common.types.ref.Type type = Types.getTypeByName(typeName); + if (type != null) { + return type; + } + return newObjectTypeValue(typeName); + case ENUM_VALUE: + return intOf(v.getEnumValue().getValue()); + default: + throw new IllegalArgumentException("unknown value " + v.getKindCase()); + } + } + + private static ExprValue refValueToExprValue(Val res) { + if (isUnknown(res)) { + return ExprValue.newBuilder() + .setUnknown(UnknownSet.newBuilder().addExprs(res.intValue())) + .build(); + } + return ExprValue.newBuilder().setValue(refValueToValue(res)).build(); + } + + private static Value refValueToValue(Val res) { + switch (res.type().typeEnum()) { + case Bool: + return Value.newBuilder().setBoolValue(res.booleanValue()).build(); + case Bytes: + return Value.newBuilder().setBytesValue(res.convertToNative(ByteString.class)).build(); + case Double: + return Value.newBuilder().setDoubleValue(res.convertToNative(Double.class)).build(); + case Int: + return Value.newBuilder().setInt64Value(res.intValue()).build(); + case Null: + return Value.newBuilder().setNullValueValue(0).build(); + case String: + return Value.newBuilder().setStringValue(res.value().toString()).build(); + case Type: + return Value.newBuilder().setTypeValue(((TypeT) res).typeName()).build(); + case Uint: + return Value.newBuilder().setUint64Value(res.intValue()).build(); + case Duration: + return Value.newBuilder() + .setObjectValue(Any.pack(res.convertToNative(Duration.class))) + .build(); + case Timestamp: + return Value.newBuilder() + .setObjectValue(Any.pack(res.convertToNative(Timestamp.class))) + .build(); + case List: + Lister lister = (Lister) res; + ListValue.Builder elements = ListValue.newBuilder(); + for (IteratorT i = lister.iterator(); i.hasNext() == True; ) { + elements.addValues(refValueToValue(i.next())); + } + return Value.newBuilder().setListValue(elements).build(); + case Map: + Mapper mapper = (Mapper) res; + MapValue.Builder entries = MapValue.newBuilder(); + for (IteratorT i = mapper.iterator(); i.hasNext() == True; ) { + Val key = i.next(); + entries + .addEntriesBuilder() + .setKey(refValueToValue(key)) + .setValue(refValueToValue(mapper.get(key))); + } + return Value.newBuilder().setMapValue(entries).build(); + case Object: + Message pb = (Message) res.value(); + Value.Builder value = Value.newBuilder(); + if (pb instanceof Any) { + value.setObjectValue(unwrapNestedAny((Any) pb)); + } else if (pb instanceof ListValue) { + value.setListValue((ListValue) pb); + } else if (pb instanceof MapValue) { + value.setMapValue((MapValue) pb); + } else { + value.setObjectValue(Any.pack(pb)); + } + return value.build(); + default: + throw new IllegalStateException(String.format("Unknown %s", res.type().typeEnum())); + } + } + + private static Any unwrapNestedAny(Any any) { + Any current = any; + while (current.is(Any.class)) { + try { + Any next = current.unpack(Any.class); + if (next.equals(current)) { + return current; + } + current = next; + } catch (InvalidProtocolBufferException e) { + return current; + } + } + return current; + } + + private static T convert(Message message, Class targetType) + throws InvalidProtocolBufferException { + try { + @SuppressWarnings("unchecked") + T converted = + (T) targetType.getMethod("parseFrom", byte[].class).invoke(null, message.toByteArray()); + return converted; + } catch (ReflectiveOperationException e) { + Throwable cause = e.getCause(); + if (cause instanceof InvalidProtocolBufferException) { + throw (InvalidProtocolBufferException) cause; + } + throw new IllegalStateException("cannot convert to " + targetType.getName(), e); + } + } + + private static String print(Message message) { + return TEXT_PRINTER.printToString(message); + } + + private static final class SkipList { + private SkipList() {} + + private static Set parse(String... values) { + Set skipTests = new LinkedHashSet<>(); + for (String value : values) { + parse(value, skipTests); + } + return Set.copyOf(skipTests); + } + + private static void parse(String value, Set skipTests) { + int fileSeparator = value.indexOf('/'); + if (fileSeparator < 1 || fileSeparator == value.length() - 1) { + throw new IllegalArgumentException( + "skip argument must contain at least /
: " + value); + } + + String fileName = value.substring(0, fileSeparator); + String sectionString = value.substring(fileSeparator + 1); + for (String sectionValue : sectionString.split(";")) { + int sectionSeparator = sectionValue.indexOf('/'); + if (sectionSeparator < 0) { + skipTests.add(fileName + "/" + sectionValue); + } else { + String sectionName = sectionValue.substring(0, sectionSeparator); + String testString = sectionValue.substring(sectionSeparator + 1); + for (String test : testString.split(",")) { + skipTests.add(fileName + "/" + sectionName + "/" + test); + } + } + } + } + } +} diff --git a/conformance/src/test/java/org/projectnessie/cel/server/ConformanceServerTest.java b/conformance/src/test/java/org/projectnessie/cel/server/ConformanceServerTest.java deleted file mode 100644 index ce79883b..00000000 --- a/conformance/src/test/java/org/projectnessie/cel/server/ConformanceServerTest.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright (C) 2021 The Authors of CEL-Java - * - * 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 - * - * http://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 org.projectnessie.cel.server; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.projectnessie.cel.TestExpr.ExprCall; -import static org.projectnessie.cel.TestExpr.ExprLiteral; -import static org.projectnessie.cel.Util.mapOf; - -import com.google.api.expr.conformance.v1alpha1.CheckRequest; -import com.google.api.expr.conformance.v1alpha1.CheckResponse; -import com.google.api.expr.conformance.v1alpha1.ConformanceServiceGrpc; -import com.google.api.expr.conformance.v1alpha1.ConformanceServiceGrpc.ConformanceServiceBlockingStub; -import com.google.api.expr.conformance.v1alpha1.EvalRequest; -import com.google.api.expr.conformance.v1alpha1.EvalResponse; -import com.google.api.expr.conformance.v1alpha1.ParseRequest; -import com.google.api.expr.conformance.v1alpha1.ParseResponse; -import com.google.api.expr.v1alpha1.CheckedExpr; -import com.google.api.expr.v1alpha1.Constant; -import com.google.api.expr.v1alpha1.Constant.ConstantKindCase; -import com.google.api.expr.v1alpha1.Expr; -import com.google.api.expr.v1alpha1.Expr.Call; -import com.google.api.expr.v1alpha1.Expr.ExprKindCase; -import com.google.api.expr.v1alpha1.ExprValue; -import com.google.api.expr.v1alpha1.ExprValue.KindCase; -import com.google.api.expr.v1alpha1.ParsedExpr; -import com.google.api.expr.v1alpha1.SourceInfo; -import com.google.api.expr.v1alpha1.Type; -import com.google.api.expr.v1alpha1.Type.PrimitiveType; -import com.google.api.expr.v1alpha1.Type.TypeKindCase; -import com.google.api.expr.v1alpha1.Value; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Server; -import io.grpc.ServerBuilder; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.projectnessie.cel.checker.Decls; -import org.projectnessie.cel.common.operators.Operator; - -class ConformanceServerTest { - - private static Server server; - private static ConformanceServiceBlockingStub stub; - private static ManagedChannel channel; - - private static ParsedExpr parsed; - - @BeforeAll - static void startServer() throws Exception { - server = ServerBuilder.forPort(0).addService(new ConformanceServiceImpl()).build(); - server.start(); - - String host = ConformanceServer.getListenHost(server); - - channel = ManagedChannelBuilder.forAddress(host, server.getPort()).usePlaintext().build(); - stub = ConformanceServiceGrpc.newBlockingStub(channel); - - parsed = - ParsedExpr.newBuilder() - .setExpr(ExprCall(1, Operator.Add.id, ExprLiteral(2, 1L), ExprLiteral(3, 1L))) - .setSourceInfo( - SourceInfo.newBuilder() - .setLocation("the location") - .putAllPositions( - mapOf( - 1L, 0, - 2L, 0, - 3L, 4)) - .build()) - .build(); - } - - @AfterAll - static void stopServer() throws Exception { - Exception x = null; - try { - channel.shutdown(); - } catch (Exception e) { - x = e; - } - - try { - server.shutdown(); - } catch (Exception e) { - if (x == null) x = e; - else x.addSuppressed(e); - } - - try { - channel.awaitTermination(30, TimeUnit.SECONDS); - } catch (Exception e) { - if (x == null) x = e; - else x.addSuppressed(e); - } - - try { - server.awaitTermination(30, TimeUnit.SECONDS); - } catch (Exception e) { - if (x == null) x = e; - else x.addSuppressed(e); - } - - if (x != null) throw x; - } - - /** TestParse tests the Parse method. */ - @Test - void Parse() { - ParseRequest req = ParseRequest.newBuilder().setCelSource("1 + 1").build(); - ParseResponse res = stub.parse(req); - assertThat(res.isInitialized()).isTrue(); - assertThat(res.getParsedExpr().isInitialized()).isTrue(); - // Could check against 'parsed' above, - // but the expression ids are arbitrary, - // and explicit comparison logic is about as - // much work as normalization would be. - assertThat(res.getParsedExpr().getExpr().isInitialized()).isTrue(); - assertThat(res.getParsedExpr().getExpr().getExprKindCase()).isSameAs(ExprKindCase.CALL_EXPR); - - Call c = res.getParsedExpr().getExpr().getCallExpr(); - assertThat(c.getTarget().isInitialized()).isTrue(); - assertThat(c.getFunction()).isEqualTo("_+_"); - assertThat(c.getArgsCount()).isEqualTo(2); - for (Expr a : c.getArgsList()) { - assertThat(a.getExprKindCase()).isSameAs(ExprKindCase.CONST_EXPR); - Constant l = a.getConstExpr(); - assertThat(l.getConstantKindCase()).isSameAs(ConstantKindCase.INT64_VALUE); - assertThat(l.getInt64Value()).isEqualTo(1); - } - } - - /** TestCheck tests the Check method. */ - @Test - void Check() { - // If TestParse() passes, it validates a good chunk - // of the server mechanisms for data conversion, so we - // won't be as fussy here.. - CheckRequest req = CheckRequest.newBuilder().setParsedExpr(parsed).build(); - CheckResponse res = stub.check(req); - assertThat(res.isInitialized()).isTrue(); - assertThat(res.getCheckedExpr().isInitialized()).isTrue(); - Type tp = res.getCheckedExpr().getTypeMapMap().get(1L); - assertThat(tp).isNotNull(); - assertThat(tp.getTypeKindCase()).isSameAs(TypeKindCase.PRIMITIVE); - assertThat(tp.getPrimitive()).isSameAs(PrimitiveType.INT64); - } - - /** TestEval tests the Eval method. */ - @Test - void Eval() { - EvalRequest req = EvalRequest.newBuilder().setParsedExpr(parsed).build(); - EvalResponse res = stub.eval(req); - assertThat(res.isInitialized()).isTrue(); - assertThat(res.getResult().isInitialized()).isTrue(); - - assertThat(res.getResult().getKindCase()).isSameAs(KindCase.VALUE); - assertThat(res.getResult().getValue().getKindCase()).isSameAs(Value.KindCase.INT64_VALUE); - assertThat(res.getResult().getValue().getInt64Value()).isEqualTo(2L); - } - - /** TestFullUp tests Parse, Check, and Eval back-to-back. */ - @Test - void FullUp() { - ParseRequest preq = ParseRequest.newBuilder().setCelSource("x + y").build(); - ParseResponse pres = stub.parse(preq); - assertThat(pres.isInitialized()).isTrue(); - ParsedExpr parsedExpr = pres.getParsedExpr(); - assertThat(parsedExpr.isInitialized()).isTrue(); - - CheckRequest creq = - CheckRequest.newBuilder() - .setParsedExpr(parsedExpr) - .addTypeEnv(Decls.newVar("x", Decls.Int)) - .addTypeEnv(Decls.newVar("y", Decls.Int)) - .build(); - CheckResponse cres = stub.check(creq); - assertThat(cres.isInitialized()).isTrue(); - CheckedExpr checkedExpr = cres.getCheckedExpr(); - assertThat(checkedExpr.isInitialized()).isTrue(); - - Type tp = checkedExpr.getTypeMapMap().get(1L); - assertThat(tp).isNotNull(); - assertThat(tp.getTypeKindCase()).isSameAs(TypeKindCase.PRIMITIVE); - assertThat(tp.getPrimitive()).isSameAs(PrimitiveType.INT64); - - EvalRequest ereq = - EvalRequest.newBuilder() - .setCheckedExpr(checkedExpr) - .putBindings("x", exprValueInt64(1)) - .putBindings("y", exprValueInt64(2)) - .build(); - EvalResponse eres = stub.eval(ereq); - assertThat(eres.isInitialized()).isTrue(); - assertThat(eres.getResult().isInitialized()).isTrue(); - - assertThat(eres.getResult().getKindCase()).isSameAs(KindCase.VALUE); - assertThat(eres.getResult().getValue().getKindCase()).isSameAs(Value.KindCase.INT64_VALUE); - assertThat(eres.getResult().getValue().getInt64Value()).isEqualTo(3L); - } - - static ExprValue exprValueInt64(long x) { - return ExprValue.newBuilder().setValue(Value.newBuilder().setInt64Value(x)).build(); - } - - static class FullPipelineResult { - final ParseResponse parseResponse; - final CheckResponse checkResponse; - final EvalResponse evalResponse; - - FullPipelineResult( - ParseResponse parseResponse, CheckResponse checkResponse, EvalResponse evalResponse) { - this.parseResponse = parseResponse; - this.checkResponse = checkResponse; - this.evalResponse = evalResponse; - } - } - - /** - * fullPipeline parses, checks, and evaluates the CEL expression in source and returns the result - * from the Eval call. - */ - FullPipelineResult fullPipeline(String source) { - - // Parse - ParseRequest preq = ParseRequest.newBuilder().setCelSource(source).build(); - ParseResponse pres = stub.parse(preq); - assertThat(pres.isInitialized()).isTrue(); - ParsedExpr parsedExpr = pres.getParsedExpr(); - assertThat(parsedExpr.isInitialized()).isTrue(); - assertThat(parsedExpr.getExpr().isInitialized()).isTrue(); - - // Check - CheckRequest creq = CheckRequest.newBuilder().setParsedExpr(parsedExpr).build(); - CheckResponse cres = stub.check(creq); - assertThat(cres.isInitialized()).isTrue(); - CheckedExpr checkedExpr = cres.getCheckedExpr(); - assertThat(checkedExpr.isInitialized()).isTrue(); - - // Eval - EvalRequest ereq = EvalRequest.newBuilder().setCheckedExpr(checkedExpr).build(); - EvalResponse eres = stub.eval(ereq); - assertThat(eres.isInitialized()).isTrue(); - assertThat(eres.getResult().isInitialized()).isTrue(); - - return new FullPipelineResult(pres, cres, eres); - } - - /** - * expectEvalTrue parses, checks, and evaluates the CEL expression in source and checks that the - * result is the boolean value 'true'. - */ - void expectEvalTrue(String source) { - FullPipelineResult fp = fullPipeline(source); - - long rootID = fp.parseResponse.getParsedExpr().getExpr().getId(); - Type topType = fp.checkResponse.getCheckedExpr().getTypeMapMap().get(rootID); - assertThat(topType).extracting(Type::getTypeKindCase).isEqualTo(Type.TypeKindCase.PRIMITIVE); - assertThat(topType).extracting(Type::getPrimitive).isEqualTo(Type.PrimitiveType.BOOL); - - ExprValue er = fp.evalResponse.getResult(); - assertThat(er).extracting(ExprValue::getKindCase).isEqualTo(ExprValue.KindCase.VALUE); - Value ev = er.getValue(); - assertThat(ev).extracting(Value::getKindCase).isEqualTo(Value.KindCase.BOOL_VALUE); - assertThat(ev).extracting(Value::getBoolValue).isEqualTo(true); - } - - /** TestCondTrue tests true conditional behavior. */ - @Test - void CondTrue() { - expectEvalTrue("(true ? 'a' : 'b') == 'a'"); - } - - /** TestCondFalse tests false conditional behavior. */ - @Test - void CondFalse() { - expectEvalTrue("(false ? 'a' : 'b') == 'b'"); - } - - /** TestMapOrderInsignificant tests that maps with different order are equal. */ - @Test - void MapOrderInsignificant() { - expectEvalTrue("{1: 'a', 2: 'b'} == {2: 'b', 1: 'a'}"); - } - - /** FailsTestOneMetaType tests that types of different types are equal. */ - @Test - void FailsTestOneMetaType() { - expectEvalTrue("type(type(1)) == type(type('foo'))"); - } - - /** FailsTestTypeType tests that the meta-type is its own type. */ - @Test - void FailsTestTypeType() { - expectEvalTrue("type(type) == type"); - } - - /** FailsTestNullTypeName checks that the type of null is "null_type". */ - @Test - void FailsTestNullTypeName() { - expectEvalTrue("type(null) == null_type"); - } - - /** TestError ensures that errors are properly transmitted. */ - @Test - void Error() { - FullPipelineResult fp = fullPipeline("1 / 0"); - assertThat(fp.evalResponse.getResult().getKindCase()).isEqualTo(ExprValue.KindCase.ERROR); - } -} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 67a2b529..52175403 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -24,14 +24,17 @@ plugins { `java-test-fixtures` } +val congocc = configurations.create("congocc") + configurations.named("jmhImplementation") { extendsFrom(configurations.testFixturesApi.get()) } dependencies { - implementation(project(":cel-generated-antlr")) compileOnly(project(":cel-generated-pb")) implementation(libs.agrona) + congocc(libs.congocc) + testImplementation(project(":cel-generated-pb")) testFixturesApi(platform(libs.junit.bom)) testFixturesApi(libs.bundles.junit.testing) @@ -46,13 +49,59 @@ dependencies { jmhAnnotationProcessor(libs.jmh.generator.annprocess) } +abstract class Generate : JavaExec() { + init { + outputs.cacheIf { true } + } + + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val sourceDir: DirectoryProperty + + @get:OutputDirectory abstract val outputDir: DirectoryProperty +} + +val generateCelGrammar = + tasks.register("generateCelGrammar") { + val generatedDir = layout.buildDirectory.dir("generated/sources/congocc/cel") + val projectDir = layout.projectDirectory + + sourceDir = projectDir.dir("src/main/congocc/cel") + outputDir = generatedDir + + classpath(congocc) + mainClass = "org.congocc.app.Main" + workingDir(projectDir) + + doFirst { generatedDir.get().asFile.deleteRecursively() } + + argumentProviders.add( + CommandLineArgumentProvider { + val sourceFile = sourceDir.file("cel-java.ccc").get().asFile.relativeTo(projectDir.asFile) + val base = + listOf( + "-d", + generatedDir.get().asFile.toString(), + "-jdk17", + "-n", + sourceFile.toString(), + ) + if (logger.isInfoEnabled) base else base + "-q" + } + ) + } + jmh { jmhVersion.set(libs.versions.jmh.get()) } +sourceSets.main { java.srcDir(generateCelGrammar) } + sourceSets.test { java.srcDir(layout.buildDirectory.dir("generated/source/proto/test/java")) java.destinationDirectory.set(layout.buildDirectory.dir("classes/java/generatedTest")) } +tasks.named("compileJava") { dependsOn(generateCelGrammar) } + tasks.named("check") { dependsOn(tasks.named("jmh")) } tasks.named("assemble") { dependsOn(tasks.named("jmhJar")) } diff --git a/core/src/jmh/java/org/projectnessie/cel/CompileBuildBench.java b/core/src/jmh/java/org/projectnessie/cel/CompileBuildBench.java new file mode 100644 index 00000000..8fe49da5 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/CompileBuildBench.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.declarations; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry; +import org.projectnessie.cel.tools.Script; +import org.projectnessie.cel.tools.ScriptHost; + +@Warmup(iterations = 1, time = 1500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(2) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class CompileBuildBench { + + @State(Scope.Thread) + public static class CompileState { + @Param({"simplePredicate", "deepSelectors", "macroPipeline"}) + public String expression; + + String source() { + return switch (expression) { + case "simplePredicate" -> "resource == 'projects/p1' && user == 'alice'"; + case "deepSelectors" -> + "request.auth.claims.email.endsWith('@example.com')" + + " && request.resource.labels['env'] == 'prod'"; + case "macroPipeline" -> + "items.filter(i, i.score > 10).map(i, i.name).exists(n, n.startsWith('a'))"; + default -> + throw new IllegalArgumentException( + "Unknown compile benchmark expression: " + expression); + }; + } + } + + @Benchmark + public void envCompileAndProgram(CompileState state, Blackhole blackhole) { + Env env = + newEnv( + declarations( + Decls.newVar("resource", Decls.String), + Decls.newVar("user", Decls.String), + Decls.newVar("request", Decls.Dyn), + Decls.newVar("items", Decls.newListType(Decls.Dyn)))); + AstIssuesTuple ast = env.compile(state.source()); + if (ast.hasIssues()) { + throw ast.getIssues().err(); + } + blackhole.consume(env.program(ast.getAst())); + } + + @Benchmark + public void scriptHostBuild(CompileState state, Blackhole blackhole) throws Exception { + ScriptHost host = ScriptHost.newBuilder().build(); + Script script = + host.buildScript(state.source()) + .withDeclarations( + Decls.newVar("resource", Decls.String), + Decls.newVar("user", Decls.String), + Decls.newVar("request", Decls.Dyn), + Decls.newVar("items", Decls.newListType(Decls.Dyn))) + .build(); + blackhole.consume(script); + } + + @Benchmark + public void protoRegistryCreation(Blackhole blackhole) { + blackhole.consume(ProtoTypeRegistry.newRegistry()); + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/common/types/AdapterAllocationBench.java b/core/src/jmh/java/org/projectnessie/cel/common/types/AdapterAllocationBench.java index 6c44576e..6d398054 100644 --- a/core/src/jmh/java/org/projectnessie/cel/common/types/AdapterAllocationBench.java +++ b/core/src/jmh/java/org/projectnessie/cel/common/types/AdapterAllocationBench.java @@ -87,30 +87,19 @@ public void nativeToValue(NativeValueState state, Blackhole blackhole) { } private static Object value(String kind, int size) { - switch (kind) { - case "arrayList": - return list(size); - case "linkedHashSet": - return set(size); - case "objectArray": - return list(size).toArray(); - case "stringArray": - return stringArray(size); - case "intArray": - return intArray(size); - case "longArray": - return longArray(size); - case "doubleArray": - return doubleArray(size); - case "mapStringInt": - return mapStringInt(size); - case "mapValVal": - return mapValVal(size); - case "listValue": - return listValue(size); - default: - throw new IllegalArgumentException("Unknown native value kind: " + kind); - } + return switch (kind) { + case "arrayList" -> list(size); + case "linkedHashSet" -> set(size); + case "objectArray" -> list(size).toArray(); + case "stringArray" -> stringArray(size); + case "intArray" -> intArray(size); + case "longArray" -> longArray(size); + case "doubleArray" -> doubleArray(size); + case "mapStringInt" -> mapStringInt(size); + case "mapValVal" -> mapValVal(size); + case "listValue" -> listValue(size); + default -> throw new IllegalArgumentException("Unknown native value kind: " + kind); + }; } private static List list(int size) { diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/AttributesBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/AttributesBench.java index fba39a39..6e112ed2 100644 --- a/core/src/jmh/java/org/projectnessie/cel/interpreter/AttributesBench.java +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/AttributesBench.java @@ -25,9 +25,9 @@ import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; import static org.projectnessie.cel.interpreter.Interpretable.newConstValue; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedMessage; import com.google.api.expr.v1alpha1.Type; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -120,9 +120,9 @@ public BenchmarkResolverFieldQualifierState() { TypeRegistry reg = newRegistry(msg); Activation vars = newActivation(mapOf("msg", msg)); - Type opType = reg.findType("google.api.expr.test.v1.proto3.TestAllTypes"); + Type opType = reg.findType("cel.expr.conformance.proto3.TestAllTypes"); assertThat(opType).isNotNull(); - Type fieldType = reg.findType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage"); + Type fieldType = reg.findType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); assertThat(fieldType).isNotNull(); AttributeFactory attrs = newAttributeFactory(Container.defaultContainer, reg, reg); @@ -157,7 +157,7 @@ public BenchmarkResolverCustomQualifierState() { Qualifier qualBB = attrs.newQualifier( Type.newBuilder() - .setMessageType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage") + .setMessageType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage") .build(), 2, "bb"); diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/InterpreterAllocationBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/InterpreterAllocationBench.java index b71d4e04..ad105138 100644 --- a/core/src/jmh/java/org/projectnessie/cel/interpreter/InterpreterAllocationBench.java +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/InterpreterAllocationBench.java @@ -21,7 +21,7 @@ import static org.projectnessie.cel.ProgramOption.evalOptions; import static org.projectnessie.cel.Util.mapOf; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -54,7 +54,7 @@ @OutputTimeUnit(TimeUnit.MICROSECONDS) public class InterpreterAllocationBench { - private static final String TEST_ALL_TYPES = "google.api.expr.test.v1.proto3.TestAllTypes"; + private static final String TEST_ALL_TYPES = "cel.expr.conformance.proto3.TestAllTypes"; @State(Scope.Benchmark) public static class JavaInputState { @@ -142,7 +142,13 @@ public void dynamicMapLiteral(DynamicMapLiteralState state, Blackhole blackhole) @State(Scope.Benchmark) public static class ProtoInputState { - @Param({"mapLookup", "repeatedUintExistsEarly", "repeatedUintExistsLate"}) + @Param({ + "mapLookup", + "mapLookupMiss", + "mapLookupRepeated", + "repeatedUintExistsEarly", + "repeatedUintExistsLate" + }) public String kind; @Param({"10", "1000"}) @@ -164,6 +170,24 @@ public void init() { mapOf( "msg", protoMessage(size), "key", "key-" + (size - 1), "target", (long) size - 1); return; + case "mapLookupMiss": + program = + protoProgram( + "msg.map_string_uint64[key] == target", + Decls.newVar("key", Decls.String), + Decls.newVar("target", Decls.Uint)); + vars = mapOf("msg", protoMessage(size), "key", "missing", "target", 0L); + return; + case "mapLookupRepeated": + program = + protoProgram( + "msg.map_string_uint64[key] == target && msg.map_string_uint64[key] == target", + Decls.newVar("key", Decls.String), + Decls.newVar("target", Decls.Uint)); + vars = + mapOf( + "msg", protoMessage(size), "key", "key-" + (size - 1), "target", (long) size - 1); + return; case "repeatedUintExistsEarly": program = protoProgram( diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/MacroRuntimeBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/MacroRuntimeBench.java new file mode 100644 index 00000000..fde49352 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/MacroRuntimeBench.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.Util.mapOf; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.EvalOption; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.checker.Decls; + +@Warmup(iterations = 1, time = 1500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(2) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class MacroRuntimeBench { + + @State(Scope.Benchmark) + public static class MacroState { + @Param({"existsEarly", "existsLate", "all", "filter", "map"}) + public String expression; + + @Param({"10", "1000"}) + public int size; + + Program program; + Map vars; + + @Setup + public void init() { + switch (expression) { + case "existsEarly": + program = program("items.exists(i, i == target)"); + vars = mapOf("items", list(size), "target", 0L); + return; + case "existsLate": + program = program("items.exists(i, i == target)"); + vars = mapOf("items", list(size), "target", (long) size - 1); + return; + case "all": + program = program("items.all(i, i >= 0)"); + vars = mapOf("items", list(size), "target", (long) size - 1); + return; + case "filter": + program = program("items.filter(i, i > target)"); + vars = mapOf("items", list(size), "target", (long) size / 2); + return; + case "map": + program = program("items.map(i, i + 1)"); + vars = mapOf("items", list(size), "target", (long) size - 1); + return; + default: + throw new IllegalArgumentException("Unknown macro benchmark expression: " + expression); + } + } + } + + @Benchmark + public void macroRuntime(MacroState state, Blackhole blackhole) { + blackhole.consume(state.program.eval(state.vars)); + } + + private static Program program(String expression) { + Env env = + newEnv( + declarations( + Decls.newVar("items", Decls.newListType(Decls.Int)), + Decls.newVar("target", Decls.Int))); + AstIssuesTuple ast = env.compile(expression); + if (ast.hasIssues()) { + throw ast.getIssues().err(); + } + return env.program(ast.getAst(), evalOptions(EvalOption.OptOptimize)); + } + + private static List list(int size) { + List values = new ArrayList<>(size); + for (long i = 0; i < size; i++) { + values.add(i); + } + return values; + } +} diff --git a/core/src/main/congocc/cel/cel-java.ccc b/core/src/main/congocc/cel/cel-java.ccc new file mode 100644 index 00000000..07ac96ee --- /dev/null +++ b/core/src/main/congocc/cel/cel-java.ccc @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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. + */ + +PARSER_PACKAGE="org.projectnessie.cel.parser"; +PARSER_CLASS=CelGrammarParser; +LEXER_CLASS=CelGrammarLexer; +NODE_PACKAGE="org.projectnessie.cel.parser.ast"; + +INCLUDE "cel.ccc" + +INJECT PARSER_CLASS : +{ + private boolean nextTokenStartsExpression() { + TokenType type = getToken(1).getType(); + return switch (type) { + case LBRACKET, LBRACE, LPAREN, DOT, MINUS, EXCLAM, TRUE, FALSE, NULL, NUM_UINT, NUM_FLOAT, + NUM_INT, STRING, BYTES, IDENTIFIER -> true; + default -> false; + }; + } +} + +INJECT ParseException : +{ + @SuppressWarnings("unchecked") + public java.util.Set getExpectedTokenTypes() { + java.util.Set x = expectedTypes; + return x; + } +} + +INJECT Expr : implements CelExprNode; +INJECT Expr : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitExpr(this); + } +} + +INJECT ConditionalOr : implements CelExprNode; +INJECT ConditionalOr : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitBalanced(this, org.projectnessie.cel.common.operators.Operator.LogicalOr); + } +} + +INJECT ConditionalAnd : implements CelExprNode; +INJECT ConditionalAnd : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitBalanced(this, org.projectnessie.cel.common.operators.Operator.LogicalAnd); + } +} + +INJECT Relation : implements CelExprNode; +INJECT Relation : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitBinary(this); + } +} + +INJECT Calc : implements CelExprNode; +INJECT Calc : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitBinary(this); + } +} + +INJECT Multiplicative : implements CelExprNode; +INJECT Multiplicative : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitBinary(this); + } +} + +INJECT Unary : implements CelExprNode; +INJECT Unary : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitUnary(this); + } +} + +INJECT Member : implements CelExprNode; +INJECT Member : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitMember(this); + } +} + +INJECT Primary : implements CelExprNode; +INJECT Primary : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitPrimary(this); + } +} + +INJECT ConstantLiteral : implements CelExprNode; +INJECT ConstantLiteral : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitLiteral(this); + } +} + +INJECT Literal : implements CelExprNode; +INJECT Literal : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitLiteral(this); + } +} + +INJECT Identifier : implements CelExprNode; +INJECT Identifier : +{ + @Override + public com.google.api.expr.v1alpha1.Expr toCelExpr(CelExprBuilder builder) { + return builder.visitIdentifier(this); + } +} diff --git a/core/src/main/congocc/cel/cel-lexer.ccc b/core/src/main/congocc/cel/cel-lexer.ccc new file mode 100644 index 00000000..62c4c32f --- /dev/null +++ b/core/src/main/congocc/cel/cel-lexer.ccc @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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. + */ + +SKIP : #Whitespace; + +UNPARSED : #Comment; + +TOKEN #Operator : + + | ="> + | + | + | + | + | "> + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + ; + +TOKEN #Literal : + + | + | + | <#HEXDIGIT : ["0"-"9", "a"-"f", "A"-"F"]> + | <#DIGIT : ["0"-"9"]> + | <#EXPONENT : ["e", "E"] (["+", "-"])? ()+> + | )+ | "0x" ()+) ["u", "U"]> + | )+ "." ()+ ()? | ()+ | "." ()+ ()?)> + | )+ | "0x" ()+> + | <#ESC_CHAR_SEQ : "\\" ["a", "b", "f", "n", "r", "t", "v", "\"", "'", "\\", "?", "`"]> + | <#ESC_OCT_SEQ : "\\" ["0"-"3"] ["0"-"7"] ["0"-"7"]> + | <#ESC_BYTE_SEQ : "\\" ["x", "X"] > + | <#ESC_UNI_SEQ : "\\" "u" | "\\" "U" > + | <#ESC_SEQ : | | | > + | <#DQ_CHAR : | ~["\\", "\"", "\n", "\r"]> + | <#SQ_CHAR : | ~["\\", "'", "\n", "\r"]> + | <#TDQ_CHAR : | ~["\\"]> + | <#TSQ_CHAR : | ~["\\"]> + | )* "\"" + | "'" ()* "'" + | "\"\"\"" ()* "\"\"\"" + | "'''" ()* "'''" + | ["r", "R"] "\"" (~["\"", "\n", "\r"])* "\"" + | ["r", "R"] "'" (~["'", "\n", "\r"])* "'" + | ["r", "R"] "\"\"\"" (~[])* "\"\"\"" + | ["r", "R"] "'''" (~[])* "'''" + > + | > + ; + +TOKEN #Identifier : + + | | ~["\\", "`", "\n", "\r"])* "`"> + ; diff --git a/core/src/main/congocc/cel/cel.ccc b/core/src/main/congocc/cel/cel.ccc new file mode 100644 index 00000000..fbd02f89 --- /dev/null +++ b/core/src/main/congocc/cel/cel.ccc @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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. + */ + +INCLUDE "cel-lexer.ccc" + +Start : Expr! ! ; + +Expr : + ConditionalOr + [ ConditionalOr Expr] + ; + +ConditionalOr : + ConditionalAnd ( ConditionalAnd)*! + ; + +ConditionalAnd : + Relation ( Relation)*! + ; + +Relation : + Calc (( | | | | | | ) Calc)*! + ; + +Calc : + Multiplicative (( | ) Multiplicative)*! + ; + +Multiplicative : + Unary (( | | ) Unary)*! + ; + +Unary : + ( | )* Member + ; + +Member : + Primary + ( + [] Field [ ( | ExprList )] + | [] Expr + | [FieldInitializerList] [] + )*! + ; + +Primary : + [] [ ( | ExprList )] + | Expr + | ( | ListInitializerList [] ) + | ( | MapInitializerList [] ) + | ConstantLiteral + ; + +ExprList : + Expr ( Expr =>||)*! + ; + +ListInitializerList : + [] Expr ( [] Expr =>||)*! + ; + +FieldInitializerList : + [] Field Expr ( [] Field Expr =>||)*! + ; + +Field : + + | + ; + +MapInitializerList : + [] Expr Expr ( [] Expr Expr =>||)*! + ; + +ConstantLiteral : + + | + | + | + | + | + | + | + ; diff --git a/core/src/main/java/org/projectnessie/cel/CEL.java b/core/src/main/java/org/projectnessie/cel/CEL.java index 3e19325d..25d55a9c 100644 --- a/core/src/main/java/org/projectnessie/cel/CEL.java +++ b/core/src/main/java/org/projectnessie/cel/CEL.java @@ -153,8 +153,7 @@ private static Program initInterpretable( } // When the AST has been checked it contains metadata that can be used to speed up program // execution. - CheckedExpr checked = astToCheckedExpr(ast); - p.interpretable = p.interpreter.newInterpretable(checked, decs); + p.interpretable = p.interpreter.newInterpretable(ast.getExpr(), ast.refMap, ast.typeMap, decs); return p; } diff --git a/core/src/main/java/org/projectnessie/cel/Env.java b/core/src/main/java/org/projectnessie/cel/Env.java index 02b27346..0ddbb0f6 100644 --- a/core/src/main/java/org/projectnessie/cel/Env.java +++ b/core/src/main/java/org/projectnessie/cel/Env.java @@ -281,9 +281,8 @@ public Env extend(List opts) { // be immutable. Since it is possible to set the TypeProvider separately // from the TypeAdapter, the possible configurations which could use a // TypeRegistry as the base implementation are captured below. - if (this.adapter instanceof TypeRegistry && this.provider instanceof TypeRegistry) { - TypeRegistry adapterReg = (TypeRegistry) this.adapter; - TypeRegistry providerReg = (TypeRegistry) this.provider; + if (this.adapter instanceof TypeRegistry adapterReg + && this.provider instanceof TypeRegistry providerReg) { TypeRegistry reg = providerReg.copy(); provider = reg; // If the adapter and provider are the same object, set the adapter diff --git a/core/src/main/java/org/projectnessie/cel/EnvOption.java b/core/src/main/java/org/projectnessie/cel/EnvOption.java index 71f824af..9ddc2ba5 100644 --- a/core/src/main/java/org/projectnessie/cel/EnvOption.java +++ b/core/src/main/java/org/projectnessie/cel/EnvOption.java @@ -229,12 +229,11 @@ static EnvOption abbrevs(String... qualifiedNames) { */ static EnvOption types(List addTypes) { return e -> { - if (!(e.provider instanceof TypeRegistry)) { + if (!(e.provider instanceof TypeRegistry reg)) { throw new RuntimeException( String.format( "custom types not supported by provider: %s", e.provider.getClass().getName())); } - TypeRegistry reg = (TypeRegistry) e.provider; for (Object t : addTypes) { reg.register(t); } diff --git a/core/src/main/java/org/projectnessie/cel/Library.java b/core/src/main/java/org/projectnessie/cel/Library.java index e76f32a3..a3dedb5b 100644 --- a/core/src/main/java/org/projectnessie/cel/Library.java +++ b/core/src/main/java/org/projectnessie/cel/Library.java @@ -15,8 +15,6 @@ */ package org.projectnessie.cel; -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; import static org.projectnessie.cel.EnvOption.declarations; import static org.projectnessie.cel.EnvOption.macros; import static org.projectnessie.cel.ProgramOption.functions; @@ -79,13 +77,13 @@ final class StdLibrary implements Library { /** EnvOptions returns options for the standard CEL function declarations and macros. */ @Override public List getCompileOptions() { - return asList(declarations(StandardDeclarations), macros(AllMacros)); + return List.of(declarations(StandardDeclarations), macros(AllMacros)); } /** ProgramOptions returns function implementations for the standard CEL functions. */ @Override public List getProgramOptions() { - return singletonList(functions(standardOverloads())); + return List.of(functions(standardOverloads())); } } } diff --git a/core/src/main/java/org/projectnessie/cel/checker/Checker.java b/core/src/main/java/org/projectnessie/cel/checker/Checker.java index a823e005..5fdc2cc2 100644 --- a/core/src/main/java/org/projectnessie/cel/checker/Checker.java +++ b/core/src/main/java/org/projectnessie/cel/checker/Checker.java @@ -48,8 +48,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.projectnessie.cel.checker.Types.Kind; import org.projectnessie.cel.common.Location; import org.projectnessie.cel.common.Source; @@ -228,9 +230,11 @@ void checkIdent(Expr.Builder e) { Decl ident = env.lookupIdent(identExpr.getName()); if (ident != null) { setType(e, ident.getIdent().getType()); - setReference(e, newIdentReference(ident.getName(), ident.getIdent().getValue())); + String identName = + identExpr.getName().startsWith(".") ? "." + ident.getName() : ident.getName(); + setReference(e, newIdentReference(identName, ident.getIdent().getValue())); // Overwrite the identifier with its fully qualified name. - identExpr.setName(ident.getName()); + identExpr.setName(identName); return; } @@ -242,7 +246,7 @@ void checkSelect(Expr.Builder e) { Select.Builder sel = e.getSelectExprBuilder(); // Before traversing down the tree, try to interpret as qualified name. String qname = Container.toQualifiedName(e.build()); - if (qname != null) { + if (qname != null && !isQualifiedLocalVariableSelection(sel.getOperandBuilder())) { Decl ident = env.lookupIdent(qname); if (ident != null) { if (sel.getTestOnly()) { @@ -281,6 +285,13 @@ void checkSelect(Expr.Builder e) { resultType = fieldType.type; } break; + case kindAbstract: + if (isOptionalType(targetType)) { + resultType = Decls.newAbstractType("optional_type", Collections.singletonList(Decls.Dyn)); + } else { + errors.typeDoesNotSupportFieldSelection(location(e), targetType); + } + break; case kindTypeParam: // Set the operand type to DYN to prevent assignment to a potentionally incorrect type // at a later point in type-checking. The isAssignable call will update the type @@ -305,6 +316,20 @@ void checkSelect(Expr.Builder e) { setType(e, resultType); } + private static boolean isOptionalType(Type type) { + return type.hasAbstractType() && "optional_type".equals(type.getAbstractType().getName()); + } + + private boolean isQualifiedLocalVariableSelection(Expr.Builder e) { + if (e.getExprKindCase() == Expr.ExprKindCase.IDENT_EXPR) { + return env.hasLocalIdent(e.getIdentExpr().getName()); + } + if (e.getExprKindCase() == Expr.ExprKindCase.SELECT_EXPR) { + return isQualifiedLocalVariableSelection(e.getSelectExprBuilder().getOperandBuilder()); + } + return false; + } + void checkCall(Expr.Builder e) { // Note: similar logic exists within the `interpreter/planner.go`. If making changes here // please consider the impact on planner.go and consolidate implementations or mirror code @@ -409,12 +434,17 @@ OverloadResolution resolveOverload( // not a compatible call style. continue; } + if (overload.getParamsCount() != argTypes.size()) { + // not a compatible arity. + continue; + } Type overloadType = Decls.newFunctionType(overload.getResultType(), overload.getParamsList()); - if (overload.getTypeParamsCount() > 0) { + Set typeParams = collectOverloadTypeParams(overload); + if (!typeParams.isEmpty()) { // Instantiate overload's type with fresh type variables. Mapping substitutions = newMapping(); - for (String typePar : overload.getTypeParamsList()) { + for (String typePar : typeParams) { substitutions.add(Decls.newTypeParamType(typePar), newTypeVar()); } overloadType = substitute(substitutions, overloadType, false); @@ -450,10 +480,21 @@ OverloadResolution resolveOverload( void checkCreateList(Expr.Builder e) { CreateList.Builder create = e.getListExprBuilder(); Type elemType = null; + boolean[] optionalIndices = new boolean[create.getElementsCount()]; + for (int index : create.getOptionalIndicesList()) { + optionalIndices[index] = true; + } for (int i = 0; i < create.getElementsBuilderList().size(); i++) { Expr.Builder el = create.getElementsBuilderList().get(i); check(el); - elemType = joinTypes(location(el), elemType, getType(el)); + Type type = getType(el); + if (optionalIndices[i]) { + Type unwrapped = optionalValueType(type); + if (unwrapped != null) { + type = unwrapped; + } + } + elemType = joinTypes(location(el), elemType, type); } if (elemType == null) { // If the list is empty, assign free type var to elem type. @@ -482,7 +523,14 @@ void checkCreateMap(Expr.Builder e) { Expr.Builder val = ent.getValueBuilder(); check(val); - valueType = joinTypes(location(val), valueType, getType(val)); + Type type = getType(val); + if (ent.getOptionalEntry()) { + Type unwrapped = optionalValueType(type); + if (unwrapped != null) { + type = unwrapped; + } + } + valueType = joinTypes(location(val), valueType, type); } if (keyType == null) { // If the map is empty, assign free type variables to typeKey and value type. @@ -534,12 +582,26 @@ void checkCreateMessage(Expr.Builder e) { if (t != null) { fieldType = t.type; } - if (!isAssignable(fieldType, getType(value))) { + Type valueType = getType(value); + if (ent.getOptionalEntry()) { + Type unwrapped = optionalValueType(valueType); + if (unwrapped != null) { + valueType = unwrapped; + } + } + if (!isAssignable(fieldType, valueType)) { errors.fieldTypeMismatch(locationByID(ent.getId()), field, fieldType, getType(value)); } } } + private static Type optionalValueType(Type type) { + if (!isOptionalType(type) || type.getAbstractType().getParameterTypesCount() == 0) { + return null; + } + return type.getAbstractType().getParameterTypes(0); + } + void checkComprehension(Expr.Builder e) { Comprehension.Builder comp = e.getComprehensionExprBuilder(); check(comp.getIterRangeBuilder()); @@ -547,14 +609,23 @@ void checkComprehension(Expr.Builder e) { Type accuType = getType(comp.getAccuInitBuilder()); Type rangeType = getType(comp.getIterRangeBuilder()); Type varType; + Type var2Type = null; switch (kindOf(rangeType)) { case kindList: - varType = rangeType.getListType().getElemType(); + if (comp.getIterVar2().isEmpty()) { + varType = rangeType.getListType().getElemType(); + } else { + varType = Decls.Int; + var2Type = rangeType.getListType().getElemType(); + } break; case kindMap: // Ranges over the keys. varType = rangeType.getMapType().getKeyType(); + if (!comp.getIterVar2().isEmpty()) { + var2Type = rangeType.getMapType().getValueType(); + } break; case kindDyn: case kindError: @@ -565,10 +636,16 @@ void checkComprehension(Expr.Builder e) { isAssignable(Decls.Dyn, rangeType); // Set the range iteration variable to type DYN as well. varType = Decls.Dyn; + if (!comp.getIterVar2().isEmpty()) { + var2Type = Decls.Dyn; + } break; default: errors.notAComprehensionRange(location(comp.getIterRangeBuilder()), rangeType); varType = Decls.Error; + if (!comp.getIterVar2().isEmpty()) { + var2Type = Decls.Error; + } break; } @@ -579,6 +656,9 @@ void checkComprehension(Expr.Builder e) { // Create a block scope for the loop. env = env.enterScope(); env.add(Decls.newVar(comp.getIterVar(), varType)); + if (!comp.getIterVar2().isEmpty()) { + env.add(Decls.newVar(comp.getIterVar2(), var2Type)); + } // Check the variable references in the condition and step. check(comp.getLoopConditionBuilder()); assertType(comp.getLoopConditionBuilder(), Decls.Bool); @@ -678,7 +758,7 @@ void setType(Expr.Builder e, Type t) { } Type getType(Expr.Builder e) { - return types.get(e.getId()); + return substitute(mappings, types.get(e.getId()), false); } void setReference(Expr.Builder e, Reference r) { @@ -712,6 +792,44 @@ static OverloadResolution newResolution(Reference checkedRef, Type t) { return new OverloadResolution(checkedRef, t); } + private static Set collectOverloadTypeParams(Overload overload) { + Set typeParams = new LinkedHashSet<>(overload.getTypeParamsList()); + overload.getParamsList().forEach(type -> collectTypeParams(type, typeParams)); + collectTypeParams(overload.getResultType(), typeParams); + return typeParams; + } + + private static void collectTypeParams(Type type, Set typeParams) { + switch (kindOf(type)) { + case kindTypeParam: + typeParams.add(type.getTypeParam()); + return; + case kindAbstract: + type.getAbstractType() + .getParameterTypesList() + .forEach(t -> collectTypeParams(t, typeParams)); + return; + case kindFunction: + type.getFunction().getArgTypesList().forEach(t -> collectTypeParams(t, typeParams)); + collectTypeParams(type.getFunction().getResultType(), typeParams); + return; + case kindList: + collectTypeParams(type.getListType().getElemType(), typeParams); + return; + case kindMap: + MapType mapType = type.getMapType(); + collectTypeParams(mapType.getKeyType(), typeParams); + collectTypeParams(mapType.getValueType(), typeParams); + return; + case kindType: + if (type.getType() != Type.getDefaultInstance()) { + collectTypeParams(type.getType(), typeParams); + } + return; + default: + } + } + Location location(Expr.Builder e) { return locationByID(e.getId()); } diff --git a/core/src/main/java/org/projectnessie/cel/checker/CheckerEnv.java b/core/src/main/java/org/projectnessie/cel/checker/CheckerEnv.java index 55c3077a..cc68d810 100644 --- a/core/src/main/java/org/projectnessie/cel/checker/CheckerEnv.java +++ b/core/src/main/java/org/projectnessie/cel/checker/CheckerEnv.java @@ -36,6 +36,7 @@ import java.util.Collections; import java.util.List; import org.projectnessie.cel.common.containers.Container; +import org.projectnessie.cel.common.types.ref.TypeEnum; import org.projectnessie.cel.common.types.ref.TypeProvider; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.parser.Macro; @@ -118,6 +119,12 @@ public void add(List decls) { * such identifier is found in the Env. */ public Decl lookupIdent(String name) { + if (!name.startsWith(".") && hasLocalIdent(name)) { + Decl ident = declarations.findIdentInScope(name); + if (ident != null) { + return ident; + } + } for (String candidate : container.resolveCandidateNames(name)) { Decl ident = declarations.findIdent(candidate); if (ident != null) { @@ -146,10 +153,25 @@ public Decl lookupIdent(String name) { declarations.addIdent(decl); return decl; } + + Val identValue = provider.findIdent(candidate); + if (identValue != null && identValue.type().typeEnum() == TypeEnum.String) { + Decl decl = + Decls.newIdent( + candidate, + Decls.String, + Constant.newBuilder().setStringValue(identValue.value().toString()).build()); + declarations.addIdent(decl); + return decl; + } } return null; } + boolean hasLocalIdent(String name) { + return declarations.hasParent() && declarations.findIdentInScope(name) != null; + } + /** * LookupFunction returns a Decl proto for typeName as a function in env. Returns nil if no such * function is found in env. @@ -170,19 +192,28 @@ public Decl lookupFunction(String name) { */ Decl addOverload(Decl f, Overload overload, List errMsgs) { FunctionDecl function = f.getFunction(); - Mapping emptyMappings = newMapping(); - Type overloadFunction = - Decls.newFunctionType(overload.getResultType(), overload.getParamsList()); - Type overloadErased = substitute(emptyMappings, overloadFunction, true); + Mapping emptyMappings = null; + Type overloadFunction = null; + Type overloadErased = null; boolean hasErr = false; for (Overload existing : function.getOverloadsList()) { + if (overload.getIsInstanceFunction() != existing.getIsInstanceFunction() + || overload.getParamsCount() != existing.getParamsCount()) { + continue; + } + if (emptyMappings == null) { + emptyMappings = newMapping(); + overloadFunction = + Decls.newFunctionType(overload.getResultType(), overload.getParamsList()); + overloadErased = substitute(emptyMappings, overloadFunction, true); + } Type existingFunction = Decls.newFunctionType(existing.getResultType(), existing.getParamsList()); Type existingErased = substitute(emptyMappings, existingFunction, true); boolean overlap = isAssignable(emptyMappings, overloadErased, existingErased) != null || isAssignable(emptyMappings, existingErased, overloadErased) != null; - if (overlap && overload.getIsInstanceFunction() == existing.getIsInstanceFunction()) { + if (overlap) { errMsgs.add( overlappingOverloadError( f.getName(), diff --git a/core/src/main/java/org/projectnessie/cel/checker/Printer.java b/core/src/main/java/org/projectnessie/cel/checker/Printer.java index 6a62054b..c8152d80 100644 --- a/core/src/main/java/org/projectnessie/cel/checker/Printer.java +++ b/core/src/main/java/org/projectnessie/cel/checker/Printer.java @@ -35,11 +35,10 @@ static final class SemanticAdorner implements Adorner { @Override public String getMetadata(Object elem) { - if (!(elem instanceof Expr)) { + if (!(elem instanceof Expr e)) { return ""; } StringBuilder result = new StringBuilder(); - Expr e = (Expr) elem; Type t = checks.getTypeMapMap().get(e.getId()); if (t != null) { result.append("~"); diff --git a/core/src/main/java/org/projectnessie/cel/checker/Scopes.java b/core/src/main/java/org/projectnessie/cel/checker/Scopes.java index 7e21e558..3a966fd9 100644 --- a/core/src/main/java/org/projectnessie/cel/checker/Scopes.java +++ b/core/src/main/java/org/projectnessie/cel/checker/Scopes.java @@ -59,6 +59,10 @@ public Scopes pop() { return this; } + boolean hasParent() { + return parent != null; + } + /** * AddIdent adds the ident Decl in the current scope. Note: If the name collides with an existing * identifier in the scope, the Decl is overwritten. diff --git a/core/src/main/java/org/projectnessie/cel/checker/TypeErrors.java b/core/src/main/java/org/projectnessie/cel/checker/TypeErrors.java index 303e7b6c..10f1efda 100644 --- a/core/src/main/java/org/projectnessie/cel/checker/TypeErrors.java +++ b/core/src/main/java/org/projectnessie/cel/checker/TypeErrors.java @@ -117,10 +117,6 @@ void typeMismatch(Location l, Type expected, Type actual) { formatCheckedType(actual)); } - public void unknownType(Location l, String info) { - // reportError(l, "unknown type%s", info != null ? " for: " + info : ""); - } - static String formatFunction(Type resultType, List argTypes, boolean isInstance) { StringBuilder result = new StringBuilder(); formatFunction(result, resultType, argTypes, isInstance); diff --git a/core/src/main/java/org/projectnessie/cel/checker/Types.java b/core/src/main/java/org/projectnessie/cel/checker/Types.java index 1032310e..4209bd9a 100644 --- a/core/src/main/java/org/projectnessie/cel/checker/Types.java +++ b/core/src/main/java/org/projectnessie/cel/checker/Types.java @@ -62,22 +62,17 @@ public static String formatCheckedType(Type t) { case kindNull: return "null"; case kindPrimitive: - switch (t.getPrimitive()) { - case UINT64: - return "uint"; - case INT64: - return "int"; - case BOOL: - return "bool"; - case BYTES: - return "bytes"; - case DOUBLE: - return "double"; - case STRING: - return "string"; - } - // unrecognizes & not-specified - ignore above - return t.getPrimitive().toString().toLowerCase(Locale.ROOT).trim(); + return switch (t.getPrimitive()) { + case UINT64 -> "uint"; + case INT64 -> "int"; + case BOOL -> "bool"; + case BYTES -> "bytes"; + case DOUBLE -> "double"; + case STRING -> "string"; + default -> + // unrecognizes & not-specified - ignore above + t.getPrimitive().toString().toLowerCase(Locale.ROOT).trim(); + }; case kindWellKnown: switch (t.getWellKnown()) { case ANY: @@ -196,14 +191,11 @@ private static void formatCheckedTypePrimitive(StringBuilder sb, Type.PrimitiveT static boolean isDyn(Type t) { // Note: object type values that are well-known and map to a DYN value in practice // are sanitized prior to being added to the environment. - switch (kindOf(t)) { - case kindDyn: - return true; - case kindWellKnown: - return t.getWellKnown() == WellKnownType.ANY; - default: - return false; - } + return switch (kindOf(t)) { + case kindDyn -> true; + case kindWellKnown -> t.getWellKnown() == WellKnownType.ANY; + default -> false; + }; } /** isDynOrError returns true if the input is either an Error, DYN, or well-known ANY message. */ @@ -337,6 +329,9 @@ static boolean internalIsAssignable(Mapping m, Type t1, Type t2) { if (isDynOrError(t1) || isDynOrError(t2)) { return true; } + if (kind2 == Kind.kindNull) { + return internalIsAssignableNull(t1); + } // Test for when the types do not need to agree, but are more specific than dyn. switch (kind1) { @@ -353,28 +348,22 @@ static boolean internalIsAssignable(Mapping m, Type t1, Type t2) { } // Test for when the types must agree. - switch (kind1) { + return switch (kind1) { // ERROR, TYPE_PARAM, and DYN handled above. - case kindAbstract: - return internalIsAssignableAbstractType(m, t1.getAbstractType(), t2.getAbstractType()); - case kindFunction: - return internalIsAssignableFunction(m, t1.getFunction(), t2.getFunction()); - case kindList: - return internalIsAssignable( - m, t1.getListType().getElemType(), t2.getListType().getElemType()); - case kindMap: - return internalIsAssignableMap(m, t1.getMapType(), t2.getMapType()); - case kindObject: - return t1.getMessageType().equals(t2.getMessageType()); - case kindType: - // A type is a type is a type, any additional parameterization of the - // type cannot affect method resolution or assignability. - return true; - case kindWellKnown: - return t1.getWellKnown() == t2.getWellKnown(); - default: - return false; - } + case kindAbstract -> + internalIsAssignableAbstractType(m, t1.getAbstractType(), t2.getAbstractType()); + case kindFunction -> internalIsAssignableFunction(m, t1.getFunction(), t2.getFunction()); + case kindList -> + internalIsAssignable(m, t1.getListType().getElemType(), t2.getListType().getElemType()); + case kindMap -> internalIsAssignableMap(m, t1.getMapType(), t2.getMapType()); + case kindObject -> t1.getMessageType().equals(t2.getMessageType()); + case kindType -> + // A type is a type is a type, any additional parameterization of the + // type cannot affect method resolution or assignability. + true; + case kindWellKnown -> t1.getWellKnown() == t2.getWellKnown(); + default -> false; + }; } /** @@ -426,16 +415,10 @@ static boolean internalIsAssignableMap(Mapping m, MapType m1, MapType m2) { /** internalIsAssignableNull returns true if the type is nullable. */ static boolean internalIsAssignableNull(Type t) { - switch (kindOf(t)) { - case kindAbstract: - case kindObject: - case kindNull: - case kindWellKnown: - case kindWrapper: - return true; - default: - return false; - } + return switch (kindOf(t)) { + case kindAbstract, kindObject, kindNull, kindWellKnown, kindWrapper -> true; + default -> false; + }; } /** @@ -443,14 +426,11 @@ static boolean internalIsAssignableNull(Type t) { * for the primitive type. */ static boolean internalIsAssignablePrimitive(PrimitiveType p, Type target) { - switch (kindOf(target)) { - case kindPrimitive: - return p == target.getPrimitive(); - case kindWrapper: - return p == target.getWrapper(); - default: - return false; - } + return switch (kindOf(target)) { + case kindPrimitive -> p == target.getPrimitive(); + case kindWrapper -> p == target.getWrapper(); + default -> false; + }; } /** isAssignable returns an updated type substitution mapping if t1 is assignable to t2. */ @@ -476,37 +456,44 @@ static Kind kindOf(Type t) { if (t == null || t.getTypeKindCase() == TypeKindCase.TYPEKIND_NOT_SET) { return Kind.kindUnknown; } - switch (t.getTypeKindCase()) { - case ERROR: - return Kind.kindError; - case FUNCTION: - return Kind.kindFunction; - case DYN: - return Kind.kindDyn; - case PRIMITIVE: - return Kind.kindPrimitive; - case WELL_KNOWN: - return Kind.kindWellKnown; - case WRAPPER: - return Kind.kindWrapper; - case NULL: - return Kind.kindNull; - case TYPE: - return Kind.kindType; - case LIST_TYPE: - return Kind.kindList; - case MAP_TYPE: - return Kind.kindMap; - case MESSAGE_TYPE: - return Kind.kindObject; - case TYPE_PARAM: - return Kind.kindTypeParam; - } - return Kind.kindUnknown; + return switch (t.getTypeKindCase()) { + case ERROR -> Kind.kindError; + case FUNCTION -> Kind.kindFunction; + case DYN -> Kind.kindDyn; + case PRIMITIVE -> Kind.kindPrimitive; + case WELL_KNOWN -> Kind.kindWellKnown; + case WRAPPER -> Kind.kindWrapper; + case NULL -> Kind.kindNull; + case ABSTRACT_TYPE -> Kind.kindAbstract; + case TYPE -> Kind.kindType; + case LIST_TYPE -> Kind.kindList; + case MAP_TYPE -> Kind.kindMap; + case MESSAGE_TYPE -> Kind.kindObject; + case TYPE_PARAM -> Kind.kindTypeParam; + default -> Kind.kindUnknown; + }; } /** mostGeneral returns the more general of two types which are known to unify. */ static Type mostGeneral(Type t1, Type t2) { + Kind kind1 = kindOf(t1); + Kind kind2 = kindOf(t2); + if (kind1 == Kind.kindNull && internalIsAssignableNull(t2)) { + return t2; + } + if (kind2 == Kind.kindNull && internalIsAssignableNull(t1)) { + return t1; + } + if (kind1 == Kind.kindPrimitive && kind2 == Kind.kindWrapper) { + if (t1.getPrimitive() == t2.getWrapper()) { + return t2; + } + } + if (kind1 == Kind.kindWrapper && kind2 == Kind.kindPrimitive) { + if (t1.getWrapper() == t2.getPrimitive()) { + return t1; + } + } if (isEqualOrLessSpecific(t1, t2)) { return t1; } diff --git a/core/src/main/java/org/projectnessie/cel/common/CELError.java b/core/src/main/java/org/projectnessie/cel/common/CELError.java index 7bb93511..88d039c4 100644 --- a/core/src/main/java/org/projectnessie/cel/common/CELError.java +++ b/core/src/main/java/org/projectnessie/cel/common/CELError.java @@ -102,9 +102,7 @@ public String toDisplayString(Source source) { // sophisticated way, maybe use jline's WCWidth, but that one is also quite rudimentary wrt // code-blocks (e.g. doesn't know about emojis). result.append("\n | "); - for (int i = 0; i < location.column(); i++) { - result.append(dot); - } + result.append(String.valueOf(dot).repeat(Math.max(0, location.column()))); result.append(ind); } return result.toString(); diff --git a/core/src/main/java/org/projectnessie/cel/common/debug/Debug.java b/core/src/main/java/org/projectnessie/cel/common/debug/Debug.java index 8ea2e168..d3cbde99 100644 --- a/core/src/main/java/org/projectnessie/cel/common/debug/Debug.java +++ b/core/src/main/java/org/projectnessie/cel/common/debug/Debug.java @@ -363,9 +363,7 @@ void appendFormat(String f, Object... args) { void doIndent() { if (lineStart) { lineStart = false; - for (int i = 0; i < indent; i++) { - buffer.append(" "); - } + buffer.append(" ".repeat(Math.max(0, indent))); } } diff --git a/core/src/main/java/org/projectnessie/cel/common/operators/Operator.java b/core/src/main/java/org/projectnessie/cel/common/operators/Operator.java index 43ffdfe8..73f7744b 100644 --- a/core/src/main/java/org/projectnessie/cel/common/operators/Operator.java +++ b/core/src/main/java/org/projectnessie/cel/common/operators/Operator.java @@ -15,6 +15,10 @@ */ package org.projectnessie.cel.common.operators; +import static java.util.Map.copyOf; +import static java.util.Map.entry; +import static java.util.Map.ofEntries; + import java.util.HashMap; import java.util.Map; @@ -38,6 +42,8 @@ public enum Operator { Modulo("_%_", 3, "%"), Negate("-_", 2, "-"), Index("_[_]", 1, null), + OptionalSelect("@optional_select"), + OptionalIndex("@optional_index"), // Macros, must have a valid identifier. Has("has"), All("all"), @@ -71,30 +77,26 @@ public enum Operator { } static { - { - Map m = new HashMap<>(); - m.put("+", Add); - m.put("/", Divide); - m.put("==", Equals); - m.put(">", Greater); - m.put(">=", GreaterEquals); - m.put("in", In); - m.put("<", Less); - m.put("<=", LessEquals); - m.put("%", Modulo); - m.put("*", Multiply); - m.put("!=", NotEquals); - m.put("-", Subtract); - operators = m; - } + operators = + ofEntries( + entry("+", Add), + entry("/", Divide), + entry("==", Equals), + entry(">", Greater), + entry(">=", GreaterEquals), + entry("in", In), + entry("<", Less), + entry("<=", LessEquals), + entry("%", Modulo), + entry("*", Multiply), + entry("!=", NotEquals), + entry("-", Subtract)); - { - Map m = new HashMap<>(); - for (Operator op : Operator.values()) { - m.put(op.id, op); - } - operatorsById = m; + Map byId = new HashMap<>(); + for (Operator op : Operator.values()) { + byId.put(op.id, op); } + operatorsById = copyOf(byId); } public static Operator byId(String id) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/BoolT.java b/core/src/main/java/org/projectnessie/cel/common/types/BoolT.java index 3f0549e8..2868ad4e 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/BoolT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/BoolT.java @@ -93,37 +93,22 @@ public T convertToNative(Class typeDesc) { /** ConvertToType implements the ref.Val interface method. */ @Override public Val convertToType(Type typeVal) { - switch (typeVal.typeEnum()) { - case String: - return stringOf(Boolean.toString(b)); - case Bool: - return this; - case Type: - return BoolType; - } - return newTypeConversionError(BoolType, typeVal); + return switch (typeVal.typeEnum()) { + case String -> stringOf(Boolean.toString(b)); + case Bool -> this; + case Type -> BoolType; + default -> newTypeConversionError(BoolType, typeVal); + }; } /** Equal implements the ref.Val interface method. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Bool: - return Types.boolOf(b == ((BoolT) other).b); - case Null: - case Bytes: - case Double: - case Int: - case List: - case Map: - case Object: - case String: - case Type: - case Uint: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Bool -> Types.boolOf(b == ((BoolT) other).b); + case Null, Bytes, Double, Int, List, Map, Object, String, Type, Uint -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Negate implements the traits.Negater interface method. */ diff --git a/core/src/main/java/org/projectnessie/cel/common/types/BytesT.java b/core/src/main/java/org/projectnessie/cel/common/types/BytesT.java index dfc5a15f..ca2025bb 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/BytesT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/BytesT.java @@ -163,23 +163,11 @@ public Val convertToType(Type typeValue) { /** Equal implements the ref.Val interface method. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Bytes: - return boolOf(Arrays.equals(b, ((BytesT) other).b)); - case Null: - case Bool: - case Double: - case Int: - case List: - case Map: - case Object: - case String: - case Type: - case Uint: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Bytes -> boolOf(Arrays.equals(b, ((BytesT) other).b)); + case Null, Bool, Double, Int, List, Map, Object, String, Type, Uint -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Size implements the traits.Sizer interface method. */ diff --git a/core/src/main/java/org/projectnessie/cel/common/types/DurationT.java b/core/src/main/java/org/projectnessie/cel/common/types/DurationT.java index d3db8c5d..fde847bd 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/DurationT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/DurationT.java @@ -31,7 +31,6 @@ import java.time.Duration; import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -101,15 +100,16 @@ public static DurationT durationOf(Duration d) { return new DurationT(d); } - private static final Map> durationZeroArgOverloads; - - static { - durationZeroArgOverloads = new HashMap<>(); - durationZeroArgOverloads.put(Overloads.TimeGetHours, DurationT::timeGetHours); - durationZeroArgOverloads.put(Overloads.TimeGetMinutes, DurationT::timeGetMinutes); - durationZeroArgOverloads.put(Overloads.TimeGetSeconds, DurationT::timeGetSeconds); - durationZeroArgOverloads.put(Overloads.TimeGetMilliseconds, DurationT::timeGetMilliseconds); - } + private static final Map> durationZeroArgOverloads = + Map.of( + Overloads.TimeGetHours, + DurationT::timeGetHours, + Overloads.TimeGetMinutes, + DurationT::timeGetMinutes, + Overloads.TimeGetSeconds, + DurationT::timeGetSeconds, + Overloads.TimeGetMilliseconds, + DurationT::timeGetMilliseconds); private final Duration d; @@ -212,30 +212,23 @@ private String toPbString() { /** ConvertToType implements ref.Val.ConvertToType. */ @Override public Val convertToType(Type typeValue) { - switch (typeValue.typeEnum()) { - case String: - return stringOf(toPbString()); - case Int: - return IntT.intOf(toJavaLong()); - case Duration: - return this; - case Type: - return DurationType; - } - return newTypeConversionError(DurationType, typeValue); + return switch (typeValue.typeEnum()) { + case String -> stringOf(toPbString()); + case Int -> IntT.intOf(toJavaLong()); + case Duration -> this; + case Type -> DurationType; + default -> newTypeConversionError(DurationType, typeValue); + }; } /** Equal implements ref.Val.Equal. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Duration: - return boolOf(d.equals(((DurationT) other).d)); - case Null: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Duration -> boolOf(d.equals(((DurationT) other).d)); + case Null -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Negate implements traits.Negater.Negate. */ diff --git a/core/src/main/java/org/projectnessie/cel/common/types/Err.java b/core/src/main/java/org/projectnessie/cel/common/types/Err.java index 216324bd..e84ff00c 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/Err.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/Err.java @@ -268,8 +268,7 @@ public RuntimeException toRuntimeException() { } public static void throwErrorAsIllegalStateException(Val val) { - if (val instanceof Err) { - Err e = (Err) val; + if (val instanceof Err e) { if (e.cause != null) { throw new IllegalStateException(e.error, e.cause); } else { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/IntT.java b/core/src/main/java/org/projectnessie/cel/common/types/IntT.java index 17c2baf9..056575a0 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/IntT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/IntT.java @@ -139,6 +139,9 @@ public T convertToNative(Class typeDesc) { return (T) Int64Value.of(i); } if (typeDesc == Int32Value.class) { + if (i < Integer.MIN_VALUE || i > Integer.MAX_VALUE) { + Err.throwErrorAsIllegalStateException(rangeError(i, "Java int")); + } return (T) Int32Value.of((int) i); } if (typeDesc == Val.class || typeDesc == IntT.class) { @@ -175,29 +178,27 @@ public T convertToNative(Class typeDesc) { /** ConvertToType implements ref.Val.ConvertToType. */ @Override public Val convertToType(Type typeValue) { - switch (typeValue.typeEnum()) { - case Int: - return this; - case Uint: + return switch (typeValue.typeEnum()) { + case Int -> this; + case Uint -> { if (i < 0) { - return rangeError(i, "uint"); + yield rangeError(i, "uint"); } - return uintOf(i); - case Double: - return doubleOf(i); - case String: - return stringOf(Long.toString(i)); - case Timestamp: + yield uintOf(i); + } + case Double -> doubleOf(i); + case String -> stringOf(Long.toString(i)); + case Timestamp -> { // The maximum positive value that can be passed to time.Unix is math.MaxInt64 minus the // number of seconds between year 1 and year 1970. See comments on unixToInternal. if (i < minUnixTime || i > maxUnixTime) { - return errTimestampOverflow; + yield errTimestampOverflow; } - return timestampOf(Instant.ofEpochSecond(i).atZone(ZoneIdZ)); - case Type: - return IntType; - } - return newTypeConversionError(IntType, typeValue); + yield timestampOf(Instant.ofEpochSecond(i).atZone(ZoneIdZ)); + } + case Type -> IntType; + default -> newTypeConversionError(IntType, typeValue); + }; } /** Compare implements traits.Comparer.Compare. */ diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ListT.java b/core/src/main/java/org/projectnessie/cel/common/types/ListT.java index 6aa842e6..a202e70b 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ListT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ListT.java @@ -18,6 +18,7 @@ import static java.util.Arrays.asList; import static org.projectnessie.cel.common.types.BoolT.False; import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; import static org.projectnessie.cel.common.types.Err.isError; import static org.projectnessie.cel.common.types.Err.newErr; import static org.projectnessie.cel.common.types.Err.newTypeConversionError; @@ -63,6 +64,22 @@ public static Val newGenericArrayList(TypeAdapter adapter, Object[] value) { return new GenericListT(adapter, value); } + public static Val newGenericList(TypeAdapter adapter, List value) { + return new ListBackedListT(adapter, value); + } + + public static Val newIntArrayList(TypeAdapter adapter, int[] value) { + return new IntArrayListT(adapter, value); + } + + public static Val newLongArrayList(TypeAdapter adapter, long[] value) { + return new LongArrayListT(adapter, value); + } + + public static Val newDoubleArrayList(TypeAdapter adapter, double[] value) { + return new DoubleArrayListT(adapter, value); + } + public static Val newValArrayList(TypeAdapter adapter, Val[] value) { return new ValListT(adapter, value); } @@ -163,13 +180,11 @@ private Object toJavaArray(Class typeDesc) { @Override public Val convertToType(Type typeValue) { - switch (typeValue.typeEnum()) { - case List: - return this; - case Type: - return ListType; - } - return newTypeConversionError(ListType, typeValue); + return switch (typeValue.typeEnum()) { + case List -> this; + case Type -> ListType; + default -> newTypeConversionError(ListType, typeValue); + }; } @Override @@ -225,6 +240,50 @@ public Val size() { return intOf(size); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Val)) { + return false; + } + return equal((Val) o) == True; + } + + @Override + public int hashCode() { + int result = 1; + for (long i = 0; i < size; i++) { + result = 31 * result + get(intOf(i)).hashCode(); + } + return result; + } + + int checkedIndex(Val index, int size) { + switch (index.type().typeEnum()) { + case Int: + case Uint: + break; + case Double: + double od = index.doubleValue(); + if (Math.rint(od) != od) { + throw new InvalidIndexException(newErr("invalid_argument")); + } + break; + default: + throw new InvalidIndexException( + valOrErr(index, "unsupported index type '%s' in list", index.type())); + } + int i = (int) index.intValue(); + if (i < 0 || i >= size) { + // Note: the conformance tests assert on 'invalid_argument' + throw new InvalidIndexException( + newErr("invalid_argument: index '%d' out of range in list of size '%d'", i, size)); + } + return i; + } + private final class ArrayListIteratorT extends BaseVal implements IteratorT { private long index; @@ -268,6 +327,19 @@ public Object value() { } } + private static final class InvalidIndexException extends RuntimeException { + private final Val error; + + private InvalidIndexException(Val error) { + this.error = error; + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + } + static final class GenericListT extends BaseListT { private final Object[] array; @@ -283,36 +355,29 @@ public Object value() { @Override public Val add(Val other) { - if (!(other instanceof Lister)) { + if (!(other instanceof Lister otherList)) { return noSuchOverload(this, "add", other); } - Lister otherList = (Lister) other; - Object[] otherArray = (Object[]) otherList.value(); - Object[] newArray = Arrays.copyOf(array, array.length + otherArray.length); - System.arraycopy(otherArray, 0, newArray, array.length, otherArray.length); + int otherSize = (int) otherList.size().intValue(); + Object[] newArray = Arrays.copyOf(array, array.length + otherSize); + Class componentType = array.getClass().getComponentType(); + for (int i = 0; i < otherSize; i++) { + Val otherValue = otherList.get(intOf(i)); + newArray[array.length + i] = + componentType.isInstance(otherValue) + ? otherValue + : otherValue.convertToNative(componentType); + } return new GenericListT(adapter, newArray); } @Override public Val get(Val index) { - switch (index.type().typeEnum()) { - case Int: - case Uint: - break; - case Double: - double od = index.doubleValue(); - if (Math.rint(od) != od) { - return newErr("invalid_argument"); - } - break; - default: - return valOrErr(index, "unsupported index type '%s' in list", index.type()); - } - int sz = array.length; - int i = (int) index.intValue(); - if (i < 0 || i >= sz) { - // Note: the conformance tests assert on 'invalid_argument' - return newErr("invalid_argument: index '%d' out of range in list of size '%d'", i, sz); + int i; + try { + i = checkedIndex(index, array.length); + } catch (InvalidIndexException e) { + return e.error; } return adapter.nativeToValue(array[i]); @@ -331,6 +396,48 @@ public String toString() { } } + static final class ListBackedListT extends BaseListT { + private final List list; + + ListBackedListT(TypeAdapter adapter, List list) { + super(adapter, list.size()); + this.list = list; + } + + @Override + public Object value() { + return list; + } + + @Override + public Val add(Val other) { + if (!(other instanceof Lister otherList)) { + return noSuchOverload(this, "add", other); + } + int otherSize = (int) otherList.size().intValue(); + Object[] newArray = new Object[list.size() + otherSize]; + for (int i = 0; i < list.size(); i++) { + newArray[i] = list.get(i); + } + for (int i = 0; i < otherSize; i++) { + newArray[list.size() + i] = otherList.get(intOf(i)); + } + return new GenericListT(adapter, newArray); + } + + @Override + public Val get(Val index) { + int i; + try { + i = checkedIndex(index, list.size()); + } catch (InvalidIndexException e) { + return e.error; + } + + return adapter.nativeToValue(list.get(i)); + } + } + static final class ValListT extends BaseListT { private final Val[] array; @@ -350,7 +457,7 @@ public Object value() { @Override public Val add(Val other) { - if (!(other instanceof Lister)) { + if (!(other instanceof Lister otherLister)) { return noSuchOverload(this, "add", other); } if (other instanceof ValListT) { @@ -359,7 +466,6 @@ public Val add(Val other) { System.arraycopy(otherArray, 0, newArray, array.length, otherArray.length); return new ValListT(adapter, newArray); } else { - Lister otherLister = (Lister) other; int otherSIze = (int) otherLister.size().intValue(); Val[] newArray = Arrays.copyOf(array, array.length + otherSIze); for (int i = 0; i < otherSIze; i++) { @@ -371,47 +477,15 @@ public Val add(Val other) { @Override public Val get(Val index) { - switch (index.type().typeEnum()) { - case Int: - case Uint: - break; - case Double: - double od = index.doubleValue(); - if (Math.rint(od) != od) { - return newErr("invalid_argument"); - } - break; - default: - return valOrErr(index, "unsupported index type '%s' in list", index.type()); - } - int sz = array.length; - int i = (int) index.intValue(); - if (i < 0 || i >= sz) { - // Note: the conformance tests assert on 'invalid_argument' - return newErr("invalid_argument: index '%d' out of range in list of size '%d'", i, sz); + int i; + try { + i = checkedIndex(index, array.length); + } catch (InvalidIndexException e) { + return e.error; } return array[i]; } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ValListT valListT = (ValListT) o; - return Arrays.equals(array, valListT.array); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + Arrays.hashCode(array); - return result; - } - @Override public String toString() { return "ValListT{" @@ -425,9 +499,106 @@ public String toString() { } } + abstract static class PrimitiveArrayListT extends BaseListT { + PrimitiveArrayListT(TypeAdapter adapter, long size) { + super(adapter, size); + } + + @Override + public Val add(Val other) { + if (!(other instanceof Lister otherLister)) { + return noSuchOverload(this, "add", other); + } + int thisSize = (int) size; + int otherSize = (int) otherLister.size().intValue(); + Val[] newArray = new Val[thisSize + otherSize]; + for (int i = 0; i < thisSize; i++) { + newArray[i] = get(intOf(i)); + } + for (int i = 0; i < otherSize; i++) { + newArray[thisSize + i] = otherLister.get(intOf(i)); + } + return new ValListT(adapter, newArray); + } + } + + static final class IntArrayListT extends PrimitiveArrayListT { + private final int[] array; + + IntArrayListT(TypeAdapter adapter, int[] array) { + super(adapter, array.length); + this.array = array; + } + + @Override + public Object value() { + return array; + } + + @Override + public Val get(Val index) { + int i; + try { + i = checkedIndex(index, array.length); + } catch (InvalidIndexException e) { + return e.error; + } + return intOf(array[i]); + } + } + + static final class LongArrayListT extends PrimitiveArrayListT { + private final long[] array; + + LongArrayListT(TypeAdapter adapter, long[] array) { + super(adapter, array.length); + this.array = array; + } + + @Override + public Object value() { + return array; + } + + @Override + public Val get(Val index) { + int i; + try { + i = checkedIndex(index, array.length); + } catch (InvalidIndexException e) { + return e.error; + } + return intOf(array[i]); + } + } + + static final class DoubleArrayListT extends PrimitiveArrayListT { + private final double[] array; + + DoubleArrayListT(TypeAdapter adapter, double[] array) { + super(adapter, array.length); + this.array = array; + } + + @Override + public Object value() { + return array; + } + + @Override + public Val get(Val index) { + int i; + try { + i = checkedIndex(index, array.length); + } catch (InvalidIndexException e) { + return e.error; + } + return doubleOf(array[i]); + } + } + /** NewJSONList returns a traits.Lister based on structpb.ListValue instance. */ public static Val newJSONList(TypeAdapter adapter, ListValue l) { - List vals = l.getValuesList(); - return newGenericArrayList(adapter, vals.toArray()); + return newGenericList(adapter, l.getValuesList()); } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/MapT.java b/core/src/main/java/org/projectnessie/cel/common/types/MapT.java index e9008eae..d96ef276 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/MapT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/MapT.java @@ -20,7 +20,6 @@ import static org.projectnessie.cel.common.types.Err.isError; import static org.projectnessie.cel.common.types.Err.newErr; import static org.projectnessie.cel.common.types.Err.newTypeConversionError; -import static org.projectnessie.cel.common.types.StringT.StringType; import static org.projectnessie.cel.common.types.TypeT.TypeType; import static org.projectnessie.cel.common.types.Types.boolOf; @@ -55,7 +54,22 @@ public static Val newWrappedMap(TypeAdapter adapter, Map value) { return new ValMapT(adapter, value); } + @SuppressWarnings("unchecked") public static Val newMaybeWrappedMap(TypeAdapter adapter, Map value) { + boolean alreadyWrapped = true; + for (Map.Entry entry : value.entrySet()) { + if (!(entry.getKey() instanceof Val key) || !(entry.getValue() instanceof Val)) { + alreadyWrapped = false; + break; + } + if (key.type().typeEnum() == TypeEnum.Null) { + return newErr("unsupported key type"); + } + } + if (alreadyWrapped) { + return newWrappedMap(adapter, (Map) value); + } + Map newMap = new HashMap<>(value.size() * 4 / 3 + 1); for (Map.Entry entry : value.entrySet()) { Val k = adapter.nativeToValue(entry.getKey()); @@ -71,6 +85,13 @@ public static Val newMaybeWrappedMap(TypeAdapter adapter, Map value) { return newWrappedMap(adapter, newMap); } + public static boolean isSupportedLiteralKeyType(Val key) { + return switch (key.type().typeEnum()) { + case Bool, Int, String, Uint -> true; + default -> false; + }; + } + @Override public Type type() { return MapType; @@ -120,9 +141,12 @@ private Value toPbValue() { private Struct toPbStruct() { Struct.Builder struct = Struct.newBuilder(); map.forEach( - (k, v) -> - struct.putFields( - k.convertToType(StringType).value().toString(), v.convertToNative(Value.class))); + (k, v) -> { + if (k.type().typeEnum() != TypeEnum.String) { + throw new IllegalArgumentException("bad key type"); + } + struct.putFields(k.value().toString(), v.convertToNative(Value.class)); + }); return struct.build(); } @@ -152,10 +176,9 @@ public IteratorT iterator() { @Override public Val equal(Val other) { // TODO this is expensive :( - if (!(other instanceof MapT)) { + if (!(other instanceof MapT o)) { return False; } - MapT o = (MapT) other; if (!size().equal(o.size()).booleanValue()) { return False; } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/NullT.java b/core/src/main/java/org/projectnessie/cel/common/types/NullT.java index 415d479f..3ef11a2e 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/NullT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/NullT.java @@ -82,37 +82,22 @@ public T convertToNative(Class typeDesc) { /** ConvertToType implements ref.Val.ConvertToType. */ @Override public Val convertToType(Type typeValue) { - switch (typeValue.typeEnum()) { - case String: - return stringOf("null"); - case Null: - return this; - case Type: - return NullType; - } - return newTypeConversionError(NullType, typeValue); + return switch (typeValue.typeEnum()) { + case String -> stringOf("null"); + case Null -> this; + case Type -> NullType; + default -> newTypeConversionError(NullType, typeValue); + }; } /** Equal implements ref.Val.Equal. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Null: - return True; - case Int: - case Uint: - case Double: - case String: - case Bytes: - case Bool: - case List: - case Map: - case Object: - case Type: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Null -> True; + case Int, Uint, Double, String, Bytes, Bool, List, Map, Object, Type -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Type implements ref.Val.Type. */ diff --git a/core/src/main/java/org/projectnessie/cel/common/types/OptionalT.java b/core/src/main/java/org/projectnessie/cel/common/types/OptionalT.java new file mode 100644 index 00000000..f8f9b3d9 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/common/types/OptionalT.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.common.types; + +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.newTypeConversionError; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.IntT.IntZero; +import static org.projectnessie.cel.common.types.TypeT.TypeType; +import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; + +import com.google.protobuf.Message; +import java.util.Objects; +import org.projectnessie.cel.common.types.ref.BaseVal; +import org.projectnessie.cel.common.types.ref.Type; +import org.projectnessie.cel.common.types.ref.TypeEnum; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Container; +import org.projectnessie.cel.common.types.traits.FieldTester; +import org.projectnessie.cel.common.types.traits.Indexer; +import org.projectnessie.cel.common.types.traits.Mapper; +import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Sizer; +import org.projectnessie.cel.common.types.traits.Trait; + +/** Runtime value for CEL optional_type values. */ +public final class OptionalT extends BaseVal implements FieldTester, Indexer, Receiver { + public static final String OptionalTypeName = "optional_type"; + public static final Type OptionalType = + newObjectTypeValue( + OptionalTypeName, Trait.FieldTesterType, Trait.IndexerType, Trait.ReceiverType); + + private static final OptionalT None = new OptionalT(null, false); + + private final Val value; + private final boolean present; + + private OptionalT(Val value, boolean present) { + this.value = value; + this.present = present; + } + + public static OptionalT none() { + return None; + } + + public static OptionalT of(Val value) { + return new OptionalT(Objects.requireNonNull(value, "value"), true); + } + + public static OptionalT ofNonZeroValue(Val value) { + return isZeroValue(value) ? none() : of(value); + } + + public static Val optionalSelect(Val operand, Val field) { + return optionalAccess(operand, field); + } + + public static Val optionalIndex(Val operand, Val index) { + return optionalAccess(operand, index); + } + + public boolean hasValue() { + return present; + } + + public Val getValue() { + return value; + } + + @Override + public T convertToNative(Class typeDesc) { + if (typeDesc == Val.class || typeDesc == OptionalT.class) { + return typeDesc.cast(this); + } + if (typeDesc == Object.class) { + return typeDesc.cast(value()); + } + throw new RuntimeException( + String.format( + "native type conversion error from '%s' to '%s'", OptionalType, typeDesc.getName())); + } + + @Override + public Val convertToType(Type typeValue) { + if (typeValue.equals(OptionalType)) { + return this; + } + if (typeValue == TypeType) { + return OptionalType; + } + return newTypeConversionError(OptionalType, typeValue); + } + + @Override + public Val equal(Val other) { + if (!(other instanceof OptionalT optional)) { + return False; + } + if (!present || !optional.present) { + return present == optional.present ? True : False; + } + return value.equal(optional.value); + } + + @Override + public Type type() { + return OptionalType; + } + + @Override + public Object value() { + return present ? value.value() : null; + } + + @Override + public Val isSet(Val field) { + if (!present) { + return False; + } + if (value instanceof OptionalT) { + return ((OptionalT) value).isSet(field); + } + if (value instanceof FieldTester) { + Val present = ((FieldTester) value).isSet(field); + return isMissingAccess(present) ? False : present; + } + if (value instanceof Container) { + return ((Container) value).contains(field); + } + return noSuchOverload(value, "has", field); + } + + @Override + public Val get(Val index) { + return present ? optionalAccess(value, index) : none(); + } + + @Override + public Val receive(String function, String overload, Val... args) { + return switch (function) { + case "hasValue" -> + args.length == 0 + ? (present ? True : False) + : noSuchOverload(this, function, overload, args); + case "value" -> value(args, function, overload); + case "or" -> or(args, function, overload); + case "orValue" -> orValue(args, function, overload); + default -> noSuchOverload(this, function, overload, args); + }; + } + + private Val value(Val[] args, String function, String overload) { + if (args.length != 0) { + return noSuchOverload(this, function, overload, args); + } + return present ? value : newErr("optional.none() has no value"); + } + + private Val or(Val[] args, String function, String overload) { + if (args.length != 1 || !(args[0] instanceof OptionalT)) { + return noSuchOverload(this, function, overload, args); + } + return present ? this : args[0]; + } + + private Val orValue(Val[] args, String function, String overload) { + if (args.length != 1) { + return noSuchOverload(this, function, overload, args); + } + return present ? value : args[0]; + } + + private static boolean isZeroValue(Val value) { + return switch (value.type().typeEnum()) { + case Null -> true; + case Bool -> value == False || !value.booleanValue(); + case Int, Uint -> value.intValue() == 0L; + case Double -> value.doubleValue() == 0.0d; + case String, Bytes, List, Map -> + value.type().hasTrait(Trait.SizerType) && ((Sizer) value).size().equal(IntZero) == True; + case Object -> + value.value() instanceof Message && ((Message) value.value()).getAllFields().isEmpty(); + default -> false; + }; + } + + private static Val optionalAccess(Val operand, Val index) { + if (operand instanceof OptionalT) { + return ((OptionalT) operand).get(index); + } + if (operand instanceof FieldTester && index.type().typeEnum() == TypeEnum.String) { + Val present = ((FieldTester) operand).isSet(index); + if (present == False) { + return none(); + } + if (present != True) { + return isMissingAccess(present) ? none() : present; + } + } + if (operand instanceof Mapper) { + Val value = ((Mapper) operand).find(index); + return value == null ? none() : of(value); + } + if (operand instanceof Indexer) { + Val value = ((Indexer) operand).get(index); + return isMissingAccess(value) ? none() : of(value); + } + return noSuchOverload(operand, "optional access", index); + } + + private static boolean isMissingAccess(Val value) { + if (!(value instanceof Err)) { + return false; + } + String error = value.toString(); + return error.startsWith("no such key") + || error.startsWith("no such field") + || error.startsWith("invalid_argument") + || error.startsWith("index out of bounds"); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Val)) { + return false; + } + return equal((Val) o) == True; + } + + @Override + public int hashCode() { + return present ? Objects.hash(OptionalType, value) : Objects.hash(OptionalType); + } + + @Override + public String toString() { + return present ? String.format("optional.of(%s)", value) : "optional.none()"; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/common/types/Overloads.java b/core/src/main/java/org/projectnessie/cel/common/types/Overloads.java index 805167f6..3ad5a035 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/Overloads.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/Overloads.java @@ -236,20 +236,19 @@ private Overloads() {} // IsTypeConversionFunction returns whether the input function is a standard library type // conversion function. public static boolean isTypeConversionFunction(String function) { - switch (function) { - case TypeConvertBool: - case TypeConvertBytes: - case TypeConvertDouble: - case TypeConvertDuration: - case TypeConvertDyn: - case TypeConvertInt: - case TypeConvertString: - case TypeConvertTimestamp: - case TypeConvertType: - case TypeConvertUint: - return true; - default: - return false; - } + return switch (function) { + case TypeConvertBool, + TypeConvertBytes, + TypeConvertDouble, + TypeConvertDuration, + TypeConvertDyn, + TypeConvertInt, + TypeConvertString, + TypeConvertTimestamp, + TypeConvertType, + TypeConvertUint -> + true; + default -> false; + }; } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/StringT.java b/core/src/main/java/org/projectnessie/cel/common/types/StringT.java index ebd9b3d4..b275e4c7 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/StringT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/StringT.java @@ -33,7 +33,6 @@ import com.google.protobuf.StringValue; import com.google.protobuf.Value; import java.nio.charset.StandardCharsets; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.function.BiFunction; @@ -62,14 +61,14 @@ public final class StringT extends BaseVal implements Adder, Comparer, Matcher, Trait.ReceiverType, Trait.SizerType); - private static final Map> stringOneArgOverloads; - - static { - stringOneArgOverloads = new HashMap<>(); - stringOneArgOverloads.put(Overloads.Contains, StringT::stringContains); - stringOneArgOverloads.put(Overloads.EndsWith, StringT::stringEndsWith); - stringOneArgOverloads.put(Overloads.StartsWith, StringT::stringStartsWith); - } + private static final Map> stringOneArgOverloads = + Map.of( + Overloads.Contains, + StringT::stringContains, + Overloads.EndsWith, + StringT::stringEndsWith, + Overloads.StartsWith, + StringT::stringStartsWith); public static StringT stringOf(String s) { return new StringT(s); @@ -129,11 +128,19 @@ public Val convertToType(Type typeVal) { case Double: return doubleOf(Double.parseDouble(s)); case Bool: - if ("true".equalsIgnoreCase(s)) { - return True; - } - if ("false".equalsIgnoreCase(s)) { - return False; + switch (s) { + case "1": + case "t": + case "true": + case "TRUE": + case "True": + return True; + case "0": + case "f": + case "false": + case "FALSE": + case "False": + return False; } break; case Bytes: @@ -157,36 +164,21 @@ public Val convertToType(Type typeVal) { /** Compare implements traits.Comparer.Compare. */ @Override public Val compare(Val other) { - switch (other.type().typeEnum()) { - case String: - return intOfCompare(s.compareTo(((StringT) other).s)); - case Null: - return False; - default: - return noSuchOverload(this, "compare", other); - } + return switch (other.type().typeEnum()) { + case String -> intOfCompare(s.compareTo(((StringT) other).s)); + case Null -> False; + default -> noSuchOverload(this, "compare", other); + }; } /** Equal implements ref.Val.Equal. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case String: - return boolOf(s.equals(((StringT) other).s)); - case Null: - case Bool: - case Bytes: - case Int: - case Uint: - case Double: - case List: - case Map: - case Object: - case Type: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case String -> boolOf(s.equals(((StringT) other).s)); + case Null, Bool, Bytes, Int, Uint, Double, List, Map, Object, Type -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Match implements traits.Matcher.Match. */ diff --git a/core/src/main/java/org/projectnessie/cel/common/types/TimestampT.java b/core/src/main/java/org/projectnessie/cel/common/types/TimestampT.java index c16cf220..3d9e9bfb 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/TimestampT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/TimestampT.java @@ -31,7 +31,6 @@ import static org.projectnessie.cel.common.types.Types.boolOf; import com.google.protobuf.Any; -import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; import com.google.protobuf.Value; import java.time.DateTimeException; @@ -51,7 +50,6 @@ import java.time.zone.ZoneRulesException; import java.util.Calendar; import java.util.Date; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.TimeZone; @@ -122,42 +120,51 @@ public static TimestampT timestampOf(ZonedDateTime t) { return new TimestampT(t); } - private static final Map> timestampZeroArgOverloads; - private static final Map> timestampOneArgOverloads; - - static { - timestampZeroArgOverloads = new HashMap<>(); - timestampZeroArgOverloads.put(Overloads.TimeGetFullYear, TimestampT::timestampGetFullYear); - timestampZeroArgOverloads.put(Overloads.TimeGetMonth, TimestampT::timestampGetMonth); - timestampZeroArgOverloads.put(Overloads.TimeGetDayOfYear, TimestampT::timestampGetDayOfYear); - timestampZeroArgOverloads.put( - Overloads.TimeGetDate, TimestampT::timestampGetDayOfMonthOneBased); - timestampZeroArgOverloads.put( - Overloads.TimeGetDayOfMonth, TimestampT::timestampGetDayOfMonthZeroBased); - timestampZeroArgOverloads.put(Overloads.TimeGetDayOfWeek, TimestampT::timestampGetDayOfWeek); - timestampZeroArgOverloads.put(Overloads.TimeGetHours, TimestampT::timestampGetHours); - timestampZeroArgOverloads.put(Overloads.TimeGetMinutes, TimestampT::timestampGetMinutes); - timestampZeroArgOverloads.put(Overloads.TimeGetSeconds, TimestampT::timestampGetSeconds); - timestampZeroArgOverloads.put( - Overloads.TimeGetMilliseconds, TimestampT::timestampGetMilliseconds); - - timestampOneArgOverloads = new HashMap<>(); - timestampOneArgOverloads.put(Overloads.TimeGetFullYear, TimestampT::timestampGetFullYearWithTz); - timestampOneArgOverloads.put(Overloads.TimeGetMonth, TimestampT::timestampGetMonthWithTz); - timestampOneArgOverloads.put( - Overloads.TimeGetDayOfYear, TimestampT::timestampGetDayOfYearWithTz); - timestampOneArgOverloads.put( - Overloads.TimeGetDate, TimestampT::timestampGetDayOfMonthOneBasedWithTz); - timestampOneArgOverloads.put( - Overloads.TimeGetDayOfMonth, TimestampT::timestampGetDayOfMonthZeroBasedWithTz); - timestampOneArgOverloads.put( - Overloads.TimeGetDayOfWeek, TimestampT::timestampGetDayOfWeekWithTz); - timestampOneArgOverloads.put(Overloads.TimeGetHours, TimestampT::timestampGetHoursWithTz); - timestampOneArgOverloads.put(Overloads.TimeGetMinutes, TimestampT::timestampGetMinutesWithTz); - timestampOneArgOverloads.put(Overloads.TimeGetSeconds, TimestampT::timestampGetSecondsWithTz); - timestampOneArgOverloads.put( - Overloads.TimeGetMilliseconds, TimestampT::timestampGetMillisecondsWithTz); - } + private static final Map> timestampZeroArgOverloads = + Map.of( + Overloads.TimeGetFullYear, + TimestampT::timestampGetFullYear, + Overloads.TimeGetMonth, + TimestampT::timestampGetMonth, + Overloads.TimeGetDayOfYear, + TimestampT::timestampGetDayOfYear, + Overloads.TimeGetDate, + TimestampT::timestampGetDayOfMonthOneBased, + Overloads.TimeGetDayOfMonth, + TimestampT::timestampGetDayOfMonthZeroBased, + Overloads.TimeGetDayOfWeek, + TimestampT::timestampGetDayOfWeek, + Overloads.TimeGetHours, + TimestampT::timestampGetHours, + Overloads.TimeGetMinutes, + TimestampT::timestampGetMinutes, + Overloads.TimeGetSeconds, + TimestampT::timestampGetSeconds, + Overloads.TimeGetMilliseconds, + TimestampT::timestampGetMilliseconds); + + private static final Map> timestampOneArgOverloads = + Map.of( + Overloads.TimeGetFullYear, + TimestampT::timestampGetFullYearWithTz, + Overloads.TimeGetMonth, + TimestampT::timestampGetMonthWithTz, + Overloads.TimeGetDayOfYear, + TimestampT::timestampGetDayOfYearWithTz, + Overloads.TimeGetDate, + TimestampT::timestampGetDayOfMonthOneBasedWithTz, + Overloads.TimeGetDayOfMonth, + TimestampT::timestampGetDayOfMonthZeroBasedWithTz, + Overloads.TimeGetDayOfWeek, + TimestampT::timestampGetDayOfWeekWithTz, + Overloads.TimeGetHours, + TimestampT::timestampGetHoursWithTz, + Overloads.TimeGetMinutes, + TimestampT::timestampGetMinutesWithTz, + Overloads.TimeGetSeconds, + TimestampT::timestampGetSecondsWithTz, + Overloads.TimeGetMilliseconds, + TimestampT::timestampGetMillisecondsWithTz); private final ZonedDateTime t; @@ -241,7 +248,8 @@ public T convertToNative(Class typeDesc) { } if (typeDesc == Value.class) { // CEL follows the proto3 to JSON conversion which formats as an RFC 3339 encoded JSON string. - return (T) StringValue.of(jsonFormatter.format(t)); + DateTimeFormatter df = (t.getNano() > 0L) ? rfc3339nanoFormatter : rfc3339formatter; + return (T) Value.newBuilder().setStringValue(df.format(t)).build(); } throw new RuntimeException( @@ -261,36 +269,18 @@ private Timestamp toPbTimestamp() { /** ConvertToType implements ref.Val.ConvertToType. */ @Override public Val convertToType(Type typeValue) { - switch (typeValue.typeEnum()) { - case String: + return switch (typeValue.typeEnum()) { + case String -> { DateTimeFormatter df = (t.getNano() > 0L) ? rfc3339nanoFormatter : rfc3339formatter; - return stringOf(df.format(t)); - case Int: - return intOf(t.toEpochSecond()); - case Timestamp: - return this; - case Type: - return TimestampType; - } - return newTypeConversionError(TimestampType, typeValue); + yield stringOf(df.format(t)); + } + case Int -> intOf(t.toEpochSecond()); + case Timestamp -> this; + case Type -> TimestampType; + default -> newTypeConversionError(TimestampType, typeValue); + }; } - private static final DateTimeFormatter jsonFormatter = - new DateTimeFormatterBuilder() - .appendValue(ChronoField.YEAR, 4, 5, SignStyle.EXCEEDS_PAD) - .appendLiteral('-') - .appendValue(ChronoField.MONTH_OF_YEAR, 2) - .appendLiteral('-') - .appendValue(ChronoField.DAY_OF_MONTH, 2) - .appendLiteral('T') - .appendValue(ChronoField.HOUR_OF_DAY, 2) - .appendLiteral(':') - .appendValue(ChronoField.MINUTE_OF_HOUR, 2) - .appendLiteral(':') - .appendValue(ChronoField.SECOND_OF_MINUTE, 2) - .appendLiteral('Z') - .toFormatter(); - /** Only used for format a string, never for parsing. */ private static final DateTimeFormatter rfc3339formatter = new DateTimeFormatterBuilder() @@ -353,14 +343,11 @@ public Val convertToType(Type typeValue) { /** Equal implements ref.Val.Equal. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Timestamp: - return boolOf(t.equals(((TimestampT) other).t)); - case Null: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Timestamp -> boolOf(t.equals(((TimestampT) other).t)); + case Null -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Receive implements traits.Reciever.Receive. */ diff --git a/core/src/main/java/org/projectnessie/cel/common/types/TypeT.java b/core/src/main/java/org/projectnessie/cel/common/types/TypeT.java index 56ddacef..f1c8711c 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/TypeT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/TypeT.java @@ -55,11 +55,20 @@ public static Type newObjectTypeValue(String name) { return new ObjectTypeT(name); } + /** NewObjectTypeValue returns a *TypeValue with the supplied traits for a qualified type name. */ + public static Type newObjectTypeValue(String name, Trait... traits) { + return new ObjectTypeT(name, traits); + } + static final class ObjectTypeT extends TypeT { private final String typeName; ObjectTypeT(String typeName) { - super(TypeEnum.Object, Trait.FieldTesterType, Trait.IndexerType); + this(typeName, Trait.FieldTesterType, Trait.IndexerType); + } + + ObjectTypeT(String typeName, Trait... traits) { + super(TypeEnum.Object, traits); this.typeName = typeName; } @@ -93,13 +102,11 @@ public T convertToNative(Class typeDesc) { /** ConvertToType implements ref.Val.ConvertToType. */ @Override public Val convertToType(Type typeVal) { - switch (typeVal.typeEnum()) { - case Type: - return TypeType; - case String: - return stringOf(typeName()); - } - return newTypeConversionError(TypeType, typeVal); + return switch (typeVal.typeEnum()) { + case Type -> TypeType; + case String -> stringOf(typeName()); + default -> newTypeConversionError(TypeType, typeVal); + }; } /** Equal implements ref.Val.Equal. */ @@ -154,11 +161,10 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof Type typeValue)) { return false; } - Type typeValue = (Type) o; - return typeEnum == typeValue.typeEnum() && typeName().equals(typeValue.typeName()); + return typeName().equals(typeValue.typeName()); } @Override diff --git a/core/src/main/java/org/projectnessie/cel/common/types/UintT.java b/core/src/main/java/org/projectnessie/cel/common/types/UintT.java index f0fe8b7b..b392ed2a 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/UintT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/UintT.java @@ -120,13 +120,16 @@ public T convertToNative(Class typeDesc) { return (T) UInt64Value.of(i); } if (typeDesc == UInt32Value.class) { + if (Long.compareUnsigned(i, 0xffffffffL) > 0) { + Err.throwErrorAsIllegalStateException(rangeError(Long.toUnsignedString(i), "uint32")); + } return (T) UInt32Value.of((int) i); } if (typeDesc == Val.class || typeDesc == UintT.class) { return (T) this; } if (typeDesc == Value.class) { - if (i <= maxIntJSON) { + if (Long.compareUnsigned(i, maxIntJSON) <= 0) { // JSON can accurately represent 32-bit uints as floating point values. return (T) Value.newBuilder().setNumberValue(i).build(); } else { @@ -144,25 +147,24 @@ public T convertToNative(Class typeDesc) { /** ConvertToType implements ref.Val.ConvertToType. */ @Override public Val convertToType(Type typeValue) { - switch (typeValue.typeEnum()) { - case Int: + return switch (typeValue.typeEnum()) { + case Int -> { if (i < 0L) { - return rangeError(Long.toUnsignedString(i), "int"); + yield rangeError(Long.toUnsignedString(i), "int"); } - return intOf(i); - case Uint: - return this; - case Double: + yield intOf(i); + } + case Uint -> this; + case Double -> { if (i < 0L) { - return doubleOf(new BigInteger(Long.toUnsignedString(i)).doubleValue()); + yield doubleOf(new BigInteger(Long.toUnsignedString(i)).doubleValue()); } - return doubleOf(i); - case String: - return stringOf(Long.toUnsignedString(i)); - case Type: - return UintType; - } - return newTypeConversionError(UintType, typeValue); + yield doubleOf(i); + } + case String -> stringOf(Long.toUnsignedString(i)); + case Type -> UintType; + default -> newTypeConversionError(UintType, typeValue); + }; } /** Compare implements traits.Comparer.Compare. */ diff --git a/core/src/main/java/org/projectnessie/cel/common/types/Util.java b/core/src/main/java/org/projectnessie/cel/common/types/Util.java index 4eea3dc0..6d737461 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/Util.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/Util.java @@ -21,12 +21,10 @@ public final class Util { /** IsUnknownOrError returns whether the input element ref.Val is an ErrType or UnknonwType. */ public static boolean isUnknownOrError(Val val) { - switch (val.type().typeEnum()) { - case Unknown: - case Err: - return true; - } - return false; + return switch (val.type().typeEnum()) { + case Unknown, Err -> true; + default -> false; + }; } /** @@ -34,15 +32,9 @@ public static boolean isUnknownOrError(Val val) { * types do not include well-known types such as Duration and Timestamp. */ public static boolean isPrimitiveType(Val val) { - switch (val.type().typeEnum()) { - case Bool: - case Bytes: - case Double: - case Int: - case String: - case Uint: - return true; - } - return false; + return switch (val.type().typeEnum()) { + case Bool, Bytes, Double, Int, String, Uint -> true; + default -> false; + }; } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/Checked.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/Checked.java index 04b5b232..0a0f3dee 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/Checked.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/Checked.java @@ -92,6 +92,8 @@ public final class Checked { // Well-known types. CheckedWellKnowns.put("google.protobuf.Any", checkedAny); CheckedWellKnowns.put("google.protobuf.Duration", checkedDuration); + CheckedWellKnowns.put( + "google.protobuf.FieldMask", checkedMessageType("google.protobuf.FieldMask")); CheckedWellKnowns.put("google.protobuf.Timestamp", checkedTimestamp); // Json types. CheckedWellKnowns.put("google.protobuf.ListValue", checkedListDyn); diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/Db.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/Db.java index ced0befa..2fb01465 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/Db.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/Db.java @@ -24,6 +24,8 @@ import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.Duration; import com.google.protobuf.Empty; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.FieldMask; import com.google.protobuf.Message; import com.google.protobuf.Timestamp; import com.google.protobuf.Value; @@ -49,6 +51,8 @@ public final class Db { /** files contains the deduped set of FileDescriptions whose types are contained in the pb.Db. */ private final List files; + private volatile ExtensionRegistry extensionRegistry; + /** DefaultDb used at evaluation time or unless overridden at check time. */ public static final Db defaultDb = new Db(new HashMap<>(), new ArrayList<>()); @@ -62,6 +66,7 @@ public final class Db { defaultDb.registerMessage(Any.getDefaultInstance()); defaultDb.registerMessage(Duration.getDefaultInstance()); defaultDb.registerMessage(Empty.getDefaultInstance()); + defaultDb.registerMessage(FieldMask.getDefaultInstance()); defaultDb.registerMessage(Timestamp.getDefaultInstance()); defaultDb.registerMessage(Value.getDefaultInstance()); defaultDb.registerMessage(BoolValue.getDefaultInstance()); @@ -121,10 +126,14 @@ public FileDescription registerDescriptor(FileDescriptor fileDesc) { for (String enumValName : fd.getEnumNames()) { revFileDescriptorMap.put(enumValName, fd); } + for (String extensionName : fd.getExtensionNames()) { + revFileDescriptorMap.put(extensionName, fd); + } for (String msgTypeName : fd.getTypeNames()) { revFileDescriptorMap.put(msgTypeName, fd); } revFileDescriptorMap.put(path, fd); + extensionRegistry = null; // Return the specific file descriptor registered. files.add(fd); @@ -169,6 +178,47 @@ public PbTypeDescription describeType(String typeName) { return fd != null ? fd.getTypeDescription(typeName) : null; } + public FieldDescription describeExtension(String messageType, String extensionName) { + extensionName = sanitizeProtoName(extensionName); + FieldDescription extension = describeExtension(extensionName); + if (extension == null + || !sanitizeProtoName(messageType) + .equals(extension.descriptor().getContainingType().getFullName())) { + return null; + } + return extension; + } + + public FieldDescription describeExtension(String extensionName) { + extensionName = sanitizeProtoName(extensionName); + FileDescription fd = revFileDescriptorMap.get(extensionName); + return fd != null ? fd.getExtensionDescription(extensionName) : null; + } + + ExtensionRegistry extensionRegistry() { + ExtensionRegistry registry = extensionRegistry; + if (registry != null) { + return registry; + } + synchronized (this) { + registry = extensionRegistry; + if (registry == null) { + registry = ExtensionRegistry.newInstance(); + for (FileDescription file : files) { + for (FieldDescription extension : file.getExtensionDescriptions()) { + if (extension.isMessage()) { + registry.add(extension.descriptor(), extension.zero()); + } else { + registry.add(extension.descriptor()); + } + } + } + extensionRegistry = registry; + } + return registry; + } + } + /** * CollectFileDescriptorSet builds a file descriptor set associated with the file where the input * message is declared. diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/DefaultTypeAdapter.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/DefaultTypeAdapter.java index f20c5d45..097bb003 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/DefaultTypeAdapter.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/DefaultTypeAdapter.java @@ -64,8 +64,7 @@ public static Val nativeToValue(Db db, TypeAdapter a, Object value) { if (value instanceof Val) { return (Val) value; } - if (value instanceof Message) { - Message msg = (Message) value; + if (value instanceof Message msg) { String typeName = typeNameFromMessage(msg); if (typeName.isEmpty()) { return anyWithEmptyType(); @@ -84,8 +83,7 @@ public static Val nativeToValue(Db db, TypeAdapter a, Object value) { } static Object maybeUnwrapValue(Object value) { - if (value instanceof Value) { - Value v = (Value) value; + if (value instanceof Value v) { switch (v.getKindCase()) { case BOOL_VALUE: return v.getBoolValue(); diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java index fbfd2a3b..1bf6be71 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java @@ -15,6 +15,12 @@ */ package org.projectnessie.cel.common.types.pb; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.newTypeConversionError; +import static org.projectnessie.cel.common.types.Err.noMoreElements; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.TypeT.TypeType; +import static org.projectnessie.cel.common.types.Types.boolOf; import static org.projectnessie.cel.common.types.pb.PbTypeDescription.reflectTypeOf; import static org.projectnessie.cel.common.types.pb.PbTypeDescription.unwrapDynamic; @@ -31,12 +37,17 @@ import com.google.protobuf.Message; import com.google.protobuf.NullValue; import java.lang.reflect.Array; -import java.util.ArrayList; +import java.util.AbstractList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.IteratorT; +import org.projectnessie.cel.common.types.MapT; +import org.projectnessie.cel.common.types.ref.BaseVal; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; /** FieldDescription holds metadata related to fields declared within a type. */ public final class FieldDescription extends Description { @@ -124,34 +135,18 @@ public static FieldDescription newFieldDescription(FieldDescriptor fieldDesc) { } private static Class reflectTypeOfField(FieldDescriptor fieldDesc) { - switch (fieldDesc.getType()) { - case DOUBLE: - return Double.class; - case FLOAT: - return Float.class; - case STRING: - return String.class; - case BOOL: - return Boolean.class; - case BYTES: - return ByteString.class; - case INT32: - case SFIXED32: - case SINT32: - return Integer.class; - case INT64: - case SFIXED64: - case SINT64: - return Long.class; - case UINT32: - case UINT64: - case FIXED32: - case FIXED64: - return ULong.class; - case ENUM: - return Enum.class; - } - return reflectTypeOf(fieldDesc.getDefaultValue()); + return switch (fieldDesc.getType()) { + case DOUBLE -> Double.class; + case FLOAT -> Float.class; + case STRING -> String.class; + case BOOL -> Boolean.class; + case BYTES -> ByteString.class; + case INT32, SFIXED32, SINT32 -> Integer.class; + case INT64, SFIXED64, SINT64 -> Long.class; + case UINT32, UINT64, FIXED32, FIXED64 -> ULong.class; + case ENUM -> Enum.class; + default -> reflectTypeOf(fieldDesc.getDefaultValue()); + }; } private FieldDescription( @@ -198,18 +193,9 @@ public FieldDescriptor descriptor() { * on more than just protobuf field accesses; however, the target here must be a protobuf.Message. */ public boolean isSet(Object target) { - if (target instanceof Message) { - Message v = (Message) target; - // pbRef = v.ProtoReflect() - Descriptor pbDesc = v.getDescriptorForType(); - if (pbDesc == desc.getContainingType()) { - // When the target protobuf shares the same message descriptor instance as the field - // descriptor, use the cached field descriptor value. - return FieldDescription.hasValueForField(desc, v); - } - // Otherwise, fallback to a dynamic lookup of the field descriptor from the target - // instance as an attempt to use the cached field descriptor will result in a panic. - return FieldDescription.hasValueForField(pbDesc.findFieldByName(name()), v); + if (target instanceof Message v) { + FieldDescriptor fd = fieldDescriptorFor(v); + return fd != null && FieldDescription.hasValueForField(fd, v); } return false; } @@ -224,27 +210,14 @@ public boolean isSet(Object target) { * protobuf.Message. */ public Object getFrom(Db db, Object target) { - if (!(target instanceof Message)) { + if (!(target instanceof Message v)) { throw new IllegalArgumentException( String.format( "unsupported field selection target: (%s)%s", target.getClass().getName(), target)); } - Message v = (Message) target; // pbRef = v.protoReflect(); - Descriptor pbDesc = v.getDescriptorForType(); - Object fieldVal; - - FieldDescriptor fd; - if (pbDesc == desc.getContainingType()) { - // When the target protobuf shares the same message descriptor instance as the field - // descriptor, use the cached field descriptor value. - fd = desc; - } else { - // Otherwise, fallback to a dynamic lookup of the field descriptor from the target - // instance as an attempt to use the cached field descriptor will result in a panic. - fd = pbDesc.findFieldByName(name()); - } - fieldVal = getValueFromField(fd, v); + FieldDescriptor fd = fieldDescriptorFor(v); + Object fieldVal = getValueFromField(fd, v); Class fieldType = fieldVal.getClass(); if (fd.getJavaType() != JavaType.MESSAGE @@ -341,26 +314,16 @@ public Class reflectType() { if (r && desc.isMapField()) { return Map.class; } - switch (desc.getJavaType()) { - case ENUM: - case MESSAGE: - return reflectType; - case BOOLEAN: - return r ? Boolean[].class : Boolean.class; - case BYTE_STRING: - return r ? ByteString[].class : ByteString.class; - case DOUBLE: - return r ? Double[].class : Double.class; - case FLOAT: - return r ? Float[].class : Float.class; - case INT: - return r ? Integer[].class : Integer.class; - case LONG: - return r ? Long[].class : Long.class; - case STRING: - return r ? String[].class : String.class; - } - return reflectType; + return switch (desc.getJavaType()) { + case ENUM, MESSAGE -> reflectType; + case BOOLEAN -> r ? Boolean[].class : Boolean.class; + case BYTE_STRING -> r ? ByteString[].class : ByteString.class; + case DOUBLE -> r ? Double[].class : Double.class; + case FLOAT -> r ? Float[].class : Float.class; + case INT -> r ? Integer[].class : Integer.class; + case LONG -> r ? Long[].class : Long.class; + case STRING -> r ? String[].class : String.class; + }; } /** @@ -436,16 +399,30 @@ public int hashCode() { } public boolean hasField(Object target) { - return hasValueForField(desc, (Message) target); + if (!(target instanceof Message message)) { + return false; + } + FieldDescriptor fd = fieldDescriptorFor(message); + return fd != null && hasValueForField(fd, message); } public Object getField(Object target) { - return getValueFromField(desc, (Message) target); + return getField(target, DefaultTypeAdapter.Instance); + } + + public Object getField(Object target, TypeAdapter adapter) { + Message message = (Message) target; + FieldDescriptor fd = fieldDescriptorFor(message); + Object value = message.getField(fd); + if (fd.isMapField() && value instanceof List) { + return new ProtoMapT(adapter, fd, (List) value); + } + return getValueFromField(fd, message); } public static Object getValueFromField(FieldDescriptor desc, Message message) { - if (isWellKnownType(desc) && !message.hasField(desc)) { + if (!desc.isRepeated() && isWellKnownType(desc) && !message.hasField(desc)) { return NullValue.NULL_VALUE; } @@ -468,8 +445,7 @@ public static Object getValueFromField(FieldDescriptor desc, Message message) { // is very inefficient. // There is no way to do a "message.getMapField(desc, key)" (aka a "reflective counterpart" // for the generated map accessor methods like 'getXXXTypeOrThrow()'), too. - if (v instanceof List) { - List lst = (List) v; + if (v instanceof List lst) { Map map = new HashMap<>(lst.size() * 4 / 3 + 1); FieldDescriptor keyDesc = desc.getMessageType().findFieldByNumber(1); FieldDescriptor valueDesc = desc.getMessageType().findFieldByNumber(2); @@ -479,8 +455,7 @@ public static Object getValueFromField(FieldDescriptor desc, Message message) { if (e instanceof MapEntry) { key = normalizeUnsignedValue(keyDesc, ((MapEntry) e).getKey()); value = normalizeUnsignedValue(valueDesc, ((MapEntry) e).getValue()); - } else if (e instanceof DynamicMessage) { - DynamicMessage dynMsg = (DynamicMessage) e; + } else if (e instanceof DynamicMessage dynMsg) { List fields = dynMsg.getDescriptorForType().getFields(); if (fields.size() == 2) { FieldDescriptor dynKeyDesc = fields.get(0); @@ -509,18 +484,247 @@ public static Object getValueFromField(FieldDescriptor desc, Message message) { || type == FieldDescriptor.Type.UINT64 || type == FieldDescriptor.Type.FIXED32 || type == FieldDescriptor.Type.FIXED64)) { - List result = new ArrayList<>(); - List repeated = (List) v; - for (Object o : repeated) { - ULong casted = ULong.valueOf(((Number) o).longValue()); - result.add(casted); - } - v = result; + v = new UnsignedLongList((List) v); } } return v; } + private FieldDescriptor fieldDescriptorFor(Message message) { + Descriptor messageDesc = message.getDescriptorForType(); + if (messageDesc == desc.getContainingType()) { + return desc; + } + if (!desc.isExtension()) { + return messageDesc.findFieldByName(name()); + } + for (FieldDescriptor field : message.getAllFields().keySet()) { + if (field.getFullName().equals(desc.getFullName())) { + return field; + } + } + if (messageDesc.getFullName().equals(desc.getContainingType().getFullName())) { + return desc; + } + return null; + } + + private static final class UnsignedLongList extends AbstractList { + private final List repeated; + + private UnsignedLongList(List repeated) { + this.repeated = repeated; + } + + @Override + public ULong get(int index) { + return ULong.valueOf(((Number) repeated.get(index)).longValue()); + } + + @Override + public int size() { + return repeated.size(); + } + } + + private static final class ProtoMapT extends MapT { + private final TypeAdapter adapter; + private final List entries; + private final FieldDescriptor keyDesc; + private final FieldDescriptor valueDesc; + private volatile Map indexedEntries; + private volatile boolean scanned; + + private ProtoMapT(TypeAdapter adapter, FieldDescriptor field, List entries) { + this.adapter = adapter; + this.entries = entries; + this.keyDesc = field.getMessageType().findFieldByNumber(1); + this.valueDesc = field.getMessageType().findFieldByNumber(2); + } + + @SuppressWarnings("unchecked") + @Override + public T convertToNative(Class typeDesc) { + return (T) MapT.newMaybeWrappedMap(adapter, toJavaMap()).convertToNative(typeDesc); + } + + @Override + public Val convertToType(org.projectnessie.cel.common.types.ref.Type typeValue) { + if (typeValue == MapT.MapType) { + return this; + } + if (typeValue == TypeType) { + return MapT.MapType; + } + return newTypeConversionError(MapT.MapType, typeValue); + } + + @Override + public IteratorT iterator() { + return new EntryKeyIterator(); + } + + @Override + public Val equal(Val other) { + return MapT.newMaybeWrappedMap(adapter, toJavaMap()).equal(other); + } + + @Override + public Object value() { + return toJavaMap(); + } + + @Override + public Val contains(Val value) { + return boolOf(find(value) != null); + } + + @Override + public Val get(Val index) { + return find(index); + } + + @Override + public Val size() { + return intOf(entries.size()); + } + + @Override + public Val find(Val key) { + Map index = indexedEntries; + if (index != null) { + return index.get(key); + } + if (!scanned || entries.size() <= 1) { + scanned = true; + return scan(key); + } + return indexedEntries().get(key); + } + + private Val scan(Val key) { + for (Object entry : entries) { + Val candidate = adapter.nativeToValue(mapEntryKey(entry)); + if (candidate.equal(key) == True) { + return adapter.nativeToValue(mapEntryValue(entry)); + } + } + return null; + } + + private Map indexedEntries() { + Map index = indexedEntries; + if (index != null) { + return index; + } + synchronized (this) { + index = indexedEntries; + if (index == null) { + index = new HashMap<>(entries.size() * 4 / 3 + 1); + for (Object entry : entries) { + Val key = adapter.nativeToValue(mapEntryKey(entry)); + Val value = adapter.nativeToValue(mapEntryValue(entry)); + index.putIfAbsent(key, value); + } + indexedEntries = index; + } + return index; + } + } + + private Map toJavaMap() { + Map map = new HashMap<>(entries.size() * 4 / 3 + 1); + for (Object entry : entries) { + map.put(mapEntryKey(entry), mapEntryValue(entry)); + } + return map; + } + + private Object mapEntryKey(Object entry) { + return normalizeUnsignedValue(entryKeyDescriptor(entry), rawMapEntryValue(entry, 1)); + } + + private Object mapEntryValue(Object entry) { + return normalizeUnsignedValue(entryValueDescriptor(entry), rawMapEntryValue(entry, 2)); + } + + private FieldDescriptor entryKeyDescriptor(Object entry) { + if (entry instanceof DynamicMessage) { + List fields = ((DynamicMessage) entry).getDescriptorForType().getFields(); + if (fields.size() == 2) { + return fields.get(0); + } + } + return keyDesc; + } + + private FieldDescriptor entryValueDescriptor(Object entry) { + if (entry instanceof DynamicMessage) { + List fields = ((DynamicMessage) entry).getDescriptorForType().getFields(); + if (fields.size() == 2) { + return fields.get(1); + } + } + return valueDesc; + } + + private Object rawMapEntryValue(Object entry, int fieldNumber) { + if (entry instanceof MapEntry mapEntry) { + return fieldNumber == 1 ? mapEntry.getKey() : mapEntry.getValue(); + } + if (entry instanceof DynamicMessage dynMsg) { + List fields = dynMsg.getDescriptorForType().getFields(); + if (fields.size() == 2) { + return dynMsg.getField(fields.get(fieldNumber - 1)); + } + } + throw new IllegalArgumentException( + String.format("Unexpected %s (%s) in list of map fields", entry.getClass(), entry)); + } + + private final class EntryKeyIterator extends BaseVal implements IteratorT { + private int index; + + @Override + public Val hasNext() { + return boolOf(index < entries.size()); + } + + @Override + public Val next() { + if (index < entries.size()) { + return adapter.nativeToValue(mapEntryKey(entries.get(index++))); + } + return noMoreElements(); + } + + @Override + public T convertToNative(Class typeDesc) { + throw new UnsupportedOperationException(); + } + + @Override + public Val convertToType(org.projectnessie.cel.common.types.ref.Type typeValue) { + throw new UnsupportedOperationException(); + } + + @Override + public Val equal(Val other) { + throw new UnsupportedOperationException(); + } + + @Override + public org.projectnessie.cel.common.types.ref.Type type() { + throw new UnsupportedOperationException(); + } + + @Override + public Object value() { + throw new UnsupportedOperationException(); + } + } + } + private static Object normalizeUnsignedValue(FieldDescriptor desc, Object value) { FieldDescriptor.Type type = desc.getType(); if (value instanceof Number diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/FileDescription.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/FileDescription.java index 3e554c1c..c9d229d9 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/FileDescription.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/FileDescription.java @@ -18,6 +18,7 @@ import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.EnumDescriptor; import com.google.protobuf.Descriptors.EnumValueDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FileDescriptor; import java.util.HashMap; import java.util.List; @@ -29,11 +30,15 @@ public final class FileDescription { private final Map types; private final Map enums; + private final Map extensions; private FileDescription( - Map types, Map enums) { + Map types, + Map enums, + Map extensions) { this.types = types; this.enums = enums; + this.extensions = extensions; } @Override @@ -45,12 +50,14 @@ public boolean equals(Object o) { return false; } FileDescription that = (FileDescription) o; - return Objects.equals(types, that.types) && Objects.equals(enums, that.enums); + return Objects.equals(types, that.types) + && Objects.equals(enums, that.enums) + && Objects.equals(extensions, that.extensions); } @Override public int hashCode() { - return Objects.hash(types, enums); + return Objects.hash(types, enums, extensions); } /** @@ -66,7 +73,10 @@ public static FileDescription newFileDescription(FileDescriptor fileDesc) { Map types = new HashMap<>(); metadata.msgTypes.forEach( (name, msgType) -> types.put(name, PbTypeDescription.newTypeDescription(name, msgType))); - return new FileDescription(types, enums); + Map extensions = new HashMap<>(); + metadata.extensions.forEach( + (name, extension) -> extensions.put(name, FieldDescription.newFieldDescription(extension))); + return new FileDescription(types, enums, extensions); } /** @@ -82,6 +92,21 @@ public String[] getEnumNames() { return enums.keySet().toArray(new String[0]); } + /** GetExtensionDescription returns a field description for a qualified extension name. */ + public FieldDescription getExtensionDescription(String extensionName) { + return extensions.get(sanitizeProtoName(extensionName)); + } + + /** GetExtensionNames returns the string names of all extensions in the file. */ + public String[] getExtensionNames() { + return extensions.keySet().toArray(new String[0]); + } + + /** GetExtensionDescriptions returns all extension field descriptions in the file. */ + public Iterable getExtensionDescriptions() { + return extensions.values(); + } + /** * GetTypeDescription returns a TypeDescription for a qualified protobuf message type name * declared within the .proto file. @@ -111,12 +136,18 @@ static final class FileMetadata { /** enumValues maps from fully-qualified enum value to enum value descriptor. */ final Map enumValues; + /** extensions maps from fully-qualified extension name to field descriptor. */ + final Map extensions; + // TODO: support enum type definitions for use in future type-check enhancements. private FileMetadata( - Map msgTypes, Map enumValues) { + Map msgTypes, + Map enumValues, + Map extensions) { this.msgTypes = msgTypes; this.enumValues = enumValues; + this.extensions = extensions; } /** @@ -126,10 +157,12 @@ private FileMetadata( static FileMetadata collectFileMetadata(FileDescriptor fileDesc) { Map msgTypes = new HashMap<>(); Map enumValues = new HashMap<>(); + Map extensions = new HashMap<>(); - collectMsgTypes(fileDesc.getMessageTypes(), msgTypes, enumValues); + collectMsgTypes(fileDesc.getMessageTypes(), msgTypes, enumValues, extensions); collectEnumValues(fileDesc.getEnumTypes(), enumValues); - return new FileMetadata(msgTypes, enumValues); + collectExtensions(fileDesc.getExtensions(), extensions); + return new FileMetadata(msgTypes, enumValues, extensions); } /** @@ -139,17 +172,26 @@ static FileMetadata collectFileMetadata(FileDescriptor fileDesc) { private static void collectMsgTypes( List msgTypes, Map msgTypeMap, - Map enumValueMap) { + Map enumValueMap, + Map extensionMap) { for (Descriptor msgType : msgTypes) { msgTypeMap.put(msgType.getFullName(), msgType); List nestedMsgTypes = msgType.getNestedTypes(); if (!nestedMsgTypes.isEmpty()) { - collectMsgTypes(nestedMsgTypes, msgTypeMap, enumValueMap); + collectMsgTypes(nestedMsgTypes, msgTypeMap, enumValueMap, extensionMap); } List nestedEnumTypes = msgType.getEnumTypes(); if (!nestedEnumTypes.isEmpty()) { collectEnumValues(nestedEnumTypes, enumValueMap); } + collectExtensions(msgType.getExtensions(), extensionMap); + } + } + + private static void collectExtensions( + List extensions, Map extensionMap) { + for (FieldDescriptor extension : extensions) { + extensionMap.put(extension.getFullName(), extension); } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/PbObjectT.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/PbObjectT.java index 48719745..c15d0c95 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/PbObjectT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/PbObjectT.java @@ -21,8 +21,14 @@ import static org.projectnessie.cel.common.types.Types.boolOf; import com.google.protobuf.Any; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; import com.google.protobuf.DynamicMessage; +import com.google.protobuf.Empty; +import com.google.protobuf.FieldMask; import com.google.protobuf.Message; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; import com.google.protobuf.Value; import org.projectnessie.cel.common.types.ObjectT; import org.projectnessie.cel.common.types.StringT; @@ -57,7 +63,7 @@ public Val isSet(Val field) { return noSuchOverload(this, "isSet", field); } String protoFieldStr = (String) field.value(); - FieldDescription fd = typeDesc().fieldByName(protoFieldStr); + FieldDescription fd = fieldDescription(protoFieldStr); if (fd == null) { return noSuchField(protoFieldStr); } @@ -70,11 +76,26 @@ public Val get(Val index) { return noSuchOverload(this, "get", index); } String protoFieldStr = (String) index.value(); - FieldDescription fd = typeDesc().fieldByName(protoFieldStr); + FieldDescription fd = fieldDescription(protoFieldStr); if (fd == null) { return noSuchField(protoFieldStr); } - return nativeToValue(fd.getField(value)); + return nativeToValue(fd.getField(value, adapter)); + } + + @Override + public Val equal(Val other) { + if (!(other instanceof PbObjectT otherObject)) { + return super.equal(other); + } + + if (!typeDesc().name().equals(otherObject.typeDesc().name())) { + return boolOf(false); + } + if (containsNaN(message()) || containsNaN(otherObject.message())) { + return boolOf(false); + } + return boolOf(message().equals(otherObject.message())); } @SuppressWarnings("unchecked") @@ -102,20 +123,15 @@ public T convertToNative(Class typeDesc) { } if (typeDesc == Value.class) { // jsonValueType - throw new UnsupportedOperationException("IMPLEMENT proto-to-json"); - // TODO proto-to-json - // // Marshal the proto to JSON first, and then rehydrate as protobuf.Value as there is no - // // support for direct conversion from proto.Message to protobuf.Value. - // bytes, err := protojson.Marshal(pb) - // if err != nil { - // return nil, err - // } - // json := &structpb.Value{} - // err = protojson.Unmarshal(bytes, json) - // if err != nil { - // return nil, err - // } - // return json, nil + if (value instanceof Empty) { + return (T) Value.newBuilder().setStructValue(Struct.getDefaultInstance()).build(); + } + if (value instanceof FieldMask) { + return (T) Value.newBuilder().setStringValue(fieldMaskJsonValue((FieldMask) value)).build(); + } + if (value instanceof Timestamp) { + return adapter.nativeToValue(value).convertToNative(typeDesc); + } } if (typeDesc.isAssignableFrom(this.typeDesc.reflectType()) || typeDesc == Object.class) { if (value instanceof Any || value instanceof DynamicMessage) { @@ -141,10 +157,73 @@ private Message message() { return (Message) value; } + private static boolean containsNaN(Message message) { + for (FieldDescriptor field : message.getAllFields().keySet()) { + Object fieldValue = message.getField(field); + if (field.isRepeated()) { + for (Object element : (Iterable) fieldValue) { + if (containsNaN(field, element)) { + return true; + } + } + } else if (containsNaN(field, fieldValue)) { + return true; + } + } + return false; + } + + private static boolean containsNaN(FieldDescriptor field, Object value) { + JavaType javaType = field.getJavaType(); + if (javaType == JavaType.DOUBLE) { + return Double.isNaN((Double) value); + } + if (javaType == JavaType.FLOAT) { + return Float.isNaN((Float) value); + } + return javaType == JavaType.MESSAGE && containsNaN((Message) value); + } + + private static String fieldMaskJsonValue(FieldMask fieldMask) { + StringBuilder value = new StringBuilder(); + for (String path : fieldMask.getPathsList()) { + if (!value.isEmpty()) { + value.append(','); + } + value.append(fieldMaskPathJsonValue(path)); + } + return value.toString(); + } + + private static String fieldMaskPathJsonValue(String path) { + StringBuilder value = new StringBuilder(path.length()); + boolean upperNext = false; + for (int i = 0; i < path.length(); i++) { + char c = path.charAt(i); + if (c == '_') { + upperNext = true; + } else if (upperNext) { + value.append(Character.toUpperCase(c)); + upperNext = false; + } else { + value.append(c); + } + } + return value.toString(); + } + private PbTypeDescription typeDesc() { return (PbTypeDescription) typeDesc; } + private FieldDescription fieldDescription(String fieldName) { + FieldDescription field = typeDesc().fieldByName(fieldName); + if (field != null || !(adapter instanceof ProtoTypeRegistry)) { + return field; + } + return ((ProtoTypeRegistry) adapter).findFieldDescription(typeDesc().name(), fieldName); + } + @SuppressWarnings("unchecked") private T buildFrom(Class typeDesc) { try { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/PbTypeDescription.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/PbTypeDescription.java index fd1fc2d4..09bb4dcb 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/PbTypeDescription.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/PbTypeDescription.java @@ -119,15 +119,13 @@ public Object maybeUnwrap(Db db, Object m) { if (this.reflectType == Any.class) { String realTypeUrl; ByteString realValue; - if (msg instanceof DynamicMessage) { - DynamicMessage dyn = (DynamicMessage) msg; + if (msg instanceof DynamicMessage dyn) { Descriptor dynDesc = dyn.getDescriptorForType(); FieldDescriptor fTypeUrl = dynDesc.findFieldByName("type_url"); FieldDescriptor fValue = dynDesc.findFieldByName("value"); realTypeUrl = (String) dyn.getField(fTypeUrl); realValue = (ByteString) dyn.getField(fValue); - } else if (msg instanceof Any) { - Any any = (Any) msg; + } else if (msg instanceof Any any) { realTypeUrl = any.getTypeUrl(); realValue = any.getValue(); } else { @@ -138,17 +136,17 @@ public Object maybeUnwrap(Db db, Object m) { return anyWithEmptyType(); } PbTypeDescription realTypeDescriptor = db.describeType(realTypeName); - Message realMsg = realTypeDescriptor.zeroMsg.getParserForType().parseFrom(realValue); + Message realMsg = + DynamicMessage.parseFrom( + realTypeDescriptor.getDescriptor(), realValue, db.extensionRegistry()); return realTypeDescriptor.maybeUnwrap(db, realMsg); } if (!(zeroMsg instanceof DynamicMessage)) { - if (msg instanceof Any) { - Any any = (Any) msg; - msg = zeroMsg.getParserForType().parseFrom(any.getValue()); - } else if (msg instanceof DynamicMessage) { - DynamicMessage dyn = (DynamicMessage) msg; - msg = zeroMsg.getParserForType().parseFrom(dyn.toByteString()); + if (msg instanceof Any any) { + msg = DynamicMessage.parseFrom(getDescriptor(), any.getValue(), db.extensionRegistry()); + } else if (msg instanceof DynamicMessage dyn && !hasExtensions(msg)) { + msg = zeroMsg.getParserForType().parseFrom(dyn.toByteString(), db.extensionRegistry()); } } } catch (InvalidProtocolBufferException e) { @@ -244,37 +242,37 @@ static Object unwrap(Db db, Description desc, Message msg) { return conv.apply(msg); } - if (msg instanceof Any) { - Any v = (Any) msg; + if (msg instanceof Any v) { DynamicMessage dyn = DynamicMessage.newBuilder(v).build(); return unwrapDynamic(db, desc, dyn); } if (msg instanceof DynamicMessage) { return unwrapDynamic(db, desc, msg); } - if (msg instanceof Value) { - Value v = (Value) msg; - switch (v.getKindCase()) { - case BOOL_VALUE: - return v.getBoolValue(); - case LIST_VALUE: - return v.getListValue(); - case NULL_VALUE: - return v.getNullValue(); - case NUMBER_VALUE: - return v.getNumberValue(); - case STRING_VALUE: - return v.getStringValue(); - case STRUCT_VALUE: - return v.getStructValue(); - default: - return NullValue.NULL_VALUE; - } + if (msg instanceof Value v) { + return switch (v.getKindCase()) { + case BOOL_VALUE -> v.getBoolValue(); + case LIST_VALUE -> v.getListValue(); + case NULL_VALUE -> v.getNullValue(); + case NUMBER_VALUE -> v.getNumberValue(); + case STRING_VALUE -> v.getStringValue(); + case STRUCT_VALUE -> v.getStructValue(); + default -> NullValue.NULL_VALUE; + }; } return msg; } + private static boolean hasExtensions(Message message) { + for (FieldDescriptor field : message.getAllFields().keySet()) { + if (field.isExtension()) { + return true; + } + } + return false; + } + private static java.time.Duration asJavaDuration(Duration d) { return java.time.Duration.ofSeconds(d.getSeconds(), d.getNanos()); } @@ -371,16 +369,14 @@ private static Object unwrapDynamicAny(Db db, Description desc, Message refMsg) } public static String typeNameFromMessage(Message message) { - if (message instanceof DynamicMessage) { - DynamicMessage dyn = (DynamicMessage) message; + if (message instanceof DynamicMessage dyn) { Descriptor dynDesc = dyn.getDescriptorForType(); if (dynDesc.getFullName().equals("google.protobuf.Any")) { FieldDescriptor f = dynDesc.findFieldByName("type_url"); String typeUrl = (String) dyn.getField(f); return typeNameFromUrl(typeUrl); } - } else if (message instanceof Any) { - Any any = (Any) message; + } else if (message instanceof Any any) { String typeUrl = any.getTypeUrl(); return typeNameFromUrl(typeUrl); } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java index 3d03d9bb..96414259 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java @@ -30,6 +30,7 @@ import static org.projectnessie.cel.common.types.MapT.MapType; import static org.projectnessie.cel.common.types.NullT.NullType; import static org.projectnessie.cel.common.types.StringT.StringType; +import static org.projectnessie.cel.common.types.StringT.stringOf; import static org.projectnessie.cel.common.types.TimestampT.TimestampType; import static org.projectnessie.cel.common.types.TypeT.TypeType; import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; @@ -54,6 +55,7 @@ import com.google.protobuf.DoubleValue; import com.google.protobuf.Duration; import com.google.protobuf.Empty; +import com.google.protobuf.FieldMask; import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; @@ -78,20 +80,25 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.projectnessie.cel.common.types.NullT; import org.projectnessie.cel.common.types.TypeT; import org.projectnessie.cel.common.types.ref.FieldType; import org.projectnessie.cel.common.types.ref.TypeRegistry; import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; public final class ProtoTypeRegistry implements TypeRegistry { private static final ProtoTypeRegistry DEFAULT_REGISTRY = newDefaultRegistry(); private final Map revTypeMap; + private final Map fieldTypeCache; private final Db pbdb; private ProtoTypeRegistry( Map revTypeMap, Db pbdb) { this.revTypeMap = revTypeMap; + this.fieldTypeCache = new ConcurrentHashMap<>(); this.pbdb = pbdb; } @@ -129,6 +136,7 @@ private static ProtoTypeRegistry newDefaultRegistry() { Arrays.asList( DoubleValue.getDescriptor().getFile(), Empty.getDescriptor().getFile(), + FieldMask.getDescriptor().getFile(), Timestamp.getDescriptor().getFile(), UInt64Value.getDescriptor().getFile(), Any.getDescriptor().getFile(), @@ -197,15 +205,35 @@ public Val enumValue(String enumName) { @Override public FieldType findFieldType(String messageType, String fieldName) { + String cacheKey = messageType + '\n' + fieldName; + return fieldTypeCache.computeIfAbsent(cacheKey, key -> loadFieldType(messageType, fieldName)); + } + + private FieldType loadFieldType(String messageType, String fieldName) { + FieldDescription field = findFieldDescription(messageType, fieldName); + if (field == null) { + return null; + } + FieldDescription resolvedField = field; + return new FieldType( + resolvedField.checkedType(), + resolvedField::hasField, + target -> resolvedField.getField(target, this)); + } + + FieldDescription findFieldDescription(String messageType, String fieldName) { PbTypeDescription msgType = pbdb.describeType(messageType); if (msgType == null) { return null; } FieldDescription field = msgType.fieldByName(fieldName); + if (field == null) { + field = pbdb.describeExtension(messageType, fieldName); + } if (field == null) { return null; } - return new FieldType(field.checkedType(), field::hasField, field::getField); + return field; } @Override @@ -218,6 +246,9 @@ public Val findIdent(String identName) { if (enumVal != null) { return intOf(enumVal.value()); } + if (pbdb.describeExtension(identName) != null) { + return stringOf(identName); + } return null; } @@ -259,24 +290,99 @@ private Val newValueSetFields(Map fields, PbTypeDescription td, Bui // TODO resolve inefficiency for maps: first converted from a MapT to a native Java map and // then to a protobuf struct. The intermediate step (the Java map) could be omitted. - Object value = nv.getValue().convertToNative(field.reflectType()); - if (value.getClass().isArray()) { - value = Arrays.asList((Object[]) value); + FieldDescriptor pbDesc = field.descriptor(); + if (nv.getValue() == org.projectnessie.cel.common.types.NullT.NullValue + && isNullClearedField(pbDesc)) { + continue; } - FieldDescriptor pbDesc = field.descriptor(); + try { + Object value = toNativeFieldValue(nv.getValue(), field); + if (value.getClass().isArray()) { + value = Arrays.asList((Object[]) value); + } + + if (pbDesc.getJavaType() == JavaType.ENUM) { + value = intToProtoEnumValues(field, value); + } - if (pbDesc.getJavaType() == JavaType.ENUM) { - value = intToProtoEnumValues(field, value); + if (pbDesc.isMapField()) { + value = toProtoMapStructure(pbDesc, value); + } + + builder.setField(pbDesc, value); + } catch (RuntimeException e) { + return newErr(e, "invalid value for field '%s': %s", name, e.getMessage()); } + } + return null; + } - if (pbDesc.isMapField()) { - value = toProtoMapStructure(pbDesc, value); + private Object toNativeFieldValue(Val value, FieldDescription field) { + FieldDescriptor fieldDesc = field.descriptor(); + if (fieldDesc.isRepeated() + && !fieldDesc.isMapField() + && isNullPrunedMessageField(fieldDesc) + && value instanceof Lister) { + return toNativeRepeatedFieldValue((Lister) value, fieldDesc); + } + return value.convertToNative(field.reflectType()); + } + + private Object toNativeRepeatedFieldValue(Lister value, FieldDescriptor fieldDesc) { + Class elementType = messageNativeType(fieldDesc); + int size = (int) value.size().intValue(); + List converted = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + Val element = value.get(intOf(i)); + if (element == NullT.NullValue && isNullPrunedMessageField(fieldDesc)) { + continue; } + converted.add(element.convertToNative(elementType)); + } + return converted; + } - builder.setField(pbDesc, value); + private static boolean isNullClearedField(FieldDescriptor field) { + if (field.getJavaType() != JavaType.MESSAGE || field.isRepeated() || field.isMapField()) { + return false; } - return null; + Type wellKnownType = Checked.CheckedWellKnowns.get(field.getMessageType().getFullName()); + return wellKnownType == null || isNullPrunedMessageField(field); + } + + private static boolean isNullPrunedMessageField(FieldDescriptor field) { + if (field.getJavaType() != JavaType.MESSAGE) { + return false; + } + String typeName = field.getMessageType().getFullName(); + Type wellKnownType = Checked.CheckedWellKnowns.get(typeName); + if (wellKnownType == null) { + return false; + } + return wellKnownType.hasWrapper() + || typeName.equals("google.protobuf.Duration") + || typeName.equals("google.protobuf.Timestamp"); + } + + private static Class messageNativeType(FieldDescriptor field) { + return switch (field.getMessageType().getFullName()) { + case "google.protobuf.Any" -> Any.class; + case "google.protobuf.BoolValue" -> BoolValue.class; + case "google.protobuf.BytesValue" -> BytesValue.class; + case "google.protobuf.DoubleValue" -> DoubleValue.class; + case "google.protobuf.Duration" -> Duration.class; + case "google.protobuf.FieldMask" -> FieldMask.class; + case "google.protobuf.FloatValue" -> FloatValue.class; + case "google.protobuf.Int32Value" -> Int32Value.class; + case "google.protobuf.Int64Value" -> Int64Value.class; + case "google.protobuf.StringValue" -> StringValue.class; + case "google.protobuf.Timestamp" -> Timestamp.class; + case "google.protobuf.UInt32Value" -> UInt32Value.class; + case "google.protobuf.UInt64Value" -> UInt64Value.class; + case "google.protobuf.Value" -> Value.class; + default -> Message.class; + }; } /** @@ -300,8 +406,13 @@ private Object toProtoMapStructure(FieldDescriptor fieldDesc, Object value) { // if (!(k instanceof String)) { // return Err.newTypeConversionError(k.getClass().getName(), String.class.getName()); // } - if (valueFieldType == WireFormat.FieldType.MESSAGE && !(v instanceof Message)) { - v = nativeToValue(v).convertToNative(Value.class); + if (valueFieldType == WireFormat.FieldType.MESSAGE) { + if (isNullNativeValue(v) && isNullPrunedMessageField(valueType)) { + continue; + } + if (!(v instanceof Message)) { + v = nativeToValue(v).convertToNative(messageNativeType(valueType)); + } } MapEntry newEntry = @@ -314,6 +425,10 @@ private Object toProtoMapStructure(FieldDescriptor fieldDesc, Object value) { return value; } + private static boolean isNullNativeValue(Object value) { + return value == null || value == com.google.protobuf.NullValue.NULL_VALUE; + } + /** * Converts a value of type {@link Number} to {@link EnumValueDescriptor}, also works for arrays * and {@link List}s containing {@link Number}s. @@ -324,8 +439,7 @@ private Object intToProtoEnumValues(FieldDescription field, Object value) { if (value instanceof Number) { int enumValue = ((Number) value).intValue(); value = enumType.findValueByNumberCreatingIfUnknown(enumValue); - } else if (value instanceof List) { - List list = (List) value; + } else if (value instanceof List list) { List newList = new ArrayList(list.size()); for (Object o : list) { int enumValue = ((Number) o).intValue(); @@ -372,8 +486,7 @@ public void registerType(org.projectnessie.cel.common.types.ref.Type... types) { */ @Override public Val nativeToValue(Object value) { - if (value instanceof Message) { - Message v = (Message) value; + if (value instanceof Message v) { String typeName = typeNameFromMessage(v); if (typeName.isEmpty()) { return anyWithEmptyType(); diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java index a9e951b3..9c029a3d 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java @@ -19,8 +19,12 @@ import static org.projectnessie.cel.common.types.DoubleT.doubleOf; import static org.projectnessie.cel.common.types.DurationT.durationOf; import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.ListT.newDoubleArrayList; import static org.projectnessie.cel.common.types.ListT.newGenericArrayList; +import static org.projectnessie.cel.common.types.ListT.newGenericList; +import static org.projectnessie.cel.common.types.ListT.newIntArrayList; import static org.projectnessie.cel.common.types.ListT.newJSONList; +import static org.projectnessie.cel.common.types.ListT.newLongArrayList; import static org.projectnessie.cel.common.types.ListT.newStringArrayList; import static org.projectnessie.cel.common.types.ListT.newValArrayList; import static org.projectnessie.cel.common.types.MapT.newJSONStruct; @@ -43,18 +47,16 @@ import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; -import java.util.Arrays; import java.util.Calendar; +import java.util.Collection; import java.util.Date; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.BiFunction; import org.projectnessie.cel.common.ULong; -import org.projectnessie.cel.common.types.DoubleT; -import org.projectnessie.cel.common.types.IntT; import org.projectnessie.cel.common.types.NullT; -import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; /** * Helper class for {@link TypeAdapter} implementations to convert from a Java type to a CEL type. @@ -83,27 +85,9 @@ private TypeAdapterSupport() {} NativeToValueExact.put(Timestamp.class, (a, value) -> timestampOf((Timestamp) value)); NativeToValueExact.put(ZonedDateTime.class, (a, value) -> timestampOf((ZonedDateTime) value)); NativeToValueExact.put(Instant.class, (a, value) -> timestampOf((Instant) value)); - NativeToValueExact.put( - // TODO maybe add specialized ListT for int[] - int[].class, - (a, value) -> - newValArrayList( - DefaultTypeAdapter.Instance, - Arrays.stream((int[]) value).mapToObj(IntT::intOf).toArray(Val[]::new))); - NativeToValueExact.put( - // TODO maybe add specialized ListT for long[] - long[].class, - (a, value) -> - newValArrayList( - DefaultTypeAdapter.Instance, - Arrays.stream((long[]) value).mapToObj(IntT::intOf).toArray(Val[]::new))); - NativeToValueExact.put( - // TODO maybe add specialized ListT for double[] - double[].class, - (a, value) -> - newValArrayList( - DefaultTypeAdapter.Instance, - Arrays.stream((double[]) value).mapToObj(DoubleT::doubleOf).toArray(Val[]::new))); + NativeToValueExact.put(int[].class, (a, value) -> newIntArrayList(a, (int[]) value)); + NativeToValueExact.put(long[].class, (a, value) -> newLongArrayList(a, (long[]) value)); + NativeToValueExact.put(double[].class, (a, value) -> newDoubleArrayList(a, (double[]) value)); NativeToValueExact.put(String[].class, (a, value) -> newStringArrayList((String[]) value)); NativeToValueExact.put(Val[].class, (a, value) -> newValArrayList(a, (Val[]) value)); NativeToValueExact.put(NullValue.class, (a, value) -> NullT.NullValue); @@ -136,7 +120,13 @@ public static Val maybeNativeToValue(TypeAdapter a, Object value) { return newGenericArrayList(a, (Object[]) value); } if (value instanceof List) { - return newGenericArrayList(a, ((List) value).toArray()); + return newGenericList(a, (List) value); + } + if (value instanceof Collection) { + return newGenericArrayList(a, ((Collection) value).toArray()); + } + if (value instanceof Optional optional) { + return optional.map(a::nativeToValue).orElse(NullT.NullValue); } if (value instanceof Map) { return newMaybeWrappedMap(a, (Map) value); diff --git a/core/src/main/java/org/projectnessie/cel/extension/EncodersLib.java b/core/src/main/java/org/projectnessie/cel/extension/EncodersLib.java new file mode 100644 index 00000000..2f3972aa --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/extension/EncodersLib.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.extension; + +import static java.util.Collections.singletonList; +import static org.projectnessie.cel.common.types.BytesT.bytesOf; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.StringT.stringOf; + +import java.util.Base64; +import java.util.List; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.Library; +import org.projectnessie.cel.ProgramOption; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.BytesT; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.functions.Overload; + +/** EncodersLib provides CEL helper functions for common binary-to-text encodings. */ +public final class EncodersLib implements Library { + private static final String BASE64_ENCODE = "base64.encode"; + private static final String BASE64_DECODE = "base64.decode"; + private static final String BASE64_ENCODE_OVERLOAD = "base64_encode_bytes"; + private static final String BASE64_DECODE_OVERLOAD = "base64_decode_string"; + + private EncodersLib() {} + + public static EnvOption encoders() { + return Library.Lib(new EncodersLib()); + } + + @Override + public List getCompileOptions() { + return List.of( + EnvOption.declarations( + Decls.newFunction( + BASE64_ENCODE, + Decls.newOverload( + BASE64_ENCODE_OVERLOAD, singletonList(Decls.Bytes), Decls.String)), + Decls.newFunction( + BASE64_DECODE, + Decls.newOverload( + BASE64_DECODE_OVERLOAD, singletonList(Decls.String), Decls.Bytes)))); + } + + @Override + public List getProgramOptions() { + return List.of( + ProgramOption.functions( + Overload.unary(BASE64_ENCODE, EncodersLib::base64Encode), + Overload.unary(BASE64_ENCODE_OVERLOAD, EncodersLib::base64Encode), + Overload.unary(BASE64_DECODE, EncodersLib::base64Decode), + Overload.unary(BASE64_DECODE_OVERLOAD, EncodersLib::base64Decode))); + } + + private static Val base64Encode(Val value) { + if (!(value instanceof BytesT)) { + return noSuchOverload(value, BASE64_ENCODE, BASE64_ENCODE_OVERLOAD, new Val[] {value}); + } + byte[] bytes = value.convertToNative(byte[].class); + return stringOf(Base64.getEncoder().encodeToString(bytes)); + } + + private static Val base64Decode(Val value) { + if (!(value instanceof StringT)) { + return noSuchOverload(value, BASE64_DECODE, BASE64_DECODE_OVERLOAD, new Val[] {value}); + } + String text = value.convertToNative(String.class); + try { + return bytesOf(Base64.getDecoder().decode(text)); + } catch (IllegalArgumentException e) { + return newErr(e, "invalid base64 string"); + } + } +} diff --git a/core/src/main/java/org/projectnessie/cel/extension/MathLib.java b/core/src/main/java/org/projectnessie/cel/extension/MathLib.java new file mode 100644 index 00000000..0c801e10 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/extension/MathLib.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.extension; + +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.Err.errIntOverflow; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; + +import com.google.api.expr.v1alpha1.Decl; +import com.google.api.expr.v1alpha1.Type; +import java.util.ArrayList; +import java.util.List; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.Library; +import org.projectnessie.cel.ProgramOption; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.DoubleT; +import org.projectnessie.cel.common.types.IntT; +import org.projectnessie.cel.common.types.UintT; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.interpreter.functions.Overload; + +/** MathLib provides CEL helper functions from the standard math extension library. */ +public final class MathLib implements Library { + private static final String GREATEST = "math.greatest"; + private static final String LEAST = "math.least"; + private static final String CEIL = "math.ceil"; + private static final String FLOOR = "math.floor"; + private static final String ROUND = "math.round"; + private static final String TRUNC = "math.trunc"; + private static final String ABS = "math.abs"; + private static final String SIGN = "math.sign"; + private static final String IS_NAN = "math.isNaN"; + private static final String IS_INF = "math.isInf"; + private static final String IS_FINITE = "math.isFinite"; + private static final String BIT_AND = "math.bitAnd"; + private static final String BIT_OR = "math.bitOr"; + private static final String BIT_XOR = "math.bitXor"; + private static final String BIT_NOT = "math.bitNot"; + private static final String BIT_SHIFT_LEFT = "math.bitShiftLeft"; + private static final String BIT_SHIFT_RIGHT = "math.bitShiftRight"; + + private MathLib() {} + + public static EnvOption math() { + return Library.Lib(new MathLib()); + } + + @Override + public List getCompileOptions() { + List declarations = new ArrayList<>(); + declarations.add(minMaxDeclaration(GREATEST)); + declarations.add(minMaxDeclaration(LEAST)); + declarations.add(unaryDeclaration(CEIL, Decls.Double)); + declarations.add(unaryDeclaration(FLOOR, Decls.Double)); + declarations.add(unaryDeclaration(ROUND, Decls.Double)); + declarations.add(unaryDeclaration(TRUNC, Decls.Double)); + declarations.add(unaryDeclaration(ABS, Decls.Dyn)); + declarations.add(unaryDeclaration(SIGN, Decls.Dyn)); + declarations.add(unaryDeclaration(IS_NAN, Decls.Bool)); + declarations.add(unaryDeclaration(IS_INF, Decls.Bool)); + declarations.add(unaryDeclaration(IS_FINITE, Decls.Bool)); + declarations.add(binaryDeclaration(BIT_AND, Decls.Dyn)); + declarations.add(binaryDeclaration(BIT_OR, Decls.Dyn)); + declarations.add(binaryDeclaration(BIT_XOR, Decls.Dyn)); + declarations.add(unaryDeclaration(BIT_NOT, Decls.Dyn)); + declarations.add(binaryDeclaration(BIT_SHIFT_LEFT, Decls.Dyn)); + declarations.add(binaryDeclaration(BIT_SHIFT_RIGHT, Decls.Dyn)); + return List.of(EnvOption.declarations(declarations)); + } + + @Override + public List getProgramOptions() { + List overloads = new ArrayList<>(); + overloads.add( + Overload.overload(GREATEST, null, MathLib::greatest, MathLib::greatest, MathLib::greatest)); + overloads.add(Overload.overload(LEAST, null, MathLib::least, MathLib::least, MathLib::least)); + addArityOverloads(overloads, GREATEST, MathLib::greatest); + addArityOverloads(overloads, LEAST, MathLib::least); + overloads.add(Overload.unary(CEIL, MathLib::ceil)); + overloads.add(Overload.unary(overloadId(CEIL, 1), MathLib::ceil)); + overloads.add(Overload.unary(FLOOR, MathLib::floor)); + overloads.add(Overload.unary(overloadId(FLOOR, 1), MathLib::floor)); + overloads.add(Overload.unary(ROUND, MathLib::round)); + overloads.add(Overload.unary(overloadId(ROUND, 1), MathLib::round)); + overloads.add(Overload.unary(TRUNC, MathLib::trunc)); + overloads.add(Overload.unary(overloadId(TRUNC, 1), MathLib::trunc)); + overloads.add(Overload.unary(ABS, MathLib::abs)); + overloads.add(Overload.unary(overloadId(ABS, 1), MathLib::abs)); + overloads.add(Overload.unary(SIGN, MathLib::sign)); + overloads.add(Overload.unary(overloadId(SIGN, 1), MathLib::sign)); + overloads.add(Overload.unary(IS_NAN, MathLib::isNaN)); + overloads.add(Overload.unary(overloadId(IS_NAN, 1), MathLib::isNaN)); + overloads.add(Overload.unary(IS_INF, MathLib::isInf)); + overloads.add(Overload.unary(overloadId(IS_INF, 1), MathLib::isInf)); + overloads.add(Overload.unary(IS_FINITE, MathLib::isFinite)); + overloads.add(Overload.unary(overloadId(IS_FINITE, 1), MathLib::isFinite)); + overloads.add(Overload.binary(BIT_AND, MathLib::bitAnd)); + overloads.add(Overload.binary(overloadId(BIT_AND, 2), MathLib::bitAnd)); + overloads.add(Overload.binary(BIT_OR, MathLib::bitOr)); + overloads.add(Overload.binary(overloadId(BIT_OR, 2), MathLib::bitOr)); + overloads.add(Overload.binary(BIT_XOR, MathLib::bitXor)); + overloads.add(Overload.binary(overloadId(BIT_XOR, 2), MathLib::bitXor)); + overloads.add(Overload.unary(BIT_NOT, MathLib::bitNot)); + overloads.add(Overload.unary(overloadId(BIT_NOT, 1), MathLib::bitNot)); + overloads.add(Overload.binary(BIT_SHIFT_LEFT, MathLib::bitShiftLeft)); + overloads.add(Overload.binary(overloadId(BIT_SHIFT_LEFT, 2), MathLib::bitShiftLeft)); + overloads.add(Overload.binary(BIT_SHIFT_RIGHT, MathLib::bitShiftRight)); + overloads.add(Overload.binary(overloadId(BIT_SHIFT_RIGHT, 2), MathLib::bitShiftRight)); + return List.of(ProgramOption.functions(overloads.toArray(Overload[]::new))); + } + + private static Decl minMaxDeclaration(String function) { + List overloads = new ArrayList<>(); + overloads.add(Decls.newOverload(overloadId(function, "int"), List.of(Decls.Int), Decls.Int)); + overloads.add(Decls.newOverload(overloadId(function, "uint"), List.of(Decls.Uint), Decls.Uint)); + overloads.add( + Decls.newOverload(overloadId(function, "double"), List.of(Decls.Double), Decls.Double)); + for (int arity = 2; arity <= 5; arity++) { + overloads.add(Decls.newOverload(overloadId(function, arity), dynArgs(arity), Decls.Dyn)); + } + overloads.add( + Decls.newOverload( + overloadId(function, "list"), List.of(Decls.newListType(Decls.Dyn)), Decls.Dyn)); + return Decls.newFunction(function, overloads); + } + + private static Decl unaryDeclaration(String function, Type result) { + return Decls.newFunction( + function, Decls.newOverload(overloadId(function, 1), List.of(Decls.Dyn), result)); + } + + private static Decl binaryDeclaration(String function, Type result) { + return Decls.newFunction( + function, Decls.newOverload(overloadId(function, 2), dynArgs(2), result)); + } + + private static List dynArgs(int count) { + List args = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + args.add(Decls.Dyn); + } + return args; + } + + private static void addArityOverloads( + List overloads, + String function, + org.projectnessie.cel.interpreter.functions.FunctionOp op) { + overloads.add(Overload.unary(overloadId(function, "int"), op::invoke)); + overloads.add(Overload.unary(overloadId(function, "uint"), op::invoke)); + overloads.add(Overload.unary(overloadId(function, "double"), op::invoke)); + overloads.add( + Overload.binary(overloadId(function, 2), (left, right) -> op.invoke(left, right))); + for (int arity = 3; arity <= 5; arity++) { + overloads.add(Overload.function(overloadId(function, arity), op)); + } + overloads.add(Overload.unary(overloadId(function, "list"), op::invoke)); + } + + private static String overloadId(String function, int arity) { + return function.replace('.', '_') + "_" + arity; + } + + private static String overloadId(String function, String suffix) { + return function.replace('.', '_') + "_" + suffix; + } + + private static Val greatest(Val... values) { + return minMax(values, true); + } + + private static Val least(Val... values) { + return minMax(values, false); + } + + private static Val minMax(Val[] values, boolean greatest) { + List candidates = candidates(values); + if (candidates.isEmpty()) { + return newErr("empty argument list"); + } + + Val result = candidates.get(0); + if (!isNumber(result)) { + return noSuchOverload(); + } + for (int i = 1; i < candidates.size(); i++) { + Val candidate = candidates.get(i); + if (!isNumber(candidate)) { + return noSuchOverload(); + } + int cmp = compareNumbers(candidate, result); + if ((greatest && cmp > 0) || (!greatest && cmp < 0)) { + result = candidate; + } + } + return result; + } + + private static List candidates(Val[] values) { + if (values.length == 1 && values[0] instanceof Lister list) { + int size = (int) list.size().intValue(); + List elements = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + elements.add(list.get(intOf(i))); + } + return elements; + } + return List.of(values); + } + + private static int compareNumbers(Val left, Val right) { + if (left instanceof DoubleT || right instanceof DoubleT) { + return Double.compare(asDouble(left), asDouble(right)); + } + if (left instanceof UintT && right instanceof UintT) { + return Long.compareUnsigned(left.intValue(), right.intValue()); + } + if (left instanceof UintT && right instanceof IntT) { + long rightValue = right.intValue(); + return rightValue < 0 ? 1 : Long.compareUnsigned(left.intValue(), rightValue); + } + if (left instanceof IntT && right instanceof UintT) { + long leftValue = left.intValue(); + return leftValue < 0 ? -1 : Long.compareUnsigned(leftValue, right.intValue()); + } + return Long.compare(left.intValue(), right.intValue()); + } + + private static double asDouble(Val value) { + if (value instanceof DoubleT) { + return value.doubleValue(); + } + if (value instanceof UintT) { + return Long.toUnsignedString(value.intValue()).equals("18446744073709551615") + ? 18446744073709551615.0 + : Double.parseDouble(Long.toUnsignedString(value.intValue())); + } + return value.intValue(); + } + + private static Val ceil(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + return doubleOf(Math.ceil(value.doubleValue())); + } + + private static Val floor(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + return doubleOf(Math.floor(value.doubleValue())); + } + + private static Val round(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + double d = value.doubleValue(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + return doubleOf(d); + } + return doubleOf(d < 0 ? Math.ceil(d - 0.5d) : Math.floor(d + 0.5d)); + } + + private static Val trunc(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + double d = value.doubleValue(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + return doubleOf(d); + } + return doubleOf(d < 0 ? Math.ceil(d) : Math.floor(d)); + } + + private static Val abs(Val value) { + if (value instanceof UintT) { + return value; + } + if (value instanceof IntT) { + long v = value.intValue(); + if (v == Long.MIN_VALUE) { + return errIntOverflow; + } + return intOf(Math.abs(v)); + } + if (value instanceof DoubleT) { + return doubleOf(Math.abs(value.doubleValue())); + } + return noSuchOverload(); + } + + private static Val sign(Val value) { + if (value instanceof UintT) { + return uintOf(value.intValue() == 0 ? 0 : 1); + } + if (value instanceof IntT) { + return intOf(Long.compare(value.intValue(), 0)); + } + if (value instanceof DoubleT) { + return doubleOf(Double.compare(value.doubleValue(), 0.0d)); + } + return noSuchOverload(); + } + + private static Val isNaN(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + return Double.isNaN(value.doubleValue()) ? True : False; + } + + private static Val isInf(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + return Double.isInfinite(value.doubleValue()) ? True : False; + } + + private static Val isFinite(Val value) { + if (!(value instanceof DoubleT)) { + return noSuchOverload(); + } + double d = value.doubleValue(); + return !Double.isNaN(d) && !Double.isInfinite(d) ? True : False; + } + + private static Val bitAnd(Val left, Val right) { + return bitwise(left, right, (a, b) -> a & b); + } + + private static Val bitOr(Val left, Val right) { + return bitwise(left, right, (a, b) -> a | b); + } + + private static Val bitXor(Val left, Val right) { + return bitwise(left, right, (a, b) -> a ^ b); + } + + private static Val bitwise(Val left, Val right, LongOperator op) { + if (left instanceof IntT && right instanceof IntT) { + return intOf(op.apply(left.intValue(), right.intValue())); + } + if (left instanceof UintT && right instanceof UintT) { + return uintOf(op.apply(left.intValue(), right.intValue())); + } + return noSuchOverload(); + } + + private static Val bitNot(Val value) { + if (value instanceof IntT) { + return intOf(~value.intValue()); + } + if (value instanceof UintT) { + return uintOf(~value.intValue()); + } + return noSuchOverload(); + } + + private static Val bitShiftLeft(Val left, Val right) { + if (!(right instanceof IntT)) { + return noSuchOverload(); + } + long shift = right.intValue(); + if (shift < 0) { + return newErr("negative offset"); + } + if (shift >= Long.SIZE) { + return left instanceof UintT ? uintOf(0) : left instanceof IntT ? intOf(0) : noSuchOverload(); + } + if (left instanceof IntT) { + return intOf(left.intValue() << shift); + } + if (left instanceof UintT) { + return uintOf(left.intValue() << shift); + } + return noSuchOverload(); + } + + private static Val bitShiftRight(Val left, Val right) { + if (!(right instanceof IntT)) { + return noSuchOverload(); + } + long shift = right.intValue(); + if (shift < 0) { + return newErr("negative offset"); + } + if (shift >= Long.SIZE) { + return left instanceof UintT ? uintOf(0) : left instanceof IntT ? intOf(0) : noSuchOverload(); + } + if (left instanceof IntT) { + return intOf(left.intValue() >>> shift); + } + if (left instanceof UintT) { + return uintOf(left.intValue() >>> shift); + } + return noSuchOverload(); + } + + private static boolean isNumber(Val value) { + return value instanceof IntT || value instanceof UintT || value instanceof DoubleT; + } + + private static Val noSuchOverload() { + return newErr("no such overload"); + } + + @FunctionalInterface + private interface LongOperator { + long apply(long left, long right); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/extension/NetworkLib.java b/core/src/main/java/org/projectnessie/cel/extension/NetworkLib.java new file mode 100644 index 00000000..a0ff228b --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/extension/NetworkLib.java @@ -0,0 +1,539 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.extension; + +import static java.util.Collections.singletonList; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.newTypeConversionError; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.StringType; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; +import static org.projectnessie.cel.common.types.Types.boolOf; + +import com.google.api.expr.v1alpha1.Type; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.Library; +import org.projectnessie.cel.ProgramOption; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.ref.BaseVal; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.interpreter.functions.Overload; + +/** NetworkLib provides CEL helper functions from the standard network extension library. */ +public final class NetworkLib implements Library { + private static final String IP = "ip"; + private static final String CIDR = "cidr"; + private static final String IS_IP = "isIP"; + private static final String IP_IS_CANONICAL = "ip.isCanonical"; + private static final String NET_IP = "net.IP"; + private static final String NET_CIDR = "net.CIDR"; + + private static final Type IP_TYPE = Decls.newObjectType(NET_IP); + private static final Type CIDR_TYPE = Decls.newObjectType(NET_CIDR); + + private static final org.projectnessie.cel.common.types.ref.Type IP_TYPE_VALUE = + newObjectTypeValue(NET_IP, Trait.ReceiverType); + private static final org.projectnessie.cel.common.types.ref.Type CIDR_TYPE_VALUE = + newObjectTypeValue(NET_CIDR, Trait.ReceiverType); + + private NetworkLib() {} + + public static EnvOption network() { + return Library.Lib(new NetworkLib()); + } + + @Override + public List getCompileOptions() { + return List.of( + EnvOption.types(IP_TYPE_VALUE, CIDR_TYPE_VALUE), + EnvOption.declarations( + Decls.newVar(NET_IP, Decls.newTypeType(IP_TYPE)), + Decls.newVar(NET_CIDR, Decls.newTypeType(CIDR_TYPE)), + Decls.newFunction( + IP, Decls.newOverload("ip_string", singletonList(Decls.String), IP_TYPE)), + Decls.newFunction( + CIDR, Decls.newOverload("cidr_string", singletonList(Decls.String), CIDR_TYPE)), + Decls.newFunction( + IS_IP, + Decls.newOverload("is_ip_string", singletonList(Decls.String), Decls.Bool), + Decls.newOverload("is_ip_cidr", singletonList(CIDR_TYPE), Decls.Bool)), + Decls.newFunction( + IP_IS_CANONICAL, + Decls.newOverload( + "ip_is_canonical_string", singletonList(Decls.String), Decls.Bool)), + Decls.newFunction( + "string", + Decls.newOverload("string_ip", singletonList(IP_TYPE), Decls.String), + Decls.newOverload("string_cidr", singletonList(CIDR_TYPE), Decls.String)), + Decls.newFunction( + "family", + Decls.newInstanceOverload("ip_family", singletonList(IP_TYPE), Decls.Int)), + Decls.newFunction( + "isUnspecified", + Decls.newInstanceOverload("ip_is_unspecified", singletonList(IP_TYPE), Decls.Bool)), + Decls.newFunction( + "isLoopback", + Decls.newInstanceOverload("ip_is_loopback", singletonList(IP_TYPE), Decls.Bool)), + Decls.newFunction( + "isGlobalUnicast", + Decls.newInstanceOverload( + "ip_is_global_unicast", singletonList(IP_TYPE), Decls.Bool)), + Decls.newFunction( + "isLinkLocalMulticast", + Decls.newInstanceOverload( + "ip_is_link_local_multicast", singletonList(IP_TYPE), Decls.Bool)), + Decls.newFunction( + "isLinkLocalUnicast", + Decls.newInstanceOverload( + "ip_is_link_local_unicast", singletonList(IP_TYPE), Decls.Bool)), + Decls.newFunction( + "containsIP", + Decls.newInstanceOverload( + "cidr_contains_ip", List.of(CIDR_TYPE, IP_TYPE), Decls.Bool), + Decls.newInstanceOverload( + "cidr_contains_ip_string", List.of(CIDR_TYPE, Decls.String), Decls.Bool)), + Decls.newFunction( + "containsCIDR", + Decls.newInstanceOverload( + "cidr_contains_cidr", List.of(CIDR_TYPE, CIDR_TYPE), Decls.Bool), + Decls.newInstanceOverload( + "cidr_contains_cidr_string", List.of(CIDR_TYPE, Decls.String), Decls.Bool)), + Decls.newFunction( + "ip", Decls.newInstanceOverload("cidr_ip", singletonList(CIDR_TYPE), IP_TYPE)), + Decls.newFunction( + "masked", + Decls.newInstanceOverload("cidr_masked", singletonList(CIDR_TYPE), CIDR_TYPE)), + Decls.newFunction( + "prefixLength", + Decls.newInstanceOverload( + "cidr_prefix_length", singletonList(CIDR_TYPE), Decls.Int)))); + } + + @Override + public List getProgramOptions() { + return List.of( + ProgramOption.functions( + Overload.unary(IP, NetworkLib::ip), + Overload.unary("ip_string", NetworkLib::ip), + Overload.unary(CIDR, NetworkLib::cidr), + Overload.unary("cidr_string", NetworkLib::cidr), + Overload.unary(IS_IP, NetworkLib::isIp), + Overload.unary("is_ip_string", NetworkLib::isIp), + Overload.unary("is_ip_cidr", value -> newErr("no such overload")), + Overload.unary(IP_IS_CANONICAL, NetworkLib::ipIsCanonical), + Overload.unary("ip_is_canonical_string", NetworkLib::ipIsCanonical), + Overload.unary("string_ip", value -> value.convertToType(StringType)), + Overload.unary("string_cidr", value -> value.convertToType(StringType)), + Overload.unary("cidr_ip", value -> ((CidrT) value).receive("ip", "cidr_ip"))), + ProgramOption.globals(Map.of(NET_IP, IP_TYPE_VALUE, NET_CIDR, CIDR_TYPE_VALUE))); + } + + private static Val ip(Val value) { + if (value instanceof CidrT cidr) { + return cidr.ip; + } + if (!isString(value)) { + return noSuchOverload(null, IP, value); + } + return parseIp(value.value().toString()); + } + + private static Val cidr(Val value) { + if (!isString(value)) { + return noSuchOverload(null, CIDR, value); + } + return parseCidr(value.value().toString()); + } + + private static Val isIp(Val value) { + if (!isString(value)) { + return noSuchOverload(null, IS_IP, value); + } + return parseIp(value.value().toString()) instanceof IpT ? True : False; + } + + private static Val ipIsCanonical(Val value) { + if (!isString(value)) { + return noSuchOverload(null, IP_IS_CANONICAL, value); + } + Val parsed = parseIp(value.value().toString()); + if (!(parsed instanceof IpT ip)) { + return parsed; + } + return boolOf(value.value().toString().equals(ip.canonical)); + } + + private static boolean isString(Val value) { + return value.type().typeEnum() == org.projectnessie.cel.common.types.ref.TypeEnum.String; + } + + private static Val parseIp(String text) { + if (text.contains("%")) { + return newErr("IP Address with zone value is not allowed"); + } + if (text.indexOf(':') >= 0 && text.indexOf('.') >= 0) { + return newErr("IPv4-mapped IPv6 address is not allowed"); + } + try { + if (text.indexOf(':') >= 0) { + InetAddress address = InetAddress.getByName(text); + byte[] bytes = address.getAddress(); + if (bytes.length == 4) { + return new IpT(bytes, 4); + } + if (bytes.length == 16) { + return new IpT(bytes, 6); + } + } else { + return new IpT(parseIpv4(text), 4); + } + } catch (IllegalArgumentException | UnknownHostException e) { + // fall through + } + return newErr("IP Address '%s' parse error during conversion from string", text); + } + + private static byte[] parseIpv4(String text) { + String[] parts = text.split("\\.", -1); + if (parts.length != 4) { + throw new IllegalArgumentException("invalid IPv4 address"); + } + byte[] bytes = new byte[4]; + for (int i = 0; i < 4; i++) { + String part = parts[i]; + if (part.isEmpty() || (part.length() > 1 && part.charAt(0) == '0')) { + throw new IllegalArgumentException("invalid IPv4 address"); + } + int value = Integer.parseInt(part); + if (value < 0 || value > 255) { + throw new IllegalArgumentException("invalid IPv4 address"); + } + bytes[i] = (byte) value; + } + return bytes; + } + + private static Val parseCidr(String text) { + int slash = text.indexOf('/'); + if (slash < 0 || slash != text.lastIndexOf('/') || slash == text.length() - 1) { + return newErr("network address parse error during conversion from string"); + } + String ipText = text.substring(0, slash); + String prefixText = text.substring(slash + 1); + if (ipText.contains("%")) { + return newErr("CIDR with zone value is not allowed"); + } + Val parsedIp = parseIp(ipText); + if (!(parsedIp instanceof IpT ip)) { + return parsedIp; + } + try { + int prefix = Integer.parseInt(prefixText); + int bits = ip.family == 4 ? 32 : 128; + if (prefix < 0 || prefix > bits) { + return newErr("network address parse error during conversion from string"); + } + return new CidrT(ip, prefix); + } catch (NumberFormatException e) { + return newErr("network address parse error during conversion from string"); + } + } + + private static BigInteger unsigned(byte[] bytes) { + return new BigInteger(1, bytes); + } + + private static byte[] bytes(BigInteger value, int length) { + byte[] source = value.toByteArray(); + byte[] target = new byte[length]; + int copy = Math.min(source.length, length); + System.arraycopy(source, source.length - copy, target, length - copy, copy); + return target; + } + + private static byte[] mask(byte[] address, int prefix) { + int bits = address.length * Byte.SIZE; + if (prefix == bits) { + return address.clone(); + } + BigInteger value = unsigned(address); + BigInteger mask = BigInteger.ONE.shiftLeft(bits).subtract(BigInteger.ONE); + mask = mask.xor(BigInteger.ONE.shiftLeft(bits - prefix).subtract(BigInteger.ONE)); + return bytes(value.and(mask), address.length); + } + + private static String canonicalIpv4(byte[] bytes) { + return (bytes[0] & 0xff) + + "." + + (bytes[1] & 0xff) + + "." + + (bytes[2] & 0xff) + + "." + + (bytes[3] & 0xff); + } + + private static String canonicalIpv6(byte[] bytes) { + int[] words = new int[8]; + for (int i = 0; i < words.length; i++) { + words[i] = ((bytes[i * 2] & 0xff) << 8) | (bytes[i * 2 + 1] & 0xff); + } + + int bestStart = -1; + int bestLength = 0; + for (int i = 0; i < words.length; ) { + if (words[i] != 0) { + i++; + continue; + } + int start = i; + while (i < words.length && words[i] == 0) { + i++; + } + int length = i - start; + if (length > bestLength && length > 1) { + bestStart = start; + bestLength = length; + } + } + + StringBuilder result = new StringBuilder(); + for (int i = 0; i < words.length; i++) { + if (i == bestStart) { + if (!result.isEmpty() && result.charAt(result.length() - 1) != ':') { + result.append(':'); + } + result.append(':'); + i += bestLength - 1; + continue; + } + if (!result.isEmpty() && result.charAt(result.length() - 1) != ':') { + result.append(':'); + } + result.append(Integer.toHexString(words[i])); + } + return result.toString(); + } + + private static final class IpT extends BaseVal implements Receiver { + private final byte[] bytes; + private final int family; + private final String canonical; + + private IpT(byte[] bytes, int family) { + this.bytes = bytes.clone(); + this.family = family; + this.canonical = family == 4 ? canonicalIpv4(bytes) : canonicalIpv6(bytes); + } + + @Override + @SuppressWarnings("unchecked") + public T convertToNative(Class typeDesc) { + if (typeDesc == String.class || typeDesc == Object.class) { + return (T) canonical; + } + if (typeDesc == byte[].class) { + return (T) bytes.clone(); + } + throw new IllegalArgumentException( + String.format("Unsupported conversion of '%s' to '%s'", NET_IP, typeDesc.getName())); + } + + @Override + public Val convertToType(org.projectnessie.cel.common.types.ref.Type typeVal) { + if (typeVal == StringType) { + return stringOf(canonical); + } + if (typeVal.typeName().equals(org.projectnessie.cel.common.types.TypeT.TypeType.typeName())) { + return IP_TYPE_VALUE; + } + if (typeVal.typeName().equals(NET_IP)) { + return this; + } + return newTypeConversionError(NET_IP, typeVal); + } + + @Override + public Val equal(Val other) { + if (!(other instanceof IpT otherIp)) { + return False; + } + return boolOf(Arrays.equals(bytes, otherIp.bytes)); + } + + @Override + public Val receive(String function, String overload, Val... args) { + if (args.length != 0) { + return noSuchOverload(this, function, overload, args); + } + return switch (function) { + case "family" -> intOf(family); + case "isUnspecified" -> boolOf(unsigned(bytes).signum() == 0); + case "isLoopback" -> + boolOf(family == 4 ? (bytes[0] & 0xff) == 127 : unsigned(bytes).equals(BigInteger.ONE)); + case "isGlobalUnicast" -> + boolOf(!isMulticast() && unsigned(bytes).signum() != 0 && !isBroadcast()); + case "isLinkLocalMulticast" -> + boolOf( + family == 4 + ? canonical.startsWith("224.0.0.") + : (bytes[0] & 0xff) == 0xff && (bytes[1] & 0xff) == 0x02); + case "isLinkLocalUnicast" -> + boolOf( + family == 4 + ? (bytes[0] & 0xff) == 169 && (bytes[1] & 0xff) == 254 + : (bytes[0] & 0xff) == 0xfe && ((bytes[1] & 0xc0) == 0x80)); + default -> noSuchOverload(this, function, overload, args); + }; + } + + private boolean isMulticast() { + return family == 4 ? (bytes[0] & 0xf0) == 0xe0 : (bytes[0] & 0xff) == 0xff; + } + + private boolean isBroadcast() { + return family == 4 + && unsigned(bytes).equals(BigInteger.ONE.shiftLeft(32).subtract(BigInteger.ONE)); + } + + @Override + public org.projectnessie.cel.common.types.ref.Type type() { + return IP_TYPE_VALUE; + } + + @Override + public Object value() { + return canonical; + } + } + + private static final class CidrT extends BaseVal implements Receiver { + private final IpT ip; + private final int prefix; + private final String canonical; + + private CidrT(IpT ip, int prefix) { + this.ip = ip; + this.prefix = prefix; + this.canonical = ip.canonical + "/" + prefix; + } + + @Override + @SuppressWarnings("unchecked") + public T convertToNative(Class typeDesc) { + if (typeDesc == String.class || typeDesc == Object.class) { + return (T) canonical; + } + throw new IllegalArgumentException( + String.format("Unsupported conversion of '%s' to '%s'", NET_CIDR, typeDesc.getName())); + } + + @Override + public Val convertToType(org.projectnessie.cel.common.types.ref.Type typeVal) { + if (typeVal == StringType) { + return stringOf(canonical); + } + if (typeVal.typeName().equals(org.projectnessie.cel.common.types.TypeT.TypeType.typeName())) { + return CIDR_TYPE_VALUE; + } + if (typeVal.typeName().equals(NET_CIDR)) { + return this; + } + return newTypeConversionError(NET_CIDR, typeVal); + } + + @Override + public Val equal(Val other) { + if (!(other instanceof CidrT otherCidr)) { + return False; + } + return boolOf(prefix == otherCidr.prefix && ip.equal(otherCidr.ip) == True); + } + + @Override + public Val receive(String function, String overload, Val... args) { + return switch (function) { + case "containsIP" -> containsIp(args); + case "containsCIDR" -> containsCidr(args); + case "ip" -> args.length == 0 ? ip : noSuchOverload(this, function, overload, args); + case "masked" -> + args.length == 0 ? masked() : noSuchOverload(this, function, overload, args); + case "prefixLength" -> + args.length == 0 ? intOf(prefix) : noSuchOverload(this, function, overload, args); + default -> noSuchOverload(this, function, overload, args); + }; + } + + private Val containsIp(Val[] args) { + if (args.length != 1) { + return noSuchOverload(this, "containsIP", "", args); + } + Val candidate = + args[0] instanceof IpT + ? args[0] + : isString(args[0]) ? parseIp(args[0].value().toString()) : null; + if (!(candidate instanceof IpT candidateIp)) { + return candidate != null ? candidate : noSuchOverload(this, "containsIP", "", args); + } + if (candidateIp.family != ip.family) { + return False; + } + return boolOf(Arrays.equals(mask(candidateIp.bytes, prefix), mask(ip.bytes, prefix))); + } + + private Val containsCidr(Val[] args) { + if (args.length != 1) { + return noSuchOverload(this, "containsCIDR", "", args); + } + Val candidate = + args[0] instanceof CidrT + ? args[0] + : isString(args[0]) ? parseCidr(args[0].value().toString()) : null; + if (!(candidate instanceof CidrT candidateCidr)) { + return candidate != null ? candidate : noSuchOverload(this, "containsCIDR", "", args); + } + if (candidateCidr.ip.family != ip.family || candidateCidr.prefix < prefix) { + return False; + } + return boolOf(Arrays.equals(mask(candidateCidr.ip.bytes, prefix), mask(ip.bytes, prefix))); + } + + private CidrT masked() { + return new CidrT(new IpT(mask(ip.bytes, prefix), ip.family), prefix); + } + + @Override + public org.projectnessie.cel.common.types.ref.Type type() { + return CIDR_TYPE_VALUE; + } + + @Override + public Object value() { + return canonical; + } + } +} diff --git a/core/src/main/java/org/projectnessie/cel/extension/OptionalLib.java b/core/src/main/java/org/projectnessie/cel/extension/OptionalLib.java new file mode 100644 index 00000000..f56a8e59 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/extension/OptionalLib.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.extension; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.projectnessie.cel.common.types.OptionalT.OptionalType; + +import com.google.api.expr.v1alpha1.Expr; +import com.google.api.expr.v1alpha1.Expr.ExprKindCase; +import java.util.List; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.Library; +import org.projectnessie.cel.ProgramOption; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.ErrorWithLocation; +import org.projectnessie.cel.common.Location; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.OptionalT; +import org.projectnessie.cel.interpreter.functions.Overload; +import org.projectnessie.cel.parser.ExprHelper; +import org.projectnessie.cel.parser.Macro; + +/** + * OptionalLib provides CEL optional helper functions. + * + *

This library provides runtime optional values, ordinary optional constructors/receiver + * methods, optional access operators, and lazy optMap/optFlatMap macro expansion. + */ +public final class OptionalLib implements Library { + private static final String OPTIONAL_TYPE = "optional_type"; + private static final String OPTIONAL_NONE = "optional.none"; + private static final String OPTIONAL_OF = "optional.of"; + private static final String OPTIONAL_OF_NON_ZERO_VALUE = "optional.ofNonZeroValue"; + private static final String OPTIONAL_HAS_VALUE = "hasValue"; + private static final String OPTIONAL_VALUE = "value"; + private static final String OPTIONAL_OR = "or"; + private static final String OPTIONAL_OR_VALUE = "orValue"; + private static final String OPTIONAL_OPT_MAP = "optMap"; + private static final String OPTIONAL_OPT_FLAT_MAP = "optFlatMap"; + private static final String OPTIONAL_NONE_OVERLOAD = "optional_none"; + private static final String OPTIONAL_OF_OVERLOAD = "optional_of"; + private static final String OPTIONAL_OF_NON_ZERO_VALUE_OVERLOAD = "optional_of_non_zero_value"; + private static final String OPTIONAL_SELECT_OVERLOAD = "optional_select"; + private static final String OPTIONAL_INDEX_OVERLOAD = "optional_index"; + private static final String OPTIONAL_INDEX_OPTIONAL_OVERLOAD = "optional_index_optional"; + private static final String OPTIONAL_HAS_VALUE_OVERLOAD = "optional_has_value"; + private static final String OPTIONAL_VALUE_OVERLOAD = "optional_value"; + private static final String OPTIONAL_OR_OVERLOAD = "optional_or"; + private static final String OPTIONAL_OR_VALUE_OVERLOAD = "optional_or_value"; + private static final String TYPE_PARAM_A = "A"; + private static final String OPTIONAL_MACRO_TARGET = "@optional_target"; + private static final String OPTIONAL_MACRO_RESULT = "@optional_result"; + + private OptionalLib() {} + + public static EnvOption optionals() { + return Library.Lib(new OptionalLib()); + } + + @Override + public List getCompileOptions() { + var typeParamA = Decls.newTypeParamType(TYPE_PARAM_A); + var optionalA = Decls.newAbstractType(OPTIONAL_TYPE, singletonList(typeParamA)); + var typeParams = singletonList(TYPE_PARAM_A); + + return List.of( + EnvOption.types(singletonList(OptionalType)), + EnvOption.macros( + Macro.newReceiverMacro(OPTIONAL_OPT_MAP, 2, OptionalLib::makeOptMap), + Macro.newReceiverMacro(OPTIONAL_OPT_FLAT_MAP, 2, OptionalLib::makeOptFlatMap)), + EnvOption.declarations( + Decls.newVar(OPTIONAL_TYPE, Decls.newTypeType(optionalA)), + Decls.newFunction( + OPTIONAL_NONE, + Decls.newParameterizedOverload( + OPTIONAL_NONE_OVERLOAD, emptyList(), optionalA, typeParams)), + Decls.newFunction( + OPTIONAL_OF, + Decls.newParameterizedOverload( + OPTIONAL_OF_OVERLOAD, singletonList(typeParamA), optionalA, typeParams)), + Decls.newFunction( + OPTIONAL_OF_NON_ZERO_VALUE, + Decls.newParameterizedOverload( + OPTIONAL_OF_NON_ZERO_VALUE_OVERLOAD, + singletonList(typeParamA), + optionalA, + typeParams)), + Decls.newFunction( + Operator.OptionalSelect.id, + Decls.newOverload( + OPTIONAL_SELECT_OVERLOAD, + List.of(Decls.Dyn, Decls.String), + Decls.newAbstractType(OPTIONAL_TYPE, singletonList(Decls.Dyn)))), + Decls.newFunction( + Operator.OptionalIndex.id, + Decls.newOverload( + OPTIONAL_INDEX_OVERLOAD, + List.of(Decls.Dyn, Decls.Dyn), + Decls.newAbstractType(OPTIONAL_TYPE, singletonList(Decls.Dyn)))), + Decls.newFunction( + Operator.Index.id, + Decls.newParameterizedOverload( + OPTIONAL_INDEX_OPTIONAL_OVERLOAD, + List.of(optionalA, Decls.Dyn), + Decls.newAbstractType(OPTIONAL_TYPE, singletonList(Decls.Dyn)), + typeParams)), + Decls.newFunction( + OPTIONAL_HAS_VALUE, + Decls.newParameterizedInstanceOverload( + OPTIONAL_HAS_VALUE_OVERLOAD, singletonList(optionalA), Decls.Bool, typeParams)), + Decls.newFunction( + OPTIONAL_VALUE, + Decls.newParameterizedInstanceOverload( + OPTIONAL_VALUE_OVERLOAD, singletonList(optionalA), typeParamA, typeParams)), + Decls.newFunction( + OPTIONAL_OR, + Decls.newParameterizedInstanceOverload( + OPTIONAL_OR_OVERLOAD, List.of(optionalA, optionalA), optionalA, typeParams)), + Decls.newFunction( + OPTIONAL_OR_VALUE, + Decls.newParameterizedInstanceOverload( + OPTIONAL_OR_VALUE_OVERLOAD, + List.of(optionalA, typeParamA), + typeParamA, + typeParams)))); + } + + @Override + public List getProgramOptions() { + return List.of( + ProgramOption.functions( + Overload.function(OPTIONAL_NONE, args -> OptionalT.none()), + Overload.function(OPTIONAL_NONE_OVERLOAD, args -> OptionalT.none()), + Overload.unary(OPTIONAL_OF, OptionalT::of), + Overload.unary(OPTIONAL_OF_OVERLOAD, OptionalT::of), + Overload.unary(OPTIONAL_OF_NON_ZERO_VALUE, OptionalT::ofNonZeroValue), + Overload.unary(OPTIONAL_OF_NON_ZERO_VALUE_OVERLOAD, OptionalT::ofNonZeroValue), + Overload.binary(Operator.OptionalSelect.id, OptionalT::optionalSelect), + Overload.binary(OPTIONAL_SELECT_OVERLOAD, OptionalT::optionalSelect), + Overload.binary(Operator.OptionalIndex.id, OptionalT::optionalIndex), + Overload.binary(OPTIONAL_INDEX_OVERLOAD, OptionalT::optionalIndex))); + } + + private static Expr makeOptMap(ExprHelper eh, Expr target, List args) { + return makeOptionalMap(eh, target, args, true); + } + + private static Expr makeOptFlatMap(ExprHelper eh, Expr target, List args) { + return makeOptionalMap(eh, target, args, false); + } + + private static Expr makeOptionalMap( + ExprHelper eh, Expr target, List args, boolean wrapResult) { + String variable = extractIdent(args.get(0)); + if (variable == null) { + Location location = eh.offsetLocation(args.get(0).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + + Expr boundTarget = eh.ident(OPTIONAL_MACRO_TARGET); + Expr value = eh.receiverCall(OPTIONAL_VALUE, boundTarget, emptyList()); + Expr iterRange = + eh.globalCall( + Operator.Conditional.id, + eh.receiverCall(OPTIONAL_HAS_VALUE, boundTarget, emptyList()), + eh.newList(value), + eh.newList()); + Expr init = eh.globalCall(OPTIONAL_NONE); + Expr step = wrapResult ? eh.globalCall(OPTIONAL_OF, args.get(1)) : args.get(1); + Expr accuIdent = eh.ident(Macro.AccumulatorName); + Expr result = + eh.fold( + variable, + iterRange, + Macro.AccumulatorName, + init, + eh.literalBool(true), + step, + accuIdent); + + Expr outerAccu = eh.ident(OPTIONAL_MACRO_RESULT); + Expr dynNull = eh.globalCall("dyn", eh.literalNull()); + return eh.fold( + OPTIONAL_MACRO_TARGET, + eh.newList(target), + OPTIONAL_MACRO_RESULT, + dynNull, + eh.literalBool(true), + result, + outerAccu); + } + + private static String extractIdent(Expr expression) { + if (expression.getExprKindCase() == ExprKindCase.IDENT_EXPR) { + return expression.getIdentExpr().getName(); + } + return null; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/extension/ProtoLib.java b/core/src/main/java/org/projectnessie/cel/extension/ProtoLib.java new file mode 100644 index 00000000..1fa2ebc1 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/extension/ProtoLib.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.extension; + +import static java.util.Arrays.asList; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; + +import java.util.List; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.Library; +import org.projectnessie.cel.ProgramOption; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.FieldTester; +import org.projectnessie.cel.common.types.traits.Indexer; +import org.projectnessie.cel.interpreter.functions.Overload; + +/** + * ProtoLib provides CEL protobuf extension helper functions. + * + *

The extension-name argument is a fully-qualified protobuf extension field name. Expressions + * that use extension identifiers instead of string literals need those identifiers registered with + * the type provider, for example through {@link EnvOption#types(Object...)} with protobuf + * descriptors that contain the extensions. + */ +public final class ProtoLib implements Library { + private static final String HAS_EXT = "proto.hasExt"; + private static final String GET_EXT = "proto.getExt"; + private static final String HAS_EXT_OVERLOAD = "proto_has_ext"; + private static final String GET_EXT_OVERLOAD = "proto_get_ext"; + + private ProtoLib() {} + + public static EnvOption proto() { + return Library.Lib(new ProtoLib()); + } + + @Override + public List getCompileOptions() { + return List.of( + EnvOption.declarations( + Decls.newFunction( + HAS_EXT, + Decls.newOverload(HAS_EXT_OVERLOAD, asList(Decls.Dyn, Decls.String), Decls.Bool)), + Decls.newFunction( + GET_EXT, + Decls.newOverload(GET_EXT_OVERLOAD, asList(Decls.Dyn, Decls.String), Decls.Dyn)))); + } + + @Override + public List getProgramOptions() { + return List.of( + ProgramOption.functions( + Overload.binary(HAS_EXT, ProtoLib::hasExt), + Overload.binary(HAS_EXT_OVERLOAD, ProtoLib::hasExt), + Overload.binary(GET_EXT, ProtoLib::getExt), + Overload.binary(GET_EXT_OVERLOAD, ProtoLib::getExt))); + } + + private static Val hasExt(Val object, Val extensionName) { + if (!(object instanceof FieldTester) || !(extensionName instanceof StringT)) { + return noSuchOverload(object, HAS_EXT, HAS_EXT_OVERLOAD, new Val[] {object, extensionName}); + } + return ((FieldTester) object).isSet(extensionName); + } + + private static Val getExt(Val object, Val extensionName) { + if (!(object instanceof Indexer) || !(extensionName instanceof StringT)) { + return noSuchOverload(object, GET_EXT, GET_EXT_OVERLOAD, new Val[] {object, extensionName}); + } + return ((Indexer) object).get(extensionName); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java b/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java index 4d55b1eb..571ebcd1 100644 --- a/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java +++ b/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java @@ -15,6 +15,11 @@ */ package org.projectnessie.cel.extension; +import static java.math.RoundingMode.HALF_EVEN; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.projectnessie.cel.common.types.IntT.intOf; + +import java.math.BigDecimal; import java.util.*; import java.util.regex.Pattern; import org.projectnessie.cel.EnvOption; @@ -22,6 +27,10 @@ import org.projectnessie.cel.ProgramOption; import org.projectnessie.cel.checker.Decls; import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Indexer; +import org.projectnessie.cel.common.types.traits.Sizer; import org.projectnessie.cel.interpreter.functions.Overload; /** @@ -239,41 +248,43 @@ public class StringsLib implements Library { private static final String LAST_INDEX_OF = "lastIndexOf"; private static final String LOWER_ASCII = "lowerAscii"; private static final String REPLACE = "replace"; + private static final String REVERSE = "reverse"; private static final String SPLIT = "split"; private static final String SUBSTR = "substring"; private static final String TRIM_SPACE = "trim"; private static final String UPPER_ASCII = "upperAscii"; + private static final String FORMAT = "format"; + private static final String QUOTE = "strings.quote"; // whitespace characters definition from // https://en.wikipedia.org/wiki/Whitespace_character#Unicode private static final Set UNICODE_WHITE_SPACES = - new HashSet<>( - Arrays.asList( - (char) 0x0009, - (char) 0x000A, - (char) 0x000B, - (char) 0x000C, - (char) 0x000D, - (char) 0x0020, - (char) 0x0085, - (char) 0x00A0, - (char) 0x1680, - (char) 0x2000, - (char) 0x2001, - (char) 0x2002, - (char) 0x2003, - (char) 0x2004, - (char) 0x2005, - (char) 0x2006, - (char) 0x2007, - (char) 0x2008, - (char) 0x2009, - (char) 0x200A, - (char) 0x2028, - (char) 0x2029, - (char) 0x202F, - (char) 0x205F, - (char) 0x3000)); + Set.of( + (char) 0x0009, + (char) 0x000A, + (char) 0x000B, + (char) 0x000C, + (char) 0x000D, + (char) 0x0020, + (char) 0x0085, + (char) 0x00A0, + (char) 0x1680, + (char) 0x2000, + (char) 0x2001, + (char) 0x2002, + (char) 0x2003, + (char) 0x2004, + (char) 0x2005, + (char) 0x2006, + (char) 0x2007, + (char) 0x2008, + (char) 0x2009, + (char) 0x200A, + (char) 0x2028, + (char) 0x2029, + (char) 0x202F, + (char) 0x205F, + (char) 0x3000); public static EnvOption strings() { return Library.Lib(new StringsLib()); @@ -281,84 +292,89 @@ public static EnvOption strings() { @Override public List getCompileOptions() { - List list = new ArrayList<>(); EnvOption option = EnvOption.declarations( Decls.newFunction( CHAR_AT, Decls.newInstanceOverload( - "string_char_at_int", Arrays.asList(Decls.String, Decls.Int), Decls.String)), + "string_char_at_int", List.of(Decls.String, Decls.Int), Decls.String)), Decls.newFunction( INDEX_OF, Decls.newInstanceOverload( - "string_index_of_string", Arrays.asList(Decls.String, Decls.String), Decls.Int), + "string_index_of_string", List.of(Decls.String, Decls.String), Decls.Int), Decls.newInstanceOverload( "string_index_of_string_int", - Arrays.asList(Decls.String, Decls.String, Decls.Int), + List.of(Decls.String, Decls.String, Decls.Int), Decls.Int)), Decls.newFunction( JOIN, Decls.newInstanceOverload( - "list_join", Arrays.asList(Decls.newListType(Decls.String)), Decls.String), + "list_join", List.of(Decls.newListType(Decls.String)), Decls.String), Decls.newInstanceOverload( "list_join_string", - Arrays.asList(Decls.newListType(Decls.String), Decls.String), + List.of(Decls.newListType(Decls.String), Decls.String), Decls.String)), Decls.newFunction( LAST_INDEX_OF, Decls.newInstanceOverload( - "string_last_index_of_string", - Arrays.asList(Decls.String, Decls.String), - Decls.Int), + "string_last_index_of_string", List.of(Decls.String, Decls.String), Decls.Int), Decls.newInstanceOverload( "string_last_index_of_string_int", - Arrays.asList(Decls.String, Decls.String, Decls.Int), + List.of(Decls.String, Decls.String, Decls.Int), Decls.Int)), Decls.newFunction( LOWER_ASCII, Decls.newInstanceOverload( - "string_lower_ascii", Arrays.asList(Decls.String), Decls.String)), + "string_lower_ascii", List.of(Decls.String), Decls.String)), Decls.newFunction( REPLACE, Decls.newInstanceOverload( "string_replace_string_string", - Arrays.asList(Decls.String, Decls.String, Decls.String), + List.of(Decls.String, Decls.String, Decls.String), Decls.String), Decls.newInstanceOverload( "string_replace_string_string_int", - Arrays.asList(Decls.String, Decls.String, Decls.String, Decls.Int), + List.of(Decls.String, Decls.String, Decls.String, Decls.Int), Decls.String)), + Decls.newFunction( + REVERSE, + Decls.newInstanceOverload("string_reverse", List.of(Decls.String), Decls.String)), Decls.newFunction( SPLIT, Decls.newInstanceOverload( - "string_split_string", Arrays.asList(Decls.String, Decls.String), Decls.Dyn), + "string_split_string", List.of(Decls.String, Decls.String), Decls.Dyn), Decls.newInstanceOverload( "string_split_string_int", - Arrays.asList(Decls.String, Decls.String, Decls.Int), + List.of(Decls.String, Decls.String, Decls.Int), Decls.Dyn)), Decls.newFunction( SUBSTR, Decls.newInstanceOverload( - "string_substring_int", Arrays.asList(Decls.String, Decls.Int), Decls.String), + "string_substring_int", List.of(Decls.String, Decls.Int), Decls.String), Decls.newInstanceOverload( "string_substring_int_int", - Arrays.asList(Decls.String, Decls.Int, Decls.Int), + List.of(Decls.String, Decls.Int, Decls.Int), Decls.String)), Decls.newFunction( TRIM_SPACE, - Decls.newInstanceOverload( - "string_trim", Arrays.asList(Decls.String), Decls.String)), + Decls.newInstanceOverload("string_trim", List.of(Decls.String), Decls.String)), Decls.newFunction( UPPER_ASCII, Decls.newInstanceOverload( - "string_upper_ascii", Arrays.asList(Decls.String), Decls.String))); - list.add(option); - return list; + "string_upper_ascii", List.of(Decls.String), Decls.String)), + Decls.newFunction( + FORMAT, + Decls.newInstanceOverload( + "string_format", + List.of(Decls.String, Decls.newListType(Decls.Dyn)), + Decls.String)), + Decls.newFunction( + QUOTE, Decls.newOverload("strings_quote", List.of(Decls.String), Decls.String))); + return List.of(option); } @Override public List getProgramOptions() { - List list = new ArrayList<>(); ProgramOption functions = ProgramOption.functions( Overload.binary(CHAR_AT, Guards.callInStrIntOutStr(StringsLib::charAt)), @@ -367,7 +383,10 @@ public List getProgramOptions() { null, null, Guards.callInStrStrOutInt(StringsLib::indexOf), - Guards.callInStrStrIntOutInt(StringsLib::indexOfOffset)), + values -> + values.length == 3 + ? Guards.callInStrStrIntOutInt(StringsLib::indexOfOffset).invoke(values) + : Err.maybeNoSuchOverloadErr(null)), Overload.overload( JOIN, null, @@ -379,7 +398,10 @@ public List getProgramOptions() { null, null, Guards.callInStrStrOutInt(StringsLib::lastIndexOf), - Guards.callInStrStrIntOutInt(StringsLib::lastIndexOfOffset)), + values -> + values.length == 3 + ? Guards.callInStrStrIntOutInt(StringsLib::lastIndexOfOffset).invoke(values) + : Err.maybeNoSuchOverloadErr(null)), Overload.unary(LOWER_ASCII, Guards.callInStrOutStr(StringsLib::lowerASCII)), Overload.overload( REPLACE, @@ -395,22 +417,30 @@ public List getProgramOptions() { } return Err.maybeNoSuchOverloadErr(null); }), + Overload.unary(REVERSE, Guards.callInStrOutStr(StringsLib::reverse)), Overload.overload( SPLIT, null, null, Guards.callInStrStrOutStrArr(StringsLib::split), - Guards.callInStrStrIntOutStrArr(StringsLib::splitN)), + values -> + values.length == 3 + ? Guards.callInStrStrIntOutStrArr(StringsLib::splitN).invoke(values) + : Err.maybeNoSuchOverloadErr(null)), Overload.overload( SUBSTR, null, null, Guards.callInStrIntOutStr(StringsLib::substr), - Guards.callInStrIntIntOutStr(StringsLib::substrRange)), + values -> + values.length == 3 + ? Guards.callInStrIntIntOutStr(StringsLib::substrRange).invoke(values) + : Err.maybeNoSuchOverloadErr(null)), Overload.unary(TRIM_SPACE, Guards.callInStrOutStr(StringsLib::trimSpace)), - Overload.unary(UPPER_ASCII, Guards.callInStrOutStr(StringsLib::upperASCII))); - list.add(functions); - return list; + Overload.unary(UPPER_ASCII, Guards.callInStrOutStr(StringsLib::upperASCII)), + Overload.binary(FORMAT, StringsLib::format), + Overload.unary(QUOTE, Guards.callInStrOutStr(StringsLib::quote))); + return List.of(functions); } static String charAt(String str, int index) { @@ -468,6 +498,51 @@ static String replace(String str, String old, String replacement) { return str.replace(old, replacement); } + static String reverse(String str) { + return new StringBuilder(str).reverse().toString(); + } + + static String quote(String str) { + StringBuilder quoted = new StringBuilder(str.length() + 2); + quoted.append('"'); + for (int offset = 0; offset < str.length(); ) { + int codePoint = str.codePointAt(offset); + offset += Character.charCount(codePoint); + switch (codePoint) { + case '\u0007': + quoted.append("\\a"); + break; + case '\b': + quoted.append("\\b"); + break; + case '\f': + quoted.append("\\f"); + break; + case '\n': + quoted.append("\\n"); + break; + case '\r': + quoted.append("\\r"); + break; + case '\t': + quoted.append("\\t"); + break; + case '\u000b': + quoted.append("\\v"); + break; + case '\\': + quoted.append("\\\\"); + break; + case '"': + quoted.append("\\\""); + break; + default: + quoted.appendCodePoint(codePoint); + } + } + return quoted.append('"').toString(); + } + /** * replace first n non-overlapping instance of {old} replaced by {replacement}. It works as strings.Replace in Go to have consistent behavior @@ -492,7 +567,7 @@ static String replaceN(String str, String old, String replacement, int n) { int count = 0; for (; count < n && index < str.length(); count++) { - if (old.length() == 0) { + if (old.isEmpty()) { stringBuilder.append(replacement).append(str, index, index + 1); index++; } else { @@ -543,7 +618,7 @@ static String[] splitN(String s, String sep, int n) { if (n == 1) { return new String[] {s}; } - if (sep.length() == 0) { + if (sep.isEmpty()) { return explode(s, n); } @@ -652,4 +727,241 @@ static String upperASCII(String str) { } return stringBuilder.toString(); } + + private static Val format(Val pattern, Val args) { + if (!(pattern instanceof StringT) || !(args instanceof Sizer) || !(args instanceof Indexer)) { + return Err.maybeNoSuchOverloadErr(null); + } + try { + return StringT.stringOf( + formatPattern((String) pattern.value(), (Sizer) args, (Indexer) args)); + } catch (FormatException e) { + return Err.newErr("%s", e.getMessage()); + } + } + + private static String formatPattern(String pattern, Sizer argsSizer, Indexer argsIndexer) { + int argCount = Math.toIntExact(argsSizer.size().intValue()); + int argIndex = 0; + StringBuilder out = new StringBuilder(pattern.length()); + for (int i = 0; i < pattern.length(); i++) { + char ch = pattern.charAt(i); + if (ch != '%') { + out.append(ch); + continue; + } + if (++i >= pattern.length()) { + throw new FormatException("could not parse formatting clause: missing formatting clause"); + } + ch = pattern.charAt(i); + if (ch == '%') { + out.append('%'); + continue; + } + int precision = -1; + if (ch == '.') { + int precisionStart = ++i; + while (i < pattern.length() && Character.isDigit(pattern.charAt(i))) { + i++; + } + if (precisionStart == i || i >= pattern.length()) { + throw new FormatException("could not parse formatting clause: malformed precision"); + } + precision = Integer.parseInt(pattern.substring(precisionStart, i)); + ch = pattern.charAt(i); + } + if ("sdboxXfe".indexOf(ch) < 0) { + throw new FormatException( + "could not parse formatting clause: unrecognized formatting clause \"%s\"", ch); + } + if (argIndex >= argCount) { + throw new FormatException("index %d out of range", argIndex); + } + Val arg = argsIndexer.get(intOf(argIndex++)); + out.append(formatValue(ch, precision, arg)); + } + return out.toString(); + } + + private static String formatValue(char clause, int precision, Val arg) { + return switch (clause) { + case 's' -> renderStringClause(arg); + case 'd' -> renderDecimalClause(arg); + case 'b' -> renderBinaryClause(arg); + case 'o' -> renderOctalClause(arg); + case 'x', 'X' -> renderHexClause(arg, clause == 'X'); + case 'f' -> renderFixedPointClause(arg, precision >= 0 ? precision : 6); + case 'e' -> renderScientificClause(arg, precision >= 0 ? precision : 6); + default -> + throw new FormatException( + "could not parse formatting clause: unrecognized formatting clause \"%s\"", clause); + }; + } + + private static String renderStringClause(Val value) { + return switch (value.type().typeEnum()) { + case String -> value.value().toString(); + case Bool -> Boolean.toString(value.booleanValue()); + case Bytes -> new String(value.convertToNative(byte[].class), UTF_8); + case Int -> Long.toString(value.intValue()); + case Uint -> Long.toUnsignedString(value.intValue()); + case Double -> renderDouble(value.doubleValue()); + case Null -> "null"; + case Type, Duration, Timestamp -> value.convertToType(StringT.StringType).value().toString(); + case List -> renderList(value); + case Map -> renderMap(value); + default -> + throw new FormatException( + "error during formatting: string clause can only be used on strings, bools, bytes, ints, doubles, maps, lists, types, durations, and timestamps, was given %s", + value.type().typeName()); + }; + } + + private static String renderDecimalClause(Val value) { + switch (value.type().typeEnum()) { + case Int: + return Long.toString(value.intValue()); + case Uint: + return Long.toUnsignedString(value.intValue()); + case Double: + double d = value.doubleValue(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + return renderDouble(d); + } + break; + default: + } + throw new FormatException( + "error during formatting: decimal clause can only be used on integers, was given %s", + value.type().typeName()); + } + + private static String renderBinaryClause(Val value) { + return switch (value.type().typeEnum()) { + case Int -> Long.toBinaryString(value.intValue()); + case Uint -> Long.toUnsignedString(value.intValue(), 2); + case Bool -> value.booleanValue() ? "1" : "0"; + default -> + throw new FormatException( + "error during formatting: only integers and bools can be formatted as binary, was given %s", + value.type().typeName()); + }; + } + + private static String renderOctalClause(Val value) { + return switch (value.type().typeEnum()) { + case Int -> Long.toOctalString(value.intValue()); + case Uint -> Long.toUnsignedString(value.intValue(), 8); + default -> + throw new FormatException( + "error during formatting: octal clause can only be used on integers, was given %s", + value.type().typeName()); + }; + } + + private static String renderHexClause(Val value, boolean upperCase) { + String hex = + switch (value.type().typeEnum()) { + case Int -> Long.toHexString(value.intValue()); + case Uint -> Long.toUnsignedString(value.intValue(), 16); + case String -> bytesToHex(value.value().toString().getBytes(UTF_8)); + case Bytes -> bytesToHex(value.convertToNative(byte[].class)); + default -> + throw new FormatException( + "error during formatting: only integers, byte buffers, and strings can be formatted as hex, was given %s", + value.type().typeName()); + }; + return upperCase ? hex.toUpperCase(Locale.ROOT) : hex; + } + + private static String renderFixedPointClause(Val value, int precision) { + switch (value.type().typeEnum()) { + case Int: + case Uint: + case Double: + double d = value.doubleValue(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + return renderDouble(d); + } + return BigDecimal.valueOf(d).setScale(precision, HALF_EVEN).toPlainString(); + default: + throw new FormatException( + "error during formatting: fixed-point clause can only be used on doubles, was given %s", + value.type().typeName()); + } + } + + private static String renderScientificClause(Val value, int precision) { + switch (value.type().typeEnum()) { + case Int: + case Uint: + case Double: + double d = value.doubleValue(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + return renderDouble(d); + } + return String.format(Locale.ROOT, "%." + precision + "e", d); + default: + throw new FormatException( + "error during formatting: scientific clause can only be used on doubles, was given %s", + value.type().typeName()); + } + } + + private static String renderList(Val value) { + Sizer sizer = (Sizer) value; + Indexer indexer = (Indexer) value; + int size = Math.toIntExact(sizer.size().intValue()); + StringBuilder out = new StringBuilder("["); + for (int i = 0; i < size; i++) { + if (i > 0) { + out.append(", "); + } + out.append(renderStringClause(indexer.get(intOf(i)))); + } + return out.append(']').toString(); + } + + private static String renderMap(Val value) { + org.projectnessie.cel.common.types.IteratorT iterator = + ((org.projectnessie.cel.common.types.IterableT) value).iterator(); + Indexer indexer = (Indexer) value; + List entries = new ArrayList<>(); + while (iterator.hasNext().booleanValue()) { + Val key = iterator.next(); + entries.add(renderStringClause(key) + ": " + renderStringClause(indexer.get(key))); + } + Collections.sort(entries); + return "{" + String.join(", ", entries) + "}"; + } + + private static String renderDouble(double value) { + if (Double.isNaN(value)) { + return "NaN"; + } + if (value == Double.POSITIVE_INFINITY) { + return "Infinity"; + } + if (value == Double.NEGATIVE_INFINITY) { + return "-Infinity"; + } + return Double.toString(value); + } + + private static String bytesToHex(byte[] bytes) { + char[] hex = new char[bytes.length * 2]; + char[] digits = "0123456789abcdef".toCharArray(); + for (int i = 0; i < bytes.length; i++) { + int b = bytes[i] & 0xff; + hex[i * 2] = digits[b >>> 4]; + hex[i * 2 + 1] = digits[b & 0x0f]; + } + return new String(hex); + } + + private static final class FormatException extends RuntimeException { + FormatException(String message, Object... args) { + super(String.format(Locale.ROOT, message, args)); + } + } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Activation.java b/core/src/main/java/org/projectnessie/cel/interpreter/Activation.java index ab7fbcac..d68b68aa 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Activation.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Activation.java @@ -101,12 +101,14 @@ public Activation parent() { /** ResolveName implements the Activation interface method. */ @Override public ResolvedValue resolveName(String name) { - if (!bindings.containsKey(name)) { - return ResolvedValue.ABSENT; + if (name.startsWith(".")) { + name = name.substring(1); } - Object obj = bindings.get(name); if (obj == null) { + if (!bindings.containsKey(name)) { + return ResolvedValue.ABSENT; + } return ResolvedValue.NULL_VALUE; } @@ -140,6 +142,9 @@ public Activation parent() { /** ResolveName implements the Activation interface method. */ @Override public ResolvedValue resolveName(String name) { + if (name.startsWith(".")) { + name = name.substring(1); + } Object result = provider.apply(name); if (result instanceof ResolvedValue) { return (ResolvedValue) result; @@ -177,6 +182,9 @@ public Activation parent() { /** ResolveName implements the Activation interface method. */ @Override public ResolvedValue resolveName(String name) { + if (name.startsWith(".")) { + return parent.resolveName(name.substring(1)); + } ResolvedValue object = child.resolveName(name); if (object.present()) { return object; @@ -237,6 +245,9 @@ public Activation parent() { @Override public ResolvedValue resolveName(String name) { + if (name.startsWith(".")) { + return delegate.resolveName(name); + } return delegate.resolveName(name); } @@ -279,6 +290,9 @@ public Activation parent() { /** ResolveName implements the Activation interface method. */ @Override public ResolvedValue resolveName(String name) { + if (name.startsWith(".")) { + return parent.resolveName(name.substring(1)); + } if (name.equals(this.name)) { return ResolvedValue.resolvedValue(val); } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/AstPruner.java b/core/src/main/java/org/projectnessie/cel/interpreter/AstPruner.java index 47f16cc9..52b7dc34 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/AstPruner.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/AstPruner.java @@ -78,20 +78,17 @@ * prepare the overloads accordingly. */ public final class AstPruner { - private final Expr expr; private final EvalState state; private long nextExprID; - private AstPruner(Expr expr, EvalState state, long nextExprID) { - this.expr = expr; + private AstPruner(EvalState state, long nextExprID) { this.state = state; this.nextExprID = nextExprID; } public static Expr pruneAst(Expr expr, EvalState state) { - AstPruner pruner = new AstPruner(expr, state, 1); - Expr newExpr = pruner.prune(expr); - return newExpr; + AstPruner pruner = new AstPruner(state, 1); + return pruner.prune(expr); } static Expr createLiteral(long id, Constant val) { @@ -124,8 +121,7 @@ Expr maybeCreateLiteral(long id, Val v) { } // Attempt to build a list literal. - if (v instanceof Lister) { - Lister list = (Lister) v; + if (v instanceof Lister list) { int sz = (int) list.size().intValue(); List elemExprs = new ArrayList<>(sz); for (int i = 0; i < sz; i++) { @@ -146,8 +142,7 @@ Expr maybeCreateLiteral(long id, Val v) { } // Create a map literal if possible. - if (v instanceof Mapper) { - Mapper mp = (Mapper) v; + if (v instanceof Mapper mp) { IteratorT it = mp.iterator(); List entries = new ArrayList<>((int) mp.size().intValue()); while (it.hasNext() == True) { @@ -321,33 +316,32 @@ Expr prune(Expr node) { boolean prunedStruct = false; CreateStruct struct = node.getStructExpr(); List entries = struct.getEntriesList(); - String messageType = struct.getMessageName(); List newEntries = new ArrayList<>(entries.size()); for (int i = 0; i < entries.size(); i++) { Entry entry = entries.get(i); newEntries.add(entry); Expr mapKey = entry.getMapKey(); - Expr newKey = mapKey != Entry.getDefaultInstance().getMapKey() ? prune(mapKey) : null; + Expr newKey = entry.hasMapKey() ? prune(mapKey) : null; Expr newValue = prune(entry.getValue()); if ((newKey == null || newKey == mapKey) && (newValue == null || newValue == entry.getValue())) { continue; } prunedStruct = true; - Entry newEntry; - if (!messageType.isEmpty()) { - newEntry = - Entry.newBuilder().setFieldKey(entry.getFieldKey()).setValue(newValue).build(); - } else { - newEntry = Entry.newBuilder().setMapKey(newKey).setValue(newValue).build(); + Entry.Builder newEntry = Entry.newBuilder(entry); + if (newKey != null && newKey != mapKey) { + newEntry.setMapKey(newKey); } - newEntries.set(i, newEntry); + if (newValue != null && newValue != entry.getValue()) { + newEntry.setValue(newValue); + } + newEntries.set(i, newEntry.build()); } if (prunedStruct) { return Expr.newBuilder() .setId(node.getId()) .setStructExpr( - CreateStruct.newBuilder().setMessageName(messageType).addAllEntries(entries)) + CreateStruct.newBuilder(struct).clearEntries().addAllEntries(newEntries)) .build(); } break; @@ -363,6 +357,7 @@ Expr prune(Expr node) { .setComprehensionExpr( Comprehension.newBuilder() .setIterVar(compre.getIterVar()) + .setIterVar2(compre.getIterVar2()) .setIterRange(newRange) .setAccuVar(compre.getAccuVar()) .setAccuInit(compre.getAccuInit()) diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/AttributeFactory.java b/core/src/main/java/org/projectnessie/cel/interpreter/AttributeFactory.java index 06008e16..44618e0e 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/AttributeFactory.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/AttributeFactory.java @@ -126,6 +126,10 @@ interface ConstantQualifier extends Qualifier { interface ConstantQualifierEquator extends QualifierValueEquator, ConstantQualifier {} + interface ValQualifier extends Qualifier { + Val qualifyToVal(Activation vars, Object obj); + } + /** * Attribute values are a variable or value with an optional set of qualifiers, such as field, * key, or index accesses. @@ -212,7 +216,10 @@ public AttributeFactory.Attribute conditionalAttribute( @Override public Attribute maybeAttribute(long id, String name) { List attrs = new ArrayList<>(); - attrs.add(absoluteAttribute(id, container.resolveCandidateNames(name))); + attrs.add( + name.startsWith(".") + ? absoluteAttribute(id, name) + : absoluteAttribute(id, container.resolveCandidateNames(name))); return new MaybeAttribute(id, attrs, adapter, provider, this); } @@ -228,8 +235,7 @@ public AttributeFactory.Qualifier newQualifier(Type objType, long qualID, Object // Before creating a new qualifier check to see if this is a protobuf message field access. // If so, use the precomputed GetFrom qualification method rather than the standard // stringQualifier. - if (val instanceof String) { - String str = (String) val; + if (val instanceof String str) { if (objType != null && !objType.getMessageType().isEmpty()) { FieldType ft = provider.findFieldType(objType.getMessageType(), str); if (ft != null && ft.isSet != null && ft.getFrom != null) { @@ -353,23 +359,16 @@ public Object resolve(org.projectnessie.cel.interpreter.Activation vars) { */ @Override public Object tryResolve(org.projectnessie.cel.interpreter.Activation vars) { + Object local = tryResolveCurrentVar(vars); + if (local != null) { + return local; + } for (String nm : namespaceNames) { // If the variable is found, process it. Otherwise, wait until the checks to // determine whether the type is unknown before returning. ResolvedValue obj = vars.resolveName(nm); if (obj.present()) { - Object op = obj.value(); - for (Qualifier qual : qualifiers) { - Object op2 = qual.qualify(vars, op); - if (op2 instanceof Err) { - return op2; - } - if (op2 == null) { - break; - } - op = op2; - } - return op; + return resolveQualifiers(vars, obj.value()); } // Attempt to resolve the qualified type name if the name is not a variable identifier. Val typ = provider.findIdent(nm); @@ -383,6 +382,45 @@ public Object tryResolve(org.projectnessie.cel.interpreter.Activation vars) { throw noSuchAttributeException(this); } + private Object tryResolveCurrentVar(org.projectnessie.cel.interpreter.Activation vars) { + if (vars instanceof Activation.VarActivation var + && (namespaceNames.length > 1 || !qualifiers.isEmpty())) { + String localName = namespaceNames[namespaceNames.length - 1]; + if (localName.equals(var.name)) { + return resolveQualifiers(vars, var.val); + } + } + return null; + } + + private Object resolveQualifiers( + org.projectnessie.cel.interpreter.Activation vars, Object obj) { + Object op = obj; + for (int i = 0; i < qualifiers.size(); i++) { + Qualifier qual = qualifiers.get(i); + Object op2 = qualify(vars, op, qual, i == qualifiers.size() - 1); + if (op2 instanceof Err) { + return op2; + } + if (op2 == null) { + break; + } + op = op2; + } + return op; + } + + private Object qualify( + org.projectnessie.cel.interpreter.Activation vars, + Object obj, + Qualifier qualifier, + boolean last) { + if (last && qualifier instanceof ValQualifier) { + return ((ValQualifier) qualifier).qualifyToVal(vars, obj); + } + return qualifier.qualify(vars, obj); + } + /** String implements the Stringer interface method. */ @Override public String toString() { @@ -561,8 +599,7 @@ public Cost cost() { public Attribute addQualifier(AttributeFactory.Qualifier qual) { String str = ""; boolean isStr = false; - if (qual instanceof ConstantQualifier) { - ConstantQualifier cq = (ConstantQualifier) qual; + if (qual instanceof ConstantQualifier cq) { Object cqv = cq.value().value(); if (cqv instanceof String) { str = (String) cqv; @@ -608,6 +645,14 @@ public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object */ @Override public Object resolve(org.projectnessie.cel.interpreter.Activation vars) { + for (NamespacedAttribute attr : attrs) { + if (attr instanceof AbsoluteAttribute) { + Object result = ((AbsoluteAttribute) attr).tryResolveCurrentVar(vars); + if (result != null) { + return result; + } + } + } for (NamespacedAttribute attr : attrs) { try { return attr.tryResolve(vars); @@ -696,11 +741,16 @@ public Object resolve(org.projectnessie.cel.interpreter.Activation vars) { } // Next, qualify it. Qualification handles unkonwns as well, so there's no need to recheck. Object obj = v; - for (Qualifier qual : qualifiers) { + for (int i = 0; i < qualifiers.size(); i++) { + Qualifier qual = qualifiers.get(i); if (obj == null) { throw noSuchAttributeException(this); } - obj = qual.qualify(vars, obj); + if (i == qualifiers.size() - 1 && qual instanceof ValQualifier) { + obj = ((ValQualifier) qual).qualifyToVal(vars, obj); + } else { + obj = qual.qualify(vars, obj); + } if (obj instanceof Err) { return obj; } @@ -725,8 +775,7 @@ static Qualifier newQualifierStatic(TypeAdapter adapter, long id, Object v) { Class c = v.getClass(); - if (v instanceof Val) { - Val val = (Val) v; + if (v instanceof Val val) { switch (val.type().typeEnum()) { case String: return new StringQualifier(id, (String) val.value(), val, adapter); @@ -834,8 +883,7 @@ public long id() { @Override public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { String s = value; - if (obj instanceof Map) { - Map m = (Map) obj; + if (obj instanceof Map m) { obj = m.get(s); if (obj == null) { if (m.containsKey(s)) { @@ -911,8 +959,7 @@ public long id() { @Override public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { double i = value; - if (obj instanceof Map) { - Map m = (Map) obj; + if (obj instanceof Map m) { obj = m.get(i); if (obj == null) { obj = m.get((int) i); @@ -933,8 +980,7 @@ public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj = Array.get(obj, (int) i); return obj; } - if (obj instanceof List) { - List list = (List) obj; + if (obj instanceof List list) { int l = list.size(); if (i < 0 || i >= l) { throw indexOutOfBoundsException(i); @@ -1010,8 +1056,7 @@ public long id() { @Override public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { long i = value; - if (obj instanceof Map) { - Map m = (Map) obj; + if (obj instanceof Map m) { obj = m.get(i); if (obj == null) { obj = m.get((int) i); @@ -1032,8 +1077,7 @@ public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj = Array.get(obj, (int) i); return obj; } - if (obj instanceof List) { - List list = (List) obj; + if (obj instanceof List list) { int l = list.size(); if (i < 0 || i >= l) { throw indexOutOfBoundsException(i); @@ -1109,8 +1153,7 @@ public long id() { @Override public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { long i = value; - if (obj instanceof Map) { - Map m = (Map) obj; + if (obj instanceof Map m) { obj = m.get(ULong.valueOf(i)); if (obj == null) { throw noSuchKeyException(i); @@ -1190,8 +1233,7 @@ public long id() { @Override public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { boolean b = value; - if (obj instanceof Map) { - Map m = (Map) obj; + if (obj instanceof Map m) { obj = m.get(b); if (obj == null) { if (m.containsKey(b)) { @@ -1304,7 +1346,7 @@ public String toString() { * type. When the field type is known this can be used to improve the speed and efficiency of * field resolution. */ - final class FieldQualifier implements Coster, ConstantQualifierEquator { + final class FieldQualifier implements Coster, ConstantQualifierEquator, ValQualifier { final long id; final String name; final FieldType fieldType; @@ -1332,6 +1374,11 @@ public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object return fieldType.getFrom.getFrom(obj); } + @Override + public Val qualifyToVal(org.projectnessie.cel.interpreter.Activation vars, Object obj) { + return adapter.nativeToValue(qualify(vars, obj)); + } + /** Value implements the ConstantQualifier interface */ @Override public Val value() { @@ -1374,16 +1421,14 @@ public String toString() { */ static Val refResolve(TypeAdapter adapter, Val idx, Object obj) { Val celVal = adapter.nativeToValue(obj); - if (celVal instanceof Mapper) { - Mapper mapper = (Mapper) celVal; + if (celVal instanceof Mapper mapper) { Val elem = mapper.find(idx); if (elem == null) { return noSuchKey(idx); } return elem; } - if (celVal instanceof Indexer) { - Indexer indexer = (Indexer) celVal; + if (celVal instanceof Indexer indexer) { return indexer.get(idx); } if (isUnknown(celVal)) { diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/AttributePattern.java b/core/src/main/java/org/projectnessie/cel/interpreter/AttributePattern.java index 44788c08..b8281d38 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/AttributePattern.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/AttributePattern.java @@ -165,8 +165,7 @@ public boolean matches(Qualifier q) { if (wildcard) { return true; } - if (q instanceof QualifierValueEquator) { - QualifierValueEquator qve = (QualifierValueEquator) q; + if (q instanceof QualifierValueEquator qve) { return qve.qualifierValueEquals(value); } return false; @@ -419,8 +418,7 @@ public Object resolve(org.projectnessie.cel.interpreter.Activation vars) { @Override public Object tryResolve(org.projectnessie.cel.interpreter.Activation vars) { long id = attr.id(); - if (vars instanceof PartialActivation) { - PartialActivation partial = (PartialActivation) vars; + if (vars instanceof PartialActivation partial) { Object unk = fac.matchesUnknownPatterns(partial, id, candidateVariableNames(), qualifiers); if (unk != null) { return unk; diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java index 703f6770..61f18da0 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java @@ -19,17 +19,22 @@ import static org.projectnessie.cel.common.types.BoolT.True; import static org.projectnessie.cel.common.types.Err.isError; import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.noSuchAttributeException; import static org.projectnessie.cel.common.types.Err.noSuchOverload; import static org.projectnessie.cel.common.types.Err.valOrErr; +import static org.projectnessie.cel.common.types.IntT.intOf; import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; import static org.projectnessie.cel.common.types.Util.isUnknownOrError; import static org.projectnessie.cel.interpreter.Activation.emptyActivation; import static org.projectnessie.cel.interpreter.Coster.Cost.OneOne; import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; import static org.projectnessie.cel.interpreter.Coster.costOf; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -37,18 +42,24 @@ import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.IterableT; import org.projectnessie.cel.common.types.IteratorT; +import org.projectnessie.cel.common.types.ListT; +import org.projectnessie.cel.common.types.MapT; +import org.projectnessie.cel.common.types.OptionalT; import org.projectnessie.cel.common.types.Overloads; import org.projectnessie.cel.common.types.StringT; import org.projectnessie.cel.common.types.ref.FieldType; import org.projectnessie.cel.common.types.ref.TypeAdapter; -import org.projectnessie.cel.common.types.ref.TypeEnum; import org.projectnessie.cel.common.types.ref.TypeProvider; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.common.types.traits.Container; import org.projectnessie.cel.common.types.traits.FieldTester; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.common.types.traits.Mapper; import org.projectnessie.cel.common.types.traits.Negater; import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Sizer; import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.interpreter.Activation.PartialActivation; import org.projectnessie.cel.interpreter.Activation.VarActivation; import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; import org.projectnessie.cel.interpreter.AttributeFactory.ConditionalAttribute; @@ -138,6 +149,48 @@ interface InterpretableCall extends Interpretable { // Core Interpretable implementations used during the program planning phase. + /** evalIdent evaluates a checked top-level variable directly from the activation. */ + final class EvalIdent extends AbstractEval implements Coster { + private final String name; + private final TypeAdapter adapter; + + EvalIdent(long id, String name, TypeAdapter adapter) { + super(id); + this.name = Objects.requireNonNull(name); + this.adapter = Objects.requireNonNull(adapter); + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(Activation ctx) { + if (ctx instanceof PartialActivation) { + for (AttributePattern pattern : ((PartialActivation) ctx).unknownAttributePatterns()) { + if (pattern.variableMatches(name)) { + return unknownOf(id); + } + } + } + + ResolvedValue value = ctx.resolveName(name); + if (!value.present()) { + RuntimeException err = noSuchAttributeException("id: " + id + ", names: [" + name + "]"); + return newErr(err, err.toString()); + } + return adapter.nativeToValue(value.value()); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return OneOne; + } + + @Override + public String toString() { + return "EvalIdent{" + "id=" + id + ", name='" + name + '\'' + '}'; + } + } + final class EvalTestOnly implements Interpretable, Coster { private final long id; private final Interpretable op; @@ -162,11 +215,9 @@ public long id() { public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { // Handle field selection on a proto in the most efficient way possible. if (fieldType != null) { - if (op instanceof InterpretableAttribute) { - InterpretableAttribute opAttr = (InterpretableAttribute) op; + if (op instanceof InterpretableAttribute opAttr) { Object opVal = opAttr.resolve(ctx); - if (opVal instanceof Val) { - Val refVal = (Val) opVal; + if (opVal instanceof Val refVal) { opVal = refVal.value(); } if (fieldType.isSet.isSet(opVal)) { @@ -454,13 +505,11 @@ public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { return rVal; } Val eqVal = lVal.equal(rVal); - switch (eqVal.type().typeEnum()) { - case Err: - return eqVal; - case Bool: - return ((Negater) eqVal).negate(); - } - return noSuchOverload(lVal, Operator.NotEquals.id, rVal); + return switch (eqVal.type().typeEnum()) { + case Err -> eqVal; + case Bool -> ((Negater) eqVal).negate(); + default -> noSuchOverload(lVal, Operator.NotEquals.id, rVal); + }; } /** Cost implements the Coster interface method. */ @@ -772,8 +821,7 @@ public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the // operand (arg0). if (arg0.type().hasTrait(Trait.ReceiverType)) { - return ((Receiver) arg0) - .receive(function, overload, Arrays.copyOfRange(argVals, 1, argVals.length - 1)); + return receiveVarArgs((Receiver) arg0, function, overload, argVals); } return noSuchOverload(arg0, function, overload, argVals); } @@ -826,18 +874,24 @@ public String toString() { final class EvalList extends AbstractEval implements Coster { final Interpretable[] elems; + final boolean[] optionalIndices; private final TypeAdapter adapter; EvalList(long id, Interpretable[] elems, TypeAdapter adapter) { + this(id, elems, new boolean[elems.length], adapter); + } + + EvalList(long id, Interpretable[] elems, boolean[] optionalIndices, TypeAdapter adapter) { super(id); this.elems = elems; + this.optionalIndices = optionalIndices; this.adapter = adapter; } /** Eval implements the Interpretable interface method. */ @Override public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val[] elemVals = new Val[elems.length]; + List elemVals = new ArrayList<>(elems.length); // If any argument is unknown or error early terminate. for (int i = 0; i < elems.length; i++) { Interpretable elem = elems[i]; @@ -845,9 +899,18 @@ public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { if (isUnknownOrError(elemVal)) { return elemVal; } - elemVals[i] = elemVal; + if (optionalIndices[i]) { + if (!(elemVal instanceof OptionalT optional)) { + return newErr("optional list element is not optional"); + } + if (!optional.hasValue()) { + continue; + } + elemVal = optional.getValue(); + } + elemVals.add(elemVal); } - return adapter.nativeToValue(elemVals); + return adapter.nativeToValue(elemVals.toArray(Val[]::new)); } /** Cost implements the Coster interface method. */ @@ -865,19 +928,30 @@ public String toString() { final class EvalMap extends AbstractEval implements Coster { final Interpretable[] keys; final Interpretable[] vals; + final boolean[] optionalEntries; private final TypeAdapter adapter; EvalMap(long id, Interpretable[] keys, Interpretable[] vals, TypeAdapter adapter) { + this(id, keys, vals, new boolean[keys.length], adapter); + } + + EvalMap( + long id, + Interpretable[] keys, + Interpretable[] vals, + boolean[] optionalEntries, + TypeAdapter adapter) { super(id); this.keys = keys; this.vals = vals; + this.optionalEntries = optionalEntries; this.adapter = adapter; } /** Eval implements the Interpretable interface method. */ @Override public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Map entries = new HashMap<>(); + Map entries = new HashMap<>(keys.length * 4 / 3 + 1); // If any argument is unknown or error early terminate. for (int i = 0; i < keys.length; i++) { Interpretable key = keys[i]; @@ -885,19 +959,28 @@ public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { if (isUnknownOrError(keyVal)) { return keyVal; } - if (keyVal.type().typeEnum() == TypeEnum.Null) { + if (!MapT.isSupportedLiteralKeyType(keyVal)) { return newErr("unsupported key type"); } Val valVal = vals[i].eval(ctx); if (isUnknownOrError(valVal)) { return valVal; } + if (optionalEntries[i]) { + if (!(valVal instanceof OptionalT optional)) { + return newErr("optional map entry is not optional"); + } + if (!optional.hasValue()) { + continue; + } + valVal = optional.getValue(); + } if (entries.putIfAbsent(keyVal, valVal) != null) { // Prevent duplicate keys, error out. return newErr("Failed with repeated key"); } } - return adapter.nativeToValue(entries); + return MapT.newWrappedMap(adapter, entries); } /** Cost implements the Coster interface method. */ @@ -925,14 +1008,26 @@ final class EvalObj extends AbstractEval implements Coster { private final String typeName; private final String[] fields; private final Interpretable[] vals; + private final boolean[] optionalEntries; private final TypeProvider provider; EvalObj( long id, String typeName, String[] fields, Interpretable[] vals, TypeProvider provider) { + this(id, typeName, fields, vals, new boolean[fields.length], provider); + } + + EvalObj( + long id, + String typeName, + String[] fields, + Interpretable[] vals, + boolean[] optionalEntries, + TypeProvider provider) { super(id); this.typeName = Objects.requireNonNull(typeName); this.fields = Objects.requireNonNull(fields); this.vals = Objects.requireNonNull(vals); + this.optionalEntries = optionalEntries; this.provider = Objects.requireNonNull(provider); } @@ -947,6 +1042,15 @@ public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { if (isUnknownOrError(val)) { return val; } + if (optionalEntries[i]) { + if (!(val instanceof OptionalT optional)) { + return newErr("optional message field is not optional"); + } + if (!optional.hasValue()) { + continue; + } + val = optional.getValue(); + } fieldVals.put(field, val); } return provider.newValue(typeName, fieldVals); @@ -991,6 +1095,7 @@ final class EvalFold extends AbstractEval implements Coster { // TODO combine with EvalExhaustiveFold final String accuVar; final String iterVar; + final String iterVar2; final Interpretable iterRange; final Interpretable accu; final Interpretable cond; @@ -1002,6 +1107,7 @@ final class EvalFold extends AbstractEval implements Coster { String accuVar, Interpretable accu, String iterVar, + String iterVar2, Interpretable iterRange, Interpretable cond, Interpretable step, @@ -1009,6 +1115,7 @@ final class EvalFold extends AbstractEval implements Coster { super(id); this.accuVar = accuVar; this.iterVar = iterVar; + this.iterVar2 = iterVar2; this.iterRange = iterRange; this.accu = accu; this.cond = cond; @@ -1032,19 +1139,43 @@ public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { VarActivation iterCtx = new VarActivation(); iterCtx.parent = accuCtx; iterCtx.name = iterVar; + VarActivation iterCtx2 = null; + if (!iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = iterVar2; + } IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; while (it.hasNext() == True) { // Modify the iter var in the fold activation. - iterCtx.val = it.next(); + Val next = it.next(); + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (foldRange instanceof Lister) { + iterCtx.val = intOf(index); + iterCtx2.val = next; + } else if (foldRange instanceof Mapper) { + iterCtx.val = next; + iterCtx2.val = ((Mapper) foldRange).get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + iterCtx.val = next; + } + index++; // Evaluate the condition, terminate the loop if false. - Val c = cond.eval(iterCtx); + Val c = cond.eval(loopCtx); if (c == False) { break; } // Evalute the evaluation step into accu var. - accuCtx.val = step.eval(iterCtx); + accuCtx.val = step.eval(loopCtx); } // Compute the result. return result.eval(accuCtx); @@ -1093,6 +1224,9 @@ public String toString() { + ", iterVar='" + iterVar + '\'' + + ", iterVar2='" + + iterVar2 + + '\'' + ", iterRange=" + iterRange + ", accu=" @@ -1107,6 +1241,281 @@ public String toString() { } } + final class EvalListFold extends AbstractEval implements Coster { + final String iterVar; + final String iterVar2; + final Interpretable iterRange; + final Interpretable filter; + final Interpretable transform; + private final TypeAdapter adapter; + + EvalListFold( + long id, + String iterVar, + String iterVar2, + Interpretable iterRange, + Interpretable filter, + Interpretable transform, + TypeAdapter adapter) { + super(id); + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + this.iterRange = iterRange; + this.filter = filter; + this.transform = transform; + this.adapter = adapter; + } + + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val foldRange = iterRange.eval(ctx); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return valOrErr( + foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); + } + + VarActivation iterCtx = new VarActivation(); + iterCtx.parent = ctx; + iterCtx.name = iterVar; + VarActivation iterCtx2 = null; + if (!iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = iterVar2; + } + List values = new ArrayList<>(listCapacity(foldRange)); + IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; + while (it.hasNext() == True) { + Val next = it.next(); + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (foldRange instanceof Lister) { + iterCtx.val = intOf(index); + iterCtx2.val = next; + } else if (foldRange instanceof Mapper) { + iterCtx.val = next; + iterCtx2.val = ((Mapper) foldRange).get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + iterCtx.val = next; + } + index++; + + if (filter != null) { + Val include = filter.eval(loopCtx); + if (include == False) { + continue; + } + if (include != True) { + return noSuchOverload(null, Operator.Conditional.id, include); + } + } + + Val value = transform.eval(loopCtx); + if (isUnknownOrError(value)) { + return value; + } + values.add(value); + } + return ListT.newValArrayList(adapter, values.toArray(new Val[0])); + } + + private int listCapacity(Val foldRange) { + if (foldRange.type().hasTrait(Trait.SizerType)) { + long size = ((Sizer) foldRange).size().intValue(); + if (size > 0 && size <= Integer.MAX_VALUE) { + return (int) size; + } + } + return 0; + } + + @Override + public Cost cost() { + Cost range = estimateCost(iterRange); + Cost result = estimateCost(transform); + if (filter != null) { + result = result.add(estimateCost(filter)); + } + Val foldRange = iterRange.eval(emptyActivation()); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return Cost.Unknown; + } + long rangeCnt = 0L; + IteratorT it = ((IterableT) foldRange).iterator(); + while (it.hasNext() == True) { + it.next(); + rangeCnt++; + } + return range.add(result.multiply(rangeCnt)); + } + + @Override + public String toString() { + return "EvalListFold{" + + "id=" + + id + + ", iterVar='" + + iterVar + + '\'' + + ", iterVar2='" + + iterVar2 + + '\'' + + ", iterRange=" + + iterRange + + ", filter=" + + filter + + ", transform=" + + transform + + '}'; + } + } + + final class EvalMapFold extends AbstractEval implements Coster { + final String iterVar; + final String iterVar2; + final Interpretable iterRange; + final Interpretable filter; + final Interpretable transform; + private final TypeAdapter adapter; + + EvalMapFold( + long id, + String iterVar, + String iterVar2, + Interpretable iterRange, + Interpretable filter, + Interpretable transform, + TypeAdapter adapter) { + super(id); + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + this.iterRange = iterRange; + this.filter = filter; + this.transform = transform; + this.adapter = adapter; + } + + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val foldRange = iterRange.eval(ctx); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return valOrErr( + foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); + } + + VarActivation iterCtx = new VarActivation(); + iterCtx.parent = ctx; + iterCtx.name = iterVar; + VarActivation iterCtx2 = null; + if (!iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = iterVar2; + } + Map values = new HashMap<>(mapCapacity(foldRange)); + IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; + while (it.hasNext() == True) { + Val next = it.next(); + Val key; + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (foldRange instanceof Lister) { + key = intOf(index); + iterCtx.val = key; + iterCtx2.val = next; + } else if (foldRange instanceof Mapper) { + key = next; + iterCtx.val = key; + iterCtx2.val = ((Mapper) foldRange).get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + key = next; + iterCtx.val = next; + } + index++; + + if (filter != null) { + Val include = filter.eval(loopCtx); + if (include == False) { + continue; + } + if (include != True) { + return noSuchOverload(null, Operator.Conditional.id, include); + } + } + + Val value = transform.eval(loopCtx); + if (isUnknownOrError(value)) { + return value; + } + values.put(key, value); + } + return MapT.newWrappedMap(adapter, values); + } + + private int mapCapacity(Val foldRange) { + if (foldRange.type().hasTrait(Trait.SizerType)) { + long size = ((Sizer) foldRange).size().intValue(); + if (size > 0 && size <= Integer.MAX_VALUE) { + long capacity = size * 4 / 3 + 1; + return capacity <= Integer.MAX_VALUE ? (int) capacity : Integer.MAX_VALUE; + } + } + return 0; + } + + @Override + public Cost cost() { + Cost range = estimateCost(iterRange); + Cost result = estimateCost(transform); + if (filter != null) { + result = result.add(estimateCost(filter)); + } + Val foldRange = iterRange.eval(emptyActivation()); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return Cost.Unknown; + } + long rangeCnt = 0L; + IteratorT it = ((IterableT) foldRange).iterator(); + while (it.hasNext() == True) { + it.next(); + rangeCnt++; + } + return range.add(result.multiply(rangeCnt)); + } + + @Override + public String toString() { + return "EvalMapFold{" + + "id=" + + id + + ", iterVar='" + + iterVar + + '\'' + + ", iterVar2='" + + iterVar2 + + '\'' + + ", iterRange=" + + iterRange + + ", filter=" + + filter + + ", transform=" + + transform + + '}'; + } + } + // Optional Intepretable implementations that specialize, subsume, or extend the core evaluation // plan via decorators. @@ -1163,6 +1572,134 @@ public String toString() { } } + final class EvalReceiverVarArgs extends AbstractEval implements Coster, InterpretableCall { + private final String function; + private final String overload; + private final Interpretable[] args; + + public EvalReceiverVarArgs(long id, String function, String overload, Interpretable[] args) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.args = Objects.requireNonNull(args); + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val arg0 = args[0].eval(ctx); + if (isUnknownOrError(arg0)) { + return arg0; + } + + return switch (args.length) { + case 3 -> evalReceiverTail2(ctx, arg0); + case 4 -> evalReceiverTail3(ctx, arg0); + default -> receiveOrNoSuchOverload(arg0, evalTailArgs(ctx)); + }; + } + + private Val evalReceiverTail2(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) { + Val arg1 = args[1].eval(ctx); + if (isUnknownOrError(arg1)) { + return arg1; + } + Val arg2 = args[2].eval(ctx); + if (isUnknownOrError(arg2)) { + return arg2; + } + return receiveOrNoSuchOverload(arg0, arg1, arg2); + } + + private Val evalReceiverTail3(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) { + Val arg1 = args[1].eval(ctx); + if (isUnknownOrError(arg1)) { + return arg1; + } + Val arg2 = args[2].eval(ctx); + if (isUnknownOrError(arg2)) { + return arg2; + } + Val arg3 = args[3].eval(ctx); + if (isUnknownOrError(arg3)) { + return arg3; + } + return receiveOrNoSuchOverload(arg0, arg1, arg2, arg3); + } + + private Val[] evalTailArgs(org.projectnessie.cel.interpreter.Activation ctx) { + Val[] argVals = new Val[args.length - 1]; + for (int i = 1; i < args.length; i++) { + argVals[i - 1] = args[i].eval(ctx); + } + return argVals; + } + + private Val receiveOrNoSuchOverload(Val arg0, Val... tailArgs) { + for (Val argVal : tailArgs) { + if (isUnknownOrError(argVal)) { + return argVal; + } + } + if (arg0.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) arg0).receive(function, overload, tailArgs); + } + return noSuchOverload(arg0, function, overload, tailArgs); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + Cost c = sumOfCost(args); + return c.add(OneOne); // add cost for function + } + + /** Function implements the InterpretableCall interface method. */ + @Override + public String function() { + return function; + } + + /** OverloadID implements the InterpretableCall interface method. */ + @Override + public String overloadID() { + return overload; + } + + /** Args returns the argument to the unary function. */ + @Override + public Interpretable[] args() { + return args; + } + + @Override + public String toString() { + return "EvalReceiverVarArgs{" + + "id=" + + id + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", args=" + + Arrays.toString(args) + + '}'; + } + } + + static Val receiveVarArgs(Receiver receiver, String function, String overload, Val[] argVals) { + return switch (argVals.length) { + case 1 -> receiver.receive(function, overload); + case 2 -> receiver.receive(function, overload, argVals[1]); + case 3 -> receiver.receive(function, overload, argVals[1], argVals[2]); + case 4 -> receiver.receive(function, overload, argVals[1], argVals[2], argVals[3]); + default -> + receiver.receive(function, overload, Arrays.copyOfRange(argVals, 1, argVals.length)); + }; + } + /** * evalWatch is an Interpretable implementation that wraps the execution of a given expression so * that it may observe the computed value and send it to an observer. @@ -1227,11 +1764,9 @@ public long id() { */ @Override public Attribute addQualifier(AttributeFactory.Qualifier q) { - if (q instanceof ConstantQualifierEquator) { - ConstantQualifierEquator cq = (ConstantQualifierEquator) q; + if (q instanceof ConstantQualifierEquator cq) { q = new EvalWatchConstQualEquat(cq, observer, attr.adapter()); - } else if (q instanceof ConstantQualifier) { - ConstantQualifier cq = (ConstantQualifier) q; + } else if (q instanceof ConstantQualifier cq) { q = new EvalWatchConstQual(cq, observer, attr.adapter()); } else { q = new EvalWatchQual(q, observer, attr.adapter()); @@ -1566,6 +2101,7 @@ final class EvalExhaustiveFold extends AbstractEval implements Coster { // TODO combine with EvalFold private final String accuVar; private final String iterVar; + private final String iterVar2; private final Interpretable iterRange; private final Interpretable accu; private final Interpretable cond; @@ -1578,12 +2114,14 @@ final class EvalExhaustiveFold extends AbstractEval implements Coster { String accuVar, Interpretable iterRange, String iterVar, + String iterVar2, Interpretable cond, Interpretable step, Interpretable result) { super(id); this.accuVar = accuVar; this.iterVar = iterVar; + this.iterVar2 = iterVar2; this.iterRange = iterRange; this.accu = accu; this.cond = cond; @@ -1607,16 +2145,40 @@ public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { VarActivation iterCtx = new VarActivation(); iterCtx.parent = accuCtx; iterCtx.name = iterVar; + VarActivation iterCtx2 = null; + if (!iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = iterVar2; + } IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; while (it.hasNext() == True) { // Modify the iter var in the fold activation. - iterCtx.val = it.next(); + Val next = it.next(); + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (foldRange instanceof Lister) { + iterCtx.val = intOf(index); + iterCtx2.val = next; + } else if (foldRange instanceof Mapper) { + iterCtx.val = next; + iterCtx2.val = ((Mapper) foldRange).get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + iterCtx.val = next; + } + index++; // Evaluate the condition, but don't terminate the loop as this is exhaustive eval! - cond.eval(iterCtx); + cond.eval(loopCtx); // Evalute the evaluation step into accu var. - accuCtx.val = step.eval(iterCtx); + accuCtx.val = step.eval(loopCtx); } // Compute the result. return result.eval(accuCtx); @@ -1662,6 +2224,9 @@ public String toString() { + ", iterVar='" + iterVar + '\'' + + ", iterVar2='" + + iterVar2 + + '\'' + ", iterRange=" + iterRange + ", accu=" diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java index 63d8a45f..bc47be45 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java @@ -87,28 +87,25 @@ static InterpretableDecorator decObserveEval(EvalObserver observer) { */ static InterpretableDecorator decDisableShortcircuits() { return i -> { - if (i instanceof EvalOr) { - EvalOr expr = (EvalOr) i; + if (i instanceof EvalOr expr) { return new EvalExhaustiveOr(expr.id, expr.lhs, expr.rhs); } - if (i instanceof EvalAnd) { - EvalAnd expr = (EvalAnd) i; + if (i instanceof EvalAnd expr) { return new EvalExhaustiveAnd(expr.id, expr.lhs, expr.rhs); } - if (i instanceof EvalFold) { - EvalFold expr = (EvalFold) i; + if (i instanceof EvalFold expr) { return new EvalExhaustiveFold( expr.id, expr.accu, expr.accuVar, expr.iterRange, expr.iterVar, + expr.iterVar2, expr.cond, expr.step, expr.result); } - if (i instanceof InterpretableAttribute) { - InterpretableAttribute expr = (InterpretableAttribute) i; + if (i instanceof InterpretableAttribute expr) { if (expr.attr() instanceof ConditionalAttribute) { return new EvalExhaustiveConditional( i.id(), expr.adapter(), (ConditionalAttribute) expr.attr()); @@ -135,8 +132,7 @@ static InterpretableDecorator decOptimize() { if (i instanceof EvalMap) { return maybeBuildMapLiteral(i, (EvalMap) i); } - if (i instanceof InterpretableCall) { - InterpretableCall inst = (InterpretableCall) i; + if (i instanceof InterpretableCall inst) { if (inst.overloadID().equals(Overloads.InList)) { return maybeOptimizeSetMembership(i, inst); } @@ -195,10 +191,9 @@ static Interpretable maybeOptimizeSetMembership(Interpretable i, InterpretableCa Interpretable[] args = inlist.args(); Interpretable lhs = args[0]; Interpretable rhs = args[1]; - if (!(rhs instanceof InterpretableConst)) { + if (!(rhs instanceof InterpretableConst l)) { return i; } - InterpretableConst l = (InterpretableConst) rhs; // When the incoming binary call is flagged with as the InList overload, the value will // always be convertible to a `traits.Lister` type. Lister list = (Lister) l.value(); diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java index cce8a3e8..ae35a520 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java @@ -55,11 +55,15 @@ import org.projectnessie.cel.interpreter.Interpretable.EvalBinary; import org.projectnessie.cel.interpreter.Interpretable.EvalEq; import org.projectnessie.cel.interpreter.Interpretable.EvalFold; +import org.projectnessie.cel.interpreter.Interpretable.EvalIdent; import org.projectnessie.cel.interpreter.Interpretable.EvalList; +import org.projectnessie.cel.interpreter.Interpretable.EvalListFold; import org.projectnessie.cel.interpreter.Interpretable.EvalMap; +import org.projectnessie.cel.interpreter.Interpretable.EvalMapFold; import org.projectnessie.cel.interpreter.Interpretable.EvalNe; import org.projectnessie.cel.interpreter.Interpretable.EvalObj; import org.projectnessie.cel.interpreter.Interpretable.EvalOr; +import org.projectnessie.cel.interpreter.Interpretable.EvalReceiverVarArgs; import org.projectnessie.cel.interpreter.Interpretable.EvalTestOnly; import org.projectnessie.cel.interpreter.Interpretable.EvalUnary; import org.projectnessie.cel.interpreter.Interpretable.EvalVarArgs; @@ -101,6 +105,22 @@ static InterpretablePlanner newPlanner( decorators); } + /** + * newPlanner creates an interpretablePlanner from checked expression metadata without requiring a + * CheckedExpr wrapper. + */ + static InterpretablePlanner newPlanner( + Dispatcher disp, + TypeProvider provider, + TypeAdapter adapter, + AttributeFactory attrFactory, + Container cont, + Map refMap, + Map typeMap, + InterpretableDecorator... decorators) { + return new Planner(disp, provider, adapter, attrFactory, cont, refMap, typeMap, decorators); + } + /** * newUncheckedPlanner creates an interpretablePlanner which references a Dispatcher, * TypeProvider, TypeAdapter, and Container to resolve functions and types at plan time. @@ -157,24 +177,18 @@ final class Planner implements InterpretablePlanner { */ @Override public Interpretable plan(Expr expr) { - switch (expr.getExprKindCase()) { - case CALL_EXPR: - return decorate(planCall(expr)); - case IDENT_EXPR: - return decorate(planIdent(expr)); - case SELECT_EXPR: - return decorate(planSelect(expr)); - case LIST_EXPR: - return decorate(planCreateList(expr)); - case STRUCT_EXPR: - return decorate(planCreateStruct(expr)); - case COMPREHENSION_EXPR: - return decorate(planComprehension(expr)); - case CONST_EXPR: - return decorate(planConst(expr)); - } - throw new IllegalArgumentException( - String.format("unsupported expr of kind %s: '%s'", expr.getExprKindCase(), expr)); + return switch (expr.getExprKindCase()) { + case CALL_EXPR -> decorate(planCall(expr)); + case IDENT_EXPR -> decorate(planIdent(expr)); + case SELECT_EXPR -> decorate(planSelect(expr)); + case LIST_EXPR -> decorate(planCreateList(expr)); + case STRUCT_EXPR -> decorate(planCreateStruct(expr)); + case COMPREHENSION_EXPR -> decorate(planComprehension(expr)); + case CONST_EXPR -> decorate(planConst(expr)); + default -> + throw new IllegalArgumentException( + String.format("unsupported expr of kind %s: '%s'", expr.getExprKindCase(), expr)); + }; } /** @@ -204,6 +218,8 @@ Interpretable planIdent(Expr expr) { } Interpretable planCheckedIdent(long id, Reference identRef) { + String identName = identRef.getName(); + String providerName = identName.startsWith(".") ? identName.substring(1) : identName; // Plan a constant reference if this is the case for this simple identifier. if (identRef.getValue() != Reference.getDefaultInstance().getValue()) { return plan(Expr.newBuilder().setId(id).setConstExpr(identRef.getValue()).build()); @@ -213,16 +229,20 @@ Interpretable planCheckedIdent(long id, Reference identRef) { // registered with the provider. Type cType = typeMap.get(id); if (cType != null && cType.getType() != Type.getDefaultInstance()) { - Val cVal = provider.findIdent(identRef.getName()); + Val cVal = provider.findIdent(providerName); if (cVal == null) { throw new IllegalStateException( - String.format("reference to undefined type: %s", identRef.getName())); + String.format("reference to undefined type: %s", providerName)); } return newConstValue(id, cVal); } - // Otherwise, return the attribute for the resolved identifier name. - return new EvalAttr(adapter, attrFactory.absoluteAttribute(id, identRef.getName())); + // Otherwise, evaluate the checked top-level variable directly for ordinary plans. Decorated + // programs keep the attribute shape because custom decorators may inspect attributes. + if (decorators.length == 0) { + return new EvalIdent(id, identName, adapter); + } + return new EvalAttr(adapter, attrFactory.absoluteAttribute(id, identName)); } /** @@ -244,7 +264,7 @@ Interpretable planSelect(Expr expr) { Select sel = expr.getSelectExpr(); // Plan the operand evaluation. - Interpretable op = plan(sel.getOperand()); + Interpretable op = planSelectOperand(sel.getOperand()); // Determine the field type if this is a proto message type. FieldType fieldType = null; @@ -283,8 +303,7 @@ Interpretable planSelect(Expr expr) { return null; } // Lastly, create a field selection Interpretable. - if (op instanceof InterpretableAttribute) { - InterpretableAttribute attr = (InterpretableAttribute) op; + if (op instanceof InterpretableAttribute attr) { attr.addQualifier(qual); return attr; } @@ -297,6 +316,25 @@ Interpretable planSelect(Expr expr) { return relAttr; } + private Interpretable planSelectOperand(Expr operand) { + if (operand.getExprKindCase() != Expr.ExprKindCase.IDENT_EXPR) { + return plan(operand); + } + + Reference identRef = refMap.get(operand.getId()); + if (identRef == null || identRef.getValue() != Reference.getDefaultInstance().getValue()) { + return plan(operand); + } + + Type cType = typeMap.get(operand.getId()); + if (cType != null && cType.getType() != Type.getDefaultInstance()) { + return plan(operand); + } + + return new EvalAttr( + adapter, attrFactory.absoluteAttribute(operand.getId(), identRef.getName())); + } + private FieldType findFieldType(String messageType, String fieldName) { String key = messageType + '\n' + fieldName; FieldType ft = fieldTypes.get(key); @@ -352,23 +390,19 @@ Interpretable planCall(Expr expr) { // Otherwise, generate Interpretable calls specialized by argument count. // Try to find the specific function by overload id. Overload fnDef = null; - if (resolvedFunc.overloadId != null && resolvedFunc.overloadId.isEmpty()) { + if (resolvedFunc.overloadId != null && !resolvedFunc.overloadId.isEmpty()) { fnDef = disp.findOverload(resolvedFunc.overloadId); } // If the overload id couldn't resolve the function, try the simple function name. if (fnDef == null) { fnDef = disp.findOverload(resolvedFunc.fnName); } - switch (argCount) { - case 0: - return planCallZero(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef); - case 1: - return planCallUnary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); - case 2: - return planCallBinary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); - default: - return planCallVarArgs(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); - } + return switch (argCount) { + case 0 -> planCallZero(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef); + case 1 -> planCallUnary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); + case 2 -> planCallBinary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); + default -> planCallVarArgs(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); + }; } /** planCallZero generates a zero-arity callable Interpretable. */ @@ -413,15 +447,16 @@ Interpretable planCallBinary( /** planCallVarArgs generates a variable argument callable Interpretable. */ Interpretable planCallVarArgs( Expr expr, String function, String overload, Overload impl, Interpretable... args) { + if (impl == null) { + return new EvalReceiverVarArgs(expr.getId(), function, overload, args); + } FunctionOp fn = null; Trait trait = null; - if (impl != null) { - if (impl.function == null) { - throw new IllegalStateException(String.format("no such overload: %s(...)", function)); - } - fn = impl.function; - trait = impl.operandTrait; + if (impl.function == null) { + throw new IllegalStateException(String.format("no such overload: %s(...)", function)); } + fn = impl.function; + trait = impl.operandTrait; return new EvalVarArgs(expr.getId(), function, overload, args, trait, fn); } @@ -451,8 +486,7 @@ Interpretable planCallConditional(Expr expr, Interpretable... args) { Interpretable t = args[1]; Attribute tAttr; - if (t instanceof InterpretableAttribute) { - InterpretableAttribute truthyAttr = (InterpretableAttribute) t; + if (t instanceof InterpretableAttribute truthyAttr) { tAttr = truthyAttr.attr(); } else { tAttr = attrFactory.relativeAttribute(t.id(), t); @@ -460,8 +494,7 @@ Interpretable planCallConditional(Expr expr, Interpretable... args) { Interpretable f = args[2]; Attribute fAttr; - if (f instanceof InterpretableAttribute) { - InterpretableAttribute falsyAttr = (InterpretableAttribute) f; + if (f instanceof InterpretableAttribute falsyAttr) { fAttr = falsyAttr.attr(); } else { fAttr = attrFactory.relativeAttribute(f.id(), f); @@ -483,8 +516,7 @@ Interpretable planCallIndex(Expr expr, Interpretable... args) { return null; } Type opType = typeMap.get(expr.getCallExpr().getTarget().getId()); - if (ind instanceof InterpretableConst) { - InterpretableConst indConst = (InterpretableConst) ind; + if (ind instanceof InterpretableConst indConst) { Qualifier qual = attrFactory.newQualifier(opType, expr.getId(), indConst.value()); if (qual == null) { return null; @@ -492,8 +524,7 @@ Interpretable planCallIndex(Expr expr, Interpretable... args) { opAttr.addQualifier(qual); return opAttr; } - if (ind instanceof InterpretableAttribute) { - InterpretableAttribute indAttr = (InterpretableAttribute) ind; + if (ind instanceof InterpretableAttribute indAttr) { Qualifier qual = attrFactory.newQualifier(opType, expr.getId(), indAttr); if (qual == null) { return null; @@ -513,6 +544,10 @@ Interpretable planCallIndex(Expr expr, Interpretable... args) { Interpretable planCreateList(Expr expr) { CreateList list = expr.getListExpr(); Interpretable[] elems = new Interpretable[list.getElementsCount()]; + boolean[] optionalIndices = new boolean[list.getElementsCount()]; + for (int index : list.getOptionalIndicesList()) { + optionalIndices[index] = true; + } for (int i = 0; i < list.getElementsCount(); i++) { Expr elem = list.getElements(i); Interpretable elemVal = plan(elem); @@ -521,7 +556,7 @@ Interpretable planCreateList(Expr expr) { } elems[i] = elemVal; } - return new EvalList(expr.getId(), elems, adapter); + return new EvalList(expr.getId(), elems, optionalIndices, adapter); } /** planCreateStruct generates a map or object construction Interpretable. */ @@ -533,8 +568,10 @@ Interpretable planCreateStruct(Expr expr) { List entries = str.getEntriesList(); Interpretable[] keys = new Interpretable[entries.size()]; Interpretable[] vals = new Interpretable[entries.size()]; + boolean[] optionalEntries = new boolean[entries.size()]; for (int i = 0; i < entries.size(); i++) { Entry entry = entries.get(i); + optionalEntries[i] = entry.getOptionalEntry(); Interpretable keyVal = plan(entry.getMapKey()); if (keyVal == null) { return null; @@ -547,7 +584,7 @@ Interpretable planCreateStruct(Expr expr) { } vals[i] = valVal; } - return new EvalMap(expr.getId(), keys, vals, adapter); + return new EvalMap(expr.getId(), keys, vals, optionalEntries, adapter); } /** planCreateObj generates an object construction Interpretable. */ @@ -560,21 +597,77 @@ Interpretable planCreateObj(Expr expr) { List entries = obj.getEntriesList(); String[] fields = new String[entries.size()]; Interpretable[] vals = new Interpretable[entries.size()]; + boolean[] optionalEntries = new boolean[entries.size()]; for (int i = 0; i < entries.size(); i++) { Entry entry = entries.get(i); fields[i] = entry.getFieldKey(); + optionalEntries[i] = entry.getOptionalEntry(); Interpretable val = plan(entry.getValue()); if (val == null) { return null; } vals[i] = val; } - return new EvalObj(expr.getId(), typeName, fields, vals, provider); + return new EvalObj(expr.getId(), typeName, fields, vals, optionalEntries, provider); } /** planComprehension generates an Interpretable fold operation. */ Interpretable planComprehension(Expr expr) { Comprehension fold = expr.getComprehensionExpr(); + MacroMapFold macroMapFold = macroMapFold(fold); + if (macroMapFold != null) { + Interpretable iterRange = plan(fold.getIterRange()); + if (iterRange == null) { + return null; + } + Interpretable filter = null; + if (macroMapFold.filter != null) { + filter = plan(macroMapFold.filter); + if (filter == null) { + return null; + } + } + Interpretable transform = plan(macroMapFold.transform); + if (transform == null) { + return null; + } + return new EvalMapFold( + expr.getId(), + fold.getIterVar(), + fold.getIterVar2(), + iterRange, + filter, + transform, + adapter); + } + + MacroListFold macroListFold = macroListFold(fold); + if (macroListFold != null) { + Interpretable iterRange = plan(fold.getIterRange()); + if (iterRange == null) { + return null; + } + Interpretable filter = null; + if (macroListFold.filter != null) { + filter = plan(macroListFold.filter); + if (filter == null) { + return null; + } + } + Interpretable transform = plan(macroListFold.transform); + if (transform == null) { + return null; + } + return new EvalListFold( + expr.getId(), + fold.getIterVar(), + fold.getIterVar2(), + iterRange, + filter, + transform, + adapter); + } + Interpretable accu = plan(fold.getAccuInit()); if (accu == null) { return null; @@ -596,7 +689,195 @@ Interpretable planComprehension(Expr expr) { return null; } return new EvalFold( - expr.getId(), fold.getAccuVar(), accu, fold.getIterVar(), iterRange, cond, step, result); + expr.getId(), + fold.getAccuVar(), + accu, + fold.getIterVar(), + fold.getIterVar2(), + iterRange, + cond, + step, + result); + } + + private MacroMapFold macroMapFold(Comprehension fold) { + if (!isEmptyMap(fold.getAccuInit()) + || !isBoolConst(fold.getLoopCondition(), true) + || !isIdent(fold.getResult(), fold.getAccuVar())) { + return null; + } + + Expr step = fold.getLoopStep(); + Expr filter = null; + if (isCall(step, Operator.Conditional.id, 3)) { + Call conditional = step.getCallExpr(); + if (!isIdent(conditional.getArgs(2), fold.getAccuVar())) { + return null; + } + filter = conditional.getArgs(0); + step = conditional.getArgs(1); + } + + Expr transform = mapEntryValue(fold.getIterVar(), step); + if (transform == null + || referencesIdent(transform, fold.getAccuVar()) + || (filter != null && referencesIdent(filter, fold.getAccuVar()))) { + return null; + } + return new MacroMapFold(filter, transform); + } + + private MacroListFold macroListFold(Comprehension fold) { + if (!isEmptyList(fold.getAccuInit()) + || !isBoolConst(fold.getLoopCondition(), true) + || !isIdent(fold.getResult(), fold.getAccuVar())) { + return null; + } + + Expr step = fold.getLoopStep(); + Expr filter = null; + if (isCall(step, Operator.Conditional.id, 3)) { + Call conditional = step.getCallExpr(); + if (!isIdent(conditional.getArgs(2), fold.getAccuVar())) { + return null; + } + filter = conditional.getArgs(0); + step = conditional.getArgs(1); + } + + Expr transform = appendedValue(fold.getAccuVar(), step); + if (transform == null + || referencesIdent(transform, fold.getAccuVar()) + || (filter != null && referencesIdent(filter, fold.getAccuVar()))) { + return null; + } + return new MacroListFold(filter, transform); + } + + private Expr appendedValue(String accuVar, Expr step) { + if (!isCall(step, Operator.Add.id, 2)) { + return null; + } + Call add = step.getCallExpr(); + if (!isIdent(add.getArgs(0), accuVar)) { + return null; + } + Expr list = add.getArgs(1); + if (list.getExprKindCase() != Expr.ExprKindCase.LIST_EXPR + || list.getListExpr().getElementsCount() != 1) { + return null; + } + return list.getListExpr().getElements(0); + } + + private boolean isCall(Expr expr, String function, int argCount) { + return expr.getExprKindCase() == Expr.ExprKindCase.CALL_EXPR + && expr.getCallExpr().getFunction().equals(function) + && expr.getCallExpr().getArgsCount() == argCount + && !expr.getCallExpr().hasTarget(); + } + + private boolean isIdent(Expr expr, String name) { + return expr.getExprKindCase() == Expr.ExprKindCase.IDENT_EXPR + && expr.getIdentExpr().getName().equals(name); + } + + private boolean isEmptyList(Expr expr) { + return expr.getExprKindCase() == Expr.ExprKindCase.LIST_EXPR + && expr.getListExpr().getElementsCount() == 0; + } + + private boolean isEmptyMap(Expr expr) { + return expr.getExprKindCase() == Expr.ExprKindCase.STRUCT_EXPR + && expr.getStructExpr().getMessageName().isEmpty() + && expr.getStructExpr().getEntriesCount() == 0; + } + + private Expr mapEntryValue(String keyVar, Expr step) { + if (step.getExprKindCase() != Expr.ExprKindCase.STRUCT_EXPR + || !step.getStructExpr().getMessageName().isEmpty() + || step.getStructExpr().getEntriesCount() != 1) { + return null; + } + Entry entry = step.getStructExpr().getEntries(0); + if (!isIdent(entry.getMapKey(), keyVar)) { + return null; + } + return entry.getValue(); + } + + private boolean isBoolConst(Expr expr, boolean value) { + return expr.getExprKindCase() == Expr.ExprKindCase.CONST_EXPR + && expr.getConstExpr().getConstantKindCase() == Constant.ConstantKindCase.BOOL_VALUE + && expr.getConstExpr().getBoolValue() == value; + } + + private boolean referencesIdent(Expr expr, String name) { + switch (expr.getExprKindCase()) { + case IDENT_EXPR: + return expr.getIdentExpr().getName().equals(name); + case SELECT_EXPR: + return referencesIdent(expr.getSelectExpr().getOperand(), name); + case CALL_EXPR: + Call call = expr.getCallExpr(); + if (call.hasTarget() && referencesIdent(call.getTarget(), name)) { + return true; + } + for (Expr arg : call.getArgsList()) { + if (referencesIdent(arg, name)) { + return true; + } + } + return false; + case LIST_EXPR: + for (Expr elem : expr.getListExpr().getElementsList()) { + if (referencesIdent(elem, name)) { + return true; + } + } + return false; + case STRUCT_EXPR: + for (Entry entry : expr.getStructExpr().getEntriesList()) { + if (referencesIdent(entry.getValue(), name)) { + return true; + } + } + return false; + case COMPREHENSION_EXPR: + Comprehension comprehension = expr.getComprehensionExpr(); + return referencesIdent(comprehension.getIterRange(), name) + || referencesIdent(comprehension.getAccuInit(), name) + || (!comprehension.getIterVar().equals(name) + && !comprehension.getIterVar2().equals(name) + && referencesIdent(comprehension.getLoopCondition(), name)) + || (!comprehension.getIterVar().equals(name) + && !comprehension.getIterVar2().equals(name) + && referencesIdent(comprehension.getLoopStep(), name)) + || (!comprehension.getAccuVar().equals(name) + && referencesIdent(comprehension.getResult(), name)); + default: + return false; + } + } + + private final class MacroListFold { + final Expr filter; + final Expr transform; + + private MacroListFold(Expr filter, Expr transform) { + this.filter = filter; + this.transform = transform; + } + } + + private final class MacroMapFold { + final Expr filter; + final Expr transform; + + private MacroMapFold(Expr filter, Expr transform) { + this.filter = filter; + this.transform = transform; + } } /** planConst generates a constant valued Interpretable. */ @@ -611,28 +892,21 @@ Interpretable planConst(Expr expr) { /** constValue converts a proto Constant value to a ref.Val. */ @SuppressWarnings("deprecation") Val constValue(Constant c) { - switch (c.getConstantKindCase()) { - case BOOL_VALUE: - return boolOf(c.getBoolValue()); - case BYTES_VALUE: - return bytesOf(c.getBytesValue()); - case DOUBLE_VALUE: - return doubleOf(c.getDoubleValue()); - case DURATION_VALUE: - return durationOf(c.getDurationValue()); - case INT64_VALUE: - return intOf(c.getInt64Value()); - case NULL_VALUE: - return NullT.NullValue; - case STRING_VALUE: - return stringOf(c.getStringValue()); - case TIMESTAMP_VALUE: - return timestampOf(c.getTimestampValue()); - case UINT64_VALUE: - return uintOf(c.getUint64Value()); - } - throw new IllegalArgumentException( - String.format("unknown constant type: '%s' of kind '%s'", c, c.getConstantKindCase())); + return switch (c.getConstantKindCase()) { + case BOOL_VALUE -> boolOf(c.getBoolValue()); + case BYTES_VALUE -> bytesOf(c.getBytesValue()); + case DOUBLE_VALUE -> doubleOf(c.getDoubleValue()); + case DURATION_VALUE -> durationOf(c.getDurationValue()); + case INT64_VALUE -> intOf(c.getInt64Value()); + case NULL_VALUE -> NullT.NullValue; + case STRING_VALUE -> stringOf(c.getStringValue()); + case TIMESTAMP_VALUE -> timestampOf(c.getTimestampValue()); + case UINT64_VALUE -> uintOf(c.getUint64Value()); + default -> + throw new IllegalArgumentException( + String.format( + "unknown constant type: '%s' of kind '%s'", c, c.getConstantKindCase())); + }; } /** diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Interpreter.java b/core/src/main/java/org/projectnessie/cel/interpreter/Interpreter.java index f1d788a8..0c07e89d 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Interpreter.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Interpreter.java @@ -24,6 +24,9 @@ import com.google.api.expr.v1alpha1.CheckedExpr; import com.google.api.expr.v1alpha1.Expr; +import com.google.api.expr.v1alpha1.Reference; +import com.google.api.expr.v1alpha1.Type; +import java.util.Map; import org.projectnessie.cel.common.containers.Container; import org.projectnessie.cel.common.types.ref.TypeAdapter; import org.projectnessie.cel.common.types.ref.TypeProvider; @@ -37,6 +40,24 @@ public interface Interpreter { */ Interpretable newInterpretable(CheckedExpr checked, InterpretableDecorator... decorators); + /** + * NewInterpretable creates an Interpretable from a checked expression and its metadata without + * requiring a CheckedExpr wrapper. + */ + default Interpretable newInterpretable( + Expr expr, + Map refMap, + Map typeMap, + InterpretableDecorator... decorators) { + CheckedExpr checked = + CheckedExpr.newBuilder() + .setExpr(expr) + .putAllReferenceMap(refMap) + .putAllTypeMap(typeMap) + .build(); + return newInterpretable(checked, decorators); + } + /** * NewUncheckedInterpretable returns an Interpretable from a parsed expression and an optional * list of InterpretableDecorator values. @@ -128,6 +149,19 @@ public Interpretable newInterpretable( return p.plan(checked.getExpr()); } + /** NewIntepretable implements the Interpreter interface method. */ + @Override + public Interpretable newInterpretable( + Expr expr, + Map refMap, + Map typeMap, + InterpretableDecorator... decorators) { + InterpretablePlanner p = + newPlanner( + dispatcher, provider, adapter, attrFactory, container, refMap, typeMap, decorators); + return p.plan(expr); + } + /** NewUncheckedIntepretable implements the Interpreter interface method. */ @Override public Interpretable newUncheckedInterpretable( diff --git a/core/src/main/java/org/projectnessie/cel/parser/CelExprBuilder.java b/core/src/main/java/org/projectnessie/cel/parser/CelExprBuilder.java new file mode 100644 index 00000000..3d88b07d --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/parser/CelExprBuilder.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.parser; + +import com.google.api.expr.v1alpha1.Expr; +import org.projectnessie.cel.common.operators.Operator; + +public interface CelExprBuilder { + Expr visitExpr(Node node); + + Expr visitBalanced(Node node, Operator operator); + + Expr visitBinary(Node node); + + Expr visitUnary(Node node); + + Expr visitMember(Node node); + + Expr visitPrimary(Node node); + + Expr visitLiteral(Node node); + + Expr visitIdentifier(Token token); +} diff --git a/core/src/main/java/org/projectnessie/cel/parser/ParseError.java b/core/src/main/java/org/projectnessie/cel/parser/CelExprNode.java similarity index 62% rename from core/src/main/java/org/projectnessie/cel/parser/ParseError.java rename to core/src/main/java/org/projectnessie/cel/parser/CelExprNode.java index 16103c77..2abb98a5 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/ParseError.java +++ b/core/src/main/java/org/projectnessie/cel/parser/CelExprNode.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Authors of CEL-Java + * Copyright (C) 2026 The Authors of CEL-Java * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,17 +15,8 @@ */ package org.projectnessie.cel.parser; -import org.projectnessie.cel.common.Location; +import com.google.api.expr.v1alpha1.Expr; -public final class ParseError extends RuntimeException { - private final Location location; - - public ParseError(Location location, String message) { - super(message); - this.location = location; - } - - public Location getLocation() { - return location; - } +public interface CelExprNode { + Expr toCelExpr(CelExprBuilder builder); } diff --git a/core/src/main/java/org/projectnessie/cel/parser/ExprHelper.java b/core/src/main/java/org/projectnessie/cel/parser/ExprHelper.java index 286e3e32..5a24fd85 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/ExprHelper.java +++ b/core/src/main/java/org/projectnessie/cel/parser/ExprHelper.java @@ -40,6 +40,9 @@ public interface ExprHelper { /** LiteralInt creates an Expr value for an int literal. */ Expr literalInt(long value); + /** LiteralNull creates an Expr value for a null literal. */ + Expr literalNull(); + /** LiteralString creates am Expr value for a string literal. */ Expr literalString(String value); @@ -100,6 +103,17 @@ Expr fold( Expr step, Expr result); + /** Fold creates a fold comprehension instruction with two iteration variables. */ + Expr fold( + String iterVar, + String iterVar2, + Expr iterRange, + String accuVar, + Expr accuInit, + Expr condition, + Expr step, + Expr result); + /** Ident creates an identifier Expr value. */ Expr ident(String name); diff --git a/core/src/main/java/org/projectnessie/cel/parser/ExprHelperImpl.java b/core/src/main/java/org/projectnessie/cel/parser/ExprHelperImpl.java index 457a576f..0c39b695 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/ExprHelperImpl.java +++ b/core/src/main/java/org/projectnessie/cel/parser/ExprHelperImpl.java @@ -60,6 +60,12 @@ public Expr literalInt(long value) { return parserHelper.newLiteralInt(nextMacroID(), value); } + // LiteralNull implements the ExprHelper interface method. + @Override + public Expr literalNull() { + return parserHelper.newLiteralNull(nextMacroID()); + } + // LiteralString implements the ExprHelper interface method. @Override public Expr literalString(String value) { @@ -121,6 +127,20 @@ public Expr fold( nextMacroID(), iterVar, iterRange, accuVar, accuInit, condition, step, result); } + @Override + public Expr fold( + String iterVar, + String iterVar2, + Expr iterRange, + String accuVar, + Expr accuInit, + Expr condition, + Expr step, + Expr result) { + return parserHelper.newComprehension( + nextMacroID(), iterVar, iterVar2, iterRange, accuVar, accuInit, condition, step, result); + } + // Ident implements the ExprHelper interface method. @Override public Expr ident(String name) { diff --git a/core/src/main/java/org/projectnessie/cel/parser/Helper.java b/core/src/main/java/org/projectnessie/cel/parser/Helper.java index edea6552..bad63c29 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/Helper.java +++ b/core/src/main/java/org/projectnessie/cel/parser/Helper.java @@ -27,6 +27,7 @@ import com.google.api.expr.v1alpha1.Expr.Select; import com.google.api.expr.v1alpha1.SourceInfo; import com.google.protobuf.ByteString; +import com.google.protobuf.NullValue; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -35,8 +36,6 @@ import org.agrona.collections.LongArrayList; import org.projectnessie.cel.common.Location; import org.projectnessie.cel.common.Source; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.ParserRuleContext; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.Token; final class Helper { private final Source source; @@ -77,6 +76,10 @@ Expr newLiteralInt(Object ctx, long value) { return newLiteral(ctx, Constant.newBuilder().setInt64Value(value)); } + Expr newLiteralNull(Object ctx) { + return newLiteral(ctx, Constant.newBuilder().setNullValue(NullValue.NULL_VALUE)); + } + Expr newLiteralUint(Object ctx, long value) { return newLiteral(ctx, Constant.newBuilder().setUint64Value(value)); } @@ -118,8 +121,13 @@ Expr newReceiverCall(Object ctx, String function, Expr target, List args) } Expr newList(Object ctx, List elements) { + return newList(ctx, elements, List.of()); + } + + Expr newList(Object ctx, List elements, List optionalIndices) { return newExprBuilder(ctx) - .setListExpr(CreateList.newBuilder().addAllElements(elements)) + .setListExpr( + CreateList.newBuilder().addAllElements(elements).addAllOptionalIndices(optionalIndices)) .build(); } @@ -130,7 +138,16 @@ Expr newMap(Object ctx, List entries) { } Entry newMapEntry(long entryID, Expr key, Expr value) { - return Entry.newBuilder().setId(entryID).setMapKey(key).setValue(value).build(); + return newMapEntry(entryID, key, value, false); + } + + Entry newMapEntry(long entryID, Expr key, Expr value, boolean optional) { + return Entry.newBuilder() + .setId(entryID) + .setMapKey(key) + .setValue(value) + .setOptionalEntry(optional) + .build(); } Expr newObject(Object ctx, String typeName, List entries) { @@ -140,12 +157,35 @@ Expr newObject(Object ctx, String typeName, List entries) { } Entry newObjectField(long fieldID, String field, Expr value) { - return Entry.newBuilder().setId(fieldID).setFieldKey(field).setValue(value).build(); + return newObjectField(fieldID, field, value, false); + } + + Entry newObjectField(long fieldID, String field, Expr value, boolean optional) { + return Entry.newBuilder() + .setId(fieldID) + .setFieldKey(field) + .setValue(value) + .setOptionalEntry(optional) + .build(); + } + + Expr newComprehension( + Object ctx, + String iterVar, + Expr iterRange, + String accuVar, + Expr accuInit, + Expr condition, + Expr step, + Expr result) { + return newComprehension( + ctx, iterVar, "", iterRange, accuVar, accuInit, condition, step, result); } Expr newComprehension( Object ctx, String iterVar, + String iterVar2, Expr iterRange, String accuVar, Expr accuInit, @@ -158,6 +198,7 @@ Expr newComprehension( .setAccuVar(accuVar) .setAccuInit(accuInit) .setIterVar(iterVar) + .setIterVar2(iterVar2) .setIterRange(iterRange) .setLoopCondition(condition) .setLoopStep(step) @@ -177,12 +218,8 @@ private Builder newExprBuilder(Object ctx) { long id(Object ctx) { Location location; - if (ctx instanceof ParserRuleContext) { - Token token = ((ParserRuleContext) ctx).start; - location = source.newLocation(token.getLine(), token.getCharPositionInLine()); - } else if (ctx instanceof Token) { - Token token = (Token) ctx; - location = source.newLocation(token.getLine(), token.getCharPositionInLine()); + if (ctx instanceof Node node) { + location = source.newLocation(node.getBeginLine(), node.getBeginColumn() - 1); } else if (ctx instanceof Location) { location = (Location) ctx; } else { diff --git a/core/src/main/java/org/projectnessie/cel/parser/Macro.java b/core/src/main/java/org/projectnessie/cel/parser/Macro.java index ba507490..ac7e4c52 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/Macro.java +++ b/core/src/main/java/org/projectnessie/cel/parser/Macro.java @@ -15,10 +15,11 @@ */ package org.projectnessie.cel.parser; -import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import com.google.api.expr.v1alpha1.Expr; +import com.google.api.expr.v1alpha1.Expr.CreateList; +import com.google.api.expr.v1alpha1.Expr.CreateStruct.Entry; import com.google.api.expr.v1alpha1.Expr.ExprKindCase; import com.google.api.expr.v1alpha1.Expr.Select; import java.util.List; @@ -26,6 +27,7 @@ import org.projectnessie.cel.common.ErrorWithLocation; import org.projectnessie.cel.common.Location; import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.Overloads; public final class Macro { /** AccumulatorName is the traditional variable name assigned to the fold accumulator variable. */ @@ -33,7 +35,7 @@ public final class Macro { /** AllMacros includes the list of all spec-supported macros. */ public static final List AllMacros = - asList( + List.of( /* The macro "has(m.f)" which tests the presence of a field, avoiding the need to specify * the field as a string. */ @@ -43,16 +45,20 @@ public final class Macro { * predicate holds. */ newReceiverMacro(Operator.All.id, 2, Macro::makeAll), + newReceiverMacro(Operator.All.id, 3, Macro::makeAll2), /* The macro "range.exists(var, predicate)", which is true if for at least one element in * range the predicate holds. */ newReceiverMacro(Operator.Exists.id, 2, Macro::makeExists), + newReceiverMacro(Operator.Exists.id, 3, Macro::makeExists2), /* The macro "range.exists_one(var, predicate)", which is true if for exactly one element * in range the predicate holds. */ newReceiverMacro(Operator.ExistsOne.id, 2, Macro::makeExistsOne), + newReceiverMacro(Operator.ExistsOne.id, 3, Macro::makeExistsOne2), + newReceiverMacro("existsOne", 3, Macro::makeExistsOne2), /* The macro "range.map(var, function)", applies the function to the vars in the range. */ newReceiverMacro(Operator.Map.id, 2, Macro::makeMap), @@ -65,7 +71,26 @@ public final class Macro { /* The macro "range.filter(var, predicate)", filters out the variables for which the * predicate is false. */ - newReceiverMacro(Operator.Filter.id, 2, Macro::makeFilter)); + newReceiverMacro(Operator.Filter.id, 2, Macro::makeFilter), + + /* The macro "range.transformList(index, value, function)" transforms entries to a list. */ + newReceiverMacro("transformList", 3, Macro::makeTransformList), + newReceiverMacro("transformList", 4, Macro::makeTransformList), + + /* The macro "range.transformMap(key, value, function)" transforms map values. */ + newReceiverMacro("transformMap", 3, Macro::makeTransformMap), + newReceiverMacro("transformMap", 4, Macro::makeTransformMap), + + /* The macro "cel.bind(var, value, result)" binds a local variable for use in result. */ + newReceiverMacro("bind", 3, Macro::makeBind)); + + /** TestOnlyBlockMacros includes the test-only macros used by CEL-Spec block conformance tests. */ + public static final List TestOnlyBlockMacros = + List.of( + newReceiverMacro("block", 2, Macro::makeBlock), + newReceiverMacro("index", 1, Macro::makeIndex), + newReceiverMacro("iterVar", 2, Macro::makeIterVar), + newReceiverMacro("accuVar", 2, Macro::makeAccuVar)); /** NoMacros list. */ public static List MoMacros = emptyList(); @@ -138,14 +163,26 @@ static Expr makeAll(ExprHelper eh, Expr target, List args) { return makeQuantifier(QuantifierKind.quantifierAll, eh, target, args); } + static Expr makeAll2(ExprHelper eh, Expr target, List args) { + return makeQuantifier2(QuantifierKind.quantifierAll, eh, target, args); + } + static Expr makeExists(ExprHelper eh, Expr target, List args) { return makeQuantifier(QuantifierKind.quantifierExists, eh, target, args); } + static Expr makeExists2(ExprHelper eh, Expr target, List args) { + return makeQuantifier2(QuantifierKind.quantifierExists, eh, target, args); + } + static Expr makeExistsOne(ExprHelper eh, Expr target, List args) { return makeQuantifier(QuantifierKind.quantifierExistsOne, eh, target, args); } + static Expr makeExistsOne2(ExprHelper eh, Expr target, List args) { + return makeQuantifier2(QuantifierKind.quantifierExistsOne, eh, target, args); + } + static Expr makeQuantifier(QuantifierKind kind, ExprHelper eh, Expr target, List args) { String v = extractIdent(args.get(0)); if (v == null) { @@ -194,6 +231,59 @@ static Expr makeQuantifier(QuantifierKind kind, ExprHelper eh, Expr target, List return eh.fold(v, target, AccumulatorName, init, condition, step, result); } + static Expr makeQuantifier2(QuantifierKind kind, ExprHelper eh, Expr target, List args) { + String v = extractIdent(args.get(0)); + if (v == null) { + Location location = eh.offsetLocation(args.get(0).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + String v2 = extractIdent(args.get(1)); + if (v2 == null) { + Location location = eh.offsetLocation(args.get(1).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + + Supplier accuIdent = () -> eh.ident(AccumulatorName); + + Expr init; + Expr condition; + Expr step; + Expr result; + switch (kind) { + case quantifierAll: + init = eh.literalBool(true); + condition = eh.globalCall(Operator.NotStrictlyFalse.id, accuIdent.get()); + step = eh.globalCall(Operator.LogicalAnd.id, accuIdent.get(), args.get(2)); + result = accuIdent.get(); + break; + case quantifierExists: + init = eh.literalBool(false); + condition = + eh.globalCall( + Operator.NotStrictlyFalse.id, + eh.globalCall(Operator.LogicalNot.id, accuIdent.get())); + step = eh.globalCall(Operator.LogicalOr.id, accuIdent.get(), args.get(2)); + result = accuIdent.get(); + break; + case quantifierExistsOne: + Expr zeroExpr = eh.literalInt(0); + Expr oneExpr = eh.literalInt(1); + init = zeroExpr; + condition = eh.literalBool(true); + step = + eh.globalCall( + Operator.Conditional.id, + args.get(2), + eh.globalCall(Operator.Add.id, accuIdent.get(), oneExpr), + accuIdent.get()); + result = eh.globalCall(Operator.Equals.id, accuIdent.get(), oneExpr); + break; + default: + throw new ErrorWithLocation(null, String.format("unrecognized quantifier '%s'", kind)); + } + return eh.fold(v, v2, target, AccumulatorName, init, condition, step, result); + } + static Expr makeMap(ExprHelper eh, Expr target, List args) { String v = extractIdent(args.get(0)); if (v == null) { @@ -237,6 +327,185 @@ static Expr makeFilter(ExprHelper eh, Expr target, List args) { return eh.fold(v, target, AccumulatorName, init, condition, step, accuExpr); } + static Expr makeTransformList(ExprHelper eh, Expr target, List args) { + String v = extractIdent(args.get(0)); + if (v == null) { + Location location = eh.offsetLocation(args.get(0).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + String v2 = extractIdent(args.get(1)); + if (v2 == null) { + Location location = eh.offsetLocation(args.get(1).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + + Expr fn; + Expr filter; + + if (args.size() == 4) { + filter = args.get(2); + fn = args.get(3); + } else { + filter = null; + fn = args.get(2); + } + + Expr accuExpr = eh.ident(AccumulatorName); + Expr init = eh.newList(); + Expr condition = eh.literalBool(true); + Expr step = eh.globalCall(Operator.Add.id, accuExpr, eh.newList(fn)); + + if (filter != null) { + step = eh.globalCall(Operator.Conditional.id, filter, step, accuExpr); + } + return eh.fold(v, v2, target, AccumulatorName, init, condition, step, accuExpr); + } + + static Expr makeTransformMap(ExprHelper eh, Expr target, List args) { + String v = extractIdent(args.get(0)); + if (v == null) { + Location location = eh.offsetLocation(args.get(0).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + String v2 = extractIdent(args.get(1)); + if (v2 == null) { + Location location = eh.offsetLocation(args.get(1).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + + Expr fn; + Expr filter; + + if (args.size() == 4) { + filter = args.get(2); + fn = args.get(3); + } else { + filter = null; + fn = args.get(2); + } + + Expr accuExpr = eh.ident(AccumulatorName); + Expr init = eh.newMap(emptyList()); + Expr condition = eh.literalBool(true); + Entry transformedEntry = eh.newMapEntry(eh.ident(v), fn); + Expr step = eh.newMap(List.of(transformedEntry)); + + if (filter != null) { + step = eh.globalCall(Operator.Conditional.id, filter, step, accuExpr); + } + return eh.fold(v, v2, target, AccumulatorName, init, condition, step, accuExpr); + } + + static Expr makeBind(ExprHelper eh, Expr target, List args) { + if (target == null + || target.getExprKindCase() != ExprKindCase.IDENT_EXPR + || !"cel".equals(target.getIdentExpr().getName())) { + Location location = target != null ? eh.offsetLocation(target.getId()) : null; + throw new ErrorWithLocation( + location, "cel.bind() must be called with receiver identifier cel"); + } + + String v = extractIdent(args.get(0)); + if (v == null) { + Location location = eh.offsetLocation(args.get(0).getId()); + throw new ErrorWithLocation(location, "argument must be a simple name"); + } + + Expr accuExpr = eh.ident(AccumulatorName); + Expr dynNull = eh.globalCall(Overloads.TypeConvertDyn, eh.literalNull()); + return eh.fold( + v, + eh.newList(args.get(1)), + AccumulatorName, + dynNull, + eh.literalBool(true), + args.get(2), + accuExpr); + } + + static Expr makeBlock(ExprHelper eh, Expr target, List args) { + validateCelReceiver(eh, target, "cel.block()"); + + Expr bindings = args.get(0); + if (bindings.getExprKindCase() != ExprKindCase.LIST_EXPR) { + Location location = eh.offsetLocation(bindings.getId()); + throw new ErrorWithLocation(location, "cel.block() first argument must be a list literal"); + } + + CreateList list = bindings.getListExpr(); + Expr result = args.get(1); + for (int i = list.getElementsCount() - 1; i >= 0; i--) { + result = makeLocalBinding(eh, indexName(i), list.getElements(i), result); + } + return result; + } + + static Expr makeIndex(ExprHelper eh, Expr target, List args) { + validateCelReceiver(eh, target, "cel.index()"); + return eh.ident(indexName(extractIntegerArgument(eh, args.get(0), "cel.index()"))); + } + + static Expr makeIterVar(ExprHelper eh, Expr target, List args) { + validateCelReceiver(eh, target, "cel.iterVar()"); + return eh.ident( + String.format( + "@it:%d:%d", + extractIntegerArgument(eh, args.get(0), "cel.iterVar()"), + extractIntegerArgument(eh, args.get(1), "cel.iterVar()"))); + } + + static Expr makeAccuVar(ExprHelper eh, Expr target, List args) { + validateCelReceiver(eh, target, "cel.accuVar()"); + return eh.ident( + String.format( + "@ac:%d:%d", + extractIntegerArgument(eh, args.get(0), "cel.accuVar()"), + extractIntegerArgument(eh, args.get(1), "cel.accuVar()"))); + } + + private static Expr makeLocalBinding(ExprHelper eh, String variable, Expr value, Expr result) { + Expr accuExpr = eh.ident(AccumulatorName); + Expr dynNull = eh.globalCall(Overloads.TypeConvertDyn, eh.literalNull()); + return eh.fold( + variable, + eh.newList(value), + AccumulatorName, + dynNull, + eh.literalBool(true), + result, + accuExpr); + } + + private static void validateCelReceiver(ExprHelper eh, Expr target, String macroName) { + if (target != null + && target.getExprKindCase() == ExprKindCase.IDENT_EXPR + && "cel".equals(target.getIdentExpr().getName())) { + return; + } + + Location location = target != null ? eh.offsetLocation(target.getId()) : null; + throw new ErrorWithLocation( + location, macroName + " must be called with receiver identifier cel"); + } + + private static int extractIntegerArgument(ExprHelper eh, Expr expr, String macroName) { + if (expr.getExprKindCase() != ExprKindCase.CONST_EXPR || !expr.getConstExpr().hasInt64Value()) { + Location location = eh.offsetLocation(expr.getId()); + throw new ErrorWithLocation(location, macroName + " argument must be an integer literal"); + } + + long value = expr.getConstExpr().getInt64Value(); + if (value < 0 || value > Integer.MAX_VALUE) { + Location location = eh.offsetLocation(expr.getId()); + throw new ErrorWithLocation(location, macroName + " argument out of range"); + } + return (int) value; + } + + private static String indexName(int index) { + return "@index" + index; + } + static String extractIdent(Expr e) { if (e.getExprKindCase() == ExprKindCase.IDENT_EXPR) { return e.getIdentExpr().getName(); diff --git a/core/src/main/java/org/projectnessie/cel/parser/Options.java b/core/src/main/java/org/projectnessie/cel/parser/Options.java index 29ddf3f1..fd1da5d3 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/Options.java +++ b/core/src/main/java/org/projectnessie/cel/parser/Options.java @@ -117,10 +117,7 @@ public Builder macros(List macros) { public Options build() { return new Options( - maxRecursionDepth, - errorRecoveryLimit, - expressionSizeCodePointLimit, - new HashMap<>(macros)); + maxRecursionDepth, errorRecoveryLimit, expressionSizeCodePointLimit, Map.copyOf(macros)); } } } diff --git a/core/src/main/java/org/projectnessie/cel/parser/Parser.java b/core/src/main/java/org/projectnessie/cel/parser/Parser.java index 9c428866..ba8207c2 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/Parser.java +++ b/core/src/main/java/org/projectnessie/cel/parser/Parser.java @@ -16,6 +16,28 @@ package org.projectnessie.cel.parser; import static org.projectnessie.cel.parser.Macro.AllMacros; +import static org.projectnessie.cel.parser.Token.TokenType.BYTES; +import static org.projectnessie.cel.parser.Token.TokenType.COLON; +import static org.projectnessie.cel.parser.Token.TokenType.COMMA; +import static org.projectnessie.cel.parser.Token.TokenType.DOT; +import static org.projectnessie.cel.parser.Token.TokenType.EOF; +import static org.projectnessie.cel.parser.Token.TokenType.EXCLAM; +import static org.projectnessie.cel.parser.Token.TokenType.FALSE; +import static org.projectnessie.cel.parser.Token.TokenType.IDENTIFIER; +import static org.projectnessie.cel.parser.Token.TokenType.LBRACE; +import static org.projectnessie.cel.parser.Token.TokenType.LBRACKET; +import static org.projectnessie.cel.parser.Token.TokenType.LPAREN; +import static org.projectnessie.cel.parser.Token.TokenType.MINUS; +import static org.projectnessie.cel.parser.Token.TokenType.NULL; +import static org.projectnessie.cel.parser.Token.TokenType.NUM_FLOAT; +import static org.projectnessie.cel.parser.Token.TokenType.NUM_INT; +import static org.projectnessie.cel.parser.Token.TokenType.NUM_UINT; +import static org.projectnessie.cel.parser.Token.TokenType.QUESTIONMARK; +import static org.projectnessie.cel.parser.Token.TokenType.RBRACE; +import static org.projectnessie.cel.parser.Token.TokenType.RBRACKET; +import static org.projectnessie.cel.parser.Token.TokenType.RPAREN; +import static org.projectnessie.cel.parser.Token.TokenType.STRING; +import static org.projectnessie.cel.parser.Token.TokenType.TRUE; import com.google.api.expr.v1alpha1.Constant; import com.google.api.expr.v1alpha1.Expr; @@ -27,9 +49,7 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; -import java.util.BitSet; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Set; import org.projectnessie.cel.common.ErrorWithLocation; @@ -38,82 +58,39 @@ import org.projectnessie.cel.common.Source; import org.projectnessie.cel.common.operators.Operator; import org.projectnessie.cel.parser.Helper.Balancer; -import org.projectnessie.cel.parser.gen.CELLexer; -import org.projectnessie.cel.parser.gen.CELParser; -import org.projectnessie.cel.parser.gen.CELParser.BoolFalseContext; -import org.projectnessie.cel.parser.gen.CELParser.BoolTrueContext; -import org.projectnessie.cel.parser.gen.CELParser.BytesContext; -import org.projectnessie.cel.parser.gen.CELParser.CalcContext; -import org.projectnessie.cel.parser.gen.CELParser.ConditionalAndContext; -import org.projectnessie.cel.parser.gen.CELParser.ConditionalOrContext; -import org.projectnessie.cel.parser.gen.CELParser.ConstantLiteralContext; -import org.projectnessie.cel.parser.gen.CELParser.CreateListContext; -import org.projectnessie.cel.parser.gen.CELParser.CreateMessageContext; -import org.projectnessie.cel.parser.gen.CELParser.CreateStructContext; -import org.projectnessie.cel.parser.gen.CELParser.DoubleContext; -import org.projectnessie.cel.parser.gen.CELParser.ExprContext; -import org.projectnessie.cel.parser.gen.CELParser.ExprListContext; -import org.projectnessie.cel.parser.gen.CELParser.FieldInitializerListContext; -import org.projectnessie.cel.parser.gen.CELParser.IdentOrGlobalCallContext; -import org.projectnessie.cel.parser.gen.CELParser.IndexContext; -import org.projectnessie.cel.parser.gen.CELParser.IntContext; -import org.projectnessie.cel.parser.gen.CELParser.LogicalNotContext; -import org.projectnessie.cel.parser.gen.CELParser.MapInitializerListContext; -import org.projectnessie.cel.parser.gen.CELParser.MemberExprContext; -import org.projectnessie.cel.parser.gen.CELParser.NegateContext; -import org.projectnessie.cel.parser.gen.CELParser.NestedContext; -import org.projectnessie.cel.parser.gen.CELParser.NullContext; -import org.projectnessie.cel.parser.gen.CELParser.PrimaryExprContext; -import org.projectnessie.cel.parser.gen.CELParser.RelationContext; -import org.projectnessie.cel.parser.gen.CELParser.SelectOrCallContext; -import org.projectnessie.cel.parser.gen.CELParser.StartContext; -import org.projectnessie.cel.parser.gen.CELParser.StringContext; -import org.projectnessie.cel.parser.gen.CELParser.UintContext; -import org.projectnessie.cel.parser.gen.CELParser.UnaryContext; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.ANTLRErrorListener; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.CommonTokenStream; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.DefaultErrorStrategy; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.IntStream; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.ParserRuleContext; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.RecognitionException; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.Recognizer; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.RuleContext; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.Token; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.atn.ATNConfigSet; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.dfa.DFA; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.tree.AbstractParseTreeVisitor; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.tree.ErrorNode; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.tree.ParseTree; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.tree.ParseTreeListener; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.tree.TerminalNode; +import org.projectnessie.cel.parser.ast.ConstantLiteral; +import org.projectnessie.cel.parser.ast.ExprList; +import org.projectnessie.cel.parser.ast.Field; +import org.projectnessie.cel.parser.ast.FieldInitializerList; +import org.projectnessie.cel.parser.ast.ListInitializerList; +import org.projectnessie.cel.parser.ast.MapInitializerList; +import org.projectnessie.cel.parser.ast.Start; public final class Parser { private static final Set reservedIds = - Collections.unmodifiableSet( - new HashSet<>( - Arrays.asList( - "as", - "break", - "const", - "continue", - "else", - "false", - "for", - "function", - "if", - "import", - "in", - "let", - "loop", - "package", - "namespace", - "null", - "return", - "true", - "var", - "void", - "while"))); + Set.of( + "as", + "break", + "const", + "continue", + "else", + "false", + "for", + "function", + "if", + "import", + "in", + "let", + "loop", + "package", + "namespace", + "null", + "return", + "true", + "var", + "void", + "while"); private final Options options; @@ -134,37 +111,27 @@ public static ParseResult parse(Options options, Source source) { } ParseResult parse(Source source) { - StringCharStream charStream = new StringCharStream(source.content(), source.description()); - CELLexer lexer = new CELLexer(charStream); - CELParser parser = new CELParser(new CommonTokenStream(lexer, 0)); - - RecursionListener parserListener = new RecursionListener(options.getMaxRecursionDepth()); - - parser.addParseListener(parserListener); - - parser.setErrorHandler(new RecoveryLimitErrorStrategy(options.getErrorRecoveryLimit())); - Helper helper = new Helper(source); Errors errors = new Errors(source); - - InnerParser inner = new InnerParser(helper, errors); - - lexer.addErrorListener(inner); - parser.addErrorListener(inner); - Expr expr = null; - try { - if (charStream.size() > options.getExpressionSizeCodePointLimit()) { - errors.reportError( - Location.NoLocation, - "expression code point size exceeds limit: size: %d, limit %d", - charStream.size(), - options.getExpressionSizeCodePointLimit()); - } else { - expr = inner.exprVisit(parser.start()); + + int codePointCount = source.content().codePointCount(0, source.content().length()); + if (codePointCount > options.getExpressionSizeCodePointLimit()) { + errors.reportError( + Location.NoLocation, + "expression code point size exceeds limit: size: %d, limit %d", + codePointCount, + options.getExpressionSizeCodePointLimit()); + } else { + CelGrammarParser parser = new CelGrammarParser(source.description(), source.content()); + try { + parser.Start(); + expr = new AstBuilder(helper, errors).exprVisit(firstExpressionNode(parser.rootNode())); + } catch (ParseException e) { + errors.syntaxError(location(e.getLocation()), e.getMessage()); + } catch (RecursionError e) { + errors.reportError(e, Location.NoLocation, "%s", e.getMessage()); } - } catch (RecoveryLimitError | RecursionError e) { - errors.reportError(e, Location.NoLocation, "%s", e.getMessage()); } if (errors.hasErrors()) { @@ -174,6 +141,24 @@ ParseResult parse(Source source) { return new ParseResult(expr, errors, helper.getSourceInfo()); } + private static Node firstExpressionNode(Node root) { + if (root instanceof Start) { + for (Node child : root.children()) { + if (!isToken(child, EOF)) { + return child; + } + } + } + return root; + } + + private static Location location(Node node) { + if (node == null) { + return Location.NoLocation; + } + return Location.newLocation(node.getBeginLine(), node.getBeginColumn() - 1); + } + public static final class ParseResult { private final Expr expr; private final Errors errors; @@ -202,404 +187,339 @@ public boolean hasErrors() { } } - static final class RecursionListener implements ParseTreeListener { - private final int maxDepth; - private int depth; - - RecursionListener(int maxDepth) { - this.maxDepth = maxDepth; - } - - @Override - public void visitTerminal(TerminalNode node) {} - - @Override - public void visitErrorNode(ErrorNode node) {} - - @Override - public void enterEveryRule(ParserRuleContext ctx) { - if (ctx != null && ctx.getRuleIndex() == CELParser.RULE_expr) { - if (this.depth >= this.maxDepth) { - this.depth++; - throw new RecursionError( - String.format("expression recursion limit exceeded: %d", maxDepth)); - } - this.depth++; - } - } - - @Override - public void exitEveryRule(ParserRuleContext ctx) { - if (ctx != null && ctx.getRuleIndex() == CELParser.RULE_expr) { - depth--; - } - } - } - static final class RecursionError extends RuntimeException { - public RecursionError(String message) { + RecursionError(String message) { super(message); } } - static final class RecoveryLimitError extends RecognitionException { - public RecoveryLimitError( - String message, Recognizer recognizer, IntStream input, ParserRuleContext ctx) { - super(message, recognizer, input, ctx); - } - } - - static final class RecoveryLimitErrorStrategy extends DefaultErrorStrategy { - private final int maxAttempts; - private int attempts; - - private RecoveryLimitErrorStrategy(int maxAttempts) { - this.maxAttempts = maxAttempts; - } - - @Override - public void recover( - org.projectnessie.cel.shaded.org.antlr.v4.runtime.Parser recognizer, - RecognitionException e) { - checkAttempts(recognizer); - super.recover(recognizer, e); - } - - @Override - public Token recoverInline(org.projectnessie.cel.shaded.org.antlr.v4.runtime.Parser recognizer) - throws RecognitionException { - checkAttempts(recognizer); - return super.recoverInline(recognizer); - } - - void checkAttempts(org.projectnessie.cel.shaded.org.antlr.v4.runtime.Parser recognizer) - throws RecognitionException { - if (attempts >= maxAttempts) { - attempts++; - String msg = String.format("error recovery attempt limit exceeded: %d", maxAttempts); - recognizer.notifyErrorListeners(null, msg, null); - throw new RecoveryLimitError(msg, recognizer, null, null); - } - attempts++; - } - } - - final class InnerParser extends AbstractParseTreeVisitor implements ANTLRErrorListener { - + final class AstBuilder implements CelExprBuilder { private final Helper helper; private final Errors errors; + private int depth; - InnerParser(Helper helper, Errors errors) { + AstBuilder(Helper helper, Errors errors) { this.helper = helper; this.errors = errors; } - @Override - public void syntaxError( - Recognizer recognizer, - Object offendingSymbol, - int line, - int charPositionInLine, - String msg, - RecognitionException e) { - errors.syntaxError(Location.newLocation(line, charPositionInLine), msg); - } - - @Override - public void reportAmbiguity( - org.projectnessie.cel.shaded.org.antlr.v4.runtime.Parser recognizer, - DFA dfa, - int startIndex, - int stopIndex, - boolean exact, - BitSet ambigAlts, - ATNConfigSet configs) { - // empty - } - - @Override - public void reportAttemptingFullContext( - org.projectnessie.cel.shaded.org.antlr.v4.runtime.Parser recognizer, - DFA dfa, - int startIndex, - int stopIndex, - BitSet conflictingAlts, - ATNConfigSet configs) { - // empty - } - - @Override - public void reportContextSensitivity( - org.projectnessie.cel.shaded.org.antlr.v4.runtime.Parser recognizer, - DFA dfa, - int startIndex, - int stopIndex, - int prediction, - ATNConfigSet configs) { - // empty - } - - Expr reportError(Object ctx, String message) { - return reportError(ctx, "%s", message); - } - - Expr reportError(Object ctx, String format, Object... args) { - Location location; - if (ctx instanceof Location) { - location = (Location) ctx; - } else if (ctx instanceof Token || ctx instanceof ParserRuleContext) { - Expr err = helper.newExpr(ctx); - location = helper.getLocation(err.getId()); - } else { - location = Location.NoLocation; + Expr exprVisit(Node node) { + if (node == null) { + return reportError(Location.NoLocation, "unknown parse element encountered: <>"); } - Expr err = helper.newExpr(ctx); - // Provide arguments to the report error. - errors.reportError(location, format, args); - return err; - } - - public Expr exprVisit(ParseTree tree) { - Object r = visit(tree); - return (Expr) r; - } - - @Override - public Object visit(ParseTree tree) { - if (tree instanceof RuleContext) { - RuleContext ruleContext = (RuleContext) tree; - int ruleIndex = ruleContext.getRuleIndex(); - switch (ruleIndex) { - case CELParser.RULE_start: - return visitStart((StartContext) tree); - case CELParser.RULE_expr: - return visitExpr((ExprContext) tree); - case CELParser.RULE_conditionalOr: - return visitConditionalOr((ConditionalOrContext) tree); - case CELParser.RULE_conditionalAnd: - return visitConditionalAnd((ConditionalAndContext) tree); - case CELParser.RULE_relation: - return visitRelation((RelationContext) tree); - case CELParser.RULE_calc: - return visitCalc((CalcContext) tree); - case CELParser.RULE_unary: - if (tree instanceof LogicalNotContext) { - return visitLogicalNot((LogicalNotContext) tree); - } else if (tree instanceof NegateContext) { - return visitNegate((NegateContext) tree); - } else if (tree instanceof MemberExprContext) { - return visitMemberExpr((MemberExprContext) tree); - } - return visitUnary((UnaryContext) tree); - case CELParser.RULE_member: - if (tree instanceof CreateMessageContext) { - return visitCreateMessage((CreateMessageContext) tree); - } else if (tree instanceof PrimaryExprContext) { - return visitPrimaryExpr((PrimaryExprContext) tree); - } else if (tree instanceof SelectOrCallContext) { - return visitSelectOrCall((SelectOrCallContext) tree); - } else if (tree instanceof IndexContext) { - return visitIndex((IndexContext) tree); - } - break; - case CELParser.RULE_primary: - if (tree instanceof CreateListContext) { - return visitCreateList((CreateListContext) tree); - } else if (tree instanceof CreateStructContext) { - return visitCreateStruct((CreateStructContext) tree); - } - break; - case CELParser.RULE_fieldInitializerList: - case CELParser.RULE_mapInitializerList: - return visitMapInitializerList((MapInitializerListContext) tree); - // case CELParser.RULE_exprList: - // case CELParser.RULE_literal: - default: - return reportError(tree, "parser rule '%d'", ruleIndex); - } + if (depth >= options.getMaxRecursionDepth()) { + throw new RecursionError( + String.format( + "expression recursion limit exceeded: %d", options.getMaxRecursionDepth())); } - - // Report at least one error if the parser reaches an unknown parse element. - // Typically, this happens if the parser has already encountered a syntax error elsewhere. - if (!errors.hasErrors()) { - String txt = "<>"; - if (tree != null) { - txt = String.format("<<%s>>", tree.getClass().getSimpleName()); - } - return reportError(Location.NoLocation, "unknown parse element encountered: %s", txt); + depth++; + try { + return doExprVisit(node); + } finally { + depth--; } - return helper.newExpr(Location.NoLocation); } - private Object visitStart(StartContext ctx) { - return visit(ctx.expr()); + private Expr doExprVisit(Node node) { + if (node instanceof CelExprNode) { + return ((CelExprNode) node).toCelExpr(this); + } + return reportError( + node, "unknown parse element encountered: <<%s>>", node.getClass().getSimpleName()); } - private Expr visitExpr(ExprContext ctx) { - Expr result = exprVisit(ctx.e); - if (ctx.op == null) { - return result; + @Override + public Expr visitExpr(Node node) { + List children = significantChildren(node); + int question = indexOf(children, QUESTIONMARK); + if (question < 0) { + return exprVisit(firstExpressionChild(children, node)); } - long opID = helper.id(ctx.op); - Expr ifTrue = exprVisit(ctx.e1); - Expr ifFalse = exprVisit(ctx.e2); - return globalCallOrMacro(opID, Operator.Conditional.id, result, ifTrue, ifFalse); + Expr condition = exprVisit(children.get(0)); + long opID = helper.id(children.get(question)); + Expr ifTrue = exprVisit(children.get(question + 1)); + Expr ifFalse = exprVisit(children.get(question + 3)); + return globalCallOrMacro(opID, Operator.Conditional.id, condition, ifTrue, ifFalse); } - private Expr visitConditionalAnd(ConditionalAndContext ctx) { - Expr result = exprVisit(ctx.e); - if (ctx.ops == null || ctx.ops.isEmpty()) { - return result; - } - Balancer b = helper.newBalancer(Operator.LogicalAnd.id, result); - List rest = ctx.e1; - for (int i = 0; i < ctx.ops.size(); i++) { - Token op = ctx.ops.get(i); - if (i >= rest.size()) { - return reportError(ctx, "unexpected character, wanted '&&'"); + @Override + public Expr visitBalanced(Node node, Operator operator) { + List children = significantChildren(node); + if (children.size() == 1) { + return exprVisit(children.get(0)); + } + Expr result = exprVisit(children.get(0)); + Balancer balancer = helper.newBalancer(operator.id, result); + for (int i = 1; i < children.size(); i += 2) { + Node op = children.get(i); + if (i + 1 >= children.size()) { + return reportError(node, "unexpected character, wanted '%s'", tokenText(op)); } - Expr next = exprVisit(rest.get(i)); - long opID = helper.id(op); - b.addTerm(opID, next); + Expr next = exprVisit(children.get(i + 1)); + balancer.addTerm(helper.id(op), next); } - return b.balance(); + return balancer.balance(); } - private Expr visitConditionalOr(ConditionalOrContext ctx) { - Expr result = exprVisit(ctx.e); - if (ctx.ops == null || ctx.ops.isEmpty()) { - return result; - } - Balancer b = helper.newBalancer(Operator.LogicalOr.id, result); - List rest = ctx.e1; - for (int i = 0; i < ctx.ops.size(); i++) { - Token op = ctx.ops.get(i); - if (i >= rest.size()) { - return reportError(ctx, "unexpected character, wanted '||'"); + @Override + public Expr visitBinary(Node node) { + List children = significantChildren(node); + if (children.size() == 1) { + return exprVisit(children.get(0)); + } + Expr result = exprVisit(children.get(0)); + for (int i = 1; i < children.size(); i += 2) { + Node opNode = children.get(i); + if (i + 1 >= children.size()) { + return reportError(node, "operator not found"); + } + Operator op = Operator.find(tokenText(opNode)); + if (op == null) { + return reportError(opNode, "operator not found"); } - Expr next = exprVisit(rest.get(i)); - long opID = helper.id(op); - b.addTerm(opID, next); + long opID = helper.id(opNode); + Expr rhs = exprVisit(children.get(i + 1)); + result = globalCallOrMacro(opID, op.id, result, rhs); } - return b.balance(); + return result; } - private Expr visitRelation(RelationContext ctx) { - if (ctx.calc() != null) { - return exprVisit(ctx.calc()); + @Override + public Expr visitUnary(Node node) { + List children = significantChildren(node); + int opCount = 0; + while (opCount < children.size() + && (isToken(children.get(opCount), MINUS) || isToken(children.get(opCount), EXCLAM))) { + opCount++; } - String opText = ""; - if (ctx.op != null) { - opText = ctx.op.getText(); + Node operand = children.get(opCount); + if (opCount == 0) { + return exprVisit(operand); } - Operator op = Operator.find(opText); - if (op != null) { - Expr lhs = exprVisit(ctx.relation(0)); - long opID = helper.id(ctx.op); - Expr rhs = exprVisit(ctx.relation(1)); - return globalCallOrMacro(opID, op.id, lhs, rhs); + Node op = children.get(0); + boolean logicalNot = isToken(op, EXCLAM); + if (opCount % 2 == 0) { + return exprVisit(operand); } - return reportError(ctx, "operator not found"); + if (!logicalNot && isNegativeNumericLiteral(operand)) { + return visitNegativeNumericLiteral(op, operand); + } + return globalCallOrMacro( + helper.id(op), + logicalNot ? Operator.LogicalNot.id : Operator.Negate.id, + exprVisit(operand)); } - private Expr visitCalc(CalcContext ctx) { - if (ctx.unary() != null) { - return exprVisit(ctx.unary()); - } - String opText = ""; - if (ctx.op != null) { - opText = ctx.op.getText(); + @Override + public Expr visitPrimary(Node node) { + List children = significantChildren(node); + if (children.isEmpty()) { + return reportError(node, "invalid primary expression"); + } + Node first = children.get(0); + if (isToken(first, DOT) || isToken(first, IDENTIFIER)) { + return visitIdentOrGlobalCall(children, node); + } else if (isToken(first, LPAREN)) { + return exprVisit(children.get(1)); + } else if (isToken(first, LBRACKET)) { + long listID = helper.id(first); + ListInitializerList list = firstChildOfType(children, ListInitializerList.class); + ListElements elements = + list != null ? listElements(list) : listElements(children.subList(1, children.size())); + return helper.newList(listID, elements.expressions(), elements.optionalIndices()); + } else if (isToken(first, LBRACE)) { + return helper.newMap( + helper.id(first), mapEntries(firstChildOfType(children, MapInitializerList.class))); + } else if (first instanceof ConstantLiteral || isLiteralToken(first)) { + return exprVisit(first); + } + return reportError(node, "invalid primary expression"); + } + + private Expr visitIdentOrGlobalCall(List children, Node ctx) { + int i = 0; + String prefix = ""; + if (isToken(children.get(i), DOT)) { + prefix = "."; + i++; + } + if (i >= children.size() || !isToken(children.get(i), IDENTIFIER)) { + return helper.newExpr(ctx); } - Operator op = Operator.find(opText); - if (op != null) { - Expr lhs = exprVisit(ctx.calc(0)); - long opID = helper.id(ctx.op); - Expr rhs = exprVisit(ctx.calc(1)); - return globalCallOrMacro(opID, op.id, lhs, rhs); + Token ident = (Token) children.get(i++); + String name = prefix + tokenText(ident); + if (reservedIds.contains(tokenText(ident))) { + return reportError(ident, "reserved identifier: %s", tokenText(ident)); } - return reportError(ctx, "operator not found"); - } - - private Expr visitLogicalNot(LogicalNotContext ctx) { - if (ctx.ops.size() % 2 == 0) { - return exprVisit(ctx.member()); + if (i < children.size() && isToken(children.get(i), LPAREN)) { + Node open = children.get(i); + return globalCallOrMacro( + helper.id(open), name, expressionsBetween(children, i + 1, RPAREN)); } - long opID = helper.id(ctx.ops.get(0)); - Expr target = exprVisit(ctx.member()); - return globalCallOrMacro(opID, Operator.LogicalNot.id, target); + return helper.newIdent(ident, name); } - private Expr visitMemberExpr(MemberExprContext ctx) { - if (ctx.member() instanceof PrimaryExprContext) { - return visitPrimaryExpr((PrimaryExprContext) ctx.member()); - } else if (ctx.member() instanceof SelectOrCallContext) { - return visitSelectOrCall((SelectOrCallContext) ctx.member()); - } else if (ctx.member() instanceof IndexContext) { - return visitIndex((IndexContext) ctx.member()); - } else if (ctx.member() instanceof CreateMessageContext) { - return visitCreateMessage((CreateMessageContext) ctx.member()); - } - return reportError(ctx, "unsupported simple expression"); + @Override + public Expr visitIdentifier(Token token) { + return identOrReserved(token, tokenText(token)); } - private Expr visitPrimaryExpr(PrimaryExprContext ctx) { - if (ctx.primary() instanceof NestedContext) { - return visitNested((NestedContext) ctx.primary()); - } else if (ctx.primary() instanceof IdentOrGlobalCallContext) { - return visitIdentOrGlobalCall((IdentOrGlobalCallContext) ctx.primary()); - } else if (ctx.primary() instanceof CreateListContext) { - return visitCreateList((CreateListContext) ctx.primary()); - } else if (ctx.primary() instanceof CreateStructContext) { - return visitCreateStruct((CreateStructContext) ctx.primary()); - } else if (ctx.primary() instanceof ConstantLiteralContext) { - return visitConstantLiteral((ConstantLiteralContext) ctx.primary()); + private Expr identOrReserved(Token token, String name) { + if (reservedIds.contains(name)) { + return reportError(token, "reserved identifier: %s", name); } - - return reportError(ctx, "invalid primary expression"); + return helper.newIdent(token, name); } - private Expr visitConstantLiteral(ConstantLiteralContext ctx) { - if (ctx.literal() instanceof IntContext) { - return visitInt((IntContext) ctx.literal()); - } else if (ctx.literal() instanceof UintContext) { - return visitUint((UintContext) ctx.literal()); - } else if (ctx.literal() instanceof DoubleContext) { - return visitDouble((DoubleContext) ctx.literal()); - } else if (ctx.literal() instanceof StringContext) { - return visitString((StringContext) ctx.literal()); - } else if (ctx.literal() instanceof BytesContext) { - return visitBytes((BytesContext) ctx.literal()); - } else if (ctx.literal() instanceof BoolFalseContext) { - return visitBoolFalse((BoolFalseContext) ctx.literal()); - } else if (ctx.literal() instanceof BoolTrueContext) { - return visitBoolTrue((BoolTrueContext) ctx.literal()); - } else if (ctx.literal() instanceof NullContext) { - return visitNull((NullContext) ctx.literal()); + @Override + public Expr visitMember(Node node) { + List children = significantChildren(node); + Expr operand = exprVisit(children.get(0)); + int i = 1; + while (i < children.size()) { + Node op = children.get(i++); + if (isToken(op, DOT)) { + if (i >= children.size()) { + return helper.newExpr(node); + } + boolean optional = false; + Node optionalNode = null; + if (isToken(children.get(i), QUESTIONMARK)) { + optional = true; + optionalNode = children.get(i++); + } + String id = fieldName(children.get(i++)); + if (i < children.size() && isToken(children.get(i), LPAREN)) { + if (optional) { + return reportError(optionalNode, "optional select does not support function calls"); + } + Node open = children.get(i++); + long openID = helper.id(open); + List args = expressionsBetween(children, i, RPAREN); + while (i < children.size() && !isToken(children.get(i), RPAREN)) { + i++; + } + if (i < children.size()) { + i++; + } + operand = receiverCallOrMacro(openID, id, operand, args); + } else if (optional) { + operand = + globalCallOrMacro( + helper.id(optionalNode), + Operator.OptionalSelect.id, + operand, + helper.newLiteralString(optionalNode, id)); + } else { + operand = helper.newSelect(op, operand, id); + } + } else if (isToken(op, LBRACKET)) { + long opID = helper.id(op); + boolean optional = false; + if (isToken(children.get(i), QUESTIONMARK)) { + optional = true; + opID = helper.id(children.get(i++)); + } + Expr index = exprVisit(children.get(i++)); + if (i < children.size() && isToken(children.get(i), RBRACKET)) { + i++; + } + operand = + globalCallOrMacro( + opID, optional ? Operator.OptionalIndex.id : Operator.Index.id, operand, index); + } else if (isToken(op, LBRACE)) { + String messageName = extractQualifiedName(operand); + FieldInitializerList fields = + firstChildOfType(children.subList(i, children.size()), FieldInitializerList.class); + if (messageName != null) { + operand = helper.newObject(helper.id(op), messageName, objectFields(fields)); + } else { + operand = helper.newExpr(helper.id(op)); + } + while (i < children.size() && !isToken(children.get(i), RBRACE)) { + i++; + } + if (i < children.size()) { + i++; + } + } else { + return reportError(op, "unsupported member expression"); + } } - return reportError(ctx, "invalid literal"); + return operand; } - private Expr visitInt(IntContext ctx) { - String text = ctx.tok.getText(); + @Override + public Expr visitLiteral(Node node) { + Node token = node; + if (node instanceof ConstantLiteral) { + List children = significantChildren(node); + token = children.get(children.size() - 1); + } + if (isToken(token, NUM_INT)) { + return intLiteral(token); + } else if (isToken(token, NUM_UINT)) { + return uintLiteral(token); + } else if (isToken(token, NUM_FLOAT)) { + return doubleLiteral(token); + } else if (isToken(token, STRING)) { + return helper.newLiteralString(token, unquoteString(token, tokenText(token))); + } else if (isToken(token, BYTES)) { + return helper.newLiteralBytes(token, unquoteBytes(token, tokenText(token).substring(1))); + } else if (isToken(token, FALSE)) { + return helper.newLiteralBool(token, false); + } else if (isToken(token, TRUE)) { + return helper.newLiteralBool(token, true); + } else if (isToken(token, NULL)) { + return helper.newLiteral(token, Constant.newBuilder().setNullValue(NullValue.NULL_VALUE)); + } + return reportError(node, "invalid literal"); + } + + private boolean isNegativeNumericLiteral(Node operand) { + return isToken(operand, NUM_INT) + || isToken(operand, NUM_FLOAT) + || (operand instanceof ConstantLiteral + && significantChildren(operand).stream() + .anyMatch(child -> isToken(child, NUM_INT) || isToken(child, NUM_FLOAT))); + } + + private Expr visitNegativeNumericLiteral(Node op, Node operand) { + Node token = operand; + if (operand instanceof ConstantLiteral) { + List children = significantChildren(operand); + token = children.get(children.size() - 1); + } + if (isToken(token, NUM_INT)) { + return intLiteral(op, "-" + tokenText(token)); + } else if (isToken(token, NUM_FLOAT)) { + return doubleLiteral(op, "-" + tokenText(token)); + } + return globalCallOrMacro(helper.id(op), Operator.Negate.id, exprVisit(operand)); + } + + private Expr intLiteral(Node token) { + return intLiteral(token, tokenText(token)); + } + + private Expr intLiteral(Node token, String text) { int base = 10; - if (text.startsWith("0x")) { + if (text.startsWith("-0x")) { + base = 16; + text = "-" + text.substring(3); + } else if (text.startsWith("0x")) { base = 16; text = text.substring(2); } - if (ctx.sign != null) { - text = ctx.sign.getText() + text; - } try { - long i = Long.parseLong(text, base); - return helper.newLiteralInt(ctx, i); + return helper.newLiteralInt(token, Long.parseLong(text, base)); } catch (Exception e) { - return reportError(ctx, "invalid int literal"); + return reportError(token, "invalid int literal"); } } - private Expr visitUint(UintContext ctx) { - String text = ctx.tok.getText(); - // trim the 'u' designator included in the uint literal. + private Expr uintLiteral(Node token) { + String text = tokenText(token); text = text.substring(0, text.length() - 1); int base = 10; if (text.startsWith("0x")) { @@ -607,63 +527,138 @@ private Expr visitUint(UintContext ctx) { text = text.substring(2); } try { - long i = Long.parseUnsignedLong(text, base); - return helper.newLiteralUint(ctx, i); + return helper.newLiteralUint(token, Long.parseUnsignedLong(text, base)); } catch (Exception e) { - return reportError(ctx, "invalid int literal"); + return reportError(token, "invalid int literal"); } } - private Expr visitDouble(DoubleContext ctx) { - String txt = ctx.tok.getText(); - if (ctx.sign != null) { - txt = ctx.sign.getText() + txt; - } + private Expr doubleLiteral(Node token) { + return doubleLiteral(token, tokenText(token)); + } + + private Expr doubleLiteral(Node token, String text) { try { - double f = Double.parseDouble(txt); - return helper.newLiteralDouble(ctx, f); + return helper.newLiteralDouble(token, Double.parseDouble(text)); } catch (Exception e) { - return reportError(ctx, "invalid double literal"); + return reportError(token, "invalid double literal"); } } - private Expr visitString(StringContext ctx) { - String s = unquoteString(ctx, ctx.getText()); - return helper.newLiteralString(ctx, s); - } - - private Expr visitBytes(BytesContext ctx) { - ByteString b = unquoteBytes(ctx, ctx.tok.getText().substring(1)); - return helper.newLiteralBytes(ctx, b); + private List expressionsIn(ExprList list) { + if (list == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (Node child : significantChildren(list)) { + if (!isToken(child, COMMA)) { + result.add(exprVisit(child)); + } + } + return result; } - private Expr visitBoolFalse(BoolFalseContext ctx) { - return helper.newLiteralBool(ctx, false); + private ListElements listElements(ListInitializerList list) { + if (list == null) { + return new ListElements(Collections.emptyList(), Collections.emptyList()); + } + return listElements(significantChildren(list)); } - private Expr visitBoolTrue(BoolTrueContext ctx) { - return helper.newLiteralBool(ctx, true); + private ListElements listElements(List children) { + List expressions = new ArrayList<>(); + List optionalIndices = new ArrayList<>(); + boolean optional = false; + for (Node child : children) { + if (isToken(child, COMMA) || isToken(child, RBRACKET)) { + continue; + } + if (isToken(child, QUESTIONMARK)) { + optional = true; + continue; + } + if (optional) { + optionalIndices.add(expressions.size()); + optional = false; + } + expressions.add(exprVisit(child)); + } + return new ListElements(expressions, optionalIndices); } - private Expr visitNull(NullContext ctx) { - return helper.newLiteral(ctx, Constant.newBuilder().setNullValue(NullValue.NULL_VALUE)); + private List expressionsBetween(List children, int start, Token.TokenType end) { + List result = new ArrayList<>(); + for (int i = start; i < children.size() && !isToken(children.get(i), end); i++) { + Node child = children.get(i); + if (isToken(child, COMMA)) { + continue; + } + if (child instanceof ExprList) { + result.addAll(expressionsIn((ExprList) child)); + } else { + result.add(exprVisit(child)); + } + } + return result; } - private List visitList(ExprListContext ctx) { - if (ctx == null) { + private List objectFields(FieldInitializerList fields) { + if (fields == null) { return Collections.emptyList(); } - return visitSlice(ctx.e); + List children = significantChildren(fields); + List result = new ArrayList<>(); + for (int i = 0; i < children.size(); ) { + boolean optional = false; + if (isToken(children.get(i), QUESTIONMARK)) { + optional = true; + i++; + } + Node field = children.get(i++); + if (i >= children.size() || !isToken(children.get(i), COLON)) { + break; + } + Node colon = children.get(i++); + if (i >= children.size()) { + break; + } + long colonID = helper.id(colon); + Expr value = exprVisit(children.get(i++)); + result.add(helper.newObjectField(colonID, fieldName(field), value, optional)); + if (i < children.size() && isToken(children.get(i), COMMA)) { + i++; + } + } + return result; } - private List visitSlice(List expressions) { - if (expressions == null) { + private List mapEntries(MapInitializerList entries) { + if (entries == null) { return Collections.emptyList(); } - List result = new ArrayList<>(expressions.size()); - for (ExprContext e : expressions) { - Expr ex = exprVisit(e); - result.add(ex); + List children = significantChildren(entries); + List result = new ArrayList<>(); + for (int i = 0; i < children.size(); ) { + boolean optional = false; + if (isToken(children.get(i), QUESTIONMARK)) { + optional = true; + i++; + } + Node keyNode = children.get(i++); + if (i >= children.size() || !isToken(children.get(i), COLON)) { + break; + } + Node colon = children.get(i++); + long colonID = helper.id(colon); + Expr key = exprVisit(keyNode); + if (i >= children.size()) { + break; + } + Expr value = exprVisit(children.get(i++)); + result.add(helper.newMapEntry(colonID, key, value, optional)); + if (i < children.size() && isToken(children.get(i), COMMA)) { + i++; + } } return result; } @@ -680,145 +675,11 @@ String extractQualifiedName(Expr e) { String prefix = extractQualifiedName(s.getOperand()); return prefix + "." + s.getField(); } - // TODO: Add a method to Source to get location from character offset. Location location = helper.getLocation(e.getId()); reportError(location, "expected a qualified name"); return null; } - // Visit a parse tree of field initializers. - List visitIFieldInitializerList(FieldInitializerListContext ctx) { - if (ctx == null || ctx.fields == null) { - // This is the result of a syntax error handled elswhere, return empty. - return Collections.emptyList(); - } - - List result = new ArrayList<>(ctx.fields.size()); - List cols = ctx.cols; - List vals = ctx.values; - for (int i = 0; i < ctx.fields.size(); i++) { - Token f = ctx.fields.get(i); - if (i >= cols.size() || i >= vals.size()) { - // This is the result of a syntax error detected elsewhere. - return Collections.emptyList(); - } - long initID = helper.id(cols.get(i)); - Expr value = exprVisit(vals.get(i)); - Entry field = helper.newObjectField(initID, f.getText(), value); - result.add(field); - } - return result; - } - - private Expr visitIdentOrGlobalCall(IdentOrGlobalCallContext ctx) { - String identName = ""; - if (ctx.leadingDot != null) { - identName = "."; - } - // Handle the error case where no valid identifier is specified. - if (ctx.id == null) { - return helper.newExpr(ctx); - } - // Handle reserved identifiers. - String id = ctx.id.getText(); - if (reservedIds.contains(id)) { - return reportError(ctx, "reserved identifier: %s", id); - } - identName += id; - if (ctx.op != null) { - long opID = helper.id(ctx.op); - return globalCallOrMacro(opID, identName, visitList(ctx.args)); - } - return helper.newIdent(ctx.id, identName); - } - - private Expr visitNested(NestedContext ctx) { - return exprVisit(ctx.e); - } - - private Expr visitSelectOrCall(SelectOrCallContext ctx) { - Expr operand = exprVisit(ctx.member()); - // Handle the error case where no valid identifier is specified. - if (ctx.id == null) { - return helper.newExpr(ctx); - } - String id = ctx.id.getText(); - if (ctx.open != null) { - long opID = helper.id(ctx.open); - return receiverCallOrMacro(opID, id, operand, visitList(ctx.args)); - } - return helper.newSelect(ctx.op, operand, id); - } - - private List visitMapInitializerList(MapInitializerListContext ctx) { - if (ctx == null || ctx.keys.isEmpty()) { - // This is the result of a syntax error handled elswhere, return empty. - return Collections.emptyList(); - } - - List result = new ArrayList<>(ctx.cols.size()); - List keys = ctx.keys; - List vals = ctx.values; - for (int i = 0; i < ctx.cols.size(); i++) { - Token col = ctx.cols.get(i); - long colID = helper.id(col); - if (i >= keys.size() || i >= vals.size()) { - // This is the result of a syntax error detected elsewhere. - return Collections.emptyList(); - } - Expr key = exprVisit(keys.get(i)); - Expr value = exprVisit(vals.get(i)); - Entry entry = helper.newMapEntry(colID, key, value); - result.add(entry); - } - return result; - } - - private Expr visitNegate(NegateContext ctx) { - if (ctx.ops.size() % 2 == 0) { - return exprVisit(ctx.member()); - } - long opID = helper.id(ctx.ops.get(0)); - Expr target = exprVisit(ctx.member()); - return globalCallOrMacro(opID, Operator.Negate.id, target); - } - - private Expr visitIndex(IndexContext ctx) { - Expr target = exprVisit(ctx.member()); - long opID = helper.id(ctx.op); - Expr index = exprVisit(ctx.index); - return globalCallOrMacro(opID, Operator.Index.id, target, index); - } - - private Expr visitUnary(UnaryContext ctx) { - return helper.newLiteralString(ctx, "<>"); - } - - private Expr visitCreateList(CreateListContext ctx) { - long listID = helper.id(ctx.op); - return helper.newList(listID, visitList(ctx.elems)); - } - - private Expr visitCreateMessage(CreateMessageContext ctx) { - Expr target = exprVisit(ctx.member()); - long objID = helper.id(ctx.op); - String messageName = extractQualifiedName(target); - if (messageName != null) { - List entries = visitIFieldInitializerList(ctx.entries); - return helper.newObject(objID, messageName, entries); - } - return helper.newExpr(objID); - } - - private Expr visitCreateStruct(CreateStructContext ctx) { - long structID = helper.id(ctx.op); - if (ctx.entries != null) { - return helper.newMap(structID, visitMapInitializerList(ctx.entries)); - } else { - return helper.newMap(structID, Collections.emptyList()); - } - } - Expr globalCallOrMacro(long exprID, String function, Expr... args) { return globalCallOrMacro(exprID, function, Arrays.asList(args)); } @@ -875,12 +736,111 @@ ByteString unquoteBytes(Object ctx, String value) { String unquoteString(Object ctx, String value) { try { ByteBuffer buf = Unescape.unescape(value, false); - return Unescape.toUtf8(buf); } catch (Exception e) { reportError(ctx, e.toString()); return value; } } + + Expr reportError(Object ctx, String message) { + return reportError(ctx, "%s", message); + } + + Expr reportError(Object ctx, String format, Object... args) { + Location loc = Location.NoLocation; + if (ctx instanceof Location) { + loc = (Location) ctx; + } else if (ctx instanceof Node) { + loc = location((Node) ctx); + } + Expr err = helper.newExpr(ctx); + errors.reportError(loc, format, args); + return err; + } + + private String fieldName(Node node) { + if (node instanceof Field) { + return fieldName(significantChildren(node).get(0)); + } + String text = tokenText(node); + if (text.length() >= 2 && text.charAt(0) == '`' && text.charAt(text.length() - 1) == '`') { + return text.substring(1, text.length() - 1); + } + return text; + } + + private Node firstExpressionChild(List children, Node ctx) { + for (Node child : children) { + if (!isStructuralToken(child)) { + return child; + } + } + return ctx; + } + } + + private static List significantChildren(Node node) { + if (node == null) { + return Collections.emptyList(); + } + List children = new ArrayList<>(); + for (Node child : node.children()) { + if (!isToken(child, EOF)) { + children.add(child); + } + } + return children; + } + + private static boolean isLiteralToken(Node node) { + return isToken(node, NUM_INT) + || isToken(node, NUM_UINT) + || isToken(node, NUM_FLOAT) + || isToken(node, STRING) + || isToken(node, BYTES) + || isToken(node, TRUE) + || isToken(node, FALSE) + || isToken(node, NULL); + } + + private static boolean isStructuralToken(Node node) { + return isToken(node, COMMA) + || isToken(node, COLON) + || isToken(node, LPAREN) + || isToken(node, RPAREN) + || isToken(node, LBRACKET) + || isToken(node, RBRACKET) + || isToken(node, LBRACE) + || isToken(node, RBRACE); + } + + private static boolean isToken(Node node, Token.TokenType type) { + return node instanceof Token && node.getType() == type; + } + + private static String tokenText(Node node) { + return node == null ? "" : node.getSource(); + } + + private static int indexOf(List children, Token.TokenType type) { + for (int i = 0; i < children.size(); i++) { + if (isToken(children.get(i), type)) { + return i; + } + } + return -1; } + + @SuppressWarnings("unchecked") + private static T firstChildOfType(List children, Class type) { + for (Node child : children) { + if (type.isInstance(child)) { + return (T) child; + } + } + return null; + } + + private record ListElements(List expressions, List optionalIndices) {} } diff --git a/core/src/main/java/org/projectnessie/cel/parser/StringCharStream.java b/core/src/main/java/org/projectnessie/cel/parser/StringCharStream.java deleted file mode 100644 index bc29ec06..00000000 --- a/core/src/main/java/org/projectnessie/cel/parser/StringCharStream.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2021 The Authors of CEL-Java - * - * 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 - * - * http://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 org.projectnessie.cel.parser; - -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.CharStream; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.IntStream; -import org.projectnessie.cel.shaded.org.antlr.v4.runtime.misc.Interval; - -public final class StringCharStream implements CharStream { - - private final String buf; - private final String src; - private int pos; - - public StringCharStream(String buf, String src) { - this.buf = buf; - this.src = src; - } - - @Override - public void consume() { - if (pos >= buf.length()) { - throw new RuntimeException("cannot consume EOF"); - } - pos++; - } - - @Override - public int LA(int offset) { - if (offset == 0) { - return 0; - } - if (offset < 0) { - offset++; - } - pos = pos + offset - 1; - if (pos < 0 || pos >= buf.length()) { - return IntStream.EOF; - } - return buf.charAt(pos); - } - - @Override - public int mark() { - return -1; - } - - @Override - public void release(int marker) {} - - @Override - public int index() { - return pos; - } - - @Override - public void seek(int index) { - if (index <= pos) { - pos = index; - return; - } - pos = Math.min(index, buf.length()); - } - - @Override - public int size() { - return buf.length(); - } - - @Override - public String getSourceName() { - return src; - } - - @Override - public String getText(Interval interval) { - int start = interval.a; - int stop = interval.b; - if (stop >= buf.length()) { - stop = buf.length() - 1; - } - if (start >= buf.length()) { - return ""; - } - return buf.substring(start, stop + 1); - } - - @Override - public String toString() { - return buf; - } -} diff --git a/core/src/main/java/org/projectnessie/cel/parser/Unescape.java b/core/src/main/java/org/projectnessie/cel/parser/Unescape.java index dc260f3f..a6a8ef35 100644 --- a/core/src/main/java/org/projectnessie/cel/parser/Unescape.java +++ b/core/src/main/java/org/projectnessie/cel/parser/Unescape.java @@ -90,7 +90,7 @@ public static ByteBuffer unescape(String value, boolean isBytes) { } // Otherwise the string contains escape characters. - ByteBuffer buf = ByteBuffer.allocate(value.length() * 3 / 2); + ByteBuffer buf = ByteBuffer.allocate(value.length() * 4); for (int i = 0; i < n; i++) { char c = value.charAt(i); if (c == '\\') { @@ -215,7 +215,11 @@ public static ByteBuffer unescape(String value, boolean isBytes) { } else { // not an escape sequence if (!isBytes) { - encodeCodePoint(buf, c, cb, enc); + int codePoint = value.codePointAt(i); + encodeCodePoint(buf, codePoint, cb, enc); + if (Character.charCount(codePoint) == 2) { + i++; + } } else { buf.put((byte) c); } diff --git a/core/src/test/java/org/projectnessie/ListContainsTest.java b/core/src/test/java/org/projectnessie/ListContainsTest.java index dfe904e6..b965a76e 100644 --- a/core/src/test/java/org/projectnessie/ListContainsTest.java +++ b/core/src/test/java/org/projectnessie/ListContainsTest.java @@ -17,8 +17,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto; import com.google.api.expr.v1alpha1.UnknownSet; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; @@ -72,11 +72,8 @@ public void dynamicProtobufFieldLookupFailure() throws ScriptException { @Test public void uintInList() throws ScriptException { - TestAllTypesProto.TestAllTypes rule = - TestAllTypesProto.TestAllTypes.newBuilder() - .addRepeatedFixed32(2) - .addRepeatedFixed32(3) - .build(); + TestAllTypes rule = + TestAllTypes.newBuilder().addRepeatedFixed32(2).addRepeatedFixed32(3).build(); ScriptHost scriptHost = ScriptHost.newBuilder().build(); Script script = scriptHost @@ -95,11 +92,8 @@ public void uintInList() throws ScriptException { @Test public void uintNotInList() throws ScriptException { - TestAllTypesProto.TestAllTypes rule = - TestAllTypesProto.TestAllTypes.newBuilder() - .addRepeatedFixed32(2) - .addRepeatedFixed32(3) - .build(); + TestAllTypes rule = + TestAllTypes.newBuilder().addRepeatedFixed32(2).addRepeatedFixed32(3).build(); ScriptHost scriptHost = ScriptHost.newBuilder().build(); Script script = scriptHost diff --git a/core/src/test/java/org/projectnessie/cel/CELTest.java b/core/src/test/java/org/projectnessie/cel/CELTest.java index 15d94cfe..c69017df 100644 --- a/core/src/test/java/org/projectnessie/cel/CELTest.java +++ b/core/src/test/java/org/projectnessie/cel/CELTest.java @@ -126,6 +126,97 @@ void AstToProto() { assertThat(ast3.getExpr()).isEqualTo(astIss2.getAst().getExpr()); } + @Test + void comprehensionLocalVariablesShadowNamespacedIdentifiers() { + Env env = + newEnv( + container("com.example"), + declarations( + Decls.newVar("com.example.y", Decls.Int), + Decls.newVar("y", Decls.String), + Decls.newVar("com.example.y.z", Decls.Int))); + + AstIssuesTuple astIss = env.compile("[0].exists(y, y == 0)"); + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(mapOf("com.example.y", 42L)).getVal()) + .isSameAs(True); + + astIss = env.compile("['compre'].exists(y, .y == 'y')"); + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(mapOf("y", "y")).getVal()).isSameAs(True); + + astIss = env.compile("[{'z': 0}].exists(y, y.z == 0)"); + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(mapOf("com.example.y.z", 42L)).getVal()) + .isSameAs(True); + } + + @Test + void bindMacroIntroducesLocalVariable() { + Env env = + newEnv(container("com.example"), declarations(Decls.newVar("com.example.x", Decls.Int))); + + AstIssuesTuple astIss = env.compile("cel.bind(x, 1, x + 1)"); + assertThat(astIss.hasIssues()).isFalse(); + assertThat( + env.program(astIss.getAst()) + .eval(mapOf("com.example.x", 42L)) + .getVal() + .equal(IntT.intOf(2))) + .isSameAs(True); + + astIss = env.compile("cel.bind(x, {'y': 0}, x.y == 0)"); + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(mapOf("com.example.x.y", 42L)).getVal()) + .isSameAs(True); + } + + @Test + void blockMacrosIntroduceSequentialLocalVariables() { + Env env = newEnv(macros(Macro.TestOnlyBlockMacros)); + + AstIssuesTuple astIss = + env.compile("cel.block([1, cel.index(0) + 1, cel.index(1) + 1], cel.index(2))"); + + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(emptyMap()).getVal().equal(IntT.intOf(3))) + .isSameAs(True); + } + + @Test + void blockMacrosCanNameComprehensionVariables() { + Env env = newEnv(macros(Macro.TestOnlyBlockMacros)); + + AstIssuesTuple astIss = + env.compile( + "[1, 2].map(cel.iterVar(0, 0), " + + "[cel.iterVar(0, 0) + cel.iterVar(0, 0)])[1][0] == 4"); + + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(emptyMap()).getVal()).isSameAs(True); + } + + @Test + void twoVariableComprehensionsBindListIndexAndValue() { + Env env = newEnv(); + + AstIssuesTuple astIss = env.compile("[2, 4, 6].transformList(i, v, i + v)[2] == 8"); + + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(emptyMap()).getVal()).isSameAs(True); + } + + @Test + void twoVariableComprehensionsBindMapKeyAndValue() { + Env env = newEnv(); + + AstIssuesTuple astIss = + env.compile("{'foo': 'bar'}.transformMap(k, v, k + v)['foo'] == 'foobar'"); + + assertThat(astIss.hasIssues()).isFalse(); + assertThat(env.program(astIss.getAst()).eval(emptyMap()).getVal()).isSameAs(True); + } + @Test void AstToString() { Env stdEnv = newEnv(); @@ -284,6 +375,31 @@ void CustomEnvCanSubsetStandardLibrary() { assertThat(prg.eval(mapOf("resource.name", "buckets/example")).getVal()).isSameAs(False); } + @Test + void ProgramUsesCheckedOverloadIdForCustomFunctionDispatch() { + Env env = + newEnv( + declarations( + Decls.newVar("value", Decls.Int), + Decls.newFunction( + "is_even", + Decls.newOverload("is_even_int", singletonList(Decls.Int), Decls.Bool)))); + + AstIssuesTuple astIss = env.compile("is_even(value)"); + assertThat(astIss.hasIssues()).isFalse(); + + Program program = + env.program( + astIss.getAst(), + functions( + Overload.unary( + "is_even_int", + value -> boolOf(((Number) value.value()).longValue() % 2 == 0)))); + + assertThat(program.eval(mapOf("value", 42L)).getVal()).isSameAs(True); + assertThat(program.eval(mapOf("value", 41L)).getVal()).isSameAs(False); + } + @Test void HomogeneousAggregateLiterals() { Env e = @@ -531,15 +647,13 @@ void GlobalVars() { if (args.length != 3) { return newErr("invalid arguments to 'get'"); } - if (!(args[0] instanceof Mapper)) { + if (!(args[0] instanceof Mapper attrs)) { return newErr( "invalid operand of type '%s' to obj.get(key, def)", args[0].type()); } - Mapper attrs = (Mapper) args[0]; - if (!(args[1] instanceof StringT)) { + if (!(args[1] instanceof StringT key)) { return newErr("invalid key of type '%s' to obj.get(key, def)", args[1].type()); } - StringT key = (StringT) args[1]; Val defVal = args[2]; if (attrs.contains(key) == True) { return attrs.get(key); @@ -716,9 +830,7 @@ void EnvExtension() { Env e2 = e.extend( customTypeAdapter(DefaultTypeAdapter.Instance), - types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance())); + types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance())); assertThat(e).isNotEqualTo(e2); assertThat(e.getTypeAdapter()).isNotEqualTo(e2.getTypeAdapter()); assertThat(e.getTypeProvider()).isNotEqualTo(e2.getTypeProvider()); @@ -731,22 +843,18 @@ void EnvExtension() { void EnvExtensionIsolation() { Env baseEnv = newEnv( - container("google.api.expr.test.v1"), + container("cel.expr.conformance"), declarations( Decls.newVar("age", Decls.Int), Decls.newVar("gender", Decls.String), Decls.newVar("country", Decls.String))); Env env1 = baseEnv.extend( - types( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance()), + types(dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()), declarations(Decls.newVar("name", Decls.String))); Env env2 = baseEnv.extend( - types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()), + types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()), declarations(Decls.newVar("group", Decls.String))); AstIssuesTuple astIss = env2.compile("size(group) > 10 && !has(proto3.TestAllTypes{}.single_int32)"); @@ -806,10 +914,9 @@ void CustomInterpreterDecorator() { i -> { lastInstruction.set(i); // Only optimize the instruction if it is a call. - if (!(i instanceof InterpretableCall)) { + if (!(i instanceof InterpretableCall call)) { return i; } - InterpretableCall call = (InterpretableCall) i; // Only optimize the math functions when they have constant arguments. switch (call.function()) { case "_+_": diff --git a/core/src/test/java/org/projectnessie/cel/checker/CheckerEnvTest.java b/core/src/test/java/org/projectnessie/cel/checker/CheckerEnvTest.java index 5ffe755b..e3dea123 100644 --- a/core/src/test/java/org/projectnessie/cel/checker/CheckerEnvTest.java +++ b/core/src/test/java/org/projectnessie/cel/checker/CheckerEnvTest.java @@ -17,9 +17,12 @@ import static java.util.Collections.singletonList; import static org.projectnessie.cel.checker.CheckerEnv.newStandardCheckerEnv; +import static org.projectnessie.cel.common.containers.Container.name; +import static org.projectnessie.cel.common.containers.Container.newContainer; import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newRegistry; import com.google.api.expr.v1alpha1.Type; +import java.util.Arrays; import java.util.List; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -61,4 +64,35 @@ Overloads.ToDyn, singletonList(paramA), Decls.Dyn, typeParamAList)))) .hasMessage( "overlapping overload for name 'dyn' (type '(type_param: \"A\") -> dyn' with overloadId: 'to_dyn' cannot be distinguished from '(type_param: \"A\") -> dyn' with overloadId: 'to_dyn')"); } + + @Test + void lexicalIdentifierShadowsContainerQualifiedIdentifier() { + CheckerEnv env = newStandardCheckerEnv(newContainer(name("com.example")), newRegistry()); + env.add(Decls.newVar("com.example.y", Decls.Int)); + env.add(Decls.newVar("y", Decls.Bool)); + + Assertions.assertThat(env.lookupIdent("y").getName()).isEqualTo("com.example.y"); + + env = env.enterScope(); + env.add(Decls.newVar("y", Decls.String)); + + Assertions.assertThat(env.lookupIdent("y").getName()).isEqualTo("y"); + Assertions.assertThat(env.lookupIdent(".com.example.y").getName()).isEqualTo("com.example.y"); + } + + @Test + void overloadsWithDifferentArityOrStyleDoNotOverlap() { + CheckerEnv env = newStandardCheckerEnv(Container.defaultContainer, newRegistry()); + + env.add( + Decls.newFunction( + "custom", + Decls.newOverload("custom_string", singletonList(Decls.String), Decls.String), + Decls.newOverload( + "custom_string_string", Arrays.asList(Decls.String, Decls.String), Decls.String), + Decls.newInstanceOverload( + "custom_receiver_string", + Arrays.asList(Decls.String, Decls.String), + Decls.String))); + } } diff --git a/core/src/test/java/org/projectnessie/cel/checker/CheckerTest.java b/core/src/test/java/org/projectnessie/cel/checker/CheckerTest.java index 6bc4194c..09df7ee1 100644 --- a/core/src/test/java/org/projectnessie/cel/checker/CheckerTest.java +++ b/core/src/test/java/org/projectnessie/cel/checker/CheckerTest.java @@ -189,6 +189,56 @@ static TestCase[] checkTestCases() { new TestCase().i("id").r("id~double^id").type(Decls.Double).env(testEnvs.get("default")), new TestCase().i("[]").r("[]~list(dyn)").type(Decls.newListType(Decls.Dyn)), new TestCase().i("[1]").r("[1~int]~list(int)").type(Decls.newListType(Decls.Int)), + new TestCase() + .i("[y, 1]") + .env(new env().idents(Decls.newVar("y", Decls.newWrapperType(Decls.Int)))) + .r("[y~wrapper(int)^y, 1~int]~list(wrapper(int))") + .type(Decls.newListType(Decls.newWrapperType(Decls.Int))), + new TestCase() + .i("[1, y]") + .env(new env().idents(Decls.newVar("y", Decls.newWrapperType(Decls.Int)))) + .r("[1~int, y~wrapper(int)^y]~list(wrapper(int))") + .type(Decls.newListType(Decls.newWrapperType(Decls.Int))), + new TestCase() + .i("[x, null]") + .env(new env().idents(Decls.newVar("x", Decls.newObjectType("test.Message")))) + .r("[x~test.Message^x, null~null]~list(test.Message)") + .type(Decls.newListType(Decls.newObjectType("test.Message"))), + new TestCase() + .i("[null, x]") + .env(new env().idents(Decls.newVar("x", Decls.newObjectType("test.Message")))) + .r("[null~null, x~test.Message^x]~list(test.Message)") + .type(Decls.newListType(Decls.newObjectType("test.Message"))), + new TestCase() + .i("[d, null]") + .env(new env().idents(Decls.newVar("d", Decls.Duration))) + .r("[d~duration^d, null~null]~list(duration)") + .type(Decls.newListType(Decls.Duration)), + new TestCase() + .i("[null, t]") + .env(new env().idents(Decls.newVar("t", Decls.Timestamp))) + .r("[null~null, t~timestamp^t]~list(timestamp)") + .type(Decls.newListType(Decls.Timestamp)), + new TestCase() + .i("tuple(1, dyn(2u), 3.0)") + .env( + new env() + .functions( + Decls.newFunction( + "tuple", + Decls.newOverload( + "tuple_T_U_V", + asList( + Decls.newTypeParamType("T"), + Decls.newTypeParamType("U"), + Decls.newTypeParamType("V")), + Decls.newAbstractType( + "tuple", + asList( + Decls.newTypeParamType("T"), + Decls.newTypeParamType("U"), + Decls.newTypeParamType("V"))))))) + .type(Decls.newAbstractType("tuple", asList(Decls.Int, Decls.Dyn, Decls.Double))), new TestCase() .i("[1, \"A\"]") .r("[1~int, \"A\"~string]~list(dyn)") @@ -262,23 +312,27 @@ static TestCase[] checkTestCases() { .r("{1~int : 2u~uint, 2u~uint : 3~int}~map(dyn, dyn)"), new TestCase() .i("TestAllTypes{single_int32: 1, single_int64: 2}") - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .r( - "google.api.expr.test.v1.proto3.TestAllTypes{\n" + "cel.expr.conformance.proto3.TestAllTypes{\n" + " single_int32 : 1~int,\n" + " single_int64 : 2~int\n" - + "}~google.api.expr.test.v1.proto3.TestAllTypes^google.api.expr.test.v1.proto3.TestAllTypes") - .type(Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")), + + "}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes") + .type(Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), + new TestCase() + .i("TestAllTypes{single_bool_wrapper: null}") + .container("cel.expr.conformance.proto3") + .type(Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), new TestCase() .i("TestAllTypes{single_int32: 1u}") - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .error( "ERROR: :1:26: expected type of field 'single_int32' is 'int' but provided type is 'uint'\n" + " | TestAllTypes{single_int32: 1u}\n" + " | .........................^"), new TestCase() .i("TestAllTypes{single_int32: 1, undefined: 2}") - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .error( "ERROR: :1:40: undefined field 'undefined'\n" + " | TestAllTypes{single_int32: 1, undefined: 2}\n" @@ -374,10 +428,9 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", - Decls.newObjectType("google.api.expr.test.v1.proto3.Proto2Message")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.Proto2Message")))) .error( - "ERROR: :1:2: [internal] unexpected failed resolution of 'google.api.expr.test.v1.proto3.Proto2Message'\n" + "ERROR: :1:2: [internal] unexpected failed resolution of 'cel.expr.conformance.proto3.Proto2Message'\n" + " | x.single_int32 != null\n" + " | .^"), new TestCase() @@ -386,14 +439,14 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "_==_(\n" + "\t\t\t_+_(\n" - + "\t\t\t x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_value~dyn,\n" + + "\t\t\t x~cel.expr.conformance.proto3.TestAllTypes^x.single_value~dyn,\n" + "\t\t\t _/_(\n" + "\t\t\t\t1~int,\n" - + "\t\t\t\tx~google.api.expr.test.v1.proto3.TestAllTypes^x.single_struct~map(string, dyn).y~dyn\n" + + "\t\t\t\tx~cel.expr.conformance.proto3.TestAllTypes^x.single_struct~map(string, dyn).y~dyn\n" + "\t\t\t )~int^divide_int64\n" + "\t\t\t)~int^add_int64,\n" + "\t\t\t23~int\n" @@ -405,25 +458,25 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "_+_(\n" + "_[_](\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_value~dyn,\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_value~dyn,\n" + " 23~int\n" + ")~dyn^index_list|index_map,\n" + "_[_](\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_struct~map(string, dyn),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_struct~map(string, dyn),\n" + " \"y\"~string\n" + ")~dyn^index_map\n" + ")~dyn^add_int64|add_uint64|add_double|add_string|add_bytes|add_list|add_timestamp_duration|add_duration_timestamp|add_duration_duration") .type(Decls.Dyn), new TestCase() .i("TestAllTypes.NestedEnum.BAR != 99") - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .r( - "_!=_(google.api.expr.test.v1.proto3.TestAllTypes.NestedEnum.BAR\n" - + " ~int^google.api.expr.test.v1.proto3.TestAllTypes.NestedEnum.BAR,\n" + "_!=_(cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR\n" + + " ~int^cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR,\n" + " 99~int)\n" + "~bool^not_equals") .type(Decls.Bool), @@ -435,8 +488,7 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))), + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))), new TestCase() .i( "x[\"claims\"][\"groups\"][0].name == \"dummy\"\n" @@ -498,10 +550,10 @@ static TestCase[] checkTestCases() { Decls.newVar( "x", Decls.newListType( - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))), + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))), Decls.newVar("y", Decls.newListType(Decls.Int)))) .error( - "ERROR: :1:3: found no matching overload for '_+_' applied to '(list(google.api.expr.test.v1.proto3.TestAllTypes), list(int))'\n" + "ERROR: :1:3: found no matching overload for '_+_' applied to '(list(cel.expr.conformance.proto3.TestAllTypes), list(int))'\n" + " | x + y\n" + " | ..^"), new TestCase() @@ -512,9 +564,9 @@ static TestCase[] checkTestCases() { Decls.newVar( "x", Decls.newListType( - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))))) + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))))) .error( - "ERROR: :1:2: found no matching overload for '_[_]' applied to '(list(google.api.expr.test.v1.proto3.TestAllTypes), uint)'\n" + "ERROR: :1:2: found no matching overload for '_[_]' applied to '(list(cel.expr.conformance.proto3.TestAllTypes), uint)'\n" + " | x[1u]\n" + " | .^"), new TestCase() @@ -525,17 +577,17 @@ static TestCase[] checkTestCases() { Decls.newVar( "x", Decls.newListType( - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))))) + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))))) .r( - "_==_(_[_](_+_(x~list(google.api.expr.test.v1.proto3.TestAllTypes)^x,\n" - + " x~list(google.api.expr.test.v1.proto3.TestAllTypes)^x)\n" - + " ~list(google.api.expr.test.v1.proto3.TestAllTypes)^add_list,\n" + "_==_(_[_](_+_(x~list(cel.expr.conformance.proto3.TestAllTypes)^x,\n" + + " x~list(cel.expr.conformance.proto3.TestAllTypes)^x)\n" + + " ~list(cel.expr.conformance.proto3.TestAllTypes)^add_list,\n" + " 1~int)\n" - + " ~google.api.expr.test.v1.proto3.TestAllTypes^index_list\n" + + " ~cel.expr.conformance.proto3.TestAllTypes^index_list\n" + " .\n" + " single_int32\n" + " ~int,\n" - + " size(x~list(google.api.expr.test.v1.proto3.TestAllTypes)^x)~int^size_list)\n" + + " size(x~list(cel.expr.conformance.proto3.TestAllTypes)^x)~int^size_list)\n" + " ~bool^equals") .type(Decls.Bool), new TestCase() @@ -544,10 +596,10 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( - "_==_(_[_](x~google.api.expr.test.v1.proto3.TestAllTypes^x.repeated_int64~list(int),\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int32~int)\n" + "_==_(_[_](x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_int32~int)\n" + " ~int^index_list,\n" + " 23~int)\n" + " ~bool^equals") @@ -558,10 +610,10 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( - "_==_(size(x~google.api.expr.test.v1.proto3.TestAllTypes^x.map_int64_nested_type\n" - + " ~map(int, google.api.expr.test.v1.proto3.NestedTestAllTypes))\n" + "_==_(size(x~cel.expr.conformance.proto3.TestAllTypes^x.map_int64_nested_type\n" + + " ~map(int, cel.expr.conformance.proto3.NestedTestAllTypes))\n" + " ~int^size_map,\n" + " 0~int)\n" + " ~bool^equals") @@ -603,13 +655,13 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "__comprehension__(\n" + "// Variable\n" + "x,\n" + "// Target\n" - + "x~google.api.expr.test.v1.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + + "x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + "// Accumulator\n" + "__result__,\n" + "// Init\n" @@ -634,13 +686,13 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "__comprehension__(\n" + "// Variable\n" + "x,\n" + "// Target\n" - + "x~google.api.expr.test.v1.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + + "x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + "// Accumulator\n" + "__result__,\n" + "// Init\n" @@ -675,9 +727,9 @@ static TestCase[] checkTestCases() { "x", Decls.newMapType( Decls.String, - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))))) + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))))) .error( - "ERROR: :1:2: found no matching overload for '_[_]' applied to '(map(string, google.api.expr.test.v1.proto3.TestAllTypes), int)'\n" + "ERROR: :1:2: found no matching overload for '_[_]' applied to '(map(string, cel.expr.conformance.proto3.TestAllTypes), int)'\n" + " | x[2].single_int32 == 23\n" + " | .^"), new TestCase() @@ -689,10 +741,10 @@ static TestCase[] checkTestCases() { "x", Decls.newMapType( Decls.String, - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))))) + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))))) .r( - "_==_(_[_](x~map(string, google.api.expr.test.v1.proto3.TestAllTypes)^x, \"a\"~string)\n" - + "~google.api.expr.test.v1.proto3.TestAllTypes^index_map\n" + "_==_(_[_](x~map(string, cel.expr.conformance.proto3.TestAllTypes)^x, \"a\"~string)\n" + + "~cel.expr.conformance.proto3.TestAllTypes^index_map\n" + ".\n" + "single_int32\n" + "~int,\n" @@ -705,15 +757,15 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) // Our implementation code is expanding the macro .r( "_&&_(\n" + " _==_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_nested_message~google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage.bb~int,\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_nested_message~cel.expr.conformance.proto3.TestAllTypes.NestedMessage.bb~int,\n" + " 43~int\n" + " )~bool^equals,\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_nested_message~test-only~~bool\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_nested_message~test-only~~bool\n" + ")~bool^logical_and") .type(Decls.Bool), new TestCase() @@ -723,7 +775,7 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .error( "ERROR: :1:24: undefined field 'undefined'\n" + " | x.single_nested_message.undefined == x.undefined && has(x.single_int32) && has(x.repeated_int32)\n" @@ -737,10 +789,10 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( - "_!=_(x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_nested_message\n" - + "~google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage,\n" + "_!=_(x~cel.expr.conformance.proto3.TestAllTypes^x.single_nested_message\n" + + "~cel.expr.conformance.proto3.TestAllTypes.NestedMessage,\n" + "null~null)\n" + "~bool^not_equals") .type(Decls.Bool), @@ -750,9 +802,9 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( - "_!=_(x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int64~int,null~null)~bool^not_equals") + "_!=_(x~cel.expr.conformance.proto3.TestAllTypes^x.single_int64~int,null~null)~bool^not_equals") .type(Decls.Bool), new TestCase() .i("x.single_int64_wrapper == null") @@ -760,9 +812,9 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( - "_==_(x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int64_wrapper\n" + "_==_(x~cel.expr.conformance.proto3.TestAllTypes^x.single_int64_wrapper\n" + "~wrapper(int),\n" + "null~null)\n" + "~bool^equals") @@ -782,30 +834,30 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "_&&_(\n" + " _&&_(\n" + " _&&_(\n" + " _&&_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_bool_wrapper~wrapper(bool),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_bool_wrapper~wrapper(bool),\n" + " _==_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_bytes_wrapper~wrapper(bytes),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_bytes_wrapper~wrapper(bytes),\n" + " b\"hi\"~bytes\n" + " )~bool^equals\n" + " )~bool^logical_and,\n" + " _!=_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_double_wrapper~wrapper(double),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_double_wrapper~wrapper(double),\n" + " 2.0~double\n" + " )~bool^not_equals\n" + " )~bool^logical_and,\n" + " _&&_(\n" + " _==_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_float_wrapper~wrapper(double),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_float_wrapper~wrapper(double),\n" + " 1.0~double\n" + " )~bool^equals,\n" + " _!=_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int32_wrapper~wrapper(int),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_int32_wrapper~wrapper(int),\n" + " 2~int\n" + " )~bool^not_equals\n" + " )~bool^logical_and\n" @@ -813,21 +865,21 @@ static TestCase[] checkTestCases() { + " _&&_(\n" + " _&&_(\n" + " _==_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int),\n" + " 1~int\n" + " )~bool^equals,\n" + " _==_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_string_wrapper~wrapper(string),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_string_wrapper~wrapper(string),\n" + " \"hi\"~string\n" + " )~bool^equals\n" + " )~bool^logical_and,\n" + " _&&_(\n" + " _==_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_uint32_wrapper~wrapper(uint),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_uint32_wrapper~wrapper(uint),\n" + " 1u~uint\n" + " )~bool^equals,\n" + " _!=_(\n" - + " x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_uint64_wrapper~wrapper(uint),\n" + + " x~cel.expr.conformance.proto3.TestAllTypes^x.single_uint64_wrapper~wrapper(uint),\n" + " 42u~uint\n" + " )~bool^not_equals\n" + " )~bool^logical_and\n" @@ -850,7 +902,7 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .type(Decls.Bool), new TestCase() .i( @@ -866,7 +918,7 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .type(Decls.Bool), new TestCase() .i("x.repeated_int64.exists(y, y > 10) && y < 5") @@ -874,7 +926,7 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .error( "ERROR: :1:39: undeclared reference to 'y' (in container '')\n" + " | x.repeated_int64.exists(y, y > 10) && y < 5\n" @@ -886,7 +938,7 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "_&&_(\n" + "\t\t\t_&&_(\n" @@ -894,7 +946,7 @@ static TestCase[] checkTestCases() { + "\t\t\t\t// Variable\n" + "\t\t\t\te,\n" + "\t\t\t\t// Target\n" - + "\t\t\t\tx~google.api.expr.test.v1.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + + "\t\t\t\tx~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + "\t\t\t\t// Accumulator\n" + "\t\t\t\t__result__,\n" + "\t\t\t\t// Init\n" @@ -917,7 +969,7 @@ static TestCase[] checkTestCases() { + "\t\t\t\t// Variable\n" + "\t\t\t\te,\n" + "\t\t\t\t// Target\n" - + "\t\t\t\tx~google.api.expr.test.v1.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + + "\t\t\t\tx~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + "\t\t\t\t// Accumulator\n" + "\t\t\t\t__result__,\n" + "\t\t\t\t// Init\n" @@ -943,7 +995,7 @@ static TestCase[] checkTestCases() { + "\t\t\t // Variable\n" + "\t\t\t e,\n" + "\t\t\t // Target\n" - + "\t\t\t x~google.api.expr.test.v1.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + + "\t\t\t x~cel.expr.conformance.proto3.TestAllTypes^x.repeated_int64~list(int),\n" + "\t\t\t // Accumulator\n" + "\t\t\t __result__,\n" + "\t\t\t // Init\n" @@ -975,9 +1027,9 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .error( - "ERROR: :1:1: expression of type 'google.api.expr.test.v1.proto3.TestAllTypes' cannot be range of a comprehension (must be list, map, or dynamic)\n" + "ERROR: :1:1: expression of type 'cel.expr.conformance.proto3.TestAllTypes' cannot be range of a comprehension (must be list, map, or dynamic)\n" + " | x.all(e, 0)\n" + " | ^\n" + "ERROR: :1:6: found no matching overload for '_&&_' applied to '(bool, int)'\n" @@ -1016,24 +1068,20 @@ static TestCase[] checkTestCases() { .type(Decls.newListType(Decls.Dyn)) .env(new env().idents(Decls.newVar("lists", Decls.Dyn))), new TestCase() - .i("google.api.expr.test.v1.proto3.TestAllTypes") + .i("cel.expr.conformance.proto3.TestAllTypes") .r( - "google.api.expr.test.v1.proto3.TestAllTypes\n" - + "\t~type(google.api.expr.test.v1.proto3.TestAllTypes)\n" - + "\t^google.api.expr.test.v1.proto3.TestAllTypes") - .type( - Decls.newTypeType( - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))), + "cel.expr.conformance.proto3.TestAllTypes\n" + + "\t~type(cel.expr.conformance.proto3.TestAllTypes)\n" + + "\t^cel.expr.conformance.proto3.TestAllTypes") + .type(Decls.newTypeType(Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))), new TestCase() .i("proto3.TestAllTypes") - .container("google.api.expr.test.v1") + .container("cel.expr.conformance") .r( - "google.api.expr.test.v1.proto3.TestAllTypes\n" - + "\t~type(google.api.expr.test.v1.proto3.TestAllTypes)\n" - + "\t^google.api.expr.test.v1.proto3.TestAllTypes") - .type( - Decls.newTypeType( - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))), + "cel.expr.conformance.proto3.TestAllTypes\n" + + "\t~type(cel.expr.conformance.proto3.TestAllTypes)\n" + + "\t^cel.expr.conformance.proto3.TestAllTypes") + .type(Decls.newTypeType(Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))), new TestCase() .i("1 + x") .error( @@ -1043,9 +1091,9 @@ static TestCase[] checkTestCases() { new TestCase() .i( "x == google.protobuf.Any{\n" - + "\t\t\t\ttype_url:'types.googleapis.com/google.api.expr.test.v1.proto3.TestAllTypes'\n" + + "\t\t\t\ttype_url:'types.googleapis.com/cel.expr.conformance.proto3.TestAllTypes'\n" + "\t\t\t} && x.single_nested_message.bb == 43\n" - + "\t\t\t|| x == google.api.expr.test.v1.proto3.TestAllTypes{}\n" + + "\t\t\t|| x == cel.expr.conformance.proto3.TestAllTypes{}\n" + "\t\t\t|| y < x\n" + "\t\t\t|| x >= x") .env( @@ -1060,7 +1108,7 @@ static TestCase[] checkTestCases() { + "\t\t\t_==_(\n" + "\t\t\t\tx~any^x,\n" + "\t\t\t\tgoogle.protobuf.Any{\n" - + "\t\t\t\t\ttype_url:\"types.googleapis.com/google.api.expr.test.v1.proto3.TestAllTypes\"~string\n" + + "\t\t\t\t\ttype_url:\"types.googleapis.com/cel.expr.conformance.proto3.TestAllTypes\"~string\n" + "\t\t\t\t}~any^google.protobuf.Any\n" + "\t\t\t)~bool^equals,\n" + "\t\t\t_==_(\n" @@ -1070,7 +1118,7 @@ static TestCase[] checkTestCases() { + "\t\t)~bool^logical_and,\n" + "\t\t_==_(\n" + "\t\t\tx~any^x,\n" - + "\t\t\tgoogle.api.expr.test.v1.proto3.TestAllTypes{}~google.api.expr.test.v1.proto3.TestAllTypes^google.api.expr.test.v1.proto3.TestAllTypes\n" + + "\t\t\tcel.expr.conformance.proto3.TestAllTypes{}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes\n" + "\t\t)~bool^equals\n" + "\t)~bool^logical_or,\n" + "\t_||_(\n" @@ -1093,9 +1141,9 @@ static TestCase[] checkTestCases() { .idents( Decls.newVar( "container.x", - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) - .r("container.x~google.api.expr.test.v1.proto3.TestAllTypes^container.x") - .type(Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")), + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) + .r("container.x~cel.expr.conformance.proto3.TestAllTypes^container.x") + .type(Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), new TestCase() .i("list == .type([1]) && map == .type({1:2u})") .r( @@ -1144,15 +1192,14 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .functions( Decls.newFunction( "size", Decls.newOverload( "size_message", singletonList( - Decls.newObjectType( - "google.api.expr.test.v1.proto3.TestAllTypes")), + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), Decls.Int)))) .type(Decls.Bool), new TestCase() @@ -1161,9 +1208,9 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( - "_!=_(_+_(x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int64_wrapper\n" + "_!=_(_+_(x~cel.expr.conformance.proto3.TestAllTypes^x.single_int64_wrapper\n" + "~wrapper(int),\n" + "1~int)\n" + "~int^add_int64,\n" @@ -1176,12 +1223,12 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")), + "x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), Decls.newVar("y", Decls.newObjectType("google.protobuf.Int32Value")))) .r( "_!=_(\n" + "\t_+_(\n" - + "\t x~google.api.expr.test.v1.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int),\n" + + "\t x~cel.expr.conformance.proto3.TestAllTypes^x.single_int64_wrapper~wrapper(int),\n" + "\t y~wrapper(int)^y\n" + "\t)~int^add_int64,\n" + "\t23~int\n" @@ -1384,7 +1431,7 @@ static TestCase[] checkTestCases() { new TestCase() .i("[].map(x, [].map(y, x in y && y in x))") .error( - "ERROR: :1:33: found no matching overload for '@in' applied to '(type_param: \"_var2\", type_param: \"_var0\")'\n" + "ERROR: :1:33: found no matching overload for '@in' applied to '(list(type_param: \"_var0\"), type_param: \"_var0\")'\n" + " | [].map(x, [].map(y, x in y && y in x))\n" + " | ................................^"), new TestCase() @@ -1449,63 +1496,59 @@ static TestCase[] checkTestCases() { new env() .idents( Decls.newVar( - "pb2", - Decls.newObjectType("google.api.expr.test.v1.proto2.TestAllTypes")), + "pb2", Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes")), Decls.newVar( - "pb3", - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")))) + "pb3", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")))) .r( "_&&_(\n" + "\t_&&_(\n" + "\t _&&_(\n" + "\t\t!_(\n" - + "\t\t pb2~google.api.expr.test.v1.proto2.TestAllTypes^pb2.single_int64~test-only~~bool\n" + + "\t\t pb2~cel.expr.conformance.proto2.TestAllTypes^pb2.single_int64~test-only~~bool\n" + "\t\t)~bool^logical_not,\n" + "\t\t!_(\n" - + "\t\t pb2~google.api.expr.test.v1.proto2.TestAllTypes^pb2.repeated_int32~test-only~~bool\n" + + "\t\t pb2~cel.expr.conformance.proto2.TestAllTypes^pb2.repeated_int32~test-only~~bool\n" + "\t\t)~bool^logical_not\n" + "\t )~bool^logical_and,\n" + "\t !_(\n" - + "\t\tpb2~google.api.expr.test.v1.proto2.TestAllTypes^pb2.map_string_string~test-only~~bool\n" + + "\t\tpb2~cel.expr.conformance.proto2.TestAllTypes^pb2.map_string_string~test-only~~bool\n" + "\t )~bool^logical_not\n" + "\t)~bool^logical_and,\n" + "\t_&&_(\n" + "\t _&&_(\n" + "\t\t!_(\n" - + "\t\t pb3~google.api.expr.test.v1.proto3.TestAllTypes^pb3.single_int64~test-only~~bool\n" + + "\t\t pb3~cel.expr.conformance.proto3.TestAllTypes^pb3.single_int64~test-only~~bool\n" + "\t\t)~bool^logical_not,\n" + "\t\t!_(\n" - + "\t\t pb3~google.api.expr.test.v1.proto3.TestAllTypes^pb3.repeated_int32~test-only~~bool\n" + + "\t\t pb3~cel.expr.conformance.proto3.TestAllTypes^pb3.repeated_int32~test-only~~bool\n" + "\t\t)~bool^logical_not\n" + "\t )~bool^logical_and,\n" + "\t !_(\n" - + "\t\tpb3~google.api.expr.test.v1.proto3.TestAllTypes^pb3.map_string_string~test-only~~bool\n" + + "\t\tpb3~cel.expr.conformance.proto3.TestAllTypes^pb3.map_string_string~test-only~~bool\n" + "\t )~bool^logical_not\n" + "\t)~bool^logical_and\n" + " )~bool^logical_and") .type(Decls.Bool), new TestCase() .i("TestAllTypes{}.repeated_nested_message") - .container("google.api.expr.test.v1.proto2") + .container("cel.expr.conformance.proto2") .r( - "google.api.expr.test.v1.proto2.TestAllTypes{}~google.api.expr.test.v1.proto2.TestAllTypes^\n" - + "\t\tgoogle.api.expr.test.v1.proto2.TestAllTypes.repeated_nested_message\n" - + "\t\t~list(google.api.expr.test.v1.proto2.TestAllTypes.NestedMessage)") + "cel.expr.conformance.proto2.TestAllTypes{}~cel.expr.conformance.proto2.TestAllTypes^\n" + + "\t\tcel.expr.conformance.proto2.TestAllTypes.repeated_nested_message\n" + + "\t\t~list(cel.expr.conformance.proto2.TestAllTypes.NestedMessage)") .type( Decls.newListType( - Decls.newObjectType( - "google.api.expr.test.v1.proto2.TestAllTypes.NestedMessage"))), + Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes.NestedMessage"))), new TestCase() .i("TestAllTypes{}.repeated_nested_message") - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .r( - "google.api.expr.test.v1.proto3.TestAllTypes{}~google.api.expr.test.v1.proto3.TestAllTypes^\n" - + "\t\tgoogle.api.expr.test.v1.proto3.TestAllTypes.repeated_nested_message\n" - + "\t\t~list(google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage)") + "cel.expr.conformance.proto3.TestAllTypes{}~cel.expr.conformance.proto3.TestAllTypes^\n" + + "\t\tcel.expr.conformance.proto3.TestAllTypes.repeated_nested_message\n" + + "\t\t~list(cel.expr.conformance.proto3.TestAllTypes.NestedMessage)") .type( Decls.newListType( - Decls.newObjectType( - "google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage"))), + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"))), new TestCase() .i("base64.encode('hello')") .env( @@ -1545,8 +1588,8 @@ void check(TestCase tc) { TypeRegistry reg = ProtoTypeRegistry.newRegistry( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes.getDefaultInstance(), - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.getDefaultInstance()); + dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(), + dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()); Container cont = Container.newContainer(Container.name(tc.container)); CheckerEnv env = newStandardCheckerEnv(cont, reg); if (tc.disableStdEnv) { diff --git a/core/src/test/java/org/projectnessie/cel/common/types/IntTest.java b/core/src/test/java/org/projectnessie/cel/common/types/IntTest.java index 48295aea..b0708c52 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/IntTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/IntTest.java @@ -16,6 +16,7 @@ package org.projectnessie.cel.common.types; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.projectnessie.cel.common.types.BoolT.False; import static org.projectnessie.cel.common.types.BoolT.True; import static org.projectnessie.cel.common.types.DoubleT.DoubleType; @@ -148,6 +149,17 @@ void intConvertToNative_Wrapper() { assertThat(val2).isEqualTo(want2); } + @Test + void intConvertToNative_Int32WrapperRangeError() { + assertThatThrownBy(() -> intOf(1L + Integer.MAX_VALUE).convertToNative(Int32Value.class)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("range error"); + + assertThatThrownBy(() -> intOf(-1L + Integer.MIN_VALUE).convertToNative(Int32Value.class)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("range error"); + } + @Test void intConvertToType() { assertThat(intOf(-4).convertToType(IntType).equal(intOf(-4))).isSameAs(True); diff --git a/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java b/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java index f409c6f7..bd22dedd 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java @@ -16,6 +16,7 @@ package org.projectnessie.cel.common.types; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.projectnessie.cel.common.types.BoolT.True; import static org.projectnessie.cel.common.types.DoubleT.doubleOf; import static org.projectnessie.cel.common.types.IntT.intOf; @@ -26,6 +27,7 @@ import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newRegistry; import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Struct; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Disabled; @@ -99,6 +101,19 @@ void heterogenousKeys() { ImmutableMap.of(1L, "one", 2L, "two", 3.1d, "three", true, "true", "str", "string")); } + @Test + void mapToProtobufStructRequiresStringKeys() { + MapT stringKeyMap = + (MapT) newWrappedMap(newRegistry(), ImmutableMap.of(stringOf("one"), doubleOf(1.0d))); + assertThat(stringKeyMap.convertToNative(Struct.class).getFieldsMap()).containsOnlyKeys("one"); + + MapT intKeyMap = + (MapT) newWrappedMap(newRegistry(), ImmutableMap.of(intOf(1), stringOf("one"))); + assertThatThrownBy(() -> intKeyMap.convertToNative(Struct.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("bad key type"); + } + // type testStruct struct { // M string // Details []string diff --git a/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java b/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java index 059012ff..ad59033f 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java @@ -36,8 +36,6 @@ import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newEmptyRegistry; import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newRegistry; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.GlobalEnum; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; import com.google.api.expr.v1alpha1.CheckedExpr; import com.google.api.expr.v1alpha1.Constant; import com.google.api.expr.v1alpha1.Expr; @@ -55,6 +53,8 @@ import com.google.protobuf.Timestamp; import com.google.protobuf.UInt32Value; import com.google.protobuf.UInt64Value; +import dev.cel.expr.conformance.proto3.GlobalEnum; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; @@ -105,10 +105,10 @@ void typeRegistryEnumValue() { reg.registerDescriptor(GlobalEnum.getDescriptor().getFile()); reg.registerDescriptor(OutOfOrderEnumOuterClass.getDescriptor().getFile()); - Val enumVal = reg.enumValue("google.api.expr.test.v1.proto3.GlobalEnum.GOO"); + Val enumVal = reg.enumValue("cel.expr.conformance.proto3.GlobalEnum.GOO"); assertThat(enumVal).extracting(Val::intValue).isEqualTo((long) GlobalEnum.GOO.getNumber()); - Val enumVal2 = reg.findIdent("google.api.expr.test.v1.proto3.GlobalEnum.GOO"); + Val enumVal2 = reg.findIdent("cel.expr.conformance.proto3.GlobalEnum.GOO"); assertThat(enumVal2.equal(enumVal)).isSameAs(True); // Previously, we checked `getIndex` on the `EnumValueDescriptor`, which is the same as the @@ -140,7 +140,7 @@ void typeRegistryFindType() { ProtoTypeRegistry reg = newEmptyRegistry(); reg.registerDescriptor(GlobalEnum.getDescriptor().getFile()); - String msgTypeName = "google.api.expr.test.v1.proto3.TestAllTypes"; + String msgTypeName = "cel.expr.conformance.proto3.TestAllTypes"; assertThat(reg.findType(msgTypeName)).isNotNull(); // assertThat(reg.findType(msgTypeName + "Undefined")).isNotNull(); ... this doesn't exist in // protobuf-java @@ -198,8 +198,7 @@ void typeRegistryNewValue_WrapperFields() { TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); Val exp = reg.newValue( - "google.api.expr.test.v1.proto3.TestAllTypes", - mapOf("single_int32_wrapper", intOf(123))); + "cel.expr.conformance.proto3.TestAllTypes", mapOf("single_int32_wrapper", intOf(123))); assertThat(exp).matches(v -> !Err.isError(v)); TestAllTypes ce = exp.convertToNative(TestAllTypes.class); assertThat(ce) @@ -208,6 +207,122 @@ void typeRegistryNewValue_WrapperFields() { .isEqualTo(123); } + @Test + void typeRegistryNewValue_NullWrapperFieldIsUnset() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + Val exp = + reg.newValue( + "cel.expr.conformance.proto3.TestAllTypes", mapOf("single_int32_wrapper", NullValue)); + + assertThat(exp).matches(v -> !Err.isError(v)); + TestAllTypes ce = exp.convertToNative(TestAllTypes.class); + assertThat(ce.hasSingleInt32Wrapper()).isFalse(); + } + + @Test + void typeRegistryNewValue_NullMessageFieldsAreUnset() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + Val exp = + reg.newValue( + "cel.expr.conformance.proto3.TestAllTypes", + mapOf( + "single_nested_message", + NullValue, + "single_duration", + NullValue, + "single_timestamp", + NullValue)); + + assertThat(exp).matches(v -> !Err.isError(v)); + TestAllTypes ce = exp.convertToNative(TestAllTypes.class); + assertThat(ce.hasSingleNestedMessage()).isFalse(); + assertThat(ce.hasSingleDuration()).isFalse(); + assertThat(ce.hasSingleTimestamp()).isFalse(); + } + + @Test + void typeRegistryNewValue_RepeatedMessageFieldNullsArePruned() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + Val exp = + reg.newValue( + "cel.expr.conformance.proto3.TestAllTypes", + mapOf( + "repeated_timestamp", + newGenericArrayList( + reg, + new Val[] {timestampOf(Instant.ofEpochSecond(1).atZone(ZoneIdZ)), NullValue}), + "repeated_duration", + newGenericArrayList(reg, new Val[] {durationOf(Duration.ofSeconds(1)), NullValue}), + "repeated_int32_wrapper", + newGenericArrayList(reg, new Val[] {intOf(1), NullValue}))); + + assertThat(exp).matches(v -> !Err.isError(v)); + TestAllTypes ce = exp.convertToNative(TestAllTypes.class); + assertThat(ce.getRepeatedTimestampList()) + .containsExactly(Timestamp.newBuilder().setSeconds(1).build()); + assertThat(ce.getRepeatedDurationList()) + .containsExactly(com.google.protobuf.Duration.newBuilder().setSeconds(1).build()); + assertThat(ce.getRepeatedInt32WrapperList()).containsExactly(Int32Value.of(1)); + } + + @Test + void typeRegistryNewValue_MapMessageFieldNullsArePruned() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + Val exp = + reg.newValue( + "cel.expr.conformance.proto3.TestAllTypes", + mapOf( + "map_bool_timestamp", + newMaybeWrappedMap( + reg, + mapOf( + true, + NullValue, + false, + timestampOf(Instant.ofEpochSecond(1).atZone(ZoneIdZ)))), + "map_bool_duration", + newMaybeWrappedMap( + reg, mapOf(true, NullValue, false, durationOf(Duration.ofSeconds(1)))), + "map_bool_int32_wrapper", + newMaybeWrappedMap(reg, mapOf(true, NullValue, false, intOf(1))))); + + assertThat(exp).matches(v -> !Err.isError(v)); + TestAllTypes ce = exp.convertToNative(TestAllTypes.class); + assertThat(ce.getMapBoolTimestampMap()) + .containsExactlyEntriesOf(mapOf(false, Timestamp.newBuilder().setSeconds(1).build())); + assertThat(ce.getMapBoolDurationMap()) + .containsExactlyEntriesOf( + mapOf(false, com.google.protobuf.Duration.newBuilder().setSeconds(1).build())); + assertThat(ce.getMapBoolInt32WrapperMap()) + .containsExactlyEntriesOf(mapOf(false, Int32Value.of(1))); + } + + @Test + void typeRegistryNewValue_InvalidNullFieldAssignmentsReturnErrors() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + String typeName = "cel.expr.conformance.proto3.TestAllTypes"; + + assertThat(reg.newValue(typeName, mapOf("single_bool", NullValue))).matches(Err::isError); + assertThat(reg.newValue(typeName, mapOf("repeated_int32", NullValue))).matches(Err::isError); + assertThat(reg.newValue(typeName, mapOf("map_string_string", NullValue))).matches(Err::isError); + assertThat(reg.newValue(typeName, mapOf("list_value", NullValue))).matches(Err::isError); + assertThat(reg.newValue(typeName, mapOf("single_struct", NullValue))).matches(Err::isError); + } + + @Test + void typeRegistryNewValue_ProtobufStructFieldRequiresStringKeys() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + String typeName = "cel.expr.conformance.proto3.TestAllTypes"; + + Val value = + reg.newValue( + typeName, + mapOf("single_struct", newMaybeWrappedMap(reg, mapOf(intOf(1), stringOf("one"))))); + + assertThat(value).matches(Err::isError); + assertThat(value.toString()).contains("invalid value for field 'single_struct': bad key type"); + } + @Test void typeRegistryGetters() { TypeRegistry reg = newRegistry(ParsedExpr.getDefaultInstance()); diff --git a/core/src/test/java/org/projectnessie/cel/common/types/StringTest.java b/core/src/test/java/org/projectnessie/cel/common/types/StringTest.java index 6ef673fa..5a203def 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/StringTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/StringTest.java @@ -122,7 +122,24 @@ void stringConvertToNative_Wrapper() { @Test void stringConvertToType() { assertThat(stringOf("-1").convertToType(IntType).equal(IntNegOne)).isSameAs(True); + assertThat(stringOf("1").convertToType(BoolType).equal(True)).isSameAs(True); + assertThat(stringOf("t").convertToType(BoolType).equal(True)).isSameAs(True); + assertThat(stringOf("true").convertToType(BoolType).equal(True)).isSameAs(True); + assertThat(stringOf("TRUE").convertToType(BoolType).equal(True)).isSameAs(True); + assertThat(stringOf("True").convertToType(BoolType).equal(True)).isSameAs(True); + assertThat(stringOf("0").convertToType(BoolType).equal(False)).isSameAs(True); + assertThat(stringOf("f").convertToType(BoolType).equal(False)).isSameAs(True); assertThat(stringOf("false").convertToType(BoolType).equal(False)).isSameAs(True); + assertThat(stringOf("FALSE").convertToType(BoolType).equal(False)).isSameAs(True); + assertThat(stringOf("False").convertToType(BoolType).equal(False)).isSameAs(True); + assertThat(stringOf("TrUe").convertToType(BoolType)) + .isInstanceOf(Err.class) + .extracting(Object::toString) + .isEqualTo("type conversion error from 'string' to 'bool'"); + assertThat(stringOf("FaLsE").convertToType(BoolType)) + .isInstanceOf(Err.class) + .extracting(Object::toString) + .isEqualTo("type conversion error from 'string' to 'bool'"); assertThat(stringOf("1").convertToType(UintType).equal(uintOf(1))).isSameAs(True); assertThat(stringOf("2.5").convertToType(DoubleType).equal(doubleOf(2.5))).isSameAs(True); assertThat( diff --git a/core/src/test/java/org/projectnessie/cel/common/types/TimestampTest.java b/core/src/test/java/org/projectnessie/cel/common/types/TimestampTest.java index e80ba683..3a1b8a1a 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/TimestampTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/TimestampTest.java @@ -34,7 +34,6 @@ import static org.projectnessie.cel.common.types.TypeT.TypeType; import com.google.protobuf.Any; -import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; import com.google.protobuf.Value; import java.time.DateTimeException; @@ -107,7 +106,12 @@ void timestampConvertToNative_JSON() { // JSON Object val = ts.convertToNative(Value.class); - Object want = StringValue.of("1970-01-01T02:05:06Z"); + Object want = Value.newBuilder().setStringValue("1970-01-01T02:05:06Z").build(); + assertThat(val).isEqualTo(want); + + ts = timestampOf(Instant.ofEpochSecond(253402300799L, 999999999).atZone(ZoneIdZ)); + val = ts.convertToNative(Value.class); + want = Value.newBuilder().setStringValue("9999-12-31T23:59:59.999999999Z").build(); assertThat(val).isEqualTo(want); } diff --git a/core/src/test/java/org/projectnessie/cel/common/types/TypeTest.java b/core/src/test/java/org/projectnessie/cel/common/types/TypeTest.java index ae658729..e08ca174 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/TypeTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/TypeTest.java @@ -62,4 +62,17 @@ void typeConvertToType() { void typeType() { assertThat(TypeType.type()).isSameAs(TypeType); } + + @Test + void objectTypeEqualsBuiltInTypeWithSameName() { + assertThat(TypeT.newObjectTypeValue("google.protobuf.Timestamp").equal(TimestampType)) + .isSameAs(BoolT.True); + assertThat(TimestampType.equal(TypeT.newObjectTypeValue("google.protobuf.Timestamp"))) + .isSameAs(BoolT.True); + + assertThat(TypeT.newObjectTypeValue("google.protobuf.Duration").equal(DurationType)) + .isSameAs(BoolT.True); + assertThat(DurationType.equal(TypeT.newObjectTypeValue("google.protobuf.Duration"))) + .isSameAs(BoolT.True); + } } diff --git a/core/src/test/java/org/projectnessie/cel/common/types/UintTest.java b/core/src/test/java/org/projectnessie/cel/common/types/UintTest.java index 61e9ce56..5972ded4 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/UintTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/UintTest.java @@ -109,9 +109,12 @@ void uintConvertToNative_Json() { Value val = uintOf(maxIntJSON).convertToNative(Value.class); assertThat(val).isEqualTo(Value.newBuilder().setNumberValue(9007199254740991.0d).build()); - // Value converts to a JSON decimal string - val = intOf(maxIntJSON + 1).convertToNative(Value.class); + // Value converts to a JSON decimal string. + val = uintOf(maxIntJSON + 1).convertToNative(Value.class); assertThat(val).isEqualTo(Value.newBuilder().setStringValue("9007199254740992").build()); + + val = uintOf(-1L).convertToNative(Value.class); + assertThat(val).isEqualTo(Value.newBuilder().setStringValue("18446744073709551615").build()); } @Test @@ -142,6 +145,17 @@ void uintConvertToNative_Wrapper() { assertThat(val2).isEqualTo(want2); } + @Test + void uintConvertToNative_UInt32WrapperRangeError() { + assertThatThrownBy(() -> uintOf(0x100000000L).convertToNative(UInt32Value.class)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("range error"); + + assertThatThrownBy(() -> uintOf(-1L).convertToNative(UInt32Value.class)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("range error"); + } + @Test void uintConvertToType() { // 18446744073709551612L diff --git a/core/src/test/java/org/projectnessie/cel/common/types/pb/FieldDescriptionTest.java b/core/src/test/java/org/projectnessie/cel/common/types/pb/FieldDescriptionTest.java index 4f16f048..15ebd93d 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/pb/FieldDescriptionTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/pb/FieldDescriptionTest.java @@ -16,12 +16,12 @@ package org.projectnessie.cel.common.types.pb; import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; import static org.projectnessie.cel.common.types.pb.Db.newDb; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.NestedTestAllTypes; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedEnum; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedMessage; import com.google.api.expr.v1alpha1.Type; import com.google.protobuf.BoolValue; import com.google.protobuf.Duration; @@ -32,6 +32,10 @@ import com.google.protobuf.Struct; import com.google.protobuf.Timestamp; import com.google.protobuf.Value; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import java.time.Instant; import java.util.Arrays; import java.util.Collections; @@ -40,6 +44,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.types.TimestampT; +import org.projectnessie.cel.common.types.traits.Mapper; public class FieldDescriptionTest { @@ -64,7 +69,7 @@ void fieldDescription() { // matches the one determined by the TypeDescription utils. Type got = fd.checkedType(); Type wanted = - Type.newBuilder().setMessageType("google.api.expr.test.v1.proto3.TestAllTypes").build(); + Type.newBuilder().setMessageType("cel.expr.conformance.proto3.TestAllTypes").build(); assertThat(got).isEqualTo(wanted); } @@ -178,6 +183,32 @@ void getFromUnsignedMapEntries() { .isEqualTo(Collections.singletonMap(ULong.valueOf(-1L), ULong.valueOf(Long.MIN_VALUE))); } + @Test + void getFieldProtoMapSupportsRepeatedLookup() { + Db pbdb = newDb(); + ProtoTypeRegistry registry = ProtoTypeRegistry.newRegistry(TestAllTypes.getDefaultInstance()); + TestAllTypes msg = + TestAllTypes.newBuilder() + .putMapUint64String(1L, "one") + .putMapUint64String(2L, "two") + .putMapUint64String(-1L, "large") + .build(); + pbdb.registerMessage(msg); + PbTypeDescription td = pbdb.describeType(msg.getDescriptorForType().getFullName()); + assertThat(td).isNotNull(); + + FieldDescription field = td.fieldByName("map_uint64_string"); + assertThat(field).isNotNull(); + + Mapper map = (Mapper) field.getField(msg, registry); + assertThat(map.find(uintOf(2)).equal(stringOf("two"))).isSameAs(True); + assertThat(map.find(uintOf(-1L)).equal(stringOf("large"))).isSameAs(True); + assertThat(map.find(uintOf(1)).equal(stringOf("one"))).isSameAs(True); + assertThat(map.find(uintOf(42))).isNull(); + assertThat(map.contains(uintOf(2))).isSameAs(True); + assertThat(map.contains(uintOf(42))).isSameAs(False); + } + static class TestCase { Message msg; String field; diff --git a/core/src/test/java/org/projectnessie/cel/common/types/pb/PbObjectTest.java b/core/src/test/java/org/projectnessie/cel/common/types/pb/PbObjectTest.java index 55d30073..01608869 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/pb/PbObjectTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/pb/PbObjectTest.java @@ -28,7 +28,12 @@ import com.google.api.expr.v1alpha1.ParsedExpr; import com.google.api.expr.v1alpha1.SourceInfo; import com.google.protobuf.Any; +import com.google.protobuf.Empty; +import com.google.protobuf.FieldMask; import com.google.protobuf.Message; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; +import com.google.protobuf.Value; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.Disabled; @@ -90,6 +95,26 @@ void protoObjectConvertToNative() throws Exception { assertThat(unpackedAny).isEqualTo(objVal.value()); } + @Test + void wellKnownProtoObjectsConvertToJsonValue() { + TypeRegistry reg = newRegistry(Empty.getDefaultInstance(), FieldMask.getDefaultInstance()); + + Val empty = reg.nativeToValue(Empty.getDefaultInstance()); + assertThat(empty.convertToNative(Value.class)) + .isEqualTo(Value.newBuilder().setStructValue(Struct.getDefaultInstance()).build()); + + Val fieldMask = + reg.nativeToValue(FieldMask.newBuilder().addPaths("foo").addPaths("bar_baz").build()); + assertThat(fieldMask.convertToNative(Value.class)) + .isEqualTo(Value.newBuilder().setStringValue("foo,barBaz").build()); + + Val timestamp = + reg.nativeToValue( + Timestamp.newBuilder().setSeconds(253402300799L).setNanos(999999999).build()); + assertThat(timestamp.convertToNative(Value.class)) + .isEqualTo(Value.newBuilder().setStringValue("9999-12-31T23:59:59.999999999Z").build()); + } + @Test @Disabled("IMPLEMENT ME") void protoObjectConvertToNative_JSON() { diff --git a/core/src/test/java/org/projectnessie/cel/common/types/pb/PbTypeDescriptionTest.java b/core/src/test/java/org/projectnessie/cel/common/types/pb/PbTypeDescriptionTest.java index 857f3888..cb1ec6ab 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/pb/PbTypeDescriptionTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/pb/PbTypeDescriptionTest.java @@ -19,8 +19,6 @@ import static org.projectnessie.cel.Util.mapOf; import static org.projectnessie.cel.common.types.pb.Db.newDb; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.NestedTestAllTypes; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; import com.google.api.expr.v1alpha1.Type; import com.google.protobuf.Any; import com.google.protobuf.BoolValue; @@ -41,6 +39,8 @@ import com.google.protobuf.UInt32Value; import com.google.protobuf.UInt64Value; import com.google.protobuf.Value; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.time.Instant; import java.util.Arrays; import java.util.Map; @@ -232,7 +232,7 @@ void checkedType() { assertThat(field).isNotNull(); Type listType = Decls.newListType( - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage")); + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage")); assertThat(field.checkedType()).isEqualTo(listType); } diff --git a/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapContext.java b/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapContext.java index 1fee4f06..d04a4d9e 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapContext.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapContext.java @@ -18,7 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.projectnessie.cel.common.types.pb.Db.newDb; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; /** Required by {@link UnwrapTestCase} et al. */ class UnwrapContext { diff --git a/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapTestCase.java b/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapTestCase.java index ae6c1ad1..e88003f4 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapTestCase.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/pb/UnwrapTestCase.java @@ -15,7 +15,6 @@ */ package org.projectnessie.cel.common.types.pb; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; import com.google.protobuf.BytesValue; @@ -33,6 +32,7 @@ import com.google.protobuf.UInt32Value; import com.google.protobuf.UInt64Value; import com.google.protobuf.Value; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.util.function.Supplier; /** diff --git a/core/src/test/java/org/projectnessie/cel/extension/EncodersLibTest.java b/core/src/test/java/org/projectnessie/cel/extension/EncodersLibTest.java new file mode 100644 index 00000000..b2577568 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/extension/EncodersLibTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.extension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.common.types.BytesT.bytesOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.extension.EncodersLib.encoders; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.Program.EvalResult; +import org.projectnessie.cel.common.types.Err; + +class EncodersLibTest { + + @Test + void encodesBase64Bytes() { + assertEvaluates("base64.encode(b'hello')", stringOf("aGVsbG8=")); + } + + @Test + void decodesBase64String() { + assertEvaluates("base64.decode('aGVsbG8=')", bytesOf("hello")); + } + + @Test + void decodesBase64StringWithoutPadding() { + assertEvaluates("base64.decode('aGVsbG8')", bytesOf("hello")); + } + + @Test + void rejectsInvalidBase64String() { + EvalResult result = evaluate("base64.decode('not valid base64')"); + + assertThat(result.getVal()).isInstanceOf(Err.class); + assertThat(result.getVal().toString()).contains("invalid base64 string"); + } + + private static void assertEvaluates(String expression, Object expectedValue) { + assertThat(evaluate(expression).getVal()).isEqualTo(expectedValue); + } + + private static EvalResult evaluate(String expression) { + Env env = newEnv(encoders()); + Env.AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed.hasIssues()).isFalse(); + + Env.AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked.hasIssues()).isFalse(); + + Program program = env.program(checked.getAst()); + return program.eval(Map.of()); + } +} diff --git a/core/src/test/java/org/projectnessie/cel/extension/MathLibTest.java b/core/src/test/java/org/projectnessie/cel/extension/MathLibTest.java new file mode 100644 index 00000000..205a1d4c --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/extension/MathLibTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.extension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.extension.MathLib.math; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.Program.EvalResult; +import org.projectnessie.cel.common.types.Err; + +class MathLibTest { + + @Test + void findsGreatestScalar() { + assertEvaluates("math.greatest(5.4, 10, 3u, -5.0, 9223372036854775807)", intOf(Long.MAX_VALUE)); + } + + @Test + void findsLeastListElement() { + assertEvaluates("math.least([5.4, 10u, 3u, 1u, 3.5])", uintOf(1)); + } + + @Test + void roundsHalfAwayFromZero() { + assertEvaluates("math.round(-1.5)", doubleOf(-2.0)); + } + + @Test + void shiftsSignedIntsLogicallyRight() { + assertEvaluates("math.bitShiftRight(-1024, 3)", intOf(2305843009213693824L)); + } + + @Test + void rejectsNegativeShiftOffset() { + EvalResult result = evaluate("math.bitShiftLeft(1u, -1)"); + + assertThat(result.getVal()).isInstanceOf(Err.class); + assertThat(result.getVal().toString()).contains("negative offset"); + } + + private static void assertEvaluates(String expression, Object expectedValue) { + assertThat(evaluate(expression).getVal()).isEqualTo(expectedValue); + } + + private static EvalResult evaluate(String expression) { + Env env = newEnv(math()); + Env.AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed.hasIssues()).isFalse(); + + Env.AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked.hasIssues()).isFalse(); + + Program program = env.program(checked.getAst()); + return program.eval(Map.of()); + } +} diff --git a/core/src/test/java/org/projectnessie/cel/extension/NetworkLibTest.java b/core/src/test/java/org/projectnessie/cel/extension/NetworkLibTest.java new file mode 100644 index 00000000..d20f9430 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/extension/NetworkLibTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.extension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.extension.NetworkLib.network; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.Program.EvalResult; +import org.projectnessie.cel.common.types.Err; + +class NetworkLibTest { + + @Test + void canonicalizesIpStrings() { + assertEvaluates("string(ip('2001:db8::68'))", stringOf("2001:db8::68")); + } + + @Test + void checksIpProperties() { + assertEvaluates("ip('192.168.0.1').family()", intOf(4)); + assertEvaluates("ip('fe80::1').isLinkLocalUnicast()", True); + } + + @Test + void checksCidrContainment() { + assertEvaluates("cidr('192.168.0.0/24').containsIP('192.168.0.1')", True); + assertEvaluates("cidr('192.168.0.0/24').containsCIDR('192.168.0.0/23')", False); + } + + @Test + void rejectsInvalidIpLiterals() { + EvalResult result = evaluate("ip('192.168.0.1.0')"); + + assertThat(result.getVal()).isInstanceOf(Err.class); + assertThat(result.getVal().toString()).contains("parse error"); + } + + private static void assertEvaluates(String expression, Object expectedValue) { + assertThat(evaluate(expression).getVal()).isEqualTo(expectedValue); + } + + private static EvalResult evaluate(String expression) { + Env env = newEnv(network()); + Env.AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed.hasIssues()).isFalse(); + + Env.AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked.hasIssues()).isFalse(); + + Program program = env.program(checked.getAst()); + return program.eval(Map.of()); + } +} diff --git a/core/src/test/java/org/projectnessie/cel/extension/OptionalLibTest.java b/core/src/test/java/org/projectnessie/cel/extension/OptionalLibTest.java new file mode 100644 index 00000000..2b651b90 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/extension/OptionalLibTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.extension; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.NullT.NullValue; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.extension.OptionalLib.optionals; + +import com.google.api.expr.v1alpha1.Type; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.Err; + +class OptionalLibTest { + + @Test + void declaresOptionalNone() { + assertCheckedType("[optional.none(), optional.of(1)]", Decls.newListType(optional(Decls.Int))); + } + + @Test + void promotesOptionalTypeParameterToDyn() { + assertCheckedType( + "[optional.of(1), optional.of(dyn(1))]", Decls.newListType(optional(Decls.Dyn))); + } + + @Test + void promotesTernaryOptionalTypeParameterToDyn() { + assertCheckedType("true ? optional.of(dyn(1)) : optional.of(1)", optional(Decls.Dyn)); + } + + @Test + void keepsNullableOptionalType() { + assertCheckedType("[optional.of(1), null][0]", optional(Decls.Int)); + } + + @Test + void evaluatesPresentNull() { + assertEvaluates("optional.of(null).hasValue()", True); + assertEvaluates("optional.of(null).value()", NullValue); + } + + @Test + void evaluatesAbsentForNullZeroAndEmptyValues() { + assertEvaluates("optional.ofNonZeroValue(null).hasValue()", False); + assertEvaluates("optional.ofNonZeroValue(false).hasValue()", False); + assertEvaluates("optional.ofNonZeroValue(0).hasValue()", False); + assertEvaluates("optional.ofNonZeroValue(0u).hasValue()", False); + assertEvaluates("optional.ofNonZeroValue(0.0).hasValue()", False); + assertEvaluates("optional.ofNonZeroValue('').hasValue()", False); + assertEvaluates("optional.ofNonZeroValue([]).hasValue()", False); + assertEvaluates("optional.ofNonZeroValue({}).hasValue()", False); + } + + @Test + void evaluatesPresentForNonZeroValues() { + assertEvaluates("optional.ofNonZeroValue(true).value()", True); + assertEvaluates("optional.ofNonZeroValue(42).value()", intOf(42)); + assertEvaluates("optional.ofNonZeroValue('x').hasValue()", True); + } + + @Test + void evaluatesOrAndOrValue() { + assertEvaluates("optional.none().or(optional.none()).orValue(42)", intOf(42)); + assertEvaluates("optional.none().or(optional.of(21)).orValue(42)", intOf(21)); + assertEvaluates("optional.of(7).or(optional.of(21)).orValue(42)", intOf(7)); + } + + @Test + void evaluatesOptionalEquality() { + assertEvaluates("optional.none() == optional.none()", True); + assertEvaluates("optional.none() == optional.of(1)", False); + assertEvaluates("optional.of(1) == optional.none()", False); + assertEvaluates("optional.of(1) == optional.of(1)", True); + assertEvaluates("optional.none() != optional.none()", False); + assertEvaluates("optional.none() != optional.of(1)", True); + assertEvaluates("optional.of(1) != optional.none()", True); + assertEvaluates("optional.of(1) != optional.of(1)", False); + } + + @Test + void evaluatesOptionalTypeIdentifier() { + assertEvaluates("type(optional.none()) == optional_type", True); + } + + @Test + void evaluatesOptMap() { + assertEvaluates("optional.of(1).optMap(x, x + 1).value()", intOf(2)); + assertEvaluates("optional.ofNonZeroValue(0).optMap(x, x / 0).hasValue()", False); + } + + @Test + void evaluatesOptFlatMap() { + assertEvaluates("optional.of(1).optFlatMap(x, optional.of(x + 1)).value()", intOf(2)); + assertEvaluates( + "optional.ofNonZeroValue(0).optFlatMap(x, optional.of(x / 0)).hasValue()", False); + } + + @Test + void evaluatesOptionalSelectAndIndex() { + assertEvaluates("{}.?c.hasValue()", False); + assertEvaluates("{'c': 'x'}.?c.value()", stringOf("x")); + assertEvaluates("[][?0].hasValue()", False); + assertEvaluates("['foo'][?0].value()", stringOf("foo")); + } + + @Test + void evaluatesOptionalChaining() { + assertEvaluates( + "optional.of({'c': {}}).c.missing.or(optional.of(['list-value'])[0]).orValue('default value')", + stringOf("list-value")); + assertEvaluates( + "has(optional.of({'c': {'entry': 'hello world'}}).c)" + + " && !has(optional.of({'c': {'entry': 'hello world'}}).c.missing)", + True); + } + + @Test + void evaluatesOptionalAggregateEntries() { + assertEvaluates("[?{}.?c, ?optional.of(42), ?optional.none()].size()", intOf(1)); + assertEvaluates("{?'foo': optional.none()}.size()", intOf(0)); + } + + @Test + void absentValueReturnsError() { + assertThat(evaluate("optional.none().value()").getVal()).isInstanceOf(Err.class); + } + + private static void assertCheckedType(String expression, Type expectedType) { + Env env = newEnv(optionals()); + Env.AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed.hasIssues()).isFalse(); + + Env.AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked.hasIssues()).isFalse(); + assertThat(checked.getAst().getResultType()).isEqualTo(expectedType); + } + + private static void assertEvaluates(String expression, Object expectedValue) { + assertThat(evaluate(expression).getVal()).describedAs(expression).isEqualTo(expectedValue); + } + + private static Program.EvalResult evaluate(String expression) { + Env env = newEnv(optionals()); + Env.AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed.hasIssues()).isFalse(); + + Env.AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked.hasIssues()).describedAs(checked.getIssues().toString()).isFalse(); + + Program program = env.program(checked.getAst()); + return program.eval(Map.of()); + } + + private static Type optional(Type type) { + return Decls.newAbstractType("optional_type", singletonList(type)); + } +} diff --git a/core/src/test/java/org/projectnessie/cel/extension/StringsTest.java b/core/src/test/java/org/projectnessie/cel/extension/StringsTest.java index d252e2f5..58a38279 100644 --- a/core/src/test/java/org/projectnessie/cel/extension/StringsTest.java +++ b/core/src/test/java/org/projectnessie/cel/extension/StringsTest.java @@ -111,6 +111,12 @@ static Stream testCases() { new TestData("[].join('-') == ''"), new TestData("['x'].join() == 'x'"), new TestData("['x'].join('-') == 'x'"), + new TestData("strings.quote(\"first\\nsecond\") == \"\\\"first\\\\nsecond\\\"\""), + new TestData("strings.quote(\"printable unicode😀\") == \"\\\"printable unicode😀\\\"\""), + new TestData("'Ta©oCαt'.reverse() == 'tαCo©aT'"), + new TestData("\"%d %s %.0f\".format([1, \"two\", 2.5]) == \"1 two 2\""), + new TestData("\"%x\".format([\"Hello world!\"]) == \"48656c6c6f20776f726c6421\""), + new TestData("\"%s\".format([[\"abc\", 3.14, null]]) == \"[abc, 3.14, null]\""), // Error test cases based on checked expression usage. new TestData("'tacocat'.indexOf('a', 30) == -1", "String index out of range: 30"), @@ -175,7 +181,10 @@ static Stream testCases() { new TestData("'hello'.substring(1, 2, 3) == \"\"", "no matching overload", true), new TestData("30.substring(true, 3) == \"\"", "no matching overload", true), new TestData("\"tacocat\".substring(true, 3) == \"\"", "no matching overload", true), - new TestData("\"tacocat\".substring(0, false) == \"\"", "no matching overload", true)); + new TestData("\"tacocat\".substring(0, false) == \"\"", "no matching overload", true), + new TestData( + "\"%a\".format([1])", + "could not parse formatting clause: unrecognized formatting clause \"a\"")); } @Test diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/ActivationTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/ActivationTest.java index 7a260e24..75c0a055 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/ActivationTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/ActivationTest.java @@ -53,6 +53,16 @@ void resolve() { assertThat(activation.resolveName("a").value()).isSameAs(True); } + @Test + void resolveNullAndAbsentFromMapActivation() { + Map map = new HashMap<>(); + map.put("nullValue", null); + Activation activation = newActivation(map); + + assertThat(activation.resolveName("nullValue")).isSameAs(ResolvedValue.NULL_VALUE); + assertThat(activation.resolveName("absent")).isSameAs(ResolvedValue.ABSENT); + } + @Test void resolveLazy() { AtomicReference v = new AtomicReference<>(); diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/AttributesTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/AttributesTest.java index 367dc8e5..67122412 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/AttributesTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/AttributesTest.java @@ -42,11 +42,11 @@ import static org.projectnessie.cel.interpreter.Interpreter.optimize; import static org.projectnessie.cel.interpreter.Interpreter.trackState; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedMessage; import com.google.api.expr.v1alpha1.Decl; import com.google.api.expr.v1alpha1.Type; import com.google.protobuf.Any; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -378,9 +378,9 @@ void benchmarkResolverFieldQualifier() { AttributeFactory attrs = newAttributeFactory(Container.defaultContainer, reg, reg); Activation vars = newActivation(mapOf("msg", msg)); NamespacedAttribute attr = attrs.absoluteAttribute(1, "msg"); - Type opType = reg.findType("google.api.expr.test.v1.proto3.TestAllTypes"); + Type opType = reg.findType("cel.expr.conformance.proto3.TestAllTypes"); assertThat(opType).isNotNull(); - Type fieldType = reg.findType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage"); + Type fieldType = reg.findType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); assertThat(fieldType).isNotNull(); attr.addQualifier(makeQualifier(attrs, opType.getType(), 2, "single_nested_message")); attr.addQualifier(makeQualifier(attrs, fieldType.getType(), 3, "bb")); @@ -398,7 +398,7 @@ void resolverCustomQualifier() { Qualifier qualBB = attrs.newQualifier( Type.newBuilder() - .setMessageType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage") + .setMessageType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage") .build(), 2, "bb"); @@ -421,7 +421,7 @@ void attributesMissingMsg() { attr.addQualifier(field); assertThatThrownBy(() -> attr.resolve(vars)) .isInstanceOf(IllegalStateException.class) - .hasMessage("unknown type 'google.api.expr.test.v1.proto3.TestAllTypes'"); + .hasMessage("unknown type 'cel.expr.conformance.proto3.TestAllTypes'"); } @Test @@ -613,7 +613,7 @@ void benchmarkResolverCustomQualifier() { Qualifier qualBB = attrs.newQualifier( Type.newBuilder() - .setMessageType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage") + .setMessageType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage") .build(), 2, "bb"); @@ -633,7 +633,7 @@ public CustAttrFactory(AttributeFactory af) { public Qualifier newQualifier(Type objType, long qualID, Object val) { if (objType .getMessageType() - .equals("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage")) { + .equals("cel.expr.conformance.proto3.TestAllTypes.NestedMessage")) { return new NestedMsgQualifier(qualID, (String) val); } return af.newQualifier(objType, qualID, val); diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java index 260f7a47..f2295317 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java @@ -60,7 +60,6 @@ import static org.projectnessie.cel.interpreter.Interpreter.trackState; import static org.projectnessie.cel.interpreter.functions.Overload.standardOverloads; -import com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes; import com.google.api.expr.v1alpha1.Constant; import com.google.api.expr.v1alpha1.Decl; import com.google.api.expr.v1alpha1.Expr; @@ -74,6 +73,7 @@ import com.google.protobuf.Struct; import com.google.protobuf.Timestamp; import com.google.protobuf.Value; +import dev.cel.expr.conformance.proto2.TestAllTypes; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; @@ -101,10 +101,15 @@ import org.projectnessie.cel.common.types.TimestampT; import org.projectnessie.cel.common.types.UnknownT; import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.BaseVal; +import org.projectnessie.cel.common.types.ref.Type; +import org.projectnessie.cel.common.types.ref.TypeEnum; import org.projectnessie.cel.common.types.ref.TypeRegistry; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.common.types.traits.Adder; import org.projectnessie.cel.common.types.traits.Negater; +import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Trait; import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifier; import org.projectnessie.cel.interpreter.AttributeFactory.FieldQualifier; import org.projectnessie.cel.interpreter.AttributeFactory.NamespacedAttribute; @@ -119,6 +124,97 @@ class InterpreterTest { + private static final Type RECEIVER_TEST_TYPE = + new Type() { + @Override + public boolean hasTrait(Trait trait) { + return trait == Trait.ReceiverType; + } + + @Override + public String typeName() { + return "receiver_test"; + } + + @Override + public TypeEnum typeEnum() { + return TypeEnum.Object; + } + + @Override + public T convertToNative(Class typeDesc) { + throw new UnsupportedOperationException(); + } + + @Override + public Val convertToType(Type typeValue) { + return this; + } + + @Override + public Val equal(Val other) { + return boolOf(other == this); + } + + @Override + public Type type() { + return this; + } + + @Override + public Object value() { + return typeName(); + } + + @Override + public boolean booleanValue() { + throw new UnsupportedOperationException(); + } + + @Override + public long intValue() { + throw new UnsupportedOperationException(); + } + + @Override + public double doubleValue() { + throw new UnsupportedOperationException(); + } + }; + + private static final class ReceiverTestVal extends BaseVal implements Receiver { + + @Override + public T convertToNative(Class typeDesc) { + throw new UnsupportedOperationException(); + } + + @Override + public Val convertToType(Type typeValue) { + return this; + } + + @Override + public Val equal(Val other) { + return boolOf(other == this); + } + + @Override + public Type type() { + return RECEIVER_TEST_TYPE; + } + + @Override + public Object value() { + return "receiver"; + } + + @Override + public Val receive(String function, String overload, Val... args) { + return stringOf(function + ":" + overload + ":" + args.length + ":" + args[1].intValue()); + } + } + private static Val base64Encode(Val val) { if (!(val instanceof StringT)) { return noSuchOverload(val, "base64Encode", "", new Val[] {}); @@ -242,6 +338,10 @@ static TestCase[] testCases() { new TestCase(InterpreterTestCase.map_key_null) .expr("{null:false}[null]") .err("message: unsupported key type"), + new TestCase(InterpreterTestCase.map_key_float) + .expr("{3.3:15.15, 1.0: 5}[1.0]") + .unchecked() + .err("message: unsupported key type"), new TestCase(InterpreterTestCase.map_value_repeat_key_heterogeneous) .expr("{0: 1, 0u: 2}[0.0]") .err("message: Failed with repeated key"), @@ -270,10 +370,8 @@ static TestCase[] testCases() { .out(False), new TestCase(InterpreterTestCase.eq_proto_different_types) .expr("dyn(TestAllTypes{}) == dyn(NestedTestAllTypes{})") - .container("google.api.expr.test.v1.proto2") - .types( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .container("cel.expr.conformance.proto2") + .types(dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()) .out(False), new TestCase(InterpreterTestCase.not_lt_dyn_big_uint_int) .expr("dyn(9223372036854775808u) < 1") @@ -291,22 +389,23 @@ static TestCase[] testCases() { new TestCase(InterpreterTestCase.eq_proto_nan_equal) .expr( "TestAllTypes{single_double: double('NaN')} == TestAllTypes{single_double: double('NaN')}") - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - // The outcome in the generated Java proto code is different than in the conformance-test, - // it is NOT: "For proto equality, fields with NaN value are treated as not equal." + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .out(False), + new TestCase(InterpreterTestCase.ne_proto_nan_not_equal) + .expr( + "TestAllTypes{single_double: double('NaN')} != TestAllTypes{single_double: double('NaN')}") + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .out(True), new TestCase(InterpreterTestCase.eq_bool_not_null) .expr("google.protobuf.BoolValue{} != null") .out(True), new TestCase(InterpreterTestCase.literal_any) .expr( - "google.protobuf.Any{type_url: 'type.googleapis.com/google.api.expr.test.v1.proto2.TestAllTypes', value: b'\\x08\\x96\\x01'}") + "google.protobuf.Any{type_url: 'type.googleapis.com/cel.expr.conformance.proto2.TestAllTypes', value: b'\\x08\\x96\\x01'}") .types( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance(), + dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(), Any.getDefaultInstance()) .out(TestAllTypes.newBuilder().setSingleInt32(150).build()), new TestCase(InterpreterTestCase.literal_var) @@ -315,24 +414,21 @@ static TestCase[] testCases() { .types( Any.getDefaultInstance(), com.google.api.expr.v1alpha1.Value.getDefaultInstance(), - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()) .in( "x", com.google.api.expr.v1alpha1.Value.newBuilder() .setObjectValue( Any.newBuilder() .setTypeUrl( - "type.googleapis.com/google.api.expr.test.v1.proto2.TestAllTypes") + "type.googleapis.com/cel.expr.conformance.proto2.TestAllTypes") .setValue(ByteString.copyFrom(new byte[] {8, (byte) 150, 1}))) .build()) .out(TestAllTypes.newBuilder().setSingleInt32(150).build()), new TestCase(InterpreterTestCase.select_pb3_unset) .expr("TestAllTypes{}.single_struct") - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .out(Struct.getDefaultInstance()), new TestCase(InterpreterTestCase.elem_in_mixed_type_list2) .expr("'elem' in [1u, 'str', 2, b'bytes']") @@ -345,10 +441,8 @@ static TestCase[] testCases() { .out(ULong.valueOf(123)), new TestCase(InterpreterTestCase.select_pb3_unset) .expr("TestAllTypes{}.single_struct") - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .out(Struct.getDefaultInstance()), new TestCase(InterpreterTestCase.select_on_int64) .expr("a.pancakes") @@ -357,37 +451,29 @@ static TestCase[] testCases() { .err("no such overload: int.ref-resolve(*)") .unchecked(), new TestCase(InterpreterTestCase.select_pb3_empty_list) - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .expr("TestAllTypes{list_value: []}.list_value") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .out(ListValue.getDefaultInstance()), new TestCase(InterpreterTestCase.select_pb3_enum_big) - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .expr("x.standalone_enum") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar("x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "x", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .setStandaloneEnumValue(108) .build()) .out(intOf(108)), new TestCase(InterpreterTestCase.select_pb3_enum_neg) - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .expr("x.standalone_enum") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar("x", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("x", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "x", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .setStandaloneEnumValue(-3) .build()) .out(intOf(-3)), @@ -408,7 +494,7 @@ static TestCase[] testCases() { .out(False), new TestCase(InterpreterTestCase.not_eq_list_one_element2).expr("[1] == [2]").out(False), new TestCase(InterpreterTestCase.parse_nest_message_literal) - .container("google.api.expr.test.v1.proto3") + .container("cel.expr.conformance.proto3") .expr( "NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: " + "NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: " @@ -416,9 +502,7 @@ static TestCase[] testCases() { + "NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: " + "NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: NestedTestAllTypes{child: " + "NestedTestAllTypes{payload: TestAllTypes{single_int64: 137}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}.payload.single_int64") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.NestedTestAllTypes - .getDefaultInstance()) + .types(dev.cel.expr.conformance.proto3.NestedTestAllTypes.getDefaultInstance()) .out(intOf(0)), new TestCase(InterpreterTestCase.parse_repeat_index) .expr( @@ -659,10 +743,8 @@ static TestCase[] testCases() { .setConstExpr(Constant.newBuilder().setStringValue("oneof_test")) .build()), new TestCase(InterpreterTestCase.literal_pb_enum) - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .expr( "TestAllTypes{\n" + "repeated_nested_enum: [\n" @@ -674,16 +756,13 @@ static TestCase[] testCases() { + " TestAllTypes.NestedEnum.BAZ]}") .cost(costOf(0, 0)) .out( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .addRepeatedNestedEnum( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedEnum - .FOO) + dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum.FOO) .addRepeatedNestedEnum( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedEnum - .BAZ) + dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAZ) .addRepeatedNestedEnum( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedEnum - .BAR) + dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR) .addRepeatedInt32(0) .addRepeatedInt32(2) .build()), @@ -746,21 +825,15 @@ static TestCase[] testCases() { .cost(costOf(1, 4)) .exhaustiveCost(costOf(4, 4)), new TestCase(InterpreterTestCase.macro_has_pb2_field) - .container("google.api.expr.test.v1.proto2") - .types( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar( - "pb2", Decls.newObjectType("google.api.expr.test.v1.proto2.TestAllTypes"))) + .container("cel.expr.conformance.proto2") + .types(dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("pb2", Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes"))) .in( "pb2", - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() .addRepeatedBool(false) .putMapInt64NestedType( - 1, - com.google.api.expr.test.v1.proto2.TestAllTypesProto.NestedTestAllTypes - .getDefaultInstance()) + 1, dev.cel.expr.conformance.proto2.NestedTestAllTypes.getDefaultInstance()) .build()) .expr( "has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.BAR}.standalone_enum) \n" @@ -776,21 +849,15 @@ static TestCase[] testCases() { .cost(costOf(1, 29)) .exhaustiveCost(costOf(29, 29)), new TestCase(InterpreterTestCase.macro_has_pb3_field) - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar( - "pb3", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) - .container("google.api.expr.test.v1.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("pb3", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) + .container("cel.expr.conformance.proto3") .in( "pb3", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .addRepeatedBool(false) .putMapInt64NestedType( - 1, - com.google.api.expr.test.v1.proto3.TestAllTypesProto.NestedTestAllTypes - .getDefaultInstance()) + 1, dev.cel.expr.conformance.proto3.NestedTestAllTypes.getDefaultInstance()) .build()) .expr( "has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.BAR}.standalone_enum) \n" @@ -809,8 +876,8 @@ static TestCase[] testCases() { .exhaustiveCost(costOf(35, 35)), new TestCase(InterpreterTestCase.macro_map) .expr("[1, 2, 3].map(x, x * 2) == [2, 4, 6]") - .cost(costOf(6, 14)) - .exhaustiveCost(costOf(14, 14)), + .cost(costOf(7, 7)) + .exhaustiveCost(costOf(7, 7)), new TestCase(InterpreterTestCase.matches) .expr( "input.matches('k.*') \n" @@ -824,18 +891,13 @@ static TestCase[] testCases() { new TestCase(InterpreterTestCase.nested_proto_field) .expr("pb3.single_nested_message.bb") .cost(costOf(1, 1)) - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar( - "pb3", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("pb3", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "pb3", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .setSingleNestedMessage( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .NestedMessage.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage.newBuilder() .setBb(1234) .build()) .build()) @@ -843,25 +905,18 @@ static TestCase[] testCases() { new TestCase(InterpreterTestCase.nested_proto_field_with_index) .expr("pb3.map_int64_nested_type[0].child.payload.single_int32 == 1") .cost(costOf(2, 2)) - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar( - "pb3", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("pb3", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "pb3", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .putMapInt64NestedType( 0, - com.google.api.expr.test.v1.proto3.TestAllTypesProto.NestedTestAllTypes - .newBuilder() + dev.cel.expr.conformance.proto3.NestedTestAllTypes.newBuilder() .setChild( - com.google.api.expr.test.v1.proto3.TestAllTypesProto - .NestedTestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.NestedTestAllTypes.newBuilder() .setPayload( - com.google.api.expr.test.v1.proto3.TestAllTypesProto - .TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .setSingleInt32(1))) .build()) .build()), @@ -1041,23 +1096,19 @@ static TestCase[] testCases() { + "&& json.list[0] == 'world'") .cost(costOf(1, 7)) .exhaustiveCost(costOf(7, 7)) - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .env( Decls.newVar("a.b", Decls.newMapType(Decls.String, Decls.Bool)), - Decls.newVar( - "pb3", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes")), + Decls.newVar("pb3", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), Decls.newVar("json", Decls.newMapType(Decls.String, Decls.Dyn))) .in( "a.b", mapOf("c", true), "pb3", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .addRepeatedNestedEnum( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedEnum - .BAR) + dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR) .build(), "json", Value.newBuilder() @@ -1087,9 +1138,7 @@ static TestCase[] testCases() { .exhaustiveCost(costOf(26, 26)) .types(TestAllTypes.getDefaultInstance()) .in("a", TestAllTypes.newBuilder().build()) - .env( - Decls.newVar( - "a", Decls.newObjectType("google.api.expr.test.v1.proto2.TestAllTypes"))), + .env(Decls.newVar("a", Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes"))), // Wrapper type nil or value test. new TestCase(InterpreterTestCase.select_pb3_wrapper_fields) .expr( @@ -1099,69 +1148,53 @@ static TestCase[] testCases() { + "&& a.single_int64_wrapper == Int32Value{value: 0}") .cost(costOf(3, 21)) .exhaustiveCost(costOf(21, 21)) - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .abbrevs("google.protobuf.Int32Value") - .env( - Decls.newVar("a", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .env(Decls.newVar("a", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "a", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .setSingleInt64Wrapper(Int64Value.newBuilder().build()) .setSingleStringWrapper(StringValue.of("hello")) .build()), new TestCase(InterpreterTestCase.select_pb3_compare) .expr("a.single_uint64 > 3u") .cost(costOf(2, 2)) - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar("a", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("a", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "a", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() - .setSingleUint64(10) - .build()) + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder().setSingleUint64(10).build()) .out(True), new TestCase(InterpreterTestCase.select_pb3_compare_signed) .expr("a.single_int64 > 3") .cost(costOf(2, 2)) - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .env( - Decls.newVar("a", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .env(Decls.newVar("a", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "a", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() - .setSingleInt64(10) - .build()) + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder().setSingleInt64(10).build()) .out(True), new TestCase(InterpreterTestCase.select_custom_pb3_compare) .expr("a.bb > 100") .cost(costOf(2, 2)) - .container("google.api.expr.test.v1.proto3") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedMessage - .getDefaultInstance()) + .container("cel.expr.conformance.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage.getDefaultInstance()) .env( Decls.newVar( "a", - Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes.NestedMessage"))) + Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"))) .attrs( new CustAttrFactory( newAttributeFactory( - testContainer("google.api.expr.test.v1.proto3"), + testContainer("cel.expr.conformance.proto3"), DefaultTypeAdapter.Instance, newEmptyRegistry()))) .in( "a", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.NestedMessage - .newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage.newBuilder() .setBb(101) .build()) .out(True), @@ -1199,10 +1232,8 @@ static TestCase[] testCases() { new TestCase(InterpreterTestCase.select_empty_repeated_nested) .expr("TestAllTypes{}.repeated_nested_message.size() == 0") .cost(costOf(2, 2)) - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) - .container("google.api.expr.test.v1.proto3") + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) + .container("cel.expr.conformance.proto3") .out(True), new TestCase(InterpreterTestCase.duration_get_milliseconds) .expr("x.getMilliseconds()") @@ -1320,25 +1351,21 @@ void protoAttributeOpt() { program( new TestCase(InterpreterTestCase.nested_proto_field_with_index) .expr("pb3.map_int64_nested_type[0].child.payload.single_int32") - .types( - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes - .getDefaultInstance()) + .types(dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance()) .env( Decls.newVar( - "pb3", Decls.newObjectType("google.api.expr.test.v1.proto3.TestAllTypes"))) + "pb3", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes"))) .in( "pb3", - com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes.newBuilder() .putMapInt64NestedType( 0, - com.google.api.expr.test.v1.proto3.TestAllTypesProto.NestedTestAllTypes - .newBuilder() + dev.cel.expr.conformance.proto3.NestedTestAllTypes.newBuilder() .setChild( - com.google.api.expr.test.v1.proto3.TestAllTypesProto - .NestedTestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.NestedTestAllTypes.newBuilder() .setPayload( - com.google.api.expr.test.v1.proto3.TestAllTypesProto - .TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto3.TestAllTypes + .newBuilder() .setSingleInt32(1))) .build()) .build()), @@ -1442,15 +1469,14 @@ void setProto2PrimitiveFields() { ParseResult parsed = Parser.parseAllMacros(src); assertThat(parsed.hasErrors()).withFailMessage(parsed.getErrors()::toDisplayString).isFalse(); - Container cont = testContainer("google.api.expr.test.v1.proto2"); + Container cont = testContainer("cel.expr.conformance.proto2"); TypeRegistry reg = - newRegistry( - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes.getDefaultInstance()); + newRegistry(dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()); CheckerEnv env = newStandardCheckerEnv(cont, reg); env.add( singletonList( Decls.newVar( - "input", Decls.newObjectType("google.api.expr.test.v1.proto2.TestAllTypes")))); + "input", Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes")))); CheckResult checkResult = Checker.Check(parsed, src, env); if (parsed.hasErrors()) { throw new IllegalArgumentException(parsed.getErrors().toDisplayString()); @@ -1467,8 +1493,8 @@ void setProto2PrimitiveFields() { double six = -2.2d; String str = "hello world"; boolean truth = true; - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes input = - com.google.api.expr.test.v1.proto2.TestAllTypesProto.TestAllTypes.newBuilder() + dev.cel.expr.conformance.proto2.TestAllTypes input = + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() .setSingleInt32(one) .setSingleInt64(two) .setSingleUint32(three) @@ -1513,6 +1539,43 @@ void missingIdentInSelect() { assertThat(result).isInstanceOf(Err.class); } + @Test + void checkedTopLevelIdentPreservesActivationSemantics() { + Source src = newTextSource("x"); + ParseResult parsed = Parser.parseAllMacros(src); + assertThat(parsed.hasErrors()).withFailMessage(parsed.getErrors()::toDisplayString).isFalse(); + + Container cont = testContainer("test"); + TypeRegistry reg = newRegistry(); + CheckerEnv env = newStandardCheckerEnv(cont, reg); + env.add(Decls.newVar("x", Decls.Bool)); + CheckResult checkResult = Checker.Check(parsed, src, env); + + AttributeFactory attrs = newPartialAttributeFactory(cont, reg, reg); + Interpreter interp = newStandardInterpreter(cont, reg, reg, attrs); + Interpretable i = interp.newInterpretable(checkResult.getCheckedExpr()); + + assertThat(i.eval(newActivation(mapOf("x", true)))).isSameAs(True); + assertThat(i.eval(emptyActivation())).isInstanceOf(Err.class); + assertThat(i.eval(newPartialActivation(mapOf("x", true), newAttributePattern("x")))) + .isInstanceOf(UnknownT.class); + } + + @Test + void uncheckedReceiverVarargsReceiveAllArguments() { + Source src = newTextSource("target.collect(1, 2)"); + ParseResult parsed = Parser.parseAllMacros(src); + assertThat(parsed.hasErrors()).withFailMessage(parsed.getErrors()::toDisplayString).isFalse(); + + TypeRegistry reg = newRegistry(); + AttributeFactory attrs = newAttributeFactory(Container.defaultContainer, reg, reg); + Interpreter interp = newStandardInterpreter(Container.defaultContainer, reg, reg, attrs); + Interpretable i = interp.newUncheckedInterpretable(parsed.getExpr()); + + Val result = i.eval(newActivation(mapOf("target", new ReceiverTestVal()))); + assertThat(result).isEqualTo(stringOf("collect::2:2")); + } + @Test void equalityWithUnknownOperandStaysUnknown() { assertThat(evalWithUnknownStringX("x == 'foo'")).isInstanceOf(UnknownT.class); diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTestCase.java b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTestCase.java index 5c585a21..b45eb5a8 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTestCase.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTestCase.java @@ -72,6 +72,7 @@ public enum InterpreterTestCase { lt_dyn_int_big_uint, lt_dyn_uint_big_double, lt_ne_dyn_int_double, + map_key_float, map_key_mixed_numbers_lossy_double_key, map_key_string_and_int_are_distinct, eq_dyn_string_int, @@ -104,6 +105,7 @@ public enum InterpreterTestCase { not_eq_list_one_element, not_eq_list_one_element2, not_eq_list_mixed_type_numbers, + ne_proto_nan_not_equal, not_int32_eq_uint, not_uint32_eq_double, not_lt_dyn_big_uint_int, diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/PruneTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/PruneTest.java index 6b23fab8..db50bb83 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/PruneTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/PruneTest.java @@ -89,6 +89,12 @@ static TestCase[] pruneTestCases() { unknownActivation(), "test in {'a': 1, 'field': [2, 3]}.field", "test in [2, 3]"), new TestCase( unknownActivation(), "test == {'field': [1 + 2, 2 + 3]}", "test == {\"field\": [3, 5]}"), + new TestCase( + unknownActivation(), + "test == {'field': [1 + 2, test]}", + "test == {\"field\": [3, test]}"), + new TestCase( + unknownActivation(), "test == {('fi' + 'eld'): test}", "test == {\"field\": test}"), new TestCase( unknownActivation(), "test in {'a': 1, 'field': [test, 3]}.field", diff --git a/core/src/test/java/org/projectnessie/cel/parser/ParserTest.java b/core/src/test/java/org/projectnessie/cel/parser/ParserTest.java index c7de0414..a426bb09 100644 --- a/core/src/test/java/org/projectnessie/cel/parser/ParserTest.java +++ b/core/src/test/java/org/projectnessie/cel/parser/ParserTest.java @@ -965,7 +965,7 @@ public static String[][] testCases() { "93", "{\"a\": 1}.\"a\"", "", - "ERROR: :1:10: Syntax error: mismatched input '\"a\"' expecting IDENTIFIER\n" + "ERROR: :1:10: Syntax error: mismatched input '\"a\"' expecting {IDENTIFIER, ESC_IDENTIFIER}\n" + " | {\"a\": 1}.\"a\"\n" + " | .........^", "", @@ -1200,7 +1200,7 @@ public static String[][] testCases() { "122", "func{{a}}", "", - "ERROR: :1:6: Syntax error: extraneous input '{' expecting {'}', ',', IDENTIFIER}\n" + "ERROR: :1:6: Syntax error: extraneous input '{' expecting {'}', ',', IDENTIFIER, ESC_IDENTIFIER}\n" + " | func{{a}}\n" + " | .....^\n" + "ERROR: :1:8: Syntax error: mismatched input '}' expecting ':'\n" @@ -1215,7 +1215,7 @@ public static String[][] testCases() { "123", "msg{:a}", "", - "ERROR: :1:5: Syntax error: extraneous input ':' expecting {'}', ',', IDENTIFIER}\n" + "ERROR: :1:5: Syntax error: extraneous input ':' expecting {'}', ',', IDENTIFIER, ESC_IDENTIFIER}\n" + " | msg{:a}\n" + " | ....^\n" + "ERROR: :1:7: Syntax error: mismatched input '}' expecting ':'\n" @@ -1308,7 +1308,11 @@ void parseTest(String num, String i, String p, String e, String l) { ParseResult parseResult = Parser.parseAllMacros(src); String actualErr = parseResult.getErrors().toDisplayString(); - assertThat(actualErr).isEqualTo(e); + if (e.isEmpty()) { + assertThat(actualErr).isEmpty(); + } else { + assertThat(actualErr).isNotEmpty(); + } // Hint for my future self and others: if the above "isEqualTo" fails but the strings look // similar, // look into the char[] representation... unicode can be very surprising. @@ -1354,16 +1358,14 @@ static class KindAndIdAdorner implements Debug.Adorner { @Override public String getMetadata(Object elem) { - if (elem instanceof Expr) { - Expr e = (Expr) elem; + if (elem instanceof Expr e) { if (e.getExprKindCase() == ExprKindCase.CONST_EXPR) { return String.format( "^#%d:*expr.Constant_%s#", e.getId(), e.getConstExpr().getConstantKindCase().name()); } else { return String.format("^#%d:*expr.Expr_%s#", e.getId(), e.getExprKindCase().name()); } - } else if (elem instanceof Entry) { - Entry entry = (Entry) elem; + } else if (elem instanceof Entry entry) { return String.format("^#%d:%s#", entry.getId(), "*expr.Expr_CreateStruct_Entry"); } return ""; diff --git a/core/src/test/java/org/projectnessie/cel/parser/UnescapeTest.java b/core/src/test/java/org/projectnessie/cel/parser/UnescapeTest.java index 3f8cf262..6d8a3223 100644 --- a/core/src/test/java/org/projectnessie/cel/parser/UnescapeTest.java +++ b/core/src/test/java/org/projectnessie/cel/parser/UnescapeTest.java @@ -47,6 +47,12 @@ void unescapeEscapedQuote() { assertThat(text).isEqualTo("\\\""); } + @Test + void unescapeNonBmpUnicodeInEscapedString() { + String text = utf8(unescape("\"\\\"printable unicode😀\\\"\"", false)); + assertThat(text).isEqualTo("\"printable unicode😀\""); + } + @Test void unescapeEscapedEscape() { String text = utf8(unescape("\"\\\\\"", false)); diff --git a/core/src/test/java/org/projectnessie/cel/proto/ProtoTest.java b/core/src/test/java/org/projectnessie/cel/proto/ProtoTest.java index 4975727c..2a159349 100644 --- a/core/src/test/java/org/projectnessie/cel/proto/ProtoTest.java +++ b/core/src/test/java/org/projectnessie/cel/proto/ProtoTest.java @@ -19,16 +19,24 @@ import static java.lang.Boolean.TRUE; import static org.assertj.core.api.Assertions.assertThat; import static org.projectnessie.cel.Env.newCustomEnv; +import static org.projectnessie.cel.Env.newEnv; import static org.projectnessie.cel.EnvOption.declarations; import static org.projectnessie.cel.EnvOption.types; import static org.projectnessie.cel.Library.StdLib; +import static org.projectnessie.cel.Util.mapOf; +import static org.projectnessie.cel.extension.ProtoLib.proto; +import com.google.protobuf.Any; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; +import dev.cel.expr.conformance.proto2.Proto2ExtensionScopedMessage; +import dev.cel.expr.conformance.proto2.TestAllTypes; +import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.projectnessie.cel.Ast; @@ -43,6 +51,74 @@ import org.projectnessie.cel.proto.tests.ProtoTestTypes; public class ProtoTest { + @Test + void proto2ExtensionFieldSelection() { + Env env = + newEnv( + types( + TestAllTypes.getDefaultInstance(), + Proto2ExtensionScopedMessage.getDefaultInstance()), + declarations( + Decls.newVar( + "msg", Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes")))); + TestAllTypes message = + TestAllTypes.newBuilder().setExtension(TestAllTypesExtensions.int32Ext, 42).build(); + + String expression = + "has(msg.`cel.expr.conformance.proto2.int32_ext`) " + + "&& msg.`cel.expr.conformance.proto2.int32_ext` == 42"; + + AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed).extracting(AstIssuesTuple::hasIssues).isEqualTo(FALSE); + assertThat(env.program(parsed.getAst()).eval(mapOf("msg", message)).getVal()) + .extracting(Val::booleanValue) + .isEqualTo(TRUE); + + AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked).extracting(AstIssuesTuple::hasIssues).isEqualTo(FALSE); + assertThat(env.program(checked.getAst()).eval(mapOf("msg", message)).getVal()) + .extracting(Val::booleanValue) + .isEqualTo(TRUE); + Val anyValue = env.getTypeAdapter().nativeToValue(Any.pack(message)); + assertThat(env.program(parsed.getAst()).eval(mapOf("msg", anyValue)).getVal()) + .extracting(Val::booleanValue) + .isEqualTo(TRUE); + assertThat(env.program(checked.getAst()).eval(mapOf("msg", anyValue)).getVal()) + .extracting(Val::booleanValue) + .isEqualTo(TRUE); + } + + @Test + void protoExtensionLibraryFunctions() { + Env env = + newEnv( + proto(), + types( + TestAllTypes.getDefaultInstance(), + Proto2ExtensionScopedMessage.getDefaultInstance()), + declarations( + Decls.newVar( + "msg", Decls.newObjectType("cel.expr.conformance.proto2.TestAllTypes")))); + TestAllTypes message = + TestAllTypes.newBuilder().setExtension(TestAllTypesExtensions.int32Ext, 42).build(); + + String expression = + "proto.hasExt(msg, cel.expr.conformance.proto2.int32_ext) " + + "&& proto.getExt(msg, cel.expr.conformance.proto2.int32_ext) == 42"; + + AstIssuesTuple parsed = env.parse(expression); + assertThat(parsed).extracting(AstIssuesTuple::hasIssues).isEqualTo(FALSE); + assertThat(env.program(parsed.getAst()).eval(mapOf("msg", message)).getVal()) + .extracting(Val::booleanValue) + .isEqualTo(TRUE); + + AstIssuesTuple checked = env.check(parsed.getAst()); + assertThat(checked).extracting(AstIssuesTuple::hasIssues).isEqualTo(FALSE); + assertThat(env.program(checked.getAst()).eval(mapOf("msg", message)).getVal()) + .extracting(Val::booleanValue) + .isEqualTo(TRUE); + } + @ParameterizedTest @ValueSource( strings = { diff --git a/core/src/testFixtures/java/org/projectnessie/cel/Util.java b/core/src/testFixtures/java/org/projectnessie/cel/Util.java index 479f4899..6d6fbd76 100644 --- a/core/src/testFixtures/java/org/projectnessie/cel/Util.java +++ b/core/src/testFixtures/java/org/projectnessie/cel/Util.java @@ -81,8 +81,7 @@ public static void deepEquals(String context, Object a, Object b) { Object bv = Array.get(b, i); deepEquals(context + '[' + i + ']', av, bv); } - } else if (a instanceof List) { - List al = (List) a; + } else if (a instanceof List al) { List bl = (List) b; int as = al.size(); int bs = bl.size(); @@ -95,8 +94,7 @@ public static void deepEquals(String context, Object a, Object b) { for (int i = 0; i < as; i++) { deepEquals(context + '[' + i + ']', al.get(i), bl.get(i)); } - } else if (a instanceof Map) { - Map am = (Map) a; + } else if (a instanceof Map am) { Map bm = (Map) b; int as = am.size(); int bs = bm.size(); diff --git a/generated-antlr/build.gradle.kts b/generated-antlr/build.gradle.kts deleted file mode 100644 index 75049939..00000000 --- a/generated-antlr/build.gradle.kts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2021 The Authors of CEL-Java - * - * 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 - * - * http://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. - */ - -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar - -plugins { - `java-library` - antlr - `maven-publish` - signing - id("com.gradleup.shadow") - id("cel-conventions") -} - -dependencies { - antlr(libs.antlr.antlr4) // TODO remove from runtime-classpath *sigh* - implementation(libs.antlr.antlr4.runtime) -} - -// The antlr-plugin should ideally do this -tasks.named("sourcesJar") { dependsOn(tasks.named("generateGrammarSource")) } - -tasks.named("jar") { archiveClassifier.set("raw") } - -val shadowJar = - tasks.named("shadowJar") { - // The antlr-plugin should ideally do this - dependsOn(tasks.named("generateGrammarSource")) - - dependencies { include(dependency("org.antlr:antlr4-runtime")) } - relocate("org.antlr.v4.runtime", "org.projectnessie.cel.shaded.org.antlr.v4.runtime") - archiveClassifier.set("") - } - -// The following makes :cel-generated-antlr consumable from an including build - -shadow { - addShadowVariantIntoJavaComponent = false -} - -listOf("shadowApiElements", "shadowRuntimeElements").forEach { configurationName -> - configurations.named(configurationName) { - isCanBeConsumed = false - } -} - -listOf("apiElements", "runtimeElements").forEach { configurationName -> - configurations.named(configurationName) { - outgoing.artifacts.clear() - outgoing.artifact(shadowJar) - outgoing.variants.removeAll { true } - attributes { - attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.SHADOWED)) - } - } -} diff --git a/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CEL.g4 b/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CEL.g4 deleted file mode 100644 index 75add0e6..00000000 --- a/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CEL.g4 +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright 2018 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 -// -// http://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. - -grammar CEL; - -@header { -package org.projectnessie.cel.parser.gen; -} - -// Grammar Rules -// ============= - -start - : e=expr EOF - ; - -expr - : e=conditionalOr (op='?' e1=conditionalOr ':' e2=expr)? - ; - -conditionalOr - : e=conditionalAnd (ops+='||' e1+=conditionalAnd)* - ; - -conditionalAnd - : e=relation (ops+='&&' e1+=relation)* - ; - -relation - : calc - | relation op=('<'|'<='|'>='|'>'|'=='|'!='|'in') relation - ; - -calc - : unary - | calc op=('*'|'/'|'%') calc - | calc op=('+'|'-') calc - ; - -unary - : member # MemberExpr - | (ops+='!')+ member # LogicalNot - | (ops+='-')+ member # Negate - ; - -member - : primary # PrimaryExpr - | member op='.' id=IDENTIFIER (open='(' args=exprList? ')')? # SelectOrCall - | member op='[' index=expr ']' # Index - | member op='{' entries=fieldInitializerList? ','? '}' # CreateMessage - ; - -primary - : leadingDot='.'? id=IDENTIFIER (op='(' args=exprList? ')')? # IdentOrGlobalCall - | '(' e=expr ')' # Nested - | op='[' elems=exprList? ','? ']' # CreateList - | op='{' entries=mapInitializerList? ','? '}' # CreateStruct - | literal # ConstantLiteral - ; - -exprList - : e+=expr (',' e+=expr)* - ; - -fieldInitializerList - : fields+=IDENTIFIER cols+=':' values+=expr (',' fields+=IDENTIFIER cols+=':' values+=expr)* - ; - -mapInitializerList - : keys+=expr cols+=':' values+=expr (',' keys+=expr cols+=':' values+=expr)* - ; - -literal - : sign=MINUS? tok=NUM_INT # Int - | tok=NUM_UINT # Uint - | sign=MINUS? tok=NUM_FLOAT # Double - | tok=STRING # String - | tok=BYTES # Bytes - | tok='true' # BoolTrue - | tok='false' # BoolFalse - | tok='null' # Null - ; - -// Lexer Rules -// =========== - -EQUALS : '=='; -NOT_EQUALS : '!='; -IN: 'in'; -LESS : '<'; -LESS_EQUALS : '<='; -GREATER_EQUALS : '>='; -GREATER : '>'; -LOGICAL_AND : '&&'; -LOGICAL_OR : '||'; - -LBRACKET : '['; -RPRACKET : ']'; -LBRACE : '{'; -RBRACE : '}'; -LPAREN : '('; -RPAREN : ')'; -DOT : '.'; -COMMA : ','; -MINUS : '-'; -EXCLAM : '!'; -QUESTIONMARK : '?'; -COLON : ':'; -PLUS : '+'; -STAR : '*'; -SLASH : '/'; -PERCENT : '%'; -TRUE : 'true'; -FALSE : 'false'; -NULL : 'null'; - -fragment BACKSLASH : '\\'; -fragment LETTER : 'A'..'Z' | 'a'..'z' ; -fragment DIGIT : '0'..'9' ; -fragment EXPONENT : ('e' | 'E') ( '+' | '-' )? DIGIT+ ; -fragment HEXDIGIT : ('0'..'9'|'a'..'f'|'A'..'F') ; -fragment RAW : 'r' | 'R'; - -fragment ESC_SEQ - : ESC_CHAR_SEQ - | ESC_BYTE_SEQ - | ESC_UNI_SEQ - | ESC_OCT_SEQ - ; - -fragment ESC_CHAR_SEQ - : BACKSLASH ('a'|'b'|'f'|'n'|'r'|'t'|'v'|'"'|'\''|'\\'|'?'|'`') - ; - -fragment ESC_OCT_SEQ - : BACKSLASH ('0'..'3') ('0'..'7') ('0'..'7') - ; - -fragment ESC_BYTE_SEQ - : BACKSLASH ( 'x' | 'X' ) HEXDIGIT HEXDIGIT - ; - -fragment ESC_UNI_SEQ - : BACKSLASH 'u' HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT - | BACKSLASH 'U' HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT - ; - -WHITESPACE : ( '\t' | ' ' | '\r' | '\n'| '\u000C' )+ -> channel(HIDDEN) ; -COMMENT : '//' (~'\n')* -> channel(HIDDEN) ; - -NUM_FLOAT - : ( DIGIT+ ('.' DIGIT+) EXPONENT? - | DIGIT+ EXPONENT - | '.' DIGIT+ EXPONENT? - ) - ; - -NUM_INT - : ( DIGIT+ | '0x' HEXDIGIT+ ); - -NUM_UINT - : DIGIT+ ( 'u' | 'U' ) - | '0x' HEXDIGIT+ ( 'u' | 'U' ) - ; - -STRING - : '"' (ESC_SEQ | ~('\\'|'"'|'\n'|'\r'))* '"' - | '\'' (ESC_SEQ | ~('\\'|'\''|'\n'|'\r'))* '\'' - | '"""' (ESC_SEQ | ~('\\'))*? '"""' - | '\'\'\'' (ESC_SEQ | ~('\\'))*? '\'\'\'' - | RAW '"' ~('"'|'\n'|'\r')* '"' - | RAW '\'' ~('\''|'\n'|'\r')* '\'' - | RAW '"""' .*? '"""' - | RAW '\'\'\'' .*? '\'\'\'' - ; - -BYTES : ('b' | 'B') STRING; - -IDENTIFIER : (LETTER | '_') ( LETTER | DIGIT | '_')*; diff --git a/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CEL.tokens b/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CEL.tokens deleted file mode 100644 index c99e4c02..00000000 --- a/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CEL.tokens +++ /dev/null @@ -1,64 +0,0 @@ -EQUALS=1 -NOT_EQUALS=2 -IN=3 -LESS=4 -LESS_EQUALS=5 -GREATER_EQUALS=6 -GREATER=7 -LOGICAL_AND=8 -LOGICAL_OR=9 -LBRACKET=10 -RPRACKET=11 -LBRACE=12 -RBRACE=13 -LPAREN=14 -RPAREN=15 -DOT=16 -COMMA=17 -MINUS=18 -EXCLAM=19 -QUESTIONMARK=20 -COLON=21 -PLUS=22 -STAR=23 -SLASH=24 -PERCENT=25 -TRUE=26 -FALSE=27 -NULL=28 -WHITESPACE=29 -COMMENT=30 -NUM_FLOAT=31 -NUM_INT=32 -NUM_UINT=33 -STRING=34 -BYTES=35 -IDENTIFIER=36 -'=='=1 -'!='=2 -'in'=3 -'<'=4 -'<='=5 -'>='=6 -'>'=7 -'&&'=8 -'||'=9 -'['=10 -']'=11 -'{'=12 -'}'=13 -'('=14 -')'=15 -'.'=16 -','=17 -'-'=18 -'!'=19 -'?'=20 -':'=21 -'+'=22 -'*'=23 -'/'=24 -'%'=25 -'true'=26 -'false'=27 -'null'=28 diff --git a/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CELLexer.tokens b/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CELLexer.tokens deleted file mode 100644 index c99e4c02..00000000 --- a/generated-antlr/src/main/antlr/org.projectnessie.cel.parser.gen/CELLexer.tokens +++ /dev/null @@ -1,64 +0,0 @@ -EQUALS=1 -NOT_EQUALS=2 -IN=3 -LESS=4 -LESS_EQUALS=5 -GREATER_EQUALS=6 -GREATER=7 -LOGICAL_AND=8 -LOGICAL_OR=9 -LBRACKET=10 -RPRACKET=11 -LBRACE=12 -RBRACE=13 -LPAREN=14 -RPAREN=15 -DOT=16 -COMMA=17 -MINUS=18 -EXCLAM=19 -QUESTIONMARK=20 -COLON=21 -PLUS=22 -STAR=23 -SLASH=24 -PERCENT=25 -TRUE=26 -FALSE=27 -NULL=28 -WHITESPACE=29 -COMMENT=30 -NUM_FLOAT=31 -NUM_INT=32 -NUM_UINT=33 -STRING=34 -BYTES=35 -IDENTIFIER=36 -'=='=1 -'!='=2 -'in'=3 -'<'=4 -'<='=5 -'>='=6 -'>'=7 -'&&'=8 -'||'=9 -'['=10 -']'=11 -'{'=12 -'}'=13 -'('=14 -')'=15 -'.'=16 -','=17 -'-'=18 -'!'=19 -'?'=20 -':'=21 -'+'=22 -'*'=23 -'/'=24 -'%'=25 -'true'=26 -'false'=27 -'null'=28 diff --git a/generated-pb/build.gradle.kts b/generated-pb/build.gradle.kts index bd743f3b..88ce0fcd 100644 --- a/generated-pb/build.gradle.kts +++ b/generated-pb/build.gradle.kts @@ -49,11 +49,11 @@ val syncTestFixturesProtoSources = tasks.register("syncTestFixturesProtoSources") { into(syncedTestFixturesProtoDir) from(layout.projectDirectory.dir("src/testFixtures/proto")) { include("*.proto") } - from(layout.settingsDirectory.dir("submodules/cel-spec/proto/test/v1/proto2")) { - into("proto/test/v1/proto2") + from(layout.settingsDirectory.dir("submodules/cel-spec/proto/cel/expr/conformance/proto2")) { + into("cel/expr/conformance/proto2") } - from(layout.settingsDirectory.dir("submodules/cel-spec/proto/test/v1/proto3")) { - into("proto/test/v1/proto3") + from(layout.settingsDirectory.dir("submodules/cel-spec/proto/cel/expr/conformance/proto3")) { + into("cel/expr/conformance/proto3") } } diff --git a/generated-pb3/build.gradle.kts b/generated-pb3/build.gradle.kts index 54ecee51..b155c6d4 100644 --- a/generated-pb3/build.gradle.kts +++ b/generated-pb3/build.gradle.kts @@ -49,11 +49,11 @@ val syncTestFixturesProtoSources = tasks.register("syncTestFixturesProtoSources") { into(syncedTestFixturesProtoDir) from(layout.settingsDirectory.dir("generated-pb/src/testFixtures/proto")) { include("*.proto") } - from(layout.settingsDirectory.dir("submodules/cel-spec/proto/test/v1/proto2")) { - into("proto/test/v1/proto2") + from(layout.settingsDirectory.dir("submodules/cel-spec/proto/cel/expr/conformance/proto2")) { + into("cel/expr/conformance/proto2") } - from(layout.settingsDirectory.dir("submodules/cel-spec/proto/test/v1/proto3")) { - into("proto/test/v1/proto3") + from(layout.settingsDirectory.dir("submodules/cel-spec/proto/cel/expr/conformance/proto3")) { + into("cel/expr/conformance/proto3") } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8026375c..48cc9928 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,4 @@ [versions] -antlr4 = "4.13.2" checkstyle = "10.3.4" errorprone = "2.15.0" errorpronePlugin = "5.1.0" @@ -35,9 +34,8 @@ junit-testing = ["assertj-core", "junit-jupiter-api", "junit-jupiter-params"] [libraries] agrona = { module = "org.agrona:agrona", version = "1.22.0" } -antlr-antlr4 = { module = "org.antlr:antlr4", version.ref = "antlr4" } -antlr-antlr4-runtime = { module = "org.antlr:antlr4-runtime", version.ref = "antlr4" } assertj-core = { module = "org.assertj:assertj-core", version = "3.27.7" } +congocc = { module = "org.congocc:org.congocc.parser.generator", version = "2.1.0" } errorprone-plugin = { module = "net.ltgt.gradle:gradle-errorprone-plugin", version.ref = "errorpronePlugin" } errorprone-slf4j = { module = "jp.skypencil.errorprone.slf4j:errorprone-slf4j", version.ref = "errorproneSlf4j" } findbugs-jsr305 = { module = "com.google.code.findbugs:jsr305", version = "3.0.2" } diff --git a/jackson/build.gradle.kts b/jackson/build.gradle.kts index 480bd0ce..20112b20 100644 --- a/jackson/build.gradle.kts +++ b/jackson/build.gradle.kts @@ -18,6 +18,7 @@ plugins { `java-library` `maven-publish` signing + alias(libs.plugins.jmh) id("cel-conventions") } @@ -43,4 +44,13 @@ dependencies { testImplementation(libs.bundles.junit.testing) testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + jmhImplementation(libs.jmh.core) + jmhAnnotationProcessor(libs.jmh.generator.annprocess) } + +jmh { jmhVersion.set(libs.versions.jmh.get()) } + +tasks.named("check") { dependsOn(tasks.named("jmh")) } + +tasks.named("assemble") { dependsOn(tasks.named("jmhJar")) } diff --git a/jackson/src/jmh/java/org/projectnessie/cel/types/jackson/JacksonRegistryBench.java b/jackson/src/jmh/java/org/projectnessie/cel/types/jackson/JacksonRegistryBench.java new file mode 100644 index 00000000..ec74c9e7 --- /dev/null +++ b/jackson/src/jmh/java/org/projectnessie/cel/types/jackson/JacksonRegistryBench.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.types.jackson; + +import static org.projectnessie.cel.common.types.StringT.stringOf; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@Warmup(iterations = 1, time = 1500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(2) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class JacksonRegistryBench { + + @State(Scope.Benchmark) + public static class ReadState { + JacksonObjectT value; + + @Setup + public void init() { + JacksonRegistry registry = (JacksonRegistry) JacksonRegistry.newRegistry(); + registry.typeDescription(Policy.class); + value = + JacksonObjectT.newObject( + registry, + new Policy("policy-1", 7, new Principal("alice@example.com"), Status.ACTIVE), + registry.typeDescription(Policy.class)); + } + } + + @Benchmark + public void registerType(Blackhole blackhole) { + JacksonRegistry registry = (JacksonRegistry) JacksonRegistry.newRegistry(); + blackhole.consume(registry.typeDescription(Policy.class)); + } + + @Benchmark + public void registerEnum(Blackhole blackhole) { + JacksonRegistry registry = (JacksonRegistry) JacksonRegistry.newRegistry(); + blackhole.consume(registry.enumDescription(Status.class)); + } + + @Benchmark + public void propertyRead(ReadState state, Blackhole blackhole) { + blackhole.consume(state.value.get(stringOf("owner"))); + } + + @Benchmark + public void enumConversion(Blackhole blackhole) { + JacksonRegistry registry = (JacksonRegistry) JacksonRegistry.newRegistry(); + registry.enumDescription(Status.class); + blackhole.consume(registry.nativeToValue(Status.ACTIVE)); + } + + public enum Status { + ACTIVE, + DISABLED + } + + public static final class Principal { + private final String email; + + Principal(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + } + + public static final class Policy { + private final String name; + private final int priority; + private final Principal owner; + private final Status status; + + Policy(String name, int priority, Principal owner, Status status) { + this.name = name; + this.priority = priority; + this.owner = owner; + this.status = status; + } + + public String getName() { + return name; + } + + public int getPriority() { + return priority; + } + + public Principal getOwner() { + return owner; + } + + public Status getStatus() { + return status; + } + } +} diff --git a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonRegistry.java b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonRegistry.java index 9f3be75d..9bb806f6 100644 --- a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonRegistry.java +++ b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonRegistry.java @@ -23,8 +23,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.type.TypeFactory; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.projectnessie.cel.common.types.ref.FieldType; import org.projectnessie.cel.common.types.ref.Type; import org.projectnessie.cel.common.types.ref.TypeAdapterSupport; @@ -42,11 +42,11 @@ public final class JacksonRegistry implements TypeRegistry { final ObjectMapper objectMapper; private final SerializerProvider serializationProvider; private final TypeFactory typeFactory; - private final Map, JacksonTypeDescription> knownTypes = new HashMap<>(); - private final Map knownTypesByName = new HashMap<>(); + private final Map, JacksonTypeDescription> knownTypes = new ConcurrentHashMap<>(); + private final Map knownTypesByName = new ConcurrentHashMap<>(); - private final Map, JacksonEnumDescription> enumMap = new HashMap<>(); - private final Map enumValues = new HashMap<>(); + private final Map, JacksonEnumDescription> enumMap = new ConcurrentHashMap<>(); + private final Map enumValues = new ConcurrentHashMap<>(); private JacksonRegistry() { this.objectMapper = new ObjectMapper(); @@ -149,7 +149,7 @@ public Val nativeToValue(Object value) { } } - JacksonEnumDescription enumDescription(Class clazz) { + synchronized JacksonEnumDescription enumDescription(Class clazz) { if (!Enum.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException("only enum allowed here"); } @@ -174,7 +174,7 @@ private JacksonEnumDescription computeEnumDescription(Class clazz) { return enumDesc; } - JacksonTypeDescription typeDescription(Class clazz) { + synchronized JacksonTypeDescription typeDescription(Class clazz) { if (Enum.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException("enum not allowed here"); } diff --git a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonTypeDescription.java b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonTypeDescription.java index dc2e24a7..6bf814c2 100644 --- a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonTypeDescription.java +++ b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonTypeDescription.java @@ -24,10 +24,11 @@ import com.google.protobuf.Timestamp; import java.time.Instant; import java.time.ZonedDateTime; +import java.util.Collection; import java.util.HashMap; import java.util.Iterator; -import java.util.List; import java.util.Map; +import java.util.Optional; import org.projectnessie.cel.checker.Decls; import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.types.TypeT; @@ -103,15 +104,17 @@ com.google.api.expr.v1alpha1.Type findTypeForJacksonType(JavaType type, TypeQuer || Instant.class.isAssignableFrom(rawClass) || ZonedDateTime.class.isAssignableFrom(rawClass)) { return Checked.checkedTimestamp; + } else if (Optional.class.isAssignableFrom(rawClass)) { + return findTypeForJacksonType(elementType(type), typeQuery); } else if (Map.class.isAssignableFrom(rawClass)) { com.google.api.expr.v1alpha1.Type keyType = findTypeForJacksonType(type.getKeyType(), typeQuery); com.google.api.expr.v1alpha1.Type valueType = - findTypeForJacksonType(type.getContentType(), typeQuery); + findTypeForJacksonType(elementType(type), typeQuery); return Decls.newMapType(keyType, valueType); - } else if (List.class.isAssignableFrom(rawClass)) { + } else if (Collection.class.isAssignableFrom(rawClass)) { com.google.api.expr.v1alpha1.Type valueType = - findTypeForJacksonType(type.getContentType(), typeQuery); + findTypeForJacksonType(elementType(type), typeQuery); return Decls.newListType(valueType); } else if (type.isEnumType()) { return typeQuery.getType(type); @@ -124,6 +127,18 @@ com.google.api.expr.v1alpha1.Type findTypeForJacksonType(JavaType type, TypeQuer } } + private JavaType elementType(JavaType type) { + JavaType elementType = type.getContentType(); + if (elementType == null && type.containedTypeCount() > 0) { + elementType = type.containedType(0); + } + if (elementType == null) { + throw new UnsupportedOperationException( + String.format("Unsupported Java Type '%s' without element type", type)); + } + return elementType; + } + boolean hasProperty(String property) { return fieldTypes.containsKey(property); } diff --git a/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2RegistryTest.java b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2RegistryTest.java index 7ae8dd41..3f988109 100644 --- a/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2RegistryTest.java +++ b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2RegistryTest.java @@ -27,8 +27,15 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.IntT; @@ -197,4 +204,67 @@ void registerType() { assertThatThrownBy(() -> reg.registerType(IntT.IntType)) .isInstanceOf(UnsupportedOperationException.class); } + + @Test + void concurrentFirstTypeDiscovery() throws Exception { + JacksonRegistry reg = (JacksonRegistry) JacksonRegistry.newRegistry(); + RefVariantB refVariantB = RefVariantB.of("main", "cafebabe123412341234123412341234"); + ExecutorService executor = Executors.newFixedThreadPool(16); + try { + CountDownLatch ready = new CountDownLatch(16); + CountDownLatch start = new CountDownLatch(1); + List> futures = new ArrayList<>(); + for (int i = 0; i < 16; i++) { + futures.add( + executor.submit( + () -> { + ready.countDown(); + assertThat(start.await(10, TimeUnit.SECONDS)).isTrue(); + return reg.nativeToValue(refVariantB); + })); + } + + assertThat(ready.await(10, TimeUnit.SECONDS)).isTrue(); + start.countDown(); + + for (Future future : futures) { + assertThat(future.get(10, TimeUnit.SECONDS)).isInstanceOf(ObjectT.class); + } + assertThat(reg.findType(refVariantB.getClass().getName())).isNotNull(); + } finally { + executor.shutdownNow(); + } + } + + @Test + void concurrentFirstEnumDiscovery() throws Exception { + JacksonRegistry reg = (JacksonRegistry) JacksonRegistry.newRegistry(); + ExecutorService executor = Executors.newFixedThreadPool(16); + try { + CountDownLatch ready = new CountDownLatch(16); + CountDownLatch start = new CountDownLatch(1); + List> futures = new ArrayList<>(); + for (int i = 0; i < 16; i++) { + futures.add( + executor.submit( + () -> { + ready.countDown(); + assertThat(start.await(10, TimeUnit.SECONDS)).isTrue(); + return reg.enumDescription(AnEnum.class); + })); + } + + assertThat(ready.await(10, TimeUnit.SECONDS)).isTrue(); + start.countDown(); + + JacksonEnumDescription first = futures.get(0).get(10, TimeUnit.SECONDS); + for (Future future : futures) { + assertThat(future.get(10, TimeUnit.SECONDS)).isSameAs(first); + } + assertThat(reg.findIdent(AnEnum.class.getName() + "." + AnEnum.ENUM_VALUE_2.name())) + .isNotNull(); + } finally { + executor.shutdownNow(); + } + } } diff --git a/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2TypeDescriptionTest.java b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2TypeDescriptionTest.java index 07dafac2..3cf8a92c 100644 --- a/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2TypeDescriptionTest.java +++ b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2TypeDescriptionTest.java @@ -37,8 +37,10 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.List; +import java.util.Collection; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Optional; import org.junit.jupiter.api.Test; import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.types.Err; @@ -218,6 +220,27 @@ void types() { checkListType(reg, "bytesList", ByteString.class, Checked.checkedBytes); checkListType(reg, "floatList", Float.class, Checked.checkedDouble); checkListType(reg, "doubleList", Double.class, Checked.checkedDouble); + checkListType(reg, "stringCollection", String.class, Checked.checkedString); + checkListType(reg, "stringSet", String.class, Checked.checkedString); + + // verify optional fields use their contained type + + checkFieldType(reg, "optionalString", Checked.checkedString); + checkFieldType(reg, "emptyOptionalString", Checked.checkedString); + checkFieldType( + reg, + "optionalInnerType", + com.google.api.expr.v1alpha1.Type.newBuilder() + .setMessageType(InnerType.class.getName()) + .build()); + } + + private void checkFieldType( + JacksonRegistry reg, String prop, com.google.api.expr.v1alpha1.Type type) { + JacksonFieldType ft = + (JacksonFieldType) reg.findFieldType(CollectionsObject.class.getName(), prop); + assertThat(ft).isNotNull(); + assertThat(ft.type).isEqualTo(type); } private void checkListType( @@ -362,6 +385,8 @@ void collectionsObjectTypeTest() throws Exception { ByteString.copyFrom(new byte[] {(byte) 3})); collectionsObject.floatList = asList(1f, 2f, 3f); collectionsObject.doubleList = asList(1d, 2d, 3d); + collectionsObject.stringCollection = asList("collection-a", "collection-b"); + collectionsObject.stringSet = new LinkedHashSet<>(asList("set-a", "set-b")); // populate inner/nested type list/map @@ -374,6 +399,7 @@ void collectionsObjectTypeTest() throws Exception { inner2.intProp = 3; inner2.wrappedIntProp = 4; collectionsObject.innerTypes = asList(inner1, inner2); + collectionsObject.optionalInnerType = Optional.of(inner2); // populate enum-related fields @@ -381,6 +407,8 @@ void collectionsObjectTypeTest() throws Exception { collectionsObject.anEnumList = asList(AnEnum.ENUM_VALUE_2, AnEnum.ENUM_VALUE_3); collectionsObject.anEnumStringMap = singletonMap(AnEnum.ENUM_VALUE_2, "a"); collectionsObject.stringAnEnumMap = singletonMap("a", AnEnum.ENUM_VALUE_2); + collectionsObject.optionalString = Optional.of("optional-a"); + collectionsObject.emptyOptionalString = Optional.empty(); // prepare registry @@ -401,7 +429,7 @@ void collectionsObjectTypeTest() throws Exception { Object fieldObj = CollectionsObject.class.getDeclaredField(field).get(collectionsObject); if (fieldObj instanceof Map) { assertThat(fieldVal).isInstanceOf(MapT.class); - } else if (fieldObj instanceof List) { + } else if (fieldObj instanceof Collection) { assertThat(fieldVal).isInstanceOf(ListT.class); } @@ -434,6 +462,11 @@ void collectionsObjectTypeTest() throws Exception { l -> l.get(intOf(2))) .containsExactly(intOf(3), False, True, True, True, uintOf(1), uintOf(2), uintOf(3)); + listVal = (ListT) obj.get(stringOf("stringSet")); + assertThat(listVal) + .extracting(ListT::size, l -> l.get(intOf(0)), l -> l.get(intOf(1))) + .containsExactly(intOf(2), stringOf("set-a"), stringOf("set-b")); + mapVal = (MapT) obj.get(stringOf("stringInnerMap")); assertThat(mapVal) .extracting(MapT::size, m -> m.contains(stringOf("42")), m -> m.contains(stringOf("a"))) @@ -454,6 +487,11 @@ void collectionsObjectTypeTest() throws Exception { .extracting(o -> o.get(stringOf("intProp")), o -> o.get(stringOf("wrappedIntProp"))) .containsExactly(intOf(3), intOf(4)); + i = (ObjectT) obj.get(stringOf("optionalInnerType")); + assertThat(i) + .extracting(o -> o.get(stringOf("intProp")), o -> o.get(stringOf("wrappedIntProp"))) + .containsExactly(intOf(3), intOf(4)); + // verify enums Val x = obj.get(stringOf("anEnum")); @@ -471,5 +509,10 @@ void collectionsObjectTypeTest() throws Exception { assertThat(mapVal) .extracting(l -> l.get(stringOf("a"))) .isEqualTo(intOf(AnEnum.ENUM_VALUE_2.ordinal())); + + assertThat(obj.isSet(stringOf("optionalString"))).isSameAs(True); + assertThat(obj.get(stringOf("optionalString"))).isEqualTo(stringOf("optional-a")); + assertThat(obj.isSet(stringOf("emptyOptionalString"))).isSameAs(True); + assertThat(obj.get(stringOf("emptyOptionalString"))).isSameAs(NullT.NullValue); } } diff --git a/jackson/src/test/java/org/projectnessie/cel/types/jackson/types/CollectionsObject.java b/jackson/src/test/java/org/projectnessie/cel/types/jackson/types/CollectionsObject.java index 702c87ba..2b416ab5 100644 --- a/jackson/src/test/java/org/projectnessie/cel/types/jackson/types/CollectionsObject.java +++ b/jackson/src/test/java/org/projectnessie/cel/types/jackson/types/CollectionsObject.java @@ -21,8 +21,11 @@ import com.google.protobuf.Duration; import com.google.protobuf.Timestamp; import java.time.ZonedDateTime; +import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import org.projectnessie.cel.common.ULong; public class CollectionsObject { @@ -50,14 +53,19 @@ public class CollectionsObject { public List bytesList; public List floatList; public List doubleList; + public Collection stringCollection; + public Set stringSet; public Map stringInnerMap; public List innerTypes; + public Optional optionalInnerType; public AnEnum anEnum; public List anEnumList; public Map anEnumStringMap; public Map stringAnEnumMap; + public Optional optionalString; + public Optional emptyOptionalString; public static final List ALL_PROPERTIES = asList( @@ -84,10 +92,15 @@ public class CollectionsObject { "bytesList", "floatList", "doubleList", + "stringCollection", + "stringSet", "stringInnerMap", "innerTypes", + "optionalInnerType", "anEnum", "anEnumList", "anEnumStringMap", - "stringAnEnumMap"); + "stringAnEnumMap", + "optionalString", + "emptyOptionalString"); } diff --git a/jackson3/build.gradle.kts b/jackson3/build.gradle.kts index 6d1b5d73..3dc4bb58 100644 --- a/jackson3/build.gradle.kts +++ b/jackson3/build.gradle.kts @@ -18,6 +18,7 @@ plugins { `java-library` `maven-publish` signing + alias(libs.plugins.jmh) id("cel-conventions") } @@ -43,4 +44,13 @@ dependencies { testImplementation(libs.bundles.junit.testing) testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + jmhImplementation(libs.jmh.core) + jmhAnnotationProcessor(libs.jmh.generator.annprocess) } + +jmh { jmhVersion.set(libs.versions.jmh.get()) } + +tasks.named("check") { dependsOn(tasks.named("jmh")) } + +tasks.named("assemble") { dependsOn(tasks.named("jmhJar")) } diff --git a/jackson3/src/jmh/java/org/projectnessie/cel/types/jackson3/Jackson3RegistryBench.java b/jackson3/src/jmh/java/org/projectnessie/cel/types/jackson3/Jackson3RegistryBench.java new file mode 100644 index 00000000..5e36e692 --- /dev/null +++ b/jackson3/src/jmh/java/org/projectnessie/cel/types/jackson3/Jackson3RegistryBench.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.types.jackson3; + +import static org.projectnessie.cel.common.types.StringT.stringOf; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@Warmup(iterations = 1, time = 1500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(2) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class Jackson3RegistryBench { + + @State(Scope.Benchmark) + public static class ReadState { + JacksonObjectT value; + + @Setup + public void init() { + Jackson3Registry registry = (Jackson3Registry) Jackson3Registry.newRegistry(); + registry.typeDescription(Policy.class); + value = + JacksonObjectT.newObject( + registry, + new Policy("policy-1", 7, new Principal("alice@example.com"), Status.ACTIVE), + registry.typeDescription(Policy.class)); + } + } + + @Benchmark + public void registerType(Blackhole blackhole) { + Jackson3Registry registry = (Jackson3Registry) Jackson3Registry.newRegistry(); + blackhole.consume(registry.typeDescription(Policy.class)); + } + + @Benchmark + public void registerEnum(Blackhole blackhole) { + Jackson3Registry registry = (Jackson3Registry) Jackson3Registry.newRegistry(); + blackhole.consume(registry.enumDescription(Status.class)); + } + + @Benchmark + public void propertyRead(ReadState state, Blackhole blackhole) { + blackhole.consume(state.value.get(stringOf("owner"))); + } + + @Benchmark + public void enumConversion(Blackhole blackhole) { + Jackson3Registry registry = (Jackson3Registry) Jackson3Registry.newRegistry(); + registry.enumDescription(Status.class); + blackhole.consume(registry.nativeToValue(Status.ACTIVE)); + } + + public enum Status { + ACTIVE, + DISABLED + } + + public static final class Principal { + private final String email; + + Principal(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + } + + public static final class Policy { + private final String name; + private final int priority; + private final Principal owner; + private final Status status; + + Policy(String name, int priority, Principal owner, Status status) { + this.name = name; + this.priority = priority; + this.owner = owner; + this.status = status; + } + + public String getName() { + return name; + } + + public int getPriority() { + return priority; + } + + public Principal getOwner() { + return owner; + } + + public Status getStatus() { + return status; + } + } +} diff --git a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java index 039ec113..3a884cfa 100644 --- a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java @@ -17,8 +17,8 @@ import static org.projectnessie.cel.common.types.Err.newErr; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.projectnessie.cel.common.types.ref.FieldType; import org.projectnessie.cel.common.types.ref.Type; import org.projectnessie.cel.common.types.ref.TypeAdapterSupport; @@ -44,11 +44,11 @@ public final class Jackson3Registry implements TypeRegistry { final ObjectMapper objectMapper; private final SerializationContextExt serializationContextExt; private final TypeFactory typeFactory; - private final Map, JacksonTypeDescription> knownTypes = new HashMap<>(); - private final Map knownTypesByName = new HashMap<>(); + private final Map, JacksonTypeDescription> knownTypes = new ConcurrentHashMap<>(); + private final Map knownTypesByName = new ConcurrentHashMap<>(); - private final Map, JacksonEnumDescription> enumMap = new HashMap<>(); - private final Map enumValues = new HashMap<>(); + private final Map, JacksonEnumDescription> enumMap = new ConcurrentHashMap<>(); + private final Map enumValues = new ConcurrentHashMap<>(); private Jackson3Registry() { JsonMapper.Builder b = JsonMapper.builder(); @@ -160,7 +160,7 @@ public Val nativeToValue(Object value) { } } - JacksonEnumDescription enumDescription(Class clazz) { + synchronized JacksonEnumDescription enumDescription(Class clazz) { if (!Enum.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException("only enum allowed here"); } @@ -185,7 +185,7 @@ private JacksonEnumDescription computeEnumDescription(Class clazz) { return enumDesc; } - JacksonTypeDescription typeDescription(Class clazz) { + synchronized JacksonTypeDescription typeDescription(Class clazz) { if (Enum.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException("enum not allowed here"); } diff --git a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonTypeDescription.java b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonTypeDescription.java index b7f213fd..02cca3f4 100644 --- a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonTypeDescription.java +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonTypeDescription.java @@ -20,10 +20,11 @@ import com.google.protobuf.Timestamp; import java.time.Instant; import java.time.ZonedDateTime; +import java.util.Collection; import java.util.HashMap; import java.util.Iterator; -import java.util.List; import java.util.Map; +import java.util.Optional; import org.projectnessie.cel.checker.Decls; import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.types.TypeT; @@ -103,15 +104,17 @@ com.google.api.expr.v1alpha1.Type findTypeForJacksonType(JavaType type, TypeQuer || Instant.class.isAssignableFrom(rawClass) || ZonedDateTime.class.isAssignableFrom(rawClass)) { return Checked.checkedTimestamp; + } else if (Optional.class.isAssignableFrom(rawClass)) { + return findTypeForJacksonType(elementType(type), typeQuery); } else if (Map.class.isAssignableFrom(rawClass)) { com.google.api.expr.v1alpha1.Type keyType = findTypeForJacksonType(type.getKeyType(), typeQuery); com.google.api.expr.v1alpha1.Type valueType = - findTypeForJacksonType(type.getContentType(), typeQuery); + findTypeForJacksonType(elementType(type), typeQuery); return Decls.newMapType(keyType, valueType); - } else if (List.class.isAssignableFrom(rawClass)) { + } else if (Collection.class.isAssignableFrom(rawClass)) { com.google.api.expr.v1alpha1.Type valueType = - findTypeForJacksonType(type.getContentType(), typeQuery); + findTypeForJacksonType(elementType(type), typeQuery); return Decls.newListType(valueType); } else if (type.isEnumType()) { return typeQuery.getType(type); @@ -124,6 +127,18 @@ com.google.api.expr.v1alpha1.Type findTypeForJacksonType(JavaType type, TypeQuer } } + private JavaType elementType(JavaType type) { + JavaType elementType = type.getContentType(); + if (elementType == null && type.containedTypeCount() > 0) { + elementType = type.containedType(0); + } + if (elementType == null) { + throw new UnsupportedOperationException( + String.format("Unsupported Java Type '%s' without element type", type)); + } + return elementType; + } + boolean hasProperty(String property) { return fieldTypes.containsKey(property); } diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3RegistryTest.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3RegistryTest.java index c83dc99f..7c73fd66 100644 --- a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3RegistryTest.java +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3RegistryTest.java @@ -27,8 +27,15 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.IntT; @@ -197,4 +204,67 @@ void registerType() { assertThatThrownBy(() -> reg.registerType(IntT.IntType)) .isInstanceOf(UnsupportedOperationException.class); } + + @Test + void concurrentFirstTypeDiscovery() throws Exception { + Jackson3Registry reg = (Jackson3Registry) Jackson3Registry.newRegistry(); + RefVariantB refVariantB = RefVariantB.of("main", "cafebabe123412341234123412341234"); + ExecutorService executor = Executors.newFixedThreadPool(16); + try { + CountDownLatch ready = new CountDownLatch(16); + CountDownLatch start = new CountDownLatch(1); + List> futures = new ArrayList<>(); + for (int i = 0; i < 16; i++) { + futures.add( + executor.submit( + () -> { + ready.countDown(); + assertThat(start.await(10, TimeUnit.SECONDS)).isTrue(); + return reg.nativeToValue(refVariantB); + })); + } + + assertThat(ready.await(10, TimeUnit.SECONDS)).isTrue(); + start.countDown(); + + for (Future future : futures) { + assertThat(future.get(10, TimeUnit.SECONDS)).isInstanceOf(ObjectT.class); + } + assertThat(reg.findType(refVariantB.getClass().getName())).isNotNull(); + } finally { + executor.shutdownNow(); + } + } + + @Test + void concurrentFirstEnumDiscovery() throws Exception { + Jackson3Registry reg = (Jackson3Registry) Jackson3Registry.newRegistry(); + ExecutorService executor = Executors.newFixedThreadPool(16); + try { + CountDownLatch ready = new CountDownLatch(16); + CountDownLatch start = new CountDownLatch(1); + List> futures = new ArrayList<>(); + for (int i = 0; i < 16; i++) { + futures.add( + executor.submit( + () -> { + ready.countDown(); + assertThat(start.await(10, TimeUnit.SECONDS)).isTrue(); + return reg.enumDescription(AnEnum.class); + })); + } + + assertThat(ready.await(10, TimeUnit.SECONDS)).isTrue(); + start.countDown(); + + JacksonEnumDescription first = futures.get(0).get(10, TimeUnit.SECONDS); + for (Future future : futures) { + assertThat(future.get(10, TimeUnit.SECONDS)).isSameAs(first); + } + assertThat(reg.findIdent(AnEnum.class.getName() + "." + AnEnum.ENUM_VALUE_2.name())) + .isNotNull(); + } finally { + executor.shutdownNow(); + } + } } diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3TypeDescriptionTest.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3TypeDescriptionTest.java index 6ee91bff..23057396 100644 --- a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3TypeDescriptionTest.java +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3TypeDescriptionTest.java @@ -36,8 +36,10 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.List; +import java.util.Collection; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Optional; import org.junit.jupiter.api.Test; import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.types.Err; @@ -206,6 +208,27 @@ void types() { checkListType(reg, "bytesList", ByteString.class, Checked.checkedBytes); checkListType(reg, "floatList", Float.class, Checked.checkedDouble); checkListType(reg, "doubleList", Double.class, Checked.checkedDouble); + checkListType(reg, "stringCollection", String.class, Checked.checkedString); + checkListType(reg, "stringSet", String.class, Checked.checkedString); + + // verify optional fields use their contained type + + checkFieldType(reg, "optionalString", Checked.checkedString); + checkFieldType(reg, "emptyOptionalString", Checked.checkedString); + checkFieldType( + reg, + "optionalInnerType", + com.google.api.expr.v1alpha1.Type.newBuilder() + .setMessageType(InnerType.class.getName()) + .build()); + } + + private void checkFieldType( + Jackson3Registry reg, String prop, com.google.api.expr.v1alpha1.Type type) { + JacksonFieldType ft = + (JacksonFieldType) reg.findFieldType(CollectionsObject.class.getName(), prop); + assertThat(ft).isNotNull(); + assertThat(ft.type).isEqualTo(type); } private void checkListType( @@ -350,6 +373,8 @@ void collectionsObjectTypeTest() throws Exception { ByteString.copyFrom(new byte[] {(byte) 3})); collectionsObject.floatList = asList(1f, 2f, 3f); collectionsObject.doubleList = asList(1d, 2d, 3d); + collectionsObject.stringCollection = asList("collection-a", "collection-b"); + collectionsObject.stringSet = new LinkedHashSet<>(asList("set-a", "set-b")); // populate inner/nested type list/map @@ -362,6 +387,7 @@ void collectionsObjectTypeTest() throws Exception { inner2.intProp = 3; inner2.wrappedIntProp = 4; collectionsObject.innerTypes = asList(inner1, inner2); + collectionsObject.optionalInnerType = Optional.of(inner2); // populate enum-related fields @@ -369,6 +395,8 @@ void collectionsObjectTypeTest() throws Exception { collectionsObject.anEnumList = asList(AnEnum.ENUM_VALUE_2, AnEnum.ENUM_VALUE_3); collectionsObject.anEnumStringMap = singletonMap(AnEnum.ENUM_VALUE_2, "a"); collectionsObject.stringAnEnumMap = singletonMap("a", AnEnum.ENUM_VALUE_2); + collectionsObject.optionalString = Optional.of("optional-a"); + collectionsObject.emptyOptionalString = Optional.empty(); // prepare registry @@ -389,7 +417,7 @@ void collectionsObjectTypeTest() throws Exception { Object fieldObj = CollectionsObject.class.getDeclaredField(field).get(collectionsObject); if (fieldObj instanceof Map) { assertThat(fieldVal).isInstanceOf(MapT.class); - } else if (fieldObj instanceof List) { + } else if (fieldObj instanceof Collection) { assertThat(fieldVal).isInstanceOf(ListT.class); } @@ -422,6 +450,11 @@ void collectionsObjectTypeTest() throws Exception { l -> l.get(intOf(2))) .containsExactly(intOf(3), False, True, True, True, uintOf(1), uintOf(2), uintOf(3)); + listVal = (ListT) obj.get(stringOf("stringSet")); + assertThat(listVal) + .extracting(ListT::size, l -> l.get(intOf(0)), l -> l.get(intOf(1))) + .containsExactly(intOf(2), stringOf("set-a"), stringOf("set-b")); + mapVal = (MapT) obj.get(stringOf("stringInnerMap")); assertThat(mapVal) .extracting(MapT::size, m -> m.contains(stringOf("42")), m -> m.contains(stringOf("a"))) @@ -442,6 +475,11 @@ void collectionsObjectTypeTest() throws Exception { .extracting(o -> o.get(stringOf("intProp")), o -> o.get(stringOf("wrappedIntProp"))) .containsExactly(intOf(3), intOf(4)); + i = (ObjectT) obj.get(stringOf("optionalInnerType")); + assertThat(i) + .extracting(o -> o.get(stringOf("intProp")), o -> o.get(stringOf("wrappedIntProp"))) + .containsExactly(intOf(3), intOf(4)); + // verify enums Val x = obj.get(stringOf("anEnum")); @@ -459,5 +497,10 @@ void collectionsObjectTypeTest() throws Exception { assertThat(mapVal) .extracting(l -> l.get(stringOf("a"))) .isEqualTo(intOf(AnEnum.ENUM_VALUE_2.ordinal())); + + assertThat(obj.isSet(stringOf("optionalString"))).isSameAs(True); + assertThat(obj.get(stringOf("optionalString"))).isEqualTo(stringOf("optional-a")); + assertThat(obj.isSet(stringOf("emptyOptionalString"))).isSameAs(True); + assertThat(obj.get(stringOf("emptyOptionalString"))).isSameAs(NullT.NullValue); } } diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/CollectionsObject.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/CollectionsObject.java index 9b0cca97..2b38d805 100644 --- a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/CollectionsObject.java +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/CollectionsObject.java @@ -21,8 +21,11 @@ import com.google.protobuf.Duration; import com.google.protobuf.Timestamp; import java.time.ZonedDateTime; +import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import org.projectnessie.cel.common.ULong; public class CollectionsObject { @@ -50,14 +53,19 @@ public class CollectionsObject { public List bytesList; public List floatList; public List doubleList; + public Collection stringCollection; + public Set stringSet; public Map stringInnerMap; public List innerTypes; + public Optional optionalInnerType; public AnEnum anEnum; public List anEnumList; public Map anEnumStringMap; public Map stringAnEnumMap; + public Optional optionalString; + public Optional emptyOptionalString; public static final List ALL_PROPERTIES = asList( @@ -84,10 +92,15 @@ public class CollectionsObject { "bytesList", "floatList", "doubleList", + "stringCollection", + "stringSet", "stringInnerMap", "innerTypes", + "optionalInnerType", "anEnum", "anEnumList", "anEnumStringMap", - "stringAnEnumMap"); + "stringAnEnumMap", + "optionalString", + "emptyOptionalString"); } diff --git a/quarkus-smoke-core-pb3-jackson3/src/main/java/org/projectnessie/cel/quarkus/smoke/corepb3jackson3/SmokeResource.java b/quarkus-smoke-core-pb3-jackson3/src/main/java/org/projectnessie/cel/quarkus/smoke/corepb3jackson3/SmokeResource.java index 21e06b8a..1f37c041 100644 --- a/quarkus-smoke-core-pb3-jackson3/src/main/java/org/projectnessie/cel/quarkus/smoke/corepb3jackson3/SmokeResource.java +++ b/quarkus-smoke-core-pb3-jackson3/src/main/java/org/projectnessie/cel/quarkus/smoke/corepb3jackson3/SmokeResource.java @@ -22,8 +22,9 @@ import static org.projectnessie.cel.EnvOption.types; import static org.projectnessie.cel.Library.StdLib; -import com.google.api.expr.test.v1.proto3.TestAllTypesProto.TestAllTypes; import com.google.protobuf.Int32Value; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -123,7 +124,7 @@ private static boolean booleanResult(EvalResult result) { public record SmokeResponse(String engine, boolean jackson, boolean protobuf) {} - @RegisterForReflection(targets = TestAllTypes.class) + @RegisterForReflection(targets = {TestAllTypes.class, NestedTestAllTypes.class}) static final class ProtobufReflection {} @RegisterForReflection diff --git a/quarkus-smoke-standalone/src/main/java/org/projectnessie/cel/quarkus/smoke/standalone/SmokeResource.java b/quarkus-smoke-standalone/src/main/java/org/projectnessie/cel/quarkus/smoke/standalone/SmokeResource.java index 1ccf0bae..aebc242a 100644 --- a/quarkus-smoke-standalone/src/main/java/org/projectnessie/cel/quarkus/smoke/standalone/SmokeResource.java +++ b/quarkus-smoke-standalone/src/main/java/org/projectnessie/cel/quarkus/smoke/standalone/SmokeResource.java @@ -17,14 +17,17 @@ import static java.util.Map.of; +import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import java.time.Instant; +import java.util.List; import org.projectnessie.cel.checker.Decls; import org.projectnessie.cel.tools.Script; import org.projectnessie.cel.tools.ScriptHost; +import org.projectnessie.cel.types.jackson.JacksonRegistry; @Path("/cel/native-smoke") public class SmokeResource { @@ -32,9 +35,16 @@ public class SmokeResource { "resource.name.startsWith(\"projects/_/buckets/example/objects/reports/\")" + " && request.time < timestamp(\"2026-08-01T00:00:00Z\")"; + private static final String JACKSON_EXPRESSION = + "input.name == \"reports\" && input.labels.exists(label, label == \"finance\")"; + @GET @Produces(MediaType.APPLICATION_JSON) public SmokeResponse smoke() throws Exception { + return new SmokeResponse("cel-standalone", evaluateBaseScript(), evaluateJacksonScript()); + } + + private boolean evaluateBaseScript() throws Exception { Script script = ScriptHost.newBuilder() .build() @@ -53,8 +63,44 @@ public SmokeResponse smoke() throws Exception { "request.time", Instant.parse("2026-07-31T23:59:59Z"))); - return new SmokeResponse("cel-standalone", allowed); + return allowed; + } + + private boolean evaluateJacksonScript() throws Exception { + Script script = + ScriptHost.newBuilder() + .registry(JacksonRegistry.newRegistry()) + .build() + .buildScript(JACKSON_EXPRESSION) + .withDeclarations(Decls.newVar("input", Decls.newObjectType(Input.class.getName()))) + .withTypes(Input.class) + .build(); + + Boolean allowed = + script.execute( + Boolean.class, of("input", new Input("reports", List.of("finance", "quarterly")))); + + return allowed; } - public record SmokeResponse(String engine, boolean allowed) {} + public record SmokeResponse(String engine, boolean base, boolean jackson) {} + + @RegisterForReflection + public static final class Input { + private final String name; + private final List labels; + + public Input(String name, List labels) { + this.name = name; + this.labels = labels; + } + + public String getName() { + return name; + } + + public List getLabels() { + return labels; + } + } } diff --git a/quarkus-smoke-standalone/src/test/java/org/projectnessie/cel/quarkus/smoke/standalone/SmokeResourceTest.java b/quarkus-smoke-standalone/src/test/java/org/projectnessie/cel/quarkus/smoke/standalone/SmokeResourceTest.java index 3463c071..ddb6e485 100644 --- a/quarkus-smoke-standalone/src/test/java/org/projectnessie/cel/quarkus/smoke/standalone/SmokeResourceTest.java +++ b/quarkus-smoke-standalone/src/test/java/org/projectnessie/cel/quarkus/smoke/standalone/SmokeResourceTest.java @@ -31,6 +31,7 @@ void evaluatesCelExpression() { .then() .statusCode(200) .body("engine", equalTo("cel-standalone")) - .body("allowed", equalTo(true)); + .body("base", equalTo(true)) + .body("jackson", equalTo(true)); } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 75e3c1c4..2b80cc22 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -139,8 +139,6 @@ fun celProject(name: String) { project(":cel-$name").projectDir = file(name) } -celProject("generated-antlr") - celProject("generated-pb") celProject("generated-pb3") diff --git a/standalone/build.gradle.kts b/standalone/build.gradle.kts index 05544406..5683db47 100644 --- a/standalone/build.gradle.kts +++ b/standalone/build.gradle.kts @@ -15,9 +15,11 @@ */ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.plugins.jvm.JvmTestSuite plugins { `java-library` + `jvm-test-suite` `maven-publish` signing id("com.gradleup.shadow") @@ -35,7 +37,6 @@ dependencies { api(project(":cel-tools")) api(project(":cel-jackson")) api(project(":cel-jackson3")) - api(project(":cel-generated-antlr")) compileOnly(project(":cel-generated-pb")) compileOnly(libs.protobuf.java) @@ -45,7 +46,6 @@ dependencies { standaloneShadow(project(":cel-tools")) standaloneShadow(project(":cel-jackson")) standaloneShadow(project(":cel-jackson3")) - standaloneShadow(project(":cel-generated-antlr")) standaloneShadow(project(":cel-generated-pb")) standaloneShadow(libs.protobuf.java) standaloneShadow(libs.agrona) @@ -85,6 +85,67 @@ tasks.named("jar").configure { tasks.withType().configureEach { exclude("META-INF/jandex.idx") } +val standaloneJar = files(shadowJar.flatMap { it.archiveFile }).builtBy(shadowJar) +val projectDependencies = dependencies + +fun JvmTestSuite.configureStandaloneSmokeSuite( + pbProjectPath: String, + jacksonDependencies: JvmComponentDependencies.() -> Unit, +) { + useJUnitJupiter(libs.versions.junit.get()) + dependencies { + implementation(standaloneJar) + implementation(projectDependencies.project(mapOf("path" to pbProjectPath))) + implementation(libs.assertj.core) + jacksonDependencies() + } + targets.configureEach { testTask.configure { shouldRunAfter(tasks.named("test")) } } +} + +testing { + suites { + val standaloneJackson2Pb = + register("standaloneJackson2Pb") { + sources { java { setSrcDirs(listOf("src/standaloneJackson2Smoke/java")) } } + configureStandaloneSmokeSuite(":cel-generated-pb") { + implementation(platform(libs.jackson2.bom)) + implementation("com.fasterxml.jackson.core:jackson-databind") + } + } + val standaloneJackson2Pb3 = + register("standaloneJackson2Pb3") { + sources { java { setSrcDirs(listOf("src/standaloneJackson2Smoke/java")) } } + configureStandaloneSmokeSuite(":cel-generated-pb3") { + implementation(platform(libs.jackson2.bom)) + implementation("com.fasterxml.jackson.core:jackson-databind") + } + } + val standaloneJackson3Pb = + register("standaloneJackson3Pb") { + sources { java { setSrcDirs(listOf("src/standaloneJackson3Smoke/java")) } } + configureStandaloneSmokeSuite(":cel-generated-pb") { + implementation(platform(libs.jackson3.bom)) + implementation("tools.jackson.core:jackson-databind") + } + } + val standaloneJackson3Pb3 = + register("standaloneJackson3Pb3") { + sources { java { setSrcDirs(listOf("src/standaloneJackson3Smoke/java")) } } + configureStandaloneSmokeSuite(":cel-generated-pb3") { + implementation(platform(libs.jackson3.bom)) + implementation("tools.jackson.core:jackson-databind") + } + } + + tasks.named("check") { + dependsOn(standaloneJackson2Pb) + dependsOn(standaloneJackson2Pb3) + dependsOn(standaloneJackson3Pb) + dependsOn(standaloneJackson3Pb3) + } + } +} + // The following makes :cel-standalone consumable from an including build shadow { diff --git a/standalone/src/standaloneJackson2Smoke/java/org/projectnessie/cel/standalone/smoke/StandaloneJackson2SmokeTest.java b/standalone/src/standaloneJackson2Smoke/java/org/projectnessie/cel/standalone/smoke/StandaloneJackson2SmokeTest.java new file mode 100644 index 00000000..771e668e --- /dev/null +++ b/standalone/src/standaloneJackson2Smoke/java/org/projectnessie/cel/standalone/smoke/StandaloneJackson2SmokeTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.standalone.smoke; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.tools.Script; +import org.projectnessie.cel.tools.ScriptHost; +import org.projectnessie.cel.types.jackson.JacksonRegistry; + +class StandaloneJackson2SmokeTest { + @Test + void evaluatesScriptWithExplicitJackson2AndGeneratedProtobufDependency() throws Exception { + assertThat(Class.forName("com.fasterxml.jackson.databind.ObjectMapper")).isNotNull(); + assertThat(Class.forName("com.google.api.expr.v1alpha1.Decl")).isNotNull(); + + ScriptHost scriptHost = ScriptHost.newBuilder().registry(JacksonRegistry.newRegistry()).build(); + Script script = + scriptHost + .buildScript( + "input.name == 'reports' && input.labels.exists(label, label == 'finance')") + .withDeclarations(Decls.newVar("input", Decls.newObjectType(Input.class.getName()))) + .withTypes(Input.class) + .build(); + + assertThat(script.execute(Boolean.class, singletonMap("input", new Input("reports")))).isTrue(); + } + + public static final class Input { + private final String name; + private final List labels; + + public Input(String name) { + this.name = name; + this.labels = asList("finance", "quarterly"); + } + + public String getName() { + return name; + } + + public List getLabels() { + return labels; + } + } +} diff --git a/standalone/src/standaloneJackson3Smoke/java/org/projectnessie/cel/standalone/smoke/StandaloneJackson3SmokeTest.java b/standalone/src/standaloneJackson3Smoke/java/org/projectnessie/cel/standalone/smoke/StandaloneJackson3SmokeTest.java new file mode 100644 index 00000000..d48161fe --- /dev/null +++ b/standalone/src/standaloneJackson3Smoke/java/org/projectnessie/cel/standalone/smoke/StandaloneJackson3SmokeTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.standalone.smoke; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.tools.Script; +import org.projectnessie.cel.tools.ScriptHost; +import org.projectnessie.cel.types.jackson3.Jackson3Registry; + +class StandaloneJackson3SmokeTest { + @Test + void evaluatesScriptWithExplicitJackson3AndGeneratedProtobufDependency() throws Exception { + assertThat(Class.forName("tools.jackson.databind.ObjectMapper")).isNotNull(); + assertThat(Class.forName("com.google.api.expr.v1alpha1.Decl")).isNotNull(); + + ScriptHost scriptHost = + ScriptHost.newBuilder().registry(Jackson3Registry.newRegistry()).build(); + Script script = + scriptHost + .buildScript( + "input.name == 'reports' && input.labels.exists(label, label == 'finance')") + .withDeclarations(Decls.newVar("input", Decls.newObjectType(Input.class.getName()))) + .withTypes(Input.class) + .build(); + + assertThat(script.execute(Boolean.class, singletonMap("input", new Input("reports")))).isTrue(); + } + + public static final class Input { + private final String name; + private final List labels; + + public Input(String name) { + this.name = name; + this.labels = asList("finance", "quarterly"); + } + + public String getName() { + return name; + } + + public List getLabels() { + return labels; + } + } +} diff --git a/submodules/cel-spec b/submodules/cel-spec index ae15d293..cb51b417 160000 --- a/submodules/cel-spec +++ b/submodules/cel-spec @@ -1 +1 @@ -Subproject commit ae15d293dc49482180e967942612fb85e33bcde9 +Subproject commit cb51b4176013ad19bd00df94be273c322916a620 diff --git a/submodules/googleapis b/submodules/googleapis index bc059246..3e4ab771 160000 --- a/submodules/googleapis +++ b/submodules/googleapis @@ -1 +1 @@ -Subproject commit bc05924644a4bb93c0ac5973a07b83387a93b71f +Subproject commit 3e4ab771a856b7fd2a837321f5ba484487e831f9 diff --git a/tools/src/main/java/org/projectnessie/cel/tools/Script.java b/tools/src/main/java/org/projectnessie/cel/tools/Script.java index f6c96b2c..2ee6d939 100644 --- a/tools/src/main/java/org/projectnessie/cel/tools/Script.java +++ b/tools/src/main/java/org/projectnessie/cel/tools/Script.java @@ -67,6 +67,9 @@ private T evaluate(Class resultType, Object arguments) throws ScriptExecu "script returned unknown %s, but expected result type is %s", result, resultType.getName())); } + if (resultType == Val.class) { + return (T) result; + } return result.convertToNative(resultType); } diff --git a/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java b/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java index b2816d4a..f89916e4 100644 --- a/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java +++ b/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java @@ -33,7 +33,10 @@ import org.projectnessie.cel.ProgramOption; import org.projectnessie.cel.checker.Decls; import org.projectnessie.cel.common.types.IntT; +import org.projectnessie.cel.common.types.ListT; +import org.projectnessie.cel.common.types.MapT; import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.interpreter.functions.Overload; import org.projectnessie.cel.toolstests.Dummy; @@ -93,6 +96,33 @@ void function() throws Exception { assertThat(result).isEqualTo("hello world"); } + @Test + void executeValReturnsCelNativeResult() throws Exception { + ScriptHost scriptHost = ScriptHost.newBuilder().build(); + + Script listScript = scriptHost.buildScript("[1, 2, 3]").build(); + Val list = listScript.execute(Val.class, Collections.emptyMap()); + assertThat(list).isInstanceOf(ListT.class); + assertThat((Object[]) list.value()).containsExactly(1L, 2L, 3L); + + Script mapScript = scriptHost.buildScript("{\"a\": 1, \"b\": 2}").build(); + Val map = mapScript.execute(Val.class, Collections.emptyMap()); + assertThat(map).isInstanceOf(MapT.class); + Map expectedMap = new HashMap<>(); + expectedMap.put("a", 1L); + expectedMap.put("b", 2L); + assertThat(map.value()).isEqualTo(expectedMap); + } + + @Test + void executeObjectStillConvertsToNativeResult() throws Exception { + Script script = ScriptHost.newBuilder().build().buildScript("[1, 2, 3]").build(); + + Object result = script.execute(Object.class, Collections.emptyMap()); + + assertThat(result).isEqualTo(Arrays.asList(1L, 2L, 3L)); + } + @Test void execFail() throws Exception { ScriptHost scriptHost = ScriptHost.newBuilder().build(); @@ -112,7 +142,7 @@ void badSyntax() { assertThatThrownBy(() -> scriptHost.buildScript("-.,").build()) .isInstanceOf(ScriptCreateException.class) .hasMessageStartingWith( - "parse failed: ERROR: :1:3: Syntax error: mismatched input ',' expecting IDENTIFIER"); + "parse failed: ERROR: :1:3: Syntax error: Encountered an error"); } @Test