From da6607b17130ab045640618d505fda915ddb8e49 Mon Sep 17 00:00:00 2001 From: kolea2 <45548808+kolea2@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:37:31 -0500 Subject: [PATCH] feat: Structs mapper utility (#2278) * feat: Structs mapper utility * annotate with internalapi * cleaning up annotations --- .../main/java/com/google/cloud/Structs.java | 151 +++++++++++++ .../java/com/google/cloud/StructsTest.java | 209 ++++++++++++++++++ 2 files changed, 360 insertions(+) create mode 100644 java-core/google-cloud-core/src/main/java/com/google/cloud/Structs.java create mode 100644 java-core/google-cloud-core/src/test/java/com/google/cloud/StructsTest.java diff --git a/java-core/google-cloud-core/src/main/java/com/google/cloud/Structs.java b/java-core/google-cloud-core/src/main/java/com/google/cloud/Structs.java new file mode 100644 index 0000000000..889bb3364a --- /dev/null +++ b/java-core/google-cloud-core/src/main/java/com/google/cloud/Structs.java @@ -0,0 +1,151 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.util.Types; +import com.google.api.core.InternalApi; +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.protobuf.ListValue; +import com.google.protobuf.NullValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * This class contains static utility methods that operate on or return protobuf's {@code Struct} + * objects. This is considered an internal class and implementation detail. + */ +@InternalApi +public final class Structs { + + private Structs() {} + + /** + * This class wraps a protobuf's {@code Struct} object and offers a map interface to it, hiding + * protobuf types. + */ + private static final class StructMap extends AbstractMap { + + private final Set> entrySet; + + private StructMap(Struct struct) { + this.entrySet = new StructSet(struct); + } + + private static final class StructSet extends AbstractSet> { + + private static Entry valueToObject(Entry entry) { + return new AbstractMap.SimpleEntry<>( + entry.getKey(), Structs.valueToObject(entry.getValue())); + } + + private final Struct struct; + + private StructSet(Struct struct) { + this.struct = struct; + } + + @Override + public Iterator> iterator() { + return Iterators.transform( + struct.getFieldsMap().entrySet().iterator(), StructSet::valueToObject); + } + + @Override + public int size() { + return struct.getFieldsMap().size(); + } + } + + @Override + public Set> entrySet() { + return entrySet; + } + } + + /** Returns an unmodifiable map view of the {@link Struct} parameter. */ + public static Map asMap(Struct struct) { + return new StructMap(checkNotNull(struct)); + } + + /** + * Creates a new {@link Struct} object given the content of the provided {@code map} parameter. + * + *

Notice that all numbers (int, long, float and double) are serialized as double values. Enums + * are serialized as strings. + */ + public static Struct newStruct(Map map) { + Map valueMap = Maps.transformValues(checkNotNull(map), Structs::objectToValue); + return Struct.newBuilder().putAllFields(valueMap).build(); + } + + private static Object valueToObject(Value value) { + switch (value.getKindCase()) { + case NULL_VALUE: + return null; + case NUMBER_VALUE: + return value.getNumberValue(); + case STRING_VALUE: + return value.getStringValue(); + case BOOL_VALUE: + return value.getBoolValue(); + case STRUCT_VALUE: + return new StructMap(value.getStructValue()); + case LIST_VALUE: + return Lists.transform(value.getListValue().getValuesList(), Structs::valueToObject); + default: + throw new IllegalArgumentException(String.format("Unsupported protobuf value %s", value)); + } + } + + @SuppressWarnings("unchecked") + private static Value objectToValue(final Object obj) { + Value.Builder builder = Value.newBuilder(); + if (obj == null) { + builder.setNullValue(NullValue.NULL_VALUE); + return builder.build(); + } + Class objClass = obj.getClass(); + if (obj instanceof String) { + builder.setStringValue((String) obj); + } else if (obj instanceof Number) { + builder.setNumberValue(((Number) obj).doubleValue()); + } else if (obj instanceof Boolean) { + builder.setBoolValue((Boolean) obj); + } else if (obj instanceof Iterable || objClass.isArray()) { + builder.setListValue( + ListValue.newBuilder() + .addAllValues(Iterables.transform(Types.iterableOf(obj), Structs::objectToValue))); + } else if (objClass.isEnum()) { + builder.setStringValue(((Enum) obj).name()); + } else if (obj instanceof Map) { + Map map = (Map) obj; + builder.setStructValue(newStruct(map)); + } else { + throw new IllegalArgumentException(String.format("Unsupported protobuf value %s", obj)); + } + return builder.build(); + } +} diff --git a/java-core/google-cloud-core/src/test/java/com/google/cloud/StructsTest.java b/java-core/google-cloud-core/src/test/java/com/google/cloud/StructsTest.java new file mode 100644 index 0000000000..8c1610bbac --- /dev/null +++ b/java-core/google-cloud-core/src/test/java/com/google/cloud/StructsTest.java @@ -0,0 +1,209 @@ +/* + * Copyright 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.ListValue; +import com.google.protobuf.NullValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; +import java.util.HashMap; +import java.util.Map; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class StructsTest { + + private static final Double NUMBER = 42.0; + private static final String STRING = "string"; + private static final Boolean BOOLEAN = true; + private static final ImmutableList LIST = + ImmutableList.of(NUMBER, STRING, BOOLEAN); + private static final Map INNER_MAP = new HashMap<>(); + private static final Map MAP = new HashMap<>(); + private static final Value NULL_VALUE = + Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); + private static final Value NUMBER_VALUE = Value.newBuilder().setNumberValue(NUMBER).build(); + private static final Value STRING_VALUE = Value.newBuilder().setStringValue(STRING).build(); + private static final Value BOOLEAN_VALUE = Value.newBuilder().setBoolValue(BOOLEAN).build(); + private static final ListValue PROTO_LIST = + ListValue.newBuilder() + .addAllValues(ImmutableList.of(NUMBER_VALUE, STRING_VALUE, BOOLEAN_VALUE)) + .build(); + private static final Value LIST_VALUE = Value.newBuilder().setListValue(PROTO_LIST).build(); + private static final Struct INNER_STRUCT = + Struct.newBuilder() + .putAllFields( + ImmutableMap.of( + "null", NULL_VALUE, + "number", NUMBER_VALUE, + "string", STRING_VALUE, + "boolean", BOOLEAN_VALUE, + "list", LIST_VALUE)) + .build(); + private static final Value STRUCT_VALUE = Value.newBuilder().setStructValue(INNER_STRUCT).build(); + private static final ImmutableMap VALUE_MAP = + ImmutableMap.builder() + .put("null", NULL_VALUE) + .put("number", NUMBER_VALUE) + .put("string", STRING_VALUE) + .put("boolean", BOOLEAN_VALUE) + .put("list", LIST_VALUE) + .put("struct", STRUCT_VALUE) + .buildOrThrow(); + private static final Struct STRUCT = Struct.newBuilder().putAllFields(VALUE_MAP).build(); + private static final ImmutableMap EMPTY_MAP = ImmutableMap.of(); + + @BeforeClass + public static void beforeClass() { + INNER_MAP.put("null", null); + INNER_MAP.put("number", NUMBER); + INNER_MAP.put("string", STRING); + INNER_MAP.put("boolean", BOOLEAN); + INNER_MAP.put("list", LIST); + MAP.put("null", null); + MAP.put("number", NUMBER); + MAP.put("string", STRING); + MAP.put("boolean", BOOLEAN); + MAP.put("list", LIST); + MAP.put("struct", INNER_MAP); + } + + private void checkMapField(Map map, String key, T expected) { + assertThat(map).containsKey(key); + assertThat(map).containsEntry(key, expected); + } + + private void checkStructField(Struct struct, String key, Value expected) { + Map map = struct.getFieldsMap(); + checkMapField(map, key, expected); + } + + @Test + public void testAsMap() { + Map map = Structs.asMap(STRUCT); + checkMapField(map, "null", null); + checkMapField(map, "number", NUMBER); + checkMapField(map, "string", STRING); + checkMapField(map, "boolean", BOOLEAN); + checkMapField(map, "list", LIST); + checkMapField(map, "struct", INNER_MAP); + assertEquals(MAP, map); + } + + @Test + public void testAsMapPut() { + Map map = Structs.asMap(STRUCT); + try { + map.put("key", "value"); + fail(); + } catch (UnsupportedOperationException expected) { + + } + } + + @Test + public void testAsMapRemove() { + Map map = Structs.asMap(STRUCT); + try { + map.remove("null"); + fail(); + } catch (UnsupportedOperationException expected) { + + } + } + + @Test + public void testAsMapEmpty() { + Map map = Structs.asMap(Struct.getDefaultInstance()); + assertThat(map).isEmpty(); + assertEquals(EMPTY_MAP, map); + } + + @Test + public void testAsMapNull() { + try { + Structs.asMap(null); + fail(); + } catch (NullPointerException expected) { + } + } + + @Test + public void testNewStruct() { + Struct struct = Structs.newStruct(MAP); + checkStructField(struct, "null", NULL_VALUE); + checkStructField(struct, "number", NUMBER_VALUE); + checkStructField(struct, "string", STRING_VALUE); + checkStructField(struct, "boolean", BOOLEAN_VALUE); + checkStructField(struct, "list", LIST_VALUE); + checkStructField(struct, "struct", STRUCT_VALUE); + assertEquals(STRUCT, struct); + } + + @Test + public void testNewStructEmpty() { + Struct struct = Structs.newStruct(EMPTY_MAP); + assertThat(struct.getFieldsMap()).isEmpty(); + } + + @Test + public void testNewStructNull() { + try { + Structs.newStruct(null); + fail(); + } catch (NullPointerException expected) { + } + } + + @Test + public void testNumbers() { + int intNumber = Integer.MIN_VALUE; + long longNumber = Long.MAX_VALUE; + float floatNumber = Float.MIN_VALUE; + double doubleNumber = Double.MAX_VALUE; + ImmutableMap map = + ImmutableMap.of( + "int", intNumber, "long", longNumber, "float", floatNumber, "double", doubleNumber); + Struct struct = Structs.newStruct(map); + checkStructField(struct, "int", Value.newBuilder().setNumberValue(intNumber).build()); + checkStructField( + struct, "long", Value.newBuilder().setNumberValue((double) longNumber).build()); + checkStructField(struct, "float", Value.newBuilder().setNumberValue(floatNumber).build()); + checkStructField(struct, "double", Value.newBuilder().setNumberValue(doubleNumber).build()); + Map convertedMap = Structs.asMap(struct); + assertThat(convertedMap.get("int")).isInstanceOf(Double.class); + assertThat(convertedMap.get("long")).isInstanceOf(Double.class); + assertThat(convertedMap.get("float")).isInstanceOf(Double.class); + assertThat(convertedMap.get("double")).isInstanceOf(Double.class); + int convertedInteger = ((Double) convertedMap.get("int")).intValue(); + long convertedLong = ((Double) convertedMap.get("long")).longValue(); + float convertedFloat = ((Double) convertedMap.get("float")).floatValue(); + double convertedDouble = (Double) convertedMap.get("double"); + assertEquals(intNumber, convertedInteger); + assertEquals(longNumber, convertedLong); + assertEquals(floatNumber, convertedFloat, 0); + assertEquals(doubleNumber, convertedDouble, 0); + } +}