From 718d3ec7b719b0c52cfca0f486630191395b9ff2 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Sat, 20 Dec 2025 15:18:09 +0100 Subject: [PATCH] Support Jackson 3 in addition to Jackson 2 The code is very much a copy of the code for Jackson 2, adapted in a few places to Jackson 3: * Adapt to Jackson 3 type renames, especially for the `SerializationProvider` -> `SerializationContext` rename * Adapt to the Jackson 3 mapper builder pattern and getting the required `SerializationContext` to inspect types in `Jackson3Registry`. The public API type `org.projectnessie.cel.types.jackson3.Jackson3Registry` is in a different package and uses a different type name, although it results in a repetition of the Jackson major version in the type name. The dependency-free `cel-standalone` artifact includes both Jackson 3 and Jackson 2 now. --- README.md | 21 +- gradle/libs.versions.toml | 3 +- jackson/build.gradle.kts | 4 +- .../cel/types/jackson/JacksonRegistry.java | 2 +- ...tryTest.java => Jackson2RegistryTest.java} | 2 +- ...tTest.java => Jackson2ScriptHostTest.java} | 2 +- ....java => Jackson2TypeDescriptionTest.java} | 2 +- jackson3/build.gradle.kts | 46 ++ .../cel/types/jackson3/Jackson3Registry.java | 217 +++++++++ .../jackson3/JacksonEnumDescription.java | 43 ++ .../cel/types/jackson3/JacksonEnumValue.java | 43 ++ .../cel/types/jackson3/JacksonFieldType.java | 37 ++ .../cel/types/jackson3/JacksonObjectT.java | 89 ++++ .../jackson3/JacksonTypeDescription.java | 174 +++++++ .../types/jackson3/Jackson3RegistryTest.java | 182 +++++++ .../jackson3/Jackson3ScriptHostTest.java | 144 ++++++ .../jackson3/Jackson3TypeDescriptionTest.java | 451 ++++++++++++++++++ .../cel/types/jackson3/types/AnEnum.java | 22 + .../types/jackson3/types/ClassWithEnum.java | 33 ++ .../jackson3/types/CollectionsObject.java | 93 ++++ .../cel/types/jackson3/types/InnerType.java | 21 + .../cel/types/jackson3/types/MetaTest.java | 108 +++++ .../cel/types/jackson3/types/MyPojo.java | 28 ++ .../types/jackson3/types/ObjectListEnum.java | 41 ++ .../cel/types/jackson3/types/RefBase.java | 30 ++ .../cel/types/jackson3/types/RefVariantA.java | 36 ++ .../cel/types/jackson3/types/RefVariantB.java | 37 ++ .../cel/types/jackson3/types/RefVariantC.java | 39 ++ settings.gradle.kts | 2 + standalone/build.gradle.kts | 1 + 30 files changed, 1941 insertions(+), 12 deletions(-) rename jackson/src/test/java/org/projectnessie/cel/types/jackson/{JacksonRegistryTest.java => Jackson2RegistryTest.java} (99%) rename jackson/src/test/java/org/projectnessie/cel/types/jackson/{JacksonScriptHostTest.java => Jackson2ScriptHostTest.java} (99%) rename jackson/src/test/java/org/projectnessie/cel/types/jackson/{JacksonTypeDescriptionTest.java => Jackson2TypeDescriptionTest.java} (99%) create mode 100644 jackson3/build.gradle.kts create mode 100644 jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java create mode 100644 jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonEnumDescription.java create mode 100644 jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonEnumValue.java create mode 100644 jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonFieldType.java create mode 100644 jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonObjectT.java create mode 100644 jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonTypeDescription.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3RegistryTest.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3ScriptHostTest.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3TypeDescriptionTest.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/AnEnum.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/ClassWithEnum.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/CollectionsObject.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/InnerType.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/MetaTest.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/MyPojo.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/ObjectListEnum.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefBase.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefVariantA.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefVariantB.java create mode 100644 jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefVariantC.java diff --git a/README.md b/README.md index f0310719..8e489dc3 100644 --- a/README.md +++ b/README.md @@ -122,19 +122,21 @@ public class MyClass { ### Jackson example +The following example refers to Jackson 3. Support for Jackson 2 is, see [below](#jackson-2-example). + It is also possible to use plain Java and Jackson objects as arguments by using the -`org.projectnessie.cel.types.jackson.JacksonRegistry` in `org.projectnessie.cel:cel-jackson`. +`org.projectnessie.cel.types.jackson3.Jackson3Registry` in `org.projectnessie.cel:cel-jackson3`. Code sample similar to the one above. It takes a user-provided object type `MyInput`. ```java -import org.projectnessie.cel.types.jackson.JacksonRegistry; +import org.projectnessie.cel.types.jackson3.Jackson3Registry; public class MyClass { public Boolean evalWithJacksonObject(MyInput input, String checkName) { // Build the script factory ScriptHost scriptHost = ScriptHost.newBuilder() // IMPORTANT: use the Jackson registry - .registry(JacksonRegistry.newRegistry()) + .registry(Jackson3Registry.newRegistry()) .build(); // Create the script, will be parsed and checked. @@ -164,7 +166,7 @@ public class MyClass { Note that the Jackson field-names are used as property names in CEL-Java. It is not necessary to annotate "plain Java" classes with Jackson annotations. -To use the `JacksonRegistry` in your application code, add the `cel-jackson` dependency in +To use the `Jackson3Registry` in your application code, add the `cel-jackson3` dependency in addition to `cel-core` or `cel-tools`. ```xml @@ -183,7 +185,7 @@ addition to `cel-core` or `cel-tools`. org.projectnessie.cel - cel-jackson + cel-jackson3 org.projectnessie.cel @@ -196,10 +198,17 @@ or Gradle project. dependencies { implementation(enforcedPlatform("org.projectnessie.cel:cel-bom:0.5.3")) implementation("org.projectnessie.cel:cel-tools") - implementation("org.projectnessie.cel:cel-jackson") + implementation("org.projectnessie.cel:cel-jackson3") } ``` +### Jackson 2 example + +Support for Jackson 2 is similar to Jackson 3, with a few differences: + +* Use `JacksonRegistry` from `org.projectnessie.cel.types.jackson.JacksonRegistry` +* Use `org.projectnessie.cel:cel-jackson` dependency instead of `org.projectnessie.cel:cel-jackson3` + ## Dependency-free artifact The `org.projectnessie.cel:cel-standalone` contains everything from CEL-Java and has no dependencies. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 23d968c0..f487cd5e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,7 +48,8 @@ guava = { module = "com.google.guava:guava", version = "33.5.0-jre" } idea-ext = { module = "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext", version = "1.3" } immutables-value-annotations = { module = "org.immutables:value-annotations", version.ref = "immutables" } immutables-value-processor = { module = "org.immutables:value-processor", version.ref = "immutables" } -jackson-bom = { module = "com.fasterxml.jackson:jackson-bom", version = "2.20.1" } +jackson2-bom = { module = "com.fasterxml.jackson:jackson-bom", version = "2.20.1" } +jackson3-bom = { module = "tools.jackson:jackson-bom", version = "3.0.3" } jacoco-maven-plugin = { module = "org.jacoco:jacoco-maven-plugin", version.ref = "jacoco" } jandex-plugin = { module = "com.github.vlsi.gradle:jandex-plugin", version.ref = "jandexPlugin" } jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } diff --git a/jackson/build.gradle.kts b/jackson/build.gradle.kts index 26488664..03289299 100644 --- a/jackson/build.gradle.kts +++ b/jackson/build.gradle.kts @@ -23,10 +23,12 @@ plugins { `cel-conventions` } +description = "CEL Jackson 2 support" + dependencies { api(project(":cel-core")) - implementation(platform(libs.jackson.bom)) + implementation(platform(libs.jackson2.bom)) implementation("com.fasterxml.jackson.core:jackson-databind") implementation("com.fasterxml.jackson.core:jackson-core") implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-protobuf") 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 74476291..46116c3c 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 @@ -33,7 +33,7 @@ import org.projectnessie.cel.common.types.ref.Val; /** - * CEL-Java {@link TypeRegistry} to use Jackson objects as input values for CEL scripts. + * CEL-Java {@link TypeRegistry} to use Jackson 2 objects as input values for CEL scripts. * *

The implementation does not support the construction of Jackson objects in CEL expressions and * therefore returning Jackson objects from CEL expressions is not possible/implemented and results diff --git a/jackson/src/test/java/org/projectnessie/cel/types/jackson/JacksonRegistryTest.java b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2RegistryTest.java similarity index 99% rename from jackson/src/test/java/org/projectnessie/cel/types/jackson/JacksonRegistryTest.java rename to jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2RegistryTest.java index dced26f5..95e8798a 100644 --- a/jackson/src/test/java/org/projectnessie/cel/types/jackson/JacksonRegistryTest.java +++ b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2RegistryTest.java @@ -39,7 +39,7 @@ import org.projectnessie.cel.types.jackson.types.MetaTest; import org.projectnessie.cel.types.jackson.types.RefVariantB; -class JacksonRegistryTest { +class Jackson2RegistryTest { @Test void nessieBranch() { TypeRegistry reg = JacksonRegistry.newRegistry(); diff --git a/jackson/src/test/java/org/projectnessie/cel/types/jackson/JacksonScriptHostTest.java b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2ScriptHostTest.java similarity index 99% rename from jackson/src/test/java/org/projectnessie/cel/types/jackson/JacksonScriptHostTest.java rename to jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2ScriptHostTest.java index 604f21fe..0820f827 100644 --- a/jackson/src/test/java/org/projectnessie/cel/types/jackson/JacksonScriptHostTest.java +++ b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2ScriptHostTest.java @@ -31,7 +31,7 @@ import org.projectnessie.cel.types.jackson.types.MyPojo; import org.projectnessie.cel.types.jackson.types.ObjectListEnum; -public class JacksonScriptHostTest { +public class Jackson2ScriptHostTest { @Test void simple() throws Exception { diff --git a/jackson/src/test/java/org/projectnessie/cel/types/jackson/JacksonTypeDescriptionTest.java b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2TypeDescriptionTest.java similarity index 99% rename from jackson/src/test/java/org/projectnessie/cel/types/jackson/JacksonTypeDescriptionTest.java rename to jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2TypeDescriptionTest.java index c28a6318..44c4527a 100644 --- a/jackson/src/test/java/org/projectnessie/cel/types/jackson/JacksonTypeDescriptionTest.java +++ b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2TypeDescriptionTest.java @@ -54,7 +54,7 @@ import org.projectnessie.cel.types.jackson.types.CollectionsObject; import org.projectnessie.cel.types.jackson.types.InnerType; -class JacksonTypeDescriptionTest { +class Jackson2TypeDescriptionTest { @Test void basics() { diff --git a/jackson3/build.gradle.kts b/jackson3/build.gradle.kts new file mode 100644 index 00000000..d49d0907 --- /dev/null +++ b/jackson3/build.gradle.kts @@ -0,0 +1,46 @@ +/* + * 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. + */ + +plugins { + `java-library` + `maven-publish` + signing + id("org.caffinitas.gradle.testsummary") + id("org.caffinitas.gradle.testrerun") + `cel-conventions` +} + +description = "CEL Jackson 3 support" + +dependencies { + api(project(":cel-core")) + + implementation(platform(libs.jackson3.bom)) + implementation("tools.jackson.core:jackson-databind") + implementation("tools.jackson.core:jackson-core") + implementation("tools.jackson.dataformat:jackson-dataformat-protobuf") + implementation("tools.jackson.dataformat:jackson-dataformat-yaml") + + testImplementation(project(":cel-tools")) + testAnnotationProcessor(libs.immutables.value.processor) + testCompileOnly(libs.immutables.value.annotations) + testImplementation(libs.findbugs.jsr305) + + testImplementation(platform(libs.junit.bom)) + testImplementation(libs.bundles.junit.testing) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} 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 new file mode 100644 index 00000000..8b4084af --- /dev/null +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java @@ -0,0 +1,217 @@ +/* + * 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.Err.newErr; + +import java.util.HashMap; +import java.util.Map; +import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.Type; +import org.projectnessie.cel.common.types.ref.TypeAdapterSupport; +import org.projectnessie.cel.common.types.ref.TypeRegistry; +import org.projectnessie.cel.common.types.ref.Val; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.cfg.GeneratorSettings; +import tools.jackson.databind.cfg.SerializationContexts; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.ser.SerializationContextExt; +import tools.jackson.databind.ser.jdk.EnumSerializer; +import tools.jackson.databind.type.TypeFactory; + +/** + * CEL-Java {@link TypeRegistry} to use Jackson 3 objects as input values for CEL scripts. + * + *

The implementation does not support the construction of Jackson objects in CEL expressions and + * therefore returning Jackson objects from CEL expressions is not possible/implemented and results + * in {@link UnsupportedOperationException}s. + */ +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, JacksonEnumDescription> enumMap = new HashMap<>(); + private final Map enumValues = new HashMap<>(); + + private Jackson3Registry() { + JsonMapper.Builder b = JsonMapper.builder(); + SerializationContexts serializationContexts = b.serializationContexts(); + this.objectMapper = b.build(); + SerializationContexts forMapper = + serializationContexts.forMapper( + objectMapper, + objectMapper.serializationConfig(), + objectMapper.tokenStreamFactory(), + b.serializerFactory()); + this.serializationContextExt = + forMapper.createContext(objectMapper.serializationConfig(), GeneratorSettings.empty()); + this.typeFactory = objectMapper.getTypeFactory(); + } + + public static TypeRegistry newRegistry() { + return new Jackson3Registry(); + } + + @Override + public TypeRegistry copy() { + return this; + } + + @Override + public void register(Object t) { + Class cls = t instanceof Class ? (Class) t : t.getClass(); + typeDescription(cls); + } + + @Override + public void registerType(Type... types) { + throw new UnsupportedOperationException(); + } + + @Override + public Val enumValue(String enumName) { + JacksonEnumValue enumVal = enumValues.get(enumName); + if (enumVal == null) { + return newErr("unknown enum name '%s'", enumName); + } + return enumVal.ordinalValue(); + } + + @Override + public Val findIdent(String identName) { + JacksonTypeDescription td = knownTypesByName.get(identName); + if (td != null) { + return td.type(); + } + + JacksonEnumValue enumVal = enumValues.get(identName); + if (enumVal != null) { + return enumVal.ordinalValue(); + } + return null; + } + + @Override + public com.google.api.expr.v1alpha1.Type findType(String typeName) { + JacksonTypeDescription td = knownTypesByName.get(typeName); + if (td == null) { + return null; + } + return td.pbType(); + } + + @Override + public FieldType findFieldType(String messageType, String fieldName) { + JacksonTypeDescription td = knownTypesByName.get(messageType); + if (td == null) { + return null; + } + return td.fieldType(fieldName); + } + + @Override + public Val newValue(String typeName, Map fields) { + throw new UnsupportedOperationException(); + } + + @Override + public Val nativeToValue(Object value) { + if (value instanceof Val) { + return (Val) value; + } + Val maybe = TypeAdapterSupport.maybeNativeToValue(this, value); + if (maybe != null) { + return maybe; + } + + if (value instanceof Enum) { + String fq = JacksonEnumValue.fullyQualifiedName((Enum) value); + JacksonEnumValue v = enumValues.get(fq); + if (v == null) { + return newErr("unknown enum name '%s'", fq); + } + return v.ordinalValue(); + } + + try { + return JacksonObjectT.newObject(this, value, typeDescription(value.getClass())); + } catch (Exception e) { + throw new RuntimeException("oops", e); + } + } + + JacksonEnumDescription enumDescription(Class clazz) { + if (!Enum.class.isAssignableFrom(clazz)) { + throw new IllegalArgumentException("only enum allowed here"); + } + + JacksonEnumDescription ed = enumMap.get(clazz); + if (ed != null) { + return ed; + } + ed = computeEnumDescription(clazz); + enumMap.put(clazz, ed); + return ed; + } + + private JacksonEnumDescription computeEnumDescription(Class clazz) { + ValueSerializer ser = serializationContextExt.findValueSerializer(clazz); + JavaType javaType = typeFactory.constructType(clazz); + + JacksonEnumDescription enumDesc = new JacksonEnumDescription(javaType, (EnumSerializer) ser); + enumMap.put(clazz, enumDesc); + + enumDesc.buildValues().forEach(v -> enumValues.put(v.fullyQualifiedName(), v)); + + return enumDesc; + } + + JacksonTypeDescription typeDescription(Class clazz) { + if (Enum.class.isAssignableFrom(clazz)) { + throw new IllegalArgumentException("enum not allowed here"); + } + + JacksonTypeDescription td = knownTypes.get(clazz); + if (td != null) { + return td; + } + td = computeTypeDescription(clazz); + knownTypes.put(clazz, td); + return td; + } + + private JacksonTypeDescription computeTypeDescription(Class clazz) { + ValueSerializer ser = serializationContextExt.findValueSerializer(clazz); + JavaType javaType = typeFactory.constructType(clazz); + + JacksonTypeDescription typeDesc = new JacksonTypeDescription(javaType, ser, this::typeQuery); + knownTypesByName.put(clazz.getName(), typeDesc); + + return typeDesc; + } + + private com.google.api.expr.v1alpha1.Type typeQuery(JavaType javaType) { + if (javaType.isEnumType()) { + return enumDescription(javaType.getRawClass()).pbType(); + } + return typeDescription(javaType.getRawClass()).pbType(); + } +} diff --git a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonEnumDescription.java b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonEnumDescription.java new file mode 100644 index 00000000..de94164e --- /dev/null +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonEnumDescription.java @@ -0,0 +1,43 @@ +/* + * 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 java.util.List; +import java.util.stream.Stream; +import org.projectnessie.cel.common.types.pb.Checked; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ser.jdk.EnumSerializer; + +final class JacksonEnumDescription { + + private final String name; + private final com.google.api.expr.v1alpha1.Type pbType; + private final List> enumValues; + + JacksonEnumDescription(JavaType javaType, EnumSerializer ser) { + this.name = javaType.getRawClass().getName().replace('$', '.'); + this.enumValues = ser.getEnumValues().enums(); + this.pbType = Checked.checkedInt; + } + + com.google.api.expr.v1alpha1.Type pbType() { + return pbType; + } + + Stream buildValues() { + return enumValues.stream().map(JacksonEnumValue::new); + } +} diff --git a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonEnumValue.java b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonEnumValue.java new file mode 100644 index 00000000..e0bb1255 --- /dev/null +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonEnumValue.java @@ -0,0 +1,43 @@ +/* + * 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.IntT.intOf; + +import org.projectnessie.cel.common.types.ref.Val; + +final class JacksonEnumValue { + + private final Val ordinalValue; + private final Enum enumValue; + + JacksonEnumValue(Enum enumValue) { + this.ordinalValue = intOf(enumValue.ordinal()); + this.enumValue = enumValue; + } + + static String fullyQualifiedName(Enum value) { + return value.getClass().getName().replace('$', '.') + '.' + value.name(); + } + + String fullyQualifiedName() { + return fullyQualifiedName(enumValue); + } + + Val ordinalValue() { + return ordinalValue; + } +} diff --git a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonFieldType.java b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonFieldType.java new file mode 100644 index 00000000..9f7e26a0 --- /dev/null +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonFieldType.java @@ -0,0 +1,37 @@ +/* + * 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 com.google.api.expr.v1alpha1.Type; +import org.projectnessie.cel.common.types.ref.FieldGetter; +import org.projectnessie.cel.common.types.ref.FieldTester; +import org.projectnessie.cel.common.types.ref.FieldType; +import tools.jackson.databind.ser.PropertyWriter; + +final class JacksonFieldType extends FieldType { + + private final PropertyWriter propertyWriter; + + JacksonFieldType( + Type type, FieldTester isSet, FieldGetter getFrom, PropertyWriter propertyWriter) { + super(type, isSet, getFrom); + this.propertyWriter = propertyWriter; + } + + PropertyWriter propertyWriter() { + return propertyWriter; + } +} diff --git a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonObjectT.java b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonObjectT.java new file mode 100644 index 00000000..79c74cf3 --- /dev/null +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonObjectT.java @@ -0,0 +1,89 @@ +/* + * 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.Err.newTypeConversionError; +import static org.projectnessie.cel.common.types.Err.noSuchField; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.Types.boolOf; + +import org.projectnessie.cel.common.types.ObjectT; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.ref.Val; + +final class JacksonObjectT extends ObjectT { + + private JacksonObjectT(Jackson3Registry registry, Object value, JacksonTypeDescription typeDesc) { + super(registry, value, typeDesc, typeDesc.type()); + } + + static JacksonObjectT newObject( + Jackson3Registry registry, Object value, JacksonTypeDescription typeDesc) { + return new JacksonObjectT(registry, value, typeDesc); + } + + JacksonTypeDescription typeDesc() { + return (JacksonTypeDescription) typeDesc; + } + + Jackson3Registry registry() { + return (Jackson3Registry) adapter; + } + + @Override + public Val isSet(Val field) { + if (!(field instanceof StringT)) { + return noSuchOverload(this, "isSet", field); + } + String fieldName = (String) field.value(); + + if (!typeDesc().hasProperty(fieldName)) { + return noSuchField(fieldName); + } + + Object value = typeDesc().fromObject(value(), fieldName); + + return boolOf(value != null); + } + + @Override + public Val get(Val index) { + if (!(index instanceof StringT)) { + return noSuchOverload(this, "get", index); + } + String fieldName = (String) index.value(); + + if (!typeDesc().hasProperty(fieldName)) { + return noSuchField(fieldName); + } + + Object v = typeDesc().fromObject(value(), fieldName); + + return registry().nativeToValue(v); + } + + @Override + public T convertToNative(Class typeDesc) { + if (typeDesc.isAssignableFrom(value.getClass())) { + return (T) value; + } + if (typeDesc.isAssignableFrom(getClass())) { + return (T) this; + } + throw new IllegalArgumentException( + newTypeConversionError(value.getClass().getName(), typeDesc).toString()); + } +} 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 new file mode 100644 index 00000000..b7f213fd --- /dev/null +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonTypeDescription.java @@ -0,0 +1,174 @@ +/* + * 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 com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.TypeT; +import org.projectnessie.cel.common.types.pb.Checked; +import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.Type; +import org.projectnessie.cel.common.types.ref.TypeDescription; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.ser.BeanPropertyWriter; +import tools.jackson.databind.ser.PropertyWriter; + +final class JacksonTypeDescription implements TypeDescription { + + private final JavaType javaType; + private final String name; + private final Type type; + private final com.google.api.expr.v1alpha1.Type pbType; + + private final Map fieldTypes; + + JacksonTypeDescription(JavaType javaType, ValueSerializer ser, TypeQuery typeQuery) { + this.javaType = javaType; + this.name = javaType.getRawClass().getName(); + this.type = TypeT.newObjectTypeValue(name); + this.pbType = com.google.api.expr.v1alpha1.Type.newBuilder().setMessageType(name).build(); + + fieldTypes = new HashMap<>(); + + Iterator propIter = ser.properties(); + while (propIter.hasNext()) { + PropertyWriter pw = propIter.next(); + String n = pw.getName(); + + JacksonFieldType ft = + new JacksonFieldType( + findTypeForJacksonType(pw.getType(), typeQuery), + target -> fromObject(target, n) != null, + target -> fromObject(target, n), + pw); + fieldTypes.put(n, ft); + } + } + + @FunctionalInterface + interface TypeQuery { + com.google.api.expr.v1alpha1.Type getType(JavaType javaType); + } + + com.google.api.expr.v1alpha1.Type findTypeForJacksonType(JavaType type, TypeQuery typeQuery) { + Class rawClass = type.getRawClass(); + if (rawClass == boolean.class || rawClass == Boolean.class) { + return Checked.checkedBool; + } else if (rawClass == long.class + || rawClass == Long.class + || rawClass == int.class + || rawClass == Integer.class + || rawClass == short.class + || rawClass == Short.class + || rawClass == byte.class + || rawClass == Byte.class) { + return Checked.checkedInt; + } else if (rawClass == ULong.class) { + return Checked.checkedUint; + } else if (rawClass == byte[].class || rawClass == ByteString.class) { + return Checked.checkedBytes; + } else if (rawClass == double.class + || rawClass == Double.class + || rawClass == float.class + || rawClass == Float.class) { + return Checked.checkedDouble; + } else if (rawClass == String.class) { + return Checked.checkedString; + } else if (rawClass == Duration.class || rawClass == java.time.Duration.class) { + return Checked.checkedDuration; + } else if (rawClass == Timestamp.class + || Instant.class.isAssignableFrom(rawClass) + || ZonedDateTime.class.isAssignableFrom(rawClass)) { + return Checked.checkedTimestamp; + } 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); + return Decls.newMapType(keyType, valueType); + } else if (List.class.isAssignableFrom(rawClass)) { + com.google.api.expr.v1alpha1.Type valueType = + findTypeForJacksonType(type.getContentType(), typeQuery); + return Decls.newListType(valueType); + } else if (type.isEnumType()) { + return typeQuery.getType(type); + } else { + com.google.api.expr.v1alpha1.Type t = typeQuery.getType(type); + if (t == null) { + throw new UnsupportedOperationException(String.format("Unsupported Java Type '%s'", type)); + } + return t; + } + } + + boolean hasProperty(String property) { + return fieldTypes.containsKey(property); + } + + Object fromObject(Object value, String property) { + JacksonFieldType ft = fieldTypes.get(property); + if (ft == null) { + throw new IllegalArgumentException(String.format("No property named '%s'", property)); + } + PropertyWriter pw = ft.propertyWriter(); + + if (pw instanceof BeanPropertyWriter) { + try { + return ((BeanPropertyWriter) pw).get(value); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else if (pw == null) { + return null; + } else { + throw new UnsupportedOperationException( + String.format( + "Unknown property-writer '%s' for property '%s'", pw.getClass().getName(), property)); + } + } + + Type type() { + return type; + } + + com.google.api.expr.v1alpha1.Type pbType() { + return pbType; + } + + FieldType fieldType(String fieldName) { + return fieldTypes.get(fieldName); + } + + @Override + public String name() { + return name; + } + + @Override + public Class reflectType() { + return javaType.getRawClass(); + } +} 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 new file mode 100644 index 00000000..8597d2e3 --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3RegistryTest.java @@ -0,0 +1,182 @@ +/* + * 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 java.util.Collections.emptyMap; +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.MapT.newMaybeWrappedMap; +import static org.projectnessie.cel.common.types.NullT.NullValue; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.TimestampT.timestampOf; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.IntT; +import org.projectnessie.cel.common.types.ObjectT; +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.types.jackson3.types.MetaTest; +import org.projectnessie.cel.types.jackson3.types.RefVariantB; + +class Jackson3RegistryTest { + @Test + void nessieBranch() { + TypeRegistry reg = Jackson3Registry.newRegistry(); + + RefVariantB refVariantB = RefVariantB.of("main", "cafebabe123412341234123412341234"); + + Val branchVal = reg.nativeToValue(refVariantB); + assertThat(branchVal).isInstanceOf(ObjectT.class); + assertThat(branchVal.type().typeEnum()).isSameAs(TypeEnum.Object); + assertThat(branchVal.type().typeName()).isEqualTo(refVariantB.getClass().getName()); + + ObjectT branchObj = (ObjectT) branchVal; + assertThat(branchObj.isSet(stringOf("foo"))) + .isInstanceOf(Err.class) + .asString() + .isEqualTo("no such field 'foo'"); + assertThat(branchObj.isSet(stringOf("name"))).isEqualTo(True); + assertThat(branchObj.isSet(stringOf("hash"))).isEqualTo(True); + assertThat(branchObj.get(stringOf("foo"))) + .isInstanceOf(Err.class) + .asString() + .isEqualTo("no such field 'foo'"); + assertThat(branchObj.get(stringOf("name"))).isEqualTo(stringOf("main")); + assertThat(branchObj.get(stringOf("hash"))) + .isEqualTo(stringOf("cafebabe123412341234123412341234")); + } + + @Test + void nessieCommitMetaFull() { + TypeRegistry reg = Jackson3Registry.newRegistry(); + + Instant now = Instant.now(); + Instant nowMinus5 = now.minus(5, ChronoUnit.MINUTES); + + MetaTest cm = + MetaTest.builder() + .commitTime(now) + .authorTime(nowMinus5) + .committer("committer@projectnessie.org") + .author("author@projectnessie.org") + .hash("beeffeed123412341234123412341234") + .message("Feed of beef") + .signedOffBy("signed-off@projectnessie.org") + .putProperties("prop-1", "value-1") + .putProperties("prop-2", "value-2") + .build(); + Val cmVal = reg.nativeToValue(cm); + assertThat(cmVal).isInstanceOf(ObjectT.class); + assertThat(cmVal.type().typeEnum()).isSameAs(TypeEnum.Object); + assertThat(cmVal.type().typeName()).isEqualTo(cm.getClass().getName()); + assertThat(cmVal.type().typeEnum()).isSameAs(TypeEnum.Object); + ObjectT cmObj = (ObjectT) cmVal; + assertThat(cmObj.isSet(stringOf("foo"))) + .isInstanceOf(Err.class) + .asString() + .isEqualTo("no such field 'foo'"); + assertThat(cmObj.isSet(stringOf("commitTime"))).isEqualTo(True); + assertThat(cmObj.isSet(stringOf("authorTime"))).isEqualTo(True); + assertThat(cmObj.isSet(stringOf("committer"))).isEqualTo(True); + assertThat(cmObj.isSet(stringOf("author"))).isEqualTo(True); + assertThat(cmObj.isSet(stringOf("hash"))).isEqualTo(True); + assertThat(cmObj.isSet(stringOf("message"))).isEqualTo(True); + assertThat(cmObj.isSet(stringOf("signedOffBy"))).isEqualTo(True); + assertThat(cmObj.isSet(stringOf("properties"))).isEqualTo(True); + Map expectMap = new HashMap<>(); + expectMap.put("prop-1", "value-1"); + expectMap.put("prop-2", "value-2"); + assertThat(cmObj.get(stringOf("foo"))) + .isInstanceOf(Err.class) + .asString() + .isEqualTo("no such field 'foo'"); + assertThat(cmObj.get(stringOf("commitTime"))).isEqualTo(timestampOf(now)); + assertThat(cmObj.get(stringOf("authorTime"))).isEqualTo(timestampOf(nowMinus5)); + assertThat(cmObj.get(stringOf("committer"))).isEqualTo(stringOf("committer@projectnessie.org")); + assertThat(cmObj.get(stringOf("author"))).isEqualTo(stringOf("author@projectnessie.org")); + assertThat(cmObj.get(stringOf("hash"))).isEqualTo(stringOf("beeffeed123412341234123412341234")); + assertThat(cmObj.get(stringOf("message"))).isEqualTo(stringOf("Feed of beef")); + assertThat(cmObj.get(stringOf("signedOffBy"))) + .isEqualTo(stringOf("signed-off@projectnessie.org")); + assertThat(cmObj.get(stringOf("properties"))).isEqualTo(newMaybeWrappedMap(reg, expectMap)); + } + + @Test + void nessieCommitMetaPart() { + TypeRegistry reg = Jackson3Registry.newRegistry(); + + Instant now = Instant.now(); + + MetaTest cm = + MetaTest.builder() + .commitTime(now) + .committer("committer@projectnessie.org") + .hash("beeffeed123412341234123412341234") + .message("Feed of beef") + .build(); + Val cmVal = reg.nativeToValue(cm); + assertThat(cmVal).isInstanceOf(ObjectT.class); + assertThat(cmVal.type().typeEnum()).isSameAs(TypeEnum.Object); + assertThat(cmVal.type().typeName()).isEqualTo(cm.getClass().getName()); + assertThat(cmVal.type().typeEnum()).isSameAs(TypeEnum.Object); + ObjectT cmObj = (ObjectT) cmVal; + assertThat(cmObj.isSet(stringOf("foo"))) + .isInstanceOf(Err.class) + .asString() + .isEqualTo("no such field 'foo'"); + assertThat(cmObj.isSet(stringOf("commitTime"))).isEqualTo(True); + assertThat(cmObj.isSet(stringOf("authorTime"))).isEqualTo(False); + assertThat(cmObj.isSet(stringOf("committer"))).isEqualTo(True); + assertThat(cmObj.isSet(stringOf("author"))).isEqualTo(False); + assertThat(cmObj.isSet(stringOf("hash"))).isEqualTo(True); + assertThat(cmObj.isSet(stringOf("message"))).isEqualTo(True); + assertThat(cmObj.isSet(stringOf("signedOffBy"))).isEqualTo(False); + assertThat(cmObj.isSet(stringOf("properties"))).isEqualTo(True); // just empty + assertThat(cmObj.get(stringOf("foo"))) + .isInstanceOf(Err.class) + .asString() + .isEqualTo("no such field 'foo'"); + assertThat(cmObj.get(stringOf("commitTime"))).isEqualTo(timestampOf(now)); + assertThat(cmObj.get(stringOf("authorTime"))).isEqualTo(NullValue); + assertThat(cmObj.get(stringOf("committer"))).isEqualTo(stringOf("committer@projectnessie.org")); + assertThat(cmObj.get(stringOf("author"))).isEqualTo(NullValue); + assertThat(cmObj.get(stringOf("hash"))).isEqualTo(stringOf("beeffeed123412341234123412341234")); + assertThat(cmObj.get(stringOf("message"))).isEqualTo(stringOf("Feed of beef")); + assertThat(cmObj.get(stringOf("signedOffBy"))).isEqualTo(NullValue); + assertThat(cmObj.get(stringOf("properties"))).isEqualTo(newMaybeWrappedMap(reg, emptyMap())); + } + + @Test + void copy() { + TypeRegistry reg = Jackson3Registry.newRegistry(); + assertThat(reg).extracting(TypeRegistry::copy).isSameAs(reg); + } + + @Test + void registerType() { + TypeRegistry reg = Jackson3Registry.newRegistry(); + assertThatThrownBy(() -> reg.registerType(IntT.IntType)) + .isInstanceOf(UnsupportedOperationException.class); + } +} diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3ScriptHostTest.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3ScriptHostTest.java new file mode 100644 index 00000000..977bd364 --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3ScriptHostTest.java @@ -0,0 +1,144 @@ +/* + * 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 java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.ObjectT; +import org.projectnessie.cel.tools.Script; +import org.projectnessie.cel.tools.ScriptHost; +import org.projectnessie.cel.types.jackson3.types.ClassWithEnum; +import org.projectnessie.cel.types.jackson3.types.ClassWithEnum.ClassEnum; +import org.projectnessie.cel.types.jackson3.types.MetaTest; +import org.projectnessie.cel.types.jackson3.types.MyPojo; +import org.projectnessie.cel.types.jackson3.types.ObjectListEnum; + +public class Jackson3ScriptHostTest { + + @Test + void simple() throws Exception { + ScriptHost scriptHost = + ScriptHost.newBuilder().registry(Jackson3Registry.newRegistry()).build(); + + Script script = + scriptHost + .buildScript("param.author == 'foo@bar.baz'") + .withDeclarations(Decls.newVar("param", Decls.newObjectType(MetaTest.class.getName()))) + .withTypes(MetaTest.class) + .build(); + + MetaTest cmMatch = MetaTest.builder().author("foo@bar.baz").build(); + MetaTest cmNoMatch = MetaTest.builder().author("foo@foo.foo").build(); + + assertThat(script.execute(Boolean.class, singletonMap("param", cmMatch))).isTrue(); + assertThat(script.execute(Boolean.class, singletonMap("param", cmNoMatch))).isFalse(); + + script = + scriptHost + .buildScript("param") + .withDeclarations(Decls.newVar("param", Decls.newObjectType(MetaTest.class.getName()))) + .withTypes(MetaTest.class) + .build(); + + assertThat(script.execute(Object.class, singletonMap("param", cmMatch))).isEqualTo(cmMatch); + assertThat(script.execute(ObjectT.class, singletonMap("param", cmMatch)).value()) + .isEqualTo(cmMatch); + } + + @Test + void readmeExample() throws Exception { + ScriptHost scriptHost = + ScriptHost.newBuilder().registry(Jackson3Registry.newRegistry()).build(); + + Script script = + scriptHost + .buildScript("inp.property == checkName") + .withDeclarations( + Decls.newVar("inp", Decls.newObjectType(MyPojo.class.getName())), + Decls.newVar("checkName", Decls.String)) + .withTypes(MyPojo.class) + .build(); + + MyPojo pojo = new MyPojo(); + pojo.setProperty("test"); + + String checkName = "test"; + + Map arguments = new HashMap<>(); + arguments.put("inp", pojo); + arguments.put("checkName", checkName); + + assertThat(script.execute(Boolean.class, arguments)).isTrue(); + } + + @Test + void complexInput() throws Exception { + ScriptHost scriptHost = + ScriptHost.newBuilder().registry(Jackson3Registry.newRegistry()).build(); + + Script script = + scriptHost + .buildScript( + "param.entries[0].type == org.projectnessie.cel.types.jackson3.types.ClassWithEnum.ClassEnum.VAL_2") + .withDeclarations( + Decls.newVar("param", Decls.newObjectType(ObjectListEnum.class.getName()))) + .withTypes(ObjectListEnum.class) + .build(); + + ObjectListEnum val = + ObjectListEnum.builder() + .addEntries( + ObjectListEnum.Entry.builder() + .type(ClassEnum.VAL_2) + .holder(new ClassWithEnum("foo")) + .build()) + .build(); + + assertThat(script.execute(Boolean.class, singletonMap("param", val))).isTrue(); + + // same as above, but use the 'container' + + script = + scriptHost + .buildScript("param.entries[0].type == ClassWithEnum.ClassEnum.VAL_2") + .withDeclarations( + Decls.newVar("param", Decls.newObjectType(ObjectListEnum.class.getName()))) + .withContainer("org.projectnessie.cel.types.jackson3.types") + .withTypes(ObjectListEnum.class) + .build(); + + assertThat(script.execute(Boolean.class, singletonMap("param", val))).isTrue(); + + // return the enum + + script = + scriptHost + .buildScript("param.entries[0].type") + .withDeclarations( + Decls.newVar("param", Decls.newObjectType(ObjectListEnum.class.getName()))) + .withContainer("org.projectnessie.cel.types.jackson3.types") + .withTypes(ObjectListEnum.class) + .build(); + + assertThat(script.execute(Integer.class, singletonMap("param", val))) + .isEqualTo(ClassEnum.VAL_2.ordinal()); + } +} 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 new file mode 100644 index 00000000..854aacc2 --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3TypeDescriptionTest.java @@ -0,0 +1,451 @@ +/* + * 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 java.util.Arrays.asList; +import static java.util.Collections.singletonMap; +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.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.types.jackson3.Jackson3Registry.newRegistry; + +import com.google.api.expr.v1alpha1.Type.ListType; +import com.google.api.expr.v1alpha1.Type.MapType; +import com.google.api.expr.v1alpha1.Type.TypeKindCase; +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.Err; +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.NullT; +import org.projectnessie.cel.common.types.ObjectT; +import org.projectnessie.cel.common.types.TypeT; +import org.projectnessie.cel.common.types.pb.Checked; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.types.jackson3.types.AnEnum; +import org.projectnessie.cel.types.jackson3.types.CollectionsObject; +import org.projectnessie.cel.types.jackson3.types.InnerType; +import tools.jackson.databind.JavaType; + +class Jackson3TypeDescriptionTest { + + @Test + void basics() { + Jackson3Registry reg = (Jackson3Registry) newRegistry(); + + reg.register(CollectionsObject.class); + com.google.api.expr.v1alpha1.Type t = reg.findType(CollectionsObject.class.getName()); + assertThat(t) + .extracting( + com.google.api.expr.v1alpha1.Type::getMessageType, + com.google.api.expr.v1alpha1.Type::getTypeKindCase) + .containsExactly(CollectionsObject.class.getName(), TypeKindCase.MESSAGE_TYPE); + + JacksonTypeDescription td = reg.typeDescription(CollectionsObject.class); + assertThat(td) + .extracting( + JacksonTypeDescription::pbType, + JacksonTypeDescription::reflectType, + JacksonTypeDescription::name, + JacksonTypeDescription::type) + .containsExactly( + t, + CollectionsObject.class, + CollectionsObject.class.getName(), + TypeT.newObjectTypeValue(CollectionsObject.class.getName())); + + // check that the nested-class `InnerType` has been implicitly registered + + JacksonTypeDescription tdInner = reg.typeDescription(InnerType.class); + assertThat(tdInner) + .extracting( + JacksonTypeDescription::pbType, + JacksonTypeDescription::reflectType, + JacksonTypeDescription::name, + JacksonTypeDescription::type) + .containsExactly( + com.google.api.expr.v1alpha1.Type.newBuilder() + .setMessageType(InnerType.class.getName()) + .build(), + InnerType.class, + InnerType.class.getName(), + TypeT.newObjectTypeValue(InnerType.class.getName())); + + // + + assertThat(reg) + .extracting( + r -> r.findIdent(CollectionsObject.class.getName()), + r -> r.findIdent(InnerType.class.getName()), + r -> r.findIdent(AnEnum.class.getName() + '.' + AnEnum.ENUM_VALUE_2.name())) + .containsExactly( + TypeT.newObjectTypeValue(CollectionsObject.class.getName()), + TypeT.newObjectTypeValue(InnerType.class.getName()), + intOf(AnEnum.ENUM_VALUE_2.ordinal())); + + assertThatThrownBy(() -> reg.typeDescription(AnEnum.class)) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> reg.enumDescription(InnerType.class)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void types() { + Jackson3Registry reg = (Jackson3Registry) newRegistry(); + reg.register(CollectionsObject.class); + + // verify the map-type-fields + + checkMapType( + reg, + "stringBooleanMap", + String.class, + Checked.checkedString, + Boolean.class, + Checked.checkedBool); + checkMapType( + reg, "byteShortMap", Byte.class, Checked.checkedInt, Short.class, Checked.checkedInt); + checkMapType( + reg, "intLongMap", Integer.class, Checked.checkedInt, Long.class, Checked.checkedInt); + checkMapType( + reg, + "ulongTimestampMap", + ULong.class, + Checked.checkedUint, + Timestamp.class, + Checked.checkedTimestamp); + checkMapType( + reg, + "ulongZonedDateTimeMap", + ULong.class, + Checked.checkedUint, + ZonedDateTime.class, + Checked.checkedTimestamp); + checkMapType( + reg, + "stringProtoDurationMap", + String.class, + Checked.checkedString, + Duration.class, + Checked.checkedDuration); + checkMapType( + reg, + "stringJavaDurationMap", + String.class, + Checked.checkedString, + java.time.Duration.class, + Checked.checkedDuration); + checkMapType( + reg, + "stringBytesMap", + String.class, + Checked.checkedString, + ByteString.class, + Checked.checkedBytes); + checkMapType( + reg, + "floatDoubleMap", + Float.class, + Checked.checkedDouble, + Double.class, + Checked.checkedDouble); + + // verify the list-type-fields + + checkListType(reg, "stringList", String.class, Checked.checkedString); + checkListType(reg, "booleanList", Boolean.class, Checked.checkedBool); + checkListType(reg, "byteList", Byte.class, Checked.checkedInt); + checkListType(reg, "shortList", Short.class, Checked.checkedInt); + checkListType(reg, "intList", Integer.class, Checked.checkedInt); + checkListType(reg, "longList", Long.class, Checked.checkedInt); + checkListType(reg, "ulongList", ULong.class, Checked.checkedUint); + checkListType(reg, "timestampList", Timestamp.class, Checked.checkedTimestamp); + checkListType(reg, "zonedDateTimeList", ZonedDateTime.class, Checked.checkedTimestamp); + checkListType(reg, "durationList", Duration.class, Checked.checkedDuration); + checkListType(reg, "javaDurationList", java.time.Duration.class, Checked.checkedDuration); + checkListType(reg, "bytesList", ByteString.class, Checked.checkedBytes); + checkListType(reg, "floatList", Float.class, Checked.checkedDouble); + checkListType(reg, "doubleList", Double.class, Checked.checkedDouble); + } + + private void checkListType( + Jackson3Registry reg, + String prop, + Class valueClass, + com.google.api.expr.v1alpha1.Type valueType) { + JacksonFieldType ft = + (JacksonFieldType) reg.findFieldType(CollectionsObject.class.getName(), prop); + assertThat(ft).isNotNull(); + JavaType javaType = ft.propertyWriter().getType(); + + assertThat(javaType).extracting(JavaType::isCollectionLikeType).isEqualTo(true); + assertThat(javaType.getContentType()).extracting(JavaType::getRawClass).isSameAs(valueClass); + + assertThat(ft.type) + .extracting(com.google.api.expr.v1alpha1.Type::getListType) + .extracting(ListType::getElemType) + .isSameAs(valueType); + } + + private void checkMapType( + Jackson3Registry reg, + String prop, + Class keyClass, + com.google.api.expr.v1alpha1.Type keyType, + Class valueClass, + com.google.api.expr.v1alpha1.Type valueType) { + JacksonFieldType ft = + (JacksonFieldType) reg.findFieldType(CollectionsObject.class.getName(), prop); + assertThat(ft).isNotNull(); + JavaType javaType = ft.propertyWriter().getType(); + + assertThat(javaType).extracting(JavaType::isMapLikeType).isEqualTo(true); + assertThat(javaType.getKeyType()).extracting(JavaType::getRawClass).isSameAs(keyClass); + assertThat(javaType.getContentType()).extracting(JavaType::getRawClass).isSameAs(valueClass); + + assertThat(ft.type) + .extracting(com.google.api.expr.v1alpha1.Type::getMapType) + .extracting(MapType::getKeyType, MapType::getValueType) + .containsExactly(keyType, valueType); + } + + @Test + void unknownProperties() { + CollectionsObject collectionsObject = new CollectionsObject(); + + Jackson3Registry reg = (Jackson3Registry) newRegistry(); + reg.register(CollectionsObject.class); + + Val collectionsVal = reg.nativeToValue(collectionsObject); + assertThat(collectionsVal).isInstanceOf(ObjectT.class); + ObjectT obj = (ObjectT) collectionsVal; + + Val x = obj.isSet(stringOf("bart")); + assertThat(x) + .isInstanceOf(Err.class) + .extracting(e -> (Err) e) + .extracting(Err::value) + .isEqualTo("no such field 'bart'"); + + x = obj.get(stringOf("bart")); + assertThat(x) + .isInstanceOf(Err.class) + .extracting(e -> (Err) e) + .extracting(Err::value) + .isEqualTo("no such field 'bart'"); + } + + @Test + void collectionsObjectEmpty() { + CollectionsObject collectionsObject = new CollectionsObject(); + + Jackson3Registry reg = (Jackson3Registry) newRegistry(); + reg.register(CollectionsObject.class); + + Val collectionsVal = reg.nativeToValue(collectionsObject); + assertThat(collectionsVal).isInstanceOf(ObjectT.class); + ObjectT obj = (ObjectT) collectionsVal; + + for (String field : CollectionsObject.ALL_PROPERTIES) { + assertThat(obj.isSet(stringOf(field))).isSameAs(False); + assertThat(obj.get(stringOf(field))).isSameAs(NullT.NullValue); + } + } + + @Test + void collectionsObjectTypeTest() throws Exception { + CollectionsObject collectionsObject = new CollectionsObject(); + + // populate (primitive) map types + + collectionsObject.stringBooleanMap = singletonMap("a", true); + collectionsObject.byteShortMap = singletonMap((byte) 1, (short) 2); + collectionsObject.intLongMap = singletonMap(1, 2L); + collectionsObject.ulongTimestampMap = + singletonMap(ULong.valueOf(1), Timestamp.newBuilder().setSeconds(1).build()); + collectionsObject.ulongZonedDateTimeMap = + singletonMap( + ULong.valueOf(1), + ZonedDateTime.of(LocalDateTime.ofEpochSecond(1, 0, ZoneOffset.UTC), ZoneId.of("UTC"))); + collectionsObject.stringProtoDurationMap = + singletonMap("a", Duration.newBuilder().setSeconds(1).build()); + collectionsObject.stringJavaDurationMap = singletonMap("a", java.time.Duration.ofSeconds(1)); + collectionsObject.stringBytesMap = + singletonMap("a", ByteString.copyFrom(new byte[] {(byte) 1})); + collectionsObject.floatDoubleMap = singletonMap(1f, 2d); + + // populate (primitive) list types + + collectionsObject.stringList = asList("a", "b", "c"); + collectionsObject.booleanList = asList(true, true, false, false); + collectionsObject.byteList = asList((byte) 1, (byte) 2, (byte) 3); + collectionsObject.shortList = asList((short) 4, (short) 5, (short) 6); + collectionsObject.intList = asList(7, 8, 9); + collectionsObject.longList = asList(10L, 11L, 12L); + collectionsObject.ulongList = asList(ULong.valueOf(1), ULong.valueOf(2), ULong.valueOf(3)); + collectionsObject.timestampList = + asList( + Timestamp.newBuilder().setSeconds(1).build(), + Timestamp.newBuilder().setSeconds(2).build(), + Timestamp.newBuilder().setSeconds(3).build()); + collectionsObject.zonedDateTimeList = + asList( + ZonedDateTime.of(LocalDateTime.ofEpochSecond(1, 0, ZoneOffset.UTC), ZoneId.of("UTC")), + ZonedDateTime.of(LocalDateTime.ofEpochSecond(2, 0, ZoneOffset.UTC), ZoneId.of("UTC")), + ZonedDateTime.of(LocalDateTime.ofEpochSecond(3, 0, ZoneOffset.UTC), ZoneId.of("UTC"))); + collectionsObject.durationList = + asList( + Duration.newBuilder().setSeconds(1).build(), + Duration.newBuilder().setSeconds(2).build(), + Duration.newBuilder().setSeconds(3).build()); + collectionsObject.javaDurationList = + asList( + java.time.Duration.ofSeconds(1), + java.time.Duration.ofSeconds(2), + java.time.Duration.ofSeconds(3)); + collectionsObject.bytesList = + asList( + ByteString.copyFrom(new byte[] {(byte) 1}), + ByteString.copyFrom(new byte[] {(byte) 2}), + ByteString.copyFrom(new byte[] {(byte) 3})); + collectionsObject.floatList = asList(1f, 2f, 3f); + collectionsObject.doubleList = asList(1d, 2d, 3d); + + // populate inner/nested type list/map + + InnerType inner1 = new InnerType(); + inner1.intProp = 1; + inner1.wrappedIntProp = 2; + collectionsObject.stringInnerMap = singletonMap("a", inner1); + + InnerType inner2 = new InnerType(); + inner2.intProp = 3; + inner2.wrappedIntProp = 4; + collectionsObject.innerTypes = asList(inner1, inner2); + + // populate enum-related fields + + collectionsObject.anEnum = AnEnum.ENUM_VALUE_2; + 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); + + // prepare registry + + Jackson3Registry reg = (Jackson3Registry) newRegistry(); + reg.register(CollectionsObject.class); + + Val collectionsVal = reg.nativeToValue(collectionsObject); + assertThat(collectionsVal).isInstanceOf(ObjectT.class); + ObjectT obj = (ObjectT) collectionsVal; + + // briefly verify all fields + + for (String field : CollectionsObject.ALL_PROPERTIES) { + assertThat(obj.isSet(stringOf(field))).isSameAs(True); + assertThat(obj.get(stringOf(field))).isNotNull(); + + Val fieldVal = obj.get(stringOf(field)); + Object fieldObj = CollectionsObject.class.getDeclaredField(field).get(collectionsObject); + if (fieldObj instanceof Map) { + assertThat(fieldVal).isInstanceOf(MapT.class); + } else if (fieldObj instanceof List) { + assertThat(fieldVal).isInstanceOf(ListT.class); + } + + assertThat(fieldVal.equal(reg.nativeToValue(fieldObj))).isSameAs(True); + } + + // check a few properties manually/explicitly + + MapT mapVal = (MapT) obj.get(stringOf("intLongMap")); + assertThat(mapVal) + .extracting( + MapT::size, + m -> m.contains(intOf(42)), + m -> m.contains(intOf(1)), + m -> m.contains(intOf(2)), + m -> m.contains(intOf(3)), + m -> m.get(intOf(1))) + .containsExactly(intOf(1), False, True, False, False, intOf(2)); + + ListT listVal = (ListT) obj.get(stringOf("ulongList")); + assertThat(listVal) + .extracting( + ListT::size, + l -> l.contains(uintOf(42)), + l -> l.contains(uintOf(1)), + l -> l.contains(uintOf(2)), + l -> l.contains(uintOf(3)), + l -> l.get(intOf(0)), + l -> l.get(intOf(1)), + l -> l.get(intOf(2))) + .containsExactly(intOf(3), False, True, True, True, uintOf(1), uintOf(2), uintOf(3)); + + mapVal = (MapT) obj.get(stringOf("stringInnerMap")); + assertThat(mapVal) + .extracting(MapT::size, m -> m.contains(stringOf("42")), m -> m.contains(stringOf("a"))) + .containsExactly(intOf(1), False, True); + ObjectT i = (ObjectT) mapVal.get(stringOf("a")); + assertThat(i) + .extracting(o -> o.get(stringOf("intProp")), o -> o.get(stringOf("wrappedIntProp"))) + .containsExactly(intOf(1), intOf(2)); + + listVal = (ListT) obj.get(stringOf("innerTypes")); + assertThat(listVal).extracting(ListT::size).isEqualTo(intOf(2)); + i = (ObjectT) listVal.get(intOf(0)); + assertThat(i) + .extracting(o -> o.get(stringOf("intProp")), o -> o.get(stringOf("wrappedIntProp"))) + .containsExactly(intOf(1), intOf(2)); + i = (ObjectT) listVal.get(intOf(1)); + 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")); + assertThat(x).isInstanceOf(IntT.class).isEqualTo(intOf(AnEnum.ENUM_VALUE_2.ordinal())); + listVal = (ListT) obj.get(stringOf("anEnumList")); + assertThat(listVal) + .extracting(l -> l.get(intOf(0)), l -> l.get(intOf(1))) + .containsExactly( + intOf(AnEnum.ENUM_VALUE_2.ordinal()), intOf(AnEnum.ENUM_VALUE_3.ordinal())); + mapVal = (MapT) obj.get(stringOf("anEnumStringMap")); + assertThat(mapVal) + .extracting(l -> l.get(intOf(AnEnum.ENUM_VALUE_2.ordinal()))) + .isEqualTo(stringOf("a")); + mapVal = (MapT) obj.get(stringOf("stringAnEnumMap")); + assertThat(mapVal) + .extracting(l -> l.get(stringOf("a"))) + .isEqualTo(intOf(AnEnum.ENUM_VALUE_2.ordinal())); + } +} diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/AnEnum.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/AnEnum.java new file mode 100644 index 00000000..f1e93b00 --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/AnEnum.java @@ -0,0 +1,22 @@ +/* + * 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.types; + +public enum AnEnum { + ENUM_VALUE_1, + ENUM_VALUE_2, + ENUM_VALUE_3 +} diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/ClassWithEnum.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/ClassWithEnum.java new file mode 100644 index 00000000..40ae9c9c --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/ClassWithEnum.java @@ -0,0 +1,33 @@ +/* + * 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.types; + +public class ClassWithEnum { + public enum ClassEnum { + VAL_1, + VAL_2, + VAL_3, + VAL_4 + } + + public String value; + + public ClassWithEnum() {} + + public ClassWithEnum(String value) { + this.value = value; + } +} 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 new file mode 100644 index 00000000..9b0cca97 --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/CollectionsObject.java @@ -0,0 +1,93 @@ +/* + * 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.types; + +import static java.util.Arrays.asList; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import org.projectnessie.cel.common.ULong; + +public class CollectionsObject { + public Map stringBooleanMap; + public Map byteShortMap; + public Map intLongMap; + public Map ulongTimestampMap; + public Map ulongZonedDateTimeMap; + public Map stringProtoDurationMap; + public Map stringJavaDurationMap; + public Map stringBytesMap; + public Map floatDoubleMap; + + public List stringList; + public List booleanList; + public List byteList; + public List shortList; + public List intList; + public List longList; + public List ulongList; + public List timestampList; + public List zonedDateTimeList; + public List durationList; + public List javaDurationList; + public List bytesList; + public List floatList; + public List doubleList; + + public Map stringInnerMap; + public List innerTypes; + + public AnEnum anEnum; + public List anEnumList; + public Map anEnumStringMap; + public Map stringAnEnumMap; + + public static final List ALL_PROPERTIES = + asList( + "stringBooleanMap", + "byteShortMap", + "intLongMap", + "ulongTimestampMap", + "ulongZonedDateTimeMap", + "stringProtoDurationMap", + "stringJavaDurationMap", + "stringBytesMap", + "floatDoubleMap", + "stringList", + "booleanList", + "byteList", + "shortList", + "intList", + "longList", + "ulongList", + "timestampList", + "zonedDateTimeList", + "durationList", + "javaDurationList", + "bytesList", + "floatList", + "doubleList", + "stringInnerMap", + "innerTypes", + "anEnum", + "anEnumList", + "anEnumStringMap", + "stringAnEnumMap"); +} diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/InnerType.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/InnerType.java new file mode 100644 index 00000000..7beedd09 --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/InnerType.java @@ -0,0 +1,21 @@ +/* + * 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.types; + +public class InnerType { + public int intProp; + public Integer wrappedIntProp; +} diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/MetaTest.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/MetaTest.java new file mode 100644 index 00000000..67be1277 --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/MetaTest.java @@ -0,0 +1,108 @@ +/* + * 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.types; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import javax.annotation.Nullable; +import org.immutables.value.Value; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.deser.std.StdDeserializer; +import tools.jackson.databind.ser.std.StdSerializer; + +@Value.Immutable(prehash = true) +@JsonSerialize(as = ImmutableMetaTest.class) +@JsonDeserialize(as = ImmutableMetaTest.class) +public abstract class MetaTest { + + @Nullable + public abstract String getHash(); + + @Nullable + public abstract String getCommitter(); + + @Nullable + public abstract String getAuthor(); + + @Nullable + public abstract String getSignedOffBy(); + + @Nullable + public abstract String getMessage(); + + @Nullable + @JsonSerialize(using = InstantSerializer.class) + @JsonDeserialize(using = InstantDeserializer.class) + public abstract Instant getCommitTime(); + + @Nullable + @JsonSerialize(using = InstantSerializer.class) + @JsonDeserialize(using = InstantDeserializer.class) + public abstract Instant getAuthorTime(); + + public abstract Map getProperties(); + + public ImmutableMetaTest.Builder toBuilder() { + return ImmutableMetaTest.builder().from(this); + } + + public static ImmutableMetaTest.Builder builder() { + return ImmutableMetaTest.builder(); + } + + public static MetaTest fromMessage(String message) { + return ImmutableMetaTest.builder().message(message).build(); + } + + public static class InstantSerializer extends StdSerializer { + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_INSTANT; + + public InstantSerializer() { + this(Instant.class); + } + + protected InstantSerializer(Class t) { + super(t); + } + + @Override + public void serialize( + Instant value, JsonGenerator gen, SerializationContext serializationContext) { + gen.writeString(FORMATTER.format(value)); + } + } + + public static class InstantDeserializer extends StdDeserializer { + public InstantDeserializer() { + this(null); + } + + protected InstantDeserializer(Class vc) { + super(vc); + } + + @Override + public Instant deserialize(JsonParser p, DeserializationContext ctxt) { + return Instant.parse(p.getString()); + } + } +} diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/MyPojo.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/MyPojo.java new file mode 100644 index 00000000..1347abf7 --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/MyPojo.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 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.types; + +public class MyPojo { + private String property; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } +} diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/ObjectListEnum.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/ObjectListEnum.java new file mode 100644 index 00000000..a1c2d906 --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/ObjectListEnum.java @@ -0,0 +1,41 @@ +/* + * 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.types; + +import java.util.List; +import org.immutables.value.Value; + +@Value.Immutable(prehash = true) +public interface ObjectListEnum { + + static ImmutableObjectListEnum.Builder builder() { + return ImmutableObjectListEnum.builder(); + } + + List getEntries(); + + @Value.Immutable(prehash = true) + interface Entry { + + static ImmutableEntry.Builder builder() { + return ImmutableEntry.builder(); + } + + ClassWithEnum.ClassEnum getType(); + + ClassWithEnum getHolder(); + } +} diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefBase.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefBase.java new file mode 100644 index 00000000..4a091fff --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefBase.java @@ -0,0 +1,30 @@ +/* + * 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.types; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.Nullable; + +@JsonSubTypes({@Type(RefVariantB.class), @Type(RefVariantA.class), @Type(RefVariantC.class)}) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") +public interface RefBase { + String getName(); + + @Nullable + String getHash(); +} diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefVariantA.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefVariantA.java new file mode 100644 index 00000000..2304bef2 --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefVariantA.java @@ -0,0 +1,36 @@ +/* + * 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.types; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.immutables.value.Value; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; + +@Value.Immutable(prehash = true) +@JsonSerialize(as = ImmutableRefVariantA.class) +@JsonDeserialize(as = ImmutableRefVariantA.class) +@JsonTypeName("A") +public interface RefVariantA extends RefBase { + + static ImmutableRefVariantA.Builder builder() { + return ImmutableRefVariantA.builder(); + } + + static RefVariantA of(String name, String hash) { + return builder().name(name).hash(hash).build(); + } +} diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefVariantB.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefVariantB.java new file mode 100644 index 00000000..f78620c2 --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefVariantB.java @@ -0,0 +1,37 @@ +/* + * 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.types; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.immutables.value.Value; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; + +/** Api representation of an Nessie Tag/Branch. This object is akin to a Ref in Git terminology. */ +@Value.Immutable(prehash = true) +@JsonSerialize(as = ImmutableRefVariantB.class) +@JsonDeserialize(as = ImmutableRefVariantB.class) +@JsonTypeName("B") +public interface RefVariantB extends RefBase { + + static ImmutableRefVariantB.Builder builder() { + return ImmutableRefVariantB.builder(); + } + + static RefVariantB of(String name, String hash) { + return builder().name(name).hash(hash).build(); + } +} diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefVariantC.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefVariantC.java new file mode 100644 index 00000000..01a890fd --- /dev/null +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/types/RefVariantC.java @@ -0,0 +1,39 @@ +/* + * 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.types; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.immutables.value.Value; +import org.immutables.value.Value.Derived; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; + +@Value.Immutable(prehash = true) +@JsonSerialize(as = ImmutableRefVariantC.class) +@JsonDeserialize(as = ImmutableRefVariantC.class) +@JsonTypeName("C") +public abstract class RefVariantC implements RefBase { + + @Override + @Derived + public String getHash() { + return getName(); + } + + public static RefVariantC of(String hash) { + return ImmutableRefVariantC.builder().name(hash).build(); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e985dd46..577c237e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -101,6 +101,8 @@ celProject("core") celProject("jackson") +celProject("jackson3") + celProject("conformance") celProject("tools") diff --git a/standalone/build.gradle.kts b/standalone/build.gradle.kts index 9b63039d..493c41b2 100644 --- a/standalone/build.gradle.kts +++ b/standalone/build.gradle.kts @@ -48,6 +48,7 @@ shadowJar.configure { include(project(":cel-tools")) include(project(":cel-core")) include(project(":cel-jackson")) + include(project(":cel-jackson3")) include(project(":cel-generated-pb")) include(project(":cel-generated-antlr"))