Java tutorial
/* * Copyright 2016 Google Inc. All Rights Reserved. * * 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.api.server.spi.tools; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.api.server.spi.ObjectMapperUtil; import com.google.api.server.spi.ServiceContext; import com.google.api.server.spi.config.AnnotationBoolean; import com.google.api.server.spi.config.Api; import com.google.api.server.spi.config.ApiCacheControl; import com.google.api.server.spi.config.ApiClass; import com.google.api.server.spi.config.ApiConfigException; import com.google.api.server.spi.config.ApiMethod; import com.google.api.server.spi.config.ApiMethod.HttpMethod; import com.google.api.server.spi.config.ApiNamespace; import com.google.api.server.spi.config.ApiResourceProperty; import com.google.api.server.spi.config.ApiTransformer; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.DefaultValue; import com.google.api.server.spi.config.ResourcePropertySchema; import com.google.api.server.spi.config.ResourceSchema; import com.google.api.server.spi.config.ResourceTransformer; import com.google.api.server.spi.config.jsonwriter.JsonConfigWriter; import com.google.api.server.spi.config.model.ApiMethodConfig; import com.google.api.server.spi.config.validation.CollectionResourceException; import com.google.api.server.spi.config.validation.DuplicateParameterNameException; import com.google.api.server.spi.config.validation.DuplicateRestPathException; import com.google.api.server.spi.config.validation.GenericTypeException; import com.google.api.server.spi.config.validation.InconsistentApiConfigurationException; import com.google.api.server.spi.config.validation.InvalidNamespaceException; import com.google.api.server.spi.config.validation.InvalidParameterAnnotationsException; import com.google.api.server.spi.config.validation.InvalidReturnTypeException; import com.google.api.server.spi.config.validation.MissingParameterNameException; import com.google.api.server.spi.config.validation.NestedCollectionException; import com.google.api.server.spi.config.validation.OverloadedMethodException; import com.google.api.server.spi.response.CollectionResponse; import com.google.api.server.spi.testing.ArrayEndpoint; import com.google.api.server.spi.testing.Bar; import com.google.api.server.spi.testing.Baz; import com.google.api.server.spi.testing.BoundedGenericEndpoint; import com.google.api.server.spi.testing.BridgeInheritanceEndpoint; import com.google.api.server.spi.testing.CollectionContravarianceEndpoint; import com.google.api.server.spi.testing.CollectionCovarianceEndpoint; import com.google.api.server.spi.testing.DeepGenericHierarchyFailEndpoint; import com.google.api.server.spi.testing.DeepGenericHierarchySuccessEndpoint; import com.google.api.server.spi.testing.DefaultValueSerializer; import com.google.api.server.spi.testing.DuplicateMethodEndpoint; import com.google.api.server.spi.testing.Endpoint0; import com.google.api.server.spi.testing.Endpoint1; import com.google.api.server.spi.testing.Endpoint2; import com.google.api.server.spi.testing.Endpoint3; import com.google.api.server.spi.testing.Endpoint4; import com.google.api.server.spi.testing.Endpoint5; import com.google.api.server.spi.testing.ParentChildEndpoint; import com.google.api.server.spi.testing.PrimitiveEndpoint; import com.google.api.server.spi.testing.RecursiveEndpoint; import com.google.api.server.spi.testing.RestfulResourceEndpointBase; import com.google.api.server.spi.testing.SimpleContravarianceEndpoint; import com.google.api.server.spi.testing.SimpleCovarianceEndpoint; import com.google.api.server.spi.testing.SimpleLevelOverridingApi; import com.google.api.server.spi.testing.SimpleOverloadEndpoint; import com.google.api.server.spi.testing.SimpleOverrideEndpoint; import com.google.api.server.spi.testing.SubclassedEndpoint; import com.google.api.server.spi.testing.SubclassedOverridingEndpoint; import com.google.appengine.api.users.User; import com.google.common.reflect.TypeToken; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import javax.inject.Named; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; /** * Tests for {@link AnnotationApiConfigGenerator}. */ @RunWith(JUnit4.class) public class AnnotationApiConfigGeneratorTest { private final ObjectMapper objectMapper = ObjectMapperUtil.createStandardObjectMapper(); protected ApiConfigGenerator g; @Before public void setUp() throws Exception { g = createApiConfigGenerator(); } protected ApiConfigGenerator createApiConfigGenerator() throws Exception { return new AnnotationApiConfigGenerator(); } @Test public void testEndpointWithOnlyDefaultConfiguration() throws Exception { String apiConfigSource = g.generateConfig(Endpoint0.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); verifyApi(root, "thirdParty.api", "https://myapp.appspot.com/_ah/api", "myapi", "v1", "", "https://myapp.appspot.com/_ah/spi", false, true); JsonNode methodGetFoo = root.path("methods").path("myapi.endpoint0.getFoo"); verifyMethod(methodGetFoo, "foo/{id}", HttpMethod.GET, Endpoint0.class.getName() + ".getFoo", "autoTemplate(backendResponse)"); verifyMethodRequest(methodGetFoo.path("request"), "empty", 1); verifyMethodRequestParameter(methodGetFoo.path("request"), "id", "string", true, false); verifyEndpoint0Schema(root, Endpoint0.class.getName()); } @SuppressWarnings("unused") @Api private static class MultipleEndpoint1 { @ApiMethod(name = "add", path = "item", httpMethod = HttpMethod.POST) public void add(@Named("id") String id) { } } @SuppressWarnings("unused") @Api private static class MultipleEndpoint2 { @ApiMethod(name = "delete", path = "item/{id}", httpMethod = HttpMethod.DELETE) public void delete(@Named("id") String id) { } } @Test public void testMultipleServiceClasses() throws Exception { String apiConfigSource = g.generateConfig(MultipleEndpoint1.class, MultipleEndpoint2.class) .get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); assertEquals("item", root.path("methods").path("myapi.add").path("path").asText()); assertEquals("item/{id}", root.path("methods").path("myapi.delete").path("path").asText()); } @Test public void testFullyConfiguredEndpoint() throws Exception { String apiConfigSource = g.generateConfig(Endpoint1.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); verifyEndpoint1(Endpoint1.class, root); verifyEndpoint1Schema(Endpoint1.class, root); } @Test public void testEmptyRequestBodies() throws Exception { String apiConfigSource = g.generateConfig(Endpoint1.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); JsonNode methods = root.path("methods"); // myapi.foos.get JsonNode methodGetFoo = methods.path("myapi.foos.get"); assertTrue(methodGetFoo.path("request").path("body").asText().equals("empty")); assertFalse(root.path("descriptor").path("methods").path(methodGetFoo.path("rosyMethod").asText()) .has("request")); // myapi.foos.insert JsonNode methodInsertFoo = methods.path("myapi.foos.insert"); assertFalse(methodInsertFoo.path("request").path("body").asText().equals("empty")); assertTrue(root.path("descriptor").path("methods").path(methodInsertFoo.path("rosyMethod").asText()) .has("request")); // myapi.foos.execute0 JsonNode methodExecute0 = methods.path("myapi.foos.execute0"); assertTrue(methodExecute0.path("request").path("body").asText().equals("empty")); assertFalse(root.path("descriptor").path("methods").path(methodExecute0.path("rosyMethod").asText()) .has("request")); } @Test public void testEmptyParameterBodies() throws Exception { String apiConfigSource = g.generateConfig(Endpoint1.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); JsonNode methods = root.path("methods"); // foos.list JsonNode methodListFoo = methods.path("myapi.foos.list"); assertFalse(methodListFoo.path("request").has("parameters")); // foos.get JsonNode methodGetFoo = methods.path("myapi.foos.get"); assertTrue(methodGetFoo.path("request").has("parameters")); assertFalse(0 == methodGetFoo.path("request").path("parameters").size()); //foos.insert JsonNode methodInsertFoo = methods.path("myapi.foos.insert"); assertFalse(methodInsertFoo.path("request").has("parameters")); } @Test public void testEndpointWithNoPublicMethods() throws Exception { String apiConfigSource = g.generateConfig(Endpoint2.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); JsonNode methods = root.path("methods"); assertNull(methods.findValue("api2.foos.invisible0")); assertNull(methods.findValue("api2.foos.invisible1")); } @Test public void testEndpointWithInheritance() throws Exception { String apiConfigSource = g.generateConfig(Endpoint3.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); verifyEndpoint1(Endpoint3.class, root); verifyEndpoint3(root); verifyEndpoint1Schema(Endpoint3.class, root); verifyEndpoint3Schema(root); } @Test public void testEndpointWithBridgeMethods() throws Exception { String apiConfigSource = g.generateConfig(BridgeInheritanceEndpoint.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); JsonNode methods = root.path("methods"); assertEquals(2, methods.size()); assertNull(methods.findValue("myapi.fn1")); assertNotNull(methods.findValue("myapi.api6.foos.fn1")); assertNotNull(methods.findValue("myapi.api6.foos.fn2")); } @Test public void testDuplicateMethodEndpoint() throws Exception { try { g.generateConfig(DuplicateMethodEndpoint.class); fail("Config generation for endpoint with overloaded method should have failed."); } catch (OverloadedMethodException expected) { // expected } } @Test public void testSimpleOverrideEndpoint() throws Exception { String apiConfigSource = g.generateConfig(SimpleOverrideEndpoint.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); JsonNode methods = root.path("methods"); assertEquals(1, methods.size()); } @Test public void testSimpleOverloadEndpoint() throws Exception { try { g.generateConfig(SimpleOverloadEndpoint.class); fail("Config generation for endpoint with overloaded inherited method should have failed"); } catch (OverloadedMethodException expected) { // expected } } @Test public void testSimpleCovarianceEndpoint() throws Exception { try { g.generateConfig(SimpleCovarianceEndpoint.class); fail("Config generation for endpoint with covariant inherited method should have failed"); } catch (OverloadedMethodException expected) { // expected } } @Test public void testSimpleContravarianceEndpoint() throws Exception { try { g.generateConfig(SimpleContravarianceEndpoint.class); fail("Config generation for endpoint with contravariant inherited method should have failed"); } catch (OverloadedMethodException expected) { // expected } } @Test public void testCollectionCovarianceEndpoint() throws Exception { try { g.generateConfig(CollectionCovarianceEndpoint.class); fail("Config generation for endpoint with covariant inherited method should have failed"); } catch (OverloadedMethodException expected) { // expected } } @Test public void testCollectionContravarianceEndpoint() throws Exception { try { g.generateConfig(CollectionContravarianceEndpoint.class); fail("Config generation for endpoint with contravariant inherited method should have failed"); } catch (OverloadedMethodException expected) { // expected } } @Test public void testFullySpecializedDateEndpoint() throws Exception { String apiConfigSource = g.generateConfig(RestfulResourceEndpointBase.FullySpecializedEndpoint.class) .get("fullApi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); JsonNode methods = root.path("methods"); assertEquals(6, methods.size()); } @Test public void testGenericBasePartiallySpecializedEndpoint() throws Exception { String apiConfigSource = g.generateConfig(RestfulResourceEndpointBase.PartiallySpecializedEndpoint.class) .get("partialApi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); JsonNode methods = root.path("methods"); assertEquals(6, methods.size()); JsonNode rootPartial = objectMapper.readValue(apiConfigSource, JsonNode.class); JsonNode methodsPartial = rootPartial.path("descriptor").path("methods"); JsonNode rootFull = objectMapper.readValue( g.generateConfig(RestfulResourceEndpointBase.FullySpecializedEndpoint.class).get("fullApi-v1.api"), JsonNode.class); JsonNode methodsFull = rootFull.path("descriptor").path("methods"); String partialPrefix = RestfulResourceEndpointBase.PartiallySpecializedEndpoint.class.getName(); String fullPrefix = RestfulResourceEndpointBase.FullySpecializedEndpoint.class.getName(); // list JsonNode methodListPartial = methodsFull.path(partialPrefix + ".list"); JsonNode methodListFull = methodsFull.path(fullPrefix + ".list"); assertNotNull(methodListPartial); assertEquals(methodListPartial.path("request").asText(), methodListFull.path("request").asText()); // misc JsonNode methodMiscPartial = methodsFull.path("GenericRestBasePartiallySpecialized.misc"); JsonNode methodMiscFull = methodsFull.path("GenericRestBaseFullySpecialized.misc"); assertEquals(methodMiscPartial.path("request").asText(), methodMiscFull.path("request").asText()); } @Test public void testDeepGenericHierarchySuccessEndpoint() throws Exception { String apiConfigSource = g.generateConfig(DeepGenericHierarchySuccessEndpoint.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); JsonNode methods = root.path("methods"); assertEquals(1, methods.size()); } @Test public void testBoundedGenericEndpoint() throws Exception { String apiConfigSource = g.generateConfig(BoundedGenericEndpoint.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); JsonNode methods = root.path("methods"); assertEquals(1, methods.size()); } @Test public void testWildcardParameterTypes() throws Exception { @Api final class WildcardEndpoint { @SuppressWarnings("unused") public void foo(Map<String, ? extends Integer> map) { } } try { g.generateConfig(WildcardEndpoint.class); fail("Config generation for service class with wildcard parameter type should have failed"); } catch (IllegalArgumentException e) { // expected } } @Test public void testDeepGenericHierarchyFailEndpoint() throws Exception { try { g.generateConfig(DeepGenericHierarchyFailEndpoint.class); fail("Config generation for endpoint with contravariant inherited method should have failed"); } catch (OverloadedMethodException expected) { // expected } } @Test public void testServiceWithMergedInheritance() throws Exception { String apiConfigSource = g.generateConfig(SubclassedEndpoint.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); verifyEndpoint1(SubclassedEndpoint.class, root); verifyEndpoint1Schema(SubclassedEndpoint.class, root); } @Test public void testServiceWithOverridingInheritance() throws Exception { String apiConfigSource = g.generateConfig(SubclassedOverridingEndpoint.class).get("myapi-v2.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); verifySubclassedOverridingEndpoint(SubclassedOverridingEndpoint.class, root); verifySubclassedOverridingEndpointSchema(SubclassedOverridingEndpoint.class, root); } @Test public void testServiceWithApiNameOverride() throws Exception { String apiConfigSource = g.generateConfig(ServiceContext.create("abc", "xyz"), Endpoint4.class) .get("api4-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); assertEquals("api4", root.path("name").asText()); JsonNode methods = root.path("methods"); verifyEndpoint0Schema(root, Endpoint4.class.getName()); } @Test public void testDefaultEndpointOnGooglePlex() throws Exception { String apiConfigSource = g .generateConfig(ServiceContext.create("google.com:myapp", "myapi"), Endpoint0.class) .get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); assertEquals("myapi", root.path("name").asText()); assertEquals("https://myapp.googleplex.com/_ah/api", root.path("root").asText()); assertEquals("https://myapp.googleplex.com/_ah/spi", root.path("adapter").path("bns").asText()); } @Test public void testEndpointWithNestedBeans() throws Exception { String apiConfigSource = g.generateConfig(Endpoint5.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); verifyEndpoint5Schema(root); } @Test public void testPrimitiveSchemas() throws Exception { String apiConfigSource = g.generateConfig(PrimitiveEndpoint.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); verifyPrimitiveSchemas(root); } @Test public void testEndpointNoApiAnnotation() throws Exception { try { g.generateConfig(Object.class); fail("Config generation for service class with no @Api annotation should have failed"); } catch (ApiConfigException e) { // expected } } @Api static class TestUnnamedParameterType { public String getFoo(String id) { return null; } } @Test public void testEndpointWithUnnamedParameterTypes() throws Exception { try { g.generateConfig(TestUnnamedParameterType.class); fail("Config generation for service class with unnamed parameter type should have failed"); } catch (MissingParameterNameException e) { // expected } } @Test public void testSimpleLevelOverriding() throws Exception { String apiConfigSource = g.generateConfig(SimpleLevelOverridingApi.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); JsonNode methods = root.path("methods"); // myapi.resource1.noOverrides JsonNode noOverrides = methods.path("myapi.resource1.noOverrides"); assertFalse(noOverrides.isMissingNode()); verifyStrings(objectMapper.convertValue(noOverrides.path("scopes"), String[].class), new String[] { "s0a", "s1a" }); verifyStrings(objectMapper.convertValue(noOverrides.path("audiences"), String[].class), new String[] { "a0a", "a1a" }); verifyStrings(objectMapper.convertValue(noOverrides.path("clientIds"), String[].class), new String[] { "c0a", "c1a" }); assertEquals("resource1", objectMapper.convertValue(noOverrides.path("path"), String.class)); assertEquals(AuthLevel.REQUIRED, objectMapper.convertValue(noOverrides.path("authLevel"), AuthLevel.class)); // myapi.resource1.overrides JsonNode overrides = methods.path("myapi.resource1.overrides"); assertFalse(overrides.isMissingNode()); verifyStrings(objectMapper.convertValue(overrides.path("scopes"), String[].class), new String[] { "s0b", "s1b" }); verifyStrings(objectMapper.convertValue(overrides.path("audiences"), String[].class), new String[] { "a0b", "a1b" }); verifyStrings(objectMapper.convertValue(overrides.path("clientIds"), String[].class), new String[] { "c0b", "c1b" }); assertEquals("overridden", objectMapper.convertValue(overrides.path("path"), String.class)); assertEquals(AuthLevel.OPTIONAL, objectMapper.convertValue(overrides.path("authLevel"), AuthLevel.class)); } private void verifyPrimitiveSchemas(JsonNode root) { JsonNode schemas = root.path("descriptor").path("schemas"); JsonNode primitive = schemas.path("PrimitiveBean"); verifyPrimitiveSchema(primitive); JsonNode primitiveCollection = schemas.path("PrimitiveBeanCollection"); verifyArraySchemaRef(primitiveCollection.path("properties").path("items"), "PrimitiveBean"); String className = PrimitiveEndpoint.class.getName(); verifyEmptyMethodRequest(root, className + ".getPrimitive"); verifyMethodResponseRef(root, className + ".getPrimitive", "PrimitiveBean"); verifyEmptyMethodRequest(root, className + ".getPrimitives"); verifyMethodResponseRef(root, className + ".getPrimitives", "PrimitiveBeanCollection"); } private void verifyPrimitiveSchema(JsonNode primitive) { verifyObjectSchema(primitive, "PrimitiveBean", "object"); verifyObjectPropertySchema(primitive, "bool", "boolean"); verifyObjectPropertySchema(primitive, "byte", "integer"); verifyObjectPropertySchema(primitive, "char", "string"); verifyObjectPropertySchema(primitive, "double", "number"); verifyObjectPropertySchema(primitive, "float", "number", "float"); verifyObjectPropertySchema(primitive, "int", "integer"); verifyObjectPropertySchema(primitive, "long", "string", "int64"); verifyObjectPropertySchema(primitive, "short", "integer"); verifyObjectPropertySchema(primitive, "str", "string"); } @Test public void testRecursiveSchemas() throws Exception { String apiConfigSource = g.generateConfig(RecursiveEndpoint.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); verifyRecursiveSchemas(root); } private void verifyRecursiveSchemas(JsonNode root) { JsonNode schemas = root.path("descriptor").path("schemas"); JsonNode recursive = schemas.path("RecursiveBean"); verifyObjectSchema(recursive, "RecursiveBean", "object"); verifyObjectPropertySchema(recursive, "name", "string"); verifyObjectPropertyRef(recursive, "child", "RecursiveBean"); verifyObjectPropertyRef(recursive, "primitive", "PrimitiveBean"); JsonNode primitive = schemas.path("PrimitiveBean"); verifyPrimitiveSchema(primitive); String className = RecursiveEndpoint.class.getName(); verifyEmptyMethodRequest(root, className + ".getRecursive"); verifyMethodResponseRef(root, className + ".getRecursive", "RecursiveBean"); verifyMethodRequestRef(root, className + ".updateRecursive", "RecursiveBean"); verifyMethodResponseRef(root, className + ".updateRecursive", "RecursiveBean"); } @Test public void testParentChildSchemas() throws Exception { String apiConfigSource = g.generateConfig(ParentChildEndpoint.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); verifyParentChildSchemas(root); } private void verifyParentChildSchemas(JsonNode root) { JsonNode schemas = root.path("descriptor").path("schemas"); JsonNode parent = schemas.path("ParentBean"); verifyObjectSchema(parent, "ParentBean", "object"); verifyObjectPropertySchema(parent, "name", "string"); verifyArraySchemaRef(parent.path("properties").path("children"), "ChildBean"); JsonNode child = schemas.path("ChildBean"); verifyObjectSchema(child, "ChildBean", "object"); verifyObjectPropertySchema(child, "id", "integer"); verifyObjectPropertyRef(child, "parent", "ParentBean"); verifyArraySchema(child.path("properties").path("names"), "string"); JsonNode children = schemas.path("ChildBeanCollection"); verifyArraySchemaRef(children.path("properties").path("items"), "ChildBean"); verifyEmptyMethodRequest(root, ParentChildEndpoint.class.getName() + ".getParent"); verifyMethodResponseRef(root, ParentChildEndpoint.class.getName() + ".getParent", "ParentBean"); verifyEmptyMethodRequest(root, ParentChildEndpoint.class.getName() + ".listChildren"); verifyMethodResponseRef(root, ParentChildEndpoint.class.getName() + ".listChildren", "ChildBeanCollection"); } @Test public void testEndpointWithMultidimensionalArrays() throws Exception { String apiConfigSource = g.generateConfig(ArrayEndpoint.class).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class); verifyArrayEndpointSchema(root); } private void verifyArrayEndpointSchema(JsonNode root) { JsonNode schemas = root.path("descriptor").path("schemas"); JsonNode fooCollection = schemas.path("FooCollection"); verifyArraySchemaRef(fooCollection.path("properties").path("items"), "Foo"); JsonNode fooCollectionCollection = schemas.path("FooCollectionCollection"); verifyArraySchemaRef(fooCollectionCollection.path("properties").path("items"), "Foo", 2); JsonNode integerCollection = schemas.path("IntegerCollection"); verifyArraySchema(integerCollection.path("properties").path("items"), "integer"); JsonNode baz = schemas.path("Baz"); verifyObjectSchema(baz, "Baz", "object"); verifyObjectPropertyRef(baz, "foo", "Foo"); verifyArraySchemaRef(baz.path("properties").path("foos"), "Foo"); String backendName = ArrayEndpoint.class.getName(); verifyEmptyMethodRequest(root, backendName + ".getFoos"); verifyMethodResponseRef(root, backendName + ".getFoos", "FooCollection"); verifyEmptyMethodRequest(root, backendName + ".getAllFoos"); verifyMethodResponseRef(root, backendName + ".getAllFoos", "FooCollectionCollection"); verifyEmptyMethodRequest(root, backendName + ".getArrayedFoos"); verifyMethodResponseRef(root, backendName + ".getArrayedFoos", "FooCollection"); verifyEmptyMethodRequest(root, backendName + ".getAllArrayedFoosFoos"); verifyMethodResponseRef(root, backendName + ".getAllArrayedFoos", "FooCollectionCollection"); verifyEmptyMethodRequest(root, backendName + ".getFoosResponse"); verifyMethodResponseRef(root, backendName + ".getFoosResponse", "CollectionResponse_Foo"); verifyEmptyMethodRequest(root, backendName + ".getAllFoosResponse"); verifyMethodResponseRef(root, backendName + ".getAllFoosResponse", "CollectionResponse_FooCollection"); verifyEmptyMethodRequest(root, backendName + ".getAllNestedFoosResponse"); verifyMethodResponseRef(root, backendName + ".getAllNestedFoosResponse", "CollectionResponse_FooCollectionCollection"); verifyEmptyMethodRequest(root, backendName + ".getIntegers"); verifyMethodResponseRef(root, backendName + ".getIntegers", "IntegerCollection"); verifyEmptyMethodRequest(root, backendName + ".getObjectIntegers"); verifyMethodResponseRef(root, backendName + ".getObjectIntegers", "IntegerCollection"); verifyEmptyMethodRequest(root, backendName + ".getIntegersResponse"); verifyMethodResponseRef(root, backendName + ".getIntegersResponse", "CollectionResponse_Integer"); JsonNode arrayEndpoint = schemas.path("ArrayEndpoint"); verifyArraySchemaRef(arrayEndpoint.path("properties").path("foos"), "Foo"); verifyArraySchemaRef(arrayEndpoint.path("properties").path("allFoos"), "Foo", 2); verifyArraySchemaRef(arrayEndpoint.path("properties").path("arrayedFoos"), "Foo"); verifyArraySchemaRef(arrayEndpoint.path("properties").path("allArrayedFoos"), "Foo", 2); verifyArraySchema(arrayEndpoint.path("properties").path("integers"), "integer"); verifyArraySchema(arrayEndpoint.path("properties").path("objectIntegers"), "integer"); } private void verifyApi(JsonNode root, String superConfig, String apiRoot, String apiName, String apiVersion, String description, String backendRoot, boolean allowCookieAuth, boolean defaultVersion) { verifyApi(root, superConfig, apiRoot, apiName, apiVersion, description, backendRoot, allowCookieAuth, defaultVersion, null); } private void verifyApi(JsonNode root, String superConfig, String apiRoot, String apiName, String apiVersion, String description, String backendRoot, boolean allowCookieAuth, boolean defaultVersion, String[] blockedRegions) { assertEquals(superConfig, root.path("extends").asText()); assertEquals(false, root.path("abstract").asBoolean()); assertEquals(defaultVersion, root.path("defaultVersion").asBoolean()); assertEquals(apiRoot, root.path("root").asText()); assertEquals(apiName, root.path("name").asText()); assertEquals(apiVersion, root.path("version").asText()); assertEquals(description, root.path("description").asText()); JsonNode adapter = root.path("adapter"); assertEquals(backendRoot, adapter.path("bns").asText()); JsonNode auth = root.path("auth"); assertEquals(allowCookieAuth, auth.path("allowCookieAuth").asBoolean()); if (blockedRegions != null) { ArrayNode blockedRegionsNode = (ArrayNode) auth.path("blockedRegions"); for (int i = 0; i < blockedRegions.length; i++) { assertEquals(blockedRegions[i], blockedRegionsNode.get(i).asText()); } } } private void verifyEndpoint3(JsonNode root) { JsonNode methods = root.path("methods"); // myapi.listBars JsonNode methodListBars = methods.path("myapi.endpoint3.listBars"); verifyMethod(methodListBars, "bar", HttpMethod.GET, Endpoint3.class.getName() + ".listBars", "autoTemplate(backendResponse)"); verifyMethodRequest(methodListBars.path("request"), "empty", 0); // myapi.getBar JsonNode methodGetBar = methods.path("myapi.endpoint3.getBar"); verifyMethod(methodGetBar, "bar/{id}", HttpMethod.GET, Endpoint3.class.getName() + ".getBar", "autoTemplate(backendResponse)"); verifyMethodRequest(methodGetBar.path("request"), "empty", 1); verifyMethodRequestParameter(methodGetBar.path("request"), "id", "string", true, false); // myapi.insertBar JsonNode methodInsertBar = methods.path("myapi.endpoint3.insertBar"); verifyMethod(methodInsertBar, "bar", HttpMethod.POST, Endpoint3.class.getName() + ".insertBar", "autoTemplate(backendResponse)"); verifyMethodRequest(methodInsertBar.path("request"), "autoTemplate(backendRequest)", 0); // myapi.updateBar JsonNode methodUpdateBar = methods.path("myapi.endpoint3.updateBar"); verifyMethod(methodUpdateBar, "bar/{id}", HttpMethod.PUT, Endpoint3.class.getName() + ".updateBar", "autoTemplate(backendResponse)"); verifyMethodRequest(methodUpdateBar.path("request"), "autoTemplate(backendRequest)", 1); verifyMethodRequestParameter(methodUpdateBar.path("request"), "id", "string", true, false); // myapi.removeBar JsonNode methodRemoveBar = methods.path("myapi.endpoint3.removeBar"); verifyMethod(methodRemoveBar, "bar/{id}", HttpMethod.DELETE, Endpoint3.class.getName() + ".removeBar", "empty"); verifyMethodRequest(methodRemoveBar.path("request"), "empty", 1); verifyMethodRequestParameter(methodRemoveBar.path("request"), "id", "string", true, false); } private void verifyEndpoint1(Class<? extends Endpoint1> serviceClass, JsonNode root) { verifyApi(root, "thirdParty.api", "https://myapp.appspot.com/_ah/api", "myapi", "v1", "API for testing", "https://myapp.appspot.com/_ah/spi", true, true, new String[] { "CU" }); verifyFrontendLimits(root.path("frontendLimits"), 1, 2, 3); ArrayNode rules = (ArrayNode) root.path("frontendLimits").path("rules"); verifyFrontendLimitRule(rules.get(0), "match0", 1, 2, 3, "analyticsId0"); verifyFrontendLimitRule(rules.get(1), "match10", 11, 12, 13, "analyticsId10"); verifyCacheControl(root.path("cacheControl"), ApiCacheControl.Type.PUBLIC, 1); JsonNode methods = root.path("methods"); String serviceClassName = serviceClass.getName(); // myapi.foos.listFoos JsonNode methodListFoos = methods.path("myapi.foos.list"); verifyMethod(methodListFoos, "foos", HttpMethod.GET, serviceClassName + ".listFoos", "autoTemplate(backendResponse)"); verifyMethodRequest(methodListFoos.path("request"), "empty", 0); verifyStrings(objectMapper.convertValue(methodListFoos.path("scopes"), String[].class), new String[] { "s0", "s1 s2" }); verifyStrings(objectMapper.convertValue(methodListFoos.path("audiences"), String[].class), new String[] { "a0", "a1" }); verifyStrings(objectMapper.convertValue(methodListFoos.path("clientIds"), String[].class), new String[] { "c0", "c1" }); // myapi.foos.get JsonNode methodGetFoo = methods.path("myapi.foos.get"); verifyMethod(methodGetFoo, "foos/{id}", HttpMethod.GET, serviceClassName + ".getFoo", "autoTemplate(backendResponse)"); verifyMethodRequest(methodGetFoo.path("request"), "empty", 1); verifyMethodRequestParameter(methodGetFoo.path("request"), "id", "string", true, false); verifyStrings(objectMapper.convertValue(methodGetFoo.path("scopes"), String[].class), new String[] { "ss0", "ss1 ss2" }); verifyStrings(objectMapper.convertValue(methodGetFoo.path("audiences"), String[].class), new String[] { "aa0", "aa1" }); verifyStrings(objectMapper.convertValue(methodGetFoo.path("clientIds"), String[].class), new String[] { "cc0", "cc1" }); // myapi.foos.insert JsonNode methodInsertFoo = methods.path("myapi.foos.insert"); verifyMethod(methodInsertFoo, "foos", HttpMethod.POST, serviceClassName + ".insertFoo", "autoTemplate(backendResponse)"); verifyMethodRequest(methodInsertFoo.path("request"), "autoTemplate(backendRequest)", 0); // myapi.foos.update JsonNode methodUpdateFoo = methods.path("myapi.foos.update"); verifyMethod(methodUpdateFoo, "foos/{id}", HttpMethod.PUT, serviceClassName + ".updateFoo", "autoTemplate(backendResponse)"); verifyMethodRequest(methodUpdateFoo.path("request"), "autoTemplate(backendRequest)", 1); verifyMethodRequestParameter(methodUpdateFoo.path("request"), "id", "string", true, false); // myapi.foos.remove JsonNode methodRemoveFoo = methods.path("myapi.foos.remove"); verifyMethod(methodRemoveFoo, "foos/{id}", HttpMethod.DELETE, serviceClassName + ".removeFoo", "empty"); verifyMethodRequest(methodRemoveFoo.path("request"), "empty", 1); verifyMethodRequestParameter(methodRemoveFoo.path("request"), "id", "string", true, false); // myapi.foos.execute0 JsonNode methodExecute0 = methods.path("myapi.foos.execute0"); verifyMethod(methodExecute0, "execute0", HttpMethod.POST, serviceClassName + ".execute0", "autoTemplate(backendResponse)"); verifyMethodRequest(methodExecute0.path("request"), "empty", 9); JsonNode methodExecute0Request = methodExecute0.path("request"); verifyMethodRequestParameter(methodExecute0Request, "id", "string", true, false); verifyMethodRequestParameter(methodExecute0Request, "i0", "int32", true, false); verifyMethodRequestParameter(methodExecute0Request, "i1", "int32", false, false); verifyMethodRequestParameter(methodExecute0Request, "long0", "int64", true, false); verifyMethodRequestParameter(methodExecute0Request, "long1", "int64", false, false); verifyMethodRequestParameter(methodExecute0Request, "b0", "boolean", true, false); verifyMethodRequestParameter(methodExecute0Request, "b1", "boolean", false, false); verifyMethodRequestParameter(methodExecute0Request, "f", "float", true, false); verifyMethodRequestParameter(methodExecute0Request, "d", "double", false, false); // myapi.foos.execute2 JsonNode methodExecute2 = methods.path("myapi.foos.execute2"); verifyMethod(methodExecute2, "execute2/{serialized}", HttpMethod.POST, serviceClassName + ".execute2", "empty"); JsonNode methodExecute2Request = methodExecute2.path("request"); verifyMethodRequestParameter(methodExecute2Request, "serialized", "string", true, false); } private void verifySubclassedOverridingEndpoint(Class<? extends SubclassedOverridingEndpoint> serviceClass, JsonNode root) { verifyApi(root, "thirdParty.api", "https://myapp.appspot.com/_ah/api", "myapi", "v2", "overridden description", "https://myapp.appspot.com/_ah/spi", true, false); verifyFrontendLimits(root.path("frontendLimits"), 1, 4, 3); ArrayNode rules = (ArrayNode) root.path("frontendLimits").path("rules"); verifyFrontendLimitRule(rules.get(0), "match0", 1, 2, 3, "analyticsId0"); verifyFrontendLimitRule(rules.get(1), "match10", 11, 12, 13, "analyticsId10"); verifyCacheControl(root.path("cacheControl"), ApiCacheControl.Type.PUBLIC, 2); JsonNode methods = root.path("methods"); String serviceClassName = serviceClass.getName(); String servicePrefix = "myapi." + serviceClass.getSimpleName(); // myapi.foos.listFoos JsonNode methodListFoos = methods.path("myapi.foos.list"); verifyMethod(methodListFoos, "foos", HttpMethod.GET, serviceClassName + ".listFoos", "autoTemplate(backendResponse)"); verifyMethodRequest(methodListFoos.path("request"), "empty", 0); verifyStrings(objectMapper.convertValue(methodListFoos.path("scopes"), String[].class), new String[] { "s0", "s1 s2" }); verifyStrings(objectMapper.convertValue(methodListFoos.path("audiences"), String[].class), new String[] { "a0", "a1" }); verifyStrings(objectMapper.convertValue(methodListFoos.path("clientIds"), String[].class), new String[] { "c0", "c1" }); // myapi.foos.get assertTrue(methods.path(servicePrefix + ".foos.get").isMissingNode()); // myapi.foos.get2 JsonNode methodGetFoo = methods.path("myapi.foos.get2"); verifyMethod(methodGetFoo, "foos/{id}", HttpMethod.GET, serviceClassName + ".getFoo", "autoTemplate(backendResponse)"); verifyMethodRequest(methodGetFoo.path("request"), "empty", 1); verifyMethodRequestParameter(methodGetFoo.path("request"), "id", "string", true, false); verifyStrings(objectMapper.convertValue(methodGetFoo.path("scopes"), String[].class), new String[] { "ss0a", "ss1a" }); verifyStrings(objectMapper.convertValue(methodGetFoo.path("audiences"), String[].class), new String[] { "aa0a", "aa1a" }); verifyStrings(objectMapper.convertValue(methodGetFoo.path("clientIds"), String[].class), new String[] { "cc0a", "cc1a" }); // myapi.foos.insert JsonNode methodInsertFoo = methods.path("myapi.foos.insert"); verifyMethod(methodInsertFoo, "foos", HttpMethod.POST, serviceClassName + ".insertFoo", "autoTemplate(backendResponse)"); verifyMethodRequest(methodInsertFoo.path("request"), "autoTemplate(backendRequest)", 0); // myapi.foos.update JsonNode methodUpdateFoo = methods.path("myapi.foos.update"); verifyMethod(methodUpdateFoo, "foos/{id}", HttpMethod.PUT, serviceClassName + ".updateFoo", "autoTemplate(backendResponse)"); verifyMethodRequest(methodUpdateFoo.path("request"), "autoTemplate(backendRequest)", 1); verifyMethodRequestParameter(methodUpdateFoo.path("request"), "id", "string", true, false); // myapi.foos.remove JsonNode methodRemoveFoo = methods.path("myapi.foos.remove"); verifyMethod(methodRemoveFoo, "foos/{id}", HttpMethod.DELETE, serviceClassName + ".removeFoo", "empty"); verifyMethodRequest(methodRemoveFoo.path("request"), "empty", 1); verifyMethodRequestParameter(methodRemoveFoo.path("request"), "id", "string", true, false); // myapi.foos.execute0 JsonNode methodExecute0 = methods.path("myapi.foos.execute0"); verifyMethod(methodExecute0, "execute0", HttpMethod.POST, serviceClassName + ".execute0", "autoTemplate(backendResponse)"); verifyMethodRequest(methodExecute0.path("request"), "empty", 9); JsonNode methodExecute0Request = methodExecute0.path("request"); verifyMethodRequestParameter(methodExecute0Request, "id", "string", true, false); verifyMethodRequestParameter(methodExecute0Request, "i0", "int32", true, false); verifyMethodRequestParameter(methodExecute0Request, "i1", "int32", false, false); verifyMethodRequestParameter(methodExecute0Request, "long0", "int64", true, false); verifyMethodRequestParameter(methodExecute0Request, "long1", "int64", false, false); verifyMethodRequestParameter(methodExecute0Request, "b0", "boolean", true, false); verifyMethodRequestParameter(methodExecute0Request, "b1", "boolean", false, false); verifyMethodRequestParameter(methodExecute0Request, "f", "float", true, false); verifyMethodRequestParameter(methodExecute0Request, "d", "double", false, false); // myapi.foos.execute2 JsonNode methodExecute2 = methods.path("myapi.foos.execute2"); verifyMethod(methodExecute2, "execute2/{serialized}", HttpMethod.POST, serviceClassName + ".execute2", "empty"); JsonNode methodExecute2Request = methodExecute2.path("request"); verifyMethodRequestParameter(methodExecute2Request, "serialized", "int32", true, false); } private void verifyFrontendLimits(JsonNode frontendLimits, int unregisteredUserQps, int unregisteredQps, int unregisteredDaily) { assertEquals(unregisteredUserQps, frontendLimits.path("unregisteredUserQps").asInt()); assertEquals(unregisteredQps, frontendLimits.path("unregisteredQps").asInt()); assertEquals(unregisteredDaily, frontendLimits.path("unregisteredDaily").asInt()); } private void verifyFrontendLimitRule(JsonNode rule, String match, int qps, int userQps, int daily, String analyticsId) { assertEquals(match, rule.path("match").asText()); assertEquals(qps, rule.path("qps").asInt()); assertEquals(userQps, rule.path("userQps").asInt()); assertEquals(daily, rule.path("daily").asInt()); assertEquals(analyticsId, rule.path("analyticsId").asText()); } private void verifyCacheControl(JsonNode cacheControl, String type, int maxAge) { assertEquals(type, cacheControl.path("type").asText()); assertEquals(maxAge, cacheControl.path("maxAge").asInt()); } private void verifyMethod(JsonNode method, String restPath, String httpMethod, String backendMethod, String responseBody) { assertFalse(method.isMissingNode()); assertEquals(restPath, method.path("path").asText()); assertEquals(httpMethod, method.path("httpMethod").asText()); assertEquals(backendMethod, method.path("rosyMethod").asText()); JsonNode response = method.path("response"); assertEquals(responseBody, response.path("body").asText()); } private void verifyMethodRequest(JsonNode request, String requestBody, int parameterCount) { assertEquals(requestBody, request.path("body").asText()); if (!requestBody.equals("empty")) { assertEquals("resource", request.path("bodyName").asText()); } JsonNode parameters = request.path("parameters"); assertEquals(parameterCount, parameters.size()); } private void verifyMethodRequestParameter(JsonNode request, String parameterName, String type, boolean required, boolean repeated, String... enumValues) { assertFalse(request.isMissingNode()); JsonNode parameters = request.path("parameters"); JsonNode parameter = parameters.path(parameterName); assertEquals(required, parameter.path("required").asBoolean()); assertEquals(repeated, parameter.path("repeated").asBoolean()); assertEquals(type, parameter.path("type").asText()); if (enumValues.length > 0) { JsonNode enumValueNodes = parameter.path("enum"); assertFalse(enumValueNodes.isMissingNode()); assertEquals(enumValues.length, enumValueNodes.size()); for (String enumValue : enumValues) { assertNotNull(enumValueNodes.get(enumValue)); } } } private void verifyObjectSchema(JsonNode schema, String id, String type) { assertEquals(id, schema.path("id").asText()); assertEquals(type, schema.path("type").asText()); } private void verifyObjectPropertySchema(JsonNode schema, String name, String type) { assertEquals(type, schema.path("properties").path(name).path("type").asText()); } private void verifyObjectPropertySchema(JsonNode schema, String name, String type, String format) { assertEquals(type, schema.path("properties").path(name).path("type").asText()); assertEquals(format, schema.path("properties").path(name).path("format").asText()); } private void verifyObjectPropertyRef(JsonNode schema, String name, String ref) { assertEquals(ref, schema.path("properties").path(name).path("$ref").asText()); } private void verifyArraySchema(JsonNode array, String itemType) { verifyArraySchema(array, itemType, 1); } private void verifyArraySchema(JsonNode array, String itemType, int dimensions) { for (int i = 0; i < dimensions; i++) { assertEquals("array", array.path("type").asText()); array = array.path("items"); } assertEquals(itemType, array.path("type").asText()); } private void verifyArraySchemaRef(JsonNode array, String itemType) { verifyArraySchemaRef(array, itemType, 1); } private void verifyArraySchemaRef(JsonNode array, String itemType, int dimensions) { for (int i = 0; i < dimensions; i++) { assertEquals("array", array.path("type").asText()); array = array.path("items"); } assertEquals(itemType, array.path("$ref").asText()); } private void verifyEmptyMethodRequest(JsonNode root, String backendMethodName) { verifyMethodRequest(root, backendMethodName, "", ""); verifyMethodRequestRef(root, backendMethodName, ""); } private void verifyMethodRequest(JsonNode root, String backendMethodName, String requestType, String requestFormat) { JsonNode request = root.path("descriptor").path("methods").path(backendMethodName).path("request"); assertEquals(requestType, request.path("type").asText()); assertEquals(requestFormat, request.path("format").asText()); } private void verifyMethodRequestRef(JsonNode root, String backendMethodName, String requestType) { JsonNode request = root.path("descriptor").path("methods").path(backendMethodName).path("request"); assertEquals(requestType, request.path("$ref").asText()); } private void verifyEmptyMethodResponse(JsonNode root, String backendMethodName) { verifyMethodResponse(root, backendMethodName, "", ""); verifyMethodResponseRef(root, backendMethodName, ""); } private void verifyMethodResponse(JsonNode root, String backendMethodName, String responseType, String responseFormat) { JsonNode response = root.path("descriptor").path("methods").path(backendMethodName).path("response"); assertEquals(responseType, response.path("type").asText()); assertEquals(responseFormat, response.path("format").asText()); } private void verifyMethodResponseRef(JsonNode root, String backendMethodName, String responseType) { JsonNode response = root.path("descriptor").path("methods").path(backendMethodName).path("response"); assertEquals(responseType, response.path("$ref").asText()); } private void verifyEndpoint0Schema(JsonNode root, String className) { JsonNode schemas = root.path("descriptor").path("schemas"); JsonNode foo = schemas.path("Foo"); verifyObjectSchema(foo, "Foo", "object"); verifyObjectPropertySchema(foo, "name", "string"); verifyObjectPropertySchema(foo, "value", "integer"); verifyEmptyMethodRequest(root, className + ".getFoo"); verifyMethodResponseRef(root, className + ".getFoo", "Foo"); } private void verifyEndpoint1Schema(Class<? extends Endpoint1> serviceClass, JsonNode root) { JsonNode schemas = root.path("descriptor").path("schemas"); JsonNode foo = schemas.path("Foo"); verifyObjectSchema(foo, "Foo", "object"); verifyObjectPropertySchema(foo, "name", "string"); verifyObjectPropertySchema(foo, "value", "integer"); JsonNode fooCollection = schemas.path("FooCollection"); verifyArraySchemaRef(fooCollection.path("properties").path("items"), "Foo"); String serviceClassName = serviceClass.getName(); verifyEmptyMethodRequest(root, serviceClassName + ".listFoos"); verifyMethodResponseRef(root, serviceClassName + ".listFoos", "FooCollection"); verifyEmptyMethodRequest(root, serviceClassName + ".getFoo"); verifyMethodResponseRef(root, serviceClassName + ".getFoo", "Foo"); verifyMethodRequestRef(root, serviceClassName + ".insertFoo", "Foo"); verifyMethodResponseRef(root, serviceClassName + ".insertFoo", "Foo"); verifyMethodRequestRef(root, serviceClassName + ".updateFoo", "Foo"); verifyMethodResponseRef(root, serviceClassName + ".updateFoo", "Foo"); verifyEmptyMethodRequest(root, serviceClassName + ".removeFoo"); verifyEmptyMethodResponse(root, serviceClassName + ".removeFoo"); verifyEmptyMethodRequest(root, serviceClassName + ".execute0"); verifyMethodResponseRef(root, serviceClassName + ".execute0", JsonConfigWriter.ANY_SCHEMA_NAME); verifyMethodRequestRef(root, serviceClassName + ".execute1", "Foo"); verifyMethodResponseRef(root, serviceClassName + ".execute1", JsonConfigWriter.MAP_SCHEMA_NAME); } private void verifyEndpoint3Schema(JsonNode root) { JsonNode schemas = root.path("descriptor").path("schemas"); JsonNode foo = schemas.path("Bar"); verifyObjectSchema(foo, "Bar", "object"); verifyObjectPropertySchema(foo, "name", "string"); verifyObjectPropertySchema(foo, "value", "integer"); JsonNode foos = schemas.path("BarCollection"); verifyArraySchemaRef(foos.path("properties").path("items"), "Bar"); verifyEmptyMethodRequest(root, Endpoint3.class.getName() + ".listBars"); verifyMethodResponseRef(root, Endpoint3.class.getName() + ".listBars", "BarCollection"); verifyEmptyMethodRequest(root, Endpoint3.class.getName() + ".getBar"); verifyMethodResponseRef(root, Endpoint3.class.getName() + ".getBar", "Bar"); verifyMethodRequestRef(root, Endpoint3.class.getName() + ".insertBar", "Bar"); verifyMethodResponseRef(root, Endpoint3.class.getName() + ".insertBar", "Bar"); verifyMethodRequestRef(root, Endpoint3.class.getName() + ".updateBar", "Bar"); verifyMethodResponseRef(root, Endpoint3.class.getName() + ".updateBar", "Bar"); verifyEmptyMethodRequest(root, Endpoint3.class.getName() + ".removeBar"); verifyEmptyMethodResponse(root, Endpoint3.class.getName() + ".removeBar"); } private void verifyEndpoint5Schema(JsonNode root) { JsonNode schemas = root.path("descriptor").path("schemas"); JsonNode foo = schemas.path("Foo"); verifyObjectSchema(foo, "Foo", "object"); verifyObjectPropertySchema(foo, "name", "string"); verifyObjectPropertySchema(foo, "value", "integer"); JsonNode baz = schemas.path("Baz"); verifyObjectSchema(baz, "Baz", "object"); verifyObjectPropertyRef(baz, "foo", "Foo"); verifyArraySchemaRef(baz.path("properties").path("foos"), "Foo"); verifyEmptyMethodRequest(root, Endpoint5.class.getName() + ".getBaz"); verifyMethodResponseRef(root, Endpoint5.class.getName() + ".getBaz", "Baz"); } private void verifySubclassedOverridingEndpointSchema( Class<? extends SubclassedOverridingEndpoint> serviceClass, JsonNode root) { JsonNode schemas = root.path("descriptor").path("schemas"); JsonNode foo = schemas.path("Foo"); verifyObjectSchema(foo, "Foo", "object"); verifyObjectPropertySchema(foo, "name", "string"); verifyObjectPropertySchema(foo, "value", "integer"); JsonNode fooCollection = schemas.path("FooCollection"); verifyArraySchemaRef(fooCollection.path("properties").path("items"), "Foo"); String serviceClassName = serviceClass.getName(); verifyEmptyMethodRequest(root, serviceClassName + ".listFoos"); verifyMethodResponseRef(root, serviceClassName + ".listFoos", "FooCollection"); verifyEmptyMethodRequest(root, serviceClassName + ".getFoo"); verifyMethodResponseRef(root, serviceClassName + ".getFoo", "Foo"); verifyMethodRequestRef(root, serviceClassName + ".insertFoo", "Foo"); verifyMethodResponseRef(root, serviceClassName + ".insertFoo", "Foo"); verifyMethodRequestRef(root, serviceClassName + ".updateFoo", "Foo"); verifyMethodResponseRef(root, serviceClassName + ".updateFoo", "Foo"); verifyEmptyMethodRequest(root, serviceClassName + ".removeFoo"); verifyEmptyMethodResponse(root, serviceClassName + ".removeFoo"); verifyEmptyMethodRequest(root, serviceClassName + ".execute0"); verifyMethodResponseRef(root, serviceClassName + ".execute0", JsonConfigWriter.ANY_SCHEMA_NAME); verifyMethodRequestRef(root, serviceClassName + ".execute1", "Foo"); verifyMethodResponseRef(root, serviceClassName + ".execute1", JsonConfigWriter.MAP_SCHEMA_NAME); } private enum Outcome { WON, LOST, TIE } @SuppressWarnings("unused") private static class Bean { public Date getDate() { return null; } public void setDate(Date date) { } } @Test public void testValidDateInParameter() throws Exception { @Api class DateParameter { @SuppressWarnings("unused") public void foo(@Named("date") Date date) { } } String apiConfigSource = g.generateConfig(DateParameter.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode request = root.path("methods").path("myapi.dateParameter.foo").path("request"); verifyMethodRequestParameter(request, "date", "datetime", true, false); assertTrue(root.path("descriptor").path("schemas").path("Outcome").isMissingNode()); verifyEmptyMethodRequest(root, DateParameter.class.getName() + ".pick"); } @Test public void testDateCollection() throws Exception { @Api class DateParameters { @SuppressWarnings("unused") public void foo(@Named("date") Date date, @Named("dates1") Collection<Date> dates1, @Named("dates2") Date[] dates2) { } } String apiConfigSource = g.generateConfig(DateParameters.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode request = root.path("methods").path("myapi.dateParameters.foo").path("request"); verifyMethodRequestParameter(request, "date", "datetime", true, false); verifyMethodRequestParameter(request, "dates1", "datetime", true, true); verifyMethodRequestParameter(request, "dates2", "datetime", true, true); verifyEmptyMethodRequest(root, DateParameters.class.getName() + ".foo"); } @Test public void testInvalidDateInEndpointRequest() throws Exception { @Api class DateParameter { @SuppressWarnings("unused") public void foo(Date date) { } } try { g.generateConfig(DateParameter.class).get("myapi-v1.api"); fail("Dates should not be treated as resources"); } catch (MissingParameterNameException expected) { // expected } } private static class EnumBean { @SuppressWarnings("unused") public Outcome getOutcome() { return null; } } @Test public void testValidEnumInParameter() throws Exception { @Api class EnumParameter { @SuppressWarnings("unused") public void pick(@Named("outcome") Outcome outcome) { } } String apiConfigSource = g.generateConfig(EnumParameter.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode request = root.path("methods").path("myapi.enumParameter.pick").path("request"); verifyMethodRequestParameter(request, "outcome", "string", true, false, "WON", "LOST", "TIE"); assertTrue(root.path("descriptor").path("schemas").path("Outcome").isMissingNode()); verifyEmptyMethodRequest(root, EnumParameter.class.getName() + ".pick"); } @Test public void testValidEnumInParameterAndResource() throws Exception { @Api class EnumParameter { @SuppressWarnings("unused") public void pick(@Named("outcomeParam") Outcome outcome, EnumBean date) { } } String apiConfigSource = g.generateConfig(EnumParameter.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode outcomeSchema = root.path("descriptor").path("schemas").path("Outcome"); assertEquals("Outcome", outcomeSchema.path("id").asText()); assertEquals("string", outcomeSchema.path("type").asText()); JsonNode enumConfig = outcomeSchema.path("enum"); assertTrue(enumConfig.isArray()); assertEquals(3, enumConfig.size()); assertEquals(Outcome.WON.toString(), enumConfig.get(0).asText()); assertEquals(Outcome.LOST.toString(), enumConfig.get(1).asText()); assertEquals(Outcome.TIE.toString(), enumConfig.get(2).asText()); JsonNode enumSchema = root.path("descriptor").path("schemas").path("EnumBean"); assertEquals("EnumBean", enumSchema.path("id").asText()); assertEquals("object", enumSchema.path("type").asText()); assertNotNull(enumSchema.get("properties")); assertNotNull(enumSchema.path("properties").path("outcome").get("$ref")); assertEquals("Outcome", enumSchema.path("properties").path("outcome").path("$ref").asText()); JsonNode methodSchema = root.path("descriptor").path("methods") .path(EnumParameter.class.getName() + ".pick"); assertNotNull(methodSchema.get("request")); assertNotNull(methodSchema.path("request").get("$ref")); assertEquals("EnumBean", methodSchema.path("request").get("$ref").asText()); } /** * A {@code Bar} resource serializer with a property of type {@code Baz}. */ protected static class BarResourceSerializer extends DefaultValueSerializer<Bar, Map<String, Object>> implements ResourceTransformer<Bar> { public BarResourceSerializer() { } @Override public ResourceSchema getResourceSchema() { return ResourceSchema.builderForType(Bar.class) .addProperty("someBaz", ResourcePropertySchema.of(TypeToken.of(Baz.class))).build(); } } @Test public void testSerializedPropertyInResourceSchema() throws Exception { class BazToDateSerializer extends DefaultValueSerializer<Baz, Date> { } @Api(transformers = { BazToDateSerializer.class, BarResourceSerializer.class }) class BarEndpoint { @SuppressWarnings("unused") public Bar getBar() { return null; } } String apiConfigSource = g.generateConfig(BarEndpoint.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode bar = root.path("descriptor").path("schemas").path("Bar"); verifyObjectPropertySchema(bar, "someBaz", "string", "date-time"); } @Test public void testChainedSerializer() throws Exception { class BarToBazSerializer extends DefaultValueSerializer<Bar, Baz> { } class BazToDateSerializer extends DefaultValueSerializer<Baz, Date> { } class Qux { @SuppressWarnings("unused") public Bar getSomeBar() { return null; } } @Api(transformers = { BazToDateSerializer.class, BarToBazSerializer.class }) class QuxEndpoint { @SuppressWarnings("unused") public Qux getQux() { return null; } } String apiConfigSource = g.generateConfig(QuxEndpoint.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode qux = root.path("descriptor").path("schemas").path("Qux"); verifyObjectPropertySchema(qux, "someBar", "string", "date-time"); } @Test public void testSerializedEnum() throws Exception { class OutcomeToIntegerSerializer extends DefaultValueSerializer<Outcome, Integer> { } @Api(transformers = { OutcomeToIntegerSerializer.class }) class EnumParameter { @SuppressWarnings("unused") public void foo(@Named("outcome") Outcome outcome) { } } String apiConfigSource = g.generateConfig(EnumParameter.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode request = root.path("methods").path("myapi.enumParameter.foo").path("request"); verifyMethodRequestParameter(request, "outcome", "int32", true, false); } @Test public void testNonSerializedEnumShouldAlwaysBeString() throws Exception { class StringToIntegerSerializer extends DefaultValueSerializer<String, Integer> { } @Api(transformers = { StringToIntegerSerializer.class }) class EnumParameter { @SuppressWarnings("unused") public void foo(@Named("outcome") Outcome outcome) { } } String apiConfigSource = g.generateConfig(EnumParameter.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode request = root.path("methods").path("myapi.enumParameter.foo").path("request"); verifyMethodRequestParameter(request, "outcome", "string", true, false, "WON", "LOST", "TIE"); assertTrue(root.path("descriptor").path("schemas").path("Outcome").isMissingNode()); verifyEmptyMethodRequest(root, EnumParameter.class.getName() + ".pick"); } @Test public void testEnumCollection() throws Exception { @Api class EnumParameters { @SuppressWarnings("unused") public void foo(@Named("outcome") Outcome outcome, @Named("outcomes1") Collection<Outcome> outcomes1, @Named("outcomes2") Outcome[] outcomes2) { } } String apiConfigSource = g.generateConfig(EnumParameters.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode request = root.path("methods").path("myapi.enumParameters.foo").path("request"); verifyMethodRequestParameter(request, "outcome", "string", true, false, "WON", "LOST", "TIE"); verifyMethodRequestParameter(request, "outcomes1", "string", true, true, "WON", "LOST", "TIE"); verifyMethodRequestParameter(request, "outcomes2", "string", true, true, "WON", "LOST", "TIE"); assertTrue(root.path("descriptor").path("schemas").path("Outcome").isMissingNode()); verifyEmptyMethodRequest(root, EnumParameters.class.getName() + ".foo"); } @Test public void testInvalidEnumInEndpointRequest() throws Exception { @Api class EnumParameter { @SuppressWarnings("unused") public void pick(Outcome outcome) { } } try { g.generateConfig(EnumParameter.class).get("myapi-v1.api"); fail("Enums should not be treated as resources"); } catch (MissingParameterNameException expected) { // expected } } @SuppressWarnings("unused") @Api private static class Endpoint { public void remove(@Named("id") String id) { } public void delete(@Named("id") String id) { } } @Test public void testBareRemoveAndDelete() throws Exception { String apiConfigSource = g.generateConfig(Endpoint.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); // if we have no other clue, we should just use "remove" or "delete" as the REST path assertEquals("remove/{id}", root.path("methods").path("myapi.endpoint.remove").path("path").asText()); assertEquals("delete/{id}", root.path("methods").path("myapi.endpoint.delete").path("path").asText()); } @Api static class TestEndpoint { public Foo getItem(@Named("required") String required, @Nullable @Named("optional") String optional) { return null; } public List<Bar> list() { return null; } public CollectionResponse<Baz> listWithPagination() { return null; } private static class MyCollectionResponse<T> extends CollectionResponse<T> { protected MyCollectionResponse(Collection<T> items, String nextPageToken) { super(items, nextPageToken); } } public MyCollectionResponse<Foo> listWithMyCollectionResponse() { return null; } } @Test public void testOptionalParameters() throws Exception { String apiConfigSource = g.generateConfig(TestEndpoint.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); assertEquals("foo/{required}", root.path("methods").path("myapi.testEndpoint.getItem").path("path").asText()); } private void verifyStrings(String[] a0, String[] a1) { assertEquals(a0.length, a1.length); for (int i = 0; i < a0.length; i++) { assertEquals(a0[i], a1[i]); } } @Test public void testCollectionResponse() throws Exception { String apiConfigSource = g.generateConfig(TestEndpoint.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); // regular collection response assertEquals("bar", root.path("methods").path("myapi.testEndpoint.list").path("path").asText()); assertNotNull(root.path("descriptor").path("schemas").path("Bar")); assertNotNull(root.path("descriptor").path("schemas").path("BarCollection")); assertEquals("array", root.path("descriptor").path("schemas").path("BarCollection").path("properties") .path("items").path("type").asText()); assertEquals("Bar", root.path("descriptor").path("schemas").path("BarCollection").path("properties") .path("items").path("items").path("$ref").asText()); String backendName = TestEndpoint.class.getName(); // specific CollectionResponse assertEquals("baz", root.path("methods").path("myapi.testEndpoint.listWithPagination").path("path").asText()); assertEquals("CollectionResponse_Baz", root.path("descriptor").path("methods") .path(backendName + ".listWithPagination").path("response").path("$ref").asText()); assertNotNull(root.path("descriptor").path("schemas").path("Baz")); assertNotNull(root.path("descriptor").path("schemas").path("CollectionResponse_Baz")); assertEquals("array", root.path("descriptor").path("schemas").path("CollectionResponse_Baz") .path("properties").path("items").path("type").asText()); assertEquals("Baz", root.path("descriptor").path("schemas").path("CollectionResponse_Baz") .path("properties").path("items").path("items").path("$ref").asText()); assertEquals("string", root.path("descriptor").path("schemas").path("CollectionResponse_Baz") .path("properties").path("nextPageToken").path("type").asText()); assertEquals("CollectionResponse_Baz", root.path("descriptor").path("methods") .path(backendName + ".listWithPagination").path("response").path("$ref").asText()); // subclass of CollectionResponse assertEquals("foo", root.path("methods").path("myapi.testEndpoint.listWithMyCollectionResponse").path("path").asText()); assertEquals("MyCollectionResponse_Foo", root.path("descriptor").path("methods") .path(backendName + ".listWithMyCollectionResponse").path("response").path("$ref").asText()); assertNotNull(root.path("descriptor").path("schemas").path("Foo")); assertNotNull(root.path("descriptor").path("schemas").path("MyCollectionResponse_Foo")); assertEquals("array", root.path("descriptor").path("schemas").path("MyCollectionResponse_Foo") .path("properties").path("items").path("type").asText()); assertEquals("Foo", root.path("descriptor").path("schemas").path("MyCollectionResponse_Foo") .path("properties").path("items").path("items").path("$ref").asText()); assertEquals("string", root.path("descriptor").path("schemas").path("MyCollectionResponse_Foo") .path("properties").path("nextPageToken").path("type").asText()); assertEquals("MyCollectionResponse_Foo", root.path("descriptor").path("methods") .path(backendName + ".listWithMyCollectionResponse").path("response").path("$ref").asText()); } @Test public void testAbstract() throws Exception { @Api(isAbstract = AnnotationBoolean.TRUE) class Test { } String apiConfigSource = g.generateConfig(Test.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); assertTrue(root.get("abstract").asBoolean()); } @Test public void testRepeatedParameterName() throws Exception { @Api class Test { @ApiMethod(path = "path") public void foo(@Named("id") String id1, @Named("id") Long id2) { } } try { g.generateConfig(Test.class).get("myapi-v1.api"); fail("Config generation for endpoint with two parameters named the same should have failed."); } catch (DuplicateParameterNameException expected) { // expected } } @Test public void testNullablePathParameter() throws Exception { @Api class Test { @ApiMethod(path = "path/{id}") public void foo(@Named("id") @Nullable String id) { } } try { g.generateConfig(Test.class); fail("Config generation for endpoint with nullable path parameter should have failed."); } catch (InvalidParameterAnnotationsException expected) { // expected } } @Api private static class DefaultValuedPathParameterEndpoint<T> { @ApiMethod(path = "path/{id}") public void foo(@Named("id") @DefaultValue("bar") T id) { } } @Test public void testDefaultValuedPathParameterBoolean() throws Exception { final class Test extends DefaultValuedPathParameterEndpoint<Boolean> { } try { g.generateConfig(Test.class); fail("Config generation for endpoint with default-valued path parameter should have failed."); } catch (InvalidParameterAnnotationsException expected) { // expected } } @Test public void testDefaultValuedPathParameterInteger() throws Exception { final class Test extends DefaultValuedPathParameterEndpoint<Integer> { } try { g.generateConfig(Test.class); fail("Config generation for endpoint with default-valued path parameter should have failed."); } catch (InvalidParameterAnnotationsException expected) { // expected } } @Test public void testDefaultValuedPathParameterLong() throws Exception { final class Test extends DefaultValuedPathParameterEndpoint<Long> { } try { g.generateConfig(Test.class); fail("Config generation for endpoint with default-valued path parameter should have failed."); } catch (InvalidParameterAnnotationsException expected) { // expected } } @Test public void testDefaultValuedPathParameterString() throws Exception { final class Test extends DefaultValuedPathParameterEndpoint<String> { } try { g.generateConfig(Test.class); fail("Config generation for endpoint with default-valued path parameter should have failed."); } catch (InvalidParameterAnnotationsException expected) { // expected } } @Api private static class DefaultValuedEndpoint<T> { @SuppressWarnings("unused") public void foo(T id) { } } @Test public void testValidDefaultValuedParameterBoolean() throws Exception { final class Test extends DefaultValuedEndpoint<Boolean> { @Override public void foo(@Named("id") @DefaultValue("true") Boolean id) { } } assertEquals(true, implValidTestDefaultValuedParameter(Test.class).asBoolean()); } @Test public void testValidDefaultValuedParameterInteger() throws Exception { final class Test extends DefaultValuedEndpoint<Integer> { @Override public void foo(@Named("id") @DefaultValue("2718") Integer id) { } } assertEquals(2718, implValidTestDefaultValuedParameter(Test.class).asInt()); } @Test public void testValidDefaultValuedParameterLong() throws Exception { final class Test extends DefaultValuedEndpoint<Long> { @Override public void foo(@Named("id") @DefaultValue("3141") Long id) { } } assertEquals(3141L, implValidTestDefaultValuedParameter(Test.class).asLong()); } @Test public void testValidDefaultValuedParameterString() throws Exception { final class Test extends DefaultValuedEndpoint<String> { @Override public void foo(@Named("id") @DefaultValue("bar") String id) { } } assertEquals("bar", implValidTestDefaultValuedParameter(Test.class).asText()); } private <T> JsonNode implValidTestDefaultValuedParameter(Class<? extends DefaultValuedEndpoint<T>> clazz) throws Exception { String apiConfigSource = g.generateConfig(clazz).get("myapi-v1.api"); JsonNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode methodFoo = root.path("methods").path("myapi.test.foo"); JsonNode parameters = methodFoo.path("request").path("parameters"); JsonNode parameter = parameters.path("id"); JsonNode defaultValue = parameter.path("default"); assertFalse(parameter.path("required").isMissingNode()); assertEquals(false, parameter.path("required").asBoolean()); assertFalse(defaultValue.isMissingNode()); return defaultValue; } @Test public void testInvalidDefaultValuedParameterBoolean() throws Exception { implInvalidTestDefaultValuedParameter(new DefaultValuedEndpoint<Boolean>() { @Override public void foo(@Named("id") @DefaultValue("bar") Boolean id) { } }.getClass()); } @Test public void testInvalidDefaultValuedParameterInteger() throws Exception { implInvalidTestDefaultValuedParameter(new DefaultValuedEndpoint<Integer>() { @Override public void foo(@Named("id") @DefaultValue("bar") Integer id) { } }.getClass()); } @Test public void testInvalidDefaultValuedParameterLong() throws Exception { implInvalidTestDefaultValuedParameter(new DefaultValuedEndpoint<Long>() { @Override public void foo(@Named("id") @DefaultValue("bar") Long id) { } }.getClass()); } private <T> void implInvalidTestDefaultValuedParameter(Class<? extends DefaultValuedEndpoint<T>> clazz) throws Exception { try { g.generateConfig(clazz); fail("Config generation for endpoint with bad default for given type should have failed."); } catch (IllegalArgumentException expected) { // expected } } @Api class SameRestPathDifferentType<T1, T2> { @SuppressWarnings("unused") public List<Bar> list(@Named("id") T1 id) { return null; } @SuppressWarnings("unused") public Bar get(@Named("id") T2 id) { return null; } } @Test public void testSameRestPathStringString() throws Exception { implSameRestPathDifferentTypeTest(new SameRestPathDifferentType<String, String>() { }.getClass()); } @Test public void testSameRestPathIntegerLong() throws Exception { implSameRestPathDifferentTypeTest(new SameRestPathDifferentType<Long, String>() { }.getClass()); } @Test public void testSameRestPathIntegerString() throws Exception { implSameRestPathDifferentTypeTest(new SameRestPathDifferentType<Integer, String>() { }.getClass()); } @Test public void testSameRestPathIntegerBoolean() throws Exception { implSameRestPathDifferentTypeTest(new SameRestPathDifferentType<Integer, Boolean>() { }.getClass()); } private <T1, T2> void implSameRestPathDifferentTypeTest( Class<? extends SameRestPathDifferentType<T1, T2>> clazz) throws Exception { try { g.generateConfig(clazz).get("myapi-v1.api"); fail("Multiple methods with same RESTful signature"); } catch (DuplicateRestPathException expected) { // expected } } /** * Test if the generated API has the expected method paths. First try this out without any * explicit paths. */ public void testMethodPaths_repeatRestPath() throws Exception { @SuppressWarnings("unused") @Api class Dates { @ApiMethod(httpMethod = "GET", path = "dates/{id1}/{id2}") public Date get(@Named("id1") String id1, @Named("id2") String id2) { return null; } @ApiMethod(httpMethod = "GET", path = "dates/{x}/{y}") public List<Date> listFromXY(@Named("x") String x, @Named("y") String y) { return null; } } try { g.generateConfig(Dates.class).get("myapi-v1.api"); fail("Multiple methods with same RESTful signature"); } catch (DuplicateRestPathException expected) { // expected } } @Test public void testMethodPaths_apiAtCustomPath() throws Exception { @SuppressWarnings("unused") @Api(resource = "foos") class Foos { public List<Foo> list() { return null; } public Foo get(@Named("id") Long id) { return null; } } String apiConfigSource = g.generateConfig(Foos.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); verifyMethodPathAndHttpMethod(root, "myapi.foos.list", "foos", "GET"); verifyMethodPathAndHttpMethod(root, "myapi.foos.get", "foos/{id}", "GET"); } @Test public void testMethodPaths_apiAndMethodsAtCustomPaths() throws Exception { @Api(resource = "foo") class AcmeCo { @ApiMethod(path = "foos") public List<Foo> list() { return null; } @SuppressWarnings("unused") public List<Foo> listAllTheThings() { return null; } @ApiMethod(path = "give") public Foo giveMeOne() { return null; } } String apiConfigSource = g.generateConfig(AcmeCo.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); verifyMethodPathAndHttpMethod(root, "myapi.foo.list", "foos", "GET"); verifyMethodPathAndHttpMethod(root, "myapi.foo.listAllTheThings", "foo", "GET"); verifyMethodPathAndHttpMethod(root, "myapi.foo.giveMeOne", "give", "POST"); } private static void verifyMethodPathAndHttpMethod(JsonNode root, String methodName, String expectedPath, String expectedHttpMethod) { JsonNode methodNode = root.path("methods").path(methodName); assertEquals(expectedPath, methodNode.path("path").asText()); assertEquals(expectedHttpMethod, methodNode.path("httpMethod").asText()); } /** * A class for testing inheritance in Endpoints. */ abstract static class GenFoos<T> { public List<T> list() { return null; } public T get(@Named("id") long id) { return null; } public T insert(T object) { return null; } public T update(T updated) { return null; } public void remove(@Named("id") long id) { // empty } private static void verifyMethodPathsAndHttpMethods(Class<? extends GenFoos<?>> serviceClass, String path, JsonNode root, String resource) { String classPart = resource != null ? resource : serviceClass.getSimpleName(); String servicePrefix = ApiMethodConfig.methodNameFormatter("myapi." + classPart); verifyMethodPathAndHttpMethod(root, servicePrefix + ".insert", path, "POST"); verifyMethodPathAndHttpMethod(root, servicePrefix + ".update", path, "PUT"); verifyMethodPathAndHttpMethod(root, servicePrefix + ".list", path, "GET"); verifyMethodPathAndHttpMethod(root, servicePrefix + ".remove", path + "/{id}", "DELETE"); verifyMethodPathAndHttpMethod(root, servicePrefix + ".get", path + "/{id}", "GET"); } } /** * A class for testing inheritance in Endpoints with resource specified. */ @Api(resource = "bars") abstract static class GenBars<T> extends GenFoos<T> { private static void verifyMethodPathsAndHttpMethods(Class<? extends GenBars<?>> serviceClass, JsonNode root) { GenFoos.verifyMethodPathsAndHttpMethods(serviceClass, "bars", root, "bars"); } } static class Foo { } @Test public void testMethodPaths_inheritanceResourceSpecified() throws Exception { @Api(resource = "foos") class Foos extends GenFoos<Foo> { } String apiConfigSource = g.generateConfig(Foos.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); GenFoos.verifyMethodPathsAndHttpMethods(Foos.class, "foos", root, "foos"); } @Test public void testMethodPaths_inheritanceResourceInheritedAndSpecified() throws Exception { @Api(resource = "foos") class Foos extends GenBars<Foo> { } String apiConfigSource = g.generateConfig(Foos.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); GenFoos.verifyMethodPathsAndHttpMethods(Foos.class, "foos", root, "foos"); } @Test public void testMethodPaths_inheritanceResourceUnspecified() throws Exception { @Api class Foos extends GenFoos<Foo> { } String apiConfigSource = g.generateConfig(Foos.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); verifyMethodPathAndHttpMethod(root, "myapi.foos.insert", "foo", "POST"); verifyMethodPathAndHttpMethod(root, "myapi.foos.update", "foo", "PUT"); verifyMethodPathAndHttpMethod(root, "myapi.foos.list", "foo", "GET"); verifyMethodPathAndHttpMethod(root, "myapi.foos.remove", "remove/{id}", "DELETE"); verifyMethodPathAndHttpMethod(root, "myapi.foos.get", "foo/{id}", "GET"); } @Test public void testMethodPaths_inheritanceResourceInheritedAndUnspecified() throws Exception { @Api class Foos extends GenBars<Foos> { } String apiConfigSource = g.generateConfig(Foos.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); GenBars.verifyMethodPathsAndHttpMethods(Foos.class, root); } @Test public void testMultipleGenericClasses() throws Exception { @Api class GenFoo extends GenBars<Foo> { } @Api @ApiClass(resource = "resource2") class GenBar extends GenBars<Bar> { } String apiConfigSource = g.generateConfig(GenFoo.class, GenBar.class).get("myapi-v1.api"); } @Api private static class SimpleFoo<R> { @SuppressWarnings("unused") public void foo(R param) { } } @Test public void testRequestDoesContainMap() throws Exception { checkRequestIsNotEmpty(new SimpleFoo<Map<ServletContext, User>>() { }.getClass()); } @Test public void testRequestDoesNotContainUser() throws Exception { checkRequestIsEmpty(new SimpleFoo<User>() { }.getClass()); } @Test public void testRequestDoesNotContainHttpServletRequest() throws Exception { checkRequestIsEmpty(new SimpleFoo<HttpServletRequest>() { }.getClass()); } @Test public void testRequestDoesNotContainServletContext() throws Exception { checkRequestIsEmpty(new SimpleFoo<ServletContext>() { }.getClass()); } private void checkRequestIsEmpty(Class<? extends SimpleFoo<?>> clazz) throws Exception { checkRequest(clazz, true); } private void checkRequestIsNotEmpty(Class<? extends SimpleFoo<?>> clazz) throws Exception { checkRequest(clazz, false); } private void checkRequest(Class<? extends SimpleFoo<?>> clazz, boolean isEmpty) throws Exception { String apiConfigSource = g.generateConfig(clazz).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); String test = clazz.getName() + ".foo"; JsonNode methodFooRequest = root.path("descriptor").path("methods").path(clazz.getName() + ".foo") .path("request"); assertEquals(isEmpty, methodFooRequest.isMissingNode()); } @Api(transformers = { SerializerTestConvertedBeanFromApiSerializer.class }) private static class SerializerTestEndpoint { @SuppressWarnings("unused") public SerializerTestBean getFoo() { return null; } @SuppressWarnings("unused") public SerializerTestOverridingPropertyBean getBar() { return null; } @SuppressWarnings("unused") public SerializerTestConvertedBeanFromApi getBaz() { return null; } @SuppressWarnings("unused") public void testMethodRequestBody(SerializerTestConvertedBean body) { } } @Api private static class BadMethodRequestEndpoint { @SuppressWarnings("unused") public void testBadMethodRequestBody(StringBean body) { } } private static class SerializerTestBean { @ApiResourceProperty(name = "baz") public String getFoo() { return null; } @ApiResourceProperty(ignored = AnnotationBoolean.TRUE) public String getBar() { return null; } @SuppressWarnings("unused") public SerializerTestConvertedBean getConverted() { return null; } } @ApiTransformer(SerializerTestConvertedBeanSerializer.class) private static class SerializerTestConvertedBean { @SuppressWarnings("unused") public Integer getFoo() { return null; } } private static class SerializerTestConvertedBeanFromApi { @SuppressWarnings("unused") public Integer getFoo() { return null; } } private static class SerializerTestConvertedToBean { @SuppressWarnings("unused") public String getFoo() { return null; } } private static class SerializerTestConvertedBeanSerializer extends DefaultValueSerializer<SerializerTestConvertedBean, SerializerTestConvertedToBean> { } private static class SerializerTestConvertedBeanFromApiSerializer extends DefaultValueSerializer<SerializerTestConvertedBeanFromApi, SerializerTestConvertedToBean> { } private static class SerializerTestOverridingPropertyBean { @ApiResourceProperty(name = "bar") public String getFoo() { return null; } @ApiResourceProperty(ignored = AnnotationBoolean.TRUE) public Integer getBar() { return null; } } @ApiTransformer(StringBeanSerializer.class) private static class StringBean { @SuppressWarnings("unused") public String getFoo() { return null; } } private static class StringBeanSerializer extends DefaultValueSerializer<StringBean, String> { } @Test public void testCustomSerialization_renamedProperty() throws Exception { String apiConfigSource = g.generateConfig(SerializerTestEndpoint.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode schemas = root.path("descriptor").path("schemas"); verifyObjectPropertySchema(schemas.path("SerializerTestBean"), "baz", "string"); } @Test public void testCustomSerialization_ignoredProperty() throws Exception { String apiConfigSource = g.generateConfig(SerializerTestEndpoint.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode schemas = root.path("descriptor").path("schemas"); assertTrue(schemas.path("SerializerTestBean").path("properties").path("bar").isMissingNode()); } @Test public void testCustomSerialization_customizedProperty() throws Exception { String apiConfigSource = g.generateConfig(SerializerTestEndpoint.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode schemas = root.path("descriptor").path("schemas"); verifyObjectPropertyRef(schemas.path("SerializerTestBean"), "converted", "SerializerTestConvertedToBean"); } @Test public void testCustomSerialization_customizedPropertyFromApi() throws Exception { String apiConfigSource = g.generateConfig(SerializerTestEndpoint.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); verifyMethodResponseRef(root, SerializerTestEndpoint.class.getName() + ".getBaz", "SerializerTestConvertedToBean"); } @Test public void testCustomSerialization_overridenProperty() throws Exception { String apiConfigSource = g.generateConfig(SerializerTestEndpoint.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode schemas = root.path("descriptor").path("schemas"); verifyObjectPropertySchema(schemas.path("SerializerTestOverridingPropertyBean"), "bar", "string"); } @Test public void testCustomSerialization_requestBody() throws Exception { Class<?> clazz = SerializerTestEndpoint.class; String apiConfigSource = g.generateConfig(clazz).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); verifyMethodRequestRef(root, clazz.getName() + ".testMethodRequestBody", "SerializerTestConvertedToBean"); } @Test public void testCustomSerialization_badRequestBodyAfterSerialization() throws Exception { try { g.generateConfig(BadMethodRequestEndpoint.class).get("myapi-v1.api"); fail("Type serialized to string should not be usable as a method request body"); } catch (MissingParameterNameException e) { // expected } } @Api private static class EmptyRequestEndpoint { @SuppressWarnings("unused") public void foo() { } @SuppressWarnings("unused") public Void bar() { return null; } } @Test public void testEmptyRequestBody_void() throws Exception { testEmpty("foo", "request"); } @Test public void testEmptyResponseBody_void() throws Exception { testEmpty("foo", "response"); } @Test public void testEmptyRequestBody_Void() throws Exception { testEmpty("bar", "request"); } @Test public void testEmptyResponseBody_Void() throws Exception { testEmpty("bar", "response"); } private void testEmpty(String methodName, String requestOrResponse) throws Exception { String apiConfigSource = g.generateConfig(EmptyRequestEndpoint.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode fooNode = root.path("descriptor").path("methods") .path(EmptyRequestEndpoint.class.getName() + "." + methodName); assertFalse(fooNode.isMissingNode()); assertTrue(fooNode.path(requestOrResponse).isMissingNode()); } @Api private static class StringRequestEndpoint { @SuppressWarnings("unused") public void foo(String body) { } } @Api private static class StringResponseEndpoint { @SuppressWarnings("unused") public String foo() { return null; } } @Test public void testStringRequestThrowsException() throws Exception { try { String apiConfigSource = g.generateConfig(StringRequestEndpoint.class).get("myapi-v1.api"); fail("String request body didn't fail in config generation"); } catch (MissingParameterNameException e) { // expected } } @Test public void testStringResponseThrowsException() throws Exception { try { String apiConfigSource = g.generateConfig(StringResponseEndpoint.class).get("myapi-v1.api"); fail("String response body didn't fail in config generation"); } catch (InvalidReturnTypeException e) { // expected } } @Test public void testNamespaceParameters() throws Exception { @Api class DefaultApi { } doTestNamespaceParameters(DefaultApi.class, null, null, null); @Api(namespace = @ApiNamespace(ownerDomain = "ownerdomain.com.br", ownerName = "")) class OwnedApi { } try { doTestNamespaceParameters(OwnedApi.class, "ownerdomain.com.br", null, null); fail("Forgot to specify required owner name"); } catch (InvalidNamespaceException expected) { // expected } @Api(namespace = @ApiNamespace(ownerName = "Owner Name", ownerDomain = "")) class NamedApi { } try { doTestNamespaceParameters(NamedApi.class, null, "Owner Name", null); fail("Forgot to specify required owner domain"); } catch (InvalidNamespaceException expected) { // expected } @Api(namespace = @ApiNamespace(packagePath = "Package path", ownerDomain = "", ownerName = "")) class PackagedApi { } try { doTestNamespaceParameters(PackagedApi.class, null, null, "Package path"); fail("Forgot to specify both owner domain and owner name"); } catch (InvalidNamespaceException expected) { // expected } @Api(namespace = @ApiNamespace(ownerDomain = "ownerdomain.com.br", ownerName = "Owner Name")) class PartiallyNamespacedApi { } doTestNamespaceParameters(PartiallyNamespacedApi.class, "ownerdomain.com.br", "Owner Name", null); @Api(namespace = @ApiNamespace(ownerDomain = "ownerdomain.com.br", ownerName = "Owner Name", packagePath = "Package path")) class FullyNamespacedApi { } doTestNamespaceParameters(FullyNamespacedApi.class, "ownerdomain.com.br", "Owner Name", "Package path"); } private void doTestNamespaceParameters(Class<?> clazz, String ownerDomain, String ownerName, String packagePath) throws Exception { String apiConfigSource = g.generateConfig(clazz).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); if (ownerDomain != null) { assertFalse(root.path("ownerDomain").isMissingNode()); assertEquals(ownerDomain, root.get("ownerDomain").asText()); } if (ownerName != null) { assertFalse(root.path("ownerName").isMissingNode()); assertEquals(ownerName, root.get("ownerName").asText()); } if (packagePath != null) { assertFalse(root.path("packagePath").isMissingNode()); assertEquals(packagePath, root.get("packagePath").asText()); } } @Test public void testArrayRequest() throws Exception { @Api class StringArray { @SuppressWarnings("unused") public void foo(@Named("collection") String[] s) { } } String apiConfigSource = g.generateConfig(StringArray.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode methodNode = root.path("methods").path("myapi.stringArray.foo"); assertFalse(methodNode.isMissingNode()); verifyMethodRequestParameter(methodNode.get("request"), "collection", "string", true, true); } @Test public void testGenericArrayRequest() throws Exception { @Api abstract class GenericArray<T> { @SuppressWarnings("unused") public void foo(@Named("collection") T[] t) { } } // TODO: remove with JDK8, dummy to force inclusion of GenericArray to InnerClass attribute // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=2210448 GenericArray<Integer> dummy = new GenericArray<Integer>() { }; class Int32Array extends GenericArray<Integer> { } String apiConfigSource = g.generateConfig(Int32Array.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode methodNode = root.path("methods").path("myapi.int32Array.foo"); assertFalse(methodNode.isMissingNode()); verifyMethodRequestParameter(methodNode.get("request"), "collection", "int32", true, true); } @Test public void testCollectionRequest() throws Exception { @Api class BooleanCollection { @SuppressWarnings("unused") public void foo(@Named("collection") Collection<Boolean> b) { } } String apiConfigSource = g.generateConfig(BooleanCollection.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode methodNode = root.path("methods").path("myapi.booleanCollection.foo"); assertFalse(methodNode.isMissingNode()); verifyMethodRequestParameter(methodNode.get("request"), "collection", "boolean", true, true); } @Test public void testGenericCollectionRequest() throws Exception { @Api abstract class GenericCollection<T> { @SuppressWarnings("unused") public void foo(@Named("collection") Collection<T> t) { } } // TODO: remove with JDK8, dummy to force inclusion of GenericArray to InnerClass attribute // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=2210448 GenericCollection<Long> dummy = new GenericCollection<Long>() { }; class Int64Collection extends GenericCollection<Long> { } String apiConfigSource = g.generateConfig(Int64Collection.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode methodNode = root.path("methods").path("myapi.int64Collection.foo"); assertFalse(methodNode.isMissingNode()); verifyMethodRequestParameter(methodNode.get("request"), "collection", "int64", true, true); } @Test public void testComplexTypeArrayRequest() throws Exception { @Api class ComplexArray { @SuppressWarnings("unused") public void foo(@Named("collection") Bean[] beans) { } } try { g.generateConfig(ComplexArray.class).get("myapi-v1.api"); fail("Non-primitive array should have failed"); } catch (CollectionResourceException expected) { // expected } } @Test public void testComplexTypeCollectionRequest() throws Exception { @Api class ComplexCollection { @SuppressWarnings("unused") public void foo(@Named("collection") Collection<Object> dates) { } } try { g.generateConfig(ComplexCollection.class).get("myapi-v1.api"); fail("Non-primitive array should have failed"); } catch (CollectionResourceException expected) { // expected } } @Test public void testUnnamedTypeArrayRequest() throws Exception { @Api class UnnamedArray { @SuppressWarnings("unused") public void foo(Long[] dates) { } } try { g.generateConfig(UnnamedArray.class).get("myapi-v1.api"); fail("Non-primitive array should have failed"); } catch (MissingParameterNameException expected) { // expected } } @Test public void testUnnamedTypeCollectionRequest() throws Exception { @Api class UnnamedCollection { @SuppressWarnings("unused") public void foo(Collection<String> dates) { } } try { g.generateConfig(UnnamedCollection.class).get("myapi-v1.api"); fail("Non-primitive array should have failed"); } catch (MissingParameterNameException expected) { // expected } } @Test public void testMultipleCollectionRequests() throws Exception { @Api class MultipleCollections { @SuppressWarnings("unused") public void foo(@Named("ids") Long[] ids, @Named("authors") List<String> authors) { } } String apiConfigSource = g.generateConfig(MultipleCollections.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode methodNode = root.path("methods").path("myapi.multipleCollections.foo"); assertFalse(methodNode.isMissingNode()); verifyMethodRequestParameter(methodNode.get("request"), "ids", "int64", true, true); verifyMethodRequestParameter(methodNode.get("request"), "authors", "string", true, true); } @Test public void testResourceAndCollection() throws Exception { @Api class ResourceAndCollection { @SuppressWarnings("unused") public void foo(Bean resource, @Named("authors") List<String> authors) { } } String apiConfigSource = g.generateConfig(ResourceAndCollection.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode methodNode = root.path("methods").path("myapi.resourceAndCollection.foo"); assertFalse(methodNode.isMissingNode()); verifyMethodRequestParameter(methodNode.get("request"), "authors", "string", true, true); assertEquals(1, methodNode.path("request").path("parameters").size()); verifyMethodRequestRef(root, ResourceAndCollection.class.getName() + ".foo", "Bean"); } @Test public void testCollectionOfArrays() throws Exception { @Api class NestedCollections { @SuppressWarnings("unused") public void foo(@Named("authors") List<String[]> authors) { } } try { g.generateConfig(NestedCollections.class).get("myapi-v1.api"); fail("Nested collections should fail"); } catch (NestedCollectionException expected) { // expected } } @Test public void testArraysOfCollections() throws Exception { @Api class NestedCollections { @SuppressWarnings("unused") public void foo(@Named("authors") List<String>[] authors) { } } try { g.generateConfig(NestedCollections.class).get("myapi-v1.api"); fail("Nested collections should fail"); } catch (NestedCollectionException expected) { // expected } } @Test public void testCollectionOfCollections() throws Exception { @Api class NestedCollections { @SuppressWarnings("unused") public void foo(@Named("authors") List<List<String>> authors) { } } try { g.generateConfig(NestedCollections.class).get("myapi-v1.api"); fail("Nested collections should fail"); } catch (NestedCollectionException expected) { // expected } } @Test public void testArrayOfArrays() throws Exception { @Api class NestedCollections { @SuppressWarnings("unused") public void foo(@Named("authors") String[][] authors) { } } try { g.generateConfig(NestedCollections.class).get("myapi-v1.api"); fail("Nested collections should fail"); } catch (NestedCollectionException expected) { // expected } } @Test public void testArrayOfResources() throws Exception { @Api class ResourceCollection { @SuppressWarnings("unused") public void foo(List<Bean> beans) { } } try { g.generateConfig(ResourceCollection.class).get("myapi-v1.api"); fail("Array of resources should fail"); } catch (CollectionResourceException expected) { // expected } } @Test public void testArrayOfSerializedResources() throws Exception { @Api class ResourceCollection { @SuppressWarnings("unused") public void foo(@Named("beans") List<StringBean> beans) { } } String apiConfigSource = g.generateConfig(ResourceCollection.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); JsonNode methodNode = root.path("methods").path("myapi.resourceCollection.foo"); assertFalse(methodNode.isMissingNode()); verifyMethodRequestParameter(methodNode.get("request"), "beans", "string", true, true); } @Test public void testCanonicalName() throws Exception { @Api(name = "myapi", canonicalName = "My API") class CanonicalApi { } String apiConfigSource = g.generateConfig(CanonicalApi.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); assertEquals("myapi", root.path("name").asText()); assertEquals("My API", root.path("canonicalName").asText()); } @Test public void testTitle() throws Exception { @Api(name = "myapi", title = "My API Title") class ApiWithTitle { } String apiConfigSource = g.generateConfig(ApiWithTitle.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); assertEquals("myapi", root.path("name").asText()); assertEquals("My API Title", root.path("title").asText()); } @Test public void testDocumentationLink() throws Exception { @Api(name = "myapi", documentationLink = "http://go/documentation") class ApiWithDocs { } String apiConfigSource = g.generateConfig(ApiWithDocs.class).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); assertEquals("myapi", root.path("name").asText()); assertEquals("http://go/documentation", root.path("documentation").asText()); } @Test public void testGenericParameterTypes() throws Exception { @Api final class Test<T> { @SuppressWarnings("unused") public void setT(T t) { } } try { g.generateConfig(Test.class).get("myapi-v1.api"); fail(); } catch (GenericTypeException e) { // Expected. } } @Test public void testGenericParameterTypeThroughMethodCall() throws Exception { this.<Integer>genericParameterTypeTestImpl(); } @Test public void testConsistentApiWideConfig() throws Exception { @Api(scopes = { "scopes" }) @ApiClass(scopes = { "foo" }) final class Test1 { } @Api(scopes = { "scopes" }) @ApiClass(scopes = { "bar" }) final class Test2 { } g.generateConfig(Test1.class, Test2.class); } @Test public void testInconsistentApiWideConfig() throws Exception { @Api(scopes = { "foo" }) @ApiClass(scopes = { "scopes" }) final class Test1 { } @Api(scopes = { "bar" }) @ApiClass(scopes = { "scopes" }) final class Test2 { } try { g.generateConfig(Test1.class, Test2.class); fail(); } catch (InconsistentApiConfigurationException e) { // Expected exception. } } private <T> void genericParameterTypeTestImpl() throws Exception { @Api class Bar<T1> { @SuppressWarnings("unused") public void bar(T1 t1) { } } class Foo extends Bar<T> { } try { g.generateConfig(Foo.class).get("myapi-v1.api"); fail(); } catch (GenericTypeException e) { // Expected. } } @Test public void testAnonymousClass() throws Exception { @Api() class Test { @SuppressWarnings("unused") public void test() { } } String apiConfigSource = g.generateConfig(new Test() { }.getClass()).get("myapi-v1.api"); ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class); assertEquals("test", root.path("methods").path("myapi.test").path("path").asText()); } @Test public void testGenerateConfigRetainsOrder() throws Exception { @Api(name = "onetoday", description = "OneToday API") final class OneToday { } @Api(name = "onetodayadmin", description = "One Today Admin API") final class OneTodayAdmin { } Map<String, String> configs = g.generateConfig(OneToday.class, OneTodayAdmin.class); Iterator<String> iterator = configs.keySet().iterator(); assertEquals("onetoday-v1.api", iterator.next()); assertEquals("onetodayadmin-v1.api", iterator.next()); } }