Skip to content

Commit da6607b

Browse files
authored
feat: Structs mapper utility (#2278)
* feat: Structs mapper utility * annotate with internalapi * cleaning up annotations
1 parent a621b8c commit da6607b

File tree

2 files changed

+360
-0
lines changed

2 files changed

+360
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright 2016 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud;
17+
18+
import static com.google.common.base.Preconditions.checkNotNull;
19+
20+
import com.google.api.client.util.Types;
21+
import com.google.api.core.InternalApi;
22+
import com.google.common.collect.Iterables;
23+
import com.google.common.collect.Iterators;
24+
import com.google.common.collect.Lists;
25+
import com.google.common.collect.Maps;
26+
import com.google.protobuf.ListValue;
27+
import com.google.protobuf.NullValue;
28+
import com.google.protobuf.Struct;
29+
import com.google.protobuf.Value;
30+
import java.util.AbstractMap;
31+
import java.util.AbstractSet;
32+
import java.util.Iterator;
33+
import java.util.Map;
34+
import java.util.Set;
35+
36+
/**
37+
* This class contains static utility methods that operate on or return protobuf's {@code Struct}
38+
* objects. This is considered an internal class and implementation detail.
39+
*/
40+
@InternalApi
41+
public final class Structs {
42+
43+
private Structs() {}
44+
45+
/**
46+
* This class wraps a protobuf's {@code Struct} object and offers a map interface to it, hiding
47+
* protobuf types.
48+
*/
49+
private static final class StructMap extends AbstractMap<String, Object> {
50+
51+
private final Set<Entry<String, Object>> entrySet;
52+
53+
private StructMap(Struct struct) {
54+
this.entrySet = new StructSet(struct);
55+
}
56+
57+
private static final class StructSet extends AbstractSet<Entry<String, Object>> {
58+
59+
private static Entry<String, Object> valueToObject(Entry<String, Value> entry) {
60+
return new AbstractMap.SimpleEntry<>(
61+
entry.getKey(), Structs.valueToObject(entry.getValue()));
62+
}
63+
64+
private final Struct struct;
65+
66+
private StructSet(Struct struct) {
67+
this.struct = struct;
68+
}
69+
70+
@Override
71+
public Iterator<Entry<String, Object>> iterator() {
72+
return Iterators.transform(
73+
struct.getFieldsMap().entrySet().iterator(), StructSet::valueToObject);
74+
}
75+
76+
@Override
77+
public int size() {
78+
return struct.getFieldsMap().size();
79+
}
80+
}
81+
82+
@Override
83+
public Set<Entry<String, Object>> entrySet() {
84+
return entrySet;
85+
}
86+
}
87+
88+
/** Returns an unmodifiable map view of the {@link Struct} parameter. */
89+
public static Map<String, Object> asMap(Struct struct) {
90+
return new StructMap(checkNotNull(struct));
91+
}
92+
93+
/**
94+
* Creates a new {@link Struct} object given the content of the provided {@code map} parameter.
95+
*
96+
* <p>Notice that all numbers (int, long, float and double) are serialized as double values. Enums
97+
* are serialized as strings.
98+
*/
99+
public static Struct newStruct(Map<String, ?> map) {
100+
Map<String, Value> valueMap = Maps.transformValues(checkNotNull(map), Structs::objectToValue);
101+
return Struct.newBuilder().putAllFields(valueMap).build();
102+
}
103+
104+
private static Object valueToObject(Value value) {
105+
switch (value.getKindCase()) {
106+
case NULL_VALUE:
107+
return null;
108+
case NUMBER_VALUE:
109+
return value.getNumberValue();
110+
case STRING_VALUE:
111+
return value.getStringValue();
112+
case BOOL_VALUE:
113+
return value.getBoolValue();
114+
case STRUCT_VALUE:
115+
return new StructMap(value.getStructValue());
116+
case LIST_VALUE:
117+
return Lists.transform(value.getListValue().getValuesList(), Structs::valueToObject);
118+
default:
119+
throw new IllegalArgumentException(String.format("Unsupported protobuf value %s", value));
120+
}
121+
}
122+
123+
@SuppressWarnings("unchecked")
124+
private static Value objectToValue(final Object obj) {
125+
Value.Builder builder = Value.newBuilder();
126+
if (obj == null) {
127+
builder.setNullValue(NullValue.NULL_VALUE);
128+
return builder.build();
129+
}
130+
Class<?> objClass = obj.getClass();
131+
if (obj instanceof String) {
132+
builder.setStringValue((String) obj);
133+
} else if (obj instanceof Number) {
134+
builder.setNumberValue(((Number) obj).doubleValue());
135+
} else if (obj instanceof Boolean) {
136+
builder.setBoolValue((Boolean) obj);
137+
} else if (obj instanceof Iterable<?> || objClass.isArray()) {
138+
builder.setListValue(
139+
ListValue.newBuilder()
140+
.addAllValues(Iterables.transform(Types.iterableOf(obj), Structs::objectToValue)));
141+
} else if (objClass.isEnum()) {
142+
builder.setStringValue(((Enum<?>) obj).name());
143+
} else if (obj instanceof Map) {
144+
Map<String, Object> map = (Map<String, Object>) obj;
145+
builder.setStructValue(newStruct(map));
146+
} else {
147+
throw new IllegalArgumentException(String.format("Unsupported protobuf value %s", obj));
148+
}
149+
return builder.build();
150+
}
151+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
* Copyright 2016 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud;
17+
18+
import static com.google.common.truth.Truth.assertThat;
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.fail;
21+
22+
import com.google.common.collect.ImmutableList;
23+
import com.google.common.collect.ImmutableMap;
24+
import com.google.protobuf.ListValue;
25+
import com.google.protobuf.NullValue;
26+
import com.google.protobuf.Struct;
27+
import com.google.protobuf.Value;
28+
import java.util.HashMap;
29+
import java.util.Map;
30+
import org.junit.BeforeClass;
31+
import org.junit.Test;
32+
import org.junit.runner.RunWith;
33+
import org.junit.runners.JUnit4;
34+
35+
@RunWith(JUnit4.class)
36+
public class StructsTest {
37+
38+
private static final Double NUMBER = 42.0;
39+
private static final String STRING = "string";
40+
private static final Boolean BOOLEAN = true;
41+
private static final ImmutableList<Object> LIST =
42+
ImmutableList.<Object>of(NUMBER, STRING, BOOLEAN);
43+
private static final Map<String, Object> INNER_MAP = new HashMap<>();
44+
private static final Map<String, Object> MAP = new HashMap<>();
45+
private static final Value NULL_VALUE =
46+
Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build();
47+
private static final Value NUMBER_VALUE = Value.newBuilder().setNumberValue(NUMBER).build();
48+
private static final Value STRING_VALUE = Value.newBuilder().setStringValue(STRING).build();
49+
private static final Value BOOLEAN_VALUE = Value.newBuilder().setBoolValue(BOOLEAN).build();
50+
private static final ListValue PROTO_LIST =
51+
ListValue.newBuilder()
52+
.addAllValues(ImmutableList.of(NUMBER_VALUE, STRING_VALUE, BOOLEAN_VALUE))
53+
.build();
54+
private static final Value LIST_VALUE = Value.newBuilder().setListValue(PROTO_LIST).build();
55+
private static final Struct INNER_STRUCT =
56+
Struct.newBuilder()
57+
.putAllFields(
58+
ImmutableMap.of(
59+
"null", NULL_VALUE,
60+
"number", NUMBER_VALUE,
61+
"string", STRING_VALUE,
62+
"boolean", BOOLEAN_VALUE,
63+
"list", LIST_VALUE))
64+
.build();
65+
private static final Value STRUCT_VALUE = Value.newBuilder().setStructValue(INNER_STRUCT).build();
66+
private static final ImmutableMap<String, Value> VALUE_MAP =
67+
ImmutableMap.<String, Value>builder()
68+
.put("null", NULL_VALUE)
69+
.put("number", NUMBER_VALUE)
70+
.put("string", STRING_VALUE)
71+
.put("boolean", BOOLEAN_VALUE)
72+
.put("list", LIST_VALUE)
73+
.put("struct", STRUCT_VALUE)
74+
.buildOrThrow();
75+
private static final Struct STRUCT = Struct.newBuilder().putAllFields(VALUE_MAP).build();
76+
private static final ImmutableMap<String, Object> EMPTY_MAP = ImmutableMap.of();
77+
78+
@BeforeClass
79+
public static void beforeClass() {
80+
INNER_MAP.put("null", null);
81+
INNER_MAP.put("number", NUMBER);
82+
INNER_MAP.put("string", STRING);
83+
INNER_MAP.put("boolean", BOOLEAN);
84+
INNER_MAP.put("list", LIST);
85+
MAP.put("null", null);
86+
MAP.put("number", NUMBER);
87+
MAP.put("string", STRING);
88+
MAP.put("boolean", BOOLEAN);
89+
MAP.put("list", LIST);
90+
MAP.put("struct", INNER_MAP);
91+
}
92+
93+
private <T> void checkMapField(Map<String, T> map, String key, T expected) {
94+
assertThat(map).containsKey(key);
95+
assertThat(map).containsEntry(key, expected);
96+
}
97+
98+
private void checkStructField(Struct struct, String key, Value expected) {
99+
Map<String, Value> map = struct.getFieldsMap();
100+
checkMapField(map, key, expected);
101+
}
102+
103+
@Test
104+
public void testAsMap() {
105+
Map<String, Object> map = Structs.asMap(STRUCT);
106+
checkMapField(map, "null", null);
107+
checkMapField(map, "number", NUMBER);
108+
checkMapField(map, "string", STRING);
109+
checkMapField(map, "boolean", BOOLEAN);
110+
checkMapField(map, "list", LIST);
111+
checkMapField(map, "struct", INNER_MAP);
112+
assertEquals(MAP, map);
113+
}
114+
115+
@Test
116+
public void testAsMapPut() {
117+
Map<String, Object> map = Structs.asMap(STRUCT);
118+
try {
119+
map.put("key", "value");
120+
fail();
121+
} catch (UnsupportedOperationException expected) {
122+
123+
}
124+
}
125+
126+
@Test
127+
public void testAsMapRemove() {
128+
Map<String, Object> map = Structs.asMap(STRUCT);
129+
try {
130+
map.remove("null");
131+
fail();
132+
} catch (UnsupportedOperationException expected) {
133+
134+
}
135+
}
136+
137+
@Test
138+
public void testAsMapEmpty() {
139+
Map<String, Object> map = Structs.asMap(Struct.getDefaultInstance());
140+
assertThat(map).isEmpty();
141+
assertEquals(EMPTY_MAP, map);
142+
}
143+
144+
@Test
145+
public void testAsMapNull() {
146+
try {
147+
Structs.asMap(null);
148+
fail();
149+
} catch (NullPointerException expected) {
150+
}
151+
}
152+
153+
@Test
154+
public void testNewStruct() {
155+
Struct struct = Structs.newStruct(MAP);
156+
checkStructField(struct, "null", NULL_VALUE);
157+
checkStructField(struct, "number", NUMBER_VALUE);
158+
checkStructField(struct, "string", STRING_VALUE);
159+
checkStructField(struct, "boolean", BOOLEAN_VALUE);
160+
checkStructField(struct, "list", LIST_VALUE);
161+
checkStructField(struct, "struct", STRUCT_VALUE);
162+
assertEquals(STRUCT, struct);
163+
}
164+
165+
@Test
166+
public void testNewStructEmpty() {
167+
Struct struct = Structs.newStruct(EMPTY_MAP);
168+
assertThat(struct.getFieldsMap()).isEmpty();
169+
}
170+
171+
@Test
172+
public void testNewStructNull() {
173+
try {
174+
Structs.newStruct(null);
175+
fail();
176+
} catch (NullPointerException expected) {
177+
}
178+
}
179+
180+
@Test
181+
public void testNumbers() {
182+
int intNumber = Integer.MIN_VALUE;
183+
long longNumber = Long.MAX_VALUE;
184+
float floatNumber = Float.MIN_VALUE;
185+
double doubleNumber = Double.MAX_VALUE;
186+
ImmutableMap<String, Object> map =
187+
ImmutableMap.<String, Object>of(
188+
"int", intNumber, "long", longNumber, "float", floatNumber, "double", doubleNumber);
189+
Struct struct = Structs.newStruct(map);
190+
checkStructField(struct, "int", Value.newBuilder().setNumberValue(intNumber).build());
191+
checkStructField(
192+
struct, "long", Value.newBuilder().setNumberValue((double) longNumber).build());
193+
checkStructField(struct, "float", Value.newBuilder().setNumberValue(floatNumber).build());
194+
checkStructField(struct, "double", Value.newBuilder().setNumberValue(doubleNumber).build());
195+
Map<String, Object> convertedMap = Structs.asMap(struct);
196+
assertThat(convertedMap.get("int")).isInstanceOf(Double.class);
197+
assertThat(convertedMap.get("long")).isInstanceOf(Double.class);
198+
assertThat(convertedMap.get("float")).isInstanceOf(Double.class);
199+
assertThat(convertedMap.get("double")).isInstanceOf(Double.class);
200+
int convertedInteger = ((Double) convertedMap.get("int")).intValue();
201+
long convertedLong = ((Double) convertedMap.get("long")).longValue();
202+
float convertedFloat = ((Double) convertedMap.get("float")).floatValue();
203+
double convertedDouble = (Double) convertedMap.get("double");
204+
assertEquals(intNumber, convertedInteger);
205+
assertEquals(longNumber, convertedLong);
206+
assertEquals(floatNumber, convertedFloat, 0);
207+
assertEquals(doubleNumber, convertedDouble, 0);
208+
}
209+
}

0 commit comments

Comments
 (0)