From 240246ac5fdac52fa8885ab6952092c0e64c7a79 Mon Sep 17 00:00:00 2001 From: Aaron Steinfeld Date: Tue, 25 Aug 2020 15:32:40 -0700 Subject: [PATCH 1/8] add attribute projections --- attribute-projections/build.gradle.kts | 15 ++++++++ .../projection/AttributeProjection.java | 12 +++++++ .../AttributeProjectionRegistry.java | 27 ++++++++++++++ .../service/projection/DefaultValue.java | 30 ++++++++++++++++ .../projection/StringConcatenation.java | 35 +++++++++++++++++++ .../service/projection/StringHash.java | 35 +++++++++++++++++++ settings.gradle.kts | 1 + 7 files changed, 155 insertions(+) create mode 100644 attribute-projections/build.gradle.kts create mode 100644 attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjection.java create mode 100644 attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistry.java create mode 100644 attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/DefaultValue.java create mode 100644 attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringConcatenation.java create mode 100644 attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringHash.java diff --git a/attribute-projections/build.gradle.kts b/attribute-projections/build.gradle.kts new file mode 100644 index 00000000..c35dd36c --- /dev/null +++ b/attribute-projections/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + `java-library` + jacoco + id("org.hypertrace.jacoco-report-plugin") + id("org.hypertrace.publish-plugin") +} + +dependencies { + api(project(":attribute-service-api")) + implementation("com.github.f4b6a3:uuid-creator:2.7.7") +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjection.java b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjection.java new file mode 100644 index 00000000..cb3e6301 --- /dev/null +++ b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjection.java @@ -0,0 +1,12 @@ +package org.hypertrace.core.attribute.service.projection; + +import java.util.List; +import org.hypertrace.core.attribute.service.v1.AttributeKind; +import org.hypertrace.core.attribute.service.v1.LiteralValue; + +public interface AttributeProjection { + + AttributeKind getArgumentKindAtIndex(int index); + + Object project(List arguments); +} diff --git a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistry.java b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistry.java new file mode 100644 index 00000000..05706c74 --- /dev/null +++ b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistry.java @@ -0,0 +1,27 @@ +package org.hypertrace.core.attribute.service.projection; + +import static org.hypertrace.core.attribute.service.projection.DefaultValue.DEFAULT_VALUE; +import static org.hypertrace.core.attribute.service.projection.StringConcatenation.STRING_CONCATENATION; +import static org.hypertrace.core.attribute.service.projection.StringHash.STRING_HASH; +import static org.hypertrace.core.attribute.service.v1.ProjectionOperator.PROJECTION_OPERATOR_CONCAT; +import static org.hypertrace.core.attribute.service.v1.ProjectionOperator.PROJECTION_OPERATOR_DEFAULT; +import static org.hypertrace.core.attribute.service.v1.ProjectionOperator.PROJECTION_OPERATOR_HASH; + +import java.util.Map; +import java.util.Optional; +import org.hypertrace.core.attribute.service.v1.ProjectionOperator; + +public class AttributeProjectionRegistry { + + private static final Map PROJECTION_MAP = + Map.of( + PROJECTION_OPERATOR_CONCAT, STRING_CONCATENATION, + PROJECTION_OPERATOR_HASH, STRING_HASH, + PROJECTION_OPERATOR_DEFAULT, DEFAULT_VALUE); + + public Optional getProjection(ProjectionOperator projectionOperator) { + return Optional.ofNullable(PROJECTION_MAP.get(projectionOperator)); + } + + +} diff --git a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/DefaultValue.java b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/DefaultValue.java new file mode 100644 index 00000000..327cea31 --- /dev/null +++ b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/DefaultValue.java @@ -0,0 +1,30 @@ +package org.hypertrace.core.attribute.service.projection; + +import java.util.List; +import org.hypertrace.core.attribute.service.v1.AttributeKind; + +public class DefaultValue implements AttributeProjection { + + public static DefaultValue DEFAULT_VALUE = new DefaultValue(); + + public static Object defaultValue(List values) { + for (Object value : values) { + if (value != null) { + return value; + } + } + return null; + } + + private DefaultValue() {} + + @Override + public AttributeKind getArgumentKindAtIndex(int index) { + return AttributeKind.KIND_UNDEFINED; + } + + @Override + public Object project(List arguments) { + return defaultValue(arguments); + } +} diff --git a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringConcatenation.java b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringConcatenation.java new file mode 100644 index 00000000..e885d781 --- /dev/null +++ b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringConcatenation.java @@ -0,0 +1,35 @@ +package org.hypertrace.core.attribute.service.projection; + +import java.util.List; +import org.hypertrace.core.attribute.service.v1.AttributeKind; + +public class StringConcatenation implements AttributeProjection { + public static StringConcatenation STRING_CONCATENATION = new StringConcatenation(); + + public static String concatenate(List strings) { + if (strings.size() == 1) { + return strings.get(0); + } + if (strings.size() == 2) { + return strings.get(0) + strings.get(1); + } + StringBuilder builder = new StringBuilder(); + for (String string : strings) { + builder.append(string); + } + return builder.toString(); + } + + private StringConcatenation() {} + + @Override + public AttributeKind getArgumentKindAtIndex(int index) { + return AttributeKind.TYPE_STRING; + } + + @Override + @SuppressWarnings("unchecked") + public Object project(List arguments) { + return concatenate((List) (Object) arguments); + } +} diff --git a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringHash.java b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringHash.java new file mode 100644 index 00000000..b05c0652 --- /dev/null +++ b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringHash.java @@ -0,0 +1,35 @@ +package org.hypertrace.core.attribute.service.projection; + +import static org.hypertrace.core.attribute.service.projection.StringConcatenation.concatenate; + +import com.github.f4b6a3.uuid.UuidCreator; +import com.github.f4b6a3.uuid.creator.rfc4122.NameBasedSha1UuidCreator; +import java.util.List; +import java.util.UUID; +import org.hypertrace.core.attribute.service.v1.AttributeKind; + +public class StringHash implements AttributeProjection { + + private static final NameBasedSha1UuidCreator HASHER = + UuidCreator.getNameBasedSha1Creator() + .withNamespace(UUID.fromString("5088c92d-5e9c-43f4-a35b-2589474d5642")); + + public static StringHash STRING_HASH = new StringHash(); + + public static String hash(List strings) { + return HASHER.create(concatenate(strings)).toString(); + } + + private StringHash() {} + + @Override + public AttributeKind getArgumentKindAtIndex(int index) { + return AttributeKind.TYPE_STRING; + } + + @Override + @SuppressWarnings("unchecked") + public Object project(List arguments) { + return hash((List) (Object) arguments); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c3bfd7a0..06e98052 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,3 +18,4 @@ include(":attribute-service-impl") include(":attribute-service") include(":attribute-service-tenant-api") include(":caching-attribute-service-client") +include(":attribute-projections") From 37cd76214dcd6a2e587dd1971e2cff33a845f570 Mon Sep 17 00:00:00 2001 From: Aaron Steinfeld Date: Fri, 11 Sep 2020 12:23:19 -0400 Subject: [PATCH 2/8] refactor: move type conversion inside attribute projections --- .gitignore | 6 +- .../build.gradle.kts | 4 +- .../projection/functions/Concatenate.java | 18 ++ .../projection/functions/DefaultValue.java | 13 + .../service/projection/functions/Hash.java | 20 ++ .../projection/functions/ConcatenateTest.java | 23 ++ .../functions/DefaultValueTest.java | 21 ++ .../projection/functions/HashTest.java | 21 ++ .../build.gradle.kts | 17 ++ .../AbstractAttributeProjection.java | 41 ++++ .../projection/AttributeProjection.java | 17 ++ .../AttributeProjectionRegistry.java | 21 +- .../projection/BinaryAttributeProjection.java | 24 ++ .../projection/UnaryAttributeProjection.java | 23 ++ .../service/projection/ValueCoercer.java | 230 ++++++++++++++++++ .../AttributeProjectionRegistryTest.java | 28 +++ .../projection/AttributeProjection.java | 12 - .../service/projection/DefaultValue.java | 30 --- .../projection/StringConcatenation.java | 35 --- .../service/projection/StringHash.java | 35 --- .../service/v1/attribute_metadata.proto | 7 +- settings.gradle.kts | 4 +- 22 files changed, 518 insertions(+), 132 deletions(-) rename {attribute-projections => attribute-projection-functions}/build.gradle.kts (67%) create mode 100644 attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/Concatenate.java create mode 100644 attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/DefaultValue.java create mode 100644 attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/Hash.java create mode 100644 attribute-projection-functions/src/test/java/org/hypertrace/core/attribute/service/projection/functions/ConcatenateTest.java create mode 100644 attribute-projection-functions/src/test/java/org/hypertrace/core/attribute/service/projection/functions/DefaultValueTest.java create mode 100644 attribute-projection-functions/src/test/java/org/hypertrace/core/attribute/service/projection/functions/HashTest.java create mode 100644 attribute-projection-registry/build.gradle.kts create mode 100644 attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java create mode 100644 attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjection.java rename {attribute-projections => attribute-projection-registry}/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistry.java (52%) create mode 100644 attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/BinaryAttributeProjection.java create mode 100644 attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/UnaryAttributeProjection.java create mode 100644 attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/ValueCoercer.java create mode 100644 attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistryTest.java delete mode 100644 attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjection.java delete mode 100644 attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/DefaultValue.java delete mode 100644 attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringConcatenation.java delete mode 100644 attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringHash.java diff --git a/.gitignore b/.gitignore index f86f577e..0ca52e8f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,11 +16,7 @@ test-output *.patch *.log.gz *.code-workspace -.idea/*.xml -.idea/libraries/ -.idea/dictionaries/ -.idea/codeStyles/ -.idea/.name +.idea # Local config to handle using Java 8 vs java 11. .java-version *.tgz diff --git a/attribute-projections/build.gradle.kts b/attribute-projection-functions/build.gradle.kts similarity index 67% rename from attribute-projections/build.gradle.kts rename to attribute-projection-functions/build.gradle.kts index c35dd36c..1c6f0d81 100644 --- a/attribute-projections/build.gradle.kts +++ b/attribute-projection-functions/build.gradle.kts @@ -6,8 +6,10 @@ plugins { } dependencies { - api(project(":attribute-service-api")) + api("com.google.code.findbugs:jsr305:3.0.2") implementation("com.github.f4b6a3:uuid-creator:2.7.7") + + testImplementation("org.junit.jupiter:junit-jupiter:5.6.2") } tasks.test { diff --git a/attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/Concatenate.java b/attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/Concatenate.java new file mode 100644 index 00000000..5a648587 --- /dev/null +++ b/attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/Concatenate.java @@ -0,0 +1,18 @@ +package org.hypertrace.core.attribute.service.projection.functions; + +import static java.util.Objects.isNull; +import static java.util.Objects.requireNonNullElse; + +import javax.annotation.Nullable; + +public class Concatenate { + private static final String DEFAULT_STRING = ""; + + @Nullable + public static String concatenate(@Nullable String first, @Nullable String second) { + if (isNull(first) && isNull(second)) { + return null; + } + return requireNonNullElse(first, DEFAULT_STRING) + requireNonNullElse(second, DEFAULT_STRING); + } +} diff --git a/attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/DefaultValue.java b/attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/DefaultValue.java new file mode 100644 index 00000000..5ce21096 --- /dev/null +++ b/attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/DefaultValue.java @@ -0,0 +1,13 @@ +package org.hypertrace.core.attribute.service.projection.functions; + +import static java.util.Objects.nonNull; + +import javax.annotation.Nullable; + +public class DefaultValue { + + @Nullable + public static String defaultString(@Nullable String value, @Nullable String defaultValue) { + return nonNull(value) && !value.isEmpty() ? value : defaultValue; + } +} diff --git a/attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/Hash.java b/attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/Hash.java new file mode 100644 index 00000000..b5cb4a43 --- /dev/null +++ b/attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/Hash.java @@ -0,0 +1,20 @@ +package org.hypertrace.core.attribute.service.projection.functions; + +import static java.util.Objects.nonNull; + +import com.github.f4b6a3.uuid.UuidCreator; +import com.github.f4b6a3.uuid.creator.rfc4122.NameBasedSha1UuidCreator; +import java.util.UUID; +import javax.annotation.Nullable; + +public class Hash { + + private static final NameBasedSha1UuidCreator HASHER = + UuidCreator.getNameBasedSha1Creator() + .withNamespace(UUID.fromString("5088c92d-5e9c-43f4-a35b-2589474d5642")); + + @Nullable + public static String hash(@Nullable String value) { + return nonNull(value) ? HASHER.create(value).toString() : null; + } +} diff --git a/attribute-projection-functions/src/test/java/org/hypertrace/core/attribute/service/projection/functions/ConcatenateTest.java b/attribute-projection-functions/src/test/java/org/hypertrace/core/attribute/service/projection/functions/ConcatenateTest.java new file mode 100644 index 00000000..ce99a06c --- /dev/null +++ b/attribute-projection-functions/src/test/java/org/hypertrace/core/attribute/service/projection/functions/ConcatenateTest.java @@ -0,0 +1,23 @@ +package org.hypertrace.core.attribute.service.projection.functions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class ConcatenateTest { + + @Test + void concatenatesNormalStrings() { + assertEquals("foobar", Concatenate.concatenate("foo", "bar")); + assertEquals("foo", Concatenate.concatenate("foo", "")); + assertEquals("bar", Concatenate.concatenate("", "bar")); + } + + @Test + void concatenatesNullStrings() { + assertEquals("foo", Concatenate.concatenate("foo", null)); + assertEquals("bar", Concatenate.concatenate(null, "bar")); + assertNull(Concatenate.concatenate(null, null)); + } +} diff --git a/attribute-projection-functions/src/test/java/org/hypertrace/core/attribute/service/projection/functions/DefaultValueTest.java b/attribute-projection-functions/src/test/java/org/hypertrace/core/attribute/service/projection/functions/DefaultValueTest.java new file mode 100644 index 00000000..6a9756c0 --- /dev/null +++ b/attribute-projection-functions/src/test/java/org/hypertrace/core/attribute/service/projection/functions/DefaultValueTest.java @@ -0,0 +1,21 @@ +package org.hypertrace.core.attribute.service.projection.functions; + +import static org.hypertrace.core.attribute.service.projection.functions.DefaultValue.defaultString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +public class DefaultValueTest { + @Test + void givesDefaultIfStringValueNullOrEmpty() { + assertEquals("default", defaultString(null, "default")); + assertEquals("default", defaultString("", "default")); + assertEquals("foo", defaultString("foo", "default")); + } + + @Test + void returnsNullIfNullDefaultGiven() { + assertNull(defaultString(null, null)); + } +} diff --git a/attribute-projection-functions/src/test/java/org/hypertrace/core/attribute/service/projection/functions/HashTest.java b/attribute-projection-functions/src/test/java/org/hypertrace/core/attribute/service/projection/functions/HashTest.java new file mode 100644 index 00000000..ee3d2129 --- /dev/null +++ b/attribute-projection-functions/src/test/java/org/hypertrace/core/attribute/service/projection/functions/HashTest.java @@ -0,0 +1,21 @@ +package org.hypertrace.core.attribute.service.projection.functions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class HashTest { + + @Test + void createsMatchingHashesForMatchingInputs() { + assertEquals(Hash.hash("foo"), Hash.hash("foo")); + assertNotEquals(Hash.hash("foo"), Hash.hash("bar")); + } + + @Test + void hashesNullToNull() { + assertNull(Hash.hash(null)); + } +} diff --git a/attribute-projection-registry/build.gradle.kts b/attribute-projection-registry/build.gradle.kts new file mode 100644 index 00000000..da5e76c8 --- /dev/null +++ b/attribute-projection-registry/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + `java-library` + jacoco + id("org.hypertrace.jacoco-report-plugin") + id("org.hypertrace.publish-plugin") +} + +dependencies { + api(project(":attribute-service-api")) + implementation(project(":attribute-projection-functions")) + implementation("com.google.guava:guava:29.0-jre") + testImplementation("org.junit.jupiter:junit-jupiter:5.6.2") +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java new file mode 100644 index 00000000..505ff52f --- /dev/null +++ b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java @@ -0,0 +1,41 @@ +package org.hypertrace.core.attribute.service.projection; + +import com.google.common.base.Preconditions; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.hypertrace.core.attribute.service.v1.AttributeKind; +import org.hypertrace.core.attribute.service.v1.LiteralValue; + +abstract class AbstractAttributeProjection implements AttributeProjection { + + private final AttributeKind resultKind; + private final List argumentKinds; + + protected AbstractAttributeProjection( + AttributeKind resultKind, List argumentKinds) { + this.resultKind = resultKind; + this.argumentKinds = argumentKinds; + } + + @Override + public LiteralValue project(List arguments) { + Preconditions.checkArgument(arguments.size() == argumentKinds.size()); + List unwrappedArguments = new ArrayList<>(argumentKinds.size()); + for (int index = 0; index < arguments.size(); index++) { + Optional unwrappedArgument = + ValueCoercer.fromLiteral(arguments.get(index), this.argumentKinds.get(index)); + unwrappedArguments.set(index, unwrappedArgument); + } + Object unwrappedResult = this.doUnwrappedProjection(unwrappedArguments); + return ValueCoercer.toLiteral(unwrappedResult, this.resultKind) + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Projection result %s could not be converted to expected type %s", + unwrappedResult, this.resultKind))); + } + + protected abstract R doUnwrappedProjection(List arguments); +} diff --git a/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjection.java b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjection.java new file mode 100644 index 00000000..4486c926 --- /dev/null +++ b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjection.java @@ -0,0 +1,17 @@ +package org.hypertrace.core.attribute.service.projection; + +import java.util.List; +import org.hypertrace.core.attribute.service.v1.LiteralValue; + +public interface AttributeProjection { + /** + * Performs the projection operation with the provided arguments. + * + * @param arguments to the projection + * @return the result of the projection + * @throws IllegalArgumentException if the provided arguments do not match the expected arity of + * the projection, can not be converted to the expected input types or produce a result that + * can't be converted to the expected output type. + */ + LiteralValue project(List arguments); +} diff --git a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistry.java b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistry.java similarity index 52% rename from attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistry.java rename to attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistry.java index 05706c74..d82b959f 100644 --- a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistry.java +++ b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistry.java @@ -1,27 +1,30 @@ package org.hypertrace.core.attribute.service.projection; -import static org.hypertrace.core.attribute.service.projection.DefaultValue.DEFAULT_VALUE; -import static org.hypertrace.core.attribute.service.projection.StringConcatenation.STRING_CONCATENATION; -import static org.hypertrace.core.attribute.service.projection.StringHash.STRING_HASH; import static org.hypertrace.core.attribute.service.v1.ProjectionOperator.PROJECTION_OPERATOR_CONCAT; -import static org.hypertrace.core.attribute.service.v1.ProjectionOperator.PROJECTION_OPERATOR_DEFAULT; import static org.hypertrace.core.attribute.service.v1.ProjectionOperator.PROJECTION_OPERATOR_HASH; import java.util.Map; import java.util.Optional; +import org.hypertrace.core.attribute.service.projection.functions.Concatenate; +import org.hypertrace.core.attribute.service.projection.functions.Hash; +import org.hypertrace.core.attribute.service.v1.AttributeKind; import org.hypertrace.core.attribute.service.v1.ProjectionOperator; public class AttributeProjectionRegistry { private static final Map PROJECTION_MAP = Map.of( - PROJECTION_OPERATOR_CONCAT, STRING_CONCATENATION, - PROJECTION_OPERATOR_HASH, STRING_HASH, - PROJECTION_OPERATOR_DEFAULT, DEFAULT_VALUE); + PROJECTION_OPERATOR_CONCAT, + new BinaryAttributeProjection<>( + AttributeKind.TYPE_STRING, + AttributeKind.TYPE_STRING, + AttributeKind.TYPE_STRING, + Concatenate::concatenate), + PROJECTION_OPERATOR_HASH, + new UnaryAttributeProjection<>( + AttributeKind.TYPE_STRING, AttributeKind.TYPE_STRING, Hash::hash)); public Optional getProjection(ProjectionOperator projectionOperator) { return Optional.ofNullable(PROJECTION_MAP.get(projectionOperator)); } - - } diff --git a/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/BinaryAttributeProjection.java b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/BinaryAttributeProjection.java new file mode 100644 index 00000000..3f3bdde4 --- /dev/null +++ b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/BinaryAttributeProjection.java @@ -0,0 +1,24 @@ +package org.hypertrace.core.attribute.service.projection; + +import java.util.List; +import java.util.function.BiFunction; +import org.hypertrace.core.attribute.service.v1.AttributeKind; + +class BinaryAttributeProjection extends AbstractAttributeProjection { + private final BiFunction projectionImplementation; + + BinaryAttributeProjection( + AttributeKind resultKind, + AttributeKind firstArgumentKind, + AttributeKind secondArgumentKind, + BiFunction projectionImplementation) { + super(resultKind, List.of(firstArgumentKind, secondArgumentKind)); + this.projectionImplementation = projectionImplementation; + } + + @Override + @SuppressWarnings("unchecked") + protected R doUnwrappedProjection(List arguments) { + return this.projectionImplementation.apply((T) arguments.get(0), (U) arguments.get(1)); + } +} diff --git a/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/UnaryAttributeProjection.java b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/UnaryAttributeProjection.java new file mode 100644 index 00000000..c54637c3 --- /dev/null +++ b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/UnaryAttributeProjection.java @@ -0,0 +1,23 @@ +package org.hypertrace.core.attribute.service.projection; + +import java.util.List; +import java.util.function.Function; +import org.hypertrace.core.attribute.service.v1.AttributeKind; + +class UnaryAttributeProjection extends AbstractAttributeProjection { + private final Function projectionImplementation; + + UnaryAttributeProjection( + AttributeKind resultKind, + AttributeKind argumentKind, + Function projectionImplementation) { + super(resultKind, List.of(argumentKind)); + this.projectionImplementation = projectionImplementation; + } + + @Override + @SuppressWarnings("unchecked") + protected R doUnwrappedProjection(List arguments) { + return this.projectionImplementation.apply((T) arguments.get(0)); + } +} diff --git a/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/ValueCoercer.java b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/ValueCoercer.java new file mode 100644 index 00000000..45174cd3 --- /dev/null +++ b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/ValueCoercer.java @@ -0,0 +1,230 @@ +package org.hypertrace.core.attribute.service.projection; + +import static java.util.Objects.isNull; +import static java.util.Objects.requireNonNull; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Instant; +import java.time.temporal.TemporalAccessor; +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.hypertrace.core.attribute.service.v1.AttributeKind; +import org.hypertrace.core.attribute.service.v1.LiteralValue; + +class ValueCoercer { + + public static Optional fromLiteral(LiteralValue value, AttributeKind attributeKind) { + switch (attributeKind) { + case TYPE_DOUBLE: + return extractDoubleValue(value); + case TYPE_INT64: + return extractLongValue(value); + case TYPE_TIMESTAMP: + return extractTimestamp(value); + case TYPE_BOOL: + return extractBooleanValue(value); + case TYPE_STRING: + case TYPE_BYTES: // Treating bytes as equivalent to string + return extractStringValue(value); + default: + return Optional.empty(); + } + } + + public static Optional toLiteral(Object value, AttributeKind attributeKind) { + if (isNull(value)) { + return Optional.empty(); + } + if (isAssignableToAnyOfClasses(value.getClass(), CharSequence.class)) { + return toLiteral(value.toString(), attributeKind); + } + if (isAssignableToAnyOfClasses(value.getClass(), Boolean.class)) { + return toLiteral((boolean) value, attributeKind); + } + if (isAssignableToAnyOfClasses(value.getClass(), Long.class, Integer.class, BigInteger.class, Double.class, Float.class, BigDecimal.class)) { + return toLiteral((Number) value, attributeKind); + } + if (isAssignableToAnyOfClasses(value.getClass(), TemporalAccessor.class)) { + return toLiteral((TemporalAccessor) value, attributeKind); + } + return Optional.empty(); + } + + private static Optional toLiteral( + @Nonnull String stringValue, AttributeKind attributeKind) { + switch (attributeKind) { + case TYPE_DOUBLE: + return tryParseDouble(stringValue).map(ValueCoercer::doubleLiteral); + case TYPE_INT64: + return tryParseLong(stringValue).map(ValueCoercer::longLiteral); + case TYPE_BOOL: + return tryParseBoolean(stringValue).map(ValueCoercer::booleanLiteral); + case TYPE_STRING: + return Optional.of(stringLiteral(stringValue)); + case TYPE_TIMESTAMP: + return tryParseLong(stringValue) + .or(() -> tryParseTimestamp(stringValue)) + .map(ValueCoercer::longLiteral); + default: + return Optional.empty(); + } + } + + private static Optional toLiteral( + boolean booleanValue, AttributeKind attributeKind) { + switch (attributeKind) { + case TYPE_BOOL: + return Optional.of(booleanLiteral(booleanValue)); + case TYPE_STRING: + return Optional.of(stringLiteral(String.valueOf(booleanValue))); + default: + return Optional.empty(); + } + } + + private static Optional toLiteral( + @Nonnull TemporalAccessor temporal, AttributeKind attributeKind) { + Instant instant = Instant.from(temporal); + switch (attributeKind) { + case TYPE_STRING: + return Optional.of(stringLiteral(instant.toString())); + case TYPE_INT64: + case TYPE_TIMESTAMP: + return Optional.of(longLiteral(instant.toEpochMilli())); + default: + return Optional.empty(); + } + } + + private static Optional toLiteral(Number numberValue, AttributeKind attributeKind) { + switch (attributeKind) { + case TYPE_DOUBLE: + return Optional.of(doubleLiteral(numberValue)); + case TYPE_TIMESTAMP: + case TYPE_INT64: // Timestamp and long both convert the same + return Optional.of(longLiteral(numberValue)); + case TYPE_STRING: + return Optional.of(stringLiteral(String.valueOf(numberValue))); + default: + return Optional.empty(); + } + } + + private static Optional extractBooleanValue(LiteralValue value) { + switch (value.getValueCase()) { + case BOOLEAN_VALUE: + return Optional.of(value.getBooleanValue()); + case STRING_VALUE: + return tryParseBoolean(value.getStringValue()); + default: + return Optional.empty(); + } + } + + private static Optional extractStringValue(LiteralValue value) { + switch (value.getValueCase()) { + case BOOLEAN_VALUE: + return Optional.of(String.valueOf(value.getBooleanValue())); + case INT_VALUE: + return Optional.of(String.valueOf(value.getIntValue())); + case FLOAT_VALUE: + return Optional.of(String.valueOf(value.getFloatValue())); + case STRING_VALUE: + return Optional.of(value.getStringValue()); + default: + return Optional.empty(); + } + } + + private static Optional extractLongValue(LiteralValue value) { + switch (value.getValueCase()) { + case FLOAT_VALUE: + return Optional.of(Double.valueOf(value.getFloatValue()).longValue()); + case INT_VALUE: + return Optional.of(value.getIntValue()); + case STRING_VALUE: + return tryParseLong(value.getStringValue()); + default: + return Optional.empty(); + } + } + + private static Optional extractDoubleValue(LiteralValue value) { + switch (value.getValueCase()) { + case FLOAT_VALUE: + return Optional.of(value.getFloatValue()); + case INT_VALUE: + return Optional.of(Long.valueOf(value.getIntValue()).doubleValue()); + case STRING_VALUE: + return tryParseDouble(value.getStringValue()); + default: + return Optional.empty(); + } + } + + private static Optional extractTimestamp(LiteralValue value) { + return extractLongValue(value).or(() -> tryParseTimestamp(value.getStringValue())); + } + + private static boolean isAssignableToAnyOfClasses( + Class classToCheck, Class... classesAllowed) { + for (Class allowedClass : classesAllowed) { + if (allowedClass.isAssignableFrom(classToCheck)) { + return true; + } + } + return false; + } + + private static LiteralValue stringLiteral(@Nonnull String stringValue) { + return LiteralValue.newBuilder().setStringValue(stringValue).build(); + } + + private static LiteralValue longLiteral(@Nonnull Number number) { + return LiteralValue.newBuilder().setIntValue(number.longValue()).build(); + } + + private static LiteralValue doubleLiteral(@Nonnull Number number) { + return LiteralValue.newBuilder().setFloatValue(number.doubleValue()).build(); + } + + private static LiteralValue booleanLiteral(boolean booleanValue) { + return LiteralValue.newBuilder().setBooleanValue(booleanValue).build(); + } + + private static Optional tryParseLong(@Nullable String intString) { + try { + return Optional.of(Long.valueOf(requireNonNull(intString))); + } catch (Throwable ignored) { + return Optional.empty(); + } + } + + private static Optional tryParseDouble(@Nullable String doubleString) { + try { + return Optional.of(Double.valueOf(requireNonNull(doubleString))); + } catch (Throwable ignored) { + return Optional.empty(); + } + } + + private static Optional tryParseBoolean(@Nullable String booleanString) { + if ("true".equalsIgnoreCase(booleanString)) { + return Optional.of(Boolean.TRUE); + } + if ("false".equalsIgnoreCase(booleanString)) { + return Optional.of(Boolean.FALSE); + } + return Optional.empty(); + } + + private static Optional tryParseTimestamp(@Nullable String dateString) { + try { + return Optional.of(Instant.parse(requireNonNull(dateString))).map(Instant::toEpochMilli); + } catch (Throwable ignored) { + return Optional.empty(); + } + } +} diff --git a/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistryTest.java b/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistryTest.java new file mode 100644 index 00000000..271f2584 --- /dev/null +++ b/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/AttributeProjectionRegistryTest.java @@ -0,0 +1,28 @@ +package org.hypertrace.core.attribute.service.projection; + +import static org.hypertrace.core.attribute.service.v1.ProjectionOperator.PROJECTION_OPERATOR_UNSET; +import static org.hypertrace.core.attribute.service.v1.ProjectionOperator.UNRECOGNIZED; + +import java.util.Arrays; +import org.hypertrace.core.attribute.service.v1.ProjectionOperator; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class AttributeProjectionRegistryTest { + + AttributeProjectionRegistry registry = new AttributeProjectionRegistry(); + + @Test + void supportsAllDeclaredFunctionTypes() { + Arrays.stream(ProjectionOperator.values()) + .filter( + projectionOperator -> + projectionOperator != UNRECOGNIZED + && projectionOperator != PROJECTION_OPERATOR_UNSET) + .forEach( + projectionOperator -> + Assertions.assertTrue( + registry.getProjection(projectionOperator).isPresent(), + "Projection declared but not present in registry " + projectionOperator)); + } +} diff --git a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjection.java b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjection.java deleted file mode 100644 index cb3e6301..00000000 --- a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/AttributeProjection.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.hypertrace.core.attribute.service.projection; - -import java.util.List; -import org.hypertrace.core.attribute.service.v1.AttributeKind; -import org.hypertrace.core.attribute.service.v1.LiteralValue; - -public interface AttributeProjection { - - AttributeKind getArgumentKindAtIndex(int index); - - Object project(List arguments); -} diff --git a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/DefaultValue.java b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/DefaultValue.java deleted file mode 100644 index 327cea31..00000000 --- a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/DefaultValue.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.hypertrace.core.attribute.service.projection; - -import java.util.List; -import org.hypertrace.core.attribute.service.v1.AttributeKind; - -public class DefaultValue implements AttributeProjection { - - public static DefaultValue DEFAULT_VALUE = new DefaultValue(); - - public static Object defaultValue(List values) { - for (Object value : values) { - if (value != null) { - return value; - } - } - return null; - } - - private DefaultValue() {} - - @Override - public AttributeKind getArgumentKindAtIndex(int index) { - return AttributeKind.KIND_UNDEFINED; - } - - @Override - public Object project(List arguments) { - return defaultValue(arguments); - } -} diff --git a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringConcatenation.java b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringConcatenation.java deleted file mode 100644 index e885d781..00000000 --- a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringConcatenation.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.hypertrace.core.attribute.service.projection; - -import java.util.List; -import org.hypertrace.core.attribute.service.v1.AttributeKind; - -public class StringConcatenation implements AttributeProjection { - public static StringConcatenation STRING_CONCATENATION = new StringConcatenation(); - - public static String concatenate(List strings) { - if (strings.size() == 1) { - return strings.get(0); - } - if (strings.size() == 2) { - return strings.get(0) + strings.get(1); - } - StringBuilder builder = new StringBuilder(); - for (String string : strings) { - builder.append(string); - } - return builder.toString(); - } - - private StringConcatenation() {} - - @Override - public AttributeKind getArgumentKindAtIndex(int index) { - return AttributeKind.TYPE_STRING; - } - - @Override - @SuppressWarnings("unchecked") - public Object project(List arguments) { - return concatenate((List) (Object) arguments); - } -} diff --git a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringHash.java b/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringHash.java deleted file mode 100644 index b05c0652..00000000 --- a/attribute-projections/src/main/java/org/hypertrace/core/attribute/service/projection/StringHash.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.hypertrace.core.attribute.service.projection; - -import static org.hypertrace.core.attribute.service.projection.StringConcatenation.concatenate; - -import com.github.f4b6a3.uuid.UuidCreator; -import com.github.f4b6a3.uuid.creator.rfc4122.NameBasedSha1UuidCreator; -import java.util.List; -import java.util.UUID; -import org.hypertrace.core.attribute.service.v1.AttributeKind; - -public class StringHash implements AttributeProjection { - - private static final NameBasedSha1UuidCreator HASHER = - UuidCreator.getNameBasedSha1Creator() - .withNamespace(UUID.fromString("5088c92d-5e9c-43f4-a35b-2589474d5642")); - - public static StringHash STRING_HASH = new StringHash(); - - public static String hash(List strings) { - return HASHER.create(concatenate(strings)).toString(); - } - - private StringHash() {} - - @Override - public AttributeKind getArgumentKindAtIndex(int index) { - return AttributeKind.TYPE_STRING; - } - - @Override - @SuppressWarnings("unchecked") - public Object project(List arguments) { - return hash((List) (Object) arguments); - } -} diff --git a/attribute-service-api/src/main/proto/org/hypertrace/core/attribute/service/v1/attribute_metadata.proto b/attribute-service-api/src/main/proto/org/hypertrace/core/attribute/service/v1/attribute_metadata.proto index 9e8942ac..c436ab9c 100644 --- a/attribute-service-api/src/main/proto/org/hypertrace/core/attribute/service/v1/attribute_metadata.proto +++ b/attribute-service-api/src/main/proto/org/hypertrace/core/attribute/service/v1/attribute_metadata.proto @@ -140,14 +140,14 @@ message Empty { message AttributeDefinition { oneof value { - string span_path = 1; - Projection projection = 2; + Projection projection = 1; + // TODO non-projected attributes } } message Projection { oneof value { - string attribute_key = 1; + string attribute_id = 1; LiteralValue literal = 2; ProjectionExpression expression = 3; } @@ -171,5 +171,4 @@ enum ProjectionOperator { PROJECTION_OPERATOR_UNSET = 0; PROJECTION_OPERATOR_CONCAT = 1; PROJECTION_OPERATOR_HASH = 2; - PROJECTION_OPERATOR_DEFAULT = 3; } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 06e98052..470092e0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,4 +18,6 @@ include(":attribute-service-impl") include(":attribute-service") include(":attribute-service-tenant-api") include(":caching-attribute-service-client") -include(":attribute-projections") +include(":attribute-projection-functions") +include(":attribute-projection-registry") + From 871f5f814f7365c2e53dc2496626c9ebde3dfe3a Mon Sep 17 00:00:00 2001 From: Aaron Steinfeld Date: Fri, 11 Sep 2020 12:33:49 -0400 Subject: [PATCH 3/8] chore: finish unwrapping arguments --- .../projection/AbstractAttributeProjection.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java index 505ff52f..a961344f 100644 --- a/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java +++ b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java @@ -3,7 +3,6 @@ import com.google.common.base.Preconditions; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import org.hypertrace.core.attribute.service.v1.AttributeKind; import org.hypertrace.core.attribute.service.v1.LiteralValue; @@ -23,8 +22,13 @@ public LiteralValue project(List arguments) { Preconditions.checkArgument(arguments.size() == argumentKinds.size()); List unwrappedArguments = new ArrayList<>(argumentKinds.size()); for (int index = 0; index < arguments.size(); index++) { - Optional unwrappedArgument = - ValueCoercer.fromLiteral(arguments.get(index), this.argumentKinds.get(index)); + int argumentIndex = index; + LiteralValue argumentLiteral = arguments.get(argumentIndex); + AttributeKind attributeKind = this.argumentKinds.get(argumentIndex); + Object unwrappedArgument = + ValueCoercer.fromLiteral(argumentLiteral, attributeKind) + .orElseThrow(() -> new IllegalArgumentException(String.format("Projection argument %s at index %d could not be converted to expected type %s", argumentLiteral, argumentIndex, attributeKind))) + unwrappedArguments.set(index, unwrappedArgument); } Object unwrappedResult = this.doUnwrappedProjection(unwrappedArguments); From 1607a292a0964bc6201cbdfaf789748e644a6107 Mon Sep 17 00:00:00 2001 From: Aaron Steinfeld Date: Fri, 11 Sep 2020 12:39:58 -0400 Subject: [PATCH 4/8] fix: missing semicolon --- .../service/projection/AbstractAttributeProjection.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java index a961344f..e9a62b9c 100644 --- a/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java +++ b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java @@ -27,7 +27,12 @@ public LiteralValue project(List arguments) { AttributeKind attributeKind = this.argumentKinds.get(argumentIndex); Object unwrappedArgument = ValueCoercer.fromLiteral(argumentLiteral, attributeKind) - .orElseThrow(() -> new IllegalArgumentException(String.format("Projection argument %s at index %d could not be converted to expected type %s", argumentLiteral, argumentIndex, attributeKind))) + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Projection argument %s at index %d could not be converted to expected type %s", + argumentLiteral, argumentIndex, attributeKind))); unwrappedArguments.set(index, unwrappedArgument); } From 5bb693ec42ee7afa13d5a9915cb048d09e0975a7 Mon Sep 17 00:00:00 2001 From: Aaron Steinfeld Date: Fri, 11 Sep 2020 17:13:33 -0400 Subject: [PATCH 5/8] docs: added comment on hasher --- .../core/attribute/service/projection/functions/Hash.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/Hash.java b/attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/Hash.java index b5cb4a43..b0a35565 100644 --- a/attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/Hash.java +++ b/attribute-projection-functions/src/main/java/org/hypertrace/core/attribute/service/projection/functions/Hash.java @@ -9,6 +9,11 @@ public class Hash { + /** + * Unique, randomly generated namespace that should never be used directly. Changing this value + * would change any existing projections containing a hash and orphan any data persisted against + * that such as stored entities. + */ private static final NameBasedSha1UuidCreator HASHER = UuidCreator.getNameBasedSha1Creator() .withNamespace(UUID.fromString("5088c92d-5e9c-43f4-a35b-2589474d5642")); From 0dd8c99575af9f1dca3e93742e24e6c285d69f6f Mon Sep 17 00:00:00 2001 From: Aaron Steinfeld Date: Mon, 14 Sep 2020 17:11:30 -0400 Subject: [PATCH 6/8] feat: add peristence for attribute definition --- attribute-service-impl/build.gradle.kts | 1 + .../service/model/AttributeMetadataModel.java | 56 ++++++++++++++- .../model/AttributeMetadataModelTest.java | 71 ++++++++++++++++++- 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/attribute-service-impl/build.gradle.kts b/attribute-service-impl/build.gradle.kts index 8295d76f..83278fe4 100644 --- a/attribute-service-impl/build.gradle.kts +++ b/attribute-service-impl/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:2.9.5") implementation("com.typesafe:config:1.3.2") implementation("org.slf4j:slf4j-api:1.7.25") + implementation("com.google.protobuf:protobuf-java-util:3.13.0") testImplementation("org.mockito:mockito-core:3.3.3") testImplementation("org.junit.jupiter:junit-jupiter:5.6.2") diff --git a/attribute-service-impl/src/main/java/org/hypertrace/core/attribute/service/model/AttributeMetadataModel.java b/attribute-service-impl/src/main/java/org/hypertrace/core/attribute/service/model/AttributeMetadataModel.java index adfc7a34..3ce3c125 100644 --- a/attribute-service-impl/src/main/java/org/hypertrace/core/attribute/service/model/AttributeMetadataModel.java +++ b/attribute-service-impl/src/main/java/org/hypertrace/core/attribute/service/model/AttributeMetadataModel.java @@ -1,9 +1,19 @@ package org.hypertrace.core.attribute.service.model; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.protobuf.Message; +import com.google.protobuf.util.JsonFormat; import java.io.IOException; import java.util.Collections; import java.util.List; @@ -11,6 +21,7 @@ import java.util.Objects; import java.util.stream.Collectors; import org.hypertrace.core.attribute.service.v1.AggregateFunction; +import org.hypertrace.core.attribute.service.v1.AttributeDefinition; import org.hypertrace.core.attribute.service.v1.AttributeKind; import org.hypertrace.core.attribute.service.v1.AttributeMetadata; import org.hypertrace.core.attribute.service.v1.AttributeScope; @@ -54,6 +65,10 @@ public class AttributeMetadataModel implements Document { private List sources = Collections.emptyList(); private Map> metadata = Collections.emptyMap(); + @JsonSerialize(using = ProtobufMessageSerializer.class) + @JsonDeserialize(using = AttributeDefinitionDeserializer.class) + private AttributeDefinition definition = AttributeDefinition.getDefaultInstance(); + public AttributeMetadataModel() {} public static AttributeMetadataModel fromDTO(AttributeMetadata attributeMetadata) { @@ -73,6 +88,7 @@ public static AttributeMetadataModel fromDTO(AttributeMetadata attributeMetadata attributeMetadata.getOnlyAggregationsAllowed()); attributeMetadataModel.setSources(attributeMetadata.getSourcesList()); attributeMetadataModel.setGroupable(attributeMetadata.getGroupable()); + attributeMetadataModel.setDefinition(attributeMetadata.getDefinition()); attributeMetadataModel.setMetadata( attributeMetadata.getMetadataMap().entrySet().stream() .collect( @@ -209,6 +225,14 @@ public void setMetadata(Map> metadata) { this.metadata = metadata; } + public AttributeDefinition getDefinition() { + return definition; + } + + public void setDefinition(AttributeDefinition definition) { + this.definition = definition; + } + public AttributeMetadata toDTO() { return toDTOBuilder().build(); } @@ -237,7 +261,8 @@ public AttributeMetadata.Builder toDTOBuilder() { stringMapEntry -> AttributeSourceMetadata.newBuilder() .putAllSourceMetadata(stringMapEntry.getValue()) - .build()))); + .build()))) + .setDefinition(this.definition); if (unit != null) { builder.setUnit(unit); @@ -320,7 +345,8 @@ public boolean equals(Object o) { && Objects.equals(groupable, that.groupable) && Objects.equals(supportedAggregations, that.supportedAggregations) && Objects.equals(sources, that.sources) - && Objects.equals(metadata, that.metadata); + && Objects.equals(metadata, that.metadata) + && Objects.equals(definition, that.definition); } @Override @@ -340,6 +366,30 @@ public int hashCode() { supportedAggregations, onlyAggregationsAllowed, sources, - metadata); + metadata, + definition); + } + + private static class ProtobufMessageSerializer extends JsonSerializer { + private static final JsonFormat.Printer PRINTER = + JsonFormat.printer().omittingInsignificantWhitespace(); + + @Override + public void serialize(Message message, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + generator.writeRawValue(PRINTER.print(message)); + } + } + + private static class AttributeDefinitionDeserializer extends JsonDeserializer { + private static final JsonFormat.Parser PARSER = JsonFormat.parser(); + + @Override + public Message deserialize(JsonParser parser, DeserializationContext context) + throws IOException { + AttributeDefinition.Builder builder = AttributeDefinition.newBuilder(); + PARSER.merge(parser.readValueAsTree().toString(), builder); + return builder.build(); + } } } diff --git a/attribute-service-impl/src/test/java/org/hypertrace/core/attribute/service/model/AttributeMetadataModelTest.java b/attribute-service-impl/src/test/java/org/hypertrace/core/attribute/service/model/AttributeMetadataModelTest.java index 953fdbc3..8aa3844e 100644 --- a/attribute-service-impl/src/test/java/org/hypertrace/core/attribute/service/model/AttributeMetadataModelTest.java +++ b/attribute-service-impl/src/test/java/org/hypertrace/core/attribute/service/model/AttributeMetadataModelTest.java @@ -4,12 +4,14 @@ import java.io.IOException; import java.util.Collections; import java.util.Map; +import org.hypertrace.core.attribute.service.v1.AttributeDefinition; import org.hypertrace.core.attribute.service.v1.AttributeKind; import org.hypertrace.core.attribute.service.v1.AttributeMetadata; import org.hypertrace.core.attribute.service.v1.AttributeScope; import org.hypertrace.core.attribute.service.v1.AttributeSource; import org.hypertrace.core.attribute.service.v1.AttributeSourceMetadata; import org.hypertrace.core.attribute.service.v1.AttributeType; +import org.hypertrace.core.attribute.service.v1.Projection; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -30,6 +32,10 @@ public void testAttributeMetadataModelJsonSerDes() throws IOException { attributeMetadataModel.setUnit("ms"); attributeMetadataModel.setValueKind(AttributeKind.TYPE_STRING); attributeMetadataModel.setTenantId("tenantId"); + attributeMetadataModel.setDefinition( + AttributeDefinition.newBuilder() + .setProjection(Projection.newBuilder().setAttributeId("test")) + .build()); String json = attributeMetadataModel.toJson(); String expectedJson = @@ -45,6 +51,7 @@ public void testAttributeMetadataModelJsonSerDes() throws IOException { + "\"supportedAggregations\":[]," + "\"onlyAggregationsAllowed\":false," + "\"sources\":[]," + + "\"definition\":{\"projection\":{\"attributeId\":\"test\"}}," + "\"id\":\"EVENT.key\"," + "\"value_kind\":\"TYPE_STRING\"," + "\"display_name\":\"Some Name\"," @@ -69,6 +76,7 @@ public void testAttributeMetaModelToFromDto() { .setType(AttributeType.ATTRIBUTE) .setUnit("ms") .setValueKind(AttributeKind.TYPE_STRING) + .setDefinition(AttributeDefinition.getDefaultInstance()) .putAllMetadata( Collections.singletonMap( AttributeSource.EDS.name(), @@ -157,7 +165,6 @@ public void testAttributeMetaModelGroupableFromJson() throws IOException { metadata = deserializedModel.toDTO(); Assertions.assertTrue(metadata.getGroupable()); - json = "{" + "\"fqn\":\"fqn\"," @@ -183,4 +190,66 @@ public void testAttributeMetaModelGroupableFromJson() throws IOException { metadata = deserializedModel.toDTO(); Assertions.assertFalse(metadata.getGroupable()); } + + @Test + public void testAttributeDefinitionBackwardsCompatibility() throws IOException { + String json = + "{" + + "\"fqn\":\"fqn\"," + + "\"key\":\"key\"," + + "\"scope\":\"EVENT\"," + + "\"materialized\":true," + + "\"unit\":\"ms\"," + + "\"type\":\"ATTRIBUTE\"," + + "\"labels\":[\"item1\"]," + + "\"groupable\":true," + + "\"supportedAggregations\":[]," + + "\"onlyAggregationsAllowed\":false," + + "\"sources\":[]," + + "\"id\":\"EVENT.key\"," + + "\"value_kind\":\"TYPE_BOOL\"," + + "\"display_name\":\"Some Name\"," + + "\"tenant_id\":\"tenantId\"" + + "}"; + + AttributeMetadataModel deserializedModel = AttributeMetadataModel.fromJson(json); + Assertions.assertEquals( + AttributeDefinition.getDefaultInstance(), deserializedModel.getDefinition()); + AttributeMetadata metadata = deserializedModel.toDTO(); + Assertions.assertEquals(AttributeDefinition.getDefaultInstance(), metadata.getDefinition()); + + AttributeMetadataModel modelFromMetadataWithoutDefinition = + AttributeMetadataModel.fromDTO( + AttributeMetadata.newBuilder() + .setFqn("fqn") + .setId("id") + .setKey("key") + .setDisplayName("Display") + .setMaterialized(true) + .setScope(AttributeScope.EVENT) + .setType(AttributeType.ATTRIBUTE) + .setUnit("ms") + .setValueKind(AttributeKind.TYPE_STRING) + .build()); + + String expectedJson = + "{" + + "\"fqn\":\"fqn\"," + + "\"key\":\"key\"," + + "\"scope\":\"EVENT\"," + + "\"materialized\":true," + + "\"unit\":\"ms\"," + + "\"type\":\"ATTRIBUTE\"," + + "\"labels\":[]," + + "\"groupable\":false," + + "\"supportedAggregations\":[]," + + "\"onlyAggregationsAllowed\":false," + + "\"sources\":[]," + + "\"definition\":{}," + + "\"id\":\"EVENT.key\"," + + "\"value_kind\":\"TYPE_STRING\"," + + "\"display_name\":\"Display\"," + + "\"tenant_id\":null}"; + Assertions.assertEquals(expectedJson, modelFromMetadataWithoutDefinition.toJson()); + } } From f420c9399fdfa80e236beb9d73b99ec57b44ff9c Mon Sep 17 00:00:00 2001 From: Aaron Steinfeld Date: Mon, 14 Sep 2020 17:24:52 -0400 Subject: [PATCH 7/8] test: fix impl tests --- .../core/attribute/service/AttributeServiceImplTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/attribute-service-impl/src/test/java/org/hypertrace/core/attribute/service/AttributeServiceImplTest.java b/attribute-service-impl/src/test/java/org/hypertrace/core/attribute/service/AttributeServiceImplTest.java index 0ad5ac81..26bb7b09 100644 --- a/attribute-service-impl/src/test/java/org/hypertrace/core/attribute/service/AttributeServiceImplTest.java +++ b/attribute-service-impl/src/test/java/org/hypertrace/core/attribute/service/AttributeServiceImplTest.java @@ -14,6 +14,7 @@ import java.util.Optional; import java.util.stream.Collectors; import org.hypertrace.core.attribute.service.v1.AggregateFunction; +import org.hypertrace.core.attribute.service.v1.AttributeDefinition; import org.hypertrace.core.attribute.service.v1.AttributeKind; import org.hypertrace.core.attribute.service.v1.AttributeMetadata; import org.hypertrace.core.attribute.service.v1.AttributeMetadataFilter; @@ -84,6 +85,7 @@ public void testFindAll() { .setScope(AttributeScope.EVENT) .setDisplayName("EVENT name") .setValueKind(AttributeKind.TYPE_STRING) + .setDefinition(AttributeDefinition.getDefaultInstance()) .setGroupable(true) .setType(AttributeType.ATTRIBUTE) // Add default aggregations. See SupportedAggregationsDecorator @@ -97,6 +99,7 @@ public void testFindAll() { .setScope(AttributeScope.EVENT) .setDisplayName("EVENT duration") .setGroupable(false) + .setDefinition(AttributeDefinition.getDefaultInstance()) .setValueKind(AttributeKind.TYPE_INT64) .setType(AttributeType.METRIC) // Add default aggregations. See SupportedAggregationsDecorator @@ -241,6 +244,7 @@ public void testFindAttributes() { .setScope(AttributeScope.EVENT) .setDisplayName("EVENT name") .setValueKind(AttributeKind.TYPE_STRING) + .setDefinition(AttributeDefinition.getDefaultInstance()) .setGroupable(true) .setType(AttributeType.ATTRIBUTE) // Add default aggregations. See SupportedAggregationsDecorator @@ -255,6 +259,7 @@ public void testFindAttributes() { .setDisplayName("EVENT duration") .setGroupable(false) .setValueKind(AttributeKind.TYPE_INT64) + .setDefinition(AttributeDefinition.getDefaultInstance()) .setType(AttributeType.METRIC) // Add default aggregations. See SupportedAggregationsDecorator .addAllSupportedAggregations( From 6c4270da18af4cb8e3107499a14c566021948faa Mon Sep 17 00:00:00 2001 From: Aaron Steinfeld Date: Wed, 16 Sep 2020 13:18:53 -0400 Subject: [PATCH 8/8] test: add tests --- .../AbstractAttributeProjection.java | 4 +- .../service/projection/ValueCoercer.java | 23 +- .../BinaryAttributeProjectionTest.java | 58 +++++ .../projection/LiteralConstructors.java | 22 ++ .../UnaryAttributeProjectionTest.java | 24 ++ .../service/projection/ValueCoercerTest.java | 208 ++++++++++++++++++ 6 files changed, 331 insertions(+), 8 deletions(-) create mode 100644 attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/BinaryAttributeProjectionTest.java create mode 100644 attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/LiteralConstructors.java create mode 100644 attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/UnaryAttributeProjectionTest.java create mode 100644 attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/ValueCoercerTest.java diff --git a/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java index e9a62b9c..d4f36e91 100644 --- a/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java +++ b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/AbstractAttributeProjection.java @@ -34,13 +34,13 @@ public LiteralValue project(List arguments) { "Projection argument %s at index %d could not be converted to expected type %s", argumentLiteral, argumentIndex, attributeKind))); - unwrappedArguments.set(index, unwrappedArgument); + unwrappedArguments.add(argumentIndex, unwrappedArgument); } Object unwrappedResult = this.doUnwrappedProjection(unwrappedArguments); return ValueCoercer.toLiteral(unwrappedResult, this.resultKind) .orElseThrow( () -> - new IllegalArgumentException( + new UnsupportedOperationException( String.format( "Projection result %s could not be converted to expected type %s", unwrappedResult, this.resultKind))); diff --git a/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/ValueCoercer.java b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/ValueCoercer.java index 45174cd3..2add7705 100644 --- a/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/ValueCoercer.java +++ b/attribute-projection-registry/src/main/java/org/hypertrace/core/attribute/service/projection/ValueCoercer.java @@ -26,7 +26,6 @@ public static Optional fromLiteral(LiteralValue value, AttributeKind attribut case TYPE_BOOL: return extractBooleanValue(value); case TYPE_STRING: - case TYPE_BYTES: // Treating bytes as equivalent to string return extractStringValue(value); default: return Optional.empty(); @@ -43,7 +42,14 @@ public static Optional toLiteral(Object value, AttributeKind attri if (isAssignableToAnyOfClasses(value.getClass(), Boolean.class)) { return toLiteral((boolean) value, attributeKind); } - if (isAssignableToAnyOfClasses(value.getClass(), Long.class, Integer.class, BigInteger.class, Double.class, Float.class, BigDecimal.class)) { + if (isAssignableToAnyOfClasses( + value.getClass(), + Long.class, + Integer.class, + BigInteger.class, + Double.class, + Float.class, + BigDecimal.class)) { return toLiteral((Number) value, attributeKind); } if (isAssignableToAnyOfClasses(value.getClass(), TemporalAccessor.class)) { @@ -90,6 +96,8 @@ private static Optional toLiteral( switch (attributeKind) { case TYPE_STRING: return Optional.of(stringLiteral(instant.toString())); + case TYPE_DOUBLE: + return Optional.of(doubleLiteral(instant.toEpochMilli())); case TYPE_INT64: case TYPE_TIMESTAMP: return Optional.of(longLiteral(instant.toEpochMilli())); @@ -102,9 +110,12 @@ private static Optional toLiteral(Number numberValue, AttributeKin switch (attributeKind) { case TYPE_DOUBLE: return Optional.of(doubleLiteral(numberValue)); - case TYPE_TIMESTAMP: - case TYPE_INT64: // Timestamp and long both convert the same + case TYPE_INT64: return Optional.of(longLiteral(numberValue)); + case TYPE_TIMESTAMP: + return numberValue.longValue() >= 0 + ? Optional.of(longLiteral(numberValue)) + : Optional.empty(); case TYPE_STRING: return Optional.of(stringLiteral(String.valueOf(numberValue))); default: @@ -141,7 +152,7 @@ private static Optional extractStringValue(LiteralValue value) { private static Optional extractLongValue(LiteralValue value) { switch (value.getValueCase()) { case FLOAT_VALUE: - return Optional.of(Double.valueOf(value.getFloatValue()).longValue()); + return Optional.of((long) value.getFloatValue()); case INT_VALUE: return Optional.of(value.getIntValue()); case STRING_VALUE: @@ -156,7 +167,7 @@ private static Optional extractDoubleValue(LiteralValue value) { case FLOAT_VALUE: return Optional.of(value.getFloatValue()); case INT_VALUE: - return Optional.of(Long.valueOf(value.getIntValue()).doubleValue()); + return Optional.of((double) value.getIntValue()); case STRING_VALUE: return tryParseDouble(value.getStringValue()); default: diff --git a/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/BinaryAttributeProjectionTest.java b/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/BinaryAttributeProjectionTest.java new file mode 100644 index 00000000..b6538b79 --- /dev/null +++ b/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/BinaryAttributeProjectionTest.java @@ -0,0 +1,58 @@ +package org.hypertrace.core.attribute.service.projection; + +import static org.hypertrace.core.attribute.service.projection.LiteralConstructors.booleanLiteral; +import static org.hypertrace.core.attribute.service.projection.LiteralConstructors.doubleLiteral; +import static org.hypertrace.core.attribute.service.projection.LiteralConstructors.longLiteral; +import static org.hypertrace.core.attribute.service.projection.LiteralConstructors.stringLiteral; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import org.hypertrace.core.attribute.service.v1.AttributeKind; +import org.junit.jupiter.api.Test; + +class BinaryAttributeProjectionTest { + + private final BinaryAttributeProjection sumProjection = + new BinaryAttributeProjection<>( + AttributeKind.TYPE_INT64, AttributeKind.TYPE_INT64, AttributeKind.TYPE_INT64, Long::sum); + + @Test + void projectsForAnyConvertibleArgTypes() { + assertEquals( + longLiteral(5), sumProjection.project(List.of(longLiteral(4), stringLiteral("1")))); + assertEquals( + longLiteral(6), sumProjection.project(List.of(doubleLiteral(5.0d), longLiteral(1)))); + assertEquals( + longLiteral(7), sumProjection.project(List.of(stringLiteral("3"), doubleLiteral(4.0f)))); + } + + @Test + void throwsIfArgListIsOfUnexpectedLength() { + assertThrows(IllegalArgumentException.class, () -> sumProjection.project(List.of())); + assertThrows( + IllegalArgumentException.class, + () -> sumProjection.project(List.of(longLiteral(1), longLiteral(2), longLiteral(3)))); + } + + @Test + void throwsIfAnyArgIsNotConvertible() { + assertThrows( + IllegalArgumentException.class, + () -> sumProjection.project(List.of(booleanLiteral(true), longLiteral(1)))); + assertThrows( + IllegalArgumentException.class, + () -> sumProjection.project(List.of(longLiteral(1), booleanLiteral(true)))); + } + + @Test + void throwsIfResultIsNotConvertibleToExpectedType() { + BinaryAttributeProjection badProjection = + new BinaryAttributeProjection<>( + AttributeKind.TYPE_BOOL, AttributeKind.TYPE_INT64, AttributeKind.TYPE_INT64, Long::sum); + + assertThrows( + UnsupportedOperationException.class, + () -> badProjection.project(List.of(longLiteral(2), longLiteral(1)))); + } +} diff --git a/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/LiteralConstructors.java b/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/LiteralConstructors.java new file mode 100644 index 00000000..c992b8e5 --- /dev/null +++ b/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/LiteralConstructors.java @@ -0,0 +1,22 @@ +package org.hypertrace.core.attribute.service.projection; + +import javax.annotation.Nonnull; +import org.hypertrace.core.attribute.service.v1.LiteralValue; + +class LiteralConstructors { + static LiteralValue stringLiteral(@Nonnull String stringValue) { + return LiteralValue.newBuilder().setStringValue(stringValue).build(); + } + + static LiteralValue longLiteral(long longValue) { + return LiteralValue.newBuilder().setIntValue(longValue).build(); + } + + static LiteralValue doubleLiteral(double doubleValue) { + return LiteralValue.newBuilder().setFloatValue(doubleValue).build(); + } + + static LiteralValue booleanLiteral(boolean booleanValue) { + return LiteralValue.newBuilder().setBooleanValue(booleanValue).build(); + } +} diff --git a/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/UnaryAttributeProjectionTest.java b/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/UnaryAttributeProjectionTest.java new file mode 100644 index 00000000..3e0bd95d --- /dev/null +++ b/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/UnaryAttributeProjectionTest.java @@ -0,0 +1,24 @@ +package org.hypertrace.core.attribute.service.projection; + +import static org.hypertrace.core.attribute.service.projection.LiteralConstructors.doubleLiteral; +import static org.hypertrace.core.attribute.service.projection.LiteralConstructors.longLiteral; +import static org.hypertrace.core.attribute.service.projection.LiteralConstructors.stringLiteral; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.hypertrace.core.attribute.service.v1.AttributeKind; +import org.junit.jupiter.api.Test; + +class UnaryAttributeProjectionTest { + + private final UnaryAttributeProjection incrementProjection = + new UnaryAttributeProjection<>( + AttributeKind.TYPE_INT64, AttributeKind.TYPE_INT64, x -> x + 1); + + @Test + void projectsForAnyConvertibleArgTypes() { + assertEquals(longLiteral(5), incrementProjection.project(List.of(longLiteral(4)))); + assertEquals(longLiteral(6), incrementProjection.project(List.of(doubleLiteral(5.0d)))); + assertEquals(longLiteral(7), incrementProjection.project(List.of(stringLiteral("6")))); + } +} diff --git a/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/ValueCoercerTest.java b/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/ValueCoercerTest.java new file mode 100644 index 00000000..7b86903a --- /dev/null +++ b/attribute-projection-registry/src/test/java/org/hypertrace/core/attribute/service/projection/ValueCoercerTest.java @@ -0,0 +1,208 @@ +package org.hypertrace.core.attribute.service.projection; + +import static java.util.Optional.empty; +import static org.hypertrace.core.attribute.service.projection.LiteralConstructors.booleanLiteral; +import static org.hypertrace.core.attribute.service.projection.LiteralConstructors.doubleLiteral; +import static org.hypertrace.core.attribute.service.projection.LiteralConstructors.longLiteral; +import static org.hypertrace.core.attribute.service.projection.LiteralConstructors.stringLiteral; +import static org.hypertrace.core.attribute.service.projection.ValueCoercer.fromLiteral; +import static org.hypertrace.core.attribute.service.projection.ValueCoercer.toLiteral; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.Optional; +import org.hypertrace.core.attribute.service.v1.AttributeKind; +import org.hypertrace.core.attribute.service.v1.LiteralValue; +import org.junit.jupiter.api.Test; + +class ValueCoercerTest { + // 1600000000000 epoch => 2020-09-13T12:26:40.000Z + private static final String TEST_TIMESTAMP_STRING = "2020-09-13T12:26:40Z"; + private static final long TEST_TIMESTAMP_MS = 1600000000000L; + private static final Instant TEST_TIMESTAMP_INSTANT = Instant.ofEpochMilli(TEST_TIMESTAMP_MS); + + @Test + void coercesEmptyFromNull() { + assertEquals(empty(), toLiteral(null, AttributeKind.TYPE_STRING)); + assertEquals(empty(), toLiteral(null, AttributeKind.TYPE_BOOL)); + assertEquals(empty(), toLiteral(null, AttributeKind.TYPE_DOUBLE)); + assertEquals(empty(), toLiteral(null, AttributeKind.TYPE_INT64)); + } + + @Test + void coercesFromString() { + assertEquals(Optional.of(stringLiteral("test")), toLiteral("test", AttributeKind.TYPE_STRING)); + assertEquals(Optional.of(stringLiteral("")), toLiteral("", AttributeKind.TYPE_STRING)); + + assertEquals(empty(), toLiteral("test", AttributeKind.TYPE_INT64)); + assertEquals(Optional.of(longLiteral(10)), toLiteral("10", AttributeKind.TYPE_INT64)); + assertEquals(Optional.of(longLiteral(-10)), toLiteral("-10", AttributeKind.TYPE_INT64)); + assertEquals(empty(), toLiteral("10.0", AttributeKind.TYPE_INT64)); + + assertEquals(empty(), toLiteral("test", AttributeKind.TYPE_DOUBLE)); + assertEquals(Optional.of(doubleLiteral(10)), toLiteral("10", AttributeKind.TYPE_DOUBLE)); + assertEquals(Optional.of(doubleLiteral(-10)), toLiteral("-10", AttributeKind.TYPE_DOUBLE)); + assertEquals(Optional.of(doubleLiteral(10.5)), toLiteral("10.5", AttributeKind.TYPE_DOUBLE)); + + assertEquals(empty(), toLiteral("test", AttributeKind.TYPE_BOOL)); + assertEquals(Optional.of(booleanLiteral(true)), toLiteral("true", AttributeKind.TYPE_BOOL)); + assertEquals(Optional.of(booleanLiteral(true)), toLiteral("TRUE", AttributeKind.TYPE_BOOL)); + assertEquals(Optional.of(booleanLiteral(false)), toLiteral("fALse", AttributeKind.TYPE_BOOL)); + assertEquals(Optional.of(booleanLiteral(false)), toLiteral("false", AttributeKind.TYPE_BOOL)); + + assertEquals(empty(), toLiteral("test", AttributeKind.TYPE_BOOL)); + assertEquals(Optional.of(booleanLiteral(true)), toLiteral("true", AttributeKind.TYPE_BOOL)); + assertEquals(Optional.of(booleanLiteral(true)), toLiteral("TRUE", AttributeKind.TYPE_BOOL)); + assertEquals(Optional.of(booleanLiteral(false)), toLiteral("fALse", AttributeKind.TYPE_BOOL)); + assertEquals(Optional.of(booleanLiteral(false)), toLiteral("false", AttributeKind.TYPE_BOOL)); + + assertEquals(empty(), toLiteral("test", AttributeKind.TYPE_TIMESTAMP)); + assertEquals( + Optional.of(longLiteral(TEST_TIMESTAMP_MS)), + toLiteral(String.valueOf(TEST_TIMESTAMP_MS), AttributeKind.TYPE_TIMESTAMP)); + assertEquals( + Optional.of(longLiteral(TEST_TIMESTAMP_MS)), + toLiteral(TEST_TIMESTAMP_STRING, AttributeKind.TYPE_TIMESTAMP)); + } + + @Test + void coercesFromBoolean() { + assertEquals(Optional.of(booleanLiteral(true)), toLiteral(true, AttributeKind.TYPE_BOOL)); + assertEquals(Optional.of(booleanLiteral(false)), toLiteral(false, AttributeKind.TYPE_BOOL)); + + assertEquals(Optional.of(stringLiteral("true")), toLiteral(true, AttributeKind.TYPE_STRING)); + assertEquals(Optional.of(stringLiteral("false")), toLiteral(false, AttributeKind.TYPE_STRING)); + + assertEquals(empty(), toLiteral(true, AttributeKind.TYPE_INT64)); + assertEquals(empty(), toLiteral(true, AttributeKind.TYPE_DOUBLE)); + assertEquals(empty(), toLiteral(true, AttributeKind.TYPE_TIMESTAMP)); + } + + @Test + void coercesFromLong() { + assertEquals(Optional.of(longLiteral(10L)), toLiteral(10L, AttributeKind.TYPE_INT64)); + assertEquals(Optional.of(longLiteral(-10L)), toLiteral(-10L, AttributeKind.TYPE_INT64)); + + assertEquals(Optional.of(doubleLiteral(10d)), toLiteral(10L, AttributeKind.TYPE_DOUBLE)); + assertEquals(Optional.of(doubleLiteral(-10d)), toLiteral(-10L, AttributeKind.TYPE_DOUBLE)); + + assertEquals(Optional.of(stringLiteral("10")), toLiteral(10L, AttributeKind.TYPE_STRING)); + assertEquals(Optional.of(stringLiteral("-10")), toLiteral(-10L, AttributeKind.TYPE_STRING)); + + assertEquals(Optional.of(longLiteral(10L)), toLiteral(10L, AttributeKind.TYPE_TIMESTAMP)); + assertEquals(empty(), toLiteral(-10L, AttributeKind.TYPE_TIMESTAMP)); + + assertEquals(empty(), toLiteral(10L, AttributeKind.TYPE_BOOL)); + } + + @Test + void coercesFromDouble() { + assertEquals(Optional.of(longLiteral(10L)), toLiteral(10d, AttributeKind.TYPE_INT64)); + assertEquals(Optional.of(longLiteral(-10L)), toLiteral(-10d, AttributeKind.TYPE_INT64)); + + assertEquals(Optional.of(doubleLiteral(10d)), toLiteral(10d, AttributeKind.TYPE_DOUBLE)); + assertEquals(Optional.of(doubleLiteral(-10d)), toLiteral(-10d, AttributeKind.TYPE_DOUBLE)); + + assertEquals(Optional.of(stringLiteral("10.0")), toLiteral(10d, AttributeKind.TYPE_STRING)); + assertEquals(Optional.of(stringLiteral("-10.0")), toLiteral(-10d, AttributeKind.TYPE_STRING)); + + assertEquals(Optional.of(longLiteral(10L)), toLiteral(10d, AttributeKind.TYPE_TIMESTAMP)); + assertEquals(empty(), toLiteral(-10d, AttributeKind.TYPE_TIMESTAMP)); + + assertEquals(empty(), toLiteral(10d, AttributeKind.TYPE_BOOL)); + } + + @Test + void coercesFromTemporal() { + assertEquals( + Optional.of(longLiteral(TEST_TIMESTAMP_MS)), + toLiteral(TEST_TIMESTAMP_INSTANT, AttributeKind.TYPE_TIMESTAMP)); + assertEquals( + Optional.of(longLiteral(TEST_TIMESTAMP_MS)), + toLiteral( + TEST_TIMESTAMP_INSTANT.atOffset(ZoneOffset.of("+07:00")), + AttributeKind.TYPE_TIMESTAMP)); + + assertEquals( + Optional.of(longLiteral(TEST_TIMESTAMP_MS)), + toLiteral(TEST_TIMESTAMP_INSTANT, AttributeKind.TYPE_INT64)); + assertEquals( + Optional.of(doubleLiteral(TEST_TIMESTAMP_MS)), + toLiteral(TEST_TIMESTAMP_INSTANT, AttributeKind.TYPE_DOUBLE)); + + assertEquals( + Optional.of(stringLiteral(TEST_TIMESTAMP_STRING)), + toLiteral(TEST_TIMESTAMP_INSTANT, AttributeKind.TYPE_STRING)); + + assertEquals(empty(), toLiteral(TEST_TIMESTAMP_INSTANT, AttributeKind.TYPE_BOOL)); + } + + @Test + void coercesToDouble() { + assertEquals(Optional.of(10.0d), fromLiteral(doubleLiteral(10.0d), AttributeKind.TYPE_DOUBLE)); + assertEquals(Optional.of(10.0d), fromLiteral(longLiteral(10), AttributeKind.TYPE_DOUBLE)); + assertEquals(Optional.of(10.0d), fromLiteral(stringLiteral("10"), AttributeKind.TYPE_DOUBLE)); + assertEquals(Optional.of(10.0d), fromLiteral(stringLiteral("10.0"), AttributeKind.TYPE_DOUBLE)); + assertEquals(empty(), fromLiteral(stringLiteral("test"), AttributeKind.TYPE_DOUBLE)); + assertEquals(empty(), fromLiteral(booleanLiteral(true), AttributeKind.TYPE_DOUBLE)); + assertEquals( + empty(), fromLiteral(LiteralValue.getDefaultInstance(), AttributeKind.TYPE_DOUBLE)); + } + + @Test + void coercesToLong() { + assertEquals(Optional.of(10L), fromLiteral(doubleLiteral(10.0d), AttributeKind.TYPE_INT64)); + assertEquals(Optional.of(10L), fromLiteral(longLiteral(10), AttributeKind.TYPE_INT64)); + assertEquals(Optional.of(10L), fromLiteral(stringLiteral("10"), AttributeKind.TYPE_INT64)); + assertEquals(empty(), fromLiteral(stringLiteral("10.0"), AttributeKind.TYPE_INT64)); + assertEquals(empty(), fromLiteral(stringLiteral("test"), AttributeKind.TYPE_INT64)); + assertEquals(empty(), fromLiteral(booleanLiteral(true), AttributeKind.TYPE_INT64)); + assertEquals(empty(), fromLiteral(LiteralValue.getDefaultInstance(), AttributeKind.TYPE_INT64)); + } + + @Test + void coercesToTimestamp() { + assertEquals( + Optional.of(TEST_TIMESTAMP_MS), + fromLiteral(doubleLiteral(TEST_TIMESTAMP_MS), AttributeKind.TYPE_TIMESTAMP)); + assertEquals( + Optional.of(TEST_TIMESTAMP_MS), + fromLiteral(longLiteral(TEST_TIMESTAMP_MS), AttributeKind.TYPE_TIMESTAMP)); + assertEquals( + Optional.of(TEST_TIMESTAMP_MS), + fromLiteral(stringLiteral(TEST_TIMESTAMP_STRING), AttributeKind.TYPE_TIMESTAMP)); + assertEquals( + Optional.of(TEST_TIMESTAMP_MS), + fromLiteral( + stringLiteral(String.valueOf(TEST_TIMESTAMP_MS)), AttributeKind.TYPE_TIMESTAMP)); + assertEquals(empty(), fromLiteral(stringLiteral("test"), AttributeKind.TYPE_TIMESTAMP)); + assertEquals(empty(), fromLiteral(booleanLiteral(true), AttributeKind.TYPE_TIMESTAMP)); + assertEquals( + empty(), fromLiteral(LiteralValue.getDefaultInstance(), AttributeKind.TYPE_TIMESTAMP)); + } + + @Test + void coercesToBoolean() { + assertEquals(empty(), fromLiteral(doubleLiteral(10.0d), AttributeKind.TYPE_BOOL)); + assertEquals(empty(), fromLiteral(longLiteral(10), AttributeKind.TYPE_BOOL)); + assertEquals(Optional.of(true), fromLiteral(stringLiteral("true"), AttributeKind.TYPE_BOOL)); + assertEquals(Optional.of(false), fromLiteral(stringLiteral("false"), AttributeKind.TYPE_BOOL)); + assertEquals(empty(), fromLiteral(stringLiteral("test"), AttributeKind.TYPE_BOOL)); + assertEquals(Optional.of(true), fromLiteral(booleanLiteral(true), AttributeKind.TYPE_BOOL)); + assertEquals(Optional.of(false), fromLiteral(booleanLiteral(false), AttributeKind.TYPE_BOOL)); + assertEquals(empty(), fromLiteral(LiteralValue.getDefaultInstance(), AttributeKind.TYPE_BOOL)); + } + + @Test + void coercesToString() { + assertEquals(Optional.of("10.0"), fromLiteral(doubleLiteral(10.0d), AttributeKind.TYPE_STRING)); + assertEquals(Optional.of("10"), fromLiteral(longLiteral(10), AttributeKind.TYPE_STRING)); + assertEquals( + Optional.of("true"), fromLiteral(stringLiteral("true"), AttributeKind.TYPE_STRING)); + assertEquals(Optional.of(""), fromLiteral(stringLiteral(""), AttributeKind.TYPE_STRING)); + assertEquals(Optional.of("true"), fromLiteral(booleanLiteral(true), AttributeKind.TYPE_STRING)); + assertEquals( + empty(), fromLiteral(LiteralValue.getDefaultInstance(), AttributeKind.TYPE_STRING)); + } +}