package com.google.api.server.spi.config.model; import static com.google.api.server.spi.config.model.EndpointsFlag.MAP_SCHEMA_FORCE_JSON_MAP_SCHEMA; import static com.google.api.server.spi.config.model.EndpointsFlag.MAP_SCHEMA_IGNORE_UNSUPPORTED_KEY_TYPES; import static com.google.api.server.spi.config.model.EndpointsFlag.MAP_SCHEMA_SUPPORT_ARRAYS_VALUES; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.ServiceContext; import com.google.api.server.spi.TypeLoader; import com.google.api.server.spi.config.Api; import com.google.api.server.spi.config.ApiConfigLoader; import com.google.api.server.spi.config.Transformer; import com.google.api.server.spi.config.annotationreader.ApiConfigAnnotationReader; import com.google.api.server.spi.config.model.ApiParameterConfig.Classification; import com.google.api.server.spi.config.model.Schema.Field; import com.google.api.server.spi.config.model.Schema.SchemaReference; import com.google.api.server.spi.response.CollectionResponse; import com.google.api.server.spi.testing.EnumEndpoint; import com.google.api.server.spi.testing.EnumValue; import com.google.api.server.spi.testing.TestEnum; import com.google.common.reflect.TypeParameter; import com.google.common.reflect.TypeToken; import org.junit.Before; import org.junit.Test; import java.util.HashMap; import java.util.Map; /** * Tests for {@link SchemaRepository}. */ public class SchemaRepositoryTest { private SchemaRepository repo; private ApiConfigLoader configLoader; private ApiConfig config; @Before public void setUp() throws Exception { TypeLoader typeLoader = new TypeLoader(getClass().getClassLoader()); ApiConfigAnnotationReader annotationReader = new ApiConfigAnnotationReader(typeLoader.getAnnotationTypes()); this.repo = new SchemaRepository(typeLoader); this.configLoader = new ApiConfigLoader(new ApiConfig.Factory(), typeLoader, annotationReader); this.config = configLoader.loadConfiguration(ServiceContext.create(), FooEndpoint.class); } @Test public void getOrAdd_genericRequestType() throws Exception { ApiMethodConfig methodConfig = fooEndpointSetParameterized(); checkParameterizedSchema( repo.getOrAdd(getRequestResource(methodConfig).getType(), config), Integer.class); } @Test public void getOrAdd_genericReturnType() throws Exception { ApiMethodConfig methodConfig = fooEndpointSetParameterized(); checkParameterizedSchema( repo.getOrAdd(methodConfig.getReturnType(), config), Integer.class); } @Test public void getOrAdd_responseCollection() throws Exception { ApiMethodConfig methodConfig = getMethodConfig("getIntegerCollection"); checkIntegerCollectionResponse(repo.getOrAdd(methodConfig.getReturnType(), config)); } @Test public void getOrAdd_intArray() throws Exception { ApiMethodConfig methodConfig = getMethodConfig("getPrimitiveIntegerArray"); checkIntegerCollection(repo.getOrAdd(methodConfig.getReturnType(), config)); } @Test public void getOrAdd_any() throws Exception { ApiMethodConfig methodConfig = getMethodConfig("getAny"); assertThat(repo.getOrAdd(methodConfig.getReturnType(), config)) .isEqualTo(SchemaRepository.ANY_SCHEMA); } @Test public void getOrAdd_mapType() throws Exception { //unsupported map types still use JsonMap schema checkJsonMap("getStringArrayMap"); //non-string key values generate an exception try { checkJsonMap("getArrayStringMap"); fail("Should have failed to generate map schema"); } catch (IllegalArgumentException e) { //expected exception } //supported map types generate proper map schema ApiMethodConfig methodConfig = getMethodConfig("getStringEnumMap"); Schema schema = repo.getOrAdd(methodConfig.getReturnType(), config); assertThat(schema).isEqualTo(Schema.builder() .setName("Map_String_TestEnum") .setType("object") .setMapValueSchema(Field.builder() .setName(SchemaRepository.MAP_UNUSED_MSG) .setType(FieldType.ENUM) .setSchemaReference(SchemaReference.create(repo, config, TypeToken.of(TestEnum.class))) .build()) .build()); } @Test public void getOrAdd_mapSubType() throws Exception { Schema expectedSchema = Schema.builder() .setName("Map_String_String") .setType("object") .setMapValueSchema(Field.builder() .setName(SchemaRepository.MAP_UNUSED_MSG) .setType(FieldType.STRING) .build()) .build(); assertThat(repo.getOrAdd(getMethodConfig("getMyMap").getReturnType(), config)) .isEqualTo(expectedSchema); assertThat(repo.getOrAdd(getMethodConfig("getMySubMap").getReturnType(), config)) .isEqualTo(expectedSchema); } @Test public void getOrAdd_mapTypeUnsupportedKeys() throws Exception { System.setProperty(MAP_SCHEMA_IGNORE_UNSUPPORTED_KEY_TYPES.systemPropertyName, "true"); try { checkJsonMap("getArrayStringMap"); } finally { System.clearProperty(MAP_SCHEMA_IGNORE_UNSUPPORTED_KEY_TYPES.systemPropertyName); } } @Test public void getOrAdd_NestedMap() throws Exception { Schema expectedSchema = Schema.builder() .setName("Map_String_Map_String_String") .setType("object") .setMapValueSchema(Field.builder() .setName(SchemaRepository.MAP_UNUSED_MSG) .setType(FieldType.OBJECT) .setSchemaReference(SchemaReference.create(repo, config, new TypeToken<Map<String, String>>() {})) .build()) .build(); assertThat(repo.getOrAdd(getMethodConfig("getNestedMap").getReturnType(), config)) .isEqualTo(expectedSchema); } @Test public void getOrAdd_ParameterizedMap() throws Exception { checkJsonMap("getParameterizedMap"); checkJsonMap("getParameterizedKeyMap"); checkJsonMap("getParameterizedValueMap"); } @Test public void getOrAdd_RawMap() throws Exception { checkJsonMap("getRawMap"); } @Test public void getOrAdd_mapTypeArrayValues() throws Exception { System.setProperty(MAP_SCHEMA_SUPPORT_ARRAYS_VALUES.systemPropertyName, "true"); try { ApiMethodConfig methodConfig = getMethodConfig("getStringArrayMap"); Schema schema = repo.getOrAdd(methodConfig.getReturnType(), config); assertThat(schema).isEqualTo(Schema.builder() .setName("Map_String_StringCollection") .setType("object") .setMapValueSchema(Field.builder() .setName(SchemaRepository.MAP_UNUSED_MSG) .setType(FieldType.ARRAY) .setArrayItemSchema(Field.builder() .setName(SchemaRepository.ARRAY_UNUSED_MSG) .setType(FieldType.STRING) .build()) .build()) .build()); } finally { System.clearProperty(MAP_SCHEMA_SUPPORT_ARRAYS_VALUES.systemPropertyName); } } @Test public void getOrAdd_jsonMap() throws Exception { System.setProperty(MAP_SCHEMA_FORCE_JSON_MAP_SCHEMA.systemPropertyName, "true"); try { checkJsonMap("getStringEnumMap"); checkJsonMap("getStringArrayMap"); checkJsonMap("getArrayStringMap"); } finally { System.clearProperty(MAP_SCHEMA_FORCE_JSON_MAP_SCHEMA.systemPropertyName); } } private void checkJsonMap(String methodName) throws Exception { ApiMethodConfig methodConfig = getMethodConfig(methodName); assertThat(repo.getOrAdd(methodConfig.getReturnType(), config)) .isEqualTo(SchemaRepository.MAP_SCHEMA); } @Test public void getOrAdd_transformer() throws Exception { ApiMethodConfig methodConfig = getMethodConfig("getTransformed"); checkParameterizedSchema( repo.getOrAdd(methodConfig.getReturnType(), config), String.class); } @Test public void getOrAdd_primitiveReturn() throws Exception { try { repo.getOrAdd(TypeToken.of(int.class), config); fail("expected IllegalArgumentException"); } catch (IllegalArgumentException expected) { // expected } } @Test public void getOrAdd_enum() throws Exception { assertThat(repo.getOrAdd(TypeToken.of(TestEnum.class), config)) .isEqualTo(Schema.builder() .setName("TestEnum") .setType("string") .addEnumValue("VALUE1") .addEnumValue("VALUE2") .addEnumDescription("") .addEnumDescription("") .build()); } @Test public void getOrAdd_recursiveSchema() throws Exception { TypeToken<SelfReferencingObject> type = TypeToken.of(SelfReferencingObject.class); // This test checks the case where a schema is added multiple times. The second time it's added // to an API, it recurses through the schema. If the schema is self-referencing, we must not // stack overflow. repo.getOrAdd(type, config); assertThat(repo.getOrAdd(type, config)) .isEqualTo(Schema.builder() .setName("SelfReferencingObject") .setType("object") .addField("foo", Field.builder() .setName("foo") .setType(FieldType.OBJECT) .setSchemaReference(SchemaReference.create(repo, config, type)) .build()) .build()); } @Test public void get() { TypeToken<Parameterized<Integer>> type = new TypeToken<Parameterized<Integer>>() {}; assertThat(repo.get(type, config)).isNull(); repo.getOrAdd(type, config); checkParameterizedSchema(repo.get(type, config), Integer.class); } @Test public void getOrAdd_multipleApis() throws Exception { // Adding the same resource to multiple APIs should add field resources as well. ApiConfig config2 = configLoader.loadConfiguration(ServiceContext.create(), EnumEndpoint.class); repo.getOrAdd(new TypeToken<EnumValue>() {}, config); repo.getOrAdd(new TypeToken<EnumValue>() {}, config2); assertThat(repo.getAllSchemaForApi(config2.getApiKey().withoutRoot())).containsExactly( Schema.builder() .setName("TestEnum") .setType("string") .addEnumValue("VALUE1") .addEnumValue("VALUE2") .addEnumDescription("") .addEnumDescription("") .build(), Schema.builder() .setName("EnumValue") .setType("object") .addField("value", Field.builder() .setName("value") .setSchemaReference(SchemaReference.create(repo, config2, new TypeToken<TestEnum>() {})) .setType(FieldType.ENUM) .build()) .build()); } @Api(transformers = {ParameterizedShortTransformer.class}) private static class FooEndpoint { public Parameterized<Integer> setParameterized(Parameterized<Integer> p) { return null; } public CollectionResponse<Integer> getIntegerCollection() { return null; } public int[] getPrimitiveIntegerArray() { return null; } public Object getAny() { return null; } public Map<String, TestEnum> getStringEnumMap() { return null; } public Map<String, String[]> getStringArrayMap() { return null; } public Map<String[], String> getArrayStringMap() { return null; } public MyMap getMyMap() { return null; } public Map<String, Map<String, String>> getNestedMap() { return null; } public <K, V> Map<K, V> getParameterizedMap() { return null; } public <K> Map<K, String> getParameterizedKeyMap() { return null; } public <V> Map<String, V> getParameterizedValueMap() { return null; } public Map getRawMap() { return null; } public MySubMap getMySubMap() { return null; } public Parameterized<Short> getTransformed() { return null; } } private static class MyMap extends HashMap<String, String> { } private static class MySubMap extends MyMap { } private static class Parameterized<T> { public T getFoo() { return null; } public void setFoo(T foo) { } public Parameterized<T> getNext() { return null; } public TestEnum getTestEnum() { return null; } } private static class ParameterizedShortTransformer implements Transformer<Parameterized<Short>, Parameterized<String>> { @Override public Parameterized<String> transformTo(Parameterized<Short> in) { return null; } @Override public Parameterized<Short> transformFrom(Parameterized<String> in) { return null; } } private static class SelfReferencingObject { public SelfReferencingObject getFoo() { return null; } } private ApiMethodConfig fooEndpointSetParameterized() throws Exception { return getMethodConfig("setParameterized", Parameterized.class); } private ApiMethodConfig getMethodConfig(String name, Class<?>... params) throws Exception { return config.getApiClassConfig().getMethods().get( EndpointMethod.create(FooEndpoint.class, FooEndpoint.class.getMethod(name, params))); } private static ApiParameterConfig getRequestResource(ApiMethodConfig methodConfig) { for (ApiParameterConfig parameterConfig : methodConfig.getParameterConfigs()) { if (parameterConfig.getClassification() == Classification.RESOURCE) { return parameterConfig; } } throw new IllegalStateException("no resource on method"); } private <T> void checkParameterizedSchema(Schema schema, Class<T> type) { assertThat(schema).isEqualTo(Schema.builder() .setName("Parameterized_" + type.getSimpleName()) .setType("object") .addField("foo", Field.builder() .setName("foo") .setType(FieldType.fromType(TypeToken.of(type))) .build()) .addField("next", Field.builder() .setName("next") .setType(FieldType.OBJECT) .setSchemaReference( SchemaReference.create(repo, config, new TypeToken<Parameterized<T>>() {} .where(new TypeParameter<T>() {}, TypeToken.of(type)))) .build()) .addField("testEnum", Field.builder() .setName("testEnum") .setType(FieldType.ENUM) .setSchemaReference(SchemaReference.create(repo, config, TypeToken.of(TestEnum.class))) .build()) .build()); } private static void checkIntegerCollectionResponse(Schema schema) { assertThat(schema).isEqualTo(Schema.builder() .setName("CollectionResponse_Integer") .setType("object") .addField("items", Field.builder() .setName("items") .setType(FieldType.ARRAY) .setArrayItemSchema(Field.builder() .setName("unused for array items") .setType(FieldType.INT32) .build()) .build()) .addField("nextPageToken", Field.builder() .setName("nextPageToken") .setType(FieldType.STRING) .build()) .build()); } private static void checkIntegerCollection(Schema schema) { assertThat(schema).isEqualTo(Schema.builder() .setName("IntegerCollection") .setType("object") .addField("items", Field.builder() .setName("items") .setType(FieldType.ARRAY) .setArrayItemSchema(Field.builder() .setName(SchemaRepository.ARRAY_UNUSED_MSG) .setType(FieldType.INT32) .build()) .build()) .build()); } }